diff --git a/API_SPEC.md b/API_SPEC.md index e1c8e71..a50e331 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -2,146 +2,199 @@ 라온누리 서버 API 명세서 -## ⚠️ 최신 변경사항 (2025-12-02) +--- -### 📰 피드 시스템 API -- **GET /api/feed/daily-prompt**: 오늘의 글감 조회 - - 매일 새로운 주제 제안 (AI 생성) - - 캐싱: 2분 - - 응답: `{ prompt: string, category: string, keywords: string[] }` -- **GET /api/feed/inspirations**: 추천 영감 목록 - - Vertex AI 생성 영감 (제목, 내용, 이미지) - - Unsplash API 이미지 (작가 크레딧 포함) - - 캐싱: 2분 - - 응답: `Inspiration[]` (id, title, content, imageUrl, unsplashCredit) -- **GET /api/feed/weekly-stats**: 주간 통계 조회 - - 주간 목표 및 진행률 - - 권한: 로그인 필요 - - 응답: `{ weeklyGoal: number, writingsThisWeek: number, ... }` -- **GET /api/feed/team-activity**: 팀 활동 조회 - - 최근 팀 글 활동 요약 - - 권한: 로그인 필요 - - 응답: `TeamActivity[]` -- **POST /api/user/[uid]/weekly-goal**: 주간 목표 설정 - - 권한: 본인만 수정 가능 - - 요청: `{ weeklyGoal: number }` - - 응답: `{ success: true }` +## 🔧 API 개발 필수 가이드 -### 🎨 AI 영감 생성 (Cloud Functions) -- **generateDailyInspirations** (Scheduled): 매일 새벽 자동 생성 - - Vertex AI (Gemini 2.5 Flash) - - Unsplash API 이미지 검색 및 다운로드 - - Firebase Storage 저장 - - Firestore `inspirations` 컬렉션 저장 - - 크레딧 정보 저장 (작가 이름, 프로필 링크) -- **generateInspirationsManual** (HTTP): 관리자 수동 생성 - - POST 요청으로 즉시 생성 - - 기존 영감 덮어쓰기 옵션 - - Rate Limit: 1초 간격 +### RESTful API 설계 원칙 -### 📦 FeedManager -- **메서드**: `getDailyPrompt()`, `getInspirations()`, `getWeeklyStats()`, `getTeamActivity()` -- **캐싱**: 2분 TTL (BaseManager 캐싱 활용) -- **타입 시스템**: `Inspiration`, `DailyPrompt`, `WeeklyStats`, `TeamActivity` +**HTTP Method로 동작 구분** (경로로 구분하지 않음): -## ⚠️ 변경사항 (2025-11-27) +```typescript +// ✅ 올바른 방식 (RESTful) +POST /api/team/:id/allowed-names // 이름 추가 +DELETE /api/team/:id/allowed-names // 이름 제거 -### 🖼️ 팀 커버 이미지 API -- **POST /api/team/[teamId]/cover-image**: 팀 커버 이미지 업로드 - - FormData로 이미지 파일 전송 (JPEG/PNG/WebP/GIF, 최대 5MB) - - Firebase Storage 업로드 (`teams/{teamId}/cover-{timestamp}.{ext}`) - - 기존 이미지 자동 삭제 - - 파일 공개 설정 (makePublic) - - 팀 문서 coverImage 필드 업데이트 - - 권한: 팀 소유자만 -- **DELETE /api/team/[teamId]/cover-image**: 팀 커버 이미지 삭제 - - Storage에서 파일 삭제 - - 팀 문서 coverImage 필드 제거 - - 권한: 팀 소유자만 -- **TeamManager 메서드**: `uploadCoverImage()`, `deleteCoverImage()` -- **컴포넌트**: TeamCoverImageUploader (드래그앤드롭, 미리보기, AspectRatio 16:9) +POST /api/team/:id/allowed-emails // 이메일 추가 +DELETE /api/team/:id/allowed-emails // 이메일 제거 -## ⚠️ 변경사항 (2025-11-26) +// ❌ 잘못된 방식 (경로로 구분) +POST /api/team/:id/allowed-names/add // ❌ +POST /api/team/:id/allowed-names/remove // ❌ +``` -### 🔗 익명 계정 연결 기능 -- **POST /api/auth/merge-account**: 익명 계정 데이터를 정식 계정으로 마이그레이션 - - Firestore 데이터 이전 (writings, topics, comments, userReactions, teams) - - Realtime DB 데이터 이전 (drafts, monitoring, previewRequests) - - 원자성 보장 (Firestore Batch, Realtime DB Transaction) - - 병합 완료 후 통계 반환 -- **서비스 레이어**: `src/services/firebaseAuth.ts` (mergeAndLoginWithEmail, mergeAndLoginWithGoogle) -- **상태 관리**: `src/store/authStore.ts` (mergeWithEmail, mergeWithGoogle 액션) -- **UI 통합**: LoginForm/SignupForm mode prop, LoginDialog link 모드 +**API Route 파일 구조**: +```typescript +// src/app/api/team/[teamId]/allowed-names/route.ts -## ⚠️ 변경사항 (2025-11-12) +export async function POST(request: NextRequest, context: RouteContext) { + // 추가 로직 +} -### ✅ Writing API 구현 완료 -- **POST /api/writing**: 글 생성 (서버에서 wordCount/charCount 자동 계산) -- **GET /api/writing/[id]**: 글 조회 (작성자만 접근) -- **PUT /api/writing/[id]**: 글 수정 (작성자만 접근) -- **DELETE /api/writing/[id]**: 글 삭제 (작성자만 접근) -- **POST /api/writing/user**: 사용자 글 목록 -- **POST /api/writing/recent**: 최근 글 (limit 파라미터) -- **서버 레이어**: `src/lib/server/writing.ts` (Firestore CRUD) +export async function DELETE(request: NextRequest, context: RouteContext) { + // 삭제 로직 +} +``` -### ⚡ Content Hash 기반 3단계 스마트 캐싱 -- **POST /api/analyze-pattern**: contentHash 파라미터 추가 -- **L1 캐시**: localStorage (영구, LRU 10개) ~1ms -- **L2 캐시**: Firestore `patternAnalyses` 컬렉션 (영구) ~100ms -- **L3 캐시**: Server in-memory (5분, 50개) ~50ms -- **해시 생성**: `id:updatedAt` 조합 (SHA-256) -- **자동 변경 감지**: 글 추가/수정 → 해시 변경 → 재분석 -- **AI 비용 절감**: 동일 글 세트는 전체 사용자 기준 1회만 분석 -- **서버 레이어**: `src/lib/server/patternAnalysis.ts` (Firestore CRUD) - -## ⚠️ 변경사항 (2025-11-11) - -### 🤖 실시간 피드백 시스템 -- **POST /api/analyze-text**: Vertex AI 기반 텍스트 분석 API - - Delta 전송 지원 (previousText 파라미터) - - 서버 캐싱 (In-Memory LRU, TTL 1분) - - Multi-region failover (3개 region) - - 점수, 찾은 단어, 수정 제안 반환 - -### 🌏 Multi-Region Failover -- Vertex AI 3개 region 자동 전환 -- Rate Limit 대응 (RPM 15 → 45) -- Region health tracking -- Exponential backoff +**HTTP Method 사용 가이드**: +- **GET**: 조회 (캐싱 가능, 멱등성) +- **POST**: 생성, 추가, 복잡한 조회 +- **PUT**: 전체 수정 (멱등성) +- **PATCH**: 부분 수정 +- **DELETE**: 삭제 (멱등성) --- -## ⚠️ 2025-11-10 변경사항 +### Firebase Admin SDK 사용 규칙 -### 🔐 5단계 보안 레벨 시스템 -팀 생성 시 5가지 보안 레벨 선택 가능: -- **Level 1 (OPEN)**: 완전 개방, 닉네임 공유 로그인 -- **Level 2 (NAME_LIST)**: 명단 기반, allowedNames 체크 -- **Level 3 (AUTH_REQUIRED)**: 로그인 필수, 정식 계정 누구나 -- **Level 4 (EMAIL_LIST)**: 이메일 화이트리스트, allowedEmails 체크 -- **Level 5 (CLOSED)**: 닫힌 팀, 신규 가입 불가 +**❌ 절대 사용 금지**: `getFirestore()` 직접 호출 +**✅ 필수 사용**: `adminFbClient` 싱글톤 인스턴스 -### 📦 User 타입 분리 -- **FirestoreUser**: DB 저장용 (최소 데이터만) -- **User**: UI 사용용 (Firebase Auth + Firestore 결합) -- 이름, 이메일, 사진 등은 Firebase Auth가 Single Source of Truth +```typescript +// ❌ 잘못된 방식 - 초기화 문제 및 인증 오류 발생 가능 +import {getFirestore} from "firebase-admin/firestore"; +const db = getFirestore(); +const doc = await db.collection('users').doc(uid).get(); -### 🏷️ 닉네임 저장 위치 변경 -- ❌ 기존: `users.nicknames[teamId]` -- ✅ 신규: `team.members[uid].nickname` - -### 🎯 RESTful API 설계 규칙 -**HTTP Method로 동작 구분** (경로가 아님): +// ✅ 올바른 방식 - 싱글톤 인스턴스 사용 +import {adminFbClient} from "@/lib/firebase-admin"; +const doc = await adminFbClient.collection('users').doc(uid).get(); ``` -✅ POST /api/resource → 생성/추가 -✅ DELETE /api/resource → 삭제/제거 -✅ PUT /api/resource → 전체 수정 -✅ GET /api/resource → 조회 -❌ POST /api/resource/add → 사용 금지 -❌ POST /api/resource/remove → 사용 금지 +**이유**: +- `getFirestore()`는 매번 호출 시 초기화 상태 불확실 +- `adminFbClient`는 `src/lib/firebase-admin.ts`에서 한 번만 초기화 +- 환경변수 `FIREBASE_SERVICE_ACCOUNT_KEY`를 통해 명시적 인증 보장 + +--- + +### API Route 응답 헬퍼 함수 (필수) + +**모든 API Route는 `src/lib/api-response.ts`의 헬퍼 함수를 사용해야 합니다.** + +```typescript +import { + successResponse, + errorResponse, + unauthorizedResponse, + forbiddenResponse, + notFoundResponse, + validationErrorResponse, + internalErrorResponse +} from "@/lib/api-response"; +import {UnwrapApiResponse} from "@/types/api"; + +// ✅ 성공 응답 패턴 +export async function GET(request: NextRequest) { + const data = await fetchData(); + + const response: UnwrapApiResponse = { + items: data, + totalCount: data.length, + }; + return successResponse(response); +} + +// ✅ 에러 응답 패턴 +export async function POST(request: NextRequest) { + const authHeader = request.headers.get("authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return unauthorizedResponse(); // 401 + } + + const body = await request.json(); + if (!body.title?.trim()) { + return validationErrorResponse("제목이 비어있습니다"); // 400 + } + + const user = await getUser(body.userId); + if (!user) { + return notFoundResponse("사용자를 찾을 수 없습니다"); // 404 + } + + if (user.id !== currentUserId) { + return forbiddenResponse(); // 403 + } + + try { + // ... 로직 + } catch (error) { + return internalErrorResponse(); // 500 + } +} ``` +**❌ 절대 사용 금지**: 수동 응답 객체 생성 +```typescript +// ❌ Bad - 직접 NextResponse.json 사용 금지 +return NextResponse.json( + {success: false, error: "에러 메시지", code: "ERROR_CODE"}, + {status: 400} +); + +// ✅ Good - 헬퍼 함수 사용 +return validationErrorResponse("에러 메시지"); +``` + +--- + +### Manager ApiCall 패턴 (클라이언트) + +**Manager에서 `ApiCall` 사용 시 주의사항**: + +`ApiCall`은 성공 시 **언래핑된 데이터만** 반환하고, 실패 시 **자동으로 에러를 throw**합니다. + +```typescript +import {SingletonManager} from "./ManagerBase"; +import {HttpMethod, UnwrapApiResponse} from "@/types/api"; +import type {CreateItemRequest, CreateItemResponse} from "@/types/api/item"; + +class ItemManager extends SingletonManager { + // ✅ 올바른 방식 - ApiCall은 자동으로 언래핑 + async createItem(data: CreateItemRequest): Promise { + const response = await this.ApiCall( + HttpMethod.POST, + '/item', + data + ); + + // response는 이미 {item: Item} 형태 (언래핑됨) + return response.item; + } + + // ❌ 잘못된 방식 - success 체크 불필요 + async createItemWrong(data: CreateItemRequest): Promise { + const response = await this.ApiCall( + HttpMethod.POST, + '/item', + data + ); + + // ❌ response.success는 존재하지 않음 (타입 에러) + if (!response.success || !response.item) { + throw new Error("생성 실패"); + } + + return response.item; + } + + // ✅ 반환값이 없는 경우 (DELETE 등) + async deleteItem(id: string): Promise { + await this.ApiCall( + HttpMethod.DELETE, + `/item/${id}`, + null + ); + // 성공하면 그대로 종료, 실패하면 자동으로 에러 throw + } +} +``` + +**핵심 원칙**: +- `ApiCall` 반환값 = `UnwrapApiResponse` (success, error 필드 없음) +- 에러 처리는 `try-catch`로 (자동 throw) +- `response.success` 체크 코드는 타입 에러 발생 + --- ## 개요 @@ -368,9 +421,7 @@ const result = await analyzeText("오늘 날씨가 좋다."); - 권한: 팀 소유자만 - 팀 외 글(자유 주제, 다른 팀)은 제외 -**🆕 Content Hash 기반 3단계 캐싱** (2025-11-12): - -**캐싱 전략**: +**캐싱 전략** (Content Hash 기반 3단계): - **L1 (Client)**: localStorage에 contentHash를 키로 저장 (영구, LRU 10개) ~1ms - **L2 (Firestore)**: `patternAnalyses/{contentHash}` 컬렉션에 저장 (영구) ~100ms - **L3 (Server)**: In-memory Map에 저장 (5분 TTL, 최대 50개) ~50ms diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..43484bc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,589 @@ +# Changelog + +라온누리 프로젝트의 모든 주요 변경사항을 기록합니다. + +--- + +## [Unreleased] + +### 계획 중 +- 서버 사이드 Redis 캐싱 +- Rate Limiting 구현 + +--- + +## [2025-12-02] - 피드 시스템 및 영감 생성 + +### Added +- **피드 시스템 API**: 4개 엔드포인트 추가 + - GET /api/feed/daily-prompt - 오늘의 글감 조회 + - GET /api/feed/inspirations - 추천 영감 목록 + - GET /api/feed/weekly-stats - 주간 통계 + - GET /api/feed/team-activity - 팀 활동 + - POST /api/user/[uid]/weekly-goal - 주간 목표 설정 +- **FeedManager**: 피드 데이터 관리 (2분 캐싱) +- **피드 컴포넌트**: TodayTopicCard, WeeklyGoalCard, RecentActivityCard, InspirationCard +- **GlassCard 컴포넌트**: 반투명 배경 + 블러 효과 통일 +- **AI 영감 자동 생성**: Cloud Function `generateDailyInspirations` (매일 새벽) + - Vertex AI (Gemini 2.5 Flash) + - Unsplash API 이미지 검색/다운로드 + - Firebase Storage 저장 + - 크레딧 정보 저장 +- **AI 영감 수동 생성**: HTTP Function `generateInspirationsManual` +- **Vertex AI 라이브러리 변경**: @google-cloud/vertexai → @google/genai v1.29.0 +- **Unsplash API 키**: Secret Manager로 관리 +- **Container 레이아웃 확장**: maxW 1200px → 1400px +- **Tlab신영복체 폰트**: 글쓰기 페이지 제목에 적용 +- **ModeSelectionCard UI 개선**: 이미지 드롭 쉐도우, 전환 효과 + +### Changed +- Vertex AI 클라이언트: 싱글톤 패턴으로 인스턴스 재사용 +- Vertex AI 모델 업그레이드: gemini-1.5-flash → gemini-2.5-flash +- InspirationCard: Unsplash 크레딧 표시 제거 (저장은 유지) + +### Fixed +- Unsplash Rate Limit 방지: API 호출 간격 1초 추가 +- 영감 재생성 시 기존 데이터 덮어쓰기 옵션 + +--- + +## [2025-12-01] - 글쓰기 모드 선택 + +### Added +- **글쓰기 모드 선택 화면**: 글부터 쓰기 / 그림부터 올리기 +- **URL 파라미터**: ?mode=wrt|img로 모드 관리 +- **ImageDropzone 컴포넌트**: 드래그앤드롭 이미지 업로드, 미리보기 +- **ImageFirstLayout**: 이미지+에디터 2컬럼 레이아웃 +- **pendingImageStore**: IndexedDB 기반 이미지 임시 저장 (새로고침 유지) +- **캐릭터 이미지**: startWriting.png, uploadImage.png + +### Changed +- 모드 선택 카드 디자인: 그라데이션 배경, 상단 장식 바, 제목 배지 +- 모드 선택 카드 이미지: 크기 증가 180px → 240px +- 호버 애니메이션 개선 + +### Translations +- modeSelection namespace 추가 (ko/en/ja) + +--- + +## [2025-11-28] - 이미지 업로드 플로우 개편 + +### Added +- **/imageUpload 페이지**: AI 생성/직접 업로드 선택 +- **WritingManager.uploadUserImage()**: 클라이언트 사이드 이미지 리사이즈 + - Canvas API 사용 (1920x1080 최대, 85% 품질) + - Firebase Storage 업로드 +- **WritingManager.analyzeWritingBackground()**: fire-and-forget 패턴 +- **가격정책 페이지** (/pricing) + - 4개 플랜: Free, Classroom, Academy, School + - 월간/연간 결제 토글 (20% 할인) + - FAQ 섹션 +- **댓글 시스템** + - Comment 데이터 모델 (계층 구조, 반응형) + - CommentList/CommentItem/CommentInput 컴포넌트 + - API Routes 구현 (GET/POST/PUT/DELETE) + - 낙관적 업데이트 + +### Changed +- Write 페이지 저장 플로우: 저장 → 백그라운드 분석 → /imageUpload 리다이렉트 +- Interaction 페이지: 이미지 없으면 /imageUpload로 자동 이동 +- authStore.isLoading 초기값: false → true (리다이렉트 방지) + +### Removed +- GenerateImageDialog 제거 (플로우 변경) + +### Translations +- imageUpload namespace 추가 (ko/en/ja, 15개 키) +- pricing namespace 추가 (ko/en/ja, 40개 키) + +--- + +## [2025-11-27] - 팀 공개 설정 및 커버 이미지 + +### Added +- **팀 공개 설정 시스템** + - Team 타입 확장: isPublic, allowPublicWritings, description + - 팀 관리 페이지: 공개 설정 UI + - 공개 팀 조회 API (커서 기반 페이지네이션) + - 공개 글 조회 API (팀별) +- **팀 커버 이미지 시스템** + - Team.coverImage 필드 + - POST/DELETE /api/team/[teamId]/cover-image + - TeamCoverImageUploader 컴포넌트 (드래그앤드롭, 16:9) + - Firebase Storage 업로드 (5MB 제한) + - TeamCard 이미지 표시 +- **공개 팀 목록 페이지** (/team/all) + - TeamCard 컴포넌트 (글래스모피즘) + - 무한 스크롤 페이지네이션 +- **채점 시스템 전면 개편** + - 0~1 품질 기반 점수 (5단계) + - 계층적 설정 (기본→팀→주제) + - TeamScoringSettings/TeamRubricSettings 컴포넌트 + +### Changed +- 팀 상세 페이지: 3가지 뷰 모드 (멤버/공개/접근 불가) +- 팀원 아바타 표시: photoURL 지원, 정식/익명 아이콘 구분 + +### Translations +- teams namespace 추가 (공개 팀 목록) +- team.manage.publicSettings namespace +- team.coverImage namespace (16개 키) + +--- + +## [2025-11-26] - 익명 계정 연결 기능 + +### Added +- **POST /api/auth/merge-account**: 익명 계정 데이터 병합 API + - Firestore 데이터 마이그레이션 (writings, topics, comments 등) + - Realtime DB 데이터 마이그레이션 (drafts, monitoring) + - 원자성 보장 (Batch, Transaction) +- **서비스 레이어**: mergeAndLoginWithEmail, mergeAndLoginWithGoogle +- **authStore**: mergeWithEmail, mergeWithGoogle 액션 +- **LoginForm/SignupForm**: mode prop ('auth'|'link') +- **LoginDialog**: link 모드 지원 + +### Changed +- 용어 변경: "병합" → "연결", "익명" → "임시" + +### Removed +- LinkAccountFlow 컴포넌트 (기존 폼 재사용) + +### Translations +- linkAccountDescription, mergeButton, linkButton 등 15개 키 (ko/en/ja) + +--- + +## [2025-11-25] - 글 분석 및 이미지 왜곡 영역 + +### Added +- **POST /api/writing/[id]/analyze**: 서버 데이터 기반 글 분석 +- **상호작용 컴포넌트**: AnalysisNeededBanner, AreaUnlockBadge, ImprovementHint, ScoreBadge +- **점수 기반 영역 해금**: areaLimit.ts 로직 +- **DistortionAreaData**: 이미지 왜곡 영역 데이터 모델 +- **InteractiveImageViewer**: 왜곡 효과 + 애니메이션 +- **팀 멤버 닉네임**: POST /api/team/add-member에 nickname 파라미터 + +### Changed +- WritingAnalysis 타입 확장 +- 글 수정 시 왜곡 영역 저장/불러오기 + +--- + +## [2025-11-23] - 이미지 비율 유지 및 CORS + +### Added +- **Firebase Storage CORS 설정** + - firebase-storage-cors.json 생성 + - gsutil cors set 명령어로 적용 + - Canvas 이미지 조작 허용 + +### Changed +- **InteractiveImage 비율 유지** + - useEffect로 이미지 원본 크기 측정 + - Box에 aspectRatio 속성 적용 + - Canvas 찌그러짐 방지 + +### Fixed +- 캐시 무효화: cacheBustedSrc (?t=Date.now()) +- CDN 캐시 문제 해결 (쿼리 파라미터) + +--- + +## [2025-11-22] - Server Component 전환 및 BackButton + +### Added +- **BackButton 공통 컴포넌트** + - router.back() 기본 동작 + - href prop으로 특정 경로 이동 + - label prop으로 텍스트 커스터마이징 + +### Changed +- **글 상세보기 Server Component 전환** + - /writing/[writingId]/page.tsx + - Firebase Admin SDK 사용 + - SEO 최적화 (서버 HTML 생성) + - SNS 공유 미리보기 지원 + - 초기 로딩 성능 개선 + +--- + +## [2025-11-21] - 주제 생성 Dialog 통합 및 UI 개선 + +### Added +- **수정 모드 UX 대폭 개선** + - Sticky 헤더바 (오렌지 그라데이션, z-index:100) + - 강조된 테두리 (2px solid 오렌지) + - 섀도우 효과 (글로우) + - 색상 시스템 정립 + +### Changed +- **주제 생성 Dialog 통합** + - CreateTeamTopicDialog 삭제 (674줄) + - CreateTopicDialog로 통합 (개인/팀 공용) + - onSubmit 콜백 패턴 +- **홈 페이지 모듈화** + - 6개 컴포넌트 분리 (src/components/home/) + - 580줄 → 223줄 (62% 감소) +- **수정 모드 주제 변경 차단** + - TopicSelector readonly prop + - 데이터 무결성 보장 + +### Removed +- "새 글쓰기" 버튼 (write 페이지) +- 미사용 i18n 키 (newWriting, discardConfirm) + +--- + +## [2025-11-20] - AI 이미지 생성 스타일 개선 + +### Changed +- **Imagen 4.0 Fast 업그레이드** +- **일관된 스타일 가이드**: 애니메이션/만화 스타일 +- **한국 문화권 고려**: "Korean elementary student" 명시 +- **Negative Prompt 강화**: 과도한 사실주의, 어두운 분위기 차단 +- **키워드 개수 증가**: 6-12개 + +--- + +## [2025-11-19] - Draft 클라우드 동기화 및 팀 코드 개선 + +### Added +- **Draft 클라우드 동기화** + - localStorage + Realtime DB 하이브리드 + - syncStatus 필드 ('local'|'synced'|'syncing') + - DraftManager: syncToCloud, loadDraftsFromCloud, mergeDrafts + - SavedDraftsDialog 배지 표시 +- **팀 코드 예약 반환 로직** + - generate-code API: previousCode 파라미터 + - 새 코드 받기 시 이전 예약 해제 + - 중복 호출 방지 (isGeneratingCode) + +### Changed +- **AI Delta 전송 로직 개선** + - diff-match-patch 도입 + - 정확한 diff 계산 + - 5자 미만 변경 누적 + - 완화된 기준 (80%, 200자) + +### Security +- **XSS 방지**: HTML Sanitization 구현 (sanitize-html) + - 백엔드 자동 세탁 (src/lib/server/writing.ts) + - 26개 유닛 테스트 + +--- + +## [2025-11-18] - 다국어 및 팀 코드 개선 + +### Added +- **팀 코드 다국어 생성** + - 언어별 단어 목록 (한국어/영어/일본어) + - generateTeamCode(locale) +- **Realtime DB 팀 코드 예약 시스템** + - teamCodeReservation.ts 서버 레이어 + - Transaction 기반 atomic 예약 + - 5분 TTL, onDisconnect 자동 정리 + - Race condition 완전 방지 +- **팀 나가기 기능** + - 팀 상세 페이지 "팀 나가기" 버튼 + - POST /api/team/remove-member 권한 체크 +- **Level 1 중복 체크 로직 (UID 기반)** + - Custom Token API (익명 계정만 발급) + - 정식 계정 탈취 방지 +- **내가 쓴 글 목록 + 글 수정 기능** + - WritingCard 컴포넌트 + - /writings 전체 글 목록 페이지 + - /write 페이지 수정 모드 (?id=xxx) +- **주제별 학생 분석 API** + - GET /api/topic/[topicId]/writers + - TopicMemberAnalysisSection UI + +### Changed +- **팀 관리 컴포넌트 다국어 완성** + - TeamTopicManager, LiveWritingMonitor + - StudentLoginFlow 일본어 번역 + +### Translations +- team.create, team.detail, team.manage 일본어 (126개 키) +- writings 섹션 (ko/en/ja, 22개 키) +- errors.team namespace (6개 키) + +--- + +## [2025-11-17] - AI 이미지 생성 및 AI 설정 UI + +### Added +- **AI 이미지 생성 시스템** + - Vertex AI Imagen 3.0 통합 + - AI 장면 분리 기능 (Gemini Flash) + - AI 프롬프트 최적화 (키워드 추출) + - 4단계 플로우 (장면 추출 → 선택 → 최적화 → 생성) + - sceneExtractionService.ts, imagenService.ts, imageStorage.ts + - SceneSelector 컴포넌트 + - GenerateImageDialog 개선 +- **AI 설정 고급 UI** + - AIConfigDialog 컴포넌트 + - Slider 3개 (멈춤 감지/최대 힌트/쿨다운) + - 커스텀 힌트 레벨 카드 (우상단 삼각형 인디케이터) + +### Translations +- write.generateImage, write.extractScenes namespace (22개 키) +- team.manage.aiConfig namespace (28개 키, ko/en/ja) + +--- + +## [2025-11-14] - 개별 글 분석 및 AI 도우미 + +### Added +- **개별 글 분석 결과 저장** + - Writing.analysis 필드 + - SpellingError 타입 + - generateWritingContentHash 유틸 + - contentHash 기반 재분석 방지 (90% 비용 절감) +- **AI 글쓰기 도우미 시스템** + - 4단계 점진적 힌트 (질문 → 방향 → 선택지 → 예시) + - useWritingInactivityDetection 훅 (5분 멈춤 감지) + - InactivityPrompt, HintDisplay 컴포넌트 + - writingAssistanceService.ts + - POST /api/writing-assistance + - GET/PUT /api/team/[teamId]/ai-config + - Team.aiAssistanceConfig 필드 + +### Changed +- 글쓰기 페이지: 저장 시 AI 분석 병렬 수행 +- 실시간 분석 제거 (저장 시에만) + +### Translations +- aiAssist namespace (19개 키, ko/en/ja) + +--- + +## [2025-11-13] - 다국어 지원 시스템 (i18n) + +### Added +- **next-intl 라이브러리** 설치 및 설정 +- **[locale] 라우팅 구조**: /ko/*, /en/*, /ja/* +- **middleware.ts**: 브라우저 언어 자동 감지 +- **3개 언어 번역 파일**: messages/ko.json, en.json, ja.json (각 407줄, 220+ 키) +- **LocaleSwitcher 드롭다운**: 국기 이모지, 체크 표시 + +### Changed +- **전체 페이지 번역** + - Navbar (4개 링크) + - Landing 페이지 (20+ 항목) + - Home, Write, Team 페이지 + - 인증 컴포넌트 (70+ 항목) +- site.ts 텍스트 → 번역 파일로 이동 + +### Config +- next.config.ts: next-intl 플러그인 +- i18n/routing.ts, i18n/request.ts 설정 +- NEXT_LOCALE 쿠키 저장 + +--- + +## [2025-11-12] - 패턴 분석 캐싱 및 실시간 모니터링 + +### Added +- **Content Hash 기반 3단계 캐싱** + - L1: localStorage (영구, LRU 10개) ~1ms + - L2: Firestore patternAnalyses (영구) ~100ms + - L3: Server in-memory (5분, 50개) ~50ms + - AI 비용 절감 (전체 사용자 기준 1회 분석) +- **실시간 글쓰기 모니터링** + - Firebase Realtime Database 기반 + - WritingSessionManager (5초 주기) + - LiveWritingMonitor 컴포넌트 + - 3가지 상태 관리 (작성 중/나감/대기) + - 작성 속도 계산 (글자/분) + - Sparkline 그래프 (Area Chart) + - 30초 타임아웃 체크 +- **Writing API 구현 완료** + - POST/GET/PUT/DELETE /api/writing + - src/lib/server/writing.ts (Firestore CRUD) +- **패턴 분석 - 팀 소유자 기능** + - 3가지 분석 타입 (self/by-team/by-topic) + - TopicMemberAnalysisSection UI + +### Performance +- 캐시 히트: ~1ms (localStorage) +- 완전 무료 (Realtime DB 100명) + +--- + +## [2025-11-11] - 실시간 피드백 시스템 + +### Added +- **Vertex AI 기반 텍스트 분석** + - POST /api/analyze-text + - Delta 전송 지원 (40% 비용 절감) + - Multi-region failover (3개 region) + - 서버 캐싱 (LRU, 1분) +- **Google AI SDK 마이그레이션**: @google-cloud/vertexai → @google/genai v1.29.0 +- **텍스트 분석 평가 기준 개편**: 오감(4점) + 감정(2점) + 대화(2점) + 의성어(2점) +- **분석 히스토리 시스템**: Draft.analysisHistory (최대 5개) +- **맞춤법 검사 서비스**: spellingService.ts (독립 debounce 5초) +- **글 작성 패턴 분석**: WritingPatternDialog/Display +- **실시간 하이라이트** + - SpellingHighlight/SensoryWordHighlight Extensions + - EditorTooltip (Portal + ESC/외부 클릭) +- **Toast 알림**: 분석 시작/완료/실패 + +### Changed +- `descriptive` → `emotion` +- 프롬프트 최적화 (칭찬 강화, 제안 0~1개) + +--- + +## [2025-11-10] - 5단계 보안 레벨 시스템 + +### Added +- **TeamSecurityLevel enum** (1-5) + - OPEN, NAME_LIST, AUTH_REQUIRED, EMAIL_LIST, CLOSED +- **명단 관리 API** + - POST/DELETE /api/team/:teamId/allowed-names + - POST/DELETE /api/team/:teamId/allowed-emails + - POST /api/team/:teamId/security-level +- **SecurityLevelSelector**: RadioCard 기반 UI +- **다중 글조각 관리**: DraftManager (최대 10개) +- **SavedDraftsDialog**: syncStatus 배지 + +### Changed +- **User 타입 최소화** + - FirestoreUser/User 분리 + - Firebase Auth = Single Source of Truth +- **닉네임 저장 위치**: users.nicknames → team.members[uid].nickname +- **memberUids 제거**: Object.keys(members) 사용 + +### Removed +- useColorModeValue() 전체 제거 (Semantic token 사용) + +### Translations +- securitySelector namespace (13개 키, ko/en/ja) + +--- + +## [2025-11-07] - 매니저 패턴 API 전환 + +### Added +- **API 타입 시스템**: ApiResponse, HttpMethod Enum +- **클라이언트 캐싱**: BaseManager (TTL, 자동 무효화) +- **팀 주제 시스템**: 팀 소유자가 팀 주제 생성 +- **API 명세서**: API_SPEC.md (23개 엔드포인트) + +### Changed +- TeamManager, StudentManager → API 호출 방식 +- Firestore 직접 호출 제거 +- **아키텍처 단순화**: students → users 컬렉션, PIN 제거 +- TopicSelector: 팀/개인 배지 표시 + +### Removed +- 그룹(Group) 기능 완전 제거 + +--- + +## [2025-11-06] - 팀 코드 시스템 + +### Added +- **한글 팀 코드 생성**: "춤추는 파란 사자" +- **Anonymous Auth**: Firebase 익명 로그인 +- **teamService, studentService**: 백엔드 로직 +- **PIN SHA-256**: 암호화 저장 +- **정식 계정 연결**: linkWithCredential +- **팀 관리 UI**: 팀 목록, 생성, 멤버, 관리 페이지 +- **학생 관리 기능**: 이름 수정, 강퇴 (Menu + Dialog) + +### Changed +- 용어 변경: "클래스" → "팀", "교사" → "소유자" +- currentStudent 중심 authStore 재설계 +- Semantic token 적극 활용 + +--- + +## [2025-10-31] - 개인 주제 생성 UI + +### Added +- **CreateTopicDialog**: 태그 입력 필드 + - 키보드 네비게이션 + - 방향키 선택 + - Backspace/Delete 삭제 + +--- + +## [2025-10-30] - 글쓰기 페이지 및 주제 선택 + +### Added +- **글쓰기 페이지**: Tiptap 순수 텍스트 에디터 + - Editable 제목 + - LocalStorage 자동 저장 + - 하단 고정 버튼 +- **Firestore 연동**: 글 저장/불러오기/삭제 +- **Manager 패턴 도입**: WritingManager (싱글톤) +- **주제 선택 기능** + - TopicManager, TopicSelector + - 개인 주제 시스템 + - 템플릿 미리채우기 + +--- + +## [2025-10-29] - 인증 기반 라우팅 + +### Added +- **인증 기반 라우팅**: `/`와 `/home` 분리 +- 자동 리다이렉트 구현 + +--- + +## [2025-10-28] - 회원가입 및 테마 + +### Added +- **회원가입 기능** + - LoginDialog 페이드 전환 + - 비밀번호 강도 게이지 + - HIBP API 연동 +- **커스텀 테마**: Chakra UI v3 테마 시스템 +- **SEO 최적화**: 메타데이터, OpenGraph, StructuredData +- **랜딩 페이지**: Hero, Features, How It Works, CTA +- **네비게이션**: Navbar, 다크모드 지원 + +--- + +## [2025-10-27] - 인증 시스템 + +### Added +- **로그인 기능**: 이메일/비밀번호 +- **Google OAuth**: 소셜 로그인 +- **상태 관리**: Zustand 인증 스토어 + +--- + +## [2025-10-26] - Firebase 연동 + +### Added +- **Firebase Auth**: 설정 및 초기화 + +--- + +## [2025-10-25] - 프로젝트 초기화 + +### Added +- **프로젝트 설정**: Next.js 16, React 19, TypeScript + +--- + +## 범례 + +- **Added**: 새로운 기능 +- **Changed**: 기존 기능 변경 +- **Deprecated**: 곧 제거될 기능 +- **Removed**: 제거된 기능 +- **Fixed**: 버그 수정 +- **Security**: 보안 관련 변경 +- **Performance**: 성능 개선 +- **Translations**: 다국어 번역 추가/수정 +- **Config**: 설정 파일 변경 + +--- + +© 2024 BlueNovaLab. All rights reserved. \ No newline at end of file diff --git a/DATA_MODELS.md b/DATA_MODELS.md index f251d98..3d24f64 100644 --- a/DATA_MODELS.md +++ b/DATA_MODELS.md @@ -1,7 +1,5 @@ # 라온누리 - 데이터 모델 및 스키마 -> 최종 업데이트: 2025-11-28 (댓글 시스템 추가) - 이 문서는 Firestore 데이터베이스 및 Firebase Realtime Database 구조와 TypeScript 타입 정의를 설명합니다. **참고**: API 타입 정의는 [API_SPEC.md](./API_SPEC.md)를 참조하세요. diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..016386a --- /dev/null +++ b/DEVELOPMENT_GUIDE.md @@ -0,0 +1,517 @@ +# Development Guide + +라온누리 프로젝트의 구현 세부사항 가이드입니다. + +--- + +## AI Delta 전송 + +**diff-match-patch** 사용 (`src/app/api/analyze-text/route.ts`) + +### 동작 원리 + +- 5자 미만 변경은 누적 (previousText 유지) +- Delta 전송 기준: 5자 이상, 80% 미만, 200자 미만 +- 정확한 diff 계산 (앞/중간/뒤 수정 모두 감지) + +### 구현 예시 + +```typescript +// src/app/api/analyze-text/route.ts +import * as dmp from "diff-match-patch"; + +const differ = new dmp.diff_match_patch(); +const diffs = differ.diff_main(previousText, currentText); +differ.diff_cleanupSemantic(diffs); + +// 변경 비율 계산 +const changeRatio = calculateChangeRatio(diffs, previousText.length); + +// Delta 전송 판단 +if (changeSize >= 5 && changeRatio < 0.8 && changeSize < 200) { + // Delta 전송 + return analyzeText(currentText, previousText); +} else { + // 전체 전송 또는 스킵 +} +``` + +### 테스트 케이스 + +- ✅ 뒷부분 추가 +- ✅ 중간 수정 +- ✅ 앞부분 추가 +- ✅ 삭제 +- ✅ 대량 변경 +- ✅ 누적 변경 + +**참조**: `src/app/api/analyze-text/route.ts:46-120` + +--- + +## Draft 저장 + +**localStorage + Realtime DB 하이브리드** + +### 구조 + +- **DraftManager**: `syncToCloud()`, `mergeDrafts()` 사용 +- **Draft.syncStatus**: 'local' | 'synced' | 'syncing' +- **페이지 로드 시**: `mergeDrafts()` 호출 (로그인 시) + +### 동작 플로우 + +```typescript +// 1. 로컬 저장 (즉시) +draftManager.saveDraft(draft); // localStorage + +// 2. 클라우드 동기화 (2초 debounce) +draftManager.syncToCloud(draftId); // Realtime DB + +// 3. 기기 간 동기화 (페이지 로드 시) +const mergedDrafts = await draftManager.mergeDrafts(); +// updatedAt 비교로 최신 버전 선택 +``` + +### Realtime DB 구조 + +``` +drafts/ + {userId}/ + {draftId}/ + title: string + content: string + topicId?: string + updatedAt: number + syncStatus: 'synced' +``` + +### Security Rules + +```json +{ + "rules": { + "drafts": { + "$userId": { + ".read": "auth != null && auth.uid == $userId", + ".write": "auth != null && auth.uid == $userId" + } + } + } +} +``` + +**참조**: `src/managers/DraftManager.ts` + +--- + +## Firebase Cloud Functions + +### 파일 분리 구조 + +- **index.ts**: export만 +- **로직**: 별도 파일 + +```typescript +// functions/src/index.ts +import { cleanupExpiredReservations } from "./scheduledFunctions/cleanupExpiredReservations"; +import { cleanupExpiredDrafts } from "./scheduledFunctions/cleanupExpiredDrafts"; + +export { cleanupExpiredReservations, cleanupExpiredDrafts }; +``` + +### 배포된 Functions + +| 함수 | 타입 | 실행 주기 | 설명 | +|------|------|----------|------| +| `cleanupExpiredReservations` | Scheduled | 매 시간 | 팀 코드 예약 정리 | +| `cleanupExpiredDrafts` | Scheduled | 매일 새벽 3시 | 180일+ drafts 정리 | +| `generateDailyInspirations` | Scheduled | 매일 새벽 | AI 영감 자동 생성 | +| `generateInspirationsManual` | HTTP | 수동 호출 | 관리자 수동 영감 생성 | +| `onTeamDeleted` | Firestore Trigger | 팀 삭제 시 | Cascade 삭제 (writings 제외) | +| `onWritingCreated` | Firestore Trigger | 글 생성 시 | 추후 자동 분석 | + +### 공통 규칙 + +- **한글 로그**: 모든 logger 메시지 한글 작성 +- **리전**: asia-northeast1 (도쿄) +- **배포**: `cd functions && npm run build && firebase deploy --only functions` + +**참조**: `functions/src/` + +--- + +## AI 영감 생성 시스템 + +### 기술 스택 + +- **Vertex AI**: Gemini 2.5 Flash 모델 (`@google/genai`) +- **Unsplash API**: 이미지 검색 및 다운로드 +- **싱글톤 패턴**: `vertexAI.ts`에서 클라이언트 재사용 + +### 구현 + +```typescript +// functions/src/services/vertexAI.ts +import { GoogleGenerativeAI } from "@google/genai"; + +let vertexAI: GoogleGenerativeAI | null = null; + +export function getVertexAI(): GoogleGenerativeAI { + if (!vertexAI) { + vertexAI = new GoogleGenerativeAI({ + apiKey: process.env.GOOGLE_API_KEY, + }); + } + return vertexAI; +} +``` + +### 데이터 저장 + +```typescript +// Inspiration 타입 +interface Inspiration { + id: string; + title: string; + content: string; + imageUrl: string; + unsplashCredit?: { + name: string; + profileUrl: string; + }; + createdAt: Timestamp; +} +``` + +### 자동 생성 플로우 + +1. **Vertex AI**: 3개 영감 생성 (제목 + 내용) +2. **Unsplash API**: 키워드로 이미지 검색 +3. **Firebase Storage**: 이미지 다운로드 및 저장 +4. **Firestore**: `inspirations` 컬렉션에 저장 +5. **Rate Limit**: API 호출 간격 1초 + +**참조**: `functions/src/scheduledFunctions/generateDailyInspirations.ts` + +--- + +## 팀 코드 예약 시스템 + +### Race Condition 방지 + +**Realtime DB Transaction** 사용: + +```typescript +// src/lib/server/teamCodeReservation.ts +export async function generateAndReserveTeamCode( + userId: string, + locale: string = "ko" +): Promise { + const maxAttempts = 10; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const code = generateTeamCode(locale); + const codeRef = realtimeDb.ref(`teamCodeReservations/${code}`); + + // Atomic 예약 + const result = await codeRef.transaction((current) => { + if (current !== null) return; // 이미 예약됨 + + return { + userId, + createdAt: Date.now(), + expiresAt: Date.now() + 5 * 60 * 1000, // 5분 TTL + locale, + }; + }); + + if (result.committed) { + return code; // 성공 + } + } + + throw new Error("팀 코드 생성 실패"); +} +``` + +### 예약 해제 + +```typescript +// 새 코드 받기 시 이전 예약 해제 +export async function POST(request: NextRequest) { + const { previousCode } = await request.json(); + + if (previousCode) { + await releaseTeamCodeReservation(previousCode); + } + + const newCode = await generateAndReserveTeamCode(userId); + return successResponse({ code: newCode }); +} +``` + +### 실패 시 반환 + +```typescript +// 팀 생성 실패 시에도 예약 해제 +try { + const team = await createTeam({ code, ... }); + await releaseTeamCodeReservation(code); + return successResponse({ team }); +} catch (error) { + await releaseTeamCodeReservation(code); // catch 블록에서도 해제 + throw error; +} +``` + +**참조**: `src/lib/server/teamCodeReservation.ts` + +--- + +## XSS 보안 + +### 백엔드 자동 세탁 + +**`src/lib/server/writing.ts`**에서 모든 HTML 자동 sanitize + +```typescript +import { sanitizeHtml } from "@/utils/sanitizeHtml"; + +// 글 생성 시 +const sanitizedTitle = sanitizeHtml(data.title); +const sanitizedContent = sanitizeHtml(data.content); + +// 글 수정 시 +if (data.title) { + updateData.title = sanitizeHtml(data.title); +} +if (data.content) { + updateData.content = sanitizeHtml(data.content); +} +``` + +### sanitizeHtml 함수 + +**라이브러리**: `sanitize-html` + +```typescript +// src/utils/sanitizeHtml.ts +import sanitizeHtmlLib from "sanitize-html"; + +export function sanitizeHtml(dirty: string): string { + return sanitizeHtmlLib(dirty, { + allowedTags: [ + "p", "strong", "em", "h1", "h2", "h3", "h4", "h5", "h6", + "ul", "ol", "li", "a", "img", "blockquote", "code", "pre", + "br", "hr", "span", "div" + ], + allowedAttributes: { + a: ["href", "target", "rel"], + img: ["src", "alt", "width", "height"], + }, + allowedSchemes: ["http", "https", "mailto"], + }); +} +``` + +**특징**: +- ✅ 안전한 HTML 보존 +- ❌ `