feat: 공통 Skill 관리 및 링크 스크립트 추가

This commit is contained in:
hyeonggil
2026-04-12 16:32:51 +09:00
parent 5f1b0e9130
commit 982661cb1c
43 changed files with 2570 additions and 2 deletions

View File

@@ -26,8 +26,14 @@
2. 이 공통 지침은 프로젝트 지침과 충돌하지 않는 범위 내에서 항상 적용됩니다.
3. 충돌이 발생하면 프로젝트 지침을 따르되, 차이를 사용자에게 알려주세요.
## 공통 Skill
공통 저장소는 `skills/` 폴더에 팀 공용 Claude skill 들을 함께 관리합니다.
프로젝트는 `scripts/link-skills.sh``.claude/skills/` 에 심볼릭 링크하여 사용합니다.
자세한 내용은 `skills/README.md` 를 참고하세요.
## 업데이트
- 공통 지침은 이 레파지토리(`fe-common-rules`)에서만 수정합니다.
- 각 프로젝트는 `scripts/update-common.sh`(또는 `git submodule update --remote`)로 최신 버전을 받아갑니다.
- 각 프로젝트는 `scripts/update.sh`(또는 `git submodule update --remote`)로 최신 버전을 받아갑니다.
- 수정 제안은 PR로 받습니다. 자세한 내용은 루트 `README.md` 참고.

View File

@@ -22,9 +22,16 @@ fe-common-rules/
│ ├── overview.md
│ ├── conventions.md
│ └── architecture.md
├── skills/ # 팀 공용 Claude skill 모음
│ ├── README.md
│ ├── vue-component-review/
│ │ └── SKILL.md
│ └── conventional-commit/
│ └── SKILL.md
├── scripts/
│ ├── install.sh # 프로젝트에 submodule 설치 + 템플릿 복사
│ ├── 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 샘플
@@ -138,6 +145,65 @@ bash .claude/common/scripts/init-project.sh --diff
---
## 🛠 공통 Skill 사용
Claude Code 는 프로젝트의 `.claude/skills/<skill-name>/SKILL.md` 를 자동으로 탐색합니다.
공통 저장소는 `skills/` 폴더에 팀 공용 skill 들을 두고, 각 프로젝트는 **심볼릭 링크**로
이를 `.claude/skills/` 에 연결합니다. 링크 방식이기 때문에 `git submodule update` 한 번으로
모든 프로젝트가 최신 skill 을 자동으로 사용하게 됩니다.
### 설치 (install.sh 사용 시 자동)
`install.sh` 는 submodule 추가, 템플릿 복사, **공통 skill 링크** 까지 한 번에 처리합니다.
별도 조치가 필요하지 않습니다.
### 이미 설치된 프로젝트 — link-skills.sh
```bash
# 모든 공통 skill 을 .claude/skills/ 로 링크 (기존 링크는 건너뜀)
bash .claude/common/scripts/link-skills.sh
# 실제 링크 없이 어떤 작업이 일어날지 미리보기
bash .claude/common/scripts/link-skills.sh --dry-run
# 기존 링크/폴더를 덮어쓰기
bash .claude/common/scripts/link-skills.sh --force
# 특정 skill 하나만 링크
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 합니다.

View File

@@ -0,0 +1,39 @@
# 팀 공통 Claude 지침 (fe-common-rules)
이 파일은 팀 전체에서 공통으로 사용하는 Claude 작업 지침의 **엔트리 포인트**입니다.
각 프로젝트의 `CLAUDE.md`에서 `@.claude/common/CLAUDE.md` 형태로 참조하여 사용합니다.
## 적용 범위
- 팀 내 모든 프론트엔드 프로젝트
- Claude Code / Claude 기반 자동화 작업 전반
## 포함된 지침
아래 문서들은 Claude가 이 파일을 읽을 때 자동으로 함께 로드됩니다.
@rules/coding-conventions.md
@rules/framework-rules.md
@rules/commit-pr.md
@rules/claude-workflow.md
## 우선순위
1. 프로젝트 `CLAUDE.md`에 명시된 **프로젝트 지침**이 최우선입니다.
2. 이 공통 지침은 프로젝트 지침과 충돌하지 않는 범위 내에서 항상 적용됩니다.
3. 충돌이 발생하면 프로젝트 지침을 따르되, 차이를 사용자에게 알려주세요.
## 공통 Skill
공통 저장소는 `skills/` 폴더에 팀 공용 Claude skill 들을 함께 관리합니다.
프로젝트는 `scripts/link-skills.sh``.claude/skills/` 에 심볼릭 링크하여 사용합니다.
자세한 내용은 `skills/README.md` 를 참고하세요.
## 업데이트
- 공통 지침은 이 레파지토리(`fe-common-rules`)에서만 수정합니다.
- 각 프로젝트는 `scripts/update.sh`(또는 `git submodule update --remote`)로 최신 버전을 받아갑니다.
- 수정 제안은 PR로 받습니다. 자세한 내용은 루트 `README.md` 참고.

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는 각 그룹 내에서 별도 블록으로 분리
그룹 사이에는 빈 줄을 한 줄 둡니다.

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 실패가 있어도 괜찮습니다.

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개월 이상 업데이트 없음)는 도입하지 않습니다.

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

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'"

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

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'"

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 프리셋과 다름
- ...
### ✅ 좋은 점
- 간단한 컴포넌트에 잘 맞는 단일 책임 구조
- ...
```
## 주의사항
- 사용자가 리팩토링을 **요청하지 않은 경우** 코드를 직접 수정하지 말고 리뷰만 수행한다.
- 프로젝트 지침과 공통 지침이 충돌하면 프로젝트 지침을 따르되, 차이를 사용자에게 알린다.
- 실제 코드 동작 변경(기능 수정)은 리뷰 범위가 아니다. 별도 작업으로 분리 제안한다.

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 스펙: <링크>

