create-claude-pipeline 0.1.0
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/bin/cli.js +359 -0
- package/package.json +32 -0
- package/template/.claude/agents/be-developer.md +218 -0
- package/template/.claude/agents/designer.md +192 -0
- package/template/.claude/agents/fe-developer.md +175 -0
- package/template/.claude/agents/infra-developer.md +270 -0
- package/template/.claude/agents/planner.md +126 -0
- package/template/.claude/agents/pm.md +130 -0
- package/template/.claude/agents/qa-engineer.md +270 -0
- package/template/.claude/agents/security-reviewer.md +281 -0
- package/template/.claude/settings.json +5 -0
- package/template/.claude/skills/analyze-requirements/SKILL.md +166 -0
- package/template/.claude/skills/api-integration/SKILL.md +354 -0
- package/template/.claude/skills/assemble-context/SKILL.md +192 -0
- package/template/.claude/skills/db-migration/SKILL.md +228 -0
- package/template/.claude/skills/explore-be-codebase/SKILL.md +260 -0
- package/template/.claude/skills/explore-codebase/SKILL.md +190 -0
- package/template/.claude/skills/explore-design-system/SKILL.md +150 -0
- package/template/.claude/skills/explore-fe-codebase/SKILL.md +209 -0
- package/template/.claude/skills/explore-implementation/SKILL.md +147 -0
- package/template/.claude/skills/explore-infra/SKILL.md +242 -0
- package/template/.claude/skills/implement-api/SKILL.md +477 -0
- package/template/.claude/skills/implement-components/SKILL.md +217 -0
- package/template/.claude/skills/review-auth/SKILL.md +175 -0
- package/template/.claude/skills/scan-vulnerabilities/SKILL.md +200 -0
- package/template/.claude/skills/write-cicd/SKILL.md +293 -0
- package/template/.claude/skills/write-design-spec/SKILL.md +363 -0
- package/template/.claude/skills/write-dockerfile/SKILL.md +269 -0
- package/template/.claude/skills/write-plan-doc/SKILL.md +164 -0
- package/template/.claude/skills/write-plan-doc/assets/plan_template.html +251 -0
- package/template/.claude/skills/write-qa-report/SKILL.md +151 -0
- package/template/.claude/skills/write-security-report/SKILL.md +185 -0
- package/template/.claude/skills/write-test-cases/SKILL.md +234 -0
- package/template/.claude-pipeline/dashboard/.env.example +1 -0
- package/template/.claude-pipeline/dashboard/.eslintrc.json +3 -0
- package/template/.claude-pipeline/dashboard/README.md +36 -0
- package/template/.claude-pipeline/dashboard/next.config.mjs +6 -0
- package/template/.claude-pipeline/dashboard/package-lock.json +8148 -0
- package/template/.claude-pipeline/dashboard/package.json +36 -0
- package/template/.claude-pipeline/dashboard/postcss.config.mjs +8 -0
- package/template/.claude-pipeline/dashboard/server.ts +24 -0
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/checkpoint/route.ts +23 -0
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/outputs/[...filepath]/route.ts +18 -0
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/[id]/route.ts +10 -0
- package/template/.claude-pipeline/dashboard/src/app/api/pipelines/route.ts +64 -0
- package/template/.claude-pipeline/dashboard/src/app/favicon.ico +0 -0
- package/template/.claude-pipeline/dashboard/src/app/fonts/GeistMonoVF.woff +0 -0
- package/template/.claude-pipeline/dashboard/src/app/fonts/GeistVF.woff +0 -0
- package/template/.claude-pipeline/dashboard/src/app/globals.css +52 -0
- package/template/.claude-pipeline/dashboard/src/app/layout.tsx +33 -0
- package/template/.claude-pipeline/dashboard/src/app/page.tsx +49 -0
- package/template/.claude-pipeline/dashboard/src/app/pipeline/[id]/page.tsx +84 -0
- package/template/.claude-pipeline/dashboard/src/components/agent-card.tsx +40 -0
- package/template/.claude-pipeline/dashboard/src/components/agent-logs.tsx +65 -0
- package/template/.claude-pipeline/dashboard/src/components/artifact-viewer.tsx +130 -0
- package/template/.claude-pipeline/dashboard/src/components/checkpoint-banner.tsx +59 -0
- package/template/.claude-pipeline/dashboard/src/components/new-pipeline-modal.tsx +63 -0
- package/template/.claude-pipeline/dashboard/src/components/output-list.tsx +57 -0
- package/template/.claude-pipeline/dashboard/src/components/phase-dots.tsx +37 -0
- package/template/.claude-pipeline/dashboard/src/components/pipeline-card.tsx +53 -0
- package/template/.claude-pipeline/dashboard/src/components/resizable-panels.tsx +91 -0
- package/template/.claude-pipeline/dashboard/src/hooks/use-pipeline-detail.ts +65 -0
- package/template/.claude-pipeline/dashboard/src/hooks/use-pipelines.ts +60 -0
- package/template/.claude-pipeline/dashboard/src/hooks/use-websocket.ts +58 -0
- package/template/.claude-pipeline/dashboard/src/lib/agents.ts +30 -0
- package/template/.claude-pipeline/dashboard/src/lib/checkpoint.ts +37 -0
- package/template/.claude-pipeline/dashboard/src/lib/pipelines.ts +91 -0
- package/template/.claude-pipeline/dashboard/src/lib/watcher.ts +90 -0
- package/template/.claude-pipeline/dashboard/src/lib/ws-server.ts +123 -0
- package/template/.claude-pipeline/dashboard/src/types/pipeline.ts +61 -0
- package/template/.claude-pipeline/dashboard/tailwind.config.ts +31 -0
- package/template/.claude-pipeline/dashboard/tsconfig.json +26 -0
- package/template/CLAUDE.md +301 -0
- package/template/references/context-structure.md +34 -0
- package/template/references/pm-context-assembly.md +34 -0
- package/template/references/task-context-template.md +65 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-dockerfile
|
|
3
|
+
description: "인프라 엔지니어가 FE/BE용 Dockerfile을 작성할 때 멀티 스테이지 빌드와 레이어 캐싱 최적화를 적용하기 위해 참조하는 skill. Infra Agent가 Dockerfile을 새로 작성하거나, 기존 Dockerfile을 개선하거나, 컨테이너 이미지를 최적화할 때 반드시 사용한다. 'Dockerfile 작성', 'Docker 이미지 빌드', '컨테이너화', '멀티 스테이지 빌드', '이미지 크기 최적화', '.dockerignore', 'HEALTHCHECK' 등의 상황에서 트리거된다."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write Dockerfile
|
|
7
|
+
|
|
8
|
+
인프라 엔지니어가 FE/BE 서비스용 Dockerfile을 작성할 때 따르는 패턴이다.
|
|
9
|
+
|
|
10
|
+
Dockerfile은 한 번 작성하면 모든 빌드와 배포에서 반복 실행된다. 잘못된 레이어 순서는 매 빌드마다 불필요한 재설치를 유발하고, 단일 스테이지 빌드는 프로덕션 이미지에 빌드 도구를 포함시켜 이미지 크기와 보안 위험을 키운다. 이 skill은 "빌드는 빠르게, 이미지는 작게, 실행은 안전하게"를 달성하는 패턴을 제공한다.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 멀티 스테이지 빌드 구조
|
|
15
|
+
|
|
16
|
+
하나의 Dockerfile을 3개 스테이지로 나눈다. 각 스테이지는 자기 역할만 수행하고, 최종 이미지(runner)에는 실행에 필요한 것만 남긴다. 이렇게 하면 빌드 도구(TypeScript 컴파일러, devDependencies 등)가 프로덕션 이미지에 들어가지 않아 이미지 크기가 줄고 공격 표면이 줄어든다.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Stage 1 (deps) → 의존성 설치만
|
|
20
|
+
Stage 2 (builder) → 빌드만
|
|
21
|
+
Stage 3 (runner) → 실행에 필요한 것만 복사
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Next.js (FE) Dockerfile
|
|
25
|
+
|
|
26
|
+
```dockerfile
|
|
27
|
+
# ──── Stage 1: 의존성 설치 ────
|
|
28
|
+
FROM node:20-alpine AS deps
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
|
|
31
|
+
# 의존성 파일만 먼저 복사 — 소스가 바뀌어도 이 레이어는 캐시됨
|
|
32
|
+
COPY package.json package-lock.json ./
|
|
33
|
+
RUN npm ci --ignore-scripts
|
|
34
|
+
|
|
35
|
+
# ──── Stage 2: 빌드 ────
|
|
36
|
+
FROM node:20-alpine AS builder
|
|
37
|
+
WORKDIR /app
|
|
38
|
+
|
|
39
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
40
|
+
COPY . .
|
|
41
|
+
|
|
42
|
+
# Next.js standalone 모드 — 필요한 파일만 .next/standalone에 출력
|
|
43
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
44
|
+
RUN npm run build
|
|
45
|
+
|
|
46
|
+
# ──── Stage 3: 실행 ────
|
|
47
|
+
FROM node:20-alpine AS runner
|
|
48
|
+
WORKDIR /app
|
|
49
|
+
|
|
50
|
+
ENV NODE_ENV=production
|
|
51
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
52
|
+
|
|
53
|
+
# 보안: root가 아닌 전용 유저로 실행
|
|
54
|
+
RUN addgroup --system --gid 1001 nodejs \
|
|
55
|
+
&& adduser --system --uid 1001 nextjs
|
|
56
|
+
|
|
57
|
+
# standalone 빌드 결과만 복사
|
|
58
|
+
COPY --from=builder /app/public ./public
|
|
59
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
60
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
61
|
+
|
|
62
|
+
USER nextjs
|
|
63
|
+
|
|
64
|
+
EXPOSE 3000
|
|
65
|
+
ENV PORT=3000
|
|
66
|
+
ENV HOSTNAME="0.0.0.0"
|
|
67
|
+
|
|
68
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
69
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
|
|
70
|
+
|
|
71
|
+
CMD ["node", "server.js"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Express/Fastify (BE) Dockerfile
|
|
75
|
+
|
|
76
|
+
```dockerfile
|
|
77
|
+
# ──── Stage 1: 의존성 설치 ────
|
|
78
|
+
FROM node:20-alpine AS deps
|
|
79
|
+
WORKDIR /app
|
|
80
|
+
|
|
81
|
+
COPY package.json package-lock.json ./
|
|
82
|
+
RUN npm ci --ignore-scripts
|
|
83
|
+
|
|
84
|
+
# ──── Stage 2: 빌드 ────
|
|
85
|
+
FROM node:20-alpine AS builder
|
|
86
|
+
WORKDIR /app
|
|
87
|
+
|
|
88
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
89
|
+
COPY . .
|
|
90
|
+
|
|
91
|
+
RUN npm run build
|
|
92
|
+
|
|
93
|
+
# ──── Stage 3: 실행 ────
|
|
94
|
+
FROM node:20-alpine AS runner
|
|
95
|
+
WORKDIR /app
|
|
96
|
+
|
|
97
|
+
ENV NODE_ENV=production
|
|
98
|
+
|
|
99
|
+
# 보안: 전용 유저 생성
|
|
100
|
+
RUN addgroup --system --gid 1001 appgroup \
|
|
101
|
+
&& adduser --system --uid 1001 appuser
|
|
102
|
+
|
|
103
|
+
# 프로덕션 의존성만 설치 — devDependencies 제외
|
|
104
|
+
COPY package.json package-lock.json ./
|
|
105
|
+
RUN npm ci --ignore-scripts --omit=dev
|
|
106
|
+
|
|
107
|
+
# 빌드 결과물만 복사
|
|
108
|
+
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
|
|
109
|
+
|
|
110
|
+
# Prisma 사용 시 — 스키마와 클라이언트도 복사
|
|
111
|
+
# COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
|
112
|
+
# COPY --from=builder /app/prisma ./prisma
|
|
113
|
+
|
|
114
|
+
USER appuser
|
|
115
|
+
|
|
116
|
+
EXPOSE 4000
|
|
117
|
+
ENV PORT=4000
|
|
118
|
+
|
|
119
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
120
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:4000/health || exit 1
|
|
121
|
+
|
|
122
|
+
CMD ["node", "dist/index.js"]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 레이어 캐싱 최적화
|
|
128
|
+
|
|
129
|
+
Docker는 각 명령어를 레이어로 캐싱한다. 레이어가 변경되면 그 이후 모든 레이어가 무효화된다. 따라서 변경 빈도가 낮은 것을 위에, 높은 것을 아래에 배치해야 캐시 히트율이 높아진다.
|
|
130
|
+
|
|
131
|
+
### 올바른 순서
|
|
132
|
+
|
|
133
|
+
```dockerfile
|
|
134
|
+
# 1. 베이스 이미지 — 거의 안 바뀜
|
|
135
|
+
FROM node:20-alpine
|
|
136
|
+
|
|
137
|
+
# 2. 시스템 패키지 — 거의 안 바뀜
|
|
138
|
+
RUN apk add --no-cache curl
|
|
139
|
+
|
|
140
|
+
# 3. 의존성 파일 복사 — 패키지 추가/삭제 시에만 바뀜
|
|
141
|
+
COPY package.json package-lock.json ./
|
|
142
|
+
|
|
143
|
+
# 4. 의존성 설치 — 3이 바뀔 때만 재실행
|
|
144
|
+
RUN npm ci
|
|
145
|
+
|
|
146
|
+
# 5. 소스 코드 복사 — 코드 수정마다 바뀜
|
|
147
|
+
COPY . .
|
|
148
|
+
|
|
149
|
+
# 6. 빌드 — 5가 바뀔 때마다 재실행
|
|
150
|
+
RUN npm run build
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 흔한 실수
|
|
154
|
+
|
|
155
|
+
```dockerfile
|
|
156
|
+
# ❌ 나쁜 예: 소스를 먼저 복사하면 코드 한 줄 바꿔도 npm ci가 다시 실행됨
|
|
157
|
+
COPY . .
|
|
158
|
+
RUN npm ci
|
|
159
|
+
RUN npm run build
|
|
160
|
+
|
|
161
|
+
# ✅ 좋은 예: 의존성 파일만 먼저 복사
|
|
162
|
+
COPY package.json package-lock.json ./
|
|
163
|
+
RUN npm ci
|
|
164
|
+
COPY . .
|
|
165
|
+
RUN npm run build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 보안 원칙
|
|
171
|
+
|
|
172
|
+
### root 유저로 실행하지 않기
|
|
173
|
+
|
|
174
|
+
컨테이너가 root로 실행되면 취약점을 통해 호스트 시스템까지 영향을 받을 수 있다. 전용 유저를 생성하고 해당 유저로 실행한다.
|
|
175
|
+
|
|
176
|
+
```dockerfile
|
|
177
|
+
RUN addgroup --system --gid 1001 appgroup \
|
|
178
|
+
&& adduser --system --uid 1001 appuser
|
|
179
|
+
USER appuser
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 시크릿을 이미지에 넣지 않기
|
|
183
|
+
|
|
184
|
+
`ARG`나 `ENV`로 시크릿을 전달하면 이미지 레이어에 기록되어 누구나 `docker history`로 볼 수 있다.
|
|
185
|
+
|
|
186
|
+
```dockerfile
|
|
187
|
+
# ❌ 시크릿이 이미지에 기록됨
|
|
188
|
+
ARG DATABASE_URL
|
|
189
|
+
ENV DATABASE_URL=$DATABASE_URL
|
|
190
|
+
|
|
191
|
+
# ✅ 런타임에 환경변수로 주입
|
|
192
|
+
# docker run -e DATABASE_URL=... my-image
|
|
193
|
+
# 또는 docker-compose의 env_file 사용
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
빌드 시 시크릿이 필요한 경우(예: private npm registry) `--mount=type=secret`을 사용한다:
|
|
197
|
+
|
|
198
|
+
```dockerfile
|
|
199
|
+
RUN --mount=type=secret,id=npmrc,target=/app/.npmrc npm ci
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### .dockerignore 반드시 작성
|
|
203
|
+
|
|
204
|
+
빌드 컨텍스트에 불필요한 파일이 포함되면 빌드 시간과 이미지 크기가 늘어나고, `.env` 같은 시크릿이 이미지에 포함될 수 있다.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
# .dockerignore
|
|
208
|
+
node_modules
|
|
209
|
+
.next
|
|
210
|
+
dist
|
|
211
|
+
.git
|
|
212
|
+
.env
|
|
213
|
+
.env.*
|
|
214
|
+
*.md
|
|
215
|
+
.vscode
|
|
216
|
+
.idea
|
|
217
|
+
coverage
|
|
218
|
+
__tests__
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 헬스체크
|
|
224
|
+
|
|
225
|
+
헬스체크가 없으면 Docker와 오케스트레이터(docker-compose, Kubernetes)가 컨테이너 상태를 알 수 없다. 서비스가 hang 상태여도 "healthy"로 판단되어 트래픽이 계속 들어온다.
|
|
226
|
+
|
|
227
|
+
```dockerfile
|
|
228
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
229
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:PORT/health || exit 1
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| 옵션 | 값 | 설명 |
|
|
233
|
+
|------|-----|------|
|
|
234
|
+
| `--interval` | 30s | 체크 간격 |
|
|
235
|
+
| `--timeout` | 3s | 응답 대기 시간 |
|
|
236
|
+
| `--start-period` | 10s | 시작 후 유예 시간 (앱 초기화 대기) |
|
|
237
|
+
| `--retries` | 3 | 연속 실패 시 unhealthy 판정 |
|
|
238
|
+
|
|
239
|
+
alpine 이미지에는 `curl`이 없으므로 `wget`을 사용한다. curl이 필요하면 `apk add --no-cache curl`로 설치한다.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 이미지 크기 최소화
|
|
244
|
+
|
|
245
|
+
| 기법 | 효과 |
|
|
246
|
+
|------|------|
|
|
247
|
+
| `alpine` 기반 이미지 사용 | ~120MB → ~50MB |
|
|
248
|
+
| 멀티 스테이지로 빌드 도구 제외 | devDependencies, TypeScript 등 제거 |
|
|
249
|
+
| `npm ci --omit=dev` (runner) | 프로덕션 의존성만 설치 |
|
|
250
|
+
| Next.js `standalone` 모드 | 필요한 node_modules만 포함 |
|
|
251
|
+
| `.dockerignore`로 불필요 파일 제외 | 빌드 컨텍스트 축소 |
|
|
252
|
+
| `--no-cache` 플래그로 apk 캐시 제거 | 패키지 매니저 캐시 제거 |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Dockerfile 작성 체크리스트
|
|
257
|
+
|
|
258
|
+
Dockerfile을 작성한 후 아래를 확인한다:
|
|
259
|
+
|
|
260
|
+
1. [ ] **멀티 스테이지** — deps / builder / runner 3단계 구성
|
|
261
|
+
2. [ ] **레이어 캐싱** — package.json 먼저 복사 → npm ci → 소스 복사 순서
|
|
262
|
+
3. [ ] **alpine 이미지** — `node:XX-alpine` 사용
|
|
263
|
+
4. [ ] **비root 유저** — `adduser` + `USER` 지시어 설정
|
|
264
|
+
5. [ ] **시크릿 미포함** — ARG/ENV에 비밀번호, API 키 없음
|
|
265
|
+
6. [ ] **.dockerignore** — node_modules, .env, .git 등 제외
|
|
266
|
+
7. [ ] **HEALTHCHECK** — 헬스체크 엔드포인트 설정
|
|
267
|
+
8. [ ] **EXPOSE** — 사용하는 포트 명시
|
|
268
|
+
9. [ ] **ENV NODE_ENV=production** — runner 스테이지에 설정
|
|
269
|
+
10. [ ] **이미지 빌드 테스트** — `docker build` 성공 확인
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-plan-doc
|
|
3
|
+
description: "기획자 Agent가 Phase 1에서 기획안을 작성할 때 사용하는 skill. context/01_plan.md를 7개 섹션(개요, 유저 스토리, 기능 명세, 화면 목록, API 초안, 엣지케이스, 비기능 요구사항) 구조로 작성하고, 사람이 보기 편한 HTML 보고서(context/01_plan.html)도 함께 생성한다. '기획안 작성', '01_plan 생성', 'Phase 1 시작', '기획 문서 만들기' 등의 상황에서 반드시 사용한다."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write Plan Doc
|
|
7
|
+
|
|
8
|
+
기획자 Agent가 `context/01_plan.md`를 작성하고, 같은 내용을 사람이 보기 편한 `context/01_plan.html`로 변환하는 skill이다.
|
|
9
|
+
|
|
10
|
+
마크다운은 다른 Agent들이 기계적으로 읽는 용도이고, HTML은 사용자가 브라우저에서 검토하는 용도이다. 두 파일의 **내용은 동일**하되 **형식만 다르다**.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Step 1: 입력 확인
|
|
15
|
+
|
|
16
|
+
기획안을 작성하려면 `context/00_requirements.md`가 필요하다. 이 파일에서 다음을 추출한다:
|
|
17
|
+
|
|
18
|
+
- 요청 내용 (무엇을 만드는지)
|
|
19
|
+
- 작업 유형 (신규/수정/버그/리팩토링)
|
|
20
|
+
- 영향 범위 (FE/BE/Infra)
|
|
21
|
+
- 관련 기존 파일 목록
|
|
22
|
+
|
|
23
|
+
`00_requirements.md`가 없으면 작성을 중단하고 "Phase 0이 아직 완료되지 않았습니다"라고 알린다.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Step 2: context/01_plan.md 작성
|
|
28
|
+
|
|
29
|
+
아래 7개 섹션을 **이 순서대로** 작성한다. 섹션을 건너뛰지 않는다.
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
# 기획안: [기능명]
|
|
33
|
+
|
|
34
|
+
## 1. 개요
|
|
35
|
+
|
|
36
|
+
### 목적
|
|
37
|
+
(이 기능이 왜 필요한지, 어떤 문제를 해결하는지)
|
|
38
|
+
|
|
39
|
+
### 핵심 가치
|
|
40
|
+
(사용자에게 어떤 가치를 제공하는지 1~3개 bullet)
|
|
41
|
+
|
|
42
|
+
### 작업 범위
|
|
43
|
+
(이번에 포함하는 것 / 포함하지 않는 것을 명확히 구분)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. 유저 스토리
|
|
48
|
+
|
|
49
|
+
| ID | 사용자 유형 | 스토리 | 우선순위 |
|
|
50
|
+
|----|-----------|--------|---------|
|
|
51
|
+
| US-01 | ... | ...로서 ...하고 싶다, ...하기 위해 | P0/P1/P2 |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. 기능 명세
|
|
56
|
+
|
|
57
|
+
| ID | 기능명 | 설명 | 관련 스토리 | 우선순위 |
|
|
58
|
+
|----|-------|------|-----------|---------|
|
|
59
|
+
| F-01 | ... | ... | US-01 | P0/P1/P2 |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 4. 화면 목록
|
|
64
|
+
|
|
65
|
+
| ID | 화면명 | 경로 | 주요 요소 | 관련 기능 |
|
|
66
|
+
|----|-------|------|----------|----------|
|
|
67
|
+
| S-01 | ... | /path | ... | F-01 |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 5. API 초안
|
|
72
|
+
|
|
73
|
+
| Method | Path | 설명 | 인증 여부 | 관련 기능 |
|
|
74
|
+
|--------|------|------|----------|----------|
|
|
75
|
+
| POST | /api/... | ... | Y/N | F-01 |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6. 엣지케이스 & 예외 처리
|
|
80
|
+
|
|
81
|
+
| ID | 상황 | 예상 동작 | 관련 기능 |
|
|
82
|
+
|----|------|----------|----------|
|
|
83
|
+
| E-01 | ... | ... | F-01 |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 7. 비기능 요구사항
|
|
88
|
+
|
|
89
|
+
| 항목 | 기준 | 비고 |
|
|
90
|
+
|------|------|------|
|
|
91
|
+
| 성능 | ... | ... |
|
|
92
|
+
| 보안 | ... | ... |
|
|
93
|
+
| 접근성 | ... | ... |
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 작성 가이드
|
|
97
|
+
|
|
98
|
+
**ID 체계** — 각 항목에 고유 ID를 부여한다(US-01, F-01, S-01, E-01). 섹션 간 상호 참조할 때 이 ID를 사용한다. 이렇게 하면 나중에 설계/구현 단계에서 "F-03 구현해" 같이 정확하게 지칭할 수 있다.
|
|
99
|
+
|
|
100
|
+
**우선순위 기준:**
|
|
101
|
+
- **P0**: 없으면 기능이 동작하지 않는 것
|
|
102
|
+
- **P1**: 없어도 동작하지만 사용자 경험에 큰 영향
|
|
103
|
+
- **P2**: 있으면 좋은 것 (nice-to-have)
|
|
104
|
+
|
|
105
|
+
**API 초안의 수준** — 이 단계에서는 대략적인 엔드포인트와 역할만 정의한다. 요청/응답 상세 스키마는 Phase 2(BE 설계자)가 담당한다. 기획자가 API를 너무 상세하게 쓰면 설계자의 재량이 줄어들므로, 의도를 전달하는 수준으로 작성한다.
|
|
106
|
+
|
|
107
|
+
**엣지케이스** — 이 섹션이 기획의 품질을 결정한다. "해피 패스만 생각하면 안 됨"이라는 원칙을 기억하고, 최소 3~5개의 경계 조건을 도출한다. 예: 동시 접근, 빈 입력, 권한 없는 사용자, 네트워크 끊김 등.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 3: context/01_plan.html 생성
|
|
112
|
+
|
|
113
|
+
`context/01_plan.md` 작성이 완료되면, 같은 내용을 HTML 보고서로 변환한다.
|
|
114
|
+
|
|
115
|
+
### HTML 생성 방법
|
|
116
|
+
|
|
117
|
+
`assets/plan_template.html` 파일을 읽어서 템플릿으로 사용한다.
|
|
118
|
+
|
|
119
|
+
템플릿에는 두 개의 플레이스홀더가 있다:
|
|
120
|
+
- `{{PLAN_TITLE}}` → 기획안 제목 (예: "소셜 로그인 기능")
|
|
121
|
+
- `{{PLAN_CONTENT}}` → 마크다운을 HTML로 변환한 본문
|
|
122
|
+
|
|
123
|
+
**변환 규칙:**
|
|
124
|
+
1. 각 `## N. 섹션명`을 `<section>` 블록으로 감싼다
|
|
125
|
+
2. 마크다운 표를 `<table>` 태그로 변환한다
|
|
126
|
+
3. bullet 리스트를 `<ul><li>` 로 변환한다
|
|
127
|
+
4. ID(US-01, F-01 등)를 `<code>` 태그로 감싼다
|
|
128
|
+
|
|
129
|
+
변환 결과를 `context/01_plan.html`에 저장한다.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Step 4: 완료 보고
|
|
134
|
+
|
|
135
|
+
두 파일 모두 저장한 후 보고한다:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
기획안 작성 완료:
|
|
139
|
+
- context/01_plan.md (Agent용 마크다운)
|
|
140
|
+
- context/01_plan.html (브라우저에서 열어 검토)
|
|
141
|
+
|
|
142
|
+
섹션 요약:
|
|
143
|
+
1. 개요: [한 줄 요약]
|
|
144
|
+
2. 유저 스토리: N개
|
|
145
|
+
3. 기능 명세: N개
|
|
146
|
+
4. 화면 목록: N개
|
|
147
|
+
5. API 초안: N개
|
|
148
|
+
6. 엣지케이스: N개
|
|
149
|
+
7. 비기능 요구사항: N개 항목
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 품질 체크리스트
|
|
155
|
+
|
|
156
|
+
파일을 저장하기 전에 점검한다:
|
|
157
|
+
|
|
158
|
+
- [ ] 7개 섹션이 모두 있는가?
|
|
159
|
+
- [ ] 모든 항목에 고유 ID가 부여되었는가?
|
|
160
|
+
- [ ] 섹션 간 상호 참조(관련 기능, 관련 스토리)가 올바른가?
|
|
161
|
+
- [ ] 엣지케이스가 최소 3개 이상인가?
|
|
162
|
+
- [ ] API 초안이 상세 스키마 없이 의도 전달 수준인가?
|
|
163
|
+
- [ ] HTML 파일이 브라우저에서 깨지지 않는 완전한 문서인가?
|
|
164
|
+
- [ ] MD와 HTML의 내용이 일치하는가?
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{PLAN_TITLE}} — 기획안</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f8f9fa;
|
|
10
|
+
--card-bg: #ffffff;
|
|
11
|
+
--border: #e2e8f0;
|
|
12
|
+
--text: #1a202c;
|
|
13
|
+
--text-muted: #718096;
|
|
14
|
+
--primary: #3182ce;
|
|
15
|
+
--primary-light: #ebf8ff;
|
|
16
|
+
--sidebar-width: 240px;
|
|
17
|
+
--p0: #e53e3e;
|
|
18
|
+
--p1: #dd6b20;
|
|
19
|
+
--p2: #38a169;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
line-height: 1.6;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Sidebar */
|
|
32
|
+
.sidebar {
|
|
33
|
+
position: fixed;
|
|
34
|
+
top: 0;
|
|
35
|
+
left: 0;
|
|
36
|
+
width: var(--sidebar-width);
|
|
37
|
+
height: 100vh;
|
|
38
|
+
background: var(--card-bg);
|
|
39
|
+
border-right: 1px solid var(--border);
|
|
40
|
+
padding: 24px 16px;
|
|
41
|
+
overflow-y: auto;
|
|
42
|
+
z-index: 10;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sidebar h2 {
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
letter-spacing: 0.05em;
|
|
49
|
+
color: var(--text-muted);
|
|
50
|
+
margin-bottom: 16px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sidebar nav a {
|
|
54
|
+
display: block;
|
|
55
|
+
padding: 8px 12px;
|
|
56
|
+
margin-bottom: 4px;
|
|
57
|
+
color: var(--text);
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
transition: background 0.15s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sidebar nav a:hover,
|
|
65
|
+
.sidebar nav a.active {
|
|
66
|
+
background: var(--primary-light);
|
|
67
|
+
color: var(--primary);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Main */
|
|
71
|
+
.main {
|
|
72
|
+
margin-left: var(--sidebar-width);
|
|
73
|
+
padding: 40px 48px;
|
|
74
|
+
max-width: 960px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.main h1 {
|
|
78
|
+
font-size: 28px;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
margin-bottom: 8px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.main .subtitle {
|
|
84
|
+
color: var(--text-muted);
|
|
85
|
+
font-size: 14px;
|
|
86
|
+
margin-bottom: 32px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Section cards */
|
|
90
|
+
section {
|
|
91
|
+
background: var(--card-bg);
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
border-radius: 12px;
|
|
94
|
+
padding: 28px 32px;
|
|
95
|
+
margin-bottom: 24px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
section h2 {
|
|
99
|
+
font-size: 20px;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
margin-bottom: 16px;
|
|
102
|
+
padding-bottom: 8px;
|
|
103
|
+
border-bottom: 2px solid var(--primary);
|
|
104
|
+
color: var(--primary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
section h3 {
|
|
108
|
+
font-size: 16px;
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
margin: 16px 0 8px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
section p {
|
|
114
|
+
margin-bottom: 12px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
section ul, section ol {
|
|
118
|
+
margin: 8px 0 12px 20px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
section li {
|
|
122
|
+
margin-bottom: 4px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Tables */
|
|
126
|
+
table {
|
|
127
|
+
width: 100%;
|
|
128
|
+
border-collapse: collapse;
|
|
129
|
+
margin: 12px 0;
|
|
130
|
+
font-size: 14px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
thead th {
|
|
134
|
+
background: var(--bg);
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
text-align: left;
|
|
137
|
+
padding: 10px 12px;
|
|
138
|
+
border-bottom: 2px solid var(--border);
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
tbody td {
|
|
143
|
+
padding: 10px 12px;
|
|
144
|
+
border-bottom: 1px solid var(--border);
|
|
145
|
+
vertical-align: top;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
tbody tr:hover {
|
|
149
|
+
background: #f7fafc;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ID badges */
|
|
153
|
+
code {
|
|
154
|
+
background: var(--primary-light);
|
|
155
|
+
color: var(--primary);
|
|
156
|
+
padding: 2px 6px;
|
|
157
|
+
border-radius: 4px;
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Priority badges */
|
|
163
|
+
.badge {
|
|
164
|
+
display: inline-block;
|
|
165
|
+
padding: 2px 8px;
|
|
166
|
+
border-radius: 10px;
|
|
167
|
+
font-size: 12px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
color: #fff;
|
|
170
|
+
}
|
|
171
|
+
.badge-p0 { background: var(--p0); }
|
|
172
|
+
.badge-p1 { background: var(--p1); }
|
|
173
|
+
.badge-p2 { background: var(--p2); }
|
|
174
|
+
|
|
175
|
+
/* Method badges */
|
|
176
|
+
.method {
|
|
177
|
+
display: inline-block;
|
|
178
|
+
padding: 2px 8px;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
font-size: 12px;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
font-family: monospace;
|
|
183
|
+
}
|
|
184
|
+
.method-get { background: #c6f6d5; color: #22543d; }
|
|
185
|
+
.method-post { background: #fefcbf; color: #744210; }
|
|
186
|
+
.method-put { background: #bee3f8; color: #2a4365; }
|
|
187
|
+
.method-patch { background: #e9d8fd; color: #44337a; }
|
|
188
|
+
.method-delete { background: #fed7d7; color: #742a2a; }
|
|
189
|
+
|
|
190
|
+
/* Auth badge */
|
|
191
|
+
.auth-y { color: var(--p0); font-weight: 600; }
|
|
192
|
+
.auth-n { color: var(--text-muted); }
|
|
193
|
+
|
|
194
|
+
/* Responsive */
|
|
195
|
+
@media (max-width: 768px) {
|
|
196
|
+
.sidebar { display: none; }
|
|
197
|
+
.main { margin-left: 0; padding: 20px 16px; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Print */
|
|
201
|
+
@media print {
|
|
202
|
+
.sidebar { display: none; }
|
|
203
|
+
.main { margin-left: 0; max-width: 100%; }
|
|
204
|
+
section { break-inside: avoid; border: 1px solid #ccc; }
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
207
|
+
</head>
|
|
208
|
+
<body>
|
|
209
|
+
|
|
210
|
+
<aside class="sidebar">
|
|
211
|
+
<h2>목차</h2>
|
|
212
|
+
<nav>
|
|
213
|
+
<a href="#sec-1">1. 개요</a>
|
|
214
|
+
<a href="#sec-2">2. 유저 스토리</a>
|
|
215
|
+
<a href="#sec-3">3. 기능 명세</a>
|
|
216
|
+
<a href="#sec-4">4. 화면 목록</a>
|
|
217
|
+
<a href="#sec-5">5. API 초안</a>
|
|
218
|
+
<a href="#sec-6">6. 엣지케이스</a>
|
|
219
|
+
<a href="#sec-7">7. 비기능 요구사항</a>
|
|
220
|
+
</nav>
|
|
221
|
+
</aside>
|
|
222
|
+
|
|
223
|
+
<main class="main">
|
|
224
|
+
<h1>{{PLAN_TITLE}}</h1>
|
|
225
|
+
<p class="subtitle">기획안 · 생성일: <span id="gen-date"></span></p>
|
|
226
|
+
|
|
227
|
+
{{PLAN_CONTENT}}
|
|
228
|
+
</main>
|
|
229
|
+
|
|
230
|
+
<script>
|
|
231
|
+
document.getElementById('gen-date').textContent = new Date().toLocaleDateString('ko-KR');
|
|
232
|
+
|
|
233
|
+
// Sidebar active state on scroll
|
|
234
|
+
const sections = document.querySelectorAll('section[id]');
|
|
235
|
+
const navLinks = document.querySelectorAll('.sidebar nav a');
|
|
236
|
+
|
|
237
|
+
const observer = new IntersectionObserver(entries => {
|
|
238
|
+
entries.forEach(entry => {
|
|
239
|
+
if (entry.isIntersecting) {
|
|
240
|
+
navLinks.forEach(link => link.classList.remove('active'));
|
|
241
|
+
const active = document.querySelector(`.sidebar nav a[href="#${entry.target.id}"]`);
|
|
242
|
+
if (active) active.classList.add('active');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}, { rootMargin: '-20% 0px -60% 0px' });
|
|
246
|
+
|
|
247
|
+
sections.forEach(sec => observer.observe(sec));
|
|
248
|
+
</script>
|
|
249
|
+
|
|
250
|
+
</body>
|
|
251
|
+
</html>
|