create-ait-cmp-app 0.0.0-stage → 1.0.0-alpha02

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +31 -2
  2. package/cli.js +268 -0
  3. package/package.json +29 -3
  4. package/templates/app/.editorconfig +15 -0
  5. package/templates/app/LICENSE +21 -0
  6. package/templates/app/README.md +25 -0
  7. package/templates/app/_github/workflows/ci.yml +56 -0
  8. package/templates/app/_gitignore +11 -0
  9. package/templates/app/_yarn.lock +2576 -0
  10. package/templates/app/_yarnrc.yml +1 -0
  11. package/templates/app/androidApp/README.md +19 -0
  12. package/templates/app/androidApp/build.gradle.kts +29 -0
  13. package/templates/app/androidApp/src/main/AndroidManifest.xml +15 -0
  14. package/templates/app/androidApp/src/main/kotlin/io/github/l2hyunwoo/ait/sample/android/MainActivity.kt +49 -0
  15. package/templates/app/apps-in-toss.config.ts +2 -0
  16. package/templates/app/appsInTossApp/README.md +23 -0
  17. package/templates/app/appsInTossApp/build.gradle.kts +34 -0
  18. package/templates/app/appsInTossApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
  19. package/templates/app/appsInTossApp/src/webMain/kotlin/Main.kt +68 -0
  20. package/templates/app/appsInTossApp/src/webMain/resources/index.html +4 -0
  21. package/templates/app/build.gradle.kts +37 -0
  22. package/templates/app/docs/development.md +56 -0
  23. package/templates/app/eslint.config.mjs +19 -0
  24. package/templates/app/gradle/libs.versions.toml +31 -0
  25. package/templates/app/gradle/wrapper/gradle-wrapper.jar +0 -0
  26. package/templates/app/gradle/wrapper/gradle-wrapper.properties +10 -0
  27. package/templates/app/gradle.properties +3 -0
  28. package/templates/app/gradlew +248 -0
  29. package/templates/app/gradlew.bat +112 -0
  30. package/templates/app/iosApp/README.md +17 -0
  31. package/templates/app/iosApp/ServiceSample.xcodeproj/project.pbxproj +323 -0
  32. package/templates/app/iosApp/ServiceSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  33. package/templates/app/iosApp/ServiceSample.xcodeproj/xcshareddata/xcschemes/ServiceSample.xcscheme +91 -0
  34. package/templates/app/iosApp/iosApp/ServiceSampleApp.swift +20 -0
  35. package/templates/app/licenses/NotoSansKR-OFL.txt +93 -0
  36. package/templates/app/package.json +31 -0
  37. package/templates/app/scripts/lint-kotlin.mjs +8 -0
  38. package/templates/app/scripts/web.mjs +52 -0
  39. package/templates/app/settings.gradle.kts +17 -0
  40. package/templates/app/shared-core/README.md +19 -0
  41. package/templates/app/shared-core/build.gradle.kts +37 -0
  42. package/templates/app/shared-core/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/Notes.kt +37 -0
  43. package/templates/app/shared-core/src/commonTest/kotlin/io/github/l2hyunwoo/ait/sample/NotesApiClientTest.kt +17 -0
  44. package/templates/app/shared-core/src/jsMain/kotlin/io/github/l2hyunwoo/ait/sample/BrowserNote.kt +12 -0
  45. package/templates/app/shared-ui/README.md +22 -0
  46. package/templates/app/shared-ui/build.gradle.kts +49 -0
  47. package/templates/app/shared-ui/src/commonMain/composeResources/files/NotoSansKR-OFL.txt +93 -0
  48. package/templates/app/shared-ui/src/commonMain/composeResources/font/NotoSansKR.ttf +0 -0
  49. package/templates/app/shared-ui/src/commonMain/kotlin/io/github/l2hyunwoo/ait/sample/ServiceApp.kt +121 -0
  50. package/templates/app/shared-ui/src/iosMain/kotlin/io/github/l2hyunwoo/ait/sample/MainViewController.kt +40 -0
  51. package/templates/app/webApp/README.md +18 -0
  52. package/templates/app/webApp/build.gradle.kts +23 -0
  53. package/templates/app/webApp/src/webMain/kotlin/BrowserDraftStorage.kt +9 -0
  54. package/templates/app/webApp/src/webMain/kotlin/Main.kt +8 -0
  55. package/templates/app/webApp/src/webMain/resources/index.html +4 -0
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # create-ait-cmp-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 앱인토스용 Compose Multiplatform 프로젝트를 대화형으로 만드는 CLI입니다. 앱인토스 전용 앱으로 시작하고 Android·iOS·독립 웹 타깃을 함께 선택할 수 있습니다.
4
+
5
+ ## 생성하는 프로젝트
6
+
7
+ `shared-core`에 서비스 모델·API 클라이언트·저장 계약을, `shared-ui`에 공통 화면을 둡니다. `appsInTossApp`은 토스 연동을 담당합니다. 선택한 타깃의 호스트 디렉터리, Gradle Wrapper, Yarn 잠금 파일, 린트 설정, 기본 GitHub Actions CI를 포함합니다.
8
+
9
+ 생성 앱은 CLI와 같은 버전의 SDK를 Maven Central에서 가져옵니다.
10
+
11
+ ## 사용
12
+
13
+ Node 24 이상과 Corepack이 필요합니다.
14
+
15
+ ```sh
16
+ corepack enable
17
+ yarn dlx create-ait-cmp-app@alpha my-app
18
+ ```
19
+
20
+ 타깃·Kotlin 패키지·콘솔 appName·표시 이름을 질문하고, 생성 전에 요약과 확인을 받습니다. 기본 타깃은 앱인토스만 선택합니다.
21
+
22
+ 자동화 환경에서는 입력값을 명시합니다.
23
+
24
+ ```sh
25
+ yarn dlx create-ait-cmp-app@alpha my-app --yes \
26
+ --package io.example.myapp --app-name my-app \
27
+ --display-name '내 앱' --targets apps-in-toss,android,ios,web
28
+ ```
29
+
30
+ `--skip-install`로 설치를 생략할 수 있습니다. 기본 설치는 프로젝트에 지정한 Yarn 4의 `install --immutable`을 사용합니다. 기존 파일·비어 있지 않은 경로·심볼릭 링크를 보호하며 덮어쓰기 옵션은 제공하지 않습니다.
31
+
32
+ Git 초기화·커밋·앱인토스 콘솔 등록·배포·네이티브 도구 설치는 수행하지 않습니다. 생성 앱의 README는 앱 구성을, 개발 가이드는 실행·검사·CI 방법을 안내합니다. 전체 옵션은 `yarn dlx create-ait-cmp-app@alpha --help`로 확인합니다.
package/cli.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ import { lstat, readdir, readFile, mkdir, writeFile, chmod, realpath } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createInterface } from 'node:readline/promises';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const {version: VERSION} = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
9
+ const TARGETS = ['apps-in-toss', 'android', 'ios', 'web'];
10
+ const TEMPLATE = fileURLToPath(new URL('./templates/app/', import.meta.url));
11
+ const HELP = `Usage: create-ait-cmp-app [directory] [options]
12
+
13
+ Create a Compose Multiplatform app. Interactive prompts are the default.
14
+ --package <id> Kotlin package, Android application ID and iOS bundle ID
15
+ --app-name <name> Apps in Toss console appName (lowercase letters, digits, hyphens)
16
+ --display-name <name> App title
17
+ --targets <list> apps-in-toss,android,ios,web (default: apps-in-toss)
18
+ --yes Skip prompts; directory and all three metadata options required
19
+ --skip-install Generate files without running corepack yarn install --immutable
20
+ --help Show help
21
+ --version Show version
22
+
23
+ Node.js 24 or newer is required. Existing files and symlink paths are protected.
24
+ `;
25
+ const KEYWORDS = new Set('as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while'.split(' '));
26
+
27
+ export function validate(field, value) {
28
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} is required.`);
29
+ if (field === 'project' && !/^[a-z][a-z0-9-]{0,79}$/.test(value)) {
30
+ throw new Error('Project name must start with a lowercase letter and contain only lowercase letters, digits and hyphens (max 80 characters).');
31
+ }
32
+ if (field === 'package' && (!/^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/.test(value)
33
+ || value.split('.').some(part => KEYWORDS.has(part) || part.includes('_')))) {
34
+ throw new Error('Package must have at least two dot-separated segments starting with letters, using letters and digits only, without Kotlin keywords.');
35
+ }
36
+ if (field === 'app-name' && !/^[a-z][a-z0-9-]{0,79}$/.test(value)) {
37
+ throw new Error('appName must start with a lowercase letter and contain lowercase letters, digits and hyphens (max 80 characters).');
38
+ }
39
+ // Reject control characters in display names before writing any files.
40
+ // eslint-disable-next-line no-control-regex
41
+ if (field === 'display-name' && (/[\u0000-\u001f\u007f]/.test(value) || value.length > 100)) {
42
+ throw new Error('Display name must contain 1–100 characters without control characters.');
43
+ }
44
+ if (field === 'targets') {
45
+ const selected = value.split(',');
46
+ if (!selected.includes('apps-in-toss') || selected.some(t => !TARGETS.includes(t)) || new Set(selected).size !== selected.length) {
47
+ throw new Error(`Targets must include apps-in-toss and use each of ${TARGETS.join(',')} at most once.`);
48
+ }
49
+ }
50
+ return value;
51
+ }
52
+
53
+ export function parseArgs(argv) {
54
+ const options = {};
55
+ const flags = new Set(['yes', 'skip-install', 'help', 'version']);
56
+ const values = new Set(['package', 'app-name', 'display-name', 'targets']);
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ if (arg.startsWith('--')) {
60
+ const [name, ...rest] = arg.slice(2).split('=');
61
+ if (Object.hasOwn(options, name)) throw new Error(`Duplicate option --${name}.`);
62
+ if (flags.has(name) && !rest.length) options[name] = true;
63
+ else if (values.has(name)) {
64
+ const value = rest.length ? rest.join('=') : argv[++i];
65
+ if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for --${name}.`);
66
+ options[name] = validate(name, value);
67
+ } else throw new Error(`Unknown option ${arg}. Use --help.`);
68
+ } else {
69
+ if (arg.startsWith('-')) throw new Error(`Unknown option ${arg}. Use --help.`);
70
+ if (options.directory !== undefined) throw new Error('Provide only one target directory.');
71
+ options.directory = arg;
72
+ }
73
+ }
74
+ return options;
75
+ }
76
+
77
+ export async function checkDirectory(directory) {
78
+ // Check every ancestor so a symlink cannot redirect a nominally safe destination.
79
+ let current = path.parse(directory).root;
80
+ for (const part of directory.slice(current.length).split(path.sep).filter(Boolean)) {
81
+ current = path.join(current, part);
82
+ try {
83
+ const stat = await lstat(current);
84
+ if (stat.isSymbolicLink()) throw new Error(`Refusing symlink path: ${current}`);
85
+ if (!stat.isDirectory()) throw new Error(`Path is not a directory: ${current}`);
86
+ } catch (error) {
87
+ if (error.code === 'ENOENT') break;
88
+ throw error;
89
+ }
90
+ }
91
+ try {
92
+ const entries = await readdir(directory);
93
+ if (entries.some(entry => entry !== '.git')) throw new Error(`Directory is not empty: ${directory}`);
94
+ if (entries.includes('.git') && (await lstat(path.join(directory, '.git'))).isSymbolicLink()) {
95
+ throw new Error('Refusing symlink .git entry.');
96
+ }
97
+ } catch (error) {
98
+ if (error.code !== 'ENOENT') throw error;
99
+ }
100
+ }
101
+
102
+ function escapeValue(value, filename) {
103
+ const ext = path.extname(filename);
104
+ if (['.xml', '.plist', '.html'].includes(ext)) {
105
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;');
106
+ }
107
+ if (['.json', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.kt', '.kts', '.swift', '.pbxproj'].includes(ext)) {
108
+ const escaped = JSON.stringify(value).slice(1, -1).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
109
+ if (ext === '.swift') return escaped.replaceAll('\\u2028', '\\u{2028}').replaceAll('\\u2029', '\\u{2029}');
110
+ return ['.kt', '.kts'].includes(ext) ? escaped.replaceAll('$', '\\$') : escaped;
111
+ }
112
+ if (ext === '.properties') return value.replace(/[\\:=#! ]/g, char => `\\${char}`);
113
+ if (['.md', '.txt'].includes(ext)) return value;
114
+ throw new Error(`Template metadata has no escaping contract for ${filename}.`);
115
+ }
116
+
117
+ async function templateFiles(root, options, prefix = '') {
118
+ const files = [];
119
+ const omitted = new Set([!options.targets.includes('android') && 'androidApp', !options.targets.includes('ios') && 'iosApp', !options.targets.includes('web') && 'webApp']);
120
+ for (const entry of await readdir(path.join(root, prefix), { withFileTypes: true })) {
121
+ if (!prefix && omitted.has(entry.name)) continue;
122
+ const relative = path.join(prefix, entry.name);
123
+ if (entry.isSymbolicLink()) throw new Error(`Template symlink is not supported: ${relative}`);
124
+ if (entry.isDirectory()) files.push(...await templateFiles(root, options, relative));
125
+ else if (entry.isFile()) {
126
+ const source = await readFile(path.join(root, relative));
127
+ let content = source;
128
+ const text = source.toString('utf8');
129
+ if (/__(PROJECT_NAME|PACKAGE_NAME|APP_NAME|DISPLAY_NAME)__/.test(text)) {
130
+ const metadata = { PROJECT_NAME: options.project, PACKAGE_NAME: options.package, APP_NAME: options['app-name'], DISPLAY_NAME: options['display-name'] };
131
+ content = Buffer.from(text.replace(/__(PROJECT_NAME|PACKAGE_NAME|APP_NAME|DISPLAY_NAME)__/g,
132
+ (_, key) => escapeValue(metadata[key], relative)));
133
+ }
134
+ // Package replacement can change the import order required by .editorconfig.
135
+ if (relative.endsWith('.kt')) {
136
+ content = Buffer.from(content.toString('utf8').replace(/^(?:import [^\n]+\n)+/gm,
137
+ block => `${block.trimEnd().split('\n').sort().join('\n')}\n`));
138
+ }
139
+ if (relative === 'package.json') {
140
+ const json = JSON.parse(content.toString('utf8'));
141
+ json.name = options.project;
142
+ if (relative === 'package.json' && !options.targets.includes('web')) {
143
+ delete json.scripts?.['dev:web'];
144
+ delete json.scripts?.['build:web'];
145
+ }
146
+ content = Buffer.from(`${JSON.stringify(json, null, 2)}\n`);
147
+ }
148
+ if (relative === '_yarn.lock') {
149
+ // Yarn requires canonical entry order even when only the workspace name changes.
150
+ const blocks = content.toString('utf8').replaceAll('ait-cmp-template', options.project).trimEnd().split('\n\n');
151
+ const firstEntry = blocks.findIndex(block => block.startsWith('"'));
152
+ if (firstEntry < 0) throw new Error('Bundled Yarn lockfile has no entries.');
153
+ content = Buffer.from([...blocks.slice(0, firstEntry), ...blocks.slice(firstEntry).sort()].join('\n\n') + '\n');
154
+ }
155
+ const destination = relative.startsWith(`_github${path.sep}`)
156
+ ? `.github${relative.slice('_github'.length)}`
157
+ : ({'_gitignore':'.gitignore','_yarn.lock':'yarn.lock','_yarnrc.yml':'.yarnrc.yml'})[relative] ?? relative;
158
+ files.push({ relative: destination, content, mode: (await lstat(path.join(root, relative))).mode & 0o777 });
159
+ }
160
+ }
161
+ return files;
162
+ }
163
+
164
+ function install(directory) {
165
+ return new Promise((resolve, reject) => {
166
+ const command = process.platform === 'win32' ? 'corepack.cmd' : 'corepack';
167
+ const child = spawn(command, ['yarn', 'install', '--immutable'], {
168
+ cwd: directory, stdio: 'inherit', shell: process.platform === 'win32'
169
+ });
170
+ child.once('error', reject);
171
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error(`corepack yarn install --immutable exited with ${code ?? 'a signal'}.`)));
172
+ });
173
+ }
174
+
175
+ export async function run(argv, context = {}) {
176
+ const output = context.output ?? process.stdout;
177
+ const input = context.input ?? process.stdin;
178
+ const log = message => output.write(`${message}\n`);
179
+ const options = parseArgs(argv);
180
+ if (options.help) { output.write(HELP); return 0; }
181
+ if (options.version) { log(VERSION); return 0; }
182
+ if (Number(process.versions.node.split('.')[0]) < 24) throw new Error('Node.js 24 or newer is required.');
183
+ if (!options.yes && !input.isTTY && !context.ask) {
184
+ throw new Error('No interactive terminal. Use <directory> --yes --package <id> --app-name <name> --display-name <name>.');
185
+ }
186
+ if (options.yes) {
187
+ const missing = ['directory', 'package', 'app-name', 'display-name'].filter(key => options[key] === undefined);
188
+ if (missing.length) throw new Error(`--yes requires ${missing.map(key => key === 'directory' ? '<directory>' : `--${key}`).join(', ')}.`);
189
+ }
190
+ let rl;
191
+ let cancelled = false;
192
+ let abortQuestion;
193
+ const ask = context.ask ?? (question => {
194
+ if (!rl) {
195
+ rl = createInterface({ input, output });
196
+ rl.on('SIGINT', () => { cancelled = true; abortQuestion?.abort(); rl.close(); });
197
+ }
198
+ abortQuestion = new AbortController();
199
+ return rl.question(question, { signal: abortQuestion.signal });
200
+ });
201
+ async function prompt(field, label, fallback) {
202
+ if (options[field] !== undefined) return;
203
+ for (;;) {
204
+ const answer = (await ask(`${label} [${fallback}]: `)).trim() || fallback;
205
+ try {
206
+ if (field === 'directory') validate('project', path.basename(path.resolve(context.cwd ?? process.cwd(), answer)));
207
+ options[field] = field === 'directory' ? answer : validate(field, answer);
208
+ return;
209
+ }
210
+ catch (error) { log(error.message); }
211
+ }
212
+ }
213
+ try {
214
+ if (!options.yes) await prompt('directory', 'Project directory', 'my-app');
215
+ validate('directory', options.directory);
216
+ const directory = path.resolve(context.cwd ?? process.cwd(), options.directory);
217
+ options.project = validate('project', path.basename(directory));
218
+ await checkDirectory(directory);
219
+ if (!options.yes) {
220
+ await prompt('targets', 'Targets (comma-separated)', 'apps-in-toss');
221
+ await prompt('package', 'Kotlin package / application ID', `io.example.${options.project.replaceAll('-', '')}`);
222
+ await prompt('app-name', 'Apps in Toss appName (must match your console registration)', options.project);
223
+ await prompt('display-name', 'Display name', options.project);
224
+ }
225
+ options.targets = (options.targets ?? 'apps-in-toss').split(',');
226
+ log(`\nDirectory: ${directory}\nProject: ${options.project}\nTargets: ${options.targets.join(', ')}\nPackage: ${options.package}\nappName: ${options['app-name']}\nDisplay name: ${options['display-name']}\nInstall dependencies: ${!options['skip-install']}`);
227
+ if (!options.yes && !/^(y|yes)$/i.test((await ask('Create project? [y/N]: ')).trim())) {
228
+ log('Cancelled.'); return 0;
229
+ }
230
+ rl?.close();
231
+ // Render everything before creating a directory: invalid templates leave no output.
232
+ const files = await templateFiles(context.template ?? TEMPLATE, options);
233
+ if (!files.length) throw new Error('Bundled template is empty.');
234
+ await checkDirectory(directory);
235
+ await mkdir(directory, { recursive: true });
236
+ for (const file of files) {
237
+ const destination = path.join(directory, file.relative);
238
+ await mkdir(path.dirname(destination), { recursive: true });
239
+ await writeFile(destination, file.content, { flag: 'wx', mode: file.mode });
240
+ await chmod(destination, file.mode);
241
+ }
242
+ log(`\nCreated ${options.project}.`);
243
+ if (!options['skip-install']) {
244
+ try { await (context.install ?? install)(directory); }
245
+ catch (error) {
246
+ log(`Dependency installation failed: ${error.message}\nGenerated files are preserved. Retry corepack yarn install --immutable in ${directory}.`);
247
+ return 1;
248
+ }
249
+ }
250
+ log(`\nNext steps in ${directory}:`);
251
+ if (options['skip-install']) log(' corepack yarn install --immutable');
252
+ log(' yarn lint\n yarn dev\n yarn build');
253
+ if (options.targets.includes('android')) log(' ./gradlew :androidApp:assembleDebug (requires JDK 17 and Android SDK)');
254
+ if (options.targets.includes('ios')) log(' Open iosApp in Xcode on macOS and configure your signing team.');
255
+ if (options.targets.includes('web')) log(' yarn dev:web\n yarn build:web');
256
+ log('Configure the Apps in Toss console values and logo before a production build.');
257
+ return 0;
258
+ } catch (error) {
259
+ if (cancelled || error.code === 'ABORT_ERR' || error.code === 'ERR_USE_AFTER_CLOSE') { log('Cancelled.'); return 130; }
260
+ throw error;
261
+ } finally { rl?.close(); }
262
+ }
263
+
264
+ if (process.argv[1] && await realpath(process.argv[1]).catch(() => '') === fileURLToPath(import.meta.url)) {
265
+ run(process.argv.slice(2)).then(code => { process.exitCode = code; }).catch(error => {
266
+ process.stderr.write(`Error: ${error.message}\n`); process.exitCode = 1;
267
+ });
268
+ }
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "create-ait-cmp-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-alpha02",
4
+ "description": "앱인토스용 Compose Multiplatform 프로젝트 생성 도구",
5
+ "type": "module",
6
+ "bin": "cli.js",
7
+ "engines": {
8
+ "node": ">=24"
9
+ },
10
+ "files": [
11
+ "cli.js",
12
+ "templates"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "executableFiles": [
20
+ "cli.js",
21
+ "templates/app/gradlew"
22
+ ],
23
+ "access": "public",
24
+ "provenance": false
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/l2hyunwoo/apps-in-toss-cmp-sdk.git",
29
+ "directory": "packages/create-ait-cmp-app"
30
+ },
31
+ "packageManager": "yarn@4.18.1"
6
32
  }
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ indent_style = space
8
+ indent_size = 2
9
+
10
+ [*.{kt,kts}]
11
+ indent_size = 4
12
+ ktlint_code_style = intellij_idea
13
+ ij_kotlin_imports_layout = *
14
+ ktlint_function_naming_ignore_when_annotated_with = Composable
15
+ ktlint_standard_no-wildcard-imports = disabled
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 l2hyunwoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ 앱인토스에서 사용하는 Compose Multiplatform 메모 서비스입니다. 메모 목록을 보고 내용을 수정·저장할 수 있으며, 공통 화면과 서비스 로직을 다른 앱·웹에 재사용할 수 있습니다.
4
+
5
+ ## 기능
6
+
7
+ - 메모 목록·상세·편집 화면
8
+ - 기기에 초안 저장과 텍스트 공유
9
+ - 앱인토스 전용 로그인·호스트 이벤트·안전 영역 연결
10
+
11
+ ## 구성
12
+
13
+ | 모듈 | 역할 |
14
+ | --- | --- |
15
+ | [shared-core](shared-core/README.md) | 데이터 모델·조회 API·저장 계약 |
16
+ | [shared-ui](shared-ui/README.md) | 재사용 가능한 Compose 화면 |
17
+ | [appsInTossApp](appsInTossApp/README.md) | 토스 연동과 앱인토스 진입점 |
18
+
19
+ Android·iOS·독립 웹 호스트는 프로젝트 생성 시 선택한 경우에 포함됩니다. 공통 UI는 Compose Material 3을 사용하며 TDS는 포함하지 않습니다.
20
+
21
+ ## 현재 제공 범위
22
+
23
+ 조회는 데모 데이터를 사용하고 초안은 기기에 저장합니다. 운영 서버·계정 연동·서버 인증 교환은 별도로 연결해야 합니다. 개발용 mock의 로그인 결과는 인증에 사용할 수 없습니다.
24
+
25
+ [개발 가이드](docs/development.md)에서 실행·검사·GitHub Actions CI와 플랫폼 확장 방법을 확인할 수 있습니다.
@@ -0,0 +1,56 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ concurrency:
14
+ group: ${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ javascript:
19
+ name: JS 린트
20
+ runs-on: ubuntu-24.04
21
+ timeout-minutes: 10
22
+ steps:
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
+ with:
25
+ persist-credentials: false
26
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
27
+ with:
28
+ node-version: '24'
29
+ package-manager-cache: false
30
+ - name: Yarn 4 설치
31
+ run: corepack enable
32
+ - name: 의존성 설치
33
+ run: yarn install --immutable
34
+ - name: JS·TS 린트
35
+ run: yarn lint:js
36
+
37
+ kotlin:
38
+ name: Kotlin 린트·테스트
39
+ runs-on: ubuntu-24.04
40
+ timeout-minutes: 20
41
+ steps:
42
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
+ with:
44
+ persist-credentials: false
45
+ - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
46
+ with:
47
+ distribution: temurin
48
+ java-version: '17'
49
+ - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
50
+ with:
51
+ cache-provider: basic
52
+ cache-read-only: ${{ github.event_name == 'pull_request' }}
53
+ - name: Kotlin·Gradle 린트
54
+ run: ./gradlew ktlintCheck --no-daemon --console=plain
55
+ - name: Kotlin JVM 테스트
56
+ run: ./gradlew :shared-core:jvmTest --no-daemon --console=plain
@@ -0,0 +1,11 @@
1
+ **/build/
2
+ .gradle/
3
+ .kotlin/
4
+ node_modules/
5
+ local.properties
6
+ **/dist/
7
+ *.ait
8
+ .DS_Store
9
+ **/xcuserdata/
10
+ kotlin-js-store/
11
+ .yarn/