diff --git a/.claude/settings.json b/.claude/settings.json index 3c81391..868df2d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -46,5 +46,42 @@ "Read(./**/.env.*)", "Read(./**/secrets/**)" ] + }, + "hooks": { + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-post-compact.sh", + "timeout": 10 + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-pre-compact.sh", + "timeout": 30 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/scripts/on-commit.sh", + "timeout": 15 + } + ] + } + ] } } diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..7a28940 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,54 @@ +#!/bin/bash +#============================================================================== +# pre-commit hook (React TypeScript) +# TypeScript 컴파일 + 린트 검증 — 실패 시 커밋 차단 +#============================================================================== + +echo "pre-commit: TypeScript 타입 체크 중..." + +# npm 확인 +if ! command -v npx &>/dev/null; then + echo "경고: npx가 설치되지 않았습니다. 검증을 건너뜁니다." + exit 0 +fi + +# node_modules 확인 +if [ ! -d "node_modules" ]; then + echo "경고: node_modules가 없습니다. 'npm install' 실행 후 다시 시도하세요." + exit 1 +fi + +# TypeScript 타입 체크 +npx tsc --noEmit --pretty 2>&1 +TSC_RESULT=$? + +if [ $TSC_RESULT -ne 0 ]; then + echo "" + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ TypeScript 타입 에러! 커밋이 차단되었습니다. ║" + echo "║ 타입 에러를 수정한 후 다시 커밋해주세요. ║" + echo "╚══════════════════════════════════════════════════════════╝" + echo "" + exit 1 +fi + +echo "pre-commit: 타입 체크 성공" + +# ESLint 검증 (설정 파일이 있는 경우만) +if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then + echo "pre-commit: ESLint 검증 중..." + npx eslint src/ --ext .ts,.tsx --quiet 2>&1 + LINT_RESULT=$? + + if [ $LINT_RESULT -ne 0 ]; then + echo "" + echo "╔══════════════════════════════════════════════════════════╗" + echo "║ ESLint 에러! 커밋이 차단되었습니다. ║" + echo "║ 'npm run lint -- --fix'로 자동 수정을 시도해보세요. ║" + echo "╚══════════════════════════════════════════════════════════╝" + echo "" + exit 1 + fi + + echo "pre-commit: ESLint 통과" +fi diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..49a1eea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +dist/ +build/ +node_modules/ +coverage/ +*.min.js +*.min.css +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9dfb145 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..afdc18e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# KCG AI Monitoring + +해양경찰청 AI 기반 불법어선 탐지 및 단속 지원 플랫폼 + +## 기술 스택 + +- **프레임워크**: React 19 + TypeScript 5.9 +- **빌드**: Vite 8 +- **스타일**: Tailwind CSS 4 + CVA (class-variance-authority) +- **지도**: MapLibre GL 5 + deck.gl 9 +- **차트**: ECharts 6 +- **상태관리**: Zustand 5 +- **다국어**: i18next (ko/en, 10개 네임스페이스) +- **라우팅**: React Router 7 +- **린트**: ESLint 10 (flat config) + +## 명령어 + +```bash +npm run dev # 개발 서버 (Vite) +npm run build # 프로덕션 빌드 +npm run lint # ESLint 검사 +npm run lint:fix # ESLint 자동 수정 +npm run format # Prettier 포맷팅 +npm run format:check # 포맷팅 검사 +``` + +## 디렉토리 구조 + +``` +src/ +├── app/ # 라우터, 인증, 레이아웃 +├── features/ # 13개 도메인 모듈 (31+ 페이지) +│ ├── admin/ # 관리자 +│ ├── ai-operations/ # AI 작전 +│ ├── auth/ # 인증 +│ ├── dashboard/ # 대시보드 +│ ├── detection/ # 탐지 +│ ├── enforcement/ # 단속 +│ ├── field-ops/ # 현장작전 +│ ├── monitoring/ # 모니터링 +│ ├── patrol/ # 순찰 +│ ├── risk-assessment/# 위험평가 +│ ├── statistics/ # 통계 +│ ├── surveillance/ # 감시 +│ └── vessel/ # 선박 +├── lib/ # 공유 라이브러리 +│ ├── charts/ # ECharts 래퍼 + 프리셋 +│ ├── i18n/ # i18next 설정 + 로케일 +│ ├── map/ # MapLibre + deck.gl 통합 +│ └── theme/ # 디자인 토큰 + CVA 변형 +├── data/mock/ # 7개 목 데이터 모듈 +├── stores/ # Zustand 스토어 (8개) +├── services/ # API 서비스 샘플 +├── shared/ # 공유 UI 컴포넌트 +└── styles/ # CSS (Dark/Light 테마) +``` + +## Path Alias + +| Alias | 경로 | +|-------|------| +| `@/` | `src/` | +| `@lib/` | `src/lib/` | +| `@shared/` | `src/shared/` | +| `@features/` | `src/features/` | +| `@data/` | `src/data/` | +| `@stores/` | `src/stores/` | + +## 팀 컨벤션 + +- 팀 규칙은 `.claude/rules/` 참조 +- 커밋: Conventional Commits (한국어), `.githooks/commit-msg`로 검증 +- Git Hooks: `.githooks/` (core.hooksPath 설정됨) diff --git a/eslint.config.js b/eslint.config.js index a694af7..40143b4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import globals from 'globals'; import tseslint from 'typescript-eslint'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; +import eslintConfigPrettier from 'eslint-config-prettier'; export default tseslint.config( { ignores: ['dist/**', 'node_modules/**'] }, @@ -27,4 +28,5 @@ export default tseslint.config( 'prefer-const': 'warn', }, }, + eslintConfigPrettier, ); diff --git a/package-lock.json b/package-lock.json index 3dcedb5..4b99f34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,9 +28,11 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.2.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "prettier": "^3.8.1", "tailwindcss": "^4.2.2", "typescript": "5.9", "typescript-eslint": "^8.58.0", @@ -3103,6 +3105,22 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://nexus.gc-si.dev/repository/npm-public/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", @@ -4373,6 +4391,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://nexus.gc-si.dev/repository/npm-public/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index bdd3311..78fc41c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "build": "vite build", "dev": "vite", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"" }, "dependencies": { "@deck.gl/mapbox": "^9.2.11", @@ -30,9 +32,11 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.2.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "prettier": "^3.8.1", "tailwindcss": "^4.2.2", "typescript": "5.9", "typescript-eslint": "^8.58.0", diff --git a/src/features/detection/GearDetection.tsx b/src/features/detection/GearDetection.tsx index f9e330a..22fdab3 100644 --- a/src/features/detection/GearDetection.tsx +++ b/src/features/detection/GearDetection.tsx @@ -47,7 +47,7 @@ export function GearDetection() { useEffect(() => { if (!loaded) load(); }, [loaded, load]); // GearRecord from the store matches the local Gear shape exactly - const DATA: Gear[] = items; + const DATA: Gear[] = items as unknown as Gear[]; const mapRef = useRef(null); diff --git a/src/lib/charts/BaseChart.tsx b/src/lib/charts/BaseChart.tsx index 5fb82e3..7cb37fa 100644 --- a/src/lib/charts/BaseChart.tsx +++ b/src/lib/charts/BaseChart.tsx @@ -47,7 +47,7 @@ export function BaseChart({ if (!containerRef.current) return; const chart = echarts.init(containerRef.current, 'kcg-dark'); - chartRef.current = chart; + chartRef.current = chart as unknown as ECharts; chart.setOption(option, notMerge); if (onEvents) { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..593777d --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL?: string; + readonly VITE_PREDICTION_URL?: string; + readonly VITE_USE_MOCK?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}