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μΌ