@choblue/claude-code-toolkit 1.2.6 → 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 +58 -18
- package/.claude/hooks/prompt-hook.sh +4 -4
- package/.claude/skills/Coding/SKILL.md +5 -4
- 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 +15 -10
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
97eedbf98bcf7cd6a3c3c2e61afd04ae61af9b7c
|
package/.claude/CLAUDE.md
CHANGED
|
@@ -70,33 +70,74 @@ Step 4: 수정/삭제 → 확인
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
## 3.
|
|
73
|
+
## 3. Task Header (의사결정 가시화)
|
|
74
|
+
|
|
75
|
+
모든 티어에서 작업 시작 시 Task Header를 출력하여 의사결정을 가시화한다.
|
|
76
|
+
|
|
77
|
+
### S 티어 헤더
|
|
78
|
+
```
|
|
79
|
+
📋 [작업명]
|
|
80
|
+
⚡ S — [판단 근거 한 줄]
|
|
81
|
+
📁 [대상 파일]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### M 티어 헤더
|
|
85
|
+
```
|
|
86
|
+
📋 [작업명]
|
|
87
|
+
⚡ M — [판단 근거 한 줄]
|
|
88
|
+
📚 [사용할 스킬 목록]
|
|
89
|
+
🔄 [에이전트 흐름]
|
|
90
|
+
📁 [예상 파일 수와 대상]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### L 티어 헤더
|
|
94
|
+
```
|
|
95
|
+
📋 [작업명]
|
|
96
|
+
⚡ L — [판단 근거 한 줄]
|
|
97
|
+
📚 [사용할 스킬 목록]
|
|
98
|
+
🔄 [에이전트 흐름]
|
|
99
|
+
📁 [예상 파일 수와 대상]
|
|
100
|
+
📌 Plan:
|
|
101
|
+
○ Step 1: [작업 단위]
|
|
102
|
+
○ Step 2: [작업 단위]
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
L 티어에서는 각 Step 진행에 따라 상태를 갱신한다:
|
|
107
|
+
- `○` 대기 → `▶ ⏳` 진행 중 → `✔` 완료
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 4. 티어별 워크플로우
|
|
74
112
|
|
|
75
113
|
### S 티어 (trivial)
|
|
76
114
|
Main Agent가 직접 처리한다. 서브에이전트 위임 불필요.
|
|
77
|
-
1.
|
|
78
|
-
2.
|
|
115
|
+
1. **Task Header 출력**
|
|
116
|
+
2. 파일 읽기 → 직접 수정 → 완료
|
|
117
|
+
3. 필요 시 `git-manager`로 커밋
|
|
79
118
|
|
|
80
119
|
### M 티어 (moderate)
|
|
81
120
|
TDD/Review를 생략하고 핵심 단계만 수행한다.
|
|
82
|
-
1. **
|
|
83
|
-
2. **
|
|
84
|
-
3. **
|
|
121
|
+
1. **Task Header 출력**
|
|
122
|
+
2. **Planning**: 요구사항 정리, 필요 시 `explore`로 탐색
|
|
123
|
+
3. **Implementation**: `code-writer` 에이전트에 구현 위임 (단위별로 나눠 호출)
|
|
124
|
+
4. **Commit**: `git-manager`로 커밋/PR 생성
|
|
85
125
|
|
|
86
126
|
### L 티어 (complex)
|
|
87
127
|
파일 기반 설계 후 **단위별로** 구현한다.
|
|
88
|
-
1. **
|
|
89
|
-
2. **
|
|
90
|
-
3.
|
|
91
|
-
4.
|
|
92
|
-
5. **
|
|
128
|
+
1. **Task Header 출력**
|
|
129
|
+
2. **Research**: `explore`로 탐색 → `research.md` 작성 (관련 코드 분석, 제약 조건)
|
|
130
|
+
3. **Plan**: `plan.md` 작성 (접근 방식, 변경 파일, 트레이드 오프, **단위별 작업 순서**)
|
|
131
|
+
4. **주석 사이클**: 사용자가 plan.md에 메모 → 반영 → **승인 전까지 구현 금지**
|
|
132
|
+
5. **Implementation + Test**: plan.md의 각 단위를 순서대로 `implementer`에 위임 (단위당 1회 호출)
|
|
133
|
+
6. **Review**: `code-reviewer`로 리뷰 → `git-manager`로 커밋/PR
|
|
93
134
|
|
|
94
135
|
### 풀스택 작업 (FE + BE 동시 변경)
|
|
95
136
|
티어는 영향도 기준으로 판단하되, 위임 순서는 BE 선행 → FE 후행을 따른다.
|
|
96
137
|
|
|
97
138
|
---
|
|
98
139
|
|
|
99
|
-
##
|
|
140
|
+
## 5. 문서 참조 가이드
|
|
100
141
|
|
|
101
142
|
### Agents (서브에이전트 프롬프트)
|
|
102
143
|
- `.claude/agents/explore.md` - 코드베이스 탐색 전문가
|
|
@@ -108,9 +149,8 @@ TDD/Review를 생략하고 핵심 단계만 수행한다.
|
|
|
108
149
|
- `.claude/agents/git-manager.md` - Git 작업 전문가
|
|
109
150
|
|
|
110
151
|
### Skills (도메인 지식)
|
|
111
|
-
- `.claude/skills/Coding
|
|
112
|
-
|
|
113
|
-
- `backend.md` - NestJS 백엔드 규칙
|
|
152
|
+
- `.claude/skills/Coding/SKILL.md` - 공통 코딩 원칙
|
|
153
|
+
- `.claude/skills/NestJS/SKILL.md` - NestJS 백엔드 규칙 (레이어, DTO, DI, 에러 핸들링)
|
|
114
154
|
- `.claude/skills/React/SKILL.md` - React 컴포넌트, 훅, 상태 관리
|
|
115
155
|
- `.claude/skills/NextJS/SKILL.md` - Next.js App Router, SSR, Server Actions
|
|
116
156
|
- `.claude/skills/TailwindCSS/SKILL.md` - Tailwind CSS 유틸리티 패턴
|
|
@@ -121,8 +161,8 @@ TDD/Review를 생략하고 핵심 단계만 수행한다.
|
|
|
121
161
|
- `.claude/skills/TypeORM/SKILL.md` - TypeORM Entity, Repository, QueryBuilder
|
|
122
162
|
- `.claude/skills/TDD/` - TDD 테스트 원칙 및 패턴
|
|
123
163
|
- `SKILL.md` - 공통 TDD 원칙
|
|
124
|
-
- `frontend.md` - React 테스트 규칙
|
|
125
|
-
- `backend.md` - NestJS 테스트 규칙
|
|
164
|
+
- `references/frontend.md` - React 테스트 규칙
|
|
165
|
+
- `references/backend.md` - NestJS 테스트 규칙
|
|
126
166
|
- `.claude/skills/DDD/SKILL.md` - DDD 전술적 패턴 (Entity, VO, Aggregate, Repository, Domain Event)
|
|
127
167
|
- `.claude/skills/Planning/SKILL.md` - 작업 계획 (티어 판단, 작업 분해, 의존성 확인)
|
|
128
168
|
- `.claude/skills/Git/SKILL.md` - Git 커밋/PR/브랜치 규칙
|
|
@@ -143,7 +183,7 @@ TDD/Review를 생략하고 핵심 단계만 수행한다.
|
|
|
143
183
|
|
|
144
184
|
---
|
|
145
185
|
|
|
146
|
-
##
|
|
186
|
+
## 6. 프로젝트별 오버라이드
|
|
147
187
|
|
|
148
188
|
프로젝트 루트에 `CLAUDE.md`가 있으면 이 글로벌 규칙보다 우선한다.
|
|
149
189
|
프로젝트별 규칙은 글로벌 규칙을 확장하되, 충돌 시 프로젝트 규칙을 따른다.
|
|
@@ -12,10 +12,10 @@ INPUT=$(cat)
|
|
|
12
12
|
|
|
13
13
|
# ─── 1. Quality Gate ───
|
|
14
14
|
cat << 'EOF'
|
|
15
|
-
[Quality Gate] 티어를 판단하고 워크플로우를 따르라.
|
|
16
|
-
- S
|
|
17
|
-
- M
|
|
18
|
-
- L
|
|
15
|
+
[Quality Gate] 티어를 판단하고 Task Header를 출력한 후 워크플로우를 따르라.
|
|
16
|
+
- S: 📋 ⚡ 📁
|
|
17
|
+
- M: 📋 ⚡ 📚 🔄 📁
|
|
18
|
+
- L: 📋 ⚡ 📚 🔄 📁 📌Plan
|
|
19
19
|
EOF
|
|
20
20
|
|
|
21
21
|
# ─── 2. Skill Detector ───
|
|
@@ -6,9 +6,9 @@ description: 공통 코딩 원칙과 패턴. 코드 작성 시 항상 참조하
|
|
|
6
6
|
# Coding Skill - 공통 원칙
|
|
7
7
|
|
|
8
8
|
이 문서는 모든 코드 작성 시 적용되는 공통 원칙을 정의한다.
|
|
9
|
-
FE/BE
|
|
10
|
-
-
|
|
11
|
-
-
|
|
9
|
+
FE/BE 상세 규칙은 기술별 스킬에서 다룬다.
|
|
10
|
+
- BE: `../NestJS/SKILL.md`, `../TypeORM/SKILL.md`
|
|
11
|
+
- FE: `../React/SKILL.md`, `../NextJS/SKILL.md`, `../TailwindCSS/SKILL.md` 등
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -208,4 +208,5 @@ try {
|
|
|
208
208
|
- [ ] 시스템 경계에서 에러 핸들링이 되어 있는가?
|
|
209
209
|
- [ ] 불필요한 복잡도가 없는가?
|
|
210
210
|
- [ ] `any` 타입을 사용하지 않았는가?
|
|
211
|
-
- [ ] 기존 프로젝트 패턴과 일관성이 있는가?
|
|
211
|
+
- [ ] 기존 프로젝트 패턴과 일관성이 있는가?
|
|
212
|
+
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: nestjs
|
|
3
|
+
description: NestJS 백엔드 개발 가이드. 레이어별 책임, DTO, 에러 핸들링, DI, 네이밍 컨벤션 등 NestJS 코드 작성 시 참조한다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# NestJS Skill - 백엔드 핵심 규칙
|
|
2
7
|
|
|
3
8
|
NestJS 백엔드 코드에 적용되는 규칙이다.
|
|
4
|
-
공통 원칙은
|
|
9
|
+
공통 코딩 원칙은 `../Coding/SKILL.md`를 함께 참고한다.
|
|
5
10
|
|
|
6
11
|
---
|
|
7
12
|
|
|
@@ -98,307 +98,7 @@ export function UserTabs() {
|
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
|
-
## 3.
|
|
102
|
-
|
|
103
|
-
### Server Component에서 직접 fetch
|
|
104
|
-
- Server Component에서 `async/await`로 직접 데이터를 가져온다
|
|
105
|
-
- `useEffect`로 데이터를 가져오지 않는다
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
// Good - Server Component에서 직접 페칭
|
|
109
|
-
export default async function UserPage({ params }: { params: { id: string } }) {
|
|
110
|
-
const user = await getUser(params.id);
|
|
111
|
-
return <UserProfile user={user} />;
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Revalidation 전략
|
|
116
|
-
- **Time-based**: `next: { revalidate: 60 }` - 일정 시간마다 갱신
|
|
117
|
-
- **On-demand**: `revalidateTag()`, `revalidatePath()` - 특정 이벤트 시 갱신
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
// Time-based revalidation
|
|
121
|
-
const data = await fetch('https://api.example.com/users', {
|
|
122
|
-
next: { revalidate: 3600 }, // 1시간마다 갱신
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Tag-based revalidation
|
|
126
|
-
const data = await fetch('https://api.example.com/users', {
|
|
127
|
-
next: { tags: ['users'] },
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Server Action에서 revalidation 트리거
|
|
131
|
-
'use server';
|
|
132
|
-
import { revalidateTag } from 'next/cache';
|
|
133
|
-
|
|
134
|
-
export async function createUser(formData: FormData) {
|
|
135
|
-
await saveUser(formData);
|
|
136
|
-
revalidateTag('users');
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## 4. Route Handlers
|
|
143
|
-
|
|
144
|
-
### 위치 및 구조
|
|
145
|
-
- `app/api/` 디렉토리 하위에 `route.ts` 파일로 정의한다
|
|
146
|
-
- HTTP 메서드를 named export로 정의한다
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// app/api/users/route.ts
|
|
150
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
151
|
-
|
|
152
|
-
export async function GET(request: NextRequest) {
|
|
153
|
-
const users = await getUsers();
|
|
154
|
-
return NextResponse.json(users);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function POST(request: NextRequest) {
|
|
158
|
-
const body = await request.json();
|
|
159
|
-
const user = await createUser(body);
|
|
160
|
-
return NextResponse.json(user, { status: 201 });
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### 동적 라우트
|
|
165
|
-
```typescript
|
|
166
|
-
// app/api/users/[id]/route.ts
|
|
167
|
-
export async function GET(
|
|
168
|
-
request: NextRequest,
|
|
169
|
-
{ params }: { params: { id: string } },
|
|
170
|
-
) {
|
|
171
|
-
const user = await getUser(params.id);
|
|
172
|
-
if (!user) {
|
|
173
|
-
return NextResponse.json({ error: 'Not Found' }, { status: 404 });
|
|
174
|
-
}
|
|
175
|
-
return NextResponse.json(user);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export async function PUT(
|
|
179
|
-
request: NextRequest,
|
|
180
|
-
{ params }: { params: { id: string } },
|
|
181
|
-
) {
|
|
182
|
-
const body = await request.json();
|
|
183
|
-
const user = await updateUser(params.id, body);
|
|
184
|
-
return NextResponse.json(user);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function DELETE(
|
|
188
|
-
request: NextRequest,
|
|
189
|
-
{ params }: { params: { id: string } },
|
|
190
|
-
) {
|
|
191
|
-
await deleteUser(params.id);
|
|
192
|
-
return new NextResponse(null, { status: 204 });
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
---
|
|
197
|
-
|
|
198
|
-
## 5. Middleware
|
|
199
|
-
|
|
200
|
-
### 용도
|
|
201
|
-
- 인증/인가 체크
|
|
202
|
-
- 리다이렉트 처리
|
|
203
|
-
- 요청/응답 헤더 수정
|
|
204
|
-
- 국제화(i18n) 라우팅
|
|
205
|
-
|
|
206
|
-
### 작성 패턴
|
|
207
|
-
```typescript
|
|
208
|
-
// middleware.ts (프로젝트 루트)
|
|
209
|
-
import { NextResponse } from 'next/server';
|
|
210
|
-
import type { NextRequest } from 'next/server';
|
|
211
|
-
|
|
212
|
-
export function middleware(request: NextRequest) {
|
|
213
|
-
const token = request.cookies.get('token')?.value;
|
|
214
|
-
|
|
215
|
-
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
216
|
-
return NextResponse.redirect(new URL('/login', request.url));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return NextResponse.next();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export const config = {
|
|
223
|
-
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
|
224
|
-
};
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## 6. Metadata
|
|
230
|
-
|
|
231
|
-
### 정적 Metadata
|
|
232
|
-
```typescript
|
|
233
|
-
// app/about/page.tsx
|
|
234
|
-
import type { Metadata } from 'next';
|
|
235
|
-
|
|
236
|
-
export const metadata: Metadata = {
|
|
237
|
-
title: '소개 - 서비스명',
|
|
238
|
-
description: '서비스 소개 페이지입니다.',
|
|
239
|
-
};
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### 동적 Metadata
|
|
243
|
-
```typescript
|
|
244
|
-
// app/users/[id]/page.tsx
|
|
245
|
-
import type { Metadata } from 'next';
|
|
246
|
-
|
|
247
|
-
export async function generateMetadata({
|
|
248
|
-
params,
|
|
249
|
-
}: {
|
|
250
|
-
params: { id: string };
|
|
251
|
-
}): Promise<Metadata> {
|
|
252
|
-
const user = await getUser(params.id);
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
title: `${user.name} - 프로필`,
|
|
256
|
-
description: `${user.name}의 프로필 페이지입니다.`,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
## 7. Server Actions
|
|
264
|
-
|
|
265
|
-
### 정의 및 사용
|
|
266
|
-
- `'use server'` 지시문으로 서버 액션을 정의한다
|
|
267
|
-
- form의 `action` 속성이나 이벤트 핸들러에서 호출한다
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// actions/user.ts
|
|
271
|
-
'use server';
|
|
272
|
-
|
|
273
|
-
import { revalidatePath } from 'next/cache';
|
|
274
|
-
|
|
275
|
-
export async function createUser(formData: FormData) {
|
|
276
|
-
const name = formData.get('name') as string;
|
|
277
|
-
const email = formData.get('email') as string;
|
|
278
|
-
|
|
279
|
-
await db.user.create({ data: { name, email } });
|
|
280
|
-
revalidatePath('/users');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function deleteUser(userId: string) {
|
|
284
|
-
await db.user.delete({ where: { id: userId } });
|
|
285
|
-
revalidatePath('/users');
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Form에서 사용
|
|
290
|
-
```typescript
|
|
291
|
-
// components/CreateUserForm.tsx
|
|
292
|
-
import { createUser } from '@/actions/user';
|
|
293
|
-
|
|
294
|
-
export function CreateUserForm() {
|
|
295
|
-
return (
|
|
296
|
-
<form action={createUser}>
|
|
297
|
-
<input name="name" type="text" required />
|
|
298
|
-
<input name="email" type="email" required />
|
|
299
|
-
<button type="submit">생성</button>
|
|
300
|
-
</form>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## 8. 디렉토리 구조 패턴
|
|
308
|
-
|
|
309
|
-
### Route Groups - `(group)`
|
|
310
|
-
- URL에 영향을 주지 않고 라우트를 그룹화한다
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
app/
|
|
314
|
-
(marketing)/
|
|
315
|
-
about/page.tsx # /about
|
|
316
|
-
blog/page.tsx # /blog
|
|
317
|
-
(dashboard)/
|
|
318
|
-
layout.tsx # dashboard 전용 레이아웃
|
|
319
|
-
settings/page.tsx # /settings
|
|
320
|
-
profile/page.tsx # /profile
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Private Folders - `_folder`
|
|
324
|
-
- 라우팅에서 제외되는 내부 폴더
|
|
325
|
-
|
|
326
|
-
```
|
|
327
|
-
app/
|
|
328
|
-
_components/ # 라우팅에 포함되지 않음
|
|
329
|
-
Header.tsx
|
|
330
|
-
Footer.tsx
|
|
331
|
-
_lib/ # 내부 유틸리티
|
|
332
|
-
utils.ts
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Parallel Routes - `@slot`
|
|
336
|
-
- 동일 레이아웃에서 여러 페이지를 동시에 렌더링한다
|
|
337
|
-
|
|
338
|
-
```
|
|
339
|
-
app/
|
|
340
|
-
layout.tsx # children + @analytics + @team 동시 렌더링
|
|
341
|
-
@analytics/
|
|
342
|
-
page.tsx
|
|
343
|
-
@team/
|
|
344
|
-
page.tsx
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Intercepting Routes - `(.)`, `(..)`, `(...)`
|
|
348
|
-
- 현재 레이아웃 내에서 다른 라우트를 가로채서 표시한다
|
|
349
|
-
|
|
350
|
-
```
|
|
351
|
-
app/
|
|
352
|
-
feed/
|
|
353
|
-
page.tsx
|
|
354
|
-
(..)photo/[id]/ # /photo/:id를 모달로 가로챔
|
|
355
|
-
page.tsx
|
|
356
|
-
photo/[id]/
|
|
357
|
-
page.tsx # 직접 접근 시 전체 페이지
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
## 9. Image/Font 최적화
|
|
363
|
-
|
|
364
|
-
### next/image
|
|
365
|
-
- 모든 이미지는 `next/image`를 사용한다
|
|
366
|
-
- `width`, `height`를 명시하거나 `fill` 속성을 사용한다
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
import Image from 'next/image';
|
|
370
|
-
|
|
371
|
-
// 크기 지정
|
|
372
|
-
<Image src="/hero.png" alt="히어로 이미지" width={800} height={400} />
|
|
373
|
-
|
|
374
|
-
// fill 모드 (부모 기준 채움)
|
|
375
|
-
<div className="relative h-64 w-full">
|
|
376
|
-
<Image src="/banner.png" alt="배너" fill className="object-cover" />
|
|
377
|
-
</div>
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### next/font
|
|
381
|
-
- Google Fonts는 `next/font/google`을 사용한다
|
|
382
|
-
- 커스텀 폰트는 `next/font/local`을 사용한다
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
// app/layout.tsx
|
|
386
|
-
import { Inter } from 'next/font/google';
|
|
387
|
-
|
|
388
|
-
const inter = Inter({ subsets: ['latin'] });
|
|
389
|
-
|
|
390
|
-
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
391
|
-
return (
|
|
392
|
-
<html lang="ko" className={inter.className}>
|
|
393
|
-
<body>{children}</body>
|
|
394
|
-
</html>
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## 10. 네이밍 컨벤션
|
|
101
|
+
## 3. 네이밍 컨벤션
|
|
402
102
|
|
|
403
103
|
| 대상 | 규칙 | 예시 |
|
|
404
104
|
|------|------|------|
|
|
@@ -416,7 +116,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
416
116
|
|
|
417
117
|
---
|
|
418
118
|
|
|
419
|
-
##
|
|
119
|
+
## 4. 금지 사항
|
|
420
120
|
|
|
421
121
|
- Client Component에서 무거운 데이터 페칭 로직 작성 금지
|
|
422
122
|
- 불필요한 `'use client'` 선언 금지 - 서버에서 가능하면 서버에서 처리
|
|
@@ -426,4 +126,14 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
426
126
|
- `<img>` 태그 직접 사용 금지 - `next/image` 사용
|
|
427
127
|
- 외부 폰트를 `<link>` 태그로 로드 금지 - `next/font` 사용
|
|
428
128
|
- `router.push()`를 Server Component에서 사용 금지 - `redirect()` 사용
|
|
429
|
-
- API Route에서 비즈니스 로직 직접 구현 금지 - 별도 서비스 레이어로 분리
|
|
129
|
+
- API Route에서 비즈니스 로직 직접 구현 금지 - 별도 서비스 레이어로 분리
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 심화 참조
|
|
134
|
+
|
|
135
|
+
| 파일 | 내용 |
|
|
136
|
+
|------|------|
|
|
137
|
+
| `references/data-fetching.md` | 데이터 페칭 (Server Component fetch, Revalidation) + Route Handlers (CRUD, 동적 라우트) |
|
|
138
|
+
| `references/middleware-actions.md` | Middleware (인증, 리다이렉트, matcher) + Server Actions (form action, revalidation) |
|
|
139
|
+
| `references/optimization.md` | Metadata (정적/동적) + 디렉토리 구조 패턴 (Route Groups, Parallel/Intercepting Routes) + Image/Font 최적화 |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 데이터 페칭 & Route Handlers
|
|
2
|
+
|
|
3
|
+
## 데이터 페칭
|
|
4
|
+
|
|
5
|
+
### Server Component에서 직접 fetch
|
|
6
|
+
- Server Component에서 `async/await`로 직접 데이터를 가져온다
|
|
7
|
+
- `useEffect`로 데이터를 가져오지 않는다
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Good - Server Component에서 직접 페칭
|
|
11
|
+
export default async function UserPage({ params }: { params: { id: string } }) {
|
|
12
|
+
const user = await getUser(params.id);
|
|
13
|
+
return <UserProfile user={user} />;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Revalidation 전략
|
|
18
|
+
- **Time-based**: `next: { revalidate: 60 }` - 일정 시간마다 갱신
|
|
19
|
+
- **On-demand**: `revalidateTag()`, `revalidatePath()` - 특정 이벤트 시 갱신
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Time-based revalidation
|
|
23
|
+
const data = await fetch('https://api.example.com/users', {
|
|
24
|
+
next: { revalidate: 3600 }, // 1시간마다 갱신
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Tag-based revalidation
|
|
28
|
+
const data = await fetch('https://api.example.com/users', {
|
|
29
|
+
next: { tags: ['users'] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Server Action에서 revalidation 트리거
|
|
33
|
+
'use server';
|
|
34
|
+
import { revalidateTag } from 'next/cache';
|
|
35
|
+
|
|
36
|
+
export async function createUser(formData: FormData) {
|
|
37
|
+
await saveUser(formData);
|
|
38
|
+
revalidateTag('users');
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Route Handlers
|
|
45
|
+
|
|
46
|
+
### 위치 및 구조
|
|
47
|
+
- `app/api/` 디렉토리 하위에 `route.ts` 파일로 정의한다
|
|
48
|
+
- HTTP 메서드를 named export로 정의한다
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// app/api/users/route.ts
|
|
52
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
53
|
+
|
|
54
|
+
export async function GET(request: NextRequest) {
|
|
55
|
+
const users = await getUsers();
|
|
56
|
+
return NextResponse.json(users);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function POST(request: NextRequest) {
|
|
60
|
+
const body = await request.json();
|
|
61
|
+
const user = await createUser(body);
|
|
62
|
+
return NextResponse.json(user, { status: 201 });
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 동적 라우트
|
|
67
|
+
```typescript
|
|
68
|
+
// app/api/users/[id]/route.ts
|
|
69
|
+
export async function GET(
|
|
70
|
+
request: NextRequest,
|
|
71
|
+
{ params }: { params: { id: string } },
|
|
72
|
+
) {
|
|
73
|
+
const user = await getUser(params.id);
|
|
74
|
+
if (!user) {
|
|
75
|
+
return NextResponse.json({ error: 'Not Found' }, { status: 404 });
|
|
76
|
+
}
|
|
77
|
+
return NextResponse.json(user);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function PUT(
|
|
81
|
+
request: NextRequest,
|
|
82
|
+
{ params }: { params: { id: string } },
|
|
83
|
+
) {
|
|
84
|
+
const body = await request.json();
|
|
85
|
+
const user = await updateUser(params.id, body);
|
|
86
|
+
return NextResponse.json(user);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function DELETE(
|
|
90
|
+
request: NextRequest,
|
|
91
|
+
{ params }: { params: { id: string } },
|
|
92
|
+
) {
|
|
93
|
+
await deleteUser(params.id);
|
|
94
|
+
return new NextResponse(null, { status: 204 });
|
|
95
|
+
}
|
|
96
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Middleware & Server Actions
|
|
2
|
+
|
|
3
|
+
## Middleware
|
|
4
|
+
|
|
5
|
+
### 용도
|
|
6
|
+
- 인증/인가 체크
|
|
7
|
+
- 리다이렉트 처리
|
|
8
|
+
- 요청/응답 헤더 수정
|
|
9
|
+
- 국제화(i18n) 라우팅
|
|
10
|
+
|
|
11
|
+
### 작성 패턴
|
|
12
|
+
```typescript
|
|
13
|
+
// middleware.ts (프로젝트 루트)
|
|
14
|
+
import { NextResponse } from 'next/server';
|
|
15
|
+
import type { NextRequest } from 'next/server';
|
|
16
|
+
|
|
17
|
+
export function middleware(request: NextRequest) {
|
|
18
|
+
const token = request.cookies.get('token')?.value;
|
|
19
|
+
|
|
20
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
21
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return NextResponse.next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const config = {
|
|
28
|
+
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Server Actions
|
|
35
|
+
|
|
36
|
+
### 정의 및 사용
|
|
37
|
+
- `'use server'` 지시문으로 서버 액션을 정의한다
|
|
38
|
+
- form의 `action` 속성이나 이벤트 핸들러에서 호출한다
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// actions/user.ts
|
|
42
|
+
'use server';
|
|
43
|
+
|
|
44
|
+
import { revalidatePath } from 'next/cache';
|
|
45
|
+
|
|
46
|
+
export async function createUser(formData: FormData) {
|
|
47
|
+
const name = formData.get('name') as string;
|
|
48
|
+
const email = formData.get('email') as string;
|
|
49
|
+
|
|
50
|
+
await db.user.create({ data: { name, email } });
|
|
51
|
+
revalidatePath('/users');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function deleteUser(userId: string) {
|
|
55
|
+
await db.user.delete({ where: { id: userId } });
|
|
56
|
+
revalidatePath('/users');
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Form에서 사용
|
|
61
|
+
```typescript
|
|
62
|
+
// components/CreateUserForm.tsx
|
|
63
|
+
import { createUser } from '@/actions/user';
|
|
64
|
+
|
|
65
|
+
export function CreateUserForm() {
|
|
66
|
+
return (
|
|
67
|
+
<form action={createUser}>
|
|
68
|
+
<input name="name" type="text" required />
|
|
69
|
+
<input name="email" type="email" required />
|
|
70
|
+
<button type="submit">생성</button>
|
|
71
|
+
</form>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|