View File

@@ -0,0 +1,42 @@
# 아키텍처
> 양식 출처: `.claude/common/templates/project/architecture.md`
> 샘플 프로젝트의 예시로 채워진 버전입니다.
## 레이어 구조
```
┌───────────────────────────────────┐
│ pages (Nuxt) │ ← 라우팅, 데이터 페칭 진입점
├───────────────────────────────────┤
│ components │ ← 재사용 UI (도메인/공용 분리)
├───────────────────────────────────┤
│ composables (use*) │ ← 상태 + 로직 (뷰 독립)
├───────────────────────────────────┤
│ composables/api (wrapper) │ ← 서버 통신 단일 창구
├───────────────────────────────────┤
│ server/api │ ← Nuxt 서버 라우트
└───────────────────────────────────┘
```
## 의존 규칙
- 하위 레이어는 상위 레이어를 import 할 수 없습니다. (단방향 의존)
- `components` 는 비즈니스 로직을 직접 수행하지 않고, composable 에 위임합니다.
- 전역 상태는 필요한 경우에만 Pinia store 로 승격합니다.
## 데이터 흐름
1. 사용자 이벤트 → `components` 에서 `composable` 호출
2. `composable``composables/api` 래퍼 호출
3. 래퍼 → `$fetch` 로 Nuxt `server/api` 또는 외부 API 요청
4. 응답은 타입이 보장된 상태로 composable → component 로 전달
## 상태 관리 가이드
| 상태 종류 | 권장 위치 |
| -------------------- | --------------------------------- |
| 컴포넌트 로컬 상태 | `ref` / `reactive` |
| 페이지 단위 공유 상태| `provide/inject` 또는 composable |
| 앱 전역 상태 | Pinia store |
| 서버 데이터 | `useAsyncData` / `useFetch` |

View File

@@ -0,0 +1,34 @@
# 프로젝트 전용 컨벤션
> 양식 출처: `.claude/common/templates/project/conventions.md`
> 샘플 프로젝트의 예시로 채워진 버전입니다.
> 공통 지침(`.claude/common/`)과 충돌할 경우 이 문서가 우선합니다.
## 디렉토리 규칙
- `components/` — 재사용 컴포넌트. 도메인별 하위 폴더(`user/`, `order/`)로 분리
- `composables/``useXxx` 형태의 재사용 로직
- `pages/` — Nuxt 파일 기반 라우팅
- `server/api/` — 서버 라우트 핸들러
- `types/` — 전역/공통 타입 정의
## 컴포넌트 규칙 (공통 규칙 오버라이드)
- 컴포넌트 파일 길이 제한: **150줄** (공통 200줄보다 엄격)
- 컴포넌트 당 `defineProps` 최대 **7개**. 초과 시 객체 props 로 묶기
## 스타일
- 색상은 `tailwind.config.ts``theme.extend.colors` 에 등록된 토큰만 사용 (임의 색상 금지)
- 다크모드 클래스 prefix: `dark:`
## 네트워크
- API 호출은 반드시 `composables/api/` 의 래퍼를 통해 수행
- 직접 `$fetch` / `fetch` 사용 금지 (테스트 목적 제외)
## 테스트
- Vitest 를 기본 테스트 러너로 사용
- 테스트 파일은 소스 옆에 `*.spec.ts` 로 배치
- 공통 유틸과 composable 은 커버리지 80% 이상 유지

