@su-record/vibe 2.3.0 → 2.3.2
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/.claude/settings.json +35 -35
- package/.claude/settings.local.json +24 -25
- package/.claude/vibe/constitution.md +184 -184
- package/.claude/vibe/rules/core/communication-guide.md +104 -104
- package/.claude/vibe/rules/core/development-philosophy.md +52 -52
- package/.claude/vibe/rules/core/quick-start.md +120 -120
- package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
- package/.claude/vibe/rules/languages/go.md +396 -396
- package/.claude/vibe/rules/languages/java-spring.md +586 -586
- package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
- package/.claude/vibe/rules/languages/python-django.md +371 -371
- package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
- package/.claude/vibe/rules/languages/rust.md +425 -425
- package/.claude/vibe/rules/languages/swift-ios.md +516 -516
- package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
- package/.claude/vibe/rules/languages/typescript-node.md +375 -375
- package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
- package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
- package/.claude/vibe/rules/languages/typescript-react.md +525 -525
- package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
- package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
- package/.claude/vibe/rules/quality/checklist.md +276 -276
- package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
- package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
- package/.claude/vibe/rules/standards/code-structure.md +291 -291
- package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
- package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
- package/.claude/vibe/setup.sh +31 -31
- package/.claude/vibe/templates/constitution-template.md +184 -184
- package/.claude/vibe/templates/contract-backend-template.md +517 -517
- package/.claude/vibe/templates/contract-frontend-template.md +594 -594
- package/.claude/vibe/templates/feature-template.md +96 -96
- package/.claude/vibe/templates/spec-template.md +199 -199
- package/CLAUDE.md +345 -323
- package/LICENSE +21 -21
- package/README.md +744 -724
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/e2e-tester.md +266 -266
- package/agents/explorer.md +48 -48
- package/agents/implementer.md +53 -53
- package/agents/research/best-practices-agent.md +139 -139
- package/agents/research/codebase-patterns-agent.md +147 -147
- package/agents/research/framework-docs-agent.md +181 -181
- package/agents/research/security-advisory-agent.md +167 -167
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +152 -152
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +119 -119
- package/agents/tester.md +49 -49
- package/agents/ui-previewer.md +137 -137
- package/commands/vibe.analyze.md +245 -180
- package/commands/vibe.reason.md +223 -183
- package/commands/vibe.review.md +200 -136
- package/commands/vibe.run.md +838 -836
- package/commands/vibe.spec.md +419 -383
- package/commands/vibe.utils.md +101 -101
- package/commands/vibe.verify.md +282 -241
- package/dist/cli/index.js +385 -385
- package/dist/lib/MemoryManager.d.ts.map +1 -1
- package/dist/lib/MemoryManager.js +119 -114
- package/dist/lib/MemoryManager.js.map +1 -1
- package/dist/lib/PythonParser.js +108 -108
- package/dist/lib/gemini-mcp.js +15 -15
- package/dist/lib/gemini-oauth.js +35 -35
- package/dist/lib/gpt-mcp.js +17 -17
- package/dist/lib/gpt-oauth.js +44 -44
- package/dist/tools/analytics/getUsageAnalytics.js +12 -12
- package/dist/tools/index.d.ts +50 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
- package/dist/tools/planning/generatePrd.js +46 -46
- package/dist/tools/prompt/enhancePromptGemini.js +160 -160
- package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/hooks/hooks.json +121 -103
- package/package.json +73 -69
- package/skills/git-worktree.md +178 -178
- package/skills/priority-todos.md +236 -236
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
# 🟢 TypeScript + Node.js Backend 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄
|
|
11
|
-
✅ 중첩 ≤ 3단계
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Express.js 규칙
|
|
16
|
-
|
|
17
|
-
### 1. 라우터 구조화
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// ✅ routes/user.routes.ts
|
|
21
|
-
import { Router } from 'express';
|
|
22
|
-
import { UserController } from '@/controllers/user.controller';
|
|
23
|
-
import { authMiddleware } from '@/middleware/auth';
|
|
24
|
-
import { validate } from '@/middleware/validate';
|
|
25
|
-
import { createUserSchema, updateUserSchema } from '@/schemas/user.schema';
|
|
26
|
-
|
|
27
|
-
const router = Router();
|
|
28
|
-
const controller = new UserController();
|
|
29
|
-
|
|
30
|
-
router.get('/', controller.findAll);
|
|
31
|
-
router.get('/:id', controller.findOne);
|
|
32
|
-
router.post('/', validate(createUserSchema), controller.create);
|
|
33
|
-
router.put('/:id', authMiddleware, validate(updateUserSchema), controller.update);
|
|
34
|
-
router.delete('/:id', authMiddleware, controller.delete);
|
|
35
|
-
|
|
36
|
-
export default router;
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### 2. Controller 패턴
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// ✅ controllers/user.controller.ts
|
|
43
|
-
import { Request, Response, NextFunction } from 'express';
|
|
44
|
-
import { UserService } from '@/services/user.service';
|
|
45
|
-
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
46
|
-
|
|
47
|
-
export class UserController {
|
|
48
|
-
private userService = new UserService();
|
|
49
|
-
|
|
50
|
-
findAll = async (req: Request, res: Response, next: NextFunction) => {
|
|
51
|
-
try {
|
|
52
|
-
const { page = 1, limit = 10 } = req.query;
|
|
53
|
-
const users = await this.userService.findAll({
|
|
54
|
-
page: Number(page),
|
|
55
|
-
limit: Number(limit),
|
|
56
|
-
});
|
|
57
|
-
res.json(users);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
next(error);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
findOne = async (req: Request, res: Response, next: NextFunction) => {
|
|
64
|
-
try {
|
|
65
|
-
const user = await this.userService.findById(req.params.id);
|
|
66
|
-
if (!user) {
|
|
67
|
-
return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
|
|
68
|
-
}
|
|
69
|
-
res.json(user);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
next(error);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
create = async (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
-
try {
|
|
77
|
-
const dto: CreateUserDto = req.body;
|
|
78
|
-
const user = await this.userService.create(dto);
|
|
79
|
-
res.status(201).json(user);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
next(error);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 3. Service 레이어
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
// ✅ services/user.service.ts
|
|
91
|
-
import { prisma } from '@/lib/prisma';
|
|
92
|
-
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
93
|
-
import { hashPassword } from '@/utils/crypto';
|
|
94
|
-
import { AppError } from '@/utils/errors';
|
|
95
|
-
|
|
96
|
-
export class UserService {
|
|
97
|
-
async findAll(options: { page: number; limit: number }) {
|
|
98
|
-
const { page, limit } = options;
|
|
99
|
-
const skip = (page - 1) * limit;
|
|
100
|
-
|
|
101
|
-
const [users, total] = await Promise.all([
|
|
102
|
-
prisma.user.findMany({ skip, take: limit }),
|
|
103
|
-
prisma.user.count(),
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
data: users,
|
|
108
|
-
meta: {
|
|
109
|
-
page,
|
|
110
|
-
limit,
|
|
111
|
-
total,
|
|
112
|
-
totalPages: Math.ceil(total / limit),
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async findById(id: string) {
|
|
118
|
-
return prisma.user.findUnique({ where: { id } });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async create(dto: CreateUserDto) {
|
|
122
|
-
const existing = await prisma.user.findUnique({
|
|
123
|
-
where: { email: dto.email },
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (existing) {
|
|
127
|
-
throw new AppError('이미 존재하는 이메일입니다', 409);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const hashedPassword = await hashPassword(dto.password);
|
|
131
|
-
|
|
132
|
-
return prisma.user.create({
|
|
133
|
-
data: {
|
|
134
|
-
...dto,
|
|
135
|
-
password: hashedPassword,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## NestJS 규칙
|
|
143
|
-
|
|
144
|
-
### 1. Module 구조
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
// ✅ user/user.module.ts
|
|
148
|
-
import { Module } from '@nestjs/common';
|
|
149
|
-
import { UserController } from './user.controller';
|
|
150
|
-
import { UserService } from './user.service';
|
|
151
|
-
import { UserRepository } from './user.repository';
|
|
152
|
-
|
|
153
|
-
@Module({
|
|
154
|
-
controllers: [UserController],
|
|
155
|
-
providers: [UserService, UserRepository],
|
|
156
|
-
exports: [UserService],
|
|
157
|
-
})
|
|
158
|
-
export class UserModule {}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### 2. Controller (NestJS)
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// ✅ user/user.controller.ts
|
|
165
|
-
import {
|
|
166
|
-
Controller,
|
|
167
|
-
Get,
|
|
168
|
-
Post,
|
|
169
|
-
Put,
|
|
170
|
-
Delete,
|
|
171
|
-
Param,
|
|
172
|
-
Body,
|
|
173
|
-
Query,
|
|
174
|
-
UseGuards,
|
|
175
|
-
ParseIntPipe,
|
|
176
|
-
} from '@nestjs/common';
|
|
177
|
-
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
178
|
-
import { UserService } from './user.service';
|
|
179
|
-
import { CreateUserDto, UpdateUserDto, UserQueryDto } from './dto';
|
|
180
|
-
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
|
|
181
|
-
import { CurrentUser } from '@/auth/decorators/current-user.decorator';
|
|
182
|
-
|
|
183
|
-
@ApiTags('users')
|
|
184
|
-
@Controller('users')
|
|
185
|
-
export class UserController {
|
|
186
|
-
constructor(private readonly userService: UserService) {}
|
|
187
|
-
|
|
188
|
-
@Get()
|
|
189
|
-
@ApiOperation({ summary: '사용자 목록 조회' })
|
|
190
|
-
async findAll(@Query() query: UserQueryDto) {
|
|
191
|
-
return this.userService.findAll(query);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
@Get(':id')
|
|
195
|
-
@ApiOperation({ summary: '사용자 상세 조회' })
|
|
196
|
-
async findOne(@Param('id', ParseIntPipe) id: number) {
|
|
197
|
-
return this.userService.findById(id);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
@Post()
|
|
201
|
-
@ApiOperation({ summary: '사용자 생성' })
|
|
202
|
-
async create(@Body() dto: CreateUserDto) {
|
|
203
|
-
return this.userService.create(dto);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
@Put(':id')
|
|
207
|
-
@UseGuards(JwtAuthGuard)
|
|
208
|
-
@ApiBearerAuth()
|
|
209
|
-
@ApiOperation({ summary: '사용자 수정' })
|
|
210
|
-
async update(
|
|
211
|
-
@Param('id', ParseIntPipe) id: number,
|
|
212
|
-
@Body() dto: UpdateUserDto,
|
|
213
|
-
@CurrentUser() user: User,
|
|
214
|
-
) {
|
|
215
|
-
return this.userService.update(id, dto, user);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### 3. Service (NestJS)
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// ✅ user/user.service.ts
|
|
224
|
-
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
|
225
|
-
import { UserRepository } from './user.repository';
|
|
226
|
-
import { CreateUserDto, UpdateUserDto } from './dto';
|
|
227
|
-
import * as bcrypt from 'bcrypt';
|
|
228
|
-
|
|
229
|
-
@Injectable()
|
|
230
|
-
export class UserService {
|
|
231
|
-
constructor(private readonly userRepository: UserRepository) {}
|
|
232
|
-
|
|
233
|
-
async findAll(query: UserQueryDto) {
|
|
234
|
-
return this.userRepository.findAll(query);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async findById(id: number) {
|
|
238
|
-
const user = await this.userRepository.findById(id);
|
|
239
|
-
if (!user) {
|
|
240
|
-
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
|
241
|
-
}
|
|
242
|
-
return user;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async create(dto: CreateUserDto) {
|
|
246
|
-
const existing = await this.userRepository.findByEmail(dto.email);
|
|
247
|
-
if (existing) {
|
|
248
|
-
throw new ConflictException('이미 존재하는 이메일입니다');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
|
252
|
-
return this.userRepository.create({
|
|
253
|
-
...dto,
|
|
254
|
-
password: hashedPassword,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### 4. DTO와 Validation
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
// ✅ user/dto/create-user.dto.ts
|
|
264
|
-
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
|
265
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
266
|
-
|
|
267
|
-
export class CreateUserDto {
|
|
268
|
-
@ApiProperty({ example: 'user@example.com' })
|
|
269
|
-
@IsEmail({}, { message: '올바른 이메일 형식이 아닙니다' })
|
|
270
|
-
email: string;
|
|
271
|
-
|
|
272
|
-
@ApiProperty({ example: 'password123' })
|
|
273
|
-
@IsString()
|
|
274
|
-
@MinLength(8, { message: '비밀번호는 8자 이상이어야 합니다' })
|
|
275
|
-
password: string;
|
|
276
|
-
|
|
277
|
-
@ApiProperty({ example: '홍길동' })
|
|
278
|
-
@IsString()
|
|
279
|
-
name: string;
|
|
280
|
-
|
|
281
|
-
@ApiPropertyOptional({ example: '010-1234-5678' })
|
|
282
|
-
@IsOptional()
|
|
283
|
-
@IsString()
|
|
284
|
-
phone?: string;
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## 공통 규칙
|
|
289
|
-
|
|
290
|
-
### 에러 처리
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
// ✅ utils/errors.ts
|
|
294
|
-
export class AppError extends Error {
|
|
295
|
-
constructor(
|
|
296
|
-
message: string,
|
|
297
|
-
public statusCode: number = 500,
|
|
298
|
-
public code?: string,
|
|
299
|
-
) {
|
|
300
|
-
super(message);
|
|
301
|
-
this.name = 'AppError';
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ✅ middleware/error.middleware.ts
|
|
306
|
-
import { Request, Response, NextFunction } from 'express';
|
|
307
|
-
import { AppError } from '@/utils/errors';
|
|
308
|
-
|
|
309
|
-
export function errorHandler(
|
|
310
|
-
err: Error,
|
|
311
|
-
req: Request,
|
|
312
|
-
res: Response,
|
|
313
|
-
next: NextFunction,
|
|
314
|
-
) {
|
|
315
|
-
console.error(err);
|
|
316
|
-
|
|
317
|
-
if (err instanceof AppError) {
|
|
318
|
-
return res.status(err.statusCode).json({
|
|
319
|
-
success: false,
|
|
320
|
-
message: err.message,
|
|
321
|
-
code: err.code,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
res.status(500).json({
|
|
326
|
-
success: false,
|
|
327
|
-
message: '서버 오류가 발생했습니다',
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Validation (Zod)
|
|
333
|
-
|
|
334
|
-
```typescript
|
|
335
|
-
// ✅ schemas/user.schema.ts
|
|
336
|
-
import { z } from 'zod';
|
|
337
|
-
|
|
338
|
-
export const createUserSchema = z.object({
|
|
339
|
-
body: z.object({
|
|
340
|
-
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
|
341
|
-
password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'),
|
|
342
|
-
name: z.string().min(1, '이름을 입력해주세요'),
|
|
343
|
-
phone: z.string().optional(),
|
|
344
|
-
}),
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
export type CreateUserInput = z.infer<typeof createUserSchema>['body'];
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
## 파일 구조
|
|
351
|
-
|
|
352
|
-
```
|
|
353
|
-
src/
|
|
354
|
-
├── controllers/ # 라우트 핸들러
|
|
355
|
-
├── services/ # 비즈니스 로직
|
|
356
|
-
├── repositories/ # 데이터 액세스
|
|
357
|
-
├── dto/ # Data Transfer Objects
|
|
358
|
-
├── schemas/ # Validation 스키마 (Zod)
|
|
359
|
-
├── middleware/ # Express 미들웨어
|
|
360
|
-
├── routes/ # 라우터 정의
|
|
361
|
-
├── utils/ # 유틸리티
|
|
362
|
-
├── types/ # TypeScript 타입
|
|
363
|
-
├── config/ # 설정
|
|
364
|
-
└── lib/ # 외부 라이브러리 래퍼
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
## 체크리스트
|
|
368
|
-
|
|
369
|
-
- [ ] Controller → Service → Repository 레이어 분리
|
|
370
|
-
- [ ] DTO로 입출력 타입 정의
|
|
371
|
-
- [ ] Zod/class-validator로 입력 검증
|
|
372
|
-
- [ ] 커스텀 에러 클래스 사용
|
|
373
|
-
- [ ] 에러 미들웨어로 중앙 처리
|
|
374
|
-
- [ ] `any` 타입 사용 금지
|
|
375
|
-
- [ ] async/await + try/catch 또는 에러 미들웨어
|
|
1
|
+
# 🟢 TypeScript + Node.js Backend 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Express.js 규칙
|
|
16
|
+
|
|
17
|
+
### 1. 라우터 구조화
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ✅ routes/user.routes.ts
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
import { UserController } from '@/controllers/user.controller';
|
|
23
|
+
import { authMiddleware } from '@/middleware/auth';
|
|
24
|
+
import { validate } from '@/middleware/validate';
|
|
25
|
+
import { createUserSchema, updateUserSchema } from '@/schemas/user.schema';
|
|
26
|
+
|
|
27
|
+
const router = Router();
|
|
28
|
+
const controller = new UserController();
|
|
29
|
+
|
|
30
|
+
router.get('/', controller.findAll);
|
|
31
|
+
router.get('/:id', controller.findOne);
|
|
32
|
+
router.post('/', validate(createUserSchema), controller.create);
|
|
33
|
+
router.put('/:id', authMiddleware, validate(updateUserSchema), controller.update);
|
|
34
|
+
router.delete('/:id', authMiddleware, controller.delete);
|
|
35
|
+
|
|
36
|
+
export default router;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Controller 패턴
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// ✅ controllers/user.controller.ts
|
|
43
|
+
import { Request, Response, NextFunction } from 'express';
|
|
44
|
+
import { UserService } from '@/services/user.service';
|
|
45
|
+
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
46
|
+
|
|
47
|
+
export class UserController {
|
|
48
|
+
private userService = new UserService();
|
|
49
|
+
|
|
50
|
+
findAll = async (req: Request, res: Response, next: NextFunction) => {
|
|
51
|
+
try {
|
|
52
|
+
const { page = 1, limit = 10 } = req.query;
|
|
53
|
+
const users = await this.userService.findAll({
|
|
54
|
+
page: Number(page),
|
|
55
|
+
limit: Number(limit),
|
|
56
|
+
});
|
|
57
|
+
res.json(users);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
next(error);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
findOne = async (req: Request, res: Response, next: NextFunction) => {
|
|
64
|
+
try {
|
|
65
|
+
const user = await this.userService.findById(req.params.id);
|
|
66
|
+
if (!user) {
|
|
67
|
+
return res.status(404).json({ message: '사용자를 찾을 수 없습니다' });
|
|
68
|
+
}
|
|
69
|
+
res.json(user);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
next(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
create = async (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
+
try {
|
|
77
|
+
const dto: CreateUserDto = req.body;
|
|
78
|
+
const user = await this.userService.create(dto);
|
|
79
|
+
res.status(201).json(user);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
next(error);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Service 레이어
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// ✅ services/user.service.ts
|
|
91
|
+
import { prisma } from '@/lib/prisma';
|
|
92
|
+
import { CreateUserDto, UpdateUserDto } from '@/dto/user.dto';
|
|
93
|
+
import { hashPassword } from '@/utils/crypto';
|
|
94
|
+
import { AppError } from '@/utils/errors';
|
|
95
|
+
|
|
96
|
+
export class UserService {
|
|
97
|
+
async findAll(options: { page: number; limit: number }) {
|
|
98
|
+
const { page, limit } = options;
|
|
99
|
+
const skip = (page - 1) * limit;
|
|
100
|
+
|
|
101
|
+
const [users, total] = await Promise.all([
|
|
102
|
+
prisma.user.findMany({ skip, take: limit }),
|
|
103
|
+
prisma.user.count(),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
data: users,
|
|
108
|
+
meta: {
|
|
109
|
+
page,
|
|
110
|
+
limit,
|
|
111
|
+
total,
|
|
112
|
+
totalPages: Math.ceil(total / limit),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async findById(id: string) {
|
|
118
|
+
return prisma.user.findUnique({ where: { id } });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async create(dto: CreateUserDto) {
|
|
122
|
+
const existing = await prisma.user.findUnique({
|
|
123
|
+
where: { email: dto.email },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (existing) {
|
|
127
|
+
throw new AppError('이미 존재하는 이메일입니다', 409);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hashedPassword = await hashPassword(dto.password);
|
|
131
|
+
|
|
132
|
+
return prisma.user.create({
|
|
133
|
+
data: {
|
|
134
|
+
...dto,
|
|
135
|
+
password: hashedPassword,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## NestJS 규칙
|
|
143
|
+
|
|
144
|
+
### 1. Module 구조
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// ✅ user/user.module.ts
|
|
148
|
+
import { Module } from '@nestjs/common';
|
|
149
|
+
import { UserController } from './user.controller';
|
|
150
|
+
import { UserService } from './user.service';
|
|
151
|
+
import { UserRepository } from './user.repository';
|
|
152
|
+
|
|
153
|
+
@Module({
|
|
154
|
+
controllers: [UserController],
|
|
155
|
+
providers: [UserService, UserRepository],
|
|
156
|
+
exports: [UserService],
|
|
157
|
+
})
|
|
158
|
+
export class UserModule {}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2. Controller (NestJS)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// ✅ user/user.controller.ts
|
|
165
|
+
import {
|
|
166
|
+
Controller,
|
|
167
|
+
Get,
|
|
168
|
+
Post,
|
|
169
|
+
Put,
|
|
170
|
+
Delete,
|
|
171
|
+
Param,
|
|
172
|
+
Body,
|
|
173
|
+
Query,
|
|
174
|
+
UseGuards,
|
|
175
|
+
ParseIntPipe,
|
|
176
|
+
} from '@nestjs/common';
|
|
177
|
+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
178
|
+
import { UserService } from './user.service';
|
|
179
|
+
import { CreateUserDto, UpdateUserDto, UserQueryDto } from './dto';
|
|
180
|
+
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
|
|
181
|
+
import { CurrentUser } from '@/auth/decorators/current-user.decorator';
|
|
182
|
+
|
|
183
|
+
@ApiTags('users')
|
|
184
|
+
@Controller('users')
|
|
185
|
+
export class UserController {
|
|
186
|
+
constructor(private readonly userService: UserService) {}
|
|
187
|
+
|
|
188
|
+
@Get()
|
|
189
|
+
@ApiOperation({ summary: '사용자 목록 조회' })
|
|
190
|
+
async findAll(@Query() query: UserQueryDto) {
|
|
191
|
+
return this.userService.findAll(query);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Get(':id')
|
|
195
|
+
@ApiOperation({ summary: '사용자 상세 조회' })
|
|
196
|
+
async findOne(@Param('id', ParseIntPipe) id: number) {
|
|
197
|
+
return this.userService.findById(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Post()
|
|
201
|
+
@ApiOperation({ summary: '사용자 생성' })
|
|
202
|
+
async create(@Body() dto: CreateUserDto) {
|
|
203
|
+
return this.userService.create(dto);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Put(':id')
|
|
207
|
+
@UseGuards(JwtAuthGuard)
|
|
208
|
+
@ApiBearerAuth()
|
|
209
|
+
@ApiOperation({ summary: '사용자 수정' })
|
|
210
|
+
async update(
|
|
211
|
+
@Param('id', ParseIntPipe) id: number,
|
|
212
|
+
@Body() dto: UpdateUserDto,
|
|
213
|
+
@CurrentUser() user: User,
|
|
214
|
+
) {
|
|
215
|
+
return this.userService.update(id, dto, user);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 3. Service (NestJS)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// ✅ user/user.service.ts
|
|
224
|
+
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
|
225
|
+
import { UserRepository } from './user.repository';
|
|
226
|
+
import { CreateUserDto, UpdateUserDto } from './dto';
|
|
227
|
+
import * as bcrypt from 'bcrypt';
|
|
228
|
+
|
|
229
|
+
@Injectable()
|
|
230
|
+
export class UserService {
|
|
231
|
+
constructor(private readonly userRepository: UserRepository) {}
|
|
232
|
+
|
|
233
|
+
async findAll(query: UserQueryDto) {
|
|
234
|
+
return this.userRepository.findAll(query);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async findById(id: number) {
|
|
238
|
+
const user = await this.userRepository.findById(id);
|
|
239
|
+
if (!user) {
|
|
240
|
+
throw new NotFoundException('사용자를 찾을 수 없습니다');
|
|
241
|
+
}
|
|
242
|
+
return user;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async create(dto: CreateUserDto) {
|
|
246
|
+
const existing = await this.userRepository.findByEmail(dto.email);
|
|
247
|
+
if (existing) {
|
|
248
|
+
throw new ConflictException('이미 존재하는 이메일입니다');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
|
252
|
+
return this.userRepository.create({
|
|
253
|
+
...dto,
|
|
254
|
+
password: hashedPassword,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 4. DTO와 Validation
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// ✅ user/dto/create-user.dto.ts
|
|
264
|
+
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
|
265
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
266
|
+
|
|
267
|
+
export class CreateUserDto {
|
|
268
|
+
@ApiProperty({ example: 'user@example.com' })
|
|
269
|
+
@IsEmail({}, { message: '올바른 이메일 형식이 아닙니다' })
|
|
270
|
+
email: string;
|
|
271
|
+
|
|
272
|
+
@ApiProperty({ example: 'password123' })
|
|
273
|
+
@IsString()
|
|
274
|
+
@MinLength(8, { message: '비밀번호는 8자 이상이어야 합니다' })
|
|
275
|
+
password: string;
|
|
276
|
+
|
|
277
|
+
@ApiProperty({ example: '홍길동' })
|
|
278
|
+
@IsString()
|
|
279
|
+
name: string;
|
|
280
|
+
|
|
281
|
+
@ApiPropertyOptional({ example: '010-1234-5678' })
|
|
282
|
+
@IsOptional()
|
|
283
|
+
@IsString()
|
|
284
|
+
phone?: string;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 공통 규칙
|
|
289
|
+
|
|
290
|
+
### 에러 처리
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// ✅ utils/errors.ts
|
|
294
|
+
export class AppError extends Error {
|
|
295
|
+
constructor(
|
|
296
|
+
message: string,
|
|
297
|
+
public statusCode: number = 500,
|
|
298
|
+
public code?: string,
|
|
299
|
+
) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.name = 'AppError';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ✅ middleware/error.middleware.ts
|
|
306
|
+
import { Request, Response, NextFunction } from 'express';
|
|
307
|
+
import { AppError } from '@/utils/errors';
|
|
308
|
+
|
|
309
|
+
export function errorHandler(
|
|
310
|
+
err: Error,
|
|
311
|
+
req: Request,
|
|
312
|
+
res: Response,
|
|
313
|
+
next: NextFunction,
|
|
314
|
+
) {
|
|
315
|
+
console.error(err);
|
|
316
|
+
|
|
317
|
+
if (err instanceof AppError) {
|
|
318
|
+
return res.status(err.statusCode).json({
|
|
319
|
+
success: false,
|
|
320
|
+
message: err.message,
|
|
321
|
+
code: err.code,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
res.status(500).json({
|
|
326
|
+
success: false,
|
|
327
|
+
message: '서버 오류가 발생했습니다',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Validation (Zod)
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// ✅ schemas/user.schema.ts
|
|
336
|
+
import { z } from 'zod';
|
|
337
|
+
|
|
338
|
+
export const createUserSchema = z.object({
|
|
339
|
+
body: z.object({
|
|
340
|
+
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
|
341
|
+
password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'),
|
|
342
|
+
name: z.string().min(1, '이름을 입력해주세요'),
|
|
343
|
+
phone: z.string().optional(),
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>['body'];
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## 파일 구조
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
src/
|
|
354
|
+
├── controllers/ # 라우트 핸들러
|
|
355
|
+
├── services/ # 비즈니스 로직
|
|
356
|
+
├── repositories/ # 데이터 액세스
|
|
357
|
+
├── dto/ # Data Transfer Objects
|
|
358
|
+
├── schemas/ # Validation 스키마 (Zod)
|
|
359
|
+
├── middleware/ # Express 미들웨어
|
|
360
|
+
├── routes/ # 라우터 정의
|
|
361
|
+
├── utils/ # 유틸리티
|
|
362
|
+
├── types/ # TypeScript 타입
|
|
363
|
+
├── config/ # 설정
|
|
364
|
+
└── lib/ # 외부 라이브러리 래퍼
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## 체크리스트
|
|
368
|
+
|
|
369
|
+
- [ ] Controller → Service → Repository 레이어 분리
|
|
370
|
+
- [ ] DTO로 입출력 타입 정의
|
|
371
|
+
- [ ] Zod/class-validator로 입력 검증
|
|
372
|
+
- [ ] 커스텀 에러 클래스 사용
|
|
373
|
+
- [ ] 에러 미들웨어로 중앙 처리
|
|
374
|
+
- [ ] `any` 타입 사용 금지
|
|
375
|
+
- [ ] async/await + try/catch 또는 에러 미들웨어
|