.claude/skills/verify-frontend-state/SKILL.md
Verifies frontend state management patterns + performance anti-patterns — TanStack Query for server state (no useState), onSuccess setQueryData prohibition, dynamic imports for code splitting, Server/Client component separation. Run after adding/modifying components or hooks.
npx skillsauth add junnv93/equipment_management_system verify-frontend-stateInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
useQuery/useMutation 사용setQueryData 호출 금지| File | Purpose |
|------|---------|
| apps/frontend/hooks/use-optimistic-mutation.ts | SSOT: optimistic mutation 훅 |
| apps/frontend/lib/api/query-config.ts | queryKeys 팩토리 + QUERY_CONFIG 프리셋 |
| apps/frontend/lib/api/cache-invalidation.ts | 캐시 무효화 SSOT |
| apps/frontend/hooks/use-date-formatter.ts | 사용자 dateFormat 적용 날짜 포맷 훅 |
각 Step의 bash 명령어, 코드 예시: references/step-details.md 참조
PASS: useState<Equipment[]> 등 서버 데이터 타입 useState 0개.
PASS: onSuccess 내 setQueryData 0개. 참고: onMutate에서의 setQueryData는 정상.
PASS: 상태 변경 mutation에 useOptimisticMutation 사용. INFO: 단순 API는 직접 사용 가능.
PASS: useOptimisticMutation 소비자가 별도 invalidateQueries 미호출. invalidateQueries에 await 있거나 router.push보다 먼저 완료 보장.
queryKeys.approvals.counts() 대신 queryKeys.approvals.countsAll 사용CheckoutCacheInvalidation 정적 상수 사용PASS: refetchInterval: 60000 등 하드코딩 0개.
PASS: 사용자 표시 날짜는 useDateFormatter().fmtDate 사용. formatDate/date-fns format 직접 import 0개.
'use client' + 서버 함수(getServerAuthSession, serverApiClient) 0개PASS: 클라이언트 컴포넌트에서 hasPermission 직접 import 0개.
useToast / toast 는 반드시 @/components/ui/use-toast 1곳에서만 import. 과거 @/hooks/use-toast 사본은 별도 memoryState/listeners 를 가져 <Toaster /> 가 구독하지 않아 해당 컴포넌트의 토스트가 화면에 렌더되지 않는 silent production bug 를 유발했다 (2026-04-08 toast-ssot-dedup 작업에서 6개 컴포넌트 영향 확인).
검증:
grep -rn "@/hooks/use-toast" apps/frontend --include="*.ts" --include="*.tsx"
PASS: 0 hit.
pageSize: 1로 total count만 조회하는 쿼리가 전체 목록 쿼리와 동일한 queryKey를 사용하면, 1건 응답이 전체 목록 캐시를 오염시켜 올바른 데이터가 보이지 않는 버그를 유발한다.
규칙:
pageSize: 1 + 총 건수만 사용하는 위젯 → pendingCount(), summaryCount() 등 전용 키 사용검증:
# pageSize: 1 쿼리 중 전용 count 키를 쓰지 않고 일반 목록 키를 재사용하는 패턴 탐지
grep -rn "pageSize.*1\b" apps/frontend --include="*.tsx" --include="*.ts" -B 3 | grep "queryKey" | grep -v "Count\|count\|summary\|Summary"
참고 패턴 (정상):
// ✅ 전용 키 사용
queryKey: queryKeys.checkouts.pendingCount(), // ['checkouts', 'pending-count']
queryFn: () => checkoutApi.getPendingChecks({ pageSize: 1 }),
// ❌ 목록 키 재사용 (오염 위험)
queryKey: queryKeys.checkouts.pending(), // ['checkouts', 'pending', undefined]
queryFn: () => checkoutApi.getPendingChecks({ pageSize: 1 }),
배경: CheckoutsContent.tsx에서 pending() 키로 pageSize:1 결과를 캐시 → PendingChecksClient가 동일 키의 1건 캐시를 사용해 전체 탭에서 1건만 표시되던 버그 발생 (2026-04-13 수정).
PASS: pageSize:1 쿼리가 전용 count 키를 사용하거나 0건.
Radix Toast 는 의도적으로 시각 토스트(<li role="status">) + visually-hidden status mirror (<span role="status" aria-live="assertive">) 를 동시에 노출한다 (스크린리더 a11y). 따라서 page.getByText('...').first() 같은 직접 매칭은 strict mode 충돌을 우회할 뿐 의도(시각 발화 검증)가 코드에 드러나지 않고, mirror span 의 "Notification " 접두사 변경 시 silent break 위험이 있다.
규칙:
apps/frontend/tests/e2e/shared/helpers/toast-helpers.ts 의 expectToastVisible(page, text) 사용toastLocator(page, text) 로 좁힌 뒤 .or() 로 결합.first() 사용 금지검증:
# 토스트로 보이는 한국어 종결형 문구에 .first() 직접 매칭
grep -rn "getByText.*되었\|getByText.*완료\|getByText.*실패.*\.first()" \
apps/frontend/tests/e2e --include="*.spec.ts"
PASS: 토스트 텍스트에 직접 .first() 적용된 매칭 0건. helper 우회만 한정 검출.
| # | 검사 | 상태 | 상세 |
| --- | -------------------------- | --------- | ----------------------------- |
| 1 | useState 서버 상태 | PASS/FAIL | 위반 위치 목록 |
| 2 | onSuccess setQueryData | PASS/FAIL | 위반 위치 목록 |
| 3 | useOptimisticMutation 사용 | PASS/INFO | 직접 useMutation 위치 |
| 4 | invalidateQueries 위치 | PASS/FAIL | onSuccess 내 위치 |
| 4b | Navigate-Before-Invalidate | PASS/FAIL | await 없는 invalidate→navigate |
| 5 | QUERY_CONFIG 프리셋 | PASS/INFO | 직접 설정 위치 |
| 5b | countsAll prefix 무효화 | PASS/FAIL | approvals.counts() 사용 위치 |
| 5c | CheckoutCacheInvalidation | PASS/FAIL | 직접 queryKeys 조합 위치 |
| 6 | REFETCH_STRATEGIES 사용 | PASS/INFO | refetchInterval 하드코딩 위치 |
| 7 | useDateFormatter 컨벤션 | PASS/FAIL | 직접 formatDate/format import |
| 8a | Client에서 서버 함수 호출 | PASS/FAIL | 'use client' + 서버 함수 위치 |
| 8b | 무거운 라이브러리 동적 분할 | PASS/FAIL | 정적 import 위치 |
| 9 | useAuth().can() 권한 SSOT | PASS/FAIL | hasPermission 직접 import 위치 |
| 10 | useToast SSOT 단일 경로 | PASS/FAIL | `@/hooks/use-toast` import 위치 |
| 11 | E2E 토스트 helper 사용 | PASS/FAIL | 토스트 텍스트 직접 .first() 위치 |
| 12 | count 전용 쿼리 키 분리 | PASS/FAIL | pageSize:1 쿼리가 목록 키 재사용하는 위치 |
useState<FormData> 등 폼 입력 관리data.isBlob 조건부 revoke + 언마운트 클린업이 정상.blobUrlRef.current에 현재 blob URL 추적, cleanup에서 revokeObjectURL 호출. deps: [open, doc?.id]. 이 패턴은 stale closure 방지를 위한 ref 기반 cleanup으로 정상.staleTime: Infinity (런타임 불변 서버 설정값) — QUERY_CONFIG 프리셋이 없는 값이지만, 앱 재시작 없이 변경되지 않는 서버 설정(NextAuth 인증 제공자 목록, 기능 플래그 등)에 대해 staleTime: Infinity를 주석과 함께 직접 사용하는 것은 정상. 예: AuthProviders.tsx의 useQuery({ queryFn: getProviders, staleTime: Infinity }). 주석 없이 사용하면 QUERY_CONFIG 프리셋 누락으로 보고.testing
Verifies Zod validation pattern compliance — ZodValidationPipe usage (no class-validator), versionedSchema inclusion in state-change DTOs, controller pipe application, query DTO consistency. Run after adding/modifying DTOs or controller endpoints.
testing
Verifies cross-feature workflow E2E test coverage against critical-workflows.md checklist. Checks WF-01~WF-35 coverage, step completeness, role correctness, side-effect verification, and status transition assertions. Run after adding workflow tests or before PR.
testing
SSOT(Single Source of Truth) 임포트 소스를 검증합니다. 타입/enum/상수가 올바른 패키지에서 임포트되는지 확인. 타입/enum 추가/수정 후 사용.
development
Verifies SQL safety — LIKE wildcard escaping, N+1 query pattern detection, COUNT(DISTINCT) for fan-out JOINs, RBAC INNER JOIN enforcement. Run after adding/modifying search or list API endpoints.