diff --git a/DATA_MODELS.md b/DATA_MODELS.md index 2f535dd..d71bcc6 100644 --- a/DATA_MODELS.md +++ b/DATA_MODELS.md @@ -32,14 +32,16 @@ firestore ``` realtime-db -โ”œโ”€โ”€ monitoring/ # ๐Ÿ†• ์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง +โ”œโ”€โ”€ monitoring/ # ๐Ÿ†• ์‹ค์‹œ๊ฐ„ ์ŠคํŠœ๋””์˜ค ๋ชจ๋‹ˆํ„ฐ๋ง โ”‚ โ””โ”€โ”€ {teamId}/ โ”‚ โ””โ”€โ”€ {topicId}/ -โ”‚ โ””โ”€โ”€ {userId}/ -โ”œโ”€โ”€ previewRequests/ # ๐Ÿ†• ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ -โ”‚ โ””โ”€โ”€ {userId}/ -โ”œโ”€โ”€ previewResponses/ # ๐Ÿ†• ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‘๋‹ต -โ”‚ โ””โ”€โ”€ {requestId}/ +โ”‚ โ””โ”€โ”€ {uid}/ +โ”œโ”€โ”€ sessionPresence/ # ๐Ÿ†• ์„ธ์…˜ ์กด์žฌ ์ถ”์  +โ”‚ โ””โ”€โ”€ {teamId}/ +โ”œโ”€โ”€ drafts/ # ๊ธ€์กฐ๊ฐ ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™” +โ”‚ โ””โ”€โ”€ {uid}/ +โ”œโ”€โ”€ handoffs/ # QR ์ด๋ฏธ์ง€ ํ•ธ๋“œ์˜คํ”„ +โ”‚ โ””โ”€โ”€ {handoffId}/ ``` **๋ฒ”๋ก€**: @@ -1007,25 +1009,32 @@ interface UserSticker { **๊ฒฝ๋กœ**: `monitoring/{teamId}/{topicId}/{userId}` ```typescript -interface WritingStats { - userId: string; // ์ž‘์„ฑ์ž UID - contentLength: number; // ํ˜„์žฌ ๊ธ€์ž ์ˆ˜ (๊ณต๋ฐฑ ํฌํ•จ) - wordCount: number; // ํ˜„์žฌ ๋‹จ์–ด ์ˆ˜ - topicId: string; // ํ˜„์žฌ ์ž‘์„ฑ ์ค‘์ธ ์ฃผ์ œ ID - lastUpdated: number; // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ (timestamp) - ํ•ญ์ƒ ๋ณ€๊ฒฝ๋จ +interface StudioMonitorSection { + key: string; // ์„น์…˜ ID (opening, blank_0, continuation_0 ๋“ฑ) + label: string; // ์„น์…˜ ๋ผ๋ฒจ (์‹œ์ž‘, ๋นˆ์นธ, ์ด์–ด์“ฐ๊ธฐ ๋“ฑ) + question?: string; // ๋นˆ์นธ ์งˆ๋ฌธ (optional) + text: string; // ํ˜„์žฌ ํ…์ŠคํŠธ (โ—‹โ—‹โ—‹โ—‹โ—‹ = ๋ฏธ์™„ ๋นˆ์นธ) } -interface SpeedDataPoint { - speed: number; // ์ž‘์„ฑ ์†๋„ (๊ธ€์ž/๋ถ„) - timestamp: number; // ๊ธฐ๋ก ์‹œ๊ฐ„ +interface StudioMonitorSnapshot { + userId: string; + topicId: string; + stage: StudioStage; // ํ˜„์žฌ ๋‹จ๊ณ„ (intro, upload, firstWriting ๋“ฑ) + stageIndex: number; // ๋‹จ๊ณ„ ์ธ๋ฑ์Šค (0~5) + stageCount: number; // ์ด ๋‹จ๊ณ„ ์ˆ˜ (6) + stageLabel: string; // ๋‹จ๊ณ„๋ช… (์•ˆ๋‚ด, ์‚ฌ์ง„, 1์ฐจ์ž‘๋ฌธ ๋“ฑ) + title: string; // ๊ธ€ ์ œ๋ชฉ + hasImage: boolean; // ๊ทธ๋ฆผ ์—…๋กœ๋“œ ์—ฌ๋ถ€ + seeds: {label: string; value: string}[]; // ์”จ์•— (์ฃผ์ธ๊ณต, ๋ฐฐ๊ฒฝ ๋“ฑ) + sections: StudioMonitorSection[]; // ํšŒ์ฐจ frame.roles ์ˆœ์„œ + closingLabel?: string; // ๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅ + lastUpdated: number; // ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ํƒ€์ž„์Šคํƒฌํ”„ } -interface MonitoringData extends WritingStats { - displayName: string; // ํ‘œ์‹œ ์ด๋ฆ„ - speedHistory: SpeedDataPoint[]; // ์†๋„ ํžˆ์Šคํ† ๋ฆฌ (์ตœ๊ทผ 10๊ฐœ) - currentSpeed: number; // ํ˜„์žฌ ์†๋„ (๊ธ€์ž/๋ถ„) - isActive: boolean; // ํ™œ์„ฑ ์ƒํƒœ (false๋ฉด ๋‚˜๊ฐ„ ์ƒํƒœ) - preview?: string; // ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์„ ํƒ์ ) +interface SessionPresenceEntry { + userId: string; + joinedAt: number; + lastSeen: number; } ``` @@ -1033,21 +1042,36 @@ interface MonitoringData extends WritingStats { ```json { "monitoring": { - "team_abc123": { + "team_abc": { "topic_xyz": { - "user_001": { - "userId": "user_001", - "contentLength": 1500, - "wordCount": 300, + "uid_001": { + "userId": "uid_001", "topicId": "topic_xyz", - "lastUpdated": 1731398400000 - }, - "user_002": { - "userId": "user_002", - "contentLength": 800, - "wordCount": 160, - "topicId": "topic_xyz", - "lastUpdated": 1731398395000 + "stage": "secondWriting", + "stageIndex": 3, + "stageCount": 6, + "stageLabel": "๋‹ค๋“ฌ๊ธฐ", + "title": "์šฐ๋ฆฌ์˜ ๋ชจํ—˜", + "hasImage": true, + "seeds": [ + {"label": "์ฃผ์ธ๊ณต", "value": "์šฉ๊ฐํ•œ ์—ฌ์šฐ"}, + {"label": "๋ฐฐ๊ฒฝ", "value": "์–ด๋‘์šด ์ˆฒ"} + ], + "sections": [ + { + "key": "opening", + "label": "์‹œ์ž‘", + "text": "ํ•œ ๋งˆ๋ฆฌ์˜ ์—ฌ์šฐ๊ฐ€..." + }, + { + "key": "blank_0", + "label": "๋นˆ์นธ", + "question": "์—ฌ์šฐ๋Š” ๋ฌด์—‡์„ ๋ณด์•˜์„๊นŒ?", + "text": "โ—‹โ—‹โ—‹โ—‹โ—‹" + } + ], + "closingLabel": "ํ–‰๋ณตํ•œ ๊ฒฐ๋ง๋กœ ๋๋‚ธ๋‹ค", + "lastUpdated": 1692614400000 } } } @@ -1055,74 +1079,10 @@ interface MonitoringData extends WritingStats { } ``` -**ํด๋ผ์ด์–ธํŠธ ์ธก ๊ณ„์‚ฐ** (์„ ์ƒ๋‹˜ ํ™”๋ฉด): -```typescript -// MonitoringData (UI์šฉ) -{ - userId: "user_001", - contentLength: 1500, - wordCount: 300, - topicId: "topic_xyz", - lastUpdated: 1731398400000, - displayName: "์ฒ ์ˆ˜", - currentSpeed: 420, // ๊ธ€์ž/๋ถ„ (ํด๋ผ์ด์–ธํŠธ ๊ณ„์‚ฐ) - isActive: true, // ํ™œ์„ฑ ์ƒํƒœ (30์ดˆ ํƒ€์ž„์•„์›ƒ ์ฒดํฌ) - speedHistory: [ // ์ตœ๊ทผ 10๊ฐœ (ํด๋ผ์ด์–ธํŠธ ๊ณ„์‚ฐ) - { speed: 480, timestamp: 1731398370000 }, - { speed: 420, timestamp: 1731398375000 }, - { speed: 360, timestamp: 1731398380000 }, - { speed: 0, timestamp: 1731398385000 }, // ๋ฉˆ์ถค! - { speed: 0, timestamp: 1731398390000 }, - { speed: 420, timestamp: 1731398395000 }, // ์žฌ์‹œ์ž‘ - { speed: 480, timestamp: 1731398400000 } - ] -} -``` - -**ํ™œ์„ฑ ์ƒํƒœ ํŒ๋‹จ ๋กœ์ง**: -```typescript -// 1. Firebase์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  โ†’ isActive: true -// 2. 30์ดˆ ํƒ€์ž„์•„์›ƒ ์ฒดํฌ -const timeSinceUpdate = Date.now() - lastUpdated; -if (timeSinceUpdate > 30000) { - isActive = false; // 30์ดˆ ์ด์ƒ ์—…๋ฐ์ดํŠธ ์—†์Œ โ†’ ๋‚˜๊ฐ -} - -// 3. Firebase์—์„œ ์‚ญ์ œ๋˜์–ด๋„ ๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€ -if (!newData[userId] && prevData[userId]) { - keep prevData[userId] with isActive: false; -} -``` - -#### 11.2. previewRequests (๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ) - -**๊ฒฝ๋กœ**: `previewRequests/{userId}/{requestId}` - -```typescript -interface PreviewRequest { - requestedBy: string; // ์š”์ฒญํ•œ ๊ด€๋ฆฌ์ž UID - timestamp: number; // ์š”์ฒญ ์‹œ๊ฐ„ - requestId: string; // ๊ณ ์œ  ์š”์ฒญ ID -} -``` - -#### 11.3. previewResponses (๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‘๋‹ต) - -**๊ฒฝ๋กœ**: `previewResponses/{requestId}` - -```typescript -interface PreviewResponse { - content: string; // ํ˜„์žฌ ์ž‘์„ฑ ์ค‘์ธ ๊ธ€ ๋‚ด์šฉ - timestamp: number; // ์‘๋‹ต ์‹œ๊ฐ„ - requestId: string; // ์š”์ฒญ ID (๋งค์นญ์šฉ) -} -``` - ### ์—…๋ฐ์ดํŠธ ์ฃผ๊ธฐ -- **ํ†ต๊ณ„ ์ „์†ก**: 5์ดˆ๋งˆ๋‹ค (ํ•™์ƒ์ด ํŒ€ ์ฃผ์ œ๋กœ ์ž‘์„ฑ ์ค‘์ผ ๋•Œ๋งŒ) +- **์Šค๋ƒ…์ƒท ์ „์†ก**: ์ฆ‰์‹œ 1ํšŒ + 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ, ๋ณ€๊ฒฝ ์‹œ ์ฆ‰์‹œ (800ms ๋””๋ฐ”์šด์Šค) - **์ž๋™ ์ •๋ฆฌ**: `onDisconnect().remove()`๋กœ ํŽ˜์ด์ง€ ์ดํƒˆ ์‹œ ์ž๋™ ์‚ญ์ œ -- **๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ์š”์ฒญ ์‹œ์—๋งŒ (10์ดˆ ํƒ€์ž„์•„์›ƒ) ### Security Rules @@ -1134,34 +1094,20 @@ interface PreviewResponse { "monitoring": { "$teamId": { "$topicId": { - "$userId": { - ".read": "auth != null && (auth.uid == $userId || root.child('teamOwners').child($teamId).val() == auth.uid)", - ".write": "auth != null && auth.uid == $userId" + ".read": "auth != null", + "$uid": { + ".write": "auth != null && auth.uid == $uid" } } } - }, - "previewRequests": { - "$userId": { - ".read": "auth != null && auth.uid == $userId", - ".write": "auth != null" - } - }, - "previewResponses": { - "$requestId": { - ".read": "auth != null", - ".write": "auth != null" - } } } } ``` **๊ถŒํ•œ**: -- ํ†ต๊ณ„ ์“ฐ๊ธฐ: ๋ณธ์ธ๋งŒ -- ํ†ต๊ณ„ ์ฝ๊ธฐ: ๋ณธ์ธ + ํŒ€ ์†Œ์œ ์ž -- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ: ๋ˆ„๊ตฌ๋‚˜ ์“ฐ๊ธฐ, ๋Œ€์ƒ์ž๋งŒ ์ฝ๊ธฐ -- ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‘๋‹ต: ๋ˆ„๊ตฌ๋‚˜ ์ฝ๊ธฐ/์“ฐ๊ธฐ (requestId๋กœ ํ•„ํ„ฐ๋ง) +- ์Šค๋ƒ…์ƒท ์“ฐ๊ธฐ: ๋ณธ์ธ๋งŒ +- ์Šค๋ƒ…์ƒท ์ฝ๊ธฐ: ์ธ์ฆ๋œ ๋ชจ๋“  ์‚ฌ์šฉ์ž **TypeScript**: `src/types/writingSession.ts` diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 32cedd9..4483ec3 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -471,26 +471,16 @@ - **์ฟ ํ‚ค ์ €์žฅ**: ์‚ฌ์šฉ์ž๊ฐ€ ์„ ํƒํ•œ ์–ธ์–ด ๊ธฐ์–ต (`NEXT_LOCALE`) - **site.ts ํ…์ŠคํŠธ ์ด๋™**: ์‚ฌ์ดํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋„ ๋ฒˆ์—ญ ํŒŒ์ผ๋กœ ๊ด€๋ฆฌ -**์—…๋ฐ์ดํŠธ** (2025-11-12 PM): -- ๐Ÿ“ก **์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ** - - Firebase Realtime Database ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  (Redis Pub/Sub ๋ฐฉ์‹) - - **๋ชจ๋“  ํŒ€ ๋ฉค๋ฒ„ ํ‘œ์‹œ** (getUsersByTeam) - - **3๊ฐ€์ง€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์ž๋™ ์ •๋ ฌ**: - - ๐ŸŸข ์ž‘์„ฑ ์ค‘ (isActive: true, lastUpdated < 30์ดˆ) - ์ดˆ๋ก ๋ฐฐ์ง€, ํ•‘ํฌ ํ…Œ๋‘๋ฆฌ - - ๐ŸŸ  ๋‚˜๊ฐ (isActive: false, ๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€) - ์ฃผํ™ฉ ๋ฐฐ์ง€, ์ฃผํ™ฉ ํ…Œ๋‘๋ฆฌ - - โšช ๋Œ€๊ธฐ ์ค‘ (ํ•œ ๋ฒˆ๋„ ์ž‘์„ฑ ์•ˆ ํ•จ) - ํšŒ์ƒ‰ ๋ฐฐ์ง€, ํˆฌ๋ช…๋„ 60% - - 5์ดˆ ์ฃผ๊ธฐ ์ž๋™ ์—…๋ฐ์ดํŠธ (๊ธ€์ž ์ˆ˜, ๋‹จ์–ด ์ˆ˜, lastUpdated ํƒ€์ž„์Šคํƒฌํ”„) - - **์ž‘์„ฑ ์†๋„ ๊ณ„์‚ฐ** (ํด๋ผ์ด์–ธํŠธ ์ธก, charDiff * 12 = ๊ธ€์ž/๋ถ„) - - **Sparkline ๊ทธ๋ž˜ํ”„** (Area Chart, ์ตœ๊ทผ 10๊ฐœ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ, 0 ํ‘œ์‹œ) - - **์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํˆดํŒ** (๋งˆ์šฐ์Šค ์˜ค๋ฒ„ ์‹œ ์†๋„ + ๋ช‡ ์ดˆ ์ „ ๋ฐ์ดํ„ฐ) - - ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ-์‘๋‹ต (๊ด€๋ฆฌ์ž ํด๋ฆญ โ†’ ํ•™์ƒ ์‘๋‹ต โ†’ Dialog, ์ž‘์„ฑ ์ค‘๋งŒ) - - **30์ดˆ ํƒ€์ž„์•„์›ƒ**: ์—…๋ฐ์ดํŠธ ์—†์œผ๋ฉด "๋‚˜๊ฐ" ์ฒ˜๋ฆฌ - - **๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€**: Firebase ์‚ญ์ œ๋˜์–ด๋„ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ์œ ์ง€ - - onDisconnect() ์ž๋™ ์ •๋ฆฌ (ํŽ˜์ด์ง€ ์ดํƒˆ ์‹œ ์„ธ์…˜ ์‚ญ์ œ) - - LiveWritingMonitor ์ปดํฌ๋„ŒํŠธ (์ฃผ์ œ Select, StudentMonitorCard) - - WritingSessionManager (Realtime DB ์ž‘์—…, ์ƒ์„ธ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ) - - ์™„์ „ ๋ฌด๋ฃŒ (๋™์‹œ ์ ‘์† 100๋ช…๊นŒ์ง€, 1GB/day) - - Security Rules (๋ณธ์ธ ์“ฐ๊ธฐ, ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ฝ๊ธฐ) +**์—…๋ฐ์ดํŠธ** (2026-08-18): +- ๐Ÿ“ก **์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ - ์ŠคํŠœ๋””์˜ค ๊ฐœํŽธ** + - Firebase Realtime Database ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  (์ŠคํŠœ๋””์˜ค ํ”Œ๋กœ์šฐ ์ „์šฉ) + - **StudioMonitorSnapshot ํŽ˜์ด๋กœ๋“œ**: ๋‹จ๊ณ„(stageIndex/stageLabel), ์ œ๋ชฉ, ๊ทธ๋ฆผ ์œ ๋ฌด, ์”จ์•—, ๋ฌธ์žฅ ์Šฌ๋กฏ(์งˆ๋ฌธ ํฌํ•จ), ๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅ + - **ํ•™์ƒ ํŽ˜์ด์ง€**: startMonitoring(teamId, topicId) ์ฆ‰์‹œ 1ํšŒ + 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ, buildMonitorSnapshot() ํ—ฌํผ๋กœ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ, 800ms ๋””๋ฐ”์šด์Šค๋กœ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์ „์†ก, onDisconnect().remove() ์ž๋™ ์ •๋ฆฌ + - **๊ต์‚ฌ ํŽ˜์ด์ง€**: LiveClassMonitor ์ปดํฌ๋„ŒํŠธ, subscribeToTopic ๊ตฌ๋…, 3๊ฐ€์ง€ ์ƒํƒœ (๋ฏธ์ ‘์†/์ ‘์†์ค‘/์ง„ํ–‰์ค‘ = 30์ดˆ ์ด๋‚ด ์—…๋ฐ์ดํŠธ), ์นด๋“œ ํ—ค๋” "{n}/{total}๋‹จ๊ณ„ ยท ๋‹จ๊ณ„ ์ด๋ฆ„", ํŽผ์นจ ์‹œ ์ œ๋ชฉยท๊ทธ๋ฆผ ์—ฌ๋ถ€ยท์”จ์•—ยท๋ฌธ์žฅ ์Šฌ๋กฏ(์งˆ๋ฌธ)ยท๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅยท๋งˆ์ง€๋ง‰ ํ™œ๋™ ํ‘œ์‹œ + - WritingSessionManager ๋ฉ”์„œ๋“œ: startMonitoring, pushSnapshot, stopMonitoring, subscribeToTopic, joinSessionPresence, subscribeToSessionPresence (debug logs ์ œ๊ฑฐ) + - **์ œ๊ฑฐ๋จ**: ๊ธ€์ž ์ˆ˜/๋‹จ์–ด ์ˆ˜ ํ†ต๊ณ„, ์ž‘์„ฑ ์†๋„ ๊ณ„์‚ฐ/Sparkline, previewRequests/previewResponses, ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ-์‘๋‹ต, ClassMonitorMockShowcase, LiveWritingMonitor mentions + - i18n: `team.manage.liveMonitor.*` โ†’ `team.manage.classMonitor.*` (statusActive, stageProgress, timeFormat, detail ์žฌ์ž‘์„ฑ, ko/en/ja) + - Security Rules: previewRequests/Responses ๊ทœ์น™ ์ œ๊ฑฐ, monitoring/sessionPresence/drafts/handoffs ์œ ์ง€ **์—…๋ฐ์ดํŠธ** (2025-11-12 AM): - โœ… **Writing API ๊ตฌํ˜„ ์™„๋ฃŒ** @@ -1137,7 +1127,7 @@ | **TeamManager** | `TeamManager.ts` | ํŒ€ ๊ด€๋ จ API ํ˜ธ์ถœ (์ƒ์„ฑ, ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ, ๋ฉค๋ฒ„ ๊ด€๋ฆฌ, ๐Ÿ†• **bulkCreateMembers**) | โœ… ์™„๋ฃŒ | | **UserManager** | `UserManager.ts` | ์‚ฌ์šฉ์ž ๊ด€๋ จ API ํ˜ธ์ถœ (์ƒ์„ฑ, ์กฐํšŒ, ์ˆ˜์ •, ๋‹‰๋„ค์ž„ ๊ด€๋ฆฌ) **[NEW]** | โœ… ์™„๋ฃŒ | | **DraftManager** | `DraftManager.ts` | ๊ธ€์กฐ๊ฐ ๊ด€๋ฆฌ (**localStorage + Realtime DB ํ•˜์ด๋ธŒ๋ฆฌ๋“œ**, ๊ธฐ๊ธฐ ๊ฐ„ ๋™๊ธฐํ™”, ์ตœ๋Œ€ 10๊ฐœ, CRUD, syncStatus) | โœ… ์™„๋ฃŒ | -| **WritingSessionManager** | `WritingSessionManager.ts` | ๐Ÿ†• **์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ์„ธ์…˜ ๊ด€๋ฆฌ** (Firebase Realtime DB ์ž‘์—…) | โœ… ์™„๋ฃŒ | +| **WritingSessionManager** | `WritingSessionManager.ts` | ๐Ÿ†• **์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ์„ธ์…˜ ๊ด€๋ฆฌ** (Firebase Realtime DB ์Šค๋ƒ…์ƒท/์„ธ์…˜ ์กด์žฌ ์ถ”์ ) | โœ… ์™„๋ฃŒ | | **WritingManager** | `WritingManager.ts` | ๊ธ€์“ฐ๊ธฐ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (CRUD, ํ†ต๊ณ„) | โœ… ์™„๋ฃŒ | | **TopicManager** | `TopicManager.ts` | ์ฃผ์ œ ๊ด€๋ จ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (CRUD, ํ…œํ”Œ๋ฆฟ ์ฒ˜๋ฆฌ) | โœ… ์™„๋ฃŒ | | **AIUsageManager** | `AIUsageManager.ts` | ๐Ÿ†• **AI ์‚ฌ์šฉ๋Ÿ‰ ๊ด€๋ฆฌ** (ํ”Œ๋žœ ์ •๋ณด, ์‚ฌ์šฉ๋Ÿ‰ ์กฐํšŒ, ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์ฒดํฌ, 1๋ถ„ ์บ์‹ฑ) | โœ… ์™„๋ฃŒ | @@ -1157,12 +1147,12 @@ - โœ… **ํƒ€์ž… ์•ˆ์ „์„ฑ**: Request/Response ํƒ€์ž… ์™„์ „ ์ •์˜ - โœ… **๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ UI ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ** -**WritingSessionManager ์ฃผ์š” ๋ฉ”์„œ๋“œ** (2025-11-12): -- `startMonitoring(teamId, topicId, getStatsCallback)`: 5์ดˆ ์ฃผ๊ธฐ ํ†ต๊ณ„ ์ „์†ก ์‹œ์ž‘ -- `stopMonitoring()`: ํ†ต๊ณ„ ์ „์†ก ์ค‘์ง€ (ํŽ˜์ด์ง€ ์ดํƒˆ ์‹œ) -- `subscribeToTopic(teamId, topicId, callback)`: ์‹ค์‹œ๊ฐ„ ๊ตฌ๋… (๊ด€๋ฆฌ์ž์šฉ) -- `requestPreview(targetUserId)`: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ (Promise ๋ฐ˜ํ™˜) -- `listenForPreviewRequests(onRequestCallback)`: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ ๋ฆฌ์Šค๋„ˆ (ํ•™์ƒ์šฉ) +**WritingSessionManager ์ฃผ์š” ๋ฉ”์„œ๋“œ** (2026-08-18): +- `startMonitoring(teamId, topicId, getSnapshot)`: ์ฆ‰์‹œ 1ํšŒ + 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ, onDisconnect().remove() +- `pushSnapshot(payload)`: ๋ณ€๊ฒฝ ์ฆ‰์‹œ ์ „์†ก (ํ•™์ƒ ํŽ˜์ด์ง€ 800ms ๋””๋ฐ”์šด์Šค ํ˜ธ์ถœ) +- `stopMonitoring()`: ๋ชจ๋‹ˆํ„ฐ๋ง ์ค‘์ง€ +- `subscribeToTopic(teamId, topicId, cb)`: ์‹ค์‹œ๊ฐ„ ๊ตฌ๋… (๊ต์‚ฌ์šฉ) +- `joinSessionPresence(teamId, topicId)`, `subscribeToSessionPresence(teamId, topicId, cb)`: ์„ธ์…˜ ์กด์žฌ ์ถ”์  **WritingManager ์ฃผ์š” ๋ฉ”์„œ๋“œ** (2025-11-28 ์ถ”๊ฐ€): - ๐Ÿ†• `uploadUserImage(writingId, file)`: ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ @@ -1249,7 +1239,7 @@ firebase functions:log --only cleanupExpiredReservations | **Scene ํƒ€์ž…** | `scene.ts` | ๐Ÿ†• **์žฅ๋ฉด ๋ฐ์ดํ„ฐ ๋ชจ๋ธ** (Scene, SceneExtractionResponse) | โœ… ์™„๋ฃŒ | | **Draft ํƒ€์ž…** | `draft.ts` | ๊ธ€์กฐ๊ฐ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (Draft, DraftListItem, **AnalysisHistoryItem**, **syncStatus**: 'local'\|'synced'\|'syncing') | โœ… ์™„๋ฃŒ | | **WritingPattern ํƒ€์ž…** | `writingPattern.ts` | **๊ธ€ ์ž‘์„ฑ ํŒจํ„ด ๋ถ„์„** ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (WritingPatternAnalysis) | โœ… ์™„๋ฃŒ | -| **WritingSession ํƒ€์ž…** | `writingSession.ts` | ๐Ÿ†• **์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง** ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (WritingStats, PreviewRequest/Response, MonitoringData) | โœ… ์™„๋ฃŒ | +| **WritingSession ํƒ€์ž…** | `writingSession.ts` | ๐Ÿ†• **์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง** ๋ฐ์ดํ„ฐ ๋ชจ๋ธ (StudioMonitorSnapshot, StudioMonitorSection, SessionPresenceEntry) | โœ… ์™„๋ฃŒ | | ~~**User ํƒ€์ž…**~~ | ~~`user.ts`~~ | ~~์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋ชจ๋ธ~~ | โš ๏ธ Deprecated (firestoreUser.ts๋กœ ๋ณ€๊ฒฝ) | | ~~**Student ํƒ€์ž…**~~ | ~~`student.ts`~~ | ~~ํ•™์ƒ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ~~ | โš ๏ธ Deprecated (user.ts๋กœ ๋Œ€์ฒด) | | **Topic ํƒ€์ž…** | `topic.ts` | ์ฃผ์ œ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ, TopicCategory/Difficulty/OwnerType Enum, ํŒ€ ์ฃผ์ œ ์œ ํ‹ธ ํ•จ์ˆ˜ | โœ… ์™„๋ฃŒ | @@ -1552,7 +1542,7 @@ project_w/ โ”‚ โ”œโ”€โ”€ firestoreUser.ts # โœ… ์‚ฌ์šฉ์ž ํƒ€์ž… (FirestoreUser, User) โ”‚ โ”œโ”€โ”€ draft.ts # โœ… ๊ธ€์กฐ๊ฐ ํƒ€์ž… โ”‚ โ”œโ”€โ”€ writingPattern.ts # โœ… ํŒจํ„ด ๋ถ„์„ ํƒ€์ž… -โ”‚ โ”œโ”€โ”€ writingSession.ts # ๐Ÿ†• ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ํƒ€์ž… (WritingStats, PreviewRequest/Response) +โ”‚ โ”œโ”€โ”€ writingSession.ts # ๐Ÿ†• ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ํƒ€์ž… (StudioMonitorSnapshot, StudioMonitorSection) โ”‚ โ”œโ”€โ”€ api/ # โœ… API Request/Response ํƒ€์ž… โ”‚ โ”‚ โ”œโ”€โ”€ topic.ts # โœ… ์ฃผ์ œ API (ํŒ€/๊ฐœ์ธ ์ฃผ์ œ) โ”‚ โ”‚ โ”œโ”€โ”€ team.ts # โœ… ํŒ€ API diff --git a/ROADMAP.md b/ROADMAP.md index 92adc23..67cb73a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -81,6 +81,7 @@ | **ํŒจํ„ด ๋ถ„์„ - ํŒ€ ์†Œ์œ ์ž ๊ธฐ๋Šฅ** | **3๊ฐ€์ง€ ๋ถ„์„ ํƒ€์ž… (self/by-team/by-topic), API ๊ถŒํ•œ ์ฒดํฌ, ํŒ€ ์ฃผ์ œ ํ•„ํ„ฐ๋ง, WritingPatternDialog Props ํ™•์žฅ, TopicMemberAnalysisSection (UI๋งŒ), ํŒ€ ๊ด€๋ฆฌ ํŽ˜์ด์ง€์— ๋ฉค๋ฒ„ ๋ถ„์„ ๋ฉ”๋‰ด** | **2025-11-12** | | **Content Hash ๊ธฐ๋ฐ˜ 3๋‹จ๊ณ„ ์บ์‹ฑ** | **๊ธ€ ๋ชฉ๋ก ํ•ด์‹œ ์ƒ์„ฑ (id+updatedAt), L1: localStorage (์˜๊ตฌ, LRU 10๊ฐœ), L2: Firestore patternAnalyses (์˜๊ตฌ), L3: Server in-memory (5๋ถ„, 50๊ฐœ), ๋ณ€๊ฒฝ ์ž๋™ ๊ฐ์ง€, AI ๋น„์šฉ ์ ˆ๊ฐ (์ „์ฒด ์‚ฌ์šฉ์ž ๊ธฐ์ค€ 1ํšŒ ๋ถ„์„), contentHash.ts + patternCacheManager.ts + patternAnalysis.ts** | **2025-11-12** | | **์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง** | **Firebase Realtime Database ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  (Redis Pub/Sub ๋ฐฉ์‹), WritingSessionManager (5์ดˆ ์ฃผ๊ธฐ ํ†ต๊ณ„ ์ „์†ก, ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ-์‘๋‹ต, ์ƒ์„ธ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ), LiveWritingMonitor (์ฃผ์ œ Select, ๋ชจ๋“  ํŒ€ ๋ฉค๋ฒ„ ํ‘œ์‹œ, StudentMonitorCard), 3๊ฐ€์ง€ ์ƒํƒœ ๊ด€๋ฆฌ (์ž‘์„ฑ ์ค‘/๋‚˜๊ฐ/๋Œ€๊ธฐ ์ค‘, ์ •๋ ฌ ์ˆœ์„œ, ์ƒ‰์ƒ๋ณ„ ์‹œ๊ฐํ™”), ๊ธ€์“ฐ๊ธฐ ํŽ˜์ด์ง€ ๋ชจ๋‹ˆํ„ฐ๋ง (contentRef ์ตœ์ ํ™”), onDisconnect() ์ž๋™ ์ •๋ฆฌ, ์ž‘์„ฑ ์†๋„ ๊ณ„์‚ฐ (ํด๋ผ์ด์–ธํŠธ, ๊ธ€์ž/๋ถ„, charDiff*12), Sparkline ๊ทธ๋ž˜ํ”„ (Area Chart, ์ตœ๊ทผ 10๊ฐœ, 0 ํ‘œ์‹œ), ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํˆดํŒ (์†๋„+์‹œ๊ฐ„), 30์ดˆ ํƒ€์ž„์•„์›ƒ ์ฒดํฌ (lastUpdated), ๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€ (Firebase ์‚ญ์ œ๋˜์–ด๋„ ์œ ์ง€), database.rules.json (topicId ๋ ˆ๋ฒจ ์ฝ๊ธฐ), @chakra-ui/charts+recharts, ์™„์ „ ๋ฌด๋ฃŒ (100๋ช…)** | **2025-11-12** | +| **์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ ์ŠคํŠœ๋””์˜ค ๊ฐœํŽธ** | **RTDB monitoring ํŽ˜์ด๋กœ๋“œ๋ฅผ StudioMonitorSnapshot(๋‹จ๊ณ„ยท์ œ๋ชฉยท์”จ์•—ยท๋ฌธ์žฅ ์Šฌ๋กฏยท์งˆ๋ฌธยท๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅ)์œผ๋กœ ๊ต์ฒด, ํ•™์ƒ ์ŠคํŠœ๋””์˜ค ํŽ˜์ด์ง€๊ฐ€ 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ+๋ณ€๊ฒฝ ์ฆ‰์‹œ ์ „์†ก(800ms ๋””๋ฐ”์šด์Šค), ๊ต์‚ฌ LiveClassMonitor๊ฐ€ "n/N๋‹จ๊ณ„"์™€ ์ง„ํ–‰ ๋‚ด์šฉ์„ ํŽผ์ณ ํ‘œ์‹œ. ๊ธ€์ž์ˆ˜ยท์†๋„ยทSparklineยท๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ-์‘๋‹ต(previewRequests/Responses)ยท๋ชฉ์—… ์‡ผ์ผ€์ด์Šค ์ œ๊ฑฐ, RTDB ๋ฃฐ ์ •๋ฆฌ, i18n classMonitor ์žฌ์ž‘์„ฑ(ko/en/ja)** | **2026-08-18** | | **๋‹ค๊ตญ์–ด ์ง€์› ์‹œ์Šคํ…œ (i18n)** | **next-intl ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜ ๋ฐ ์„ค์ •, [locale] ๋ผ์šฐํŒ… ๊ตฌ์กฐ ๋ณ€๊ฒฝ (/ko/*, /en/*, /ja/*), middleware.ts ์ƒ์„ฑ (๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด ์ž๋™ ๊ฐ์ง€, Accept-Language ํ—ค๋”), 3๊ฐœ ์–ธ์–ด ๋ฒˆ์—ญ ํŒŒ์ผ ์ƒ์„ฑ (messages/ko.json, messages/en.json, messages/ja.json - ๊ฐ 407์ค„, 220+ ํ‚ค), Navbar ๋ฒˆ์—ญ (4๊ฐœ ๋งํฌ), LocaleSwitcher ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด ๊ฐœ์„  (๊ตญ๊ธฐ ์ด๋ชจ์ง€ ๐Ÿ‡ฐ๐Ÿ‡ท ๐Ÿ‡บ๐Ÿ‡ธ ๐Ÿ‡ฏ๐Ÿ‡ต, ํ˜„์žฌ ์–ธ์–ด ์ฒดํฌ ํ‘œ์‹œ, Portal ์‚ฌ์šฉ), Landing ํŽ˜์ด์ง€ ์ „์ฒด ๋ฒˆ์—ญ (Hero, Features, Steps, CTA, Footer - 20+ ํ•ญ๋ชฉ), Home ํŽ˜์ด์ง€ ์ „์ฒด ๋ฒˆ์—ญ (Welcome, QuickStart 9๊ฐœ ์•ก์…˜ ์นด๋“œ, RecentActivity), ์ธ์ฆ ์ปดํฌ๋„ŒํŠธ ์ „์ฒด ๋ฒˆ์—ญ (LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog - 70+ ํ•ญ๋ชฉ), Team ํŽ˜์ด์ง€ ์ „์ฒด ๋ฒˆ์—ญ (List, Create, Detail, Manage - 60+ ํ•ญ๋ชฉ), Write ํŽ˜์ด์ง€ ๋ฒˆ์—ญ (๋ถ„์„/์ €์žฅ ๋ฉ”์‹œ์ง€, ๋ฒ„ํŠผ), ์ผ๋ณธ์–ด ์–ด๋ฆฐ์ด ์นœํ™”์  ํ‘œํ˜„ (ํ•œ์ž ์ตœ์†Œํ™”, ใฒใ‚‰ใŒใช ์šฐ์„ ), site.ts ํ…์ŠคํŠธ๋ฅผ ๋ฒˆ์—ญ ํŒŒ์ผ๋กœ ์ด๋™ (์‚ฌ์ดํŠธ๋ช…, ํƒœ๊ทธ๋ผ์ธ, ์ €์ž‘๊ถŒ), localeDetection: true ์„ค์ •, NEXT_LOCALE ์ฟ ํ‚ค ์ €์žฅ, next.config.ts์— next-intl ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€, i18n/routing.ts + i18n/request.ts ์„ค์ •, ํƒ€์ž… ์ฒดํฌ ํ†ต๊ณผ, CLAUDE.md์— ๋‹ค๊ตญ์–ด ํ•„์ˆ˜ ๊ทœ์น™ ์ถ”๊ฐ€** | **2025-11-13** | | **๊ฐœ๋ณ„ ๊ธ€ ๋ถ„์„ ๊ฒฐ๊ณผ ์ €์žฅ** | **Writing ํƒ€์ž…์— WritingAnalysis ํ•„๋“œ ์ถ”๊ฐ€, SpellingError ํƒ€์ž… ์ •์˜, generateWritingContentHash ์œ ํ‹ธ ์ถ”๊ฐ€, ๊ธ€์“ฐ๊ธฐ ํŽ˜์ด์ง€ ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ์ œ๊ฑฐ, ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ AI ๋ถ„์„ + ๋งž์ถค๋ฒ• ๊ฒ€์‚ฌ ๋ณ‘๋ ฌ ์ˆ˜ํ–‰, ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ DB์— ์ €์žฅ, ํŒจํ„ด ๋ถ„์„ ์‹œ ์ €์žฅ๋œ ๋ถ„์„ ์žฌ์‚ฌ์šฉ (contentHash ๋น„๊ต), ๋ณ€๊ฒฝ๋œ ๊ธ€๋งŒ ์žฌ๋ถ„์„ (90% ๋น„์šฉ ์ ˆ๊ฐ), ๋งž์ถค๋ฒ• ์—๋Ÿฌ ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋Šฅ ์™„์„ฑ, API ํƒ€์ž…/์„œ๋ฒ„ ํ•จ์ˆ˜/WritingManager ์ „์ฒด ์ˆ˜์ •, getTranslations()๋กœ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋‹ค๊ตญ์–ด ์ง€์›** | **2025-11-14** | | **AI ๊ธ€์“ฐ๊ธฐ ๋„์šฐ๋ฏธ ์‹œ์Šคํ…œ** | **4๋‹จ๊ณ„ ์ ์ง„์  ํžŒํŠธ ์‹œ์Šคํ…œ (์งˆ๋ฌธ โ†’ ๋ฐฉํ–ฅ โ†’ ์„ ํƒ์ง€ โ†’ ์˜ˆ์‹œ ๋ฌธ์žฅ), useWritingInactivityDetection ํ›… (5๋ถ„ ์ž‘์„ฑ ๋ฉˆ์ถค ๊ฐ์ง€, ํƒ€์ด๋จธ ๋ฆฌ์…‹, ๋‚จ์€ ์‹œ๊ฐ„), UI ์ปดํฌ๋„ŒํŠธ 2๊ฐœ (InactivityPrompt ํ”Œ๋กœํŒ… ๋ฒ„ํŠผ, HintDisplay Dialog), 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** | diff --git a/TECH_STACK.md b/TECH_STACK.md index b33216e..10663ec 100644 --- a/TECH_STACK.md +++ b/TECH_STACK.md @@ -64,7 +64,7 @@ | ๊ธฐ์ˆ  | ๋ฒ„์ „ | ์šฉ๋„ | |-----|------|------| -| **@chakra-ui/charts** | v3.29.0 | ๐Ÿ†• **Chakra UI ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ** (์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ทธ๋ž˜ํ”„) | +| **@chakra-ui/charts** | v3.29.0 | ๐Ÿ†• **Chakra UI ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ** (ํšŒ์› ์ถ”์ด ๊ทธ๋ž˜ํ”„ MemberTrendDialog) | | **recharts** | v3.4.1 | ๐Ÿ†• **์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ** (Area, Line, Bar ์ฐจํŠธ) | ### State Management @@ -486,96 +486,76 @@ const nickname = teamManager.getMemberNickname(team, uid, user?.name); โ””โ”€> migrateLegacyDraft() - ๊ธฐ์กด ๋‹จ์ผ draft ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ``` -### 4. ์‹ค์‹œ๊ฐ„ ๊ธ€์“ฐ๊ธฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์•„ํ‚คํ…์ฒ˜ (Firebase Realtime Database) +### 4. ์‹ค์‹œ๊ฐ„ ์ŠคํŠœ๋””์˜ค ๋ชจ๋‹ˆํ„ฐ๋ง ์•„ํ‚คํ…์ฒ˜ (Firebase Realtime Database) ``` -๊ด€๋ฆฌ์ž (ํŒ€ ๊ด€๋ฆฌ ํŽ˜์ด์ง€) +๊ต์‚ฌ (ํŒ€ ๊ด€๋ฆฌ ํŽ˜์ด์ง€) โ†“ ์ฃผ์ œ ์„ ํƒ โ†“ -LiveWritingMonitor ์ปดํฌ๋„ŒํŠธ - โ”œโ”€> ์ฃผ์ œ ๋“œ๋กญ๋‹ค์šด (teamTopics) +LiveClassMonitor ์ปดํฌ๋„ŒํŠธ โ””โ”€> subscribeToTopic(teamId, topicId, callback) โ†“ Firebase Realtime DB ๊ตฌ๋… - monitoring/{teamId}/{topicId}/{userId} - โ†‘ 5์ดˆ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ -ํ•™์ƒ (๊ธ€์“ฐ๊ธฐ ํŽ˜์ด์ง€) - โ”œโ”€> ํŒ€ ์ฃผ์ œ ์„ ํƒ ๊ฐ์ง€ - โ””โ”€> startMonitoring(teamId, topicId, getStats) - โ”œโ”€> 5์ดˆ๋งˆ๋‹ค ํ†ต๊ณ„ ์ „์†ก (contentLength, wordCount) - โ”œโ”€> onDisconnect().remove() ์„ค์ • - โ””โ”€> ํŽ˜์ด์ง€ ์ดํƒˆ ์‹œ ์ž๋™ ์ •๋ฆฌ + monitoring/{teamId}/{topicId}/{uid} -๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ-์‘๋‹ต ํ”Œ๋กœ์šฐ - ๊ด€๋ฆฌ์ž: requestPreview(userId) - โ†“ ์š”์ฒญ ์ƒ์„ฑ - previewRequests/{userId}/{requestId} - โ†“ ํ•™์ƒ ๋ฆฌ์Šค๋„ˆ ๊ฐ์ง€ - ํ•™์ƒ: listenForPreviewRequests(callback) - โ†“ ํ˜„์žฌ ๊ธ€ ๋‚ด์šฉ ์ „์†ก - previewResponses/{requestId} - โ†“ ๊ด€๋ฆฌ์ž ๊ตฌ๋… - ๊ด€๋ฆฌ์ž: Promise ํ•ด๊ฒฐ โ†’ Dialog ํ‘œ์‹œ +ํ•™์ƒ (์ŠคํŠœ๋””์˜ค ํŽ˜์ด์ง€) + โ”œโ”€> startMonitoring(teamId, topicId, getSnapshot) + โ”‚ โ”œโ”€> ์ฆ‰์‹œ 1ํšŒ ์Šค๋ƒ…์ƒท ์ „์†ก + โ”‚ โ””โ”€> 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ + ๋ณ€๊ฒฝ ์‹œ ์ฆ‰์‹œ ์ „์†ก (800ms ๋””๋ฐ”์šด์Šค) + โ””โ”€> onDisconnect().remove() ์ž๋™ ์ •๋ฆฌ ``` **์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ**: - **WritingSessionManager** (`src/managers/WritingSessionManager.ts`): - - `startMonitoring(teamId, topicId, getStatsCallback)`: 5์ดˆ ์ฃผ๊ธฐ ํ†ต๊ณ„ ์ „์†ก - - `stopMonitoring()`: ์ „์†ก ์ค‘์ง€ + DB ์‚ญ์ œ - - `subscribeToTopic(teamId, topicId, callback)`: ์‹ค์‹œ๊ฐ„ ๊ตฌ๋… (๊ด€๋ฆฌ์ž) - - `requestPreview(userId)`: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ (Promise ๋ฐ˜ํ™˜) - - `listenForPreviewRequests(onRequestCallback)`: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฆฌ์Šค๋„ˆ (ํ•™์ƒ) - - ์ƒ์„ธ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์ „์†ก/์ˆ˜์‹ /์—๋Ÿฌ ์ถ”์ ) + - `startMonitoring(teamId, topicId, getSnapshot)`: ์ฆ‰์‹œ 1ํšŒ + 5์ดˆ ํ•˜ํŠธ๋น„ํŠธ, onDisconnect().remove() + - `pushSnapshot(payload)`: ์Šค๋ƒ…์ƒท ๋ณ€๊ฒฝ ์ฆ‰์‹œ ์ „์†ก + - `stopMonitoring()`: ๋ชจ๋‹ˆํ„ฐ๋ง ์ค‘์ง€ + - `subscribeToTopic(teamId, topicId, cb)`: ์‹ค์‹œ๊ฐ„ ๊ตฌ๋… (๊ต์‚ฌ) + - `joinSessionPresence()`, `subscribeToSessionPresence()`: ์„ธ์…˜ ์กด์žฌ ์ถ”์  -- **LiveWritingMonitor** (`src/components/team/LiveWritingMonitor.tsx`): +- **LiveClassMonitor** (`src/components/team/LiveClassMonitor.tsx`): - ์ฃผ์ œ ์„ ํƒ Select ์ปดํฌ๋„ŒํŠธ (Chakra UI Select) - - **๋ชจ๋“  ํŒ€ ๋ฉค๋ฒ„ ํ‘œ์‹œ** (getUsersByTeam) - **3๊ฐ€์ง€ ์ƒํƒœ ๊ด€๋ฆฌ**: - - ๐ŸŸข ์ž‘์„ฑ ์ค‘ (isActive: true, lastUpdated < 30์ดˆ) - ์ดˆ๋ก ๋ฐฐ์ง€, ํ•‘ํฌ ํ…Œ๋‘๋ฆฌ - - ๐ŸŸ  ๋‚˜๊ฐ (isActive: false, ๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€) - ์ฃผํ™ฉ ๋ฐฐ์ง€, ์ฃผํ™ฉ ํ…Œ๋‘๋ฆฌ - - โšช ๋Œ€๊ธฐ ์ค‘ (ํ•œ ๋ฒˆ๋„ ์ž‘์„ฑ ์•ˆ ํ•จ) - ํšŒ์ƒ‰ ๋ฐฐ์ง€, ํˆฌ๋ช…๋„ 60% - - **์ •๋ ฌ ์ˆœ์„œ**: ์ž‘์„ฑ ์ค‘ โ†’ ๋‚˜๊ฐ โ†’ ๋Œ€๊ธฐ ์ค‘ - - StudentMonitorCard ์ปดํฌ๋„ŒํŠธ (๊ฐœ๋ณ„ ํ•™์ƒ ์นด๋“œ) - - ์œ ์ € ์ •๋ณด์™€ ํ†ต๊ณ„ ์ž๋™ ๊ฒฐํ•ฉ (userManager ํ™œ์šฉ) - - **์ž‘์„ฑ ์†๋„ ์‹ค์‹œ๊ฐ„ ๊ณ„์‚ฐ** (ํด๋ผ์ด์–ธํŠธ ์ธก, ๊ธ€์ž/๋ถ„) - - **Sparkline ๊ทธ๋ž˜ํ”„** (Area Chart, ์ตœ๊ทผ 10๊ฐœ ํžˆ์Šคํ† ๋ฆฌ) - - **์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํˆดํŒ** (์†๋„ ๊ฐ’ + ๋ช‡ ์ดˆ ์ „ ๋ฐ์ดํ„ฐ) - - ๋ฏธ๋ฆฌ๋ณด๊ธฐ Dialog (์ž‘์„ฑ ์ค‘์ธ ํ•™์ƒ๋งŒ) - - **30์ดˆ ํƒ€์ž„์•„์›ƒ**: ์—…๋ฐ์ดํŠธ ์—†์œผ๋ฉด "๋‚˜๊ฐ" ์ฒ˜๋ฆฌ - - **๋งˆ์ง€๋ง‰ ํ†ต๊ณ„ ์œ ์ง€**: Firebase ์‚ญ์ œ๋˜์–ด๋„ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ์œ ์ง€ - - ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ํ‘œ์‹œ ("N์ดˆ ์ „") + - ๋ฏธ์ ‘์† (์Šค๋ƒ…์ƒท ์ˆ˜์‹  ์—†์Œ) + - ์ ‘์†์ค‘ (์Šค๋ƒ…์ƒท ์ˆ˜์‹ , ํ•˜ํŠธ๋น„ํŠธ๋งŒ) + - ์ง„ํ–‰์ค‘ (์Šค๋ƒ…์ƒท 30์ดˆ ์ด๋‚ด ์—…๋ฐ์ดํŠธ) + - ์นด๋“œ ํ—ค๋”: "{n}/{total}๋‹จ๊ณ„ ยท ๋‹จ๊ณ„ ์ด๋ฆ„" + - ํŽผ์นจ ์‹œ: ์ œ๋ชฉ, ๊ทธ๋ฆผ ์—ฌ๋ถ€, ์”จ์•—, ๋ฌธ์žฅ ์Šฌ๋กฏ(์งˆ๋ฌธ ํฌํ•จ), ๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅ, ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์‹œ๊ฐ„ -**Realtime DB ๊ตฌ์กฐ**: +- **Student ์ŠคํŠœ๋””์˜ค ํŽ˜์ด์ง€** (`src/app/[locale]/student/write/studio/page.tsx`): + - buildMonitorSnapshot() ํ—ฌํผ๋กœ StudioMonitorSnapshot ์ƒ์„ฑ + - teamId+topicId ์žˆ์œผ๋ฉด startMonitoring ํ˜ธ์ถœ + - 800ms ๋””๋ฐ”์šด์Šค๋กœ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ pushSnapshot ํ˜ธ์ถœ + +**Realtime DB ๊ตฌ์กฐ** (`monitoring/{teamId}/{topicId}/{uid}`): ```json { - "monitoring": { - "{teamId}": { - "{topicId}": { - "{userId}": { - "userId": "abc123", - "contentLength": 1500, - "wordCount": 300, - "topicId": "topic_123", - "lastUpdated": 1731400800000 - } - } + "userId": "uid_123", + "topicId": "topic_xyz", + "stage": "secondWriting", + "stageIndex": 3, + "stageCount": 6, + "stageLabel": "๋‹ค๋“ฌ๊ธฐ", + "title": "์šฐ๋ฆฌ์˜ ๋ชจํ—˜", + "hasImage": true, + "seeds": [ + {"label": "์ฃผ์ธ๊ณต", "value": "์šฉ๊ฐํ•œ ์—ฌ์šฐ"}, + {"label": "๋ฐฐ๊ฒฝ", "value": "์–ด๋‘์šด ์ˆฒ"} + ], + "sections": [ + { + "key": "opening", + "label": "์‹œ์ž‘", + "text": "ํ•œ ๋งˆ๋ฆฌ์˜ ์—ฌ์šฐ๊ฐ€..." + }, + { + "key": "blank_0", + "label": "๋นˆ์นธ", + "question": "์—ฌ์šฐ๋Š” ๋ฌด์—‡์„ ๋ณด์•˜์„๊นŒ?", + "text": "โ—‹โ—‹โ—‹โ—‹โ—‹" } - }, - "previewRequests": { - "{userId}": { - "{requestId}": { - "requestedBy": "admin_uid", - "timestamp": 1234567890, - "requestId": "req_xyz" - } - } - }, - "previewResponses": { - "{requestId}": { - "content": "ํ˜„์žฌ ์ž‘์„ฑ ์ค‘์ธ ๊ธ€...", - "timestamp": 1234567890, - "requestId": "req_xyz" - } - } + ], + "closingLabel": "๋งˆ๋ฌด๋ฆฌ ๋ฐฉํ–ฅ: ํ–‰๋ณตํ•œ ๊ฒฐ๋ง๋กœ ๋๋‚ธ๋‹ค", + "lastUpdated": 1692614400000 } ``` @@ -587,54 +567,19 @@ LiveWritingMonitor ์ปดํฌ๋„ŒํŠธ "$teamId": { "$topicId": { ".read": "auth != null", - "$userId": { - ".write": "auth != null && auth.uid == $userId" + "$uid": { + ".write": "auth != null && auth.uid == $uid" } } } - }, - "previewRequests": { - "$userId": { - ".read": "auth != null && auth.uid == $userId", - ".write": "auth != null" - } - }, - "previewResponses": { - "$requestId": { - ".read": "auth != null", - ".write": "auth != null" - } } } } ``` **๊ถŒํ•œ ์ •์ฑ…**: -- ํ†ต๊ณ„ ์ฝ๊ธฐ: ์ธ์ฆ๋œ ๋ชจ๋“  ์‚ฌ์šฉ์ž (ํŒ€ ์†Œ์œ ์ž๋งŒ UI ์ ‘๊ทผ ๊ฐ€๋Šฅ) -- ํ†ต๊ณ„ ์“ฐ๊ธฐ: ๋ณธ์ธ๋งŒ -- ๋ฏธ๋ฆฌ๋ณด๊ธฐ: ์š”์ฒญ์ž์™€ ๋Œ€์ƒ์ž๋งŒ - -**์ž‘์„ฑ ์†๋„ ๊ณ„์‚ฐ ๋กœ์ง** (ํด๋ผ์ด์–ธํŠธ ์ธก): -```typescript -// 5์ดˆ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ์ˆ˜์‹  -const charDiff = ํ˜„์žฌ๊ธ€์ž์ˆ˜ - ์ด์ „๊ธ€์ž์ˆ˜; -const speed = charDiff * 12; // 5์ดˆ * 12 = 60์ดˆ(1๋ถ„) - -// ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ (์ตœ๊ทผ 10๊ฐœ) -speedHistory.push({ speed, timestamp: Date.now() }); -if (speedHistory.length > 10) speedHistory.shift(); - -// Sparkline ๊ทธ๋ž˜ํ”„๋กœ ์‹œ๊ฐํ™” -- Area Chart (๋ฉด์  ๊ทธ๋ž˜ํ”„) -- Teal ์ƒ‰์ƒ, ํˆฌ๋ช…๋„ 30% -- ํˆดํŒ: ๋งˆ์šฐ์Šค ์˜ค๋ฒ„ ์‹œ "N์ž/๋ถ„" + "N์ดˆ ์ „" ํ‘œ์‹œ -- 0๋„ ํ‘œ์‹œ (์ž‘์„ฑ ๋ฉˆ์ถค ์‹œ๊ฐํ™”) -``` - -**๋น„์šฉ ํšจ์œจ์„ฑ**: -- Firebase Realtime DB Spark ํ”Œ๋žซํผ: ๋™์‹œ ์ ‘์† 100๋ช…๊นŒ์ง€ **์™„์ „ ๋ฌด๋ฃŒ** -- GB ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋ฐ˜ ๊ณผ๊ธˆ (์“ฐ๊ธฐ/์ฝ๊ธฐ ํšŸ์ˆ˜ ๋ฌด๊ด€) -- 30๋ช… ร— 1์‹œ๊ฐ„ ์ˆ˜์—… = ~1.5MB (๋ฌด๋ฃŒ ํ•œ๋„ 1GB/day์˜ 0.15%) +- ์Šค๋ƒ…์ƒท ์“ฐ๊ธฐ: ๋ณธ์ธ๋งŒ +- ์Šค๋ƒ…์ƒท ์ฝ๊ธฐ: ์ธ์ฆ๋œ ๋ชจ๋“  ์‚ฌ์šฉ์ž (๊ต์‚ฌ๋งŒ UI ์ ‘๊ทผ) ### 5. ์ƒํƒœ ๊ด€๋ฆฌ ์›์น™