From 59b68ddfae190b8fab6e285ff4a61567d522a6a0 Mon Sep 17 00:00:00 2001 From: jmj Date: Mon, 27 Apr 2026 19:18:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=A4=ED=8E=99=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 741 +++--------------- ai/CLAUDE.md | 296 +++++++ ai/src/ai_server/CLAUDE.md | 197 +++++ backend/CLAUDE.md | 284 +++++++ .../main/java/com/stackup/stackup/CLAUDE.md | 250 ++++++ docs/README.md | 69 ++ docs/api-conventions.md | 303 +++++++ docs/architecture.md | 178 +++++ docs/coding-conventions.md | 193 +++++ docs/data-flow.md | 148 ++++ docs/database.md | 375 +++++++++ docs/design-system.md | 292 +++++++ docs/environment.md | 202 +++++ docs/event-stream.md | 160 ++++ docs/git-conventions.md | 196 +++++ docs/glossary.md | 168 ++++ docs/messaging.md | 337 ++++++++ docs/observability.md | 229 ++++++ docs/product-overview.md | 85 ++ docs/security.md | 188 +++++ docs/storage.md | 142 ++++ docs/testing-strategy.md | 184 +++++ docs/ui-patterns.md | 167 ++++ frontend/CLAUDE.md | 280 +++++++ frontend/src/app/CLAUDE.md | 109 +++ frontend/src/domain/CLAUDE.md | 148 ++++ frontend/src/features/CLAUDE.md | 110 +++ frontend/src/pages/CLAUDE.md | 100 +++ frontend/src/shared/CLAUDE.md | 149 ++++ frontend/src/shared/ui/CLAUDE.md | 164 ++++ infra/CLAUDE.md | 253 ++++++ 31 files changed, 6062 insertions(+), 635 deletions(-) create mode 100644 ai/CLAUDE.md create mode 100644 ai/src/ai_server/CLAUDE.md create mode 100644 backend/CLAUDE.md create mode 100644 backend/src/main/java/com/stackup/stackup/CLAUDE.md create mode 100644 docs/README.md create mode 100644 docs/api-conventions.md create mode 100644 docs/architecture.md create mode 100644 docs/coding-conventions.md create mode 100644 docs/data-flow.md create mode 100644 docs/database.md create mode 100644 docs/design-system.md create mode 100644 docs/environment.md create mode 100644 docs/event-stream.md create mode 100644 docs/git-conventions.md create mode 100644 docs/glossary.md create mode 100644 docs/messaging.md create mode 100644 docs/observability.md create mode 100644 docs/product-overview.md create mode 100644 docs/security.md create mode 100644 docs/storage.md create mode 100644 docs/testing-strategy.md create mode 100644 docs/ui-patterns.md create mode 100644 frontend/CLAUDE.md create mode 100644 frontend/src/app/CLAUDE.md create mode 100644 frontend/src/domain/CLAUDE.md create mode 100644 frontend/src/features/CLAUDE.md create mode 100644 frontend/src/pages/CLAUDE.md create mode 100644 frontend/src/shared/CLAUDE.md create mode 100644 frontend/src/shared/ui/CLAUDE.md create mode 100644 infra/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index 7320b257..76648ae2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,680 +1,151 @@ -# StackUp — Claude Code 프로젝트 컨텍스트 +# StackUp — Claude 컨텍스트 (루트) -> IT 직군 멀티모달 AI 면접 시뮬레이터 +> **IT 직군 멀티모달 AI 면접 시뮬레이터.** 본 문서는 전체 오버뷰 + 모든 컨텍스트 문서의 **인덱스**다. 작업 디렉토리에서 가장 가까운 `CLAUDE.md`를 먼저 읽고, 필요한 횡단 관심사는 `docs/`에서 찾는다. --- -## 1. 프로젝트 개요 +## 0. 한 줄 요약 -StackUp은 IT 취업 준비생 및 이직 준비자(프론트엔드, 백엔드, 인프라, DBA)를 대상으로 한 AI 기반 모의 면접 플랫폼이다. GitHub 레포지토리와 이력서를 분석하여 개인 맞춤 면접 질문을 생성하고, 실시간 음성 기반 면접 세션을 제공하며, 비언어적 피드백(시선, 자세, 음성 분석)까지 포함한 종합 리포트를 생성한다. +GitHub 레포 + 이력서 → AI가 분석 → 개인 맞춤 모의 면접 → 음성·비언어적 분석 포함 종합 피드백. -### 핵심 차별점 (단순 ChatGPT 질문과의 차이) -- 이력서 + GitHub 레포 연동 기반 RAG 개인화 질문 생성 -- 실시간 음성 대화 (STT/TTS) 기반 꼬리질문 자동 생성 -- 비언어적 분석 (시선 처리, 자세, 말 속도, 간투어 빈도) -- 세션 히스토리 추적 및 점수 추이 시각화 -- 면접 세션 리플레이 및 습관 파악 - -### 팀 구성 (4인) -| 이름 | 역할 | 주 담당 | -|------|------|---------| -| 박상우 | 백엔드 (Core) | Spring Boot, OAuth, 세션/리포트 API, DB | -| 정준모 | AI 및 풀스택 | AI 서빙, LangChain/RAG, STT/TTS, 프롬프트 설계 | -| 조서현 | 풀스택 | RealTime 서버(Go), Core-AI 연동, GitHub 레포 분석 | -| 신재호 | 프론트엔드 | React UI, 미디어 스트림, 웹캠 연동 | +상세 비전·페르소나·메트릭: [`docs/product-overview.md`](./docs/product-overview.md) --- -## 2. 시스템 아키텍처 (확정) +## 1. 컴포넌트 한눈에 ``` - ┌─────────────────────────┐ - │ Frontend (React) │ - │ CloudFront + S3 배포 │ - └────────┬────────────────┘ - │ - ┌────────▼────────────────┐ - │ Nginx API Gateway │ - └───┬─────────────┬───────┘ - │ │ - ┌─────────────▼──┐ ┌──────▼──────────────┐ - │ Core Server │ │ RealTime Server │ - │ (Spring Boot) │ │ (Go) │ - │ │ │ - WebRTC │ - │ - GitHub OAuth │ │ - WebSocket │ - │ - 회원관리 │ │ - SSE │ - │ - 세션/리포트 │ │ - 세션 실시간 관리 │ - │ - CRUD API │ └──────────────────────┘ - └───────┬───────┘ - │ - ┌───────▼───────┐ - │ RabbitMQ │ ← Message Broker (Core ↔ AI 비동기 통신) - └───────┬───────┘ - │ - ┌───────▼────────────────────┐ - │ AI Server (Python/FastAPI) │ - │ - LangChain 기반 RAG │ - │ - 질문 생성 / 꼬리질문 │ - │ - 이력서·레포 분석 │ - │ - 음성 분석 │ - └───┬───────┬───────┬───────┘ - │ │ │ - ┌───────▼┐ ┌───▼────┐ ┌▼──────────┐ - │External │ │Local │ │ VectorDB │ - │LLM APIs │ │LLM │ │ │ - │(Gemini, │ │ │ │ │ - │ChatGPT) │ │ │ │ │ - └────────┘ └────────┘ └───────────┘ - - ┌──────────────┐ ┌────────────────┐ - │ PostgreSQL │ │ Content S3 │ - │ + pgvector │ │ (이력서 PDF, │ - │ │ │ 분석 MD, │ - │ │ │ 면접 오디오) │ - └──────────────┘ └────────────────┘ - - ┌──────────────┐ - │ Redis │ ← ephemeral session state - └──────────────┘ -``` - -### 핵심 통신 흐름 -- **Frontend → Nginx → Core Server**: REST API (인증, CRUD, 세션 생성) -- **Frontend → Nginx → RealTime Server**: WebSocket/SSE (실시간 면접 세션) -- **Core Server → RabbitMQ → AI Server**: 비동기 AI 작업 요청 (분석, 질문 생성) -- **AI Server → RabbitMQ → Core Server**: 결과 콜백 -- **AI Server → External LLM API / Local LLM**: 실제 추론 -- **AI Server → VectorDB**: RAG 검색 (임베딩 기반 유사 문서/청크 검색) -- **Core Server → PostgreSQL**: 영속 데이터 (유일한 DB 직접 접근 주체) -- **Core/AI Server → S3**: 대용량 콘텐츠 저장/조회 - -### LLM 이중 모델 전략 -- **Pro 모델**: 세션 시작 시 사용자 컨텍스트(이력서 + GitHub) 기반 질문 풀 생성 -- **Flash 모델 + RAG**: 세션 중 실시간 꼬리질문 생성 (저지연 우선) -- **Local LLM**: 카메라 기반 시선/자세 분석 (MediaPipe) - ---- +Frontend (React) + ↓ +Nginx Gateway + ↓ +┌────────────────┐ ┌──────────────────────┐ +│ Core (Spring) │ ←→ │ RealTime (Go) │ +└─────┬──────────┘ └──────────────────────┘ + │ RabbitMQ + ↓ +┌──────────────────────────────┐ +│ AI (FastAPI/LangChain) │ +└──────────────────────────────┘ -## 3. 기술 스택 - -### Backend -| 영역 | 기술 | 비고 | -|------|------|------| -| Core Server | Java, Spring Boot, Spring AI | 인증인가, CRUD, 비즈니스 로직 | -| ORM/Query | JPA/Hibernate + QueryDSL | 간단 쿼리는 JPA, 동적 쿼리는 QueryDSL | -| Migration | Flyway | 스키마 버전 관리 | -| AI Server | Python, FastAPI, LangChain | 모델 서빙, RAG, 프롬프트 체이닝 | -| RealTime Server | Go | WebRTC, WebSocket, SSE 처리 | -| Message Broker | RabbitMQ | Core ↔ AI 비동기 통신 | - -### Database & Storage -| 영역 | 기술 | 비고 | -|------|------|------| -| Primary DB | PostgreSQL + pgvector | 관계형 데이터 + 벡터 임베딩 | -| Cache | Redis | 세션 상태, 토큰 등 ephemeral 데이터 | -| Object Storage | AWS S3 | 이력서 PDF, 분석 마크다운, 오디오 파일 | - -### Frontend -| 영역 | 기술 | 비고 | -|------|------|------| -| Framework | React (TypeScript) | SPA, 반응형 웹 (모바일 웹뷰 대응) | -| API 타입 생성 | springdoc-openapi → openapi-typescript | 타입 안전성 확보 | -| 배포 | CloudFront + S3 | 정적 호스팅 | - -### Infra & DevOps -| 영역 | 기술 | 비고 | -|------|------|------| -| API Gateway | Nginx | 리버스 프록시, 라우팅 | -| Container | Docker Compose (로컬), K8s (배포) | 관리형 컨테이너 서비스 미사용 | -| 분산 추적 | X-Trace-Id 헤더 기반 | 서비스 간 요청 추적 | - -### AI/ML -| 영역 | 기술 | 비고 | -|------|------|------| -| LLM API | Gemini, ChatGPT | 외부 API | -| Local LLM | 미정 | 카메라 분석 등 | -| 음성 | STT/TTS (구체 서비스 미정) | 개발 용어 영어 혼용 대응 필요 | -| 영상 분석 | MediaPipe | 시선 처리, 자세 분석 | -| 실시간 통신 | WebRTC | 음성/영상 스트리밍 | - ---- - -## 4. 핵심 기능 명세 - -### MVP (Phase 1) -1. **GitHub OAuth 로그인 및 레포지토리 선택** - - GitHub OAuth로 로그인 - - 사용자의 레포지토리 목록 조회 → 면접에 사용할 레포 선택 - - 선택된 레포의 파일 구조, README, 코드를 읽어 프로젝트 요약 마크다운 생성 → S3 저장 - -2. **이력서 업로드 및 분석** - - PDF/웹 링크 형태 이력서 업로드 - - 텍스트 추출 → 마크다운 변환 → S3 저장 - - 분석 결과(기술 스택, 경력, 프로젝트) 메타데이터를 DB에 저장 - -3. **RAG 기반 맞춤 질문 생성** - - 이력서 + GitHub 분석 마크다운을 청킹 → 임베딩 → pgvector 저장 - - 면접 세션 시작 시 RAG로 관련 컨텍스트 검색 → Pro 모델로 질문 풀 생성 - - 카테고리별 질문: CS 기초, 프로젝트 경험, 기술 선택 이유 등 - -4. **텍스트 기반 AI 면접 세션** - - 질문 → 답변 → 꼬리질문 사이클 (Flash 모델 + RAG) - - 답변 구체성, 논리성, 구조(STAR) 평가 - - 부족한 부분 기반 꼬리질문 자동 생성 - - 세션 설정: 면접 유형(인성/기술/라이브코딩), 직군(프론트/백/인프라/DBA), 최대 질문 수, 최대 시간 - -5. **면접 히스토리 관리** - - 세션 리스트 조회, 상세 조회 - - 잘한 점 / 아쉬운 점 텍스트 요약 - - 보완할 기술 키워드 추천 - -### Phase 2 (음성) -6. **실시간 음성 면접 세션** - - STT로 사용자 답변 실시간 텍스트 변환 - - TTS로 AI 면접관 음성 출력 - - 음성 분석: 말하기 속도(WPM), 간투어("음", "그", "어") 빈도, 침묵 시간, 발음 정확도 - -### Phase 3 (리포트 고도화) -7. **종합 피드백 리포트** - - 기술 정확도, 논리 점수, 커뮤니케이션 점수 - - 음성 분석 결과 통합 - - 다음 학습 키워드 추천 (JPA 영속성, 네트워크 기초 등) - - 세션 간 점수 추이 시각화 - -### Phase 4 (확장) -8. **웹캠 기반 비언어 분석** (부가기능) - - 카메라 응시 비율 - - 시선 방향 분포 (아래/옆 보는 시간 비율) - - 자세 분석 - - 온라인/오프라인 모드 선택 (상체만 / 전신) - -9. **멀티 에이전트 면접** (부가기능) - - 기술팀장, PM, 인사팀 등 여러 AI 면접관이 순차/동시 질문 - - LangGraph 기반 멀티에이전트 구현 - ---- - -## 5. 데이터베이스 스키마 (v4 + 개선사항) - -### 현재 스키마 (13 테이블) -``` -users → refresh_tokens, user_consents, repositories, resumes, - interview_sessions, activity_logs, ai_request_logs -repositories ─┐ -resumes ──────┼→ analyzed_documents → session_contexts ← interview_sessions -interview_sessions → interview_messages → message_voice_analyses -interview_sessions → session_feedbacks -``` - -### 테이블 목록 및 역할 - -| # | 테이블 | 역할 | -|---|--------|------| -| 1 | `users` | GitHub OAuth 사용자 정보 | -| 2 | `refresh_tokens` | JWT refresh token 저장 | -| 3 | `user_consents` | 개인정보처리동의 기록 | -| 4 | `repositories` | 면접 분석용 GitHub 저장소 메타데이터 | -| 5 | `resumes` | 이력서 메타데이터 (PDF/웹 URL), 실 파일은 S3 | -| 6 | `analyzed_documents` | AI 분석 결과 메타데이터 + S3 경로 (이력서/레포 공통) | -| 7 | `interview_sessions` | 면접 세션 설정·상태·히스토리 | -| 8 | `session_contexts` | 세션 ↔ 분석 문서 N:M 연결 | -| 9 | `interview_messages` | 면접 질문·답변 시퀀스 (parent_message_id로 꼬리질문 트리) | -| 10 | `message_voice_analyses` | 답변별 음성 분석 결과 (1:1) | -| 11 | `session_feedbacks` | 종합 피드백 리포트 (1:1) | -| 12 | `activity_logs` | 사용자 행동 로그 | -| 13 | `ai_request_logs` | AI 서버 요청/응답 로그 (토큰, 지연시간, 에러) | - -### 현재 스키마 DDL (v4) - -```sql --- 1. users -CREATE TABLE users ( - id BIGSERIAL PRIMARY KEY, - github_id BIGINT NOT NULL UNIQUE, - github_username VARCHAR(100) NOT NULL, - email VARCHAR(255), - avatar_url VARCHAR(500), - github_access_token VARCHAR(500) NOT NULL, -- ⚠️ 암호화 필요 - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 2. refresh_tokens -CREATE TABLE refresh_tokens ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - token_hash VARCHAR(500) NOT NULL UNIQUE, - device_info VARCHAR(500), - expires_at TIMESTAMP NOT NULL, - is_revoked BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 3. user_consents -CREATE TABLE user_consents ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - consent_type VARCHAR(50) NOT NULL, - consent_version VARCHAR(20) NOT NULL, - is_agreed BOOLEAN NOT NULL DEFAULT TRUE, - agreed_at TIMESTAMP NOT NULL DEFAULT NOW(), - revoked_at TIMESTAMP, - ip_address VARCHAR(45), - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 4. repositories -CREATE TABLE repositories ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - github_repo_id BIGINT NOT NULL, - repo_name VARCHAR(255) NOT NULL, - repo_full_name VARCHAR(500) NOT NULL, - repo_url VARCHAR(500) NOT NULL, - default_branch VARCHAR(100) DEFAULT 'main', - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - last_synced_at TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 5. resumes -CREATE TABLE resumes ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - original_filename VARCHAR(500) NOT NULL, - file_path VARCHAR(1000) NOT NULL, -- S3 key - file_type VARCHAR(20) NOT NULL, - file_size BIGINT, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 6. analyzed_documents -CREATE TABLE analyzed_documents ( - id BIGSERIAL PRIMARY KEY, - source_type VARCHAR(20) NOT NULL, -- 'RESUME' | 'REPOSITORY' - source_id BIGINT NOT NULL, -- ⚠️ Polymorphic FK (FK 강제 불가) - document_path VARCHAR(1000) NOT NULL, -- S3 key (분석 마크다운) - summary VARCHAR(2000), - tech_stack JSONB, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 7. interview_sessions -CREATE TABLE interview_sessions ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id), - title VARCHAR(200), - memo TEXT, - mode VARCHAR(20) NOT NULL, -- ONLINE / OFFLINE - interview_type VARCHAR(30) NOT NULL, -- PERSONALITY / TECHNICAL / LIVE_CODING / INTEGRATED - job_category VARCHAR(30) NOT NULL, -- FRONTEND / BACKEND / INFRA / DBA - max_questions INT NOT NULL DEFAULT 10, - max_duration_minutes INT NOT NULL DEFAULT 60, - status VARCHAR(20) NOT NULL DEFAULT 'READY', - -- READY / IN_PROGRESS / INTERRUPTED / COMPLETED / CANCELLED - total_question_count INT DEFAULT 0, - started_at TIMESTAMP, - ended_at TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 8. session_contexts -CREATE TABLE session_contexts ( - id BIGSERIAL PRIMARY KEY, - session_id BIGINT NOT NULL REFERENCES interview_sessions(id), - document_id BIGINT NOT NULL REFERENCES analyzed_documents(id), - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 9. interview_messages -CREATE TABLE interview_messages ( - id BIGSERIAL PRIMARY KEY, - session_id BIGINT NOT NULL REFERENCES interview_sessions(id), - sequence_number INT NOT NULL, - role VARCHAR(20) NOT NULL, -- INTERVIEWER / INTERVIEWEE / SYSTEM - content TEXT, - audio_file_path VARCHAR(1000), -- S3 key - parent_message_id BIGINT REFERENCES interview_messages(id), - status VARCHAR(20) NOT NULL DEFAULT 'CREATED', - -- CREATED / COMPLETED / FAILED - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 10. message_voice_analyses -CREATE TABLE message_voice_analyses ( - id BIGSERIAL PRIMARY KEY, - message_id BIGINT NOT NULL UNIQUE REFERENCES interview_messages(id), - speaking_rate_wpm FLOAT, - silence_duration_sec FLOAT, - filler_word_counts JSONB, - pronunciation_accuracy FLOAT, - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 11. session_feedbacks -CREATE TABLE session_feedbacks ( - id BIGSERIAL PRIMARY KEY, - session_id BIGINT NOT NULL UNIQUE REFERENCES interview_sessions(id), - overall_score FLOAT, - technical_accuracy FLOAT, - logic_score FLOAT, - communication_score FLOAT, - strengths_summary TEXT, - weaknesses_summary TEXT, - improvement_keywords JSONB, - report_file_path VARCHAR(1000), - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - is_deleted BOOLEAN NOT NULL DEFAULT FALSE -); - --- 12. activity_logs -CREATE TABLE activity_logs ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT REFERENCES users(id), - action VARCHAR(50) NOT NULL, - resource_type VARCHAR(30), - resource_id BIGINT, - detail JSONB, - ip_address VARCHAR(45), - user_agent VARCHAR(500), - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- 13. ai_request_logs -CREATE TABLE ai_request_logs ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT REFERENCES users(id), - session_id BIGINT REFERENCES interview_sessions(id), - request_type VARCHAR(50) NOT NULL, - model_name VARCHAR(100), - input_tokens INT, - output_tokens INT, - latency_ms INT, - status VARCHAR(20) NOT NULL, - error_message TEXT, - created_at TIMESTAMP NOT NULL DEFAULT NOW() -); +PostgreSQL+pgvector · Redis · S3/MinIO ``` -### ⚠️ 식별된 스키마 개선 필수 사항 (우선순위순) - -**즉시 수정 (Flyway V1 마이그레이션 전)** -1. **TIMESTAMP → TIMESTAMPTZ 전환**: 모든 테이블의 TIMESTAMP을 TIMESTAMPTZ로 변경 (타임존 이슈 방지) -2. **CHECK 제약조건 추가**: status, mode, interview_type, job_category, role 등 모든 분류 컬럼에 허용값 제한 -3. **interview_messages.sequence_number UNIQUE 제약**: `(session_id, sequence_number)` 복합 유니크 인덱스 -4. **analyzed_documents 다형성 FK 해결**: `source_type + source_id` → `resume_id BIGINT NULL REFERENCES resumes(id)` + `repository_id BIGINT NULL REFERENCES repositories(id)` + CHECK (둘 중 하나만 NOT NULL) -5. **github_access_token 암호화**: 평문 저장 금지, 애플리케이션 레벨 AES 암호화 적용, 컬럼명을 `encrypted_github_access_token`으로 변경 -6. **interview_messages 빈 메시지 방지**: `CHECK (content IS NOT NULL OR audio_file_path IS NOT NULL)` - -**후속 개선** -7. **로그 테이블 파티셔닝**: activity_logs, ai_request_logs에 `PARTITION BY RANGE (created_at)` 적용 -8. **Partial Index 활용**: soft delete 테이블에 `WHERE is_deleted = FALSE` partial index -9. **session_feedbacks 점수 확장**: 면접 타입별 다른 평가 항목 → JSONB scores 필드 고려 -10. **S3 path 컨벤션 통일**: bucket은 환경변수, key만 DB 저장 - -### 미반영 도메인 (추후 테이블 추가 예정) -- 시선/자세 분석 결과 테이블 (Phase 4) -- 면접 노트 (질문별 사용자 메모 + LLM Enrich) -- 면접 대상 회사 관리 (target_companies) +자세한 다이어그램과 책임 매트릭스: [`docs/architecture.md`](./docs/architecture.md) --- -## 6. 데이터 흐름 상세 - -### 6.1 GitHub 레포 분석 파이프라인 -``` -사용자 레포 선택 - → Core Server: repositories 테이블에 레코드 생성 (status: PENDING) - → Core Server → RabbitMQ: 분석 요청 발행 - → AI Server: GitHub API로 레포 탐색 (파일 구조, README, 코드) - → AI Server: 프로젝트 요약 마크다운 생성 - → AI Server → S3: 마크다운 업로드 - → AI Server: 마크다운 청킹 → 임베딩 생성 → pgvector 저장 - → AI Server → RabbitMQ: 분석 완료 콜백 - → Core Server: analyzed_documents 레코드 생성, repositories.status → ANALYZED -``` +## 2. 레이어별 진입점 (CLAUDE.md) -### 6.2 이력서 분석 파이프라인 -``` -사용자 이력서 업로드 (PDF) - → Core Server → S3: 원본 파일 업로드 - → Core Server: resumes 테이블에 레코드 생성 (status: PENDING) - → Core Server → RabbitMQ: 분석 요청 발행 - → AI Server: PDF → 텍스트 추출 → 마크다운 변환 - → AI Server → S3: 분석 마크다운 업로드 - → AI Server: 마크다운 청킹 → 임베딩 생성 → pgvector 저장 - → AI Server → RabbitMQ: 분석 완료 콜백 - → Core Server: analyzed_documents 레코드 생성, resumes.status → ANALYZED -``` +| 레이어 | 위치 | 기술 | +|--------|------|------| +| 프론트엔드 | [`frontend/CLAUDE.md`](./frontend/CLAUDE.md) | React 19 + TypeScript + Vite, FSD 구조 | +| 백엔드 (Core) | [`backend/CLAUDE.md`](./backend/CLAUDE.md) | Java 21 + Spring Boot 4 + JPA + QueryDSL | +| AI 서버 | [`ai/CLAUDE.md`](./ai/CLAUDE.md) | Python 3.13 + FastAPI + LangChain | +| 인프라 | [`infra/CLAUDE.md`](./infra/CLAUDE.md) | Docker Compose: PG(+pgvector), RabbitMQ, MinIO | -### 6.3 면접 세션 흐름 -``` -세션 생성 (설정: 유형, 직군, 참고 문서 선택) - → Core Server: interview_sessions 생성, session_contexts 연결 - → Core Server → RabbitMQ: 질문 풀 생성 요청 (Pro 모델) - → AI Server: RAG로 관련 컨텍스트 검색 → 질문 풀 생성 - → 세션 시작 - -질문-답변 사이클 (반복): - → RealTime Server → Frontend: 질문 전송 (WebSocket/SSE) - → Frontend: 사용자 답변 (텍스트 or 음성 → STT) - → RealTime Server: interview_messages 기록 요청 - → Core Server: 메시지 저장 - → Core Server → RabbitMQ: 꼬리질문 생성 요청 (Flash 모델 + RAG) - → AI Server: 답변 평가 + 꼬리질문 생성 - → 반복... - -세션 종료: - → Core Server → RabbitMQ: 피드백 리포트 생성 요청 - → AI Server: 종합 평가 생성 - → Core Server: session_feedbacks 저장 -``` +> **RealTime Server (Go)** 는 아직 구현 디렉토리 없음. 추가 시 `realtime/CLAUDE.md` 생성. -### 6.4 저장소 전략 (Hybrid) -| 데이터 종류 | 저장소 | 이유 | -|-------------|--------|------| -| 사용자/세션/메시지 메타 | PostgreSQL | 관계형 쿼리, 트랜잭션 | -| 벡터 임베딩 | PostgreSQL (pgvector) | 단일 DB 운영 | -| 분석 마크다운 (이력서/레포) | S3 | 대용량 텍스트 | -| 면접 오디오 파일 | S3 | 대용량 바이너리 | -| 이력서 원본 PDF | S3 | 대용량 바이너리 | -| 실시간 세션 상태 | Redis | 빠른 읽기/쓰기, TTL | +각 레이어 하위 슬라이스/도메인 패키지에도 `CLAUDE.md`가 배치되어 있을 수 있다. 작업할 디렉토리에서 가장 가까운 것을 우선 읽는다. --- -## 7. API 설계 가이드라인 - -### 접근 원칙 -- **PostgreSQL 직접 접근**: Core Server만 가능 (AI Server, RealTime Server는 API 또는 RabbitMQ 경유) -- **분산 추적**: 모든 요청에 `X-Trace-Id` 헤더 전파 -- **API 문서**: springdoc-openapi로 OpenAPI 스펙 자동 생성 → 프론트에서 openapi-typescript로 타입 생성 -- **AI 작업 상태**: SSE로 프론트에 실시간 전달 +## 3. 횡단 관심사 인덱스 + +| 분류 | 문서 | +|------|------| +| 제품 비전 | [`docs/product-overview.md`](./docs/product-overview.md) | +| 시스템 아키텍처 | [`docs/architecture.md`](./docs/architecture.md) | +| 데이터 흐름 (시나리오) | [`docs/data-flow.md`](./docs/data-flow.md) | +| 데이터베이스 (DDL, ENUM, 인덱스) | [`docs/database.md`](./docs/database.md) | +| REST API 규약 | [`docs/api-conventions.md`](./docs/api-conventions.md) | +| RabbitMQ 메시징 | [`docs/messaging.md`](./docs/messaging.md) | +| 객체 스토리지 (S3/MinIO) | [`docs/storage.md`](./docs/storage.md) | +| SSE 이벤트 스펙 | [`docs/event-stream.md`](./docs/event-stream.md) | +| **디자인 시스템** | [`docs/design-system.md`](./docs/design-system.md) | +| UI 패턴 (4-state, 폼, 토스트, 키보드) | [`docs/ui-patterns.md`](./docs/ui-patterns.md) | +| 보안 (인증, 암호화, 개인정보) | [`docs/security.md`](./docs/security.md) | +| 옵저버빌리티 (trace, 로그, AI 비용) | [`docs/observability.md`](./docs/observability.md) | +| 환경 변수 카탈로그 | [`docs/environment.md`](./docs/environment.md) | +| 코딩 컨벤션 | [`docs/coding-conventions.md`](./docs/coding-conventions.md) | +| Git 컨벤션 (브랜치·커밋·PR) | [`docs/git-conventions.md`](./docs/git-conventions.md) | +| 테스트 전략 | [`docs/testing-strategy.md`](./docs/testing-strategy.md) | +| **용어집 (한↔영, ENUM)** | [`docs/glossary.md`](./docs/glossary.md) | + +전체 인덱스: [`docs/README.md`](./docs/README.md) + +산출물 (학교 제출용 PDF): +- `docs/1조(StackUp)UserStory (1).pdf` +- `docs/1조(StackUp)Product Backlog (1).pdf` -### 주요 API 그룹 (Core Server) -``` -POST /api/auth/github # GitHub OAuth 로그인 -POST /api/auth/refresh # 토큰 갱신 -DELETE /api/auth/logout # 로그아웃 - -GET /api/users/me # 내 정보 조회 -PUT /api/users/me # 내 정보 수정 -DELETE /api/users/me # 회원 탈퇴 (soft delete) +--- -GET /api/repositories # 내 GitHub 레포 목록 -POST /api/repositories # 분석용 레포 등록 -GET /api/repositories/:id # 레포 상세 (분석 상태 포함) +## 4. 핵심 설계 원칙 (전 컴포넌트 공통) -POST /api/resumes # 이력서 업로드 -GET /api/resumes # 내 이력서 목록 -GET /api/resumes/:id # 이력서 상세 +1. **PostgreSQL 단독 접근** — Core 서버만 직접 접근. AI/RealTime은 API 또는 RabbitMQ 경유. ([architecture §4.1](./docs/architecture.md)) +2. **Hybrid Storage** — 관계형 → PG, 임베딩 → pgvector, 대용량 → S3, 휘발성 → Redis. +3. **LLM 이중 모델** — 세션 시작은 Pro(품질), 꼬리질문은 Flash + RAG(저지연 < 3s). +4. **분산 추적** — 모든 요청에 `X-Trace-Id` 전파. RabbitMQ 메시지 헤더에도 동일. +5. **MVP 우선** — 필요해질 때 추가, 미래 가정 테이블/기능 만들지 않음. +6. **도메인 우선 패키징** — 백엔드는 layered가 아닌 도메인 패키지로 구성. +7. **레이어별 컨텍스트** — 작업 시 가장 가까운 `CLAUDE.md`부터 읽는다. -GET /api/documents # 분석된 문서 목록 -GET /api/documents/:id # 분석 문서 상세 +--- -POST /api/sessions # 면접 세션 생성 -GET /api/sessions # 세션 히스토리 목록 -GET /api/sessions/:id # 세션 상세 (메시지, 피드백 포함) -PATCH /api/sessions/:id/start # 세션 시작 -PATCH /api/sessions/:id/end # 세션 종료 +## 5. 팀 구성 -GET /api/sessions/:id/feedback # 세션 피드백 조회 -GET /api/users/me/stats # 내 면접 통계 (점수 추이 등) -``` +| 이름 | 역할 | +|------|------| +| 박상우 (SM) | Backend Core (Spring Boot, OAuth, 세션·리포트 API, DB) | +| 정준모 | AI 서빙 (LangChain/RAG, STT/TTS, 프롬프트) | +| 조서현 (PO) | RealTime 서버 (Go), Core-AI 연동, GitHub 분석 | +| 신재호 | Frontend (React UI, 미디어 스트림, 웹캠) | --- -## 8. 개발 환경 및 컨벤션 - -### 로컬 개발 환경 -- Docker Compose로 PostgreSQL, Redis, RabbitMQ, MinIO(S3 대체) 실행 -- 각 서버(Core, AI, RealTime)는 독립 프로세스로 실행 - -### 코드 컨벤션 -- **Java (Core Server)**: Spring Boot 표준 패키지 구조 - ``` - com.stackup.core - ├── config/ # 설정 (Security, RabbitMQ, S3 등) - ├── domain/ # Entity, Repository, Enum - ├── service/ # 비즈니스 로직 - ├── controller/ # REST API - ├── dto/ # Request/Response DTO - ├── exception/ # 커스텀 예외 - └── infra/ # 외부 연동 (GitHub API, S3, RabbitMQ publisher) - ``` -- **Python (AI Server)**: FastAPI + LangChain - ``` - ai_server/ - ├── api/ # FastAPI 라우터 (RabbitMQ consumer 포함) - ├── chain/ # LangChain 체인 정의 - ├── rag/ # RAG 파이프라인 (청킹, 임베딩, 검색) - ├── analyzer/ # 이력서/레포 분석 로직 - ├── voice/ # STT/TTS, 음성 분석 - ├── model/ # Pydantic 모델 - └── config/ # 설정 - ``` -- **Go (RealTime Server)** - ``` - realtime/ - ├── handler/ # WebSocket, SSE, WebRTC 핸들러 - ├── session/ # 세션 상태 관리 - ├── middleware/ # 인증, 로깅 - └── config/ - ``` -- **React (Frontend)** - ``` - src/ - ├── components/ # 재사용 컴포넌트 - ├── pages/ # 라우트별 페이지 - ├── hooks/ # 커스텀 훅 - ├── api/ # API 호출 (자동 생성 타입 활용) - ├── stores/ # 상태 관리 - └── types/ # 타입 정의 (openapi-typescript 생성) - ``` - -### Git 브랜치 전략 -- `main`: 배포 브랜치 -- `develop`: 통합 개발 브랜치 -- `feature/*`: 기능 개발 -- `hotfix/*`: 긴급 수정 +## 6. 빠른 시작 ---- +```bash +# 1. 환경 변수 +cp .env.example .env +cp frontend/.env.example frontend/.env.local # 도입 후 +cp ai/.env.example ai/.env -## 9. MVP 개발 순서 (권장) +# 2. 인프라 부팅 +docker compose up -d -``` -Week 1-2: 프로젝트 세팅 + DB 스키마 확정 - ├── Docker Compose 환경 구성 (PostgreSQL, Redis, RabbitMQ, MinIO) - ├── Flyway 초기 마이그레이션 (V1__init.sql) - ├── Spring Boot 프로젝트 스캐폴딩 + JPA Entity 매핑 - ├── FastAPI 프로젝트 스캐폴딩 - └── Go RealTime 서버 스캐폴딩 - -Week 3-4: 인증 + GitHub 연동 - ├── GitHub OAuth 플로우 구현 (Core Server) - ├── JWT 발급/갱신/검증 (access + refresh) - ├── GitHub API 연동: 레포 목록 조회, 파일/README 읽기 - ├── 레포 분석 파이프라인: 레포 → 마크다운 요약 → S3 저장 - └── RabbitMQ 기본 연동 (Core ↔ AI 메시지 교환) - -Week 5-6: 이력서 분석 + RAG 파이프라인 - ├── 이력서 업로드 API (S3 연동) - ├── PDF → 텍스트 → 마크다운 변환 - ├── 청킹 전략 구현 + 임베딩 생성 - ├── pgvector 저장 + 유사도 검색 - └── RAG 기반 질문 생성 프롬프트 설계/테스트 - -Week 7-8: 면접 세션 MVP - ├── 세션 CRUD API - ├── 세션 시작 → 질문 풀 생성 → 첫 질문 전달 - ├── 답변 수신 → 평가 → 꼬리질문 생성 사이클 - ├── 세션 종료 → 피드백 생성 - └── React 면접 세션 UI (텍스트 기반) - -Week 9-10: STT/TTS + 음성 면접 - ├── STT 연동 (브라우저 → 서버 스트리밍) - ├── TTS 연동 (면접관 음성 출력) - ├── 음성 분석 모듈 (WPM, filler words, 침묵) - └── RealTime 서버 WebSocket 연동 - -Week 11-12: 리포트 + 히스토리 + 통합 테스트 - ├── 피드백 리포트 고도화 - ├── 세션 히스토리 대시보드 - ├── 점수 추이 시각화 - └── 통합 테스트, 버그 수정 - -Week 13-14: 사용자 테스트 + 발표 준비 - ├── 사용자 테스트 (5명+) - ├── 성능 최적화 - ├── UI/UX 개선 - └── 발표 자료 준비 +# 3. 각 서버 실행 +cd backend && ./gradlew bootRun +cd ai && uv sync && uv run uvicorn ai_server.main:app --reload +cd frontend && npm install && npm run dev ``` ---- +상세: [`infra/CLAUDE.md §3`](./infra/CLAUDE.md), [`docs/environment.md §10`](./docs/environment.md) -## 10. 주의사항 및 원칙 +--- -### 설계 원칙 -- **MVP 우선**: 불필요한 테이블/기능은 과감히 제거. "나중에 추가하겠지" 테이블은 만들지 않음 -- **Hybrid Storage**: 구조화 데이터 → PostgreSQL, 대용량 콘텐츠 → S3, 임시 상태 → Redis -- **청킹 기반 RAG**: 전체 문서를 LLM에 넣지 않고, 청킹 → 임베딩 → 유사도 검색 → 관련 컨텍스트만 전달 -- **DB 접근 격리**: PostgreSQL 직접 접근은 Core Server만. AI/RealTime 서버는 API 또는 RabbitMQ 경유 -- **코드 기반 문서화**: 스키마의 모든 테이블과 컬럼에 역할/용도 코멘트 필수 +## 7. 작업 시 체크리스트 -### 보안 -- GitHub access token은 반드시 암호화 저장 -- JWT refresh token은 해시만 저장 (원본 저장 금지) -- 이력서에 민감정보 포함 가능 → 개인정보처리동의 기록 필수 -- S3 path에는 key만 저장 (bucket은 환경변수) +신규 기능 PR 머지 전: -### 성능 -- 꼬리질문 생성 지연: 3초 이내 목표 (Flash 모델 + RAG) -- STT 인식 정확도: 90% 이상 (개발 용어 영어 혼용 환경) -- 로그 테이블(activity_logs, ai_request_logs)은 파티셔닝 + 보관 정책 설계 +- [ ] 가장 가까운 `CLAUDE.md`의 규약 준수 +- [ ] 단위 테스트 추가 (해피 + 경계값) +- [ ] DB 스키마 변경 시 Flyway 마이그레이션 + [`docs/database.md`](./docs/database.md) 갱신 +- [ ] 새 API 시 [`docs/api-conventions.md`](./docs/api-conventions.md) 준수 + OpenAPI 자동 생성 +- [ ] 새 메시지 시 [`docs/messaging.md`](./docs/messaging.md) §1, §5 갱신 + `infra/rabbitmq/definitions.json` +- [ ] 새 환경변수 시 `.env.example` 동기화 + [`docs/environment.md`](./docs/environment.md) +- [ ] 보안 체크리스트 통과 [`docs/security.md §10`](./docs/security.md) +- [ ] PR 본문 템플릿 채우기 (`.github/PULL_REQUEST_TEMPLATE.md`) --- -## 부록: 면접 세션 설정값 ENUM 정리 +## 8. 메모 -``` -mode: ONLINE | OFFLINE -interview_type: PERSONALITY | TECHNICAL | LIVE_CODING | INTEGRATED -job_category: FRONTEND | BACKEND | INFRA | DBA -session_status: READY | IN_PROGRESS | INTERRUPTED | COMPLETED | CANCELLED -message_role: INTERVIEWER | INTERVIEWEE | SYSTEM -message_status: CREATED | COMPLETED | FAILED -repo_status: PENDING | ANALYZING | ANALYZED | FAILED -resume_status: PENDING | ANALYZING | ANALYZED | FAILED -doc_status: ACTIVE | ARCHIVED -``` +- Phase 1 (MVP) 진행 중. US-01 ~ US-20 우선. +- 디자인 시스템 토큰 파일 미생성 — 디자인 적용 첫 PR에서 `frontend/src/app/styles/tokens.css` 생성. +- Spring Security, RabbitMQ starter, Flyway는 백엔드 기능 작성 PR과 함께 도입. +- `infra/rabbitmq/definitions.json`은 현재 큐 6개 정의 (resume/repo 분석, questions/followup 생성, 콜백 2개). 피드백 큐는 US-24 작업 시 추가. +- `Redis`는 docker-compose에 미포함. 세션 ephemeral state, 멱등 키, SSE pub/sub 도입 시 추가. +- `RealTime Server (Go)`는 아직 디렉토리 없음. WebRTC/WebSocket/SSE Phase 2 시점에 작성. + +문서 변경 원칙: 코드와 함께 같은 PR에 포함. 단일 출처 원칙(SSOT) 유지. diff --git a/ai/CLAUDE.md b/ai/CLAUDE.md new file mode 100644 index 00000000..19d3d273 --- /dev/null +++ b/ai/CLAUDE.md @@ -0,0 +1,296 @@ +# AI Server — Claude 컨텍스트 + +> StackUp AI 서버. **Python 3.13 + FastAPI + LangChain**. RabbitMQ consumer로 동작하며 LLM 호출, RAG, 음성 분석을 담당. + +상위 컨텍스트: [`/CLAUDE.md`](../CLAUDE.md) · 횡단 관심사: [`/docs/`](../docs/README.md) + +--- + +## 1. 기술 스택 + +| 영역 | 기술 | +|------|------| +| Language | Python 3.13 (`.python-version` 고정) | +| 패키지 매니저 | **uv** (`uv.lock` 락파일) | +| Framework | FastAPI 0.135+ | +| ASGI | uvicorn (standard) | +| 스키마 | Pydantic 2.x + pydantic-settings | +| HTTP | httpx | +| MQ | aio-pika (async AMQP) | +| 객체 스토리지 | boto3 (S3 호환) | +| LLM | LangChain 1.x (core + community) | +| 로깅 | structlog | +| 빌드 | hatchling | +| 테스트 | pytest + pytest-asyncio | +| 포맷/린트 | black, flake8, pylint | + +--- + +## 2. 디렉토리 구조 + +``` +ai/ +├── pyproject.toml +├── uv.lock +├── .python-version +├── Dockerfile +├── .env.example +├── src/ +│ └── ai_server/ +│ ├── __init__.py +│ ├── main.py # FastAPI 앱 팩토리 +│ ├── api/ # FastAPI 라우터 (health, internal endpoints) +│ │ └── health.py +│ ├── config/ +│ │ └── settings.py # pydantic-settings (env) +│ ├── chain/ # (계획) LangChain 체인 정의 +│ ├── rag/ # (계획) 청킹·임베딩·검색 +│ ├── analyzer/ # (계획) 이력서·레포 분석 +│ ├── voice/ # (계획) STT/TTS, 음성 분석 +│ ├── messaging/ # (계획) RabbitMQ consumer/publisher +│ ├── storage/ # (계획) S3 client +│ └── model/ # (계획) Pydantic 모델 (메시지 envelope, 도메인) +└── tests/ +``` + +> 현재는 `api/`, `config/`만 존재. 기능 추가 시 위 골격대로 디렉토리 생성. + +--- + +## 3. 책임 매트릭스 + +| 모듈 | 책임 | +|------|------| +| `main.py` | FastAPI 부트스트랩, lifespan에서 RabbitMQ consumer 시작 | +| `config/settings.py` | 환경변수 → 타입 안전 설정 객체 | +| `api/` | 헬스체크 + (필요 시) 내부 디버그 API | +| `messaging/` | aio-pika consumer (큐별), publisher | +| `analyzer/` | 이력서/레포 분석 use case (PDF 추출, GitHub fetch, 마크다운 생성) | +| `chain/` | LangChain prompt template + chain composition | +| `rag/` | 청킹, 임베딩 생성, pgvector 검색 호출 (Core API 경유) | +| `voice/` | STT/TTS 어댑터, WPM/filler/silence 분석 | +| `storage/` | S3 GET/PUT 래퍼 | +| `model/` | RabbitMQ envelope, request/response Pydantic 모델 | + +--- + +## 4. 비책임 (명시적) + +- ❌ PostgreSQL 직접 접근 — Core 서버 API 경유 또는 RabbitMQ 메시지에 데이터 동봉 +- ❌ JWT 발급·검증 — 인증은 Core +- ❌ REST CRUD API 노출 — 외부 트리거는 RabbitMQ만 +- ❌ 사용자 인증 (내부 통신만) — `api/`는 헬스체크 / 내부 도구 + +--- + +## 5. 메시징 (RabbitMQ) + +본 서버는 RabbitMQ **consumer**로 작동. + +| Queue | Bind | +|-------|------| +| `q.ai.resume` | `ai.request.resume.*` | +| `q.ai.repo` | `ai.request.repo.*` | +| `q.ai.session` | `ai.request.session.*` | + +콜백 발행: `ai.callback.{type}` 익스체인지. +상세 envelope/스키마/재시도: [`/docs/messaging.md`](../docs/messaging.md). + +### consumer 패턴 (aio-pika) +```python +async def consume_resume_analyze(message: AbstractIncomingMessage) -> None: + async with message.process(requeue=False): # auto ack on exit + envelope = parse_envelope(message) + with trace_context(envelope.trace_id): + await resume_analyzer.handle(envelope.payload) +``` + +### 멱등 처리 +- envelope의 `messageId`를 Redis (`processed:msg:{id}` TTL 24h)에 기록 +- 이미 존재하면 skip + ACK + +--- + +## 6. LLM 사용 패턴 + +### 6.1 모델 선택 +| 시점 | 모델 | 용도 | +|------|------|------| +| 세션 시작 | Pro (Gemini 1.5 Pro 기본) | 질문 풀 (품질) | +| 세션 중 | Flash (Gemini 1.5 Flash) | 꼬리질문 (저지연 < 3s) | +| 분석 (이력서/레포) | Pro | 마크다운 구조화 | + +설정은 `settings.py` + 환경변수로 모델명 주입 (코드에 하드코딩 금지). + +### 6.2 LangChain 사용 +```python +from langchain_core.prompts import ChatPromptTemplate +from langchain_google_genai import ChatGoogleGenerativeAI + +prompt = ChatPromptTemplate.from_messages([ + ("system", "당신은 면접관입니다..."), + ("human", "이력서: {resume}\n질문 후보: ..."), +]) +llm = ChatGoogleGenerativeAI(model=settings.llm_pro_model) +chain = prompt | llm | StructuredOutputParser(schema=...) +``` + +- 모든 프롬프트는 `chain/prompts/{name}.py` 단일 위치 +- 프롬프트 변경은 PR로 (커밋 메시지에 의도 명시) +- 응답은 **반드시 schema validation** (Pydantic) — LLM 결과를 그대로 신뢰 X + +### 6.3 호출 로깅 +호출 시작/완료/실패를 Core 서버에 RabbitMQ로 보내 `ai_request_logs`에 기록 — 또는 자체 endpoint POST. +필드: `request_type`, `model_name`, `input_tokens`, `output_tokens`, `latency_ms`, `status`. + +상세: [`/docs/observability.md §3`](../docs/observability.md). + +--- + +## 7. RAG 파이프라인 + +### 7.1 인제스트 +1. 마크다운 입력 +2. 청킹 (LangChain `RecursiveCharacterTextSplitter`, chunk_size=1000, overlap=200) +3. 임베딩 생성 (Gemini `text-embedding-004` 또는 OpenAI `text-embedding-3-small`) +4. Core API 호출 → pgvector INSERT + +### 7.2 검색 +1. 쿼리 텍스트 → 임베딩 +2. Core API: `POST /api/internal/embeddings/search` (top_k 검색) +3. 검색 결과 + 추가 메타데이터 → 프롬프트에 주입 + +> Core가 pgvector 단일 진입점을 제공하므로 AI는 직접 pg 호출 X. + +--- + +## 8. 음성 처리 (Phase 2) + +- STT/TTS 제공자 미정 → 도입 시 본 섹션 갱신 +- 추상화 계층 두기: `voice/stt/base.py` (interface), `voice/stt/{provider}.py` +- 분석: + - WPM = words / minutes + - 간투어: 한국어 정규식 `r"\b(음+|어+|그+)\b"` 카운트 + - 침묵: VAD (Voice Activity Detection) 라이브러리 결과 합산 + +--- + +## 9. 환경 변수 + +`config/settings.py`의 `Settings` 클래스가 진실 공급원 (single source of truth). +필수 변수는 default 없음 → 부팅 실패로 누락 감지. + +```python +class Settings(BaseSettings): + rabbitmq_url: str + s3_endpoint_url: str + s3_access_key: str + s3_secret_key: str + s3_bucket_name: str + openai_api_key: str = "" + google_api_key: str = "" + llm_pro_model: str = "gemini-1.5-pro" + llm_flash_model: str = "gemini-1.5-flash" + embedding_model: str = "text-embedding-004" + embedding_dim: int = 768 + core_server_base_url: str = "http://core:8080" +``` + +전체 환경변수 카탈로그: [`/docs/environment.md §4`](../docs/environment.md). + +--- + +## 10. 로깅 (structlog) + +```python +import structlog +log = structlog.get_logger() + +log.info("resume.analyze.start", resume_id=42, trace_id=trace_id) +log.error("resume.analyze.failed", resume_id=42, error_code="PDF_PARSE_FAILED", exc_info=True) +``` + +- JSON 출력 (운영) / human pretty (로컬) +- 컨텍스트 변수로 `trace_id`, `user_id` 자동 주입 +- 민감정보 (이력서 본문, 답변 본문) 절대 X +- 자세한 정책: [`/docs/observability.md`](../docs/observability.md) + +--- + +## 11. 테스트 + +```bash +uv run pytest # 전체 +uv run pytest tests/test_rag.py # 특정 파일 +uv run pytest -k "embedding" # 키워드 +``` + +- 비동기 테스트는 `pytest-asyncio` (`@pytest.mark.asyncio`) +- LLM 호출은 mock (LangChain `FakeListLLM`) +- RabbitMQ는 Testcontainer 또는 `aio-pika` mock +- 자세한 전략: [`/docs/testing-strategy.md`](../docs/testing-strategy.md) + +--- + +## 12. 코드 스타일 + +- black (line-length 88) +- pyproject.toml 기준 +- 함수/변수: `snake_case` +- 상수: `UPPER_SNAKE_CASE` +- 클래스: `PascalCase` +- 타입 힌트 필수 (`from __future__ import annotations` 없이 PEP 604 union `int | None`) +- async first — sync IO 사용 시 명시적 이유 + +상세 공통 규약: [`/docs/coding-conventions.md`](../docs/coding-conventions.md). + +--- + +## 13. 빌드·실행 + +```bash +uv sync # 의존성 설치 +uv run uvicorn ai_server.main:app --reload # 개발 실행 +uv run python -m ai_server.messaging.runner # consumer 단독 실행 (도입 후) + +# Docker +docker build -t stackup-ai . +docker run --env-file .env -p 8000:8000 stackup-ai +``` + +`docker-compose.yml`에 ai 서비스 추가는 추후 (현재는 Core/PG/MQ/MinIO만 있음). + +--- + +## 14. 새 기능 추가 절차 + +1. 메시지 스키마 정의 → `model/messages.py` +2. RabbitMQ routing key 결정 → [`/docs/messaging.md`](../docs/messaging.md) 갱신 +3. consumer 작성 → `messaging/{name}_consumer.py` +4. 비즈니스 로직 → `analyzer/` / `chain/` / `voice/` 적절한 모듈 +5. 프롬프트 (LLM 호출 시) → `chain/prompts/{name}.py` +6. 단위 테스트 (LLM mock) + 통합 테스트 (Testcontainer) +7. 본 문서 §3, §5 갱신 + +--- + +## 15. 안티패턴 + +- ❌ 프롬프트를 코드 안 곳곳에 산재시키기 → `chain/prompts/`로 모은다 +- ❌ LLM 응답을 파싱 없이 그대로 사용 → 항상 Pydantic schema validation +- ❌ 동기 라이브러리(`requests`, `pika`) 사용 → async 라이브러리(`httpx`, `aio-pika`) +- ❌ 트랜잭션이 필요한 작업 (PG 직접 접근) → Core API 호출 +- ❌ messageId 멱등 체크 누락 → 중복 처리 위험 +- ❌ 프롬프트에 사용자 답변을 그대로 system message로 → injection 가능. 반드시 user message로. + +--- + +## 16. 현재 상태 (2026-04 기준) + +- FastAPI 부트스트랩 + 헬스체크만 구현 +- RabbitMQ consumer 미구현 → US-09(이력서 분석) 작업 시 도입 +- LangChain 기본 import만 있음, 체인·프롬프트 정의 0 +- pgvector 연동 미구현 +- 음성 모듈은 Phase 2에서 본격 작성 + +각 도입 시 본 문서 갱신. diff --git a/ai/src/ai_server/CLAUDE.md b/ai/src/ai_server/CLAUDE.md new file mode 100644 index 00000000..0e71d3b4 --- /dev/null +++ b/ai/src/ai_server/CLAUDE.md @@ -0,0 +1,197 @@ +# `ai_server/` — 패키지 가이드 + +> AI 서버의 모든 코드는 이 패키지 안에 있다. 모듈 간 의존성과 신규 기능 추가 절차를 정의. + +상위: [`/ai/CLAUDE.md`](../../CLAUDE.md) + +--- + +## 1. 모듈 의존성 그래프 + +``` +api ──┐ + ├──→ analyzer ──→ chain ──→ (LLM) +messaging │ + └──→ voice ──→ │ + │ │ + └──→ rag ────┴──→ storage (S3) + └──→ httpx (Core API) + +config: 모두가 의존 +model: 모두가 의존 (Pydantic 스키마) +``` + +원칙: +- `model`, `config`는 어디서든 import 가능 (가장 아래) +- `api`, `messaging`은 진입점 — 비즈니스 로직 import만, 비즈니스 로직이 이들을 import 금지 +- 비즈니스 모듈(`analyzer`, `chain`, `rag`, `voice`)은 서로 호출 가능, 단 `chain`은 가장 아래 + +--- + +## 2. 모듈별 가이드 + +### `config/` +환경 설정만. 다른 모듈을 import하지 않는다. + +```python +# config/settings.py +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + # 모든 환경변수 정의 + ... + +settings = Settings() # singleton +``` + +### `model/` +- RabbitMQ envelope 모델 +- 도메인 객체 (`AnalyzedResume`, `QuestionPool`, `FollowUpResult`) +- LLM 응답 schema (Pydantic) — `OutputParser`에 사용 + +```python +# model/messages.py +class ResumeAnalyzeRequest(BaseModel): + resume_id: int + s3_key: str + +class ResumeAnalyzed(BaseModel): + resume_id: int + summary: str + tech_stack: list[str] + document_s3_key: str + embedding_chunk_count: int +``` + +### `api/` +- FastAPI router 정의 +- 헬스체크 + 내부 디버그 endpoint만 +- 외부 인증 X (Core가 처리), 내부 망에서만 호출됨 + +### `messaging/` +- aio-pika consumer / publisher +- 큐별 consumer 함수 분리 +- 모든 consumer는 envelope parsing → trace_context → 비즈니스 핸들러 호출 패턴 + +```python +# messaging/resume_consumer.py +async def consume(message: AbstractIncomingMessage) -> None: + async with message.process(requeue=False): + envelope = parse_envelope(message) + if await is_processed(envelope.message_id): + return # 멱등 + with trace_context(envelope.trace_id): + await resume_analyzer.handle(envelope.payload) + await mark_processed(envelope.message_id) +``` + +### `analyzer/` +- use case 단위 (`resume_analyzer.py`, `repo_analyzer.py`, `feedback_generator.py`) +- 외부 입력 → 내부 모듈 조합 → 결과 publish +- LLM 호출 자체는 `chain/`으로 위임 + +### `chain/` +- LangChain 체인 정의 +- `chain/prompts/` 하위에 prompt 템플릿 (모든 프롬프트가 한 곳에) +- `chain/parsers/` 출력 파서 + +### `rag/` +- 청킹 (`splitter.py`) +- 임베딩 생성 (`embedder.py`) +- 검색 어댑터 (Core API client `pgvector_client.py`) + +### `voice/` (Phase 2) +- `voice/stt/` — interface + provider impls +- `voice/tts/` +- `voice/analysis/` — WPM, filler, silence + +### `storage/` +- S3 client wrapper (`s3.py`) +- key 생성 헬퍼 (`keys.py`) — [`/docs/storage.md §2`](../../../docs/storage.md) 컨벤션 준수 + +--- + +## 3. 진입점 + +### REST (FastAPI) +- `api/health.py` — 헬스체크 +- `api/internal/*` — Core가 호출할 수 있는 동기 endpoint (필요 시) + +### MQ Consumer +- `messaging/runner.py` (도입 예정) — 모든 consumer를 시작하는 entry +- `main.py` lifespan에서 자동 시작 (또는 별도 프로세스로 분리 검토) + +```python +# main.py 의 lifespan +@asynccontextmanager +async def lifespan(app: FastAPI): + connection = await connect_robust(settings.rabbitmq_url) + channel = await connection.channel() + await start_resume_consumer(channel) + await start_repo_consumer(channel) + await start_session_consumer(channel) + yield + await connection.close() +``` + +--- + +## 4. 비동기 (async) 가이드 + +- 모든 IO는 async (`httpx.AsyncClient`, `aio-pika`, `aiobotocore`) +- 동기 라이브러리가 필요하면 `run_in_executor`로 wrap +- LangChain은 `chain.ainvoke(...)` 사용 (async 메서드) +- 한 consumer 안에서 여러 LLM 호출이 필요하면 `asyncio.gather` + +--- + +## 5. 에러 분류 + +| 분류 | 처리 | +|------|------| +| 일시 장애 (네트워크, 5xx, rate limit) | retry (envelope `x-attempt` 증가) → 재시도 한도 초과 시 DLQ | +| 영구 실패 (PDF 손상, 입력 검증 실패) | 즉시 `ai.callback.*.failed` 발행 + `retriable: false` | +| 코드 버그 (parsing 오류 등) | 예외 raise → consumer가 catch → DLQ + 알림 | + +```python +class AnalysisError(Exception): + def __init__(self, code: str, message: str, retriable: bool): + ... +``` + +--- + +## 6. 신규 기능 패턴 + +이력서 분석 (US-09)을 예로 들면: + +1. `model/messages.py`에 `ResumeAnalyzeRequest`, `ResumeAnalyzed`, `ResumeFailed` 정의 +2. `messaging/resume_consumer.py` 구현 (envelope parse → handler 호출) +3. `analyzer/resume_analyzer.py` 구현 + ```python + async def handle(req: ResumeAnalyzeRequest) -> None: + pdf_bytes = await s3.get(req.s3_key) + text = extract_text(pdf_bytes) + result = await resume_chain.ainvoke({"text": text}) + md_key = f"analyzed/resume/{req.resume_id}/summary.md" + await s3.put(md_key, result.markdown) + chunks = split(result.markdown) + embeddings = await embedder.embed(chunks) + await pgvector_client.upsert(req.resume_id, chunks, embeddings) + await publisher.publish_callback(ResumeAnalyzed(...)) + ``` +4. `chain/resume_analyzer_chain.py` (prompt + LLM + parser) +5. 단위 테스트 (mock LLM) +6. 통합 테스트 (Testcontainer RabbitMQ + MinIO) +7. main.py lifespan에 consumer 등록 + +--- + +## 7. 안티패턴 + +- ❌ `time.sleep()` (async 코드에서) → `await asyncio.sleep()` +- ❌ Settings를 함수 인자로 매번 전달 → singleton 사용 OR DI 패턴 +- ❌ 프롬프트 인라인 작성 → `chain/prompts/`로 +- ❌ LLM 응답 `.json.loads()` → Pydantic parser 사용 +- ❌ Consumer에서 무한 retry → 한도 + DLQ +- ❌ 한 모듈에 수백 줄 함수 → 책임 분리 diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 00000000..8631dc1f --- /dev/null +++ b/backend/CLAUDE.md @@ -0,0 +1,284 @@ +# Backend (Core Server) — Claude 컨텍스트 + +> StackUp Core Server. **Java 21 + Spring Boot 4.0 + JPA + QueryDSL + PostgreSQL**. 시스템에서 PostgreSQL에 직접 접근하는 **유일한 컴포넌트**. + +상위 컨텍스트: [`/CLAUDE.md`](../CLAUDE.md) · 횡단 관심사: [`/docs/`](../docs/README.md) + +--- + +## 1. 기술 스택 + +| 영역 | 기술 | +|------|------| +| Language | Java 21 (toolchain) | +| Framework | Spring Boot 4.0.4 | +| Web | Spring Web (REST) | +| ORM | Spring Data JPA + Hibernate | +| Query | QueryDSL 5.1 (jakarta classifier) | +| DB | PostgreSQL + pgvector | +| Build | Gradle (Groovy DSL) | +| Test | JUnit 5 + Spring Boot Test | +| 추가 예정 | Spring Security, Spring AI, RabbitMQ starter, Redis, Flyway, springdoc-openapi | + +> 신규 의존성 추가 시 [`/docs/coding-conventions.md §6`](../docs/coding-conventions.md) 절차 + `build.gradle` 갱신. + +--- + +## 2. 패키지 구조 (도메인 우선) + +``` +com.stackup.stackup +├── StackupApplication.java +├── auth/ # 인증 (GitHub OAuth, JWT) +│ └── domain/ +├── user/ # 사용자 +│ └── domain/ +│ └── consent/ # 개인정보처리동의 +├── github/ # GitHub API 연동·레포 메타 +│ └── domain/ +├── resume/ # 이력서 +│ └── domain/ +├── document/ # 분석 문서 (analyzed_documents) +│ └── domain/ +├── session/ # 면접 세션·메시지·피드백 +│ └── domain/ +├── log/ # 로깅 도메인 +│ ├── activity/ +│ │ └── domain/ +│ └── ai/ +│ └── domain/ +└── common/ # 횡단 (Base entity, exceptions, util) + └── entity/ +``` + +> Spring Boot 표준 (`config/`, `controller/`, `service/`, `repository/`, `dto/`)이 아니라 **도메인 패키지 우선** 구조. 각 도메인 내부에서 layered 분리한다 (§3). + +--- + +## 3. 도메인 내부 구조 (각 패키지의 표준) + +``` +com.stackup.stackup.{domain}/ +├── domain/ # Entity, Enum, Value Object, Repository (interface) +│ ├── {Aggregate}.java +│ ├── {Aggregate}Repository.java +│ └── ... +├── application/ # Service, UseCase, DTO (도입 예정) +│ ├── {Aggregate}Service.java +│ └── dto/ +├── presentation/ # Controller, Request/Response (도입 예정) +│ └── {Aggregate}Controller.java +└── infrastructure/ # 외부 연동 (GitHub API client, S3, RabbitMQ pub/sub) + └── ... +``` + +- 현재는 `domain/` 하위 패키지만 존재. 기능 구현 시 `application/`, `presentation/`, `infrastructure/` 차례로 추가. +- 패키지명을 entity 명사로 (소문자), 클래스는 PascalCase. + +--- + +## 4. 도메인 인벤토리 + +| 패키지 | 책임 | 관련 US | +|--------|------|---------| +| `auth` | GitHub OAuth flow, JWT 발급/갱신/검증, refresh token, JWT 필터 | US-01 | +| `user` | 사용자 CRUD, 회원 탈퇴, 프로필 조회 | US-02, US-04 | +| `user.consent` | 개인정보처리동의 기록·조회·철회 | US-03 | +| `github` | GitHub API 연동, 레포 목록/등록/메타 동기화 | US-07, US-08 | +| `resume` | 이력서 업로드(S3)·메타 저장·목록·삭제 | US-05, US-06 | +| `document` | 분석 문서(이력서/레포 공통) 메타 + S3 경로 | US-09~12 | +| `session` | 면접 세션·메시지·피드백 (가장 큰 도메인) | US-13~20, US-24~27 | +| `log.activity` | 사용자 행동 로그 | US-31 | +| `log.ai` | AI 요청/응답 로깅 | US-30 | +| `common` | BaseEntity, 글로벌 예외 핸들러, util | — | + +각 도메인 패키지에 자체 `CLAUDE.md`를 두는 것을 권장 (현재 미생성, 도메인 코드 작성 시 함께 추가). + +--- + +## 5. 핵심 설계 원칙 + +### 5.1 PostgreSQL 단독 접근 +다른 서비스(AI/RealTime)는 PG 직접 접근 금지. 본 서버 API 또는 RabbitMQ 경유. 자세한 이유는 [`/docs/architecture.md §4.1`](../docs/architecture.md). + +### 5.2 도메인 패키지 우선 +횡단 기술(controller/service/repository) 분리 대신 **도메인 단위 응집**. 도메인 내부에서만 layered 분리. + +### 5.3 비동기 작업 발행만, AI 추론은 AI 서버 위임 +LLM 직접 호출 X. 항상 RabbitMQ로 작업 발행 + 콜백 수신. + +### 5.4 트랜잭션 경계 +- `@Transactional`은 service layer에서만 (controller/repository에서 사용 X) +- 외부 API 호출은 트랜잭션 밖에서 (DB 락 길어짐 방지) +- 메시지 발행은 commit 이후 (transaction outbox 패턴 또는 `TransactionalEventListener(AFTER_COMMIT)`) + +--- + +## 6. JPA / QueryDSL 가이드 + +- 단순 CRUD → `JpaRepository` 메서드 (`findById`, `findByUserIdAndIsDeletedFalse`) +- 동적 조건/조인 → QueryDSL custom repository +- N+1 방지: `@EntityGraph` 또는 fetch join, 측정 후 적용 +- `@OneToMany` cascade는 신중 (의도 없는 삭제 방지) +- 비식별자 ENUM은 `@Enumerated(EnumType.STRING)` 강제, ORDINAL 금지 + +QueryDSL Q-class 생성 위치: `build/generated/sources/annotationProcessor/...` (자동, 커밋 X). + +--- + +## 7. DTO 컨벤션 + +- 입력: `XxxRequest` (record 권장) +- 출력: `XxxResponse` (record 권장) +- Entity는 controller까지 노출 X — service에서 DTO 변환 +- `@Valid` + `@NotBlank` 등 validation은 Request DTO에 + +```java +public record SessionCreateRequest( + String title, + @NotNull SessionMode mode, + @NotNull InterviewType interviewType, + @NotNull JobCategory jobCategory, + @Min(1) @Max(30) Integer maxQuestions, + @Min(5) @Max(180) Integer maxDurationMinutes, + List contextDocumentIds +) {} +``` + +--- + +## 8. 예외 처리 + +### 8.1 도메인 예외 +```java +public class SessionNotInProgressException extends DomainException { + public SessionNotInProgressException(Long sessionId) { + super(ApiErrorCode.SESSION_INVALID_STATE, + "세션이 진행 중이 아닙니다. (id=%d)".formatted(sessionId)); + } +} +``` + +### 8.2 글로벌 핸들러 +`common/exception/GlobalExceptionHandler.java` 에서 `@RestControllerAdvice`로: +- `DomainException` → 4xx + 표준 에러 응답 ([`/docs/api-conventions.md §4.2`](../docs/api-conventions.md)) +- `MethodArgumentNotValidException` → 400 + details에 field 목록 +- 그 외 `Exception` → 500 + traceId 노출 (사용자에게 메시지 노출은 generic하게) + +--- + +## 9. RabbitMQ 발행/소비 + +- 발행자: 각 도메인의 `infrastructure/` (예: `session/infrastructure/SessionEventPublisher.java`) +- 소비자: `*/infrastructure/{X}MessageHandler.java` +- 메시지 envelope·routing key·재시도 정책: [`/docs/messaging.md`](../docs/messaging.md) + +--- + +## 10. S3 / MinIO + +- `common/storage/ObjectStorageClient.java` 단일 추상화 +- AWS SDK v2 사용, endpoint를 환경변수로 분기 (local: MinIO, prod: AWS S3) +- 키 컨벤션: [`/docs/storage.md §2`](../docs/storage.md) +- bucket은 환경변수, key만 DB 저장 + +--- + +## 11. GitHub API + +- `github/infrastructure/GithubApiClient.java` +- WebClient 기반, `Authorization: Bearer {github_access_token}` +- 토큰은 `GithubTokenCipher`로 복호화한 평문을 메모리에서만 사용 +- rate-limit 응답(403 + remaining=0) 처리: 429로 변환 + retry-after 응답 + +--- + +## 12. Flyway 마이그레이션 + +- `src/main/resources/db/migration/V{n}__{snake_case}.sql` +- 적용 후 수정 절대 금지 (수정 시 새 V 추가) +- DDL과 DML 분리 +- 상세: [`/docs/database.md §8`](../docs/database.md) + +--- + +## 13. 환경 변수 + +`application.properties` + `application-{profile}.properties` + 환경변수. + +```properties +spring.application.name=stackup +spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:stackup} +spring.datasource.username=${POSTGRES_USER:stackup} +spring.datasource.password=${POSTGRES_PASSWORD:stackup} +spring.jpa.hibernate.ddl-auto=validate # Flyway 사용 → validate +``` + +전체 변수 목록: [`/docs/environment.md §3`](../docs/environment.md). + +--- + +## 14. 로깅 + +- Logback JSON 포맷 (운영) / human-readable (로컬) +- MDC에 `traceId`, `userId` +- 민감정보 마스킹: `common/log/PiiMasker.java` +- 자세한 정책: [`/docs/observability.md`](../docs/observability.md) + +--- + +## 15. 테스트 + +- 단위: `*Test.java` (Spring 컨텍스트 X, 빠름) +- 통합: `*IT.java` 또는 `*IntegrationTest.java` + Testcontainers (PG/RabbitMQ) +- Builder 패턴으로 fixture (`UserBuilder.aUser()`) +- 자세한 전략: [`/docs/testing-strategy.md`](../docs/testing-strategy.md) + +--- + +## 16. 빌드·실행 + +```bash +./gradlew bootRun # 개발 실행 +./gradlew test # 단위·통합 테스트 +./gradlew build # JAR 생성 +./gradlew dependencyCheckAnalyze # 취약점 스캔 (도입 후) +java -jar build/libs/stackup-0.0.1-SNAPSHOT.jar +``` + +로컬 의존성(PG/RabbitMQ/MinIO): +```bash +docker compose up -d +``` + +--- + +## 17. 코드 스타일 + +- Lombok 적극 사용 OK (`@Getter`, `@RequiredArgsConstructor`, `@Builder`). 단 entity는 `@Setter` 금지. +- record는 DTO/응답에 우선 사용 +- final 필드 + 생성자 주입 +- 상수는 도메인 클래스 내부 `private static final` +- 자세한 공통 규약: [`/docs/coding-conventions.md`](../docs/coding-conventions.md) + +--- + +## 18. API 노출 + +- springdoc-openapi (도입 시): + - `/api/v3/api-docs` — OpenAPI JSON + - `/api/swagger-ui.html` — UI +- 신규 endpoint는 `@Operation`, `@ApiResponse` 작성 +- API 규약: [`/docs/api-conventions.md`](../docs/api-conventions.md) + +--- + +## 19. 현재 상태 (2026-04 기준) + +- 도메인 패키지 골격만 존재, 실제 구현 거의 없음 +- Spring Security 미도입 → US-01 작업 시 도입 +- RabbitMQ starter 미도입 → US-09 작업 시 도입 +- Flyway 미도입 → 첫 entity 작성 PR에서 도입 +- Spring AI 추가 검토 (벡터 검색을 Core가 제공한다면) + +각 도입 시 본 문서 §1, 관련 도메인 `CLAUDE.md` 갱신. diff --git a/backend/src/main/java/com/stackup/stackup/CLAUDE.md b/backend/src/main/java/com/stackup/stackup/CLAUDE.md new file mode 100644 index 00000000..0a3746ad --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/CLAUDE.md @@ -0,0 +1,250 @@ +# `com.stackup.stackup` — 도메인 패키지 가이드 + +> 모든 도메인 패키지가 따라야 할 공통 규칙. 새 도메인 추가 시 본 문서를 기준으로 구조를 잡는다. + +상위: [`/backend/CLAUDE.md`](../../../../../CLAUDE.md) + +--- + +## 1. 도메인 패키지 표준 구조 + +``` +{domain}/ +├── domain/ # 핵심: Entity, Enum, VO, Repository(interface) +│ ├── {Aggregate}.java +│ ├── {AggregateRepository}.java # interface (Spring Data JPA) +│ ├── {AggregateRepositoryCustom}.java + Impl # QueryDSL 커스텀 +│ ├── {EnumOrVO}.java +│ └── exception/ +│ └── {Aggregate}Exception.java +├── application/ # 비즈니스 로직, 트랜잭션 경계 +│ ├── {Aggregate}Service.java +│ ├── {Aggregate}Facade.java # 여러 도메인 조립 시 +│ ├── dto/ +│ │ ├── {Aggregate}CreateCommand.java +│ │ └── {Aggregate}Result.java +│ └── event/ +│ └── {AggregateCreated}Event.java +├── presentation/ # HTTP I/O +│ ├── {Aggregate}Controller.java +│ └── dto/ +│ ├── {Aggregate}CreateRequest.java +│ └── {Aggregate}Response.java +└── infrastructure/ # 외부 연동 + ├── {Aggregate}EventPublisher.java + ├── {Aggregate}MessageHandler.java + └── {External}Client.java +``` + +> 단순 도메인은 `application/dto`, `event/`, `infrastructure/` 가 비어 있을 수 있다 — 만들 때까지 패키지 미생성 OK. + +--- + +## 2. 의존성 방향 + +``` +presentation → application → domain + ↓ + infrastructure + ↓ + (외부 — DB, RabbitMQ, S3, GitHub) +``` + +- presentation은 application까지만 의존 +- application은 domain만 의존 (인터페이스 통해 infrastructure 사용) +- domain은 다른 도메인의 entity 직접 참조 금지 — `id`만 보유, lookup은 service에서 + +--- + +## 3. Entity 규칙 + +### 3.1 BaseEntity 상속 +```java +// common/entity/BaseEntity.java +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + @CreatedDate + @Column(nullable = false, updatable = false) + private Instant createdAt; + + @LastModifiedDate + @Column(nullable = false) + private Instant updatedAt; +} +``` + +`is_deleted`가 있는 도메인은 `SoftDeletableEntity` 별도 추상 클래스 사용 검토. + +### 3.2 Setter 금지 +- `@Setter` 클래스 레벨 금지 +- 변경은 의미 있는 도메인 메서드로: + ```java + public void start() { + if (status != READY) throw new SessionNotReadyException(id); + this.status = IN_PROGRESS; + this.startedAt = Instant.now(); + } + ``` + +### 3.3 Enum 매핑 +```java +@Enumerated(EnumType.STRING) +@Column(nullable = false, length = 20) +private SessionStatus status; +``` + +ORDINAL 절대 금지. DDL의 CHECK 제약 + Enum 1:1. + +### 3.4 ID +```java +@Id +@GeneratedValue(strategy = GenerationType.IDENTITY) +private Long id; +``` + +PG는 IDENTITY 사용 (BIGSERIAL이 IDENTITY와 호환). + +### 3.5 연관관계 +- `@ManyToOne` 기본 LAZY (`fetch = FetchType.LAZY`) +- `@OneToMany` cascade 신중 — 의도 없는 삭제 방지 +- 양방향 매핑은 정말 필요할 때만 (보통 단방향으로 충분) + +--- + +## 4. Repository 규칙 + +### 4.1 인터페이스만 노출 +```java +public interface ResumeRepository extends JpaRepository, ResumeRepositoryCustom { + Optional findByIdAndIsDeletedFalse(Long id); + List findByUserIdAndIsDeletedFalseOrderByCreatedAtDesc(Long userId); +} +``` + +### 4.2 QueryDSL 커스텀 +```java +public interface ResumeRepositoryCustom { + Page search(ResumeSearchCondition cond, Pageable pageable); +} + +@RequiredArgsConstructor +public class ResumeRepositoryImpl implements ResumeRepositoryCustom { + private final JPAQueryFactory queryFactory; + @Override + public Page search(...) { + // queryFactory.selectFrom(QResume.resume)... + } +} +``` + +### 4.3 Native Query +- `pgvector` 검색 등 ORM이 표현 어려운 경우만 +- 항상 named parameter (`:userId`), 문자열 concat 금지 + +--- + +## 5. Service 규칙 + +### 5.1 트랜잭션 +```java +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) // 클래스 레벨 readOnly +public class ResumeService { + private final ResumeRepository repo; + + @Transactional // 쓰기 메서드만 명시 + public Resume upload(Long userId, ResumeUploadCommand cmd) { ... } +} +``` + +### 5.2 외부 호출은 트랜잭션 밖 +```java +public Resume upload(...) { + String s3Key = storage.put(file); // 트랜잭션 시작 전 + return txTemplate.execute(s -> repo.save(...)); +} +``` +또는 트랜잭션 분리 + outbox 패턴. + +### 5.3 권한 체크 +- service 메서드 진입 시 owner 검증 +- `@PreAuthorize` 또는 명시적 코드 + +--- + +## 6. Controller 규칙 + +```java +@RestController +@RequestMapping("/api/resumes") +@RequiredArgsConstructor +public class ResumeController { + private final ResumeService service; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ResumeResponse upload( + @AuthenticationPrincipal UserPrincipal principal, + @Valid @RequestPart ResumeUploadRequest meta, + @RequestPart MultipartFile file + ) { + return ResumeResponse.from(service.upload(principal.userId(), meta.toCommand(file))); + } +} +``` + +원칙: +- DTO ↔ Command/Result 변환은 controller에서 +- 인증 정보는 `@AuthenticationPrincipal`로 주입 +- HTTP 상태 코드 명시 (`@ResponseStatus`) + +--- + +## 7. 이벤트 + +도메인 이벤트 발행 → application layer에서 publish: +```java +@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) +public void on(ResumeUploadedEvent event) { + rabbitPublisher.publish("ai.request.resume.analyze", event.toMessage()); +} +``` + +`AFTER_COMMIT` 으로 DB commit 이후 발행 (메시지는 발행됐는데 DB는 롤백되는 케이스 방지). + +--- + +## 8. 테스트 + +| 종류 | 위치 | 도구 | +|------|------|------| +| 도메인 단위 | `*Test.java` | JUnit + AssertJ (Spring 컨텍스트 X) | +| Repository | `*RepositoryTest.java` | `@DataJpaTest` + Testcontainer PG | +| Service | `*ServiceTest.java` | Mockito 단위 OR Testcontainer 통합 | +| Controller | `*ControllerTest.java` | `@WebMvcTest` 또는 MockMvc + 통합 | +| End-to-end | `*IT.java` | `@SpringBootTest` + Testcontainer 다중 | + +--- + +## 9. 새 도메인 추가 절차 + +1. 패키지 생성: `com.stackup.stackup.{name}/` +2. `domain/{Aggregate}.java` + Repository +3. Flyway 마이그레이션 작성 (`db/migration/V{n}__add_{name}.sql`) +4. `application/{Aggregate}Service.java` +5. `presentation/{Aggregate}Controller.java` +6. 본 패키지에 `CLAUDE.md` 작성 (선택, 복잡한 도메인은 권장) +7. `/backend/CLAUDE.md §4` 인벤토리 갱신 + +--- + +## 10. 안티패턴 + +- ❌ Entity setter 노출 +- ❌ Controller에서 `@Transactional` +- ❌ Service가 다른 service의 Repository 직접 호출 (다른 service의 public 메서드만 사용) +- ❌ DTO를 도메인 패키지에 두기 +- ❌ 한 controller가 여러 도메인 처리 (각 도메인의 controller로 분리) +- ❌ 패키지 이름에 layered (`xxx.controller`, `xxx.service`) — 도메인 우선 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..d390a4ac --- /dev/null +++ b/docs/README.md @@ -0,0 +1,69 @@ +# StackUp 문서 인덱스 + +> 이 디렉토리는 **레이어 비종속(cross-cutting) 문서**를 보관한다. 각 레이어 내부 규약은 해당 레이어의 `CLAUDE.md`를 참고한다. + +--- + +## 1. 산출물 (학교 제출용) + +| 파일 | 설명 | +|------|------| +| `1조(StackUp)UserStory (1).pdf` | User Story 60p 상세 (US-01 ~ US-31) | +| `1조(StackUp)Product Backlog (1).pdf` | Product Backlog (Epic, US, Acceptance, SP) | + +> PDF는 **버전 픽스용 산출물**이므로 직접 수정하지 않는다. 변경 사항은 아래 `.md` 문서에 반영하고 PDF 재출력 시점에 동기화한다. + +--- + +## 2. 횡단 관심사 문서 + +### 제품·아키텍처 +- [`product-overview.md`](./product-overview.md) — 제품 비전, 페르소나, 핵심 차별점 +- [`architecture.md`](./architecture.md) — 시스템 아키텍처, 컴포넌트 책임 분담 +- [`data-flow.md`](./data-flow.md) — 핵심 시나리오별 데이터 흐름 + +### 데이터·계약 +- [`database.md`](./database.md) — DDL, ENUM, 인덱싱, Flyway 정책 +- [`api-conventions.md`](./api-conventions.md) — REST API 설계 규약, 에러 코드, 페이지네이션 +- [`messaging.md`](./messaging.md) — RabbitMQ 큐/익스체인지, 메시지 스키마, 재시도 정책 +- [`storage.md`](./storage.md) — S3(MinIO) 키 컨벤션, 버킷 설계 +- [`event-stream.md`](./event-stream.md) — SSE 이벤트 스펙 + +### 디자인·프론트엔드 +- [`design-system.md`](./design-system.md) — 토큰, 컬러, 타이포그래피, 컴포넌트 인벤토리 +- [`ui-patterns.md`](./ui-patterns.md) — 반복되는 UX 패턴, 상태 처리 (loading/empty/error) + +### 보안·운영 +- [`security.md`](./security.md) — 인증·인가, 토큰 암호화, 개인정보 처리 +- [`observability.md`](./observability.md) — X-Trace-Id, 로깅 레벨, AI 요청 로깅 +- [`environment.md`](./environment.md) — 환경 변수, 로컬/스테이징/운영 분리 + +### 협업 +- [`coding-conventions.md`](./coding-conventions.md) — 언어별 공통 코딩 규약 +- [`git-conventions.md`](./git-conventions.md) — 브랜치 전략, 커밋 컨벤션, PR 템플릿 +- [`testing-strategy.md`](./testing-strategy.md) — 테스트 피라미드, 핵심 시나리오 정의 +- [`glossary.md`](./glossary.md) — 도메인 용어집 (한/영 대응) + +--- + +## 3. 레이어별 진입점 (CLAUDE.md) + +| 레이어 | 진입 문서 | 역할 | +|--------|-----------|------| +| 루트 | `/CLAUDE.md` | 전체 오버뷰 + 인덱스 | +| 프론트 | `/frontend/CLAUDE.md` | React/FSD 구조 | +| 백엔드 | `/backend/CLAUDE.md` | Spring Boot Core 서버 | +| AI | `/ai/CLAUDE.md` | FastAPI/LangChain 서버 | +| 인프라 | `/infra/CLAUDE.md` | Docker Compose, PG/RabbitMQ/MinIO | + +> 각 레이어 하위 디렉토리에도 슬라이스/도메인별 `CLAUDE.md`가 존재할 수 있다. 작업할 디렉토리에서 가장 가까운 `CLAUDE.md`를 우선 참고한다. + +--- + +## 4. 문서 작성 원칙 + +1. **단일 출처 원칙(Single Source of Truth)**: 같은 정보를 두 곳에 쓰지 않는다. 다른 문서를 링크로 참조한다. +2. **레이어 종속성 방향**: 횡단 관심사 → 레이어 → 슬라이스. 상위 문서는 하위를 모르고, 하위는 상위를 참조한다. +3. **변경 시 함께 갱신**: 코드 변경이 컨벤션·아키텍처·DB 스키마에 영향을 주면 해당 문서도 같은 PR에 포함한다. +4. **예시 우선**: 추상적 설명보다 실제 코드 스니펫·DDL·요청 예시로 보여준다. +5. **한국어 우선**: 도메인 용어는 한국어로, 기술 용어·식별자(클래스명·테이블명·ENUM)는 영문 그대로 둔다. diff --git a/docs/api-conventions.md b/docs/api-conventions.md new file mode 100644 index 00000000..fa1f0f3a --- /dev/null +++ b/docs/api-conventions.md @@ -0,0 +1,303 @@ +# REST API 설계 규약 + +> 모든 Core Server REST API는 본 문서의 규약을 따른다. OpenAPI 스펙은 springdoc-openapi가 자동 생성하고, 프론트는 `openapi-typescript`로 타입을 생성한다. + +--- + +## 1. URL 구조 + +``` +/api/{resource} # 컬렉션 +/api/{resource}/{id} # 단일 리소스 +/api/{resource}/{id}/{sub} # 서브 리소스 +/api/{resource}/{id}:{action} # 액션 (PATCH 대신 명시적 동사) +``` + +- 리소스명은 **복수형, kebab-case 금지, snake_case 금지** → `repositories`, `interview-sessions` 대신 `sessions` (짧고 명확하게) +- 액션이 RESTful 동사로 표현 안 될 때만 `:start`, `:end` 형식 사용 + +### 표준 매핑 + +| 동작 | 메서드 | 경로 예시 | +|------|--------|-----------| +| 컬렉션 조회 | GET | `/api/sessions` | +| 단건 조회 | GET | `/api/sessions/{id}` | +| 생성 | POST | `/api/sessions` | +| 부분 수정 | PATCH | `/api/sessions/{id}` | +| 전체 교체 | PUT | (거의 사용하지 않음) | +| 삭제 (soft) | DELETE | `/api/sessions/{id}` | +| 액션 트리거 | PATCH | `/api/sessions/{id}/start`, `/api/sessions/{id}/end` | + +--- + +## 2. 주요 API 그룹 + +### 2.1 인증 +``` +POST /api/auth/github GitHub OAuth 시작 (URL 발급) +GET /api/auth/github/callback OAuth 콜백 처리 +POST /api/auth/refresh 토큰 갱신 +DELETE /api/auth/logout 로그아웃 (refresh token revoke) +``` + +### 2.2 사용자 +``` +GET /api/users/me 내 정보 +PATCH /api/users/me 내 정보 수정 (제한적) +DELETE /api/users/me 회원 탈퇴 (soft) +GET /api/users/me/stats 내 통계 (점수 추이) +GET /api/users/me/consents 동의 이력 +POST /api/users/me/consents 동의 제출 +``` + +### 2.3 자료 +``` +GET /api/repositories 내가 등록한 레포 목록 +GET /api/repositories/github GitHub의 내 전체 레포 (등록 가능 후보) +POST /api/repositories 등록 +GET /api/repositories/{id} 상세 (분석 상태 포함) +DELETE /api/repositories/{id} 소프트 삭제 +POST /api/repositories/{id}/reanalyze 재분석 요청 + +POST /api/resumes 업로드 +GET /api/resumes 목록 +GET /api/resumes/{id} 상세 +DELETE /api/resumes/{id} 소프트 삭제 +POST /api/resumes/{id}/reanalyze 재분석 + +GET /api/documents 분석 문서 목록 +GET /api/documents/{id} 분석 문서 상세 (S3 URL 포함) +``` + +### 2.4 면접 세션 +``` +POST /api/sessions 생성 +GET /api/sessions 히스토리 목록 +GET /api/sessions/{id} 상세 (메시지/피드백 포함) +PATCH /api/sessions/{id} 메모/제목 수정 +PATCH /api/sessions/{id}/start 시작 (READY → IN_PROGRESS) +PATCH /api/sessions/{id}/end 종료 +DELETE /api/sessions/{id} 소프트 삭제 + +GET /api/sessions/{id}/messages 메시지 목록 +POST /api/sessions/{id}/messages 메시지 추가 (RealTime → Core 전용) + +GET /api/sessions/{id}/feedback 피드백 조회 +``` + +### 2.5 시스템 +``` +GET /api/system/health 헬스체크 (PG/Redis/MQ 상태) +GET /api/system/version 버전 정보 +``` + +--- + +## 3. 요청 규약 + +### 3.1 헤더 + +| 헤더 | 필수 | 설명 | +|------|------|------| +| `Authorization: Bearer {token}` | 인증 필요 시 | JWT access token | +| `X-Trace-Id` | 권장 | 클라이언트가 부여하지 않으면 Gateway가 부여 | +| `Content-Type: application/json` | POST/PATCH | multipart/form-data는 파일 업로드 시만 | + +### 3.2 페이지네이션 + +``` +GET /api/sessions?page=0&size=20&sort=createdAt,desc +``` + +- 0-based page +- `size` 최대 100 (초과 시 100으로 clamp) +- 정렬: `{field},{asc|desc}` 형식, 다중 정렬은 `&sort=` 반복 + +### 3.3 필터링 + +``` +GET /api/sessions?status=COMPLETED&jobCategory=BACKEND&from=2026-01-01&to=2026-04-30 +``` + +- 날짜는 ISO 8601 (`YYYY-MM-DD` 또는 RFC 3339) +- 다중 값은 콤마: `?status=COMPLETED,CANCELLED` + +--- + +## 4. 응답 규약 + +### 4.1 성공 (2xx) + +**단건**: +```json +{ + "id": 42, + "title": "백엔드 모의면접 #3", + "status": "COMPLETED", + "createdAt": "2026-04-25T14:30:00Z" +} +``` + +**컬렉션 (페이지)**: +```json +{ + "content": [ {...}, {...} ], + "page": 0, + "size": 20, + "totalElements": 137, + "totalPages": 7, + "first": true, + "last": false +} +``` + +### 4.2 에러 (4xx, 5xx) + +표준 형식: +```json +{ + "code": "RESUME_INVALID_FILE_TYPE", + "message": "PDF 파일만 업로드 가능합니다.", + "traceId": "9f4e5b...", + "timestamp": "2026-04-27T15:20:00Z", + "details": { + "uploadedType": "image/jpeg" + } +} +``` + +| 필드 | 설명 | +|------|------| +| `code` | 도메인 에러 코드 (대문자 SNAKE_CASE) — 클라이언트 분기 기준 | +| `message` | 사용자에게 노출 가능한 한국어 메시지 | +| `traceId` | 로그 추적용, 사용자 문의 시 활용 | +| `details` | 추가 컨텍스트 (validation 실패 필드 등) | + +### 4.3 표준 HTTP 상태 코드 + +| 상황 | 코드 | +|------|------| +| 성공 (조회/수정) | 200 | +| 생성 성공 | 201 (`Location` 헤더 포함) | +| 비동기 처리 시작 | 202 | +| 본문 없음 (DELETE 등) | 204 | +| 잘못된 요청 (validation 실패) | 400 | +| 인증 누락/만료 | 401 | +| 권한 부족 | 403 | +| 리소스 없음 | 404 | +| 충돌 (중복 등록 등) | 409 | +| 처리 가능하지만 의미적 오류 | 422 | +| 서버 오류 | 500 | +| 외부 의존성 장애 (LLM API 등) | 502 / 503 | + +--- + +## 5. 에러 코드 카탈로그 + +> 신규 추가 시 본 카탈로그에 반드시 등록. 같은 코드를 두 도메인에서 사용 금지. + +``` +# 인증 (AUTH_*) +AUTH_INVALID_TOKEN 401 토큰 형식 오류 +AUTH_EXPIRED_TOKEN 401 만료 +AUTH_REVOKED_TOKEN 401 revoke됨 +AUTH_GITHUB_OAUTH_FAILED 401 GitHub OAuth 실패 +AUTH_CONSENT_REQUIRED 403 필수 동의 미완료 + +# 사용자 (USER_*) +USER_NOT_FOUND 404 +USER_ALREADY_DELETED 410 + +# 이력서 (RESUME_*) +RESUME_INVALID_FILE_TYPE 400 PDF 외 업로드 +RESUME_FILE_TOO_LARGE 400 크기 초과 +RESUME_EMPTY_FILE 400 +RESUME_NOT_FOUND 404 +RESUME_IN_USE 409 활성 세션에서 사용 중 + +# 레포 (REPO_*) +REPO_NOT_FOUND 404 +REPO_ALREADY_REGISTERED 409 +REPO_GITHUB_API_FAILED 502 +REPO_PRIVATE_NO_ACCESS 403 + +# 분석 (DOC_*) +DOC_NOT_ANALYZED 422 ANALYZED 상태 아님 +DOC_ANALYSIS_FAILED 500 +DOC_NOT_FOUND 404 + +# 세션 (SESSION_*) +SESSION_INVALID_STATE 422 상태 전이 불가 +SESSION_MAX_REACHED 422 최대 질문/시간 도달 +SESSION_NOT_FOUND 404 +SESSION_FORBIDDEN 403 타인 세션 접근 + +# 시스템 (SYS_*) +SYS_RATE_LIMITED 429 +SYS_DEPENDENCY_DOWN 503 RabbitMQ/AI/LLM 다운 +SYS_INTERNAL_ERROR 500 +``` + +--- + +## 6. 멱등성 (Idempotency) + +POST 요청 중 결제·메시지 발행 등 **중복 처리 위험**이 있는 경우: + +``` +POST /api/sessions +Idempotency-Key: 8b3c5e2a-... +``` + +서버는 24시간 동안 같은 키로 들어온 요청에 대해 동일 응답을 반환. + +> Phase 1에서는 결제 없으므로 필수는 아니나, AI 작업 발행 API(`reanalyze` 등)에 적용 권장. + +--- + +## 7. 버전 관리 + +- 본 문서 기준 v1. 모든 경로에 `/api/v1/...` 접두는 **사용하지 않음** (Breaking Change 발생 시점에 도입). +- Breaking Change 정책: 신규 경로 추가 + 구 경로는 최소 1개 phase 유지 후 제거. + +--- + +## 8. OpenAPI 자동 생성 + +### Backend +- springdoc-openapi `application.properties`: + ``` + springdoc.api-docs.path=/api/v3/api-docs + springdoc.swagger-ui.path=/api/swagger-ui.html + ``` +- `@Operation`, `@Schema` 애노테이션으로 응답 예시 작성 + +### Frontend +- `openapi-typescript` 으로 `frontend/src/shared/api/generated.ts` 생성 +- 빌드 스크립트: + ```bash + npx openapi-typescript http://localhost:8080/api/v3/api-docs -o src/shared/api/generated.ts + ``` +- 변경 시 PR에 포함 + +--- + +## 9. 비동기 작업 응답 패턴 + +POST/PATCH가 즉시 완료되지 않는 경우 (AI 분석 트리거 등): + +```http +HTTP/1.1 202 Accepted +{ + "taskId": "doc-analyze-42", + "status": "QUEUED", + "statusUrl": "/api/documents/42", + "streamUrl": "/api/stream/documents/42" +} +``` + +클라이언트는: +1. SSE (`streamUrl`) 우선 구독 → 상태 푸시 수신 +2. SSE 끊기면 `statusUrl` 폴링 fallback (5초 간격) + +상세: [`event-stream.md`](./event-stream.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..34600e7b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,178 @@ +# 시스템 아키텍처 + +> 컴포넌트 단위 책임 분담과 통신 규약을 정의한다. 각 컴포넌트의 **내부 구조**는 해당 레이어의 `CLAUDE.md`를 참고. + +--- + +## 1. 컴포넌트 다이어그램 + +``` + ┌─────────────────────────┐ + │ Frontend (React) │ + │ CloudFront + S3 배포 │ + └────────┬────────────────┘ + │ + ┌────────▼────────────────┐ + │ Nginx API Gateway │ + └───┬─────────────┬───────┘ + │ │ + ┌─────────────▼──┐ ┌──────▼──────────────┐ + │ Core Server │ │ RealTime Server │ + │ (Spring Boot) │ │ (Go) │ + │ │ │ - WebRTC │ + │ - GitHub OAuth │ │ - WebSocket │ + │ - 회원관리 │ │ - SSE │ + │ - 세션/리포트 │ │ - 세션 실시간 관리 │ + │ - CRUD API │ └──────────────────────┘ + └───────┬───────┘ + │ + ┌───────▼───────┐ + │ RabbitMQ │ ← Core ↔ AI 비동기 통신 + └───────┬───────┘ + │ + ┌───────▼────────────────────┐ + │ AI Server (Python/FastAPI) │ + │ - LangChain RAG │ + │ - 질문 / 꼬리질문 │ + │ - 이력서·레포 분석 │ + │ - 음성 분석 │ + └───┬───────┬───────┬────────┘ + │ │ │ + ┌───────▼┐ ┌───▼────┐ ┌▼──────────┐ + │External │ │Local │ │ VectorDB │ + │LLM APIs │ │LLM │ │ (pgvector)│ + └────────┘ └────────┘ └───────────┘ + + ┌──────────────┐ ┌────────────────┐ + │ PostgreSQL │ │ Object Storage │ + │ + pgvector │ │ (S3 / MinIO) │ + └──────────────┘ └────────────────┘ + + ┌──────────────┐ + │ Redis │ ← 세션 ephemeral state + └──────────────┘ +``` + +--- + +## 2. 컴포넌트 책임 매트릭스 + +| 컴포넌트 | 책임 | 명시적 비책임 | +|----------|------|---------------| +| **Frontend** | UI 렌더링, 사용자 입력, 미디어 스트림 캡처, SSE 구독 | 비즈니스 로직, 인증 토큰 검증 | +| **Nginx** | 라우팅, TLS 종료, X-Trace-Id 부여 | 인증 처리 | +| **Core Server (Spring Boot)** | 인증·인가, CRUD, 트랜잭션, AI 작업 발행/콜백 처리, **PostgreSQL 단독 접근** | AI 추론, 실시간 스트리밍 | +| **RealTime Server (Go)** | WebRTC/WebSocket/SSE, 세션 ephemeral state, 미디어 스트리밍 | 영속 데이터 저장 | +| **RabbitMQ** | Core ↔ AI 비동기 메시지 큐 | RPC 동기 호출 대체 | +| **AI Server (FastAPI)** | LLM 호출, RAG 파이프라인, 임베딩, STT/TTS, 음성 분석 | 사용자 인증, REST CRUD | +| **PostgreSQL** | 영속 관계형 데이터 + 벡터 임베딩 (pgvector) | 대용량 바이너리 | +| **Redis** | 세션 일시 상태, 토큰 블랙리스트, TTL 캐시 | 영속 데이터 | +| **S3 / MinIO** | 이력서 PDF 원본, 분석 마크다운, 음성 오디오 | 메타데이터 (DB가 담당) | + +--- + +## 3. 통신 규약 + +### 3.1 동기 (HTTP REST) + +- Frontend → Nginx → **Core Server** (인증, CRUD) +- 베이스 경로: `/api/*` +- 인증: `Authorization: Bearer ` +- 추적: `X-Trace-Id` 헤더 (Nginx 또는 클라이언트가 부여) + +### 3.2 비동기 (RabbitMQ) + +- **Core → AI**: `ai.request.*` 익스체인지에 발행 +- **AI → Core**: `ai.callback.*` 익스체인지로 결과 회신 +- 메시지 스키마: [`messaging.md`](./messaging.md) 참조 + +### 3.3 실시간 (WebSocket / SSE) + +- **Frontend ↔ RealTime Server**: WebSocket (면접 질문/답변), WebRTC (음성) +- **Frontend ← RealTime Server**: SSE (AI 작업 상태: QUEUED → PROCESSING → COMPLETED/FAILED) +- 이벤트 스펙: [`event-stream.md`](./event-stream.md) 참조 + +### 3.4 외부 + +- **AI Server → Gemini / OpenAI API**: HTTPS, 모델별 레이트 리밋 준수 +- **Core Server → GitHub API**: REST v3 + GraphQL v4 혼용, OAuth access token 사용 + +--- + +## 4. 핵심 설계 원칙 + +### 4.1 PostgreSQL 단일 접근 주체 + +- **Core Server만** PostgreSQL에 직접 접근한다. +- AI Server, RealTime Server는 데이터가 필요하면: + - REST API 호출 (Core 내부 API), 또는 + - RabbitMQ 메시지에 필요한 데이터를 동봉 +- **이유**: 트랜잭션 경계 통일, 스키마 변경 영향 최소화, ORM 매핑 중복 방지. + +### 4.2 LLM 이중 모델 전략 + +| 시점 | 모델 | 이유 | +|------|------|------| +| 세션 시작 시 | **Pro 모델** | 이력서 + GitHub 컨텍스트 기반 질문 풀 생성, 품질 우선 | +| 세션 중 (꼬리질문) | **Flash 모델 + RAG** | 3초 이내 응답, 저지연 우선 | +| 카메라 분석 | **Local LLM (MediaPipe)** | 비용·프라이버시 | + +### 4.3 Hybrid Storage + +| 데이터 종류 | 저장소 | 이유 | +|-------------|--------|------| +| 사용자 / 세션 / 메시지 메타 | PostgreSQL | 관계형 쿼리, 트랜잭션 | +| 벡터 임베딩 | PostgreSQL (pgvector) | DB 단일화 | +| 분석 마크다운 (이력서/레포) | S3 | 대용량 텍스트 | +| 면접 오디오 | S3 | 대용량 바이너리 | +| 이력서 원본 PDF | S3 | 대용량 바이너리 | +| 실시간 세션 상태 | Redis | 빠른 읽기/쓰기, TTL | + +### 4.4 분산 추적 + +- 모든 서버는 `X-Trace-Id`를 수신·전파한다. +- 로그 출력 포맷에 `traceId`를 포함한다. +- RabbitMQ 메시지 헤더에도 동일 trace id를 포함한다. + +상세: [`observability.md`](./observability.md) + +--- + +## 5. 배포 토폴로지 + +| 환경 | 구성 | +|------|------| +| **로컬 개발** | Docker Compose (PG/RabbitMQ/MinIO) + 각 서버 독립 프로세스 | +| **운영** | K8s (Core/AI/RealTime), 관리형 서비스 미사용, RDS 대신 PG 컨테이너 | +| **정적 호스팅** | CloudFront + S3 (Frontend SPA) | + +> Phase 1 시점에는 운영 배포 미정. 로컬 개발 환경 우선. + +--- + +## 6. 컴포넌트 간 의존성 그래프 + +``` +Frontend ──→ Core Server ──→ PostgreSQL + │ │ ↑ + │ ├──→ S3 / MinIO + │ ├──→ Redis + │ ├──→ RabbitMQ ←──→ AI Server ──→ External LLM + │ └──→ GitHub API ──→ pgvector (Core 경유) + │ + └──→ RealTime Server ──→ Redis + └──→ Core Server (REST 내부 호출) +``` + +**의존성 역전 금지 케이스**: +- AI Server → PostgreSQL 직접 연결 ✗ +- RealTime Server → RabbitMQ 발행 ✗ (Core를 통해) +- Frontend → AI Server 직접 호출 ✗ + +--- + +## 7. 추후 확장 시 고려 사항 + +- **AI Worker 수평 확장**: RabbitMQ consumer 다중화, 멱등성 보장 필수 +- **세션 sticky routing**: WebRTC 세션 유지를 위해 Nginx 또는 K8s Ingress 레벨 sticky session +- **읽기 전용 리플리카**: 통계·히스토리 조회 부하가 늘면 PG read replica 도입 검토 diff --git a/docs/coding-conventions.md b/docs/coding-conventions.md new file mode 100644 index 00000000..74009e8c --- /dev/null +++ b/docs/coding-conventions.md @@ -0,0 +1,193 @@ +# 공통 코딩 컨벤션 + +> 언어별 세부 규약은 각 레이어 `CLAUDE.md`에 위임. 본 문서는 **모든 언어 공통** 규약. + +--- + +## 1. 명명 + +### 1.1 식별자 +- **변수/함수**: 의미 있는 영문 단어, 약어 자제 +- **불리언**: `is`, `has`, `can`, `should` 접두 (`isCompleted`, `hasResume`) +- **컬렉션**: 복수형 (`sessions`, `documentIds`) +- **상수**: 언어별 컨벤션 (Java SCREAMING_SNAKE, JS UPPER_SNAKE, Python UPPER_SNAKE) + +### 1.2 도메인 용어 +- 코드에서는 **영어**로 통일 (`session`, `interview`, `repository`) +- DB 컬럼은 snake_case +- 한국어 도메인 용어는 [glossary.md](./glossary.md) 매핑 참조 + +### 1.3 약어 금지 (예외 적은 화이트리스트) +허용: `id`, `url`, `uri`, `api`, `db`, `pg`, `mq`, `ai`, `llm`, `rag`, `stt`, `tts`, `pdf` +비권장: `usr`, `msg`, `cfg`, `tmp` → `user`, `message`, `config`, `temp` 사용 + +--- + +## 2. 함수·메서드 설계 + +- **단일 책임**: 한 함수는 한 가지 일만 +- **순수 함수 우선**: 입력 → 출력, 사이드이펙트 격리 +- **인자 ≤ 4개**: 그 이상은 객체로 묶기 +- **boolean 인자 지양**: 호출부에서 의미 불명. enum 또는 함수 분리. + ```java + // 나쁨 + createSession(true, false); + + // 좋음 + createSession(SessionMode.ONLINE, JobCategory.BACKEND); + ``` + +--- + +## 3. 주석 + +기본은 **주석 없음**. 다음 경우만 주석 작성: + +| 케이스 | 예시 | +|--------|------| +| 비명백한 제약 | `// pgvector ivfflat은 list 수 = sqrt(rows) 권장` | +| 외부 워크어라운드 | `// GitHub API 5xx 시 30초 backoff (rate-limit과 구분)` | +| 의도적 트레이드오프 | `// JPA cascade 사용 안 함: 명시적 삭제 흐름 유지` | +| TODO + 컨텍스트 | `// TODO(US-21): STT 청크 단위 처리` | + +**금지**: +- 코드 그대로 풀어쓴 주석 (`// loop over users`) +- 변경 이력·작성자 (`// 2026-04-27 수정 - 박상우`) → git blame 사용 +- 죽은 코드 보존 (`// 임시로 주석처리`) → 삭제 + +--- + +## 4. 에러 처리 + +### 4.1 예외 vs 에러 코드 +- 비즈니스 규칙 위반: 도메인 예외 (`SessionNotInProgressException`) +- 시스템/외부 장애: 일반 예외 (`RuntimeException`, `IOException`) +- API 응답: 도메인 예외 → API error code 매핑 ([api-conventions.md §5](./api-conventions.md)) + +### 4.2 절대 금지 +- 빈 catch 블록 (`} catch (Exception e) {}`) +- 일반 `Exception`을 무차별 catch +- 에러 메시지에 비밀 정보 포함 + +### 4.3 외부 호출 +- timeout 명시 (default 무한대 금지) +- retry는 idempotent 작업만, exponential backoff +- circuit breaker는 의존성 down이 전파되는 호출에 적용 + +--- + +## 5. 테스트 + +### 5.1 무엇을 테스트하나 +| Yes | No | +|-----|-----| +| 비즈니스 규칙 (상태 전이, 권한) | trivial getter/setter | +| 경계값 (0, max+1, null) | 프레임워크 기능 (Spring DI 동작 등) | +| 외부 의존성 wrap (RabbitMQ publisher) | 외부 의존성 자체 (RabbitMQ 동작) | +| 회귀 케이스 (버그 fix 후) | | + +### 5.2 테스트 명명 +``` +{메서드명_혹은_시나리오}_{조건}_{기대결과} + +예: +createSession_whenUserNotConsented_throwsConsentRequiredException +analyzeResume_whenS3UploadFails_marksResumeFailed +``` + +### 5.3 Arrange-Act-Assert +```java +// given +var user = aUser().build(); +var request = aSessionCreateRequest().mode(ONLINE).build(); + +// when +var session = sessionService.create(user, request); + +// then +assertThat(session.status()).isEqualTo(READY); +``` + +상세 전략은 [testing-strategy.md](./testing-strategy.md) 참조. + +--- + +## 6. 의존성 관리 + +### 6.1 새 라이브러리 추가 기준 +- 표준 라이브러리로 해결 가능한지 확인 +- 마지막 릴리스 날짜 (1년 이상 ago = 위험) +- 라이선스 (MIT/Apache-2.0/BSD ✓, AGPL/GPL ?) +- 별점·이슈 수 +- 대체재 비교 → PR 본문에 근거 작성 + +### 6.2 버전 고정 +- production 의존성은 **정확한 버전** (caret/tilde 회피) +- 락 파일(`package-lock.json`, `uv.lock`, gradle lock) 커밋 +- Renovate / Dependabot으로 주기적 업데이트 + +--- + +## 7. 파일·디렉토리 + +- 한 파일 한 책임 +- 줄 수 가이드: 200줄 이내 권장, 400줄 초과 시 분할 검토 +- 파일명은 export 하는 핵심 식별자와 일치 (PascalCase 컴포넌트는 PascalCase 파일명) + +--- + +## 8. 매직 넘버·문자열 + +```java +// 나쁨 +if (session.questionCount() >= 10) {...} + +// 좋음 +private static final int DEFAULT_MAX_QUESTIONS = 10; +if (session.questionCount() >= DEFAULT_MAX_QUESTIONS) {...} +``` + +상수는 가장 가까운 도메인 클래스에 위치. 전역 `Constants`는 회피. + +--- + +## 9. Boolean Trap + +```java +// 나쁨 +public Session create(User user, boolean online, boolean technical) {...} + +// 좋음 +public Session create(User user, SessionMode mode, InterviewType type) {...} +``` + +--- + +## 10. 불변성 (Immutability) + +- 가능한 한 불변 객체 (Java `record`, Kotlin `data class`, TS `readonly`) +- 컬렉션은 불변 또는 방어적 복사 +- 예외: 성능 임계 경로 + +--- + +## 11. 의존성 주입 + +- 생성자 주입 (필드 주입 금지) +- 인터페이스 + 구현체 분리는 **테스트 더블이 필요할 때만**. 단일 구현이면 클래스 직접 사용. + +--- + +## 12. 로깅 + +- 한국어 로그 메시지 OK (개발 팀 내부 사용) +- 단, 구조화 필드 키는 영어 (`userId`, `sessionId`) +- 민감 정보 절대 X — [observability.md §9](./observability.md) + +--- + +## 13. Pull Request 사이즈 + +- 권장: 변경 라인 ≤ 400 +- 초과 시 분할 검토 (리팩터 + 기능 분리, 도메인별 분리) +- 단일 commit ≠ 단일 PR. 한 PR에 의미 단위 commit 여러 개 OK. diff --git a/docs/data-flow.md b/docs/data-flow.md new file mode 100644 index 00000000..b4f28076 --- /dev/null +++ b/docs/data-flow.md @@ -0,0 +1,148 @@ +# 데이터 흐름 + +> 핵심 시나리오 4개의 end-to-end 데이터 흐름. 각 단계는 [어떤 컴포넌트] → [어떤 저장소·메시지] 형식으로 표기한다. + +--- + +## 1. GitHub 레포 분석 파이프라인 (US-10) + +``` +[사용자] 레포 선택 + → [Core] repositories INSERT (status=PENDING) + → [Core] RabbitMQ publish: ai.request.repo.analyze + → [AI] consume → GitHub API (파일 구조 + README + 주요 코드) + → [AI] LLM 호출 → 프로젝트 요약 마크다운 생성 + → [AI] S3 PUT: analyzed/repository/{repo_id}/summary.md + → [AI] 청킹 → 임베딩 → pgvector INSERT (Core API 경유) + → [AI] RabbitMQ publish: ai.callback.repo.analyzed + → [Core] consume → analyzed_documents INSERT + → repositories.status = ANALYZED + → [Core] SSE broadcast: { type: 'REPO_ANALYZED', repo_id } + → [Frontend] 상태 자동 갱신 +``` + +**실패 시**: +- AI 분석 실패 → `ai.callback.repo.failed` 발행 → `repositories.status = FAILED` +- DLQ(Dead Letter Queue)로 이동, 운영자 수동 재시도 + +--- + +## 2. 이력서 분석 파이프라인 (US-09) + +``` +[사용자] PDF 업로드 + → [Core] S3 PUT: resumes/raw/{user_id}/{uuid}.pdf + → [Core] resumes INSERT (status=PENDING, file_path=S3 key) + → [Core] RabbitMQ publish: ai.request.resume.analyze + → [AI] consume → S3 GET (PDF) → 텍스트 추출 + → [AI] LLM → 마크다운 변환 (기술 스택, 경력, 프로젝트 구조화) + → [AI] S3 PUT: analyzed/resume/{resume_id}/summary.md + → [AI] 청킹 → 임베딩 → pgvector INSERT + → [AI] RabbitMQ publish: ai.callback.resume.analyzed + → [Core] analyzed_documents INSERT (summary, tech_stack JSONB) + → resumes.status = ANALYZED + → [Frontend] SSE 수신 → UI 갱신 +``` + +--- + +## 3. 면접 세션 흐름 (US-13 ~ US-20) + +### 3.1 세션 생성 단계 + +``` +[사용자] 세션 설정 (모드/유형/직군/최대 질문 수/참조 문서) + → [Core] interview_sessions INSERT (status=READY) + → session_contexts INSERT (선택된 analyzed_documents 연결) + → [Core] RabbitMQ publish: ai.request.session.questionPool (Pro 모델) + → [AI] RAG 검색 (pgvector 유사도) → 컨텍스트 추출 + → [AI] Pro 모델 → 질문 풀 생성 (10~15개) + → [AI] RabbitMQ publish: ai.callback.session.questionPool + → [Core] 질문 풀을 Redis에 캐싱 (TTL=세션 max_duration) +``` + +### 3.2 질문-답변 사이클 (반복) + +``` +[RealTime] WebSocket → 첫 질문 전송 + → [Frontend] 표시 + 마이크 활성화 + → [사용자] 음성 답변 (Phase 2) 또는 텍스트 + → [Frontend] (음성 모드) WebRTC stream → RealTime + → [RealTime] STT 청크 단위 → 최종 텍스트 + → [RealTime] Core REST: POST /api/sessions/{id}/messages + → [Core] interview_messages INSERT (role=INTERVIEWEE, parent=직전 질문) + → [Core] RabbitMQ publish: ai.request.session.followup (Flash + RAG) + → [AI] 답변 평가 + 꼬리질문 생성 + → 답변 음성이면 음성 분석도 병행 + → [AI] RabbitMQ publish: ai.callback.session.followup + → [Core] interview_messages INSERT (role=INTERVIEWER, 꼬리질문) + → message_voice_analyses INSERT (음성 모드일 경우) + → [RealTime] WebSocket → 다음 질문 전송 +``` + +### 3.3 세션 종료 + +``` +[사용자] 종료 버튼 OR 최대 질문/시간 도달 + → [Core] interview_sessions.status = COMPLETED, ended_at = now() + → [Core] RabbitMQ publish: ai.request.session.feedback + → [AI] 전체 메시지 + 음성 분석 → 종합 평가 + → [AI] S3 PUT: feedback/{session_id}/report.md + → [AI] RabbitMQ publish: ai.callback.session.feedback + → [Core] session_feedbacks INSERT + → [Frontend] SSE → 리포트 페이지 자동 라우팅 +``` + +--- + +## 4. 인증 흐름 (US-01) + +``` +[Frontend] /api/auth/github 호출 (state 동봉) + → [Core] GitHub OAuth URL 발급 → 302 redirect + → [GitHub] 사용자 동의 → callback URL?code=xxx + → [Frontend] /api/auth/github/callback?code=xxx + → [Core] GitHub Token Exchange → access_token + → users UPSERT (github_id 기준, encrypted_github_access_token) + → JWT access_token 발급 (15분) + → JWT refresh_token 발급 → refresh_tokens INSERT (해시만 저장) + → [Core] Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict + → [Frontend] localStorage에 access_token 보관 +``` + +**Refresh 흐름**: +``` +[Frontend] 401 수신 → POST /api/auth/refresh (쿠키의 refresh_token) + → [Core] refresh_tokens 조회 (해시 비교, is_revoked, expires_at 체크) + → 새 access_token 발급, refresh_token 회전(rotate) + → [Frontend] 원 요청 재시도 +``` + +--- + +## 5. 분석 상태 실시간 알림 (US-11) + +``` +[AI 작업 시작] → RabbitMQ publish: ai.status.* (QUEUED|PROCESSING|...) + → [Core] consume → Redis pub: stream:user:{user_id} + → [RealTime] subscribe Redis → SSE broadcast (user_id로 필터) + → [Frontend] EventSource 수신 → 상태 UI 갱신 + +SSE 끊김 시: + → [Frontend] 자동 재연결 (EventSource 기본 동작) + → [Frontend] 폴링 fallback: GET /api/documents/{id} (5초 간격) +``` + +상세 이벤트 스펙은 [`event-stream.md`](./event-stream.md) 참조. + +--- + +## 6. 시퀀스 다이어그램이 필요한 시나리오 + +다음 항목들은 별도 시퀀스 다이어그램(mermaid 또는 PlantUML)을 추가할 후보: + +- [ ] WebRTC SDP/ICE 교환 (Phase 2) +- [ ] 멀티 에이전트 면접 (LangGraph) (Phase 4) +- [ ] AI Worker 수평 확장 시 메시지 분배 + +> 작성 시 본 문서에 inline mermaid 또는 별도 `docs/sequence/*.md` 로 분리. diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 00000000..c1517b84 --- /dev/null +++ b/docs/database.md @@ -0,0 +1,375 @@ +# 데이터베이스 가이드 + +> PostgreSQL + pgvector. 스키마는 Flyway로 버전 관리한다. **Core Server만 직접 접근**한다 ([architecture.md §4.1](./architecture.md)). + +--- + +## 1. 스키마 개요 (13 테이블) + +``` +users → refresh_tokens, user_consents, repositories, resumes, + interview_sessions, activity_logs, ai_request_logs + +repositories ─┐ +resumes ──────┼→ analyzed_documents → session_contexts ← interview_sessions + +interview_sessions → interview_messages → message_voice_analyses +interview_sessions → session_feedbacks +``` + +### 테이블 인벤토리 + +| # | 테이블 | 역할 | +|---|--------|------| +| 1 | `users` | GitHub OAuth 사용자 | +| 2 | `refresh_tokens` | JWT refresh token (해시 저장) | +| 3 | `user_consents` | 개인정보처리동의 이력 | +| 4 | `repositories` | 면접 분석용 GitHub 레포 메타 | +| 5 | `resumes` | 이력서 메타 (실 파일은 S3) | +| 6 | `analyzed_documents` | AI 분석 결과 메타 + S3 경로 | +| 7 | `interview_sessions` | 면접 세션 설정·상태·히스토리 | +| 8 | `session_contexts` | 세션 ↔ 분석 문서 N:M | +| 9 | `interview_messages` | 면접 질문·답변 시퀀스 (트리) | +| 10 | `message_voice_analyses` | 답변별 음성 분석 (1:1) | +| 11 | `session_feedbacks` | 종합 피드백 리포트 (1:1) | +| 12 | `activity_logs` | 사용자 행동 로그 | +| 13 | `ai_request_logs` | AI 서버 요청/응답 로그 | + +--- + +## 2. DDL (V1__init.sql 베이스) + +> 아래는 [필수 개선 사항](#5-필수-개선-사항)을 반영한 **권장 최종형** DDL이다. 초기 V1 마이그레이션은 이 형태로 작성한다. + +```sql +-- 1. users +CREATE TABLE users ( + id BIGSERIAL PRIMARY KEY, + github_id BIGINT NOT NULL UNIQUE, + github_username VARCHAR(100) NOT NULL, + email VARCHAR(255), + avatar_url VARCHAR(500), + encrypted_github_access_token VARCHAR(1000) NOT NULL, -- AES 암호화 (security.md) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); +CREATE INDEX idx_users_active ON users(id) WHERE is_deleted = FALSE; + +-- 2. refresh_tokens +CREATE TABLE refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + token_hash VARCHAR(500) NOT NULL UNIQUE, + device_info VARCHAR(500), + expires_at TIMESTAMPTZ NOT NULL, + is_revoked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 3. user_consents +CREATE TABLE user_consents ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + consent_type VARCHAR(50) NOT NULL CHECK (consent_type IN ('TOS', 'PRIVACY', 'MARKETING')), + consent_version VARCHAR(20) NOT NULL, + is_agreed BOOLEAN NOT NULL DEFAULT TRUE, + agreed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + revoked_at TIMESTAMPTZ, + ip_address VARCHAR(45), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 4. repositories +CREATE TABLE repositories ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + github_repo_id BIGINT NOT NULL, + repo_name VARCHAR(255) NOT NULL, + repo_full_name VARCHAR(500) NOT NULL, + repo_url VARCHAR(500) NOT NULL, + default_branch VARCHAR(100) DEFAULT 'main', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' + CHECK (status IN ('PENDING','ANALYZING','ANALYZED','FAILED')), + last_synced_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (user_id, github_repo_id) +); + +-- 5. resumes +CREATE TABLE resumes ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + original_filename VARCHAR(500) NOT NULL, + file_path VARCHAR(1000) NOT NULL, -- S3 key only + file_type VARCHAR(20) NOT NULL CHECK (file_type IN ('PDF')), + file_size BIGINT, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' + CHECK (status IN ('PENDING','ANALYZING','ANALYZED','FAILED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +-- 6. analyzed_documents +-- ⚠️ 다형성 FK 제거: resume_id / repository_id 둘 중 하나만 NOT NULL +CREATE TABLE analyzed_documents ( + id BIGSERIAL PRIMARY KEY, + resume_id BIGINT REFERENCES resumes(id), + repository_id BIGINT REFERENCES repositories(id), + document_path VARCHAR(1000) NOT NULL, -- S3 key + summary VARCHAR(2000), + tech_stack JSONB, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' + CHECK (status IN ('ACTIVE','ARCHIVED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + CHECK ( + (resume_id IS NOT NULL AND repository_id IS NULL) + OR + (resume_id IS NULL AND repository_id IS NOT NULL) + ) +); + +-- 7. interview_sessions +CREATE TABLE interview_sessions ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id), + title VARCHAR(200), + memo TEXT, + mode VARCHAR(20) NOT NULL CHECK (mode IN ('ONLINE','OFFLINE')), + interview_type VARCHAR(30) NOT NULL + CHECK (interview_type IN ('PERSONALITY','TECHNICAL','LIVE_CODING','INTEGRATED')), + job_category VARCHAR(30) NOT NULL + CHECK (job_category IN ('FRONTEND','BACKEND','INFRA','DBA')), + max_questions INT NOT NULL DEFAULT 10, + max_duration_minutes INT NOT NULL DEFAULT 60, + status VARCHAR(20) NOT NULL DEFAULT 'READY' + CHECK (status IN ('READY','IN_PROGRESS','INTERRUPTED','COMPLETED','CANCELLED')), + total_question_count INT DEFAULT 0, + started_at TIMESTAMPTZ, + ended_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +-- 8. session_contexts +CREATE TABLE session_contexts ( + id BIGSERIAL PRIMARY KEY, + session_id BIGINT NOT NULL REFERENCES interview_sessions(id), + document_id BIGINT NOT NULL REFERENCES analyzed_documents(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (session_id, document_id) +); + +-- 9. interview_messages +CREATE TABLE interview_messages ( + id BIGSERIAL PRIMARY KEY, + session_id BIGINT NOT NULL REFERENCES interview_sessions(id), + sequence_number INT NOT NULL, + role VARCHAR(20) NOT NULL + CHECK (role IN ('INTERVIEWER','INTERVIEWEE','SYSTEM')), + content TEXT, + audio_file_path VARCHAR(1000), + parent_message_id BIGINT REFERENCES interview_messages(id), + status VARCHAR(20) NOT NULL DEFAULT 'CREATED' + CHECK (status IN ('CREATED','COMPLETED','FAILED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (session_id, sequence_number), + CHECK (content IS NOT NULL OR audio_file_path IS NOT NULL) +); + +-- 10. message_voice_analyses +CREATE TABLE message_voice_analyses ( + id BIGSERIAL PRIMARY KEY, + message_id BIGINT NOT NULL UNIQUE REFERENCES interview_messages(id), + speaking_rate_wpm FLOAT, + silence_duration_sec FLOAT, + filler_word_counts JSONB, + pronunciation_accuracy FLOAT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 11. session_feedbacks +CREATE TABLE session_feedbacks ( + id BIGSERIAL PRIMARY KEY, + session_id BIGINT NOT NULL UNIQUE REFERENCES interview_sessions(id), + overall_score FLOAT, + technical_accuracy FLOAT, + logic_score FLOAT, + communication_score FLOAT, + strengths_summary TEXT, + weaknesses_summary TEXT, + improvement_keywords JSONB, + report_file_path VARCHAR(1000), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +-- 12. activity_logs (파티셔닝 권장 — §6 참조) +CREATE TABLE activity_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT REFERENCES users(id), + action VARCHAR(50) NOT NULL, + resource_type VARCHAR(30), + resource_id BIGINT, + detail JSONB, + ip_address VARCHAR(45), + user_agent VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 13. ai_request_logs (파티셔닝 권장) +CREATE TABLE ai_request_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT REFERENCES users(id), + session_id BIGINT REFERENCES interview_sessions(id), + request_type VARCHAR(50) NOT NULL, + model_name VARCHAR(100), + input_tokens INT, + output_tokens INT, + latency_ms INT, + status VARCHAR(20) NOT NULL, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +--- + +## 3. ENUM 카탈로그 + +> 코드(Enum)와 DB(VARCHAR + CHECK)는 **반드시 1:1 매칭**. Enum 추가 시 Flyway 마이그레이션도 같이 작성. + +``` +mode : ONLINE | OFFLINE +interview_type : PERSONALITY | TECHNICAL | LIVE_CODING | INTEGRATED +job_category : FRONTEND | BACKEND | INFRA | DBA +session_status : READY | IN_PROGRESS | INTERRUPTED | COMPLETED | CANCELLED +message_role : INTERVIEWER | INTERVIEWEE | SYSTEM +message_status : CREATED | COMPLETED | FAILED +repo_status : PENDING | ANALYZING | ANALYZED | FAILED +resume_status : PENDING | ANALYZING | ANALYZED | FAILED +doc_status : ACTIVE | ARCHIVED +consent_type : TOS | PRIVACY | MARKETING +file_type : PDF +``` + +--- + +## 4. 인덱스 전략 + +### 필수 인덱스 +| 테이블 | 컬럼 | 이유 | +|--------|------|------| +| users | github_id (UNIQUE) | 로그인 lookup | +| refresh_tokens | token_hash (UNIQUE) | refresh 검증 | +| repositories | (user_id, github_repo_id) UNIQUE | 중복 등록 방지 | +| analyzed_documents | resume_id, repository_id | FK 조회 | +| interview_sessions | user_id, created_at DESC | 히스토리 목록 | +| interview_messages | (session_id, sequence_number) UNIQUE | 시퀀스 조회 | +| interview_messages | parent_message_id | 꼬리질문 트리 traverse | +| activity_logs | (user_id, created_at DESC) | 사용자별 로그 조회 | + +### Partial Index (soft delete 패턴) +```sql +CREATE INDEX idx_repositories_active ON repositories(user_id) WHERE is_deleted = FALSE; +CREATE INDEX idx_resumes_active ON resumes(user_id) WHERE is_deleted = FALSE; +CREATE INDEX idx_sessions_active ON interview_sessions(user_id, created_at DESC) WHERE is_deleted = FALSE; +``` + +### pgvector 인덱스 +```sql +-- 임베딩 테이블 (별도 마이그레이션에서 추가) +CREATE TABLE document_embeddings ( + id BIGSERIAL PRIMARY KEY, + document_id BIGINT NOT NULL REFERENCES analyzed_documents(id), + chunk_index INT NOT NULL, + chunk_text TEXT NOT NULL, + embedding VECTOR(1536) NOT NULL, -- 모델별 차원수 확인 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (document_id, chunk_index) +); +CREATE INDEX idx_embeddings_ivfflat + ON document_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); +``` + +--- + +## 5. 필수 개선 사항 (Flyway V1 작성 전 적용) + +| 항목 | 조치 | 이유 | +|------|------|------| +| TIMESTAMP → TIMESTAMPTZ | 위 DDL에 반영 | 타임존 이슈 방지 | +| CHECK 제약 추가 | 위 DDL에 반영 | 잘못된 값 차단 | +| `(session_id, sequence_number)` UNIQUE | 반영 | 시퀀스 중복 방지 | +| `analyzed_documents` 다형성 FK 해소 | `resume_id` / `repository_id` 분리 + CHECK | FK 무결성 확보 | +| `github_access_token` 암호화 | 컬럼명 변경 + AES 암호화 (애플리케이션 레벨) | 평문 저장 금지 | +| 빈 메시지 방지 | `CHECK (content IS NOT NULL OR audio_file_path IS NOT NULL)` | 무의미 row 차단 | + +--- + +## 6. 후속 개선 (운영 단계) + +### 로그 테이블 파티셔닝 +```sql +-- activity_logs / ai_request_logs 를 월 단위 파티셔닝 +CREATE TABLE activity_logs (...) PARTITION BY RANGE (created_at); +CREATE TABLE activity_logs_2026_05 PARTITION OF activity_logs + FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); +``` + +### 보관 정책 +- `activity_logs`: 90일 후 cold storage (S3 export) +- `ai_request_logs`: 30일 보관, 통계 요약만 별도 테이블에 보존 + +### S3 path 컨벤션 +- bucket은 환경변수, key만 DB 저장 → [`storage.md`](./storage.md) + +--- + +## 7. JPA / QueryDSL 사용 가이드 + +- 단순 CRUD → JPA Repository +- 동적 조건, 조인 다수 → QueryDSL +- Native Query는 `pgvector` 검색 등 ORM이 표현 어려운 경우만 +- N+1 방지: `@EntityGraph` 또는 fetch join + +```java +@Entity +@Table(name = "interview_sessions") +public class InterviewSession { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SessionMode mode; // DB ENUM과 1:1 + // ... +} +``` + +--- + +## 8. Flyway 운영 룰 + +- 마이그레이션 파일명: `V{버전}__{snake_case_설명}.sql` +- 한 PR에 여러 마이그레이션 파일 OK, 단 dependency 순서 보장 +- 적용 후 수정 절대 금지 (수정해야 한다면 `V{n+1}__fix_*.sql` 신규 추가) +- DDL과 DML(시드 데이터)은 분리: `V1__init_schema.sql`, `V2__seed_*.sql` + +--- + +## 9. 미반영 도메인 (Phase 4 이후) + +- 시선/자세 분석 결과 테이블 (Phase 4) +- 면접 노트 (질문별 사용자 메모 + LLM Enrich) +- 면접 대상 회사 관리 (`target_companies`) + +추가 시 본 문서 §1, §2, §3에 반영. diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 00000000..3e73f35e --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,292 @@ +# 디자인 시스템 + +> StackUp 프론트엔드의 시각·행동 규약. Figma 토큰과 1:1로 동기화된다. +> 구현 위치: `frontend/src/app/styles/`, `frontend/src/shared/ui/` + +--- + +## 1. 디자인 원칙 + +1. **신뢰감 우선** — 면접 도구이므로 가벼운 톤 지양. 진중하고 차분한 시각 언어. +2. **집중 환경 보호** — 면접 화면은 노이즈 최소화. 핵심 UI만 노출, 부가 정보는 hover/click으로 점진적 공개. +3. **상태 가시성** — 분석 진행, 세션 진행 등 비동기 상태는 항상 명시적으로 보여준다. +4. **접근성 (WCAG 2.1 AA)** — 키보드 only 조작 가능, 명도 대비 4.5:1 이상. +5. **모바일 웹뷰 대응** — 데스크탑 우선이지만 mobile breakpoint에서도 깨지지 않게. + +--- + +## 2. 디자인 토큰 + +### 2.1 컬러 + +#### Brand +| 토큰 | Light | Dark | 용도 | +|------|-------|------|------| +| `--color-brand-primary` | `#2563EB` | `#3B82F6` | CTA, 활성 상태 | +| `--color-brand-primary-hover` | `#1D4ED8` | `#60A5FA` | hover | +| `--color-brand-primary-pressed` | `#1E40AF` | `#2563EB` | active | +| `--color-brand-secondary` | `#10B981` | `#34D399` | 성공·완료 | + +#### Semantic +| 토큰 | Light | Dark | 용도 | +|------|-------|------|------| +| `--color-success` | `#10B981` | `#34D399` | 성공 메시지, ANALYZED 뱃지 | +| `--color-warning` | `#F59E0B` | `#FBBF24` | 경고, PROCESSING | +| `--color-danger` | `#EF4444` | `#F87171` | 에러, FAILED, 삭제 | +| `--color-info` | `#3B82F6` | `#60A5FA` | 정보 토스트 | + +#### Neutral +| 토큰 | 16진 | 용도 | +|------|------|------| +| `--color-bg` | `#FFFFFF` / `#0B0F19` | 페이지 배경 | +| `--color-surface` | `#F9FAFB` / `#111827` | 카드, 모달 | +| `--color-surface-raised` | `#FFFFFF` / `#1F2937` | 모달, 팝오버 | +| `--color-border` | `#E5E7EB` / `#374151` | 보더 | +| `--color-text-primary` | `#111827` / `#F9FAFB` | 본문 | +| `--color-text-secondary` | `#6B7280` / `#9CA3AF` | 부가 정보 | +| `--color-text-disabled` | `#D1D5DB` / `#4B5563` | 비활성 | + +#### 도메인 컬러 (직군 / 면접 유형) +| 토큰 | 색상 | +|------|------| +| `--color-job-frontend` | `#22D3EE` (cyan) | +| `--color-job-backend` | `#A78BFA` (violet) | +| `--color-job-infra` | `#FB7185` (rose) | +| `--color-job-dba` | `#FACC15` (amber) | +| `--color-type-personality` | `#34D399` | +| `--color-type-technical` | `#60A5FA` | +| `--color-type-live-coding` | `#F472B6` | +| `--color-type-integrated` | `#A78BFA` | + +### 2.2 타이포그래피 + +폰트 스택: +```css +--font-sans: 'Pretendard Variable', 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; +--font-mono: 'JetBrains Mono', 'D2Coding', 'Menlo', monospace; +``` + +| 토큰 | 크기 / 라인높이 / 굵기 | 용도 | +|------|------------------------|------| +| `--type-display` | 48 / 56 / 700 | 랜딩 히어로 | +| `--type-h1` | 32 / 40 / 700 | 페이지 제목 | +| `--type-h2` | 24 / 32 / 700 | 섹션 제목 | +| `--type-h3` | 20 / 28 / 600 | 카드 제목 | +| `--type-h4` | 18 / 26 / 600 | 서브 제목 | +| `--type-body-lg` | 16 / 24 / 400 | 본문 (기본) | +| `--type-body-md` | 14 / 20 / 400 | 보조 본문 | +| `--type-body-sm` | 12 / 18 / 400 | 캡션, 메타 | +| `--type-code-md` | 14 / 20 / 400 (mono) | 코드 | + +### 2.3 간격 (Spacing) — 4px grid + +| 토큰 | 값 | +|------|----| +| `--space-1` | 4px | +| `--space-2` | 8px | +| `--space-3` | 12px | +| `--space-4` | 16px | +| `--space-5` | 20px | +| `--space-6` | 24px | +| `--space-8` | 32px | +| `--space-10` | 40px | +| `--space-12` | 48px | +| `--space-16` | 64px | + +### 2.4 라디우스 + +| 토큰 | 값 | 용도 | +|------|----|------| +| `--radius-sm` | 4px | 뱃지 | +| `--radius-md` | 8px | 버튼, 인풋 | +| `--radius-lg` | 12px | 카드 | +| `--radius-xl` | 16px | 모달 | +| `--radius-pill` | 9999px | 칩, 아바타 | + +### 2.5 그림자 (Elevation) + +| 토큰 | 값 | +|------|----| +| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.06)` | +| `--shadow-md` | `0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -2px rgba(0,0,0,0.06)` | +| `--shadow-lg` | `0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -4px rgba(0,0,0,0.05)` | +| `--shadow-focus-ring` | `0 0 0 3px rgba(37,99,235,0.45)` | + +### 2.6 모션 + +| 토큰 | 값 | 용도 | +|------|----|------| +| `--duration-fast` | 120ms | hover, focus | +| `--duration-normal` | 200ms | 기본 transition | +| `--duration-slow` | 320ms | 모달 enter/exit | +| `--easing-standard` | `cubic-bezier(0.4, 0, 0.2, 1)` | 기본 | +| `--easing-decelerate` | `cubic-bezier(0, 0, 0.2, 1)` | enter | +| `--easing-accelerate` | `cubic-bezier(0.4, 0, 1, 1)` | exit | + +> `prefers-reduced-motion` 사용자에게는 모든 transition을 0ms로 강제. + +### 2.7 Breakpoint + +| 토큰 | min-width | 용도 | +|------|-----------|------| +| `--bp-sm` | 640px | 모바일 가로 | +| `--bp-md` | 768px | 태블릿 | +| `--bp-lg` | 1024px | 데스크탑 | +| `--bp-xl` | 1280px | 대형 모니터 | + +--- + +## 3. 컴포넌트 인벤토리 + +> 위치: `frontend/src/shared/ui/{ComponentName}/`. 각 컴포넌트는 `index.ts`로 export. + +### Foundation +- `Button` — variant: `primary | secondary | ghost | danger`, size: `sm | md | lg`, state: `loading | disabled` +- `IconButton` — 아이콘 전용, aria-label 필수 +- `Link` — 인라인/블록, 외부 링크는 자동으로 `target="_blank" rel="noopener"` +- `Input` — text/email/url, prefix·suffix slot, 에러 메시지 슬롯 +- `Textarea` — auto-grow 옵션 +- `Select` — 단일 선택 (네이티브 select 래핑) +- `Combobox` — 검색·자동완성 (Repository 등록 등) +- `Checkbox`, `Radio`, `Switch` +- `FileUploader` — Drag & Drop, 진행률, 파일 검증 메시지 + +### Display +- `Badge` — 상태 표시 (READY/IN_PROGRESS/COMPLETED/FAILED 색상 매핑) +- `Tag` / `Chip` — 기술 스택 등 다중 라벨 +- `Avatar` — GitHub avatar +- `Progress` — 선형 / 원형 +- `Skeleton` — 로딩 placeholder +- `EmptyState` — 빈 목록 안내 (아이콘 + 설명 + CTA) +- `Card` — `header / body / footer` slot + +### Feedback +- `Toast` — 4종 (success/info/warning/error), 우상단 stack, 4초 자동 dismiss +- `Modal` — `sm / md / lg / fullscreen`, focus trap 필수 +- `Drawer` — 우측 슬라이드, 세션 설정 등 +- `Popover`, `Tooltip` — 키보드 접근 가능 +- `ConfirmDialog` — 파괴적 액션 (삭제, 회원 탈퇴) 전용 +- `Alert` — 페이지 내 inline 경고 + +### Navigation +- `TopNav` — 로고, 글로벌 액션, Avatar 드롭다운 +- `SideNav` — Workspace 좌측 메뉴 +- `Tabs` — `Underline | Pills` +- `Breadcrumb` +- `Pagination` + +### Domain-aware (`shared/ui` 가 아닌 `features/*/ui`로 위치) +- `JobCategoryBadge` — 직군별 컬러 +- `InterviewTypeBadge` — 면접 유형별 컬러 +- `SessionStatusBadge` +- `AnalysisStateIndicator` — QUEUED/PROCESSING/COMPLETED/FAILED 4단계 progress + +> **shared vs features 구분**: 도메인 의존성이 있으면 features로. 디자인 토큰만 사용하면 shared로. + +--- + +## 4. 상태별 색상 매핑 (Status Color Map) + +| 상태 | 색상 | 컴포넌트 예시 | +|------|------|----------------| +| `READY` / `PENDING` / `QUEUED` | neutral / `text-secondary` | 회색 Badge | +| `IN_PROGRESS` / `PROCESSING` / `ANALYZING` | warning | 노란 Badge + spinner | +| `COMPLETED` / `ANALYZED` / `ACTIVE` | success | 초록 Badge | +| `INTERRUPTED` | warning | 노란 Badge (느낌표) | +| `FAILED` | danger | 빨간 Badge + 재시도 버튼 | +| `CANCELLED` / `ARCHIVED` | text-disabled | 회색 strikethrough | + +**구현 컨벤션**: `frontend/src/shared/ui/StatusBadge` 단일 컴포넌트가 모든 매핑을 캡슐화. 새 상태 추가 시 이 컴포넌트만 수정. + +--- + +## 5. 페이지 레이아웃 그리드 + +``` +┌────────────────────────────────────────────────────┐ +│ TopNav (height: 64px, sticky) │ +├──────────┬─────────────────────────────────────────┤ +│ │ │ +│ SideNav │ Main Content │ +│ (240px) │ - max-width: 1280px │ +│ │ - padding: 24px (mobile: 16px) │ +│ │ │ +└──────────┴─────────────────────────────────────────┘ +``` + +특수 페이지: +- **Login**: SideNav 없음, 중앙 정렬 카드 +- **Interview**: SideNav 숨김(focus mode), 좌측 면접관, 우측 답변 영역, 하단 컨트롤 +- **Feedback Report**: TopNav만, 인쇄 친화적 레이아웃 (`@media print`) + +--- + +## 6. 다크 모드 + +- `` 로 토글 +- `prefers-color-scheme` 자동 감지 + 사용자 명시 선택 우선 +- 토큰만 갱신 (컴포넌트 코드 수정 불필요) +- 면접 진행 중에는 다크 모드 강제 (눈 피로 감소) — Phase 2에서 검토 + +--- + +## 7. 아이콘 + +- 라이브러리: **Lucide Icons** (트리쉐이킹 지원) +- 크기: 16/20/24px (line height와 정렬) +- 색상: `currentColor` (텍스트 색상 상속) +- 의미 있는 아이콘은 `aria-label` 필수, 장식용은 `aria-hidden="true"` + +--- + +## 8. 토큰 → 코드 매핑 + +`frontend/src/app/styles/tokens.css`: +```css +:root { + /* 위 표의 모든 토큰을 CSS 변수로 정의 */ + --color-brand-primary: #2563EB; + --space-4: 16px; + /* ... */ +} + +[data-theme='dark'] { + --color-brand-primary: #3B82F6; + /* ... 다크모드 오버라이드 */ +} +``` + +컴포넌트는 토큰만 참조: +```tsx +// 좋음 +