@choblue/claude-code-toolkit 1.1.1 → 1.1.4

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.
Files changed (34) hide show
  1. package/.claude/.project-map-cache +1 -0
  2. package/.claude/CLAUDE.md +12 -0
  3. package/.claude/PROJECT_MAP.md +43 -0
  4. package/.claude/agents/code-reviewer.md +8 -0
  5. package/.claude/agents/code-writer-be.md +8 -0
  6. package/.claude/agents/code-writer-fe.md +8 -0
  7. package/.claude/agents/explore.md +15 -0
  8. package/.claude/agents/git-manager.md +8 -0
  9. package/.claude/agents/test-writer-be.md +8 -0
  10. package/.claude/agents/test-writer-fe.md +8 -0
  11. package/.claude/hooks/project-map-detector.sh +71 -0
  12. package/.claude/hooks/skill-detector.sh +89 -0
  13. package/.claude/hooks/skill-keywords.conf +19 -0
  14. package/.claude/scripts/generate-project-map.sh +264 -0
  15. package/.claude/settings.json +8 -0
  16. package/.claude/skills/Coding/SKILL.md +5 -0
  17. package/.claude/skills/NextJS/SKILL.md +5 -0
  18. package/.claude/skills/React/SKILL.md +5 -0
  19. package/.claude/skills/ReactHookForm/SKILL.md +5 -0
  20. package/.claude/skills/TDD/SKILL.md +5 -0
  21. package/.claude/skills/TailwindCSS/SKILL.md +28 -5
  22. package/.claude/skills/TanStackQuery/SKILL.md +5 -0
  23. package/.claude/skills/TypeORM/SKILL.md +16 -303
  24. package/.claude/skills/TypeORM/references/advanced-queries.md +176 -0
  25. package/.claude/skills/TypeORM/references/migrations.md +62 -0
  26. package/.claude/skills/TypeORM/references/transactions.md +76 -0
  27. package/.claude/skills/TypeScript/SKILL.md +17 -225
  28. package/.claude/skills/TypeScript/references/advanced-patterns.md +146 -0
  29. package/.claude/skills/TypeScript/references/generics.md +98 -0
  30. package/.claude/skills/TypeScript/references/type-guards.md +109 -0
  31. package/.claude/skills/Zustand/SKILL.md +5 -0
  32. package/README.md +35 -4
  33. package/install.sh +8 -0
  34. package/package.json +1 -1
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: tdd
3
+ description: TDD(테스트 주도 개발) 공통 원칙. 테스트 작성 시 Red-Green-Refactor 사이클, FIRST 원칙, AAA 패턴, Mock/Stub/Spy 사용법을 참조한다.
4
+ ---
5
+
1
6
  # TDD Skill - 핵심 원칙
2
7
 
3
8
  이 문서는 모든 테스트 코드 작성 시 적용되는 공통 원칙을 정의한다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: tailwindcss
3
+ description: Tailwind CSS 유틸리티 패턴 가이드. 스타일링, 반응형 디자인, 다크 모드, CVA 패턴, cn() 유틸리티 등 Tailwind 기반 스타일 작업 시 참조한다.
4
+ ---
5
+
1
6
  # TailwindCSS Skill - Tailwind CSS 규칙
2
7
 
3
8
  Tailwind CSS를 사용하는 프로젝트에 적용되는 스타일링 규칙이다.
@@ -21,11 +26,23 @@ React 규칙은 `../React/SKILL.md`, 공통 원칙은 `../Coding/SKILL.md`를
21
26
 
22
27
  ### 클래스 조합
23
28
  - 관련 있는 클래스를 논리적 그룹으로 정렬한다
24
- - 권장 순서: 레이아웃 -> 크기 -> 간격 -> 타이포그래피 -> 색상 -> 효과
29
+ - 권장 순서: 레이아웃 크기 간격 타이포그래피 색상 효과
30
+ - **클래스가 길어지면 관심사별로 줄바꿈하여 가독성을 높인다**
25
31
 
