create-ait-app 0.0.1 → 0.0.3

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 (112) hide show
  1. package/README.md +35 -49
  2. package/package.json +2 -6
  3. package/src/cli.js +52 -0
  4. package/src/main.js +88 -396
  5. package/src/sample-configs.js +140 -0
  6. package/src/sample-inject.js +65 -0
  7. package/src/scaffold.js +111 -0
  8. package/src/skills.js +66 -0
  9. package/src/templates.js +88 -0
  10. package/src/utils/copy-dir.js +19 -0
  11. package/src/utils/fetch-text.js +47 -0
  12. package/src/utils/package-name.js +26 -0
  13. package/templates/js/eslint.config.js +14 -0
  14. package/templates/js/index.html +12 -0
  15. package/templates/js/package.json +20 -0
  16. package/templates/js/samples/iaa/src/lib/inAppAds.js +119 -0
  17. package/templates/js/samples/iaa/src/pages/InAppAdsPage.js +83 -0
  18. package/templates/js/samples/iap/src/lib/inAppPurchase.js +105 -0
  19. package/templates/js/samples/iap/src/pages/InAppPurchasePage.js +102 -0
  20. package/templates/js/src/app.js +58 -0
  21. package/templates/js/src/main.js +2 -0
  22. package/templates/js/vite.config.js +3 -0
  23. package/templates/react/README.md +26 -0
  24. package/templates/react/eslint.config.js +30 -0
  25. package/templates/react/granite.config.ts +20 -0
  26. package/templates/react/index.html +12 -0
  27. package/templates/react/package.json +27 -0
  28. package/templates/react/public/appsintoss-logo.png +0 -0
  29. package/templates/react/samples/iaa/src/hooks/useInAppAds.js +102 -0
  30. package/templates/react/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  31. package/templates/react/samples/iaa/src/pages/InAppAdsPage.jsx +75 -0
  32. package/templates/react/samples/iap/public/icon-document.png +0 -0
  33. package/templates/react/samples/iap/src/hooks/useInAppPurchase.js +95 -0
  34. package/templates/react/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  35. package/templates/react/samples/iap/src/pages/InAppPurchasePage.jsx +115 -0
  36. package/templates/react/src/App.css +104 -0
  37. package/templates/react/src/App.jsx +45 -0
  38. package/templates/react/src/index.css +27 -0
  39. package/templates/react/src/main.jsx +10 -0
  40. package/templates/react/vite.config.js +6 -0
  41. package/templates/react-ts/README.md +26 -0
  42. package/templates/react-ts/eslint.config.js +28 -0
  43. package/templates/react-ts/granite.config.ts +20 -0
  44. package/templates/react-ts/index.html +12 -0
  45. package/templates/react-ts/public/appsintoss-logo.png +0 -0
  46. package/templates/react-ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  47. package/templates/react-ts/samples/iap/public/icon-document.png +0 -0
  48. package/templates/react-ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  49. package/templates/react-ts/src/App.css +104 -0
  50. package/templates/react-ts/src/index.css +27 -0
  51. package/templates/react-ts/src/vite-env.d.ts +6 -0
  52. package/templates/react-ts/tsconfig.app.json +22 -0
  53. package/templates/react-ts/tsconfig.json +7 -0
  54. package/templates/react-ts/tsconfig.node.json +20 -0
  55. package/templates/react-ts/vite.config.ts +6 -0
  56. package/templates/react-ts-tds/README.md +26 -0
  57. package/templates/react-ts-tds/granite.config.ts +20 -0
  58. package/templates/react-ts-tds/package.json +35 -0
  59. package/templates/react-ts-tds/public/appsintoss-logo.png +0 -0
  60. package/templates/ts/README.md +26 -0
  61. package/templates/ts/eslint.config.js +15 -0
  62. package/templates/ts/granite.config.ts +20 -0
  63. package/templates/ts/index.html +12 -0
  64. package/templates/ts/package.json +22 -0
  65. package/templates/ts/public/appsintoss-logo.png +0 -0
  66. package/templates/ts/samples/iaa/src/lib/inAppAds.ts +132 -0
  67. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  68. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.ts +85 -0
  69. package/templates/ts/samples/iap/public/icon-document.png +0 -0
  70. package/templates/ts/samples/iap/src/lib/inAppPurchase.ts +114 -0
  71. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  72. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.ts +105 -0
  73. package/templates/ts/src/App.css +104 -0
  74. package/templates/ts/src/app.ts +60 -0
  75. package/templates/ts/src/index.css +27 -0
  76. package/templates/ts/src/main.ts +2 -0
  77. package/templates/ts/src/vite-env.d.ts +1 -0
  78. package/templates/ts/tsconfig.app.json +22 -0
  79. package/templates/ts/tsconfig.json +7 -0
  80. package/templates/ts/tsconfig.node.json +20 -0
  81. package/templates/ts/vite.config.ts +3 -0
  82. /package/{template → templates/js}/README.md +0 -0
  83. /package/{template → templates/js}/granite.config.ts +0 -0
  84. /package/{template → templates/js}/public/appsintoss-logo.png +0 -0
  85. /package/{template/__default/__samples → templates/js/samples}/iaa/src/pages/InAppAdsPage.css +0 -0
  86. /package/{template/__default/__samples → templates/js/samples}/iap/public/icon-document.png +0 -0
  87. /package/{template/__default/__samples → templates/js/samples}/iap/src/pages/InAppPurchasePage.css +0 -0
  88. /package/{template/__default → templates/js}/src/App.css +0 -0
  89. /package/{template/__default → templates/js}/src/index.css +0 -0
  90. /package/{template → templates/react-ts}/package.json +0 -0
  91. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  92. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  93. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  94. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  95. /package/{template/__default → templates/react-ts}/src/App.tsx +0 -0
  96. /package/{template/__default → templates/react-ts}/src/main.tsx +0 -0
  97. /package/{template → templates/react-ts-tds}/eslint.config.js +0 -0
  98. /package/{template → templates/react-ts-tds}/index.html +0 -0
  99. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  100. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  101. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/public/icon-document.png +0 -0
  102. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  103. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  104. /package/{template/__tds → templates/react-ts-tds}/src/App.css +0 -0
  105. /package/{template/__tds → templates/react-ts-tds}/src/App.tsx +0 -0
  106. /package/{template/__tds → templates/react-ts-tds}/src/index.css +0 -0
  107. /package/{template/__tds → templates/react-ts-tds}/src/main.tsx +0 -0
  108. /package/{template → templates/react-ts-tds}/src/vite-env.d.ts +0 -0
  109. /package/{template → templates/react-ts-tds}/tsconfig.app.json +0 -0
  110. /package/{template → templates/react-ts-tds}/tsconfig.json +0 -0
  111. /package/{template → templates/react-ts-tds}/tsconfig.node.json +0 -0
  112. /package/{template → templates/react-ts-tds}/vite.config.ts +0 -0
