@kood/claude-code 0.5.0 → 0.5.3
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/dist/index.js +179 -33
- package/package.json +1 -1
- package/templates/.claude/agents/analyst.md +6 -14
- package/templates/.claude/agents/architect.md +6 -14
- package/templates/.claude/agents/code-reviewer.md +8 -14
- package/templates/.claude/agents/dependency-manager.md +8 -14
- package/templates/.claude/agents/deployment-validator.md +8 -14
- package/templates/.claude/agents/designer.md +8 -0
- package/templates/.claude/agents/document-writer.md +6 -14
- package/templates/.claude/agents/explore.md +8 -3
- package/templates/.claude/agents/git-operator.md +63 -17
- package/templates/.claude/agents/implementation-executor.md +14 -37
- package/templates/.claude/agents/ko-to-en-translator.md +8 -13
- package/templates/.claude/agents/lint-fixer.md +8 -172
- package/templates/.claude/agents/planner.md +6 -14
- package/templates/.claude/agents/refactor-advisor.md +8 -14
- package/templates/.claude/commands/git-all.md +3 -167
- package/templates/.claude/commands/git-session.md +3 -71
- package/templates/.claude/commands/lint-fix.md +119 -82
- package/templates/.claude/commands/lint-init.md +2 -54
- package/templates/.claude/commands/pre-deploy.md +143 -82
- package/templates/.claude/commands/version-update.md +171 -66
- package/templates/.claude/instructions/agent-patterns/agent-coordination.md +208 -0
- package/templates/.claude/instructions/agent-patterns/index.md +264 -0
- package/templates/.claude/instructions/agent-patterns/model-routing.md +167 -0
- package/templates/.claude/instructions/agent-patterns/parallel-execution.md +91 -0
- package/templates/.claude/instructions/agent-patterns/read-parallelization.md +106 -0
- package/templates/.claude/instructions/index.md +350 -0
- package/templates/.claude/instructions/multi-agent/agent-roster.md +811 -0
- package/templates/.claude/{PARALLEL_AGENTS.md → instructions/multi-agent/coordination-guide.md} +27 -336
- package/templates/.claude/instructions/{parallel-agent-execution.md → multi-agent/execution-patterns.md} +127 -438
- package/templates/.claude/instructions/multi-agent/index.md +282 -0
- package/templates/.claude/instructions/multi-agent/performance-optimization.md +456 -0
- package/templates/.claude/instructions/tech-stack/design-standards.md +282 -0
- package/templates/.claude/instructions/tech-stack/index.md +70 -0
- package/templates/.claude/instructions/tech-stack/prisma-patterns.md +352 -0
- package/templates/.claude/instructions/tech-stack/tanstack-patterns.md +275 -0
- package/templates/.claude/instructions/validation/forbidden-patterns.md +281 -0
- package/templates/.claude/instructions/validation/index.md +32 -0
- package/templates/.claude/instructions/validation/required-behaviors.md +331 -0
- package/templates/.claude/instructions/validation/verification-checklist.md +318 -0
- package/templates/.claude/instructions/workflow-patterns/index.md +18 -0
- package/templates/.claude/instructions/workflow-patterns/phase-based-workflow.md +250 -0
- package/templates/.claude/instructions/workflow-patterns/sequential-thinking.md +217 -0
- package/templates/.claude/instructions/workflow-patterns/todowrite-pattern.md +215 -0
- package/templates/.claude/skills/bug-fix/SKILL.md +972 -0
- package/templates/.claude/skills/docs-creator/AGENTS.md +4 -1
- package/templates/.claude/skills/docs-creator/SKILL.md +258 -0
- package/templates/.claude/skills/docs-refactor/AGENTS.md +4 -1
- package/templates/.claude/skills/docs-refactor/SKILL.md +145 -0
- package/templates/.claude/skills/execute/SKILL.md +15 -242
- package/templates/.claude/skills/figma-to-code/AGENTS.md +4 -1
- package/templates/.claude/skills/figma-to-code/SKILL.md +306 -0
- package/templates/.claude/skills/global-uiux-design/AGENTS.md +4 -1
- package/templates/.claude/skills/global-uiux-design/SKILL.md +455 -125
- package/templates/.claude/skills/korea-uiux-design/AGENTS.md +4 -1
- package/templates/.claude/skills/korea-uiux-design/SKILL.md +461 -116
- package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +177 -0
- package/templates/.claude/skills/plan/SKILL.md +1102 -98
- package/templates/.claude/skills/prd/SKILL.md +367 -66
- package/templates/.claude/skills/ralph/AGENTS.md +4 -1
- package/templates/.claude/skills/ralph/SKILL.md +83 -0
- package/templates/.claude/skills/refactor/SKILL.md +1214 -0
- package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +149 -0
- package/templates/.claude/commands/bug-fix.md +0 -510
- package/templates/.claude/commands/refactor.md +0 -788
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Design Standards
|
|
2
|
+
|
|
3
|
+
**목적**: UI/UX 및 접근성 표준
|
|
4
|
+
|
|
5
|
+
## 접근성 (Accessibility)
|
|
6
|
+
|
|
7
|
+
### WCAG 2.2 AA 준수
|
|
8
|
+
|
|
9
|
+
**필수 기준:**
|
|
10
|
+
|
|
11
|
+
| 기준 | 요구사항 | 예시 |
|
|
12
|
+
|------|----------|------|
|
|
13
|
+
| **색상 대비** | 4.5:1 이상 | 텍스트-배경 대비 |
|
|
14
|
+
| **키보드 네비게이션** | 모든 기능 접근 가능 | Tab, Enter, Space |
|
|
15
|
+
| **ARIA 속성** | 적절한 레이블 | aria-label, aria-describedby |
|
|
16
|
+
| **포커스 표시** | 시각적 표시 필수 | outline, ring |
|
|
17
|
+
|
|
18
|
+
### ARIA 속성
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// ✅ 올바른 ARIA 사용
|
|
22
|
+
<button
|
|
23
|
+
aria-label="사용자 삭제"
|
|
24
|
+
aria-describedby="delete-description"
|
|
25
|
+
onClick={handleDelete}
|
|
26
|
+
>
|
|
27
|
+
<TrashIcon />
|
|
28
|
+
</button>
|
|
29
|
+
<span id="delete-description" className="sr-only">
|
|
30
|
+
이 작업은 되돌릴 수 없습니다
|
|
31
|
+
</span>
|
|
32
|
+
|
|
33
|
+
// ❌ 잘못된 예
|
|
34
|
+
<button onClick={handleDelete}>
|
|
35
|
+
<TrashIcon /> {/* 스크린 리더가 읽을 수 없음 */}
|
|
36
|
+
</button>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 키보드 네비게이션
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// ✅ 키보드 지원
|
|
43
|
+
function Modal({ isOpen, onClose }) {
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
46
|
+
if (e.key === 'Escape') onClose()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
document.addEventListener('keydown', handleEscape)
|
|
51
|
+
return () => document.removeEventListener('keydown', handleEscape)
|
|
52
|
+
}
|
|
53
|
+
}, [isOpen, onClose])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div role="dialog" aria-modal="true">
|
|
57
|
+
{/* 모달 내용 */}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 스크린 리더
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// ✅ 스크린 리더 전용 텍스트
|
|
67
|
+
<span className="sr-only">Loading...</span>
|
|
68
|
+
|
|
69
|
+
// Tailwind CSS
|
|
70
|
+
<span className="absolute inset-0 -z-10">
|
|
71
|
+
시각적으로 숨김
|
|
72
|
+
</span>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 색상 시스템
|
|
76
|
+
|
|
77
|
+
### 60-30-10 규칙
|
|
78
|
+
|
|
79
|
+
| 비율 | 용도 | 예시 |
|
|
80
|
+
|------|------|------|
|
|
81
|
+
| **60%** | 배경 | bg-gray-50 |
|
|
82
|
+
| **30%** | 보조 | bg-gray-200 |
|
|
83
|
+
| **10%** | 강조 | bg-blue-500 |
|
|
84
|
+
|
|
85
|
+
### Tailwind CSS v4 @theme
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
@theme {
|
|
89
|
+
/* 색상 팔레트 */
|
|
90
|
+
--color-primary-50: #eff6ff;
|
|
91
|
+
--color-primary-500: #3b82f6;
|
|
92
|
+
--color-primary-900: #1e3a8a;
|
|
93
|
+
|
|
94
|
+
/* 간격 */
|
|
95
|
+
--spacing-4: 1rem;
|
|
96
|
+
--spacing-8: 2rem;
|
|
97
|
+
|
|
98
|
+
/* 폰트 */
|
|
99
|
+
--font-sans: 'Inter', sans-serif;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 색상 대비 검증
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// 대비율 계산 도구 사용
|
|
107
|
+
// https://webaim.org/resources/contrastchecker/
|
|
108
|
+
|
|
109
|
+
// 예시:
|
|
110
|
+
// 텍스트: #333333 (어두운 회색)
|
|
111
|
+
// 배경: #FFFFFF (흰색)
|
|
112
|
+
// 대비율: 12.63:1 ✅ (4.5:1 이상)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 레이아웃
|
|
116
|
+
|
|
117
|
+
### 간격 시스템 (8px Grid)
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// ✅ 8의 배수 사용
|
|
121
|
+
<div className="p-4"> {/* 16px */}
|
|
122
|
+
<div className="mb-8"> {/* 32px */}
|
|
123
|
+
<h1 className="text-2xl">Title</h1>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
// ❌ 임의의 값
|
|
128
|
+
<div className="p-[13px]"> {/* 비권장 */}
|
|
129
|
+
</div>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 반응형 디자인
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
// ✅ Mobile-first
|
|
136
|
+
<div className="
|
|
137
|
+
w-full {/* 모바일 */}
|
|
138
|
+
md:w-1/2 {/* 태블릿 */}
|
|
139
|
+
lg:w-1/3 {/* 데스크톱 */}
|
|
140
|
+
">
|
|
141
|
+
Content
|
|
142
|
+
</div>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Safe Area (iOS/Android)
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// tailwindcss-safe-area 플러그인 사용
|
|
149
|
+
<div className="
|
|
150
|
+
pb-4
|
|
151
|
+
pb-[calc(1rem+env(safe-area-inset-bottom))]
|
|
152
|
+
">
|
|
153
|
+
하단 버튼
|
|
154
|
+
</div>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 타이포그래피
|
|
158
|
+
|
|
159
|
+
### 폰트 시스템 (2-3개)
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
@theme {
|
|
163
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
164
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 크기 및 행간
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// ✅ 적절한 행간
|
|
172
|
+
<p className="text-base leading-relaxed"> {/* 1.625 */}
|
|
173
|
+
긴 텍스트는 행간을 넓게
|
|
174
|
+
</p>
|
|
175
|
+
|
|
176
|
+
<h1 className="text-4xl leading-tight"> {/* 1.25 */}
|
|
177
|
+
제목은 행간을 좁게
|
|
178
|
+
</h1>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 인터랙션
|
|
182
|
+
|
|
183
|
+
### 호버/포커스 상태
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
// ✅ 명확한 상태 표시
|
|
187
|
+
<button className="
|
|
188
|
+
bg-blue-500
|
|
189
|
+
hover:bg-blue-600
|
|
190
|
+
focus:ring-4 focus:ring-blue-300
|
|
191
|
+
transition-colors
|
|
192
|
+
">
|
|
193
|
+
버튼
|
|
194
|
+
</button>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 로딩 상태
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// ✅ 로딩 인디케이터
|
|
201
|
+
{isLoading ? (
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<Spinner className="w-4 h-4" />
|
|
204
|
+
<span>Loading...</span>
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<button>Submit</button>
|
|
208
|
+
)}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 에러 상태
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// ✅ 명확한 에러 메시지
|
|
215
|
+
{error && (
|
|
216
|
+
<div
|
|
217
|
+
role="alert"
|
|
218
|
+
className="bg-red-50 border border-red-200 p-4 rounded"
|
|
219
|
+
>
|
|
220
|
+
<p className="text-red-800">{error.message}</p>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 성능
|
|
226
|
+
|
|
227
|
+
### 이미지 최적화
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
// ✅ Next.js Image 또는 적절한 최적화
|
|
231
|
+
<img
|
|
232
|
+
src="/image.jpg"
|
|
233
|
+
alt="설명"
|
|
234
|
+
loading="lazy"
|
|
235
|
+
width={800}
|
|
236
|
+
height={600}
|
|
237
|
+
/>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 코드 스플리팅
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// ✅ 동적 import
|
|
244
|
+
const HeavyComponent = lazy(() => import('./HeavyComponent'))
|
|
245
|
+
|
|
246
|
+
<Suspense fallback={<Spinner />}>
|
|
247
|
+
<HeavyComponent />
|
|
248
|
+
</Suspense>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## 체크리스트
|
|
252
|
+
|
|
253
|
+
### 접근성
|
|
254
|
+
|
|
255
|
+
- [ ] 색상 대비 4.5:1 이상
|
|
256
|
+
- [ ] 모든 interactive 요소 키보드 접근 가능
|
|
257
|
+
- [ ] ARIA 속성 적절히 사용
|
|
258
|
+
- [ ] 포커스 표시 명확
|
|
259
|
+
- [ ] 스크린 리더 지원
|
|
260
|
+
|
|
261
|
+
### 디자인
|
|
262
|
+
|
|
263
|
+
- [ ] 60-30-10 색상 규칙
|
|
264
|
+
- [ ] 8px 그리드 간격
|
|
265
|
+
- [ ] 2-3개 폰트 사용
|
|
266
|
+
- [ ] 반응형 디자인 (mobile-first)
|
|
267
|
+
- [ ] Safe Area 고려
|
|
268
|
+
|
|
269
|
+
### 인터랙션
|
|
270
|
+
|
|
271
|
+
- [ ] 호버/포커스 상태 명확
|
|
272
|
+
- [ ] 로딩 상태 표시
|
|
273
|
+
- [ ] 에러 메시지 명확
|
|
274
|
+
- [ ] 전환 애니메이션 자연스러움
|
|
275
|
+
|
|
276
|
+
### 성능
|
|
277
|
+
|
|
278
|
+
- [ ] 이미지 lazy loading
|
|
279
|
+
- [ ] 코드 스플리팅
|
|
280
|
+
- [ ] 불필요한 리렌더 방지
|
|
281
|
+
|
|
282
|
+
**디자인 표준 준수 → 접근성 + 사용성 향상**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Tech Stack
|
|
2
|
+
|
|
3
|
+
기술 스택 사용 패턴
|
|
4
|
+
|
|
5
|
+
## 파일 목록
|
|
6
|
+
|
|
7
|
+
| 파일 | 설명 |
|
|
8
|
+
|------|------|
|
|
9
|
+
| [tanstack-patterns.md](./tanstack-patterns.md) | TanStack Start/Router/Query 패턴 |
|
|
10
|
+
| [prisma-patterns.md](./prisma-patterns.md) | Prisma 7.x 사용 패턴 |
|
|
11
|
+
| [design-standards.md](./design-standards.md) | UI/UX 및 접근성 표준 |
|
|
12
|
+
|
|
13
|
+
## 사용법
|
|
14
|
+
|
|
15
|
+
```markdown
|
|
16
|
+
@.claude/instructions/tech-stack/tanstack-patterns.md
|
|
17
|
+
@.claude/instructions/tech-stack/prisma-patterns.md
|
|
18
|
+
@.claude/instructions/tech-stack/design-standards.md
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 빠른 참고
|
|
22
|
+
|
|
23
|
+
### TanStack Patterns
|
|
24
|
+
|
|
25
|
+
서버 함수, 라우팅, 쿼리 상태 관리 패턴
|
|
26
|
+
|
|
27
|
+
**주요 내용:**
|
|
28
|
+
- Server Function 작성 (GET/POST/PUT)
|
|
29
|
+
- inputValidator + middleware 체인
|
|
30
|
+
- Route loader 패턴
|
|
31
|
+
- TanStack Query (useQuery, useMutation)
|
|
32
|
+
- Optimistic Updates
|
|
33
|
+
|
|
34
|
+
### Prisma Patterns
|
|
35
|
+
|
|
36
|
+
데이터베이스 스키마 및 쿼리 작성 패턴
|
|
37
|
+
|
|
38
|
+
**주요 내용:**
|
|
39
|
+
- Prisma 7.x 버전 (prisma-client, output)
|
|
40
|
+
- Single/Multi-File 스키마 구조
|
|
41
|
+
- CRUD Operations
|
|
42
|
+
- Relations (1:N, N:N)
|
|
43
|
+
- N+1 Problem 방지
|
|
44
|
+
- Index 최적화
|
|
45
|
+
|
|
46
|
+
### Design Standards
|
|
47
|
+
|
|
48
|
+
UI/UX 및 접근성 표준
|
|
49
|
+
|
|
50
|
+
**주요 내용:**
|
|
51
|
+
- WCAG 2.2 AA 준수 (색상 대비 4.5:1+)
|
|
52
|
+
- ARIA 속성 및 키보드 네비게이션
|
|
53
|
+
- 60-30-10 색상 규칙
|
|
54
|
+
- 8px 간격 시스템
|
|
55
|
+
- Safe Area (iOS/Android)
|
|
56
|
+
- 반응형 디자인 (mobile-first)
|
|
57
|
+
|
|
58
|
+
## 체크리스트
|
|
59
|
+
|
|
60
|
+
### 문서 선택
|
|
61
|
+
|
|
62
|
+
- TanStack 작업 → [tanstack-patterns.md](./tanstack-patterns.md)
|
|
63
|
+
- 데이터베이스 작업 → [prisma-patterns.md](./prisma-patterns.md)
|
|
64
|
+
- UI/UX 작업 → [design-standards.md](./design-standards.md)
|
|
65
|
+
|
|
66
|
+
### 작업 전
|
|
67
|
+
|
|
68
|
+
- [ ] 해당 패턴 문서 읽기
|
|
69
|
+
- [ ] 체크리스트 항목 확인
|
|
70
|
+
- [ ] 코드 예시 참고
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Prisma Patterns
|
|
2
|
+
|
|
3
|
+
**목적**: Prisma 7.x 사용 패턴 및 주의사항
|
|
4
|
+
|
|
5
|
+
## 버전 정보
|
|
6
|
+
|
|
7
|
+
**Prisma 7.x 사용 중**
|
|
8
|
+
|
|
9
|
+
- 패키지명: `prisma-client` (prisma가 아님)
|
|
10
|
+
- output 경로 필수: `output = "./generated/client"`
|
|
11
|
+
|
|
12
|
+
## 클라이언트 초기화
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// src/database/prisma.ts
|
|
16
|
+
import { PrismaClient } from './generated/client'
|
|
17
|
+
|
|
18
|
+
const globalForPrisma = globalThis as unknown as {
|
|
19
|
+
prisma: PrismaClient | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
23
|
+
|
|
24
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
25
|
+
globalForPrisma.prisma = prisma
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**필수:**
|
|
30
|
+
- [ ] PrismaClient import 경로 확인 (`./generated/client`)
|
|
31
|
+
- [ ] globalThis 패턴 사용 (개발 모드 hot reload 대응)
|
|
32
|
+
|
|
33
|
+
## Schema 구조
|
|
34
|
+
|
|
35
|
+
### Single File
|
|
36
|
+
|
|
37
|
+
```prisma
|
|
38
|
+
// prisma/schema.prisma
|
|
39
|
+
|
|
40
|
+
generator client {
|
|
41
|
+
provider = "prisma-client-js"
|
|
42
|
+
output = "./generated/client"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
datasource db {
|
|
46
|
+
provider = "postgresql"
|
|
47
|
+
url = env("DATABASE_URL")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
model User {
|
|
51
|
+
id Int @id @default(autoincrement())
|
|
52
|
+
email String @unique
|
|
53
|
+
name String
|
|
54
|
+
posts Post[]
|
|
55
|
+
createdAt DateTime @default(now())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
model Post {
|
|
59
|
+
id Int @id @default(autoincrement())
|
|
60
|
+
title String
|
|
61
|
+
content String
|
|
62
|
+
authorId Int
|
|
63
|
+
author User @relation(fields: [authorId], references: [id])
|
|
64
|
+
createdAt DateTime @default(now())
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Multi-File Structure
|
|
69
|
+
|
|
70
|
+
**명시적 요청 시에만 사용**
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
prisma/schema/
|
|
74
|
+
├── +base.prisma # datasource, generator
|
|
75
|
+
├── +enum.prisma # 모든 enum
|
|
76
|
+
├── user.prisma # User 모델
|
|
77
|
+
└── post.prisma # Post 모델
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```prisma
|
|
81
|
+
// prisma/schema/+base.prisma
|
|
82
|
+
generator client {
|
|
83
|
+
provider = "prisma-client-js"
|
|
84
|
+
output = "../generated/client"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
datasource db {
|
|
88
|
+
provider = "postgresql"
|
|
89
|
+
url = env("DATABASE_URL")
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```prisma
|
|
94
|
+
// prisma/schema/+enum.prisma
|
|
95
|
+
/// 사용자 역할
|
|
96
|
+
enum UserRole {
|
|
97
|
+
/// 관리자
|
|
98
|
+
ADMIN
|
|
99
|
+
/// 일반 사용자
|
|
100
|
+
USER
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```prisma
|
|
105
|
+
// prisma/schema/user.prisma
|
|
106
|
+
/// 사용자
|
|
107
|
+
model User {
|
|
108
|
+
/// 고유 식별자
|
|
109
|
+
id Int @id @default(autoincrement())
|
|
110
|
+
/// 이메일 (고유)
|
|
111
|
+
email String @unique
|
|
112
|
+
/// 이름
|
|
113
|
+
name String
|
|
114
|
+
/// 역할
|
|
115
|
+
role UserRole @default(USER)
|
|
116
|
+
/// 생성 시각
|
|
117
|
+
createdAt DateTime @default(now())
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Multi-File 규칙:**
|
|
122
|
+
|
|
123
|
+
- [ ] 모든 모델/필드/enum에 한글 주석 필수
|
|
124
|
+
- [ ] `+base.prisma`: datasource, generator
|
|
125
|
+
- [ ] `+enum.prisma`: 모든 enum 정의
|
|
126
|
+
- [ ] `[model].prisma`: 모델별 파일
|
|
127
|
+
|
|
128
|
+
## CRUD Operations
|
|
129
|
+
|
|
130
|
+
### Create
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const user = await prisma.user.create({
|
|
134
|
+
data: {
|
|
135
|
+
email: 'user@example.com',
|
|
136
|
+
name: 'User Name'
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Read
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// 단일 조회
|
|
145
|
+
const user = await prisma.user.findUnique({
|
|
146
|
+
where: { id: 1 }
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// 목록 조회
|
|
150
|
+
const users = await prisma.user.findMany({
|
|
151
|
+
where: { role: 'USER' },
|
|
152
|
+
include: { posts: true }
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// 페이지네이션
|
|
156
|
+
const users = await prisma.user.findMany({
|
|
157
|
+
skip: 20,
|
|
158
|
+
take: 10,
|
|
159
|
+
orderBy: { createdAt: 'desc' }
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Update
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const user = await prisma.user.update({
|
|
167
|
+
where: { id: 1 },
|
|
168
|
+
data: { name: 'New Name' }
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Delete
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const user = await prisma.user.delete({
|
|
176
|
+
where: { id: 1 }
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Relations
|
|
181
|
+
|
|
182
|
+
### 1:N (One-to-Many)
|
|
183
|
+
|
|
184
|
+
```prisma
|
|
185
|
+
model User {
|
|
186
|
+
id Int @id @default(autoincrement())
|
|
187
|
+
posts Post[]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
model Post {
|
|
191
|
+
id Int @id @default(autoincrement())
|
|
192
|
+
authorId Int
|
|
193
|
+
author User @relation(fields: [authorId], references: [id])
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Include posts
|
|
199
|
+
const user = await prisma.user.findUnique({
|
|
200
|
+
where: { id: 1 },
|
|
201
|
+
include: { posts: true }
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Create with relation
|
|
205
|
+
const post = await prisma.post.create({
|
|
206
|
+
data: {
|
|
207
|
+
title: 'Post Title',
|
|
208
|
+
author: {
|
|
209
|
+
connect: { id: 1 }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### N:N (Many-to-Many)
|
|
216
|
+
|
|
217
|
+
```prisma
|
|
218
|
+
model Post {
|
|
219
|
+
id Int @id @default(autoincrement())
|
|
220
|
+
tags Tag[]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
model Tag {
|
|
224
|
+
id Int @id @default(autoincrement())
|
|
225
|
+
posts Post[]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Create with tags
|
|
231
|
+
const post = await prisma.post.create({
|
|
232
|
+
data: {
|
|
233
|
+
title: 'Post Title',
|
|
234
|
+
tags: {
|
|
235
|
+
connect: [{ id: 1 }, { id: 2 }]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Transactions
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
245
|
+
const user = await tx.user.create({
|
|
246
|
+
data: { email: 'user@example.com', name: 'User' }
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const post = await tx.post.create({
|
|
250
|
+
data: {
|
|
251
|
+
title: 'Post Title',
|
|
252
|
+
authorId: user.id
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return { user, post }
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 자동 실행 금지
|
|
261
|
+
|
|
262
|
+
**절대 자동 실행하지 않음:**
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# ❌ 금지
|
|
266
|
+
prisma db push
|
|
267
|
+
prisma migrate dev
|
|
268
|
+
prisma generate
|
|
269
|
+
|
|
270
|
+
# ✅ 올바름: 사용자에게 안내
|
|
271
|
+
echo "schema.prisma 수정 완료. 다음 명령어 실행 필요:"
|
|
272
|
+
echo " prisma db push"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**이유:**
|
|
276
|
+
- DB 변경은 중요한 작업
|
|
277
|
+
- 사용자가 직접 확인 후 실행
|
|
278
|
+
- 의도하지 않은 데이터 손실 방지
|
|
279
|
+
|
|
280
|
+
## Index 최적화
|
|
281
|
+
|
|
282
|
+
```prisma
|
|
283
|
+
model User {
|
|
284
|
+
id Int @id @default(autoincrement())
|
|
285
|
+
email String @unique
|
|
286
|
+
name String
|
|
287
|
+
createdAt DateTime @default(now())
|
|
288
|
+
|
|
289
|
+
@@index([createdAt])
|
|
290
|
+
@@index([email, name])
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Index 사용 기준:**
|
|
295
|
+
|
|
296
|
+
- [ ] WHERE 조건에 자주 사용되는 필드
|
|
297
|
+
- [ ] JOIN에 사용되는 외래 키
|
|
298
|
+
- [ ] ORDER BY에 사용되는 필드
|
|
299
|
+
|
|
300
|
+
## N+1 Problem 방지
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// ❌ N+1 Problem
|
|
304
|
+
const users = await prisma.user.findMany()
|
|
305
|
+
for (const user of users) {
|
|
306
|
+
const posts = await prisma.post.findMany({
|
|
307
|
+
where: { authorId: user.id }
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ✅ Include 사용
|
|
312
|
+
const users = await prisma.user.findMany({
|
|
313
|
+
include: { posts: true }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// ✅ Select 최적화
|
|
317
|
+
const users = await prisma.user.findMany({
|
|
318
|
+
select: {
|
|
319
|
+
id: true,
|
|
320
|
+
name: true,
|
|
321
|
+
posts: {
|
|
322
|
+
select: {
|
|
323
|
+
id: true,
|
|
324
|
+
title: true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## 체크리스트
|
|
332
|
+
|
|
333
|
+
### Schema 작성
|
|
334
|
+
|
|
335
|
+
- [ ] `generator client` output 경로 확인
|
|
336
|
+
- [ ] Multi-File 사용 시 한글 주석 필수
|
|
337
|
+
- [ ] Index 적절히 설정
|
|
338
|
+
- [ ] Relations 올바르게 정의
|
|
339
|
+
|
|
340
|
+
### 쿼리 작성
|
|
341
|
+
|
|
342
|
+
- [ ] N+1 Problem 회피 (include/select 사용)
|
|
343
|
+
- [ ] 페이지네이션 적용 (skip/take)
|
|
344
|
+
- [ ] 트랜잭션 필요 시 `$transaction` 사용
|
|
345
|
+
|
|
346
|
+
### 자동 실행 금지
|
|
347
|
+
|
|
348
|
+
- [ ] `prisma db push` 자동 실행 금지
|
|
349
|
+
- [ ] `prisma migrate dev` 자동 실행 금지
|
|
350
|
+
- [ ] `prisma generate` 자동 실행 금지
|
|
351
|
+
|
|
352
|
+
**Prisma 패턴 준수 → 타입 안전 + 성능 최적화**
|