feat: 공통 Claude 지침 및 규칙 문서 추가

This commit is contained in:
“hyeonggkim”
2026-04-13 11:05:56 +09:00
parent 2f4532815e
commit 9e2c7cd183
24 changed files with 2013 additions and 526 deletions

70
.cursorrules Normal file
View File

@@ -0,0 +1,70 @@
# Nuxt 프로젝트 — Cursor 규칙
## 언어
- 사용자와의 대화·설명·커밋 메시지 본문은 **한국어**를 사용한다.
## Git 커밋 메시지 (필수)
- **제목은 한 줄**, [Conventional Commits](https://www.conventionalcommits.org/) + **이모지**를 함께 쓴다.
- 형식: `<이모지> <type>: <한글 설명>` (이모지·타입·콜론 뒤 공백 한 칸)
- **설명(제목 본문)은 반드시 한글**로 작성한다. 영어 제목만 단독으로 쓰지 않는다.
- **명령형** 어조 (`추가`, `수정` — `추가됨`, `수정함` 지양).
- **첫 줄(제목)은 72자 미만**을 권장한다.
- **원자적 커밋**: 한 커밋에 단일 목적만 담는다. 관련 없는 변경은 분할한다.
- 커밋 생성 시 **Claude·AI 서명·Co-authored-by 등 메타 서명을 본문에 넣지 않는다.**
### 커밋 생성 프로세스 (`/commit` 등 요청 시)
1. **스테이지된 파일**이 있으면 그 파일만 대상으로 커밋한다. 없으면 사용자에게 스테이징 여부를 확인한다.
2. `git diff` 등으로 **논리적 변경 덩어리**를 분석한다.
3. 타입이 섞였거나 관심사가 다르면 **분할 커밋**을 제안한다.
4. 아래 **이모지 맵**과 타입에 맞춰 제목을 만든다.
### 타입 (type)
| type | 용도 |
|------|------|
| `feat` | 새 기능 |
| `fix` | 버그 수정 |
| `docs` | 문서 |
| `style` | 포맷팅·세미콜론 등 의미 없는 스타일 |
| `refactor` | 리팩터링 |
| `perf` | 성능 개선 |
| `test` | 테스트 |
| `chore` | 빌드·도구·잡무 |
| `ci` | CI |
| `build` | 빌드 시스템·번들러 |
### 이모지 맵 (타입·맥락에 맞게 선택)
✨ feat | 🐛 fix | 📝 docs | 💄 style | ♻️ refactor | ⚡ perf | ✅ test | 🔧 chore | 🚀 ci | 🚨 warnings | 🔒️ security | 🚚 move | 🏗️ architecture | add-dep | remove-dep | 🌱 seed | 🧑‍💻 dx | 🏷️ types | 👔 business | 🚸 ux | 🩹 minor-fix | 🥅 errors | 🔥 remove | 🎨 structure | 🚑️ hotfix | 🎉 init | 🔖 release | 🚧 wip | 💚 ci-fix | 📌 pin-deps | 👷 ci-build | 📈 analytics | ✏️ typos | ⏪️ revert | 📄 license | 💥 breaking | 🍱 assets | ♿️ accessibility | 💡 comments | 🗃️ db | 🔊 logs | 🔇 remove-logs | 🙈 gitignore | 📸 snapshots | ⚗️ experiment | 🚩 flags | 💫 animations | ⚰️ dead-code | 🦺 validation | ✈️ offline
### 분할 제안 기준
- 서로 다른 **관심사**가 한 diff에 섞인 경우
- **타입**이 혼합된 경우 (예: `feat` + `fix`)
- **파일 패턴**이 완전히 다른 영역(예: 앱 코드 vs 인프라만)인 경우
- **변경량이 크고** 커밋 단위로 나눌 수 있는 경우
### 예시
- `✨ feat: 로그인 폼 유효성 검사 추가`
- `♻️ refactor: 사용자 API 호출 로직을 composable로 분리`
- `🐛 fix: 모바일에서 헤더가 겹치는 문제 수정`
### 본문이 필요할 때
- 제목 아래 빈 줄 후 본문을 한글 bullet 또는 문단으로 적는다.
## 코드·작업 방식
- 요청 범위 밖의 리팩터·포맷 일괄 변경·무관 파일 수정을 하지 않는다.
- 기존 코드의 네이밍, import 스타일, 타입·주석 수준에 맞춘다.
- 변경 이유가 드러나는 **작고 집중된 diff**를 선호한다.
## Cursor 사용 시
- 파일을 수정하기 전에 관련 맥락(주변 코드·설정)을 읽고 일관되게 맞춘다.
- 사용자가 명시적으로 요청하지 않은 README·문서 파일은 새로 쓰거나 크게 늘리지 않는다.

View File

@@ -1,62 +1,39 @@
# CLAUDE.md # 팀 공통 Claude 지침 (fe-common-rules)
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 이 파일은 팀 전체에서 공통으로 사용하는 Claude 작업 지침의 **엔트리 포인트**입니다.
각 프로젝트의 `CLAUDE.md`에서 `@.claude/common/CLAUDE.md` 형태로 참조하여 사용합니다.
## Project Overview ## 적용 범위
- **Type**: Game service frontend agent (submodule-based monorepo) - 팀 내 모든 프론트엔드 프로젝트
- **Stack**: Nuxt 3/4, TypeScript (strict), pnpm - Claude Code / Claude 기반 자동화 작업 전반
- **Language**: Korean (코드 리뷰, 커밋 메시지, 문서)
## Commands ## 포함된 지침
### Development 아래 문서들은 Claude가 이 파일을 읽을 때 자동으로 함께 로드됩니다.
```bash
pnpm install # Install dependencies
pnpm dev # Run dev server
pnpm build # Production build
pnpm preview # Preview build
```
### Validation @rules/coding-conventions.md
```bash
pnpm build # Verify no build errors
pnpm typecheck # Verify TypeScript (if configured)
```
### Submodules @rules/framework-rules.md
```bash
git submodule update --init --recursive # Initialize
git submodule update --remote --recursive # Update all
```
## Git Workflow @rules/commit-pr.md
### Branches @rules/claude-workflow.md
- `release` - Production (verified)
- `dev` - Development/testing
- Feature branches from `dev`
### MR Guidelines ## 우선순위
- **Title**: `[Category] Description` (Feature, Fix, Refactor, Docs)
- **Target**: `dev` first, then `release` after verification
- **Review**: 1+ team member approval required
## Context Files 1. 프로젝트 `CLAUDE.md`에 명시된 **프로젝트 지침**이 최우선입니다.
2. 이 공통 지침은 프로젝트 지침과 충돌하지 않는 범위 내에서 항상 적용됩니다.
3. 충돌이 발생하면 프로젝트 지침을 따르되, 차이를 사용자에게 알려주세요.
Additional context in `.claude/` directory: ## 공통 Skill
- `contexts/` - Nuxt patterns, TypeScript conventions
- `skills/` - code-review, test-generator, refactor-component
- `agents/` - debugger, refactor, performance-optimizer
## Project-Specific Patterns 공통 저장소는 `skills/` 폴더에 팀 공용 Claude skill 들을 함께 관리합니다.
프로젝트는 `scripts/link-skills.sh``.claude/skills/` 에 심볼릭 링크하여 사용합니다.
자세한 내용은 `skills/README.md` 를 참고하세요.
### Common Nuxt Issues ## 업데이트
- Hydration mismatch → Use `<ClientOnly>`
- useFetch infinite loop → Check watch/immediate/key options
### Key Conventions - 공통 지침은 이 레파지토리(`fe-common-rules`)에서만 수정합니다.
- Use Composition API with `<script setup lang="ts">` - 각 프로젝트는 `scripts/update.sh`(또는 `git submodule update --remote`)로 최신 버전을 받아갑니다.
- Composables for reusable logic - 수정 제안은 PR로 받습니다. 자세한 내용은 루트 `README.md` 참고.
- Pinia for complex state management
- Always type API responses: `useFetch<Type>()`

316
README.md
View File

@@ -1,162 +1,238 @@
# gameservice-fe-agent # fe-common-rules
게임 서비스 프론트엔드 에이전트 프로젝트 팀 전체에서 공통으로 사용하는 **프론트엔드 Claude 지침 저장소**입니다.
각 프로젝트는 이 저장소를 **Git submodule**로 포함하고, `CLAUDE.md``@import` 구문으로 공통 지침을 불러와 사용합니다.
## 프로젝트 구조 ---
이 프로젝트는 **submodule 방식**으로 관리됩니다. ## 📁 저장소 구조
## 브랜치 전략 ```
fe-common-rules/
├── CLAUDE.md # 공통 지침 엔트리 포인트 (rules/* 를 @import)
├── README.md # 이 파일
├── rules/
│ ├── coding-conventions.md # 포맷팅, 네이밍, 타입
│ ├── framework-rules.md # Vue/Nuxt/Tailwind/라이브러리 규칙
│ ├── commit-pr.md # 커밋 & PR 규칙
│ └── claude-workflow.md # Claude 작업 방식 지침
├── templates/
│ ├── CLAUDE.md.tpl # 프로젝트 루트 CLAUDE.md 템플릿
│ └── project/ # 프로젝트 지침 양식
│ ├── overview.md
│ ├── conventions.md
│ └── architecture.md
├── skills/ # 팀 공용 Claude skill 모음
│ ├── README.md
│ ├── vue-component-review/
│ │ └── SKILL.md
│ └── conventional-commit/
│ └── SKILL.md
├── scripts/
│ ├── install.sh # submodule 설치 + 템플릿 복사 + skill 링크
│ ├── init-project.sh # .claude/project 양식만 다시 초기화/diff
│ ├── link-skills.sh # 공통 skill 을 .claude/skills 로 링크/해제
│ └── update.sh # 최신 공통 지침으로 업데이트
└── examples/
└── sample-nuxt-project/ # Nuxt4 + Vue3 + TS + Tailwind 샘플
```
- `release` - 검증 완료 브랜치 (프로덕션 배포용) ---
- `dev` - 검증 테스트 브랜치 (개발 및 테스트용)
## 설치 방법 ## 🚀 프로젝트에 도입하기
### 1. Submodule 설치 (최초 1회)
프로젝트 루트에서 아래 명령을 실행합니다.
```bash ```bash
# 저장소 클론 (submodule 포함) # 원격 저장소 URL은 팀 내부 Git 주소로 교체하세요
git clone --recurse-submodules https://git.sginfra.net/sgp-web-d/gameservice-fe-agent.git git submodule add <fe-common-rules-repo-url> .claude/common
# 이미 클론한 경우 submodule 초기화
git submodule update --init --recursive git submodule update --init --recursive
``` ```
## Submodule 업데이트 또는 이 저장소에 포함된 헬퍼 스크립트를 사용해도 됩니다.
```bash ```bash
# 모든 submodule 업데이트 curl -fsSL <install.sh raw URL> | bash
git submodule update --remote --recursive
# 특정 submodule 업데이트
git submodule update --remote <submodule-path>
``` ```
## 개발 워크플로우 설치 후 프로젝트 구조는 다음과 같이 됩니다.
1. `dev` 브랜치에서 개발 및 테스트 진행 ```
2. 검증 완료 후 `release` 브랜치로 머지 your-project/
3. `release` 브랜치에서 프로덕션 배포 ├── .claude/
│ ├── common/ ← submodule (fe-common-rules)
│ └── project/ ← 프로젝트 고유 지침
├── CLAUDE.md ← 공통 + 프로젝트 지침을 @import
└── ...
```
## 기여하기 ### 2. 프로젝트 `CLAUDE.md` 작성
### 새로운 기능 추가 프로젝트 루트의 `CLAUDE.md`에서 공통 지침과 프로젝트 지침을 함께 불러옵니다.
```markdown
# <프로젝트 이름>
## 공통 지침
@.claude/common/CLAUDE.md
## 프로젝트 지침
@.claude/project/overview.md
@.claude/project/conventions.md
```
> Claude Code는 `@경로` 구문을 만나면 해당 파일의 내용을 컨텍스트에 포함시킵니다.
> 공통 지침의 `CLAUDE.md`는 내부적으로 `rules/*.md`를 다시 `@import`하므로,
> 한 줄의 `@.claude/common/CLAUDE.md`만으로 모든 공통 지침이 로드됩니다.
### 3. 공통 지침 업데이트 받기
1. **브랜치 생성**
```bash ```bash
git checkout dev # 최신 main 브랜치의 공통 지침으로 업데이트
git pull origin dev git submodule update --remote .claude/common
git checkout -b feature/your-feature-name
# 또는 헬퍼 스크립트
bash .claude/common/scripts/update.sh
``` ```
2. **개발 및 테스트** 업데이트 이후에는 submodule 포인터가 변경되므로 반드시 커밋합니다.
- 기능 개발 및 단위 테스트 작성
- 로컬에서 충분한 테스트 진행
3. **Merge Request 생성** ```bash
- 제목: `[Feature] 기능명` git add .claude/common
- 설명: 구현 내용, 변경 사항, 테스트 결과 포함 git commit -m "chore: update fe-common-rules submodule"
- 대상 브랜치: `dev` ```
### Merge Request 작성 규칙
- **제목 형식**: `[Category] 변경 내용`
- Category: `Feature`, `Fix`, `Refactor`, `Docs` 등
- 예: `[Feature] 사용자 인증 기능 추가`
- **설명 포함 사항**:
- 변경 이유 및 목적
- 주요 변경 내용
- 테스트 결과 및 방법
- 영향 받는 부분
### 코드 리뷰 프로세스
1. **최소 1명의 팀 멤버 승인** 필요
2. **체크리스트**:
- [ ] 코드 컨벤션 준수
- [ ] 단위 테스트 작성 및 통과
- [ ] 문서화 완료
- [ ] 기존 기능에 영향 없음
3. **승인 후 dev 브랜치에 머지**
4. **dev 브랜치에서 충분한 검증 후 release 브랜치로 머지**
--- ---
## 관리 방침 ## 📝 프로젝트 지침 양식 사용
### 버전 관리 `.claude/project/` 에 들어갈 지침은 공통 저장소의 `templates/project/` 에 양식으로 관리됩니다.
- Git 커밋 히스토리를 통해 모든 변경 이력 추적 팀원은 `install.sh` 를 사용하면 submodule 설치와 동시에 이 양식을 자동으로 복사받아 작성을
- Release 브랜치는 태그로 버전 관리 (예: `v1.0.0`) 시작할 수 있습니다.
- Dev 브랜치에서 충분한 검증 후 Release로 배포
### 브랜치 보호 ### 신규 프로젝트 — install.sh 한 번으로 끝
- **Release 브랜치**: 직접 푸시 금지, MR을 통해서만 머지
- **Dev 브랜치**: 기능 브랜치에서 MR을 통해 머지
- 모든 머지는 코드 리뷰 필수
### 문서화 ```bash
- 주요 변경사항은 CHANGELOG.md에 기록 bash .claude/common/scripts/install.sh git@github.com:<org>/fe-common-rules.git
- README는 항상 최신 상태 유지 # → .claude/common 에 submodule 설치
- 커밋 메시지는 명확하고 상세하게 작성 # → templates/project/*.md 를 .claude/project/ 로 복사
# → templates/CLAUDE.md.tpl 을 루트 CLAUDE.md 로 복사
```
### 이미 submodule 이 있는 프로젝트 — init-project.sh
이미 공통 저장소를 submodule 로 연결해둔 프로젝트에서 `.claude/project/` 양식만 따로
초기화하거나, 공통 저장소에 새 양식이 추가됐을 때 사용합니다.
```bash
# 기본: 없는 파일만 복사 (기존 파일은 건너뜀)
bash .claude/common/scripts/init-project.sh
# 기존 파일을 덮어쓰고 싶을 때
bash .claude/common/scripts/init-project.sh --force
# 차이만 확인하고 복사는 하지 않기
bash .claude/common/scripts/init-project.sh --diff
```
### 양식을 추가/수정하려면
공통 저장소의 `templates/project/*.md` 를 수정하는 PR 을 올립니다. 머지 후 팀원들은
`git submodule update --remote` 로 새 양식을 받은 뒤, 필요하면
`init-project.sh --diff` 로 자신의 프로젝트 지침과 비교할 수 있습니다.
--- ---
## 사용 예시 ## 🛠 공통 Skill 사용
### 코드 리뷰 Skill 사용 Claude Code 는 프로젝트의 `.claude/skills/<skill-name>/SKILL.md` 를 자동으로 탐색합니다.
공통 저장소는 `skills/` 폴더에 팀 공용 skill 들을 두고, 각 프로젝트는 **심볼릭 링크**로
이를 `.claude/skills/` 에 연결합니다. 링크 방식이기 때문에 `git submodule update` 한 번으로
모든 프로젝트가 최신 skill 을 자동으로 사용하게 됩니다.
```markdown ### 설치 (install.sh 사용 시 자동)
<!-- .claude/skills/code-review.md -->
# Code Review Skill
## 목적 `install.sh` 는 submodule 추가, 템플릿 복사, **공통 skill 링크** 까지 한 번에 처리합니다.
PR의 코드를 리뷰하고 개선사항을 제안합니다. 별도 조치가 필요하지 않습니다.
## 사용법 ### 이미 설치된 프로젝트 — link-skills.sh
1. PR 번호를 제공
2. Claude가 변경된 파일을 분석
3. 코딩 컨벤션, 성능, 보안 관점에서 리뷰
4. 개선사항 제안
## 예시
`/code-review #123`
```
### Nuxt 개발 가이드 활용
```markdown
<!-- .claude/contexts/nuxt-best-practices.md -->
# Nuxt 베스트 프랙티스
## 컴포넌트 작성
- Composition API 사용
- Composables로 로직 재사용
- Auto-import 활용으로 간결한 코드 작성
## 성능 최적화
- 동적 import로 코드 스플리팅
- useFetch, useAsyncData로 데이터 최적화
- Lazy loading 컴포넌트 활용
```
### 기술 스택
- **Framework**: Nuxt 3, Nuxt 4
- **Language**: TypeScript
- **Package Manager**: pnpm (권장)
### 개발 환경
```bash ```bash
# 의존성 설치 # 모든 공통 skill 을 .claude/skills/ 로 링크 (기존 링크는 건너뜀)
pnpm install bash .claude/common/scripts/link-skills.sh
# 개발 서버 실행 # 실제 링크 없이 어떤 작업이 일어날지 미리보기
pnpm dev bash .claude/common/scripts/link-skills.sh --dry-run
# 프로덕션 빌드 # 기존 링크/폴더를 덮어쓰기
pnpm build bash .claude/common/scripts/link-skills.sh --force
# 프로덕션 미리보기 # 특정 skill 하나만 링크
pnpm preview bash .claude/common/scripts/link-skills.sh vue-component-review
# 공통 skill 링크 전체 해제 (프로젝트 고유 skill 은 건드리지 않음)
bash .claude/common/scripts/link-skills.sh --unlink
``` ```
### 프로젝트 고유 skill 과의 공존
`.claude/skills/` 아래에는 공통 skill 링크와 **프로젝트 자체 skill 폴더**가 함께 있을 수
있습니다. `link-skills.sh` 는 심볼릭 링크(`-L`)인 항목만 관리하므로 프로젝트 고유 skill
은 덮어쓰지 않습니다.
```
.claude/skills/
├── vue-component-review → 링크: ../common/skills/vue-component-review
├── conventional-commit → 링크: ../common/skills/conventional-commit
└── my-project-only-skill/ ← 이 프로젝트만의 skill (실제 폴더)
└── SKILL.md
```
### 새 skill 을 공통 저장소에 추가
1. `skills/<skill-name>/SKILL.md` 를 작성 (YAML frontmatter 에 `name`, `description` 필수)
2. PR 을 올리고 팀 리뷰
3. 머지 후 각 프로젝트에서 아래 실행
```bash
git submodule update --remote .claude/common
bash .claude/common/scripts/link-skills.sh # 새로 추가된 skill 이 자동 링크됨
git add .gitmodules .claude
git commit -m "chore: sync fe-common-rules"
```
---
## ✍️ 공통 지침 수정 프로세스
1. 이 저장소를 별도로 clone 합니다.
2. 브랜치를 만들고 `rules/*.md` 혹은 `CLAUDE.md`를 수정합니다.
3. PR을 열고 팀 리뷰를 받습니다.
4. 머지 후 각 프로젝트에서 `git submodule update --remote` 로 동기화합니다.
수정 시 가이드
- **기존 규칙을 바꿀 때는 이유를 PR 설명에 반드시 기록**합니다.
- 특정 프로젝트에만 해당되는 내용은 공통 지침이 아닌 프로젝트 지침으로 분리합니다.
- 충돌 가능성이 있는 규칙은 기본값을 정하고, 프로젝트에서 오버라이드 가능하도록 문장을 구성합니다.
---
## 🧭 지침 우선순위
Claude가 동시에 여러 지침을 읽는 경우 다음 우선순위를 따릅니다.
1. **프로젝트 지침 (`.claude/project/*.md`)** — 최우선
2. **공통 지침 (`.claude/common/*.md`)** — 프로젝트와 충돌하지 않는 범위 내에서 적용
3. Claude의 일반 원칙
충돌이 발생하면 Claude는 프로젝트 지침을 따르되, 차이를 사용자에게 명시적으로 알려야 합니다.
---
## 📦 샘플 프로젝트
`examples/sample-nuxt-project/` 폴더에 Nuxt4 + Vue3 + TypeScript + Tailwind CSS 기반의
샘플 구성이 들어 있습니다. 프로젝트에 이 저장소를 연결하는 실제 예시를 보려면 해당 폴더의
`README.md`를 참고하세요.

View File

@@ -1,353 +0,0 @@
# 프로모션 - 기본 템플릿 Zeplin MCP 마크업 작업 가이드
이 문서는 Zeplin MCP를 활용한 프로모션 페이지 마크업 작업 방식을 정리한 가이드입니다.
---
## 1. 환경 설정 (최초 1회)
### Zeplin MCP SSL 인증서 오류 해결
사내 네트워크/VPN 환경에서 Zeplin MCP가 `unable to get local issuer certificate` 오류를 발생시키는 경우, `~/.cursor/mcp.json`의 Zeplin 서버 `env`에 아래 항목을 추가해야 합니다.
```json
{
"mcpServers": {
"zeplin": {
"timeout": 600,
"command": "npx",
"args": ["-y", "@zeplin/mcp-server@latest"],
"env": {
"ZEPLIN_ACCESS_TOKEN": "...",
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}
```
설정 변경 후 **Cursor 재시작** 필요 (`Cmd+Shift+P``Developer: Reload Window`).
> `NODE_TLS_REJECT_UNAUTHORIZED=0`은 로컬 Cursor MCP 전용 설정이며 소스코드/배포 환경과 완전히 분리됩니다.
---
## 2. 프로젝트 구조
```
l9-fe/
├── pages/promotion/{프로젝트명}/{날짜}/index.vue
├── assets/scss/pages/promotion/{프로젝트명}/{날짜}/index.scss
└── public/assets/promotion/{프로젝트명}/{날짜}/
├── pc/
│ ├── bg_section01_ko.jpg
│ ├── bg_section01_tw.jpg
│ └── ...
└── mobile/
├── bg_section01.jpg ← 기본 BG (언어 무관, 배경 전용 화면에서 다운로드)
├── bg_section01_ko.jpg ← KO 콘텐츠 이미지 (KO 콘텐츠 화면에서 다운로드)
├── bg_section01_tw.jpg ← TW 콘텐츠 이미지 (TW 콘텐츠 화면에서 다운로드)
└── ...
```
### Vue 파일 주요 구조
- `promotion-content` 클래스: 프로젝트별 고유 클래스명 사용 (예: `dinapheri-update`)
- 섹션 구성: `section01` ~ `sectionN`
- 번역 키: `tm()` 함수로 호출, JSON 파일에서 관리
- SCSS import: `<style>` 블록에서 `@import '@/assets/scss/pages/promotion/.../index.scss'`
---
## 3. Zeplin 화면 종류와 역할
프로모션 페이지는 용도별로 여러 Zeplin 화면이 존재합니다. 작업 전에 각 화면의 역할을 먼저 파악하세요.
### PC 화면
| 화면 역할 | 내용 |
|----------|------|
| PC KR 배경+콘텐츠 | PC용 전체 화면. 섹션 배경 이미지 + 텍스트 레이어 위치 좌표 확인용 |
| PC TW 배경+콘텐츠 | TW 언어용 PC 전체 화면 |
### 모바일 화면 (3종)
모바일은 PC와 달리 **역할이 분리된 화면 3종**을 사용합니다.
| 화면 역할 | 파일명 규칙 | 설명 |
|----------|-----------|------|
| 모바일 BG 전용 (고해상도) | `bg_sectionXX.jpg` | 배경 이미지만 있는 화면. 보통 2048px 너비. `.view-mobile section``background-image`에 사용 |
| 모바일 KO 콘텐츠 | `bg_sectionXX_ko.jpg` | 배경 + KO 콘텐츠가 합성된 화면. `[lang='ko-KR'] section::before``background-image`에 사용 |
| 모바일 TW 콘텐츠 | `bg_sectionXX_tw.jpg` | 배경 + TW 콘텐츠가 합성된 화면. `[lang='zh-TW'] section::before``background-image`에 사용 |
> **섹션 1개당 이미지 3개** 사용: `bg_sectionXX.jpg` + `bg_sectionXX_ko.jpg` + `bg_sectionXX_tw.jpg`
---
## 4. Zeplin MCP 사용법
### 화면 레이어 조회
```
get_screen(url, includeVariants: false)
```
조회 결과에서 확인할 항목:
- 각 섹션 그룹 레이어의 `sourceId`, `rect.y`, `rect.height`
- 텍스트 레이어의 절대 `y` 좌표 (notice, even-date 위치 계산용)
- `exportable: true` 여부 → false이면 다운로드 불가 (디자이너에게 export 설정 요청)
### 레이어 구조 파악 방법
`get_screen`으로 조회하면 최상위 레이어 목록과 각 레이어의 `y`, `height` 값을 확인할 수 있습니다.
섹션별 절대 y 시작값과 높이는 **프로젝트마다 다르므로**, 작업 전 반드시 전체 화면을 조회해서 직접 확인하세요.
```
예시)
레이어명 섹션 절대 y 시작 높이
00_main → section01 0 1020
01_con → section02 1020 2449
02_con → section03 3469 1839
...
```
### 요소 위치 계산 공식
**PC** - CSS `top` (px 고정값):
```
CSS top = 텍스트 레이어 절대 y 해당 섹션 절대 y 시작값
예시) section03의 notice 텍스트
절대 y: 2830 section03 시작 y: 1923 = top: 907px
```
**모바일** - `toPercentage(섹션높이, 상대y)`:
```
상대 y = 텍스트 레이어 절대 y 해당 섹션 절대 y 시작값
CSS top = toPercentage(섹션높이, 상대y)
예시) section03의 even-date (섹션 y: 3469, 섹션 높이: 1839)
텍스트 절대 y: 3697
상대 y: 3697 - 3469 = 228
→ toPercentage(1839, 228)
```
> 모바일 텍스트 좌표는 **콘텐츠 화면(KO/TW)**에서 읽어야 정확합니다. BG 전용 화면에는 텍스트 레이어가 없습니다.
### 이미지 에셋 다운로드
```
download_layer_asset(layerSourceId, localPath, assetType)
```
- 다운로드 시 **UUID 파일명**으로 저장되므로 반드시 rename 필요
- `exportable: false`인 레이어는 다운로드 불가 → 디자이너에게 Zeplin export 설정 요청 후 재시도
- Zeplin 디자인이 수정되면 `sourceId`가 바뀔 수 있으므로 교체 시 화면 재조회 필수
---
## 5. SCSS 작성 규칙
### 전체 구조
```scss
/* ─── PC ─────────────────────────────── */
.view-desktop {
.promotion-content.{프로젝트클래스} {
section { /* 공통 PC 스타일 */ }
.notice-list { /* 공통 */ }
.section01 { height: 826px; }
.section02 {
height: 1100px;
}
/* ... */
}
}
/* ─── 모바일 ──────────────────────────── */
.view-mobile {
.promotion-content.{프로젝트클래스} {
section {
position: relative;
background-position: 50% 0;
background-size: auto 100%;
background-repeat: no-repeat;
&::before {
content: '';
display: block;
max-width: toRem(750);
width: 100%;
margin: 0 auto;
background: 50% 0 / 100% auto no-repeat;
}
.notice-list { /* 공통 모바일 notice-list 스타일 */ }
.even-date { /* 공통 모바일 even-date 스타일 */ }
}
/* 섹션별 개별 설정 */
.section01 {
background-image: url('/assets/.../mobile/bg_section01.jpg'); /* 기본 BG */
&::before { @include inputMobileHeight(1020); } /* 섹션 높이값 */
}
.section02 {
background-image: url('/assets/.../mobile/bg_section02.jpg');
&::before { @include inputMobileHeight(2449); }
.notice-list { top: toPercentage(2449, 2319); }
}
.section03 {
background-image: url('/assets/.../mobile/bg_section03.jpg');
&::before { @include inputMobileHeight(1839); }
.even-date { top: toPercentage(1839, 228); }
.notice-list { top: toPercentage(1839, 1673); }
}
/* ... */
}
}
/* ─── 언어별 이미지 ────────────────────── */
[lang='ko-KR'] {
.view-desktop .promotion-content.{프로젝트클래스} {
.section01 { background-image: url('/assets/.../pc/bg_section01_ko.jpg'); }
/* ... */
}
.view-mobile .promotion-content.{프로젝트클래스} {
/* ::before 에 KO 콘텐츠 이미지 적용 */
.section01::before { background-image: url('/assets/.../mobile/bg_section01_ko.jpg'); }
/* ... */
}
}
[lang='zh-TW'] {
.view-desktop .promotion-content.{프로젝트클래스} {
.section01 { background-image: url('/assets/.../pc/bg_section01_tw.jpg'); }
/* ... */
}
.view-mobile .promotion-content.{프로젝트클래스} {
.section01::before { background-image: url('/assets/.../mobile/bg_section01_tw.jpg'); }
/* ... */
}
}
```
### 모바일 이미지 레이어링 원리 (모바일 전용)
> 아래 구조는 **모바일에만 해당**합니다. PC는 section에 배경 이미지 하나만 사용합니다.
```
[section 요소 자체]
background-image: bg_sectionXX.jpg
→ 화면 전체를 채우는 배경 깔판 (언어 무관)
[section::before 요소] ← section 위에 겹쳐지는 콘텐츠 레이어
background-image: bg_sectionXX_ko.jpg ([lang='ko-KR'] 블록)
or bg_sectionXX_tw.jpg ([lang='zh-TW'] 블록)
→ max-width: 750px, margin: 0 auto 로 중앙 정렬
→ padding-top % 로 이미지 비율 유지 (inputMobileHeight mixin)
→ 배경 위에 KO/TW 콘텐츠 이미지가 가운데 자리잡음
```
즉, 배경(bg_sectionXX.jpg)은 section에, 언어별 콘텐츠 이미지(bg_sectionXX_ko/tw.jpg)는 ::before에 분리해서 적용합니다.
**PC는 section에 언어별 배경 이미지 하나만 사용** (`[lang] section { background-image: bg_sectionXX_ko.jpg }`).
### inputMobileHeight 값
`inputMobileHeight(N)``N`은 **Zeplin BG 전용 화면의 섹션 높이(px)**에서 읽어옵니다.
콘텐츠 화면과 BG 화면의 섹션 높이는 동일하므로 어느 쪽에서 읽어도 무방합니다.
### 공통 요소 스타일
| 요소 | PC 주요 스타일 | 모바일 주요 스타일 |
|------|-------------|----------------|
| `.notice-list ul li` | `font-size: 16px`, `line-height: 26px`, `color: #978c7a` | `font-size: toRem(10)`, `padding-left: toRem(12)` |
| `.notice-list li::before` | `content: '•'`, `position: absolute`, `left: 0`, `top: 50%`, `transform: translateY(-50%)` | 동일 |
| `.notice-link` | `font-weight: 600`, `color: #b08442`, `text-decoration: underline` | 동일 |
| `.even-date` | `font-size: 20px`, `color: rgba(156,148,132,0.97)` | `font-size: rem(12)`, `letter-spacing: -0.8px` |
---
## 6. 마크업 패턴
### 기본 섹션 구조 (section01 참고)
```vue
<section id="section01" class="section01">
<h3 class="blind">
<span>{{ tm('ProjectName_Section01_SubTitle') }}</span>
<em>{{ tm('ProjectName_Section01_Title') }}</em>
</h3>
<p class="blind">{{ tm('ProjectName_Section01_Desc') }}</p>
<button type="button" class="btn-partner"
@click="[sendLog('event-name', 'UI-1'), goToLink(tm('ProjectName_Section01_Btn_Url'), '_blank')]">
<span class="blind">{{ tm('ProjectName_Section01_Btn') }}</span>
</button>
</section>
```
### 이벤트 섹션 구조 (notice-list + even-date 패턴)
```vue
<section id="section03" class="section03">
<h4 class="blind">{{ tm('ProjectName_Section03_Title') }}</h4>
<p class="blind">{{ tm('ProjectName_Section03_Event_Rewards') }}</p>
<p class="even-date">{{ tm('ProjectName_Section03_Date') }}</p>
<div class="notice-list">
<ul>
<template v-for="(notice, noticeIdx) in tm('ProjectName_Section03_Notice_List')" :key="noticeIdx">
<!-- 마지막 항목에만 링크 포함 -->
<li v-if="noticeIdx === tm('ProjectName_Section03_Notice_List').length - 1">
<span v-dompurify-html="tm(`${notice}`)"></span>
<span v-dompurify-html="tm(`${notice}_Link`)"
class="notice-link"
@click="sendLog('event-name', 'UI-XX')">
</span>
</li>
<li v-else v-dompurify-html="tm(`${notice}`)"></li>
</template>
</ul>
</div>
</section>
```
---
## 7. 작업 순서 (새 세션 시작 시)
### PC 작업
1. 이 문서 숙지
2. PC KR 화면 `get_screen` 조회 → 섹션별 레이어 y, height, sourceId 확인
3. 각 섹션 텍스트 레이어 절대 y → 상대 y 계산 (`top: px`)
4. PC KR 이미지 다운로드 → `bg_sectionXX_ko.jpg`로 rename
5. PC TW 화면 조회 → TW 이미지 다운로드 → `bg_sectionXX_tw.jpg`로 rename
6. Vue 마크업 작성
7. SCSS PC 스타일 작성 → 언어별 이미지 경로 블록 추가
### 모바일 추가 작업
8. **모바일 BG 전용 화면** 조회 → 섹션 높이(`inputMobileHeight` 값) 확인
- 이미지 다운로드 → `bg_sectionXX.jpg`로 rename
9. **모바일 KO 콘텐츠 화면** 조회 → 텍스트 레이어 절대 y 확인
- `상대 y = 텍스트 절대 y 섹션 시작 y` 계산
- `toPercentage(섹션높이, 상대y)` 값 SCSS에 적용
- 이미지 다운로드 → `bg_sectionXX_ko.jpg`로 rename
10. **모바일 TW 콘텐츠 화면** 조회 (필요 시)
- 이미지 다운로드 → `bg_sectionXX_tw.jpg`로 rename
11. SCSS 모바일 스타일 작성:
- `.view-mobile section` 공통 스타일
- 섹션별 `background-image`, `inputMobileHeight`, `toPercentage`
- `[lang='ko-KR']` 블록: `section::before { background-image: ..._ko.jpg }`
- `[lang='zh-TW']` 블록: `section::before { background-image: ..._tw.jpg }`
---
## 8. 주의사항
- **`exportable: false` 레이어는 다운로드 불가** → 디자이너에게 Zeplin에서 export 설정 요청 후 재시도
- **Zeplin 디자인 수정 시 sourceId 변경 가능** → 이미지 교체 시 반드시 화면 재조회
- **모바일 텍스트 좌표는 콘텐츠 화면에서 읽을 것** → BG 전용 화면에는 텍스트 레이어 없음
- **`inputMobileHeight` 값 = BG 화면의 섹션 height** → 콘텐츠 화면과 동일하므로 어느 쪽이든 무방
- **번역 키**는 별도 JSON 파일에서 관리 (`tm()` 함수로 호출)
- **`blind` 클래스**는 스크린리더 전용 숨김 텍스트
- SCSS mixin: `inputMobileHeight`, `inputMobileSize`, `toPercentage`, `toRem`, `rem` 등은 프로젝트 공통 정의 사용

62
rules/claude-workflow.md Normal file
View File

@@ -0,0 +1,62 @@
# Claude 작업 방식 지침
이 문서는 Claude가 팀 프로젝트에서 작업할 때 따라야 할 일반적인 원칙을 정의합니다.
## 기본 원칙
1. **기존 코드 존중**: 수정 전에 관련 파일과 주변 컨벤션을 먼저 파악합니다.
2. **최소 변경**: 요구사항을 충족하는 최소한의 변경만 수행합니다. 관련 없는 리팩토링은 별도 작업으로 분리합니다.
3. **가정 대신 질문**: 요구사항이 모호하면 추측하지 말고 사용자에게 확인합니다.
4. **근거 있는 수정**: 코드 변경의 이유를 설명할 수 있어야 합니다.
## 작업 순서
1. **탐색 (Explore)**
- 관련 파일을 먼저 읽고 프로젝트 구조를 파악합니다.
- 유사한 패턴이 이미 존재하는지 확인합니다.
2. **계획 (Plan)**
- 여러 파일을 수정하거나 복잡한 작업이면 할 일 목록을 만들어 공유합니다.
- 아키텍처에 영향을 주는 변경은 착수 전에 사용자 승인을 받습니다.
3. **구현 (Implement)**
- 한 번에 하나의 논리적 변경에 집중합니다.
- 공통 지침과 프로젝트 지침을 모두 준수합니다.
4. **검증 (Verify)**
- 린트 / 타입체크 / 빌드가 깨지지 않는지 확인합니다.
- 테스트가 있는 프로젝트라면 관련 테스트를 실행합니다.
- 수동 검증이 필요한 경우 확인 방법을 사용자에게 안내합니다.
## 해서는 안 되는 것
- **임의 기능 추가 금지**: 사용자가 요청하지 않은 기능을 추가하지 않습니다.
- **기존 코드 대량 리팩토링 금지**: 요청 범위를 벗어나는 변경은 하지 않습니다.
- **주석 / 문서 임의 삭제 금지**: 불필요해 보여도 삭제 전 사용자에게 확인합니다.
- **비밀정보 출력 금지**: 환경변수, 키, 토큰 등은 코드에 하드코딩하지 않습니다.
- **의존성 버전 임의 변경 금지**: 요청이 없다면 `package.json`의 버전을 변경하지 않습니다.
- **강제 푸시 / 히스토리 재작성 금지**: `push --force`, `reset --hard` 등은 사용자의 명시적 요청 없이 실행하지 않습니다.
## 커뮤니케이션
- 답변은 간결하게, 결론을 먼저 말합니다.
- 코드를 수정했다면 **어떤 파일을 어떻게 바꿨는지** 요약합니다.
- 불확실한 부분은 솔직하게 밝히고 대안을 제시합니다.
- 긴 설명보다 실제 결과물(코드/파일)을 우선합니다.
## 파일 작업 원칙
- 새 파일 생성보다 **기존 파일 수정**을 우선합니다.
- README, 문서는 사용자가 명시적으로 요청했을 때만 생성합니다.
- 파일을 읽지 않고 수정하지 않습니다.
- 대량 변경 시에는 diff를 확인할 수 있도록 단계별로 진행합니다.
## 질문이 필요한 상황
다음의 경우 반드시 사용자에게 확인을 요청합니다.
- 요구사항의 일부가 불명확할 때
- 여러 구현 방식이 있고 각각 장단점이 뚜렷할 때
- 공통 지침과 프로젝트 지침이 충돌할 때
- 파괴적 작업(파일 삭제, 데이터 마이그레이션, 스키마 변경 등)이 필요할 때
- 외부 서비스 호출이나 결제 관련 작업일 때

View File

@@ -0,0 +1,50 @@
# 코딩 컨벤션
## 기본 원칙
- **가독성 우선**: 영리한 코드보다 읽기 쉬운 코드를 선호합니다.
- **일관성 유지**: 기존 코드의 스타일을 먼저 관찰하고 그에 맞춥니다.
- **작은 단위**: 함수와 파일은 한 가지 책임만 지도록 작게 유지합니다.
## 포맷팅
- 들여쓰기: 스페이스 2칸 (탭 사용 금지)
- 문자열: 싱글 쿼터(`'`) 사용, JSX/템플릿 속성값은 더블 쿼터(`"`)
- 세미콜론: 생략하지 않고 항상 작성
- 줄 끝 공백 제거, 파일 끝 개행 1줄 유지
- 한 줄 최대 100자 (초과 시 줄바꿈)
- Prettier 설정 파일(`.prettierrc`)이 있는 경우 해당 설정을 우선합니다.
## 네이밍
- **변수/함수**: `camelCase` (예: `userProfile`, `fetchUserData`)
- **상수**: `UPPER_SNAKE_CASE` (예: `MAX_RETRY_COUNT`)
- **컴포넌트/클래스/타입**: `PascalCase` (예: `UserCard`, `OrderStatus`)
- **파일명**
- Vue 컴포넌트: `PascalCase.vue` (예: `UserCard.vue`)
- Composable: `use` 접두사 + `camelCase` (예: `useAuth.ts`)
- 일반 TS 모듈: `kebab-case.ts` (예: `format-date.ts`)
- **이벤트 핸들러**: `handle` 또는 `on` 접두사 (예: `handleClick`, `onSubmit`)
- **불리언**: `is`, `has`, `can`, `should` 접두사 (예: `isLoading`, `hasError`)
## 타입
- `any` 사용 금지. 불가피할 경우 주석으로 이유를 남기고 `unknown`을 먼저 고려합니다.
- 함수 시그니처에는 매개변수와 반환 타입을 명시합니다.
- 공개 API(타 모듈에서 import 되는 것)는 반드시 타입을 export 합니다.
- 유니온 타입은 `as const` 또는 별도 타입 alias로 관리합니다.
## 주석
- "무엇을" 보다 "왜"를 설명합니다.
- TODO/FIXME 주석에는 작성자와 날짜 또는 이슈 번호를 포함합니다.
- 공개 함수/컴포넌트에는 JSDoc 한 줄 설명을 권장합니다.
## import 순서
1. 외부 라이브러리 (예: `vue`, `nuxt`)
2. 내부 절대 경로 (예: `~/components/...`)
3. 상대 경로 (예: `./utils`)
4. 타입 only import는 각 그룹 내에서 별도 블록으로 분리
그룹 사이에는 빈 줄을 한 줄 둡니다.

83
rules/commit-pr.md Normal file
View File

@@ -0,0 +1,83 @@
# 커밋 / PR 규칙
## 커밋 메시지
[Conventional Commits](https://www.conventionalcommits.org/)를 따릅니다.
```
<type>(<scope>): <subject>
<body>
<footer>
```
### type
- `feat`: 새로운 기능 추가
- `fix`: 버그 수정
- `refactor`: 기능 변화 없는 구조 개선
- `style`: 코드 포맷/세미콜론 등 스타일 변경
- `docs`: 문서 수정
- `test`: 테스트 추가/수정
- `chore`: 빌드, 설정, 패키지 업데이트 등
- `perf`: 성능 개선
- `ci`: CI 설정 변경
### 작성 규칙
- **subject**는 50자 이내, 명령형 현재 시제(예: `add`, `fix``added`, `fixes` 아님)
- subject 끝에 마침표를 찍지 않습니다.
- body는 "무엇을"보다 "왜"를 설명합니다. 한 줄 72자 이내로 줄바꿈합니다.
- 한 커밋에는 하나의 논리적 변경만 담습니다.
### 예시
```
feat(user): add profile image upload
프로필 이미지 업로드 요구사항에 따라 multipart 업로드 경로를 추가했습니다.
기존 텍스트 필드 업데이트 API는 변경하지 않았습니다.
Refs: #123
```
## Pull Request
### 제목
커밋 메시지와 동일한 컨벤션을 따릅니다. (`<type>(<scope>): <subject>`)
### 본문 템플릿
```markdown
## 변경 사항
- 무엇이 바뀌었는지 요약
## 배경 / 이유
- 왜 이 변경이 필요했는지
## 테스트
- 어떻게 검증했는지 (수동/자동 테스트 내용)
## 스크린샷 (UI 변경 시)
- Before / After
## 체크리스트
- [ ] 로컬에서 빌드/테스트 통과
- [ ] 린트/포맷 통과
- [ ] 공통 지침(fe-common-rules) 준수
- [ ] 관련 문서 업데이트
```
### 리뷰 기준
- 최소 1명 이상의 승인 필요
- CI(Lint / Test / Build) 전부 통과 필요
- 머지 전략은 **Squash and merge**를 기본으로 합니다.
- 리뷰어는 변경 범위에 대해 질문이 남지 않도록 배경을 충분히 이해한 뒤 승인합니다.
### Draft PR
- 작업 중간 중간 피드백이 필요한 경우 Draft로 먼저 올리는 것을 권장합니다.
- Draft 상태에서는 CI 실패가 있어도 괜찮습니다.

47
rules/framework-rules.md Normal file
View File

@@ -0,0 +1,47 @@
# 프레임워크 / 라이브러리 사용 규칙
## Vue 3 / Nuxt
### 컴포넌트 작성
- **`<script setup lang="ts">` 사용**을 기본으로 합니다. Options API는 신규 코드에서 사용하지 않습니다.
- 컴포넌트는 단일 책임 원칙을 지키며, 200줄을 넘으면 분리를 검토합니다.
- Props는 `defineProps<T>()` 제네릭 형태로 타입을 명시합니다.
- Emits는 `defineEmits<{ ... }>()` 제네릭 형태로 선언합니다.
- `ref` vs `reactive`: 원시값과 단일 객체는 `ref`, 복잡한 상태 트리는 `reactive`를 고려합니다. 일관성을 위해 팀 내에서 가능한 `ref`를 우선합니다.
### 상태 관리
- 컴포넌트 내 로컬 상태: `ref` / `reactive`
- 여러 컴포넌트가 공유하는 상태: **Pinia** 사용
- 서버 상태: Nuxt `useFetch` / `useAsyncData` 사용, 직접 `fetch` 호출은 지양합니다.
### 라우팅
- Nuxt의 파일 기반 라우팅을 사용합니다. 수동 라우트 정의는 특수한 경우에만 허용됩니다.
- 동적 라우트 파라미터는 `[param].vue` 형식을 사용합니다.
### Composable
- 재사용 가능한 로직은 `composables/` 디렉토리의 `useXxx` 함수로 추출합니다.
- Composable은 부수효과를 최소화하고, 반환 객체에 상태와 메서드를 함께 묶어 반환합니다.
## TypeScript
- `strict: true`를 유지합니다.
- 공용 타입은 `types/` 또는 각 도메인의 `types.ts`에 모아둡니다.
- 외부 API 응답은 반드시 타입을 정의하여 사용합니다.
## Tailwind CSS
- **유틸리티 클래스 우선** 사용. 공통 패턴은 컴포넌트로 추출합니다.
- `@apply`는 꼭 필요한 경우에만 사용하고, 가능한 유틸리티를 직접 나열합니다.
- 임의값 클래스(`w-[123px]`)는 디자인 시스템에 등록되지 않은 값에만 제한적으로 사용합니다.
- 조건부 클래스는 `clsx` 또는 `cn` 유틸리티를 사용하여 가독성을 확보합니다.
- 클래스 순서는 Tailwind 공식 프리셋(`prettier-plugin-tailwindcss`)을 따릅니다.
## 외부 라이브러리 도입
- 새로운 라이브러리를 추가할 때는 **PR 설명에 도입 이유, 번들 영향, 대안 검토 내용**을 기록합니다.
- 동일 기능의 라이브러리를 중복 도입하지 않습니다.
- 유지보수가 중단된 패키지(6개월 이상 업데이트 없음)는 도입하지 않습니다.

96
scripts/init-project.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
#
# fe-common-rules project template initializer
# 이미 .claude/common 이 설치된 프로젝트에서 templates/project/ 의
# 양식을 .claude/project/ 에 복사합니다.
#
# 사용법:
# bash .claude/common/scripts/init-project.sh # 없는 파일만 복사 (기본)
# bash .claude/common/scripts/init-project.sh --force # 기존 파일을 덮어씀
# bash .claude/common/scripts/init-project.sh --diff # 차이만 보여주고 복사는 안 함
#
set -euo pipefail
COMMON_PATH=".claude/common"
PROJECT_PATH=".claude/project"
TEMPLATE_DIR="$COMMON_PATH/templates/project"
MODE="safe" # safe | force | diff
for arg in "$@"; do
case "$arg" in
--force) MODE="force" ;;
--diff) MODE="diff" ;;
-h|--help)
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "❌ 알 수 없는 옵션: $arg" >&2
exit 1
;;
esac
done
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ 현재 디렉토리는 Git 저장소가 아닙니다." >&2
exit 1
fi
cd "$(git rev-parse --show-toplevel)"
if [[ ! -d "$TEMPLATE_DIR" ]]; then
echo "$TEMPLATE_DIR 가 없습니다. 먼저 submodule 을 설치/업데이트하세요:" >&2
echo " git submodule update --init --recursive" >&2
exit 1
fi
mkdir -p "$PROJECT_PATH"
echo "📝 templates/project → $PROJECT_PATH (mode=$MODE)"
echo ""
shopt -s nullglob
copied=0
skipped=0
diffed=0
for f in "$TEMPLATE_DIR"/*.md; do
name="$(basename "$f")"
dest="$PROJECT_PATH/$name"
case "$MODE" in
diff)
if [[ -f "$dest" ]]; then
if ! diff -q "$f" "$dest" >/dev/null 2>&1; then
echo "📄 diff: $dest"
diff -u "$dest" "$f" || true
echo ""
diffed=$((diffed + 1))
fi
else
echo " 새 양식: $dest (복사되지 않음. --force 로 복사하세요)"
diffed=$((diffed + 1))
fi
;;
force)
cp "$f" "$dest"
echo "$dest (덮어씀)"
copied=$((copied + 1))
;;
safe)
if [[ -f "$dest" ]]; then
echo "$dest (이미 존재)"
skipped=$((skipped + 1))
else
cp "$f" "$dest"
echo "$dest"
copied=$((copied + 1))
fi
;;
esac
done
echo ""
case "$MODE" in
diff) echo "🔍 차이가 있는 파일: $diffed";;
*) echo "🎉 완료: 복사 $copied개 / 건너뜀 $skipped개";;
esac

135
scripts/install.sh Executable file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
#
# fe-common-rules installer
# 현재 Git 프로젝트의 .claude/common 경로에 fe-common-rules 저장소를
# submodule 로 추가하고, templates/ 에서 프로젝트 지침 양식과
# CLAUDE.md 템플릿을 복사합니다.
#
# 사용법:
# bash scripts/install.sh <repo-url> [<branch>]
#
# 예:
# bash scripts/install.sh git@github.com:our-team/fe-common-rules.git main
#
set -euo pipefail
REPO_URL="${1:-}"
BRANCH="${2:-main}"
TARGET_PATH=".claude/common"
PROJECT_PATH=".claude/project"
if [[ -z "$REPO_URL" ]]; then
echo "❌ 사용법: bash scripts/install.sh <repo-url> [branch]" >&2
echo " 예: bash scripts/install.sh git@github.com:our-team/fe-common-rules.git main" >&2
exit 1
fi
# Git 프로젝트인지 확인
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ 현재 디렉토리는 Git 저장소가 아닙니다. 먼저 'git init' 후 실행해주세요." >&2
exit 1
fi
# 루트로 이동
cd "$(git rev-parse --show-toplevel)"
# 1) Submodule 추가
if [[ -d "$TARGET_PATH" ]]; then
echo "⚠️ '$TARGET_PATH' 경로가 이미 존재합니다. submodule 추가를 건너뜁니다."
else
echo "📦 fe-common-rules 를 submodule 로 추가합니다..."
git submodule add -b "$BRANCH" "$REPO_URL" "$TARGET_PATH"
git submodule update --init --recursive
echo "✅ submodule 추가 완료: $TARGET_PATH"
fi
# 2) 프로젝트 지침 양식 복사 (templates/project/ → .claude/project/)
mkdir -p "$PROJECT_PATH"
TEMPLATE_DIR="$TARGET_PATH/templates/project"
if [[ -d "$TEMPLATE_DIR" ]]; then
echo "📝 프로젝트 지침 양식을 복사합니다..."
for f in "$TEMPLATE_DIR"/*.md; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
dest="$PROJECT_PATH/$name"
if [[ -f "$dest" ]]; then
echo "$dest (이미 존재 - 건너뜀)"
else
cp "$f" "$dest"
echo "$dest"
fi
done
else
echo "⚠️ $TEMPLATE_DIR 를 찾지 못했습니다. 공통 저장소의 templates 가 오래됐을 수 있습니다."
fi
# 3) 루트 CLAUDE.md 템플릿 복사
if [[ ! -f "CLAUDE.md" ]]; then
TPL_FILE="$TARGET_PATH/templates/CLAUDE.md.tpl"
if [[ -f "$TPL_FILE" ]]; then
cp "$TPL_FILE" CLAUDE.md
echo "✅ CLAUDE.md 템플릿을 생성했습니다."
else
cat > CLAUDE.md <<'EOF'
# <프로젝트 이름>
## 공통 지침
@.claude/common/CLAUDE.md
## 프로젝트 지침
@.claude/project/overview.md
@.claude/project/conventions.md
@.claude/project/architecture.md
EOF
echo "✅ CLAUDE.md 템플릿을 생성했습니다. (fallback)"
fi
else
echo " 기존 CLAUDE.md 가 이미 존재합니다. 아래 블록을 수동으로 추가하세요:"
echo ""
echo " ## 공통 지침"
echo " @.claude/common/CLAUDE.md"
echo ""
echo " ## 프로젝트 지침"
echo " @.claude/project/overview.md"
echo " @.claude/project/conventions.md"
echo " @.claude/project/architecture.md"
echo ""
fi
# 4) 공통 skill 심볼릭 링크 (.claude/common/skills/* → .claude/skills/*)
SKILLS_SRC="$TARGET_PATH/skills"
SKILLS_DEST=".claude/skills"
if [[ -d "$SKILLS_SRC" ]]; then
echo "🔗 공통 skill 을 $SKILLS_DEST 로 링크합니다..."
mkdir -p "$SKILLS_DEST"
for dir in "$SKILLS_SRC"/*/; do
[[ -d "$dir" ]] || continue
name="$(basename "$dir")"
[[ "$name" == "README"* ]] && continue
link_src="../common/skills/$name"
link_dest="$SKILLS_DEST/$name"
if [[ -L "$link_dest" ]]; then
echo "$link_dest (이미 링크됨)"
elif [[ -e "$link_dest" ]]; then
echo " ⚠️ $link_dest (실제 파일/폴더 존재 - 건너뜀. link-skills.sh --force 로 덮어쓰기)"
else
ln -s "$link_src" "$link_dest"
echo "$link_dest$link_src"
fi
done
fi
echo ""
echo "🎉 설치가 완료되었습니다."
echo " - 공통 지침: $TARGET_PATH/CLAUDE.md"
echo " - 프로젝트 지침: $PROJECT_PATH/"
echo " - 공통 skill: $SKILLS_DEST/ (submodule 에 심볼릭 링크)"
echo " - 엔트리 파일: CLAUDE.md"
echo ""
echo "다음 작업을 진행해 주세요:"
echo " 1) $PROJECT_PATH/*.md 내용을 프로젝트에 맞게 채우기"
echo " 2) 변경 사항을 커밋하기"
echo " git add .gitmodules .claude CLAUDE.md"
echo " git commit -m 'chore: add fe-common-rules submodule'"