package/src/main.js CHANGED
@@ -1,177 +1,21 @@
1
- const { execSync } = require("child_process");
2
1
  const { select, confirm, input, checkbox } = require("@inquirer/prompts");
3
- const path = require("path");
4
2
  const fs = require("fs");
5
- const https = require("https");
6
- const http = require("http");
7
- const zlib = require("zlib");
8
-
9
- function toNpmPackageName(input) {
10
- const raw = String(input || "").trim();
11
- if (!raw) return "my-app";
12
-
13
- // Allow scoped packages: @scope/name
14
- if (raw.startsWith("@")) {
15
- const slash = raw.indexOf("/");
16
- if (slash > 1) {
17
- const scope = raw.slice(0, slash).toLowerCase();
18
- const name = raw.slice(slash + 1);
19
- const normalizedName = toNpmPackageName(name);
20
- return `${scope}/${normalizedName}`;
21
- }
22
- }
23
-
24
- return raw
25
- .toLowerCase()
26
- .replace(/\s+/g, "-")
27
- .replace(/[^a-z0-9._-]/g, "-")
28
- .replace(/-+/g, "-")
29
- .replace(/^[._-]+/, "")
30
- .replace(/[._-]+$/, "") || "my-app";
31
- }
32
-
33
- function copyDir(src, dest) {
34
- fs.mkdirSync(dest, { recursive: true });
35
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
36
- if (entry.name.startsWith("__")) continue;
37
-
38
- const srcPath = path.join(src, entry.name);
39
- const destPath = path.join(dest, entry.name);
40
- if (entry.isDirectory()) {
41
- copyDir(srcPath, destPath);
42
- } else {
43
- fs.copyFileSync(srcPath, destPath);
44
- }
45
- }
46
- }
47
-
48
- /**
49
- * 샘플별 App.tsx 주입용 메타데이터.
50
- * 새 샘플 추가 시: 1) template/../__samples/<id>/ 폴더 추가, 2) 여기에 항목 추가
51
- */
52
- const SAMPLE_CONFIG = {
53
- iap: {
54
- displayName: "인앱결제",
55
- import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
56
- route:
57
- ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
58
- getButton: (useTds) =>
59
- useTds
60
- ? '<Button color="dark" variant="weak" onClick={() => setPage("iap")}>인앱결제 테스트하기</Button>'
61
- : '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
62
- },
63
- iaa: {
64
- displayName: "인앱광고",
65
- import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
66
- route:
67
- ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
68
- getButton: (useTds) =>
69
- useTds
70
- ? '<Button color="dark" variant="weak" onClick={() => setPage("iaa")}>인앱광고 테스트하기</Button>'
71
- : '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
72
- },
73
- };
74
-
75
- const SAMPLE_PRIMARY_COLOR = [
76
- "#FF8A65",
77
- "#FD9B3C",
78
- "#E0B20C",
79
- "#3FD599",
80
- "#81C784",
81
- "#4DB6AC",
82
- "#4DD0E1",
83
- "#64B5F6",
84
- "#655DFF",
85
- "#9575CD",
86
- "#BA68C8",
87
- "#FF91D5",
88
- "#F06292",
89
- "#D7B59E",
90
- ];
91
-
92
- function fetchText(url) {
93
- return new Promise((resolve, reject) => {
94
- const get = (targetUrl) => {
95
- const client = targetUrl.startsWith("https") ? https : http;
96
- const req = client.get(
97
- targetUrl,
98
- { headers: { "Accept-Encoding": "gzip, deflate" } },
99
- (res) => {
100
- if (
101
- res.statusCode >= 300 &&
102
- res.statusCode < 400 &&
103
- res.headers.location
104
- ) {
105
- get(res.headers.location);
106
- return;
107
- }
108
- if (res.statusCode !== 200) {
109
- reject(new Error(`HTTP ${res.statusCode} for ${targetUrl}`));
110
- return;
111
- }
112
-
113
- let stream = res;
114
- const encoding = res.headers["content-encoding"];
115
- if (encoding === "gzip") {
116
- stream = res.pipe(zlib.createGunzip());
117
- } else if (encoding === "deflate") {
118
- stream = res.pipe(zlib.createInflate());
119
- }
120
-
121
- let data = "";
122
- stream.setEncoding("utf-8");
123
- stream.on("data", (chunk) => (data += chunk));
124
- stream.on("end", () => resolve(data));
125
- stream.on("error", reject);
126
- },
127
- );
128
- req.on("error", reject);
129
- };
130
- get(url);
131
- });
132
- }
133
-
134
- function parseArgs(argv) {
135
- const args = { _: [], sample: [] };
136
- for (let i = 0; i < argv.length; i++) {
137
- if (argv[i] === "--pm" && argv[i + 1]) {
138
- args.pm = argv[++i];
139
- } else if (argv[i] === "--ai" && argv[i + 1]) {
140
- args.ai = argv[++i];
141
- } else if (
142
- argv[i] === "--sample" &&
143
- argv[i + 1] &&
144
- !argv[i + 1].startsWith("--")
145
- ) {
146
- args.sample.push(...argv[++i].split(","));
147
- } else if (argv[i].startsWith("--")) {
148
- args[argv[i].slice(2)] = true;
149
- } else {
150
- args._.push(argv[i]);
151
- }
152
- }
153
- return args;
154
- }
155
-
156
- function printHelp() {
157
- console.log(`
158
- 사용법: create-ait-app [project-name] [options]
159
-
160
- options:
161
- --inline 질문을 생략하고 옵션만으로 설정합니다 (옵션 미지정 시 모두 n)
162
- --pm <name> 패키지 매니저를 지정합니다 (npm, yarn, pnpm)
163
- --tds TDS(Toss Design System) 패키지를 설치합니다
164
- --skills AI를 위한 skills 파일을 추가합니다
165
- --ai <name> AI 도구를 지정합니다 (cursor, claude, codex)
166
- --sample <name> 예제 코드를 추가합니다 (iap, iaa / 복수선택: iap,iaa)
167
- --help 이 도움말을 출력합니다
168
-
169
- links:
170
- 앱인토스 콘솔 https://apps-in-toss.toss.im/
171
- 앱인토스 개발자센터 https://developers-apps-in-toss.toss.im/
172
- 앱인토스 개발자 커뮤니티 https://techchat-apps-in-toss.toss.im/
173
- `);
174
- }
3
+ const path = require("path");
4
+ const { parseArgs, printHelp } = require("./cli");
5
+ const {
6
+ TEMPLATES_DIR,
7
+ TEMPLATE_IDS,
8
+ TEMPLATE_REGISTRY,
9
+ TEMPLATE_CHOICES,
10
+ resolveTemplateFolder,
11
+ } = require("./templates");
12
+ const { toNpmPackageName } = require("./utils/package-name");
13
+ const {
14
+ scaffoldProject,
15
+ installDependencies,
16
+ formatProject,
17
+ } = require("./scaffold");
18
+ const { writeAiSkills } = require("./skills");
175
19
 
