Skip to content

Commit 045a73c

Browse files
committed
docs(root): add new architectural patterns to claude.md
Document asyncio.to_thread bridging, Redis cache singleton, Lua rate limiter, WebSocket TOCTOU lock, short-lived DB sessions, LIKE escape, and worker retry policy patterns.
1 parent 31f13d3 commit 045a73c

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**juny** — 실시간 멀티모달 AI 어시스턴트. Host(카메라/오디오 스트리밍 주 사용자)와 Concierge(모니터링 및 오디오 개입)가 같은 LiveKit Room에 접속하고, AI가 Host의 스트림을 실시간 분석하는 구조. 역할명은 `host`/`concierge` 사용 — 시니어에 한정되지 않고 장애인 등으로 확장 가능한 중립적 명칭. Monorepo with four apps and two shared packages, orchestrated by **mise** (monorepo mode with `//path:task` syntax). No root package.json — mise is the sole task runner.
8+
9+
| App/Package | Stack | Package Manager |
10+
|---|---|---|
11+
| `apps/api` | Python 3.12, FastAPI, SQLAlchemy async, Alembic, Redis, LiveKit (WebRTC), Gemini Multimodal Live API | uv + poethepoet |
12+
| `apps/worker` | Python 3.12, FastAPI, Cloud Tasks/Pub/Sub, tenacity | uv + poethepoet |
13+
| `apps/mobile` | Flutter 3.38, Dart, Riverpod 3, Freezed, go_router | flutter/dart |
14+
| `apps/infra` | Terraform, GCP (Cloud Run, Cloud SQL, etc.) | terraform |
15+
| `packages/design-tokens` | TypeScript, OKLCH tokens → Flutter theme | bun |
16+
| `packages/i18n` | ARB source → Flutter ARB (mobile) | bun |
17+
18+
## Common Commands
19+
20+
All commands use mise. Run `mise tasks --all` to see everything.
21+
22+
```bash
23+
# Root-level
24+
mise dev # Start all services (api + worker)
25+
mise test # Test all apps
26+
mise lint # Lint all apps
27+
mise format # Format all apps
28+
mise typecheck # Type check all apps
29+
mise gen:api # Generate OpenAPI schema + mobile API client
30+
mise i18n:build # Build i18n files
31+
mise tokens:build # Build design tokens
32+
33+
# Local infrastructure (PostgreSQL 16, Redis 7, MinIO)
34+
mise infra:up # Start
35+
mise infra:down # Stop
36+
37+
# Database
38+
mise db:migrate # Run migrations
39+
mise //apps/api:migrate:create "description" # Create new migration
40+
cd apps/api && uv run alembic downgrade -1 # Rollback one migration
41+
```
42+
43+
### Per-app commands (pattern: `mise //apps/<app>:<task>`)
44+
45+
```bash
46+
# API
47+
mise //apps/api:dev | :test | :lint | :format | :typecheck | :migrate
48+
49+
# Worker
50+
mise //apps/worker:dev | :test | :lint | :format
51+
52+
# Mobile
53+
mise //apps/mobile:dev | :build | :test | :lint | :format | :gen:api | :gen:l10n
54+
```
55+
56+
### Running a single test
57+
58+
```bash
59+
# API — single file or test function
60+
cd apps/api && uv run pytest tests/test_health.py -v
61+
cd apps/api && uv run pytest tests/test_health.py::test_health_check -v
62+
63+
# Mobile
64+
cd apps/mobile && flutter test test/core/utils_test.dart
65+
```
66+
67+
### Underlying tool commands (when mise is unavailable)
68+
69+
```bash
70+
# API/Worker
71+
cd apps/api && uv run poe dev|test|lint|format|typecheck|migrate
72+
```
73+
74+
## Architecture
75+
76+
### API (`apps/api/src/`)
77+
78+
Feature-based module structure following **router → service → repository → schemas/models**:
79+
80+
- `auth/` — Authentication (PyJWT (JWS), integrated with better-auth)
81+
- `users/` — User management
82+
- `relations/` — CareRelation (N:M host↔caregiver, 4-Tier RBAC)
83+
- `wellness/` — WellnessLog (append-only, JSONB details)
84+
- `medications/` — Medication schedules (is_taken tracking)
85+
- `navigation/` — NavigationSession, LocationWaypoint (실시간 GPS 네비게이션, off-route 감지)
86+
- `common/` — Shared enums (`UserRole`, `WellnessStatus`, `NavigationStatus`), models (`PaginatedResponse[T]`), utilities
87+
- `lib/` — Infrastructure: database, dependencies (FastAPI DI), AI providers (ABC-based, `lib/ai/`), maps provider (ABC-based, `lib/maps/`), LiveKit WebRTC provider (`lib/livekit/`), storage (ABC-based), rate limiting, OpenTelemetry + structlog
88+
- `routers/` — Cross-domain routers (e.g., `live.py` for real-time WebSocket)
89+
90+
Async-first with explicit session passing. Business logic lives in services, not routers. Do NOT modify or break existing `auth` or `health` routers.
91+
92+
### Worker (`apps/worker/src/`)
93+
94+
HTTP-based background worker: `routers/` receive tasks from Cloud Tasks/Pub/Sub, `jobs/` contain execution logic, `lib/` has shared infrastructure.
95+
96+
### Mobile (`apps/mobile/lib/`)
97+
98+
Feature-first with Riverpod 3 DI, Freezed models, go_router, Retrofit+Dio networking (API client generated from OpenAPI via swagger_parser).
99+
100+
### Shared Packages
101+
102+
- **design-tokens**: Single source of truth (`src/tokens.ts`, OKLCH color space) → generates `generated_theme.dart` (mobile)
103+
- **i18n**: ARB files (en/ko/ja) → Flutter ARB for mobile.
104+
105+
### Code Generation Pipeline
106+
107+
`mise gen:api` triggers: API OpenAPI schema export → swagger_parser generates mobile client (Retrofit). Run this after changing API endpoints.
108+
109+
## Commit Conventions
110+
111+
Conventional Commits enforced by commitlint + git hooks:
112+
113+
```
114+
<type>(<scope>): <lower-case subject>
115+
```
116+
117+
- **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`
118+
- **Scopes** (required): `api`, `mobile`, `worker`, `infra`, `deps`, `docs`, `root`
119+
120+
Branch naming: `feature/*`, `fix/*`, `hotfix/*`, `release/*`
121+
122+
## Linting & Formatting
123+
124+
| Language | Tool | Config |
125+
|---|---|---|
126+
| Python | Ruff | `apps/api/ruff.toml`, `apps/worker/ruff.toml` — double quotes, 4-space indent, 88 chars |
127+
| Dart | very_good_analysis | `apps/mobile/analysis_options.yaml` |
128+
| Terraform | `terraform fmt` | Built-in |
129+
| Python types | mypy (strict) | `apps/api/pyproject.toml` — pydantic plugin enabled |
130+
131+
## Testing
132+
133+
| App | Framework | Config |
134+
|---|---|---|
135+
| API | pytest + pytest-asyncio | `apps/api/pyproject.toml``asyncio_mode = "auto"`, testpaths: `tests/` |
136+
| Worker | pytest + pytest-asyncio | `apps/worker/pyproject.toml``asyncio_mode = "auto"` |
137+
| Mobile | flutter_test + mocktail | `test/**/*_test.dart` |
138+
| Design tokens | Vitest | `packages/design-tokens/` |
139+
140+
## Key Patterns
141+
142+
- **Clean Code 원칙**: 임포트는 반드시 파일 최상단에 배치. `# noqa: E402`로 하단 임포트를 회피하지 않는다. 함수 내부 임포트는 순환 참조 방지나 lazy loading 등 명확한 사유가 있을 때만 허용. lazy loading 주장 시 해당 패키지가 실제로 optional dependency인지 `pyproject.toml`에서 확인 필수 — required dependency를 함수 내부에서 import하지 않는다.
143+
- **API 버전 프리픽스**: 모든 API 엔드포인트는 `/api/v1/` 프리픽스 필수. `main.py`에서 `APIRouter(prefix="/api/v1")`로 라우터를 묶어 등록.
144+
- **확장성 우선**: 하드코딩 대신 자동 탐색/등록 패턴 사용 (예: `pkgutil.iter_modules()`로 tool 자동 로딩). 새 모듈 추가 시 기존 코드 수정이 불필요하도록 설계.
145+
- **한글 응답**: 사용자에게 한글로 응답할 것.
146+
- **Python**: Type hints required everywhere. Pydantic models for request/response. ABCs for extensible providers (AI, storage). SQLAlchemy 2.0 style only. All code must pass `ruff check`, `ruff format`, and `mypy` before finalizing.
147+
- **File naming**: kebab-case across the project. Generated files use `*.gen.ts` suffix.
148+
- **Auth**: PyJWT (JWT/JWS) on API side.
149+
- **Observability**: OpenTelemetry instrumentation (FastAPI, SQLAlchemy, httpx, Redis) + structlog.
150+
- **Real-time AI**: Gemini Multimodal Live API (`google-genai`, NOT deprecated `google-generativeai`) + LiveKit (`livekit-api`) for WebRTC. Tool Registry 패턴으로 Gemini Function Calling 도구를 관리 — core websocket loop을 건드리지 않고 새 도구 추가 가능.
151+
- **mypy + untyped libs**: `livekit`, `google.genai`는 type stub 없음 — `pyproject.toml``[[tool.mypy.overrides]]` ignore_missing_imports 필요
152+
- **4-Tier RBAC**: `HOST`, `CONCIERGE`, `CARE_WORKER`, `ORGANIZATION`. CareRelation은 N:M (한 host에 여러 caregiver, 한 caregiver가 여러 host 관리). HOST는 caregiver 역할 불가 (서비스 레이어에서 검증).
153+
- **도메인 모듈 4계층**: 각 feature 폴더는 `model.py``schemas.py``repository.py``service.py``router.py`. repository는 데이터 접근만, service는 비즈니스 로직만, router는 HTTP 핸들링만 담당. 계층 간 책임을 혼합하지 않는다.
154+
- **Alembic env.py**: `_include_object` 필터로 모델에 정의된 테이블만 관리. 레거시/외부 테이블을 무시하여 autogenerate 충돌 방지.
155+
- **테스트 인증**: `tests/conftest.py``authed_client` fixture 제공 (`create_access_token` → Bearer header). 서비스 테스트는 `@patch("src.xxx.repository.method")`로 repository를 mock하여 격리.
156+
- **AI Tool context 주입**: `get_tool_handler(context={"db": session, "host_id": uuid})` 패턴으로 WebSocket에서 DB 세션을 도구에 전달.
157+
- **Testing**: 외부 의존성(LiveKit API, Gemini WebSocket 등)은 반드시 mock 처리. 네트워크 호출 없이 테스트가 동작해야 함.
158+
- **AI Tools (Function Calling)**: Tool의 `execute()`에서 절대 직접 `db.add()`/`db.flush()` 하지 않음 — 반드시 service 함수 호출 (4-layer 준수). Tool 테스트 시 service 함수를 patch.
159+
- **WebSocket 인증**: `?token=` query param → `decode_token()``token_type == "access"` 검증. `exp` 만료는 PyJWT가 `decode()` 시 자동 검증. 실패 시 `WS_1008_POLICY_VIOLATION`.
160+
- **asyncio bidirectional streaming**: 양방향 무한 루프 coroutine은 `asyncio.create_task` + `try/finally` cancel 패턴 사용 (`asyncio.gather`만 쓰면 후속 코드 unreachable).
161+
- **Ruff S105/S106 주의**: 테스트에서 `token_type = "access"` 할당 → S105 오탐, `token="fcm-token-abc"` 함수 인자 → S106 오탐. 각각 `# noqa: S105`, `# noqa: S106` 필요.
162+
- **테스트 결정론**: fixture의 UUID는 `uuid.uuid4()` 대신 deterministic 값 사용 (e.g., `"00000000-0000-4000-8000-000000000099"`).
163+
- **Mock 구현 로그**: mock/stub 구현체는 `logger.warning` + `_mock` suffix 이벤트명 사용 (real 구현과 구별).
164+
- **Debug 도구 게이팅**: `PingTool` 등 개발용 도구는 `settings.PROJECT_ENV != "prod"` 조건으로 등록.
165+
- **Alembic 마이그레이션 전략**: 첫 배포 전에는 마이그레이션을 단일 `0001_initial.py`로 유지. 불필요한 incremental migration 누적 금지.
166+
- **OpenAPI 재생성**: 엔드포인트 변경 후 `mise gen:api` 실행 (openapi.json 생성 → 모바일 swagger_parser 클라이언트 자동 재생성).
167+
- **LiveKit role 동기화**: `LiveRole` Literal (`livekit/auth.py`), `_ROLE_SOURCES` dict, 라우터 Query param — 3곳 모두 역할 추가/변경 시 동기화 필수.
168+
- **OAuth User 모델**: `provider`(OAuth 제공자명) + `provider_id`(제공자별 사용자 ID) 컬럼 필수. 로그인 시 auth router에서 저장.
169+
- **리소스 인가 패턴**: 도메인 라우터에서 `authorize_host_access(db, user=user, host_id=...)` / `authorize_relation_access(db, user=user, relation=...)` 호출 필수 (`lib/authorization.py`). 인증(CurrentUser)과 인가(리소스 접근 권한)를 혼동하지 않는다.
170+
- **auth 모듈도 4계층 준수**: `auth/` 역시 `router.py``service.py``repository.py` 구조. login/refresh 로직은 service에, DB 쿼리는 repository에 위치. 라우터에 `db.add()` / `select()` 직접 사용 금지.
171+
- **`TYPE_CHECKING` + `cast` 주의**: `if TYPE_CHECKING:` 블록의 심볼을 `cast(SomeType, ...)` 첫 인자로 사용하면 runtime NameError. 문자열 리터럴 `cast("module.Type", ...)` 사용.
172+
- **Settings 캐시**: `get_settings()``@lru_cache`이므로 `.env` 변경 시 서버 프로세스 완전 재시작 필요 (uvicorn auto-reload로는 캐시 미갱신).
173+
- **Seed 데이터 UUID 체계**: `scripts/seed.py`의 deterministic UUID — Users: `...0100`~, Relations: `...0200`~, Wellness: `...0300`~, Medications: `...0400`~. 테스트 토큰 생성 시 실제 seed UUID 확인 필수.
174+
175+
- **E2E 테스트 (tests/e2e/)**: 실제 PostgreSQL 사용. DB 생성은 sync fixture (session scope), engine은 function-scoped async. ASGI transport는 별도 세션 사용하므로 transaction rollback 불가 — `TRUNCATE CASCADE`로 격리. seed fixture에서 `flush()` 대신 `commit()` 필수.
176+
- **commitlint subject-case**: subject는 lower-case 시작 필수. `add E2E tests` (X) → `add e2e tests` (O).
177+
- **Flutter build_runner**: `mise exec -- dart run build_runner build --delete-conflicting-outputs`. `dart`/`flutter` 직접 실행 불가 — 반드시 `mise exec --` 접두사 사용.
178+
- **Ruff S608 주의**: E2E 테스트에서 `CREATE DATABASE` 등 관리 SQL이 SQL injection으로 오탐 — `# noqa: S608` 필요. 단, f-string 첫 줄에만 주석 추가.
179+
180+
- **FastAPI status codes**: `HTTP_422_UNPROCESSABLE_ENTITY`는 deprecated — `HTTP_422_UNPROCESSABLE_CONTENT` 사용. `HTTP_413_REQUEST_ENTITY_TOO_LARGE`도 deprecated — `HTTP_413_CONTENT_TOO_LARGE` 사용.
181+
- **pytest 0 warnings 원칙**: 테스트 실행 시 warning이 있으면 반드시 원인을 찾아 수정. 경고를 무시하지 않는다.
182+
- **로컬 포트**: API는 `8200`, Worker는 `8280`. 기본 FastAPI 포트(8000) 아님.
183+
- **커밋 작성자**: Co-Authored-By 라인 추가하지 않는다.
184+
- **mise Flutter 플러그인**: `vfox:mise-plugins/vfox-flutter` 사용 (기본 asdf 플러그인은 3.22.1까지만 지원). `mise.toml`에서 `"vfox:mise-plugins/vfox-flutter" = "3"` 형태로 선언.
185+
- **PyJWT InsecureKeyLengthWarning**: 테스트에서 짧은 시크릿 사용 시 발생하는 경고 경로는 `jwt.warnings.InsecureKeyLengthWarning` (`jwt.exceptions`가 아님). `@pytest.mark.filterwarnings("ignore::jwt.warnings.InsecureKeyLengthWarning")` 사용.
186+
- **Optional dep 테스트 mock**: `firebase_admin`, `google.cloud.tasks_v2` 등 optional dep를 테스트할 때 `patch.dict(sys.modules, {"firebase_admin": MagicMock(), ...})` + `sys.modules.pop("src.lib.xxx", None)` (캐시된 모듈 제거) 패턴 사용.
187+
- **`lib/` vs 도메인 모듈**: `lib/notifications/`는 provider 인프라 (ABC + factory), `src/notifications/`는 DeviceToken 도메인 4계층. `lib/storage/`, `lib/maps/`도 동일 패턴 — provider 인프라만 담당하고 비즈니스 로직은 도메인 모듈에 위치.
188+
- **Navigation 모듈**: `navigation/`는 실시간 GPS 네비게이션 도메인. `NavigationSession` (경로 + 진행 상태) + `LocationWaypoint` (GPS 기록). `lib/maps/`는 MapProvider ABC (Google Maps 구현). AI tools: `start_navigation`, `cancel_navigation`, `get_navigation_step`. `live.py`에서 location 메시지 → waypoint 저장, off-route 감지, Gemini 컨텍스트 피드.
189+
- **Router에서 repository 직접 import**: `navigation/router.py`처럼 router가 service와 repository를 모두 직접 import 가능 (`from src.navigation import repository, service`). `service.repository` 접근은 mypy strict에서 attr-defined 에러 발생.
190+
- **데코레이터 타입 보존 (mypy strict)**: `Callable[..., object]` 반환 시 `untyped-decorator` 에러. `TypeVar("_F", bound=Callable[..., object])` + `Callable[[_F], _F]` 패턴 사용.
191+
- **AsyncMock sync 메서드 주의**: `AsyncSession.add()`, `.expire()` 등은 sync — `AsyncMock()` 사용 시 `db.add = MagicMock()` 명시적 오버라이드 필요 (coroutine-never-awaited 방지).
192+
- **Rate limiter 테스트 격리**: 로컬 Redis 실행 중이면 rate limit 상태 누적 → 429. `conftest.py`의 autouse fixture에서 `settings.REDIS_URL = None` 패치로 in-memory limiter 강제.
193+
- **Blocking I/O → `asyncio.to_thread`**: 스토리지(MinIO/GCS), FCM, Cloud Tasks 등 sync 라이브러리 호출은 반드시 `asyncio.to_thread()`로 래핑. 이벤트 루프를 직접 블로킹하지 않는다.
194+
- **Redis 캐시 싱글톤**: `lib/cache.py``_get_redis_sync()`로 모듈 레벨 싱글톤 Redis 클라이언트 관리. 매 호출마다 `aclose()` 하지 않음. shutdown 시 `close_cache()` 호출 (`main.py` lifespan).
195+
- **Rate limiter 구현**: `InMemoryRateLimiter`는 빈 키 자동 정리 (메모리 누수 방지). `RedisRateLimiter`는 Lua 스크립트(`_LUA_SLIDING_WINDOW`)로 ZADD+ZCARD 원자화. shutdown 시 `close_rate_limiters()` 호출 (`main.py` lifespan).
196+
- **WebSocket TOCTOU 방지**: `_active_bridges` dict 접근 시 `_bridges_lock` (asyncio.Lock)으로 보호. 중복 연결 체크와 등록을 원자적으로 수행.
197+
- **WebSocket location 핸들러 DB 세션**: `_handle_location()`은 장수 WebSocket 세션의 stale 객체 방지를 위해 `async with async_session_factory() as loc_db:` short-lived 세션 사용. reroute 후 `active_nav` 재할당 필수.
198+
- **LIKE 와일드카드 이스케이프**: repository에서 `.ilike()` 사용 시 `_escape_like()``%`, `_`, `\` 이스케이프. 사용자 입력이 와일드카드로 해석되는 것을 방지.
199+
- **Worker 재시도 정책**: `_retry_if_server_error`로 HTTP 5xx만 재시도. 4xx 클라이언트 에러는 즉시 실패. `RETRYABLE_EXCEPTIONS``ConnectError` + `TimeoutException`만 포함.
200+
- **Worker 에러 응답 최소화**: 태스크 라우터의 에러 detail에 내부 job 목록을 노출하지 않음. `GET /tasks/jobs`로만 조회 가능.
201+
202+
## Local Infrastructure
203+
204+
`apps/api/docker-compose.infra.yml` provides:
205+
- PostgreSQL 16 — `localhost:5433` (postgres/postgres/juny)
206+
- Redis 7 — `localhost:6380`
207+
- MinIO — `localhost:9010` (API), `localhost:9011` (console), minioadmin/minioadmin
208+
209+
## CI/CD
210+
211+
GitHub Actions deploys to GCP Cloud Run on push to main (per-app path filters). PRs run Ruff + Flutter analyze via reviewdog. Release Please handles versioning. All GCP deployments use Workload Identity Federation (keyless).

0 commit comments

Comments
 (0)