View File

@@ -0,0 +1,33 @@
# 프로젝트 개요
> 양식 출처: `.claude/common/templates/project/overview.md`
> 샘플 프로젝트의 예시로 채워진 버전입니다.
## 서비스
- **이름**: Sample Nuxt App
- **설명**: fe-common-rules 도입 예시용 샘플 프로젝트
- **배포 환경**: 로컬 전용 (샘플)
- **저장소**: git@github.com:<org>/sample-nuxt-project.git
## 기술 스택
- **Framework**: Nuxt 4
- **UI**: Vue 3 (`<script setup lang="ts">`)
- **Language**: TypeScript (strict)
- **Styling**: Tailwind CSS
- **상태관리**: Pinia (필요 시)
- **테스트**: Vitest
- **패키지매니저**: pnpm
## 주요 기능
- 메인 페이지 (`/`)
- 샘플 컴포넌트 `HelloCard.vue`
- 공통 skill `vue-component-review` 를 활용한 컴포넌트 점검
- 공통 skill `conventional-commit` 을 활용한 커밋 메시지 작성
## 팀 / 오너
- 오너: Frontend Team
- 문의 채널: #fe-chapter (Slack)

View File

@@ -0,0 +1 @@
../common/skills/conventional-commit

View File

@@ -0,0 +1 @@
../common/skills/edm-email-html

View File

@@ -0,0 +1 @@
../common/skills/vue-component-review

24
examples/sample-nuxt-project/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@@ -0,0 +1,7 @@
# 실제 프로젝트에서는 이 파일이 .gitmodules 로 생성됩니다.
# git submodule add <repo-url> .claude/common 명령 실행 후 자동 생성되며,
# 아래와 같은 형태로 저장됩니다. (샘플은 실제 submodule 이 아니므로 예시로 포함)
[submodule ".claude/common"]
path = .claude/common
url = git@github.com:<your-org>/fe-common-rules.git
branch = main

View File

@@ -0,0 +1,16 @@
# sample-nuxt-project
Nuxt 4 + Vue 3 + TypeScript + Tailwind CSS 기반의 샘플 프로젝트입니다.
이 프로젝트는 `fe-common-rules` 공통 지침과 skill 을 submodule 형태로 연결하여 사용합니다.
## 공통 지침
@.claude/common/CLAUDE.md
## 프로젝트 지침
@.claude/project/overview.md
@.claude/project/conventions.md
@.claude/project/architecture.md

View File

