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 "<body>"`로 전달한다. +`.github/pull_request_template.md` 구조(PR 종류 / 관련 이슈 / 작업 내용 / 참고 사항 / 체크리스트)를 그대로 따라 PR 본문을 작성하고 `gh pr create --title "<title>" --body "<body>"`로 전달한다. ## 절차 1. `.github/pull_request_template.md`를 읽어 최신 템플릿 구조를 확인한다. 2. 대상 브랜치의 커밋 히스토리와 diff(`git log`, `git diff <base>...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 종류 + +- [ ] 기능 개발 +- [ ] 테스트 코드 + <!-- 둘 중 하나만 체크합니다. 기능과 테스트는 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): 신고 목록 필터 추가` +<important if="빌드, 테스트, 타입체크를 실행하거나 패키지를 설치할 때"> -## 검증 커맨드 +패키지 매니저는 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 런타임이 깨진다. +</important> -커밋 메시지, PR 본문, 코드 주석 등 Claude가 작성하는 모든 텍스트 산출물은 온전한 문장으로만 작성하고 이모티콘을 사용하지 않는다. +<important if="작업을 시작하기 전이거나, 작업을 끝내고 커밋을 보고하거나 PR을 만들기 전"> -## 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에서 충돌이 나면 임의로 해결하지 말고 충돌한 파일과 양쪽 변경 내용을 사용자에게 보여주고 어떻게 합칠지 확인받는다. +</important> -## 표준 작업 흐름 +<important if="page.tsx가 있는 라우트 하나에 국한된 작업을 시작할 때"> +- 구현 전에 `plan-route` 스킬을 실행한다. 라우트 폴더의 `_docs/plan.md`에 작업 항목을 todo 체크리스트로 기록하고 진행에 따라 갱신해, 세션이 끊겨도 다음 세션이나 다른 팀원이 이어받을 수 있게 한다. +- 여러 라우트에 걸친 작업이나 전역 공통 코드(`src/components`, `src/hooks` 등) 작업에는 적용하지 않는다. +</important> -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` 등) 작업에는 적용하지 않는다. +<important if="새 파일이나 폴더를 만들거나 코드를 다른 위치로 옮길 때"> +- 새 추상화나 새로운 디렉토리 규칙을 임의로 만들지 않는다. +- 라우트 전용 코드는 해당 라우트 폴더 하위 `_components`/`_hooks`/`_types`/`_utils`(private 폴더)에 둔다. 여러 라우트에서 재사용되면 그때 `src/components`, `src/hooks` 등 전역 폴더로 올린다. +- `hooks/`, `store/`, `utils/`는 도메인별로 묶지 않고 함수/훅 하나당 폴더 하나(`utils/cn/`, `hooks/useLogout/` 등)로 나눈다. +- `_components` 하위 컴포넌트도 컴포넌트 하나당 폴더 하나다. 그 컴포넌트 내부에서만 쓰는 하위 조각은 `_internal` 폴더에 둔다. +</important> + +<important if="운영 앱(finditem/FI-FE)에서 코드를 옮겨올 때"> +- 이 저장소의 구조 규칙을 그대로 따른다. +- `useTranslations`를 쓰던 공통 컴포넌트는 한국어 문구로 바꿔 넣는다. +</important> + +<important if="화면에 보이는 문구를 추가하거나 수정할 때"> +- 어드민은 한국어 전용이다. 번역 함수 없이 한국어로 직접 작성한다. +</important> + +<important if="React 컴포넌트나 훅을 작성하거나 수정할 때"> +- React Compiler가 켜져 있다(`next.config.ts`의 `reactCompiler: true`). `useMemo`, `useCallback`을 수동으로 작성하지 않고, 기존 코드를 복사할 때도 넣지 않는다. +- 외부 라이브러리 API가 메모이즈된 값을 명시적으로 요구하는 경우처럼 컴파일러가 커버하지 못할 때만 쓰고, 그 이유를 주석으로 남긴다. +</important> + +<important if="스타일, 색상, 간격 등 Tailwind 클래스를 다룰 때"> +- 디자인 토큰은 FI-DS에서 생성된 `src/utils/tokens/tailwind.config.js`에 있다. +</important> + +<important if="API 요청이나 응답 타입을 다루거나 서버가 내려주는 필드를 확인해야 할 때"> +- FE 타입만 보고 판단하지 않는다. 개발 서버 OpenAPI JSON을 인증 없이 `https://dev-api.finditem.kr/v3/api-docs`에서 받아 확인한다. 사람이 볼 문서는 https://dev-api.finditem.kr/swagger-ui/index.html 이다. +</important> + +<important if="계정 권한(role)별 영역을 추가하거나 로그인 후 이동, 접근 제어를 다룰 때"> +- 권한별 영역은 `src/app`의 라우트 그룹 하나로 묶고, `src/constants/ROLE_AREAS.ts`에 권한과 첫 화면을 등록한다. 미들웨어가 이 목록으로 접근을 검사하고 로그인 후 이동할 곳을 정한다. +- B2B 같은 영역을 추가할 때도 같은 방식을 따르며 모노레포로 나누지 않는다. +</important> + +<important if="게시글 상세, 공지 상세, 비밀번호 변경처럼 운영 앱에만 있는 화면으로 연결할 때"> +- `getServiceUrl`로 절대 주소를 만든다. +</important> + +<important if="테스트를 작성하거나 수정할 때"> +- e2e(`tests/e2e`)는 API를 `page.route`로 흉내 내고 `src/mock/data`의 목데이터를 쓴다. +</important> + +<important if="커밋 메시지, PR 본문, 코드 주석을 작성할 때"> +- 온전한 문장으로만 작성하고 이모티콘을 사용하지 않는다. +</important> + +<important if="git commit을 할 때"> +- 메시지 형식은 husky의 commit-msg 훅에서 commitlint가 검사한다. 허용 type과 scope 필수 규칙은 `commitlint.config.cjs`를 본다. 예: `feat(report): 신고 목록 필터 추가` +- 로컬 커밋은 응답 흐름에 맞춰 자율적으로 할 수 있다. 단, 이번 응답에서 Claude가 Edit/Write로 직접 건드린 파일만 `git add`한다(`git add -A`/`git add .` 금지). 커밋 직전 `git status`로 staging 대상이 의도한 파일과 정확히 일치하는지 확인한다. +</important> + +<important if="git push, PR 생성 등 원격 저장소에 영향을 주는 작업을 할 때"> +- 사용자가 명시적으로 요청하기 전에는 하지 않는다. force push는 요청 여부와 관계없이 하지 않는다. +- PR 생성을 요청받으면 `create-pr` 스킬을 실행한다. `gh pr create` 실행 자체는 항상 사용자 확인 후 진행한다. +</important> + +<important if="PR을 만들거나 기능과 테스트를 함께 고친 작업을 정리할 때"> +- 기능 PR에는 기능 코드만 담고, 단위 테스트(Jest)와 e2e 테스트 추가나 수정은 별도의 `test` 타입 PR로 올린다. +- 두 PR은 브랜치도 따로 만든다. 테스트 PR은 기능 브랜치에서 갈라 만들고, 본문에 대상 기능 PR 번호를 적는다. +- 기능 변경 때문에 기존 테스트가 깨져 CI가 실패하는 경우에도 테스트 수정은 테스트 PR로 분리하고, 기능 PR 본문에 깨지는 테스트와 테스트 PR 번호를 밝힌다. +- 한 작업에서 기능과 테스트를 함께 고쳤다면 PR을 만들기 전에 사용자에게 어떻게 나눌지 보여주고 확인을 받는다. +- 제목 맨 앞에 `[기능]` 또는 `[테스트]`를 붙이고 본문의 `PR 종류` 섹션에서 해당 항목을 체크한다. 예: `[기능] feat: 신고 목록 필터 추가`, `[테스트] test: 신고 목록 필터 테스트 추가`. +</important>