diff --git a/API_SPEC.md b/API_SPEC.md index 38cc341..1ea434d 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -630,7 +630,91 @@ patternAnalyses/{contentHash} --- -### πŸ†• 11. POST `/team/:teamId/security-level` - λ³΄μ•ˆ 레벨 λ³€κ²½ +### πŸ†• 11. POST `/team/get-custom-token` - Custom Token 생성 (Level 1 재둜그인) +μ‹€μ œ URL: `POST /api/team/get-custom-token` + +**μ„€λͺ…**: Level 1 (OPEN) νŒ€μ—μ„œ 같은 λ‹‰λ„€μž„μœΌλ‘œ 재둜그인 μ‹œ μ‚¬μš©. 읡λͺ… κ³„μ •λ§Œ ν—ˆμš©. + +**인증**: λΆˆν•„μš” (곡개 API) + +**Request**: +```typescript +{ + uid: string; // λ‘œκ·ΈμΈν•˜λ €λŠ” μ‚¬μš©μžμ˜ Firebase UID +} +``` + +**Response**: +```typescript +// 성곡 (읡λͺ… 계정) +{ + success: true, + data: { + customToken: string; // Firebase Custom Token + } +} + +// μ‹€νŒ¨ (정식 계정) +{ + success: false, + error: "ν•΄λ‹Ή 이름은 λ‹€λ₯Έ μ‚¬μš©μžκ°€ μ‚¬μš© μ€‘μž…λ‹ˆλ‹€." +} +``` + +**λ³΄μ•ˆ**: +- βœ… 읡λͺ… 계정 검증: Firebase Admin SDK둜 `providerData` 확인 +- βœ… 정식 계정 차단: Google/Email 계정은 Custom Token λ°œκΈ‰ λΆˆκ°€ +- βœ… νƒˆμ·¨ λ°©μ§€: 정식 계정 μ΄λ¦„μœΌλ‘œ 타인이 둜그인 λΆˆκ°€ + +**μ‚¬μš© 흐름**: +1. ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ `team.members` 검색 β†’ 같은 λ‹‰λ„€μž„ 발견 +2. ν•΄λ‹Ή UID둜 Custom Token μš”μ²­ +3. 읡λͺ… 계정이면 Token λ°œκΈ‰, 정식 계정이면 403 +4. Token으둜 `signInWithCustomToken()` 호좜 + +--- + +### πŸ†• 12. POST `/team/remove-member` - νŒ€ 멀버 제거/λ‚˜κ°€κΈ° +μ‹€μ œ URL: `POST /api/team/remove-member` + +**μ„€λͺ…**: νŒ€μ—μ„œ 멀버λ₯Ό μ œκ±°ν•©λ‹ˆλ‹€. μ†Œμœ μžλŠ” λ‹€λ₯Έ 멀버λ₯Ό 강퇴할 수 있고, 일반 λ©€λ²„λŠ” 본인을 제거(νŒ€ λ‚˜κ°€κΈ°)ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +**인증**: ν•„μˆ˜ + +**Request**: +```typescript +{ + teamId: string; + uid: string; // μ œκ±°ν•  λ©€λ²„μ˜ UID +} +``` + +**Response**: +```typescript +{ + success: true +} +``` + +**κΆŒν•œ 체크**: +- βœ… **νŒ€ μ†Œμœ μž**: λ‹€λ₯Έ 멀버 강퇴 κ°€λŠ₯ (μžμ‹ μ€ λΆˆκ°€) +- βœ… **일반 멀버**: 본인만 제거 κ°€λŠ₯ (νŒ€ λ‚˜κ°€κΈ°) +- ❌ μ†Œμœ μžκ°€ 본인을 제거: "νŒ€ μ†Œμœ μžλŠ” νŒ€μ„ λ‚˜κ°ˆ 수 μ—†μŠ΅λ‹ˆλ‹€. νŒ€μ„ μ‚­μ œν•˜κ±°λ‚˜ μ†Œμœ κΆŒμ„ μ΄μ „ν•΄μ£Όμ„Έμš”." +- ❌ 일반 멀버가 타인을 제거: "νŒ€μ„ 관리할 κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€." + +**μ—λŸ¬**: +- 404: νŒ€μ„ 찾을 수 μ—†μŒ +- 403: κΆŒν•œ μ—†μŒ (μœ„ κΆŒν•œ 체크 μ°Έμ‘°) + +**μ‚¬μš© μ˜ˆμ‹œ**: +```typescript +// νŒ€ λ‚˜κ°€κΈ° +await teamManager.removeMember(teamId, currentUser.uid); +``` + +--- + +### πŸ†• 13. POST `/team/:teamId/security-level` - λ³΄μ•ˆ 레벨 λ³€κ²½ μ‹€μ œ URL: `POST /api/team/:teamId/security-level` **인증**: ν•„μˆ˜ (νŒ€ μ†Œμœ μžλ§Œ) diff --git a/DATA_MODELS.md b/DATA_MODELS.md index fa14d34..b671aa0 100644 --- a/DATA_MODELS.md +++ b/DATA_MODELS.md @@ -1,6 +1,6 @@ # λΌμ˜¨λˆ„λ¦¬ - 데이터 λͺ¨λΈ 및 μŠ€ν‚€λ§ˆ -> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-14 (κ°œλ³„ κΈ€ 뢄석 κ²°κ³Ό μ €μž₯) +> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-18 (νŒ€ μ½”λ“œ μ˜ˆμ•½ μ‹œμŠ€ν…œ + λ‹€κ΅­μ–΄ 생성) 이 λ¬Έμ„œλŠ” Firestore λ°μ΄ν„°λ² μ΄μŠ€ 및 Firebase Realtime Database ꡬ쑰와 TypeScript νƒ€μž… μ •μ˜λ₯Ό μ„€λͺ…ν•©λ‹ˆλ‹€. @@ -34,8 +34,10 @@ realtime-db β”‚ └── {userId}/ β”œβ”€β”€ previewRequests/ # πŸ†• 미리보기 μš”μ²­ β”‚ └── {userId}/ -└── previewResponses/ # πŸ†• 미리보기 응닡 - └── {requestId}/ +β”œβ”€β”€ previewResponses/ # πŸ†• 미리보기 응닡 +β”‚ └── {requestId}/ +└── teamCodeReservations/ # πŸ†• νŒ€ μ½”λ“œ μ˜ˆμ•½ (Race Condition λ°©μ§€) + └── {code-with-hyphens}/ # 예: "μΆ€μΆ”λŠ”-νŒŒλž€-μ‚¬μž" ``` **λ²”λ‘€**: @@ -44,6 +46,57 @@ realtime-db --- +## Realtime Database 데이터 λͺ¨λΈ + +### teamCodeReservations (νŒ€ μ½”λ“œ μ˜ˆμ•½) βœ… + +**경둜**: `teamCodeReservations/{code-with-hyphens}` + +**λͺ©μ **: νŒ€ μ½”λ“œ 생성 μ‹œ Race Condition λ°©μ§€ (Atomic μ˜ˆμ•½) + +**μŠ€ν‚€λ§ˆ**: +```typescript +interface TeamCodeReservation { + userId: string; // μ˜ˆμ•½ν•œ μ‚¬μš©μž UID + createdAt: number; // μ˜ˆμ•½ μ‹œκ° (timestamp) + expiresAt: number; // 만료 μ‹œκ° (createdAt + 5λΆ„) + locale?: string; // 생성 μ–Έμ–΄ (ko, en, ja) +} +``` + +**μ˜ˆμ‹œ 데이터**: +```json +{ + "teamCodeReservations": { + "μΆ€μΆ”λŠ”-νŒŒλž€-μ‚¬μž": { + "userId": "abc123...", + "createdAt": 1700000000000, + "expiresAt": 1700000300000, + "locale": "ko" + }, + "dancing-blue-lion": { + "userId": "def456...", + "createdAt": 1700000050000, + "expiresAt": 1700000350000, + "locale": "en" + } + } +} +``` + +**νŠΉμ§•**: +- βœ… **Atomic μ˜ˆμ•½**: Transaction으둜 λ™μ‹œ μš”μ²­ 처리 +- βœ… **5λΆ„ TTL**: μžλ™ 만료 (νŒ€ 생성 μ•ˆ ν•˜λ©΄ ν•΄μ œ) +- βœ… **μžλ™ 정리**: cleanupExpiredReservations() ν•¨μˆ˜ +- βœ… **언어별 μ½”λ“œ**: ko/en/ja 각각 λ‹€λ₯Έ 단어 μ‚¬μš© + +**Security Rules**: +- `.read`: λͺ¨λ‘ ν—ˆμš© (쀑볡 체크) +- `.write`: 본인만 μˆ˜μ • κ°€λŠ₯ +- `.validate`: userId, createdAt, expiresAt ν•„μˆ˜ + TTL 검증 + +--- + ## 1. Team (νŒ€) βœ… **μ»¬λ ‰μ…˜**: `teams/{teamId}` diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 102e280..7ab3490 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -1,6 +1,6 @@ # λΌμ˜¨λˆ„λ¦¬ - ν”„λ‘œμ νŠΈ ꡬ쑰 -> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-17 (AI 이미지 생성 μ‹œμŠ€ν…œ) +> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-18 (νŒ€ μ½”λ“œ λ‹€κ΅­μ–΄ 생성 + Realtime DB μ˜ˆμ•½ μ‹œμŠ€ν…œ) μ΄ˆλ“±ν•™μƒμ„ μœ„ν•œ μ°½μž‘ κΈ€μ“°κΈ° ꡐ윑 ν”Œλž«νΌ @@ -199,8 +199,9 @@ | νŽ˜μ΄μ§€ | 경둜 | μ„€λͺ… | μ£Όμš” κΈ°λŠ₯ | λ‹€κ΅­μ–΄ | |-------|------|------|---------|--------| | **λžœλ”© νŽ˜μ΄μ§€** | `/[locale]` | μ„œλΉ„μŠ€ μ†Œκ°œ 및 홍보 (λΉ„λ‘œκ·ΈμΈ μ „μš©) | Hero, Features, How It Works, CTA, Footer
둜그인 μ‹œ `/home`으둜 μžλ™ λ¦¬λ‹€μ΄λ ‰νŠΈ
πŸ†• **전체 λ²ˆμ—­ μ™„λ£Œ** (μ‚¬μ΄νŠΈλͺ…, νƒœκ·ΈλΌμΈ, λͺ¨λ“  μ„Ήμ…˜) | βœ… μ™„λ£Œ | -| **μœ μ € ν™ˆ** | `/[locale]/home` | 인증된 μ‚¬μš©μž λŒ€μ‹œλ³΄λ“œ | ν™˜μ˜ λ©”μ‹œμ§€, λΉ λ₯Έ μ‹œμž‘ λŒ€μ‹œλ³΄λ“œ, 졜근 ν™œλ™
λΉ„λ‘œκ·ΈμΈ μ‹œ `/`둜 μžλ™ λ¦¬λ‹€μ΄λ ‰νŠΈ
정식 계정은 "λ‚΄ νŒ€" μΉ΄λ“œ μΆ”κ°€ ν‘œμ‹œ
πŸ†• **전체 λ²ˆμ—­ μ™„λ£Œ** (μ›°μ»΄ λ©”μ‹œμ§€, λͺ¨λ“  μ•‘μ…˜ μΉ΄λ“œ) | βœ… μ™„λ£Œ | -| **κΈ€μ“°κΈ°** | `/[locale]/write` | Tiptap 기반 순수 ν…μŠ€νŠΈ 에디터 + 주제 선택 | 주제 선택 (자유 주제/개인 주제/νŒ€ 주제)
πŸ†• **주제 λ³€κ²½ κ²½κ³ ** (μž‘μ„± 쀑 λ‚΄μš© μ΄ˆκΈ°ν™” μ•Œλ¦Ό, μž„μ‹œ μ €μž₯ μ•ˆλ‚΄)
제λͺ© μž…λ ₯ (Editable), 순수 ν…μŠ€νŠΈ 에디터 (ν¬λ§·νŒ… μ—†μŒ)
πŸ†• **닀쀑 글쑰각 관리** (μ΅œλŒ€ 10개), "μƒˆ κΈ€μ“°κΈ°" / "μ €μž₯된 글쑰각" λ²„νŠΌ
πŸ†• **κ°•ν™”λœ μžλ™ μ €μž₯** (2초 debounce, μ €μž₯ μƒνƒœ ν‘œμ‹œ: μ €μž₯ 쀑/μ €μž₯됨)
πŸ†• **μ €μž₯ μ‹œ AI 뢄석** (μ‹€μ‹œκ°„ 뢄석 제거, μ €μž₯ λ²„νŠΌ 클릭 μ‹œ 뢄석 μˆ˜ν–‰)
πŸ†• **뢄석 κ²°κ³Ό DB μ €μž₯** (WritingAnalysis + spellingErrors + contentHash)
ν…œν”Œλ¦Ώ λ―Έλ¦¬μ±„μš°κΈ° (제λͺ©/λ‚΄μš©), Firestore μ €μž₯
λΉ„λ‘œκ·ΈμΈλ„ μ ‘κ·Ό κ°€λŠ₯ (μ €μž₯ μ‹œ 둜그인 μœ λ„) | βœ… μ™„λ£Œ | +| **μœ μ € ν™ˆ** | `/[locale]/home` | 인증된 μ‚¬μš©μž λŒ€μ‹œλ³΄λ“œ | ν™˜μ˜ λ©”μ‹œμ§€, λΉ λ₯Έ μ‹œμž‘ λŒ€μ‹œλ³΄λ“œ, **졜근 ν™œλ™ (졜근 κΈ€ 3개 ν‘œμ‹œ)**
λΉ„λ‘œκ·ΈμΈ μ‹œ `/`둜 μžλ™ λ¦¬λ‹€μ΄λ ‰νŠΈ
정식 계정은 "λ‚΄ νŒ€" μΉ΄λ“œ μΆ”κ°€ ν‘œμ‹œ
πŸ†• **WritingCard Grid, "λͺ¨λ‘ 보기" λ²„νŠΌ**
πŸ†• **전체 λ²ˆμ—­ μ™„λ£Œ** (μ›°μ»΄ λ©”μ‹œμ§€, λͺ¨λ“  μ•‘μ…˜ μΉ΄λ“œ) | βœ… μ™„λ£Œ | +| **λ‚΄ κΈ€ λͺ¨μŒ** | `/[locale]/writings` | πŸ†• **전체 κΈ€ λͺ©λ‘ νŽ˜μ΄μ§€** | πŸ†• **μ‚¬μš©μžμ˜ λͺ¨λ“  κΈ€ ν‘œμ‹œ (Grid)**
πŸ†• **μ •λ ¬ Select (μ΅œμ‹ μˆœ/였래된순)**
πŸ†• **WritingCard μ»΄ν¬λ„ŒνŠΈ μ‚¬μš©**
πŸ†• **Empty state 처리**
πŸ†• **전체 λ²ˆμ—­ μ™„λ£Œ** (ko/en/ja) | βœ… μ™„λ£Œ | +| **κΈ€μ“°κΈ°** | `/[locale]/write` | Tiptap 기반 순수 ν…μŠ€νŠΈ 에디터 + 주제 선택 | 주제 선택 (자유 주제/개인 주제/νŒ€ 주제)
πŸ†• **κΈ€ μˆ˜μ • κΈ°λŠ₯ (URL params ?id=xxx)**
πŸ†• **μˆ˜μ • λͺ¨λ“œ λ°°μ§€ ν‘œμ‹œ**
πŸ†• **주제 λ³€κ²½ κ²½κ³ ** (μž‘μ„± 쀑 λ‚΄μš© μ΄ˆκΈ°ν™” μ•Œλ¦Ό, μž„μ‹œ μ €μž₯ μ•ˆλ‚΄)
제λͺ© μž…λ ₯ (Editable), 순수 ν…μŠ€νŠΈ 에디터 (ν¬λ§·νŒ… μ—†μŒ)
πŸ†• **닀쀑 글쑰각 관리** (μ΅œλŒ€ 10개), "μƒˆ κΈ€μ“°κΈ°" / "μ €μž₯된 글쑰각" λ²„νŠΌ
πŸ†• **κ°•ν™”λœ μžλ™ μ €μž₯** (2초 debounce, μ €μž₯ μƒνƒœ ν‘œμ‹œ: μ €μž₯ 쀑/μ €μž₯됨)
πŸ†• **μ €μž₯ μ‹œ AI 뢄석** (μ‹€μ‹œκ°„ 뢄석 제거, μ €μž₯ λ²„νŠΌ 클릭 μ‹œ 뢄석 μˆ˜ν–‰)
πŸ†• **뢄석 κ²°κ³Ό DB μ €μž₯** (WritingAnalysis + spellingErrors + contentHash)
ν…œν”Œλ¦Ώ λ―Έλ¦¬μ±„μš°κΈ° (제λͺ©/λ‚΄μš©), Firestore μ €μž₯
λΉ„λ‘œκ·ΈμΈλ„ μ ‘κ·Ό κ°€λŠ₯ (μ €μž₯ μ‹œ 둜그인 μœ λ„) | βœ… μ™„λ£Œ | | **ν…ŒμŠ€νŠΈ** | `/[locale]/test` | νŒ€ μ½”λ“œ μ‹œμŠ€ν…œ ν…ŒμŠ€νŠΈ νŽ˜μ΄μ§€ | νŒ€ μ½”λ“œ 생성/검증 ν…ŒμŠ€νŠΈ
νŒ€/학생 생성 ν…ŒμŠ€νŠΈ
학생 둜그인 ν…ŒμŠ€νŠΈ
authStore μƒνƒœ 확인 | πŸ”œ μ˜ˆμ • | | **νŒ€ λͺ©λ‘** | `/[locale]/team` | λ‚΄κ°€ λ§Œλ“  νŒ€ λͺ©λ‘ (정식 계정 μ „μš©) | νŒ€ μΉ΄λ“œ κ·Έλ¦¬λ“œ, νŒ€ 정보 (μ½”λ“œ, 멀버 수, λ³΄μ•ˆ μ„€μ •)
"μƒˆ νŒ€ λ§Œλ“€κΈ°" λ²„νŠΌ | πŸ”œ μ˜ˆμ • | | **νŒ€ 생성** | `/[locale]/team/create` | μƒˆ νŒ€ λ§Œλ“€κΈ° (정식 계정 μ „μš©) | νŒ€ 이름 μž…λ ₯, νŒ€ μ½”λ“œ μžλ™ 생성
πŸ†• **5단계 λ³΄μ•ˆ 레벨 선택** (RadioCard, μ• λ‹ˆλ©”μ΄μ…˜)
πŸ†• **λͺ…단 관리 (Level 2/4)**: TagsInput으둜 Enter/μ‰Όν‘œ μž…λ ₯
생성 ν›„ `/team/[teamId]`둜 이동 | πŸ”œ μ˜ˆμ • | @@ -319,6 +320,7 @@ | **AIAssistancePanel** | `AIAssistancePanel.tsx` | πŸ†• **AI 도움 νŒ¨λ„** (레벨 선택, 남은 횟수, μΏ¨λ‹€μš΄) | βœ… μ™„λ£Œ | | **GenerateImageDialog** | `GenerateImageDialog.tsx` | πŸ†• **AI 이미지 생성 Dialog** (4단계 ν”Œλ‘œμš°: μž₯λ©΄ μΆ”μΆœ β†’ μž₯λ©΄ 선택 β†’ 이미지 생성 β†’ κ²°κ³Ό ν‘œμ‹œ) | βœ… μ™„λ£Œ | | **SceneSelector** | `SceneSelector.tsx` | πŸ†• **μž₯λ©΄ 선택 μ»΄ν¬λ„ŒνŠΈ** (RadioCard 기반, 이λͺ¨μ§€ ν‘œμ‹œ, 원문 미리보기) | βœ… μ™„λ£Œ | +| **WritingCard** | `WritingCard.tsx` | πŸ†• **κΈ€ μΉ΄λ“œ μ»΄ν¬λ„ŒνŠΈ** (제λͺ©, λ‚ μ§œ, 미리보기, 주제/점수/이미지 λ°°μ§€, 메뉴, μ‚­μ œ) | βœ… μ™„λ£Œ | | ~~**ScoreDisplay**~~ | ~~`ScoreDisplay.tsx`~~ | ~~μ‹€μ‹œκ°„ ν”Όλ“œλ°± 점수 ν‘œμ‹œ~~ | ❌ μ‚­μ œλ¨ (ν•˜μ΄λΌμ΄νŠΈλ‘œ λŒ€μ²΄) | | ~~**SpellingErrorDisplay**~~ | ~~`SpellingErrorDisplay.tsx`~~ | ~~λ§žμΆ€λ²• 였λ₯˜ ν‘œμ‹œ~~ | ❌ μ‚­μ œλ¨ (ν•˜μ΄λΌμ΄νŠΈλ‘œ λŒ€μ²΄) | | **CreateTopicDialog** | `CreateTopicDialog.tsx` | 개인 주제 생성 λ‹€μ΄μ–Όλ‘œκ·Έ (νƒœκ·Έ μž…λ ₯ UI) | βœ… μ™„λ£Œ | @@ -399,7 +401,7 @@ | **AIConfigDialog** | `AIConfigDialog.tsx` | πŸ†• **AI λ„μš°λ―Έ κ³ κΈ‰ μ„€μ • Dialog** (Slider, μ»€μŠ€ν…€ CheckboxCard) | βœ… μ™„λ£Œ | | **SecurityLevelSelector** | `SecurityLevelSelector.tsx` | 5단계 λ³΄μ•ˆ 레벨 선택 (RadioCard, framer-motion μ• λ‹ˆλ©”μ΄μ…˜) | βœ… μ™„λ£Œ | | **AllowListManager** | `AllowListManager.tsx` | λͺ…단 관리 (이름/이메일 μΆ”κ°€/제거, TagsInput) | βœ… μ™„λ£Œ | -| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | μ£Όμ œλ³„ 학생 κΈ€μ“°κΈ° 뢄석 (Accordion, by-topic 뢄석) | 🚧 UI만 (API μΆ”ν›„ κ΅¬ν˜„) | +| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | πŸ†• **μ£Όμ œλ³„ 학생 κΈ€μ“°κΈ° 뢄석** (Accordion, μ£Όμ œλ³„ 학생 λͺ©λ‘, κΈ€ 개수, by-topic 뢄석 연동) | βœ… μ™„λ£Œ | | **LiveWritingMonitor** | `LiveWritingMonitor.tsx` | πŸ†• **μ‹€μ‹œκ°„ κΈ€μ“°κΈ° λͺ¨λ‹ˆν„°λ§** (Firebase Realtime DB, 5초 μ£ΌκΈ°) | βœ… μ™„λ£Œ | **μ£Όμš” κΈ°λŠ₯**: @@ -546,7 +548,8 @@ | μœ ν‹Έλ¦¬ν‹° | 파일λͺ… | μ„€λͺ… | μƒνƒœ | |---------|--------|------|------| | **Korean Word List** | `koreanWordList.ts` | ν•œκΈ€ 감각 동사/ν˜•μš©μ‚¬ λͺ©λ‘ (점수 κ°€μ€‘μΉ˜, μ˜μ—­ λ³€ν™˜ ν•¨μˆ˜) | βœ… μ™„λ£Œ | -| **Team Code Generator** | `teamCodeGenerator.ts` | ν•œκΈ€ νŒ€ μ½”λ“œ 생성 (ν˜•μš©μ‚¬ + 색깔 + 동물) | βœ… μ™„λ£Œ | +| **Team Code Generator** | `teamCodeGenerator.ts` | ν•œκΈ€/μ˜μ–΄/일본어 νŒ€ μ½”λ“œ 생성 (ν˜•μš©μ‚¬ + 색깔 + 동물, locale νŒŒλΌλ―Έν„°) | βœ… μ™„λ£Œ | +| **πŸ†• i18n μœ ν‹Έλ¦¬ν‹°** | `i18n.ts` | μ„œλΉ„μŠ€ λ ˆμ΄μ–΄μš© λ²ˆμ—­ ν•¨μˆ˜ (detectLocale, t), nested key 지원, νŒŒλΌλ―Έν„° μΉ˜ν™˜ | βœ… μ™„λ£Œ | | **Password Security** | `passwordSecurity.ts` | HIBP API 연동 (유좜된 λΉ„λ°€λ²ˆν˜Έ 차단) | βœ… μ™„λ£Œ | | **Password Strength** | `passwordStrength.ts` | λΉ„λ°€λ²ˆν˜Έ 강도 계산 | βœ… μ™„λ£Œ | | **Content Hash** | `contentHash.ts` | πŸ†• **SHA-256 ν•΄μ‹œ 생성** (κΈ€ λͺ©λ‘ β†’ ν•΄μ‹œ, id+updatedAt μ‘°ν•©), πŸ†• **generateWritingContentHash** (κ°œλ³„ κΈ€ content ν•΄μ‹œ), μ„œλ²„/ν΄λΌμ΄μ–ΈνŠΈ 지원 | βœ… μ™„λ£Œ | @@ -597,14 +600,16 @@ | **AI μž₯λ©΄ μΆ”μΆœ** | `/api/extract-scenes` | POST | πŸ†• **κΈ€μ—μ„œ μ£Όμš” μž₯λ©΄ μΆ”μΆœ** (Gemini Flash, 3~5개 μž₯λ©΄, 각 μž₯면별 이미지 ν”„λ‘¬ν”„νŠΈ μžλ™ 생성) | βœ… μ™„λ£Œ | | **AI 이미지 생성** | `/api/generate-image` | POST | πŸ†• **μž₯λ©΄ 기반 이미지 생성** (πŸ†• **AI ν”„λ‘¬ν”„νŠΈ μ΅œμ ν™”**, Imagen 3.0, Firebase Storage μ €μž₯, Writing μ—…λ°μ΄νŠΈ) | βœ… μ™„λ£Œ | | **주제 CRUD** | `/api/topic` | GET, POST, PUT, DELETE | 주제 생성/쑰회/μˆ˜μ •/μ‚­μ œ (9개 μ—”λ“œν¬μΈνŠΈ) | βœ… μ™„λ£Œ | +| **μ£Όμ œλ³„ μž‘μ„±μž** | `/api/topic/[topicId]/writers` | GET | πŸ†• **주제둜 κΈ€ μ“΄ 학생 λͺ©λ‘** (κΈ€ 개수, Firebase Auth κ²°ν•©, κΈ€ 개수 λ‚΄λ¦Όμ°¨μˆœ) | βœ… μ™„λ£Œ | | **μ‚¬μš©μž 관리** | `/api/user` | GET, POST, PUT | μ‚¬μš©μž 쑰회/생성/μ—…λ°μ΄νŠΈ | βœ… μ™„λ£Œ | **μ„œλ²„ λ ˆμ΄μ–΄** (`src/lib/server/`): -- `team.ts` - νŒ€ Firestore CRUD, πŸ†• **getTeamAIConfig/updateTeamAIConfig** (AI μ„€μ • 관리) +- `team.ts` - νŒ€ Firestore CRUD, πŸ†• **getTeamAIConfig/updateTeamAIConfig** (AI μ„€μ • 관리), πŸ†• **generateUniqueTeamCode(locale)** (언어별 μ½”λ“œ 생성) - `user.ts` - μ‚¬μš©μž Firestore CRUD - `topic.ts` - 주제 Firestore CRUD -- πŸ†• `writing.ts` - κΈ€ Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner) +- πŸ†• `writing.ts` - κΈ€ Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner, πŸ†• **getTopicWriters**) - πŸ†• `patternAnalysis.ts` - νŒ¨ν„΄ 뢄석 κ²°κ³Ό Firestore μ €μž₯/쑰회 (contentHash ν‚€, 영ꡬ μ €μž₯) +- πŸ†• `teamCodeReservation.ts` - **νŒ€ μ½”λ“œ μ˜ˆμ•½ μ‹œμŠ€ν…œ** (Realtime DB transaction, atomic μ˜ˆμ•½, 5λΆ„ TTL, race condition λ°©μ§€) --- diff --git a/ROADMAP.md b/ROADMAP.md index e387979..83e674d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # λΌμ˜¨λˆ„λ¦¬ - 개발 λ‘œλ“œλ§΅ -> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-17 (AI 이미지 생성 μ‹œμŠ€ν…œ) +> μ΅œμ’… μ—…λ°μ΄νŠΈ: 2025-11-18 (νŒ€ μ½”λ“œ λ‹€κ΅­μ–΄ 생성 + Realtime DB μ˜ˆμ•½) μ΄ˆλ“±ν•™μƒμ„ μœ„ν•œ μ°½μž‘ κΈ€μ“°κΈ° ꡐ윑 ν”Œλž«νΌ 개발 κ³„νš @@ -89,6 +89,13 @@ | **AI κΈ€μ“°κΈ° λ„μš°λ―Έ μ‹œμŠ€ν…œ** | **4단계 점진적 힌트 μ‹œμŠ€ν…œ (질문 β†’ λ°©ν–₯ β†’ 선택지 β†’ μ˜ˆμ‹œ λ¬Έμž₯), useWritingInactivityDetection ν›… (5λΆ„ μž‘μ„± 멈좀 감지, 타이머 리셋, 남은 μ‹œκ°„), UI μ»΄ν¬λ„ŒνŠΈ 3개 (InactivityPrompt ν”Œλ‘œνŒ… λ²„νŠΌ, HintDisplay Dialog, AIAssistancePanel), writingAssistanceService.ts (Vertex AI, 주제 λ§₯락 ν™œμš©, μ„œλ²„ 캐싱), writingAssistance.ts ν”„λ‘¬ν”„νŠΈ (4단계 Γ— 3개 μ–Έμ–΄ ko/en/ja), κΈ€μ“°κΈ° νŽ˜μ΄μ§€ 톡합 (AI 도움 μš”μ²­, μ„œλ²„ μ—λŸ¬ 처리), POST /api/writing-assistance (주제 정보 전달, νŒ€ μ„€μ • 검증), GET/PUT /api/team/[teamId]/ai-config (AI μ„€μ • 쑰회/μ—…λ°μ΄νŠΈ), Team.aiAssistanceConfig ν•„λ“œ (enabled, detectionTimeMinutes, maxHintsPerWriting, cooldownMinutes, allowedHintLevels, requireSelfEdit), DEFAULT_AI_ASSISTANCE_CONFIG μƒμˆ˜, νŒ€ 관리 νŽ˜μ΄μ§€ AI On/Off ν† κΈ€ (Switch.Root v3), TeamManager AI λ©”μ„œλ“œ (getAIConfig, updateAIConfig), lib/server/team.ts AI ν•¨μˆ˜ (getTeamAIConfig, updateTeamAIConfig), μ„œλ²„ 검증 (enabled=false β†’ 403, allowedHintLevels 체크), μ‚¬μš© μ œν•œ (κΈ€λ‹Ή 5회, 3λΆ„ μΏ¨λ‹€μš΄), λ‹€κ΅­μ–΄ λ²ˆμ—­ μΆ”κ°€ (messages/*.json, aiAssist namespace 19개 ν‚€), successResponse() 헬퍼 μ‚¬μš©, AIAssistanceRecord νƒ€μž… (timestamp, hintLevel, topicId, topicTitle, context, hintProvided, wasUsed)** | **2025-11-14** | | **AI μ„€μ • κ³ κΈ‰ UI μ™„μ„±** | **AIConfigDialog μ»΄ν¬λ„ŒνŠΈ (νŒ€ 관리 νŽ˜μ΄μ§€), Slider 3개 (멈좀 감지/μ΅œλŒ€ 힌트/μΏ¨λ‹€μš΄), μ»€μŠ€ν…€ 힌트 레벨 μΉ΄λ“œ (반투λͺ… λ°°κ²½ 번호, μš°μƒλ‹¨ μ ‘νžŒ νŽ˜μ΄μ§€ μ‚Όκ°ν˜• 인디케이터, CSS border trick, 직접 onClick 처리), SimpleGrid 2x2 λ ˆμ΄μ•„μ›ƒ, λΈŒλžœλ“œ 컬러 μƒνƒœ λ³€ν™”, Switch (AI μ œμ•ˆ κ·ΈλŒ€λ‘œ μ‚¬μš© κΈˆμ§€), ν•¨μˆ˜ν˜• μƒνƒœ μ—…λ°μ΄νŠΈ (ν΄λ‘œμ € ν•΄κ²°), λ‹€κ΅­μ–΄ 지원 (ko/en/ja, team.manage.aiConfig namespace 28개 ν‚€)** | **2025-11-17** | | **AI 이미지 생성 + μž₯λ©΄ 뢄리 + ν”„λ‘¬ν”„νŠΈ μ΅œμ ν™”** | **Vertex AI Imagen 3.0 톡합, AI μž₯λ©΄ 뢄리 κΈ°λŠ₯ (Gemini Flash), AI ν”„λ‘¬ν”„νŠΈ μ΅œμ ν™” (Gemini Flash-Lite, 원문 β†’ 핡심 ν‚€μ›Œλ“œ λ°°μ—΄ μΆ”μΆœ β†’ μ‰Όν‘œ μ—°κ²°, Fallback μ•ˆμ „μ„±), 4단계 ν”Œλ‘œμš° (μž₯λ©΄ μΆ”μΆœ β†’ μž₯λ©΄ 선택 β†’ ν”„λ‘¬ν”„νŠΈ μ΅œμ ν™” β†’ 이미지 생성 β†’ κ²°κ³Ό ν‘œμ‹œ), Scene/SceneExtractionResponse νƒ€μž… μΆ”κ°€, sceneExtractionService.ts (κΈ€ β†’ 3~5개 μž₯λ©΄ μžλ™ μΆ”μΆœ, μž₯면별 ν”„λ‘¬ν”„νŠΈ 생성), sceneExtraction.ts ν”„λ‘¬ν”„νŠΈ (ko/en/ja, Response Schema), promptOptimization.ts ν”„λ‘¬ν”„νŠΈ (ν‚€μ›Œλ“œ μΆ”μΆœ, ko/en/ja), POST /api/extract-scenes (μž₯λ©΄ μΆ”μΆœ API), SceneSelector μ»΄ν¬λ„ŒνŠΈ (RadioCard, 이λͺ¨μ§€, 원문 미리보기 80자, ItemHiddenInput μΆ”κ°€), GeneratedImage νƒ€μž… (url, prompt, generatedAt, modelName), imagenService.ts (κΈ€ β†’ 어린이 μΉœν™”μ  ν”„λ‘¬ν”„νŠΈ μžλ™ λ³€ν™˜, optimizePromptForImage ν•¨μˆ˜, μ•ˆμ „ 필터링), vertexAI.ts ν™•μž₯ (generateImage ν•¨μˆ˜, multi-region failover μž¬μ‚¬μš©), imageStorage.ts (Firebase Storage μ—…λ‘œλ“œ, base64 β†’ 곡개 URL λ³€ν™˜, toDataURL 헬퍼), POST /api/generate-image (κΆŒν•œ 체크, AI ν”„λ‘¬ν”„νŠΈ μ΅œμ ν™”, Imagen API 호좜, Storage μ €μž₯, Writing.generatedImage ν•„λ“œ μ—…λ°μ΄νŠΈ), GenerateImageDialog κ°œμ„  (4단계 ν”Œλ‘œμš°, μž₯λ©΄ 선택 UI, "λ‹€λ₯Έ μž₯λ©΄ 선택" λ²„νŠΌ), κΈ€μ“°κΈ° νŽ˜μ΄μ§€ 톡합 ("κ΅¬μ²΄ν™”ν•˜κΈ°" λ²„νŠΌ, μ €μž₯ μ™„λ£Œ ν›„ ν‘œμ‹œ, ν™ˆ 이동 제거), Firebase Storage μ΄ˆκΈ°ν™” (fbStorage), λ‹€κ΅­μ–΄ λ²ˆμ—­ (write.generateImage + write.extractScenes namespace, ko/en/ja 22개 ν‚€), νƒ€μž… 체크 톡과** | **2025-11-17** | +| **λ‚΄κ°€ μ“΄ κΈ€ λͺ©λ‘ + κΈ€ μˆ˜μ • κΈ°λŠ₯** | **WritingCard μ»΄ν¬λ„ŒνŠΈ (제λͺ©, λ‚ μ§œ, 미리보기, 주제/점수/이미지 λ°°μ§€, μ‚­μ œ 메뉴, framer-motion μ‚­μ œ μ• λ‹ˆλ©”μ΄μ…˜, layoutId), /home νŽ˜μ΄μ§€ 졜근 κΈ€ 3개 ν‘œμ‹œ (SimpleGrid, "λͺ¨λ‘ 보기" λ²„νŠΌ, AnimatePresence), /writings 전체 κΈ€ λͺ©λ‘ νŽ˜μ΄μ§€ (μ •λ ¬ Select, empty state, AnimatePresence), λ‹€κ΅­μ–΄ λ²ˆμ—­ μΆ”κ°€ (writings μ„Ήμ…˜ ko/en/ja 22개 ν‚€, home.recentActivity.viewAll), /write νŽ˜μ΄μ§€ κΈ€ μˆ˜μ • κΈ°λŠ₯ (URL params ?id=xxx, useSearchParams, isEditMode μƒνƒœ, getWriting으둜 λ‘œλ“œ, updateWriting 호좜, μˆ˜μ • λͺ¨λ“œ λ°°μ§€), λ²ˆμ—­ μΆ”κ°€ (write.editMode/editModeDesc/editModeBadge/writingNotFound/loadFailed)** | **2025-11-18** | +| **μ£Όμ œλ³„ 학생 뢄석 API** | **GET /api/topic/[topicId]/writers κ΅¬ν˜„ (νŒ€ 주제둜 κΈ€ μ“΄ 학생 λͺ©λ‘ + κΈ€ 개수), TopicWriter νƒ€μž… μΆ”κ°€ (uid, name, email, writingCount), getTopicWriters μ„œλ²„ ν•¨μˆ˜ (writings 쿼리, userId κ·Έλ£Ήν™”, Firebase Auth μ‚¬μš©μž 정보 κ²°ν•©, κΈ€ 개수 λ‚΄λ¦Όμ°¨μˆœ μ •λ ¬), TopicManager.getTopicWriters λ©”μ„œλ“œ (2λΆ„ 캐싱), TopicMemberAnalysisSection UI μ™„μ„± (Accordion, μ£Όμ œλ³„ 학생 λͺ©λ‘, "이 주제 뢄석" λ²„νŠΌ, by-topic 뢄석 연동), κΆŒν•œ 체크 (νŒ€ μ†Œμœ μžλ§Œ μ ‘κ·Ό), λ‹€κ΅­μ–΄ λ²ˆμ—­ μΆ”κ°€ (team.manage.topicAnalysis namespace, ko/en/ja 8개 ν‚€), μ•ˆλ‚΄ λ©”μ‹œμ§€ 제거** | **2025-11-18** | +| **νŒ€ 관리 μ»΄ν¬λ„ŒνŠΈ λ‹€κ΅­μ–΄ μ™„μ„±** | **TeamTopicManager λ‹€κ΅­μ–΄ 처리 (team.manage.teamTopics, 18개 ν‚€), LiveWritingMonitor λ‹€κ΅­μ–΄ 처리 (team.manage.liveMonitor, 27개 ν‚€), team/[teamId]/page.tsx λ‹€κ΅­μ–΄ 처리 (κΈ°μ‘΄ ν‚€ ν™œμš©), StudentLoginFlow 일본어 λ²ˆμ—­ (61개 ν‚€), team.create 일본어 λ²ˆμ—­ (21개 ν‚€), team.detail/manage 일본어 λ²ˆμ—­ (45개 ν‚€), securitySelector 일본어 λ²ˆμ—­ (13개 ν‚€)** | **2025-11-18** | +| **νŒ€ μ½”λ“œ λ‹€κ΅­μ–΄ 생성 + Realtime DB μ˜ˆμ•½ μ‹œμŠ€ν…œ** | **언어별 단어 λͺ©λ‘ μΆ”κ°€ (μ˜μ–΄ 130개, 일본어 124개), generateTeamCode(locale) ν•¨μˆ˜ μˆ˜μ •, teamCodeReservation.ts μ„œλ²„ λ ˆμ΄μ–΄ (Realtime DB transaction, 5λΆ„ TTL, onDisconnect μžλ™ 정리), generateAndReserveTeamCode ν•¨μˆ˜ (atomic μ˜ˆμ•½, race condition μ™„μ „ λ°©μ§€), releaseTeamCodeReservation ν•¨μˆ˜ (νŒ€ 생성 ν›„ μ˜ˆμ•½ ν•΄μ œ), database.rules.json μ—…λ°μ΄νŠΈ (teamCodeReservations κ·œμΉ™, userId 검증, TTL 검증), POST /api/team/generate-code μˆ˜μ • (인증 ν•„μˆ˜, μ˜ˆμ•½ μ‹œμŠ€ν…œ μ‚¬μš©), POST /api/team μˆ˜μ • (νŒ€ 생성 ν›„ μ˜ˆμ•½ ν•΄μ œ), TeamManager.generateUniqueTeamCode(locale) νŒŒλΌλ―Έν„° μΆ”κ°€, νŒ€ 생성 νŽ˜μ΄μ§€ "λ‹€μ‹œ μƒμ„±ν•˜κΈ°" λ²„νŠΌ μΆ”κ°€, λ²ˆμ—­ ν‚€ μΆ”κ°€ (regenerateCode, codeGenerateFailed, ko/en/ja)** | **2025-11-18** | +| **νŒ€ λ‚˜κ°€κΈ° κΈ°λŠ₯** | **νŒ€ 상세 νŽ˜μ΄μ§€μ— "νŒ€ λ‚˜κ°€κΈ°" λ²„νŠΌ μΆ”κ°€ (λ©€λ²„λ§Œ ν‘œμ‹œ), Dialog 확인창, teamManager.removeMember() 호좜, POST /api/team/remove-member κΆŒν•œ 체크 μˆ˜μ • (본인 제거 ν—ˆμš©, μ†Œμœ μž 제거 κΈˆμ§€), 성곡 μ‹œ νŒ€ λͺ©λ‘μœΌλ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ, λ²ˆμ—­ μΆ”κ°€ (team.detail namespace, leaveTeam/leaveTeamConfirm λ“± 7개 ν‚€, ko/en/ja)** | **2025-11-18** | +| **Level 1 쀑볡 체크 둜직 (UID 기반)** | **loginAsUser() ν•¨μˆ˜ μˆ˜μ • (currentUid νŒŒλΌλ―Έν„° μΆ”κ°€), Level 1 λ‹‰λ„€μž„ 쀑볡 체크 (team.members 검색), 4κ°€μ§€ μΌ€μ΄μŠ€ 처리 (λΉ„λ‘œκ·ΈμΈ/둜그인 Γ— 쀑볡 유무), Custom Token API 생성 (POST /api/team/get-custom-token, 읡λͺ… κ³„μ •λ§Œ λ°œκΈ‰), λΉ„λ‘œκ·ΈμΈ μƒνƒœ 쀑볡 μ‹œ κΈ°μ‘΄ κ³„μ •μœΌλ‘œ μžλ™ 둜그인, 둜그인 μƒνƒœ 쀑볡 μ‹œ μ—λŸ¬ (UID 일치 체크), 정식 계정 νƒˆμ·¨ λ°©μ§€ (providerData 체크), authStore.loginAsUserμ—μ„œ currentUid 전달, StudentLoginFlow μ—λŸ¬ λ©”μ‹œμ§€ 처리, λ²ˆμ—­ μΆ”κ°€ (errors.team namespace, alreadyJoinedTeam/nicknameInUse λ“± 6개 ν‚€, ko/en/ja)** | **2025-11-18** | +| **μ„œλΉ„μŠ€ λ ˆμ΄μ–΄ i18n μœ ν‹Έλ¦¬ν‹°** | **src/utils/i18n.ts 생성 (React ν›… 없이 λ²ˆμ—­ μ‚¬μš©), detectLocale() ν•¨μˆ˜ (URL path μš°μ„  β†’ navigator.language fallback), t() ν•¨μˆ˜ (nested key 지원, νŒŒλΌλ―Έν„° μΉ˜ν™˜), messages/*.json import, firebaseAuth.ts 전체 μ—λŸ¬ λ©”μ‹œμ§€ λ‹€κ΅­μ–΄ 처리 (getErrorMessage, loginAsUser, linkEmailPassword, linkGoogleAccount), convertFirebaseUser κΈ°λ³Έ 이름 λ‹€κ΅­μ–΄, λ²ˆμ—­ μΆ”κ°€ (errors.auth namespace 11개 + errors.team namespace 6개, ko/en/ja)** | **2025-11-18** | ### 🚧 μ§„ν–‰ 쀑 @@ -99,11 +106,7 @@ | ν•­λͺ© | μ„€λͺ… | μš°μ„ μˆœμœ„ | μ˜ˆμƒ 일정 | |-----|------|---------|---------| -| **μ„œλ²„ μ‚¬μ΄λ“œ Redis 캐싱** | **API Routes에 Redis 캐싱, Rate Limiting μΆ”κ°€** | 🟑 쀑간 | 2025-11-13 | -| λ‚΄κ°€ μ“΄ κΈ€ λͺ©λ‘ | `/home`에 졜근 κΈ€ ν‘œμ‹œ, κΈ€ λͺ©λ‘ νŽ˜μ΄μ§€ | πŸ”΄ λ†’μŒ | 2025-11-13 | -| κΈ€ μˆ˜μ • κΈ°λŠ₯ | κΈ°μ‘΄ κΈ€ λΆˆλŸ¬μ™€μ„œ μˆ˜μ • | πŸ”΄ λ†’μŒ | 2025-11-13 | -| 이미지 μ—…λ‘œλ“œ | Firebase Storage 연동 | 🟑 쀑간 | 2025-11-14 | -| μ£Όμ œλ³„ 학생 뢄석 API | GET /api/topic/[topicId]/writers (주제둜 κΈ€ μ“΄ 학생 λͺ©λ‘) | 🟑 쀑간 | 2025-11-14 | +| **μ„œλ²„ μ‚¬μ΄λ“œ Redis 캐싱** | **API Routes에 Redis 캐싱, Rate Limiting μΆ”κ°€** | 🟑 쀑간 | 2025-11-19 | **Phase 1 μ™„λ£Œ λͺ©ν‘œ**: 2025λ…„ 11μ›” 15일