@@ -0,0 +1,107 @@
# sample-nuxt-project
`fe-common-rules` 공통 지침과 skill 을 실제 프로젝트에서 어떻게 사용하는지 보여주는 **샘플**입니다.
> 💡 이 샘플은 실제로 실행 가능한 Nuxt 앱이 아니라, **디렉토리 구조와 CLAUDE.md / skill 연동 방식**을 보여주기 위한 최소 구성입니다.
## 구조
```
sample-nuxt-project/
├── CLAUDE.md # 공통 + 프로젝트 지침 @import 엔트리
├── .claude/
│ ├── common/ # fe-common-rules submodule
│ │ ├── CLAUDE.md
│ │ ├── rules/*.md # 코딩/프레임워크/커밋/워크플로 지침
│ │ ├── templates/project/*.md # 프로젝트 지침 양식
│ │ ├── skills/ # 팀 공용 Claude skill
│ │ │ ├── vue-component-review/SKILL.md
│ │ │ └── conventional-commit/SKILL.md
│ │ └── scripts/
│ │ ├── install.sh
│ │ ├── init-project.sh
│ │ ├── link-skills.sh
│ │ └── update.sh
│ ├── project/ # 프로젝트 고유 지침 (양식에서 복사 후 편집)
│ │ ├── overview.md
│ │ ├── conventions.md
│ │ └── architecture.md
│ └── skills/ # 공통 skill 링크 + 프로젝트 고유 skill
│ ├── vue-component-review → ../common/skills/vue-component-review
│ └── conventional-commit → ../common/skills/conventional-commit
├── package.json # Nuxt 4 + Vue 3 + TS + Tailwind 의존성
├── nuxt.config.ts
├── tsconfig.json
├── tailwind.config.ts
├── app.vue
└── .gitignore
```
## 실제 프로젝트에서는 어떻게 만드나요?
### 방법 A: install.sh 한 번으로 세팅
```bash
cd my-project
git init && git add . && git commit -m "chore: init"
curl -fsSL https://<raw>/scripts/install.sh | bash -s -- git@github.com:<org>/fe-common-rules.git
```
스크립트가 아래를 한 번에 처리합니다.
1. `.claude/common/` 에 fe-common-rules 를 submodule 로 추가
2. `.claude/common/templates/project/*.md``.claude/project/` 로 복사
3. `.claude/common/templates/CLAUDE.md.tpl` → 루트 `CLAUDE.md` 로 복사
4. `.claude/common/skills/*``.claude/skills/*` 로 심볼릭 링크
### 방법 B: 수동 설정
```bash
git submodule add git@github.com:<org>/fe-common-rules.git .claude/common
git submodule update --init --recursive
# 프로젝트 지침 양식 복사
mkdir -p .claude/project
cp .claude/common/templates/project/*.md .claude/project/
# 루트 CLAUDE.md 템플릿 복사
cp .claude/common/templates/CLAUDE.md.tpl CLAUDE.md
# 공통 skill 링크
bash .claude/common/scripts/link-skills.sh
```
## 공통 지침 / skill 업데이트
```bash
# submodule 을 최신으로 당기기
git submodule update --remote --merge .claude/common
# 새로 추가된 skill 이 있다면 자동 링크
bash .claude/common/scripts/link-skills.sh
git add .gitmodules .claude
git commit -m "chore: sync fe-common-rules"
```
심볼릭 링크 방식이므로 **이미 링크된 기존 skill 의 내용**은 submodule 업데이트만으로
자동 최신화됩니다. `link-skills.sh` 는 "새로 추가된 skill 이 있을 때" 한 번씩 실행하면 됩니다.
## 프로젝트 지침 양식 업데이트
```bash
# 양식 차이 확인
bash .claude/common/scripts/init-project.sh --diff
# 없는 양식만 새로 복사 (기존 파일 보존)
bash .claude/common/scripts/init-project.sh
# 강제 덮어쓰기
bash .claude/common/scripts/init-project.sh --force
```
## Claude 가 읽는 순서
1. 루트 `CLAUDE.md``@.claude/common/CLAUDE.md``@rules/*.md``@.claude/project/*.md`
2. Claude 가 특정 작업을 할 때 `.claude/skills/<skill>/SKILL.md` 의 description 과 매칭되면 해당 skill 을 자동 트리거
3. 충돌이 있을 경우 **프로젝트 지침이 우선**합니다.

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
const title = 'fe-common-rules 연동 샘플'
</script>
<template>
<div class="flex min-h-screen items-center justify-center bg-brand-50">
<div class="rounded-2xl bg-white px-8 py-10 shadow-md">
<h1 class="text-2xl font-bold text-brand-700">{{ title }}</h1>
<p class="mt-2 text-slate-600">
공통 지침은 <code>.claude/common</code>,
프로젝트 지침은 <code>.claude/project</code>,
공통 skill <code>.claude/skills</code> 에서 확인하세요.
</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-01-01',
devtools: { enabled: true },
modules: ['@nuxtjs/tailwindcss'],
typescript: {
strict: true,
typeCheck: true,
},
app: {
head: {
title: 'Sample Nuxt Project',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
},
},
})

View File

@@ -0,0 +1,27 @@
{
"name": "sample-nuxt-project",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"preview": "nuxt preview",
"lint": "eslint .",
"format": "prettier --write .",
"typecheck": "nuxt typecheck"
},
"dependencies": {
"nuxt": "^4.0.0",
"vue": "^3.5.0"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.0",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"prettier": "^3.3.0",
"prettier-plugin-tailwindcss": "^0.6.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
}

View File

@@ -0,0 +1,22 @@
import type { Config } from 'tailwindcss'
export default {
content: [
'./components/**/*.{vue,js,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
'./app.vue',
],
theme: {
extend: {
colors: {
brand: {
50: '#eef2ff',
500: '#6366f1',
700: '#4338ca',
},
},
},
},
plugins: [],
} satisfies Config

View File

@@ -0,0 +1,9 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}

View File

@@ -97,10 +97,35 @@ else
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 "다음 작업을 진행해 주세요:"

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

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,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 프리셋과 다름
- ...
### ✅ 좋은 점
- 간단한 컴포넌트에 잘 맞는 단일 책임 구조
- ...
```
## 주의사항
- 사용자가 리팩토링을 **요청하지 않은 경우** 코드를 직접 수정하지 말고 리뷰만 수행한다.
- 프로젝트 지침과 공통 지침이 충돌하면 프로젝트 지침을 따르되, 차이를 사용자에게 알린다.
- 실제 코드 동작 변경(기능 수정)은 리뷰 범위가 아니다. 별도 작업으로 분리 제안한다.