@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
@@ -0,0 +1,76 @@
1
+ # TypeORM Transactions - 트랜잭션 패턴 및 사용 가이드
2
+
3
+ > 이 문서는 `../SKILL.md`의 참조 문서이다.
4
+
5
+ ---
6
+
7
+ ## 1. DataSource.transaction() 패턴
8
+
9
+ 가장 간결한 트랜잭션 패턴이다. 콜백 내에서 제공되는 `EntityManager`를 사용한다.
10
+
11
+ ```typescript
12
+ @Injectable()
13
+ export class OrderService {
14
+ constructor(private readonly dataSource: DataSource) {}
15
+
16
+ async createOrder(dto: CreateOrderDto): Promise<Order> {
17
+ return this.dataSource.transaction(async (manager) => {
18
+ const order = manager.create(Order, {
19
+ userId: dto.userId,
20
+ totalAmount: dto.totalAmount,
21
+ });
22
+ await manager.save(order);
23
+
24
+ const orderItems = dto.items.map((item) =>
25
+ manager.create(OrderItem, { ...item, orderId: order.id }),
26
+ );
27
+ await manager.save(orderItems);
28
+
29
+ await manager.decrement(
30
+ Product,
31
+ { id: In(dto.items.map((i) => i.productId)) },
32
+ 'stock',
33
+ 1,
34
+ );
35
+
36
+ return order;
37
+ });
38
+ }
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 2. QueryRunner를 사용한 수동 트랜잭션
45
+
46
+ 세밀한 제어가 필요한 경우 사용한다.
47
+
48
+ ```typescript
49
+ async transferPoints(fromId: string, toId: string, amount: number): Promise<void> {
50
+ const queryRunner = this.dataSource.createQueryRunner();
51
+ await queryRunner.connect();
52
+ await queryRunner.startTransaction();
53
+
54
+ try {
55
+ await queryRunner.manager.decrement(User, { id: fromId }, 'points', amount);
56
+ await queryRunner.manager.increment(User, { id: toId }, 'points', amount);
57
+
58
+ await queryRunner.commitTransaction();
59
+ } catch (error) {
60
+ await queryRunner.rollbackTransaction();
61
+ throw error;
62
+ } finally {
63
+ await queryRunner.release(); // 반드시 release
64
+ }
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 3. 트랜잭션 선택 가이드
71
+
72
+ | 상황 | 권장 패턴 |
73
+ |------|----------|
74
+ | 단순 다중 저장 | `DataSource.transaction()` |
75
+ | 조건부 커밋/롤백 | `QueryRunner` 수동 트랜잭션 |
76
+ | 중간에 외부 API 호출 필요 | `QueryRunner` (커밋 시점 제어) |
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: typescript
3
+ description: TypeScript 고급 패턴 가이드. 타입 추론, 유틸리티 타입, 제네릭, 타입 가드, 고급 타입 패턴 등 TypeScript 코드 작성 시 참조한다.
4
+ ---
5
+
1
6
  # TypeScript Skill - 고급 패턴 규칙
2
7
 
3
8
  FE/BE 공통으로 적용되는 TypeScript 심화 규칙을 정의한다.
@@ -128,198 +133,7 @@ type UsersResult = Awaited<ReturnType<typeof getUsers>>; // User[]
128
133
 
129
134
  ---
130
135
 
131
- ## 3. 제네릭
132
-
133
- ### 함수 제네릭
134
-
135
- ```typescript
136
- // Bad - any 사용
137
- function first(arr: any[]): any {
138
- return arr[0];
139
- }
140
-
141
- // Good - 제네릭으로 타입 안전성 확보
142
- function first<T>(arr: T[]): T | undefined {
143
- return arr[0];
144
- }
145
-
146
- const num = first([1, 2, 3]); // number | undefined
147
- const str = first(['a', 'b']); // string | undefined
148
- ```
149
-
150
- ### 제약 조건 (extends)
151
-
152
- ```typescript
153
- // Bad - 모든 타입 허용
154
- function getProperty<T>(obj: T, key: string): unknown {
155
- return (obj as Record<string, unknown>)[key];
156
- }
157
-
158
- // Good - 제약 조건으로 타입 안전성 확보
159
- function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
160
- return obj[key];
161
- }
162
-
163
- const user = { name: 'Alice', age: 30 };
164
- const name = getProperty(user, 'name'); // string
165
- // getProperty(user, 'invalid'); // 컴파일 에러
166
- ```
167
-
168
- ### 제네릭 기본값
169
-
170
- ```typescript
171
- interface PaginatedResponse<T, M = Record<string, never>> {
172
- data: T[];
173
- total: number;
174
- page: number;
175
- limit: number;
176
- meta: M;
177
- }
178
-
179
- // 메타 정보 없이 사용
180
- type UserListResponse = PaginatedResponse<User>;
181
-
182
- // 메타 정보와 함께 사용
183
- type SearchResponse = PaginatedResponse<User, { query: string; took: number }>;
184
- ```
185
-
186
- ### 실전 예시 - API 응답 래퍼
187
-
188
- ```typescript
189
- // API 응답 공통 래퍼
190
- interface ApiResponse<T> {
191
- success: boolean;
192
- data: T;
193
- error: string | null;
194
- timestamp: number;
195
- }
196
-
197
- // 페이지네이션 래퍼
198
- interface PaginatedData<T> {
199
- items: T[];
200
- total: number;
201
- page: number;
202
- pageSize: number;
203
- hasNext: boolean;
204
- }
205
-
206
- // 조합하여 사용
207
- type UserListApiResponse = ApiResponse<PaginatedData<User>>;
208
-
209
- // 제네릭 API 호출 함수
210
- async function apiGet<T>(url: string): Promise<ApiResponse<T>> {
211
- const response = await fetch(url);
212
- return response.json() as Promise<ApiResponse<T>>;
213
- }
214
-
215
- const result = await apiGet<User[]>('/api/users');
216
- // result.data는 User[]로 타입 추론
217
- ```
218
-
219
- ---
220
-
221
- ## 4. 타입 가드
222
-
223
- ### is 키워드 (사용자 정의 타입 가드)
224
-
225
- ```typescript
226
- interface Admin {
227
- role: 'admin';
228
- permissions: string[];
229
- }
230
-
231
- interface Guest {
232
- role: 'guest';
233
- }
234
-
235
- type AppUser = Admin | Guest;
236
-
237
- // Bad - 타입 단언
238
- function getPermissions(user: AppUser): string[] {
239
- return (user as Admin).permissions ?? [];
240
- }
241
-
242
- // Good - 타입 가드
243
- function isAdmin(user: AppUser): user is Admin {
244
- return user.role === 'admin';
245
- }
246
-
247
- function getPermissions(user: AppUser): string[] {
248
- if (isAdmin(user)) {
249
- return user.permissions; // Admin으로 좁혀짐
250
- }
251
- return [];
252
- }
253
- ```
254
-
255
- ### in 연산자
256
-
257
- ```typescript
258
- interface Dog {
259
- bark: () => void;
260
- }
261
-
262
- interface Cat {
263
- meow: () => void;
264
- }
265
-
266
- type Pet = Dog | Cat;
267
-
268
- function makeSound(pet: Pet): void {
269
- if ('bark' in pet) {
270
- pet.bark(); // Dog으로 좁혀짐
271
- } else {
272
- pet.meow(); // Cat으로 좁혀짐
273
- }
274
- }
275
- ```
276
-
277
- ### Discriminated Union (태그드 유니언)
278
-
279
- ```typescript
280
- // 공통 판별 필드(type)를 가진 유니언
281
- type Shape =
282
- | { type: 'circle'; radius: number }
283
- | { type: 'rectangle'; width: number; height: number }
284
- | { type: 'triangle'; base: number; height: number };
285
-
286
- function calculateArea(shape: Shape): number {
287
- switch (shape.type) {
288
- case 'circle':
289
- return Math.PI * shape.radius ** 2;
290
- case 'rectangle':
291
- return shape.width * shape.height;
292
- case 'triangle':
293
- return (shape.base * shape.height) / 2;
294
- }
295
- }
296
- ```
297
-
298
- ### Exhaustive Check (never를 이용한 완전성 검사)
299
-
300
- ```typescript
301
- // 모든 케이스를 처리했는지 컴파일 타임에 검증한다
302
- function assertNever(value: never): never {
303
- throw new Error(`Unexpected value: ${value}`);
304
- }
305
-
306
- function getShapeLabel(shape: Shape): string {
307
- switch (shape.type) {
308
- case 'circle':
309
- return '원';
310
- case 'rectangle':
311
- return '직사각형';
312
- case 'triangle':
313
- return '삼각형';
314
- default:
315
- return assertNever(shape); // 새로운 Shape 추가 시 컴파일 에러 발생
316
- }
317
- }
318
- ```
319
-
320
- ---
321
-
322
- ## 5. 타입 설계 원칙
136
+ ## 3. 타입 설계 원칙
323
137
 
324
138
  ### 유니언 > enum
325
139
 
@@ -391,39 +205,9 @@ type DeepReadonly<T> = {
391
205
  };
392
206
  ```
393
207
 
394
- ### Branded Type (같은 원시 타입이지만 구분해야 할 때)
395
-
396
- ```typescript
397
- // Bad - UserId와 PostId가 모두 string이라 실수로 혼용 가능
398
- function getPost(postId: string): Post { /* ... */ }
399
- getPost(userId); // 컴파일 에러 없음 (런타임 버그)
400
-
401
- // Good - Branded Type으로 구분
402
- type Brand<T, B extends string> = T & { readonly __brand: B };
403
-
404
- type UserId = Brand<string, 'UserId'>;
405
- type PostId = Brand<string, 'PostId'>;
406
-
407
- function createUserId(id: string): UserId {
408
- return id as UserId;
409
- }
410
-
411
- function createPostId(id: string): PostId {
412
- return id as PostId;
413
- }
414
-
415
- function getPost(postId: PostId): Post { /* ... */ }
416
-
417
- const userId = createUserId('user-1');
418
- const postId = createPostId('post-1');
419
-
420
- getPost(postId); // OK
421
- // getPost(userId); // 컴파일 에러 - UserId는 PostId에 할당 불가
422
- ```
423
-
424
208
  ---
425
209
 
426
- ## 6. 비동기 타입
210
+ ## 4. 비동기 타입
427
211
 
428
212
  ### Promise<T>와 async/await 반환 타입
429
213
 
@@ -503,7 +287,15 @@ async function fetchDashboard(userId: string): Promise<{
503
287
 
504
288
  ---
505
289
 
506
- ## 7. 금지 사항
290
+ ## 참조 문서
291
+
292
+ - **[Generics](./references/generics.md)** - 제네릭 함수, 제약 조건, 고급 제네릭 패턴
293
+ - **[Type Guards](./references/type-guards.md)** - 타입 가드, 판별 유니온, 완전성 검사
294
+ - **[Advanced Patterns](./references/advanced-patterns.md)** - 조건부 타입, 매핑 타입, 템플릿 리터럴 타입
295
+
296
+ ---
297
+
298
+ ## 5. 금지 사항
507
299
 
508
300
  - `any` 사용 금지 - `unknown`을 사용한 후 타입 가드로 좁힌다
509
301
  - `as` 타입 단언 남용 금지 - 타입 가드 또는 올바른 타입 설계로 해결한다 (Branded Type 등 불가피한 경우 제외)
@@ -525,4 +317,4 @@ const data: unknown = response.body;
525
317
  if (isUser(data)) {
526
318
  const name = data.name; // 타입 가드로 안전하게 접근
527
319
  }
528
- ```
320
+ ```
@@ -0,0 +1,146 @@
1
+ # 고급 타입 패턴 (Advanced Patterns)
2
+
3
+ 이 문서는 TypeScript 고급 타입 패턴을 다룬다.
4
+ 기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
5
+
6
+ ---
7
+
8
+ ## Branded Type (같은 원시 타입이지만 구분해야 할 때)
9
+
10
+ ```typescript
11
+ // Bad - UserId와 PostId가 모두 string이라 실수로 혼용 가능
12
+ function getPost(postId: string): Post { /* ... */ }
13
+ getPost(userId); // 컴파일 에러 없음 (런타임 버그)
14
+
15
+ // Good - Branded Type으로 구분
16
+ type Brand<T, B extends string> = T & { readonly __brand: B };
17
+
18
+ type UserId = Brand<string, 'UserId'>;
19
+ type PostId = Brand<string, 'PostId'>;
20
+
21
+ function createUserId(id: string): UserId {
22
+ return id as UserId;
23
+ }
24
+
25
+ function createPostId(id: string): PostId {
26
+ return id as PostId;
27
+ }
28
+
29
+ function getPost(postId: PostId): Post { /* ... */ }
30
+
31
+ const userId = createUserId('user-1');
32
+ const postId = createPostId('post-1');
33
+
34
+ getPost(postId); // OK
35
+ // getPost(userId); // 컴파일 에러 - UserId는 PostId에 할당 불가
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 조건부 타입 (Conditional Types)
41
+
42
+ ```typescript
43
+ // 기본 조건부 타입
44
+ type IsString<T> = T extends string ? true : false;
45
+
46
+ type A = IsString<string>; // true
47
+ type B = IsString<number>; // false
48
+
49
+ // 분배 조건부 타입 (유니언에 자동 분배)
50
+ type NonNullable<T> = T extends null | undefined ? never : T;
51
+ type Result = NonNullable<string | null | undefined>; // string
52
+
53
+ // infer를 이용한 타입 추출
54
+ type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
55
+ type Resolved = UnwrapPromise<Promise<string>>; // string
56
+
57
+ // 배열 요소 타입 추출
58
+ type ElementType<T> = T extends (infer E)[] ? E : T;
59
+ type Item = ElementType<string[]>; // string
60
+
61
+ // 함수 반환 타입 추출 (ReturnType 구현)
62
+ type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 매핑 타입 (Mapped Types)
68
+
69
+ ```typescript
70
+ // 기본 매핑 타입
71
+ type Readonly<T> = {
72
+ readonly [P in keyof T]: T[P];
73
+ };
74
+
75
+ type Partial<T> = {
76
+ [P in keyof T]?: T[P];
77
+ };
78
+
79
+ // 키 리매핑 (as 절)
80
+ type Getters<T> = {
81
+ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
82
+ };
83
+
84
+ interface Person {
85
+ name: string;
86
+ age: number;
87
+ }
88
+
89
+ type PersonGetters = Getters<Person>;
90
+ // { getName: () => string; getAge: () => number; }
91
+
92
+ // 특정 타입의 키만 필터링
93
+ type StringKeys<T> = {
94
+ [K in keyof T as T[K] extends string ? K : never]: T[K];
95
+ };
96
+
97
+ type PersonStringKeys = StringKeys<Person>;
98
+ // { name: string; }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 템플릿 리터럴 타입 (Template Literal Types)
104
+
105
+ ```typescript
106
+ // 기본 사용
107
+ type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
108
+ // 'onClick' | 'onFocus' | 'onBlur'
109
+
110
+ // CSS 단위
111
+ type CSSValue = `${number}${'px' | 'rem' | 'em' | '%'}`;
112
+ const width: CSSValue = '100px'; // OK
113
+ // const invalid: CSSValue = '100vw'; // 컴파일 에러
114
+
115
+ // API 경로 타입
116
+ type ApiPath = `/api/${'users' | 'posts' | 'comments'}`;
117
+ type ApiPathWithId = `${ApiPath}/${string}`;
118
+
119
+ // 조합 활용
120
+ type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
121
+ type Endpoint = `${HTTPMethod} ${ApiPath}`;
122
+ // 'GET /api/users' | 'GET /api/posts' | ... (12개 조합)
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 깊은 불변 타입 (Deep Readonly)
128
+
129
+ ```typescript
130
+ type DeepReadonly<T> = {
131
+ readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
132
+ };
133
+
134
+ interface NestedConfig {
135
+ database: {
136
+ host: string;
137
+ port: number;
138
+ };
139
+ cache: {
140
+ ttl: number;
141
+ };
142
+ }
143
+
144
+ type FrozenConfig = DeepReadonly<NestedConfig>;
145
+ // database.host, database.port, cache.ttl 모두 readonly
146
+ ```
@@ -0,0 +1,98 @@
1
+ # 제네릭 (Generics)
2
+
3
+ 이 문서는 TypeScript 제네릭의 상세 패턴을 다룬다.
4
+ 기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
5
+
6
+ ---
7
+
8
+ ## 함수 제네릭
9
+
10
+ ```typescript
11
+ // Bad - any 사용
12
+ function first(arr: any[]): any {
13
+ return arr[0];
14
+ }
15
+
16
+ // Good - 제네릭으로 타입 안전성 확보
17
+ function first<T>(arr: T[]): T | undefined {
18
+ return arr[0];
19
+ }
20
+
21
+ const num = first([1, 2, 3]); // number | undefined
22
+ const str = first(['a', 'b']); // string | undefined
23
+ ```
24
+
25
+ ---
26
+
27
+ ## 제약 조건 (extends)
28
+
29
+ ```typescript
30
+ // Bad - 모든 타입 허용
31
+ function getProperty<T>(obj: T, key: string): unknown {
32
+ return (obj as Record<string, unknown>)[key];
33
+ }
34
+
35
+ // Good - 제약 조건으로 타입 안전성 확보
36
+ function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
37
+ return obj[key];
38
+ }
39
+
40
+ const user = { name: 'Alice', age: 30 };
41
+ const name = getProperty(user, 'name'); // string
42
+ // getProperty(user, 'invalid'); // 컴파일 에러
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 제네릭 기본값
48
+
49
+ ```typescript
50
+ interface PaginatedResponse<T, M = Record<string, never>> {
51
+ data: T[];
52
+ total: number;
53
+ page: number;
54
+ limit: number;
55
+ meta: M;
56
+ }
57
+
58
+ // 메타 정보 없이 사용
59
+ type UserListResponse = PaginatedResponse<User>;
60
+
61
+ // 메타 정보와 함께 사용
62
+ type SearchResponse = PaginatedResponse<User, { query: string; took: number }>;
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 실전 예시 - API 응답 래퍼
68
+
69
+ ```typescript
70
+ // API 응답 공통 래퍼
71
+ interface ApiResponse<T> {
72
+ success: boolean;
73
+ data: T;
74
+ error: string | null;
75
+ timestamp: number;
76
+ }
77
+
78
+ // 페이지네이션 래퍼
79
+ interface PaginatedData<T> {
80
+ items: T[];
81
+ total: number;
82
+ page: number;
83
+ pageSize: number;
84
+ hasNext: boolean;
85
+ }
86
+
87
+ // 조합하여 사용
88
+ type UserListApiResponse = ApiResponse<PaginatedData<User>>;
89
+
90
+ // 제네릭 API 호출 함수
91
+ async function apiGet<T>(url: string): Promise<ApiResponse<T>> {
92
+ const response = await fetch(url);
93
+ return response.json() as Promise<ApiResponse<T>>;
94
+ }
95
+
96
+ const result = await apiGet<User[]>('/api/users');
97
+ // result.data는 User[]로 타입 추론
98
+ ```
@@ -0,0 +1,109 @@
1
+ # 타입 가드 (Type Guards)
2
+
3
+ 이 문서는 TypeScript 타입 가드의 상세 패턴을 다룬다.
4
+ 기본 규칙은 [SKILL.md](../SKILL.md)를 참고한다.
5
+
6
+ ---
7
+
8
+ ## is 키워드 (사용자 정의 타입 가드)
9
+
10
+ ```typescript
11
+ interface Admin {
12
+ role: 'admin';
13
+ permissions: string[];
14
+ }
15
+
16
+ interface Guest {
17
+ role: 'guest';
18
+ }
19
+
20
+ type AppUser = Admin | Guest;
21
+
22
+ // Bad - 타입 단언
23
+ function getPermissions(user: AppUser): string[] {
24
+ return (user as Admin).permissions ?? [];
25
+ }
26
+
27
+ // Good - 타입 가드
28
+ function isAdmin(user: AppUser): user is Admin {
29
+ return user.role === 'admin';
30
+ }
31
+
32
+ function getPermissions(user: AppUser): string[] {
33
+ if (isAdmin(user)) {
34
+ return user.permissions; // Admin으로 좁혀짐
35
+ }
36
+ return [];
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ ## in 연산자
43
+
44
+ ```typescript
45
+ interface Dog {
46
+ bark: () => void;
47
+ }
48
+
49
+ interface Cat {
50
+ meow: () => void;
51
+ }
52
+
53
+ type Pet = Dog | Cat;
54
+
55
+ function makeSound(pet: Pet): void {
56
+ if ('bark' in pet) {
57
+ pet.bark(); // Dog으로 좁혀짐
58
+ } else {
59
+ pet.meow(); // Cat으로 좁혀짐
60
+ }
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Discriminated Union (태그드 유니언)
67
+
68
+ ```typescript
69
+ // 공통 판별 필드(type)를 가진 유니언
70
+ type Shape =
71
+ | { type: 'circle'; radius: number }
72
+ | { type: 'rectangle'; width: number; height: number }
73
+ | { type: 'triangle'; base: number; height: number };
74
+
75
+ function calculateArea(shape: Shape): number {
76
+ switch (shape.type) {
77
+ case 'circle':
78
+ return Math.PI * shape.radius ** 2;
79
+ case 'rectangle':
80
+ return shape.width * shape.height;
81
+ case 'triangle':
82
+ return (shape.base * shape.height) / 2;
83
+ }
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Exhaustive Check (never를 이용한 완전성 검사)
90
+
91
+ ```typescript
92
+ // 모든 케이스를 처리했는지 컴파일 타임에 검증한다
93
+ function assertNever(value: never): never {
94
+ throw new Error(`Unexpected value: ${value}`);
95
+ }
96
+
97
+ function getShapeLabel(shape: Shape): string {
98
+ switch (shape.type) {
99
+ case 'circle':
100
+ return '원';
101
+ case 'rectangle':
102
+ return '직사각형';
103
+ case 'triangle':
104
+ return '삼각형';
105
+ default:
106
+ return assertNever(shape); // 새로운 Shape 추가 시 컴파일 에러 발생
107
+ }
108
+ }
109
+ ```
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: zustand
3
+ description: Zustand 클라이언트 상태 관리 가이드. 스토어 설계, Selector 패턴, 미들웨어(persist, devtools, immer), Slice 패턴 등 클라이언트 상태 관리 시 참조한다.
4
+ ---
5
+
1
6
  # Zustand Skill - 클라이언트 상태 관리 규칙
2
7
 
3
8
  Zustand를 사용한 클라이언트 상태 관리 규칙을 정의한다.