176
20
  async function main() {
177
21
  const cliArgs = parseArgs(process.argv.slice(2));
@@ -183,7 +27,7 @@ async function main() {
183
27
 
184
28
  const isInline = cliArgs.inline;
185
29
  const hasAnyOptionFlag =
186
- cliArgs.tds || cliArgs.skills || cliArgs.sample.length > 0;
30
+ cliArgs.template || cliArgs.tds || cliArgs.skills || cliArgs.sample.length > 0;
187
31
 
188
32
  const projectName =
189
33
  cliArgs._[0] ||
@@ -224,22 +68,53 @@ async function main() {
224
68
  });
225
69
  }
226
70
 
227
- // --- 3. TDS ---
228
- let useTds;
229
- if (cliArgs.tds) {
230
- useTds = true;
231
- } else if (isInline) {
232
- useTds = false;
233
- } else if (hasAnyOptionFlag) {
234
- useTds = false;
71
+ let baseTemplateId = null;
72
+
73
+ if (cliArgs.template) {
74
+ if (cliArgs.template === "react-ts-tds") {
75
+ console.error(
76
+ "\n❌ react-ts-tds는 더 이상 템플릿 이름으로 사용할 수 없습니다. --template react-ts --tds를 사용해 주세요.",
77
+ );
78
+ process.exit(1);
79
+ }
80
+ if (!TEMPLATE_IDS.includes(cliArgs.template)) {
81
+ console.error(
82
+ `\n❌ 지원하지 않는 템플릿입니다: ${cliArgs.template} (${TEMPLATE_IDS.join(", ")} 중 선택)`,
83
+ );
84
+ process.exit(1);
85
+ }
86
+ baseTemplateId = cliArgs.template;
87
+ } else if (isInline || hasAnyOptionFlag) {
88
+ baseTemplateId = "react-ts";
235
89
  } else {
90
+ baseTemplateId = await select({
91
+ message: "사용할 템플릿을 선택하세요:",
92
+ choices: TEMPLATE_CHOICES,
93
+ });
94
+ }
95
+
96
+ let useTds = false;
97
+ if (cliArgs.tds) {
98
+ useTds = baseTemplateId === "react-ts";
99
+ } else if (!isInline && !hasAnyOptionFlag && baseTemplateId === "react-ts") {
236
100
  useTds = await confirm({
237
- message: "TDS(Toss Design System)를 사용할까요?",
101
+ message:
102
+ "TDS(Toss Design System)를 사용할까요? (앱인토스에 필수 아님, 기본값: 사용 안 함)",
238
103
  default: false,
239
104
  });
240
105
  }
241
106
 
242
- // --- 4. AI skills ---
107
+ const templateId = resolveTemplateFolder(baseTemplateId, useTds);
108
+ const template = TEMPLATE_REGISTRY[templateId];
109
+ const templateDir = path.join(TEMPLATES_DIR, templateId);
110
+
111
+ if (!fs.existsSync(templateDir)) {
112
+ console.error(`\n❌ 템플릿을 찾을 수 없습니다: templates/${templateId}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ const sampleConfig = template.sampleConfig;
117
+
243
118
  let useSkills;
244
119
  let aiTool;
245
120
  if (cliArgs.skills) {
@@ -277,8 +152,7 @@ async function main() {
277
152
  useSkills = aiTool !== "none";
278
153
  }
279
154
 
280
- // --- 5. 예제 코드 (복수 선택 가능) ---
281
- const validSamples = Object.keys(SAMPLE_CONFIG);
155
+ const validSamples = Object.keys(sampleConfig);
282
156
  let sampleChoices = [];
283
157
  if (cliArgs.sample.length > 0) {
284
158
  const invalid = cliArgs.sample.filter((s) => !validSamples.includes(s));
@@ -289,13 +163,11 @@ async function main() {
289
163
  process.exit(1);
290
164
  }
291
165
  sampleChoices = [...new Set(cliArgs.sample)];
292
- } else if (isInline) {
293
- sampleChoices = [];
294
- } else if (hasAnyOptionFlag) {
166
+ } else if (isInline || hasAnyOptionFlag) {
295
167
  sampleChoices = [];
296
168
  } else {
297
169
  const sampleChoiceList = validSamples.map((id) => ({
298
- name: SAMPLE_CONFIG[id].displayName,
170
+ name: sampleConfig[id].displayName,
299
171
  value: id,
300
172
  }));
301
173
  sampleChoices = await checkbox({
@@ -304,224 +176,38 @@ async function main() {
304
176
  });
305
177
  }
306
178
 
307
- console.log(`\n🚀 프로젝트를 생성합니다...\n`);
308
-
309
- const templateDir = path.resolve(__dirname, "..", "template");
179
+ console.log(
180
+ `\n🚀 프로젝트를 생성합니다... (templates/${templateId}${useTds ? ", TDS" : ""})\n`,
181
+ );
310
182
 
311
183
  try {
312
- copyDir(templateDir, targetDir);
313
-
314
- if (useTds) {
315
- copyDir(path.resolve(templateDir, "__tds"), targetDir);
316
- } else {
317
- copyDir(path.resolve(templateDir, "__default"), targetDir);
318
- }
319
-
320
- // package.json name 치환 (TDS 선택 시 React 18 사용 — TDS peer dependency)
321
- const pkgPath = path.join(targetDir, "package.json");
322
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
323
- pkg.name = packageName;
324
- if (useTds) {
325
- pkg.dependencies.react = "^18.0.0";
326
- pkg.dependencies["react-dom"] = "^18.0.0";
327
- if (pkg.devDependencies["@types/react"])
328
- pkg.devDependencies["@types/react"] = "^18.0.0";
329
- if (pkg.devDependencies["@types/react-dom"])
330
- pkg.devDependencies["@types/react-dom"] = "^18.0.0";
331
- }
332
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
333
-
334
- // pnpm: shamefully-hoist 설정 (granite가 Vite 플러그인을 찾을 수 있도록)
335
- if (packageManager === "pnpm") {
336
- fs.writeFileSync(
337
- path.join(targetDir, ".npmrc"),
338
- "shamefully-hoist=true\n",
339
- );
340
- }
341
-
342
- // granite.config.ts appName 치환
343
- const configPath = path.join(targetDir, "granite.config.ts");
344
- const configContent = fs.readFileSync(configPath, "utf-8");
345
- fs.writeFileSync(
346
- configPath,
347
- configContent
348
- .replace("{{APP_NAME}}", projectName)
349
- .replace(
350
- "{{PRIMARY_COLOR}}",
351
- SAMPLE_PRIMARY_COLOR[
352
- Math.floor(Math.random() * SAMPLE_PRIMARY_COLOR.length)
353
- ],
354
- ),
355
- );
356
-
357
- // README.md 프로젝트명 + 선택한 패키지 매니저 명령어 치환
358
- const readmePath = path.join(targetDir, "README.md");
359
- const pmDev =
360
- packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
361
- const pmBuild =
362
- packageManager === "npm" ? "npm run build" : `${packageManager} build`;
363
- const pmDeploy =
364
- packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
365
-
366
- let readmeContent = fs.readFileSync(readmePath, "utf-8");
367
- readmeContent = readmeContent
368
- .replace(/\{\{APP_NAME\}\}/g, projectName)
369
- .replace(/\{\{PM_DEV\}\}/g, pmDev)
370
- .replace(/\{\{PM_BUILD\}\}/g, pmBuild)
371
- .replace(/\{\{PM_DEPLOY\}\}/g, pmDeploy);
372
- fs.writeFileSync(readmePath, readmeContent);
373
-
374
- // 예제 코드: 선택한 샘플만 template/../__samples/<id> 에서 src 로 복사 후 App.tsx 플레이스홀더 치환
375
- const samplesDir = path.join(
376
- path.resolve(templateDir, useTds ? "__tds" : "__default"),
377
- "__samples",
378
- );
379
-
380
- for (const id of sampleChoices) {
381
- const sampleRoot = path.join(samplesDir, id);
382
-
383
- copyDir(sampleRoot, targetDir);
384
- }
385
-
386
- const srcDir = path.join(targetDir, "src");
387
- const appPath = path.join(srcDir, "App.tsx");
388
- let appContent = fs.existsSync(appPath)
389
- ? fs.readFileSync(appPath, "utf-8")
390
- : null;
391
-
392
- if (appContent) {
393
- const hasSamples = sampleChoices.length > 0;
394
- const sampleImports = hasSamples
395
- ? sampleChoices
396
- .map((id) => SAMPLE_CONFIG[id]?.import)
397
- .filter(Boolean)
398
- .join("\n") + '\nimport { useState } from "react";'
399
- : "";
400
- const pageStateAndRoutes = hasSamples
401
- ? " const [page, setPage] = useState<string | null>(null);\n\n" +
402
- sampleChoices
403
- .map((id) => SAMPLE_CONFIG[id]?.route)
404
- .filter(Boolean)
405
- .join("\n") +
406
- "\n\n"
407
- : "";
408
- const sampleButtons = hasSamples
409
- ? sampleChoices
410
- .map((id) => SAMPLE_CONFIG[id]?.getButton(useTds))
411
- .filter(Boolean)
412
- .join("\n\n ")
413
- : "";
414
-
415
- appContent = appContent
416
- .replace("{{SAMPLE_IMPORTS}}", sampleImports)
417
- .replace("{{PAGE_STATE_AND_ROUTES}}", pageStateAndRoutes)
418
- .replace("{{SAMPLE_BUTTONS}}", sampleButtons);
419
- fs.writeFileSync(appPath, appContent);
420
- }
421
-
422
- // 의존성 설치
423
- console.log(`📦 의존성을 설치합니다...\n`);
424
-
425
- const installCommands = {
426
- npm: "npm install",
427
- yarn: "yarn",
428
- pnpm: "pnpm install",
429
- };
430
- execSync(installCommands[packageManager], {
431
- stdio: "inherit",
432
- cwd: targetDir,
184
+ scaffoldProject({
185
+ templateDir,
186
+ targetDir,
187
+ template,
188
+ sampleConfig,
189
+ sampleChoices,
190
+ projectName,
191
+ packageName,
192
+ packageManager,
433
193
  });
434
194
 
435
- console.log(`\n📦 @apps-in-toss/web-framework 최신 버전을 설치합니다...\n`);
436
-
437
- const addCmd = { npm: "npm install", yarn: "yarn add", pnpm: "pnpm add" };
438
- execSync(`${addCmd[packageManager]} @apps-in-toss/web-framework@latest`, {
439
- stdio: "inherit",
440
- cwd: targetDir,
441
- });
442
-
443
- // TDS 설치
444
- if (useTds) {
445
- console.log(`\n📦 TDS 패키지를 설치합니다...\n`);
446
- const tdsPackages =
447
- "@toss/tds-mobile @toss/tds-mobile-ait @toss/tds-colors @emotion/react@^11 react@^18 react-dom@^18";
448
- execSync(`${addCmd[packageManager]} ${tdsPackages}`, {
449
- stdio: "inherit",
450
- cwd: targetDir,
451
- });
452
- }
195
+ installDependencies(targetDir, packageManager);
453
196
 
454
- // AI skills
455
197
  if (useSkills) {
456
- console.log(`\n📄 AI skills 파일을 추가합니다... (${aiTool})\n`);
457
-
458
- const aitDocs = await fetchText(
459
- "https://developers-apps-in-toss.toss.im/llms.txt",
460
- );
461
- let tdsDocs;
462
- if (useTds) {
463
- tdsDocs = await fetchText(
464
- "https://tossmini-docs.toss.im/tds-mobile/llms-full.txt",
465
- );
466
- }
467
-
468
- if (aiTool === "cursor") {
469
- const skillsDir = path.join(targetDir, ".cursor", "skills");
470
- fs.mkdirSync(skillsDir, { recursive: true });
471
- fs.writeFileSync(path.join(skillsDir, "apps-in-toss.md"), aitDocs);
472
- console.log(" ✓ .cursor/skills/apps-in-toss.md 추가 완료");
473
- if (tdsDocs) {
474
- fs.writeFileSync(path.join(skillsDir, "tds-mobile.md"), tdsDocs);
475
- console.log(" ✓ .cursor/skills/tds-mobile.md 추가 완료");
476
- }
477
- } else if (aiTool === "claude") {
478
- const docsDir = path.join(targetDir, "docs", "skills");
479
- fs.mkdirSync(docsDir, { recursive: true });
480
-
481
- fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
482
- console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
483
-
484
- if (tdsDocs) {
485
- fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
486
- console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
487
- }
488
-
489
- let claudeMd = "@docs/skills/apps-in-toss.md\n";
490
- if (tdsDocs) {
491
- claudeMd += "@docs/skills/tds-mobile.md\n";
492
- }
493
- fs.writeFileSync(path.join(targetDir, "CLAUDE.md"), claudeMd);
494
- console.log(" ✓ CLAUDE.md 추가 완료");
495
- } else if (aiTool === "codex") {
496
- const docsDir = path.join(targetDir, "docs", "skills");
497
- fs.mkdirSync(docsDir, { recursive: true });
498
-
499
- fs.writeFileSync(path.join(docsDir, "apps-in-toss.md"), aitDocs);
500
- console.log(" ✓ docs/skills/apps-in-toss.md 추가 완료");
501
-
502
- if (tdsDocs) {
503
- fs.writeFileSync(path.join(docsDir, "tds-mobile.md"), tdsDocs);
504
- console.log(" ✓ docs/skills/tds-mobile.md 추가 완료");
505
- }
506
-
507
- let agentsMd = "docs/skills/apps-in-toss.md 파일을 참고하세요.\n";
508
- if (tdsDocs) {
509
- agentsMd += "docs/skills/tds-mobile.md 파일을 참고하세요.\n";
510
- }
511
- fs.writeFileSync(path.join(targetDir, "AGENTS.md"), agentsMd);
512
- console.log(" ✓ AGENTS.md 추가 완료");
513
- }
198
+ await writeAiSkills({
199
+ targetDir,
200
+ aiTool,
201
+ useTds: template.useTds,
202
+ });
514
203
  }
515
204
 
516
- // 코드 포맷팅
517
- console.log(`\n📐 코드 포맷팅을 실행합니다...\n`);
518
- const formatCmd =
519
- packageManager === "npm" ? "npm run format" : `${packageManager} format`;
520
- execSync(formatCmd, { stdio: "inherit", cwd: targetDir });
205
+ formatProject(targetDir, packageManager);
521
206
 
522
207
  console.log(`
523
208
  ✅ 프로젝트가 성공적으로 생성되었습니다!
524
209
 
210
+ 템플릿: templates/${templateId}
525
211
  cd ${projectName}
526
212
  ${packageManager === "npm" ? "npm run dev" : `${packageManager} dev`}
527
213
  `);
@@ -531,4 +217,10 @@ async function main() {
531
217
  }
532
218
  }
533
219
 
534
- module.exports = { main };
220
+ module.exports = {
221
+ main,
222
+ TEMPLATE_IDS,
223
+ TEMPLATE_REGISTRY,
224
+ TEMPLATE_CHOICES,
225
+ resolveTemplateFolder,
226
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * React + TypeScript 샘플 주입 메타데이터 (react-ts)
3
+ */
4
+ const REACT_SAMPLE_CONFIG = {
5
+ iap: {
6
+ displayName: "인앱결제",
7
+ import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
8
+ route:
9
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
10
+ getButton: () =>
11
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
12
+ },
13
+ iaa: {
14
+ displayName: "인앱광고",
15
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
16
+ route:
17
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
18
+ getButton: () =>
19
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
20
+ },
21
+ };
22
+
23
+ /**
24
+ * React + JavaScript 샘플 주입 메타데이터 (react)
25
+ */
26
+ const REACT_JS_SAMPLE_CONFIG = {
27
+ iap: {
28
+ displayName: "인앱결제",
29
+ import:
30
+ 'import { InAppPurchasePage } from "./pages/InAppPurchasePage.jsx";',
31
+ route:
32
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
33
+ getButton: () =>
34
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iap")}>인앱결제 테스트하기</button>',
35
+ },
36
+ iaa: {
37
+ displayName: "인앱광고",
38
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage.jsx";',
39
+ route:
40
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
41
+ getButton: () =>
42
+ '<button type="button" className="app-button app-button-ghost" onClick={() => setPage("iaa")}>인앱광고 테스트하기</button>',
43
+ },
44
+ };
45
+
46
+ /**
47
+ * React + TypeScript + TDS 샘플 주입 메타데이터 (react-ts-tds)
48
+ */
49
+ const REACT_TDS_SAMPLE_CONFIG = {
50
+ iap: {
51
+ displayName: "인앱결제",
52
+ import: 'import { InAppPurchasePage } from "./pages/InAppPurchasePage";',
53
+ route:
54
+ ' if (page === "iap") return <InAppPurchasePage onBack={() => setPage(null)} />;',
55
+ getButton: () =>
56
+ '<Button color="dark" variant="weak" onClick={() => setPage("iap")}>인앱결제 테스트하기</Button>',
57
+ },
58
+ iaa: {
59
+ displayName: "인앱광고",
60
+ import: 'import { InAppAdsPage } from "./pages/InAppAdsPage";',
61
+ route:
62
+ ' if (page === "iaa") return <InAppAdsPage onBack={() => setPage(null)} />;',
63
+ getButton: () =>
64
+ '<Button color="dark" variant="weak" onClick={() => setPage("iaa")}>인앱광고 테스트하기</Button>',
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Vanilla JavaScript 샘플 주입 메타데이터 (js)
70
+ */
71
+ const JS_SAMPLE_CONFIG = {
72
+ iap: {
73
+ displayName: "인앱결제",
74
+ import:
75
+ 'import { mountInAppPurchasePage } from "./pages/InAppPurchasePage.js";',
76
+ route: ` if (currentPage === "iap") {
77
+ mountInAppPurchasePage(() => {
78
+ currentPage = null;
79
+ render();
80
+ });
81
+ return;
82
+ }`,
83
+ getButton: () =>
84
+ '<button type="button" class="app-button app-button-ghost" data-page="iap">인앱결제 테스트하기</button>',
85
+ },
86
+ iaa: {
87
+ displayName: "인앱광고",
88
+ import: 'import { mountInAppAdsPage } from "./pages/InAppAdsPage.js";',
89
+ route: ` if (currentPage === "iaa") {
90
+ mountInAppAdsPage(() => {
91
+ currentPage = null;
92
+ render();
93
+ });
94
+ return;
95
+ }`,
96
+ getButton: () =>
97
+ '<button type="button" class="app-button app-button-ghost" data-page="iaa">인앱광고 테스트하기</button>',
98
+ },
99
+ };
100
+
101
+ /**
102
+ * Vanilla TypeScript 샘플 주입 메타데이터 (ts)
103
+ */
104
+ const TS_SAMPLE_CONFIG = {
105
+ iap: {
106
+ displayName: "인앱결제",
107
+ import:
108
+ 'import { mountInAppPurchasePage } from "./pages/InAppPurchasePage.ts";',
109
+ route: ` if (currentPage === "iap") {
110
+ mountInAppPurchasePage(() => {
111
+ currentPage = null;
112
+ render();
113
+ });
114
+ return;
115
+ }`,
116
+ getButton: () =>
117
+ '<button type="button" class="app-button app-button-ghost" data-page="iap">인앱결제 테스트하기</button>',
118
+ },
119
+ iaa: {
120
+ displayName: "인앱광고",
121
+ import: 'import { mountInAppAdsPage } from "./pages/InAppAdsPage.ts";',
122
+ route: ` if (currentPage === "iaa") {
123
+ mountInAppAdsPage(() => {
124
+ currentPage = null;
125
+ render();
126
+ });
127
+ return;
128
+ }`,
129
+ getButton: () =>
130
+ '<button type="button" class="app-button app-button-ghost" data-page="iaa">인앱광고 테스트하기</button>',
131
+ },
132
+ };
133
+
134
+ module.exports = {
135
+ REACT_SAMPLE_CONFIG,
136
+ REACT_JS_SAMPLE_CONFIG,
137
+ REACT_TDS_SAMPLE_CONFIG,
138
+ JS_SAMPLE_CONFIG,
139
+ TS_SAMPLE_CONFIG,
140
+ };