Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 177 additions & 27 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ permissions:
contents: read

concurrency:
# PR push는 같은 PR의 이전 실행을 취소, main/develop push는 커밋별 독립 실행
# PR push는 같은 PR의 이전 실행을 취소, main/develop push는 커밋별 독립 실행(기준 커버리지 아티팩트 유실 방지)
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
# test 잡 matrix.shard와 함께 바꾼다 — coverage-report가 blob 개수를 이 값과 대조한다
SHARD_COUNT: 3

jobs:
# 로컬 validate(pre-push)와 같은 순서. 보호된 check status라 --no-verify·훅 미설치를 우회하지 못한다
check:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -38,42 +41,103 @@ jobs:
- name: Lint
run: pnpm lint

- name: Type check
run: pnpm typecheck

- name: Dead code / unused deps (knip)
run: pnpm knip
# build가 tsc -b를 돌리므로 별도 typecheck 단계는 두지 않는다
static:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Test with coverage
run: pnpm test:cov
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

# CODECOV_TOKEN이 레포 시크릿에 있을 때만 올린다 — 등록 전에는 건너뛰고, 등록 뒤 실패는 CI 실패로 드러낸다
- name: Upload coverage to Codecov
if: env.CODECOV_TOKEN != ''
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
- name: Setup Node.js (24.x) & pnpm cache
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
disable_search: true
name: admin-fe-lcov
fail_ci_if_error: true
node-version: '24.x'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Codegen freshness
run: pnpm codegen:check

- name: Dead code / unused deps (knip)
run: pnpm knip

- name: Build
run: |
pnpm build
test -f dist/index.html

