# 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 | 함수 | 타입 | 실행 주기 | 설명 | |------|------|----------|------| | `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` --- ## 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 보존 - ❌ `