@blastlabs/utils 1.12.1 → 1.14.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.
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // 엔티티 이름을 kebab-case로 변환
7
+ function toKebabCase(str) {
8
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
9
+ }
10
+
11
+ // 엔티티 이름을 PascalCase로 변환
12
+ function toPascalCase(str) {
13
+ return str
14
+ .split("-")
15
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
16
+ .join("");
17
+ }
18
+
19
+ // 엔티티 이름을 camelCase로 변환
20
+ function toCamelCase(str) {
21
+ const pascalCase = toPascalCase(str);
22
+ return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
23
+ }
24
+
25
+ function createEntityStructure(entityName, targetDir) {
26
+ const kebabName = toKebabCase(entityName);
27
+ const pascalName = toPascalCase(kebabName);
28
+ const camelName = toCamelCase(kebabName);
29
+
30
+ // 현재 작업 디렉토리 기준으로 생성
31
+ const basePath = path.join(targetDir, kebabName);
32
+
33
+ // 디렉토리 생성
34
+ const dirs = [
35
+ basePath,
36
+ path.join(basePath, "api"),
37
+ path.join(basePath, "api", "mapper"),
38
+ path.join(basePath, "api", "query"),
39
+ path.join(basePath, "model"),
40
+ ];
41
+
42
+ dirs.forEach((dir) => {
43
+ if (!fs.existsSync(dir)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+ });
47
+
48
+ // 파일 템플릿 생성
49
+ createFiles(basePath, kebabName, pascalName, camelName);
50
+
51
+ console.log(`✅ ${kebabName} 엔티티가 성공적으로 생성되었습니다!`);
52
+ console.log(`📍 위치: ${basePath}`);
53
+ console.log(`\n생성된 파일들:`);
54
+ console.log(`📁 ${kebabName}/`);
55
+ console.log(`├── index.ts`);
56
+ console.log(`├── api/`);
57
+ console.log(`│ ├── get-${kebabName}-list.ts`);
58
+ console.log(`│ ├── get-${kebabName}-detail.ts`);
59
+ console.log(`│ ├── ${kebabName}-queries.ts`);
60
+ console.log(`│ ├── index.ts`);
61
+ console.log(`│ ├── mapper/`);
62
+ console.log(`│ │ ├── map-${kebabName}.ts`);
63
+ console.log(`│ │ └── map-${kebabName}-detail.ts`);
64
+ console.log(`│ └── query/`);
65
+ console.log(`│ └── ${kebabName}-list-query.ts`);
66
+ console.log(`└── model/`);
67
+ console.log(` ├── ${kebabName}.ts`);
68
+ console.log(` └── ${kebabName}-detail.ts`);
69
+
70
+ console.log(`\n⚠️ 추가 작업이 필요합니다:`);
71
+ console.log(
72
+ `1. query-keys.ts 파일에 ${pascalName.toUpperCase()}_QUERY_KEYS 추가`,
73
+ );
74
+ console.log(`2. API 스키마에 맞게 model과 mapper 수정`);
75
+ console.log(`3. 실제 API 엔드포인트에 맞게 get-${kebabName}-list.ts 수정`);
76
+ }
77
+
78
+ function createFiles(basePath, kebabName, pascalName, camelName) {
79
+ // index.ts
80
+ const indexContent = `export * as ${camelName}Api from "./api";
81
+ `;
82
+
83
+ // api/index.ts
84
+ const apiIndexContent = `export { get${pascalName}List } from "./get-${kebabName}-list";
85
+ export { get${pascalName}Detail } from "./get-${kebabName}-detail";
86
+ export { ${camelName}Queries } from "./${kebabName}-queries";
87
+ `;
88
+
89
+ // api/get-{entity}-list.ts
90
+ const getListContent = `// TODO: API 클라이언트 import 추가
91
+ // import { apiClient } from "@/shared/api";
92
+ import { ${pascalName}ListQuery } from "./query/${kebabName}-list-query";
93
+
94
+ export const get${pascalName}List = async (filters: ${pascalName}ListQuery) => {
95
+ // TODO: 실제 API 호출로 변경 필요
96
+ // return apiClient.get("/api/${kebabName}", { params: filters });
97
+ throw new Error("API 호출 구현 필요");
98
+ };
99
+ `;
100
+
101
+ // api/get-{entity}-detail.ts
102
+ const getDetailContent = `// TODO: API 클라이언트 import 추가
103
+ // import { apiClient } from "@/shared/api";
104
+
105
+ export const get${pascalName}Detail = async (id: number) => {
106
+ // TODO: 실제 API 호출로 변경 필요
107
+ // return apiClient.get(\`/api/${kebabName}/\${id}\`);
108
+ throw new Error("API 호출 구현 필요");
109
+ };
110
+ `;
111
+
112
+ // api/{entity}-queries.ts
113
+ const queriesContent = `import { queryOptions } from "@tanstack/react-query";
114
+ import { ${pascalName}ListQuery } from "./query/${kebabName}-list-query";
115
+ import { get${pascalName}List } from "./get-${kebabName}-list";
116
+ import { get${pascalName}Detail } from "./get-${kebabName}-detail";
117
+ import { map${pascalName} } from "./mapper/map-${kebabName}";
118
+ import { map${pascalName}Detail } from "./mapper/map-${kebabName}-detail";
119
+
120
+ export const ${camelName}Queries = {
121
+ all: () => ["${kebabName}"],
122
+ lists: () => [...${camelName}Queries.all(), "list"],
123
+ list: (filters: ${pascalName}ListQuery) =>
124
+ queryOptions({
125
+ queryKey: [...${camelName}Queries.lists(), filters],
126
+ queryFn: () => get${pascalName}List(filters),
127
+ select: (data) => {
128
+ return {
129
+ ...data,
130
+ items: data.items.map(map${pascalName}),
131
+ };
132
+ },
133
+ }),
134
+ details: () => [...${camelName}Queries.all(), "detail"],
135
+ detail: (id: number) =>
136
+ queryOptions({
137
+ queryKey: [...${camelName}Queries.details(), id],
138
+ queryFn: () => get${pascalName}Detail(id),
139
+ select: (data) => map${pascalName}Detail(data),
140
+ }),
141
+ };
142
+ `;
143
+
144
+ // api/mapper/map-{entity}.ts
145
+ const mapperContent = `import { ${pascalName}Schema } from "../../model/${kebabName}";
146
+
147
+ // TODO: API 응답 타입을 실제 타입으로 변경
148
+ type ${pascalName}Response = {
149
+ id: number;
150
+ createDate: string;
151
+ updateDate: string;
152
+ };
153
+
154
+ export const map${pascalName} = (item: ${pascalName}Response) => {
155
+ const res = ${pascalName}Schema.safeParse({
156
+ id: item.id,
157
+ // TODO: 실제 API 응답 구조에 맞게 매핑 수정 필요
158
+ createDate: item.createDate,
159
+ updateDate: item.updateDate,
160
+ });
161
+ if (!res.success) {
162
+ console.error(res.error);
163
+ return undefined;
164
+ }
165
+ return res.data;
166
+ };
167
+ `;
168
+
169
+ // api/mapper/map-{entity}-detail.ts
170
+ const mapperDetailContent = `import { ${pascalName}DetailSchema } from "../../model/${kebabName}-detail";
171
+
172
+ // TODO: API 응답 타입을 실제 타입으로 변경
173
+ type ${pascalName}DetailResponse = {
174
+ id: number;
175
+ createDate: string;
176
+ updateDate: string;
177
+ };
178
+
179
+ export const map${pascalName}Detail = (item: ${pascalName}DetailResponse) => {
180
+ const res = ${pascalName}DetailSchema.safeParse({
181
+ id: item.id,
182
+ // TODO: 실제 API 응답 구조에 맞게 매핑 수정 필요
183
+ createDate: item.createDate,
184
+ updateDate: item.updateDate,
185
+ });
186
+ if (!res.success) {
187
+ console.error(res.error);
188
+ return undefined;
189
+ }
190
+ return res.data;
191
+ };
192
+ `;
193
+
194
+ // api/query/{entity}-list-query.ts
195
+ const queryContent = `export type ${pascalName}ListQuery = {
196
+ limit?: number;
197
+ offset?: number;
198
+ // TODO: 추가 필터링 옵션이 필요한 경우 여기에 추가
199
+ };
200
+ `;
201
+
202
+ // model/{entity}.ts
203
+ const modelContent = `import { z } from "zod";
204
+
205
+ export const ${pascalName}Schema = z.object({
206
+ id: z.number(),
207
+ createDate: z.string(),
208
+ updateDate: z.string(),
209
+ // TODO: 실제 데이터 구조에 맞게 스키마 수정 필요
210
+ });
211
+
212
+ export type ${pascalName}Type = z.infer<typeof ${pascalName}Schema>;
213
+ `;
214
+
215
+ //model/{entity}-detail.ts
216
+ const detailContent = `import { z } from "zod";
217
+
218
+ export const ${pascalName}DetailSchema = z.object({
219
+ id: z.number(),
220
+ createDate: z.string(),
221
+ updateDate: z.string(),
222
+ });
223
+
224
+ export type ${pascalName}DetailType = z.infer<typeof ${pascalName}DetailSchema>;
225
+ `;
226
+
227
+ // 파일 쓰기
228
+ fs.writeFileSync(path.join(basePath, "index.ts"), indexContent);
229
+ fs.writeFileSync(path.join(basePath, "api", "index.ts"), apiIndexContent);
230
+ fs.writeFileSync(
231
+ path.join(basePath, "api", `get-${kebabName}-list.ts`),
232
+ getListContent,
233
+ );
234
+ fs.writeFileSync(
235
+ path.join(basePath, "api", `get-${kebabName}-detail.ts`),
236
+ getDetailContent,
237
+ );
238
+ fs.writeFileSync(
239
+ path.join(basePath, "api", `${kebabName}-queries.ts`),
240
+ queriesContent,
241
+ );
242
+ fs.writeFileSync(
243
+ path.join(basePath, "api", "mapper", `map-${kebabName}.ts`),
244
+ mapperContent,
245
+ );
246
+ fs.writeFileSync(
247
+ path.join(basePath, "api", "mapper", `map-${kebabName}-detail.ts`),
248
+ mapperDetailContent,
249
+ );
250
+ fs.writeFileSync(
251
+ path.join(basePath, "api", "query", `${kebabName}-list-query.ts`),
252
+ queryContent,
253
+ );
254
+ fs.writeFileSync(
255
+ path.join(basePath, "model", `${kebabName}.ts`),
256
+ modelContent,
257
+ );
258
+ fs.writeFileSync(
259
+ path.join(basePath, "model", `${kebabName}-detail.ts`),
260
+ detailContent,
261
+ );
262
+ }
263
+
264
+ // CLI 실행
265
+ const args = process.argv.slice(2);
266
+
267
+ if (args.length === 0) {
268
+ console.log("@blastlabs/utils - FSD Entity Generator\n");
269
+ console.log("사용법: npx blastlabs-generate-entity <entity-name>\n");
270
+ console.log("⚠️ entities 폴더 내에서 실행해주세요!\n");
271
+ console.log("예시:");
272
+ console.log(" cd src/entities");
273
+ console.log(" npx blastlabs-generate-entity user");
274
+ console.log(" npx blastlabs-generate-entity my-new-entity");
275
+ console.log(" npx blastlabs-generate-entity MyNewEntity");
276
+ process.exit(1);
277
+ }
278
+
279
+ const entityName = args[0];
280
+ const targetDir = process.cwd();
281
+
282
+ createEntityStructure(entityName, targetDir);
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const TEMPLATES = {
7
+ base: "base.md",
8
+ fsd: "fsd.md",
9
+ testing: "testing.md",
10
+ nextjs: "nextjs.md",
11
+ vite: "vite.md",
12
+ monorepo: "monorepo.md",
13
+ project: "project-specific.md",
14
+ };
15
+
16
+ function parseArgs(args) {
17
+ const options = {
18
+ projectName: null,
19
+ projectDescription: "프로젝트 설명을 작성해주세요.",
20
+ framework: null, // 'nextjs' or 'vite'
21
+ fsd: false,
22
+ testing: false,
23
+ monorepo: false,
24
+ packageManager: "pnpm",
25
+ help: false,
26
+ };
27
+
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i];
30
+
31
+ if (arg === "--help" || arg === "-h") {
32
+ options.help = true;
33
+ } else if (arg === "--next" || arg === "--nextjs") {
34
+ options.framework = "nextjs";
35
+ } else if (arg === "--vite") {
36
+ options.framework = "vite";
37
+ } else if (arg === "--fsd") {
38
+ options.fsd = true;
39
+ } else if (arg === "--testing" || arg === "--test") {
40
+ options.testing = true;
41
+ } else if (arg === "--monorepo") {
42
+ options.monorepo = true;
43
+ } else if (arg === "--npm") {
44
+ options.packageManager = "npm";
45
+ } else if (arg === "--yarn") {
46
+ options.packageManager = "yarn";
47
+ } else if (arg === "--pnpm") {
48
+ options.packageManager = "pnpm";
49
+ } else if (!arg.startsWith("--") && !options.projectName) {
50
+ options.projectName = arg;
51
+ } else if (!arg.startsWith("--") && options.projectName && !options.projectDescription) {
52
+ options.projectDescription = arg;
53
+ }
54
+ }
55
+
56
+ return options;
57
+ }
58
+
59
+ function showHelp() {
60
+ console.log("@blastlabs/utils - AI Rules Generator\n");
61
+ console.log("프로젝트에 .cursorrules와 CLAUDE.md 파일을 생성합니다.\n");
62
+ console.log("사용법: npx blastlabs-init-ai-rules <project-name> [options]\n");
63
+ console.log("Options:");
64
+ console.log(" --next, --nextjs Next.js 프로젝트 규칙 포함");
65
+ console.log(" --vite Vite 프로젝트 규칙 포함");
66
+ console.log(" --fsd Feature-Sliced Design 규칙 포함");
67
+ console.log(" --testing, --test 테스트 전략 규칙 포함");
68
+ console.log(" --monorepo 모노레포 규칙 포함");
69
+ console.log(" --npm npm 사용 (기본값: pnpm)");
70
+ console.log(" --yarn yarn 사용");
71
+ console.log(" --pnpm pnpm 사용 (기본값)");
72
+ console.log("\n예시:");
73
+ console.log(' npx blastlabs-init-ai-rules "My Project" --vite --fsd');
74
+ console.log(' npx blastlabs-init-ai-rules "Admin" --next --fsd --testing --monorepo');
75
+ console.log(' npx blastlabs-init-ai-rules "Web App" "React 웹 애플리케이션" --vite');
76
+ console.log("\n⚠️ 프로젝트 루트 디렉토리에서 실행해주세요!");
77
+ }
78
+
79
+ function loadTemplate(name) {
80
+ const templatePath = path.join(__dirname, "..", "templates", name);
81
+ try {
82
+ return fs.readFileSync(templatePath, "utf-8");
83
+ } catch (error) {
84
+ console.error(`❌ 템플릿 파일을 찾을 수 없습니다: ${name}`);
85
+ return "";
86
+ }
87
+ }
88
+
89
+ function main() {
90
+ const args = process.argv.slice(2);
91
+ const options = parseArgs(args);
92
+
93
+ if (options.help || !options.projectName) {
94
+ showHelp();
95
+ process.exit(0);
96
+ }
97
+
98
+ const targetDir = process.cwd();
99
+
100
+ // 템플릿 조합
101
+ let content = loadTemplate(TEMPLATES.base);
102
+
103
+ if (options.fsd) {
104
+ content += "\n" + loadTemplate(TEMPLATES.fsd);
105
+ }
106
+
107
+ if (options.testing) {
108
+ content += "\n" + loadTemplate(TEMPLATES.testing);
109
+ }
110
+
111
+ if (options.framework === "nextjs") {
112
+ content += "\n" + loadTemplate(TEMPLATES.nextjs);
113
+ } else if (options.framework === "vite") {
114
+ content += "\n" + loadTemplate(TEMPLATES.vite);
115
+ }
116
+
117
+ if (options.monorepo) {
118
+ content += "\n" + loadTemplate(TEMPLATES.monorepo);
119
+ }
120
+
121
+ // 항상 project-specific 섹션 추가
122
+ content += "\n" + loadTemplate(TEMPLATES.project);
123
+
124
+ // 플레이스홀더 치환
125
+ const frameworkName = options.framework === "nextjs" ? "Next.js" :
126
+ options.framework === "vite" ? "Vite" : "React";
127
+
128
+ content = content
129
+ .replace(/\{\{PROJECT_NAME\}\}/g, options.projectName)
130
+ .replace(/\{\{PROJECT_DESCRIPTION\}\}/g, options.projectDescription)
131
+ .replace(/\{\{PACKAGE_MANAGER\}\}/g, options.packageManager)
132
+ .replace(/\{\{FRAMEWORK\}\}/g, frameworkName);
133
+
134
+ // .cursorrules 파일 생성
135
+ const cursorrulesPath = path.join(targetDir, ".cursorrules");
136
+ if (fs.existsSync(cursorrulesPath)) {
137
+ console.log("⚠️ .cursorrules 파일이 이미 존재합니다. 덮어쓰기를 건너뜁니다.");
138
+ } else {
139
+ fs.writeFileSync(cursorrulesPath, content);
140
+ console.log("✅ .cursorrules 생성 완료");
141
+ }
142
+
143
+ // CLAUDE.md 파일 생성 (루트)
144
+ const claudePath = path.join(targetDir, "CLAUDE.md");
145
+ if (fs.existsSync(claudePath)) {
146
+ console.log("⚠️ CLAUDE.md 파일이 이미 존재합니다. 덮어쓰기를 건너뜁니다.");
147
+ } else {
148
+ fs.writeFileSync(claudePath, content);
149
+ console.log("✅ CLAUDE.md 생성 완료");
150
+ }
151
+
152
+ console.log("\n📁 생성된 파일들:");
153
+ console.log("├── .cursorrules");
154
+ console.log("└── CLAUDE.md");
155
+
156
+ console.log("\n📦 포함된 규칙:");
157
+ console.log("├── base (공통 규칙)");
158
+ if (options.fsd) console.log("├── fsd (Feature-Sliced Design)");
159
+ if (options.testing) console.log("├── testing (테스트 전략)");
160
+ if (options.framework) console.log(`├── ${options.framework}`);
161
+ if (options.monorepo) console.log("├── monorepo");
162
+ console.log("└── project-specific");
163
+
164
+ console.log("\n💡 Tip: 생성된 파일을 프로젝트에 맞게 수정하세요!");
165
+ }
166
+
167
+ main();
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@blastlabs/utils",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "blastlabs-generate-entity": "./bin/generate-entity.cjs",
9
+ "blastlabs-init-ai-rules": "./bin/init-ai-rules.cjs"
10
+ },
7
11
  "files": [
8
- "dist"
12
+ "dist",
13
+ "bin",
14
+ "templates"
9
15
  ],
