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.
- package/README.md +35 -49
- package/package.json +2 -6
- package/src/cli.js +52 -0
- package/src/main.js +88 -396
- package/src/sample-configs.js +140 -0
- package/src/sample-inject.js +65 -0
- package/src/scaffold.js +111 -0
- package/src/skills.js +66 -0
- package/src/templates.js +88 -0
- package/src/utils/copy-dir.js +19 -0
- package/src/utils/fetch-text.js +47 -0
- package/src/utils/package-name.js +26 -0
- package/templates/js/eslint.config.js +14 -0
- package/templates/js/index.html +12 -0
- package/templates/js/package.json +20 -0
- package/templates/js/samples/iaa/src/lib/inAppAds.js +119 -0
- package/templates/js/samples/iaa/src/pages/InAppAdsPage.js +83 -0
- package/templates/js/samples/iap/src/lib/inAppPurchase.js +105 -0
- package/templates/js/samples/iap/src/pages/InAppPurchasePage.js +102 -0
- package/templates/js/src/app.js +58 -0
- package/templates/js/src/main.js +2 -0
- package/templates/js/vite.config.js +3 -0
- package/templates/react/README.md +26 -0
- package/templates/react/eslint.config.js +30 -0
- package/templates/react/granite.config.ts +20 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +27 -0
- package/templates/react/public/appsintoss-logo.png +0 -0
- package/templates/react/samples/iaa/src/hooks/useInAppAds.js +102 -0
- package/templates/react/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/react/samples/iaa/src/pages/InAppAdsPage.jsx +75 -0
- package/templates/react/samples/iap/public/icon-document.png +0 -0
- package/templates/react/samples/iap/src/hooks/useInAppPurchase.js +95 -0
- package/templates/react/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/react/samples/iap/src/pages/InAppPurchasePage.jsx +115 -0
- package/templates/react/src/App.css +104 -0
- package/templates/react/src/App.jsx +45 -0
- package/templates/react/src/index.css +27 -0
- package/templates/react/src/main.jsx +10 -0
- package/templates/react/vite.config.js +6 -0
- package/templates/react-ts/README.md +26 -0
- package/templates/react-ts/eslint.config.js +28 -0
- package/templates/react-ts/granite.config.ts +20 -0
- package/templates/react-ts/index.html +12 -0
- package/templates/react-ts/public/appsintoss-logo.png +0 -0
- package/templates/react-ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/react-ts/samples/iap/public/icon-document.png +0 -0
- package/templates/react-ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/react-ts/src/App.css +104 -0
- package/templates/react-ts/src/index.css +27 -0
- package/templates/react-ts/src/vite-env.d.ts +6 -0
- package/templates/react-ts/tsconfig.app.json +22 -0
- package/templates/react-ts/tsconfig.json +7 -0
- package/templates/react-ts/tsconfig.node.json +20 -0
- package/templates/react-ts/vite.config.ts +6 -0
- package/templates/react-ts-tds/README.md +26 -0
- package/templates/react-ts-tds/granite.config.ts +20 -0
- package/templates/react-ts-tds/package.json +35 -0
- package/templates/react-ts-tds/public/appsintoss-logo.png +0 -0
- package/templates/ts/README.md +26 -0
- package/templates/ts/eslint.config.js +15 -0
- package/templates/ts/granite.config.ts +20 -0
- package/templates/ts/index.html +12 -0
- package/templates/ts/package.json +22 -0
- package/templates/ts/public/appsintoss-logo.png +0 -0
- package/templates/ts/samples/iaa/src/lib/inAppAds.ts +132 -0
- package/templates/ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
- package/templates/ts/samples/iaa/src/pages/InAppAdsPage.ts +85 -0
- package/templates/ts/samples/iap/public/icon-document.png +0 -0
- package/templates/ts/samples/iap/src/lib/inAppPurchase.ts +114 -0
- package/templates/ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
- package/templates/ts/samples/iap/src/pages/InAppPurchasePage.ts +105 -0
- package/templates/ts/src/App.css +104 -0
- package/templates/ts/src/app.ts +60 -0
- package/templates/ts/src/index.css +27 -0
- package/templates/ts/src/main.ts +2 -0
- package/templates/ts/src/vite-env.d.ts +1 -0
- package/templates/ts/tsconfig.app.json +22 -0
- package/templates/ts/tsconfig.json +7 -0
- package/templates/ts/tsconfig.node.json +20 -0
- package/templates/ts/vite.config.ts +3 -0
- /package/{template → templates/js}/README.md +0 -0
- /package/{template → templates/js}/granite.config.ts +0 -0
- /package/{template → templates/js}/public/appsintoss-logo.png +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iaa/src/pages/InAppAdsPage.css +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iap/public/icon-document.png +0 -0
- /package/{template/__default/__samples → templates/js/samples}/iap/src/pages/InAppPurchasePage.css +0 -0
- /package/{template/__default → templates/js}/src/App.css +0 -0
- /package/{template/__default → templates/js}/src/index.css +0 -0
- /package/{template → templates/react-ts}/package.json +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
- /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
- /package/{template/__default → templates/react-ts}/src/App.tsx +0 -0
- /package/{template/__default → templates/react-ts}/src/main.tsx +0 -0
- /package/{template → templates/react-ts-tds}/eslint.config.js +0 -0
- /package/{template → templates/react-ts-tds}/index.html +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/public/icon-document.png +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
- /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/App.css +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/App.tsx +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/index.css +0 -0
- /package/{template/__tds → templates/react-ts-tds}/src/main.tsx +0 -0
- /package/{template → templates/react-ts-tds}/src/vite-env.d.ts +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.app.json +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.json +0 -0
- /package/{template → templates/react-ts-tds}/tsconfig.node.json +0 -0
- /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
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
if (cliArgs.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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:
|
|
101
|
+
message:
|
|
102
|
+
"TDS(Toss Design System)를 사용할까요? (앱인토스에 필수 아님, 기본값: 사용 안 함)",
|
|
238
103
|
default: false,
|
|
239
104
|
});
|
|
240
105
|
}
|
|
241
106
|
|
|
242
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
308
|
-
|
|
309
|
-
|
|
179
|
+
console.log(
|
|
180
|
+
`\n🚀 프로젝트를 생성합니다... (templates/${templateId}${useTds ? ", TDS" : ""})\n`,
|
|
181
|
+
);
|
|
310
182
|
|
|
311
183
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 = {
|
|
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
|
+
};
|