@choblue/claude-code-toolkit 1.1.4 → 1.2.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,234 @@
1
+ # Aggregate & Repository 심화
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. Aggregate 심화
8
+
9
+ ### 내부 Entity (OrderItem) 관리
10
+
11
+ Aggregate Root는 내부 Entity의 생명주기를 완전히 관리한다. 외부에서 내부 Entity를 직접 생성하거나 조작할 수 없다.
12
+
13
+ ```typescript
14
+ // Good - Aggregate 내부 Entity
15
+ class OrderItem {
16
+ constructor(
17
+ private readonly _productId: string, // ID로만 참조
18
+ private readonly _price: Money,
19
+ private _quantity: number,
20
+ ) {
21
+ if (quantity <= 0) {
22
+ throw new Error('Quantity must be positive');
23
+ }
24
+ }
25
+
26
+ get productId(): string { return this._productId; }
27
+ get price(): Money { return this._price; }
28
+ get quantity(): number { return this._quantity; }
29
+
30
+ get subtotal(): Money {
31
+ return this._price.multiply(this._quantity);
32
+ }
33
+
34
+ increaseQuantity(amount: number): void {
35
+ if (amount <= 0) throw new Error('Amount must be positive');
36
+ this._quantity += amount;
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Aggregate Root의 addItem / removeItem 패턴
42
+
43
+ Aggregate Root가 불변식을 보호하면서 내부 Entity를 조작하는 전체 패턴이다.
44
+
45
+ ```typescript
46
+ // Good - Aggregate Root가 내부 Entity의 추가/제거를 관리
47
+ class Order {
48
+ private readonly _id: string;
49
+ private readonly _customerId: string;
50
+ private readonly _items: OrderItem[];
51
+ private _status: OrderStatus;
52
+
53
+ constructor(id: string, customerId: string, items: OrderItem[]) {
54
+ if (items.length === 0) {
55
+ throw new Error('Order must have at least one item');
56
+ }
57
+ this._id = id;
58
+ this._customerId = customerId;
59
+ this._items = items;
60
+ this._status = OrderStatus.PENDING;
61
+ }
62
+
63
+ get id(): string { return this._id; }
64
+ get customerId(): string { return this._customerId; }
65
+ get items(): ReadonlyArray<OrderItem> { return [...this._items]; }
66
+ get status(): OrderStatus { return this._status; }
67
+
68
+ addItem(productId: string, price: Money, quantity: number): void {
69
+ if (this._status !== OrderStatus.PENDING) {
70
+ throw new Error('Cannot modify confirmed order');
71
+ }
72
+ const existing = this._items.find(i => i.productId === productId);
73
+ if (existing) {
74
+ existing.increaseQuantity(quantity);
75
+ } else {
76
+ this._items.push(new OrderItem(productId, price, quantity));
77
+ }
78
+ }
79
+
80
+ removeItem(productId: string): void {
81
+ if (this._status !== OrderStatus.PENDING) {
82
+ throw new Error('Cannot modify confirmed order');
83
+ }
84
+ const index = this._items.findIndex(i => i.productId === productId);
85
+ if (index === -1) {
86
+ throw new Error(`Item not found: ${productId}`);
87
+ }
88
+ this._items.splice(index, 1);
89
+ if (this._items.length === 0) {
90
+ throw new Error('Order must have at least one item');
91
+ }
92
+ }
93
+
94
+ get totalAmount(): Money {
95
+ return this._items.reduce(
96
+ (sum, item) => sum.add(item.subtotal),
97
+ Money.create(0, 'KRW'),
98
+ );
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Aggregate 간 ID 참조 패턴
104
+
105
+ ```typescript
106
+ // Bad - Aggregate 간 직접 객체 참조
107
+ class Order {
108
+ customer: Customer; // 다른 Aggregate를 직접 참조
109
+ items: OrderItem[];
110
+ }
111
+
112
+ class OrderItem {
113
+ product: Product; // 다른 Aggregate를 직접 참조
114
+ }
115
+
116
+ // Good - ID로만 참조
117
+ class Order {
118
+ private readonly _customerId: string; // ID 참조
119
+ private readonly _items: OrderItem[];
120
+ }
121
+
122
+ class OrderItem {
123
+ private readonly _productId: string; // ID 참조
124
+ }
125
+ ```
126
+
127
+ ### Aggregate 설계 원칙
128
+
129
+ | 원칙 | 설명 |
130
+ |------|------|
131
+ | 작게 유지 | 꼭 필요한 Entity만 포함. 크기가 커지면 분리 검토 |
132
+ | ID 참조 | Aggregate 간 객체 참조 대신 ID 참조 사용 |
133
+ | 하나의 트랜잭션 | 하나의 트랜잭션에서 하나의 Aggregate만 변경 |
134
+ | 불변식 보호 | 모든 상태 변경 시 비즈니스 규칙(불변식) 검증 |
135
+ | Root를 통한 접근 | 외부에서 내부 Entity에 직접 접근 불가 |
136
+
137
+ ---
138
+
139
+ ## 2. Repository 심화
140
+
141
+ ### Repository 인터페이스와 구현 분리
142
+
143
+ ```typescript
144
+ // domain/order/order.repository.ts (도메인 레이어 - 인터페이스만)
145
+ interface OrderRepository {
146
+ findById(id: string): Promise<Order | null>;
147
+ findByCustomerId(customerId: string): Promise<Order[]>;
148
+ save(order: Order): Promise<void>;
149
+ delete(id: string): Promise<void>;
150
+ }
151
+ ```
152
+
153
+ ### TypeORM 기반 구현체
154
+
155
+ ```typescript
156
+ // infrastructure/persistence/typeorm-order.repository.ts (인프라 레이어 - 구현)
157
+ @Injectable()
158
+ class TypeOrmOrderRepository implements OrderRepository {
159
+ constructor(
160
+ @InjectRepository(OrderEntity)
161
+ private readonly ormRepo: Repository<OrderEntity>,
162
+ ) {}
163
+
164
+ async findById(id: string): Promise<Order | null> {
165
+ const entity = await this.ormRepo.findOne({
166
+ where: { id },
167
+ relations: ['items'],
168
+ });
169
+ if (!entity) return null;
170
+ return OrderMapper.toDomain(entity);
171
+ }
172
+
173
+ async save(order: Order): Promise<void> {
174
+ const entity = OrderMapper.toEntity(order);
175
+ await this.ormRepo.save(entity);
176
+ }
177
+
178
+ async findByCustomerId(customerId: string): Promise<Order[]> {
179
+ const entities = await this.ormRepo.find({
180
+ where: { customerId },
181
+ relations: ['items'],
182
+ });
183
+ return entities.map(OrderMapper.toDomain);
184
+ }
185
+
186
+ async delete(id: string): Promise<void> {
187
+ await this.ormRepo.delete(id);
188
+ }
189
+ }
190
+ ```
191
+
192
+ ### Mapper 패턴
193
+
194
+ 도메인 모델과 ORM Entity 간 변환을 담당한다.
195
+
196
+ ```typescript
197
+ // infrastructure/persistence/order.mapper.ts
198
+ class OrderMapper {
199
+ static toDomain(entity: OrderOrmEntity): Order {
200
+ return new Order(entity.id, entity.status as OrderStatus);
201
+ }
202
+
203
+ static toEntity(domain: Order): OrderOrmEntity {
204
+ const entity = new OrderOrmEntity();
205
+ entity.id = domain.id;
206
+ entity.status = domain.status;
207
+ return entity;
208
+ }
209
+ }
210
+ ```
211
+
212
+ ### NestJS Module에서 DI 바인딩
213
+
214
+ ```typescript
215
+ // Module에서 DI 바인딩
216
+ @Module({
217
+ providers: [
218
+ {
219
+ provide: 'OrderRepository', // 또는 Symbol/abstract class 토큰
220
+ useClass: TypeOrmOrderRepository,
221
+ },
222
+ ],
223
+ })
224
+ class OrderModule {}
225
+ ```
226
+
227
+ ### Repository 설계 원칙
228
+
229
+ | 원칙 | 설명 |
230
+ |------|------|
231
+ | Aggregate Root 단위 | OrderItem용 Repository는 만들지 않는다 |
232
+ | 인터페이스 분리 | 도메인 레이어에 인터페이스, 인프라 레이어에 구현 |
233
+ | Mapper 사용 | 도메인 모델과 ORM Entity를 반드시 분리한다 |
234
+ | 의존성 역전 | Service는 인터페이스에 의존, 구현체는 DI로 주입 |
@@ -0,0 +1,202 @@
1
+ # Domain Service & Domain Event 심화
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. Domain Service 심화
8
+
9
+ ### 여러 Aggregate 간 계산 로직
10
+
11
+ Domain Service는 단일 Entity에 속하지 않는, 여러 Aggregate를 조율하는 도메인 로직을 담당한다.
12
+
13
+ ```typescript
14
+ // Bad - 특정 Entity에 속하는 로직을 Domain Service에 두기
15
+ class OrderDomainService {
16
+ // 이 로직은 Order Entity에 속해야 한다
17
+ cancelOrder(order: Order): void {
18
+ if (order.status !== OrderStatus.PENDING) {
19
+ throw new Error('Cannot cancel');
20
+ }
21
+ order.status = OrderStatus.CANCELLED;
22
+ }
23
+
24
+ // 이 로직도 Order Entity에 속해야 한다
25
+ calculateTotal(order: Order): number {
26
+ return order.items.reduce(
27
+ (sum, item) => sum + item.price * item.quantity,
28
+ 0,
29
+ );
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### 할인 계산 Domain Service
35
+
36
+ ```typescript
37
+ // Good - 여러 Aggregate(Order, Customer, Promotion)의 정보가 필요한 할인 계산
38
+ class PricingService {
39
+ calculateDiscount(
40
+ order: Order,
41
+ customerTier: CustomerTier,
42
+ promotions: Promotion[],
43
+ ): Money {
44
+ let discount = Money.create(0, 'KRW');
45
+
46
+ // 고객 등급별 할인
47
+ if (customerTier === CustomerTier.VIP) {
48
+ discount = discount.add(order.totalAmount.multiply(0.1));
49
+ }
50
+
51
+ // 프로모션 할인
52
+ for (const promo of promotions) {
53
+ if (promo.isApplicableTo(order)) {
54
+ discount = discount.add(promo.calculateDiscount(order.totalAmount));
55
+ }
56
+ }
57
+
58
+ return discount;
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### 이체 Domain Service
64
+
65
+ ```typescript
66
+ // Good - 두 Aggregate(Account) 간 이체 - 어느 한쪽에 속하지 않는 로직
67
+ class TransferService {
68
+ transfer(from: Account, to: Account, amount: Money): void {
69
+ if (!from.canWithdraw(amount)) {
70
+ throw new Error('Insufficient balance');
71
+ }
72
+ from.withdraw(amount);
73
+ to.deposit(amount);
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Domain Service vs Application Service 구분
79
+
80
+ | 구분 | Domain Service | Application Service |
81
+ |------|---------------|-------------------|
82
+ | 역할 | 순수 도메인 로직 | 유스케이스 흐름 조율 |
83
+ | 예시 | 할인 계산, 이체 규칙 | 트랜잭션, 이벤트 발행 |
84
+ | 상태 | stateless | stateless |
85
+ | 의존성 | 도메인 객체만 | Repository, EventPublisher 등 |
86
+ | 위치 | Domain 레이어 | Application 레이어 |
87
+
88
+ ---
89
+
90
+ ## 2. Domain Event 심화
91
+
92
+ ### AggregateRoot 기반 이벤트 수집
93
+
94
+ Aggregate에서 이벤트를 수집하는 기반 클래스를 정의한다.
95
+
96
+ ```typescript
97
+ // Good - AggregateRoot 기반 클래스
98
+ abstract class AggregateRoot {
99
+ private _domainEvents: DomainEvent[] = [];
100
+
101
+ get domainEvents(): ReadonlyArray<DomainEvent> {
102
+ return [...this._domainEvents];
103
+ }
104
+
105
+ protected addDomainEvent(event: DomainEvent): void {
106
+ this._domainEvents.push(event);
107
+ }
108
+
109
+ clearDomainEvents(): void {
110
+ this._domainEvents = [];
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Aggregate에서 이벤트 발생
116
+
117
+ ```typescript
118
+ // Good - Order Aggregate가 생성 시 이벤트를 수집
119
+ class Order extends AggregateRoot {
120
+ // ... 기존 필드 생략
121
+
122
+ static place(id: string, customerId: string, items: OrderItem[]): Order {
123
+ const order = new Order(id, customerId, items);
124
+ order.addDomainEvent(
125
+ new OrderPlacedEvent(
126
+ order.id,
127
+ order.customerId,
128
+ order.items.map(i => ({ productId: i.productId, quantity: i.quantity })),
129
+ order.totalAmount.amount,
130
+ ),
131
+ );
132
+ return order;
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### 이벤트 핸들러 독립 구현
138
+
139
+ 각 핸들러는 독립적으로 이벤트를 처리하며, 핸들러 간 순서 의존성이 없다.
140
+
141
+ ```typescript
142
+ // Good - 각 핸들러가 독립적으로 처리
143
+ class SendOrderConfirmationHandler {
144
+ constructor(private readonly emailService: EmailService) {}
145
+
146
+ async handle(event: OrderPlacedEvent): Promise<void> {
147
+ await this.emailService.sendOrderConfirmation(event.orderId);
148
+ }
149
+ }
150
+
151
+ class DecreaseStockHandler {
152
+ constructor(private readonly inventoryService: InventoryService) {}
153
+
154
+ async handle(event: OrderPlacedEvent): Promise<void> {
155
+ for (const item of event.items) {
156
+ await this.inventoryService.decrease(item.productId, item.quantity);
157
+ }
158
+ }
159
+ }
160
+
161
+ class AccumulatePointsHandler {
162
+ constructor(private readonly pointService: PointService) {}
163
+
164
+ async handle(event: OrderPlacedEvent): Promise<void> {
165
+ await this.pointService.accumulate(event.customerId, event.totalAmount);
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### Application Service에서의 조합
171
+
172
+ Application Service가 Repository 저장과 이벤트 발행을 조율한다.
173
+
174
+ ```typescript
175
+ // Good - Application Service가 유스케이스 흐름을 조율
176
+ class PlaceOrderUseCase {
177
+ constructor(
178
+ private readonly orderRepo: OrderRepository,
179
+ private readonly eventPublisher: DomainEventPublisher,
180
+ ) {}
181
+
182
+ async execute(command: PlaceOrderCommand): Promise<void> {
183
+ const order = Order.place(command.id, command.customerId, command.items);
184
+ await this.orderRepo.save(order);
185
+
186
+ // Aggregate에서 수집한 이벤트를 발행
187
+ await this.eventPublisher.publishAll(order.domainEvents);
188
+ order.clearDomainEvents();
189
+ }
190
+ }
191
+ ```
192
+
193
+ ### 이벤트 설계 원칙
194
+
195
+ | 원칙 | 설명 |
196
+ |------|------|
197
+ | 과거형 이름 | `OrderPlaced`, `PaymentCompleted` (not `PlaceOrder`) |
198
+ | 불변 객체 | 생성 후 변경하지 않는다 |
199
+ | 필요한 데이터만 | 이벤트에 Aggregate 전체를 넣지 않고, 핸들러가 필요한 최소 데이터만 포함 |
200
+ | 발생 시각 포함 | `occurredAt` 필드로 이벤트 발생 시각을 기록 |
201
+ | 순서 비의존 | 핸들러 간 실행 순서에 의존하지 않는다 |
202
+ | Aggregate에서 수집 | `addDomainEvent`로 수집, Application 레이어에서 `publishAll`로 발행 |
@@ -0,0 +1,225 @@
1
+ # Entity & Value Object 심화
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. Entity 심화
8
+
9
+ ### Value Object를 활용한 Entity 강화
10
+
11
+ Entity 내부에서 Value Object를 사용하여 도메인 개념을 명확히 표현한다.
12
+
13
+ ```typescript
14
+ // Good - Entity가 Value Object를 내부적으로 활용
15
+ class Order {
16
+ private readonly _id: string;
17
+ private _status: OrderStatus;
18
+ private readonly _items: OrderItem[];
19
+
20
+ constructor(id: string, items: OrderItem[]) {
21
+ if (items.length === 0) {
22
+ throw new Error('Order must have at least one item');
23
+ }
24
+ this._id = id;
25
+ this._status = OrderStatus.PENDING;
26
+ this._items = items;
27
+ }
28
+
29
+ get id(): string { return this._id; }
30
+ get status(): OrderStatus { return this._status; }
31
+ get items(): ReadonlyArray<OrderItem> { return [...this._items]; }
32
+
33
+ get totalAmount(): Money {
34
+ return this._items.reduce(
35
+ (sum, item) => sum.add(item.subtotal),
36
+ Money.create(0, 'KRW'),
37
+ );
38
+ }
39
+
40
+ cancel(): void {
41
+ if (this._status !== OrderStatus.PENDING) {
42
+ throw new Error('Only pending orders can be cancelled');
43
+ }
44
+ this._status = OrderStatus.CANCELLED;
45
+ }
46
+
47
+ confirm(): void {
48
+ if (this._status !== OrderStatus.PENDING) {
49
+ throw new Error('Only pending orders can be confirmed');
50
+ }
51
+ this._status = OrderStatus.CONFIRMED;
52
+ }
53
+
54
+ equals(other: Order): boolean {
55
+ return this._id === other._id;
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### equals 메서드 패턴
61
+
62
+ Entity의 동등성 비교는 반드시 id 기반으로 한다.
63
+
64
+ ```typescript
65
+ // Bad - 모든 속성 비교
66
+ class User {
67
+ equals(other: User): boolean {
68
+ return this.name === other.name && this.email === other.email;
69
+ }
70
+ }
71
+
72
+ // Good - id 기반 비교
73
+ class User {
74
+ private readonly _id: string;
75
+
76
+ equals(other: User): boolean {
77
+ return this._id === other._id;
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### 상태 전이(State Transition) 패턴
83
+
84
+ Entity의 상태 변경은 명시적인 메서드로 표현하고, 허용되지 않는 전이를 방어한다.
85
+
86
+ ```typescript
87
+ // Good - 상태 전이를 명시적으로 관리
88
+ class Order {
89
+ private _status: OrderStatus;
90
+
91
+ // 허용 전이: PENDING -> CONFIRMED -> SHIPPED -> DELIVERED
92
+ // PENDING -> CANCELLED
93
+
94
+ confirm(): void {
95
+ this.assertStatus(OrderStatus.PENDING, 'confirm');
96
+ this._status = OrderStatus.CONFIRMED;
97
+ }
98
+
99
+ ship(): void {
100
+ this.assertStatus(OrderStatus.CONFIRMED, 'ship');
101
+ this._status = OrderStatus.SHIPPED;
102
+ }
103
+
104
+ deliver(): void {
105
+ this.assertStatus(OrderStatus.SHIPPED, 'deliver');
106
+ this._status = OrderStatus.DELIVERED;
107
+ }
108
+
109
+ cancel(): void {
110
+ if (this._status !== OrderStatus.PENDING) {
111
+ throw new Error('Only pending orders can be cancelled');
112
+ }
113
+ this._status = OrderStatus.CANCELLED;
114
+ }
115
+
116
+ private assertStatus(expected: OrderStatus, action: string): void {
117
+ if (this._status !== expected) {
118
+ throw new Error(
119
+ `Cannot ${action} order in ${this._status} status (expected: ${expected})`,
120
+ );
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## 2. Value Object 심화
129
+
130
+ ### Money 패턴 전체 구현
131
+
132
+ Money는 대표적인 Value Object로, 금액과 통화를 함께 관리한다.
133
+
134
+ ```typescript
135
+ // Good - Money Value Object 전체 구현
136
+ class Money {
137
+ private constructor(
138
+ private readonly _amount: number,
139
+ private readonly _currency: string,
140
+ ) {}
141
+
142
+ static create(amount: number, currency: string): Money {
143
+ if (amount < 0) {
144
+ throw new Error('Amount cannot be negative');
145
+ }
146
+ if (!['KRW', 'USD', 'EUR'].includes(currency)) {
147
+ throw new Error(`Unsupported currency: ${currency}`);
148
+ }
149
+ return new Money(amount, currency);
150
+ }
151
+
152
+ get amount(): number { return this._amount; }
153
+ get currency(): string { return this._currency; }
154
+
155
+ add(other: Money): Money {
156
+ if (this._currency !== other._currency) {
157
+ throw new Error('Cannot add different currencies');
158
+ }
159
+ return Money.create(this._amount + other._amount, this._currency);
160
+ }
161
+
162
+ multiply(factor: number): Money {
163
+ return Money.create(this._amount * factor, this._currency);
164
+ }
165
+
166
+ equals(other: Money): boolean {
167
+ return this._amount === other._amount && this._currency === other._currency;
168
+ }
169
+ }
170
+ ```
171
+
172
+ ### 복합 Value Object
173
+
174
+ 여러 속성을 가진 Value Object도 동일한 패턴을 따른다.
175
+
176
+ ```typescript
177
+ // Good - 복합 Value Object (주소)
178
+ class Address {
179
+ private constructor(
180
+ private readonly _street: string,
181
+ private readonly _city: string,
182
+ private readonly _zipCode: string,
183
+ private readonly _country: string,
184
+ ) {}
185
+
186
+ static create(street: string, city: string, zipCode: string, country: string): Address {
187
+ if (!street || !city || !zipCode || !country) {
188
+ throw new Error('All address fields are required');
189
+ }
190
+ if (!/^\d{5}$/.test(zipCode)) {
191
+ throw new Error(`Invalid zip code: ${zipCode}`);
192
+ }
193
+ return new Address(street, city, zipCode, country);
194
+ }
195
+
196
+ get street(): string { return this._street; }
197
+ get city(): string { return this._city; }
198
+ get zipCode(): string { return this._zipCode; }
199
+ get country(): string { return this._country; }
200
+
201
+ equals(other: Address): boolean {
202
+ return (
203
+ this._street === other._street &&
204
+ this._city === other._city &&
205
+ this._zipCode === other._zipCode &&
206
+ this._country === other._country
207
+ );
208
+ }
209
+
210
+ // 상태 변경 시 새 인스턴스 반환 (불변 유지)
211
+ changeStreet(newStreet: string): Address {
212
+ return Address.create(newStreet, this._city, this._zipCode, this._country);
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Value Object vs 원시값 판단 기준
218
+
219
+ | 기준 | 원시값 사용 | Value Object 사용 |
220
+ |------|------------|-------------------|
221
+ | 유효성 검증 | 필요 없음 | 생성 시 검증 필요 |
222
+ | 도메인 연산 | 없음 | add, multiply 등 존재 |
223
+ | 포맷/정규화 | 불필요 | trim, lowercase 등 필요 |
224
+ | 여러 속성 조합 | 단일 값 | 복합 값 (Money = amount + currency) |
225
+ | 비즈니스 의미 | 범용적 | 도메인 특화 의미 |
@@ -0,0 +1,44 @@
1
+ # Planning Skill
2
+
3
+ 작업 계획 수립 시 참조하는 체크리스트와 템플릿.
4
+
5
+ ---
6
+
7
+ ## 1. 티어 판단
8
+
9
+ 다음 질문에 순서대로 답하라:
10
+
11
+ 1. **변경 영향도**: 핵심 도메인 로직이 바뀌는가? → Yes면 L
12
+ 2. **레이어 횡단**: FE + BE 동시 변경인가? → 영향도에 따라 M 또는 L
13
+ 3. **설계 결정**: 새로운 아키텍처/패턴 도입이 필요한가? → Yes면 L
14
+ 4. **위 모두 No**: 파일 수와 변경 단순성으로 S/M 판단
15
+
16
+ ## 2. 작업 분해 템플릿
17
+
18
+ ### 요구사항 정리
19
+ - [ ] 사용자가 원하는 최종 결과물은?
20
+ - [ ] 변경되는 레이어는? (UI / API / Domain / DB)
21
+ - [ ] 기존 코드에 영향을 주는 범위는?
22
+
23
+ ### 작업 단위 분해
24
+ 각 작업 단위는 **독립적으로 테스트 가능한 단위**로 나눈다:
25
+ ```
26
+ 1. [작업명] - [대상 파일/모듈] - [티어]
27
+ 2. [작업명] - [대상 파일/모듈] - [티어]
28
+ ```
29
+
30
+ ### 의존성 확인
31
+ - [ ] 작업 간 순서 의존성이 있는가? (예: BE API 먼저 → FE 연동)
32
+ - [ ] 외부 의존성이 있는가? (패키지 설치, DB 마이그레이션 등)
33
+
34
+ ## 3. 계획 출력 형식
35
+
36
+ 사용자에게 계획을 제시할 때 다음 형식을 따른다:
37
+
38
+ ```
39
+ **티어**: S / M / L
40
+ **변경 범위**: [레이어 목록]
41
+ **작업 목록**:
42
+ 1. [작업] → [담당 에이전트]
43
+ 2. [작업] → [담당 에이전트]
44
+ ```