10
16
  "scripts": {
11
17
  "prepare": "npm run build:tsc",
@@ -0,0 +1,111 @@
1
+ # {{PROJECT_NAME}} Development Rules
2
+
3
+ This file contains development guidelines for AI assistants (Cursor, Claude Code, etc.)
4
+
5
+ ## Project Overview
6
+
7
+ {{PROJECT_DESCRIPTION}}
8
+
9
+ ---
10
+
11
+ # 프로젝트 기본 정보
12
+
13
+ - **패키지 매니저**: {{PACKAGE_MANAGER}}
14
+ - **프레임워크**: {{FRAMEWORK}}
15
+
16
+ ## 기술 스택
17
+
18
+ - TypeScript
19
+ - React
20
+ - TanStack Query
21
+ - Tailwind CSS
22
+ - Zod
23
+
24
+ ---
25
+
26
+ # Common Rules (공통 규칙)
27
+
28
+ ## Code Style & Standards
29
+
30
+ ### TypeScript
31
+ - Use strict type checking (`strict: true` in tsconfig)
32
+ - Prefer explicit types over `any`
33
+ - Export all public types and interfaces
34
+ - Use generics for reusable components
35
+ - Use `type` for unions/intersections, `interface` for object shapes
36
+
37
+ ### Naming Conventions
38
+ - **Files/Folders**: kebab-case (`user-profile.tsx`, `use-auth.ts`)
39
+ - **Components**: PascalCase (`UserProfile`, `AuthGuard`)
40
+ - **Hooks**: camelCase with `use` prefix (`useAuth`, `useLocalStorage`)
41
+ - **Constants**: UPPER_SNAKE_CASE (`API_BASE_URL`, `MAX_RETRY_COUNT`)
42
+ - **Types/Interfaces**: PascalCase (`UserProfile`, `AuthState`)
43
+
44
+ ### Import Order
45
+ 1. React/Framework imports
46
+ 2. Third-party libraries
47
+ 3. Internal modules (absolute paths)
48
+ 4. Relative imports
49
+ 5. Types (with `type` keyword)
50
+
51
+ ```typescript
52
+ import React, { useState, useEffect } from 'react';
53
+ import { useQuery } from '@tanstack/react-query';
54
+ import { Button } from '@/components/ui';
55
+ import { formatDate } from '../utils';
56
+ import type { User } from '@/types';
57
+ ```
58
+
59
+ ## React Best Practices
60
+
61
+ ### Component Patterns
62
+ - Use functional components with hooks
63
+ - Keep components small and focused (single responsibility)
64
+ - Extract reusable logic into custom hooks
65
+ - Use composition over inheritance
66
+
67
+ ### Hooks Rules
68
+ - Only call hooks at the top level
69
+ - Only call hooks from React functions
70
+ - Use `useCallback` for event handlers passed to children
71
+ - Use `useMemo` for expensive calculations
72
+
73
+ ### State Management
74
+ - Keep state as local as possible
75
+ - Lift state up only when necessary
76
+ - Use TanStack Query for server state
77
+ - Use context or Zustand for client global state
78
+
79
+ ## Error Handling
80
+
81
+ ```typescript
82
+ try {
83
+ const result = await someAsyncOperation();
84
+ return { data: result, error: null };
85
+ } catch (error) {
86
+ console.error('Operation failed:', error);
87
+ return { data: null, error: error instanceof Error ? error : new Error('Unknown error') };
88
+ }
89
+ ```
90
+
91
+ ## Git Commit Convention
92
+
93
+ Use conventional commits format:
94
+ - `feat(scope): description` - New features
95
+ - `fix(scope): description` - Bug fixes
96
+ - `refactor(scope): description` - Code refactoring
97
+ - `docs(scope): description` - Documentation changes
98
+ - `test(scope): description` - Test changes
99
+ - `chore(scope): description` - Build/tooling changes
100
+
101
+ Examples:
102
+ - `feat(auth): add login functionality`
103
+ - `fix(api): resolve timeout error`
104
+ - `docs(readme): update installation guide`
105
+
106
+ ## Before Committing
107
+
108
+ 1. Run tests: `{{PACKAGE_MANAGER}} test`
109
+ 2. Build successfully: `{{PACKAGE_MANAGER}} build`
110
+ 3. Lint check: `{{PACKAGE_MANAGER}} lint`
111
+ 4. Format code: `{{PACKAGE_MANAGER}} format`
@@ -0,0 +1,150 @@
1
+
2
+ ---
3
+
4
+ # Feature-Sliced Design (FSD)
5
+
6
+ 프로젝트는 [FSD 공식문서](https://feature-sliced.github.io/documentation/kr/docs/get-started/overview)를 참고하여 구조화되어 있습니다.
7
+
8
+ ## 폴더 구조
9
+
10
+ ```
11
+ src/
12
+ ├── app/ # 앱 설정, providers, 라우팅
13
+ ├── entities/ # 도메인 모델, API
14
+ ├── features/ # 사용자 기능 단위
15
+ ├── shared/ # 공유 컴포넌트, 유틸
16
+ ├── views/ # 페이지 컴포넌트
17
+ └── widgets/ # 복합 UI 블록
18
+ ```
19
+
20
+ ## Entities Layer (엔티티)
21
+
22
+ 엔티티는 도메인 모델과 API 로직을 관리합니다.
23
+
24
+ ### 구조
25
+
26
+ ```
27
+ entities/
28
+ └── (엔티티 이름)/ 예: inquiry, popular-product
29
+ ├── api/
30
+ │ ├── mapper/
31
+ │ │ ├── map-(엔티티 이름).ts
32
+ │ │ └── map-(엔티티 이름)-detail.ts
33
+ │ ├── query/
34
+ │ │ ├── (엔티티 이름)-list-query.ts
35
+ │ │ └── (엔티티 이름)-detail-query.ts
36
+ │ ├── get-(엔티티 이름)-list.ts
37
+ │ ├── get-(엔티티 이름)-detail.ts
38
+ │ ├── (엔티티 이름)-queries.ts
39
+ │ ├── mutate.ts
40
+ │ └── index.ts
41
+ ├── model/
42
+ │ ├── (엔티티 이름).ts
43
+ │ ├── (엔티티 이름)-detail.ts
44
+ │ └── (엔티티 이름)-update.ts
45
+ ├── schema.ts
46
+ └── index.ts
47
+ ```
48
+
49
+ ### 각 파일의 역할
50
+
51
+ - **mapper/**: API 응답을 도메인 모델로 변환
52
+ - **query/**: 쿼리 파라미터 타입 정의
53
+ - **get-\*-list.ts**: 리스트 조회 API 함수
54
+ - **get-\*-detail.ts**: 상세 조회 API 함수
55
+ - **\*-queries.ts**: TanStack Query queryOptions 정의 (all, lists, list, details, detail 등)
56
+ - **mutate.ts**: mutation 함수들
57
+ - **model/**: 도메인 모델 타입 정의
58
+ - **schema.ts**: zod 스키마 정의 및 변환 함수
59
+ - **index.ts**: `export * as (엔티티 이름)Api from "./api"`
60
+
61
+ ### 예시
62
+
63
+ ```
64
+ entities/inquiry/
65
+ ├── api/
66
+ │ ├── mapper/
67
+ │ │ ├── map-inquiry.ts
68
+ │ │ └── map-inquiry-detail.ts
69
+ │ ├── query/
70
+ │ │ ├── inquiry-list-query.ts
71
+ │ │ └── inquiry-detail-query.ts
72
+ │ ├── get-inquiry-list.ts
73
+ │ ├── get-inquiry-detail.ts
74
+ │ └── inquiry-queries.ts
75
+ ├── model/
76
+ │ ├── inquiry.ts
77
+ │ └── inquiry-detail.ts
78
+ └── schema.ts
79
+ ```
80
+
81
+ ## Shared Layer (공유 레이어)
82
+
83
+ 프로젝트 전체에서 공유되는 코드를 관리합니다.
84
+
85
+ ### 구조
86
+
87
+ ```
88
+ shared/
89
+ ├── api/ API 인스턴스 설정
90
+ ├── constant/ 공통 상수 (path.ts 등)
91
+ ├── hooks/ 공통 React 훅
92
+ ├── lib/ 라이브러리 래퍼 및 설정
93
+ │ └── (라이브러리명)/
94
+ │ ├── provider.tsx
95
+ │ └── config.ts
96
+ ├── ui/
97
+ │ ├── component/ 재사용 가능한 UI 컴포넌트
98
+ │ │ └── (컴포넌트명)/
99
+ │ │ ├── index.tsx
100
+ │ │ ├── types.ts
101
+ │ │ └── (컴포넌트명).tsx
102
+ │ ├── theme/ 테마 관련 CSS 파일
103
+ │ └── utils/ UI 관련 유틸리티 함수
104
+ └── utils/ 일반 유틸리티 함수
105
+ ```
106
+
107
+ ### lib vs utils 구분 기준
108
+
109
+ #### `shared/lib/`
110
+ 외부 라이브러리를 프로젝트에 맞게 감싸거나 설정하는 코드
111
+ - 라이브러리 래퍼(wrapper) 컴포넌트
112
+ - 라이브러리 초기 설정 및 Provider
113
+ - 예시: `react-hook-form/form-provider.tsx`, `tanstack-query/query-client.tsx`
114
+
115
+ #### `shared/utils/`
116
+ 순수 유틸리티 함수 및 비즈니스 로직 헬퍼
117
+ - 라이브러리와 무관한 순수 함수
118
+ - 예시: `convert-price.ts`, `export-excel.ts`, `format-date.ts`
119
+
120
+ **판단 기준:**
121
+ - 라이브러리를 감싸는가? → `lib/`
122
+ - 순수 함수 또는 유틸인가? → `utils/`
123
+
124
+ ## Views Layer (페이지 레이어)
125
+
126
+ 페이지 단위의 UI와 로직을 관리합니다.
127
+
128
+ ### 구조
129
+
130
+ ```
131
+ views/
132
+ └── (페이지명)/
133
+ ├── page.tsx 메인 페이지 컴포넌트 (필수)
134
+ ├── (페이지명)-list-page.tsx
135
+ ├── (페이지명)-detail-page.tsx
136
+ ├── detail/
137
+ │ ├── page.tsx
138
+ │ ├── ui/
139
+ │ ├── hooks/ use-(페이지명)-detail-data.tsx 등
140
+ │ └── components/
141
+ ├── list/
142
+ │ ├── (컬럼명)-column.tsx
143
+ │ ├── search.tsx
144
+ │ └── (모달명)-modal.tsx
145
+ ├── hooks/
146
+ ├── components/
147
+ ├── utils/
148
+ ├── schema/
149
+ └── constants.ts
150
+ ```
@@ -0,0 +1,61 @@
1
+
2
+ ---
3
+
4
+ # Monorepo Rules
5
+
6
+ ## Structure
7
+
8
+ ```
9
+ root/
10
+ ├── apps/
11
+ │ ├── web/ # 웹 앱
12
+ │ ├── admin/ # 어드민
13
+ │ └── mobile/ # 모바일 앱
14
+ ├── packages/
15
+ │ ├── ui/ # 공유 UI 컴포넌트
16
+ │ ├── utils/ # 공유 유틸리티
17
+ │ ├── config/ # 공유 설정 (eslint, tsconfig)
18
+ │ └── types/ # 공유 TypeScript 타입
19
+ ├── package.json
20
+ └── pnpm-workspace.yaml
21
+ ```
22
+
23
+ ## Package Management
24
+ - Use workspace protocol for internal packages (`workspace:*`)
25
+ - Keep shared dependencies in root `package.json`
26
+ - App-specific dependencies in app's `package.json`
27
+
28
+ ```json
29
+ {
30
+ "dependencies": {
31
+ "@repo/ui": "workspace:*",
32
+ "@repo/utils": "workspace:*"
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## Build Order
38
+ - Build packages before apps
39
+ - Use Turborepo for build orchestration
40
+ - Configure proper `dependsOn` in turbo.json
41
+
42
+ ## Shared Configurations
43
+ - Extend base tsconfig in all packages
44
+ - Share ESLint config from `packages/config`
45
+ - Maintain consistent formatting across packages
46
+
47
+ ## Commands
48
+
49
+ ```bash
50
+ # 전체 빌드
51
+ pnpm build
52
+
53
+ # 특정 앱만 빌드
54
+ pnpm --filter @repo/admin build
55
+
56
+ # 특정 앱 dev 서버
57
+ pnpm --filter @repo/web dev
58
+
59
+ # 전체 린트
60
+ pnpm lint
61
+ ```
@@ -0,0 +1,67 @@
1
+
2
+ ---
3
+
4
+ # Next.js Rules
5
+
6
+ ## App Router
7
+ - Use Server Components by default
8
+ - Add `'use client'` only when needed (hooks, browser APIs, interactivity)
9
+ - Colocate loading.tsx, error.tsx, not-found.tsx with pages
10
+ - Use route groups `(group)` for organization without affecting URL
11
+
12
+ ## File Structure
13
+ ```
14
+ app/
15
+ ├── (auth)/
16
+ │ ├── login/page.tsx
17
+ │ └── register/page.tsx
18
+ ├── (main)/
19
+ │ ├── dashboard/page.tsx
20
+ │ └── settings/page.tsx
21
+ ├── api/
22
+ │ └── [...route]/route.ts
23
+ ├── layout.tsx
24
+ └── page.tsx
25
+ ```
26
+
27
+ ## Data Fetching
28
+ - Prefer Server Components for data fetching
29
+ - Use `fetch` with caching options in Server Components
30
+ - Use React Query/SWR for client-side data fetching
31
+ - Implement proper loading and error states
32
+
33
+ ## Environment Variables
34
+ - `NEXT_PUBLIC_*` for client-side variables
35
+ - Server-only variables without prefix
36
+ - Use `.env.local` for local development
37
+
38
+ ## SSR Safety
39
+
40
+ ```typescript
41
+ // ❌ Wrong - breaks SSR
42
+ const token = localStorage.getItem('token');
43
+
44
+ // ✅ Correct - SSR safe
45
+ const token = typeof window !== 'undefined'
46
+ ? localStorage.getItem('token')
47
+ : null;
48
+ ```
49
+
50
+ ## SSR-Safe Hook Pattern
51
+
52
+ ```typescript
53
+ const [value, setValue] = useState(() => {
54
+ if (typeof window === 'undefined') return defaultValue;
55
+ return window.localStorage.getItem(key);
56
+ });
57
+
58
+ useEffect(() => {
59
+ if (typeof window === 'undefined') return;
60
+ // Browser-only code
61
+ }, []);
62
+ ```
63
+
64
+ ## Image Optimization
65
+ - Always use `next/image` for images
66
+ - Provide width and height or use `fill`
67
+ - Use appropriate `sizes` prop for responsive images
@@ -0,0 +1,21 @@
1
+
2
+ ---
3
+
4
+ # Project Specific Rules (프로젝트별 추가 규칙)
5
+
6
+ <!-- 여기에 프로젝트 고유의 규칙을 추가하세요 -->
7
+
8
+ ## Custom Rules
9
+ - Add your project-specific rules here
10
+ - Document any exceptions to common rules
11
+ - Include team agreements and decisions
12
+
13
+ ## API Conventions
14
+ - Document API patterns used in this project
15
+ - Specify error handling strategies
16
+ - Define response/request formats
17
+
18
+ ## State Management
19
+ - Document which state library is used
20
+ - Define when to use local vs global state
21
+ - Specify state structure conventions
@@ -0,0 +1,66 @@
1
+
2
+ ---
3
+
4
+ # 테스트 전략
5
+
6
+ ## 테스트 작성 원칙
7
+
8
+ ### 컴포넌트 (Component) - 단위 테스트
9
+
10
+ - **대상**: `shared/ui/component/`, 재사용 가능한 컴포넌트
11
+ - **목적**: 컴포넌트의 독립적인 동작 검증
12
+ - **방법**:
13
+ - Props와 사용자 상호작용에 집중
14
+ - 외부 의존성(API, 라우팅)은 mock 처리
15
+ - 렌더링, 이벤트 핸들링, 상태 변경 검증
16
+
17
+ ### 페이지 (Page) - 기능 테스트
18
+
19
+ - **대상**: `views/*/page.tsx` 페이지 컴포넌트
20
+ - **목적**: 사용자 시나리오와 비즈니스 로직 검증
21
+ - **Mock 최소화 원칙**:
22
+ - 외부 의존성(API 호출, toast)만 mock
23
+ - 내부 컴포넌트는 실제로 렌더링하여 통합 테스트
24
+ - `data-testid` 대신 접근성 기반 쿼리 사용
25
+
26
+ ## Testing Library 쿼리 우선순위
27
+
28
+ **1순위 (가장 권장):**
29
+ - `getByRole` - 접근성 역할 기반 (button, textbox, checkbox 등)
30
+ - `getByLabelText` - label과 연결된 폼 요소
31
+ - `getByPlaceholderText` - placeholder가 있는 입력 필드
32
+ - `getByText` - 텍스트 내용으로 찾기
33
+ - `getByDisplayValue` - 입력 필드의 현재 값
34
+
35
+ **2순위:**
36
+ - `getByAltText` - 이미지의 alt 텍스트
37
+ - `getByTitle` - title 속성
38
+
39
+ **3순위 (최후의 수단):**
40
+ - `getByTestId` - `data-testid` (가능한 한 피해야 함)
41
+
42
+ **예시:**
43
+ ```typescript
44
+ // ✅ 좋은 예
45
+ screen.getByRole('button', { name: '저장' });
46
+ screen.getByRole('checkbox');
47
+ screen.getByText('셀러 정보를 찾을 수 없습니다.');
48
+ screen.getByLabelText(/대표자명/);
49
+
50
+ // ❌ 나쁜 예
51
+ screen.getByTestId('save-button');
52
+ ```
53
+
54
+ ## 테스트 파일 위치
55
+
56
+ - 컴포넌트와 같은 디렉토리에 `*.test.tsx`
57
+
58
+ ## 테스트 구조
59
+
60
+ ```typescript
61
+ describe('ComponentName', () => {
62
+ it('should render correctly', () => {});
63
+ it('should handle user interaction', () => {});
64
+ it('should show error state', () => {});
65
+ });
66
+ ```
@@ -0,0 +1,53 @@
1
+
2
+ ---
3
+
4
+ # Vite + React Rules
5
+
6
+ ## Environment Variables
7
+ - Use `import.meta.env.VITE_*` for environment variables
8
+ - Only `VITE_` prefixed variables are exposed to client
9
+
10
+ ```typescript
11
+ // ✅ Correct
12
+ const apiUrl = import.meta.env.VITE_API_URL;
13
+
14
+ // ❌ Wrong - won't work
15
+ const secret = import.meta.env.SECRET_KEY;
16
+ ```
17
+
18
+ ## File Structure
19
+ ```
20
+ src/
21
+ ├── assets/ # Static assets
22
+ ├── components/ # React components
23
+ ├── hooks/ # Custom hooks
24
+ ├── pages/ # Page components
25
+ ├── routes/ # Routing configuration
26
+ ├── services/ # API services
27
+ ├── stores/ # State management
28
+ ├── utils/ # Utility functions
29
+ └── main.tsx # Entry point
30
+ ```
31
+
32
+ ## Path Aliases
33
+ - Use `@/` alias for `src/` directory
34
+ - Configure in `vite.config.ts` and `tsconfig.json`
35
+
36
+ ```typescript
37
+ // vite.config.ts
38
+ resolve: {
39
+ alias: {
40
+ '@': path.resolve(__dirname, './src'),
41
+ },
42
+ }
43
+ ```
44
+
45
+ ## Build Optimization
46
+ - Use dynamic imports for code splitting
47
+ - Lazy load routes and heavy components
48
+ - Analyze bundle size with `rollup-plugin-visualizer`
49
+
50
+ ```typescript
51
+ // Lazy loading example
52
+ const Dashboard = lazy(() => import('./pages/Dashboard'));
53
+ ```