@choblue/claude-code-toolkit 1.1.2 → 1.1.6

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
+ | 비즈니스 의미 | 범용적 | 도메인 특화 의미 |
@@ -26,11 +26,23 @@ React 규칙은 `../React/SKILL.md`, 공통 원칙은 `../Coding/SKILL.md`를
26
26
 
27
27
  ### 클래스 조합
28
28
  - 관련 있는 클래스를 논리적 그룹으로 정렬한다
29
- - 권장 순서: 레이아웃 -> 크기 -> 간격 -> 타이포그래피 -> 색상 -> 효과
29
+ - 권장 순서: 레이아웃 크기 간격 타이포그래피 색상 효과
30
+ - **클래스가 길어지면 관심사별로 줄바꿈하여 가독성을 높인다**
30
31
 
31
32
  ```typescript
32
- // Good - 논리적 순서로 정렬
33
+ // Bad - 줄로 길게 나열 (화면 벗어남, 어떤 CSS가 있는지 파악 어려움)
33
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
+ >
34
46
  버튼
35
47
  </button>
36
48
  ```
@@ -227,7 +239,8 @@ export function Button({ variant = 'primary', size = 'md', className, children }
227
239
  <button
228
240
  className={cn(
229
241
  // 기본 스타일
230
- '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',
231
244
  // variant
232
245
  {
233
246
  'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
@@ -261,7 +274,12 @@ import { cn } from '@/lib/utils';
261
274
 
262
275
  const buttonVariants = cva(
263
276
  // 기본 스타일
264
- '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
+ ],
265
283
  {
266
284
  variants: {
267
285
  variant: {
@@ -370,4 +388,4 @@ const colorClasses = {
370
388
  } as const;
371
389
 
372
390
  <div className={colorClasses[color]}>
373
- ```
391
+ ```