@choblue/claude-code-toolkit 1.2.5 → 1.2.7
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.
- package/.claude/.project-map-cache +1 -1
- package/.claude/CLAUDE.md +80 -18
- package/.claude/hooks/prompt-hook.sh +4 -4
- package/.claude/prompts/feature.md +11 -0
- package/.claude/prompts/fix.md +11 -0
- package/.claude/prompts/review.md +7 -0
- package/.claude/skills/Coding/SKILL.md +5 -4
- package/.claude/skills/Curation/SKILL.md +36 -0
- package/.claude/skills/FailureRecovery/SKILL.md +47 -0
- package/.claude/skills/{Coding/backend.md → NestJS/SKILL.md} +7 -2
- package/.claude/skills/NextJS/SKILL.md +13 -303
- package/.claude/skills/NextJS/references/data-fetching.md +96 -0
- package/.claude/skills/NextJS/references/middleware-actions.md +74 -0
- package/.claude/skills/NextJS/references/optimization.md +127 -0
- package/.claude/skills/Planning/SKILL.md +30 -7
- package/.claude/skills/React/SKILL.md +25 -287
- package/.claude/skills/React/references/a11y-ux.md +134 -0
- package/.claude/skills/React/references/rendering-patterns.md +62 -0
- package/.claude/skills/React/references/state-hooks.md +73 -0
- package/.claude/skills/ReactHookForm/SKILL.md +8 -196
- package/.claude/skills/ReactHookForm/references/advanced-patterns.md +193 -0
- package/.claude/skills/TDD/SKILL.md +2 -2
- package/.claude/skills/TailwindCSS/SKILL.md +14 -240
- package/.claude/skills/TailwindCSS/references/patterns-components.md +93 -0
- package/.claude/skills/TailwindCSS/references/responsive-dark.md +102 -0
- package/.claude/skills/TailwindCSS/references/transitions.md +33 -0
- package/README.md +25 -12
- package/package.json +1 -1
- package/.claude/skills/Coding/frontend.md +0 -11
- /package/.claude/skills/TDD/{backend.md → references/backend.md} +0 -0
- /package/.claude/skills/TDD/{frontend.md → references/frontend.md} +0 -0
|
@@ -107,201 +107,7 @@ function CreateUserForm() {
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
-
## 4.
|
|
111
|
-
|
|
112
|
-
`register`를 사용할 수 없는 제어 컴포넌트 (Select, DatePicker, 커스텀 UI 라이브러리 등)에 사용한다.
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { Controller, useForm } from 'react-hook-form';
|
|
116
|
-
|
|
117
|
-
function UserForm() {
|
|
118
|
-
const { control, handleSubmit } = useForm<UserFormValues>({
|
|
119
|
-
resolver: zodResolver(userSchema),
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<form onSubmit={handleSubmit(onSubmit)}>
|
|
124
|
-
<Controller
|
|
125
|
-
name="role"
|
|
126
|
-
control={control}
|
|
127
|
-
render={({ field, fieldState: { error } }) => (
|
|
128
|
-
<>
|
|
129
|
-
<Select
|
|
130
|
-
value={field.value}
|
|
131
|
-
onChange={field.onChange}
|
|
132
|
-
options={roleOptions}
|
|
133
|
-
/>
|
|
134
|
-
{error && <span>{error.message}</span>}
|
|
135
|
-
</>
|
|
136
|
-
)}
|
|
137
|
-
/>
|
|
138
|
-
|
|
139
|
-
<Controller
|
|
140
|
-
name="birthDate"
|
|
141
|
-
control={control}
|
|
142
|
-
render={({ field, fieldState: { error } }) => (
|
|
143
|
-
<>
|
|
144
|
-
<DatePicker
|
|
145
|
-
selected={field.value}
|
|
146
|
-
onChange={field.onChange}
|
|
147
|
-
/>
|
|
148
|
-
{error && <span>{error.message}</span>}
|
|
149
|
-
</>
|
|
150
|
-
)}
|
|
151
|
-
/>
|
|
152
|
-
</form>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## 5. 에러 핸들링
|
|
160
|
-
|
|
161
|
-
### errors 객체로 직접 표시
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// 간단한 에러 표시
|
|
165
|
-
{errors.email && <span className="error">{errors.email.message}</span>}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### 재사용 가능한 에러 컴포넌트
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
interface FieldErrorProps {
|
|
172
|
-
error?: FieldError;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function FieldError({ error }: FieldErrorProps) {
|
|
176
|
-
if (!error) return null;
|
|
177
|
-
return <span className="field-error">{error.message}</span>;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// 사용
|
|
181
|
-
<input {...register('email')} />
|
|
182
|
-
<FieldError error={errors.email} />
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## 6. 폼 검증 모드
|
|
188
|
-
|
|
189
|
-
### mode 옵션
|
|
190
|
-
|
|
191
|
-
| mode | 동작 | 용도 |
|
|
192
|
-
|------|------|------|
|
|
193
|
-
| `onSubmit` (기본) | 제출 시에만 검증 | 대부분의 폼 |
|
|
194
|
-
| `onBlur` | 포커스를 벗어날 때 검증 | 긴 폼, 단계별 입력 |
|
|
195
|
-
| `onChange` | 입력할 때마다 검증 | 실시간 피드백이 필요한 필드 |
|
|
196
|
-
| `onTouched` | 첫 blur 이후부터 onChange로 검증 | 사용자 친화적 검증 |
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
const { register, handleSubmit } = useForm<FormValues>({
|
|
200
|
-
resolver: zodResolver(schema),
|
|
201
|
-
mode: 'onBlur', // 포커스를 벗어날 때 검증
|
|
202
|
-
});
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## 7. 동적 필드 (useFieldArray)
|
|
208
|
-
|
|
209
|
-
반복되는 필드 그룹을 동적으로 추가/삭제한다.
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
const orderSchema = z.object({
|
|
213
|
-
items: z.array(
|
|
214
|
-
z.object({
|
|
215
|
-
productId: z.string().min(1, '상품을 선택해주세요'),
|
|
216
|
-
quantity: z.number().min(1, '수량은 1개 이상이어야 합니다'),
|
|
217
|
-
})
|
|
218
|
-
).min(1, '최소 1개 이상의 상품을 추가해주세요'),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
type OrderForm = z.infer<typeof orderSchema>;
|
|
222
|
-
|
|
223
|
-
function OrderForm() {
|
|
224
|
-
const { control, register, handleSubmit } = useForm<OrderForm>({
|
|
225
|
-
resolver: zodResolver(orderSchema),
|
|
226
|
-
defaultValues: { items: [{ productId: '', quantity: 1 }] },
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const { fields, append, remove } = useFieldArray({
|
|
230
|
-
control,
|
|
231
|
-
name: 'items',
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
return (
|
|
235
|
-
<form onSubmit={handleSubmit(onSubmit)}>
|
|
236
|
-
{fields.map((field, index) => (
|
|
237
|
-
<div key={field.id}>
|
|
238
|
-
<input {...register(`items.${index}.productId`)} />
|
|
239
|
-
<input
|
|
240
|
-
type="number"
|
|
241
|
-
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
|
|
242
|
-
/>
|
|
243
|
-
<button type="button" onClick={() => remove(index)}>
|
|
244
|
-
삭제
|
|
245
|
-
</button>
|
|
246
|
-
</div>
|
|
247
|
-
))}
|
|
248
|
-
<button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
|
|
249
|
-
상품 추가
|
|
250
|
-
</button>
|
|
251
|
-
</form>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
---
|
|
257
|
-
|
|
258
|
-
## 8. 중첩 객체 / 배열 스키마
|
|
259
|
-
|
|
260
|
-
### 중첩 객체
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
const addressSchema = z.object({
|
|
264
|
-
zipCode: z.string().length(5, '우편번호는 5자리입니다'),
|
|
265
|
-
city: z.string().min(1, '도시를 입력해주세요'),
|
|
266
|
-
detail: z.string().min(1, '상세 주소를 입력해주세요'),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const userSchema = z.object({
|
|
270
|
-
name: z.string().min(1),
|
|
271
|
-
address: addressSchema, // 중첩 객체
|
|
272
|
-
tags: z.array(z.string()), // 문자열 배열
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
type UserForm = z.infer<typeof userSchema>;
|
|
276
|
-
|
|
277
|
-
// register로 점 표기법 사용
|
|
278
|
-
<input {...register('address.zipCode')} />
|
|
279
|
-
<input {...register('address.city')} />
|
|
280
|
-
<input {...register('address.detail')} />
|
|
281
|
-
|
|
282
|
-
// 에러 접근도 점 표기법
|
|
283
|
-
{errors.address?.zipCode && <span>{errors.address.zipCode.message}</span>}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### 스키마 재사용
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
// 공통 스키마를 조합하여 재사용한다
|
|
290
|
-
const baseUserSchema = z.object({
|
|
291
|
-
name: z.string().min(1),
|
|
292
|
-
email: z.string().email(),
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const createUserSchema = baseUserSchema.extend({
|
|
296
|
-
password: z.string().min(8),
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
const updateUserSchema = baseUserSchema.partial(); // 모든 필드 optional
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## 9. 네이밍 컨벤션
|
|
110
|
+
## 4. 네이밍 컨벤션
|
|
305
111
|
|
|
306
112
|
| 대상 | 규칙 | 예시 |
|
|
307
113
|
|------|------|------|
|
|
@@ -312,7 +118,7 @@ const updateUserSchema = baseUserSchema.partial(); // 모든 필드 optional
|
|
|
312
118
|
|
|
313
119
|
---
|
|
314
120
|
|
|
315
|
-
##
|
|
121
|
+
## 5. 금지 사항
|
|
316
122
|
|
|
317
123
|
- 스키마 없이 수동 검증 금지 - 반드시 Zod 스키마를 정의하고 `zodResolver`를 사용한다
|
|
318
124
|
- `any` 타입 사용 금지 - `z.infer<typeof schema>`로 타입을 추론한다
|
|
@@ -320,3 +126,9 @@ const updateUserSchema = baseUserSchema.partial(); // 모든 필드 optional
|
|
|
320
126
|
- `handleSubmit` 없이 `onSubmit` 직접 처리 금지 - `handleSubmit`이 검증을 수행한다
|
|
321
127
|
- 폼 타입과 스키마를 별도로 정의 금지 - 타입 불일치 위험이 있으므로 `z.infer`를 사용한다
|
|
322
128
|
- 에러 메시지 하드코딩 금지 - Zod 스키마에 메시지를 정의한다
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 심화 참조
|
|
133
|
+
|
|
134
|
+
- `references/advanced-patterns.md` - Controller 패턴, 에러 핸들링, 폼 검증 모드, 동적 필드(useFieldArray), 중첩 객체/배열 스키마
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# React Hook Form 심화 패턴
|
|
2
|
+
|
|
3
|
+
## 1. Controller 패턴
|
|
4
|
+
|
|
5
|
+
`register`를 사용할 수 없는 제어 컴포넌트 (Select, DatePicker, 커스텀 UI 라이브러리 등)에 사용한다.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
9
|
+
|
|
10
|
+
function UserForm() {
|
|
11
|
+
const { control, handleSubmit } = useForm<UserFormValues>({
|
|
12
|
+
resolver: zodResolver(userSchema),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
17
|
+
<Controller
|
|
18
|
+
name="role"
|
|
19
|
+
control={control}
|
|
20
|
+
render={({ field, fieldState: { error } }) => (
|
|
21
|
+
<>
|
|
22
|
+
<Select
|
|
23
|
+
value={field.value}
|
|
24
|
+
onChange={field.onChange}
|
|
25
|
+
options={roleOptions}
|
|
26
|
+
/>
|
|
27
|
+
{error && <span>{error.message}</span>}
|
|
28
|
+
</>
|
|
29
|
+
)}
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
<Controller
|
|
33
|
+
name="birthDate"
|
|
34
|
+
control={control}
|
|
35
|
+
render={({ field, fieldState: { error } }) => (
|
|
36
|
+
<>
|
|
37
|
+
<DatePicker
|
|
38
|
+
selected={field.value}
|
|
39
|
+
onChange={field.onChange}
|
|
40
|
+
/>
|
|
41
|
+
{error && <span>{error.message}</span>}
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
45
|
+
</form>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. 에러 핸들링
|
|
53
|
+
|
|
54
|
+
### errors 객체로 직접 표시
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// 간단한 에러 표시
|
|
58
|
+
{errors.email && <span className="error">{errors.email.message}</span>}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 재사용 가능한 에러 컴포넌트
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type FieldErrorProps = {
|
|
65
|
+
error?: FieldError;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function FieldError({ error }: FieldErrorProps) {
|
|
69
|
+
if (!error) return null;
|
|
70
|
+
return <span className="field-error">{error.message}</span>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 사용
|
|
74
|
+
<input {...register('email')} />
|
|
75
|
+
<FieldError error={errors.email} />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 3. 폼 검증 모드
|
|
81
|
+
|
|
82
|
+
### mode 옵션
|
|
83
|
+
|
|
84
|
+
| mode | 동작 | 용도 |
|
|
85
|
+
|------|------|------|
|
|
86
|
+
| `onSubmit` (기본) | 제출 시에만 검증 | 대부분의 폼 |
|
|
87
|
+
| `onBlur` | 포커스를 벗어날 때 검증 | 긴 폼, 단계별 입력 |
|
|
88
|
+
| `onChange` | 입력할 때마다 검증 | 실시간 피드백이 필요한 필드 |
|
|
89
|
+
| `onTouched` | 첫 blur 이후부터 onChange로 검증 | 사용자 친화적 검증 |
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const { register, handleSubmit } = useForm<FormValues>({
|
|
93
|
+
resolver: zodResolver(schema),
|
|
94
|
+
mode: 'onBlur', // 포커스를 벗어날 때 검증
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 4. 동적 필드 (useFieldArray)
|
|
101
|
+
|
|
102
|
+
반복되는 필드 그룹을 동적으로 추가/삭제한다.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const orderSchema = z.object({
|
|
106
|
+
items: z.array(
|
|
107
|
+
z.object({
|
|
108
|
+
productId: z.string().min(1, '상품을 선택해주세요'),
|
|
109
|
+
quantity: z.number().min(1, '수량은 1개 이상이어야 합니다'),
|
|
110
|
+
})
|
|
111
|
+
).min(1, '최소 1개 이상의 상품을 추가해주세요'),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
type OrderForm = z.infer<typeof orderSchema>;
|
|
115
|
+
|
|
116
|
+
function OrderForm() {
|
|
117
|
+
const { control, register, handleSubmit } = useForm<OrderForm>({
|
|
118
|
+
resolver: zodResolver(orderSchema),
|
|
119
|
+
defaultValues: { items: [{ productId: '', quantity: 1 }] },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const { fields, append, remove } = useFieldArray({
|
|
123
|
+
control,
|
|
124
|
+
name: 'items',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
129
|
+
{fields.map((field, index) => (
|
|
130
|
+
<div key={field.id}>
|
|
131
|
+
<input {...register(`items.${index}.productId`)} />
|
|
132
|
+
<input
|
|
133
|
+
type="number"
|
|
134
|
+
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
|
|
135
|
+
/>
|
|
136
|
+
<button type="button" onClick={() => remove(index)}>
|
|
137
|
+
삭제
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
<button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
|
|
142
|
+
상품 추가
|
|
143
|
+
</button>
|
|
144
|
+
</form>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 5. 중첩 객체 / 배열 스키마
|
|
152
|
+
|
|
153
|
+
### 중첩 객체
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const addressSchema = z.object({
|
|
157
|
+
zipCode: z.string().length(5, '우편번호는 5자리입니다'),
|
|
158
|
+
city: z.string().min(1, '도시를 입력해주세요'),
|
|
159
|
+
detail: z.string().min(1, '상세 주소를 입력해주세요'),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const userSchema = z.object({
|
|
163
|
+
name: z.string().min(1),
|
|
164
|
+
address: addressSchema, // 중첩 객체
|
|
165
|
+
tags: z.array(z.string()), // 문자열 배열
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
type UserForm = z.infer<typeof userSchema>;
|
|
169
|
+
|
|
170
|
+
// register로 점 표기법 사용
|
|
171
|
+
<input {...register('address.zipCode')} />
|
|
172
|
+
<input {...register('address.city')} />
|
|
173
|
+
<input {...register('address.detail')} />
|
|
174
|
+
|
|
175
|
+
// 에러 접근도 점 표기법
|
|
176
|
+
{errors.address?.zipCode && <span>{errors.address.zipCode.message}</span>}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 스키마 재사용
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// 공통 스키마를 조합하여 재사용한다
|
|
183
|
+
const baseUserSchema = z.object({
|
|
184
|
+
name: z.string().min(1),
|
|
185
|
+
email: z.string().email(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const createUserSchema = baseUserSchema.extend({
|
|
189
|
+
password: z.string().min(8),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const updateUserSchema = baseUserSchema.partial(); // 모든 필드 optional
|
|
193
|
+
```
|
|
@@ -7,8 +7,8 @@ description: TDD(테스트 주도 개발) 공통 원칙. 테스트 작성 시 Re
|
|
|
7
7
|
|
|
8
8
|
이 문서는 모든 테스트 코드 작성 시 적용되는 공통 원칙을 정의한다.
|
|
9
9
|
FE/BE별 상세 규칙은 각각의 파일을 참고한다.
|
|
10
|
-
- `frontend.md` - React Testing Library 기반 프론트엔드 테스트 규칙
|
|
11
|
-
- `backend.md` - NestJS 백엔드 테스트 규칙
|
|
10
|
+
- `references/frontend.md` - React Testing Library 기반 프론트엔드 테스트 규칙
|
|
11
|
+
- `references/backend.md` - NestJS 백엔드 테스트 규칙
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -49,165 +49,7 @@ React 규칙은 `../React/SKILL.md`, 공통 원칙은 `../Coding/SKILL.md`를
|
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
## 2.
|
|
53
|
-
|
|
54
|
-
### 브레이크포인트
|
|
55
|
-
| 접두사 | 최소 너비 | 용도 |
|
|
56
|
-
|--------|-----------|------|
|
|
57
|
-
| (없음) | 0px | 모바일 기본 |
|
|
58
|
-
| `sm:` | 640px | 소형 태블릿 |
|
|
59
|
-
| `md:` | 768px | 태블릿 |
|
|
60
|
-
| `lg:` | 1024px | 데스크톱 |
|
|
61
|
-
| `xl:` | 1280px | 대형 데스크톱 |
|
|
62
|
-
| `2xl:` | 1536px | 초대형 데스크톱 |
|
|
63
|
-
|
|
64
|
-
### Mobile-First 원칙
|
|
65
|
-
- 모바일 스타일을 기본으로 작성하고, 큰 화면에 대해 재정의한다
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// Good - Mobile-First
|
|
69
|
-
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
70
|
-
{items.map((item) => (
|
|
71
|
-
<Card key={item.id} data={item} />
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
// Good - 반응형 타이포그래피
|
|
76
|
-
<h1 className="text-2xl font-bold md:text-3xl lg:text-4xl">
|
|
77
|
-
제목
|
|
78
|
-
</h1>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 3. 다크 모드
|
|
84
|
-
|
|
85
|
-
### dark: prefix 사용
|
|
86
|
-
- `dark:` 접두사로 다크 모드 스타일을 정의한다
|
|
87
|
-
- v4에서는 기본적으로 `prefers-color-scheme` 미디어 쿼리로 동작한다
|
|
88
|
-
- 수동 토글(class 전략)이 필요하면 CSS에서 `@custom-variant`를 설정한다
|
|
89
|
-
|
|
90
|
-
```css
|
|
91
|
-
/* app.css - 수동 다크 모드 토글 시 */
|
|
92
|
-
@import "tailwindcss";
|
|
93
|
-
@custom-variant dark (&:where(.dark, .dark *));
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// 컴포넌트에서 사용
|
|
98
|
-
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
|
|
99
|
-
<p className="text-gray-600 dark:text-gray-300">
|
|
100
|
-
라이트/다크 모드를 지원하는 텍스트
|
|
101
|
-
</p>
|
|
102
|
-
</div>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 시맨틱 색상 활용
|
|
106
|
-
- 반복되는 라이트/다크 색상 조합은 `@theme`과 CSS 변수로 추출한다
|
|
107
|
-
|
|
108
|
-
```css
|
|
109
|
-
/* app.css */
|
|
110
|
-
@import "tailwindcss";
|
|
111
|
-
|
|
112
|
-
@theme {
|
|
113
|
-
--color-background: #ffffff;
|
|
114
|
-
--color-foreground: #0a0a0a;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.dark {
|
|
118
|
-
--color-background: #0a0a0a;
|
|
119
|
-
--color-foreground: #fafafa;
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## 4. 커스텀 설정
|
|
126
|
-
|
|
127
|
-
### @theme 디렉티브 사용
|
|
128
|
-
- CSS 파일에서 `@theme` 디렉티브로 디자인 토큰을 정의한다
|
|
129
|
-
- v4에서는 `tailwind.config.ts`가 불필요하다. 모든 커스텀 설정은 CSS의 `@theme`에서 정의한다
|
|
130
|
-
- v4는 콘텐츠 파일을 자동 감지하므로 `content` 배열 설정이 불필요하다
|
|
131
|
-
|
|
132
|
-
```css
|
|
133
|
-
/* app.css */
|
|
134
|
-
@import "tailwindcss";
|
|
135
|
-
|
|
136
|
-
@theme {
|
|
137
|
-
--color-primary-50: #eff6ff;
|
|
138
|
-
--color-primary-100: #dbeafe;
|
|
139
|
-
--color-primary-500: #3b82f6;
|
|
140
|
-
--color-primary-600: #2563eb;
|
|
141
|
-
--color-primary-700: #1d4ed8;
|
|
142
|
-
--color-success: #10b981;
|
|
143
|
-
--color-warning: #f59e0b;
|
|
144
|
-
--color-danger: #ef4444;
|
|
145
|
-
|
|
146
|
-
--spacing-18: 4.5rem;
|
|
147
|
-
--spacing-88: 22rem;
|
|
148
|
-
|
|
149
|
-
--font-sans: 'Inter', sans-serif;
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## 5. 자주 쓰는 패턴
|
|
156
|
-
|
|
157
|
-
### Flexbox 레이아웃
|
|
158
|
-
```typescript
|
|
159
|
-
// 가로 정렬 (중앙)
|
|
160
|
-
<div className="flex items-center justify-center">
|
|
161
|
-
|
|
162
|
-
// 가로 정렬 (양쪽 분산)
|
|
163
|
-
<div className="flex items-center justify-between">
|
|
164
|
-
|
|
165
|
-
// 세로 정렬
|
|
166
|
-
<div className="flex flex-col gap-4">
|
|
167
|
-
|
|
168
|
-
// 자식 요소 간격
|
|
169
|
-
<div className="flex gap-2">
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Grid 레이아웃
|
|
173
|
-
```typescript
|
|
174
|
-
// 반응형 그리드
|
|
175
|
-
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
176
|
-
|
|
177
|
-
// 고정 비율 그리드
|
|
178
|
-
<div className="grid grid-cols-[1fr_2fr] gap-4">
|
|
179
|
-
|
|
180
|
-
// 사이드바 레이아웃
|
|
181
|
-
<div className="grid grid-cols-[240px_1fr] gap-8">
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Spacing 패턴
|
|
185
|
-
```typescript
|
|
186
|
-
// 섹션 간격
|
|
187
|
-
<section className="py-12 md:py-16 lg:py-20">
|
|
188
|
-
|
|
189
|
-
// 컨테이너
|
|
190
|
-
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
191
|
-
|
|
192
|
-
// 카드
|
|
193
|
-
<div className="rounded-lg border p-6 shadow-sm">
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Typography 패턴
|
|
197
|
-
```typescript
|
|
198
|
-
// 텍스트 말줄임
|
|
199
|
-
<p className="truncate">긴 텍스트...</p>
|
|
200
|
-
|
|
201
|
-
// 여러 줄 말줄임
|
|
202
|
-
<p className="line-clamp-3">긴 텍스트...</p>
|
|
203
|
-
|
|
204
|
-
// 반응형 텍스트
|
|
205
|
-
<h1 className="text-2xl font-bold tracking-tight md:text-4xl">
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## 6. cn() 유틸리티
|
|
52
|
+
## 2. cn() 유틸리티
|
|
211
53
|
|
|
212
54
|
### clsx + tailwind-merge 조합
|
|
213
55
|
- 조건부 클래스 결합에는 `cn()` 유틸리티를 사용한다
|
|
@@ -227,12 +69,12 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
227
69
|
```typescript
|
|
228
70
|
import { cn } from '@/lib/utils';
|
|
229
71
|
|
|
230
|
-
|
|
72
|
+
type ButtonProps = {
|
|
231
73
|
variant?: 'primary' | 'secondary' | 'danger';
|
|
232
74
|
size?: 'sm' | 'md' | 'lg';
|
|
233
75
|
className?: string;
|
|
234
76
|
children: React.ReactNode;
|
|
235
|
-
}
|
|
77
|
+
};
|
|
236
78
|
|
|
237
79
|
export function Button({ variant = 'primary', size = 'md', className, children }: ButtonProps) {
|
|
238
80
|
return (
|
|
@@ -303,9 +145,8 @@ const buttonVariants = cva(
|
|
|
303
145
|
}
|
|
304
146
|
);
|
|
305
147
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
VariantProps<typeof buttonVariants> {}
|
|
148
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
149
|
+
VariantProps<typeof buttonVariants>;
|
|
309
150
|
|
|
310
151
|
export function Button({ variant, size, className, ...props }: ButtonProps) {
|
|
311
152
|
return (
|
|
@@ -322,82 +163,7 @@ export function Button({ variant, size, className, ...props }: ButtonProps) {
|
|
|
322
163
|
|
|
323
164
|
---
|
|
324
165
|
|
|
325
|
-
##
|
|
326
|
-
|
|
327
|
-
### 반복 클래스 처리
|
|
328
|
-
- 동일한 클래스 조합이 3회 이상 반복되면 컴포넌트로 추출한다
|
|
329
|
-
- `@apply`보다 컴포넌트 추출을 우선한다
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
// Bad - 동일한 클래스 반복
|
|
333
|
-
<button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">저장</button>
|
|
334
|
-
<button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">확인</button>
|
|
335
|
-
<button className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">전송</button>
|
|
336
|
-
|
|
337
|
-
// Good - 컴포넌트로 추출
|
|
338
|
-
<Button>저장</Button>
|
|
339
|
-
<Button>확인</Button>
|
|
340
|
-
<Button>전송</Button>
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
### @apply 사용 (제한적 허용)
|
|
344
|
-
- 컴포넌트 추출이 어려운 경우에만 `@apply`를 사용한다
|
|
345
|
-
- 주로 base layer의 글로벌 스타일에서 사용한다
|
|
346
|
-
- v4에서도 `@apply`는 지원되지만, 여전히 컴포넌트 추출을 우선한다
|
|
347
|
-
|
|
348
|
-
```css
|
|
349
|
-
/* 허용 - 글로벌 기본 스타일 */
|
|
350
|
-
@layer base {
|
|
351
|
-
body {
|
|
352
|
-
@apply bg-background text-foreground;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/* 지양 - 컴포넌트 스타일을 @apply로 작성 */
|
|
357
|
-
.btn-primary {
|
|
358
|
-
@apply rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700;
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
|
-
## 8. 트랜지션 & 모션
|
|
365
|
-
|
|
366
|
-
### transition 속성 명시
|
|
367
|
-
- `transition-all` 사용을 금지한다 - 변경되는 속성만 명시한다
|
|
368
|
-
- 불필요한 속성까지 트랜지션되면 성능이 저하된다
|
|
369
|
-
|
|
370
|
-
```typescript
|
|
371
|
-
// Bad - 모든 속성에 트랜지션
|
|
372
|
-
<button className="transition-all">
|
|
373
|
-
|
|
374
|
-
// Good - 필요한 속성만
|
|
375
|
-
<button className="transition-colors">
|
|
376
|
-
<div className="transition-[transform,opacity]">
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### prefers-reduced-motion 존중
|
|
380
|
-
- 애니메이션/트랜지션이 있는 요소에는 `motion-reduce:` 변형을 고려한다
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
// 모션 감소 선호 시 애니메이션 비활성화
|
|
384
|
-
<div className="animate-bounce motion-reduce:animate-none">
|
|
385
|
-
<div className="transition-transform motion-reduce:transition-none">
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### 다크 모드 color-scheme
|
|
389
|
-
- 다크 모드 지원 시 `color-scheme: dark`를 설정하여 네이티브 UI(스크롤바, 입력 필드 등)도 다크 테마에 맞춘다
|
|
390
|
-
|
|
391
|
-
```css
|
|
392
|
-
/* app.css */
|
|
393
|
-
.dark {
|
|
394
|
-
color-scheme: dark;
|
|
395
|
-
}
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
## 9. 금지 사항
|
|
166
|
+
## 3. 금지 사항
|
|
401
167
|
|
|
402
168
|
- 인라인 `style={{}}` 사용 금지 - Tailwind 유틸리티 클래스를 사용한다
|
|
403
169
|
- 임의값(arbitrary values) 남용 금지 - `w-[137px]` 같은 임의값은 최소화한다
|
|
@@ -426,3 +192,11 @@ const colorClasses = {
|
|
|
426
192
|
|
|
427
193
|
<div className={colorClasses[color]}>
|
|
428
194
|
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 심화 참조
|
|
199
|
+
|
|
200
|
+
- `references/responsive-dark.md` - 반응형 디자인, 다크 모드, 커스텀 설정 (@theme, 브레이크포인트, dark: prefix)
|
|
201
|
+
- `references/patterns-components.md` - 자주 쓰는 레이아웃/타이포그래피 패턴, 컴포넌트 추출 규칙
|
|
202
|
+
- `references/transitions.md` - 트랜지션 & 모션 (transition 속성 명시, prefers-reduced-motion, color-scheme)
|