139
scripts/link-skills.sh Executable file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env bash
#
# fe-common-rules skill linker
# 공통 저장소의 skills/* 를 프로젝트의 .claude/skills/* 로 심볼릭 링크합니다.
# 심볼릭 링크이므로 submodule 업데이트 시 skill 도 자동으로 최신 버전이 됩니다.
#
# 사용법:
# bash .claude/common/scripts/link-skills.sh # 모든 skill 링크
# bash .claude/common/scripts/link-skills.sh <skill-name> # 특정 skill 만
# bash .claude/common/scripts/link-skills.sh --dry-run # 실제 링크 없이 미리보기
# bash .claude/common/scripts/link-skills.sh --force # 기존 링크/폴더 덮어쓰기
# bash .claude/common/scripts/link-skills.sh --unlink # 공통 skill 링크 제거
#
set -euo pipefail
COMMON_PATH=".claude/common"
SKILLS_SRC="$COMMON_PATH/skills"
SKILLS_DEST=".claude/skills"
MODE="safe" # safe | force | dry-run | unlink
TARGET=""
for arg in "$@"; do
case "$arg" in
--dry-run) MODE="dry-run" ;;
--force) MODE="force" ;;
--unlink) MODE="unlink" ;;
-h|--help)
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
-*)
echo "❌ 알 수 없는 옵션: $arg" >&2
exit 1
;;
*)
TARGET="$arg"
;;
esac
done
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ 현재 디렉토리는 Git 저장소가 아닙니다." >&2
exit 1
fi
cd "$(git rev-parse --show-toplevel)"
if [[ ! -d "$SKILLS_SRC" ]]; then
echo "$SKILLS_SRC 가 없습니다. 먼저 submodule 을 설치/업데이트하세요:" >&2
echo " git submodule update --init --recursive" >&2
exit 1
fi
mkdir -p "$SKILLS_DEST"
# 링크 대상 결정
declare -a skills
if [[ -n "$TARGET" ]]; then
if [[ ! -d "$SKILLS_SRC/$TARGET" ]]; then
echo "❌ '$TARGET' skill 을 $SKILLS_SRC 에서 찾지 못했습니다." >&2
echo " 사용 가능한 skill:" >&2
ls -1 "$SKILLS_SRC" 2>/dev/null | grep -v '^README' | sed 's/^/ - /' >&2
exit 1
fi
skills=("$TARGET")
else
shopt -s nullglob
for dir in "$SKILLS_SRC"/*/; do
name="$(basename "$dir")"
# README 같은 파일은 이미 걸러지지만 추가 보호
[[ "$name" == "README"* ]] && continue
skills+=("$name")
done
fi
if [[ ${#skills[@]} -eq 0 ]]; then
echo " 링크할 skill 이 없습니다."
exit 0
fi
echo "🔗 공통 skill 링크 (mode=$MODE)"
echo " source: $SKILLS_SRC"
echo " dest: $SKILLS_DEST"
echo ""
linked=0
skipped=0
removed=0
for name in "${skills[@]}"; do
src="../common/skills/$name" # 심볼릭 링크의 상대 경로 (.claude/skills 기준)
dest="$SKILLS_DEST/$name"
case "$MODE" in
unlink)
if [[ -L "$dest" ]]; then
rm "$dest"
echo " 🗑 $dest (링크 제거)"
removed=$((removed + 1))
else
echo "$dest (링크 아님 - 건너뜀)"
fi
;;
dry-run)
if [[ -e "$dest" || -L "$dest" ]]; then
echo "$dest (이미 존재)"
else
echo " ln -s $src $dest"
fi
;;
force)
rm -rf "$dest"
ln -s "$src" "$dest"
echo "$dest$src (덮어씀)"
linked=$((linked + 1))
;;
safe)
if [[ -L "$dest" ]]; then
echo "$dest (이미 링크됨)"
skipped=$((skipped + 1))
elif [[ -e "$dest" ]]; then
echo " ⚠️ $dest (실제 파일/폴더가 존재. --force 로 덮어쓰세요)"
skipped=$((skipped + 1))
else
ln -s "$src" "$dest"
echo "$dest$src"
linked=$((linked + 1))
fi
;;
esac
done
echo ""
case "$MODE" in
unlink) echo "🎉 제거 완료: $removed 개 링크 제거됨";;
dry-run) echo "🔍 미리보기 완료 (실제 변경 없음)";;
*) echo "🎉 완료: 링크 $linked개 / 건너뜀 $skipped개";;
esac

36
scripts/update.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
#
# fe-common-rules updater
# 현재 프로젝트에 설치된 .claude/common submodule 을 최신 버전으로 갱신합니다.
#
# 사용법:
# bash .claude/common/scripts/update.sh
#
set -euo pipefail
TARGET_PATH=".claude/common"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "❌ 현재 디렉토리는 Git 저장소가 아닙니다." >&2
exit 1
fi
if [[ ! -d "$TARGET_PATH" ]]; then
echo "❌ '$TARGET_PATH' 가 존재하지 않습니다. 먼저 install.sh 로 설치하세요." >&2
exit 1
fi
echo "🔄 fe-common-rules 를 최신 버전으로 업데이트합니다..."
git submodule update --remote --merge "$TARGET_PATH"
# 변경 사항 확인
if git diff --quiet -- "$TARGET_PATH"; then
echo "✅ 이미 최신 상태입니다."
exit 0
fi
echo ""
echo "✅ 업데이트가 완료되었습니다. 변경된 submodule 포인터를 커밋하세요:"
echo ""
echo " git add $TARGET_PATH"
echo " git commit -m 'chore: update fe-common-rules submodule'"

52
skills/README.md Normal file
View File

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

View File

@@ -0,0 +1,78 @@
---
name: conventional-commit
description: Git 변경사항을 팀의 Conventional Commits 규칙(fe-common-rules/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 는 사용자가 요청할 때만 추가한다.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
---
name: vue-component-review
description: Vue 3 / Nuxt 컴포넌트 파일을 팀 공통 지침(fe-common-rules) 기준으로 리뷰할 때 사용합니다. 사용자가 "이 컴포넌트 리뷰해줘", "컨벤션 맞는지 봐줘", "컴포넌트 체크" 등을 요청하면 트리거됩니다.
---
# 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 프리셋과 다름
- ...
### ✅ 좋은 점
- 간단한 컴포넌트에 잘 맞는 단일 책임 구조
- ...
```
## 주의사항
- 사용자가 리팩토링을 **요청하지 않은 경우** 코드를 직접 수정하지 말고 리뷰만 수행한다.
- 프로젝트 지침과 공통 지침이 충돌하면 프로젝트 지침을 따르되, 차이를 사용자에게 알린다.
- 실제 코드 동작 변경(기능 수정)은 리뷰 범위가 아니다. 별도 작업으로 분리 제안한다.

9
templates/CLAUDE.md.tpl Normal file
View File

@@ -0,0 +1,9 @@
# <프로젝트 이름>
## 공통 지침
@.claude/common/CLAUDE.md
## 프로젝트 지침
@.claude/project/overview.md
@.claude/project/conventions.md
@.claude/project/architecture.md

View File

@@ -0,0 +1,46 @@
# 아키텍처
> 이 파일은 `fe-common-rules/templates/project/architecture.md` 에서 복사된 양식입니다.
> 프로젝트의 레이어 구조와 데이터 흐름을 간단히 설명해주세요.
## 레이어 구조
<프로젝트의 레이어 구조를 그림 또는 텍스트로 그려주세요>
```
┌───────────────────────────────┐
│ presentation │ ← pages / components
├───────────────────────────────┤
│ logic │ ← composables / hooks / stores
├───────────────────────────────┤
│ data access │ ← api wrapper / queries
├───────────────────────────────┤
│ server │ ← 서버 라우트 / BFF
└───────────────────────────────┘
```
## 의존 규칙
- 상위 → 하위 **단방향 의존**만 허용
- 같은 레이어 간 순환 import 금지
- <프로젝트 고유 규칙 추가>
## 데이터 흐름
1. <이벤트 발생부터 응답까지의 흐름을 간단히>
2. ...
3. ...
## 상태 관리 가이드
| 상태 종류 | 권장 위치 |
| -------------------- | ------------------------ |
| 컴포넌트 로컬 상태 | <예: ref / useState> |
| 페이지 단위 공유 상태| <예: provide/inject> |
| 앱 전역 상태 | <예: Pinia / Zustand> |
| 서버 데이터 | <예: useFetch / TanStack Query> |
## 외부 의존성
- 반드시 알아야 할 외부 서비스나 내부 API 를 나열
- 장애 발생 시 fallback 정책이 있다면 함께 기술

View File

@@ -0,0 +1,44 @@
# 프로젝트 전용 컨벤션
> 이 파일은 `fe-common-rules/templates/project/conventions.md` 에서 복사된 양식입니다.
> 공통 지침(`.claude/common/`) 외에 **이 프로젝트에서만** 적용되는 규칙을 작성하세요.
> 공통 지침과 충돌할 경우 이 문서가 우선합니다.
## 디렉토리 규칙
- `components/`<설명>
- `composables/` 또는 `hooks/`<설명>
- `pages/` 또는 `app/`<설명>
- `server/` 또는 `api/`<설명>
- `types/`<설명>
## 컴포넌트 규칙 (공통 규칙 오버라이드)
<공통 규칙과 달리 프로젝트에서만 적용할 제약을 적어주세요>
- 예) 컴포넌트 파일 길이 제한: 150줄 (공통 200줄보다 엄격)
- 예) Props 개수 최대 7개, 초과 시 객체 props 로 묶기
## 스타일
- 색상/간격/타이포는 디자인 토큰만 사용하고 임의값 금지
- 다크모드 prefix: `dark:`
- 기타 프로젝트 고유 규칙: <작성>
## 네트워크 / 데이터
- API 호출 창구: <예: composables/api wrapper 사용>
- 인증 토큰 저장 위치: <예: httpOnly 쿠키>
- 에러 핸들링 규칙: <작성>
## 금지 사항
- <예: 직접 $fetch 사용 금지>
- <예: 전역 이벤트 버스 사용 금지>
- <예: any 타입 사용 금지>
## 테스트
- 테스트 러너: <Vitest / Jest >
- 테스트 파일 위치: <소스 / __tests__ 폴더>
- 최소 커버리지: <숫자>

View File

@@ -0,0 +1,39 @@
# 프로젝트 개요
> 이 파일은 `fe-common-rules/templates/project/overview.md` 에서 복사된 양식입니다.
> 프로젝트 세팅 후 실제 내용으로 채워주세요.
## 서비스
- **이름**: <프로젝트 이름>
- **설명**: < 설명>
- **배포 환경**: <dev / staging / production URL 또는 환경>
- **저장소**: <git 주소>
## 기술 스택
- **Framework**: <예: Nuxt 4 / Next 15 / ...>
- **UI**: <예: Vue 3 <script setup> / React 19 / ...>
- **Language**: TypeScript (strict)
- **Styling**: <예: Tailwind CSS / styled-components / ...>
- **상태관리**: <예: Pinia / Zustand / Redux Toolkit / ...>
- **테스트**: <예: Vitest + Playwright / Jest + RTL / ...>
- **패키지매니저**: <pnpm / npm / yarn>
## 주요 기능
- <기능 1>
- <기능 2>
- <기능 3>
## 팀 / 오너
- 오너: < 또는 담당자>
- 문의 채널: <Slack 채널 / 이메일>
- 온콜/긴급 연락: <필요 >
## 참고 문서
- 기획 문서: <링크>
- 디자인 시스템: <링크>
- API 스펙: <링크>