feat: 새로운 스킬 및 문서 추가 (EDM HTML, 요구사항 분석기, 번역 코드 생성기)

This commit is contained in:
gil
2026-05-13 00:28:01 +09:00
parent 420b29ff43
commit 607ef1a435
61 changed files with 14343 additions and 4 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
.claude/skills/.DS_Store vendored Normal file

Binary file not shown.

52
.claude/skills/README.md Normal file
View File

@@ -0,0 +1,52 @@
# Common Skills
이 폴더는 팀 공통으로 사용할 Claude **skill** 들을 모아두는 공간입니다.
각 프로젝트는 submodule 로 이 저장소를 가져온 뒤, `.claude/common/skills/*`
`.claude/skills/*` 로 **심볼릭 링크**하여 사용합니다.
## Skill 구조
각 skill 은 하나의 폴더이며, 루트에 `SKILL.md` 를 가집니다.
```
skills/
└── <skill-name>/
├── SKILL.md # YAML frontmatter (name, description) + 지시문
└── (선택) 추가 자료 — 예시 파일, 템플릿, 스크립트 등
```
`SKILL.md` 예시:
```markdown
---
name: <skill-name>
description: <Claude skill 언제 써야 하는지 줄로>
---
# <스킬 제목>
## 언제 사용하는가
...
## 작업 순서
...
```
## 프로젝트에 연결하기
```bash
# 공통 저장소의 skill 을 .claude/skills/ 로 전부 링크
bash .claude/common/scripts/link-skills.sh
# 차이 확인
bash .claude/common/scripts/link-skills.sh --dry-run
# 특정 skill 만 링크
bash .claude/common/scripts/link-skills.sh vue-component-review
```
## 새 skill 추가 프로세스
1. 이 저장소에서 `skills/<skill-name>/SKILL.md` 를 작성
2. PR 을 올리고 팀 리뷰
3. 머지 후 각 프로젝트에서 `git submodule update --remote``link-skills.sh` 실행

View File

@@ -0,0 +1,78 @@
---
name: conventional-commit
description: Git 변경사항을 팀의 Conventional Commits 규칙(gameservice-fe-agent/rules/commit-pr.md)에 맞춰 커밋 메시지로 작성할 때 사용합니다. 사용자가 "커밋 메시지 만들어줘", "commit", "커밋해줘" 등을 요청하면 트리거됩니다.
---
# Conventional Commit 작성
이 skill 은 `git diff --staged` / `git status` 결과를 바탕으로 팀 규칙에 맞는 커밋
메시지를 작성합니다.
## 팀 규칙 요약
```
<type>(<scope>): <subject>
<body>
<footer>
```
### type
- `feat`: 새로운 기능
- `fix`: 버그 수정
- `refactor`: 기능 변화 없는 구조 개선
- `style`: 포맷/세미콜론 등 스타일
- `docs`: 문서
- `test`: 테스트 추가/수정
- `chore`: 빌드, 설정, 패키지
- `perf`: 성능 개선
- `ci`: CI 설정
### subject
- 50자 이내
- 명령형 현재 시제 (`add`, `fix``added`/`fixes` 아님)
- 끝에 마침표 금지
- 영어 소문자 시작
### body
- "무엇을"보다 **"왜"** 를 설명
- 한 줄 72자 이내
- 한 커밋 = 한 논리적 변경
## 작업 순서
1. **상태 확인**
- `git status` 로 staged/unstaged 파일 파악
- staged 변경이 없으면 사용자에게 먼저 `git add` 를 하라고 안내
2. **변경 분석**
- `git diff --staged` 로 실제 변경 내용 확인
- 변경을 한 문장으로 요약 (type + scope + subject 를 정하는 근거)
3. **메시지 작성**
- subject 는 50자 이내, 영문 명령형
- body 는 한국어로 "왜" 중심 설명 (한 줄 72자 제한)
- 이슈 번호가 있으면 `Refs: #123` footer 추가
4. **사용자 확인**
- 작성한 메시지를 보여주고 커밋 실행 여부를 물음
- 사용자가 명시적으로 "커밋해" 라고 할 때만 실제 `git commit` 실행
## 출력 예시
```
feat(user): add profile image upload
프로필 이미지 업로드 요구사항에 따라 multipart 업로드 경로를 추가했습니다.
기존 텍스트 필드 업데이트 API 는 변경하지 않았습니다.
Refs: #123
```
## 주의사항
- **논리적으로 섞인 변경**이 감지되면(예: feat + refactor 가 같이 있음) 커밋 분리를 제안한다.
- `package.json` / lock 파일이 함께 staged 되어 있으면 의존성 추가 사유를 body 에 명시한다.
- 사용자가 커밋 실행을 명시적으로 허락하기 전까지 `git commit` 명령을 직접 실행하지 않는다.
- Co-authored-by 등 footer 는 사용자가 요청할 때만 추가한다.

BIN
.claude/skills/markup-edm/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
--- ---
name: edm-email-html name: markup-edm
description: | description: |
EDM(이메일 다이렉트 마케팅) HTML을 구현하는 전체 워크플로우 스킬. EDM(이메일 다이렉트 마케팅) HTML을 구현하는 전체 워크플로우 스킬.
Figma 디자인 → HTML table 마크업 → 아웃룩 호환 → 검수까지 단계별 가이드를 제공합니다. Figma 디자인 → HTML table 마크업 → 아웃룩 호환 → 검수까지 단계별 가이드를 제공합니다.

View File

@@ -1,5 +1,5 @@
--- ---
name: requirement-analyzer name: plan-analyzer
description: | description: |
PPT 기획서를 분석하여 요구사항 명세서를 자동 생성합니다. PPT 기획서를 분석하여 요구사항 명세서를 자동 생성합니다.
Nuxt pages 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록, Nuxt pages 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록,

View File

@@ -1,5 +1,5 @@
--- ---
name: translation-keys name: plan-translation-generator
description: 번역 요청 엑셀 파일의 EN 셀을 기반으로 '번역코드' 컬럼에 함축적인 코드를 자동 생성합니다. 사용자가 "번역코드 만들어줘", "translation key 생성", "번역 키 추출" 등을 요청하면 트리거됩니다. description: 번역 요청 엑셀 파일의 EN 셀을 기반으로 '번역코드' 컬럼에 함축적인 코드를 자동 생성합니다. 사용자가 "번역코드 만들어줘", "translation key 생성", "번역 키 추출" 등을 요청하면 트리거됩니다.
--- ---

View File

@@ -0,0 +1,268 @@
---
name: ppt-maker
description: 순수 HTML, CSS, JavaScript로 웹 프레젠테이션(슬라이드)을 생성합니다. 외부 라이브러리 없이 단일 HTML 파일로 완성되며, GPU 가속 슬라이드 전환, 키보드/터치 네비게이션, 진행 표시바, 반응형 디자인을 포함합니다. 사용자가 프레젠테이션, 슬라이드, 발표자료, PPT 등을 만들어달라고 요청할 때 사용하세요. 'PPT 만들어줘', '슬라이드 생성', '발표자료 만들어줘', 'create a presentation', 'make slides about' 등의 요청에 반응합니다.
---
# PPT Maker
순수 HTML/CSS/JS 단일 파일 웹 프레젠테이션 생성기.
## 워크플로우
1. `references/template.html`을 읽어 전체 구조(CSS/JS)를 파악한다
2. 사용자의 주제, 내용, 슬라이드 수 요구사항을 정리한다
3. 슬라이드 구성을 설계한다 (어떤 타입의 슬라이드를 어떤 순서로)
4. 템플릿의 CSS와 JS를 그대로 유지하면서 슬라이드 HTML 콘텐츠만 교체한다
5. 단일 .html 파일로 출력한다
6. `open` 명령으로 브라우저에서 열어 확인한다
## 핵심 원칙
- **단일 파일**: 모든 CSS, JS가 HTML 안에 인라인
- **외부 의존성 제로**: Google Fonts만 예외 (CDN 링크)
- **GPU 가속**: 전환 애니메이션은 `transform``opacity`만 사용
- **뷰포트 피팅**: 모든 슬라이드가 스크롤 없이 화면에 딱 맞아야 함
- **접근성**: ARIA 속성, prefers-reduced-motion 대응
## 디자인 시스템
### CSS Custom Properties
```css
:root {
--bg: #0a0a0f;
--bg-subtle: #111118;
--accent: #C6A55C;
--accent-dim: rgba(198, 165, 92, 0.15);
--accent-glow: rgba(198, 165, 92, 0.3);
--text-primary: #F0EBE0;
--text-secondary: #B8B0A0;
--text-dim: #706858;
--border: #1e1c18;
--font-kr: 'Noto Sans KR', sans-serif;
--font-en: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
```
사용자가 다른 컬러 테마를 원하면 이 변수들만 교체한다.
### 타이포그래피
| 요소 | 크기 | 비고 |
|------|------|------|
| h1 | `clamp(2rem, 5vw, 4rem)` | 타이틀 슬라이드 |
| h2 | `clamp(1.4rem, 3vw, 2.4rem)` | 섹션 제목 |
| h3 | `clamp(1.1rem, 2vw, 1.6rem)` | 서브 제목 |
| p, li | `clamp(0.85rem, 1.5vw, 1.2rem)` | 본문 |
| code | `clamp(0.65rem, 1.1vw, 0.9rem)` | 코드 블록 |
### 컴포넌트 카탈로그
- **info-card**: 정보 카드 (`background: rgba(255,255,255,0.02)`, `border: 1px solid var(--border)`)
- **split**: 2열 그리드 (`grid-template-columns: 1fr 1fr`)
- **split-card**: split 내부 카드
- **badge**: 태그/라벨 (`border-radius: 100px`, accent 컬러)
- **terminal-badge**: 모노스페이스 라벨 + 깜빡이는 커서 (`> `)
- **accent-line**: 강조선 (`width: 50px; height: 2px; background: var(--accent)`)
- **code-block**: 줄 번호 + 하이라이트 지원 코드 블록
## 슬라이드 타입 카탈로그
### 1. 타이틀 슬라이드 (slide--intro)
```html
<section class="slide slide--intro active" id="slide-1"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 1" aria-hidden="false">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">카테고리</div>
<h1 class="reveal">메인 타이틀</h1>
</div>
<div class="slide__body">
<p class="reveal" style="font-size: clamp(0.95rem, 1.8vw, 1.3rem);">서브 타이틀</p>
<div class="accent-line reveal"></div>
<p class="reveal" style="font-size: clamp(0.7rem, 1vw, 0.85rem); color: var(--text-dim);">
← → 키 또는 스와이프로 이동
</p>
</div>
<div class="slide__footer">날짜</div>
</div>
</section>
```
### 2. 리스트 콘텐츠 (slide--content / slide--content-alt)
```html
<section class="slide slide--content" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">영문 키워드</div>
<h2 class="reveal">섹션 제목</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">항목 1</li>
<li class="reveal">항목 2</li>
<li class="reveal">항목 3</li>
</ul>
</div>
</div>
</section>
```
`slide--content``slide--content-alt`를 번갈아 사용하면 배경 그라데이션 방향이 달라져 시각적 변화를 준다.
### 3. 코드 슬라이드 (slide--code)
```html
<section class="slide slide--code" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">코드 제목</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="javascript" data-highlight-lines="2,4-6"><code><span class="line"><span class="kw">const</span> <span class="var">x</span> = <span class="str">42</span>;</span>
<span class="line"><span class="cmt">// 강조될 줄</span></span></code></pre>
</div>
</div>
</section>
```
**신택스 클래스**: `kw`(키워드), `fn`(함수), `str`(문자열/숫자), `cmt`(주석), `var`(변수), `op`(연산자), `tag`(태그), `attr`(속성), `val`(값)
`data-highlight-lines`에 강조할 줄 번호를 지정한다 (예: "2,4-6").
### 4. 분할 레이아웃 (slide--diagram)
```html
<section class="slide slide--diagram" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">키워드</div>
<h2 class="reveal">제목</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<p><strong style="color: var(--accent);">왼쪽 제목</strong><br>설명</p>
</div>
<div class="split-card reveal">
<p><strong style="color: #D4B87A;">오른쪽 제목</strong><br>설명</p>
</div>
</div>
</div>
</div>
</section>
```
### 5. 정보 카드 슬라이드
```html
<div class="slide__body">
<div class="info-card reveal">
<div class="card-title">// 카드 라벨</div>
<p>카드 내용</p>
</div>
<div class="info-card reveal">
<div class="card-title">// 카드 라벨 2</div>
<p>카드 내용 2</p>
</div>
</div>
```
### 6. 마무리 슬라이드 (slide--end)
```html
<section class="slide slide--end" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner" style="text-align: center; align-items: center;">
<div class="slide__body" style="align-items: center;">
<h1 class="reveal">감사합니다</h1>
<div class="accent-line reveal" style="margin: 0.8rem auto;"></div>
<p class="reveal">마무리 메시지</p>
<div class="reveal" style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<span class="badge">태그1</span>
<span class="badge">태그2</span>
</div>
</div>
</div>
</section>
```
## 콘텐츠 작성 규칙
### reveal 순차 등장
- 모든 콘텐츠 요소에 `class="reveal"` 추가
- CSS `transition-delay`로 nth-child 기반 순차 등장이 자동 적용됨
- 최대 8단계까지 지원 (0.08s 간격)
### 슬라이드 구조 필수 규칙
- 첫 번째 슬라이드만 `class="slide ... active"`, `aria-hidden="false"`
- 나머지는 `aria-hidden="true"`, active 클래스 없음
- 모든 슬라이드에 고유 `id="slide-N"` (1부터 시작)
- `role="region"`, `aria-roledescription="슬라이드"`, `aria-label="슬라이드 N"` 필수
### 슬라이드 수 가이드
- 사용자가 지정하면 그대로 따름
- 지정하지 않으면 주제에 맞게 7~12장 구성
- 기본 구성: 타이틀(1) + 개요(1) + 본론(3~8) + 마무리(1)
### 콘텐츠 밀도
- 리스트: 슬라이드당 최대 5개 항목
- 텍스트: 한 단락 2~3줄 이내
- 코드: 최대 10줄
- **뷰포트에 딱 맞아야 함 — 스크롤이 발생하면 안 된다**
## 기술 요구사항
### 네비게이션
- **키보드**: ← → / ↑ ↓ / Space / PageUp·Down / Home·End / F(전체화면)
- **터치**: 스와이프 (threshold 50px, `passive:false``preventDefault`)
- **버튼**: 좌하단 이전/다음 버튼
- **URL hash**: `#slide-N` 동기화 + 뒤로가기 지원
### 전환 애니메이션
- `transform: translateX(±100%)` + `opacity` (GPU 가속, Composite 전용)
- duration: 400~450ms
- `isAnimating` 잠금 + `transitionend` + `setTimeout(duration+150)` fallback
- `will-change`는 전환 직전 설정, 완료 후 `auto`로 해제
### UI 요소
- **진행 표시바**: 상단 2px, accent 컬러
- **슬라이드 카운터**: 우하단 `01 / 10` 형식 (2자리 zero-pad)
- **네비게이션 버튼**: 좌하단 버튼
### 접근성
- `prefers-reduced-motion: reduce`에서 transition-duration 최소화
- 모바일 터치 타겟 최소 44px (`@media (hover: none)`)
- `100dvh` 사용 (모바일 주소창 동적 대응)
## 주의사항
- `display:none` 사용 금지 — transition 불가
- 초기 위치: `transition:none` → 위치 설정 → `void el.offsetWidth``transition` 복원
- Space 키는 반드시 `preventDefault()` (페이지 스크롤 방지)
- `will-change`를 모든 슬라이드에 정적 적용하면 GPU 메모리 낭비
- 애니메이션 duration 500ms 초과 금지 (사용자 지루함 유발)
- `e.repeat` 체크로 키 반복 이벤트 차단
## 슬라이드 카운터 업데이트
JS에서 슬라이드 총 개수를 반드시 실제 슬라이드 수와 일치시킨다. `slideCounter` 초기 텍스트도 `01 / {총 슬라이드 수}`로 설정한다.
## 참고 템플릿
`references/template.html`에 10장짜리 완전한 동작 예시가 있다.
이 파일의 **CSS와 JS를 기반으로** 슬라이드 HTML 콘텐츠만 교체하여 새 프레젠테이션을 만든다.

View File

@@ -0,0 +1,914 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>바닐라 슬라이드 프레젠테이션</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&family=Inter:wght@400;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
/* ===== CSS Custom Properties ===== */
:root {
--bg: #0a0a0f;
--bg-subtle: #111118;
--accent: #C6A55C;
--accent-dim: rgba(198, 165, 92, 0.15);
--accent-glow: rgba(198, 165, 92, 0.3);
--text-primary: #F0EBE0;
--text-secondary: #B8B0A0;
--text-dim: #706858;
--border: #1e1c18;
--font-kr: 'Noto Sans KR', sans-serif;
--font-en: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
/* ===== 리셋 ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-kr), var(--font-en), sans-serif;
overflow: hidden;
background: var(--bg);
color: var(--text-primary);
}
/* ===== 뷰포트: fixed 풀스크린 ===== */
.slide-viewport {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
background: var(--bg);
overflow: hidden;
}
.slide-viewport.animating { pointer-events: none; }
/* ===== 슬라이드: absolute 풀스크린 ===== */
.slide {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: clamp(2rem, 5vw, 5rem);
opacity: 0;
transform: translateX(100%);
pointer-events: none;
transition:
opacity 400ms ease-in-out,
transform 450ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide.active {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
z-index: 2;
}
.slide.stand-left { transform: translateX(-100%); }
/* ===== 배경 효과 ===== */
.slide::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg,
transparent, transparent 2px,
rgba(255, 255, 255, 0.005) 2px,
rgba(255, 255, 255, 0.005) 4px);
pointer-events: none;
z-index: 1;
}
.slide::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(198, 165, 92, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(198, 165, 92, 0.015) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
.slide > * { position: relative; z-index: 2; }
/* ===== 콘텐츠 영역 ===== */
.slide__inner {
width: 100%;
max-width: 680px;
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
gap: clamp(0.8rem, 2vw, 1.5rem);
}
.slide__header {}
.slide__body { display: flex; flex-direction: column; justify-content: center; gap: 1rem; }
.slide__footer { font-size: clamp(0.6rem, 1vw, 0.8rem); color: var(--text-dim); text-align: right; margin-top: auto; }
/* ===== 타이포 ===== */
h1 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 700;
line-height: 1.15;
color: var(--text-primary);
}
h2 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(1.4rem, 3vw, 2.4rem);
font-weight: 700;
line-height: 1.2;
margin-bottom: 0.8rem;
color: var(--text-primary);
}
h3 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(1.1rem, 2vw, 1.6rem);
font-weight: 700;
line-height: 1.3;
color: var(--text-primary);
}
p, li {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(0.85rem, 1.5vw, 1.2rem);
line-height: 1.7;
color: var(--text-secondary);
}
ul { list-style: none; padding: 0; }
ul li {
padding: clamp(0.5rem, 0.8vw, 0.7rem) clamp(0.8rem, 1.2vw, 1rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
gap: 0.6rem;
}
ul li::before { content: '→ '; color: var(--accent); font-weight: bold; font-family: var(--font-mono); flex-shrink: 0; }
/* ===== 슬라이드 배경 변형 ===== */
.slide--intro { background: radial-gradient(ellipse at 30% 50%, rgba(198,165,92,0.06) 0%, var(--bg) 70%); }
.slide--content { background: radial-gradient(ellipse at 70% 40%, rgba(198,165,92,0.04) 0%, var(--bg) 70%); }
.slide--content-alt { background: radial-gradient(ellipse at 30% 60%, rgba(198,165,92,0.04) 0%, var(--bg) 70%); }
.slide--code { background: var(--bg); }
.slide--diagram { background: radial-gradient(ellipse at 50% 30%, rgba(198,165,92,0.05) 0%, var(--bg) 70%); }
.slide--end { background: radial-gradient(ellipse at 50% 50%, rgba(198,165,92,0.08) 0%, var(--bg) 60%); }
/* ===== reveal 애니메이션 (슬라이드 진입 시 순차 등장) ===== */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s var(--ease-out-expo), transform 0.6s var(--ease-out-expo);
}
.slide.active .reveal {
opacity: 1;
transform: translateY(0);
}
.reveal:nth-child(1) { transition-delay: 0.08s; }
.reveal:nth-child(2) { transition-delay: 0.16s; }
.reveal:nth-child(3) { transition-delay: 0.24s; }
.reveal:nth-child(4) { transition-delay: 0.32s; }
.reveal:nth-child(5) { transition-delay: 0.40s; }
.reveal:nth-child(6) { transition-delay: 0.48s; }
.reveal:nth-child(7) { transition-delay: 0.56s; }
.reveal:nth-child(8) { transition-delay: 0.64s; }
/* .slide__body 내부 자식도 순차 등장 */
.slide.active .slide__body > .reveal:nth-child(1) { transition-delay: 0.15s; }
.slide.active .slide__body > .reveal:nth-child(2) { transition-delay: 0.25s; }
.slide.active .slide__body > .reveal:nth-child(3) { transition-delay: 0.35s; }
.slide.active .slide__body > .reveal:nth-child(4) { transition-delay: 0.45s; }
.slide.active .slide__body > .reveal:nth-child(5) { transition-delay: 0.55s; }
/* ul li 순차 등장 */
.slide.active ul li.reveal:nth-child(1) { transition-delay: 0.15s; }
.slide.active ul li.reveal:nth-child(2) { transition-delay: 0.25s; }
.slide.active ul li.reveal:nth-child(3) { transition-delay: 0.35s; }
.slide.active ul li.reveal:nth-child(4) { transition-delay: 0.45s; }
.slide.active ul li.reveal:nth-child(5) { transition-delay: 0.55s; }
/* ===== 코드 블록 ===== */
.code-block {
background: #111118;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
font-family: var(--font-mono);
font-size: clamp(0.65rem, 1.1vw, 0.9rem);
line-height: 1.7;
counter-reset: line-number;
width: 100%;
}
.code-block::before {
content: attr(data-language);
display: block;
background: rgba(198,165,92,0.06);
color: var(--text-dim);
font-size: 0.75em;
padding: 0.4em 1em;
text-transform: uppercase;
letter-spacing: 0.1em;
font-family: var(--font-mono);
border-bottom: 1px solid var(--border);
}
.code-block code { display: block; padding: 1rem 1.2rem; overflow-x: auto; }
.code-block .line {
display: block;
padding: 2px 0 2px 3em;
position: relative;
counter-increment: line-number;
transition: background-color 0.3s ease, opacity 0.3s ease;
color: var(--text-secondary);
}
.code-block .line::before {
content: counter(line-number);
position: absolute;
left: 0;
width: 2.5em;
text-align: right;
color: #3a3830;
font-size: 0.85em;
user-select: none;
}
.code-block .line--highlight {
background: rgba(198, 165, 92, 0.08);
border-left: 3px solid var(--accent);
padding-left: calc(3em - 3px);
}
.code-block .line--highlight::before { color: var(--accent); }
.code-block.dim-others .line:not(.line--highlight) { opacity: 0.3; }
/* 신택스 컬러 */
.kw { color: #C6A55C; }
.fn { color: #D4B87A; }
.str { color: #A89070; }
.cmt { color: #555040; font-style: italic; }
.var { color: #E0D0B0; }
.op { color: #8A8070; }
.tag { color: #C6A55C; }
.attr { color: #D4B87A; }
.val { color: #A89070; }
/* ===== 컴포넌트 ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
.split-card {
padding: clamp(0.8rem, 1.5vw, 1.2rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 6px;
}
.info-card {
padding: clamp(1rem, 2vw, 1.5rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 6px;
width: 100%;
}
.info-card .card-title {
font-family: var(--font-mono);
font-size: clamp(0.7rem, 1vw, 0.85rem);
color: var(--text-dim);
margin-bottom: 0.8rem;
letter-spacing: 0.05em;
}
.badge {
display: inline-block;
font-family: var(--font-mono);
font-size: clamp(0.65rem, 0.9vw, 0.8rem);
color: var(--accent);
padding: 0.2em 0.6em;
border: 1px solid var(--accent-dim);
border-radius: 100px;
letter-spacing: 0.05em;
}
.terminal-badge {
font-family: var(--font-mono);
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
color: var(--accent);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.terminal-badge::before {
content: '> ';
animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.accent-line {
width: 50px;
height: 2px;
background: var(--accent);
margin: 0.5rem 0;
}
.number-label {
font-family: var(--font-mono);
font-size: clamp(0.7rem, 0.9vw, 0.8rem);
color: var(--accent);
flex-shrink: 0;
width: 2em;
}
/* ===== 네비게이션 UI ===== */
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: rgba(198,165,92,0.1);
z-index: 101;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--accent);
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-counter {
position: fixed;
bottom: 16px;
right: 24px;
font-size: 12px;
color: var(--text-dim);
font-family: var(--font-mono);
z-index: 102;
letter-spacing: 0.05em;
}
.nav-buttons {
position: fixed;
bottom: 16px;
left: 24px;
display: flex;
gap: 6px;
z-index: 102;
}
.nav-btn {
width: 32px;
height: 32px;
border: 1px solid rgba(198,165,92,0.2);
background: rgba(198,165,92,0.04);
color: var(--text-dim);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.nav-btn:hover { background: rgba(198,165,92,0.12); color: var(--accent); border-color: rgba(198,165,92,0.4); }
.speaker-notes { display: none; }
/* ===== 반응형 ===== */
@media (prefers-reduced-motion: reduce) {
.reveal, .slide { transition-duration: 0.1s !important; }
.reveal { transform: none !important; }
}
@media (hover: none) and (pointer: coarse) {
.nav-btn { width: 44px; height: 44px; }
}
@media (max-height: 600px) {
.slide { padding: clamp(1rem, 3vw, 2rem); }
h1 { font-size: clamp(1.5rem, 4vw, 2.5rem); }
h2 { font-size: clamp(1.2rem, 2.5vw, 1.8rem); }
}
</style>
</head>
<body>
<div class="slide-viewport" id="viewport">
<!-- ============ 슬라이드 01: 타이틀 ============ -->
<section class="slide slide--intro active" id="slide-1"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 1" aria-hidden="false">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Presentation</div>
<h1 class="reveal">Vanilla Slides</h1>
</div>
<div class="slide__body">
<p class="reveal" style="font-size: clamp(0.95rem, 1.8vw, 1.3rem);">
순수 HTML, CSS, JavaScript로 만드는<br>웹 기반 프레젠테이션
</p>
<div class="accent-line reveal"></div>
<p class="reveal" style="font-size: clamp(0.7rem, 1vw, 0.85rem); color: var(--text-dim);">
← → 키 또는 스와이프로 이동
</p>
</div>
<div class="slide__footer">2026.03</div>
</div>
</section>
<!-- ============ 슬라이드 02: 왜 바닐라인가 ============ -->
<section class="slide slide--content" id="slide-2"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 2" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Why vanilla?</div>
<h2 class="reveal">왜 바닐라 JS인가</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">외부 의존성 제로 — 번들 크기 0KB</li>
<li class="reveal">브라우저 표준만으로 완전한 구현</li>
<li class="reveal">커스터마이즈의 자유도 극대화</li>
<li class="reveal">단일 HTML 파일로 배포 가능</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 03: 핵심 기능 ============ -->
<section class="slide slide--content-alt" id="slide-3"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 3" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Features</div>
<h2 class="reveal">핵심 기능</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">풀스크린 뷰포트 & 반응형 레이아웃</li>
<li class="reveal">키보드 / 터치 / 스와이프 네비게이션</li>
<li class="reveal">GPU 가속 슬라이드 전환 애니메이션</li>
<li class="reveal">순차 등장 reveal 효과</li>
<li class="reveal">코드 블록 라인 하이라이트</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 04: 아키텍처 ============ -->
<section class="slide slide--diagram" id="slide-4"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 4" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Architecture</div>
<h2 class="reveal">아키텍처</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem); margin-bottom: 0.8rem;">
<strong style="color: var(--accent);">레이아웃</strong><br>
fixed 뷰포트 + absolute 슬라이드
</p>
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem);">
<strong style="color: #D4B87A;">전환</strong><br>
CSS Transition + transform/opacity
</p>
</div>
<div class="split-card reveal">
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem); margin-bottom: 0.8rem;">
<strong style="color: #B89860;">상태 관리</strong><br>
slideIndex 단일 상태 + URL hash
</p>
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem);">
<strong style="color: #A08848;">등장 효과</strong><br>
.reveal 클래스 + transition-delay
</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 05: CSS 레이아웃 ============ -->
<section class="slide slide--content" id="slide-5"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 5" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Layout</div>
<h2 class="reveal">CSS 레이아웃 전략</h2>
</div>
<div class="slide__body">
<div class="info-card reveal">
<div class="card-title">// 풀스크린 뷰포트</div>
<p style="font-size: clamp(0.8rem, 1.2vw, 1rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">position: fixed</code> 뷰포트 안에<br>
<code style="color: var(--accent); font-family: var(--font-mono);">position: absolute</code> 슬라이드를 겹쳐<br>
<code style="color: var(--accent); font-family: var(--font-mono);">transform: translateX</code>로 전환합니다
</p>
</div>
<div class="info-card reveal">
<div class="card-title">// 콘텐츠 영역</div>
<p style="font-size: clamp(0.8rem, 1.2vw, 1rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">max-width: 680px</code>로 읽기 편한 폭 유지<br>
<code style="color: var(--accent); font-family: var(--font-mono);">clamp()</code>로 반응형 타이포그래피 처리
</p>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 06: 코드 예시 — HTML ============ -->
<section class="slide slide--code" id="slide-6"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 6" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">HTML 구조</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="html" data-highlight-lines="2,4-5"><code><span class="line"><span class="op">&lt;</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide-viewport"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">section</span> <span class="attr">class</span>=<span class="str">"slide active"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide__inner"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">h2</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">&gt;</span><span class="var">제목</span><span class="op">&lt;/</span><span class="tag">h2</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">p</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">&gt;</span><span class="var">내용</span><span class="op">&lt;/</span><span class="tag">p</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;/</span><span class="tag">div</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;/</span><span class="tag">section</span><span class="op">&gt;</span></span>
<span class="line"><span class="op">&lt;/</span><span class="tag">div</span><span class="op">&gt;</span></span></code></pre>
<p class="reveal" style="font-size: clamp(0.7rem, 1.1vw, 0.95rem); color: var(--accent); margin-top: 0.5rem;">
.reveal 클래스로 순차 등장 효과가 자동 적용됩니다
</p>
</div>
</div>
</section>
<!-- ============ 슬라이드 07: 코드 예시 — JS ============ -->
<section class="slide slide--code" id="slide-7"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 7" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">슬라이드 전환 로직</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="javascript" data-highlight-lines="3,6-7"><code><span class="line"><span class="fn">goNext</span>() {</span>
<span class="line"> <span class="kw">if</span> (<span class="kw">this</span>.<span class="var">isAnimating</span>) <span class="kw">return</span>;</span>
<span class="line"> <span class="kw">this</span>.<span class="fn">_goSlide</span>(<span class="kw">this</span>.<span class="var">current</span> + <span class="str">1</span>, <span class="str">'next'</span>);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="cmt">// GPU 가속: transform + opacity만 사용</span></span>
<span class="line"><span class="var">to</span>.<span class="var">style</span>.<span class="var">transform</span> = <span class="str">'translateX(0)'</span>;</span>
<span class="line"><span class="var">from</span>.<span class="var">style</span>.<span class="var">transform</span> = <span class="str">'translateX(-100%)'</span>;</span></code></pre>
<p class="reveal" style="font-size: clamp(0.7rem, 1.1vw, 0.95rem); color: var(--text-dim); margin-top: 0.5rem;">
isAnimating 플래그 + transitionend로 이벤트 잠금
</p>
</div>
</div>
</section>
<!-- ============ 슬라이드 08: 네비게이션 ============ -->
<section class="slide slide--content-alt" id="slide-8"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 8" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Navigation</div>
<h2 class="reveal">네비게이션 지원</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<h3 style="font-size: clamp(0.85rem, 1.2vw, 1rem); color: var(--accent); margin-bottom: 0.5rem;">키보드</h3>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
← → 이전/다음<br>
Space / PageDown 다음<br>
Home / End 처음/끝<br>
F 전체화면
</p>
</div>
<div class="split-card reveal">
<h3 style="font-size: clamp(0.85rem, 1.2vw, 1rem); color: var(--accent); margin-bottom: 0.5rem;">터치</h3>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
좌 스와이프 → 다음<br>
우 스와이프 → 이전<br>
threshold: 50px<br>
touch-action: pan-y
</p>
</div>
</div>
<div class="info-card reveal" style="margin-top: 0.8rem;">
<div class="card-title">// URL 동기화</div>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">#slide-N</code> 해시로 뒤로가기 및 직접 접근 지원
</p>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 09: 주의사항 ============ -->
<section class="slide slide--content" id="slide-9"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 9" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Pitfalls</div>
<h2 class="reveal">주의사항</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal"><span style="color: var(--accent);">display:none</span> 사용 금지 — transition 불가</li>
<li class="reveal">모바일 높이는 <span style="color: var(--accent);">100dvh</span> 사용</li>
<li class="reveal"><span style="color: var(--accent);">will-change</span>는 동적 토글 (정적 적용 시 메모리 낭비)</li>
<li class="reveal"><span style="color: var(--accent);">transitionend</span> 미발생 대비 setTimeout fallback 필수</li>
<li class="reveal">터치 <span style="color: var(--accent);">passive:false</span>에서만 preventDefault 가능</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 10: 마무리 ============ -->
<section class="slide slide--end" id="slide-10"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 10" aria-hidden="true">
<div class="slide__inner" style="text-align: center; align-items: center;">
<div class="slide__body" style="align-items: center;">
<h1 class="reveal">감사합니다</h1>
<div class="accent-line reveal" style="margin: 0.8rem auto;"></div>
<p class="reveal" style="font-size: clamp(0.85rem, 1.3vw, 1.1rem);">
외부 라이브러리 0개 · 단일 HTML 파일
</p>
<div class="reveal" style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<span class="badge">HTML</span>
<span class="badge">CSS</span>
<span class="badge">JavaScript</span>
</div>
</div>
</div>
</section>
</div>
<!-- 네비게이션 UI -->
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="slide-counter" id="slideCounter">01 / 10</div>
<div class="nav-buttons">
<button class="nav-btn" id="btnPrev" aria-label="이전 슬라이드">&#8249;</button>
<button class="nav-btn" id="btnNext" aria-label="다음 슬라이드">&#8250;</button>
</div>
<script>
class Presentation {
constructor() {
this.viewport = document.getElementById('viewport');
this.slides = Array.from(this.viewport.querySelectorAll('.slide'));
this.total = this.slides.length;
this.current = 0;
this.isAnimating = false;
this.DURATION = 450;
// 코드 하이라이트 적용
this.slides.forEach(s => this._applyCodeHighlights(s));
// URL hash에서 초기 슬라이드 복원
this._restoreFromHash();
// 이벤트 바인딩
this._bindKeyboard();
this._bindTouch();
this._bindButtons();
this._bindHash();
this._updateUI();
}
// ── 한 페이지 단위 이동 ─────────────────────
goNext() {
if (this.isAnimating || this.current >= this.total - 1) return;
this._goSlide(this.current + 1, 'next');
}
goPrev() {
if (this.isAnimating || this.current <= 0) return;
this._goSlide(this.current - 1, 'prev');
}
goTo(index) {
if (this.isAnimating || index === this.current) return;
if (index < 0 || index >= this.total) return;
this._goSlide(index, index > this.current ? 'next' : 'prev');
}
// ── 슬라이드 전환 ───────────────────────────
_goSlide(index, direction) {
const isNext = direction === 'next';
const from = this.slides[this.current];
const to = this.slides[index];
this.isAnimating = true;
this.viewport.classList.add('animating');
from.style.willChange = 'transform, opacity';
to.style.willChange = 'transform, opacity';
// 진입 슬라이드 초기 위치 (transition 없이)
to.style.transition = 'none';
to.classList.remove('stand-left');
to.style.transform = isNext ? 'translateX(100%)' : 'translateX(-100%)';
to.style.opacity = '0';
void to.offsetWidth;
// transition 복원 + 애니메이션 시작
to.style.transition = '';
to.style.transform = '';
to.style.opacity = '';
from.classList.remove('active');
to.classList.add('active');
from.style.transform = isNext ? 'translateX(-100%)' : 'translateX(100%)';
from.style.opacity = '0';
from.setAttribute('aria-hidden', 'true');
to.setAttribute('aria-hidden', 'false');
this.current = index;
this._syncHash();
this._updateUI();
// 완료 감지
let done = false;
const cleanup = () => {
if (done) return;
done = true;
clearTimeout(fallback);
to.removeEventListener('transitionend', onEnd);
from.style.transform = '';
from.style.opacity = '';
from.style.transition = 'none';
from.classList.add('stand-left');
void from.offsetWidth;
from.style.transition = '';
from.style.willChange = 'auto';
to.style.willChange = 'auto';
this.isAnimating = false;
this.viewport.classList.remove('animating');
};
const onEnd = (e) => {
if (e.propertyName !== 'transform' || e.target !== to) return;
cleanup();
};
to.addEventListener('transitionend', onEnd);
const fallback = setTimeout(cleanup, this.DURATION + 150);
}
// ── 유틸리티 ─────────────────────────────────
_applyCodeHighlights(slide) {
slide.querySelectorAll('.code-block[data-highlight-lines]').forEach(pre => {
const ranges = this._parseRanges(pre.dataset.highlightLines);
const lines = pre.querySelectorAll('.line');
lines.forEach((line, i) => {
if (ranges.has(i + 1)) line.classList.add('line--highlight');
});
if (ranges.size > 0) pre.classList.add('dim-others');
});
}
_parseRanges(str) {
const result = new Set();
str.split(',').forEach(part => {
part = part.trim();
if (part.includes('-')) {
const [a, b] = part.split('-').map(Number);
for (let i = a; i <= b; i++) result.add(i);
} else {
result.add(Number(part));
}
});
return result;
}
_updateUI() {
const progress = this.total <= 1 ? 100 : (this.current / (this.total - 1)) * 100;
document.getElementById('progressFill').style.width = `${progress}%`;
const num = String(this.current + 1).padStart(2, '0');
const tot = String(this.total).padStart(2, '0');
document.getElementById('slideCounter').textContent = `${num} / ${tot}`;
}
_syncHash() {
const hash = `#slide-${this.current + 1}`;
if (location.hash !== hash) history.pushState(null, '', hash);
}
_restoreFromHash() {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1], 10) - 1;
if (idx > 0 && idx < this.total) {
this.slides[0].classList.remove('active');
this.slides[0].setAttribute('aria-hidden', 'true');
this.slides[0].classList.add('stand-left');
this.slides[idx].classList.add('active');
this.slides[idx].setAttribute('aria-hidden', 'false');
this.current = idx;
}
}
}
// ── 이벤트 바인딩 ────────────────────────────
_bindKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
const tag = document.activeElement.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
switch (e.key) {
case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown':
e.preventDefault(); this.goNext(); break;
case 'ArrowLeft': case 'ArrowUp': case 'PageUp':
e.preventDefault(); this.goPrev(); break;
case 'Home':
e.preventDefault(); this.goTo(0); break;
case 'End':
e.preventDefault(); this.goTo(this.total - 1); break;
case 'f': case 'F':
this._toggleFullscreen(); break;
}
});
}
_bindTouch() {
let startX = 0, startY = 0, startTime = 0, swiping = false;
const el = this.viewport;
el.addEventListener('touchstart', (e) => {
const t = e.changedTouches[0];
startX = t.clientX; startY = t.clientY;
startTime = Date.now(); swiping = false;
}, { passive: true });
el.addEventListener('touchmove', (e) => {
const t = e.changedTouches[0];
const dx = Math.abs(t.clientX - startX);
const dy = Math.abs(t.clientY - startY);
if (dx > dy && dx > 10) { swiping = true; e.preventDefault(); }
}, { passive: false });
el.addEventListener('touchend', (e) => {
const t = e.changedTouches[0];
const dx = t.clientX - startX;
const dy = Math.abs(t.clientY - startY);
const elapsed = Date.now() - startTime;
if (swiping && Math.abs(dx) >= 50 && dy <= 75 && elapsed <= 400) {
dx < 0 ? this.goNext() : this.goPrev();
}
swiping = false;
}, { passive: true });
el.style.touchAction = 'pan-y';
el.style.userSelect = 'none';
}
_bindButtons() {
document.getElementById('btnPrev').addEventListener('click', () => this.goPrev());
document.getElementById('btnNext').addEventListener('click', () => this.goNext());
}
_bindHash() {
window.addEventListener('hashchange', () => {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1], 10) - 1;
if (idx !== this.current) this.goTo(idx);
}
});
}
_toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen();
}
}
}
document.addEventListener('DOMContentLoaded', () => new Presentation());
</script>
</body>
</html>

View File

@@ -0,0 +1,165 @@
---
name: project-init
description: >
현재 프로젝트를 분석하여 .claude/project/ 하위 문서(overview.md, conventions.md, architecture.md)를
실제 내용으로 채웁니다. 사용자가 "/init", "프로젝트 초기화", "프로젝트 문서 작성해줘" 등을
요청할 때 트리거됩니다.
---
# Project Init — 프로젝트 문서 자동 작성
이 skill 은 현재 Git 저장소를 탐색하여 `.claude/project/` 하위 세 파일을
실제 프로젝트 정보로 채웁니다.
## 언제 사용하는가
- `/init` 실행 시
- `.claude/project/*.md` 가 템플릿 상태(placeholder 가 남아 있음)일 때
- 새로운 프로젝트에 gameservice-fe-agent 를 처음 적용할 때
## 작업 순서
### 1. 환경 파악 (읽기 전용)
아래 파일들을 순서대로 읽어 프로젝트 정보를 수집한다.
파일이 없으면 건너뛴다.
| 파일 / 명령 | 수집 정보 |
|---|---|
| `package.json` | 프로젝트 이름, 의존성, 스크립트, 패키지 매니저 |
| `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` | 패키지 매니저 확정 |
| `nuxt.config.ts` / `next.config.*` / `vite.config.*` | 프레임워크, 모듈, 빌드 설정 |
| `tsconfig.json` | TypeScript strict 여부 |
| `tailwind.config.*` | CSS 프레임워크 |
| `.eslintrc.*` / `eslint.config.*` | 린터 설정 |
| `README.md` | 서비스 설명, 팀 정보 |
| `app/` / `src/` / `pages/` 디렉토리 구조 | 레이어, 주요 기능 |
| `server/api/` 또는 `src/api/` | 외부 의존성, API 패턴 |
| `stores/` / `composables/` | 상태 관리 패턴 |
### 2. 기존 문서 상태 확인
`.claude/project/overview.md`, `conventions.md`, `architecture.md` 를 읽어
이미 작성된 내용이 있으면 덮어쓰지 않고 **비어 있는 섹션만 채운다**.
- placeholder(`<...>`) 가 남아 있는 줄 → 채움 대상
- 실제 내용이 작성된 줄 → 유지
### 3. 사용자 확인 후 작성
탐색이 끝나면 아래 요약을 사용자에게 먼저 보여준다.
```
📋 감지된 프로젝트 정보
이름: <name>
프레임워크: <framework>
패키지 매니저: <pm>
언어: TypeScript (strict: <yes/no>)
CSS: <css>
상태관리: <state>
테스트: <test>
✍️ 아래 파일을 업데이트하려고 합니다:
- CLAUDE.md (제목 · 주요 명령어 · 주의사항)
- .claude/project/overview.md
- .claude/project/conventions.md
- .claude/project/architecture.md
진행할까요? (y/n)
```
사용자가 **y** 또는 별도 지시 없이 진행을 허락할 때만 파일을 수정한다.
### 4. 각 파일 작성 규칙
#### CLAUDE.md
루트 `CLAUDE.md` 는 Claude 가 대화 시작 시 **항상** 읽는 파일이므로, 매번 하위 파일까지 탐색하지 않아도 되는 핵심 정보만 간결하게 기재한다.
- **제목** (`# <프로젝트 이름>`): `package.json``name` 필드로 교체
예) `# epic7-esports``# Epic7 Esports E7WC`
`name` 이 slug 형태면 사람이 읽기 좋은 표기로 변환한다.
- **`## 주요 명령어`** 섹션 (필수 추가):
`package.json``scripts` 에서 아래 유형을 추출하여 그룹별로 정리한다.
주석은 스크립트 이름만으로 의도를 알기 어려울 때만 달고, 뻔한 것은 생략한다.
```markdown
## 주요 명령어
# 개발 서버
<dev 계열 스크립트>
# 빌드
<build 계열 스크립트>
# 린트 / 포맷
<lint 계열 스크립트>
```
- `dev`, `start`, `local` 등 → **개발 서버** 그룹
- `build`, `build:*` → **빌드** 그룹
- `lint`, `lint:*`, `format`, `prettier` → **린트 / 포맷** 그룹
- `test`, `test:*` → **테스트** 그룹 (존재할 때만)
- 나머지 잡다한 스크립트는 포함하지 않는다.
- **`## 주의사항`** 섹션 (선택 추가):
프로젝트 고유의 "함정"이 될 수 있는 항목을 2-4개 이내로 요약한다.
소스는 `conventions.md` 의 **금지 사항** 섹션 + CSS/API 탐색 결과.
없으면 이 섹션은 추가하지 않는다.
```markdown
## 주의사항
- CSS: <탐색 결과에 따라 기재. 예: Tailwind 아님, SCSS 사용 — Tailwind 클래스 금지>
- API: <API 호출 패턴 금지/허용 규칙>
- 환경: <.env 파일 분기 규칙>
```
- 이미 제목이 실제 이름으로 바뀌어 있거나 섹션이 존재하면 덮어쓰지 않고 유지한다.
#### overview.md
- **서비스 > 이름**: `package.json` 의 `name` 필드 사용
- **서비스 > 설명**: `README.md` 첫 단락 또는 `package.json` 의 `description`
- **기술 스택**: 탐색에서 수집한 실제 값으로 채움. 모르면 `<확인 필요>` 유지
- **주요 기능**: `pages/` 또는 `app/pages/` 디렉토리 목록을 기반으로 유추
- **참고 문서**: `README.md` 에 링크가 있으면 옮기고, 없으면 `<확인 필요>` 유지
#### conventions.md
- **디렉토리 규칙**: 실제 디렉토리 구조에서 확인된 폴더만 설명, 없는 항목은 삭제
- **API 호출 창구**: `composables/` 또는 `api/` 디렉토리 패턴에서 유추
- **테스트 러너 / 위치**: `package.json` devDependencies + `vitest.config.*` 참고
- 공통 지침과 다른 점이 없다면 "공통 지침을 따른다" 로 간단히 기재
#### architecture.md
- **레이어 구조**: 실제 디렉토리 목록으로 ASCII 다이어그램을 업데이트
- **상태 관리 가이드**: 탐색에서 확인한 라이브러리로 표의 권장 위치를 채움
- **외부 의존성**: `server/api/` 파일명 또는 `useFetch` 호출에서 유추. 불확실하면 `<확인 필요>`
### 5. 완료 안내
```
✅ 문서 업데이트 완료
변경된 파일:
- CLAUDE.md
- .claude/project/overview.md
- .claude/project/conventions.md
- .claude/project/architecture.md
다음 단계:
1) 각 파일에서 <확인 필요> 항목을 직접 채워주세요.
2) 변경 사항을 커밋해 주세요:
git add CLAUDE.md .claude/project/
git commit -m "docs: 프로젝트 초기 문서 작성"
```
## 주의사항
- **파일 삭제 금지**: 기존 `.claude/project/*.md` 는 내용을 수정할 뿐 절대 삭제하지 않는다.
- **추측 표시**: 확실하지 않은 정보는 `(추정)` 을 붙이고, 사용자가 쉽게 찾아 수정할 수 있게 한다.
- **최소 변경**: 이미 올바르게 작성된 섹션은 건드리지 않는다.
- **커밋 금지**: 파일 수정 후 git commit 은 사용자 명시 요청이 없으면 실행하지 않는다.

View File

@@ -1,5 +1,5 @@
--- ---
name: vue-component-review name: verify-component-review
description: Vue 3 / Nuxt 컴포넌트 파일을 팀 공통 지침(gameservice-fe-agent) 기준으로 리뷰할 때 사용합니다. 사용자가 "이 컴포넌트 리뷰해줘", "컨벤션 맞는지 봐줘", "컴포넌트 체크" 등을 요청하면 트리거됩니다. description: Vue 3 / Nuxt 컴포넌트 파일을 팀 공통 지침(gameservice-fe-agent) 기준으로 리뷰할 때 사용합니다. 사용자가 "이 컴포넌트 리뷰해줘", "컨벤션 맞는지 봐줘", "컴포넌트 체크" 등을 요청하면 트리거됩니다.
--- ---

View File

@@ -0,0 +1,454 @@
---
name: wiki:work-log
description: 'Confluence 위키 페이지에 업무일지를 작성합니다'
license: MIT
---
# Confluence 업무일지 작성
Confluence 위키 페이지에 업무일지를 작성하는 스킬입니다.
## 빠른 참조
**프로세스:**
1. 위키 페이지 ID 또는 URL 확인 (환경변수 `WIKI_DAILY_WORK_LOG_URL` 우선 사용)
2. 날짜 입력 (MM/DD 형식)
3. 업무 내용 입력
4. 기존 페이지 읽기
5. 해당 날짜에 업무 내용 추가
6. 페이지 업데이트
**API 호출 순서:**
```typescript
// 1. 페이지 읽기
const page = await mcp__wiki__get_page_by_id({ page_id: "페이지ID" })
// 2. 내용 파싱 및 업데이트
// HTML 테이블 형식의 업무일지에서 해당 날짜 행 찾기
// 업무 내용 추가
// 3. 페이지 업데이트
await mcp__wiki__update_page({
page_id: "페이지ID",
title: page.title,
body: updatedBody
})
```
## 입력 파라미터
### 1. 위키 페이지 식별자 (조건부 필수)
- **환경변수 우선**: `.env.local``WIKI_DAILY_WORK_LOG_URL`이 설정되어 있으면 자동 사용
- **수동 입력**: 환경변수가 없거나 다른 페이지를 사용할 경우
- **페이지 ID**: 숫자로 된 페이지 ID (예: `684252757`)
- **페이지 URL**: Confluence 페이지 전체 URL
- URL에서 pageId 추출: `https://wiki.smilegate.net/pages/viewpage.action?pageId=684252757`
- `pageId=` 뒤의 숫자가 페이지 ID
### 2. 날짜 (필수)
- **형식**: `MM/DD` (예: `04/02`, `12/25`)
- 업무일지 테이블에서 해당 날짜 행을 찾는 데 사용
### 3. 업무 내용 (필수)
- **형식**: 텍스트 또는 HTML
- 업무일지의 "진행업무" 열에 추가될 내용
- HTML 마크업 지원 (리스트, 링크 등)
## 업무일지 테이블 구조
일반적인 업무일지는 다음과 같은 HTML 테이블 구조를 가집니다:
```html
<table class="fixed-width wrapped">
<tbody>
<tr>
<th style="text-align: center;">날짜</th>
<th style="text-align: center;">진행업무</th>
<th style="text-align: center;">기타 기록</th>
</tr>
<tr>
<td style="text-align: center;">04/01</td>
<td>업무 내용...</td>
<td><br /></td>
</tr>
<tr>
<td style="text-align: center;">04/02</td>
<td><br /></td>
<td><br /></td>
</tr>
</tbody>
</table>
```
## 프로세스 상세
### Step 1: 페이지 식별자 추출
URL에서 페이지 ID 추출:
```typescript
function extractPageId(input: string): string {
// 이미 숫자인 경우
if (/^\d+$/.test(input)) {
return input
}
// URL에서 pageId 추출
const match = input.match(/pageId=(\d+)/)
if (match) {
return match[1]
}
throw new Error('유효한 페이지 ID 또는 URL을 입력해주세요')
}
```
### Step 2: 환경변수 확인 및 사용자 입력 받기
먼저 `.env.local`에서 환경변수 확인:
```typescript
// .env.local 파일 읽기 (Read 툴 사용)
const envContent = await readFile('.env.local')
const wikiUrlMatch = envContent.match(/WIKI_DAILY_WORK_LOG_URL=['"](.+?)['"]/)
const defaultWikiUrl = wikiUrlMatch ? wikiUrlMatch[1] : null
// 환경변수가 있으면 자동 사용, 없으면 사용자에게 입력 받기
let pageIdInput: string
if (defaultWikiUrl) {
console.log(`✅ 환경변수에서 업무일지 URL 발견: ${defaultWikiUrl}`)
pageIdInput = defaultWikiUrl
} else {
// 사용자에게 입력 받기
const pageAnswer = await askUserQuestion({
questions: [{
question: "업무일지 페이지 ID 또는 URL을 입력해주세요",
header: "페이지",
options: [
{
label: "URL 입력",
description: "https://wiki.smilegate.net/pages/viewpage.action?pageId=..."
},
{
label: "페이지 ID 입력",
description: "숫자만 입력 (예: 684252757)"
}
],
multiSelect: false
}]
})
pageIdInput = pageAnswer.페이지 // Other 응답에서 실제 값 가져오기
}
// 날짜 입력 받기
const dateAnswer = await askUserQuestion({
questions: [{
question: "날짜를 입력해주세요 (MM/DD 형식)",
header: "날짜",
options: [
{ label: "오늘", description: "오늘 날짜 자동 입력" },
{ label: "직접 입력", description: "Other를 선택하여 MM/DD 형식으로 입력" }
],
multiSelect: false
}]
})
// 업무 내용은 별도로 입력받기 (긴 텍스트)
const contentAnswer = await askUserQuestion({
questions: [{
question: "업무 내용을 입력해주세요\n(Jira 이슈 번호를 포함하면 자동으로 링크가 생성됩니다)",
header: "업무내용",
options: [
{ label: "직접 입력", description: "Other를 선택하여 상세 내용 입력" }
],
multiSelect: false
}]
})
```
### Step 3: 날짜 처리
```typescript
function formatDate(dateInput: string): string {
if (dateInput === "오늘") {
const today = new Date()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${month}/${day}`
}
// MM/DD 형식 검증
if (!/^\d{2}\/\d{2}$/.test(dateInput)) {
throw new Error('날짜는 MM/DD 형식으로 입력해주세요 (예: 04/02)')
}
return dateInput
}
```
### Step 4: 페이지 읽기 및 파싱
```typescript
// 페이지 내용 가져오기
const page = await mcp__wiki__get_page_by_id({
page_id: pageId,
expand: "body.storage,version"
})
const currentBody = page.body.storage.value
const currentTitle = page.title
```
### Step 5: 업무 내용 포맷팅
Jira 이슈 자동 링크 변환:
```typescript
function formatWorkContent(content: string): string {
// SWV-123 형식을 Jira 링크로 변환
const withJiraLinks = content.replace(
/\b(SWV-\d+)\b/g,
'<a href="https://jira.smilegate.net/browse/$1">$1</a>'
)
// 간단한 마크다운 스타일 HTML 변환
let formatted = withJiraLinks
// 줄바꿈 처리
formatted = formatted.replace(/\n/g, '<br />')
return formatted
}
```
### Step 6: 테이블에서 날짜 행 찾기 및 업데이트
```typescript
function updateWorkLog(htmlBody: string, date: string, content: string): string {
// 날짜 셀 찾기: <td style="text-align: center;">04/02</td>
const datePattern = new RegExp(
`(<td[^>]*style="text-align: center;"[^>]*>${date}</td>\\s*<td[^>]*>)([^<]*|.*?)(</td>)`,
'i'
)
const match = htmlBody.match(datePattern)
if (!match) {
throw new Error(`날짜 ${date}에 해당하는 행을 찾을 수 없습니다.`)
}
// 기존 내용이 <br />만 있는 경우 대체, 아니면 추가
const existingContent = match[2].trim()
const newContent = existingContent === '<br />' || existingContent === ''
? content
: existingContent + '<br /><br />' + content
// 업데이트된 HTML 반환
return htmlBody.replace(datePattern, `$1${newContent}$3`)
}
```
### Step 7: 페이지 업데이트
```typescript
const updatedBody = updateWorkLog(currentBody, date, formattedContent)
await mcp__wiki__update_page({
page_id: pageId,
title: currentTitle,
body: updatedBody
})
console.log(`✅ 업무일지 업데이트 완료`)
console.log(`📅 날짜: ${date}`)
console.log(`🔗 페이지: https://wiki.smilegate.net/pages/viewpage.action?pageId=${pageId}`)
```
## 업무 내용 작성 예시
### 예시 1: 간단한 업무
**입력:**
```
로드나인 아시아 사전등록 페이지 버그 수정
```
**결과:**
```html
로드나인 아시아 사전등록 페이지 버그 수정
```
### 예시 2: Jira 이슈 포함
**입력:**
```
FE AI 표준화 로드맵 기술 검토 (SWV-955)
Schema.org 구조화된 데이터 추가 (SWV-956)
```
**결과:**
```html
FE AI 표준화 로드맵 기술 검토 (<a href="https://jira.smilegate.net/browse/SWV-955">SWV-955</a>)<br />
Schema.org 구조화된 데이터 추가 (<a href="https://jira.smilegate.net/browse/SWV-956">SWV-956</a>)
```
### 예시 3: 구조화된 업무 내용
**입력:**
```
<u>FE AI 표준화 및 SEO 개선</u>
- FE AI 표준화 로드맵 기술 검토 (SWV-955)
- Nuxt 3 기반 FE 개발 프로세스와 Claude Code AI Skills 통합
- 개발 프로세스 전 단계(기획~검증) AI 어시스턴트 활용 체계 수립
- Schema.org 구조화된 데이터 추가 (SWV-956)
- SEO 최적화 및 검색 엔진 가시성 개선 작업
- JSON-LD 형식으로 구조화된 데이터 구현 방안 검토
```
**결과:**
```html
<p><u>FE AI 표준화 및 SEO 개선</u></p>
<ul style="list-style-type: square;">
<li>FE AI 표준화 로드맵 기술 검토 (<a href="https://jira.smilegate.net/browse/SWV-955">SWV-955</a>)
<ul style="list-style-type: square;">
<li>Nuxt 3 기반 FE 개발 프로세스와 Claude Code AI Skills 통합</li>
<li>개발 프로세스 전 단계(기획~검증) AI 어시스턴트 활용 체계 수립</li>
</ul>
</li>
<li>Schema.org 구조화된 데이터 추가 (<a href="https://jira.smilegate.net/browse/SWV-956">SWV-956</a>)
<ul style="list-style-type: square;">
<li>SEO 최적화 및 검색 엔진 가시성 개선 작업</li>
<li>JSON-LD 형식으로 구조화된 데이터 구현 방안 검토</li>
</ul>
</li>
</ul>
```
## 에러 처리
### 1. 페이지를 찾을 수 없는 경우
```typescript
try {
const page = await mcp__wiki__get_page_by_id({ page_id: pageId })
} catch (error) {
console.error(`❌ 페이지를 찾을 수 없습니다: ${pageId}`)
console.error('페이지 ID 또는 URL을 확인해주세요.')
throw error
}
```
### 2. 날짜를 찾을 수 없는 경우
```typescript
if (!htmlBody.includes(`>${date}</td>`)) {
console.error(`❌ 날짜 ${date}를 찾을 수 없습니다.`)
console.error('업무일지 테이블에 해당 날짜가 있는지 확인해주세요.')
throw new Error(`날짜 ${date}에 해당하는 행을 찾을 수 없습니다.`)
}
```
### 3. 페이지 업데이트 실패
```typescript
try {
await mcp__wiki__update_page({
page_id: pageId,
title: currentTitle,
body: updatedBody
})
} catch (error) {
console.error(`❌ 페이지 업데이트 실패`)
console.error('권한이 있는지 확인해주세요.')
throw error
}
```
## 사용 예시
### 기본 사용 (환경변수 설정됨)
`.env.local``WIKI_DAILY_WORK_LOG_URL`이 설정되어 있으면:
```bash
/wiki:work-log
```
스킬 실행 후:
1. ✅ 환경변수에서 페이지 URL 자동 인식 (입력 불필요)
2. 날짜 선택: "오늘" 또는 "04/02"
3. 업무 내용 입력:
```
<u>FE AI 표준화 및 SEO 개선</u>
- FE AI 표준화 로드맵 기술 검토 (SWV-955)
- Nuxt 3 기반 FE 개발 프로세스와 Claude Code AI Skills 통합
- Schema.org 구조화된 데이터 추가 (SWV-956)
- SEO 최적화 및 검색 엔진 가시성 개선 작업
```
### 고급 사용: Jira 이슈 자동 연동
업무 내용에 Jira 이슈 번호를 포함하면 자동으로 링크가 생성됩니다:
```
로드나인 사전등록 기능 개선 (SWV-960)
SEO 메타 태그 최적화 (SWV-961)
```
→ 자동 변환:
```html
로드나인 사전등록 기능 개선 (<a href="https://jira.smilegate.net/browse/SWV-960">SWV-960</a>)<br />
SEO 메타 태그 최적화 (<a href="https://jira.smilegate.net/browse/SWV-961">SWV-961</a>)
```
## 환경변수 설정
기본 업무일지 페이지를 환경변수로 설정하면 매번 입력할 필요가 없습니다.
### `.env.local` 설정
```bash
# Confluence 업무일지 페이지 URL
WIKI_DAILY_WORK_LOG_URL='https://wiki.smilegate.net/pages/viewpage.action?pageId=YOUR_PAGE_ID'
```
**예시:**
```bash
WIKI_DAILY_WORK_LOG_URL='https://wiki.smilegate.net/pages/viewpage.action?pageId=684252757'
```
### 환경변수 우선순위
1. `.env.local`에 `WIKI_DAILY_WORK_LOG_URL` 설정됨 → 자동 사용
2. 환경변수 없음 → 사용자에게 수동 입력 요청
### 다른 페이지 사용하기
환경변수가 설정되어 있어도 다른 페이지에 작성하려면:
- 스킬 실행 시 "Other"를 선택하여 다른 페이지 URL/ID 입력 가능
## 주의사항
- **페이지 권한**: 페이지를 수정할 수 있는 권한이 필요합니다
- **날짜 형식**: MM/DD 형식을 정확히 지켜야 합니다 (예: `04/02`, `12/25`)
- **테이블 구조**: 업무일지가 표준 테이블 구조를 따라야 합니다
- **기존 내용**: 해당 날짜에 이미 내용이 있으면 추가됩니다 (덮어쓰지 않음)
- **HTML 형식**: 업무 내용은 Confluence HTML 형식을 따라야 합니다
## 토큰 효율
이 스킬 문서: ~2000 tokens
## 검증 완료
이 스킬은 실제 Confluence 업무일지 작성으로 검증되었습니다:
- 테스트 페이지: 684252757 (2604_김형길 차장)
- 작성일: 2026-04-02
- 상태: 성공적으로 업데이트 완료
- 환경변수: `.env.local`의 `WIKI_DAILY_WORK_LOG_URL` 지원 추가됨

View File

@@ -0,0 +1,85 @@
---
title: "WDG00.04.02.06.07.01 AI 활용 - CBO-플랫폼서비스개발담당"
source: "https://wiki.smilegate.net/pages/viewpage.action?pageId=694388022"
author:
- "[[김형길 (Gil)/SGP 커뮤니티 Product]]"
published:
created: 2026-05-04
description:
tags:
- "clippings"
---
## AI 활용 방안
---
퍼블리싱 FE 전체 워크프로세스에서 Claude Code Skills 기반 AI 도입 전(As-Is)과 도입 후(To-Be)를 단계별로 정리합니다.
---
## 단계별 Skill
| 단계 | Skill | 프롬프트 예시 | 산출물 |
| --- | --- | --- | --- |
| 프로젝트 Init | `project-init` | "프로젝트 초기화해줘" / `/init` | `.claude/project/*.md` |
| 기획 | `plan-analyzer` | "이 기획서 분석해줘: `<pptx 경로>`" | 요구사항 명세 MD |
| | `plan-translation-generator` | "번역코드 만들어줘: `<xlsx 경로>`" | 번역 키 채워진 xlsx |
| 마크업 | `markup-base` | "이 화면 시멘틱 HTML로 마크업해줘" | `.vue` 컴포넌트 골격 |
| | `markup-figma` | "이 Figma 마크업해줘: `<figma url>`" | `.vue` 컴포넌트 |
| | `markup-promotion` | "프로모션 페이지 마크업해줘" | 랜딩 페이지 `.vue` |
| | `markup-edm` | "EDM 메일 만들어줘: `<figma url>`" | 이메일 HTML |
| 개발 | `dev-component` | "이 화면 컴포넌트로 분리 설계해줘" | 컴포넌트 트리 + SFC 스켈레톤 |
| | `dev-docs` | "Nuxt server route로 API 만들어줘" | Nuxt 3 Best Practice 코드 |
| | `dev-api-state` | "이 API 연동 + Pinia 스토어 만들어줘" | 스토어 + 페칭 코드 |
| | `dev-unit-test` | "이 컴포넌트 단위 테스트 작성해줘" | `.spec.ts` |
| | `dev-storybook` | "이 컴포넌트 Story 만들어줘" | `.stories.ts` + 사용 가이드 |
| 코드 리뷰 | `verify-component-review` | "이 컴포넌트 리뷰해줘" | 우선순위별 리뷰 리포트 |
| 검증 | `verify-requirement` | "요구사항 대비 누락 기능 체크해줘" | 누락/불일치 리포트 |
| | `verify-perf` | "성능 최적화 분석해줘" | Core Web Vitals 개선 가이드 |
| | `verify-a11y` | "접근성(WCAG) 검증해줘" | a11y 리포트 + 수정 코드 |
| | `verify-seo-geo` | "SEO/AEO/GEO 검증해줘" | 메타 + Schema 코드 |
| | `security-review` | "보안 점검해줘" | 보안 리포트 + 패치 가이드 |
| 기타 업무 스킬 | `work-log` | "오늘 업무일지 작성해줘" | Confluence 업데이트 |
| | `work-mr-reviewer` | "이 MR 리뷰해줘: `<MR URL>`" | 리뷰 코멘트 초안 |
| | `work-code-reviewer` | "변경 코드 리뷰해줘" | 우선순위별 리뷰 리포트 |
## 📋 단계별 As-Is / To-Be
| | | | | | |
| --- | --- | --- | --- | --- | --- |
| **기획** | Phase 1 | - 기획서를 개발자가 수동으로 읽고 페이지 구조·컴포넌트를 직접 설계 - 기획 의도 해석 오류로 인한 재작업 발생 - 플로우차트 작성에 별도 시간 소요 - 개발자마다 요구사항 해석 기준 상이 | - **requirement-analyzer** Skill이 기획서를 자동 파싱 - Nuxt pages/ 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록 자동 생성 - Mermaid 플로우차트로 화면 전환 흐름 자동 시각화 - 팀 전체가 동일한 요구사항 명세 기반으로 개발 착수 | `requirement-analyzer` | |
| **다국어** | Phase 1-1 | - 번역 요청 후 결과를 다국어 번역코드 키값을 엑셀파일에서 수동 입력 - 누락된 번역 키나 중복 키값 발생 시 런타임에서야 발견 | - AI가 번역 코드 전체에서 번역 대상 문자열에서 자동 추출 생성 - 번역 키 누락 및 중복을 사전 감지 및 경고 | `translation-keys` | |
| **마크업 (기본)** | Phase 2 | - 개발자 역량에 따라 시멘틱 태그 사용 수준 불일치 - Tailwind 클래스 순서 및 패턴 제각각 - 접근성(ARIA) 검토를 별도 단계로 진행 - 반응형 브레이크포인트 구현 기준 없음 | - **markup** Skill이 시멘틱 HTML 구조 자동 생성 - Tailwind 클래스 7단계 순서 컨벤션 자동 적용 - ARIA 레이블 및 접근성 속성 자동 삽입 - 팀 전체 마크업 품질 균일화 | `markup` | |
| **PSD → Figma** | Phase 2-0 | - PSD 파일을 디자이너가 Figma에 수동 재작업 - 레이어 구조 재설계로 많은 시간 소요 - 변환 과정에서 디자인 요소 누락·변형 발생 | - **Codia AI** / **PSD Importer for Figma** 활용으로 자동 컨버팅 - PSD 레이어 구조를 유지하며 Figma 포맷으로 변환 - 디자이너 수작업 시간 대폭 단축 | Codia AI / PSD Importer for Figma | |
| **Figma → HTML** | Phase 2-1 | - 피그마 시안을 보며 개발자가 수동으로 HTML 작성 - 반응형 브레이크포인트를 수동으로 추출·적용 - 피그마 컴포넌트와 HTML 구조 간 매핑 기준 없음 | - **markup (Figma)** Skill + Figma MCP로 레이어 자동 파싱 - 피그마 컴포넌트를 Nuxt SFC 구조로 자동 매핑 - 반응형 브레이크포인트 자동 추출 및 Tailwind 클래스 적용 | `markup` + Figma MCP | |
| **프로모션 마크업** | Phase 2-2 | - 프로모션/랜딩페이지마다 마크업 패턴 상이 - 반응형 대응 일관성 없음 - 프로모션별 재작업 반복으로 생산성 저하 | - **markup-promotion** Skill로 캠페인 표준 마크업 패턴 자동 생성 - 시멘틱 구조, 반응형 일관 적용 - 프로모션 유형별 템플릿 재사용으로 개발 속도 향상 | `markup-promotion` | |
| **EDM 마크업** | Phase 2-3 | - 이메일 클라이언트별(Outlook 등) 호환성 수동 대응 - 테이블 기반 레이아웃 반복 작성으로 시간 소요 - 인라인 스타일 누락으로 렌더링 오류 발생 | - **markup-edm** Skill로 이메일 클라이언트 호환 table 기반 마크업 자동 생성 - 인라인 스타일 자동 적용으로 렌더링 오류 사전 방지 - 뉴스레터·EDM 표준 구조 일관 유지 | `markup-edm` | |
| **FE 개발 (컴포넌트)** | Phase 3 | - 개발자마다 컴포넌트 분리 기준 상이 - Nuxt 디렉토리 구조(pages/, components/, composables/) 불일치 - 컴포넌트 재사용률 낮음 (30% 수준) - 신규 인력 온보딩 기간 필요 | - **dev-component** Skill이 Atomic Design 기반 컴포넌트 트리 자동 설계 - 표준 디렉토리 구조 자동 제안으로 일관성 확보 - 컴포넌트 재사용률 70%↑ 목표 - 온보딩 기간 단축 | `dev-component` | |
| **Nuxt 공식 문서 기반 개발** | Phase 3-1 | - Nuxt 공식문서를 직접 검색하며 Best Practice 파악 - server/, middleware/, plugins/ 활용 기준 불명확 - composable 작성 시 공식 패턴 확인에 시간 소요 | - **dev-docs** Skill이 6개 참조 문서(server, middleware, plugins, composables, components, config)를 컨텍스트에 따라 선택 로드 - 즉시 Nuxt 3 Best Practice 코드 생성 - 공식문서 탐색 없이 정확한 API 활용 | `dev-docs` | |
| **단위 테스트** | Phase 3-2 | - 컴포넌트 개발 완료 후 테스트 작성 생략 또는 뒤늦게 작성 - Vue Test Utils / Vitest 설정을 매번 조사 - 테스트 케이스 구성 기준 없어 커버리지 20% 미만 - 테스트 코드 패턴이 개발자마다 상이 | - **dev-unit-test** Skill이 컴포넌트 분석 후 Vitest + Vue Test Utils 기반 단위 테스트 자동 생성 - Props / Emits / 슬롯 / 인터랙션 케이스 자동 커버 - 테스트 패턴 팀 표준화로 코드 리뷰 부담 감소 - 커버리지 80%↑ 목표 | `dev-unit-test` | |
| **컴포넌트 문서화** | Phase 3-3 | - 컴포넌트 사용법 문서화 없어 팀원이 소스 코드 직접 확인 - Storybook 미운영으로 컴포넌트 목록 파악 어려움 - Props / Emits 스펙 구두 전달 의존 - 신규 인력이 재사용 가능한 컴포넌트를 중복 개발 | - **dev-storybook** Skill로 컴포넌트 Props / Emits / 슬롯 기반 사용 가이드 자동 생성 - Storybook Story 파일 자동 생성 - 팀 내 컴포넌트 재사용 진입 장벽 감소 - 컴포넌트 카탈로그 자동 갱신 | `dev-storybook` | |
| **API 연동 & 상태관리** | Phase 4 | - useFetch / $fetch / useAsyncData 혼용으로 일관성 없음 - Pinia 스토어 설계 기준 없어 구조 제각각 - 에러 핸들링 패턴 통일 없음 - BFF 패턴 미활용으로 API 키 노출 위험 | - **dev-api-state** Skill로 상황별 데이터 페칭 패턴 자동 선택·생성 - Setup Store 기반 Pinia 스토어 표준 코드 자동 생성 - 에러 핸들링·로딩 상태 처리 패턴 통일 - server/api/ BFF 패턴으로 API 키 보호 | `dev-api-state` | |
| **코드 리뷰** | Phase 4-1 | - PR 리뷰어가 팀 컨벤션을 기억하며 수동 확인 - 리뷰어 역량 차이로 컨벤션 누락 발생 - 반복적인 스타일 지적으로 리뷰 사이클 증가 - 리뷰어 부재 시 병목 발생 - 비즈니스 로직보다 코드 스타일 지적에 리뷰 시간 소비 | - **verify-component-review** Skill이 PR 머지 전 팀 공통 지침 기준으로 자동 사전 검토 - Composition API / 타입 / Tailwind 컨벤션 위반 자동 감지 - 반복적 스타일 지적 제거로 리뷰어가 비즈니스 로직 집중 - 코드 리뷰 반려율 50%↓ 목표 | `verify-component-review` | |
| **접근성 검증** | Phase 5-0 | - 접근성 검토 별도 단계 없음 - ARIA 속성 누락 QA 단계에서야 사후 발견 - 키보드 네비게이션 테스트 수동 진행 - WCAG 기준 인지 부족으로 누락 항목 반복 발생 - 스크린 리더 대응 미검증 | - **verify-a11y** Skill로 WCAG 2.1 AA 기준 자동 검증 - ARIA 레이블 누락 / 키보드 포커스 순서 / 색상 대비 비율 자동 감지 - 개선 가이드 코드 레벨 자동 제시 - 접근성 관련 QA 반려 건수 80%↓ 목표 | `verify-a11y` | |
| **요구사항 검증** | Phase 5-1 | - 개발 완료 후 QA 단계에서 수동 검증 - 기획 대비 누락 기능을 늦게 발견하여 재작업 발생 - 기획자·개발자 간 스펙 해석 차이 사후 발견 | - **requirement-optimizer** Skill이 Phase 1 요구사항 명세와 실제 구현 결과를 자동 비교 - 기능 누락, 스펙 불일치, 미구현 항목 사전 감지 - 개선 방안 자동 제시로 QA 전 품질 확보 | `requirement-optimizer` | |
| **성능 최적화** | Phase 5-2 | - Lighthouse 수동 실행 후 결과 해석에 시간 소요 - Core Web Vitals 개선 방법 별도 조사 필요 - Lighthouse 선택적 확인 - 번들 최적화·이미지 최적화 개발자 개인 역량 의존 | - **perf-optimizer** Skill로 성능 병목 자동 분석 - 코드 스플리팅, 이미지 최적화, SSR/ISR 전략 자동 제안 - LCP < 2.5s / CLS < 0.1 / INP < 200ms 달성 가이드 - Lighthouse 점수 80+ 목표 | `perf-optimizer` | |
| **SEO · GEO · AEO 검증** | Phase 6 | - SEO 메타태그 누락률 40% 수준 - Schema.org 구조화 데이터 미적용 - AI 검색(ChatGPT, Perplexity, Google AI Overview) 대응 전무 - 검색 가시성 개선 활동 미체계화 | - **nuxt-seo-geo** Skill로 SEO · AEO · GEO 3계층 자동 감사 - useSeoMeta() / useSchemaOrg() 기반 메타·구조화 데이터 자동 생성 - FAQ·HowTo Schema 적용으로 AI 검색 직접 답변 노출 - SEO 메타 누락률 5%↓ · AI 검색 인용률 측정 체계 구축 | `nuxt-seo-geo` | |
| **보안 검토** | Phase 6-1 | - 보안 취약점 점검 별도 프로세스 없음 - XSS / CSRF 프론트엔드 보안 항목 개인 역량 의존 - npm audit 결과 미모니터링 - 민감 정보 하드코딩 발생 위험 - 서드파티 라이브러리 취약점 미파악 | - **security-review** Skill로 XSS / CSRF / 의존성 취약점 자동 감지 - 민감 정보 하드코딩 코드베이스 전체 스캔 - npm audit 결과 해석 패치 우선순위 가이드 자동 생성 - 배포 보안 체크리스트 자동 검증 | `security-review` | |
---
## 📊 성과 지표 (KPI)
| | | | |
| --- | --- | --- | --- |
| 코드 리뷰 반려율 | 35% | **15%↓** | Phase 2, 3, 4, 4-1 마크업·컴포넌트·API 패턴 표준화 + verify-component-review 사전 자동 검토 |
| 컴포넌트 재사용률 | 30% | **70%↑** | Phase 3 Atomic Design 기반 컴포넌트 아키텍처 |
| 신규 인력 온보딩 기간 | 3주 | **1주** | Phase 1, 3 요구사항 자동 분석 + Skill 기반 가이드 |
| 단위 테스트 커버리지 | 20% 미만 | **80%↑** | Phase 3-2 dev-unit-test 자동 생성 |
| Lighthouse 성능 점수 | 65~75 | **90+** | Phase 5-2 Core Web Vitals 최적화 |
| 접근성(WCAG) 관련 QA 반려 건수 | 미측정 | **80%↓** | Phase 5-0 verify-a11y 자동 감지 |
| SEO 메타 누락률 | 40% | **5%↓** | Phase 6 SEO·GEO·AEO 통합 검증 |
| AI 검색 인용률 | 미측정 | **측정 체계 구축** | Phase 6 GEO 최적화 (ChatGPT, Perplexity, AI Overview) |
| 프로모션·EDM 마크업 작업 시간 | 건당 수동 작업 | **50%↓ (자동화)** | Phase 2-2, 2-3 프로모션·EDM Skill 적용 |
| 기획-개발 스펙 불일치 건수 | QA 단계 사후 발견 | **개발 중 사전 감지** | Phase 5-1 requirement-optimizer 자동 비교 검증 |
| 보안 취약점 사전 감지율 | 0% (배포 발견) | **배포 전 100% 스캔** | Phase 6-1 security-review 자동 검증 |

View File

@@ -0,0 +1,794 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 작업 단계별 AI 참조 흐름도</title>
<style>
:root {
--navy: #17172b;
--blue: #0b63ce;
--blue-soft: #eaf4ff;
--purple: #5937b7;
--purple-soft: #f1ebff;
--green: #15975f;
--green-soft: #e8f8ef;
--orange: #d97706;
--orange-soft: #fff4dd;
--yellow: #f2b43f;
--yellow-soft: #fff8e6;
--red: #d94841;
--red-soft: #fff0f0;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dce4ef;
--text: #1f2937;
--muted: #667085;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
line-height: 1.55;
}
.hero {
padding: 34px 44px 36px;
color: #fff;
background: var(--navy);
border-bottom: 4px solid #1677ff;
}
.hero .eyebrow {
margin: 0 0 8px;
color: #8d96b8;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
margin: 0 0 10px;
font-size: 27px;
font-weight: 900;
letter-spacing: -0.04em;
}
.hero p {
margin: 0;
color: #c2c8da;
font-size: 13px;
}
.container {
width: min(1180px, calc(100% - 56px));
margin: 28px auto 48px;
}
.card {
margin-bottom: 22px;
padding: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 18px;
color: #315a9e;
font-size: 15px;
font-weight: 900;
letter-spacing: -0.02em;
}
.section-title::before {
content: "";
width: 4px;
height: 18px;
background: var(--blue);
border-radius: 999px;
}
.notice {
padding: 12px 16px;
margin-bottom: 16px;
color: #17436f;
font-size: 13px;
font-weight: 700;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.case-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.case-box {
min-height: 126px;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.case-box.case1 {
border-color: #a8caff;
background: #f7fbff;
}
.case-box.case2 {
border-color: #c8b5ff;
background: #fbf8ff;
}
.case-label {
display: inline-block;
margin-bottom: 10px;
padding: 4px 11px;
color: #fff;
font-size: 12px;
font-weight: 900;
border-radius: 999px;
}
.case1 .case-label {
background: var(--blue);
}
.case2 .case-label {
background: var(--purple);
}
.case-box h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 900;
}
.case-box p {
margin: 0;
color: #475467;
font-size: 13px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 1px solid var(--line);
}
th,
td {
padding: 12px 14px;
text-align: left;
vertical-align: top;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
th {
color: #344054;
font-weight: 900;
background: #eef4ff;
}
td:last-child,
th:last-child {
border-right: 0;
}
.path {
color: #c0392b;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
white-space: nowrap;
}
.load-badge {
display: inline-block;
padding: 3px 8px;
color: #915f00;
background: var(--yellow-soft);
border: 1px solid #e8bd54;
border-radius: 999px;
font-size: 11px;
font-weight: 900;
white-space: nowrap;
}
.load-badge.blue {
color: #0b4fa6;
background: var(--blue-soft);
border-color: #9ec3ff;
}
.quote {
padding: 12px 16px;
margin-bottom: 18px;
color: #0c55b0;
font-size: 14px;
font-weight: 700;
font-style: italic;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.quote.purple {
color: #4d2e91;
background: var(--purple-soft);
border-left-color: var(--purple);
}
.tree {
position: relative;
padding-left: 24px;
}
.tree::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 6px;
width: 2px;
background: #e4e7ec;
}
.tree-node {
position: relative;
margin: 0 0 16px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.tree-node::before {
content: "";
position: absolute;
top: 24px;
left: -18px;
width: 18px;
height: 2px;
background: #e4e7ec;
}
.tree-node:last-child {
margin-bottom: 0;
}
.node-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-weight: 900;
}
.round {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: #fff;
font-size: 12px;
font-weight: 900;
background: var(--blue);
border-radius: 999px;
flex-shrink: 0;
}
.round.green {
background: var(--green);
}
.round.orange {
background: var(--orange);
}
.round.purple {
background: var(--purple);
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px 0;
}
.chip {
display: inline-block;
padding: 4px 9px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
border: 1px solid;
border-radius: 999px;
background: #fff;
}
.chip.yellow {
color: #8a5a00;
border-color: #e6b64e;
background: var(--yellow-soft);
}
.chip.green {
color: #087443;
border-color: #8bd3aa;
background: var(--green-soft);
}
.chip.blue {
color: #0b4fa6;
border-color: #9ec3ff;
background: var(--blue-soft);
}
.chip.purple {
color: #4d2e91;
border-color: #c6b6ff;
background: var(--purple-soft);
}
.chip.red {
color: #a33a34;
border-color: #e4a39f;
background: var(--red-soft);
}
.work-box {
margin-top: 10px;
padding: 12px 14px;
border-radius: 6px;
font-size: 12px;
}
.work-box.blue {
background: var(--blue-soft);
border-left: 4px solid var(--blue);
}
.work-box.green {
background: var(--green-soft);
border-left: 4px solid var(--green);
}
.work-box.yellow {
background: var(--yellow-soft);
border-left: 4px solid var(--yellow);
}
.work-box.red {
background: var(--red-soft);
border-left: 4px solid var(--red);
}
.work-box ul {
margin: 4px 0 0;
padding-left: 18px;
}
.case2-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.stage-card {
padding: 16px;
border: 2px solid;
border-radius: 8px;
background: #fff;
}
.stage-card.purple {
border-color: var(--purple);
}
.stage-card.green {
border-color: var(--green);
}
.stage-card.orange {
border-color: var(--orange);
}
.stage-card.red {
border-color: var(--red);
}
.stage-card .stage-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
font-size: 15px;
font-weight: 900;
}
.example {
padding: 10px 12px;
margin-bottom: 12px;
font-size: 12px;
font-style: italic;
border-radius: 4px;
background: #f8f5ff;
color: #4d2e91;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
padding: 12px 14px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
font-size: 12px;
}
.legend strong {
margin-right: 4px;
}
@media (max-width: 900px) {
.container {
width: calc(100% - 28px);
}
.hero {
padding: 28px 24px;
}
.case-grid,
.case2-grid {
grid-template-columns: 1fr;
}
table,
thead,
tbody,
tr,
th,
td {
display: block;
width: 100%;
}
thead {
display: none;
}
tr {
border-bottom: 1px solid var(--line);
}
td {
border-right: 0;
}
}
</style>
</head>
<body>
<header class="hero">
<p class="eyebrow">Frontend AI Guidelines — gameservice-fe-agent</p>
<h1>프론트엔드 작업 단계별 AI 참조 흐름도</h1>
<p>
프론트엔드 개발 요청 시 공통 지침(rules/)을 기반으로 Claude AI에게 업무를 위임할 때
어떤 파일을 언제 참조하는지 정리합니다.
</p>
</header>
<main class="container">
<!-- 작업 사례 -->
<section class="card">
<h2 class="section-title">실제 작업 사례 기준</h2>
<div class="notice">
예시: <strong>게임 카드 컴포넌트에 신규 출시 뱃지 추가 (Vue 3 + Nuxt)</strong>
</div>
<div class="case-grid">
<article class="case-box case1">
<span class="case-label">CASE 1</span>
<h3>자연어 업무 요청</h3>
<p>
AI가 claude-workflow.md에 따라 탐색 → 계획 → 구현 → 검증 순서로
전체 흐름을 주도하며 진행합니다. 모호한 부분은 질문합니다.
</p>
</article>
<article class="case-box case2">
<span class="case-label">CASE 2</span>
<h3>@rules 직접 지정 · 단계 분리</h3>
<p>
개발자가 각 작업 단계마다 필요한 rules/ 파일을 직접 지정하고
탐색 · 계획 · 구현 · 검증을 나누어 요청합니다.
</p>
</article>
</div>
</section>
<!-- 공통 자동 로드 -->
<section class="card">
<h2 class="section-title">공통 자동 로드 구조 — 모든 대화에서 항상 적용</h2>
<table>
<thead>
<tr>
<th style="width: 120px;">구분</th>
<th style="width: 280px;">파일</th>
<th>주요 내용</th>
<th style="width: 120px;">로드 시점</th>
</tr>
</thead>
<tbody>
<tr>
<td>공통 지침 진입점</td>
<td><span class="path">CLAUDE.md</span></td>
<td>rules/* 전체를 @import. 공통 지침 우선순위 및 skill 사용 안내</td>
<td><span class="load-badge blue">세션 1회</span></td>
</tr>
<tr>
<td>코딩 컨벤션</td>
<td><span class="path">rules/coding-conventions.md</span></td>
<td>포맷팅(스페이스 2칸·싱글 쿼터·세미콜론), 네이밍(camelCase·PascalCase), any 금지, import 순서</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>프레임워크 규칙</td>
<td><span class="path">rules/framework-rules.md</span></td>
<td>Vue 3 &lt;script setup lang="ts"&gt;, Pinia, useFetch, Tailwind 유틸리티 우선, clsx 조건부 클래스</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>커밋 / PR 규칙</td>
<td><span class="path">rules/commit-pr.md</span></td>
<td>Conventional Commits 형식, subject 50자·명령형, Squash merge, CI + 1인 승인 필수</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>작업 방식 지침</td>
<td><span class="path">rules/claude-workflow.md</span></td>
<td>탐색→계획→구현→검증 순서, 최소 변경 원칙, 모호 시 질문, 임의 기능 추가 금지</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
</tbody>
</table>
</section>
<!-- CASE 1 Tree -->
<section class="card">
<h2 class="section-title">CASE 1 자연어 업무 요청 — 자동 참조 흐름 (Tree)</h2>
<div class="quote">"게임 카드 컴포넌트에 신규 출시 뱃지 추가해줘."</div>
<div class="tree">
<article class="tree-node">
<div class="node-head"><span class="round">0</span>자동 로드</div>
<p>모든 대화 시작 시 CLAUDE.md와 rules/* 4개 파일을 자동으로 참조합니다.</p>
<div class="chip-row">
<span class="chip blue">CLAUDE.md</span>
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">1</span>요구사항 분석 — 모호한 부분 질문</div>
<p>claude-workflow.md 원칙에 따라 추측 대신 사용자에게 확인합니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box yellow">
<strong>예상 질문</strong>
<ul>
<li>뱃지는 어떤 조건(출시일 기준 N일 이내 등)일 때 표시하나요?</li>
<li>뱃지 디자인은 기존 디자인 시스템 컴포넌트를 사용하나요?</li>
<li>GameCard 컴포넌트는 여러 페이지에서 공유 중인가요?</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">2</span>탐색 — 관련 파일 파악 · 유사 패턴 확인</div>
<p>GameCard 컴포넌트와 주변 컨벤션을 먼저 읽고 유사 뱃지 구현 패턴을 찾습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>탐색 항목</strong>
<ul>
<li>GameCard.vue 현재 구조 파악</li>
<li>기존 뱃지 컴포넌트(Badge.vue 등) 존재 여부 확인</li>
<li>props 타입 정의 위치 확인 (types/ 또는 도메인 types.ts)</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">3</span>계획 — 할 일 목록 작성 · 승인</div>
<p>변경 파일 목록과 구현 순서를 공유하고, 아키텍처 영향이 있으면 승인을 받습니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>계획 산출물</strong>
<ul>
<li>GameCard.vue — isNew prop 추가, 뱃지 렌더링 조건 분기</li>
<li>types/game.ts — isNew: boolean 필드 추가</li>
<li>기존 뱃지 컴포넌트 재사용 or 인라인 처리 결정</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round green">4</span>구현 — 코드 작성</div>
<p>계획에 따라 Vue 컴포넌트와 타입을 수정합니다. 요청 범위를 벗어나는 리팩토링은 하지 않습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>구현 항목</strong>
<ul>
<li>defineProps&lt;T&gt;()에 isNew?: boolean 추가</li>
<li>Tailwind 유틸리티 클래스로 뱃지 스타일 적용</li>
<li>조건부 클래스에 clsx 사용, 200줄 초과 시 분리 검토</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round orange">5</span>검증 — 린트 · 빌드 · 커밋 / PR 작성</div>
<p>타입체크와 빌드를 확인한 뒤 Conventional Commits 형식으로 커밋과 PR을 작성합니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
<div class="work-box red">
<strong>최종 산출물</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>커밋: <code>feat(game-card): add new-release badge</code></li>
<li>PR 본문: 변경 사항·배경·테스트·체크리스트 작성</li>
</ul>
</div>
</article>
</div>
</section>
<!-- CASE 2 -->
<section class="card">
<h2 class="section-title">CASE 2 @rules 직접 지정 · 단계별 업무 요청 — 직접 참조 흐름 (Tree)</h2>
<div class="quote purple">
개발자가 각 작업 단계마다 필요한 rules/ 파일 또는 skill을 직접 지정해 요청합니다.
</div>
<div class="case2-grid">
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">1</span>탐색 단계</h3>
<div class="example">"GameCard 컴포넌트 구조 파악해줘. @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>컴포넌트 파일 구조 및 props 정리</li>
<li>재사용 가능한 유사 패턴 목록</li>
<li>영향받는 파일 범위 파악</li>
</ul>
</div>
</article>
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">2</span>계획 단계</h3>
<div class="example">"뱃지 추가 구현 계획 잡아줘. @rules/claude-workflow.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>수정 파일 목록 및 변경 순서</li>
<li>아키텍처 영향 범위 정리</li>
<li>사용자 승인 필요 항목 명시</li>
</ul>
</div>
</article>
<article class="stage-card green">
<h3 class="stage-title"><span class="round green">3</span>구현 단계</h3>
<div class="example">"계획대로 구현해줘. @rules/coding-conventions.md @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>Vue 컴포넌트 코드 수정</li>
<li>TypeScript 타입 추가 · 수정</li>
<li>Tailwind 클래스 적용 및 clsx 처리</li>
</ul>
</div>
</article>
<article class="stage-card orange">
<h3 class="stage-title"><span class="round orange">4</span>검증 단계</h3>
<div class="example">"변경 코드 검증하고 커밋·PR 작성해줘. @rules/commit-pr.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
</div>
<div class="work-box red">
<strong>산출 내용</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>Conventional Commits 커밋 메시지 작성</li>
<li>PR 템플릿(변경사항·배경·테스트·체크리스트) 작성</li>
</ul>
</div>
</article>
</div>
<div style="margin-top: 16px; padding: 12px 14px; background: var(--purple-soft); border-left: 4px solid var(--purple); border-radius: 4px; font-size: 13px; color: #4d2e91;">
<strong>Skill 활용 팁</strong> — 반복 업무는 스킬 명령으로 더 빠르게 처리할 수 있습니다.<br>
<span style="font-family: monospace; font-size: 12px;">
/conventional-commit &nbsp;·&nbsp; /verify-component-review &nbsp;·&nbsp; /plan-analyzer &nbsp;·&nbsp; /markup-edm
</span>
</div>
</section>
<!-- 범례 -->
<section class="card">
<h2 class="section-title">범례</h2>
<div class="legend">
<strong>공통 자동</strong>
<span class="chip blue">CLAUDE.md — 세션 1회 자동 로드</span>
<span class="chip yellow">rules/* — 매 대화 턴 자동 로드 (4개 파일)</span>
<span class="chip green">단계별 주요 참조 파일</span>
<span class="chip purple">@rules — 직접 지정 (CASE 2)</span>
<span class="chip red">검증 · 커밋 / PR 단계</span>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,794 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 작업 단계별 AI 참조 흐름도</title>
<style>
:root {
--navy: #17172b;
--blue: #0b63ce;
--blue-soft: #eaf4ff;
--purple: #5937b7;
--purple-soft: #f1ebff;
--green: #15975f;
--green-soft: #e8f8ef;
--orange: #d97706;
--orange-soft: #fff4dd;
--yellow: #f2b43f;
--yellow-soft: #fff8e6;
--red: #d94841;
--red-soft: #fff0f0;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dce4ef;
--text: #1f2937;
--muted: #667085;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
line-height: 1.55;
}
.hero {
padding: 34px 44px 36px;
color: #fff;
background: var(--navy);
border-bottom: 4px solid #1677ff;
}
.hero .eyebrow {
margin: 0 0 8px;
color: #8d96b8;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
margin: 0 0 10px;
font-size: 27px;
font-weight: 900;
letter-spacing: -0.04em;
}
.hero p {
margin: 0;
color: #c2c8da;
font-size: 13px;
}
.container {
width: min(1180px, calc(100% - 56px));
margin: 28px auto 48px;
}
.card {
margin-bottom: 22px;
padding: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 18px;
color: #315a9e;
font-size: 15px;
font-weight: 900;
letter-spacing: -0.02em;
}
.section-title::before {
content: "";
width: 4px;
height: 18px;
background: var(--blue);
border-radius: 999px;
}
.notice {
padding: 12px 16px;
margin-bottom: 16px;
color: #17436f;
font-size: 13px;
font-weight: 700;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.case-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.case-box {
min-height: 126px;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.case-box.case1 {
border-color: #a8caff;
background: #f7fbff;
}
.case-box.case2 {
border-color: #c8b5ff;
background: #fbf8ff;
}
.case-label {
display: inline-block;
margin-bottom: 10px;
padding: 4px 11px;
color: #fff;
font-size: 12px;
font-weight: 900;
border-radius: 999px;
}
.case1 .case-label {
background: var(--blue);
}
.case2 .case-label {
background: var(--purple);
}
.case-box h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 900;
}
.case-box p {
margin: 0;
color: #475467;
font-size: 13px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 1px solid var(--line);
}
th,
td {
padding: 12px 14px;
text-align: left;
vertical-align: top;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
th {
color: #344054;
font-weight: 900;
background: #eef4ff;
}
td:last-child,
th:last-child {
border-right: 0;
}
.path {
color: #c0392b;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
white-space: nowrap;
}
.load-badge {
display: inline-block;
padding: 3px 8px;
color: #915f00;
background: var(--yellow-soft);
border: 1px solid #e8bd54;
border-radius: 999px;
font-size: 11px;
font-weight: 900;
white-space: nowrap;
}
.load-badge.blue {
color: #0b4fa6;
background: var(--blue-soft);
border-color: #9ec3ff;
}
.quote {
padding: 12px 16px;
margin-bottom: 18px;
color: #0c55b0;
font-size: 14px;
font-weight: 700;
font-style: italic;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.quote.purple {
color: #4d2e91;
background: var(--purple-soft);
border-left-color: var(--purple);
}
.tree {
position: relative;
padding-left: 24px;
}
.tree::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 6px;
width: 2px;
background: #e4e7ec;
}
.tree-node {
position: relative;
margin: 0 0 16px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.tree-node::before {
content: "";
position: absolute;
top: 24px;
left: -18px;
width: 18px;
height: 2px;
background: #e4e7ec;
}
.tree-node:last-child {
margin-bottom: 0;
}
.node-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-weight: 900;
}
.round {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: #fff;
font-size: 12px;
font-weight: 900;
background: var(--blue);
border-radius: 999px;
flex-shrink: 0;
}
.round.green {
background: var(--green);
}
.round.orange {
background: var(--orange);
}
.round.purple {
background: var(--purple);
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px 0;
}
.chip {
display: inline-block;
padding: 4px 9px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
border: 1px solid;
border-radius: 999px;
background: #fff;
}
.chip.yellow {
color: #8a5a00;
border-color: #e6b64e;
background: var(--yellow-soft);
}
.chip.green {
color: #087443;
border-color: #8bd3aa;
background: var(--green-soft);
}
.chip.blue {
color: #0b4fa6;
border-color: #9ec3ff;
background: var(--blue-soft);
}
.chip.purple {
color: #4d2e91;
border-color: #c6b6ff;
background: var(--purple-soft);
}
.chip.red {
color: #a33a34;
border-color: #e4a39f;
background: var(--red-soft);
}
.work-box {
margin-top: 10px;
padding: 12px 14px;
border-radius: 6px;
font-size: 12px;
}
.work-box.blue {
background: var(--blue-soft);
border-left: 4px solid var(--blue);
}
.work-box.green {
background: var(--green-soft);
border-left: 4px solid var(--green);
}
.work-box.yellow {
background: var(--yellow-soft);
border-left: 4px solid var(--yellow);
}
.work-box.red {
background: var(--red-soft);
border-left: 4px solid var(--red);
}
.work-box ul {
margin: 4px 0 0;
padding-left: 18px;
}
.case2-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.stage-card {
padding: 16px;
border: 2px solid;
border-radius: 8px;
background: #fff;
}
.stage-card.purple {
border-color: var(--purple);
}
.stage-card.green {
border-color: var(--green);
}
.stage-card.orange {
border-color: var(--orange);
}
.stage-card.red {
border-color: var(--red);
}
.stage-card .stage-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
font-size: 15px;
font-weight: 900;
}
.example {
padding: 10px 12px;
margin-bottom: 12px;
font-size: 12px;
font-style: italic;
border-radius: 4px;
background: #f8f5ff;
color: #4d2e91;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
padding: 12px 14px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
font-size: 12px;
}
.legend strong {
margin-right: 4px;
}
@media (max-width: 900px) {
.container {
width: calc(100% - 28px);
}
.hero {
padding: 28px 24px;
}
.case-grid,
.case2-grid {
grid-template-columns: 1fr;
}
table,
thead,
tbody,
tr,
th,
td {
display: block;
width: 100%;
}
thead {
display: none;
}
tr {
border-bottom: 1px solid var(--line);
}
td {
border-right: 0;
}
}
</style>
</head>
<body>
<header class="hero">
<p class="eyebrow">Frontend AI Guidelines — gameservice-fe-agent</p>
<h1>프론트엔드 작업 단계별 AI 참조 흐름도</h1>
<p>
프론트엔드 개발 요청 시 공통 지침(rules/)을 기반으로 Claude AI에게 업무를 위임할 때
어떤 파일을 언제 참조하는지 정리합니다.
</p>
</header>
<main class="container">
<!-- 작업 사례 -->
<section class="card">
<h2 class="section-title">실제 작업 사례 기준</h2>
<div class="notice">
예시: <strong>게임 카드 컴포넌트에 신규 출시 뱃지 추가 (Vue 3 + Nuxt)</strong>
</div>
<div class="case-grid">
<article class="case-box case1">
<span class="case-label">CASE 1</span>
<h3>자연어 업무 요청</h3>
<p>
AI가 claude-workflow.md에 따라 탐색 → 계획 → 구현 → 검증 순서로
전체 흐름을 주도하며 진행합니다. 모호한 부분은 질문합니다.
</p>
</article>
<article class="case-box case2">
<span class="case-label">CASE 2</span>
<h3>@rules 직접 지정 · 단계 분리</h3>
<p>
개발자가 각 작업 단계마다 필요한 rules/ 파일을 직접 지정하고
탐색 · 계획 · 구현 · 검증을 나누어 요청합니다.
</p>
</article>
</div>
</section>
<!-- 공통 자동 로드 -->
<section class="card">
<h2 class="section-title">공통 자동 로드 구조 — 모든 대화에서 항상 적용</h2>
<table>
<thead>
<tr>
<th style="width: 120px;">구분</th>
<th style="width: 280px;">파일</th>
<th>주요 내용</th>
<th style="width: 120px;">로드 시점</th>
</tr>
</thead>
<tbody>
<tr>
<td>공통 지침 진입점</td>
<td><span class="path">CLAUDE.md</span></td>
<td>rules/* 전체를 @import. 공통 지침 우선순위 및 skill 사용 안내</td>
<td><span class="load-badge blue">세션 1회</span></td>
</tr>
<tr>
<td>코딩 컨벤션</td>
<td><span class="path">rules/coding-conventions.md</span></td>
<td>포맷팅(스페이스 2칸·싱글 쿼터·세미콜론), 네이밍(camelCase·PascalCase), any 금지, import 순서</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>프레임워크 규칙</td>
<td><span class="path">rules/framework-rules.md</span></td>
<td>Vue 3 &lt;script setup lang="ts"&gt;, Pinia, useFetch, Tailwind 유틸리티 우선, clsx 조건부 클래스</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>커밋 / PR 규칙</td>
<td><span class="path">rules/commit-pr.md</span></td>
<td>Conventional Commits 형식, subject 50자·명령형, Squash merge, CI + 1인 승인 필수</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>작업 방식 지침</td>
<td><span class="path">rules/claude-workflow.md</span></td>
<td>탐색→계획→구현→검증 순서, 최소 변경 원칙, 모호 시 질문, 임의 기능 추가 금지</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
</tbody>
</table>
</section>
<!-- CASE 1 Tree -->
<section class="card">
<h2 class="section-title">CASE 1 자연어 업무 요청 — 자동 참조 흐름 (Tree)</h2>
<div class="quote">"게임 카드 컴포넌트에 신규 출시 뱃지 추가해줘."</div>
<div class="tree">
<article class="tree-node">
<div class="node-head"><span class="round">0</span>자동 로드</div>
<p>모든 대화 시작 시 CLAUDE.md와 rules/* 4개 파일을 자동으로 참조합니다.</p>
<div class="chip-row">
<span class="chip blue">CLAUDE.md</span>
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">1</span>요구사항 분석 — 모호한 부분 질문</div>
<p>claude-workflow.md 원칙에 따라 추측 대신 사용자에게 확인합니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box yellow">
<strong>예상 질문</strong>
<ul>
<li>뱃지는 어떤 조건(출시일 기준 N일 이내 등)일 때 표시하나요?</li>
<li>뱃지 디자인은 기존 디자인 시스템 컴포넌트를 사용하나요?</li>
<li>GameCard 컴포넌트는 여러 페이지에서 공유 중인가요?</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">2</span>탐색 — 관련 파일 파악 · 유사 패턴 확인</div>
<p>GameCard 컴포넌트와 주변 컨벤션을 먼저 읽고 유사 뱃지 구현 패턴을 찾습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>탐색 항목</strong>
<ul>
<li>GameCard.vue 현재 구조 파악</li>
<li>기존 뱃지 컴포넌트(Badge.vue 등) 존재 여부 확인</li>
<li>props 타입 정의 위치 확인 (types/ 또는 도메인 types.ts)</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">3</span>계획 — 할 일 목록 작성 · 승인</div>
<p>변경 파일 목록과 구현 순서를 공유하고, 아키텍처 영향이 있으면 승인을 받습니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>계획 산출물</strong>
<ul>
<li>GameCard.vue — isNew prop 추가, 뱃지 렌더링 조건 분기</li>
<li>types/game.ts — isNew: boolean 필드 추가</li>
<li>기존 뱃지 컴포넌트 재사용 or 인라인 처리 결정</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round green">4</span>구현 — 코드 작성</div>
<p>계획에 따라 Vue 컴포넌트와 타입을 수정합니다. 요청 범위를 벗어나는 리팩토링은 하지 않습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>구현 항목</strong>
<ul>
<li>defineProps&lt;T&gt;()에 isNew?: boolean 추가</li>
<li>Tailwind 유틸리티 클래스로 뱃지 스타일 적용</li>
<li>조건부 클래스에 clsx 사용, 200줄 초과 시 분리 검토</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round orange">5</span>검증 — 린트 · 빌드 · 커밋 / PR 작성</div>
<p>타입체크와 빌드를 확인한 뒤 Conventional Commits 형식으로 커밋과 PR을 작성합니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
<div class="work-box red">
<strong>최종 산출물</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>커밋: <code>feat(game-card): add new-release badge</code></li>
<li>PR 본문: 변경 사항·배경·테스트·체크리스트 작성</li>
</ul>
</div>
</article>
</div>
</section>
<!-- CASE 2 -->
<section class="card">
<h2 class="section-title">CASE 2 @rules 직접 지정 · 단계별 업무 요청 — 직접 참조 흐름 (Tree)</h2>
<div class="quote purple">
개발자가 각 작업 단계마다 필요한 rules/ 파일 또는 skill을 직접 지정해 요청합니다.
</div>
<div class="case2-grid">
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">1</span>탐색 단계</h3>
<div class="example">"GameCard 컴포넌트 구조 파악해줘. @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>컴포넌트 파일 구조 및 props 정리</li>
<li>재사용 가능한 유사 패턴 목록</li>
<li>영향받는 파일 범위 파악</li>
</ul>
</div>
</article>
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">2</span>계획 단계</h3>
<div class="example">"뱃지 추가 구현 계획 잡아줘. @rules/claude-workflow.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>수정 파일 목록 및 변경 순서</li>
<li>아키텍처 영향 범위 정리</li>
<li>사용자 승인 필요 항목 명시</li>
</ul>
</div>
</article>
<article class="stage-card green">
<h3 class="stage-title"><span class="round green">3</span>구현 단계</h3>
<div class="example">"계획대로 구현해줘. @rules/coding-conventions.md @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>Vue 컴포넌트 코드 수정</li>
<li>TypeScript 타입 추가 · 수정</li>
<li>Tailwind 클래스 적용 및 clsx 처리</li>
</ul>
</div>
</article>
<article class="stage-card orange">
<h3 class="stage-title"><span class="round orange">4</span>검증 단계</h3>
<div class="example">"변경 코드 검증하고 커밋·PR 작성해줘. @rules/commit-pr.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
</div>
<div class="work-box red">
<strong>산출 내용</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>Conventional Commits 커밋 메시지 작성</li>
<li>PR 템플릿(변경사항·배경·테스트·체크리스트) 작성</li>
</ul>
</div>
</article>
</div>
<div style="margin-top: 16px; padding: 12px 14px; background: var(--purple-soft); border-left: 4px solid var(--purple); border-radius: 4px; font-size: 13px; color: #4d2e91;">
<strong>Skill 활용 팁</strong> — 반복 업무는 스킬 명령으로 더 빠르게 처리할 수 있습니다.<br>
<span style="font-family: monospace; font-size: 12px;">
/conventional-commit &nbsp;·&nbsp; /verify-component-review &nbsp;·&nbsp; /plan-analyzer &nbsp;·&nbsp; /markup-edm
</span>
</div>
</section>
<!-- 범례 -->
<section class="card">
<h2 class="section-title">범례</h2>
<div class="legend">
<strong>공통 자동</strong>
<span class="chip blue">CLAUDE.md — 세션 1회 자동 로드</span>
<span class="chip yellow">rules/* — 매 대화 턴 자동 로드 (4개 파일)</span>
<span class="chip green">단계별 주요 참조 파일</span>
<span class="chip purple">@rules — 직접 지정 (CASE 2)</span>
<span class="chip red">검증 · 커밋 / PR 단계</span>
</div>
</section>
</main>
</body>
</html>

612
docs/fe-ai-rules.html Normal file
View File

@@ -0,0 +1,612 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 AI 활용 지침 체계</title>
<style>
:root {
--navy: #18172b;
--blue: #0b5ed7;
--blue-dark: #0756c9;
--purple: #46369a;
--green: #15803d;
--orange: #d97706;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dce4f0;
--text: #1f2937;
--muted: #667085;
--soft-blue: #eef5ff;
--soft-yellow: #fff9e8;
--soft-green: #eefaf2;
--soft-purple: #f3f0ff;
--soft-gray: #f8fafc;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
}
.hero {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 44px;
color: #fff;
background: var(--navy);
border-bottom: 3px solid #1677ff;
}
.hero h1 {
margin: 0;
font-size: 25px;
font-weight: 800;
letter-spacing: -0.04em;
}
.hero p {
margin: 0;
color: #9da8ca;
font-size: 12px;
}
.container {
width: min(1050px, calc(100% - 48px));
margin: 32px auto 48px;
}
.card {
margin-bottom: 20px;
padding: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
}
.section-title {
margin: 0 0 18px;
color: #8a9cbc;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.02em;
}
.purpose-title {
margin: 0 0 10px;
color: var(--blue);
font-size: 17px;
font-weight: 800;
line-height: 1.45;
}
.purpose-desc {
margin: 0;
font-size: 14px;
}
/* rules 4개 카드 */
.rules-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.rule-box {
padding: 18px;
border-radius: 6px;
border: 1px solid;
}
.rule-box.blue { background: var(--soft-blue); border-color: #9ec3ff; }
.rule-box.yellow { background: var(--soft-yellow); border-color: #ecc65d; }
.rule-box.green { background: var(--soft-green); border-color: #8fd1a4; }
.rule-box.purple { background: var(--soft-purple); border-color: #c4b5fd; }
.box-title {
margin: 0 0 8px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
font-weight: 800;
}
.blue .box-title { color: #003d91; }
.yellow .box-title { color: #8a5a00; }
.green .box-title { color: #065f2b; }
.purple .box-title { color: #3b2780; }
.box-label {
display: inline-block;
margin-bottom: 10px;
padding: 3px 8px;
font-size: 11px;
font-weight: 800;
border-radius: 4px;
}
.blue .box-label { color: #004da8; background: #bdd7ff; }
.yellow .box-label { color: #8a5a00; background: #ffe49c; }
.green .box-label { color: #065f2b; background: #bce9cd; }
.purple .box-label { color: #3b2780; background: #ddd6fe; }
.box-subject {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
}
.dash-list {
margin: 0;
padding: 0;
list-style: none;
color: #4b5563;
font-size: 12px;
line-height: 1.8;
}
.dash-list li::before {
content: "· ";
}
/* 파일별 상세 내용 */
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-box {
padding: 18px 20px;
background: var(--soft-gray);
border: 1px solid var(--line);
border-radius: 6px;
}
.detail-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 14px;
font-size: 14px;
font-weight: 800;
}
.detail-title code {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
color: var(--blue-dark);
background: var(--soft-blue);
padding: 2px 7px;
border-radius: 4px;
}
.kv-list {
margin: 0;
padding: 0;
list-style: none;
font-size: 13px;
}
.kv-list li {
display: flex;
gap: 8px;
padding: 5px 0;
border-bottom: 1px solid var(--line);
}
.kv-list li:last-child {
border-bottom: 0;
}
.kv-key {
flex-shrink: 0;
width: 90px;
color: #6b7280;
font-size: 12px;
}
.kv-val {
color: var(--text);
}
/* 커밋 타입 뱃지 */
.commit-types {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.ct {
padding: 3px 9px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
font-weight: 800;
border-radius: 4px;
background: #1e293b;
color: #e2e8f0;
}
/* 작업 흐름 */
.flow {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: stretch;
}
.flow-item {
position: relative;
padding: 18px 16px;
background: var(--soft-gray);
border: 1px solid var(--line);
border-radius: 6px;
}
.flow-item:not(:last-child)::after {
content: "→";
position: absolute;
top: 50%;
right: -14px;
color: #98a2b3;
font-size: 18px;
transform: translateY(-50%);
}
.flow-num {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-bottom: 10px;
color: #fff;
font-size: 12px;
font-weight: 800;
border-radius: 50%;
background: var(--blue-dark);
}
.flow-num.green { background: var(--green); }
.flow-num.orange { background: var(--orange); }
.flow-num.purple { background: var(--purple); }
.flow-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 800;
}
.flow-desc {
margin: 0 0 10px;
color: #4b5563;
font-size: 12px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.tag {
display: inline-block;
padding: 2px 7px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
border-radius: 4px;
border: 1px solid;
background: #fff;
}
.tag.blue { color: #064aa2; border-color: #9ec3ff; background: #eef5ff; }
.tag.yellow { color: #8a5a00; border-color: #ecc65d; background: #fff9e8; }
.tag.green { color: #166534; border-color: #8fd1a4; background: #eefaf2; }
.tag.purple { color: #3b2780; border-color: #c4b5fd; background: #f5f3ff; }
/* 금지 규칙 */
.no-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.no-item {
padding: 12px 14px;
background: #fff5f5;
border: 1px solid #fca5a5;
border-radius: 6px;
font-size: 13px;
color: #991b1b;
}
.no-item strong {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #7f1d1d;
}
@media (max-width: 900px) {
.hero {
display: block;
padding: 24px;
}
.hero p {
margin-top: 8px;
}
.container {
width: calc(100% - 28px);
margin-top: 20px;
}
.rules-grid,
.detail-grid,
.flow,
.no-list {
grid-template-columns: 1fr;
}
.flow-item:not(:last-child)::after {
top: auto;
right: 50%;
bottom: -18px;
transform: translateX(50%) rotate(90deg);
}
}
</style>
</head>
<body>
<header class="hero">
<div>
<h1>프론트엔드 AI 활용 지침 체계</h1>
<p>gameservice-fe-agent — rules/ 4개 파일 요약</p>
</div>
</header>
<main class="container">
<!-- 목적 -->
<section class="card">
<h2 class="section-title">문서 목적</h2>
<p class="purpose-title">
Claude AI가 프론트엔드 개발 업무를 지원할 때,<br />
어떤 지침을 언제 자동으로 참조하는지 정의합니다.
</p>
<p class="purpose-desc">
<code>rules/</code> 폴더의 4개 파일은 <code>CLAUDE.md</code>에서 <code>@import</code>로 불러오며,
모든 작업에 자동으로 적용됩니다. Vue 3 / Nuxt / TypeScript / Tailwind CSS 기반 프로젝트에서
일관성 있는 코드 작성과 협업을 지원합니다.
</p>
</section>
<!-- rules 4개 개요 -->
<section class="card">
<h2 class="section-title">rules/ 4개 파일 개요</h2>
<div class="rules-grid">
<article class="rule-box blue">
<h3 class="box-title">coding-conventions.md</h3>
<span class="box-label">코딩 컨벤션</span>
<p class="box-subject">포맷팅 · 네이밍 · 타입</p>
<ul class="dash-list">
<li>스페이스 2칸, 싱글 쿼터, 세미콜론 필수</li>
<li>camelCase / PascalCase / UPPER_SNAKE</li>
<li>any 사용 금지, 함수 반환 타입 명시</li>
<li>import 순서 (외부→절대→상대)</li>
</ul>
</article>
<article class="rule-box yellow">
<h3 class="box-title">framework-rules.md</h3>
<span class="box-label">프레임워크 규칙</span>
<p class="box-subject">Vue 3 / Nuxt / Tailwind</p>
<ul class="dash-list">
<li>&lt;script setup lang="ts"&gt; 기본</li>
<li>Pinia (공유 상태), useFetch (서버 상태)</li>
<li>Tailwind 유틸리티 우선, clsx 조건부 클래스</li>
<li>라이브러리 도입 시 PR에 이유 기록</li>
</ul>
</article>
<article class="rule-box green">
<h3 class="box-title">commit-pr.md</h3>
<span class="box-label">커밋 / PR 규칙</span>
<p class="box-subject">Conventional Commits + PR 템플릿</p>
<ul class="dash-list">
<li>type(scope): subject 형식</li>
<li>subject 50자 이내, 명령형 현재 시제</li>
<li>Squash and merge 기본 전략</li>
<li>CI(Lint/Test/Build) + 1인 이상 승인</li>
</ul>
</article>
<article class="rule-box purple">
<h3 class="box-title">claude-workflow.md</h3>
<span class="box-label">Claude 작업 방식</span>
<p class="box-subject">탐색 → 계획 → 구현 → 검증</p>
<ul class="dash-list">
<li>기존 코드 존중, 최소 변경 원칙</li>
<li>모호한 요구사항은 추측 말고 질문</li>
<li>임의 기능 추가·대량 리팩토링 금지</li>
<li>결론 먼저, 변경 파일 요약</li>
</ul>
</article>
</div>
</section>
<!-- 파일별 상세 -->
<section class="card">
<h2 class="section-title">파일별 핵심 규칙 상세</h2>
<div class="detail-grid">
<div class="detail-box">
<h3 class="detail-title"><code>coding-conventions.md</code> 포맷팅 &amp; 네이밍</h3>
<ul class="kv-list">
<li><span class="kv-key">들여쓰기</span><span class="kv-val">스페이스 2칸 (탭 금지)</span></li>
<li><span class="kv-key">문자열</span><span class="kv-val">싱글 쿼터 <code>'</code>, JSX 속성은 더블 쿼터 <code>"</code></span></li>
<li><span class="kv-key">세미콜론</span><span class="kv-val">항상 작성 (생략 금지)</span></li>
<li><span class="kv-key">최대 길이</span><span class="kv-val">한 줄 100자, 초과 시 줄바꿈</span></li>
<li><span class="kv-key">변수/함수</span><span class="kv-val">camelCase — userProfile, fetchUserData</span></li>
<li><span class="kv-key">상수</span><span class="kv-val">UPPER_SNAKE_CASE — MAX_RETRY_COUNT</span></li>
<li><span class="kv-key">컴포넌트</span><span class="kv-val">PascalCase.vue — UserCard.vue</span></li>
<li><span class="kv-key">Composable</span><span class="kv-val">use 접두사 camelCase — useAuth.ts</span></li>
<li><span class="kv-key">불리언</span><span class="kv-val">is / has / can / should 접두사</span></li>
<li><span class="kv-key">any 사용</span><span class="kv-val">금지 — 불가피 시 주석 + unknown 우선 검토</span></li>
</ul>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>framework-rules.md</code> Vue 3 / Nuxt / Tailwind</h3>
<ul class="kv-list">
<li><span class="kv-key">컴포넌트</span><span class="kv-val">&lt;script setup lang="ts"&gt; 필수, Options API 금지</span></li>
<li><span class="kv-key">컴포넌트 크기</span><span class="kv-val">200줄 초과 시 분리 검토</span></li>
<li><span class="kv-key">Props</span><span class="kv-val">defineProps&lt;T&gt;() 제네릭으로 타입 명시</span></li>
<li><span class="kv-key">Emits</span><span class="kv-val">defineEmits&lt;{ ... }&gt;() 제네릭으로 선언</span></li>
<li><span class="kv-key">ref vs reactive</span><span class="kv-val">원시값 · 단일 객체는 ref 우선</span></li>
<li><span class="kv-key">공유 상태</span><span class="kv-val">Pinia 사용</span></li>
<li><span class="kv-key">서버 상태</span><span class="kv-val">useFetch / useAsyncData (직접 fetch 지양)</span></li>
<li><span class="kv-key">라우팅</span><span class="kv-val">Nuxt 파일 기반 라우팅, [param].vue 동적 라우트</span></li>
<li><span class="kv-key">Tailwind</span><span class="kv-val">유틸리티 우선, 조건부는 clsx / cn</span></li>
<li><span class="kv-key">라이브러리</span><span class="kv-val">도입 시 PR에 이유·번들 영향·대안 기록</span></li>
</ul>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>commit-pr.md</code> Conventional Commits</h3>
<ul class="kv-list">
<li><span class="kv-key">형식</span><span class="kv-val">type(scope): subject</span></li>
<li><span class="kv-key">subject</span><span class="kv-val">50자 이내, 명령형 현재 시제, 마침표 없음</span></li>
<li><span class="kv-key">body</span><span class="kv-val">"왜"를 설명, 72자 줄바꿈</span></li>
<li><span class="kv-key">머지 전략</span><span class="kv-val">Squash and merge 기본</span></li>
<li><span class="kv-key">승인</span><span class="kv-val">최소 1인 이상 + CI 전부 통과</span></li>
<li><span class="kv-key">Draft PR</span><span class="kv-val">중간 피드백 필요 시 Draft 먼저 오픈</span></li>
</ul>
<div class="commit-types">
<span class="ct">feat</span>
<span class="ct">fix</span>
<span class="ct">refactor</span>
<span class="ct">style</span>
<span class="ct">docs</span>
<span class="ct">test</span>
<span class="ct">chore</span>
<span class="ct">perf</span>
<span class="ct">ci</span>
</div>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>claude-workflow.md</code> 작업 원칙</h3>
<ul class="kv-list">
<li><span class="kv-key">작업 순서</span><span class="kv-val">탐색 → 계획 → 구현 → 검증</span></li>
<li><span class="kv-key">기본 원칙</span><span class="kv-val">기존 코드 존중 · 최소 변경 · 근거 있는 수정</span></li>
<li><span class="kv-key">모호한 요구</span><span class="kv-val">추측 금지 → 사용자에게 확인</span></li>
<li><span class="kv-key">커뮤니케이션</span><span class="kv-val">결론 먼저, 변경 파일 요약</span></li>
<li><span class="kv-key">검증</span><span class="kv-val">린트 / 타입체크 / 빌드 통과 확인</span></li>
</ul>
</div>
</div>
</section>
<!-- 금지 규칙 -->
<section class="card">
<h2 class="section-title">claude-workflow.md — 해서는 안 되는 것</h2>
<div class="no-list">
<div class="no-item">
<strong>임의 기능 추가 금지</strong>
사용자가 요청하지 않은 기능을 추가하지 않습니다.
</div>
<div class="no-item">
<strong>대량 리팩토링 금지</strong>
요청 범위를 벗어나는 코드 변경은 하지 않습니다.
</div>
<div class="no-item">
<strong>주석·문서 임의 삭제 금지</strong>
불필요해 보여도 삭제 전 사용자에게 확인합니다.
</div>
<div class="no-item">
<strong>비밀정보 하드코딩 금지</strong>
환경변수, 키, 토큰을 코드에 직접 작성하지 않습니다.
</div>
<div class="no-item">
<strong>의존성 버전 임의 변경 금지</strong>
요청 없이 package.json 버전을 수정하지 않습니다.
</div>
<div class="no-item">
<strong>강제 푸시·히스토리 재작성 금지</strong>
push --force, reset --hard는 명시적 요청 없이 실행하지 않습니다.
</div>
</div>
</section>
<!-- 작업 흐름 -->
<section class="card">
<h2 class="section-title">Claude 작업 단계별 참조 흐름</h2>
<div class="flow">
<article class="flow-item">
<div class="flow-num">1</div>
<h3 class="flow-title">탐색 (Explore)</h3>
<p class="flow-desc">관련 파일 파악, 유사 패턴 확인</p>
<div class="tag-list">
<span class="tag blue">CLAUDE.md</span>
<span class="tag yellow">coding-conventions</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num purple">2</div>
<h3 class="flow-title">계획 (Plan)</h3>
<p class="flow-desc">할 일 목록 공유, 아키텍처 변경 시 승인</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag yellow">framework-rules</span>
<span class="tag purple">claude-workflow</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num orange">3</div>
<h3 class="flow-title">구현 (Implement)</h3>
<p class="flow-desc">코드 작성, 공통·프로젝트 지침 준수</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag yellow">framework-rules</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num green">4</div>
<h3 class="flow-title">검증 (Verify)</h3>
<p class="flow-desc">린트·타입체크·빌드, 커밋·PR 작성</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag green">commit-pr</span>
<span class="tag purple">claude-workflow</span>
</div>
</article>
</div>
</section>
</main>
</body>
</html>

800
docs/fe-ai-workflow.html Normal file
View File

@@ -0,0 +1,800 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 AI 공통 지침 - Work Flow</title>
<style>
:root {
--bg: #f6f5f1;
--panel: #ffffff;
--line: #d7d7d7;
--text: #222;
--muted: #777;
--navy: #25384a;
--blue: #1f7fbe;
--blue-bg: #e8f5fb;
--yellow: #e6a400;
--yellow-bg: #fff9dc;
--green: #17a968;
--green-bg: #e6f7ee;
--red: #d94132;
--red-bg: #fde9e9;
--orange: #e86f16;
--purple: #8d42b2;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
"Apple SD Gothic Neo",
sans-serif;
line-height: 1.45;
}
.page {
min-width: 1480px;
padding: 8px 10px 18px;
}
.layout {
display: grid;
grid-template-columns: 255px 1fr;
gap: 22px;
align-items: stretch;
}
/* Sidebar */
.sidebar {
overflow: hidden;
background: #fff;
border: 1px solid #d0d0d0;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.sidebar-head {
padding: 14px 20px;
color: #fff;
font-size: 15px;
font-weight: 800;
background: var(--navy);
}
.side-body {
padding: 14px 14px 18px;
}
.side-group {
margin-bottom: 14px;
padding: 10px;
border-radius: 7px;
border: 2px solid;
}
.side-group.yellow {
border-color: #f2ad23;
background: #fffdf4;
}
.side-group.blue {
border-color: #1f7fbe;
background: #f0f8ff;
}
.side-title,
.side-file {
display: block;
margin-bottom: 7px;
padding: 4px 8px;
text-align: center;
border-radius: 3px;
font-size: 11px;
font-weight: 800;
font-family: "SFMono-Regular", Consolas, monospace;
}
.yellow .side-title,
.yellow .side-file {
color: #7b5a00;
border: 1px solid #f2ad23;
background: #fff3bf;
}
.blue .side-title,
.blue .side-file {
color: #0f4d77;
border: 1px solid #1f7fbe;
background: #d6ecf8;
}
.side-caption {
margin: 8px 0 0;
color: #8a8a8a;
text-align: center;
font-size: 11px;
font-style: italic;
}
.side-desc {
margin: 6px 0 0;
color: #666;
font-size: 11px;
line-height: 1.5;
padding: 0 2px;
}
/* Main */
.main {
padding-top: 0;
}
.title-wrap {
padding: 10px 0 14px;
text-align: center;
}
h1 {
margin: 0;
font-size: 24px;
font-weight: 900;
letter-spacing: -0.03em;
}
.subtitle {
margin: 10px 0 0;
color: #767676;
font-size: 12px;
}
.workflow-panel {
position: relative;
min-height: 780px;
padding: 236px 42px 36px;
background: var(--panel);
border: 1px solid #cfcfcf;
border-radius: 8px;
}
.flow-row {
display: grid;
grid-template-columns: 280px 60px 280px 60px 280px 60px 280px 60px 280px 60px 280px;
align-items: center;
}
.arrow {
color: #444;
font-size: 48px;
line-height: 1;
text-align: center;
}
.node {
height: 135px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 4px solid;
border-radius: 16px;
text-align: center;
background: #fff;
}
.node .title {
margin-bottom: 8px;
font-size: 24px;
font-weight: 900;
letter-spacing: -0.04em;
}
.node .desc {
color: #555;
font-size: 17px;
}
.node.blue {
color: #0f4d77;
border-color: var(--blue);
background: var(--blue-bg);
}
.node.yellow {
color: #8a6500;
border-color: var(--yellow);
background: var(--yellow-bg);
}
.node.green {
color: #09643d;
border-color: var(--green);
background: var(--green-bg);
}
.node.red {
color: #9f2820;
border-color: var(--red);
background: var(--red-bg);
}
.node-num {
display: inline-flex;
justify-content: center;
align-items: center;
width: 23px;
height: 23px;
margin-right: 6px;
border: 2px solid currentColor;
border-radius: 50%;
font-size: 14px;
font-weight: 900;
}
/* absolute guide lines */
.rules-bar {
position: absolute;
left: 518px;
right: 43px;
top: 392px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #9a6500;
font-size: 17px;
font-weight: 900;
background: #fffef7;
border: 2px dashed #f0a000;
border-radius: 10px;
}
.v-line {
position: absolute;
width: 0;
border-left: 2px dashed #b7b7b7;
}
.v-line.yellow {
border-color: #d49a00;
}
.v-line.green {
border-color: #15975f;
}
.v-line.red {
border-color: #c0392b;
}
.v1 {
left: 645px;
top: 360px;
height: 33px;
}
.v2 {
left: 989px;
top: 355px;
height: 77px;
}
.v3 {
left: 1344px;
top: 356px;
height: 36px;
}
.v4 {
left: 1696px;
top: 356px;
height: 36px;
}
.v5 {
left: 2041px;
top: 356px;
height: 36px;
}
.doc-row {
position: absolute;
left: 520px;
right: 42px;
top: 448px;
display: grid;
grid-template-columns: 280px 60px 280px 60px 280px 60px 280px 60px 280px;
align-items: start;
}
.doc-col {
display: flex;
flex-direction: column;
gap: 10px;
}
.doc-chip {
min-height: 36px;
padding: 6px 12px;
text-align: center;
border: 2px solid;
border-radius: 8px;
font-size: 15px;
font-family: "SFMono-Regular", Consolas, monospace;
background: #fff;
}
.doc-chip.yellow {
color: #8a6500;
border-color: var(--yellow);
background: var(--yellow-bg);
font-family: inherit;
font-weight: 800;
font-size: 14px;
}
.doc-chip.green {
color: #09643d;
border-color: var(--green);
background: var(--green-bg);
}
.doc-chip.red {
color: #9f2820;
border-color: var(--red);
background: var(--red-bg);
}
.doc-chip.blue {
color: #0f4d77;
border-color: var(--blue);
background: var(--blue-bg);
}
/* feedback loop */
.loop-label {
position: absolute;
z-index: 3;
padding: 7px 22px;
font-size: 20px;
font-weight: 900;
border: 2px solid;
border-radius: 10px;
background: #fff;
}
.label-purple {
top: 30px;
left: 1180px;
color: var(--purple);
border-color: var(--purple);
background: #fff5ff;
}
.label-orange {
top: 109px;
left: 990px;
color: var(--orange);
border-color: var(--orange);
background: #fff8f2;
}
.label-red {
top: 86px;
left: 1383px;
color: var(--red);
border-color: var(--red);
background: #fff5f5;
}
.loop-line {
position: absolute;
pointer-events: none;
}
.loop-purple {
top: 54px;
left: 944px;
width: 705px;
height: 150px;
border: 4px dashed var(--purple);
border-bottom: 0;
border-radius: 18px 18px 0 0;
}
.loop-purple:before,
.loop-purple:after {
content: "";
position: absolute;
bottom: -18px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid var(--purple);
}
.loop-purple:before {
left: -16px;
}
.loop-purple:after {
right: -16px;
}
.loop-orange {
top: 137px;
left: 944px;
width: 350px;
height: 72px;
border-top: 4px dashed var(--orange);
border-left: 4px dashed var(--orange);
border-right: 4px dashed var(--orange);
border-radius: 12px 12px 0 0;
}
.loop-orange:before,
.loop-orange:after {
content: "";
position: absolute;
bottom: -18px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid var(--orange);
}
.loop-orange:before {
left: -16px;
}
.loop-orange:after {
right: -16px;
}
.loop-red {
top: 137px;
left: 1292px;
width: 355px;
height: 72px;
border-top: 4px dashed #ff4b3d;
border-left: 4px dashed #ff4b3d;
border-right: 4px dashed #ff4b3d;
border-radius: 12px 12px 0 0;
}
.loop-red:before {
content: "";
position: absolute;
bottom: -18px;
left: -16px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid #ff4b3d;
}
.feedback-box {
position: absolute;
left: 42px;
right: 42px;
bottom: 28px;
padding: 16px 36px 28px;
border: 2px solid #d1d1d1;
border-radius: 14px;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.feedback-box h2 {
margin: 0 0 22px;
text-align: center;
font-size: 23px;
font-weight: 900;
}
.legend-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
}
.legend-item {
display: flex;
align-items: center;
gap: 14px;
font-size: 20px;
}
.line-sample {
position: relative;
flex-shrink: 0;
width: 85px;
border-top: 4px dashed;
}
.line-sample:after {
content: "";
position: absolute;
right: -1px;
top: -10px;
width: 0;
height: 0;
border-left: 18px solid currentColor;
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
}
.line-sample.red {
color: #ff4b3d;
border-color: #ff4b3d;
}
.line-sample.orange {
color: var(--orange);
border-color: var(--orange);
}
.line-sample.purple {
color: var(--purple);
border-color: var(--purple);
}
.bottom-legend {
margin-top: 14px;
padding: 12px 18px;
background: #fff;
border: 1px solid #d4d4d4;
border-radius: 7px;
font-size: 12px;
}
.bottom-legend strong {
margin-right: 12px;
}
.mini-box {
display: inline-block;
width: 25px;
height: 12px;
margin: 0 5px 0 12px;
border: 2px solid;
border-radius: 2px;
vertical-align: -2px;
}
.mini-box.yellow {
border-color: var(--yellow);
background: var(--yellow-bg);
}
.mini-box.green {
border-color: var(--green);
background: var(--green-bg);
}
.mini-box.red {
border-color: var(--red);
background: var(--red-bg);
}
.mini-box.blue {
border-color: var(--blue);
background: var(--blue-bg);
}
.mini-arrow {
display: inline-block;
margin: 0 10px 0 20px;
font-size: 22px;
line-height: 0;
vertical-align: -3px;
}
.mini-arrow.red {
color: #ff4b3d;
}
.note {
margin-top: 14px;
padding: 11px 18px;
color: #777;
text-align: center;
background: #f1f1ee;
border: 1px solid #d6d6d2;
border-radius: 7px;
font-size: 12px;
}
@media (max-width: 1600px) {
.page {
transform-origin: top left;
}
}
</style>
</head>
<body>
<div class="page">
<div class="layout">
<aside class="sidebar">
<div class="sidebar-head">📁 gameservice-fe-agent 구조</div>
<div class="side-body">
<div class="side-group yellow">
<span class="side-file">CLAUDE.md</span>
<p class="side-caption">세션 시작 시 1회 자동 로드<br />rules/* 전체를 @import</p>
</div>
<div class="side-group yellow">
<span class="side-title">rules/ (매 대화 턴 자동)</span>
<span class="side-file">coding-conventions.md</span>
<span class="side-file">framework-rules.md</span>
<span class="side-file">commit-pr.md</span>
<span class="side-file">claude-workflow.md</span>
<p class="side-caption">포맷팅·네이밍·Vue·커밋·작업방식</p>
</div>
<div class="side-group blue">
<span class="side-title">skills/ (명령으로 호출)</span>
<span class="side-file">conventional-commit</span>
<span class="side-file">markup-edm</span>
<span class="side-file">plan-analyzer</span>
<span class="side-file">plan-translation-generator</span>
<span class="side-file">project-init</span>
<span class="side-file">verify-component-review</span>
<span class="side-file">work-log</span>
<p class="side-caption">link-skills.sh 로 .claude/skills/ 링크</p>
</div>
</div>
</aside>
<main class="main">
<header class="title-wrap">
<h1>프론트엔드 AI 공통 지침 - Work Flow</h1>
<p class="subtitle">gameservice-fe-agent rules/ 구조와 각 작업 단계별 참조 파일의 흐름 (피드백 루프 포함)</p>
</header>
<section class="workflow-panel">
<div class="loop-line loop-purple"></div>
<div class="loop-line loop-orange"></div>
<div class="loop-line loop-red"></div>
<div class="loop-label label-purple">설계 변경 필요</div>
<div class="loop-label label-orange">계획 재수립 필요</div>
<div class="loop-label label-red">버그 발견 · 수정</div>
<div class="flow-row">
<article class="node blue">
<div class="title">요청자 / 개발자</div>
<div class="desc">업무 요청</div>
</article>
<div class="arrow"></div>
<article class="node yellow">
<div class="title"><span class="node-num">1</span>자동 로드</div>
<div class="desc">CLAUDE.md + rules/* × 4</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">2</span>탐색</div>
<div class="desc">파일 파악 · 패턴 확인</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">3</span>계획</div>
<div class="desc">할 일 목록 · 승인</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">4</span>구현</div>
<div class="desc">코드 작성 · 지침 준수</div>
</article>
<div class="arrow"></div>
<article class="node red">
<div class="title"><span class="node-num">5</span>검증</div>
<div class="desc">린트 · 빌드 · 커밋/PR</div>
</article>
</div>
<div class="v-line yellow v1"></div>
<div class="v-line green v2"></div>
<div class="v-line green v3"></div>
<div class="v-line green v4"></div>
<div class="v-line red v5"></div>
<div class="rules-bar">rules/* × 4 — 매 대화 턴 자동 참조 (① ~ ⑤ 전 단계 공통)</div>
<div class="doc-row">
<div class="doc-col">
<div class="doc-chip yellow">CLAUDE.md (세션 시작 1회)</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">coding-conventions.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">claude-workflow.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">coding-conventions.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip red">coding-conventions.md</div>
<div class="doc-chip red">commit-pr.md</div>
</div>
</div>
<div class="feedback-box">
<h2>피드백 루프 (역방향 흐름)</h2>
<div class="legend-grid">
<div class="legend-item">
<span class="line-sample red"></span>
<span>④ 구현 → ⑤ 검증 → ④ 구현 : 버그 발견 시 수정 후 재검증</span>
</div>
<div class="legend-item">
<span class="line-sample orange"></span>
<span>③ 계획 → ④ 구현 중 재설계 : 계획 재수립 후 다시 구현</span>
</div>
<div class="legend-item">
<span class="line-sample purple"></span>
<span>⑤ 검증 → ② 탐색 : 검증 결과로 구조 변경이 필요한 경우</span>
</div>
</div>
</div>
</section>
<div class="bottom-legend">
<strong>범례</strong>
<span class="mini-box yellow"></span> 세션 시작 1회 자동 로드 (CLAUDE.md)
<span class="mini-box yellow"></span> 매 대화 턴 자동 참조 (rules/ × 4)
<span class="mini-box green"></span> 탐색·계획·구현 단계 참조
<span class="mini-box red"></span> 검증·커밋/PR 단계 참조
<span class="mini-box blue"></span> skills/ (명령으로 호출)
<span class="mini-arrow"></span> 정방향 진행
<span class="mini-arrow red">--▶</span> 피드백 루프 (역방향)
</div>
<div class="note">
피드백 루프는 구현·검증 중 발견된 이슈에 따라 이전 단계로 돌아가 재작업 후 다시 진행합니다.
모호한 요구사항은 추측하지 않고 사용자에게 확인 후 계획 단계부터 다시 시작합니다.
</div>
</main>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,662 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>gameservice-fe-agent 폴더 구조</title>
<style>
:root {
--navy: #17172b;
--blue: #0b63ce;
--blue-light: #eef4ff;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dde4ef;
--text: #1f2937;
--muted: #6b7280;
--soft: #f8fafc;
--yellow: #fff8e6;
--yellow-line: #f0bf4c;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.6;
}
.page {
min-height: 100vh;
}
.hero {
padding: 34px 56px 36px;
color: #fff;
background: var(--navy);
border-bottom: 4px solid #1677ff;
}
.hero h1 {
margin: 0 0 6px;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
}
.hero p {
margin: 0;
font-size: 13px;
color: #a9b1c7;
}
.content {
width: min(1160px, calc(100% - 80px));
margin: 34px auto 60px;
}
.card {
padding: 28px;
margin-bottom: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
}
.section-title {
margin: 0 0 18px;
font-size: 13px;
font-weight: 800;
color: #8aa0c4;
letter-spacing: -0.01em;
}
.intro p {
margin: 0 0 24px;
font-size: 17px;
font-weight: 500;
}
.intro p:last-child {
margin-bottom: 0;
}
code {
padding: 2px 6px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.9em;
color: #d24c4c;
background: #fff2f2;
border-radius: 4px;
}
strong,
.accent {
color: var(--blue);
font-weight: 800;
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--line);
table-layout: fixed;
}
th {
padding: 16px;
font-size: 14px;
text-align: left;
background: #edf3ff;
border-bottom: 1px solid var(--line);
}
td {
padding: 18px 16px;
font-size: 14px;
vertical-align: top;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
th:nth-child(1),
td:nth-child(1) {
width: 130px;
}
th:nth-child(3),
td:nth-child(3) {
width: 280px;
}
td:last-child,
th:last-child {
border-right: 0;
}
.folder-name {
font-family: "SFMono-Regular", Consolas, monospace;
font-weight: 800;
color: var(--blue);
}
.desc-title {
margin-bottom: 12px;
font-weight: 800;
}
.note {
padding: 14px 18px;
color: #445066;
background: var(--blue-light);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.config-list {
margin: 0;
padding-left: 0;
list-style: none;
color: #5f6878;
font-size: 13px;
line-height: 1.8;
}
.config-list li::before {
content: "· ";
}
.count {
display: inline-block;
margin-top: 4px;
color: var(--blue);
font-weight: 800;
}
.tree {
padding: 28px;
overflow-x: auto;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 13px;
line-height: 1.9;
color: #7a8497;
background: var(--soft);
border: 1px solid var(--line);
border-radius: 8px;
white-space: pre;
}
.tree .folder {
color: var(--blue);
font-weight: 800;
}
.tree .file {
color: #4b5563;
}
.tree .comment {
color: #9ca3af;
}
.flow-desc {
margin: 0 0 18px;
font-size: 15px;
}
.flow-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.flow-item {
position: relative;
min-height: 126px;
padding: 28px 16px 18px;
text-align: center;
background: var(--soft);
border: 1px solid var(--line);
border-radius: 8px;
}
.flow-num {
position: absolute;
top: -12px;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: #fff;
font-size: 13px;
font-weight: 800;
background: var(--blue);
border-radius: 50%;
transform: translateX(-50%);
}
.flow-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 800;
}
.flow-main {
margin: 0;
color: #5f6878;
font-size: 12px;
font-weight: 700;
}
.flow-sub {
margin: 0;
color: #9aa3b2;
font-size: 12px;
}
.summary-box {
padding: 16px 20px;
margin-bottom: 14px;
color: #a26300;
font-size: 14px;
font-weight: 700;
background: var(--yellow);
border: 1px solid var(--yellow-line);
border-radius: 6px;
}
.summary-box:last-child {
margin-bottom: 0;
color: #43516a;
background: var(--blue-light);
border-color: transparent;
border-left: 4px solid var(--blue);
}
.skill-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.skill-card {
padding: 16px 18px;
background: var(--soft);
border: 1px solid var(--line);
border-radius: 8px;
}
.skill-card .skill-name {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 13px;
font-weight: 800;
color: var(--blue);
margin-bottom: 6px;
}
.skill-card .skill-desc {
font-size: 12px;
color: #6b7280;
line-height: 1.6;
}
@media (max-width: 900px) {
.hero {
padding: 28px 24px;
}
.content {
width: calc(100% - 32px);
margin-top: 24px;
}
.card {
padding: 20px;
}
table,
thead,
tbody,
tr,
th,
td {
display: block;
width: 100% !important;
}
thead {
display: none;
}
tr {
border-bottom: 1px solid var(--line);
}
td {
border-right: 0;
}
td::before {
display: block;
margin-bottom: 6px;
color: #8aa0c4;
font-size: 12px;
font-weight: 800;
}
td:nth-child(1)::before {
content: "폴더";
}
td:nth-child(2)::before {
content: "설명";
}
td:nth-child(3)::before {
content: "구성";
}
.flow-list {
grid-template-columns: 1fr 1fr;
gap: 22px;
}
.skill-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="page">
<header class="hero">
<h1>gameservice-fe-agent 폴더 구조</h1>
<p>팀 공통 Claude 지침 저장소 — 4개 폴더 구조 설명</p>
</header>
<main class="content">
<section class="card intro">
<h2 class="section-title">개요</h2>
<p>
<code>gameservice-fe-agent</code>는 팀 전체에서 공통으로 사용하는
<strong>프론트엔드 Claude 지침 저장소</strong>입니다.
</p>
<p>
각 프로젝트는 이 저장소를 <strong>Git submodule</strong>로 포함하고,
<code>CLAUDE.md</code><code>@import</code> 구문으로 공통 지침을 불러와 사용합니다.
</p>
</section>
<section class="card">
<h2 class="section-title">폴더 구조 및 역할</h2>
<table>
<thead>
<tr>
<th>폴더 / 파일</th>
<th>설명</th>
<th>구성</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="folder-name">rules/</span></td>
<td>
<div class="desc-title">매 대화마다 자동 로드되는 공통 규칙 모음</div>
<div class="note">
<code>CLAUDE.md</code>에서 <code>@import</code>로 불러오며,
<span class="accent">모든 작업에 항상 자동으로 적용</span>됩니다.
</div>
</td>
<td>
<ul class="config-list">
<li>coding-conventions.md — 포맷팅·네이밍·타입</li>
<li>framework-rules.md — Vue/Nuxt/Tailwind 규칙</li>
<li>commit-pr.md — 커밋 &amp; PR 규칙</li>
<li>claude-workflow.md — Claude 작업 방식 지침</li>
</ul>
<span class="count">4개 규칙 파일</span>
</td>
</tr>
<tr>
<td><span class="folder-name">skills/</span></td>
<td>
<div class="desc-title">Claude Code에서 호출할 수 있는 팀 공용 스킬 모음</div>
<div class="note">
<code>link-skills.sh</code>로 프로젝트의 <code>.claude/skills/</code>
<span class="accent">심볼릭 링크</span>하여 사용합니다.
<code>submodule update</code> 한 번으로 모든 프로젝트가 최신 스킬을 받습니다.
</div>
</td>
<td>
<ul class="config-list">
<li>conventional-commit</li>
<li>markup-edm</li>
<li>plan-analyzer</li>
<li>plan-translation-generator</li>
<li>project-init</li>
<li>verify-component-review</li>
<li>work-log</li>
</ul>
<span class="count">7개 스킬</span>
</td>
</tr>
<tr>
<td><span class="folder-name">templates/</span></td>
<td>
<div class="desc-title">프로젝트 지침 초기화용 양식 파일 모음</div>
<div class="note">
<code>install.sh</code> 실행 시 <code>.claude/project/</code>로 자동 복사됩니다.
팀원은 복사된 양식에 <span class="accent">프로젝트 고유 내용을 작성</span>합니다.
</div>
</td>
<td>
<ul class="config-list">
<li>CLAUDE.md.tpl — 루트 CLAUDE.md 템플릿</li>
<li>project/overview.md — 프로젝트 개요 양식</li>
<li>project/conventions.md — 프로젝트 컨벤션 양식</li>
<li>project/architecture.md — 아키텍처 양식</li>
</ul>
<span class="count">4개 템플릿</span>
</td>
</tr>
<tr>
<td><span class="folder-name">scripts/</span></td>
<td>
<div class="desc-title">프로젝트 설정 및 유지보수 자동화 스크립트</div>
<div class="note">
신규 프로젝트 도입부터 업데이트까지
<span class="accent">반복 작업을 한 명령으로</span> 처리합니다.
</div>
</td>
<td>
<ul class="config-list">
<li>install.sh — submodule + 템플릿 복사 + skill 링크</li>
<li>init-project.sh — project/ 양식만 재초기화</li>
<li>link-skills.sh — 공통 skill 심볼릭 링크 관리</li>
<li>update.sh — 최신 공통 지침으로 업데이트</li>
</ul>
<span class="count">4개 스크립트</span>
</td>
</tr>
<tr>
<td>
<span class="folder-name">CLAUDE.md</span><br />
<span class="folder-name">README.md</span>
</td>
<td>
<div class="desc-title">저장소 진입점 문서</div>
<div class="note">
<code>CLAUDE.md</code><code>rules/*.md</code><code>@import</code>하는
<span class="accent">공통 지침 엔트리 포인트</span>입니다.
<code>README.md</code>는 팀원을 위한 사용 가이드입니다.
</div>
</td>
<td>
<ul class="config-list">
<li>CLAUDE.md — @import rules/* 엔트리</li>
<li>README.md — 도입·사용·업데이트 가이드</li>
</ul>
</td>
</tr>
</tbody>
</table>
</section>
<section class="card">
<h2 class="section-title">전체 파일 트리</h2>
<div class="tree"><span class="folder">gameservice-fe-agent/</span>
├── <span class="file">CLAUDE.md</span> <span class="comment">← 공통 지침 엔트리 포인트 (rules/* @import)</span>
├── <span class="file">README.md</span> <span class="comment">← 도입·사용·업데이트 가이드</span>
├── <span class="folder">rules/</span> <span class="comment">← 항상 자동 로드되는 공통 규칙</span>
│ ├── <span class="file">coding-conventions.md</span> <span class="comment">← 포맷팅, 네이밍, 타입</span>
│ ├── <span class="file">framework-rules.md</span> <span class="comment">← Vue 3 / Nuxt / Tailwind 규칙</span>
│ ├── <span class="file">commit-pr.md</span> <span class="comment">← Conventional Commits, PR 가이드</span>
│ └── <span class="file">claude-workflow.md</span> <span class="comment">← Claude 작업 순서 및 행동 지침</span>
├── <span class="folder">skills/</span> <span class="comment">← 팀 공용 Claude skill 모음</span>
│ ├── <span class="file">README.md</span>
│ ├── <span class="folder">conventional-commit/</span>
│ │ └── <span class="file">SKILL.md</span>
│ ├── <span class="folder">markup-edm/</span>
│ │ ├── <span class="file">SKILL.md</span>
│ │ ├── <span class="folder">assets/</span>
│ │ ├── <span class="folder">references/</span> <span class="comment">← api_reference.md, html-patterns.md</span>
│ │ └── <span class="folder">scripts/</span>
│ ├── <span class="folder">plan-analyzer/</span>
│ │ ├── <span class="file">SKILL.md</span>
│ │ ├── <span class="folder">references/</span> <span class="comment">← output-template.md</span>
│ │ └── <span class="folder">scripts/</span> <span class="comment">← extract_pptx.py</span>
│ ├── <span class="folder">plan-translation-generator/</span>
│ │ ├── <span class="file">SKILL.md</span>
│ │ └── <span class="folder">scripts/</span> <span class="comment">← generate_translation_keys.py</span>
│ ├── <span class="folder">project-init/</span>
│ │ └── <span class="file">SKILL.md</span>
│ ├── <span class="folder">verify-component-review/</span>
│ │ └── <span class="file">SKILL.md</span>
│ └── <span class="folder">work-log/</span>
│ └── <span class="file">SKILL.md</span>
├── <span class="folder">templates/</span> <span class="comment">← 프로젝트 지침 초기화 양식</span>
│ ├── <span class="file">CLAUDE.md.tpl</span> <span class="comment">← 루트 CLAUDE.md 템플릿</span>
│ └── <span class="folder">project/</span>
│ ├── <span class="file">overview.md</span>
│ ├── <span class="file">conventions.md</span>
│ └── <span class="file">architecture.md</span>
└── <span class="folder">scripts/</span> <span class="comment">← 자동화 스크립트</span>
├── <span class="file">install.sh</span> <span class="comment">← 신규 프로젝트 도입 (all-in-one)</span>
├── <span class="file">init-project.sh</span> <span class="comment">← project/ 양식만 재초기화</span>
├── <span class="file">link-skills.sh</span> <span class="comment">← skill 심볼릭 링크 관리</span>
└── <span class="file">update.sh</span> <span class="comment">← 최신 공통 지침 동기화</span></div>
</section>
<section class="card">
<h2 class="section-title">Skills 목록</h2>
<div class="skill-grid">
<div class="skill-card">
<div class="skill-name">conventional-commit</div>
<div class="skill-desc">Conventional Commits 규칙에 따른 커밋 메시지 작성 지원</div>
</div>
<div class="skill-card">
<div class="skill-name">markup-edm</div>
<div class="skill-desc">EDM(이메일 마케팅) HTML 마크업 생성 및 패턴 적용</div>
</div>
<div class="skill-card">
<div class="skill-name">plan-analyzer</div>
<div class="skill-desc">기획서(PPT 등)를 분석하여 구조화된 개발 요구사항 도출</div>
</div>
<div class="skill-card">
<div class="skill-name">plan-translation-generator</div>
<div class="skill-desc">기획서 기반 다국어 번역 키 자동 생성</div>
</div>
<div class="skill-card">
<div class="skill-name">project-init</div>
<div class="skill-desc">신규 프로젝트 공통 지침 초기화 가이드</div>
</div>
<div class="skill-card">
<div class="skill-name">verify-component-review</div>
<div class="skill-desc">Vue 컴포넌트 코드 리뷰 및 규칙 준수 검증</div>
</div>
<div class="skill-card">
<div class="skill-name">work-log</div>
<div class="skill-desc">작업 내역 로그 기록 및 정리</div>
</div>
</div>
</section>
<section class="card">
<h2 class="section-title">프로젝트 도입 흐름</h2>
<p class="flow-desc">
신규 프로젝트에서 <code>install.sh</code> 한 번으로 아래 4단계가 자동 처리됩니다:
</p>
<div class="flow-list">
<div class="flow-item">
<span class="flow-num">1</span>
<p class="flow-title">Submodule 추가</p>
<p class="flow-main">install.sh</p>
<p class="flow-sub">.claude/common/ 에 설치</p>
</div>
<div class="flow-item">
<span class="flow-num">2</span>
<p class="flow-title">템플릿 복사</p>
<p class="flow-main">templates/ → .claude/project/</p>
<p class="flow-sub">프로젝트 지침 양식 생성</p>
</div>
<div class="flow-item">
<span class="flow-num">3</span>
<p class="flow-title">Skill 링크</p>
<p class="flow-main">link-skills.sh</p>
<p class="flow-sub">.claude/skills/ 에 심볼릭 링크</p>
</div>
<div class="flow-item">
<span class="flow-num">4</span>
<p class="flow-title">CLAUDE.md 작성</p>
<p class="flow-main">@import 구문으로 연결</p>
<p class="flow-sub">공통 + 프로젝트 지침 로드</p>
</div>
</div>
</section>
<section class="card">
<h2 class="section-title">핵심 요약</h2>
<div class="summary-box">
<strong>rules/</strong> 의 4개 파일은 모든 Claude 작업에 자동으로 적용되며,
<strong>templates/</strong>로 프로젝트 고유 지침을 초기화하고,
<strong>skills/</strong>로 반복 업무를 스킬 명령으로 자동화합니다.
</div>
<div class="summary-box">
공통 지침 수정은 이 저장소에서만 진행합니다.
각 프로젝트는 <code>git submodule update --remote</code> 또는
<code>scripts/update.sh</code>로 최신 버전을 동기화합니다.
</div>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,794 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 작업 단계별 AI 참조 흐름도</title>
<style>
:root {
--navy: #17172b;
--blue: #0b63ce;
--blue-soft: #eaf4ff;
--purple: #5937b7;
--purple-soft: #f1ebff;
--green: #15975f;
--green-soft: #e8f8ef;
--orange: #d97706;
--orange-soft: #fff4dd;
--yellow: #f2b43f;
--yellow-soft: #fff8e6;
--red: #d94841;
--red-soft: #fff0f0;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dce4ef;
--text: #1f2937;
--muted: #667085;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
line-height: 1.55;
}
.hero {
padding: 34px 44px 36px;
color: #fff;
background: var(--navy);
border-bottom: 4px solid #1677ff;
}
.hero .eyebrow {
margin: 0 0 8px;
color: #8d96b8;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
margin: 0 0 10px;
font-size: 27px;
font-weight: 900;
letter-spacing: -0.04em;
}
.hero p {
margin: 0;
color: #c2c8da;
font-size: 13px;
}
.container {
width: min(1180px, calc(100% - 56px));
margin: 28px auto 48px;
}
.card {
margin-bottom: 22px;
padding: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 18px;
color: #315a9e;
font-size: 15px;
font-weight: 900;
letter-spacing: -0.02em;
}
.section-title::before {
content: "";
width: 4px;
height: 18px;
background: var(--blue);
border-radius: 999px;
}
.notice {
padding: 12px 16px;
margin-bottom: 16px;
color: #17436f;
font-size: 13px;
font-weight: 700;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.case-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.case-box {
min-height: 126px;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.case-box.case1 {
border-color: #a8caff;
background: #f7fbff;
}
.case-box.case2 {
border-color: #c8b5ff;
background: #fbf8ff;
}
.case-label {
display: inline-block;
margin-bottom: 10px;
padding: 4px 11px;
color: #fff;
font-size: 12px;
font-weight: 900;
border-radius: 999px;
}
.case1 .case-label {
background: var(--blue);
}
.case2 .case-label {
background: var(--purple);
}
.case-box h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 900;
}
.case-box p {
margin: 0;
color: #475467;
font-size: 13px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 1px solid var(--line);
}
th,
td {
padding: 12px 14px;
text-align: left;
vertical-align: top;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
th {
color: #344054;
font-weight: 900;
background: #eef4ff;
}
td:last-child,
th:last-child {
border-right: 0;
}
.path {
color: #c0392b;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
white-space: nowrap;
}
.load-badge {
display: inline-block;
padding: 3px 8px;
color: #915f00;
background: var(--yellow-soft);
border: 1px solid #e8bd54;
border-radius: 999px;
font-size: 11px;
font-weight: 900;
white-space: nowrap;
}
.load-badge.blue {
color: #0b4fa6;
background: var(--blue-soft);
border-color: #9ec3ff;
}
.quote {
padding: 12px 16px;
margin-bottom: 18px;
color: #0c55b0;
font-size: 14px;
font-weight: 700;
font-style: italic;
background: var(--blue-soft);
border-left: 4px solid var(--blue);
border-radius: 4px;
}
.quote.purple {
color: #4d2e91;
background: var(--purple-soft);
border-left-color: var(--purple);
}
.tree {
position: relative;
padding-left: 24px;
}
.tree::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 6px;
width: 2px;
background: #e4e7ec;
}
.tree-node {
position: relative;
margin: 0 0 16px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
}
.tree-node::before {
content: "";
position: absolute;
top: 24px;
left: -18px;
width: 18px;
height: 2px;
background: #e4e7ec;
}
.tree-node:last-child {
margin-bottom: 0;
}
.node-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-weight: 900;
}
.round {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: #fff;
font-size: 12px;
font-weight: 900;
background: var(--blue);
border-radius: 999px;
flex-shrink: 0;
}
.round.green {
background: var(--green);
}
.round.orange {
background: var(--orange);
}
.round.purple {
background: var(--purple);
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px 0;
}
.chip {
display: inline-block;
padding: 4px 9px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
border: 1px solid;
border-radius: 999px;
background: #fff;
}
.chip.yellow {
color: #8a5a00;
border-color: #e6b64e;
background: var(--yellow-soft);
}
.chip.green {
color: #087443;
border-color: #8bd3aa;
background: var(--green-soft);
}
.chip.blue {
color: #0b4fa6;
border-color: #9ec3ff;
background: var(--blue-soft);
}
.chip.purple {
color: #4d2e91;
border-color: #c6b6ff;
background: var(--purple-soft);
}
.chip.red {
color: #a33a34;
border-color: #e4a39f;
background: var(--red-soft);
}
.work-box {
margin-top: 10px;
padding: 12px 14px;
border-radius: 6px;
font-size: 12px;
}
.work-box.blue {
background: var(--blue-soft);
border-left: 4px solid var(--blue);
}
.work-box.green {
background: var(--green-soft);
border-left: 4px solid var(--green);
}
.work-box.yellow {
background: var(--yellow-soft);
border-left: 4px solid var(--yellow);
}
.work-box.red {
background: var(--red-soft);
border-left: 4px solid var(--red);
}
.work-box ul {
margin: 4px 0 0;
padding-left: 18px;
}
.case2-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
}
.stage-card {
padding: 16px;
border: 2px solid;
border-radius: 8px;
background: #fff;
}
.stage-card.purple {
border-color: var(--purple);
}
.stage-card.green {
border-color: var(--green);
}
.stage-card.orange {
border-color: var(--orange);
}
.stage-card.red {
border-color: var(--red);
}
.stage-card .stage-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 12px;
font-size: 15px;
font-weight: 900;
}
.example {
padding: 10px 12px;
margin-bottom: 12px;
font-size: 12px;
font-style: italic;
border-radius: 4px;
background: #f8f5ff;
color: #4d2e91;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
padding: 12px 14px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
font-size: 12px;
}
.legend strong {
margin-right: 4px;
}
@media (max-width: 900px) {
.container {
width: calc(100% - 28px);
}
.hero {
padding: 28px 24px;
}
.case-grid,
.case2-grid {
grid-template-columns: 1fr;
}
table,
thead,
tbody,
tr,
th,
td {
display: block;
width: 100%;
}
thead {
display: none;
}
tr {
border-bottom: 1px solid var(--line);
}
td {
border-right: 0;
}
}
</style>
</head>
<body>
<header class="hero">
<p class="eyebrow">Frontend AI Guidelines — gameservice-fe-agent</p>
<h1>프론트엔드 작업 단계별 AI 참조 흐름도</h1>
<p>
프론트엔드 개발 요청 시 공통 지침(rules/)을 기반으로 Claude AI에게 업무를 위임할 때
어떤 파일을 언제 참조하는지 정리합니다.
</p>
</header>
<main class="container">
<!-- 작업 사례 -->
<section class="card">
<h2 class="section-title">실제 작업 사례 기준</h2>
<div class="notice">
예시: <strong>게임 카드 컴포넌트에 신규 출시 뱃지 추가 (Vue 3 + Nuxt)</strong>
</div>
<div class="case-grid">
<article class="case-box case1">
<span class="case-label">CASE 1</span>
<h3>자연어 업무 요청</h3>
<p>
AI가 claude-workflow.md에 따라 탐색 → 계획 → 구현 → 검증 순서로
전체 흐름을 주도하며 진행합니다. 모호한 부분은 질문합니다.
</p>
</article>
<article class="case-box case2">
<span class="case-label">CASE 2</span>
<h3>@rules 직접 지정 · 단계 분리</h3>
<p>
개발자가 각 작업 단계마다 필요한 rules/ 파일을 직접 지정하고
탐색 · 계획 · 구현 · 검증을 나누어 요청합니다.
</p>
</article>
</div>
</section>
<!-- 공통 자동 로드 -->
<section class="card">
<h2 class="section-title">공통 자동 로드 구조 — 모든 대화에서 항상 적용</h2>
<table>
<thead>
<tr>
<th style="width: 120px;">구분</th>
<th style="width: 280px;">파일</th>
<th>주요 내용</th>
<th style="width: 120px;">로드 시점</th>
</tr>
</thead>
<tbody>
<tr>
<td>공통 지침 진입점</td>
<td><span class="path">CLAUDE.md</span></td>
<td>rules/* 전체를 @import. 공통 지침 우선순위 및 skill 사용 안내</td>
<td><span class="load-badge blue">세션 1회</span></td>
</tr>
<tr>
<td>코딩 컨벤션</td>
<td><span class="path">rules/coding-conventions.md</span></td>
<td>포맷팅(스페이스 2칸·싱글 쿼터·세미콜론), 네이밍(camelCase·PascalCase), any 금지, import 순서</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>프레임워크 규칙</td>
<td><span class="path">rules/framework-rules.md</span></td>
<td>Vue 3 &lt;script setup lang="ts"&gt;, Pinia, useFetch, Tailwind 유틸리티 우선, clsx 조건부 클래스</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>커밋 / PR 규칙</td>
<td><span class="path">rules/commit-pr.md</span></td>
<td>Conventional Commits 형식, subject 50자·명령형, Squash merge, CI + 1인 승인 필수</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
<tr>
<td>작업 방식 지침</td>
<td><span class="path">rules/claude-workflow.md</span></td>
<td>탐색→계획→구현→검증 순서, 최소 변경 원칙, 모호 시 질문, 임의 기능 추가 금지</td>
<td><span class="load-badge">매 대화 턴</span></td>
</tr>
</tbody>
</table>
</section>
<!-- CASE 1 Tree -->
<section class="card">
<h2 class="section-title">CASE 1 자연어 업무 요청 — 자동 참조 흐름 (Tree)</h2>
<div class="quote">"게임 카드 컴포넌트에 신규 출시 뱃지 추가해줘."</div>
<div class="tree">
<article class="tree-node">
<div class="node-head"><span class="round">0</span>자동 로드</div>
<p>모든 대화 시작 시 CLAUDE.md와 rules/* 4개 파일을 자동으로 참조합니다.</p>
<div class="chip-row">
<span class="chip blue">CLAUDE.md</span>
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">1</span>요구사항 분석 — 모호한 부분 질문</div>
<p>claude-workflow.md 원칙에 따라 추측 대신 사용자에게 확인합니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box yellow">
<strong>예상 질문</strong>
<ul>
<li>뱃지는 어떤 조건(출시일 기준 N일 이내 등)일 때 표시하나요?</li>
<li>뱃지 디자인은 기존 디자인 시스템 컴포넌트를 사용하나요?</li>
<li>GameCard 컴포넌트는 여러 페이지에서 공유 중인가요?</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">2</span>탐색 — 관련 파일 파악 · 유사 패턴 확인</div>
<p>GameCard 컴포넌트와 주변 컨벤션을 먼저 읽고 유사 뱃지 구현 패턴을 찾습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>탐색 항목</strong>
<ul>
<li>GameCard.vue 현재 구조 파악</li>
<li>기존 뱃지 컴포넌트(Badge.vue 등) 존재 여부 확인</li>
<li>props 타입 정의 위치 확인 (types/ 또는 도메인 types.ts)</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round">3</span>계획 — 할 일 목록 작성 · 승인</div>
<p>변경 파일 목록과 구현 순서를 공유하고, 아키텍처 영향이 있으면 승인을 받습니다.</p>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box blue">
<strong>계획 산출물</strong>
<ul>
<li>GameCard.vue — isNew prop 추가, 뱃지 렌더링 조건 분기</li>
<li>types/game.ts — isNew: boolean 필드 추가</li>
<li>기존 뱃지 컴포넌트 재사용 or 인라인 처리 결정</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round green">4</span>구현 — 코드 작성</div>
<p>계획에 따라 Vue 컴포넌트와 타입을 수정합니다. 요청 범위를 벗어나는 리팩토링은 하지 않습니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>구현 항목</strong>
<ul>
<li>defineProps&lt;T&gt;()에 isNew?: boolean 추가</li>
<li>Tailwind 유틸리티 클래스로 뱃지 스타일 적용</li>
<li>조건부 클래스에 clsx 사용, 200줄 초과 시 분리 검토</li>
</ul>
</div>
</article>
<article class="tree-node">
<div class="node-head"><span class="round orange">5</span>검증 — 린트 · 빌드 · 커밋 / PR 작성</div>
<p>타입체크와 빌드를 확인한 뒤 Conventional Commits 형식으로 커밋과 PR을 작성합니다.</p>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
<span class="chip yellow">claude-workflow.md</span>
</div>
<div class="work-box red">
<strong>최종 산출물</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>커밋: <code>feat(game-card): add new-release badge</code></li>
<li>PR 본문: 변경 사항·배경·테스트·체크리스트 작성</li>
</ul>
</div>
</article>
</div>
</section>
<!-- CASE 2 -->
<section class="card">
<h2 class="section-title">CASE 2 @rules 직접 지정 · 단계별 업무 요청 — 직접 참조 흐름 (Tree)</h2>
<div class="quote purple">
개발자가 각 작업 단계마다 필요한 rules/ 파일 또는 skill을 직접 지정해 요청합니다.
</div>
<div class="case2-grid">
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">1</span>탐색 단계</h3>
<div class="example">"GameCard 컴포넌트 구조 파악해줘. @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>컴포넌트 파일 구조 및 props 정리</li>
<li>재사용 가능한 유사 패턴 목록</li>
<li>영향받는 파일 범위 파악</li>
</ul>
</div>
</article>
<article class="stage-card purple">
<h3 class="stage-title"><span class="round purple">2</span>계획 단계</h3>
<div class="example">"뱃지 추가 구현 계획 잡아줘. @rules/claude-workflow.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">claude-workflow.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>수정 파일 목록 및 변경 순서</li>
<li>아키텍처 영향 범위 정리</li>
<li>사용자 승인 필요 항목 명시</li>
</ul>
</div>
</article>
<article class="stage-card green">
<h3 class="stage-title"><span class="round green">3</span>구현 단계</h3>
<div class="example">"계획대로 구현해줘. @rules/coding-conventions.md @rules/framework-rules.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">framework-rules.md</span>
</div>
<div class="work-box green">
<strong>산출 내용</strong>
<ul>
<li>Vue 컴포넌트 코드 수정</li>
<li>TypeScript 타입 추가 · 수정</li>
<li>Tailwind 클래스 적용 및 clsx 처리</li>
</ul>
</div>
</article>
<article class="stage-card orange">
<h3 class="stage-title"><span class="round orange">4</span>검증 단계</h3>
<div class="example">"변경 코드 검증하고 커밋·PR 작성해줘. @rules/commit-pr.md 참고해서"</div>
<div class="chip-row">
<span class="chip yellow">coding-conventions.md</span>
<span class="chip yellow">commit-pr.md</span>
</div>
<div class="work-box red">
<strong>산출 내용</strong>
<ul>
<li>lint / tsc / build 통과 확인</li>
<li>Conventional Commits 커밋 메시지 작성</li>
<li>PR 템플릿(변경사항·배경·테스트·체크리스트) 작성</li>
</ul>
</div>
</article>
</div>
<div style="margin-top: 16px; padding: 12px 14px; background: var(--purple-soft); border-left: 4px solid var(--purple); border-radius: 4px; font-size: 13px; color: #4d2e91;">
<strong>Skill 활용 팁</strong> — 반복 업무는 스킬 명령으로 더 빠르게 처리할 수 있습니다.<br>
<span style="font-family: monospace; font-size: 12px;">
/conventional-commit &nbsp;·&nbsp; /verify-component-review &nbsp;·&nbsp; /plan-analyzer &nbsp;·&nbsp; /markup-edm
</span>
</div>
</section>
<!-- 범례 -->
<section class="card">
<h2 class="section-title">범례</h2>
<div class="legend">
<strong>공통 자동</strong>
<span class="chip blue">CLAUDE.md — 세션 1회 자동 로드</span>
<span class="chip yellow">rules/* — 매 대화 턴 자동 로드 (4개 파일)</span>
<span class="chip green">단계별 주요 참조 파일</span>
<span class="chip purple">@rules — 직접 지정 (CASE 2)</span>
<span class="chip red">검증 · 커밋 / PR 단계</span>
</div>
</section>
</main>
</body>
</html>

612
html/fe-ai-rules.html Normal file
View File

@@ -0,0 +1,612 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 AI 활용 지침 체계</title>
<style>
:root {
--navy: #18172b;
--blue: #0b5ed7;
--blue-dark: #0756c9;
--purple: #46369a;
--green: #15803d;
--orange: #d97706;
--bg: #f5f7fb;
--card: #ffffff;
--line: #dce4f0;
--text: #1f2937;
--muted: #667085;
--soft-blue: #eef5ff;
--soft-yellow: #fff9e8;
--soft-green: #eefaf2;
--soft-purple: #f3f0ff;
--soft-gray: #f8fafc;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.55;
}
.hero {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 44px;
color: #fff;
background: var(--navy);
border-bottom: 3px solid #1677ff;
}
.hero h1 {
margin: 0;
font-size: 25px;
font-weight: 800;
letter-spacing: -0.04em;
}
.hero p {
margin: 0;
color: #9da8ca;
font-size: 12px;
}
.container {
width: min(1050px, calc(100% - 48px));
margin: 32px auto 48px;
}
.card {
margin-bottom: 20px;
padding: 24px;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
}
.section-title {
margin: 0 0 18px;
color: #8a9cbc;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.02em;
}
.purpose-title {
margin: 0 0 10px;
color: var(--blue);
font-size: 17px;
font-weight: 800;
line-height: 1.45;
}
.purpose-desc {
margin: 0;
font-size: 14px;
}
/* rules 4개 카드 */
.rules-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.rule-box {
padding: 18px;
border-radius: 6px;
border: 1px solid;
}
.rule-box.blue { background: var(--soft-blue); border-color: #9ec3ff; }
.rule-box.yellow { background: var(--soft-yellow); border-color: #ecc65d; }
.rule-box.green { background: var(--soft-green); border-color: #8fd1a4; }
.rule-box.purple { background: var(--soft-purple); border-color: #c4b5fd; }
.box-title {
margin: 0 0 8px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
font-weight: 800;
}
.blue .box-title { color: #003d91; }
.yellow .box-title { color: #8a5a00; }
.green .box-title { color: #065f2b; }
.purple .box-title { color: #3b2780; }
.box-label {
display: inline-block;
margin-bottom: 10px;
padding: 3px 8px;
font-size: 11px;
font-weight: 800;
border-radius: 4px;
}
.blue .box-label { color: #004da8; background: #bdd7ff; }
.yellow .box-label { color: #8a5a00; background: #ffe49c; }
.green .box-label { color: #065f2b; background: #bce9cd; }
.purple .box-label { color: #3b2780; background: #ddd6fe; }
.box-subject {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
}
.dash-list {
margin: 0;
padding: 0;
list-style: none;
color: #4b5563;
font-size: 12px;
line-height: 1.8;
}
.dash-list li::before {
content: "· ";
}
/* 파일별 상세 내용 */
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-box {
padding: 18px 20px;
background: var(--soft-gray);
border: 1px solid var(--line);
border-radius: 6px;
}
.detail-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 14px;
font-size: 14px;
font-weight: 800;
}
.detail-title code {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 12px;
color: var(--blue-dark);
background: var(--soft-blue);
padding: 2px 7px;
border-radius: 4px;
}
.kv-list {
margin: 0;
padding: 0;
list-style: none;
font-size: 13px;
}
.kv-list li {
display: flex;
gap: 8px;
padding: 5px 0;
border-bottom: 1px solid var(--line);
}
.kv-list li:last-child {
border-bottom: 0;
}
.kv-key {
flex-shrink: 0;
width: 90px;
color: #6b7280;
font-size: 12px;
}
.kv-val {
color: var(--text);
}
/* 커밋 타입 뱃지 */
.commit-types {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.ct {
padding: 3px 9px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
font-weight: 800;
border-radius: 4px;
background: #1e293b;
color: #e2e8f0;
}
/* 작업 흐름 */
.flow {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
align-items: stretch;
}
.flow-item {
position: relative;
padding: 18px 16px;
background: var(--soft-gray);
border: 1px solid var(--line);
border-radius: 6px;
}
.flow-item:not(:last-child)::after {
content: "→";
position: absolute;
top: 50%;
right: -14px;
color: #98a2b3;
font-size: 18px;
transform: translateY(-50%);
}
.flow-num {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-bottom: 10px;
color: #fff;
font-size: 12px;
font-weight: 800;
border-radius: 50%;
background: var(--blue-dark);
}
.flow-num.green { background: var(--green); }
.flow-num.orange { background: var(--orange); }
.flow-num.purple { background: var(--purple); }
.flow-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 800;
}
.flow-desc {
margin: 0 0 10px;
color: #4b5563;
font-size: 12px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.tag {
display: inline-block;
padding: 2px 7px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 11px;
border-radius: 4px;
border: 1px solid;
background: #fff;
}
.tag.blue { color: #064aa2; border-color: #9ec3ff; background: #eef5ff; }
.tag.yellow { color: #8a5a00; border-color: #ecc65d; background: #fff9e8; }
.tag.green { color: #166534; border-color: #8fd1a4; background: #eefaf2; }
.tag.purple { color: #3b2780; border-color: #c4b5fd; background: #f5f3ff; }
/* 금지 규칙 */
.no-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.no-item {
padding: 12px 14px;
background: #fff5f5;
border: 1px solid #fca5a5;
border-radius: 6px;
font-size: 13px;
color: #991b1b;
}
.no-item strong {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #7f1d1d;
}
@media (max-width: 900px) {
.hero {
display: block;
padding: 24px;
}
.hero p {
margin-top: 8px;
}
.container {
width: calc(100% - 28px);
margin-top: 20px;
}
.rules-grid,
.detail-grid,
.flow,
.no-list {
grid-template-columns: 1fr;
}
.flow-item:not(:last-child)::after {
top: auto;
right: 50%;
bottom: -18px;
transform: translateX(50%) rotate(90deg);
}
}
</style>
</head>
<body>
<header class="hero">
<div>
<h1>프론트엔드 AI 활용 지침 체계</h1>
<p>gameservice-fe-agent — rules/ 4개 파일 요약</p>
</div>
</header>
<main class="container">
<!-- 목적 -->
<section class="card">
<h2 class="section-title">문서 목적</h2>
<p class="purpose-title">
Claude AI가 프론트엔드 개발 업무를 지원할 때,<br />
어떤 지침을 언제 자동으로 참조하는지 정의합니다.
</p>
<p class="purpose-desc">
<code>rules/</code> 폴더의 4개 파일은 <code>CLAUDE.md</code>에서 <code>@import</code>로 불러오며,
모든 작업에 자동으로 적용됩니다. Vue 3 / Nuxt / TypeScript / Tailwind CSS 기반 프로젝트에서
일관성 있는 코드 작성과 협업을 지원합니다.
</p>
</section>
<!-- rules 4개 개요 -->
<section class="card">
<h2 class="section-title">rules/ 4개 파일 개요</h2>
<div class="rules-grid">
<article class="rule-box blue">
<h3 class="box-title">coding-conventions.md</h3>
<span class="box-label">코딩 컨벤션</span>
<p class="box-subject">포맷팅 · 네이밍 · 타입</p>
<ul class="dash-list">
<li>스페이스 2칸, 싱글 쿼터, 세미콜론 필수</li>
<li>camelCase / PascalCase / UPPER_SNAKE</li>
<li>any 사용 금지, 함수 반환 타입 명시</li>
<li>import 순서 (외부→절대→상대)</li>
</ul>
</article>
<article class="rule-box yellow">
<h3 class="box-title">framework-rules.md</h3>
<span class="box-label">프레임워크 규칙</span>
<p class="box-subject">Vue 3 / Nuxt / Tailwind</p>
<ul class="dash-list">
<li>&lt;script setup lang="ts"&gt; 기본</li>
<li>Pinia (공유 상태), useFetch (서버 상태)</li>
<li>Tailwind 유틸리티 우선, clsx 조건부 클래스</li>
<li>라이브러리 도입 시 PR에 이유 기록</li>
</ul>
</article>
<article class="rule-box green">
<h3 class="box-title">commit-pr.md</h3>
<span class="box-label">커밋 / PR 규칙</span>
<p class="box-subject">Conventional Commits + PR 템플릿</p>
<ul class="dash-list">
<li>type(scope): subject 형식</li>
<li>subject 50자 이내, 명령형 현재 시제</li>
<li>Squash and merge 기본 전략</li>
<li>CI(Lint/Test/Build) + 1인 이상 승인</li>
</ul>
</article>
<article class="rule-box purple">
<h3 class="box-title">claude-workflow.md</h3>
<span class="box-label">Claude 작업 방식</span>
<p class="box-subject">탐색 → 계획 → 구현 → 검증</p>
<ul class="dash-list">
<li>기존 코드 존중, 최소 변경 원칙</li>
<li>모호한 요구사항은 추측 말고 질문</li>
<li>임의 기능 추가·대량 리팩토링 금지</li>
<li>결론 먼저, 변경 파일 요약</li>
</ul>
</article>
</div>
</section>
<!-- 파일별 상세 -->
<section class="card">
<h2 class="section-title">파일별 핵심 규칙 상세</h2>
<div class="detail-grid">
<div class="detail-box">
<h3 class="detail-title"><code>coding-conventions.md</code> 포맷팅 &amp; 네이밍</h3>
<ul class="kv-list">
<li><span class="kv-key">들여쓰기</span><span class="kv-val">스페이스 2칸 (탭 금지)</span></li>
<li><span class="kv-key">문자열</span><span class="kv-val">싱글 쿼터 <code>'</code>, JSX 속성은 더블 쿼터 <code>"</code></span></li>
<li><span class="kv-key">세미콜론</span><span class="kv-val">항상 작성 (생략 금지)</span></li>
<li><span class="kv-key">최대 길이</span><span class="kv-val">한 줄 100자, 초과 시 줄바꿈</span></li>
<li><span class="kv-key">변수/함수</span><span class="kv-val">camelCase — userProfile, fetchUserData</span></li>
<li><span class="kv-key">상수</span><span class="kv-val">UPPER_SNAKE_CASE — MAX_RETRY_COUNT</span></li>
<li><span class="kv-key">컴포넌트</span><span class="kv-val">PascalCase.vue — UserCard.vue</span></li>
<li><span class="kv-key">Composable</span><span class="kv-val">use 접두사 camelCase — useAuth.ts</span></li>
<li><span class="kv-key">불리언</span><span class="kv-val">is / has / can / should 접두사</span></li>
<li><span class="kv-key">any 사용</span><span class="kv-val">금지 — 불가피 시 주석 + unknown 우선 검토</span></li>
</ul>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>framework-rules.md</code> Vue 3 / Nuxt / Tailwind</h3>
<ul class="kv-list">
<li><span class="kv-key">컴포넌트</span><span class="kv-val">&lt;script setup lang="ts"&gt; 필수, Options API 금지</span></li>
<li><span class="kv-key">컴포넌트 크기</span><span class="kv-val">200줄 초과 시 분리 검토</span></li>
<li><span class="kv-key">Props</span><span class="kv-val">defineProps&lt;T&gt;() 제네릭으로 타입 명시</span></li>
<li><span class="kv-key">Emits</span><span class="kv-val">defineEmits&lt;{ ... }&gt;() 제네릭으로 선언</span></li>
<li><span class="kv-key">ref vs reactive</span><span class="kv-val">원시값 · 단일 객체는 ref 우선</span></li>
<li><span class="kv-key">공유 상태</span><span class="kv-val">Pinia 사용</span></li>
<li><span class="kv-key">서버 상태</span><span class="kv-val">useFetch / useAsyncData (직접 fetch 지양)</span></li>
<li><span class="kv-key">라우팅</span><span class="kv-val">Nuxt 파일 기반 라우팅, [param].vue 동적 라우트</span></li>
<li><span class="kv-key">Tailwind</span><span class="kv-val">유틸리티 우선, 조건부는 clsx / cn</span></li>
<li><span class="kv-key">라이브러리</span><span class="kv-val">도입 시 PR에 이유·번들 영향·대안 기록</span></li>
</ul>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>commit-pr.md</code> Conventional Commits</h3>
<ul class="kv-list">
<li><span class="kv-key">형식</span><span class="kv-val">type(scope): subject</span></li>
<li><span class="kv-key">subject</span><span class="kv-val">50자 이내, 명령형 현재 시제, 마침표 없음</span></li>
<li><span class="kv-key">body</span><span class="kv-val">"왜"를 설명, 72자 줄바꿈</span></li>
<li><span class="kv-key">머지 전략</span><span class="kv-val">Squash and merge 기본</span></li>
<li><span class="kv-key">승인</span><span class="kv-val">최소 1인 이상 + CI 전부 통과</span></li>
<li><span class="kv-key">Draft PR</span><span class="kv-val">중간 피드백 필요 시 Draft 먼저 오픈</span></li>
</ul>
<div class="commit-types">
<span class="ct">feat</span>
<span class="ct">fix</span>
<span class="ct">refactor</span>
<span class="ct">style</span>
<span class="ct">docs</span>
<span class="ct">test</span>
<span class="ct">chore</span>
<span class="ct">perf</span>
<span class="ct">ci</span>
</div>
</div>
<div class="detail-box">
<h3 class="detail-title"><code>claude-workflow.md</code> 작업 원칙</h3>
<ul class="kv-list">
<li><span class="kv-key">작업 순서</span><span class="kv-val">탐색 → 계획 → 구현 → 검증</span></li>
<li><span class="kv-key">기본 원칙</span><span class="kv-val">기존 코드 존중 · 최소 변경 · 근거 있는 수정</span></li>
<li><span class="kv-key">모호한 요구</span><span class="kv-val">추측 금지 → 사용자에게 확인</span></li>
<li><span class="kv-key">커뮤니케이션</span><span class="kv-val">결론 먼저, 변경 파일 요약</span></li>
<li><span class="kv-key">검증</span><span class="kv-val">린트 / 타입체크 / 빌드 통과 확인</span></li>
</ul>
</div>
</div>
</section>
<!-- 금지 규칙 -->
<section class="card">
<h2 class="section-title">claude-workflow.md — 해서는 안 되는 것</h2>
<div class="no-list">
<div class="no-item">
<strong>임의 기능 추가 금지</strong>
사용자가 요청하지 않은 기능을 추가하지 않습니다.
</div>
<div class="no-item">
<strong>대량 리팩토링 금지</strong>
요청 범위를 벗어나는 코드 변경은 하지 않습니다.
</div>
<div class="no-item">
<strong>주석·문서 임의 삭제 금지</strong>
불필요해 보여도 삭제 전 사용자에게 확인합니다.
</div>
<div class="no-item">
<strong>비밀정보 하드코딩 금지</strong>
환경변수, 키, 토큰을 코드에 직접 작성하지 않습니다.
</div>
<div class="no-item">
<strong>의존성 버전 임의 변경 금지</strong>
요청 없이 package.json 버전을 수정하지 않습니다.
</div>
<div class="no-item">
<strong>강제 푸시·히스토리 재작성 금지</strong>
push --force, reset --hard는 명시적 요청 없이 실행하지 않습니다.
</div>
</div>
</section>
<!-- 작업 흐름 -->
<section class="card">
<h2 class="section-title">Claude 작업 단계별 참조 흐름</h2>
<div class="flow">
<article class="flow-item">
<div class="flow-num">1</div>
<h3 class="flow-title">탐색 (Explore)</h3>
<p class="flow-desc">관련 파일 파악, 유사 패턴 확인</p>
<div class="tag-list">
<span class="tag blue">CLAUDE.md</span>
<span class="tag yellow">coding-conventions</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num purple">2</div>
<h3 class="flow-title">계획 (Plan)</h3>
<p class="flow-desc">할 일 목록 공유, 아키텍처 변경 시 승인</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag yellow">framework-rules</span>
<span class="tag purple">claude-workflow</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num orange">3</div>
<h3 class="flow-title">구현 (Implement)</h3>
<p class="flow-desc">코드 작성, 공통·프로젝트 지침 준수</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag yellow">framework-rules</span>
</div>
</article>
<article class="flow-item">
<div class="flow-num green">4</div>
<h3 class="flow-title">검증 (Verify)</h3>
<p class="flow-desc">린트·타입체크·빌드, 커밋·PR 작성</p>
<div class="tag-list">
<span class="tag yellow">coding-conventions</span>
<span class="tag green">commit-pr</span>
<span class="tag purple">claude-workflow</span>
</div>
</article>
</div>
</section>
</main>
</body>
</html>

1025
html/fe-ai-workflow-ppt.html Normal file

File diff suppressed because it is too large Load Diff

800
html/fe-ai-workflow.html Normal file
View File

@@ -0,0 +1,800 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>프론트엔드 AI 공통 지침 - Work Flow</title>
<style>
:root {
--bg: #f6f5f1;
--panel: #ffffff;
--line: #d7d7d7;
--text: #222;
--muted: #777;
--navy: #25384a;
--blue: #1f7fbe;
--blue-bg: #e8f5fb;
--yellow: #e6a400;
--yellow-bg: #fff9dc;
--green: #17a968;
--green-bg: #e6f7ee;
--red: #d94132;
--red-bg: #fde9e9;
--orange: #e86f16;
--purple: #8d42b2;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
background: var(--bg);
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Noto Sans KR",
"Apple SD Gothic Neo",
sans-serif;
line-height: 1.45;
}
.page {
min-width: 1480px;
padding: 8px 10px 18px;
}
.layout {
display: grid;
grid-template-columns: 255px 1fr;
gap: 22px;
align-items: stretch;
}
/* Sidebar */
.sidebar {
overflow: hidden;
background: #fff;
border: 1px solid #d0d0d0;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.sidebar-head {
padding: 14px 20px;
color: #fff;
font-size: 15px;
font-weight: 800;
background: var(--navy);
}
.side-body {
padding: 14px 14px 18px;
}
.side-group {
margin-bottom: 14px;
padding: 10px;
border-radius: 7px;
border: 2px solid;
}
.side-group.yellow {
border-color: #f2ad23;
background: #fffdf4;
}
.side-group.blue {
border-color: #1f7fbe;
background: #f0f8ff;
}
.side-title,
.side-file {
display: block;
margin-bottom: 7px;
padding: 4px 8px;
text-align: center;
border-radius: 3px;
font-size: 11px;
font-weight: 800;
font-family: "SFMono-Regular", Consolas, monospace;
}
.yellow .side-title,
.yellow .side-file {
color: #7b5a00;
border: 1px solid #f2ad23;
background: #fff3bf;
}
.blue .side-title,
.blue .side-file {
color: #0f4d77;
border: 1px solid #1f7fbe;
background: #d6ecf8;
}
.side-caption {
margin: 8px 0 0;
color: #8a8a8a;
text-align: center;
font-size: 11px;
font-style: italic;
}
.side-desc {
margin: 6px 0 0;
color: #666;
font-size: 11px;
line-height: 1.5;
padding: 0 2px;
}
/* Main */
.main {
padding-top: 0;
}
.title-wrap {
padding: 10px 0 14px;
text-align: center;
}
h1 {
margin: 0;
font-size: 24px;
font-weight: 900;
letter-spacing: -0.03em;
}
.subtitle {
margin: 10px 0 0;
color: #767676;
font-size: 12px;
}
.workflow-panel {
position: relative;
min-height: 780px;
padding: 236px 42px 36px;
background: var(--panel);
border: 1px solid #cfcfcf;
border-radius: 8px;
}
.flow-row {
display: grid;
grid-template-columns: 280px 60px 280px 60px 280px 60px 280px 60px 280px 60px 280px;
align-items: center;
}
.arrow {
color: #444;
font-size: 48px;
line-height: 1;
text-align: center;
}
.node {
height: 135px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 4px solid;
border-radius: 16px;
text-align: center;
background: #fff;
}
.node .title {
margin-bottom: 8px;
font-size: 24px;
font-weight: 900;
letter-spacing: -0.04em;
}
.node .desc {
color: #555;
font-size: 17px;
}
.node.blue {
color: #0f4d77;
border-color: var(--blue);
background: var(--blue-bg);
}
.node.yellow {
color: #8a6500;
border-color: var(--yellow);
background: var(--yellow-bg);
}
.node.green {
color: #09643d;
border-color: var(--green);
background: var(--green-bg);
}
.node.red {
color: #9f2820;
border-color: var(--red);
background: var(--red-bg);
}
.node-num {
display: inline-flex;
justify-content: center;
align-items: center;
width: 23px;
height: 23px;
margin-right: 6px;
border: 2px solid currentColor;
border-radius: 50%;
font-size: 14px;
font-weight: 900;
}
/* absolute guide lines */
.rules-bar {
position: absolute;
left: 518px;
right: 43px;
top: 392px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
color: #9a6500;
font-size: 17px;
font-weight: 900;
background: #fffef7;
border: 2px dashed #f0a000;
border-radius: 10px;
}
.v-line {
position: absolute;
width: 0;
border-left: 2px dashed #b7b7b7;
}
.v-line.yellow {
border-color: #d49a00;
}
.v-line.green {
border-color: #15975f;
}
.v-line.red {
border-color: #c0392b;
}
.v1 {
left: 645px;
top: 360px;
height: 33px;
}
.v2 {
left: 989px;
top: 355px;
height: 77px;
}
.v3 {
left: 1344px;
top: 356px;
height: 36px;
}
.v4 {
left: 1696px;
top: 356px;
height: 36px;
}
.v5 {
left: 2041px;
top: 356px;
height: 36px;
}
.doc-row {
position: absolute;
left: 520px;
right: 42px;
top: 448px;
display: grid;
grid-template-columns: 280px 60px 280px 60px 280px 60px 280px 60px 280px;
align-items: start;
}
.doc-col {
display: flex;
flex-direction: column;
gap: 10px;
}
.doc-chip {
min-height: 36px;
padding: 6px 12px;
text-align: center;
border: 2px solid;
border-radius: 8px;
font-size: 15px;
font-family: "SFMono-Regular", Consolas, monospace;
background: #fff;
}
.doc-chip.yellow {
color: #8a6500;
border-color: var(--yellow);
background: var(--yellow-bg);
font-family: inherit;
font-weight: 800;
font-size: 14px;
}
.doc-chip.green {
color: #09643d;
border-color: var(--green);
background: var(--green-bg);
}
.doc-chip.red {
color: #9f2820;
border-color: var(--red);
background: var(--red-bg);
}
.doc-chip.blue {
color: #0f4d77;
border-color: var(--blue);
background: var(--blue-bg);
}
/* feedback loop */
.loop-label {
position: absolute;
z-index: 3;
padding: 7px 22px;
font-size: 20px;
font-weight: 900;
border: 2px solid;
border-radius: 10px;
background: #fff;
}
.label-purple {
top: 30px;
left: 1180px;
color: var(--purple);
border-color: var(--purple);
background: #fff5ff;
}
.label-orange {
top: 109px;
left: 990px;
color: var(--orange);
border-color: var(--orange);
background: #fff8f2;
}
.label-red {
top: 86px;
left: 1383px;
color: var(--red);
border-color: var(--red);
background: #fff5f5;
}
.loop-line {
position: absolute;
pointer-events: none;
}
.loop-purple {
top: 54px;
left: 944px;
width: 705px;
height: 150px;
border: 4px dashed var(--purple);
border-bottom: 0;
border-radius: 18px 18px 0 0;
}
.loop-purple:before,
.loop-purple:after {
content: "";
position: absolute;
bottom: -18px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid var(--purple);
}
.loop-purple:before {
left: -16px;
}
.loop-purple:after {
right: -16px;
}
.loop-orange {
top: 137px;
left: 944px;
width: 350px;
height: 72px;
border-top: 4px dashed var(--orange);
border-left: 4px dashed var(--orange);
border-right: 4px dashed var(--orange);
border-radius: 12px 12px 0 0;
}
.loop-orange:before,
.loop-orange:after {
content: "";
position: absolute;
bottom: -18px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid var(--orange);
}
.loop-orange:before {
left: -16px;
}
.loop-orange:after {
right: -16px;
}
.loop-red {
top: 137px;
left: 1292px;
width: 355px;
height: 72px;
border-top: 4px dashed #ff4b3d;
border-left: 4px dashed #ff4b3d;
border-right: 4px dashed #ff4b3d;
border-radius: 12px 12px 0 0;
}
.loop-red:before {
content: "";
position: absolute;
bottom: -18px;
left: -16px;
width: 0;
height: 0;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
border-top: 30px solid #ff4b3d;
}
.feedback-box {
position: absolute;
left: 42px;
right: 42px;
bottom: 28px;
padding: 16px 36px 28px;
border: 2px solid #d1d1d1;
border-radius: 14px;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.feedback-box h2 {
margin: 0 0 22px;
text-align: center;
font-size: 23px;
font-weight: 900;
}
.legend-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
}
.legend-item {
display: flex;
align-items: center;
gap: 14px;
font-size: 20px;
}
.line-sample {
position: relative;
flex-shrink: 0;
width: 85px;
border-top: 4px dashed;
}
.line-sample:after {
content: "";
position: absolute;
right: -1px;
top: -10px;
width: 0;
height: 0;
border-left: 18px solid currentColor;
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
}
.line-sample.red {
color: #ff4b3d;
border-color: #ff4b3d;
}
.line-sample.orange {
color: var(--orange);
border-color: var(--orange);
}
.line-sample.purple {
color: var(--purple);
border-color: var(--purple);
}
.bottom-legend {
margin-top: 14px;
padding: 12px 18px;
background: #fff;
border: 1px solid #d4d4d4;
border-radius: 7px;
font-size: 12px;
}
.bottom-legend strong {
margin-right: 12px;
}
.mini-box {
display: inline-block;
width: 25px;
height: 12px;
margin: 0 5px 0 12px;
border: 2px solid;
border-radius: 2px;
vertical-align: -2px;
}
.mini-box.yellow {
border-color: var(--yellow);
background: var(--yellow-bg);
}
.mini-box.green {
border-color: var(--green);
background: var(--green-bg);
}
.mini-box.red {
border-color: var(--red);
background: var(--red-bg);
}
.mini-box.blue {
border-color: var(--blue);
background: var(--blue-bg);
}
.mini-arrow {
display: inline-block;
margin: 0 10px 0 20px;
font-size: 22px;
line-height: 0;
vertical-align: -3px;
}
.mini-arrow.red {
color: #ff4b3d;
}
.note {
margin-top: 14px;
padding: 11px 18px;
color: #777;
text-align: center;
background: #f1f1ee;
border: 1px solid #d6d6d2;
border-radius: 7px;
font-size: 12px;
}
@media (max-width: 1600px) {
.page {
transform-origin: top left;
}
}
</style>
</head>
<body>
<div class="page">
<div class="layout">
<aside class="sidebar">
<div class="sidebar-head">📁 gameservice-fe-agent 구조</div>
<div class="side-body">
<div class="side-group yellow">
<span class="side-file">CLAUDE.md</span>
<p class="side-caption">세션 시작 시 1회 자동 로드<br />rules/* 전체를 @import</p>
</div>
<div class="side-group yellow">
<span class="side-title">rules/ (매 대화 턴 자동)</span>
<span class="side-file">coding-conventions.md</span>
<span class="side-file">framework-rules.md</span>
<span class="side-file">commit-pr.md</span>
<span class="side-file">claude-workflow.md</span>
<p class="side-caption">포맷팅·네이밍·Vue·커밋·작업방식</p>
</div>
<div class="side-group blue">
<span class="side-title">skills/ (명령으로 호출)</span>
<span class="side-file">conventional-commit</span>
<span class="side-file">markup-edm</span>
<span class="side-file">plan-analyzer</span>
<span class="side-file">plan-translation-generator</span>
<span class="side-file">project-init</span>
<span class="side-file">verify-component-review</span>
<span class="side-file">work-log</span>
<p class="side-caption">link-skills.sh 로 .claude/skills/ 링크</p>
</div>
</div>
</aside>
<main class="main">
<header class="title-wrap">
<h1>프론트엔드 AI 공통 지침 - Work Flow</h1>
<p class="subtitle">gameservice-fe-agent rules/ 구조와 각 작업 단계별 참조 파일의 흐름 (피드백 루프 포함)</p>
</header>
<section class="workflow-panel">
<div class="loop-line loop-purple"></div>
<div class="loop-line loop-orange"></div>
<div class="loop-line loop-red"></div>
<div class="loop-label label-purple">설계 변경 필요</div>
<div class="loop-label label-orange">계획 재수립 필요</div>
<div class="loop-label label-red">버그 발견 · 수정</div>
<div class="flow-row">
<article class="node blue">
<div class="title">요청자 / 개발자</div>
<div class="desc">업무 요청</div>
</article>
<div class="arrow"></div>
<article class="node yellow">
<div class="title"><span class="node-num">1</span>자동 로드</div>
<div class="desc">CLAUDE.md + rules/* × 4</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">2</span>탐색</div>
<div class="desc">파일 파악 · 패턴 확인</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">3</span>계획</div>
<div class="desc">할 일 목록 · 승인</div>
</article>
<div class="arrow"></div>
<article class="node green">
<div class="title"><span class="node-num">4</span>구현</div>
<div class="desc">코드 작성 · 지침 준수</div>
</article>
<div class="arrow"></div>
<article class="node red">
<div class="title"><span class="node-num">5</span>검증</div>
<div class="desc">린트 · 빌드 · 커밋/PR</div>
</article>
</div>
<div class="v-line yellow v1"></div>
<div class="v-line green v2"></div>
<div class="v-line green v3"></div>
<div class="v-line green v4"></div>
<div class="v-line red v5"></div>
<div class="rules-bar">rules/* × 4 — 매 대화 턴 자동 참조 (① ~ ⑤ 전 단계 공통)</div>
<div class="doc-row">
<div class="doc-col">
<div class="doc-chip yellow">CLAUDE.md (세션 시작 1회)</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">coding-conventions.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">claude-workflow.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip green">coding-conventions.md</div>
<div class="doc-chip green">framework-rules.md</div>
</div>
<div></div>
<div class="doc-col">
<div class="doc-chip red">coding-conventions.md</div>
<div class="doc-chip red">commit-pr.md</div>
</div>
</div>
<div class="feedback-box">
<h2>피드백 루프 (역방향 흐름)</h2>
<div class="legend-grid">
<div class="legend-item">
<span class="line-sample red"></span>
<span>④ 구현 → ⑤ 검증 → ④ 구현 : 버그 발견 시 수정 후 재검증</span>
</div>
<div class="legend-item">
<span class="line-sample orange"></span>
<span>③ 계획 → ④ 구현 중 재설계 : 계획 재수립 후 다시 구현</span>
</div>
<div class="legend-item">
<span class="line-sample purple"></span>
<span>⑤ 검증 → ② 탐색 : 검증 결과로 구조 변경이 필요한 경우</span>
</div>
</div>
</div>
</section>
<div class="bottom-legend">
<strong>범례</strong>
<span class="mini-box yellow"></span> 세션 시작 1회 자동 로드 (CLAUDE.md)
<span class="mini-box yellow"></span> 매 대화 턴 자동 참조 (rules/ × 4)
<span class="mini-box green"></span> 탐색·계획·구현 단계 참조
<span class="mini-box red"></span> 검증·커밋/PR 단계 참조
<span class="mini-box blue"></span> skills/ (명령으로 호출)
<span class="mini-arrow"></span> 정방향 진행
<span class="mini-arrow red">--▶</span> 피드백 루프 (역방향)
</div>
<div class="note">
피드백 루프는 구현·검증 중 발견된 이슈에 따라 이전 단계로 돌아가 재작업 후 다시 진행합니다.
모호한 요구사항은 추측하지 않고 사용자에게 확인 후 계획 단계부터 다시 시작합니다.
</div>
</main>
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

BIN
skills/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,150 @@
---
name: dev-api-state
description: |
API 연동 패턴(useFetch / useAsyncData / $fetch)과 Pinia 상태관리 코드를
상황에 맞게 자동 선택·생성합니다. BFF 패턴, 에러 핸들링, 로딩 상태를 포함합니다.
다음 상황에서 반드시 사용하세요:
- "이 API 연동 + 스토어 만들어줘", "Pinia store 작성해줘"
- "API 데이터 페칭 어떻게 해?", "useFetch vs useAsyncData 언제 써?"
- API 키 보호를 위한 BFF(server/api/) 패턴이 필요할 때
---
# API 연동 & 상태관리 (dev-api-state)
API 스펙 → useFetch/useAsyncData 패턴 선택 → Pinia Setup Store 코드 자동 생성.
## 언제 사용하는가
- 새 API 연동 코드와 Pinia 스토어를 함께 작성할 때
- `useFetch` / `$fetch` / `useAsyncData` 중 적절한 패턴을 결정할 때
- API 키를 클라이언트에 노출하지 않기 위한 BFF 패턴이 필요할 때
## 데이터 페칭 패턴 선택 기준
| 상황 | 권장 패턴 |
|---|---|
| SSR 페이지 초기 데이터 | `useAsyncData` + `$fetch` |
| 컴포넌트 마운트 후 데이터 | `useFetch` |
| 사용자 액션으로 트리거 | `$fetch` (직접 호출) |
| API 키 보호 필요 | `server/api/` BFF + `useFetch` |
| 복잡한 캐싱/의존성 | `useAsyncData` with key |
---
## 작업 순서
### Phase 1: API 스펙 파악
1. 엔드포인트, 요청/응답 타입을 파악한다.
2. 아래를 확인한다:
- 인증 헤더 필요 여부
- API 키 노출 위험 여부 → BFF 패턴 적용
- 캐시 전략 (항상 최신 vs TTL)
### Phase 2: 타입 정의
```ts
// types/product.ts
export interface Product {
id: string
name: string
price: number
imageUrl: string
}
export interface ProductListResponse {
data: Product[]
total: number
page: number
}
```
### Phase 3: BFF 레이어 (필요 시)
API 키 보호가 필요한 경우 server/api/ 경유:
```ts
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const response = await $fetch<ProductListResponse>(
`${process.env.API_BASE}/products`,
{
headers: { 'x-api-key': process.env.API_KEY! },
query,
}
)
return response
})
```
### Phase 4: Pinia Setup Store 생성
Setup Store 패턴을 기본으로 사용한다:
```ts
// stores/product.ts
import type { Product } from '~/types/product'
export const useProductStore = defineStore('product', () => {
const items = ref<Product[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const fetchProducts = async (page = 1) => {
isLoading.value = true
error.value = null
try {
const { data } = await useFetch('/api/products', { query: { page } })
items.value = data.value?.data ?? []
} catch (e) {
error.value = e instanceof Error ? e.message : '알 수 없는 오류'
} finally {
isLoading.value = false
}
}
return { items, isLoading, error, fetchProducts }
})
```
### Phase 5: 컴포넌트 연결 예시
```vue
<script setup lang="ts">
const productStore = useProductStore()
const { items, isLoading, error } = storeToRefs(productStore)
onMounted(() => productStore.fetchProducts())
</script>
<template>
<div v-if="isLoading" aria-live="polite">로딩 중...</div>
<div v-else-if="error" role="alert">{{ error }}</div>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
```
---
## 출력 형식
```
## API 연동: <기능명>
### 선택된 패턴
- 페칭: useFetch | useAsyncData | $fetch
- BFF: 사용 | 미사용
- 이유: [선택 근거]
### 파일 목록
- `types/<domain>.ts`
- `server/api/...` (BFF 사용 시)
- `stores/<domain>.ts`
### 코드
[타입 → BFF → 스토어 → 컴포넌트 순]
```

View File

@@ -0,0 +1,125 @@
---
name: dev-component
description: |
화면 명세를 받아 Atomic Design 기반 컴포넌트 트리를 설계하고
Nuxt 표준 디렉토리 구조와 컴포넌트 스켈레톤 코드를 자동 생성합니다.
다음 상황에서 반드시 사용하세요:
- "이 화면 컴포넌트로 분리해줘", "컴포넌트 구조 설계해줘"
- "Atomic Design으로 나눠줘", "컴포넌트 트리 만들어줘"
- 신규 기능의 컴포넌트 아키텍처를 결정해야 할 때
---
# 컴포넌트 아키텍처 설계 (dev-component)
화면 명세 → Atomic Design 기반 컴포넌트 트리 → Nuxt SFC 스켈레톤 자동 생성.
## 언제 사용하는가
- 신규 페이지/기능의 컴포넌트 구조를 잡을 때
- 마크업 완료 후 컴포넌트 분리 기준을 결정할 때
- 팀원이 병렬로 작업할 수 있도록 역할 분리가 필요할 때
## 입력
- 화면 명세 또는 완성된 마크업 코드
- `plan-analyzer` 산출물 (컴포넌트 트리 초안)이 있으면 활용
- 기존 `components/` 디렉토리 구조 (재사용 가능 컴포넌트 파악용)
---
## 작업 순서
### Phase 1: 기존 컴포넌트 파악
1. 프로젝트 `components/` 디렉토리를 읽어 재사용 가능한 컴포넌트를 파악한다.
2. 설계 전 중복 개발 방지를 위해 유사 컴포넌트 목록을 사용자에게 안내한다.
### Phase 2: Atomic Design 분류
각 UI 요소를 아래 5단계로 분류한다:
| 레벨 | 위치 | 기준 |
|---|---|---|
| **Atoms** | `components/base/` | 더 이상 분리 불가한 기본 단위 (Button, Icon, Input, Badge) |
| **Molecules** | `components/common/` | Atoms 2개 이상 조합 (SearchBar, FormField, Card) |
| **Organisms** | `components/<도메인>/` | 독립적 기능 단위 (Header, ProductList, ReviewSection) |
| **Templates** | `layouts/` | 페이지 레이아웃 골격 |
| **Pages** | `pages/` | 실제 데이터와 결합한 최종 화면 |
### Phase 3: 컴포넌트 트리 출력
```
pages/
└── <page-name>.vue
├── layouts/<layout>.vue
└── components/
├── <domain>/
│ ├── <Organism>.vue ← props: { ... }
│ └── <Organism>.vue
└── common/
└── <Molecule>.vue ← props: { ... }, emits: [...]
```
### Phase 4: Props / Emits 설계
각 컴포넌트의 인터페이스를 정의한다:
```ts
// 예시: ProductCard.vue
interface Props {
id: string
title: string
imageUrl: string
price: number
isSoldOut?: boolean
}
type Emits = {
click: [id: string]
addToCart: [id: string]
}
```
### Phase 5: 스켈레톤 코드 생성
각 컴포넌트의 SFC 스켈레톤 파일을 생성한다:
```vue
<script setup lang="ts">
interface Props {
// Phase 4 정의 붙여넣기
}
const props = defineProps<Props>()
const emit = defineEmits<{
// Phase 4 정의 붙여넣기
}>()
</script>
<template>
<!-- TODO: markup-base / markup-figma 스킬로 마크업 구현 -->
</template>
```
---
## 출력 형식
```
## 컴포넌트 설계: <페이지명>
### 재사용 가능한 기존 컴포넌트
- `components/common/Button.vue` — CTA 버튼에 활용 가능
### 컴포넌트 트리
[ASCII 트리]
### 신규 생성 파일 목록
- `components/product/ProductCard.vue` — Props: id, title, imageUrl, price
- `components/product/ProductList.vue` — Props: items[]
- ...
### 스켈레톤 코드
[각 파일 코드]
```

128
skills/dev-docs/SKILL.md Normal file
View File

@@ -0,0 +1,128 @@
---
name: dev-docs
description: |
Nuxt 3 공식 문서 기반의 Best Practice 코드를 생성합니다.
server/, middleware/, plugins/, composables/, components/, nuxt.config 등
6개 참조 영역에서 상황에 맞는 패턴을 선택해 즉시 사용 가능한 코드를 제공합니다.
다음 상황에서 반드시 사용하세요:
- "Nuxt server route 만들어줘", "composable 작성해줘"
- "middleware 어떻게 써?", "Nuxt에서 이 기능 어떻게 구현해?"
- 공식 문서를 찾아보지 않고 Nuxt 3 패턴 코드가 필요할 때
---
# Nuxt 3 공식 문서 기반 개발 (dev-docs)
Nuxt 3 공식 Best Practice를 기반으로 즉시 사용 가능한 코드를 생성합니다.
## 언제 사용하는가
- Nuxt 고유 API(useAsyncData, useFetch, defineEventHandler 등)를 처음 사용할 때
- server/, middleware/, plugins/ 등 Nuxt 레이어 구현이 필요할 때
- 공식 문서를 찾는 대신 곧바로 Best Practice 코드를 얻고 싶을 때
## 참조 영역
| 영역 | 경로 | 주요 API |
|---|---|---|
| Server Routes | `server/api/`, `server/routes/` | `defineEventHandler`, `readBody`, `getQuery` |
| Middleware | `middleware/` | `defineNuxtRouteMiddleware`, `navigateTo` |
| Plugins | `plugins/` | `defineNuxtPlugin`, `useNuxtApp` |
| Composables | `composables/` | `useState`, `useFetch`, `useAsyncData` |
| Components | `components/` | auto-import, `<ClientOnly>`, `<LazyLoad>` |
| Config | `nuxt.config.ts` | `runtimeConfig`, `modules`, `routeRules` |
---
## 작업 순서
### Phase 1: 요구사항 분석
1. 사용자 요청에서 구현 영역을 파악한다.
- "API 만들어줘" → `server/api/`
- "인증 체크" → `middleware/`
- "전역 상태" → `composables/` + `useState`
- "외부 라이브러리 초기화" → `plugins/`
2. 관련 Nuxt 3 API를 선택한다.
### Phase 2: 코드 생성
#### Server Route 예시
```ts
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
// 비즈니스 로직
return { data }
})
```
#### Middleware 예시
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/login')
}
})
```
#### Composable 예시
```ts
// composables/useUser.ts
export const useUser = () => {
const user = useState<User | null>('user', () => null)
const fetchUser = async (id: string) => {
const { data } = await useFetch<User>(`/api/users/${id}`)
user.value = data.value
}
return { user, fetchUser }
}
```
### Phase 3: TypeScript 타입 보강
- 모든 함수 파라미터/반환값 타입 명시
- 외부 API 응답은 `types/` 또는 해당 도메인의 `types.ts`에 정의
- `runtimeConfig``RuntimeConfig` 타입 확장으로 타입 안전성 확보
### Phase 4: 연관 설정 안내
생성된 코드에 필요한 `nuxt.config.ts` 변경사항이 있으면 함께 안내한다.
```ts
// nuxt.config.ts 필요 설정 예시
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: '', // 서버 전용
public: {
apiBase: '' // 클라이언트 노출 가능
}
}
})
```
---
## 출력 형식
```
## Nuxt 구현: <기능명>
### 사용 패턴
- 영역: server/api/ | middleware/ | composables/ | ...
- 주요 API: defineEventHandler, useFetch, ...
### 코드
[파일별 코드]
### nuxt.config.ts 변경 필요 사항
[있는 경우만]
### 주의사항
[SSR/CSR 차이, 캐시 주의 등]
```

View File

@@ -0,0 +1,149 @@
---
name: dev-storybook
description: |
Vue 3 컴포넌트를 받아 Storybook Story 파일과 Props/Emits/슬롯 사용 가이드를 자동 생성합니다.
팀 컴포넌트 카탈로그와 재사용 진입 장벽을 낮추는 데 사용합니다.
다음 상황에서 반드시 사용하세요:
- "이 컴포넌트 Story 만들어줘", "Storybook 파일 생성해줘"
- "컴포넌트 사용 가이드 문서 만들어줘", "Props 문서 작성해줘"
- 컴포넌트 카탈로그에 새 컴포넌트를 등록해야 할 때
---
# Storybook Story 생성 (dev-storybook)
`.vue` 컴포넌트 → `.stories.ts` 파일 + 사용 가이드 자동 생성.
## 언제 사용하는가
- 컴포넌트 완성 후 Storybook에 등록할 때
- 다른 팀원이 컴포넌트를 찾고 재사용할 수 있도록 문서화할 때
- Props 변형(variant)을 시각적으로 확인해야 할 때
## 입력
- 대상 `.vue` 파일 경로
- Storybook 버전 (없으면 Storybook 8 + CSF 3 포맷 기본 적용)
---
## 작업 순서
### Phase 1: 컴포넌트 인터페이스 파악
1. `.vue` 파일을 읽어 아래 항목을 추출한다.
- Props 목록 (타입, 기본값, 설명)
- Emits 목록
- 슬롯 목록 (default, named)
- 주요 상태 변형 (size, variant, disabled, loading 등)
### Phase 2: Story 파일 생성 (CSF 3 포맷)
파일 위치: 컴포넌트와 동일 디렉토리 또는 `stories/`
파일명: `<ComponentName>.stories.ts`
#### 기본 구조
```ts
import type { Meta, StoryObj } from '@storybook/vue3'
import ComponentName from './ComponentName.vue'
const meta: Meta<typeof ComponentName> = {
title: '<Category>/<ComponentName>',
component: ComponentName,
tags: ['autodocs'],
argTypes: {
// Props 컨트롤 정의
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost'],
description: '버튼 스타일 변형',
},
onClick: { action: 'clicked' },
},
}
export default meta
type Story = StoryObj<typeof ComponentName>
```
#### 필수 Story 목록
```ts
// 1. 기본 상태
export const Default: Story = {
args: {
// 기본값으로 렌더링
},
}
// 2. 각 변형(variant)별 Story
export const Primary: Story = {
args: { variant: 'primary', label: '확인' },
}
// 3. 비활성 상태
export const Disabled: Story = {
args: { disabled: true },
}
// 4. 로딩 상태 (해당 시)
export const Loading: Story = {
args: { isLoading: true },
}
// 5. 슬롯 사용 예시 (슬롯 있을 때)
export const WithSlot: Story = {
render: (args) => ({
components: { ComponentName },
setup() { return { args } },
template: `<ComponentName v-bind="args">슬롯 내용</ComponentName>`,
}),
}
```
### Phase 3: 사용 가이드 주석
각 Story에 JSDoc 주석으로 사용 목적을 설명한다:
```ts
/**
* 기본 상태. 가장 일반적인 사용 패턴.
* @example
* <ComponentName label="클릭" @click="handleClick" />
*/
export const Default: Story = { ... }
```
### Phase 4: argTypes 컨트롤 매핑
| Props 타입 | Storybook 컨트롤 |
|---|---|
| `string` | `text` |
| `number` | `number` |
| `boolean` | `boolean` |
| Union 타입 (`'a' \| 'b'`) | `select` |
| Array | `object` |
| Function (emit) | `action` |
---
## 출력 형식
```
## Story: <ComponentName>.stories.ts
### Story 목록
1. Default — 기본 상태
2. Primary — 주요 variant
3. Disabled — 비활성
4. Loading — 로딩 (해당 시)
### 코드
\`\`\`ts
[전체 stories 파일]
\`\`\`
### Storybook 실행
\`\`\`bash
npx storybook dev
\`\`\`
```

View File

@@ -0,0 +1,160 @@
---
name: dev-unit-test
description: |
Vue 3 컴포넌트를 받아 Vitest + Vue Test Utils 기반의 단위 테스트를 자동 생성합니다.
Props, Emits, 슬롯, 사용자 인터랙션, 비동기 동작을 모두 커버합니다.
다음 상황에서 반드시 사용하세요:
- "이 컴포넌트 단위 테스트 작성해줘", "테스트 코드 만들어줘"
- "Vitest 어떻게 써?", "Vue Test Utils 사용 방법"
- 컴포넌트 완성 후 커버리지를 확보해야 할 때
---
# 단위 테스트 생성 (dev-unit-test)
`.vue` 컴포넌트 → Vitest + Vue Test Utils 단위 테스트 자동 생성.
## 언제 사용하는가
- 컴포넌트 개발 완료 후 테스트 코드를 작성할 때
- TDD 방식으로 테스트를 먼저 작성할 때
- 팀 테스트 커버리지 기준(80%↑)을 달성해야 할 때
## 입력
- 테스트할 `.vue` 파일 경로
- 테스트 케이스 범위 (없으면 자동 도출)
---
## 작업 순서
### Phase 1: 컴포넌트 분석
1. 대상 `.vue` 파일을 읽어 아래 항목을 파악한다.
- Props 목록 (타입, 기본값, 필수 여부)
- Emits 목록 (이벤트 이름, 페이로드 타입)
- 슬롯 유무
- 외부 의존성 (composables, store, $fetch)
- 인터랙션 (버튼 클릭, 입력, 폼 제출)
- 비동기 동작 (API 호출, 로딩 상태)
- 조건부 렌더링 (`v-if`, `v-show`)
2. 테스트 케이스 목록을 도출한다.
### Phase 2: 테스트 설정
```ts
// vitest.config.ts (없으면 생성 안내)
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
},
})
```
### Phase 3: 테스트 파일 생성
파일 위치: 컴포넌트와 동일 디렉토리 또는 `__tests__/` 폴더.
파일명: `<ComponentName>.spec.ts`
```ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ComponentName from './ComponentName.vue'
describe('ComponentName', () => {
// Phase 4 케이스 작성
})
```
### Phase 4: 테스트 케이스 작성 패턴
#### Props 검증
```ts
it('title prop을 렌더링한다', () => {
const wrapper = mount(ComponentName, {
props: { title: '테스트 제목' }
})
expect(wrapper.find('h2').text()).toBe('테스트 제목')
})
it('필수 prop 누락 시 기본값을 사용한다', () => {
const wrapper = mount(ComponentName)
expect(wrapper.find('[data-testid="label"]').text()).toBe('기본값')
})
```
#### Emits 검증
```ts
it('버튼 클릭 시 click 이벤트를 emit한다', async () => {
const wrapper = mount(ComponentName, { props: { id: '1' } })
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('click')?.[0]).toEqual(['1'])
})
```
#### 조건부 렌더링
```ts
it('isLoading이 true이면 스피너를 표시한다', () => {
const wrapper = mount(ComponentName, { props: { isLoading: true } })
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="content"]').exists()).toBe(false)
})
```
#### 비동기 / 스토어 Mock
```ts
import { createTestingPinia } from '@pinia/testing'
it('마운트 시 fetchProducts를 호출한다', async () => {
const wrapper = mount(ComponentName, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })]
}
})
const store = useProductStore()
expect(store.fetchProducts).toHaveBeenCalledOnce()
})
```
#### 슬롯
```ts
it('default 슬롯 콘텐츠를 렌더링한다', () => {
const wrapper = mount(ComponentName, {
slots: { default: '<span>슬롯 내용</span>' }
})
expect(wrapper.find('span').text()).toBe('슬롯 내용')
})
```
---
## 출력 형식
```
## 단위 테스트: <ComponentName>.spec.ts
### 테스트 케이스 목록
1. [렌더링] 기본 렌더링 성공
2. [Props] title prop 표시
3. [Emits] 버튼 클릭 시 emit
4. [조건부] isLoading 상태 처리
...
### 코드
\`\`\`ts
[전체 spec 파일]
\`\`\`
### 실행 방법
\`\`\`bash
npx vitest run <파일경로>
npx vitest --coverage # 커버리지 확인
\`\`\`
```

125
skills/markup-base/SKILL.md Normal file
View File

@@ -0,0 +1,125 @@
---
name: markup-base
description: |
화면 설명, 스크린샷, 텍스트 명세를 받아 시멘틱 HTML + Tailwind CSS 기반의
Nuxt SFC 마크업을 생성합니다. 팀 공통 컨벤션(7단계 클래스 순서, ARIA, 반응형)을 자동 적용합니다.
다음 상황에서 반드시 사용하세요:
- "이 화면 마크업해줘", "HTML 구조 만들어줘", "퍼블리싱해줘"
- 화면 설명이나 스크린샷을 받고 컴포넌트 골격을 잡을 때
- Figma 없이 텍스트 명세나 와이어프레임으로 마크업을 시작할 때
---
# 기본 마크업 가이드 (markup-base)
화면 명세 → 시멘틱 HTML + Tailwind → Nuxt `.vue` SFC 골격을 생성합니다.
## 언제 사용하는가
- Figma 디자인 없이 화면 명세/스크린샷으로 마크업을 시작할 때
- 신규 페이지/섹션의 기본 구조를 빠르게 잡을 때
- 기존 마크업을 팀 컨벤션에 맞게 리팩토링할 때
## 입력
- 화면 설명 (텍스트), 와이어프레임 이미지, 또는 스크린샷
- 반응형 분기점 요건 (없으면 Tailwind 기본값 사용: `sm` 640px / `md` 768px / `lg` 1024px)
- 접근성 요건 (없으면 WCAG 2.1 AA 기본 적용)
---
## 작업 순서
### Phase 1: 화면 구조 파악
1. 제공된 화면 명세 또는 이미지를 분석해 레이아웃 영역을 식별한다.
- 반복 블록 → 컴포넌트 후보 표시
- 상태가 있는 영역 (로딩, 에러, 빈 상태) 확인
2. 반응형 분기점을 파악한다.
- 모바일 우선(mobile-first) 여부 확인
- 중단점별 레이아웃 변화 정리
### Phase 2: 시멘틱 구조 설계
1. HTML 시멘틱 태그를 선택한다.
| 영역 | 권장 태그 |
|---|---|
| 페이지 헤더 | `<header>` |
| 주 내비게이션 | `<nav aria-label="...">` |
| 주 콘텐츠 | `<main>` |
| 사이드 | `<aside>` |
| 콘텐츠 섹션 | `<section aria-labelledby="...">` |
| 독립 콘텐츠 | `<article>` |
| 페이지 푸터 | `<footer>` |
2. 인터랙션 요소를 식별한다.
- 버튼: `<button type="button">` (폼 외부), `<button type="submit">` (폼 내부)
- 링크: `<a href="...">` (페이지 이동), `<button>` (동작 트리거)
### Phase 3: Tailwind 클래스 적용
클래스 순서는 아래 7단계를 따른다:
```
1. 레이아웃 : flex, grid, block, hidden
2. 크기 : w-*, h-*, max-w-*, min-h-*
3. 여백 : m-*, p-*, gap-*, space-*
4. 배경/보더 : bg-*, border-*, rounded-*, shadow-*
5. 타이포그래피 : text-*, font-*, leading-*, tracking-*
6. 색상 : text-{color}, fill-*, stroke-*
7. 상태/반응형 : hover:, focus:, sm:, md:, lg:
```
조건부 클래스는 `clsx` 또는 `cn` 유틸리티를 사용한다.
### Phase 4: ARIA 및 접근성 처리
- 아이콘 전용 버튼: `aria-label` 필수
- 모달/다이얼로그: `role="dialog" aria-modal="true" aria-labelledby="..."`
- 로딩 상태: `aria-live="polite"` 또는 `aria-busy="true"`
- 이미지: 의미 있는 이미지 `alt` 텍스트 / 장식용 `alt=""`
- 폼 레이블: `<label for="...">` 또는 `aria-label`
### Phase 5: Nuxt SFC 출력
```vue
<script setup lang="ts">
// Props / Emits / 로컬 상태만 포함
</script>
<template>
<!-- 시멘틱 구조 -->
</template>
```
- `<style>`은 Tailwind로 커버 불가한 경우에만 추가
- `scoped`보다 Tailwind 유틸리티 우선
---
## 출력 형식
```
## 마크업: <화면명>
### 구조 개요
- 레이아웃: [설명]
- 반응형 분기: [없음 / sm, md, lg 각각 설명]
- 컴포넌트 분리 후보: [목록]
### 코드
\`\`\`vue
<script setup lang="ts">
...
</script>
<template>
...
</template>
\`\`\`
### 추가 작업 필요 항목
- [ ] 이미지 alt 텍스트 확정 필요
- [ ] 컬러 토큰 디자인 시스템 확인 필요
```

BIN
skills/markup-edm/.DS_Store vendored Normal file

Binary file not shown.

310
skills/markup-edm/SKILL.md Normal file
View File

@@ -0,0 +1,310 @@
---
name: markup-edm
description: |
EDM(이메일 다이렉트 마케팅) HTML을 구현하는 전체 워크플로우 스킬.
Figma 디자인 → HTML table 마크업 → 아웃룩 호환 → 검수까지 단계별 가이드를 제공합니다.
다음 상황에서 반드시 사용하세요:
- "EDM 만들어줘", "이메일 템플릿 구현", "뉴스레터 HTML"
- "아웃룩에서 깨지는 이메일 수정", "이메일 HTML 마크업"
- Figma 디자인을 받고 이메일 HTML로 변환할 때
- "메일 발송용 HTML", "eDM 퍼블리싱", "HTML 이메일"
- 이메일 클라이언트 호환성 문제가 있을 때
---
# EDM HTML 구현 가이드
이메일 HTML은 일반 웹과 다른 세계입니다. 2000년대 테이블 코딩이 아직도 정답이며, Flexbox와 Grid는 쓸 수 없습니다. 이 스킬은 Figma 디자인에서 시작해 모든 이메일 클라이언트에서 깨지지 않는 HTML을 만드는 과정을 안내합니다.
## 워크플로우
```
1. Figma 디자인 파악 → 2. HTML 마크업 → 3. 아웃룩 호환 → 4. 검수
```
---
## Phase 1: Figma 디자인 파악
### Figma MCP 사용 가능 시
Claude Code에 Figma MCP가 설정되어 있다면 Figma URL로 직접 디자인 데이터를 읽을 수 있습니다. MCP가 연결되어 있는지 먼저 확인하고, 가능하다면 자동 추출을 시도하세요.
추출 가능한 속성:
- 컬러 HEX값 (RGBA → HEX 자동 변환)
- 폰트 패밀리, 사이즈(px), 굵기, 줄간격
- 레이아웃 치수: 너비, 높이, padding, 섹션 간격
- 이미지 에셋 URL (CDN 업로드 필요)
- CTA 링크 (레이어 설명 필드에서 추출)
### Figma MCP 없이 진행 시
사용자에게 다음 정보를 요청하거나 스크린샷으로 파악하세요.
**필수 확인 항목:**
- 전체 이메일 너비 (권장: **600px**)
- 각 섹션 배경색, 텍스트 색상 (HEX)
- 폰트: 패밀리, 사이즈(px), 굵기, 줄간격
- 이미지: 가로×세로(px)
- 여백: 섹션 간 간격, 좌우 패딩
- CTA 버튼: 크기, 색상, 텍스트, 링크 URL
- 푸터: 회사 정보, 수신거부 링크
---
## Phase 2: HTML table 마크업
### 절대 원칙
이메일 HTML에서 반드시 지켜야 하는 규칙들입니다. 이 규칙을 어기면 특정 클라이언트에서 레이아웃이 무너집니다:
| 규칙 | 이유 |
|------|------|
| `table`, `tr`, `td`만 레이아웃에 사용 | div는 Outlook 등에서 무시됨 |
| inline CSS 우선 | Gmail이 `<head>` style 태그를 제거함 |
| `width`/`height` 속성 필수 | CSS만으론 Outlook이 무시함 |
| `margin` 사용 금지 | 빈 `<tr>`행이나 `padding`으로 대체 |
| `padding` 개별 속성 사용 | 단축 속성(`padding: 10px 20px`)은 일부 클라이언트 미지원 |
| 모든 `<table>``cellpadding="0" cellspacing="0" border="0"` | 브라우저 기본 스타일 초기화 |
### 기본 템플릿
```html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="format-detection" content="telephone=no, date=no, address=no">
<title>이메일 제목</title>
<style type="text/css">
body { margin: 0; padding: 0; width: 100%; background-color: #f5f5f5; }
table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { display: block; border: 0; outline: none; text-decoration: none; }
/* 미디어쿼리는 여기서만 (Outlook은 무시하지만 Gmail/Apple Mail에서 적용) */
@media only screen and (max-width: 600px) {
.mobile-full { width: 100% !important; display: block !important; }
.mobile-padding { padding-left: 16px !important; padding-right: 16px !important; }
.mobile-center { text-align: center !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #f5f5f5;">
<!-- 외부 래퍼: 배경색과 수평 중앙 정렬 -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f5f5f5">
<tr>
<td align="center" style="padding-top: 20px; padding-bottom: 20px;">
<!-- 600px 컨테이너 -->
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="width: 600px; max-width: 100%; background-color: #ffffff;">
<!-- 헤더 -->
<tr>
<td style="padding-top: 0;">
<!-- 로고 이미지 등 -->
</td>
</tr>
<!-- 본문 -->
<tr>
<td style="padding-top: 30px; padding-bottom: 30px;
padding-left: 30px; padding-right: 30px;">
<!-- 메인 콘텐츠 -->
</td>
</tr>
<!-- 푸터 -->
<tr>
<td bgcolor="#f5f5f5"
style="background-color: #f5f5f5;
padding-top: 20px; padding-bottom: 20px;
padding-left: 20px; padding-right: 20px;
text-align: center;">
<!-- 회사 정보 + 수신거부 링크 (필수) -->
<p style="font-family: Arial, sans-serif; font-size: 12px;
color: #999999; margin: 0; line-height: 1.5;">
회사명 | 주소<br>
<a href="[수신거부URL]"
style="color: #999999; text-decoration: underline;">
수신거부
</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
```
### 안전한 폰트
웹폰트(`@font-face`, Google Fonts)는 대부분의 이메일 클라이언트에서 지원하지 않습니다. Pretendard, Noto Sans KR 같은 폰트를 Figma에서 사용했어도 이메일에서는 안전 폰트로 대체해야 합니다.
```css
/* 권장 스택 (한국어 이메일) */
font-family: -apple-system, Arial, 'Helvetica Neue', Helvetica, sans-serif;
/* Outlook 전용 (MSO 조건부 주석 내) */
font-family: Arial, sans-serif;
```
---
## Phase 3: 아웃룩 호환성
아웃룩 2007~2019는 Word 엔진으로 이메일을 렌더링해서 현대 CSS를 거의 무시합니다. MSO 조건부 주석으로 아웃룩과 그 외 클라이언트를 분리해서 처리하세요.
### MSO 조건부 주석
```html
<!--[if mso]>
<!-- 아웃룩에서만 렌더링 -->
<![endif]-->
<!--[if !mso]><!-->
<!-- 아웃룩 제외 클라이언트에서 렌더링 -->
<!--<![endif]-->
```
### 아웃룩이 무시하는 주요 속성
| CSS 속성 | 아웃룩 동작 | 대체 방법 |
|----------|-----------|---------|
| `background-image` | 미지원 | `<img>` 태그 직접 사용 |
| `border-radius` | 무시 | VML 사용 또는 이미지 버튼 |
| `margin` | 무시 | `padding` 또는 빈 `<tr>` 행 |
| `box-shadow` | 무시 | 포기 또는 이미지로 대체 |
| `@media query` | 2007/2010 미지원 | 테이블 고정폭으로 데스크톱 설계 |
### VML 버튼 (반드시 사용)
아웃룩에서 CSS 버튼은 배경색 없는 텍스트 링크로 표시됩니다. CTA 버튼은 항상 VML을 포함하세요:
```html
<div style="text-align: center;
padding-top: 20px; padding-bottom: 20px;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w="urn:schemas-microsoft-com:office:word"
href="https://example.com"
style="height: 44px; v-text-anchor: middle; width: 200px;"
arcsize="5%"
stroke="f"
fillcolor="#FF6B6B">
<w:anchorlock/>
<center style="color: #ffffff; font-family: Arial, sans-serif;
font-size: 16px; font-weight: bold;">
지금 확인하기
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="https://example.com"
style="background-color: #FF6B6B;
color: #ffffff;
display: inline-block;
padding-top: 12px; padding-bottom: 12px;
padding-left: 30px; padding-right: 30px;
text-decoration: none;
border-radius: 5px;
font-family: Arial, sans-serif;
font-size: 16px;
font-weight: bold;">
지금 확인하기
</a>
<!--<![endif]-->
</div>
```
### 이미지 처리
이미지 차단 시에도 레이아웃이 깨지지 않도록 `alt` 텍스트와 배경색을 함께 지정하세요:
```html
<td bgcolor="#FF6B6B" style="background-color: #FF6B6B;">
<img src="https://cdn.example.com/banner.jpg"
alt="7월 여름 세일 최대 50% 할인"
width="600"
height="300"
style="display: block; width: 100%; max-width: 600px;
height: auto; border: 0;">
</td>
```
이미지는 반드시 `https://` CDN 절대 경로를 사용하세요. 로컬 경로나 상대 경로는 이메일에서 작동하지 않습니다.
---
## Phase 4: 검수 체크리스트
### 코드 구조 (필수)
- [ ] 모든 `<table>``cellpadding="0" cellspacing="0" border="0"`
- [ ] 모든 `<img>``width`, `height`, `alt` 속성
- [ ] `margin` 미사용 (padding 또는 빈 `<tr>` 행으로 대체)
- [ ] `padding` 단축 속성 제거 (개별 속성 사용)
- [ ] CTA 버튼에 VML 코드 포함
- [ ] 이미지 `src`가 HTTPS 절대 URL
### 콘텐츠 (필수)
- [ ] 푸터에 수신거부 링크 포함
- [ ] 모든 링크 href 유효성 확인
- [ ] 이미지 alt 텍스트 의미있게 작성 (장식용이면 `alt=""`)
### Figma 디자인 대비 검수
- [ ] 전체 너비 600px
- [ ] 색상 HEX값 일치
- [ ] 폰트 사이즈, 굵기 일치
- [ ] 버튼 크기, 색상 일치
- [ ] 섹션 간 여백 일치
### 테스트 도구
| 도구 | 용도 | 비용 |
|------|------|------|
| [Litmus](https://www.litmus.com) | 100+ 클라이언트 렌더링 미리보기 | 유료 |
| [Email on Acid](https://www.emailonacid.com) | 크로스 클라이언트 + 접근성 감사 | 유료 |
| [Mailtrap](https://mailtrap.io) | 개발 환경 샌드박스, 스팸 점수 | 무료 플랜 |
| [SpamTest.io](https://spamtest.io/) | 스팸 점수, SPF/DKIM/DMARC 확인 | 무료 |
**최소 테스트 클라이언트:** Gmail 웹, Outlook (Windows), Apple Mail, 모바일 Gmail
---
## 2컬럼 레이아웃 예시
```html
<!-- 데스크톱: 2열 | 모바일: 스택 -->
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="width: 600px;">
<tr>
<td width="280" valign="top"
style="width: 280px; padding-right: 20px;"
class="mobile-full">
<!-- 왼쪽 -->
</td>
<td width="280" valign="top"
style="width: 280px; padding-left: 20px;"
class="mobile-full">
<!-- 오른쪽 -->
</td>
</tr>
</table>
```
---
## 참고 자료
상세 내용은 references 폴더를 참조하세요:
- `references/html-patterns.md` — 헤더/푸터/버튼/이미지 완성 코드 패턴
- `references/verification-checklist.md` — 전체 검수 체크리스트 (시각적/기능/스팸)

View File

@@ -0,0 +1,24 @@
# Example Asset File
This placeholder represents where asset files would be stored.
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
Asset files are NOT intended to be loaded into context, but rather used within
the output Claude produces.
Example asset files from other skills:
- Brand guidelines: logo.png, slides_template.pptx
- Frontend builder: hello-world/ directory with HTML/React boilerplate
- Typography: custom-font.ttf, font-family.woff2
- Data: sample_data.csv, test_dataset.json
## Common Asset Types
- Templates: .pptx, .docx, boilerplate directories
- Images: .png, .jpg, .svg, .gif
- Fonts: .ttf, .otf, .woff, .woff2
- Boilerplate code: Project directories, starter files
- Icons: .ico, .svg
- Data files: .csv, .json, .xml, .yaml
Note: This is a text placeholder. Actual assets can be any file type.

View File

@@ -0,0 +1,34 @@
# Reference Documentation for Edm Email Html
This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.
Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples
## When Reference Docs Are Useful
Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases
## Structure Suggestions
### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits
### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices

View File

@@ -0,0 +1,327 @@
# EDM HTML 코드 패턴 모음
---
## 1컬럼 레이아웃 (전체 템플릿)
```html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="format-detection" content="telephone=no, date=no, address=no">
<title>이메일 제목</title>
<!--[if mso]>
<style type="text/css">
body, table, td, p, a { font-family: Arial, sans-serif !important; }
</style>
<![endif]-->
<style type="text/css">
body { margin: 0; padding: 0; width: 100%; background-color: #f5f5f5; }
table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { display: block; border: 0; outline: none; text-decoration: none; }
a { color: inherit; }
@media only screen and (max-width: 600px) {
.container { width: 100% !important; }
.mobile-full { width: 100% !important; display: block !important; }
.mobile-padding { padding-left: 16px !important; padding-right: 16px !important; }
.mobile-center { text-align: center !important; }
.mobile-img { width: 100% !important; height: auto !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #f5f5f5;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f5f5f5">
<tr>
<td align="center" style="padding-top: 20px; padding-bottom: 20px;">
<table width="600" cellpadding="0" cellspacing="0" border="0"
class="container"
style="width: 600px; max-width: 100%; background-color: #ffffff;">
<!-- 헤더 -->
<tr>
<td align="center"
style="padding-top: 24px; padding-bottom: 24px;
padding-left: 30px; padding-right: 30px;
border-bottom: 1px solid #e5e7eb;">
<img src="https://cdn.example.com/logo.png"
alt="회사 로고" width="120" height="40"
style="display: block; border: 0;">
</td>
</tr>
<!-- 히어로 이미지 -->
<tr>
<td style="padding: 0; line-height: 0;">
<img src="https://cdn.example.com/hero.jpg"
alt="이벤트 배너"
width="600" height="280"
class="mobile-img"
style="display: block; width: 100%; max-width: 600px;
height: auto; border: 0;">
</td>
</tr>
<!-- 본문 -->
<tr>
<td style="padding-top: 32px; padding-bottom: 32px;
padding-left: 32px; padding-right: 32px;"
class="mobile-padding">
<h1 style="font-family: Arial, sans-serif;
font-size: 24px; font-weight: bold;
color: #111827; line-height: 1.3;
margin: 0 0 16px 0;">
이메일 제목이 여기 들어갑니다
</h1>
<p style="font-family: Arial, sans-serif;
font-size: 15px; color: #374151;
line-height: 1.7;
margin: 0 0 24px 0;">
본문 내용이 여기 들어갑니다. 가독성을 위해 line-height를
1.5 이상으로 설정하는 것이 좋습니다.
</p>
<!-- CTA 버튼 -->
<div style="text-align: center;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w="urn:schemas-microsoft-com:office:word"
href="https://example.com"
style="height: 48px; v-text-anchor: middle; width: 200px;"
arcsize="8%" stroke="f" fillcolor="#1a56db">
<w:anchorlock/>
<center style="color: #ffffff; font-family: Arial, sans-serif;
font-size: 16px; font-weight: bold;">
자세히 보기
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="https://example.com"
style="background-color: #1a56db; color: #ffffff;
display: inline-block;
padding-top: 14px; padding-bottom: 14px;
padding-left: 32px; padding-right: 32px;
text-decoration: none; border-radius: 6px;
font-family: Arial, sans-serif;
font-size: 16px; font-weight: bold;">
자세히 보기
</a>
<!--<![endif]-->
</div>
</td>
</tr>
<!-- 푸터 -->
<tr>
<td bgcolor="#f9fafb"
style="background-color: #f9fafb;
border-top: 1px solid #e5e7eb;
padding-top: 24px; padding-bottom: 24px;
padding-left: 32px; padding-right: 32px;
text-align: center;">
<p style="font-family: Arial, sans-serif; font-size: 12px;
color: #9ca3af; line-height: 1.6; margin: 0 0 8px 0;">
<strong>회사명</strong> | 서울시 강남구 테헤란로 123
</p>
<p style="font-family: Arial, sans-serif; font-size: 12px;
color: #9ca3af; line-height: 1.6; margin: 0;">
<a href="https://example.com/unsubscribe"
style="color: #9ca3af; text-decoration: underline;">
수신거부
</a>
&nbsp;|&nbsp;
<a href="https://example.com/privacy"
style="color: #9ca3af; text-decoration: underline;">
개인정보처리방침
</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
```
---
## 2컬럼 이미지 + 텍스트
```html
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="width: 600px;">
<tr>
<!-- 이미지 열 (40%) -->
<td width="220" valign="top"
style="width: 220px; padding-right: 0;"
class="mobile-full">
<img src="https://cdn.example.com/product.jpg"
alt="상품명" width="220" height="220"
class="mobile-img"
style="display: block; width: 100%; height: auto; border: 0;">
</td>
<!-- 간격 -->
<td width="20" style="width: 20px; min-width: 20px;">&nbsp;</td>
<!-- 텍스트 열 (60%) -->
<td width="360" valign="top"
style="width: 360px; padding-top: 8px;"
class="mobile-full mobile-padding">
<h2 style="font-family: Arial, sans-serif;
font-size: 18px; font-weight: bold;
color: #111827; margin: 0 0 8px 0;
line-height: 1.3;">
상품명
</h2>
<p style="font-family: Arial, sans-serif; font-size: 14px;
color: #6b7280; line-height: 1.6;
margin: 0 0 16px 0;">
상품 설명이 들어갑니다. 간결하게 핵심만 작성하세요.
</p>
<p style="font-family: Arial, sans-serif; font-size: 20px;
font-weight: bold; color: #ef4444;
margin: 0 0 16px 0;">
₩29,900
</p>
<a href="https://example.com/product"
style="background-color: #111827; color: #ffffff;
display: inline-block;
padding-top: 10px; padding-bottom: 10px;
padding-left: 20px; padding-right: 20px;
text-decoration: none; border-radius: 4px;
font-family: Arial, sans-serif;
font-size: 13px; font-weight: bold;">
구매하기
</a>
</td>
</tr>
</table>
```
---
## 헤더 배너 (이미지 기반)
이미지가 차단됐을 때도 배경색이 보이도록 `bgcolor` 속성을 함께 지정합니다:
```html
<table width="600" cellpadding="0" cellspacing="0" border="0"
style="width: 600px;">
<tr>
<td bgcolor="#1a56db" style="background-color: #1a56db; line-height: 0; padding: 0;">
<img src="https://cdn.example.com/header-banner.jpg"
alt="여름 세일 최대 70% 할인"
width="600" height="240"
style="display: block; width: 100%; max-width: 600px;
height: auto; border: 0;">
</td>
</tr>
</table>
```
---
## 섹션 구분선
```html
<!-- 섹션 간 여백 -->
<tr>
<td height="32" style="height: 32px; line-height: 32px;">&nbsp;</td>
</tr>
<!-- 수평선 -->
<tr>
<td style="padding-left: 32px; padding-right: 32px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td height="1" bgcolor="#e5e7eb"
style="height: 1px; background-color: #e5e7eb; line-height: 1px;">
</td>
</tr>
</table>
</td>
</tr>
```
---
## 아웃라인(외곽선) 버튼
```html
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w="urn:schemas-microsoft-com:office:word"
href="https://example.com"
style="height: 44px; v-text-anchor: middle; width: 180px;"
arcsize="5%"
stroke="t"
strokeweight="2px"
strokecolor="#1a56db"
fillcolor="#ffffff">
<w:anchorlock/>
<center style="color: #1a56db; font-family: Arial, sans-serif;
font-size: 14px; font-weight: bold;">
더 알아보기
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a href="https://example.com"
style="background-color: #ffffff;
color: #1a56db;
display: inline-block;
padding-top: 12px; padding-bottom: 12px;
padding-left: 24px; padding-right: 24px;
text-decoration: none;
border: 2px solid #1a56db;
border-radius: 5px;
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: bold;">
더 알아보기
</a>
<!--<![endif]-->
```
---
## 소셜 아이콘 행
```html
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="padding-right: 8px;">
<a href="https://instagram.com/example" style="text-decoration: none;">
<img src="https://cdn.example.com/icon-instagram.png"
alt="Instagram" width="32" height="32"
style="display: block; border: 0;">
</a>
</td>
<td style="padding-right: 8px;">
<a href="https://facebook.com/example" style="text-decoration: none;">
<img src="https://cdn.example.com/icon-facebook.png"
alt="Facebook" width="32" height="32"
style="display: block; border: 0;">
</a>
</td>
<td>
<a href="https://youtube.com/example" style="text-decoration: none;">
<img src="https://cdn.example.com/icon-youtube.png"
alt="YouTube" width="32" height="32"
style="display: block; border: 0;">
</a>
</td>
</tr>
</table>
```

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""
Example helper script for edm-email-html
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for edm-email-html")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,107 @@
---
name: markup-figma
description: |
Figma URL 또는 Figma MCP로 추출한 디자인 데이터를 받아
Nuxt SFC + Tailwind CSS 마크업으로 자동 변환합니다.
다음 상황에서 반드시 사용하세요:
- "이 Figma 마크업해줘", "Figma 디자인 컴포넌트로 만들어줘"
- Figma URL 또는 링크를 받고 HTML/Vue 구현을 요청받았을 때
- 피그마 시안 기반 반응형 마크업이 필요할 때
---
# Figma → 마크업 변환 (markup-figma)
Figma 디자인 데이터 → Nuxt SFC + Tailwind 마크업 자동 변환.
## 언제 사용하는가
- 디자이너로부터 Figma URL을 받아 퍼블리싱을 시작할 때
- Figma MCP가 Claude Code에 연결되어 있을 때
- Figma 없이 스크린샷/Export 이미지로 구현할 때
## 입력
- Figma 프레임 URL 또는 노드 ID
- (선택) 브레이크포인트 요건, 이미지 에셋 CDN 경로
---
## 작업 순서
### Phase 1: Figma 데이터 추출
#### Figma MCP 사용 가능 시
```
Figma URL → MCP로 레이어 트리, 스타일, 치수 자동 추출
```
추출 항목:
- 컬러 HEX값 (RGBA → HEX 변환)
- 폰트: 패밀리, 사이즈(px), weight, 줄간격
- 레이아웃: width/height, padding, gap
- 컴포넌트 이름 → Vue 컴포넌트 이름 매핑 (`PascalCase`)
- 이미지 에셋 URL
#### MCP 없이 진행 시
사용자에게 아래 정보를 요청한다:
- 섹션별 스크린샷 (모바일 / 데스크톱 분리)
- 컬러 HEX, 폰트 스펙, 여백(px)
- CTA 링크 및 버튼 텍스트
### Phase 2: Tailwind 매핑
| Figma 값 | Tailwind 클래스 예시 |
|---|---|
| `width: 320px` | `w-80` 또는 `w-[320px]` |
| `padding: 16px 24px` | `py-4 px-6` |
| `gap: 12px` | `gap-3` |
| `font-size: 14px` | `text-sm` |
| `border-radius: 8px` | `rounded-lg` |
| `color: #1A1A1A` | `text-[#1A1A1A]` 또는 디자인 토큰 |
- Tailwind 디자인 토큰(`tailwind.config.ts`)이 있으면 임의값 대신 토큰 사용
- 임의값(`w-[123px]`)은 디자인 시스템에 없는 값에만 사용
### Phase 3: 컴포넌트 구조 결정
Figma 컴포넌트 → Vue 파일 매핑:
- Figma 상위 프레임 → 페이지 레이아웃 (`pages/` 또는 `layouts/`)
- Figma Instance → `components/` 파일
- 반복 컴포넌트 → Props로 추상화
### Phase 4: SFC 코드 생성
`markup-base` Phase 3~5 동일 적용 (클래스 순서, ARIA, SFC 구조).
반응형 처리:
- Figma Mobile 프레임 → base 클래스
- Figma Desktop 프레임 → `lg:` 접두사
### Phase 5: 검수 체크리스트
- [ ] 폰트 스펙 일치 여부 (px → rem 변환 확인)
- [ ] 이미지 `alt` 텍스트 작성 여부
- [ ] 인터랙션 요소 ARIA 속성 확인
- [ ] Tailwind 클래스 7단계 순서 준수
---
## 출력 형식
```
## Figma 마크업: <프레임명>
### 추출된 디자인 정보
- 컬러: [HEX 목록]
- 폰트: [패밀리 / 사이즈 목록]
- 브레이크포인트: [mobile / desktop 치수]
### 코드
\`\`\`vue
...
\`\`\`
### 미확인 항목
- [ ] [에셋 URL, 링크 등 확정 필요 항목]
```

View File

@@ -0,0 +1,138 @@
---
name: markup-promotion
description: |
프로모션/랜딩 페이지용 표준 마크업을 생성합니다.
캠페인 유형별 섹션 템플릿(히어로, 특징, CTA, 이벤트 일정 등)을 제공하고
반응형·접근성·SEO 메타를 일관되게 적용합니다.
다음 상황에서 반드시 사용하세요:
- "프로모션 페이지 만들어줘", "랜딩 페이지 마크업해줘"
- "이벤트 페이지 퍼블리싱", "캠페인 HTML 만들어줘"
- 반복적인 프로모션 구조를 빠르게 구현해야 할 때
---
# 프로모션 마크업 가이드 (markup-promotion)
캠페인 / 이벤트 / 랜딩 페이지의 표준 섹션 구조를 자동 생성합니다.
## 언제 사용하는가
- 신규 프로모션/이벤트 랜딩 페이지를 제작할 때
- 기존 캠페인 구조를 재사용해 빠르게 개발할 때
- 팀 표준 프로모션 레이아웃이 필요할 때
## 입력
- 캠페인 명칭, 기간, 주요 메시지
- 포함할 섹션 목록 (없으면 기본 구성으로 제안)
- 이미지/에셋 경로 또는 플레이스홀더 여부
- 다국어 지원 여부
---
## 작업 순서
### Phase 1: 캠페인 유형 분석
1. 제공된 정보에서 아래 유형을 판별한다.
| 유형 | 특징 |
|---|---|
| 이벤트 | 기간, 참여 조건, CTA 버튼 |
| 상품 출시 | 제품 특징, 스펙, 구매 링크 |
| 신규 서비스 | 혜택 강조, 가입 유도 |
| 콘텐츠/미디어 | 영상/갤러리 중심, SNS 공유 |
2. 필요한 섹션을 제안한다. 사용자가 별도 지정하지 않으면 기본 구성 사용.
**기본 섹션 구성:**
```
Hero → 특징/혜택 → 이벤트 규칙/일정 → FAQ → CTA → Footer
```
### Phase 2: 섹션별 마크업 생성
각 섹션은 독립 Vue 컴포넌트로 작성한다.
#### Hero 섹션
```vue
<section class="relative w-full [height] bg-[color] ...">
<div class="...">
<h1 class="...">{{ title }}</h1>
<p class="...">{{ description }}</p>
<a :href="ctaLink" class="...">{{ ctaText }}</a>
</div>
</section>
```
#### 특징/혜택 섹션
```vue
<section aria-labelledby="features-heading">
<h2 id="features-heading" class="...">{{ heading }}</h2>
<ul class="grid grid-cols-1 md:grid-cols-3 gap-6">
<li v-for="item in features" :key="item.id" class="...">
...
</li>
</ul>
</section>
```
#### CTA 섹션
- 버튼 타입: `<a>` (외부 링크) 또는 `<NuxtLink>` (내부 라우팅)
- `aria-label`로 버튼 목적 명시
### Phase 3: 반응형 + SEO + 접근성
**반응형**: 모바일 우선 (base → `md:``lg:`)
**SEO 메타** (Nuxt `useSeoMeta`):
```ts
useSeoMeta({
title: '캠페인명 | 브랜드명',
description: '캠페인 한 줄 설명',
ogTitle: '...',
ogImage: '이미지 URL',
})
```
**접근성**:
- 이미지 `alt` 텍스트 (장식용이면 `alt=""`)
- 섹션 제목 계층 (`h1``h2``h3`)
- 카운트다운 타이머: `aria-live="polite"`
### Phase 4: 다국어 처리
다국어 지원 시 i18n 키로 텍스트 분리:
```vue
<h1>{{ $t('HERO-title') }}</h1>
```
키 명명은 `plan-translation-generator` 스킬의 카테고리 가이드 참조.
---
## 출력 형식
```
## 프로모션 마크업: <캠페인명>
### 섹션 구성
1. Hero
2. 특징/혜택
3. CTA
### 파일 구조
pages/promotion/<slug>.vue
components/promotion/
├── PromoHero.vue
├── PromoFeatures.vue
└── PromoCta.vue
### 코드
[각 컴포넌트 코드]
### 체크리스트
- [ ] 이미지 alt 텍스트 확정
- [ ] CTA 링크 URL 확정
- [ ] useSeoMeta 값 확정
```

View File

@@ -0,0 +1,170 @@
---
name: plan-analyzer
description: |
PPT 기획서를 분석하여 요구사항 명세서를 자동 생성합니다.
Nuxt pages 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록,
화면 전환 플로우(Mermaid)를 구조화된 마크다운 문서로 출력합니다.
다음 상황에서 반드시 사용하세요:
- "기획서 분석해줘", "PPT 파싱", "요구사항 정리해줘"
- "라우팅 구조 뽑아줘", "컴포넌트 트리 만들어줘"
- "API 목록 정리", "화면 플로우 그려줘"
- `.pptx` 파일 경로를 제공받고 개발 요구사항 추출을 요청받았을 때
permissions:
allow:
- Bash(python3 -c "import pptx"*)
- Bash(python3*extract_pptx.py*)
---
# 요구사항 분석기 (Requirement Analyzer)
이 skill 은 PPT 기획서(`.pptx`)를 파싱하여 팀 전체가 동일한 기반으로 개발에 착수할 수 있는
구조화된 요구사항 명세서를 자동으로 생성합니다.
## 언제 사용하는가
- 기획팀에서 PPT 기획서를 전달받았을 때
- 개발 착수 전 화면 목록 / 라우팅 / API 목록을 정리해야 할 때
- 화면 전환 플로우를 다이어그램으로 시각화해야 할 때
---
## 작업 순서
### Phase 1: 파일 확인 및 추출
1. **파일 경로 확인**
- 사용자가 PPTX 파일 경로를 제공했는지 확인한다.
- 미제공 시: 파일 경로를 먼저 요청한다.
2. **의존성 확인**
스킬 디렉토리 기준 스크립트를 실행하기 전, `python-pptx` 설치 여부를 확인한다.
```bash
python3 -c "import pptx" 2>/dev/null && echo "OK" || echo "MISSING"
```
- `MISSING` 출력 시: 아래 메시지를 사용자에게 안내한다.
```
⚠️ python-pptx 패키지가 필요합니다.
설치 명령어: pip3 install python-pptx
또는 자동 설치: python3 scripts/extract_pptx.py --auto-install <파일경로>
```
3. **PPTX 추출 실행**
```bash
python3 <skill-dir>/scripts/extract_pptx.py --extract-images "<pptx경로>"
```
- `--extract-images`: 슬라이드 이미지(PNG)를 임시 디렉토리에 추출 → 시각 분석에 활용
- 출력: JSON 형태의 슬라이드 데이터 (슬라이드별 제목, 텍스트, 노트, 도형, 테이블, 이미지 경로)
4. **결과 확인**
- JSON 파싱 성공 여부 확인
- 추출된 이미지가 있으면 각 슬라이드별로 순서대로 열어 와이어프레임/목업 시각 분석
---
### Phase 2: 내용 분석
추출된 JSON 데이터와 슬라이드 이미지를 기반으로 아래 항목을 분석한다.
#### 화면(페이지) 식별
- 슬라이드 제목에서 "화면", "페이지", 경로(`/`로 시작), URL 패턴 찾기
- 와이어프레임 이미지에서 GNB, 레이아웃 영역 확인
- 중복 레이아웃 슬라이드(상세 설명용) vs 독립 화면 구분
#### UI 컴포넌트 패턴 식별
- 반복 등장하는 도형 레이블 (예: "GNB", "Footer", "Card", "Modal", "Tab")
- 슬라이드 간 동일한 레이아웃 구조 → 공통 컴포넌트 후보
- 이미지에서 시각적으로 동일한 UI 블록
#### 네비게이션 플로우 추론
- 화살표/커넥터 도형으로 연결된 화면 간 전환
- "클릭 시", "탭하면", "→" 등의 텍스트 주석
- 슬라이드 번호 순서 + 제목 키워드로 흐름 유추
#### API 엔드포인트 추출
- 테이블에 "Method", "URL", "API", "Endpoint" 컬럼이 있는 경우
- 발표자 노트에 `GET /api/...`, `POST /api/...` 형태의 텍스트
- 텍스트 박스 내 `http`, `/api`, `fetch`, `axios` 언급
---
### Phase 3: 명세서 생성
아래 7개 섹션으로 구성된 마크다운 문서를 작성한다.
추론 기반 정보는 반드시 `(추정)` 또는 `<확인 필요>` 를 표시한다.
**출력 섹션:**
```
1. 화면 목록 (Pages) — 테이블 형식
2. Nuxt 라우팅 구조 — pages/ 파일 트리
3. 컴포넌트 트리 — components/ 파일 트리 + Props 상세 테이블
4. API 엔드포인트 목록 — 테이블 형식
5. 화면 전환 플로우 — Mermaid flowchart TD
6. 공통 레이아웃 — 레이아웃별 적용 화면 테이블
7. 추가 참고사항 — 미확정 항목 / 확인 필요 목록
```
출력 형식 상세는 `references/output-template.md` 를 참조한다.
---
### Phase 4: 사용자 확인 및 저장
1. **미리보기 제공**
생성된 명세서 전체를 출력하여 사용자가 검토할 수 있도록 한다.
2. **저장 경로 확인**
```
📄 아래 경로에 요구사항 명세서를 저장하려고 합니다:
docs/requirements/<기획서파일명>.md
다른 경로를 원하시면 알려주세요. 진행할까요? (y/n)
```
3. **파일 저장**
- 사용자 승인 후 파일을 저장한다.
- `docs/requirements/` 디렉토리가 없으면 생성한다.
- 저장 완료 후 아래 메시지를 출력한다:
```
✅ 요구사항 명세서 저장 완료
docs/requirements/<파일명>.md
다음 단계:
1) <확인 필요> 항목을 기획자에게 확인하세요.
2) 컴포넌트/API 목록을 개발 태스크로 분해하세요.
3) git add docs/requirements/ && git commit -m "docs: 요구사항 명세서 추가"
```
---
## 출력 형식
`references/output-template.md` 에서 전체 마크다운 구조와 예시를 확인하세요.
### Mermaid 플로우차트 작성 규칙
```mermaid
flowchart TD
A[메인] --> B[이벤트 목록]
B --> C[이벤트 상세]
C --> D{사전등록 완료?}
D -- 완료 --> E[완료 모달]
D -- 미완료 --> F[사전등록 폼]
```
- 화면 노드: `[화면명]` (사각형)
- 조건 분기: `{조건}` (다이아몬드)
- 모달/팝업: `([모달명])` (타원)
- 화살표 레이블: 클릭 대상 버튼명 또는 조건
---
## 주의사항
- **기존 파일 수정 금지**: 프로젝트의 기존 소스 파일은 절대 수정하지 않는다. 명세서 파일만 새로 생성한다.
- **추측 표시 필수**: 기획서에 명시되지 않은 정보를 추론할 경우 반드시 `(추정)` 을 표시한다.
- **확인 필요 목록**: 확신할 수 없는 항목은 섹션 7에 `<확인 필요>` 로 모아 명시한다.
- **이미지 heavy 기획서**: 텍스트가 적고 이미지 위주인 기획서는 `--extract-images` 옵션이 필수다.
- **대용량 PPTX**: 슬라이드가 50장 이상이면 `--slides 1-20` 으로 범위를 지정하여 순차 처리한다.
- **커밋 금지**: 파일 저장 후 `git commit` 은 사용자 명시 요청이 없으면 실행하지 않는다.

View File

@@ -0,0 +1,159 @@
# 요구사항 명세서 출력 템플릿
이 파일은 `requirement-analyzer` 스킬이 생성하는 마크다운 문서의 형식 예시입니다.
실제 출력 시 각 항목을 기획서 내용으로 채우고, 불확실한 항목은 `(추정)` 또는 `<확인 필요>` 로 표시합니다.
---
```markdown
# 요구사항 명세서: <프로젝트명>
> 기획서: `<파일명.pptx>` | 생성일: <YYYY-MM-DD> | 총 슬라이드: <N>장
> 담당 기획자: <확인 필요> | 개발 일정: <확인 필요>
---
## 1. 화면 목록 (Pages)
| # | 화면명 | 라우팅 경로 | 기획서 슬라이드 | 비고 |
|---|--------|------------|--------------|------|
| 1 | 메인 (랜딩) | `/` | 3-5 | 사전등록 CTA 포함 |
| 2 | 사전등록 | `/register` | 6-9 | 폼 입력 화면 |
| 3 | 등록 완료 | `/register/complete` | 10 | 완료 안내 화면 |
| 4 | 이벤트 소개 | `/about` | 11-13 | 스크롤 단일 페이지 |
| 5 | 공지사항 | `/notice` | 14 | (추정) 목록 페이지 |
| 6 | 공지사항 상세 | `/notice/[id]` | 14 | (추정) 동적 라우트 |
---
## 2. Nuxt 라우팅 구조
```
pages/
├── index.vue # 메인 (랜딩)
├── register/
│ ├── index.vue # 사전등록 폼
│ └── complete.vue # 등록 완료
├── about.vue # 이벤트 소개
└── notice/
├── index.vue # 공지사항 목록
└── [id].vue # 공지사항 상세 (추정)
```
**레이아웃:**
```
layouts/
├── default.vue # GNB + Footer 포함 (전체 화면)
└── blank.vue # GNB/Footer 없음 (모달, 완료 화면 등)
```
---
## 3. 컴포넌트 트리
```
components/
├── common/
│ ├── AppHeader.vue # GNB / 상단 네비게이션
│ ├── AppFooter.vue # 하단 푸터
│ └── AppLogo.vue # 로고 (추정)
├── register/
│ ├── RegisterForm.vue # 사전등록 입력 폼
│ ├── RegisterComplete.vue # 완료 화면 내용
│ └── TermsModal.vue # 약관 모달 (추정)
├── notice/
│ ├── NoticeList.vue # 공지사항 목록 (추정)
│ └── NoticeItem.vue # 공지사항 카드 (추정)
└── ui/
├── BaseButton.vue # 공통 버튼
├── BaseModal.vue # 공통 모달 래퍼 (추정)
└── BaseInput.vue # 공통 입력 필드 (추정)
```
### 컴포넌트 상세
| 컴포넌트 | 사용 화면 | Props (추정) | Emits (추정) | 비고 |
|----------|----------|-------------|-------------|------|
| `AppHeader` | 전체 | `transparent?: boolean` | — | 메인에서 투명 배경 |
| `RegisterForm` | 사전등록 | — | `submit` | 폼 상태 내부 관리 |
| `BaseButton` | 전체 | `variant`, `size`, `disabled` | `click` | Tailwind 기반 |
| `TermsModal` | 사전등록 | `visible` | `close`, `agree` | <확인 필요> |
---
## 4. API 엔드포인트 목록
| # | Method | Endpoint | 설명 | 사용 화면 | 비고 |
|---|--------|----------|------|----------|------|
| 1 | POST | `/api/register` | 사전등록 제출 | 사전등록 | 폼 데이터 전송 |
| 2 | GET | `/api/notices` | 공지사항 목록 | 공지사항 | 페이지네이션 (추정) |
| 3 | GET | `/api/notices/:id` | 공지사항 상세 | 공지사항 상세 | (추정) |
| 4 | GET | `/api/event/info` | 이벤트 기본 정보 | 메인, 소개 | <확인 필요> |
> **참고**: API 경로는 기획서 노트/테이블에서 추출했습니다. 백엔드 팀과 확인이 필요합니다.
---
## 5. 화면 전환 플로우
```mermaid
flowchart TD
A[메인 랜딩] --> |사전등록 버튼 클릭| B[사전등록 폼]
A --> |공지사항 메뉴| C[공지사항 목록]
A --> |이벤트 소개 메뉴| D[이벤트 소개]
B --> |약관 보기 클릭| E([약관 모달])
E --> |닫기| B
B --> |제출 성공| F[등록 완료]
B --> |제출 실패| B
C --> |항목 클릭| G[공지사항 상세]
G --> |목록으로| C
F --> |메인으로| A
```
---
## 6. 공통 레이아웃
| 레이아웃 | 적용 화면 | 포함 컴포넌트 | 비고 |
|----------|----------|-------------|------|
| `default` | 메인, 이벤트 소개, 공지사항 | AppHeader, AppFooter | GNB 표시 |
| `blank` | 등록 완료 | — | GNB/Footer 없음 (추정) |
---
## 7. 추가 참고사항
### 확인 필요 항목
- [ ] 약관 모달 내용 및 동의 필수 항목 (슬라이드 8 — 상세 미기재)
- [ ] 공지사항 페이지네이션 방식 (무한스크롤 vs 페이지 버튼)
- [ ] 사전등록 중복 참여 처리 방식 (동일 이메일 재등록 시)
- [ ] `/api/event/info` 엔드포인트 실제 경로 및 응답 스키마
- [ ] 다국어 지원 여부 (한국어 전용 vs 영어 병행)
### 기획서에서 발견된 특이사항
- 슬라이드 7: "등록 마감 후 버튼 비활성화" — 마감일 기준 상태 분기 필요
- 슬라이드 12: 이벤트 소개 섹션 내 동영상 플레이어 포함 — Video 컴포넌트 추가 검토
- 슬라이드 14: 공지사항 페이지 기획 미완성으로 표시됨 (`<확인 필요>`)
### 미정 항목
| 항목 | 현재 상태 | 담당 |
|------|----------|------|
| OG 태그 / SNS 공유 설정 | 미기재 | <확인 필요> |
| GA / 트래킹 이벤트 정의 | 미기재 | <확인 필요> |
| 브라우저 지원 범위 | 미기재 | <확인 필요> |
```
---
## 활용 팁
- `<확인 필요>` 항목은 기획자와 킥오프 미팅 전 체크리스트로 활용하세요.
- `(추정)` 항목은 개발 착수 전 기획서를 재확인하고 제거하세요.
- API 엔드포인트 목록은 백엔드 팀과 함께 검토하여 최종 확정하세요.
- Mermaid 플로우차트는 PR 설명이나 Confluence 페이지에 바로 붙여 사용할 수 있습니다.

View File

@@ -0,0 +1,315 @@
"""
PPT 기획서 추출 스크립트 (extract_pptx.py)
========================================
PPTX 파일을 파싱하여 Claude AI가 분석할 수 있는 JSON 구조로 변환합니다.
의미 분석(화면 매핑, API 추출, 플로우 추론)은 Claude AI가 담당합니다.
사용법:
python extract_pptx.py <pptx경로> [옵션]
옵션:
--extract-images 슬라이드 이미지를 PNG로 추출 (--output-dir 로 저장 경로 지정)
--output-dir <dir> 이미지 추출 디렉토리 (기본: /tmp/pptx_<파일명>/)
--slides <범위> 처리할 슬라이드 범위 (예: 1-10, 5, 3-7,10-12)
--auto-install python-pptx 자동 설치 후 실행
--pretty JSON 출력 시 들여쓰기 적용
출력:
stdout 에 JSON 데이터 출력
"""
import sys
import os
import json
import argparse
import tempfile
import re
# ─────────────────────────────────────────────
# 의존성 확인 및 자동 설치
# ─────────────────────────────────────────────
def _ensure_pptx(auto_install: bool) -> None:
"""python-pptx 설치 여부 확인. 미설치 시 안내 또는 자동 설치."""
try:
import pptx # noqa: F401
except ImportError:
if auto_install:
import subprocess
print("📦 python-pptx 설치 중...", file=sys.stderr)
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-pptx'])
print("✅ python-pptx 설치 완료\n", file=sys.stderr)
else:
print(
"❌ python-pptx 패키지가 설치되어 있지 않습니다.\n"
"\n"
"설치 명령어:\n"
" pip3 install python-pptx\n"
"\n"
"또는 자동 설치 옵션을 사용하세요:\n"
f" python3 {sys.argv[0]} --auto-install <파일경로>",
file=sys.stderr,
)
sys.exit(1)
# ─────────────────────────────────────────────
# 슬라이드 범위 파싱
# ─────────────────────────────────────────────
def parse_slide_range(spec: str, total: int) -> set[int]:
"""
"1-5,8,10-12" 형태의 슬라이드 범위 문자열을 슬라이드 번호 set 으로 변환.
번호는 1-based.
"""
result: set[int] = set()
for part in spec.split(','):
part = part.strip()
if '-' in part:
start, end = part.split('-', 1)
result.update(range(int(start), int(end) + 1))
else:
result.add(int(part))
return {n for n in result if 1 <= n <= total}
# ─────────────────────────────────────────────
# 도형(Shape) 분류
# ─────────────────────────────────────────────
def _shape_type_name(shape) -> str:
"""python-pptx MSO_SHAPE_TYPE 값을 사람이 읽기 쉬운 문자열로 변환."""
from pptx.util import Emu # noqa: F401 — 모듈 로드 확인용
try:
return shape.shape_type.name.lower() # e.g. 'auto_shape', 'picture', 'line'
except Exception:
return 'unknown'
def _is_connector(shape) -> bool:
"""화살표/커넥터 도형 여부 확인."""
try:
# MSO_SHAPE_TYPE.LINE = 9, FREEFORM = 5
return shape.shape_type in (9,)
except Exception:
return False
# ─────────────────────────────────────────────
# 슬라이드 데이터 추출
# ─────────────────────────────────────────────
def extract_slide(slide, slide_number: int, extract_images: bool, output_dir: str) -> dict:
"""단일 슬라이드에서 모든 관련 데이터를 추출하여 dict 반환."""
from pptx.enum.shapes import PP_PLACEHOLDER # noqa: F401
result: dict = {
'number': slide_number,
'title': '',
'texts': [],
'notes': '',
'images': [],
'shapes': [],
'tables': [],
}
# ── 제목 추출 ──────────────────────────────
for shape in slide.shapes:
try:
if shape.is_placeholder:
ph_type = shape.placeholder_format.type
# PP_PLACEHOLDER.TITLE = 1, CENTER_TITLE = 3
if ph_type in (1, 3):
result['title'] = shape.text.strip()
break
except Exception:
pass
# ── 모든 도형 순회 ──────────────────────────
for shape in slide.shapes:
shape_info: dict = {
'type': _shape_type_name(shape),
'name': getattr(shape, 'name', ''),
'left': int(shape.left or 0),
'top': int(shape.top or 0),
'width': int(shape.width or 0),
'height': int(shape.height or 0),
'text': '',
}
# 텍스트 프레임
if shape.has_text_frame:
text = shape.text_frame.text.strip()
shape_info['text'] = text
if text:
result['texts'].append({
'text': text,
'left': shape_info['left'],
'top': shape_info['top'],
'width': shape_info['width'],
'height': shape_info['height'],
'shape_name': shape_info['name'],
})
# 테이블
if shape.has_table:
table = shape.table
all_rows = list(table.rows)
headers = [cell.text.strip() for cell in all_rows[0].cells]
rows = [
[cell.text.strip() for cell in row.cells]
for row in all_rows[1:]
]
result['tables'].append({'headers': headers, 'rows': rows})
shape_info['type'] = 'table'
# 이미지
if shape.shape_type == 13: # MSO_SHAPE_TYPE.PICTURE = 13
img_info: dict = {
'name': shape.name,
'left': shape_info['left'],
'top': shape_info['top'],
'width': shape_info['width'],
'height': shape_info['height'],
'path': '',
}
if extract_images:
try:
img_bytes = shape.image.blob
ext = shape.image.ext # e.g. 'png', 'jpeg'
safe_name = re.sub(r'[^\w\-.]', '_', shape.name)
img_filename = f"slide{slide_number:03d}_{safe_name}.{ext}"
img_path = os.path.join(output_dir, img_filename)
os.makedirs(output_dir, exist_ok=True)
with open(img_path, 'wb') as f:
f.write(img_bytes)
img_info['path'] = img_path
except Exception as e:
img_info['error'] = str(e)
result['images'].append(img_info)
shape_info['type'] = 'picture'
# 커넥터/화살표 처리
if _is_connector(shape):
shape_info['type'] = 'connector'
result['shapes'].append(shape_info)
# ── 발표자 노트 ────────────────────────────
try:
if slide.has_notes_slide:
notes_text = slide.notes_slide.notes_text_frame.text.strip()
result['notes'] = notes_text
except Exception:
pass
return result
# ─────────────────────────────────────────────
# 메인 추출 함수
# ─────────────────────────────────────────────
def extract_pptx(
filepath: str,
extract_images: bool = False,
output_dir: str = '',
slide_range: str = '',
pretty: bool = False,
) -> None:
"""
PPTX 파일 전체를 파싱하여 JSON 구조를 stdout 에 출력합니다.
Args:
filepath: PPTX 파일 절대/상대 경로
extract_images: True 이면 슬라이드 이미지를 output_dir 에 PNG로 추출
output_dir: 이미지 추출 디렉토리 (기본: /tmp/pptx_<파일명>/)
slide_range: 처리할 슬라이드 범위 문자열 (예: "1-10", "" = 전체)
pretty: True 이면 JSON 들여쓰기 출력
"""
from pptx import Presentation
if not os.path.exists(filepath):
print(f"❌ 파일을 찾을 수 없습니다: {filepath}", file=sys.stderr)
sys.exit(1)
prs = Presentation(filepath)
total_slides = len(prs.slides)
filename = os.path.basename(filepath)
# 출력 디렉토리 결정
if not output_dir:
stem = re.sub(r'[^\w\-]', '_', os.path.splitext(filename)[0])
output_dir = os.path.join(tempfile.gettempdir(), f'pptx_{stem}')
# 처리 대상 슬라이드 번호 결정
if slide_range:
target_slides = parse_slide_range(slide_range, total_slides)
else:
target_slides = set(range(1, total_slides + 1))
print(
f"🔍 파싱 중: {filename} ({total_slides}장 중 {len(target_slides)}장 처리)",
file=sys.stderr,
)
if extract_images:
print(f"🖼️ 이미지 추출 디렉토리: {output_dir}", file=sys.stderr)
# 슬라이드 추출
slides_data = []
for idx, slide in enumerate(prs.slides, start=1):
if idx not in target_slides:
continue
slide_data = extract_slide(slide, idx, extract_images, output_dir)
slides_data.append(slide_data)
print(f" 슬라이드 {idx}/{total_slides}: {slide_data['title'] or '(제목 없음)'}", file=sys.stderr)
output = {
'filename': filename,
'filepath': os.path.abspath(filepath),
'total_slides': total_slides,
'processed_slides': len(slides_data),
'image_output_dir': output_dir if extract_images else '',
'slides': slides_data,
}
indent = 2 if pretty else None
print(json.dumps(output, ensure_ascii=False, indent=indent))
print(f"\n✅ 추출 완료: {len(slides_data)}개 슬라이드", file=sys.stderr)
# ─────────────────────────────────────────────
# CLI 진입점
# ─────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description='PPTX 기획서를 JSON 구조로 추출합니다.',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('filepath', nargs='?', help='PPTX 파일 경로')
parser.add_argument('--extract-images', action='store_true', help='슬라이드 이미지를 PNG로 추출')
parser.add_argument('--output-dir', default='', help='이미지 추출 디렉토리')
parser.add_argument('--slides', default='', help='처리할 슬라이드 범위 (예: 1-10, 5, 3-7,10)')
parser.add_argument('--auto-install', action='store_true', help='python-pptx 자동 설치')
parser.add_argument('--pretty', action='store_true', help='JSON 들여쓰기 출력')
args = parser.parse_args()
_ensure_pptx(args.auto_install)
if not args.filepath:
parser.print_help()
print("\n❌ PPTX 파일 경로를 입력해 주세요.", file=sys.stderr)
sys.exit(1)
extract_pptx(
filepath=args.filepath,
extract_images=args.extract_images,
output_dir=args.output_dir,
slide_range=args.slides,
pretty=args.pretty,
)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,138 @@
---
name: plan-translation-generator
description: 번역 요청 엑셀 파일의 EN 셀을 기반으로 '번역코드' 컬럼에 함축적인 코드를 자동 생성합니다. 사용자가 "번역코드 만들어줘", "translation key 생성", "번역 키 추출" 등을 요청하면 트리거됩니다.
---
# 번역 코드 생성 (Translation Key Generator)
이 skill 은 번역 요청 엑셀 파일의 **EN 컬럼** 텍스트를 분석하여
`번역코드` 컬럼에 함축적이고 일관된 코드를 자동으로 작성합니다.
## 코드 생성 규칙
| 규칙 | 설명 | 예시 |
|------|------|------|
| **단어 수** | 3단어 이하 | `NAV-main` ✅ / `NAV-main-section-title` ❌ |
| **구분자** | 단어 사이 `-` (하이픈) | `EVENT-title` |
| **첫 단어** | 카테고리를 나타내는 **대문자** | `HERO`, `NAV`, `TOAST` |
| **함축성** | 텍스트의 핵심 의미를 최대한 압축 | `HERO-czn-title` (CHAOS ZERO NIGHTMARE → czn) |
## 카테고리 가이드
| 카테고리 | 사용 상황 | 예시 |
|----------|----------|------|
| `BRAND` | 브랜드명, 서비스 이름 | `BRAND-smilegate-ax` |
| `NAV` | 내비게이션, 메뉴 항목 | `NAV-main`, `NAV-location` |
| `EVENT` | 이벤트 정보 (제목/날짜/장소) | `EVENT-title`, `EVENT-date`, `EVENT-venue` |
| `HERO` | 히어로 배너 텍스트 | `HERO-miresi-title` |
| `SECTION` | 섹션 헤더/소제목 | `SECTION-intro` |
| `BTN` | 버튼 레이블 | `BTN-register`, `BTN-more` |
| `TOAST` | 토스트/알림 메시지 | `TOAST-link-copied`, `TOAST-paste-hint` |
| `LABEL` | 폼 레이블, 태그 | `LABEL-date`, `LABEL-venue` |
| `MSG` | 일반 안내 메시지 | `MSG-loading`, `MSG-empty` |
| `MODAL` | 모달/팝업 내 텍스트 | `MODAL-confirm-title` |
## 작업 순서
1. **파일 확인**
- 사용자가 엑셀 파일 경로를 제공했는지 확인
- 미제공 시: 파일 경로를 먼저 요청
2. **데이터 파악**
```python
import openpyxl
wb = openpyxl.load_workbook('파일경로.xlsx')
ws = wb.active
# 헤더 행에서 '번역코드', 'EN' 컬럼 인덱스 찾기
```
- 1행(헤더)에서 `번역코드` 컬럼과 `EN` 컬럼 위치를 동적으로 탐지
- 데이터가 있는 행(EN 값이 None이 아닌 행)만 처리
3. **코드 생성**
- EN 텍스트를 분석해 카테고리와 핵심 키워드 추출
- 이미 `번역코드` 값이 있는 행은 **덮어쓰지 않음** (사용자 확인 후 진행)
- 생성한 코드 목록을 사용자에게 미리 보여주고 승인 요청
4. **중복 코드 검사** ← 저장 전 반드시 수행
- 기존에 이미 작성된 코드 + 이번에 새로 생성한 코드를 **전체 합산**하여 중복 여부 검사
- 중복이 발견되면 **저장을 중단**하고 사용자에게 아래 형식으로 보고:
```
⚠️ 중복 코드 발견
- HERO-title: 3행, 12행 (2건)
- EVENT-date: 8행, 15행 (2건)
```
- 중복 해소 방법을 제안하고 사용자 승인 후 재생성
- 중복이 없으면 "중복 없음 확인" 메시지 출력 후 저장 진행
5. **엑셀 저장**
- 사용자 승인 후 `번역코드` 컬럼에 값 기입 및 저장
- 저장 완료 메시지와 변경 내역 요약 출력
6. **참조 MD 파일 생성**
- 엑셀 저장 완료 후 로컬 참조용 MD 파일을 생성 또는 갱신한다.
- 파일 위치: `docs/translation/<엑셀파일명>.md` (확장자 `.xlsx` → `.md` 치환)
- 예) `smilegate-ax-2026.xlsx` → `docs/translation/smilegate-ax-2026.md`
- `docs/translation/` 디렉토리가 없으면 자동 생성한다.
- 파일 내용: 엑셀의 **전체** 번역코드(기존 + 신규)를 아래 형식으로 작성
- 헤더에서 `KO` 컬럼을 탐지하여 함께 기록한다 (개발 시 번역코드 매칭 참조용)
```markdown
# 번역 코드 참조 — <엑셀파일명>
> 생성일: YYYY-MM-DD
> 원본 파일: <엑셀파일 상대경로>
| 번역코드 | KO | EN |
|----------|----|----|
| BRAND-smilegate-ax | 스마일게이트 x AX | Smilegate x AX |
| NAV-main | 메인 | Main |
| ... | ... | ... |
```
- 파일이 이미 존재하면 **덮어쓰지 않는다**. 새로 생성된 코드 행만 테이블 하단에 추가(append)한다.
- 이미 파일에 존재하는 번역코드는 중복 추가하지 않는다.
- 생성 완료 후 경로와 추가된 행 수를 사용자에게 출력한다.
## 코드 작성 예시 (실제 케이스)
| EN 텍스트 | 생성 코드 | 근거 |
|-----------|----------|------|
| `Smilegate x AX` | `BRAND-smilegate-ax` | 브랜드명 전체 |
| `Main` | `NAV-main` | 내비 메인 항목 |
| `Program & Events` | `NAV-program-events` | 내비 항목, 특수문자 제거 |
| `Smilegate x Anime Expo` | `EVENT-title` | 이벤트 대표 제목 |
| `07/02/2026 (THU)~ 07/05/2026 (SUN)` | `EVENT-date` | 날짜 데이터 |
| `Los Angeles Convention Center` | `EVENT-venue` | 행사 장소 |
| `The link has been copied.` | `TOAST-link-copied` | 토스트 알림, 링크 복사 완료 |
| `You may now paste it elsewhere.` | `TOAST-paste-hint` | 토스트 알림, 붙여넣기 안내 |
| `MIRESI ARRIVES AT <br> ANIME EXPO!` | `HERO-miresi-title` | 히어로 배너, 캐릭터명 |
| `CHAOS ZERO NIGHTMARE ARRIVES AT <br> ANIME EXPO!` | `HERO-czn-title` | 히어로 배너, 약어 처리 |
## 처리 스크립트
스크립트 파일: `scripts/generate_translation_keys.py`
```bash
python scripts/generate_translation_keys.py <엑셀파일경로>
```
스크립트 주요 함수:
| 함수 | 역할 |
|------|------|
| `load_existing_codes(ws, code_col)` | 기존 번역 코드를 `{행번호: 코드}` 로 수집 |
| `check_duplicates(existing, new)` | 기존+신규 코드 합산 중복 검사, 결과 반환 |
| `run(filepath, new_codes)` | 중복 검사 → 저장 실행 (중복 시 중단) |
> `new_codes` 딕셔너리는 Claude AI가 EN 텍스트를 분석하여 직접 채웁니다.
> 스크립트는 중복 검사와 저장만 담당합니다.
## 주의사항
- **행 순서 고정**: 엑셀의 기존 행 순서를 절대 변경하지 않는다. 코드는 원래 행 위치에 그대로 기입한다.
- **이미지 셀 보존**: 셀에 이미지가 삽입된 경우 해당 셀 및 시트의 이미지를 덮어쓰거나 삭제하지 않는다. `openpyxl.load_workbook(path, keep_vba=True)` 옵션을 사용하고, 이미지 객체(`ws._images`)를 건드리지 않는다.
- **기존 코드 보존**: 이미 `번역코드` 값이 있는 셀은 덮어쓰지 않는다.
- **중복 코드 방지**: 동일한 코드가 두 행에 생기지 않도록 확인한다.
- **HTML 태그 무시**: `<br>`, `<b>` 등 마크업 태그는 의미 분석에서 제외한다.
- **날짜/숫자 데이터**: 날짜 형식 셀은 `EVENT-date`, `LABEL-date` 등 의미 기반으로 처리한다.
- **약어 처리**: 3단어 초과 시 캐릭터명·고유명사를 약어(초성/이니셜)로 압축한다.
- 예) `CHAOS ZERO NIGHTMARE` → `czn`
- 사용자가 코드를 직접 수정 요청하면 수정 후 재저장한다.

View File

@@ -0,0 +1,142 @@
"""
번역 코드 자동 생성 스크립트
사용법: python generate_translation_keys.py <엑셀파일경로>
주의:
- 행 순서를 변경하지 않는다 (원래 행 위치에만 값 기입)
- 셀에 삽입된 이미지를 보존한다 (ws._images 미수정)
"""
import sys
import openpyxl
def generate_translation_key(en_text: str) -> str:
"""
EN 텍스트를 분석하여 번역 코드를 생성합니다.
규칙:
- 3단어 이하 (CATEGORY-word1-word2)
- 단어 사이 '-' 구분
- 첫 단어는 카테고리 대문자
- 함축적인 의미로 생성
NOTE: 이 함수는 템플릿입니다.
실제 코드 생성은 Claude AI가 EN 텍스트의 의미를 분석하여 수행합니다.
"""
raise NotImplementedError("AI가 EN 텍스트를 분석하여 코드를 직접 생성합니다.")
def load_existing_codes(ws, code_col: int) -> dict[int, str]:
"""기존에 입력된 번역 코드를 {행번호: 코드} 형태로 반환합니다."""
return {
row: ws.cell(row=row, column=code_col).value
for row in range(2, ws.max_row + 1)
if ws.cell(row=row, column=code_col).value
}
def check_duplicates(existing: dict[int, str], new: dict[int, str]) -> dict[str, list[int]]:
"""
기존 코드 + 신규 코드 전체를 합산하여 중복 검사합니다.
반환: {중복코드: [행번호, ...]}
"""
seen: dict[str, int] = {}
duplicates: dict[str, list[int]] = {}
for row_num, code in {**existing, **new}.items():
if code in seen:
duplicates.setdefault(code, [seen[code]]).append(row_num)
else:
seen[code] = row_num
return duplicates
def run(filepath: str, new_codes: dict[int, str]) -> None:
"""
신규 코드를 중복 검사 후 엑셀에 저장합니다.
Args:
filepath: 엑셀 파일 경로
new_codes: {행번호: 생성된코드} 딕셔너리
"""
# keep_vba=True: 이미지·VBA 등 파일 내 임베딩 요소를 보존
wb = openpyxl.load_workbook(filepath, keep_vba=True)
ws = wb.active
# 이미지가 있는 셀 좌표를 미리 수집 (덮어쓰기 방지)
image_cells: set[tuple[int, int]] = set()
for img in getattr(ws, '_images', []):
anchor = img.anchor
if hasattr(anchor, '_from'):
image_cells.add((anchor._from.row + 1, anchor._from.col + 1)) # 1-based
# 헤더에서 컬럼 인덱스 탐지
headers = [cell.value for cell in ws[1]]
if '번역코드' not in headers or 'EN' not in headers:
print("❌ 헤더에 '번역코드' 또는 'EN' 컬럼이 없습니다.")
sys.exit(1)
code_col = headers.index('번역코드') + 1 # 1-based
en_col = headers.index('EN') + 1 # 1-based
# 기존 코드 수집
existing_codes = load_existing_codes(ws, code_col)
# 빈 행 / 이미 코드 있는 행 필터링
rows_to_fill: list[tuple[int, str]] = []
for row in ws.iter_rows(min_row=2):
row_num = row[0].row
en_val = row[en_col - 1].value
code_val = row[code_col - 1].value
if not en_val:
continue # 빈 행 스킵
if code_val:
continue # 기존 코드 보존
if (row_num, code_col) in image_cells:
print(f" ⚠️ Row {row_num}: 이미지 셀 — 스킵 (보존)")
continue # 이미지 있는 셀 보존
if row_num in new_codes:
rows_to_fill.append((row_num, new_codes[row_num]))
if not rows_to_fill:
print(" 입력할 신규 코드가 없습니다.")
return
# 중복 검사
new_codes_filtered = dict(rows_to_fill)
duplicates = check_duplicates(existing_codes, new_codes_filtered)
if duplicates:
print("⚠️ 중복 코드 발견 — 저장 중단")
for code, rows in duplicates.items():
print(f" - {code}: {', '.join(map(str, rows))}행 ({len(rows)}건)")
print("\n중복을 해소한 뒤 다시 실행해 주세요.")
sys.exit(1)
# 중복 없음 → 저장
print("✅ 중복 없음 확인")
for row_num, code in rows_to_fill:
ws.cell(row=row_num, column=code_col, value=code)
en_text = ws.cell(row=row_num, column=en_col).value
print(f" Row {row_num}: {code:<30}\"{en_text}\"")
wb.save(filepath)
print(f"\n저장 완료: {filepath}")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("사용법: python generate_translation_keys.py <엑셀파일경로>")
sys.exit(1)
# new_codes는 Claude AI가 EN 텍스트를 분석하여 채워줍니다.
# 예시:
# new_codes = {
# 2: 'BRAND-smilegate-ax',
# 3: 'NAV-main',
# }
new_codes: dict[int, str] = {}
run(sys.argv[1], new_codes)

268
skills/ppt-maker/SKILL.md Normal file
View File

@@ -0,0 +1,268 @@
---
name: ppt-maker
description: 순수 HTML, CSS, JavaScript로 웹 프레젠테이션(슬라이드)을 생성합니다. 외부 라이브러리 없이 단일 HTML 파일로 완성되며, GPU 가속 슬라이드 전환, 키보드/터치 네비게이션, 진행 표시바, 반응형 디자인을 포함합니다. 사용자가 프레젠테이션, 슬라이드, 발표자료, PPT 등을 만들어달라고 요청할 때 사용하세요. 'PPT 만들어줘', '슬라이드 생성', '발표자료 만들어줘', 'create a presentation', 'make slides about' 등의 요청에 반응합니다.
---
# PPT Maker
순수 HTML/CSS/JS 단일 파일 웹 프레젠테이션 생성기.
## 워크플로우
1. `references/template.html`을 읽어 전체 구조(CSS/JS)를 파악한다
2. 사용자의 주제, 내용, 슬라이드 수 요구사항을 정리한다
3. 슬라이드 구성을 설계한다 (어떤 타입의 슬라이드를 어떤 순서로)
4. 템플릿의 CSS와 JS를 그대로 유지하면서 슬라이드 HTML 콘텐츠만 교체한다
5. 단일 .html 파일로 출력한다
6. `open` 명령으로 브라우저에서 열어 확인한다
## 핵심 원칙
- **단일 파일**: 모든 CSS, JS가 HTML 안에 인라인
- **외부 의존성 제로**: Google Fonts만 예외 (CDN 링크)
- **GPU 가속**: 전환 애니메이션은 `transform``opacity`만 사용
- **뷰포트 피팅**: 모든 슬라이드가 스크롤 없이 화면에 딱 맞아야 함
- **접근성**: ARIA 속성, prefers-reduced-motion 대응
## 디자인 시스템
### CSS Custom Properties
```css
:root {
--bg: #0a0a0f;
--bg-subtle: #111118;
--accent: #C6A55C;
--accent-dim: rgba(198, 165, 92, 0.15);
--accent-glow: rgba(198, 165, 92, 0.3);
--text-primary: #F0EBE0;
--text-secondary: #B8B0A0;
--text-dim: #706858;
--border: #1e1c18;
--font-kr: 'Noto Sans KR', sans-serif;
--font-en: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
```
사용자가 다른 컬러 테마를 원하면 이 변수들만 교체한다.
### 타이포그래피
| 요소 | 크기 | 비고 |
|------|------|------|
| h1 | `clamp(2rem, 5vw, 4rem)` | 타이틀 슬라이드 |
| h2 | `clamp(1.4rem, 3vw, 2.4rem)` | 섹션 제목 |
| h3 | `clamp(1.1rem, 2vw, 1.6rem)` | 서브 제목 |
| p, li | `clamp(0.85rem, 1.5vw, 1.2rem)` | 본문 |
| code | `clamp(0.65rem, 1.1vw, 0.9rem)` | 코드 블록 |
### 컴포넌트 카탈로그
- **info-card**: 정보 카드 (`background: rgba(255,255,255,0.02)`, `border: 1px solid var(--border)`)
- **split**: 2열 그리드 (`grid-template-columns: 1fr 1fr`)
- **split-card**: split 내부 카드
- **badge**: 태그/라벨 (`border-radius: 100px`, accent 컬러)
- **terminal-badge**: 모노스페이스 라벨 + 깜빡이는 커서 (`> `)
- **accent-line**: 강조선 (`width: 50px; height: 2px; background: var(--accent)`)
- **code-block**: 줄 번호 + 하이라이트 지원 코드 블록
## 슬라이드 타입 카탈로그
### 1. 타이틀 슬라이드 (slide--intro)
```html
<section class="slide slide--intro active" id="slide-1"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 1" aria-hidden="false">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">카테고리</div>
<h1 class="reveal">메인 타이틀</h1>
</div>
<div class="slide__body">
<p class="reveal" style="font-size: clamp(0.95rem, 1.8vw, 1.3rem);">서브 타이틀</p>
<div class="accent-line reveal"></div>
<p class="reveal" style="font-size: clamp(0.7rem, 1vw, 0.85rem); color: var(--text-dim);">
← → 키 또는 스와이프로 이동
</p>
</div>
<div class="slide__footer">날짜</div>
</div>
</section>
```
### 2. 리스트 콘텐츠 (slide--content / slide--content-alt)
```html
<section class="slide slide--content" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">영문 키워드</div>
<h2 class="reveal">섹션 제목</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">항목 1</li>
<li class="reveal">항목 2</li>
<li class="reveal">항목 3</li>
</ul>
</div>
</div>
</section>
```
`slide--content``slide--content-alt`를 번갈아 사용하면 배경 그라데이션 방향이 달라져 시각적 변화를 준다.
### 3. 코드 슬라이드 (slide--code)
```html
<section class="slide slide--code" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">코드 제목</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="javascript" data-highlight-lines="2,4-6"><code><span class="line"><span class="kw">const</span> <span class="var">x</span> = <span class="str">42</span>;</span>
<span class="line"><span class="cmt">// 강조될 줄</span></span></code></pre>
</div>
</div>
</section>
```
**신택스 클래스**: `kw`(키워드), `fn`(함수), `str`(문자열/숫자), `cmt`(주석), `var`(변수), `op`(연산자), `tag`(태그), `attr`(속성), `val`(값)
`data-highlight-lines`에 강조할 줄 번호를 지정한다 (예: "2,4-6").
### 4. 분할 레이아웃 (slide--diagram)
```html
<section class="slide slide--diagram" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">키워드</div>
<h2 class="reveal">제목</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<p><strong style="color: var(--accent);">왼쪽 제목</strong><br>설명</p>
</div>
<div class="split-card reveal">
<p><strong style="color: #D4B87A;">오른쪽 제목</strong><br>설명</p>
</div>
</div>
</div>
</div>
</section>
```
### 5. 정보 카드 슬라이드
```html
<div class="slide__body">
<div class="info-card reveal">
<div class="card-title">// 카드 라벨</div>
<p>카드 내용</p>
</div>
<div class="info-card reveal">
<div class="card-title">// 카드 라벨 2</div>
<p>카드 내용 2</p>
</div>
</div>
```
### 6. 마무리 슬라이드 (slide--end)
```html
<section class="slide slide--end" id="slide-N"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 N" aria-hidden="true">
<div class="slide__inner" style="text-align: center; align-items: center;">
<div class="slide__body" style="align-items: center;">
<h1 class="reveal">감사합니다</h1>
<div class="accent-line reveal" style="margin: 0.8rem auto;"></div>
<p class="reveal">마무리 메시지</p>
<div class="reveal" style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<span class="badge">태그1</span>
<span class="badge">태그2</span>
</div>
</div>
</div>
</section>
```
## 콘텐츠 작성 규칙
### reveal 순차 등장
- 모든 콘텐츠 요소에 `class="reveal"` 추가
- CSS `transition-delay`로 nth-child 기반 순차 등장이 자동 적용됨
- 최대 8단계까지 지원 (0.08s 간격)
### 슬라이드 구조 필수 규칙
- 첫 번째 슬라이드만 `class="slide ... active"`, `aria-hidden="false"`
- 나머지는 `aria-hidden="true"`, active 클래스 없음
- 모든 슬라이드에 고유 `id="slide-N"` (1부터 시작)
- `role="region"`, `aria-roledescription="슬라이드"`, `aria-label="슬라이드 N"` 필수
### 슬라이드 수 가이드
- 사용자가 지정하면 그대로 따름
- 지정하지 않으면 주제에 맞게 7~12장 구성
- 기본 구성: 타이틀(1) + 개요(1) + 본론(3~8) + 마무리(1)
### 콘텐츠 밀도
- 리스트: 슬라이드당 최대 5개 항목
- 텍스트: 한 단락 2~3줄 이내
- 코드: 최대 10줄
- **뷰포트에 딱 맞아야 함 — 스크롤이 발생하면 안 된다**
## 기술 요구사항
### 네비게이션
- **키보드**: ← → / ↑ ↓ / Space / PageUp·Down / Home·End / F(전체화면)
- **터치**: 스와이프 (threshold 50px, `passive:false``preventDefault`)
- **버튼**: 좌하단 이전/다음 버튼
- **URL hash**: `#slide-N` 동기화 + 뒤로가기 지원
### 전환 애니메이션
- `transform: translateX(±100%)` + `opacity` (GPU 가속, Composite 전용)
- duration: 400~450ms
- `isAnimating` 잠금 + `transitionend` + `setTimeout(duration+150)` fallback
- `will-change`는 전환 직전 설정, 완료 후 `auto`로 해제
### UI 요소
- **진행 표시바**: 상단 2px, accent 컬러
- **슬라이드 카운터**: 우하단 `01 / 10` 형식 (2자리 zero-pad)
- **네비게이션 버튼**: 좌하단 버튼
### 접근성
- `prefers-reduced-motion: reduce`에서 transition-duration 최소화
- 모바일 터치 타겟 최소 44px (`@media (hover: none)`)
- `100dvh` 사용 (모바일 주소창 동적 대응)
## 주의사항
- `display:none` 사용 금지 — transition 불가
- 초기 위치: `transition:none` → 위치 설정 → `void el.offsetWidth``transition` 복원
- Space 키는 반드시 `preventDefault()` (페이지 스크롤 방지)
- `will-change`를 모든 슬라이드에 정적 적용하면 GPU 메모리 낭비
- 애니메이션 duration 500ms 초과 금지 (사용자 지루함 유발)
- `e.repeat` 체크로 키 반복 이벤트 차단
## 슬라이드 카운터 업데이트
JS에서 슬라이드 총 개수를 반드시 실제 슬라이드 수와 일치시킨다. `slideCounter` 초기 텍스트도 `01 / {총 슬라이드 수}`로 설정한다.
## 참고 템플릿
`references/template.html`에 10장짜리 완전한 동작 예시가 있다.
이 파일의 **CSS와 JS를 기반으로** 슬라이드 HTML 콘텐츠만 교체하여 새 프레젠테이션을 만든다.

View File

@@ -0,0 +1,914 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>바닐라 슬라이드 프레젠테이션</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&family=Inter:wght@400;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
/* ===== CSS Custom Properties ===== */
:root {
--bg: #0a0a0f;
--bg-subtle: #111118;
--accent: #C6A55C;
--accent-dim: rgba(198, 165, 92, 0.15);
--accent-glow: rgba(198, 165, 92, 0.3);
--text-primary: #F0EBE0;
--text-secondary: #B8B0A0;
--text-dim: #706858;
--border: #1e1c18;
--font-kr: 'Noto Sans KR', sans-serif;
--font-en: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
/* ===== 리셋 ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-kr), var(--font-en), sans-serif;
overflow: hidden;
background: var(--bg);
color: var(--text-primary);
}
/* ===== 뷰포트: fixed 풀스크린 ===== */
.slide-viewport {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
background: var(--bg);
overflow: hidden;
}
.slide-viewport.animating { pointer-events: none; }
/* ===== 슬라이드: absolute 풀스크린 ===== */
.slide {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: clamp(2rem, 5vw, 5rem);
opacity: 0;
transform: translateX(100%);
pointer-events: none;
transition:
opacity 400ms ease-in-out,
transform 450ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide.active {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
z-index: 2;
}
.slide.stand-left { transform: translateX(-100%); }
/* ===== 배경 효과 ===== */
.slide::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg,
transparent, transparent 2px,
rgba(255, 255, 255, 0.005) 2px,
rgba(255, 255, 255, 0.005) 4px);
pointer-events: none;
z-index: 1;
}
.slide::after {
content: '';
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(198, 165, 92, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(198, 165, 92, 0.015) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
.slide > * { position: relative; z-index: 2; }
/* ===== 콘텐츠 영역 ===== */
.slide__inner {
width: 100%;
max-width: 680px;
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
gap: clamp(0.8rem, 2vw, 1.5rem);
}
.slide__header {}
.slide__body { display: flex; flex-direction: column; justify-content: center; gap: 1rem; }
.slide__footer { font-size: clamp(0.6rem, 1vw, 0.8rem); color: var(--text-dim); text-align: right; margin-top: auto; }
/* ===== 타이포 ===== */
h1 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 700;
line-height: 1.15;
color: var(--text-primary);
}
h2 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(1.4rem, 3vw, 2.4rem);
font-weight: 700;
line-height: 1.2;
margin-bottom: 0.8rem;
color: var(--text-primary);
}
h3 {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(1.1rem, 2vw, 1.6rem);
font-weight: 700;
line-height: 1.3;
color: var(--text-primary);
}
p, li {
font-family: var(--font-kr), var(--font-en), sans-serif;
font-size: clamp(0.85rem, 1.5vw, 1.2rem);
line-height: 1.7;
color: var(--text-secondary);
}
ul { list-style: none; padding: 0; }
ul li {
padding: clamp(0.5rem, 0.8vw, 0.7rem) clamp(0.8rem, 1.2vw, 1rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
gap: 0.6rem;
}
ul li::before { content: '→ '; color: var(--accent); font-weight: bold; font-family: var(--font-mono); flex-shrink: 0; }
/* ===== 슬라이드 배경 변형 ===== */
.slide--intro { background: radial-gradient(ellipse at 30% 50%, rgba(198,165,92,0.06) 0%, var(--bg) 70%); }
.slide--content { background: radial-gradient(ellipse at 70% 40%, rgba(198,165,92,0.04) 0%, var(--bg) 70%); }
.slide--content-alt { background: radial-gradient(ellipse at 30% 60%, rgba(198,165,92,0.04) 0%, var(--bg) 70%); }
.slide--code { background: var(--bg); }
.slide--diagram { background: radial-gradient(ellipse at 50% 30%, rgba(198,165,92,0.05) 0%, var(--bg) 70%); }
.slide--end { background: radial-gradient(ellipse at 50% 50%, rgba(198,165,92,0.08) 0%, var(--bg) 60%); }
/* ===== reveal 애니메이션 (슬라이드 진입 시 순차 등장) ===== */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s var(--ease-out-expo), transform 0.6s var(--ease-out-expo);
}
.slide.active .reveal {
opacity: 1;
transform: translateY(0);
}
.reveal:nth-child(1) { transition-delay: 0.08s; }
.reveal:nth-child(2) { transition-delay: 0.16s; }
.reveal:nth-child(3) { transition-delay: 0.24s; }
.reveal:nth-child(4) { transition-delay: 0.32s; }
.reveal:nth-child(5) { transition-delay: 0.40s; }
.reveal:nth-child(6) { transition-delay: 0.48s; }
.reveal:nth-child(7) { transition-delay: 0.56s; }
.reveal:nth-child(8) { transition-delay: 0.64s; }
/* .slide__body 내부 자식도 순차 등장 */
.slide.active .slide__body > .reveal:nth-child(1) { transition-delay: 0.15s; }
.slide.active .slide__body > .reveal:nth-child(2) { transition-delay: 0.25s; }
.slide.active .slide__body > .reveal:nth-child(3) { transition-delay: 0.35s; }
.slide.active .slide__body > .reveal:nth-child(4) { transition-delay: 0.45s; }
.slide.active .slide__body > .reveal:nth-child(5) { transition-delay: 0.55s; }
/* ul li 순차 등장 */
.slide.active ul li.reveal:nth-child(1) { transition-delay: 0.15s; }
.slide.active ul li.reveal:nth-child(2) { transition-delay: 0.25s; }
.slide.active ul li.reveal:nth-child(3) { transition-delay: 0.35s; }
.slide.active ul li.reveal:nth-child(4) { transition-delay: 0.45s; }
.slide.active ul li.reveal:nth-child(5) { transition-delay: 0.55s; }
/* ===== 코드 블록 ===== */
.code-block {
background: #111118;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
font-family: var(--font-mono);
font-size: clamp(0.65rem, 1.1vw, 0.9rem);
line-height: 1.7;
counter-reset: line-number;
width: 100%;
}
.code-block::before {
content: attr(data-language);
display: block;
background: rgba(198,165,92,0.06);
color: var(--text-dim);
font-size: 0.75em;
padding: 0.4em 1em;
text-transform: uppercase;
letter-spacing: 0.1em;
font-family: var(--font-mono);
border-bottom: 1px solid var(--border);
}
.code-block code { display: block; padding: 1rem 1.2rem; overflow-x: auto; }
.code-block .line {
display: block;
padding: 2px 0 2px 3em;
position: relative;
counter-increment: line-number;
transition: background-color 0.3s ease, opacity 0.3s ease;
color: var(--text-secondary);
}
.code-block .line::before {
content: counter(line-number);
position: absolute;
left: 0;
width: 2.5em;
text-align: right;
color: #3a3830;
font-size: 0.85em;
user-select: none;
}
.code-block .line--highlight {
background: rgba(198, 165, 92, 0.08);
border-left: 3px solid var(--accent);
padding-left: calc(3em - 3px);
}
.code-block .line--highlight::before { color: var(--accent); }
.code-block.dim-others .line:not(.line--highlight) { opacity: 0.3; }
/* 신택스 컬러 */
.kw { color: #C6A55C; }
.fn { color: #D4B87A; }
.str { color: #A89070; }
.cmt { color: #555040; font-style: italic; }
.var { color: #E0D0B0; }
.op { color: #8A8070; }
.tag { color: #C6A55C; }
.attr { color: #D4B87A; }
.val { color: #A89070; }
/* ===== 컴포넌트 ===== */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
.split-card {
padding: clamp(0.8rem, 1.5vw, 1.2rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 6px;
}
.info-card {
padding: clamp(1rem, 2vw, 1.5rem);
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 6px;
width: 100%;
}
.info-card .card-title {
font-family: var(--font-mono);
font-size: clamp(0.7rem, 1vw, 0.85rem);
color: var(--text-dim);
margin-bottom: 0.8rem;
letter-spacing: 0.05em;
}
.badge {
display: inline-block;
font-family: var(--font-mono);
font-size: clamp(0.65rem, 0.9vw, 0.8rem);
color: var(--accent);
padding: 0.2em 0.6em;
border: 1px solid var(--accent-dim);
border-radius: 100px;
letter-spacing: 0.05em;
}
.terminal-badge {
font-family: var(--font-mono);
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
color: var(--accent);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.terminal-badge::before {
content: '> ';
animation: blink 1s step-end infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.accent-line {
width: 50px;
height: 2px;
background: var(--accent);
margin: 0.5rem 0;
}
.number-label {
font-family: var(--font-mono);
font-size: clamp(0.7rem, 0.9vw, 0.8rem);
color: var(--accent);
flex-shrink: 0;
width: 2em;
}
/* ===== 네비게이션 UI ===== */
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: rgba(198,165,92,0.1);
z-index: 101;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--accent);
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-counter {
position: fixed;
bottom: 16px;
right: 24px;
font-size: 12px;
color: var(--text-dim);
font-family: var(--font-mono);
z-index: 102;
letter-spacing: 0.05em;
}
.nav-buttons {
position: fixed;
bottom: 16px;
left: 24px;
display: flex;
gap: 6px;
z-index: 102;
}
.nav-btn {
width: 32px;
height: 32px;
border: 1px solid rgba(198,165,92,0.2);
background: rgba(198,165,92,0.04);
color: var(--text-dim);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.nav-btn:hover { background: rgba(198,165,92,0.12); color: var(--accent); border-color: rgba(198,165,92,0.4); }
.speaker-notes { display: none; }
/* ===== 반응형 ===== */
@media (prefers-reduced-motion: reduce) {
.reveal, .slide { transition-duration: 0.1s !important; }
.reveal { transform: none !important; }
}
@media (hover: none) and (pointer: coarse) {
.nav-btn { width: 44px; height: 44px; }
}
@media (max-height: 600px) {
.slide { padding: clamp(1rem, 3vw, 2rem); }
h1 { font-size: clamp(1.5rem, 4vw, 2.5rem); }
h2 { font-size: clamp(1.2rem, 2.5vw, 1.8rem); }
}
</style>
</head>
<body>
<div class="slide-viewport" id="viewport">
<!-- ============ 슬라이드 01: 타이틀 ============ -->
<section class="slide slide--intro active" id="slide-1"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 1" aria-hidden="false">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Presentation</div>
<h1 class="reveal">Vanilla Slides</h1>
</div>
<div class="slide__body">
<p class="reveal" style="font-size: clamp(0.95rem, 1.8vw, 1.3rem);">
순수 HTML, CSS, JavaScript로 만드는<br>웹 기반 프레젠테이션
</p>
<div class="accent-line reveal"></div>
<p class="reveal" style="font-size: clamp(0.7rem, 1vw, 0.85rem); color: var(--text-dim);">
← → 키 또는 스와이프로 이동
</p>
</div>
<div class="slide__footer">2026.03</div>
</div>
</section>
<!-- ============ 슬라이드 02: 왜 바닐라인가 ============ -->
<section class="slide slide--content" id="slide-2"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 2" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Why vanilla?</div>
<h2 class="reveal">왜 바닐라 JS인가</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">외부 의존성 제로 — 번들 크기 0KB</li>
<li class="reveal">브라우저 표준만으로 완전한 구현</li>
<li class="reveal">커스터마이즈의 자유도 극대화</li>
<li class="reveal">단일 HTML 파일로 배포 가능</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 03: 핵심 기능 ============ -->
<section class="slide slide--content-alt" id="slide-3"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 3" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Features</div>
<h2 class="reveal">핵심 기능</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal">풀스크린 뷰포트 & 반응형 레이아웃</li>
<li class="reveal">키보드 / 터치 / 스와이프 네비게이션</li>
<li class="reveal">GPU 가속 슬라이드 전환 애니메이션</li>
<li class="reveal">순차 등장 reveal 효과</li>
<li class="reveal">코드 블록 라인 하이라이트</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 04: 아키텍처 ============ -->
<section class="slide slide--diagram" id="slide-4"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 4" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Architecture</div>
<h2 class="reveal">아키텍처</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem); margin-bottom: 0.8rem;">
<strong style="color: var(--accent);">레이아웃</strong><br>
fixed 뷰포트 + absolute 슬라이드
</p>
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem);">
<strong style="color: #D4B87A;">전환</strong><br>
CSS Transition + transform/opacity
</p>
</div>
<div class="split-card reveal">
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem); margin-bottom: 0.8rem;">
<strong style="color: #B89860;">상태 관리</strong><br>
slideIndex 단일 상태 + URL hash
</p>
<p style="font-size: clamp(0.8rem, 1.3vw, 1.05rem);">
<strong style="color: #A08848;">등장 효과</strong><br>
.reveal 클래스 + transition-delay
</p>
</div>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 05: CSS 레이아웃 ============ -->
<section class="slide slide--content" id="slide-5"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 5" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Layout</div>
<h2 class="reveal">CSS 레이아웃 전략</h2>
</div>
<div class="slide__body">
<div class="info-card reveal">
<div class="card-title">// 풀스크린 뷰포트</div>
<p style="font-size: clamp(0.8rem, 1.2vw, 1rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">position: fixed</code> 뷰포트 안에<br>
<code style="color: var(--accent); font-family: var(--font-mono);">position: absolute</code> 슬라이드를 겹쳐<br>
<code style="color: var(--accent); font-family: var(--font-mono);">transform: translateX</code>로 전환합니다
</p>
</div>
<div class="info-card reveal">
<div class="card-title">// 콘텐츠 영역</div>
<p style="font-size: clamp(0.8rem, 1.2vw, 1rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">max-width: 680px</code>로 읽기 편한 폭 유지<br>
<code style="color: var(--accent); font-family: var(--font-mono);">clamp()</code>로 반응형 타이포그래피 처리
</p>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 06: 코드 예시 — HTML ============ -->
<section class="slide slide--code" id="slide-6"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 6" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">HTML 구조</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="html" data-highlight-lines="2,4-5"><code><span class="line"><span class="op">&lt;</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide-viewport"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">section</span> <span class="attr">class</span>=<span class="str">"slide active"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide__inner"</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">h2</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">&gt;</span><span class="var">제목</span><span class="op">&lt;/</span><span class="tag">h2</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;</span><span class="tag">p</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">&gt;</span><span class="var">내용</span><span class="op">&lt;/</span><span class="tag">p</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;/</span><span class="tag">div</span><span class="op">&gt;</span></span>
<span class="line"> <span class="op">&lt;/</span><span class="tag">section</span><span class="op">&gt;</span></span>
<span class="line"><span class="op">&lt;/</span><span class="tag">div</span><span class="op">&gt;</span></span></code></pre>
<p class="reveal" style="font-size: clamp(0.7rem, 1.1vw, 0.95rem); color: var(--accent); margin-top: 0.5rem;">
.reveal 클래스로 순차 등장 효과가 자동 적용됩니다
</p>
</div>
</div>
</section>
<!-- ============ 슬라이드 07: 코드 예시 — JS ============ -->
<section class="slide slide--code" id="slide-7"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 7" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<h2 class="reveal" style="color: var(--accent);">슬라이드 전환 로직</h2>
</div>
<div class="slide__body">
<pre class="code-block reveal" data-language="javascript" data-highlight-lines="3,6-7"><code><span class="line"><span class="fn">goNext</span>() {</span>
<span class="line"> <span class="kw">if</span> (<span class="kw">this</span>.<span class="var">isAnimating</span>) <span class="kw">return</span>;</span>
<span class="line"> <span class="kw">this</span>.<span class="fn">_goSlide</span>(<span class="kw">this</span>.<span class="var">current</span> + <span class="str">1</span>, <span class="str">'next'</span>);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="cmt">// GPU 가속: transform + opacity만 사용</span></span>
<span class="line"><span class="var">to</span>.<span class="var">style</span>.<span class="var">transform</span> = <span class="str">'translateX(0)'</span>;</span>
<span class="line"><span class="var">from</span>.<span class="var">style</span>.<span class="var">transform</span> = <span class="str">'translateX(-100%)'</span>;</span></code></pre>
<p class="reveal" style="font-size: clamp(0.7rem, 1.1vw, 0.95rem); color: var(--text-dim); margin-top: 0.5rem;">
isAnimating 플래그 + transitionend로 이벤트 잠금
</p>
</div>
</div>
</section>
<!-- ============ 슬라이드 08: 네비게이션 ============ -->
<section class="slide slide--content-alt" id="slide-8"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 8" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Navigation</div>
<h2 class="reveal">네비게이션 지원</h2>
</div>
<div class="slide__body">
<div class="split">
<div class="split-card reveal">
<h3 style="font-size: clamp(0.85rem, 1.2vw, 1rem); color: var(--accent); margin-bottom: 0.5rem;">키보드</h3>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
← → 이전/다음<br>
Space / PageDown 다음<br>
Home / End 처음/끝<br>
F 전체화면
</p>
</div>
<div class="split-card reveal">
<h3 style="font-size: clamp(0.85rem, 1.2vw, 1rem); color: var(--accent); margin-bottom: 0.5rem;">터치</h3>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
좌 스와이프 → 다음<br>
우 스와이프 → 이전<br>
threshold: 50px<br>
touch-action: pan-y
</p>
</div>
</div>
<div class="info-card reveal" style="margin-top: 0.8rem;">
<div class="card-title">// URL 동기화</div>
<p style="font-size: clamp(0.75rem, 1.1vw, 0.95rem);">
<code style="color: var(--accent); font-family: var(--font-mono);">#slide-N</code> 해시로 뒤로가기 및 직접 접근 지원
</p>
</div>
</div>
</div>
</section>
<!-- ============ 슬라이드 09: 주의사항 ============ -->
<section class="slide slide--content" id="slide-9"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 9" aria-hidden="true">
<div class="slide__inner">
<div class="slide__header">
<div class="terminal-badge reveal">Pitfalls</div>
<h2 class="reveal">주의사항</h2>
</div>
<div class="slide__body">
<ul>
<li class="reveal"><span style="color: var(--accent);">display:none</span> 사용 금지 — transition 불가</li>
<li class="reveal">모바일 높이는 <span style="color: var(--accent);">100dvh</span> 사용</li>
<li class="reveal"><span style="color: var(--accent);">will-change</span>는 동적 토글 (정적 적용 시 메모리 낭비)</li>
<li class="reveal"><span style="color: var(--accent);">transitionend</span> 미발생 대비 setTimeout fallback 필수</li>
<li class="reveal">터치 <span style="color: var(--accent);">passive:false</span>에서만 preventDefault 가능</li>
</ul>
</div>
</div>
</section>
<!-- ============ 슬라이드 10: 마무리 ============ -->
<section class="slide slide--end" id="slide-10"
role="region" aria-roledescription="슬라이드" aria-label="슬라이드 10" aria-hidden="true">
<div class="slide__inner" style="text-align: center; align-items: center;">
<div class="slide__body" style="align-items: center;">
<h1 class="reveal">감사합니다</h1>
<div class="accent-line reveal" style="margin: 0.8rem auto;"></div>
<p class="reveal" style="font-size: clamp(0.85rem, 1.3vw, 1.1rem);">
외부 라이브러리 0개 · 단일 HTML 파일
</p>
<div class="reveal" style="display: flex; gap: 0.5rem; margin-top: 1rem;">
<span class="badge">HTML</span>
<span class="badge">CSS</span>
<span class="badge">JavaScript</span>
</div>
</div>
</div>
</section>
</div>
<!-- 네비게이션 UI -->
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<div class="slide-counter" id="slideCounter">01 / 10</div>
<div class="nav-buttons">
<button class="nav-btn" id="btnPrev" aria-label="이전 슬라이드">&#8249;</button>
<button class="nav-btn" id="btnNext" aria-label="다음 슬라이드">&#8250;</button>
</div>
<script>
class Presentation {
constructor() {
this.viewport = document.getElementById('viewport');
this.slides = Array.from(this.viewport.querySelectorAll('.slide'));
this.total = this.slides.length;
this.current = 0;
this.isAnimating = false;
this.DURATION = 450;
// 코드 하이라이트 적용
this.slides.forEach(s => this._applyCodeHighlights(s));
// URL hash에서 초기 슬라이드 복원
this._restoreFromHash();
// 이벤트 바인딩
this._bindKeyboard();
this._bindTouch();
this._bindButtons();
this._bindHash();
this._updateUI();
}
// ── 한 페이지 단위 이동 ─────────────────────
goNext() {
if (this.isAnimating || this.current >= this.total - 1) return;
this._goSlide(this.current + 1, 'next');
}
goPrev() {
if (this.isAnimating || this.current <= 0) return;
this._goSlide(this.current - 1, 'prev');
}
goTo(index) {
if (this.isAnimating || index === this.current) return;
if (index < 0 || index >= this.total) return;
this._goSlide(index, index > this.current ? 'next' : 'prev');
}
// ── 슬라이드 전환 ───────────────────────────
_goSlide(index, direction) {
const isNext = direction === 'next';
const from = this.slides[this.current];
const to = this.slides[index];
this.isAnimating = true;
this.viewport.classList.add('animating');
from.style.willChange = 'transform, opacity';
to.style.willChange = 'transform, opacity';
// 진입 슬라이드 초기 위치 (transition 없이)
to.style.transition = 'none';
to.classList.remove('stand-left');
to.style.transform = isNext ? 'translateX(100%)' : 'translateX(-100%)';
to.style.opacity = '0';
void to.offsetWidth;
// transition 복원 + 애니메이션 시작
to.style.transition = '';
to.style.transform = '';
to.style.opacity = '';
from.classList.remove('active');
to.classList.add('active');
from.style.transform = isNext ? 'translateX(-100%)' : 'translateX(100%)';
from.style.opacity = '0';
from.setAttribute('aria-hidden', 'true');
to.setAttribute('aria-hidden', 'false');
this.current = index;
this._syncHash();
this._updateUI();
// 완료 감지
let done = false;
const cleanup = () => {
if (done) return;
done = true;
clearTimeout(fallback);
to.removeEventListener('transitionend', onEnd);
from.style.transform = '';
from.style.opacity = '';
from.style.transition = 'none';
from.classList.add('stand-left');
void from.offsetWidth;
from.style.transition = '';
from.style.willChange = 'auto';
to.style.willChange = 'auto';
this.isAnimating = false;
this.viewport.classList.remove('animating');
};
const onEnd = (e) => {
if (e.propertyName !== 'transform' || e.target !== to) return;
cleanup();
};
to.addEventListener('transitionend', onEnd);
const fallback = setTimeout(cleanup, this.DURATION + 150);
}
// ── 유틸리티 ─────────────────────────────────
_applyCodeHighlights(slide) {
slide.querySelectorAll('.code-block[data-highlight-lines]').forEach(pre => {
const ranges = this._parseRanges(pre.dataset.highlightLines);
const lines = pre.querySelectorAll('.line');
lines.forEach((line, i) => {
if (ranges.has(i + 1)) line.classList.add('line--highlight');
});
if (ranges.size > 0) pre.classList.add('dim-others');
});
}
_parseRanges(str) {
const result = new Set();
str.split(',').forEach(part => {
part = part.trim();
if (part.includes('-')) {
const [a, b] = part.split('-').map(Number);
for (let i = a; i <= b; i++) result.add(i);
} else {
result.add(Number(part));
}
});
return result;
}
_updateUI() {
const progress = this.total <= 1 ? 100 : (this.current / (this.total - 1)) * 100;
document.getElementById('progressFill').style.width = `${progress}%`;
const num = String(this.current + 1).padStart(2, '0');
const tot = String(this.total).padStart(2, '0');
document.getElementById('slideCounter').textContent = `${num} / ${tot}`;
}
_syncHash() {
const hash = `#slide-${this.current + 1}`;
if (location.hash !== hash) history.pushState(null, '', hash);
}
_restoreFromHash() {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1], 10) - 1;
if (idx > 0 && idx < this.total) {
this.slides[0].classList.remove('active');
this.slides[0].setAttribute('aria-hidden', 'true');
this.slides[0].classList.add('stand-left');
this.slides[idx].classList.add('active');
this.slides[idx].setAttribute('aria-hidden', 'false');
this.current = idx;
}
}
}
// ── 이벤트 바인딩 ────────────────────────────
_bindKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
const tag = document.activeElement.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
switch (e.key) {
case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown':
e.preventDefault(); this.goNext(); break;
case 'ArrowLeft': case 'ArrowUp': case 'PageUp':
e.preventDefault(); this.goPrev(); break;
case 'Home':
e.preventDefault(); this.goTo(0); break;
case 'End':
e.preventDefault(); this.goTo(this.total - 1); break;
case 'f': case 'F':
this._toggleFullscreen(); break;
}
});
}
_bindTouch() {
let startX = 0, startY = 0, startTime = 0, swiping = false;
const el = this.viewport;
el.addEventListener('touchstart', (e) => {
const t = e.changedTouches[0];
startX = t.clientX; startY = t.clientY;
startTime = Date.now(); swiping = false;
}, { passive: true });
el.addEventListener('touchmove', (e) => {
const t = e.changedTouches[0];
const dx = Math.abs(t.clientX - startX);
const dy = Math.abs(t.clientY - startY);
if (dx > dy && dx > 10) { swiping = true; e.preventDefault(); }
}, { passive: false });
el.addEventListener('touchend', (e) => {
const t = e.changedTouches[0];
const dx = t.clientX - startX;
const dy = Math.abs(t.clientY - startY);
const elapsed = Date.now() - startTime;
if (swiping && Math.abs(dx) >= 50 && dy <= 75 && elapsed <= 400) {
dx < 0 ? this.goNext() : this.goPrev();
}
swiping = false;
}, { passive: true });
el.style.touchAction = 'pan-y';
el.style.userSelect = 'none';
}
_bindButtons() {
document.getElementById('btnPrev').addEventListener('click', () => this.goPrev());
document.getElementById('btnNext').addEventListener('click', () => this.goNext());
}
_bindHash() {
window.addEventListener('hashchange', () => {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1], 10) - 1;
if (idx !== this.current) this.goTo(idx);
}
});
}
_toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen();
}
}
}
document.addEventListener('DOMContentLoaded', () => new Presentation());
</script>
</body>
</html>

View File

@@ -0,0 +1,141 @@
---
name: security-review
description: |
프론트엔드 코드에서 XSS, CSRF, 민감 정보 하드코딩, npm 의존성 취약점을 자동 감지하고
우선순위별 보안 리포트와 패치 가이드를 생성합니다.
다음 상황에서 반드시 사용하세요:
- "보안 점검해줘", "취약점 검사해줘", "npm audit 분석해줘"
- PR/배포 전 보안 체크리스트를 확인할 때
- 민감 정보 하드코딩이 의심될 때
---
# 프론트엔드 보안 검토 (security-review)
코드베이스 + npm audit → XSS / CSRF / 민감 정보 / 의존성 취약점 자동 스캔 → 보안 리포트.
## 언제 사용하는가
- PR 머지 전 보안 사전 검토
- 배포 전 최종 보안 체크리스트
- 의존성 업데이트 후 취약점 확인
## 스캔 범위
| 항목 | 설명 |
|---|---|
| XSS | `v-html`, `innerHTML`, `dangerouslySetInnerHTML` 사용 |
| 민감 정보 하드코딩 | API 키, 토큰, 비밀번호, 시크릿 리터럴 |
| CSRF | `SameSite` 쿠키 설정, CORS 구성 |
| 오픈 리다이렉트 | 검증 없는 외부 URL 리다이렉트 |
| npm 의존성 취약점 | CVE 등급별 취약 패키지 |
| 환경 변수 노출 | `NUXT_PUBLIC_` 외 시크릿을 클라이언트에 노출 |
---
## 작업 순서
### Phase 1: 코드 정적 분석
1. 소스 파일을 스캔해 아래 패턴을 탐지한다.
#### XSS 위험
```vue
<!-- ⚠️ 위험: v-html에 사용자 입력 직접 바인딩 -->
<div v-html="userInput" />
<!-- 안전: DOMPurify로 새니타이징 -->
<div v-html="sanitize(userInput)" />
```
탐지 패턴:
- `v-html` + 사용자 입력 변수 바인딩
- `innerHTML =` 직접 할당
- `eval()`, `Function()` 사용
#### 민감 정보 하드코딩
탐지 패턴:
- `apiKey`, `api_key`, `secret`, `password`, `token` 변수에 리터럴 문자열 할당
- `.env` 파일이 아닌 소스 코드에 키 형태 문자열 (예: `sk-...`, `Bearer ...`)
- `nuxt.config.ts`에 시크릿을 `public` 섹션에 노출
```ts
// ⚠️ 위험
const apiKey = 'sk-abc123...'
// ✅ 안전
const apiKey = useRuntimeConfig().apiKey // 서버 전용
```
#### 오픈 리다이렉트
```ts
// ⚠️ 위험: 검증 없는 외부 URL 리다이렉트
const to = route.query.redirect as string
navigateTo(to)
// ✅ 안전: 내부 경로만 허용
const to = route.query.redirect as string
if (to && to.startsWith('/')) navigateTo(to)
```
#### Nuxt 환경 변수 분리 확인
- 서버 전용 시크릿: `runtimeConfig.apiSecret` (클라이언트 미노출)
- 공개 가능한 값만: `runtimeConfig.public.apiBase`
### Phase 2: npm 의존성 취약점 스캔
```bash
npm audit --json
```
결과를 파싱해 CVE 등급별로 분류:
- **Critical / High**: 즉시 패치 필요
- **Moderate**: 스프린트 내 패치
- **Low**: 모니터링
각 취약 패키지에 대해 안전 버전 및 업그레이드 명령어를 제시한다.
### Phase 3: 보안 헤더 확인
`nuxt.config.ts``routeRules` 또는 서버 설정에서 아래 헤더를 확인한다:
```ts
// nuxt.config.ts 권장 보안 헤더
routeRules: {
'/**': {
headers: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
}
}
}
```
---
## 출력 형식
```
## 보안 검토 리포트
### 🔴 Critical (즉시 조치)
- [파일:라인] 취약점 설명 + 수정 코드
### 🟡 High (스프린트 내 조치)
- [파일:라인] 취약점 설명 + 개선 방향
### 🟢 Moderate / Low
- [목록]
### npm 취약점
- Critical: N건 — `npm audit fix --force` 권장 패키지: [목록]
- High: N건 — 안전 버전: [버전 목록]
### 보안 헤더 현황
- 적용됨: [목록]
- 미적용: [목록 + 추가 코드]
### ✅ 정상 항목
- 환경 변수 분리 적절, v-html 미사용 등
```

130
skills/verify-a11y/SKILL.md Normal file
View File

@@ -0,0 +1,130 @@
---
name: verify-a11y
description: |
Vue 3 / HTML 코드를 WCAG 2.1 AA 기준으로 자동 감사하고
ARIA 레이블 누락, 키보드 포커스 순서, 색상 대비 비율 등의 문제와
코드 레벨 개선 방안을 제시합니다.
다음 상황에서 반드시 사용하세요:
- "접근성 검증해줘", "WCAG 체크해줘", "a11y 확인해줘"
- ARIA 속성이 빠진 것 같을 때
- QA 전 접근성 관련 반려를 사전에 방지하고 싶을 때
---
# 접근성 검증 (verify-a11y)
Vue 3 / HTML → WCAG 2.1 AA 자동 감사 → 코드 레벨 개선 방안 리포트.
## 언제 사용하는가
- 마크업 완료 후 접근성 기준 충족 여부를 확인할 때
- 스크린 리더 사용자를 위한 ARIA 적용이 필요할 때
- 키보드 전용 사용자 지원이 필요할 때
## 검사 기준: WCAG 2.1 AA
| 원칙 | 핵심 항목 |
|---|---|
| **인식 가능** | 이미지 대체 텍스트, 색상 대비 비율, 캡션 |
| **운용 가능** | 키보드 접근성, 포커스 순서, 충분한 시간 |
| **이해 가능** | 레이블, 오류 식별, 일관된 네비게이션 |
| **견고성** | 유효한 HTML, ARIA 역할 |
---
## 작업 순서
### Phase 1: 코드 수집
1. 검증 대상 파일을 읽는다. (`.vue`, `.html`, 템플릿 코드)
2. 검증 범위가 넓으면 화면 단위로 분리해 진행한다.
### Phase 2: 체크리스트 점검
#### 이미지 & 미디어
- [ ] 의미 있는 `<img>`: `alt` 텍스트 존재
- [ ] 장식용 `<img>`: `alt=""`
- [ ] `<video>`: 자막(captions) 또는 스크립트 제공
#### 색상 대비
- [ ] 일반 텍스트: 대비 비율 4.5:1 이상
- [ ] 대형 텍스트 (18px+ 또는 14px+ bold): 3:1 이상
- [ ] 아이콘/그래픽 UI: 3:1 이상
#### 키보드 접근성
- [ ] 모든 인터랙션 요소가 키보드로 접근 가능
- [ ] 포커스 순서가 시각적 레이아웃과 일치
- [ ] 포커스 링(outline) 가시성 확보 (`:focus-visible`)
- [ ] 모달/드롭다운 열릴 때 포커스 이동, 닫힐 때 원래 요소로 복귀
#### ARIA 적용
- [ ] `<button>` 아이콘 전용: `aria-label` 또는 `aria-labelledby`
- [ ] 모달: `role="dialog"`, `aria-modal="true"`, `aria-labelledby`
- [ ] 폼 에러: `aria-describedby` → 에러 메시지 연결
- [ ] 로딩 영역: `aria-live="polite"` 또는 `aria-busy="true"`
- [ ] 탭 UI: `role="tablist"`, `role="tab"`, `role="tabpanel"`, `aria-selected`
- [ ] 아코디언: `aria-expanded`, `aria-controls`
- [ ] 드롭다운 메뉴: `aria-haspopup`, `aria-expanded`
#### 폼
- [ ] 모든 입력 필드에 연결된 `<label>` 존재
- [ ] 필수 필드: `aria-required="true"` 또는 `required`
- [ ] 에러 메시지: `role="alert"` 또는 `aria-live="assertive"`
#### 제목 계층
- [ ] `h1``h2``h3` 순서 준수 (건너뛰기 없음)
- [ ] 페이지에 `h1` 1개만 존재
#### 랜드마크
- [ ] `<header>`, `<nav>`, `<main>`, `<footer>` 적절히 사용
- [ ] 여러 `<nav>`가 있으면 `aria-label`로 구분
### Phase 3: 개선 방안 코드 제시
```vue
<!-- Before: 아이콘 버튼 접근성 없음 -->
<button @click="close">
<IconClose />
</button>
<!-- After: aria-label 추가 -->
<button @click="close" aria-label="닫기">
<IconClose aria-hidden="true" />
</button>
```
```vue
<!-- Before: 에러 접근성 없음 -->
<input type="email" />
<p class="text-red-500">올바른 이메일을 입력하세요</p>
<!-- After -->
<input
type="email"
:aria-describedby="hasError ? 'email-error' : undefined"
:aria-invalid="hasError"
/>
<p v-if="hasError" id="email-error" role="alert">
올바른 이메일을 입력하세요
</p>
```
---
## 출력 형식
```
## 접근성 검증 리포트: <파일명>
### 🚨 Critical — WCAG 위반 (N건)
- [라인 NN] 문제 설명 + 수정 코드
### ⚠️ Warning — 개선 권장 (N건)
- [라인 NN] 문제 설명 + 수정 방향
### 💡 Info — 선택 개선 (N건)
- [라인 NN] 개선 사항
### ✅ 적절히 처리된 항목
- ARIA 레이블 적용됨, 색상 대비 충족 등
```

View File

@@ -0,0 +1,87 @@
---
name: verify-component-review
description: Vue 3 / Nuxt 컴포넌트 파일을 팀 공통 지침(gameservice-fe-agent) 기준으로 리뷰할 때 사용합니다. 사용자가 "이 컴포넌트 리뷰해줘", "컨벤션 맞는지 봐줘", "컴포넌트 체크" 등을 요청하면 트리거됩니다.
---
# Vue 컴포넌트 리뷰
이 skill 은 `.vue` 파일 하나 또는 여러 개에 대해 팀 공통 지침 기준으로 리뷰 체크리스트를
수행합니다. 프로젝트 전용 컨벤션(`.claude/project/conventions.md`)이 있으면 그 규칙을
**우선**합니다.
## 체크리스트
### 1. 파일 구조
- [ ] `<script setup lang="ts">` 를 사용하는가? (Options API 금지)
- [ ] 파일 길이가 팀/프로젝트 제한을 넘지 않는가? (공통 200줄, 프로젝트별 오버라이드 확인)
- [ ] 하나의 컴포넌트가 단일 책임을 지키는가?
### 2. Props / Emits
- [ ] `defineProps<T>()` 제네릭 형태로 타입을 명시했는가?
- [ ] `defineEmits<{ ... }>()` 제네릭 형태로 선언했는가?
- [ ] Props 개수가 많다면 객체 props 로 묶여 있는가?
- [ ] 불리언 prop 은 `is`/`has`/`can`/`should` 로 시작하는가?
### 3. 반응성
- [ ] `ref``reactive` 를 팀 규칙대로 사용하고 있는가?
- [ ] 불필요한 `reactive` 래핑이 없는가?
- [ ] 계산된 값은 `computed` 로 뽑아냈는가?
### 4. 스타일 (Tailwind)
- [ ] 임의값 클래스(`w-[123px]`) 남용이 없는가?
- [ ] 색상/간격 토큰을 사용하는가? (임의 색상 금지)
- [ ] 조건부 클래스는 `clsx` / `cn` 로 가독성 확보되었는가?
- [ ] 클래스 순서는 `prettier-plugin-tailwindcss` 규칙을 따르는가?
### 5. 네이밍
- [ ] 파일명: `PascalCase.vue`
- [ ] 이벤트 핸들러: `handle*` 또는 `on*` 접두사
- [ ] 상수: `UPPER_SNAKE_CASE`
### 6. 의존성 / 로직
- [ ] 비즈니스 로직이 컴포넌트에 직접 박혀있지 않고 composable 로 추출되었는가?
- [ ] `$fetch` / `fetch` 직접 호출이 없는가? (프로젝트 규칙에 따라 api wrapper 사용)
- [ ] `any` 타입 사용이 없는가?
## 작업 순서
1. 리뷰 대상 파일을 읽는다. 여러 파일이면 하나씩 순차 처리한다.
2. `.claude/project/conventions.md` 가 있으면 먼저 읽고, 공통 규칙과의 차이를 기억한다.
3. 위 체크리스트를 항목별로 점검하고, 위반 사항을 발견하면 **파일명:라인번호**
함께 문제 요약 + 수정 예시를 제시한다.
4. 단순 포맷 이슈는 "Prettier/ESLint 로 자동 해결 가능" 이라고 덧붙인다.
5. 마지막에 우선순위별 요약(Critical / Warning / Nit)을 출력한다.
## 출력 형식
```
## 리뷰 결과: <파일명>
### 🚨 Critical (반드시 수정)
- [라인 23] Props 타입이 `any` 로 선언됨. `defineProps<{ id: string }>()` 로 변경
- ...
### ⚠️ Warning (수정 권장)
- [라인 45] 파일 길이 210줄. 하위 컴포넌트 2개로 분리 검토
- ...
### 💡 Nit (선택)
- [라인 12] 클래스 순서가 Tailwind 프리셋과 다름
- ...
### ✅ 좋은 점
- 간단한 컴포넌트에 잘 맞는 단일 책임 구조
- ...
```
## 주의사항
- 사용자가 리팩토링을 **요청하지 않은 경우** 코드를 직접 수정하지 말고 리뷰만 수행한다.
- 프로젝트 지침과 공통 지침이 충돌하면 프로젝트 지침을 따르되, 차이를 사용자에게 알린다.
- 실제 코드 동작 변경(기능 수정)은 리뷰 범위가 아니다. 별도 작업으로 분리 제안한다.

141
skills/verify-perf/SKILL.md Normal file
View File

@@ -0,0 +1,141 @@
---
name: verify-perf
description: |
Nuxt 프로젝트의 성능 병목을 분석하고 Core Web Vitals 기준으로
코드 스플리팅, 이미지 최적화, SSR/ISR 전략 등 구체적인 개선 방안을 제시합니다.
다음 상황에서 반드시 사용하세요:
- "성능 최적화해줘", "Lighthouse 점수 올려줘"
- "LCP가 느린데 어떻게 해?", "번들 사이즈 줄여줘"
- Core Web Vitals 기준 미달 시 개선 방향이 필요할 때
---
# 성능 최적화 분석 (verify-perf)
Nuxt 프로젝트 → 성능 병목 자동 분석 → Core Web Vitals 개선 가이드 생성.
## 언제 사용하는가
- 배포 전 성능 기준(LCP < 2.5s / CLS < 0.1 / INP < 200ms) 충족 여부 확인
- Lighthouse 점수 80+ 달성을 위한 구체적 액션이 필요할
- 번들 사이즈 증가, 이미지 로딩 지연 특정 성능 문제가 있을
## Core Web Vitals 목표
| 지표 | 목표 | 기준 |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | 가장 콘텐츠 렌더링 |
| CLS (Cumulative Layout Shift) | < 0.1 | 레이아웃 안정성 |
| INP (Interaction to Next Paint) | < 200ms | 입력 반응성 |
| FCP (First Contentful Paint) | < 1.8s | 콘텐츠 노출 |
---
## 작업 순서
### Phase 1: 코드 분석
1. 아래 파일을 읽어 성능 관련 설정을 파악한다.
- `nuxt.config.ts`: 이미지 모듈, SSR 설정, 번들러 옵션
- `pages/` `components/`: 이미지 태그, 폰트 로딩, 라이브러리 import
- `package.json`: 번들 크기 영향이 패키지 확인
2. 일반적인 병목 패턴을 체크한다.
**번들 크기**
- `import * from '...'` (tree-shaking 비활성화)
- 전체 import 라이브러리 (lodash, moment )
- 대형 아이콘 라이브러리 전체 import
**이미지**
- `<img>` 직접 사용 (Nuxt `<NuxtImg>` 미사용)
- `width`, `height` 미지정 (CLS 발생)
- 레이지 로딩 미적용
**렌더링 전략**
- 정적 콘텐츠에 `useFetch` 남용
- 불필요한 CSR 컴포넌트
### Phase 2: 개선 방안 생성
#### 이미지 최적화
```vue
<!-- Before -->
<img src="/hero.jpg" alt="히어로" />
<!-- After: Nuxt Image 모듈 활용 -->
<NuxtImg
src="/hero.jpg"
alt="히어로"
width="1200"
height="600"
loading="lazy"
format="webp"
/>
```
#### 번들 최적화
```ts
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'pinia'],
}
}
}
}
}
})
```
#### SSR / SSG / ISR 전략
```ts
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // 정적 홈
'/products/**': { swr: 3600 }, // ISR: 1시간 캐시
'/dashboard/**': { ssr: false }, // CSR: 인증 필요 페이지
}
})
```
#### 컴포넌트 지연 로딩
```vue
<!-- 무거운 컴포넌트 lazy 로딩 -->
<LazyHeavyChart v-if="isVisible" />
```
### Phase 3: 우선순위 정렬
발견된 개선사항을 impact/effort 매트릭스로 정렬한다:
- 🔴 High impact, Low effort 즉시 적용
- 🟡 High impact, High effort 스프린트 계획
- 🟢 Low impact, Low effort 여유 있을
- Low impact, High effort 보류
---
## 출력 형식
```
## 성능 분석 리포트
### 현황 추정
- 예상 LCP: ~ (분석된 병목 기준)
- 주요 이슈: [번들 사이즈 | 이미지 | 렌더링 전략]
### 🔴 즉시 적용 (High impact, Low effort)
1. [문제] → [해결책 + 코드]
### 🟡 스프린트 계획 (High impact, High effort)
1. [문제] → [해결 방향]
### 예상 개선 효과
- LCP: Xs → Xs
- 번들: XkB → XkB (XX% 감소)
```

View File

@@ -0,0 +1,90 @@
---
name: verify-requirement
description: |
plan-analyzer가 생성한 요구사항 명세와 실제 구현 코드를 자동 비교하여
누락 기능, 스펙 불일치, 미구현 항목을 사전에 감지합니다.
다음 상황에서 반드시 사용하세요:
- "요구사항 대비 누락 기능 체크해줘", "기획서랑 구현 맞는지 확인해줘"
- QA 전 스펙 불일치를 미리 잡고 싶을 때
- 개발 완료 후 기획 의도와 일치하는지 검증할 때
---
# 요구사항 검증 (verify-requirement)
plan-analyzer 명세 ↔ 실제 구현 코드 자동 비교 → 누락/불일치 리포트 생성.
## 언제 사용하는가
- 개발 완료 후 QA 이전에 스펙 누락 여부를 확인할 때
- 기획자와 개발자 간 스펙 해석 차이를 사전에 해소할 때
- 릴리스 전 체크리스트를 자동화하고 싶을 때
## 입력
- `plan-analyzer` 산출물 (요구사항 명세 MD 파일)
- 검증 대상 디렉토리 또는 파일 목록
- (선택) 검증 범위: 화면 목록, API 엔드포인트, 컴포넌트 트리
---
## 작업 순서
### Phase 1: 요구사항 명세 파싱
1. plan-analyzer 산출물에서 아래 항목을 추출한다.
- 화면(페이지) 목록과 라우팅 경로
- 각 화면의 핵심 기능 목록
- API 엔드포인트 목록
- 컴포넌트 트리
2. 명세가 없는 경우 사용자에게 파일 경로를 요청한다.
### Phase 2: 구현 현황 파악
1. `pages/` 디렉토리를 스캔해 실제 라우팅 구현 현황을 파악한다.
2. `components/` 디렉토리에서 구현된 컴포넌트 목록을 수집한다.
3. `server/api/` 에서 구현된 엔드포인트를 파악한다.
4. 각 화면 파일에서 아래를 확인한다.
- 기획서에 명시된 기능 구현 여부
- 폼/버튼/모달 등 인터랙션 요소 구현 여부
- 에러/로딩/빈 상태 처리 여부
### Phase 3: 비교 및 분류
발견된 차이를 3단계로 분류한다:
| 등급 | 기준 | 예시 |
|---|---|---|
| 🚨 Critical | 핵심 기능 미구현 | 결제 버튼 없음, 필수 API 미연동 |
| ⚠️ Warning | 일부 기능 누락 또는 스펙 불일치 | 에러 상태 처리 없음, 페이지네이션 미구현 |
| 💡 Info | 선택 기능 누락 또는 UX 개선 사항 | 로딩 스피너 없음, 빈 상태 메시지 없음 |
### Phase 4: 개선 방안 제시
각 Critical / Warning 항목에 대해 구체적인 구현 방향을 제안한다.
---
## 출력 형식
```
## 요구사항 검증 리포트
### 검증 범위
- 화면: N개 / 구현: N개 (일치율 NN%)
- API: N개 / 구현: N개 (일치율 NN%)
- 컴포넌트: N개 / 구현: N개 (일치율 NN%)
### 🚨 Critical (N건)
- [화면명] 기능 설명 — 구체적 해결 방안
### ⚠️ Warning (N건)
- [화면명] 기능 설명 — 구체적 해결 방안
### 💡 Info (N건)
- [화면명] 개선 사항
### ✅ 일치 항목
- [목록]
```

View File

@@ -0,0 +1,143 @@
---
name: verify-seo-geo
description: |
Nuxt 프로젝트를 SEO · AEO · GEO 3계층으로 자동 감사하고
useSeoMeta, useSchemaOrg 기반 메타/구조화 데이터 코드를 자동 생성합니다.
AI 검색(ChatGPT, Perplexity, Google AI Overview) 대응까지 포함합니다.
다음 상황에서 반드시 사용하세요:
- "SEO 검증해줘", "메타 태그 확인해줘", "AEO/GEO 대응해줘"
- Schema.org 구조화 데이터가 필요할 때
- AI 검색 노출을 높이고 싶을 때
---
# SEO · GEO · AEO 검증 (verify-seo-geo)
Nuxt 프로젝트 → SEO / AEO / GEO 3계층 자동 감사 → 메타 + Schema 코드 생성.
## 언제 사용하는가
- 배포 전 SEO 메타 누락 여부를 확인할 때
- AI 검색 엔진(ChatGPT, Perplexity 등)에 콘텐츠 인용률을 높이고 싶을 때
- Schema.org 구조화 데이터를 처음 적용할 때
## 3계층 정의
| 계층 | 목표 | 핵심 기술 |
|---|---|---|
| **SEO** | 전통 검색 엔진 노출 | 메타 태그, Open Graph, sitemap |
| **AEO** (Answer Engine Optimization) | 검색 결과 직접 답변(Featured Snippet) | FAQ Schema, HowTo Schema |
| **GEO** (Generative Engine Optimization) | AI 검색 인용 | 구조화 데이터, 명확한 문서 구조 |
---
## 작업 순서
### Phase 1: 현황 감사
1. 대상 페이지 파일을 읽어 아래 항목을 체크한다.
#### SEO 기본 체크리스트
- [ ] `useSeoMeta()` 사용 여부
- [ ] `title`, `description` 설정 여부 (title 50~60자, description 150~160자)
- [ ] `ogTitle`, `ogDescription`, `ogImage` 설정 여부
- [ ] `canonical` URL 설정 여부
- [ ] `<NuxtImg>` 사용 + `alt` 텍스트 여부
- [ ] H1 태그 존재 및 키워드 포함 여부
- [ ] 페이지 로딩 속도 (verify-perf 연계)
#### AEO / GEO 체크리스트
- [ ] FAQ, HowTo, Article 등 Schema.org 적용 여부
- [ ] 구조화 데이터 JSON-LD 유효성
- [ ] 콘텐츠 답변 가능성 (질문 → 명확한 답변 구조)
### Phase 2: SEO 코드 생성
```ts
// pages/product/[id].vue
useSeoMeta({
title: `${product.name} | 브랜드명`,
description: product.description.slice(0, 155),
ogTitle: `${product.name} | 브랜드명`,
ogDescription: product.description.slice(0, 155),
ogImage: product.imageUrl,
ogType: 'product',
twitterCard: 'summary_large_image',
})
useHead({
link: [{ rel: 'canonical', href: `https://example.com/product/${product.id}` }],
})
```
### Phase 3: Schema.org 구조화 데이터
#### FAQ Schema (AEO)
```ts
useSchemaOrg([
defineQuestion({
name: '자주 묻는 질문 1',
acceptedAnswer: { text: '답변 내용' },
}),
])
```
#### HowTo Schema
```ts
useSchemaOrg([
defineHowTo({
name: '사용 방법',
step: [
{ name: '1단계', text: '설명' },
{ name: '2단계', text: '설명' },
],
}),
])
```
#### Article Schema (GEO)
```ts
useSchemaOrg([
defineArticle({
headline: article.title,
description: article.summary,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
author: [{ name: article.author }],
}),
])
```
### Phase 4: GEO 콘텐츠 구조 개선 제안
AI 검색 인용을 높이기 위한 콘텐츠 구조 권고:
- 명확한 소제목(H2/H3)으로 콘텐츠 분절
- 핵심 개념은 첫 단락에 요약
- 테이블/리스트로 스캔 가능한 정보 구조화
- 정의·비교·절차 형식의 콘텐츠 우선 작성
---
## 출력 형식
```
## SEO/GEO/AEO 검증 리포트: <페이지명>
### SEO 현황
- title: 있음 / 없음 / 글자수 초과
- description: 있음 / 없음
- OG 태그: NN/6 항목 적용
- Schema.org: 없음 / FAQ / Article / ...
### 🚨 Critical
- [누락된 필수 메타 항목]
### ⚠️ Warning
- [개선 권장 항목]
### 생성된 코드
[useSeoMeta + useSchemaOrg 코드]
### GEO 개선 제안
[콘텐츠 구조 개선 방향]
```

View File

@@ -0,0 +1,109 @@
---
name: work-code-reviewer
description: |
현재 브랜치 변경 코드 또는 특정 파일을 받아 팀 공통 지침 기준으로 종합 리뷰를 수행합니다.
컨벤션, 로직, 보안, 성능, 접근성을 통합 검토하고 우선순위별 리포트를 생성합니다.
다음 상황에서 반드시 사용하세요:
- "변경된 코드 리뷰해줘", "이 파일 코드 리뷰해줘"
- PR 올리기 전 셀프 리뷰가 필요할 때
- 특정 파일의 전반적인 품질을 점검할 때
---
# 코드 리뷰어 (work-code-reviewer)
변경 코드 / 파일 → 컨벤션 · 로직 · 보안 · 성능 통합 리뷰 → 우선순위별 리포트.
## 언제 사용하는가
- MR 올리기 전 셀프 리뷰 / 사전 점검
- 특정 파일의 전반적인 코드 품질 검토
- 리뷰 요청 전 자가 진단
## 입력
- `git diff` 출력, 파일 경로 목록, 또는 코드 스니펫
- (선택) 리뷰 초점 (컨벤션 / 로직 / 보안 / 전체)
---
## 작업 순서
### Phase 1: 검토 범위 파악
1. 대상 파일 또는 diff를 읽는다.
2. 파일 유형별로 적용할 검토 항목을 결정한다.
- `.vue` → 컨벤션 + 컴포넌트 구조 + 접근성
- `.ts` → 타입 안전성 + 로직
- `server/` → 보안 + 에러 핸들링
- `stores/` → 상태 관리 패턴
- `composables/` → 부수효과 최소화
### Phase 2: 통합 체크리스트
#### 코드 품질
- [ ] 함수/파일 단일 책임 원칙
- [ ] 중복 코드 없음 (재사용 가능 composable/유틸 추출 여부)
- [ ] 매직 넘버/문자열 → 상수 또는 i18n 키
- [ ] 불필요한 `console.log` 제거
#### 타입 안전성
- [ ] `any` 타입 미사용
- [ ] 외부 API 응답 타입 정의
- [ ] 함수 파라미터/반환 타입 명시
#### 에러 핸들링
- [ ] API 호출 try-catch 또는 onError 처리
- [ ] 에러 상태 UI 존재 여부
- [ ] 엣지 케이스 (null, undefined, 빈 배열) 처리
#### 성능
- [ ] 불필요한 watch / watchEffect 없음
- [ ] computed 캐싱 활용
- [ ] 과도한 reactive 래핑 없음
#### 보안
- [ ] `v-html` 입력 새니타이징
- [ ] 민감 정보 하드코딩 없음
- [ ] 클라이언트 노출 불필요한 데이터 없음
#### Vue 컨벤션 (`.vue` 파일)
`verify-component-review` 스킬의 체크리스트 전체 적용.
### Phase 3: 리뷰 코멘트 작성
`work-mr-reviewer`의 코멘트 형식과 동일:
- 🚨 Critical / ⚠️ Warning / 💡 Nit 3단계 분류
- 파일명:라인번호 명시
- 수정 코드 예시 포함
### Phase 4: 요약
개선 전/후 예상 품질 변화를 간단히 서술한다.
---
## 출력 형식
```
## 코드 리뷰: <파일명 또는 기능명>
### 검토 요약
- 파일: N개
- 주요 이슈: [컨벤션 | 로직 | 보안 | 성능]
### 🚨 Critical (N건)
[파일:라인] 문제 + 수정 코드
### ⚠️ Warning (N건)
[파일:라인] 문제 + 수정 방향
### 💡 Nit (N건)
[목록]
### ✅ 잘된 점
[1~3개]
### 개선 후 기대 효과
[한 줄 요약]
```

View File

@@ -0,0 +1,111 @@
---
name: work-mr-reviewer
description: |
GitLab MR URL 또는 diff를 받아 팀 공통 지침(gameservice-fe-agent) 기준으로
코드 리뷰 코멘트 초안을 자동 생성합니다. 리뷰어가 비즈니스 로직에 집중할 수 있도록
컨벤션/스타일 지적은 AI가 사전 처리합니다.
다음 상황에서 반드시 사용하세요:
- "이 MR 리뷰해줘", "MR 코멘트 작성해줘"
- "이 PR 어떤지 봐줘: <URL>"
- 리뷰어 역할로 MR을 검토하기 전 사전 검토가 필요할 때
---
# GitLab MR 리뷰어 (work-mr-reviewer)
MR URL 또는 diff → 팀 공통 지침 기준 리뷰 코멘트 초안 자동 생성.
## 언제 사용하는가
- MR 리뷰 요청을 받았을 때 사전 분석이 필요할 때
- 리뷰 코멘트 작성 시간을 줄이고 싶을 때
- 컨벤션 위반 사항을 일괄 감지하고 싶을 때
## 입력
- GitLab MR URL 또는 `git diff` 출력
- (선택) 리뷰 우선순위 (컨벤션 중심 / 로직 중심 / 전체)
---
## 작업 순서
### Phase 1: 변경사항 파악
1. MR URL이 제공된 경우 diff를 가져온다.
2. 변경된 파일 목록과 변경 규모를 파악한다.
3. 변경 유형을 분류한다: 신규 기능 / 버그픽스 / 리팩토링 / 설정 변경
### Phase 2: 자동 컨벤션 검토
`verify-component-review` 스킬의 체크리스트를 변경된 Vue 파일에 적용한다:
- `<script setup lang="ts">` 사용 여부
- `defineProps<T>()` / `defineEmits<T>()` 제네릭 형태
- `any` 타입 사용 여부
- Tailwind 클래스 7단계 순서
- 네이밍 컨벤션 (camelCase / PascalCase / UPPER_SNAKE_CASE)
- import 순서
### Phase 3: 로직 / 구조 검토
- 컴포넌트 분리 기준 적절성 (200줄 초과 여부)
- 비즈니스 로직의 composable 분리 여부
- 에러 핸들링 / 로딩 상태 처리 여부
- 불필요한 API 호출 또는 상태 중복 여부
- 보안 취약점 (XSS, 민감 정보 등)
### Phase 4: 코멘트 작성
리뷰 코멘트는 아래 형식을 따른다:
```
[등급] 파일명:라인번호
문제: [간단한 설명]
이유: [왜 문제인지 — 팀 규칙 또는 Best Practice 근거]
제안:
\`\`\`
[수정 코드 예시]
\`\`\`
```
등급:
- 🚨 **Blocker** — 반드시 수정 후 머지 (보안, 런타임 에러, 명백한 버그)
- ⚠️ **Major** — 수정 권장 (컨벤션 위반, 성능 문제)
- 💡 **Minor** — 선택적 개선 (가독성, 스타일)
- 💬 **Question** — 로직 확인 필요 (질문 형태)
### Phase 5: 요약
- 전체 변경사항에 대한 한 줄 요약
- Blocker 건수 명시
- 칭찬할 만한 좋은 패턴 1~2개 언급
---
## 출력 형식
```
## MR 리뷰: <MR 제목>
### 변경 요약
- 변경 파일: N개
- 유형: 신규 기능 / 버그픽스 / ...
### 🚨 Blocker (N건)
...
### ⚠️ Major (N건)
...
### 💡 Minor (N건)
...
### 💬 Questions
...
### ✅ 잘된 점
- [좋은 패턴 1~2개]
> 이 리뷰는 AI 초안입니다. 최종 리뷰어의 판단으로 조정해 주세요.
```