|
1 | 1 | # CLAUDE.md |
2 | 2 |
|
3 | | -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 3 | +## 프로젝트 개요 |
4 | 4 |
|
5 | | -## Project Overview |
| 5 | +KOIN은 한국기술교육대학교(KOREATECH) 재학생을 위한 캠퍼스 서비스 웹앱이다. 시간표, 버스, 식당, 가게, 커뮤니티, 동아리, 분실물, 졸업 계산기를 제공한다. |
6 | 6 |
|
7 | | -KOIN is a campus service web application for Korea University of Technology and Education (KOREATECH). It provides timetable management, bus schedules, cafeteria menus, store/shop listings, community articles, clubs, lost & found, and graduation calculators. |
| 7 | +**기술 스택:** Next.js 15 (Pages Router) · React 19 · TypeScript strict · Yarn 4 (Berry) PnP · Node 20.11.1 |
8 | 8 |
|
9 | | -Built with **Next.js 15 (Pages Router)**, **React 19**, and **TypeScript** (strict mode). |
10 | | - |
11 | | -- Package manager: **Yarn 4 (Berry)** with PnP. Never use `npm install`. |
12 | | -- Node version: **20.11.1** |
13 | | - |
14 | | -## Commands |
| 9 | +## 주요 명령어 |
15 | 10 |
|
16 | 11 | ```bash |
17 | | -yarn start # Dev server (next dev) |
18 | | -yarn build # Type-check (tsc) then production build |
| 12 | +yarn start # 개발 서버 (next dev) |
| 13 | +yarn build # 타입 체크(tsc) + 프로덕션 빌드 |
19 | 14 | yarn lint # ESLint + Stylelint |
20 | | -yarn lint:eslint # ESLint only (src/) |
21 | | -yarn lint:stylelint # Stylelint only (src/**/*.scss) |
22 | | -yarn log # Generate analytics logging hooks from Notion spec |
23 | | -``` |
24 | | - |
25 | | -## Architecture |
26 | | - |
27 | | -### Routing (Pages Router) |
28 | | - |
29 | | -File-based routing in `src/pages/`. Type-safe route builder in `src/static/routes.ts`: |
30 | | - |
31 | | -```typescript |
32 | | -ROUTES.StoreDetail({ id: '123' }); // → '/store/123' |
33 | | -``` |
34 | | - |
35 | | -Pages can declare static properties: |
36 | | - |
37 | | -```typescript |
38 | | -Page.getLayout = (page: React.ReactNode) => <SSRLayout>{page}</SSRLayout>; |
39 | | -Page.requireAuth = true; |
40 | | -Page.title = 'Page Title'; |
41 | | -``` |
42 | | - |
43 | | -### API Layer |
44 | | - |
45 | | -`src/api/` uses a class-based pattern with `APIClient` wrapper (`src/utils/ts/apiClient.ts`): |
46 | | - |
47 | | -1. Define request class in `APIDetail.ts` implementing `APIRequest<ResponseType>` |
48 | | -2. Define request/response types in `entity.ts` |
49 | | -3. Export callable function via `APIClient.of(DetailClass)` in `index.ts` |
50 | | - |
51 | | -```typescript |
52 | | -export class Login implements APIRequest<LoginResponse> { |
53 | | - path = '/user/login'; |
54 | | - method = HTTP_METHOD.POST; |
55 | | - data: LoginRequest; |
56 | | -} |
57 | | -// index.ts |
58 | | -export const login = APIClient.of(Login); |
| 15 | +yarn lint:eslint # ESLint (src/) |
| 16 | +yarn lint:stylelint # Stylelint (src/**/*.scss) |
| 17 | +yarn log # Notion 스펙에서 분석 로깅 훅 생성 |
59 | 18 | ``` |
60 | 19 |
|
61 | | -The APIClient handles token refresh (401), maintenance mode (503), and user type verification (403) automatically. |
62 | | - |
63 | | -### State Management |
64 | | - |
65 | | -- **Server state**: TanStack React Query (`staleTime: 60000`, `retry: false`). SSR via `getServerSideProps` + `HydrationBoundary`. |
66 | | -- **Client state**: Zustand stores in `src/utils/zustand/`. Many stores separate `State` and `Actions` types — follow the existing pattern of each file. |
67 | | - |
68 | | -Zustand stores export selectors: |
69 | | - |
70 | | -```typescript |
71 | | -export const useStateSelector = () => useStore((state) => state.prop); |
72 | | -export const useActions = () => useStore((state) => state.action); |
73 | | -``` |
74 | | - |
75 | | -### Styling |
76 | | - |
77 | | -SCSS with CSS Modules (`[Component].module.scss`) using BEM methodology. Desktop-first approach with mobile overrides via responsive mixins in `src/utils/scss/`: |
78 | | - |
79 | | -```scss |
80 | | -@include media.media-breakpoint(mobile) { |
81 | | - /* breakpoint: 576px */ |
82 | | -} |
83 | | -``` |
84 | | - |
85 | | -### Component Organization |
86 | | - |
87 | | -Feature-based: `src/components/[Feature]/` with co-located hooks in `hooks/` subdirectory and styles. Split responsive views into `MobileView/` and `PCView/` directories when layouts differ. Shared UI in `src/components/ui/`, layouts in `src/components/layout/`, modals in `src/components/modal/`. |
88 | | - |
89 | | -### Layout |
90 | | - |
91 | | -Two layout components in `src/components/layout/`: |
92 | | - |
93 | | -- **`SSRLayout`**: No Suspense wrapping. Used for SSR pages via `getLayout`. |
94 | | -- **`Layout`** (default): Wraps Header/children in Suspense boundaries. Hides Footer in native WebView. |
| 20 | +**패키지 매니저:** Yarn 4만 사용. `npm install` 금지. |
95 | 21 |
|
96 | | -Misusing these causes hydration errors. |
| 22 | +## 아키텍처 핵심 포인터 |
97 | 23 |
|
98 | | -### Custom Hooks |
99 | | - |
100 | | -Located in `src/utils/hooks/`, organized by category: |
101 | | - |
102 | | -- `auth/` — useAuth, useAutoLogin, useLoginRedirect, useLogout |
103 | | -- `ui/` — useBodyScrollLock, useOutsideClick, useEscapeKeyDown |
104 | | -- `state/` — useBooleanState, useLocalStorage, useWebStorage, useMount |
105 | | -- `layout/` — useMediaQuery, useModalPortal |
106 | | -- `analytics/` — useLogger, useScrollLogging |
107 | | - |
108 | | -### Internal Packages |
109 | | - |
110 | | -- **`@bcsdlab/koin`**: `isKoinError()` type guard, `sendClientError()` (sends errors to internal Slack). |
111 | | -- **`@bcsdlab/utils`**: `cn()` (className merger), `sha256()` (Web Crypto hashing). |
112 | | - |
113 | | -These are internal BCSD Lab packages — do not suggest replacing them with external alternatives. |
114 | | - |
115 | | -### Cookie Management |
116 | | - |
117 | | -Cookie keys are environment-aware via `IS_STAGE` flag in `src/static/url.ts`. Stage and production use different cookie key names (e.g., `STAGE_AUTH_TOKEN_KEY` vs `AUTH_TOKEN_KEY`) and different domains (`.stage.koreatech.in` vs `.koreatech.in`). Always use `COOKIE_KEY` constants and `getCookieDomain()` — never hard-code cookie names or domains. |
118 | | - |
119 | | -### iOS Native Bridge |
120 | | - |
121 | | -WebKit message handlers for token sync between web and native app via `window.webkit.messageHandlers`. Bridge functions in `src/utils/ts/iosBridge.ts`. |
122 | | - |
123 | | -### Analytics |
124 | | - |
125 | | -Generated logging hooks in `src/generated/analytics/` from Notion spec via `yarn log`. Google Analytics + GTM + Sentry error tracking. |
126 | | - |
127 | | -## Code Conventions |
128 | | - |
129 | | -### Imports |
130 | | - |
131 | | -Absolute imports via `*` → `src/*` path mapping. Use `import X from 'components/...'` not `'../../../components/...'`. Relative parent imports (`../*`) are forbidden by ESLint. |
132 | | - |
133 | | -Import order (enforced): React/Next → builtins → external packages → internal (`@/**`) → parent/sibling → types → styles (`.scss`). |
134 | | - |
135 | | -### Naming |
136 | | - |
137 | | -- Components/directories: PascalCase (`ArticleList.tsx`) |
138 | | -- Utilities: camelCase (`apiClient.ts`) |
139 | | -- Hooks: `use[Name].ts` |
140 | | -- Styles: `[Component].module.scss` |
141 | | -- API types: `entity.ts`, API classes: `APIDetail.ts` |
142 | | - |
143 | | -### Formatting |
144 | | - |
145 | | -Prettier: 120 char width, single quotes, trailing commas, 2-space indent. Stylelint enforces `stylelint-config-standard-scss`. |
146 | | - |
147 | | -## PR Review Rules (for claude-code-action) |
148 | | - |
149 | | -Write all review comments in Korean. |
150 | | - |
151 | | -Focus on correctness, regression risk, security, and performance before style. |
152 | | - |
153 | | -Use this output format for every finding: |
154 | | - |
155 | | -- Severity: `[P0]` (blocks merge), `[P1]` (should fix), `[P2]` (suggestion) |
156 | | -- Location: `file:line` |
157 | | -- Why it matters |
158 | | -- Minimal fix suggestion |
159 | | - |
160 | | -### Error Handling |
161 | | - |
162 | | -- Always use `isKoinError()` type guard before accessing error properties on API errors. |
163 | | -- In ErrorBoundary, use `isAxiosError()` type guard to branch handling. |
164 | | - |
165 | | -```typescript |
166 | | -onError: (error) => { |
167 | | - if (isKoinError(error)) { |
168 | | - showToast('error', error.message || 'fallback message'); |
169 | | - } else { |
170 | | - showToast('error', 'fallback message'); |
171 | | - } |
172 | | -}; |
173 | | -``` |
| 24 | +| 영역 | 패턴 요약 | 위치 | |
| 25 | +|------|-----------|------| |
| 26 | +| 라우팅 | Pages Router, `ROUTES` 헬퍼 사용 | `src/static/routes.ts` | |
| 27 | +| API | `APIRequest<T>` 클래스 → `APIClient.of()` export | `src/api/[domain]/` | |
| 28 | +| 서버 상태 | React Query (`staleTime: 60000, retry: false`) | `src/api/[domain]/queries.ts` | |
| 29 | +| 클라이언트 상태 | Zustand (`State`/`Actions` 타입 분리) | `src/utils/zustand/` | |
| 30 | +| 스타일 | SCSS Modules + BEM, 데스크톱 우선 | `[Component].module.scss` | |
| 31 | +| 레이아웃 | SSR 페이지 → `SSRLayout`, 클라이언트 → `Layout` | `src/components/layout/` | |
| 32 | +| 내부 패키지 | `@bcsdlab/koin` (isKoinError, sendClientError), `@bcsdlab/utils` (cn, sha256) | 교체 금지 | |
174 | 33 |
|
175 | | -### React Query |
| 34 | +## 필수 준수 규칙 |
176 | 35 |
|
177 | | -- Query keys as arrays: `['resource', 'action', params]`. |
178 | | -- Prefer `useSuspenseQuery` when a Suspense boundary wraps the component for blocking UI. Use `useQuery` for conditional fetching (`enabled`), background refresh, or non-blocking patterns. |
179 | | -- Invalidate cache via `queryClient.invalidateQueries()` after mutations. |
180 | | -- Every mutation `onError` must follow the error handling pattern above. |
| 36 | +1. **임포트:** 절대 경로 사용 (`import X from 'components/...'`). 상위 경로(`../`) ESLint 금지. |
| 37 | +2. **에러 핸들링:** mutation `onError`에 반드시 `isKoinError()` 타입 가드 → `showToast()` 패턴 적용. |
| 38 | +3. **쿠키:** `COOKIE_KEY` 상수 + `getCookieDomain()` 사용. 쿠키명/도메인 하드코딩 금지. |
| 39 | +4. **SSR 안전성:** `window`/`document`/`localStorage` 접근 시 `typeof window !== 'undefined'` 체크 필수. |
| 40 | +5. **라우팅:** `ROUTES` 헬퍼 사용. 경로 문자열 하드코딩 금지. |
| 41 | +6. **로깅:** `console.log` 금지. `console.warn`/`console.error`만 허용. |
| 42 | +7. **토스트:** `showToast(type, message)` 사용. `toast()` 직접 호출 금지. |
| 43 | +8. **iOS 브릿지:** `window.webkit` optional chaining 유지 (`src/utils/ts/iosBridge.ts`). |
| 44 | +9. **분석 로깅:** 주요 사용자 인터랙션마다 `useLogger()` 훅 포함. |
181 | 45 |
|
182 | | -### Analytics Logging |
| 46 | +## PR 리뷰 규칙 (claude-code-action용) |
183 | 47 |
|
184 | | -- All user interactions (click, swipe, page load) must include logging. |
185 | | -- Use the `useLogger()` hook with `team`, `event_label`, `value` structure. |
186 | | -- Define logging constants (`loggingTitle`, `loggingValue`) at the top of the component. |
| 48 | +리뷰 댓글은 **한국어**로 작성. 정확성 > 회귀 위험 > 보안 > 성능 > 스타일 순으로 검토. |
187 | 49 |
|
188 | | -### General |
| 50 | +**출력 형식 (모든 발견에 적용):** |
| 51 | +- 심각도: `[P0]` (머지 차단) · `[P1]` (수정 필요) · `[P2]` (제안) |
| 52 | +- 위치: `file:line` |
| 53 | +- 영향과 최소 수정안 |
189 | 54 |
|
190 | | -- No `console.log` (ESLint warn). Only `console.warn` and `console.error` allowed. |
191 | | -- Import SVGs as React components via `@svgr/webpack`. |
192 | | -- Use `showToast(type, message)` utility instead of calling `toast()` directly. Type: `'success' | 'error' | 'info' | 'warning' | 'default'`. |
| 55 | +**검토 제외:** |
| 56 | +- lint/import-order 포매팅 노이즈 |
| 57 | +- 자동 생성 파일만 변경 (`src/generated/**`, `analytics.events.json`) |
| 58 | +- `@bcsdlab/koin`, `@bcsdlab/utils` 교체 제안 |
193 | 59 |
|
194 | | -### Project-Specific Must-Checks |
| 60 | +## 유효성 검사 정책 |
195 | 61 |
|
196 | | -- **SSR safety**: Guard `window`, `document`, `localStorage`, `sessionStorage` with browser checks. Verify correct layout usage (`SSRLayout` for SSR pages, `Layout` for client pages). |
197 | | -- **React Query SSR hydration**: Verify prefetch query keys and hydration state keys are consistent. |
198 | | -- **Auth/API stability**: Do not break token refresh lock, 401/403/503 handling, or retry flow. |
199 | | -- **Cookie safety**: Must use `COOKIE_KEY` constants and `getCookieDomain()` — never hard-code cookie names or domains. Verify stage/production separation is preserved. |
200 | | -- **iOS bridge stability**: Preserve `window.webkit` optional chaining and native callback contract. |
201 | | -- **Routing consistency**: Prefer `ROUTES` helpers over hard-coded path strings. |
202 | | -- **Next.js performance**: Flag async waterfalls, unnecessary heavy static imports, and missed dynamic imports. |
| 62 | +- 기본: `yarn lint` |
| 63 | +- `yarn build` 실패는 환경 제약(API 접근 불가 등)이 원인일 수 있으므로, 변경 코드가 직접 원인이 아니면 비차단 처리. |
203 | 64 |
|
204 | | -### Validation Policy |
| 65 | +## 하네스: KOIN AI 파이프라인 |
205 | 66 |
|
206 | | -- Primary check: `yarn lint`. |
207 | | -- `yarn build` may fail due to environment constraints (missing API access, sandbox limitations); treat as non-blocking unless the changed code directly caused the failure. |
| 67 | +**목표:** SonarCloud 코드 품질 분석 및 자동 수정, 기능 구현 자동화 PR 생성 |
208 | 68 |
|
209 | | -### Do Not Review |
| 69 | +**트리거:** "SonarCloud 분석", "SonarCloud 이슈 수정", "코드 품질 개선", "기능 구현해줘", "새 기능 추가해줘", "PR 만들어줘", "파이프라인 실행", "다시 실행", "재실행", "이전 결과 개선", "업데이트" 등의 요청 시 `koin-pipeline` 스킬을 사용하라. |
210 | 70 |
|
211 | | -- Pure formatting/import-order noise already covered by lint. |
212 | | -- Generated artifacts only (`analytics.events.json`, `src/generated/**`) unless generation logic changed. |
213 | | -- `@bcsdlab/koin` and `@bcsdlab/utils` are internal packages — do not suggest external replacements. |
| 71 | +**변경 이력:** |
| 72 | +| 날짜 | 변경 내용 | 대상 | 사유 | |
| 73 | +|------|----------|------|------| |
| 74 | +| 2026-04-21 | 초기 구성 | 전체 | SonarCloud 품질 관리 + 기능 구현 자동화 파이프라인 | |
| 75 | +| 2026-04-21 | 전체 재작성 | CLAUDE.md | 영어→한국어, 스킬 중복 제거, 분량 축소 (224줄→80줄) | |
0 commit comments