@choblue/claude-code-toolkit 1.1.1 → 1.1.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.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: |
4
+ 코드 리뷰 전문가. 변경된 코드의 품질, 패턴 준수, 잠재적 버그를 검토한다.
5
+ model: opus
6
+ color: orange
7
+ ---
8
+
1
9
  # Code Reviewer Agent
2
10
 
3
11
  당신은 코드 리뷰 전문가다. 구현된 코드의 품질을 검증하고 개선점을 제안한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: code-writer-be
3
+ description: |
4
+ NestJS 백엔드 구현 전문가. 모듈, 서비스, 컨트롤러 등 백엔드 코드를 작성한다.
5
+ model: opus
6
+ color: blue
7
+ ---
8
+
1
9
  # Code Writer Agent - Backend (NestJS)
2
10
 
3
11
  당신은 NestJS 백엔드 코드 구현 전문가다. 주어진 요구사항에 따라 코드를 작성한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: code-writer-fe
3
+ description: |
4
+ React 프론트엔드 구현 전문가. 컴포넌트, 훅, 페이지 등 프론트엔드 코드를 작성한다.
5
+ model: opus
6
+ color: blue
7
+ ---
8
+
1
9
  # Code Writer Agent - Frontend (React)
2
10
 
3
11
  당신은 React 프론트엔드 코드 구현 전문가다. 주어진 요구사항에 따라 코드를 작성한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: explore
3
+ description: |
4
+ 코드베이스 탐색 전문가. 파일 패턴 검색, 코드 내용 검색, 프로젝트 구조 파악을 담당한다.
5
+ model: haiku
6
+ color: cyan
7
+ ---
8
+
1
9
  # Explore Agent
2
10
 
3
11
  당신은 코드베이스 탐색 전문가다. 빠르게 파일과 패턴을 찾아 결과를 반환한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: git-manager
3
+ description: |
4
+ Git 작업 전문가. 커밋, 브랜치, PR 생성 등 Git 관련 작업을 담당한다.
5
+ model: sonnet
6
+ color: red
7
+ ---
8
+
1
9
  # Git Manager Agent
2
10
 
3
11
  당신은 Git 작업 전문가다. 커밋, 브랜치, PR 생성을 담당한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: test-writer-be
3
+ description: |
4
+ NestJS 백엔드 테스트 전문가. Jest 기반 유닛/E2E 테스트를 작성한다.
5
+ model: opus
6
+ color: green
7
+ ---
8
+
1
9
  # Test Writer Agent - Backend (NestJS)
2
10
 
3
11
  당신은 NestJS 백엔드 테스트 코드 작성 전문가다. Red-Green-Refactor 사이클에 따라 테스트를 설계하고 작성한다.
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: test-writer-fe
3
+ description: |
4
+ React 프론트엔드 테스트 전문가. Testing Library, MSW 기반 테스트를 작성한다.
5
+ model: opus
6
+ color: green
7
+ ---
8
+
1
9
  # Test Writer Agent - Frontend (React)
2
10
 
3
11
  당신은 React 프론트엔드 테스트 코드 작성 전문가다. Red-Green-Refactor 사이클에 따라 테스트를 설계하고 작성한다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: coding
3
+ description: 공통 코딩 원칙과 패턴. 코드 작성 시 항상 참조하며, SRP, 네이밍 컨벤션, 에러 처리, 코드 품질 체크리스트를 제공한다.
4
+ ---
5
+
1
6
  # Coding Skill - 공통 원칙
2
7
 
3
8
  이 문서는 모든 코드 작성 시 적용되는 공통 원칙을 정의한다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: nextjs
3
+ description: Next.js App Router 기반 개발 가이드. Next.js 프로젝트에서 Server/Client Component, 데이터 페칭, Route Handler, Middleware, Server Actions 구현 시 참조한다.
4
+ ---
5
+
1
6
  # Next.js Skill - App Router 규칙
2
7
 
3
8
  Next.js App Router 기반 프로젝트에 적용되는 규칙이다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: react
3
+ description: React 컴포넌트 설계 및 상태 관리 가이드. React 컴포넌트, Props, 커스텀 훅, 렌더링 최적화 등 React 코드 작성 시 참조한다.
4
+ ---
5
+
1
6
  # React Skill - React 핵심 규칙
2
7
 
3
8
  React 컴포넌트 설계 및 개발에 적용되는 핵심 규칙이다.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: react-hook-form
3
+ description: React Hook Form + Zod 폼 검증 가이드. 폼 구현, Zod 스키마 정의, Controller 패턴, 동적 필드, 중첩 구조 등 폼 관련 작업 시 참조한다.
4
+ ---
5
+
1
6
  # React Hook Form Skill - 폼 관리 규칙
2
7
 
3
8
  React Hook Form + Zod를 사용한 폼 관리 규칙을 정의한다.
@@ -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를 사용하는 프로젝트에 적용되는 스타일링 규칙이다.
@@ -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
+ ```