26
32
  ```typescript
27
- // Good - 논리적 순서로 정렬
33
+ // Bad - 줄로 길게 나열 (화면 벗어남, 어떤 CSS가 있는지 파악 어려움)
28
34
  <button className="flex items-center justify-center w-full h-10 px-4 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
35
+
36
+ // Good - 관심사별 줄바꿈
37
+ <button
38
+ className={cn(
39
+ 'flex items-center justify-center', // 레이아웃
40
+ 'w-full h-10 px-4', // 크기/간격
41
+ 'text-sm font-medium text-white', // 타이포그래피
42
+ 'bg-blue-600 rounded-lg', // 배경/모양
43
+ 'hover:bg-blue-700', // 상태
44
+ )}
45
+ >
29
46
  버튼
30
47
  </button>
31
48
  ```
@@ -222,7 +239,8 @@ export function Button({ variant = 'primary', size = 'md', className, children }
222
239
  <button
223
240
  className={cn(
224
241
  // 기본 스타일
225
- 'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
242
+ 'inline-flex items-center justify-center',
243
+ 'rounded-lg font-medium transition-colors',
226
244
  // variant
227
245
  {
228
246
  'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
@@ -256,7 +274,12 @@ import { cn } from '@/lib/utils';
256
274
 
257
275
  const buttonVariants = cva(
258
276
  // 기본 스타일
259
- 'inline-flex items-center justify-center rounded-lg font-medium transition-colors disabled:pointer-events-none disabled:opacity-50',
277
+ [
278
+ 'inline-flex items-center justify-center', // 레이아웃
279
+ 'rounded-lg font-medium', // 모양/타이포
280
+ 'transition-colors', // 효과
281
+ 'disabled:pointer-events-none disabled:opacity-50', // 상태
282
+ ],
260
283
  {
261
284
  variants: {
262
285
  variant: {
@@ -365,4 +388,4 @@ const colorClasses = {
365
388
  } as const;
366
389
 
367
390
  <div className={colorClasses[color]}>
368
- ```
391
+ ```
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: tanstack-query
3
+ description: TanStack Query 서버 상태 관리 가이드. useQuery, useMutation, queryKey 팩토리, Optimistic Update, Prefetching 등 서버 상태 관리 시 참조한다.
4
+ ---
5
+
1
6
  # TanStack Query Skill - 서버 상태 관리 규칙
2
7
 
3
8
  TanStack Query (React Query)를 사용한 서버 상태 관리 규칙을 정의한다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: typeorm
3
+ description: TypeORM Entity, Repository, QueryBuilder 가이드. NestJS에서 TypeORM을 사용한 Entity 정의, Relations, Repository 패턴, 마이그레이션, 트랜잭션 등 데이터베이스 작업 시 참조한다.
4
+ ---
5
+
1
6
  # TypeORM Skill - TypeORM 규칙
2
7
 
3
8
  NestJS와 함께 사용하는 TypeORM 패턴과 규칙을 정의한다.
@@ -279,307 +284,7 @@ const count = await this.userRepository.count({ where: { isActive: true } });
279
284
 
280
285
  ---
281
286
 
282
- ## 4. QueryBuilder
283
-
284
- ### 기본 사용법
285
-
286
- ```typescript
287
- const users = await this.userRepository
288
- .createQueryBuilder('user')
289
- .select(['user.id', 'user.name', 'user.email'])
290
- .where('user.isActive = :isActive', { isActive: true })
291
- .andWhere('user.role = :role', { role: UserRole.ADMIN })
292
- .orderBy('user.createdAt', 'DESC')
293
- .take(20)
294
- .skip(0)
295
- .getMany();
296
- ```
297
-
298
- ### JOIN
299
-
300
- ```typescript
301
- // leftJoinAndSelect - 관계가 없어도 결과에 포함 (LEFT JOIN)
302
- const users = await this.userRepository
303
- .createQueryBuilder('user')
304
- .leftJoinAndSelect('user.posts', 'post')
305
- .where('user.id = :id', { id: userId })
306
- .getOne();
307
-
308
- // innerJoinAndSelect - 관계가 있는 경우만 포함 (INNER JOIN)
309
- const usersWithPosts = await this.userRepository
310
- .createQueryBuilder('user')
311
- .innerJoinAndSelect('user.posts', 'post')
312
- .getMany();
313
- ```
314
-
315
- ### 서브쿼리
316
-
317
- ```typescript
318
- // 서브쿼리로 특정 조건의 사용자 조회
319
- const users = await this.userRepository
320
- .createQueryBuilder('user')
321
- .where((qb) => {
322
- const subQuery = qb
323
- .subQuery()
324
- .select('post.authorId')
325
- .from(Post, 'post')
326
- .where('post.createdAt > :date', { date: startDate })
327
- .getQuery();
328
- return `user.id IN ${subQuery}`;
329
- })
330
- .getMany();
331
- ```
332
-
333
- ### getOne vs getMany vs getRawOne vs getRawMany
334
-
335
- | 메서드 | 반환 타입 | 용도 |
336
- |--------|----------|------|
337
- | `getOne()` | `Entity \| null` | Entity 단건 조회 |
338
- | `getMany()` | `Entity[]` | Entity 목록 조회 |
339
- | `getRawOne()` | `object \| undefined` | 가공된 데이터 단건 (집계 등) |
340
- | `getRawMany()` | `object[]` | 가공된 데이터 목록 |
341
-
342
- ```typescript
343
- // 집계 쿼리 - getRawOne 사용
344
- const result = await this.postRepository
345
- .createQueryBuilder('post')
346
- .select('COUNT(post.id)', 'totalCount')
347
- .addSelect('AVG(post.viewCount)', 'avgViews')
348
- .where('post.authorId = :authorId', { authorId })
349
- .getRawOne();
350
- // result: { totalCount: '42', avgViews: '128.5' }
351
- ```
352
-
353
- ### find vs QueryBuilder 가이드
354
-
355
- | 상황 | 권장 방법 |
356
- |------|----------|
357
- | 단순 CRUD (조건 1~2개) | `find`, `findOne`, `findOneBy` |
358
- | 단순 관계 로딩 | `find` + `relations` 옵션 |
359
- | 복잡한 WHERE 조건 (OR, 중첩) | `QueryBuilder` |
360
- | JOIN 조건이 복잡한 경우 | `QueryBuilder` |
361
- | 집계 함수 (COUNT, SUM, AVG) | `QueryBuilder` + `getRawOne/getRawMany` |
362
- | 서브쿼리가 필요한 경우 | `QueryBuilder` |
363
- | 동적 조건 조합 | `QueryBuilder` |
364
-
365
- ---
366
-
367
- ## 5. 마이그레이션
368
-
369
- ### CLI 명령어
370
-
371
- ```bash
372
- # 마이그레이션 자동 생성 (Entity 변경 감지)
373
- npx typeorm migration:generate src/migrations/AddUserRole -d src/data-source.ts
374
-
375
- # 마이그레이션 수동 생성 (빈 파일)
376
- npx typeorm migration:create src/migrations/SeedInitialData
377
-
378
- # 마이그레이션 실행
379
- npx typeorm migration:run -d src/data-source.ts
380
-
381
- # 마이그레이션 되돌리기 (가장 최근 1개)
382
- npx typeorm migration:revert -d src/data-source.ts
383
- ```
384
-
385
- ### 마이그레이션 파일 작성 패턴
386
-
387
- ```typescript
388
- export class AddUserRole1700000000000 implements MigrationInterface {
389
- public async up(queryRunner: QueryRunner): Promise<void> {
390
- await queryRunner.addColumn(
391
- 'user',
392
- new TableColumn({
393
- name: 'role',
394
- type: 'enum',
395
- enum: ['user', 'admin', 'moderator'],
396
- default: `'user'`,
397
- }),
398
- );
399
-
400
- await queryRunner.createIndex(
401
- 'user',
402
- new TableIndex({
403
- name: 'IDX_USER_ROLE',
404
- columnNames: ['role'],
405
- }),
406
- );
407
- }
408
-
409
- public async down(queryRunner: QueryRunner): Promise<void> {
410
- await queryRunner.dropIndex('user', 'IDX_USER_ROLE');
411
- await queryRunner.dropColumn('user', 'role');
412
- }
413
- }
414
- ```
415
-
416
- ### 마이그레이션 원칙
417
- - `up`과 `down`을 반드시 쌍으로 작성한다 (되돌릴 수 있어야 한다)
418
- - `down`은 `up`의 역순으로 실행한다
419
- - `synchronize: true`는 개발 환경에서만 사용한다 - 프로덕션에서는 마이그레이션만 사용한다
420
-
421
- ---
422
-
423
- ## 6. 트랜잭션
424
-
425
- ### DataSource.transaction() 패턴
426
-
427
- 가장 간결한 트랜잭션 패턴이다. 콜백 내에서 제공되는 `EntityManager`를 사용한다.
428
-
429
- ```typescript
430
- @Injectable()
431
- export class OrderService {
432
- constructor(private readonly dataSource: DataSource) {}
433
-
434
- async createOrder(dto: CreateOrderDto): Promise<Order> {
435
- return this.dataSource.transaction(async (manager) => {
436
- const order = manager.create(Order, {
437
- userId: dto.userId,
438
- totalAmount: dto.totalAmount,
439
- });
440
- await manager.save(order);
441
-
442
- const orderItems = dto.items.map((item) =>
443
- manager.create(OrderItem, { ...item, orderId: order.id }),
444
- );
445
- await manager.save(orderItems);
446
-
447
- await manager.decrement(
448
- Product,
449
- { id: In(dto.items.map((i) => i.productId)) },
450
- 'stock',
451
- 1,
452
- );
453
-
454
- return order;
455
- });
456
- }
457
- }
458
- ```
459
-
460
- ### QueryRunner를 사용한 수동 트랜잭션
461
-
462
- 세밀한 제어가 필요한 경우 사용한다.
463
-
464
- ```typescript
465
- async transferPoints(fromId: string, toId: string, amount: number): Promise<void> {
466
- const queryRunner = this.dataSource.createQueryRunner();
467
- await queryRunner.connect();
468
- await queryRunner.startTransaction();
469
-
470
- try {
471
- await queryRunner.manager.decrement(User, { id: fromId }, 'points', amount);
472
- await queryRunner.manager.increment(User, { id: toId }, 'points', amount);
473
-
474
- await queryRunner.commitTransaction();
475
- } catch (error) {
476
- await queryRunner.rollbackTransaction();
477
- throw error;
478
- } finally {
479
- await queryRunner.release(); // 반드시 release
480
- }
481
- }
482
- ```
483
-
484
- ### 트랜잭션 선택 가이드
485
-
486
- | 상황 | 권장 패턴 |
487
- |------|----------|
488
- | 단순 다중 저장 | `DataSource.transaction()` |
489
- | 조건부 커밋/롤백 | `QueryRunner` 수동 트랜잭션 |
490
- | 중간에 외부 API 호출 필요 | `QueryRunner` (커밋 시점 제어) |
491
-
492
- ---
493
-
494
- ## 7. 성능 최적화
495
-
496
- ### N+1 문제 방지
497
-
498
- ```typescript
499
- // Bad - N+1 문제 발생 (users를 조회 후 각 user마다 posts를 개별 조회)
500
- const users = await this.userRepository.find();
501
- for (const user of users) {
502
- const posts = await this.postRepository.find({ where: { authorId: user.id } });
503
- }
504
-
505
- // Good - relations으로 한 번에 조회
506
- const users = await this.userRepository.find({
507
- relations: ['posts'],
508
- });
509
-
510
- // Good - QueryBuilder로 한 번에 조회
511
- const users = await this.userRepository
512
- .createQueryBuilder('user')
513
- .leftJoinAndSelect('user.posts', 'post')
514
- .getMany();
515
- ```
516
-
517
- ### select로 필요한 컬럼만 조회
518
-
519
- ```typescript
520
- // Bad - 모든 컬럼 조회
521
- const users = await this.userRepository.find();
522
-
523
- // Good - 필요한 컬럼만 조회
524
- const users = await this.userRepository.find({
525
- select: { id: true, name: true, email: true },
526
- });
527
-
528
- // Good - QueryBuilder에서 select
529
- const users = await this.userRepository
530
- .createQueryBuilder('user')
531
- .select(['user.id', 'user.name', 'user.email'])
532
- .getMany();
533
- ```
534
-
535
- ### 인덱스 설정
536
-
537
- ```typescript
538
- @Entity()
539
- @Index(['email'], { unique: true })
540
- @Index(['isActive', 'role']) // 복합 인덱스 - 자주 함께 조회하는 컬럼
541
- @Index(['createdAt']) // 정렬에 자주 사용되는 컬럼
542
- export class User {
543
- @PrimaryGeneratedColumn('uuid')
544
- id: string;
545
-
546
- @Column({ type: 'varchar' })
547
- email: string;
548
-
549
- @Column({ type: 'boolean', default: true })
550
- isActive: boolean;
551
-
552
- @Column({ type: 'enum', enum: UserRole })
553
- role: UserRole;
554
-
555
- @CreateDateColumn()
556
- createdAt: Date;
557
- }
558
- ```
559
-
560
- ### 인덱스 설정 원칙
561
- - WHERE 조건에 자주 사용되는 컬럼에 인덱스를 설정한다
562
- - ORDER BY에 자주 사용되는 컬럼에 인덱스를 설정한다
563
- - 복합 인덱스는 카디널리티가 높은 컬럼을 앞에 배치한다
564
- - 불필요한 인덱스는 쓰기 성능을 저하시키므로 남용하지 않는다
565
-
566
- ### Pagination
567
-
568
- ```typescript
569
- async findUsers(page: number, limit: number): Promise<{ data: User[]; total: number }> {
570
- const [data, total] = await this.userRepository.findAndCount({
571
- order: { createdAt: 'DESC' },
572
- take: limit,
573
- skip: (page - 1) * limit,
574
- });
575
-
576
- return { data, total };
577
- }
578
- ```
579
-
580
- ---
581
-
582
- ## 8. 네이밍 컨벤션
287
+ ## 4. 네이밍 컨벤션
583
288
 
584
289
  | 대상 | 규칙 | 예시 |
585
290
  |------|------|------|
@@ -608,7 +313,15 @@ export const dataSource = new DataSource({
608
313
 
609
314
  ---
610
315
 
611
- ## 9. 금지 사항
316
+ ## 참조 문서
317
+
318
+ - **[Advanced Queries](./references/advanced-queries.md)** - QueryBuilder 고급 사용법, 서브쿼리, 성능 최적화, N+1 문제 해결
319
+ - **[Migrations](./references/migrations.md)** - 마이그레이션 생성, 실행, 롤백 및 작성 패턴
320
+ - **[Transactions](./references/transactions.md)** - 트랜잭션 패턴 및 사용 가이드
321
+
322
+ ---
323
+
324
+ ## 5. 금지 사항
612
325
 
613
326
  - `synchronize: true` 프로덕션 사용 금지 - 마이그레이션을 사용한다
614
327
  - Raw SQL 직접 실행 금지 (`query()` 메서드 사용 금지) - QueryBuilder를 사용한다
@@ -618,4 +331,4 @@ export const dataSource = new DataSource({
618
331
  - `eager: true` 남용 금지 - 필요할 때 `relations` 옵션으로 로딩한다
619
332
  - `find` 시 `where` 조건 없이 전체 조회 금지 (대량 데이터 위험) - 반드시 조건 또는 pagination을 사용한다
620
333
  - Repository 외 레이어에서 직접 쿼리 실행 금지 - `../Coding/backend.md` 레이어 규칙을 따른다
621
- - 마이그레이션 `down` 메서드 누락 금지 - 항상 롤백 가능해야 한다
334
+ - 마이그레이션 `down` 메서드 누락 금지 - 항상 롤백 가능해야 한다
@@ -0,0 +1,176 @@
1
+ # TypeORM Advanced Queries - QueryBuilder 고급 사용법 및 성능 최적화
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. QueryBuilder
8
+
9
+ ### 기본 사용법
10
+
11
+ ```typescript
12
+ const users = await this.userRepository
13
+ .createQueryBuilder('user')
14
+ .select(['user.id', 'user.name', 'user.email'])
15
+ .where('user.isActive = :isActive', { isActive: true })
16
+ .andWhere('user.role = :role', { role: UserRole.ADMIN })
17
+ .orderBy('user.createdAt', 'DESC')
18
+ .take(20)
19
+ .skip(0)
20
+ .getMany();
21
+ ```
22
+
23
+ ### JOIN
24
+
25
+ ```typescript
26
+ // leftJoinAndSelect - 관계가 없어도 결과에 포함 (LEFT JOIN)
27
+ const users = await this.userRepository
28
+ .createQueryBuilder('user')
29
+ .leftJoinAndSelect('user.posts', 'post')
30
+ .where('user.id = :id', { id: userId })
31
+ .getOne();
32
+
33
+ // innerJoinAndSelect - 관계가 있는 경우만 포함 (INNER JOIN)
34
+ const usersWithPosts = await this.userRepository
35
+ .createQueryBuilder('user')
36
+ .innerJoinAndSelect('user.posts', 'post')
37
+ .getMany();
38
+ ```
39
+
40
+ ### 서브쿼리
41
+
42
+ ```typescript
43
+ // 서브쿼리로 특정 조건의 사용자 조회
44
+ const users = await this.userRepository
45
+ .createQueryBuilder('user')
46
+ .where((qb) => {
47
+ const subQuery = qb
48
+ .subQuery()
49
+ .select('post.authorId')
50
+ .from(Post, 'post')
51
+ .where('post.createdAt > :date', { date: startDate })
52
+ .getQuery();
53
+ return `user.id IN ${subQuery}`;
54
+ })
55
+ .getMany();
56
+ ```
57
+
58
+ ### getOne vs getMany vs getRawOne vs getRawMany
59
+
60
+ | 메서드 | 반환 타입 | 용도 |
61
+ |--------|----------|------|
62
+ | `getOne()` | `Entity \| null` | Entity 단건 조회 |
63
+ | `getMany()` | `Entity[]` | Entity 목록 조회 |
64
+ | `getRawOne()` | `object \| undefined` | 가공된 데이터 단건 (집계 등) |
65
+ | `getRawMany()` | `object[]` | 가공된 데이터 목록 |
66
+
67
+ ```typescript
68
+ // 집계 쿼리 - getRawOne 사용
69
+ const result = await this.postRepository
70
+ .createQueryBuilder('post')
71
+ .select('COUNT(post.id)', 'totalCount')
72
+ .addSelect('AVG(post.viewCount)', 'avgViews')
73
+ .where('post.authorId = :authorId', { authorId })
74
+ .getRawOne();
75
+ // result: { totalCount: '42', avgViews: '128.5' }
76
+ ```
77
+
78
+ ### find vs QueryBuilder 가이드
79
+
80
+ | 상황 | 권장 방법 |
81
+ |------|----------|
82
+ | 단순 CRUD (조건 1~2개) | `find`, `findOne`, `findOneBy` |
83
+ | 단순 관계 로딩 | `find` + `relations` 옵션 |
84
+ | 복잡한 WHERE 조건 (OR, 중첩) | `QueryBuilder` |
85
+ | JOIN 조건이 복잡한 경우 | `QueryBuilder` |
86
+ | 집계 함수 (COUNT, SUM, AVG) | `QueryBuilder` + `getRawOne/getRawMany` |
87
+ | 서브쿼리가 필요한 경우 | `QueryBuilder` |
88
+ | 동적 조건 조합 | `QueryBuilder` |
89
+
90
+ ---
91
+
92
+ ## 2. 성능 최적화
93
+
94
+ ### N+1 문제 방지
95
+
96
+ ```typescript
97
+ // Bad - N+1 문제 발생 (users를 조회 후 각 user마다 posts를 개별 조회)
98
+ const users = await this.userRepository.find();
99
+ for (const user of users) {
100
+ const posts = await this.postRepository.find({ where: { authorId: user.id } });
101
+ }
102
+
103
+ // Good - relations으로 한 번에 조회
104
+ const users = await this.userRepository.find({
105
+ relations: ['posts'],
106
+ });
107
+
108
+ // Good - QueryBuilder로 한 번에 조회
109
+ const users = await this.userRepository
110
+ .createQueryBuilder('user')
111
+ .leftJoinAndSelect('user.posts', 'post')
112
+ .getMany();
113
+ ```
114
+
115
+ ### select로 필요한 컬럼만 조회
116
+
117
+ ```typescript
118
+ // Bad - 모든 컬럼 조회
119
+ const users = await this.userRepository.find();
120
+
121
+ // Good - 필요한 컬럼만 조회
122
+ const users = await this.userRepository.find({
123
+ select: { id: true, name: true, email: true },
124
+ });
125
+
126
+ // Good - QueryBuilder에서 select
127
+ const users = await this.userRepository
128
+ .createQueryBuilder('user')
129
+ .select(['user.id', 'user.name', 'user.email'])
130
+ .getMany();
131
+ ```
132
+
133
+ ### 인덱스 설정
134
+
135
+ ```typescript
136
+ @Entity()
137
+ @Index(['email'], { unique: true })
138
+ @Index(['isActive', 'role']) // 복합 인덱스 - 자주 함께 조회하는 컬럼
139
+ @Index(['createdAt']) // 정렬에 자주 사용되는 컬럼
140
+ export class User {
141
+ @PrimaryGeneratedColumn('uuid')
142
+ id: string;
143
+
144
+ @Column({ type: 'varchar' })
145
+ email: string;
146
+
147
+ @Column({ type: 'boolean', default: true })
148
+ isActive: boolean;
149
+
150
+ @Column({ type: 'enum', enum: UserRole })
151
+ role: UserRole;
152
+
153
+ @CreateDateColumn()
154
+ createdAt: Date;
155
+ }
156
+ ```
157
+
158
+ ### 인덱스 설정 원칙
159
+ - WHERE 조건에 자주 사용되는 컬럼에 인덱스를 설정한다
160
+ - ORDER BY에 자주 사용되는 컬럼에 인덱스를 설정한다
161
+ - 복합 인덱스는 카디널리티가 높은 컬럼을 앞에 배치한다
162
+ - 불필요한 인덱스는 쓰기 성능을 저하시키므로 남용하지 않는다
163
+
164
+ ### Pagination
165
+
166
+ ```typescript
167
+ async findUsers(page: number, limit: number): Promise<{ data: User[]; total: number }> {
168
+ const [data, total] = await this.userRepository.findAndCount({
169
+ order: { createdAt: 'DESC' },
170
+ take: limit,
171
+ skip: (page - 1) * limit,
172
+ });
173
+
174
+ return { data, total };
175
+ }
176
+ ```
@@ -0,0 +1,62 @@
1
+ # TypeORM Migrations - 마이그레이션 가이드
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. CLI 명령어
8
+
9
+ ```bash
10
+ # 마이그레이션 자동 생성 (Entity 변경 감지)
11
+ npx typeorm migration:generate src/migrations/AddUserRole -d src/data-source.ts
12
+
13
+ # 마이그레이션 수동 생성 (빈 파일)
14
+ npx typeorm migration:create src/migrations/SeedInitialData
15
+
16
+ # 마이그레이션 실행
17
+ npx typeorm migration:run -d src/data-source.ts
18
+
19
+ # 마이그레이션 되돌리기 (가장 최근 1개)
20
+ npx typeorm migration:revert -d src/data-source.ts
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 2. 마이그레이션 파일 작성 패턴
26
+
27
+ ```typescript
28
+ export class AddUserRole1700000000000 implements MigrationInterface {
29
+ public async up(queryRunner: QueryRunner): Promise<void> {
30
+ await queryRunner.addColumn(
31
+ 'user',
32
+ new TableColumn({
33
+ name: 'role',
34
+ type: 'enum',
35
+ enum: ['user', 'admin', 'moderator'],
36
+ default: `'user'`,
37
+ }),
38
+ );
39
+
40
+ await queryRunner.createIndex(
41
+ 'user',
42
+ new TableIndex({
43
+ name: 'IDX_USER_ROLE',
44
+ columnNames: ['role'],
45
+ }),
46
+ );
47
+ }
48
+
49
+ public async down(queryRunner: QueryRunner): Promise<void> {
50
+ await queryRunner.dropIndex('user', 'IDX_USER_ROLE');
51
+ await queryRunner.dropColumn('user', 'role');
52
+ }
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 3. 마이그레이션 원칙
59
+
60
+ - `up`과 `down`을 반드시 쌍으로 작성한다 (되돌릴 수 있어야 한다)
61
+ - `down`은 `up`의 역순으로 실행한다
62
+ - `synchronize: true`는 개발 환경에서만 사용한다 - 프로덕션에서는 마이그레이션만 사용한다