diff --git a/.claude/hooks/guard-preview-branch.mjs b/.claude/hooks/guard-preview-branch.mjs
index 83cdfb4..892f1fc 100755
--- a/.claude/hooks/guard-preview-branch.mjs
+++ b/.claude/hooks/guard-preview-branch.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
//
-// 비개발자가 Claude로 작업할 때 반드시 .claude/worktrees 아래의 preview/ 브랜치 워크트리에서만
+// 비개발자가 Claude로 작업할 때 반드시 .claude/worktrees 아래의 work/ 브랜치 워크트리에서만
// 파일을 고치고 커밋하게 강제하는 PreToolUse 훅이다.
//
// 이 훅은 .claude/settings.json에 등록되어 있어 기본으로 켜져 있다. 개발자는 개인 파일인
@@ -12,10 +12,10 @@
//
// 막는 범위는 다음과 같다.
// - 이 저장소 안의 파일을 Edit/Write할 때, 그 파일이 .claude/worktrees/<이름>/ 아래에 있고
-// 그 워크트리의 브랜치가 preview/로 시작하지 않으면 막는다. 메인 워킹 디렉토리는 항상 막는다.
+// 그 워크트리의 브랜치가 work/로 시작하지 않으면 막는다. 메인 워킹 디렉토리는 항상 막는다.
// - 저장소의 .claude/hooks, .claude/scripts, .claude/settings.json은 워크트리 안이라도 막는다.
-// - Bash의 git commit, merge, rebase, reset, cherry-pick은 preview/ 워크트리 안에서만 허용한다.
-// - git push는 preview/ 브랜치로만 허용하고, force push와 develop, main으로의 push, gh pr merge는 막는다.
+// - Bash의 git commit, merge, rebase, reset, cherry-pick은 work/ 워크트리 안에서만 허용한다.
+// - git push는 work/ 브랜치로만 허용하고, force push와 preview, main으로의 push, gh pr merge는 막는다.
//
// 저장소 밖의 파일(스크래치패드, 메모리 등)은 검사하지 않는다.
@@ -26,9 +26,11 @@ import { fileURLToPath } from 'node:url';
if (process.env.FI_ADMIN_DEVELOPER === '1') process.exit(0);
+// 작업 브랜치 이름 앞부분. 저장소에 preview 브랜치가 있어 git이 preview/<이름> 브랜치를 만들 수 없으므로 work/를 쓴다.
+const BRANCH_PREFIX = 'work/';
const GUIDE =
- '작업은 preview/ 브랜치 워크트리에서만 할 수 있습니다. 먼저 `bash .claude/scripts/new-preview-worktree.sh <영문-작업-이름>`을 실행해 ' +
+ '작업은 work/ 브랜치 워크트리에서만 할 수 있습니다. 먼저 `bash .claude/scripts/new-preview-worktree.sh <영문-작업-이름>`을 실행해 ' +
'.claude/worktrees/<작업-이름>/ 워크트리를 만들고, 그 안의 파일만 수정하십시오.';
const block = (reason) => {
@@ -67,13 +69,13 @@ const existingDir = (path) => {
return realpathSync(dir);
};
-// 주어진 디렉토리가 이 저장소의 preview/ 워크트리 안이면 그 워크트리 경로를, 아니면 이유를 돌려준다.
+// 주어진 디렉토리가 이 저장소의 work/ 워크트리 안이면 그 워크트리 경로를, 아니면 이유를 돌려준다.
const checkLocation = (dir) => {
if (!isInside(dir, WORKTREES)) return { error: '메인 워킹 디렉토리에서는 작업할 수 없습니다.' };
const top = git(dir, 'rev-parse', '--show-toplevel');
if (!top || realpathSync(top) === ROOT) return { error: '워크트리 밖입니다.' };
const branch = git(dir, 'rev-parse', '--abbrev-ref', 'HEAD');
- if (!branch.startsWith('preview/')) return { error: `현재 브랜치(${branch || '알 수 없음'})가 preview/로 시작하지 않습니다.` };
+ if (!branch.startsWith(BRANCH_PREFIX)) return { error: `현재 브랜치(${branch || '알 수 없음'})가 ${BRANCH_PREFIX}로 시작하지 않습니다.` };
return { top: realpathSync(top), branch };
};
@@ -116,7 +118,8 @@ if (tool === 'Bash') {
if (/\bgit\s+(-C\s+\S+\s+)?push\b/.test(command)) {
if (/\s(--force\S*|-f|--mirror|--delete|-d)\b|\s\+\S/.test(command)) block('강제 push와 브랜치 삭제는 할 수 없습니다.');
- if (/\b(develop|main|master)\b/.test(command)) block('develop, main으로는 push할 수 없습니다.');
+ // preview 브랜치 자체로 가는 push만 막도록 preview 뒤에 공백이나 명령 끝이 오는 경우로 좁힌다.
+ if (/\b(main|master)\b|(^|[\s:])preview(?=\s|$)/.test(command)) block('preview, main으로는 push할 수 없습니다.');
const location = checkLocation(runDir);
if (location.error) block(location.error);
}
diff --git a/.claude/scripts/new-preview-worktree.sh b/.claude/scripts/new-preview-worktree.sh
index b5c3f90..0ae94a7 100755
--- a/.claude/scripts/new-preview-worktree.sh
+++ b/.claude/scripts/new-preview-worktree.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# 최신 origin/develop에서 preview/<작업-이름> 브랜치를 만들고 .claude/worktrees/<작업-이름>/에
+# 최신 origin/preview에서 work/<작업-이름> 브랜치를 만들고 .claude/worktrees/<작업-이름>/에
# 워크트리로 체크아웃한다. 비개발자가 Claude로 작업을 시작할 때 Claude가 이 스크립트를 실행한다.
#
# bash .claude/scripts/new-preview-worktree.sh notice-banner-text
@@ -18,7 +18,8 @@ fi
root=$(cd "$(dirname "$0")/../.." && pwd)
dir="$root/.claude/worktrees/$name"
-branch="preview/$name"
+# 저장소에 preview 브랜치가 있으면 git이 preview/<이름> 브랜치를 만들 수 없으므로 work/를 쓴다.
+branch="work/$name"
if [ -e "$dir" ]; then
echo "이미 있는 워크트리입니다: $dir" >&2
@@ -29,8 +30,8 @@ if git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
exit 1
fi
-git -C "$root" fetch --quiet origin develop
-git -C "$root" worktree add --quiet --no-track -b "$branch" "$dir" origin/develop
+git -C "$root" fetch --quiet origin preview
+git -C "$root" worktree add --quiet --no-track -b "$branch" "$dir" origin/preview
copied=0
for file in "$root"/.env* "$root/next-env.d.ts"; do
diff --git a/.claude/skills/create-pr/SKILL.md b/.claude/skills/create-pr/SKILL.md
index a374c07..2c47a83 100644
--- a/.claude/skills/create-pr/SKILL.md
+++ b/.claude/skills/create-pr/SKILL.md
@@ -5,14 +5,17 @@ description: 사용자가 PR(풀 리퀘스트) 생성을 요청했을 때 이
# create-pr
-`.github/pull_request_template.md` 구조(관련 이슈 / 작업 내용 / 참고 사항 / 체크리스트)를 그대로 따라 PR 본문을 작성하고 `gh pr create --title "
" --body ""`로 전달한다.
+`.github/pull_request_template.md` 구조(PR 종류 / 관련 이슈 / 작업 내용 / 참고 사항 / 체크리스트)를 그대로 따라 PR 본문을 작성하고 `gh pr create --title "" --body ""`로 전달한다.
## 절차
1. `.github/pull_request_template.md`를 읽어 최신 템플릿 구조를 확인한다.
2. 대상 브랜치의 커밋 히스토리와 diff(`git log`, `git diff ...HEAD`)를 확인한다 — 마지막 커밋만이 아니라 브랜치에 포함된 모든 커밋을 근거로 삼는다.
3. 제목은 `브랜치명의 앞 키워드: 간단한 요약` 형식으로 짓는다. 예를 들어 브랜치가 `feat/dashboard`면 `feat: 대시보드 응답 시간 차트 기간별 필터링 추가`처럼 `/` 앞부분을 그대로 키워드로 쓰고, 콜론 뒤에 브랜치 전체 작업을 온전한 문장 대신 간단한 명사구로 요약한다.
+ - 제목 맨 앞에 PR 종류를 붙인다. 테스트 코드(Jest, `tests/e2e`)만 바꾼 PR은 `[테스트]`, 그 밖의 기능 PR은 `[기능]`이다. 예: `[기능] feat: 대시보드 기간 필터 추가`, `[테스트] test: 대시보드 기간 필터 테스트 추가`.
+ - diff에 기능 코드와 테스트 코드가 섞여 있으면 PR을 만들지 말고, 루트 CLAUDE.md의 PR 분리 규칙에 따라 어떻게 나눌지 사용자에게 먼저 확인한다.
4. 템플릿 섹션별로 본문을 작성한다:
+ - **PR 종류**: 제목에 붙인 종류와 같은 항목 하나만 `[x]`로 체크한다.
- **관련 이슈**: 사용자가 이슈 번호를 알려준 경우에만 채운다. 모르면 템플릿 형식만 남기고 비워둔다 (번호를 추측해서 채우지 않는다).
- **작업 내용**: 커밋 히스토리와 diff를 근거로 실제 변경된 내용을 온전한 문장으로 요약한다.
- **참고 사항**: 리뷰어가 알아야 할 맥락(브레이킹 체인지, 후속 작업 필요 여부, 임시 조치 등)이 있으면 적고 없으면 생략한다.
diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml
new file mode 100644
index 0000000..604a094
--- /dev/null
+++ b/.github/auto_assign.yml
@@ -0,0 +1,8 @@
+addReviewers: true
+
+addAssignees: author
+
+reviewers:
+ - wlrnjs
+
+numberOfReviewers: 0
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 2dd2902..751cccc 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,5 +1,11 @@
# Pull Request
+## PR 종류
+
+- [ ] 기능 개발
+- [ ] 테스트 코드
+
+
## 관련 이슈
- close #이슈번호
diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml
new file mode 100644
index 0000000..1468c7f
--- /dev/null
+++ b/.github/workflows/auto-assign.yml
@@ -0,0 +1,24 @@
+name: Auto Assign Reviewers
+on:
+ pull_request:
+ types: [opened, reopened, ready_for_review]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ addReviewers:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Create GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@v3
+ with:
+ app-id: ${{ vars.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
+ - uses: kentaro-m/auto-assign-action@v2.0.1
+ with:
+ repo-token: ${{ steps.app-token.outputs.token }}
+ configuration-path: .github/auto_assign.yml
diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml
index 97cf5d4..97975f2 100644
--- a/.github/workflows/jest.yml
+++ b/.github/workflows/jest.yml
@@ -2,7 +2,7 @@ name: CI - Jest
on:
pull_request:
- branches: [develop]
+ branches: [preview]
permissions:
contents: read
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 3729543..aaf4666 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -2,7 +2,7 @@ name: Playwright Tests
on:
pull_request:
- branches: [develop]
+ branches: [preview]
permissions:
contents: read
diff --git a/.github/workflows/preview-promotion.yml b/.github/workflows/preview-promotion.yml
new file mode 100644
index 0000000..9422264
--- /dev/null
+++ b/.github/workflows/preview-promotion.yml
@@ -0,0 +1,73 @@
+name: Auto Main PR
+
+on:
+ push:
+ branches:
+ - preview
+
+# PR 생성과 수정은 GitHub App 토큰으로 하므로 기본 토큰은 체크아웃용 읽기 권한만 둔다.
+permissions:
+ contents: read
+
+# 연달아 머지되면 PR 생성이 겹칠 수 있으므로, 목록을 매번 새로 계산하는 최신 실행만 남긴다.
+concurrency:
+ group: preview-promotion
+ cancel-in-progress: true
+
+jobs:
+ create-or-update-pr:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Create GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@v3
+ with:
+ app-id: ${{ vars.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+
+ - name: Create or Update Pull Request
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ GH_REPO: ${{ github.repository }}
+ shell: bash
+ run: |
+ # if 조건 안에서는 rev-list가 실패해도 멈추지 않으므로 변수에 먼저 담는다.
+ count=$(git rev-list --count origin/main..origin/preview)
+ if [ "$count" -eq 0 ]; then
+ echo "main에 반영할 변경 사항이 없습니다."
+ exit 0
+ fi
+
+ # preview에 머지된 PR만 모으기 위해 preview의 first-parent 머지 커밋에서 PR 번호를 뽑는다.
+ pr_numbers=$(git log origin/main..origin/preview --first-parent --merges --format='%s' \
+ | sed -n 's/^Merge pull request #\([0-9]*\) .*/\1/p')
+
+ {
+ echo "## 작업 요약"
+ echo "Preview 브랜치의 최신 변경 사항을 Main 브랜치로 반영합니다."
+ echo
+ echo "## 포함된 PR"
+ if [ -z "$pr_numbers" ]; then
+ echo "- 없음"
+ fi
+ for number in $pr_numbers; do
+ gh pr view "$number" --json number,title,author \
+ --jq '"- #\(.number) \(.title) (@\(.author.login))"'
+ done
+ } > pr-body.md
+
+ existing=$(gh pr list --base main --head preview --state open --json number --jq '.[0].number // empty')
+
+ if [ -n "$existing" ]; then
+ gh pr edit "$existing" --body-file pr-body.md
+ else
+ gh pr create --base main --head preview \
+ --title "[배포 대기] main 최신화" \
+ --body-file pr-body.md \
+ --label "🌿 main"
+ fi
diff --git a/CLAUDE.md b/CLAUDE.md
index 9229efd..f76b43a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,12 +1,8 @@
# FI-ADMIN
-[찾아줘!](https://www.finditem.kr/) 서비스의 관리자 페이지 프론트엔드. 운영 앱(`finditem/FI-FE`)의 `/admin` 라우트를 별도 저장소로 분리한 Next.js 15(App Router) 단일 앱이다.
+[찾아줘!](https://www.finditem.kr/) 서비스의 관리자 페이지 프론트엔드다. 운영 앱(`finditem/FI-FE`)의 `/admin` 라우트를 별도 저장소로 분리한 Next.js 15(App Router) 단일 앱이며, 운영 주소는 https://a.finditem.kr 이다.
-## 주소
-
-- 운영 어드민: https://a.finditem.kr
-- 개발 서버 API 문서(Swagger): https://dev-api.finditem.kr/swagger-ui/index.html
-- 같은 문서의 OpenAPI JSON은 인증 없이 `https://dev-api.finditem.kr/v3/api-docs`로 받을 수 있다. 서버가 내려주는 필드는 FE 타입만 보지 말고 여기서 확인한다.
+스택은 Next.js 15, React 19, TypeScript 5, Tailwind CSS 3, Jest(단위), Playwright(e2e), pnpm이다. 운영 앱과 달리 next-intl, Sentry, PWA, Capacitor, MSW, Storybook, ESLint는 넣지 않았다.
## 절대 규칙
@@ -20,26 +16,18 @@
- 환경 변수 값이 필요하거나 env 때문에 생긴 문제로 보이면 사용자에게 직접 확인해 달라고 요청한다.
- 막는 훅은 `.claude/hooks/guard-env.mjs`이고 개발자를 포함해 누구도 끌 수 없다.
-### 2. 작업은 `.claude/worktrees` 아래 preview 브랜치 워크트리에서만 한다
+### 2. 작업은 `.claude/worktrees` 아래 `work/` 브랜치 워크트리에서만 한다
-이 저장소는 비개발자도 Claude로 작업한다. 메인 워킹 디렉토리나 `develop`에서 바로 고치지 않도록 다음 순서를 반드시 지킨다.
+이 저장소는 비개발자도 Claude로 작업한다. 메인 워킹 디렉토리나 `preview`에서 바로 고치지 않도록 다음 순서를 반드시 지킨다.
-1. 파일을 고치기 전에 `bash .claude/scripts/new-preview-worktree.sh <작업-이름>`을 실행한다. 작업 이름은 영문 소문자와 하이픈으로 짓는다(예: `notice-banner-text`). 스크립트가 최신 `origin/develop`에서 `preview/<작업-이름>` 브랜치를 만들어 `.claude/worktrees/<작업-이름>/`에 체크아웃하고, 필요한 설정 파일을 복사한다.
+1. 파일을 고치기 전에 `bash .claude/scripts/new-preview-worktree.sh <작업-이름>`을 실행한다. 작업 이름은 영문 소문자와 하이픈으로 짓는다(예: `notice-banner-text`). 스크립트가 최신 `origin/preview`에서 `work/<작업-이름>` 브랜치를 만들어 `.claude/worktrees/<작업-이름>/`에 체크아웃하고, 필요한 설정 파일을 복사한다.
2. 이후 모든 수정과 명령은 그 워크트리 경로 안에서 한다. 같은 작업을 이어서 할 때는 기존 워크트리를 쓰고 새로 만들지 않는다.
3. 화면 확인이 필요하면 사용자에게 그 워크트리 경로에서 `pnpm dev`를 직접 실행해 달라고 안내한다.
-4. 커밋과 push는 사용자에게 무엇을 바꿨는지 설명하고 허락을 받은 뒤에 한다. push는 `preview/` 브랜치로만 하고, `develop`과 `main`으로 push하거나 force push하거나 PR을 머지하지 않는다. `develop` 반영은 개발자가 PR 리뷰를 거쳐 한다.
+4. 커밋과 push는 사용자에게 무엇을 바꿨는지 설명하고 허락을 받은 뒤에 한다. push는 `work/` 브랜치로만 하고, `preview`와 `main`으로 push하거나 force push하거나 PR을 머지하지 않는다. `preview` 반영은 개발자가 PR 리뷰를 거쳐 한다.
5. 비개발자에게 설명할 때는 git 용어를 줄이고, 무엇이 바뀌었는지와 어디서 확인하면 되는지를 먼저 말한다.
막는 훅은 `.claude/hooks/guard-preview-branch.mjs`다. 개발자는 개인 파일인 `.claude/settings.local.json`에 `{"env": {"FI_ADMIN_DEVELOPER": "1"}}`을 넣어 이 훅만 끄고 `feat/`, `fix/` 등 기존 브랜치 흐름으로 작업할 수 있다. 이 훅이 켜진 상태에서는 `.claude/hooks`, `.claude/scripts`, `.claude/settings.json`도 수정할 수 없다.
-## 스택
-
-- Next.js 15 / React 19 / TypeScript 5 (`next.config.ts`에서 `reactCompiler: true` — React Compiler 활성화)
-- Tailwind CSS 3 (디자인 토큰은 `src/utils/tokens/tailwind.config.js`, FI-DS에서 생성된 값)
-- 테스트: Jest(단위), Playwright(e2e, `tests/e2e`). e2e는 API를 `page.route`로 흉내 내고 `src/mock/data`의 목데이터를 쓴다.
-- 패키지 매니저: pnpm(`package.json`의 `packageManager`). npm과 npx 대신 `pnpm add`, `pnpm exec`를 쓴다.
-- 운영 앱과 달리 next-intl, Sentry, PWA, Capacitor, MSW, Storybook, ESLint는 넣지 않았다. 어드민은 한국어 전용이므로 문구는 번역 함수 없이 한국어로 작성한다.
-
## 구조
```
@@ -55,44 +43,97 @@ src/
mock/
```
-- 계정 권한(role)별 영역은 `src/app`의 라우트 그룹 하나로 묶고, `src/constants/ROLE_AREAS.ts`에 권한과 첫 화면을 등록한다. 미들웨어가 이 목록으로 접근을 검사하고 로그인 후 이동할 곳을 정한다. B2B 같은 영역을 추가할 때도 같은 방식을 따르며, 모노레포로 나누지 않는다.
-- 게시글 상세, 공지 상세, 비밀번호 변경처럼 운영 앱에만 있는 화면으로 연결할 때는 `getServiceUrl`로 절대 주소를 만든다.
-- 라우트 전용 코드는 해당 라우트 폴더 하위 `_components`/`_hooks`/`_types`/`_utils`(private 폴더)에 둔다. 여러 라우트에서 재사용되면 그때 `src/components`, `src/hooks` 등 전역 폴더로 올린다.
-- `hooks/`, `store/`, `utils/`는 도메인별로 묶지 않고, 함수/훅 하나당 폴더 하나(`utils/cn/`, `hooks/useLogout/` 등)로 세분화하는 컨벤션이다. 새 유틸/훅을 추가할 때도 이 패턴을 따른다.
-- `_components` 하위 컴포넌트도 동일하게 컴포넌트 하나당 폴더 하나. 그 컴포넌트 내부에서만 쓰는 하위 조각은 `_internal` 폴더에 둔다.
-- 운영 앱에서 코드를 옮겨올 때도 위 구조를 그대로 따른다. `useTranslations`를 쓰던 공통 컴포넌트는 한국어 문구로 바꿔 넣는다.
-
-## 커밋 컨벤션 (commitlint 강제)
-
-- type: `feat`, `fix`, `docs`, `hotfix`, `refactor`, `test`, `chore`, `rename`, `asset`, `design`, `a11y` 중 하나
-- scope 필수 (비워두면 커밋 실패)
-- 예: `feat(report): 신고 목록 필터 추가`
+
-## 검증 커맨드
+패키지 매니저는 pnpm이다. npm과 npx 대신 `pnpm add`, `pnpm exec`를 쓴다.
-- 기본: `pnpm test` + `pnpm build` (타입체크 포함). 대부분의 회귀를 이 둘로 잡는다.
-- e2e: `pnpm build` 뒤 `CI=1 E2E_PORT=3918 pnpm test:e2e`. `CI`를 주면 dev 서버 대신 빌드 결과로 서버를 띄우고, 포트를 바꿔 켜 둔 dev 서버와 겹치지 않게 한다.
-- 타입만 확인하면 되는 경우는 `pnpm exec tsc --noEmit`으로 대신한다. dev 서버가 켜져 있을 때 `pnpm build`를 돌리면 같은 `.next`를 덮어써 dev 런타임이 깨진다.
+| 명령 | 용도 |
+|---|---|
+| `pnpm test` | Jest 단위 테스트 |
+| `pnpm build` | 프로덕션 빌드(타입체크 포함). `pnpm test`와 함께 기본 검증이다 |
+| `pnpm exec tsc --noEmit` | 타입만 확인할 때 `pnpm build` 대신 쓴다 |
+| `CI=1 E2E_PORT=3918 pnpm test:e2e` | `pnpm build` 뒤에 실행하는 e2e. `CI`를 주면 빌드 결과로 서버를 띄우고, 포트를 바꿔 dev 서버와 겹치지 않게 한다 |
+| `pnpm dev` | 사용자가 이미 띄워 둔 상태라고 가정한다. Claude는 직접 실행하지 않는다 |
-## 텍스트 작성 원칙
+dev 서버가 켜져 있을 때 `pnpm build`를 돌리면 같은 `.next`를 덮어써 dev 런타임이 깨진다.
+
-커밋 메시지, PR 본문, 코드 주석 등 Claude가 작성하는 모든 텍스트 산출물은 온전한 문장으로만 작성하고 이모티콘을 사용하지 않는다.
+
-## React Compiler와 메모이제이션
+다른 사람이 먼저 반영한 변경과 어긋나거나 충돌을 PR 단계에서야 발견하지 않도록 원격 `preview`의 최신 내용을 받는다.
-이 프로젝트는 React Compiler가 켜져 있다(`next.config.ts`의 `reactCompiler: true`). 컴포넌트/훅 내부에서 `useMemo`, `useCallback`을 수동으로 작성하지 않는다 — 컴파일러가 자동으로 처리한다. 새 훅이나 컴포넌트를 작성할 때도, 기존 코드를 참고해 복사할 때도 이 패턴을 넣지 않는다. 외부 라이브러리 API가 메모이즈된 함수/값을 명시적으로 요구하는 경우처럼 컴파일러가 커버하지 못하는 예외적 상황에서만 사용하고, 그 경우 왜 필요한지 주석으로 남긴다.
+- `git fetch origin --prune`을 실행한다. 새 작업은 최신 `origin/preview`에서 워크트리를 만든다(`new-preview-worktree.sh`는 이 fetch를 스스로 한다).
+- 기존 워크트리에서 이어서 작업하거나 작업을 끝냈을 때는 워킹트리가 깨끗한지 확인한 뒤 그 워크트리에서 `git merge origin/preview`로 합친다.
+- 메인 워킹 디렉토리의 `preview`도 `git -C <저장소 루트> pull --ff-only origin preview`로 맞춘다. 커밋되지 않은 변경이 있거나 fast-forward가 안 되면 건드리지 않고 사용자에게 알린다.
+- 이미 push한 브랜치를 rebase하면 force push가 필요해지므로 rebase가 아니라 merge로 한다.
+- merge에서 충돌이 나면 임의로 해결하지 말고 충돌한 파일과 양쪽 변경 내용을 사용자에게 보여주고 어떻게 합칠지 확인받는다.
+
-## 표준 작업 흐름
+
+- 구현 전에 `plan-route` 스킬을 실행한다. 라우트 폴더의 `_docs/plan.md`에 작업 항목을 todo 체크리스트로 기록하고 진행에 따라 갱신해, 세션이 끊겨도 다음 세션이나 다른 팀원이 이어받을 수 있게 한다.
+- 여러 라우트에 걸친 작업이나 전역 공통 코드(`src/components`, `src/hooks` 등) 작업에는 적용하지 않는다.
+
-1. 기존 코드 패턴과 디렉토리 구조를 그대로 따른다. 새 추상화나 새로운 디렉토리 규칙을 임의로 만들지 않는다.
-2. `pnpm dev`는 사용자가 이미 띄워서 켜둔 상태라고 가정한다. Claude가 직접 실행하지 않는다 — 장기 실행 프로세스라 포트 충돌이나 좀비 프로세스를 남길 수 있다.
-3. 로컬 `git commit`은 응답 흐름에 맞춰 자율적으로 수행할 수 있다. 단, 이번 응답에서 Claude가 Edit/Write로 직접 건드린 파일만 `git add`한다 (`git add -A`/`git add .` 금지). 커밋 직전 `git status`로 staging 대상이 의도한 파일과 정확히 일치하는지 확인한다.
-4. `git push`, PR 생성 등 원격 저장소에 영향을 주는 작업은 사용자가 명시적으로 요청하기 전에는 수행하지 않는다. force-push는 요청 여부와 관계없이 수행하지 않는다.
-
-## PR 생성
-
-사용자가 PR 생성을 요청하면 `create-pr` 스킬을 실행한다. `gh pr create` 실행 자체는 항상 사용자 확인 후 진행한다 (위 4번 규칙).
-
-## 라우트 작업 계획
-
-특정 라우트(`page.tsx`가 있는 디렉토리) 하나에 국한된 작업을 시작하기 전에 `plan-route` 스킬을 실행한다. 해당 라우트 폴더의 `_docs/plan.md`에 작업 항목을 todo 체크리스트로 기록하고 진행에 따라 갱신해, 세션이 끊겨도 다음 세션이나 다른 팀원이 이어받을 수 있게 한다. 여러 라우트에 걸친 작업이나 전역 공통 코드(`src/components`, `src/hooks` 등) 작업에는 적용하지 않는다.
+
+- 새 추상화나 새로운 디렉토리 규칙을 임의로 만들지 않는다.
+- 라우트 전용 코드는 해당 라우트 폴더 하위 `_components`/`_hooks`/`_types`/`_utils`(private 폴더)에 둔다. 여러 라우트에서 재사용되면 그때 `src/components`, `src/hooks` 등 전역 폴더로 올린다.
+- `hooks/`, `store/`, `utils/`는 도메인별로 묶지 않고 함수/훅 하나당 폴더 하나(`utils/cn/`, `hooks/useLogout/` 등)로 나눈다.
+- `_components` 하위 컴포넌트도 컴포넌트 하나당 폴더 하나다. 그 컴포넌트 내부에서만 쓰는 하위 조각은 `_internal` 폴더에 둔다.
+
+
+
+- 이 저장소의 구조 규칙을 그대로 따른다.
+- `useTranslations`를 쓰던 공통 컴포넌트는 한국어 문구로 바꿔 넣는다.
+
+
+
+- 어드민은 한국어 전용이다. 번역 함수 없이 한국어로 직접 작성한다.
+
+
+
+- React Compiler가 켜져 있다(`next.config.ts`의 `reactCompiler: true`). `useMemo`, `useCallback`을 수동으로 작성하지 않고, 기존 코드를 복사할 때도 넣지 않는다.
+- 외부 라이브러리 API가 메모이즈된 값을 명시적으로 요구하는 경우처럼 컴파일러가 커버하지 못할 때만 쓰고, 그 이유를 주석으로 남긴다.
+
+
+
+- 디자인 토큰은 FI-DS에서 생성된 `src/utils/tokens/tailwind.config.js`에 있다.
+
+
+
+- FE 타입만 보고 판단하지 않는다. 개발 서버 OpenAPI JSON을 인증 없이 `https://dev-api.finditem.kr/v3/api-docs`에서 받아 확인한다. 사람이 볼 문서는 https://dev-api.finditem.kr/swagger-ui/index.html 이다.
+
+
+
+- 권한별 영역은 `src/app`의 라우트 그룹 하나로 묶고, `src/constants/ROLE_AREAS.ts`에 권한과 첫 화면을 등록한다. 미들웨어가 이 목록으로 접근을 검사하고 로그인 후 이동할 곳을 정한다.
+- B2B 같은 영역을 추가할 때도 같은 방식을 따르며 모노레포로 나누지 않는다.
+
+
+
+- `getServiceUrl`로 절대 주소를 만든다.
+
+
+
+- e2e(`tests/e2e`)는 API를 `page.route`로 흉내 내고 `src/mock/data`의 목데이터를 쓴다.
+
+
+
+- 온전한 문장으로만 작성하고 이모티콘을 사용하지 않는다.
+
+
+
+- 메시지 형식은 husky의 commit-msg 훅에서 commitlint가 검사한다. 허용 type과 scope 필수 규칙은 `commitlint.config.cjs`를 본다. 예: `feat(report): 신고 목록 필터 추가`
+- 로컬 커밋은 응답 흐름에 맞춰 자율적으로 할 수 있다. 단, 이번 응답에서 Claude가 Edit/Write로 직접 건드린 파일만 `git add`한다(`git add -A`/`git add .` 금지). 커밋 직전 `git status`로 staging 대상이 의도한 파일과 정확히 일치하는지 확인한다.
+
+
+
+- 사용자가 명시적으로 요청하기 전에는 하지 않는다. force push는 요청 여부와 관계없이 하지 않는다.
+- PR 생성을 요청받으면 `create-pr` 스킬을 실행한다. `gh pr create` 실행 자체는 항상 사용자 확인 후 진행한다.
+
+
+
+- 기능 PR에는 기능 코드만 담고, 단위 테스트(Jest)와 e2e 테스트 추가나 수정은 별도의 `test` 타입 PR로 올린다.
+- 두 PR은 브랜치도 따로 만든다. 테스트 PR은 기능 브랜치에서 갈라 만들고, 본문에 대상 기능 PR 번호를 적는다.
+- 기능 변경 때문에 기존 테스트가 깨져 CI가 실패하는 경우에도 테스트 수정은 테스트 PR로 분리하고, 기능 PR 본문에 깨지는 테스트와 테스트 PR 번호를 밝힌다.
+- 한 작업에서 기능과 테스트를 함께 고쳤다면 PR을 만들기 전에 사용자에게 어떻게 나눌지 보여주고 확인을 받는다.
+- 제목 맨 앞에 `[기능]` 또는 `[테스트]`를 붙이고 본문의 `PR 종류` 섹션에서 해당 항목을 체크한다. 예: `[기능] feat: 신고 목록 필터 추가`, `[테스트] test: 신고 목록 필터 테스트 추가`.
+