✨ feat: 새로운 스킬 및 문서 추가 (EDM HTML, 요구사항 분석기, 번역 코드 생성기)
This commit is contained in:
BIN
.claude/skills/.DS_Store
vendored
Normal file
BIN
.claude/skills/.DS_Store
vendored
Normal file
Binary file not shown.
52
.claude/skills/README.md
Normal file
52
.claude/skills/README.md
Normal 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` 실행
|
||||||
78
.claude/skills/conventional-commit/SKILL.md
Normal file
78
.claude/skills/conventional-commit/SKILL.md
Normal 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
BIN
.claude/skills/markup-edm/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: edm-email-html
|
name: markup-edm
|
||||||
description: |
|
description: |
|
||||||
EDM(이메일 다이렉트 마케팅) HTML을 구현하는 전체 워크플로우 스킬.
|
EDM(이메일 다이렉트 마케팅) HTML을 구현하는 전체 워크플로우 스킬.
|
||||||
Figma 디자인 → HTML table 마크업 → 아웃룩 호환 → 검수까지 단계별 가이드를 제공합니다.
|
Figma 디자인 → HTML table 마크업 → 아웃룩 호환 → 검수까지 단계별 가이드를 제공합니다.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: requirement-analyzer
|
name: plan-analyzer
|
||||||
description: |
|
description: |
|
||||||
PPT 기획서를 분석하여 요구사항 명세서를 자동 생성합니다.
|
PPT 기획서를 분석하여 요구사항 명세서를 자동 생성합니다.
|
||||||
Nuxt pages 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록,
|
Nuxt pages 라우팅 구조, 컴포넌트 트리, API 엔드포인트 목록,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: translation-keys
|
name: plan-translation-generator
|
||||||
description: 번역 요청 엑셀 파일의 EN 셀을 기반으로 '번역코드' 컬럼에 함축적인 코드를 자동 생성합니다. 사용자가 "번역코드 만들어줘", "translation key 생성", "번역 키 추출" 등을 요청하면 트리거됩니다.
|
description: 번역 요청 엑셀 파일의 EN 셀을 기반으로 '번역코드' 컬럼에 함축적인 코드를 자동 생성합니다. 사용자가 "번역코드 만들어줘", "translation key 생성", "번역 키 추출" 등을 요청하면 트리거됩니다.
|
||||||
---
|
---
|
||||||
|
|
||||||
268
.claude/skills/ppt-maker/SKILL.md
Normal file
268
.claude/skills/ppt-maker/SKILL.md
Normal 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 콘텐츠만 교체하여 새 프레젠테이션을 만든다.
|
||||||
914
.claude/skills/ppt-maker/references/template.html
Normal file
914
.claude/skills/ppt-maker/references/template.html
Normal 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"><</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide-viewport"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">section</span> <span class="attr">class</span>=<span class="str">"slide active"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide__inner"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">h2</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">></span><span class="var">제목</span><span class="op"></</span><span class="tag">h2</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">p</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">></span><span class="var">내용</span><span class="op"></</span><span class="tag">p</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"></</span><span class="tag">div</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"></</span><span class="tag">section</span><span class="op">></span></span>
|
||||||
|
<span class="line"><span class="op"></</span><span class="tag">div</span><span class="op">></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="이전 슬라이드">‹</button>
|
||||||
|
<button class="nav-btn" id="btnNext" aria-label="다음 슬라이드">›</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>
|
||||||
165
.claude/skills/project-init/SKILL.md
Normal file
165
.claude/skills/project-init/SKILL.md
Normal 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 은 사용자 명시 요청이 없으면 실행하지 않는다.
|
||||||
@@ -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) 기준으로 리뷰할 때 사용합니다. 사용자가 "이 컴포넌트 리뷰해줘", "컨벤션 맞는지 봐줘", "컴포넌트 체크" 등을 요청하면 트리거됩니다.
|
||||||
---
|
---
|
||||||
|
|
||||||
454
.claude/skills/work-log/SKILL.md
Normal file
454
.claude/skills/work-log/SKILL.md
Normal 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` 지원 추가됨
|
||||||
85
docs/WDG00.04.02.06.07.01 AI 활용 - CBO-플랫폼서비스개발담당.md
Normal file
85
docs/WDG00.04.02.06.07.01 AI 활용 - CBO-플랫폼서비스개발담당.md
Normal 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 자동 검증 |
|
||||||
794
docs/fe-ai-reference-flow 2.html
Normal file
794
docs/fe-ai-reference-flow 2.html
Normal 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 <script setup lang="ts">, 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<T>()에 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 · /verify-component-review · /plan-analyzer · /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>
|
||||||
794
docs/fe-ai-reference-flow.html
Normal file
794
docs/fe-ai-reference-flow.html
Normal 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 <script setup lang="ts">, 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<T>()에 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 · /verify-component-review · /plan-analyzer · /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
612
docs/fe-ai-rules.html
Normal 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><script setup lang="ts"> 기본</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> 포맷팅 & 네이밍</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"><script setup lang="ts"> 필수, 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<T>() 제네릭으로 타입 명시</span></li>
|
||||||
|
<li><span class="kv-key">Emits</span><span class="kv-val">defineEmits<{ ... }>() 제네릭으로 선언</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
800
docs/fe-ai-workflow.html
Normal 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>
|
||||||
662
html/fe-agent-structure.html
Normal file
662
html/fe-agent-structure.html
Normal 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 — 커밋 & 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>
|
||||||
794
html/fe-ai-reference-flow.html
Normal file
794
html/fe-ai-reference-flow.html
Normal 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 <script setup lang="ts">, 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<T>()에 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 · /verify-component-review · /plan-analyzer · /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
612
html/fe-ai-rules.html
Normal 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><script setup lang="ts"> 기본</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> 포맷팅 & 네이밍</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"><script setup lang="ts"> 필수, 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<T>() 제네릭으로 타입 명시</span></li>
|
||||||
|
<li><span class="kv-key">Emits</span><span class="kv-val">defineEmits<{ ... }>() 제네릭으로 선언</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
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
800
html/fe-ai-workflow.html
Normal 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>
|
||||||
576
session-report-20260512-2348.html
Normal file
576
session-report-20260512-2348.html
Normal file
File diff suppressed because one or more lines are too long
BIN
skills/.DS_Store
vendored
Normal file
BIN
skills/.DS_Store
vendored
Normal file
Binary file not shown.
150
skills/dev-api-state/SKILL.md
Normal file
150
skills/dev-api-state/SKILL.md
Normal 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 → 스토어 → 컴포넌트 순]
|
||||||
|
```
|
||||||
125
skills/dev-component/SKILL.md
Normal file
125
skills/dev-component/SKILL.md
Normal 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
128
skills/dev-docs/SKILL.md
Normal 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 차이, 캐시 주의 등]
|
||||||
|
```
|
||||||
149
skills/dev-storybook/SKILL.md
Normal file
149
skills/dev-storybook/SKILL.md
Normal 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
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
160
skills/dev-unit-test/SKILL.md
Normal file
160
skills/dev-unit-test/SKILL.md
Normal 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
125
skills/markup-base/SKILL.md
Normal 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
BIN
skills/markup-edm/.DS_Store
vendored
Normal file
Binary file not shown.
310
skills/markup-edm/SKILL.md
Normal file
310
skills/markup-edm/SKILL.md
Normal 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` — 전체 검수 체크리스트 (시각적/기능/스팸)
|
||||||
24
skills/markup-edm/assets/example_asset.txt
Normal file
24
skills/markup-edm/assets/example_asset.txt
Normal 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.
|
||||||
34
skills/markup-edm/references/api_reference.md
Normal file
34
skills/markup-edm/references/api_reference.md
Normal 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
|
||||||
327
skills/markup-edm/references/html-patterns.md
Normal file
327
skills/markup-edm/references/html-patterns.md
Normal 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>
|
||||||
|
|
|
||||||
|
<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;"> </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;"> </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>
|
||||||
|
```
|
||||||
19
skills/markup-edm/scripts/example.py
Executable file
19
skills/markup-edm/scripts/example.py
Executable 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()
|
||||||
107
skills/markup-figma/SKILL.md
Normal file
107
skills/markup-figma/SKILL.md
Normal 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, 링크 등 확정 필요 항목]
|
||||||
|
```
|
||||||
138
skills/markup-promotion/SKILL.md
Normal file
138
skills/markup-promotion/SKILL.md
Normal 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 값 확정
|
||||||
|
```
|
||||||
170
skills/plan-analyzer/SKILL.md
Normal file
170
skills/plan-analyzer/SKILL.md
Normal 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` 은 사용자 명시 요청이 없으면 실행하지 않는다.
|
||||||
159
skills/plan-analyzer/references/output-template.md
Normal file
159
skills/plan-analyzer/references/output-template.md
Normal 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 페이지에 바로 붙여 사용할 수 있습니다.
|
||||||
315
skills/plan-analyzer/scripts/extract_pptx.py
Normal file
315
skills/plan-analyzer/scripts/extract_pptx.py
Normal 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()
|
||||||
138
skills/plan-translation-generator/SKILL.md
Normal file
138
skills/plan-translation-generator/SKILL.md
Normal 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`
|
||||||
|
- 사용자가 코드를 직접 수정 요청하면 수정 후 재저장한다.
|
||||||
@@ -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
268
skills/ppt-maker/SKILL.md
Normal 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 콘텐츠만 교체하여 새 프레젠테이션을 만든다.
|
||||||
914
skills/ppt-maker/references/template.html
Normal file
914
skills/ppt-maker/references/template.html
Normal 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"><</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide-viewport"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">section</span> <span class="attr">class</span>=<span class="str">"slide active"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">div</span> <span class="attr">class</span>=<span class="str">"slide__inner"</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">h2</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">></span><span class="var">제목</span><span class="op"></</span><span class="tag">h2</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"><</span><span class="tag">p</span> <span class="attr">class</span>=<span class="str">"reveal"</span><span class="op">></span><span class="var">내용</span><span class="op"></</span><span class="tag">p</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"></</span><span class="tag">div</span><span class="op">></span></span>
|
||||||
|
<span class="line"> <span class="op"></</span><span class="tag">section</span><span class="op">></span></span>
|
||||||
|
<span class="line"><span class="op"></</span><span class="tag">div</span><span class="op">></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="이전 슬라이드">‹</button>
|
||||||
|
<button class="nav-btn" id="btnNext" aria-label="다음 슬라이드">›</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>
|
||||||
141
skills/security-review/SKILL.md
Normal file
141
skills/security-review/SKILL.md
Normal 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
130
skills/verify-a11y/SKILL.md
Normal 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 레이블 적용됨, 색상 대비 충족 등
|
||||||
|
```
|
||||||
87
skills/verify-component-review/SKILL.md
Normal file
87
skills/verify-component-review/SKILL.md
Normal 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
141
skills/verify-perf/SKILL.md
Normal 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% 감소)
|
||||||
|
```
|
||||||
90
skills/verify-requirement/SKILL.md
Normal file
90
skills/verify-requirement/SKILL.md
Normal 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건)
|
||||||
|
- [화면명] 개선 사항
|
||||||
|
|
||||||
|
### ✅ 일치 항목
|
||||||
|
- [목록]
|
||||||
|
```
|
||||||
143
skills/verify-seo-geo/SKILL.md
Normal file
143
skills/verify-seo-geo/SKILL.md
Normal 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 개선 제안
|
||||||
|
[콘텐츠 구조 개선 방향]
|
||||||
|
```
|
||||||
109
skills/work-code-reviewer/SKILL.md
Normal file
109
skills/work-code-reviewer/SKILL.md
Normal 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개]
|
||||||
|
|
||||||
|
### 개선 후 기대 효과
|
||||||
|
[한 줄 요약]
|
||||||
|
```
|
||||||
111
skills/work-mr-reviewer/SKILL.md
Normal file
111
skills/work-mr-reviewer/SKILL.md
Normal 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 초안입니다. 최종 리뷰어의 판단으로 조정해 주세요.
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user