# PR 코멘트에 커버리지 표. check와 별도 잡이라 필수 체크 이름이 안정적이다
# 샤드는 커버리지 일부만 가져 임계를 0으로 끈다. 임계는 coverage-report가 합친 결과로 검사한다
test:
name: test (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- name: Setup Node.js (24.x) & pnpm cache
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24.x'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Test shard with coverage
env:
SHARD: ${{ matrix.shard }}
run: >-
pnpm exec vitest run --coverage --shard="$SHARD/$SHARD_COUNT"
--reporter=default --reporter=blob --outputFile.blob="blob-report/blob-$SHARD.json"
--coverage.thresholds.lines=0 --coverage.thresholds.functions=0
--coverage.thresholds.branches=0 --coverage.thresholds.statements=0

# 기본 경로(.vitest/blob)는 숨김 폴더라 upload-artifact가 건너뛴다
- name: Upload blob report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: blob-${{ matrix.shard }}
path: blob-report/
# 실패한 샤드도 올리므로 재실행 때 같은 이름이 생긴다(덮어쓰지 않으면 업로드 실패로 빨간불이 이어짐)
overwrite: true
retention-days: 7
if-no-files-found: error

# 필수 체크라 always()로 돈다 — needs 실패로 건너뛰면(skipped) 통과로 잡힌다
coverage-report:
if: github.event_name == 'pull_request'
needs: test
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
actions: read
steps:
- name: Require all test shards passed
env:
TEST_RESULT: ${{ needs.test.result }}
run: |
if [ "$TEST_RESULT" != "success" ]; then
echo "test 샤드 결과: $TEST_RESULT"
exit 1
fi

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand All @@ -89,12 +153,98 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Test with coverage
run: pnpm test:cov
- name: Download blob reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: blob-*
merge-multiple: true
path: blob-report

# 빠진 샤드가 있으면 부분 커버리지가 된다. 임계(vitest.config.ts)는 합친 결과에 적용된다
- name: Merge shard reports (coverage thresholds)
run: |
count=$(find blob-report -name 'blob-*.json' | wc -l)
if [ "$count" -ne "$SHARD_COUNT" ]; then
echo "blob ${count}개, 기대 ${SHARD_COUNT}개"
exit 1
fi
pnpm exec vitest run --merge-reports=blob-report --coverage

# develop·main push의 요약이 이후 PR의 비교 기준이 된다. 업로드 실패가 배포를 막지 않게 둔다
- name: Upload base coverage (push)
if: github.event_name == 'push'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-summary
path: |
coverage/coverage-summary.json
coverage/coverage-final.json
overwrite: true
retention-days: 90
if-no-files-found: error

# CODECOV_TOKEN이 레포 시크릿에 있을 때만 올린다 — 등록 전에는 건너뛰고, 등록 뒤 실패는 CI 실패로 드러낸다
- name: Upload coverage to Codecov
if: env.CODECOV_TOKEN != ''
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
disable_search: true
name: admin-fe-lcov
fail_ci_if_error: true

# 기준은 이 레포 base 브랜치의 성공한 push 실행만 쓴다(PR 실행 아티팩트는 믿지 않음). base 커밋 실행 우선, 그다음 최신 순으로
# 아티팩트가 있는 실행을 최대 5개까지 찾는다. 임계 미달로 병합이 실패해도 요약이 있으면 댓글을 갱신한다
- name: Fetch base coverage (PR)
id: base
if: ${{ !cancelled() && github.event_name == 'pull_request' && hashFiles('coverage/coverage-summary.json') != '' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.base_ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
run_ids=$(gh api "repos/$REPO/actions/workflows/pr-check.yml/runs?branch=$BASE_REF&event=push&status=success&per_page=50" \
| jq -r --arg repo "$REPO" --arg ref "$BASE_REF" --arg sha "$BASE_SHA" '
[.workflow_runs[] | select(.event == "push" and .head_branch == $ref and .head_repository.full_name == $repo)]
| (map(select(.head_sha == $sha)) + .) | map(.id)
| reduce .[] as $id ([]; if any(.[]; . == $id) then . else . + [$id] end)
| .[:5][]')
for run_id in $run_ids; do
if gh run download "$run_id" -R "$REPO" -n coverage-summary -D base-coverage \
&& [ -f base-coverage/coverage-summary.json ]; then
echo "기준 실행: $run_id"
echo "summary=base-coverage/coverage-summary.json" >> "$GITHUB_OUTPUT"
exit 0
fi
done
echo "기준 아티팩트 없음 — 비교 없이 리포트"

- name: Coverage report (vitest)
if: always()
- name: Coverage report (PR comment)
if: ${{ !cancelled() && github.event_name == 'pull_request' && hashFiles('coverage/coverage-summary.json') != '' }}
uses: davelosert/vitest-coverage-report-action@c4bbc33a89b7ace0e63d35f1f7d4bcee31155a73 # v2.13.0
with:
json-summary-compare-path: ${{ steps.base.outputs.summary }}

# 필수 체크 집계. 건너뜀·취소도 실패로 본다 — 보호된 status라 --no-verify·훅 미설치를 우회하지 못한다
check:
if: always()
needs: [lint, static, test, coverage-report]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Require all jobs succeeded
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
echo "$NEEDS" | jq -e 'all(.[]; .result == "success")' > /dev/null

pr-title:
if: github.event_name == 'pull_request'
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
node_modules
/dist
/coverage
# vitest blob(기본 .vitest/blob, CI 샤드는 blob-report/)
.vitest/
blob-report/
*.tsbuildinfo
.pnpm-store

Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# push 전 로컬 검증 — CI check와 같은 순서. --no-verify 금지
# push 전 로컬 검증 — CI의 lint·static·test·coverage-report가 나눠 하는 검사를 한 번에. --no-verify 금지
pnpm validate
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ pnpm dev # http://localhost:5173 — /graphql·/auth는 localhost:400

## 명령어

| 명령 | 내용 |
| ----------------------------- | --------------------------------------------------------------------------- |
| `pnpm validate` | lint → typecheck → knip → 테스트(커버리지) → 빌드. pre-push 훅과 동일합니다 |
| `pnpm test` / `pnpm test:cov` | Vitest |
| `pnpm lint` / `pnpm format` | ESLint(경계 규칙 포함) / Prettier |
| `pnpm build` / `pnpm preview` | 운영 빌드 / 로컬 미리보기 |
| 명령 | 내용 |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| `pnpm validate` | lint → typecheck → codegen:check → knip → 테스트(커버리지) → 빌드. pre-push 훅과 동일합니다 |
| `pnpm test` / `pnpm test:cov` | Vitest |
| `pnpm lint` / `pnpm format` | ESLint(경계 규칙 포함) / Prettier |
| `pnpm build` / `pnpm preview` | 운영 빌드 / 로컬 미리보기 |

## 배포

Expand Down
7 changes: 6 additions & 1 deletion docs/guide/architecture-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ src/

- `*.spec.ts(x)`를 소스 옆에. `it`은 한국어 평서형.
- 네트워크는 MSW로 계약 기반 mock(응답 모양은 codegen 타입을 따른다). fetch를 직접 stub하지 않는다.
- 커버리지 임계는 `vitest.config.ts`. shadcn 복사본(`src/shared/ui`)·codegen 산출물·라우트 트리는 제외.
- 커버리지 임계는 `vitest.config.ts`. shadcn 복사본(`src/shared/ui`)·codegen 산출물·라우트 트리는 제외. CI는 샤드별 임계를 끄고 합친 결과로 검사한다(샤드 하나는 일부 커버리지만 가진다).

## 8. 명령어와 게이트

Expand All @@ -78,5 +78,10 @@ pnpm schema:pull [ref] # BE SDL 스냅샷 갱신(기본 main). BE_DIR=../caqui
pnpm codegen # 스냅샷 + 문서 → src/graphql/generated (커밋 대상, CI가 codegen:check로 신선도 검사)
```

- CI(`pr-check.yml`)는 잡을 나눠 병렬로 돈다. 필수 체크 `check`는 아래 잡이 모두 `success`인지 집계한다(건너뜀·취소도 실패).
- `lint`
- `static`: `codegen:check` → `knip` → `build`(`tsc -b` 포함이라 typecheck 단계는 따로 없다)
- `test`: Vitest 3샤드, 결과는 blob 아티팩트로 넘긴다
- `coverage-report`: blob을 합쳐 임계 검사 → Codecov → PR 댓글. 비교 기준은 base 브랜치(develop·main) push 실행이 올린 커버리지 요약
- 커밋은 Conventional Commits + 한국어 본문(commitlint). 브랜치는 `<type>/<대상>`.
- PR 본문에 `## 플랜 대조` 표. 봇 리뷰(Codex·CodeRabbit)는 BE와 같은 절차로 처리한다.
3 changes: 2 additions & 1 deletion docs/guide/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| D4 | 2026-09-27 | TanStack Query + graphql-request + graphql-codegen(client-preset) | 정규화 캐시 없이 invalidate로 충분한 CRUD 화면. 서버 진실 우선, 낙관적 업데이트 없음 |
| D5 | 2026-09-27 | Tailwind v4 + shadcn/ui(new-york, neutral) + TanStack Table + react-hook-form + zod, 차트 Recharts | FE-v2와 같은 스타일 체계. 컴포넌트는 레포가 소유(`src/shared/ui`) |
| D6 | 2026-09-27 | 디자인 토큰은 FE-v2 계승(primary #7c5cff 계열, gray 스케일, status 3색, radius 10px, Pretendard), 라이트/다크 | 서비스와 같은 브랜드 인상, 장시간 사용 도구라 다크 필요 |
| D7 | 2026-09-27 | Vitest + Testing Library + MSW. E2E 없음. 커버리지 lines/statements 80 · branches 70 · functions 80, Codecov patch 80 | 관리자 도구 범위에 맞는 비용. shadcn 복사본은 커버리지 제외 |
| D7 | 2026-09-27 | Vitest + Testing Library + MSW. E2E 없음. 커버리지 lines/statements 80 · branches 70 · functions 80(대체됨 → D16), Codecov patch 80 | 관리자 도구 범위에 맞는 비용. shadcn 복사본은 커버리지 제외 |
| D8 | 2026-09-27 | 배포: 이 레포가 Dockerfile(nginx) · `infra/compose.yml` · `deploy.yml` 소유. GHCR `ghcr.io/caquick/caquick-admin-fe:<sha>`(arm64), 맥미니 셀프호스트 러너, BE compose 네트워크 `caquick_default`에 external 참여, 기존 cloudflared가 라우팅 | BE와 독립 배포·롤백. BE 레포 변경은 Tunnel 호스트 1줄 |
| D9 | 2026-09-27 | 도메인 `admin.caquick.site` → 컨테이너 `caquick-admin:80`. API `https://api.caquick.site` | 같은 부모 도메인이라 refresh 쿠키가 same-site로 전송된다 — 쿠키 도메인 변경 불필요, BE는 CORS 오리진만 추가 |
| D10 | 2026-09-27 | 인증: REST `/auth/admin/*`. accessToken은 메모리, refresh는 httpOnly 쿠키. 401이면 refresh 1회 후 재시도, 부팅 시 refresh로 복원, `mustChangePassword`면 변경 화면 강제 | 토큰을 localStorage에 두지 않는다(XSS 반경). 로컬은 Vite 프록시로 same-origin |
Expand All @@ -19,3 +19,4 @@
| D13 | 2026-09-27 | BE SDL 스냅샷(`schema/schema.graphql`) 커밋 + `pnpm schema:pull`. CI는 codegen 신선도만 게이트, BE main과의 drift는 advisory | CI가 BE 체크아웃에 의존하지 않게 |
| D14 | 2026-09-27 | 문서: `CLAUDE.md`·`.claude/`·`AGENTS.md`·`docs/*`(guide 제외)·`.figma/` gitignore. `docs/guide/`·README 커밋 | BE와 같은 방식 |
| D15 | 2026-09-27 | 의존 방향은 ESLint boundaries로 강제(shared→features 금지, feature 간은 index.ts만). 검사기는 반증 케이스로 확인하고 넣는다 | BE `arch:check`와 같은 역할 |
| D16 | 2026-10-05 | 커버리지 임계를 실측 정수 내림으로 상향: statements 95 · branches 88 · functions 94 · lines 96. CI는 테스트를 3샤드로 나누고 coverage-report가 합친 결과로 검사 | 실측(95.54·88.07·94.29·96.07)보다 한참 낮은 임계는 회귀를 못 잡는다. 샤드 합산이 단일 실행과 같음을 확인 |
2 changes: 1 addition & 1 deletion schema/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 생성 파일 — 수정하지 않는다. caquick-be local@251a957ea22bb84be535aa3d6cb8a4214e5e585b 의 src/features/**/*.graphql 64개를 경로순으로 합쳤다.
# 생성 파일 — 수정하지 않는다. caquick-be local@746d6fa87c2438aeef9afd739885f33274e1f021 의 src/features/**/*.graphql 64개를 경로순으로 합쳤다.
# 갱신: pnpm schema:pull [ref]

# ---- src/features/audit-log/audit-log.types.graphql ----
Expand Down
3 changes: 2 additions & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default defineConfig({
'src/main.tsx',
'src/**/*.d.ts',
],
thresholds: { lines: 80, statements: 80, branches: 70, functions: 80 },
// 실측(샤드 합산 = 단일 실행)의 정수 내림. CI는 coverage-report가 합친 결과로 검사한다
thresholds: { lines: 96, statements: 95, branches: 88, functions: 94 },
},
},
});
Loading