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 ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { initCommand } from '../src/commands/init.js';
4
+
5
+ initCommand();
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
+ }