byuckchon-frontend-cli 1.0.0
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/bin/index.js +5 -0
- package/package.json +22 -0
- package/src/commands/init.js +39 -0
- package/src/constants/versions.js +49 -0
- package/src/generators/createBaseFiles.js +681 -0
- package/src/generators/createFolders.js +67 -0
- package/src/generators/createPackageJson.js +86 -0
- package/src/generators/createProject.js +31 -0
- package/src/generators/createReadme.js +73 -0
- package/src/prompts/initPrompts.js +26 -0
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "byuckchon-frontend-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "byuckchon frontend CLI for creating React and Next.js projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"byuckchon-frontend-cli": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.3.0",
|
|
14
|
+
"inquirer": "^9.3.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { createProject } from '../generators/createProject.js';
|
|
4
|
+
import { askInitQuestions } from '../prompts/initPrompts.js';
|
|
5
|
+
|
|
6
|
+
export async function initCommand() {
|
|
7
|
+
console.log(chalk.bold.cyan('\n cli-test — 프로젝트 생성기\n'));
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const answers = await askInitQuestions();
|
|
11
|
+
|
|
12
|
+
console.log(
|
|
13
|
+
chalk.dim(
|
|
14
|
+
`\n ${answers.framework} 프로젝트를 생성하는 중... (${answers.projectName})\n`,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await createProject({ ...answers, typescript: true });
|
|
19
|
+
|
|
20
|
+
console.log(
|
|
21
|
+
chalk.bold.green(`\n ✓ ${answers.projectName} 프로젝트가 생성되었습니다!\n`),
|
|
22
|
+
);
|
|
23
|
+
console.log(chalk.yellow(' 다음 명령어로 시작하세요:\n'));
|
|
24
|
+
console.log(chalk.white(` cd ${answers.projectName}`));
|
|
25
|
+
console.log(chalk.white(' npm run dev\n'));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === 'EEXIST') {
|
|
28
|
+
console.error(
|
|
29
|
+
chalk.red(`\n 오류: '${error.path}' 폴더가 이미 존재합니다.\n`),
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
console.error(
|
|
33
|
+
chalk.red('\n 프로젝트 생성 중 오류가 발생했습니다:'),
|
|
34
|
+
error.message,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const versions = {
|
|
2
|
+
// Core frameworks
|
|
3
|
+
react: '^18.3.1',
|
|
4
|
+
'react-dom': '^18.3.1',
|
|
5
|
+
'next-react': '^19.2.1',
|
|
6
|
+
'next-react-dom': '^19.2.1',
|
|
7
|
+
next: '15.1.9',
|
|
8
|
+
|
|
9
|
+
// Build tool (React only)
|
|
10
|
+
vite: '^6.0.0',
|
|
11
|
+
'@vitejs/plugin-react': '^4.3.0',
|
|
12
|
+
'vite-plugin-svgr': '^4.3.0',
|
|
13
|
+
'@svgr/webpack': '^8.1.0',
|
|
14
|
+
|
|
15
|
+
// TypeScript
|
|
16
|
+
typescript: '^5.7.0',
|
|
17
|
+
'@types/react': '^18.3.3',
|
|
18
|
+
'@types/react-dom': '^18.3.0',
|
|
19
|
+
'@types/node': '^22.0.0',
|
|
20
|
+
|
|
21
|
+
// State & Data
|
|
22
|
+
zustand: '^5.0.3',
|
|
23
|
+
axios: '^1.8.4',
|
|
24
|
+
'@tanstack/react-query': '^5.74.4',
|
|
25
|
+
|
|
26
|
+
// Styling
|
|
27
|
+
tailwindcss: '^4.1.4',
|
|
28
|
+
'@tailwindcss/vite': '^4.1.4',
|
|
29
|
+
'@tailwindcss/postcss': '^4',
|
|
30
|
+
|
|
31
|
+
// Linting
|
|
32
|
+
eslint: '^8.57.0',
|
|
33
|
+
'eslint-config-expo': '^8.0.0',
|
|
34
|
+
'eslint-config-next': '^15.0.0',
|
|
35
|
+
'eslint-import-resolver-typescript': '^3.6.0',
|
|
36
|
+
'eslint-plugin-import': '^2.29.0',
|
|
37
|
+
'eslint-plugin-react': '^7.34.0',
|
|
38
|
+
'eslint-plugin-react-hooks': '^4.6.0',
|
|
39
|
+
'@typescript-eslint/eslint-plugin': '^7.0.0',
|
|
40
|
+
'@typescript-eslint/parser': '^7.0.0',
|
|
41
|
+
|
|
42
|
+
// Formatting
|
|
43
|
+
prettier: '^3.3.0',
|
|
44
|
+
'prettier-plugin-tailwindcss': '^0.6.11',
|
|
45
|
+
'style-dictionary': '^5.4.0',
|
|
46
|
+
zod: '^3.24.3',
|
|
47
|
+
'@trivago/prettier-plugin-sort-imports': '^5.2.2',
|
|
48
|
+
'eslint-plugin-unused-imports': '^4.1.4',
|
|
49
|
+
};
|
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
async function write(filePath, content) {
|
|
5
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ─── 공통 설정 파일 ────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
async function createPrettierConfig(rootDir) {
|
|
11
|
+
const config = {
|
|
12
|
+
semi: true,
|
|
13
|
+
trailingComma: 'all',
|
|
14
|
+
singleQuote: true,
|
|
15
|
+
tabWidth: 2,
|
|
16
|
+
useTabs: false,
|
|
17
|
+
printWidth: 80,
|
|
18
|
+
plugins: [
|
|
19
|
+
'@trivago/prettier-plugin-sort-imports',
|
|
20
|
+
'prettier-plugin-tailwindcss',
|
|
21
|
+
],
|
|
22
|
+
importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'],
|
|
23
|
+
importOrderSeparation: true,
|
|
24
|
+
importOrderSortSpecifiers: true,
|
|
25
|
+
};
|
|
26
|
+
await write(path.join(rootDir, '.prettierrc'), JSON.stringify(config, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function createEslintConfig(rootDir) {
|
|
30
|
+
await write(
|
|
31
|
+
path.join(rootDir, '.eslintrc.cjs'),
|
|
32
|
+
`module.exports = {
|
|
33
|
+
env: {
|
|
34
|
+
browser: true,
|
|
35
|
+
es2022: true,
|
|
36
|
+
node: true,
|
|
37
|
+
},
|
|
38
|
+
extends: ['expo', 'eslint:recommended'],
|
|
39
|
+
plugins: ['unused-imports'],
|
|
40
|
+
rules: {
|
|
41
|
+
'unused-imports/no-unused-imports': 'error',
|
|
42
|
+
'unused-imports/no-unused-vars': [
|
|
43
|
+
'warn',
|
|
44
|
+
{
|
|
45
|
+
vars: 'all',
|
|
46
|
+
varsIgnorePattern: '^_',
|
|
47
|
+
args: 'after-used',
|
|
48
|
+
argsIgnorePattern: '^_',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
'react/self-closing-comp': [
|
|
52
|
+
'warn',
|
|
53
|
+
{
|
|
54
|
+
component: true,
|
|
55
|
+
html: true,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
settings: {
|
|
60
|
+
'import/resolver': {
|
|
61
|
+
typescript: {},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function createNextEslintConfig(rootDir) {
|
|
70
|
+
await write(
|
|
71
|
+
path.join(rootDir, '.eslintrc.cjs'),
|
|
72
|
+
`// 현 파일이 eslint config type 을 따른다는 선언
|
|
73
|
+
/** @type {import("eslint").Linter.Config} */
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
root: true,
|
|
77
|
+
|
|
78
|
+
// next.js 공식 eslint 규칙 적용
|
|
79
|
+
extends: ["next/core-web-vitals", "next/typescript"],
|
|
80
|
+
|
|
81
|
+
// import 문 자동정렬, 유효성 검사, 경로 오류 방지
|
|
82
|
+
plugins: ["import"],
|
|
83
|
+
|
|
84
|
+
rules: {
|
|
85
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
86
|
+
"import/order": [
|
|
87
|
+
"error",
|
|
88
|
+
{
|
|
89
|
+
// builtin: node 내장 모듈, external: npm 패키지, internal: 프로젝트 내 모듈, parent: 상위 경로, sibling: 형제 경로, index: 인덱스 파일
|
|
90
|
+
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
|
|
91
|
+
// 특정 패턴 그룹에 속하는 모듈 순서 지정
|
|
92
|
+
pathGroups: [
|
|
93
|
+
{
|
|
94
|
+
pattern: "react",
|
|
95
|
+
group: "external",
|
|
96
|
+
position: "before",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
pattern: "next/**",
|
|
100
|
+
group: "external",
|
|
101
|
+
position: "before",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
pattern: "@/**",
|
|
105
|
+
group: "internal",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
// 중복 정렬 방지
|
|
109
|
+
pathGroupsExcludedImportTypes: ["react"],
|
|
110
|
+
// 알파벳 순서대로 오름차순 정렬
|
|
111
|
+
alphabetize: { order: "asc", caseInsensitive: true },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// 정렬 제외 파일 목록
|
|
117
|
+
ignorePatterns: ["node_modules/", ".next/", "out/", "build/", "next-env.d.ts"],
|
|
118
|
+
};
|
|
119
|
+
`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function createGitignore(rootDir, framework) {
|
|
124
|
+
const base = `# Dependencies
|
|
125
|
+
node_modules/
|
|
126
|
+
|
|
127
|
+
# Build
|
|
128
|
+
dist/
|
|
129
|
+
build/
|
|
130
|
+
|
|
131
|
+
# Env files
|
|
132
|
+
.env
|
|
133
|
+
.env.*
|
|
134
|
+
.env.local
|
|
135
|
+
.env.*.local
|
|
136
|
+
|
|
137
|
+
# OS
|
|
138
|
+
.DS_Store
|
|
139
|
+
Thumbs.db
|
|
140
|
+
|
|
141
|
+
# Editor
|
|
142
|
+
.vscode/
|
|
143
|
+
.idea/
|
|
144
|
+
*.suo
|
|
145
|
+
*.sw?
|
|
146
|
+
|
|
147
|
+
# Logs
|
|
148
|
+
npm-debug.log*
|
|
149
|
+
yarn-error.log*
|
|
150
|
+
|
|
151
|
+
# Additional ignores
|
|
152
|
+
node_modules
|
|
153
|
+
dist
|
|
154
|
+
dist-ssr
|
|
155
|
+
*.local
|
|
156
|
+
.env
|
|
157
|
+
.env.production
|
|
158
|
+
.history
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
const nextExtra = `
|
|
162
|
+
# Next.js
|
|
163
|
+
.next/
|
|
164
|
+
out/
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
await write(path.join(rootDir, '.gitignore'), base + (framework === 'next' ? nextExtra : ''));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function createVscodeSettings(rootDir) {
|
|
171
|
+
await fs.mkdir(path.join(rootDir, '.vscode'), { recursive: true });
|
|
172
|
+
await write(
|
|
173
|
+
path.join(rootDir, '.vscode/settings.json'),
|
|
174
|
+
JSON.stringify(
|
|
175
|
+
{
|
|
176
|
+
'editor.defaultFormatter': 'esbenp.prettier-vscode',
|
|
177
|
+
'editor.formatOnSave': true,
|
|
178
|
+
'eslint.validate': [
|
|
179
|
+
'javascript',
|
|
180
|
+
'typescript',
|
|
181
|
+
'javascriptreact',
|
|
182
|
+
'typescriptreact',
|
|
183
|
+
],
|
|
184
|
+
'editor.codeActionsOnSave': {
|
|
185
|
+
'source.organizeImports': 'always',
|
|
186
|
+
'source.fixAll.eslint': 'always',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── React (Vite) ─────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
async function createReactBaseFiles(rootDir, config) {
|
|
198
|
+
// index.html
|
|
199
|
+
await write(
|
|
200
|
+
path.join(rootDir, 'index.html'),
|
|
201
|
+
`<!doctype html>
|
|
202
|
+
<html lang="ko">
|
|
203
|
+
<head>
|
|
204
|
+
<meta charset="UTF-8" />
|
|
205
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
206
|
+
<title>${config.projectName}</title>
|
|
207
|
+
</head>
|
|
208
|
+
<body>
|
|
209
|
+
<div id="root"></div>
|
|
210
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
211
|
+
</body>
|
|
212
|
+
</html>
|
|
213
|
+
`,
|
|
214
|
+
);
|
|
215
|
+
await write(path.join(rootDir, 'public/robots.txt'), `User-agent: *\nDisallow: /\n`);
|
|
216
|
+
|
|
217
|
+
// vite.config.ts
|
|
218
|
+
await write(
|
|
219
|
+
path.join(rootDir, 'vite.config.ts'),
|
|
220
|
+
`import tailwindcss from '@tailwindcss/vite';
|
|
221
|
+
import react from '@vitejs/plugin-react';
|
|
222
|
+
import { defineConfig } from 'vite';
|
|
223
|
+
import svgr from 'vite-plugin-svgr';
|
|
224
|
+
|
|
225
|
+
// https://vite.dev/config/
|
|
226
|
+
export default defineConfig({
|
|
227
|
+
plugins: [
|
|
228
|
+
react(),
|
|
229
|
+
tailwindcss(),
|
|
230
|
+
svgr({
|
|
231
|
+
svgrOptions: {
|
|
232
|
+
icon: true,
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
],
|
|
236
|
+
resolve: {
|
|
237
|
+
alias: {
|
|
238
|
+
'@': '/src',
|
|
239
|
+
'@icons': '/src/assets/icons',
|
|
240
|
+
'@images': '/src/assets/images',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
`,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// tsconfig.json
|
|
248
|
+
await write(
|
|
249
|
+
path.join(rootDir, 'tsconfig.json'),
|
|
250
|
+
JSON.stringify(
|
|
251
|
+
{
|
|
252
|
+
files: [],
|
|
253
|
+
references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }],
|
|
254
|
+
},
|
|
255
|
+
null,
|
|
256
|
+
2,
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// tsconfig.app.json
|
|
261
|
+
await write(
|
|
262
|
+
path.join(rootDir, 'tsconfig.app.json'),
|
|
263
|
+
JSON.stringify(
|
|
264
|
+
{
|
|
265
|
+
compilerOptions: {
|
|
266
|
+
tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo',
|
|
267
|
+
target: 'ES2020',
|
|
268
|
+
useDefineForClassFields: true,
|
|
269
|
+
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
270
|
+
module: 'ESNext',
|
|
271
|
+
skipLibCheck: true,
|
|
272
|
+
moduleResolution: 'bundler',
|
|
273
|
+
allowImportingTsExtensions: true,
|
|
274
|
+
isolatedModules: true,
|
|
275
|
+
moduleDetection: 'force',
|
|
276
|
+
noEmit: true,
|
|
277
|
+
jsx: 'react-jsx',
|
|
278
|
+
strict: true,
|
|
279
|
+
noUnusedLocals: true,
|
|
280
|
+
noUnusedParameters: true,
|
|
281
|
+
noFallthroughCasesInSwitch: true,
|
|
282
|
+
noUncheckedSideEffectImports: true,
|
|
283
|
+
baseUrl: '.',
|
|
284
|
+
paths: {
|
|
285
|
+
'@/*': ['src/*'],
|
|
286
|
+
'@icons/*': ['src/assets/icons/*'],
|
|
287
|
+
'@images/*': ['src/assets/images/*'],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
include: ['src', 'src/svg.d.ts'],
|
|
291
|
+
},
|
|
292
|
+
null,
|
|
293
|
+
2,
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// tsconfig.node.json
|
|
298
|
+
await write(
|
|
299
|
+
path.join(rootDir, 'tsconfig.node.json'),
|
|
300
|
+
JSON.stringify(
|
|
301
|
+
{
|
|
302
|
+
compilerOptions: {
|
|
303
|
+
tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo',
|
|
304
|
+
target: 'ES2022',
|
|
305
|
+
lib: ['ES2023'],
|
|
306
|
+
module: 'ESNext',
|
|
307
|
+
skipLibCheck: true,
|
|
308
|
+
moduleResolution: 'bundler',
|
|
309
|
+
allowImportingTsExtensions: true,
|
|
310
|
+
isolatedModules: true,
|
|
311
|
+
moduleDetection: 'force',
|
|
312
|
+
noEmit: true,
|
|
313
|
+
strict: true,
|
|
314
|
+
noUnusedLocals: true,
|
|
315
|
+
noUnusedParameters: true,
|
|
316
|
+
noFallthroughCasesInSwitch: true,
|
|
317
|
+
noUncheckedSideEffectImports: true,
|
|
318
|
+
},
|
|
319
|
+
include: ['vite.config.ts'],
|
|
320
|
+
},
|
|
321
|
+
null,
|
|
322
|
+
2,
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
await createPrettierConfig(rootDir);
|
|
327
|
+
await createEslintConfig(rootDir);
|
|
328
|
+
await createGitignore(rootDir, 'react');
|
|
329
|
+
await createVscodeSettings(rootDir);
|
|
330
|
+
await write(
|
|
331
|
+
path.join(rootDir, 'token.config.js'),
|
|
332
|
+
`import StyleDictionary from "style-dictionary";
|
|
333
|
+
|
|
334
|
+
// kebab-case 변환
|
|
335
|
+
StyleDictionary.registerTransform({
|
|
336
|
+
name: "name/kebab",
|
|
337
|
+
type: "name",
|
|
338
|
+
transform: (token) =>
|
|
339
|
+
token.path
|
|
340
|
+
.join("-")
|
|
341
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
342
|
+
.toLowerCase(),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Tailwind v4 @theme 변수 생성
|
|
346
|
+
StyleDictionary.registerFormat({
|
|
347
|
+
name: "css/tailwind-theme",
|
|
348
|
+
format: ({ dictionary }) => {
|
|
349
|
+
let css = "";
|
|
350
|
+
const withPx = (value) =>
|
|
351
|
+
typeof value === "string" && /^\\d+(\\.\\d+)?$/.test(value)
|
|
352
|
+
? \`\${value}px\`
|
|
353
|
+
: value;
|
|
354
|
+
|
|
355
|
+
css += "@theme {\\n";
|
|
356
|
+
dictionary.allTokens.forEach((token) => {
|
|
357
|
+
if (token.$type === "color") {
|
|
358
|
+
css += \` --color-\${token.name}: \${token.$value};\\n\`;
|
|
359
|
+
}
|
|
360
|
+
if (token.$type === "typography" && token.$value) {
|
|
361
|
+
const typo = token.$value;
|
|
362
|
+
if (typo.fontFamily) {
|
|
363
|
+
css += \` --font-family-\${token.name}: \${typo.fontFamily};\\n\`;
|
|
364
|
+
}
|
|
365
|
+
if (typo.fontWeight) {
|
|
366
|
+
css += \` --font-weight-\${token.name}: \${typo.fontWeight};\\n\`;
|
|
367
|
+
}
|
|
368
|
+
if (typo.fontSize) {
|
|
369
|
+
css += \` --font-size-\${token.name}: \${withPx(typo.fontSize)};\\n\`;
|
|
370
|
+
}
|
|
371
|
+
if (typo.lineHeight) {
|
|
372
|
+
css += \` --line-height-\${token.name}: \${withPx(typo.lineHeight)};\\n\`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
css += "}\\n\\n";
|
|
377
|
+
|
|
378
|
+
return css;
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
export default {
|
|
383
|
+
source: ["src/tokens.json"],
|
|
384
|
+
platforms: {
|
|
385
|
+
css: {
|
|
386
|
+
transforms: ["name/kebab"], // 일단 attribute/cti 제거
|
|
387
|
+
buildPath: "src/",
|
|
388
|
+
files: [
|
|
389
|
+
{
|
|
390
|
+
destination: "tokens.css",
|
|
391
|
+
format: "css/tailwind-theme",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
`,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// src/App.css
|
|
401
|
+
await write(
|
|
402
|
+
path.join(rootDir, 'src/App.css'),
|
|
403
|
+
`@import "./tokens.css";
|
|
404
|
+
@import 'tailwindcss';
|
|
405
|
+
`,
|
|
406
|
+
);
|
|
407
|
+
await write(path.join(rootDir, 'src/tokens.css'), '');
|
|
408
|
+
await write(path.join(rootDir, 'src/tokens.json'), '{}\n');
|
|
409
|
+
|
|
410
|
+
// src/main.tsx
|
|
411
|
+
await write(
|
|
412
|
+
path.join(rootDir, 'src/main.tsx'),
|
|
413
|
+
`import { createRoot } from 'react-dom/client';
|
|
414
|
+
|
|
415
|
+
import App from './App.tsx';
|
|
416
|
+
|
|
417
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
418
|
+
`,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
await write(
|
|
422
|
+
path.join(rootDir, 'src/vite-env.d.ts'),
|
|
423
|
+
`/// <reference types="vite/client" />\n`,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
await write(
|
|
427
|
+
path.join(rootDir, 'src/global.d.ts'),
|
|
428
|
+
`declare module '*.svg' {
|
|
429
|
+
import React from 'react';
|
|
430
|
+
export const ReactComponent: React.FunctionComponent<
|
|
431
|
+
React.SVGProps<SVGSVGElement>
|
|
432
|
+
>;
|
|
433
|
+
const src: string;
|
|
434
|
+
|
|
435
|
+
export default src;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
declare module '*.svg?react' {
|
|
439
|
+
import React from 'react';
|
|
440
|
+
const Component: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
|
441
|
+
|
|
442
|
+
export default Component;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
declare module '*.webp' {
|
|
446
|
+
const value: any;
|
|
447
|
+
export = value;
|
|
448
|
+
}
|
|
449
|
+
`,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// src/App.tsx
|
|
453
|
+
await write(
|
|
454
|
+
path.join(rootDir, 'src/App.tsx'),
|
|
455
|
+
`import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
456
|
+
|
|
457
|
+
import './App.css';
|
|
458
|
+
|
|
459
|
+
const queryClient = new QueryClient({
|
|
460
|
+
defaultOptions: {
|
|
461
|
+
queries: {
|
|
462
|
+
refetchOnWindowFocus: false,
|
|
463
|
+
refetchOnMount: false,
|
|
464
|
+
refetchOnReconnect: false,
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
function App() {
|
|
470
|
+
return (
|
|
471
|
+
<main>
|
|
472
|
+
<QueryClientProvider client={queryClient}>
|
|
473
|
+
<div>Hi! Byuckchon Frontend Developer</div>
|
|
474
|
+
</QueryClientProvider>
|
|
475
|
+
</main>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export default App;
|
|
480
|
+
`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── Next.js (App Router) ─────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
async function createNextBaseFiles(rootDir, config) {
|
|
487
|
+
// next.config.ts
|
|
488
|
+
await write(
|
|
489
|
+
path.join(rootDir, 'next.config.ts'),
|
|
490
|
+
`import type { NextConfig } from "next";
|
|
491
|
+
|
|
492
|
+
const nextConfig: NextConfig = {
|
|
493
|
+
experimental: {
|
|
494
|
+
turbo: {
|
|
495
|
+
rules: {
|
|
496
|
+
"*.svg": {
|
|
497
|
+
loaders: ["@svgr/webpack"],
|
|
498
|
+
as: "*.tsx",
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
webpack(config) {
|
|
504
|
+
config.module.rules.push({
|
|
505
|
+
test: /\\.svg$/,
|
|
506
|
+
use: ["@svgr/webpack"],
|
|
507
|
+
});
|
|
508
|
+
return config;
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
export default nextConfig;
|
|
513
|
+
`,
|
|
514
|
+
);
|
|
515
|
+
await write(path.join(rootDir, 'public/robots.txt'), `User-agent: *\nDisallow: /\n`);
|
|
516
|
+
|
|
517
|
+
// tsconfig.json (Next.js)
|
|
518
|
+
await write(
|
|
519
|
+
path.join(rootDir, 'tsconfig.json'),
|
|
520
|
+
JSON.stringify(
|
|
521
|
+
{
|
|
522
|
+
compilerOptions: {
|
|
523
|
+
target: 'ES2017',
|
|
524
|
+
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
525
|
+
allowJs: true,
|
|
526
|
+
skipLibCheck: true,
|
|
527
|
+
strict: true,
|
|
528
|
+
noEmit: true,
|
|
529
|
+
esModuleInterop: true,
|
|
530
|
+
module: 'esnext',
|
|
531
|
+
moduleResolution: 'bundler',
|
|
532
|
+
resolveJsonModule: true,
|
|
533
|
+
isolatedModules: true,
|
|
534
|
+
jsx: 'preserve',
|
|
535
|
+
incremental: true,
|
|
536
|
+
plugins: [{ name: 'next' }],
|
|
537
|
+
paths: { '@/*': ['./src/*'] },
|
|
538
|
+
},
|
|
539
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
540
|
+
exclude: ['node_modules'],
|
|
541
|
+
},
|
|
542
|
+
null,
|
|
543
|
+
2,
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await createPrettierConfig(rootDir);
|
|
548
|
+
await createNextEslintConfig(rootDir);
|
|
549
|
+
await createGitignore(rootDir, 'next');
|
|
550
|
+
await createVscodeSettings(rootDir);
|
|
551
|
+
await write(
|
|
552
|
+
path.join(rootDir, 'postcss.config.mjs'),
|
|
553
|
+
`const config = {
|
|
554
|
+
plugins: ["@tailwindcss/postcss"],
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export default config;
|
|
558
|
+
`,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// src/app/globals.css
|
|
562
|
+
await write(
|
|
563
|
+
path.join(rootDir, 'src/app/globals.css'),
|
|
564
|
+
`@import "../src/tokens.css";
|
|
565
|
+
@import 'tailwindcss';
|
|
566
|
+
`,
|
|
567
|
+
);
|
|
568
|
+
await write(path.join(rootDir, 'src/tokens.css'), '');
|
|
569
|
+
|
|
570
|
+
// src/app/layout.tsx
|
|
571
|
+
await write(
|
|
572
|
+
path.join(rootDir, 'src/app/layout.tsx'),
|
|
573
|
+
`import type { Metadata } from 'next';
|
|
574
|
+
|
|
575
|
+
import './globals.css';
|
|
576
|
+
|
|
577
|
+
export const metadata: Metadata = {
|
|
578
|
+
title: '${config.projectName}',
|
|
579
|
+
description: 'Generated by cli-test',
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
export default function RootLayout({
|
|
583
|
+
children,
|
|
584
|
+
}: {
|
|
585
|
+
children: React.ReactNode;
|
|
586
|
+
}) {
|
|
587
|
+
return (
|
|
588
|
+
<html lang="ko">
|
|
589
|
+
<head>
|
|
590
|
+
<meta charSet="utf-8" />
|
|
591
|
+
</head>
|
|
592
|
+
<body>{children}</body>
|
|
593
|
+
</html>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
`,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// src/app/page.tsx
|
|
600
|
+
await write(
|
|
601
|
+
path.join(rootDir, 'src/app/page.tsx'),
|
|
602
|
+
`export default function MainPage() {
|
|
603
|
+
return (
|
|
604
|
+
<main className="flex min-h-screen items-center justify-center bg-gray-50">
|
|
605
|
+
<div className="text-center">
|
|
606
|
+
<h1 className="text-4xl font-bold text-gray-900">${config.projectName}</h1>
|
|
607
|
+
<p className="mt-3 text-lg font-medium text-gray-700">
|
|
608
|
+
Hi! Byuckchon Frontend Developer
|
|
609
|
+
</p>
|
|
610
|
+
<p className="mt-4 text-gray-500">Next.js + TypeScript + Tailwind</p>
|
|
611
|
+
</div>
|
|
612
|
+
</main>
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
`,
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
await write(
|
|
619
|
+
path.join(rootDir, 'src/app/error.tsx'),
|
|
620
|
+
`'use client';
|
|
621
|
+
|
|
622
|
+
export default function Error() {
|
|
623
|
+
return <div>Something went wrong.</div>;
|
|
624
|
+
}
|
|
625
|
+
`,
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
await write(
|
|
629
|
+
path.join(rootDir, 'src/app/not-found.tsx'),
|
|
630
|
+
`export default function NotFound() {
|
|
631
|
+
return <div>Page not found.</div>;
|
|
632
|
+
}
|
|
633
|
+
`,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
await write(
|
|
637
|
+
path.join(rootDir, 'src/global.d.ts'),
|
|
638
|
+
`declare module '*.svg' {
|
|
639
|
+
import React from 'react';
|
|
640
|
+
export const ReactComponent: React.FunctionComponent<
|
|
641
|
+
React.SVGProps<SVGSVGElement>
|
|
642
|
+
>;
|
|
643
|
+
const src: string;
|
|
644
|
+
|
|
645
|
+
export default src;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
declare module '*.svg?react' {
|
|
649
|
+
import React from 'react';
|
|
650
|
+
const Component: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
|
651
|
+
|
|
652
|
+
export default Component;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
declare module '*.webp' {
|
|
656
|
+
const value: any;
|
|
657
|
+
export = value;
|
|
658
|
+
}
|
|
659
|
+
`,
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
await write(
|
|
663
|
+
path.join(rootDir, 'src/types.d.ts'),
|
|
664
|
+
`declare module "*.svg" {
|
|
665
|
+
import React from "react";
|
|
666
|
+
const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
|
|
667
|
+
export default ReactComponent;
|
|
668
|
+
}
|
|
669
|
+
`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ─── 진입점 ───────────────────────────────────────────────────────────────────
|
|
674
|
+
|
|
675
|
+
export async function createBaseFiles(rootDir, config) {
|
|
676
|
+
if (config.framework === 'react') {
|
|
677
|
+
await createReactBaseFiles(rootDir, config);
|
|
678
|
+
} else if (config.framework === 'next') {
|
|
679
|
+
await createNextBaseFiles(rootDir, config);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React 폴더 구조
|
|
6
|
+
* lib → store → api → hooks → context → components → layout → page
|
|
7
|
+
* assets는 모든 레이어에서 참조 가능
|
|
8
|
+
*/
|
|
9
|
+
const REACT_FOLDERS = [
|
|
10
|
+
'public',
|
|
11
|
+
'src/assets',
|
|
12
|
+
'src/assets/icons',
|
|
13
|
+
'src/assets/images',
|
|
14
|
+
'src/lib',
|
|
15
|
+
'src/store',
|
|
16
|
+
'src/api',
|
|
17
|
+
'src/hooks',
|
|
18
|
+
'src/context',
|
|
19
|
+
'src/components',
|
|
20
|
+
'src/layout',
|
|
21
|
+
'src/page',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Next.js App Router 폴더 구조
|
|
26
|
+
*/
|
|
27
|
+
const NEXT_FOLDERS = [
|
|
28
|
+
'public',
|
|
29
|
+
'src/app',
|
|
30
|
+
'src/assets',
|
|
31
|
+
'src/assets/common',
|
|
32
|
+
'src/assets/pages',
|
|
33
|
+
'src/components',
|
|
34
|
+
'src/constant',
|
|
35
|
+
'src/hooks',
|
|
36
|
+
'src/lib',
|
|
37
|
+
'src/providers',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
async function writeFile(filePath, content) {
|
|
41
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createFolders(rootDir, config) {
|
|
45
|
+
const folders =
|
|
46
|
+
config.framework === 'react' ? REACT_FOLDERS : NEXT_FOLDERS;
|
|
47
|
+
|
|
48
|
+
for (const folder of folders) {
|
|
49
|
+
await fs.mkdir(path.join(rootDir, folder), { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 각 레이어에 placeholder 파일 생성
|
|
53
|
+
await writeFile(path.join(rootDir, 'src/lib/index.ts'), '// 유틸리티 함수, 상수, 헬퍼\n');
|
|
54
|
+
await writeFile(path.join(rootDir, 'src/hooks/index.ts'), '// 커스텀 훅\n');
|
|
55
|
+
await writeFile(path.join(rootDir, 'src/components/index.ts'), '// 재사용 가능한 UI 컴포넌트\n');
|
|
56
|
+
|
|
57
|
+
if (config.framework === 'react') {
|
|
58
|
+
await writeFile(path.join(rootDir, 'src/store/index.ts'), '// Zustand 스토어\n');
|
|
59
|
+
await writeFile(path.join(rootDir, 'src/api/index.ts'), '// Axios API 호출\n');
|
|
60
|
+
await writeFile(path.join(rootDir, 'src/context/index.tsx'), '// React Context\n');
|
|
61
|
+
await writeFile(path.join(rootDir, 'src/layout/index.tsx'), '// 레이아웃 컴포넌트\n');
|
|
62
|
+
await writeFile(path.join(rootDir, 'src/page/index.tsx'), '// 페이지 컴포넌트\n');
|
|
63
|
+
} else {
|
|
64
|
+
await writeFile(path.join(rootDir, 'src/constant/index.ts'), '// 상수 정의\n');
|
|
65
|
+
await writeFile(path.join(rootDir, 'src/providers/index.tsx'), '// 전역 Provider\n');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { versions } from '../constants/versions.js';
|
|
5
|
+
|
|
6
|
+
export async function createPackageJson(rootDir, config) {
|
|
7
|
+
const isReact = config.framework === 'react';
|
|
8
|
+
|
|
9
|
+
const pkg = {
|
|
10
|
+
name: config.projectName,
|
|
11
|
+
version: '0.0.1',
|
|
12
|
+
private: true,
|
|
13
|
+
...(isReact ? { type: 'module' } : {}),
|
|
14
|
+
scripts: isReact
|
|
15
|
+
? {
|
|
16
|
+
dev: 'vite',
|
|
17
|
+
build: 'tsc -b && vite build',
|
|
18
|
+
'tokens:build': 'style-dictionary build --config token.config.js',
|
|
19
|
+
lint: 'eslint . --ext ts,tsx',
|
|
20
|
+
preview: 'vite preview',
|
|
21
|
+
format: 'prettier --write "src/**/*.{ts,tsx,css}"',
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
dev: 'next dev',
|
|
25
|
+
build: 'next build',
|
|
26
|
+
'tokens:build': 'style-dictionary build --config token.config.js',
|
|
27
|
+
start: 'next start',
|
|
28
|
+
lint: 'next lint',
|
|
29
|
+
format: 'prettier --write "src/**/*.{ts,tsx,css}"',
|
|
30
|
+
},
|
|
31
|
+
dependencies: {
|
|
32
|
+
react: isReact ? versions.react : versions['next-react'],
|
|
33
|
+
'react-dom': isReact ? versions['react-dom'] : versions['next-react-dom'],
|
|
34
|
+
...(isReact ? {} : { next: versions.next }),
|
|
35
|
+
zustand: versions.zustand,
|
|
36
|
+
...(isReact
|
|
37
|
+
? {
|
|
38
|
+
axios: versions.axios,
|
|
39
|
+
'@tanstack/react-query': versions['@tanstack/react-query'],
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
zod: versions.zod,
|
|
43
|
+
},
|
|
44
|
+
devDependencies: {
|
|
45
|
+
'@types/react': versions['@types/react'],
|
|
46
|
+
'@types/react-dom': versions['@types/react-dom'],
|
|
47
|
+
'@types/node': versions['@types/node'],
|
|
48
|
+
'@trivago/prettier-plugin-sort-imports':
|
|
49
|
+
versions['@trivago/prettier-plugin-sort-imports'],
|
|
50
|
+
eslint: versions.eslint,
|
|
51
|
+
'eslint-config-expo': versions['eslint-config-expo'],
|
|
52
|
+
'eslint-import-resolver-typescript':
|
|
53
|
+
versions['eslint-import-resolver-typescript'],
|
|
54
|
+
'eslint-plugin-import': versions['eslint-plugin-import'],
|
|
55
|
+
'eslint-plugin-react': versions['eslint-plugin-react'],
|
|
56
|
+
'eslint-plugin-react-hooks': versions['eslint-plugin-react-hooks'],
|
|
57
|
+
'eslint-plugin-unused-imports': versions['eslint-plugin-unused-imports'],
|
|
58
|
+
prettier: versions.prettier,
|
|
59
|
+
'prettier-plugin-tailwindcss': versions['prettier-plugin-tailwindcss'],
|
|
60
|
+
'style-dictionary': versions['style-dictionary'],
|
|
61
|
+
tailwindcss: versions.tailwindcss,
|
|
62
|
+
typescript: versions.typescript,
|
|
63
|
+
...(isReact
|
|
64
|
+
? {
|
|
65
|
+
'@tailwindcss/vite': versions['@tailwindcss/vite'],
|
|
66
|
+
'@typescript-eslint/eslint-plugin':
|
|
67
|
+
versions['@typescript-eslint/eslint-plugin'],
|
|
68
|
+
'@typescript-eslint/parser': versions['@typescript-eslint/parser'],
|
|
69
|
+
'@vitejs/plugin-react': versions['@vitejs/plugin-react'],
|
|
70
|
+
vite: versions.vite,
|
|
71
|
+
'vite-plugin-svgr': versions['vite-plugin-svgr'],
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
'@tailwindcss/postcss': versions['@tailwindcss/postcss'],
|
|
75
|
+
'@svgr/webpack': versions['@svgr/webpack'],
|
|
76
|
+
'eslint-config-next': versions['eslint-config-next'],
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await fs.writeFile(
|
|
82
|
+
path.join(rootDir, 'package.json'),
|
|
83
|
+
JSON.stringify(pkg, null, 2),
|
|
84
|
+
'utf-8',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec as execCallback } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
|
|
6
|
+
import { createBaseFiles } from './createBaseFiles.js';
|
|
7
|
+
import { createFolders } from './createFolders.js';
|
|
8
|
+
import { createPackageJson } from './createPackageJson.js';
|
|
9
|
+
import { createReadme } from './createReadme.js';
|
|
10
|
+
|
|
11
|
+
const exec = promisify(execCallback);
|
|
12
|
+
const BYUCKCHON_PACKAGES = [
|
|
13
|
+
'@byuckchon-frontend/hooks',
|
|
14
|
+
'@byuckchon-frontend/utils',
|
|
15
|
+
'@byuckchon-frontend/basic-ui',
|
|
16
|
+
'@byuckchon-frontend/core',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export async function createProject(config) {
|
|
20
|
+
const rootDir = path.resolve(config.projectName);
|
|
21
|
+
|
|
22
|
+
await fs.mkdir(rootDir);
|
|
23
|
+
await createFolders(rootDir, config);
|
|
24
|
+
await createPackageJson(rootDir, config);
|
|
25
|
+
await createBaseFiles(rootDir, config);
|
|
26
|
+
await createReadme(rootDir, config);
|
|
27
|
+
|
|
28
|
+
// 최신 버전(latest 포함) 의존성을 실제로 설치해 lockfile까지 생성
|
|
29
|
+
await exec('npm install', { cwd: rootDir });
|
|
30
|
+
await exec(`npm install ${BYUCKCHON_PACKAGES.join(' ')}`, { cwd: rootDir });
|
|
31
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function createReadme(rootDir, config) {
|
|
5
|
+
const isReact = config.framework === 'react';
|
|
6
|
+
|
|
7
|
+
const content = `# ${config.projectName}
|
|
8
|
+
|
|
9
|
+
## 기술 스택
|
|
10
|
+
|
|
11
|
+
| 항목 | 내용 |
|
|
12
|
+
|------|------|
|
|
13
|
+
| Framework | ${isReact ? 'React 19' : 'Next.js 15 (App Router)'} |
|
|
14
|
+
| Language | TypeScript |
|
|
15
|
+
| Build Tool | ${isReact ? 'Vite' : 'Next.js built-in'} |
|
|
16
|
+
| Styling | Tailwind CSS |
|
|
17
|
+
| State | Zustand |
|
|
18
|
+
| HTTP | Axios |
|
|
19
|
+
| Lint | ESLint + Prettier |
|
|
20
|
+
|
|
21
|
+
## 시작하기
|
|
22
|
+
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
# 의존성 설치
|
|
25
|
+
npm install
|
|
26
|
+
|
|
27
|
+
# 개발 서버 실행
|
|
28
|
+
npm run dev
|
|
29
|
+
|
|
30
|
+
# 프로덕션 빌드
|
|
31
|
+
npm run build
|
|
32
|
+
|
|
33
|
+
# 코드 포맷팅
|
|
34
|
+
npm run format
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## 프로젝트 구조
|
|
38
|
+
|
|
39
|
+
\`\`\`
|
|
40
|
+
src/
|
|
41
|
+
├── assets/ 정적 파일 (이미지, 폰트 등)
|
|
42
|
+
├── lib/ 유틸리티 함수, 상수
|
|
43
|
+
├── store/ Zustand 상태 관리
|
|
44
|
+
├── api/ Axios API 호출
|
|
45
|
+
├── hooks/ 커스텀 훅
|
|
46
|
+
├── context/ React Context
|
|
47
|
+
├── components/ 재사용 가능한 UI 컴포넌트${
|
|
48
|
+
isReact
|
|
49
|
+
? `
|
|
50
|
+
├── layout/ 레이아웃 컴포넌트
|
|
51
|
+
└── page/ 페이지 컴포넌트`
|
|
52
|
+
: `
|
|
53
|
+
└── app/ Next.js App Router (layout, page 등)`
|
|
54
|
+
}
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
${
|
|
58
|
+
isReact
|
|
59
|
+
? `## 레이어 의존성 규칙
|
|
60
|
+
|
|
61
|
+
각 레이어는 아래 방향으로만 import 해야 합니다.
|
|
62
|
+
|
|
63
|
+
\`\`\`
|
|
64
|
+
lib → store → api → hooks → context → components → layout → page
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
\`assets\`는 모든 레이어에서 자유롭게 참조 가능합니다.`
|
|
68
|
+
: ''
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
await fs.writeFile(path.join(rootDir, 'README.md'), content, 'utf-8');
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
|
|
3
|
+
export async function askInitQuestions() {
|
|
4
|
+
return inquirer.prompt([
|
|
5
|
+
{
|
|
6
|
+
type: 'input',
|
|
7
|
+
name: 'projectName',
|
|
8
|
+
message: '프로젝트 이름을 입력해주세요:',
|
|
9
|
+
validate: (input) => {
|
|
10
|
+
if (!input.trim()) return '프로젝트 이름을 입력해주세요.';
|
|
11
|
+
if (!/^[a-z0-9\-_]+$/i.test(input))
|
|
12
|
+
return '영문, 숫자, -, _ 만 사용 가능합니다.';
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'list',
|
|
18
|
+
name: 'framework',
|
|
19
|
+
message: '어떤 프레임워크를 사용할까요?',
|
|
20
|
+
choices: [
|
|
21
|
+
{ name: 'React (Vite + TypeScript)', value: 'react' },
|
|
22
|
+
{ name: 'Next.js (App Router + TypeScript)', value: 'next' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
}
|