docs: Sync documentation from private repository
This commit is contained in:
parent
5f299edf5e
commit
7a5cff07af
182
DATA_MODELS.md
182
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,96 +1042,47 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**클라이언트 측 계산** (선생님 화면):
|
||||
```typescript
|
||||
// MonitoringData (UI용)
|
||||
"stage": "secondWriting",
|
||||
"stageIndex": 3,
|
||||
"stageCount": 6,
|
||||
"stageLabel": "다듬기",
|
||||
"title": "우리의 모험",
|
||||
"hasImage": true,
|
||||
"seeds": [
|
||||
{"label": "주인공", "value": "용감한 여우"},
|
||||
{"label": "배경", "value": "어두운 숲"}
|
||||
],
|
||||
"sections": [
|
||||
{
|
||||
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 }
|
||||
]
|
||||
"key": "opening",
|
||||
"label": "시작",
|
||||
"text": "한 마리의 여우가..."
|
||||
},
|
||||
{
|
||||
"key": "blank_0",
|
||||
"label": "빈칸",
|
||||
"question": "여우는 무엇을 보았을까?",
|
||||
"text": "○○○○○"
|
||||
}
|
||||
],
|
||||
"closingLabel": "행복한 결말로 끝낸다",
|
||||
"lastUpdated": 1692614400000
|
||||
}
|
||||
```
|
||||
|
||||
**활성 상태 판단 로직**:
|
||||
```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,23 +1094,11 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"previewRequests": {
|
||||
"$userId": {
|
||||
".read": "auth != null && auth.uid == $userId",
|
||||
".write": "auth != null"
|
||||
}
|
||||
},
|
||||
"previewResponses": {
|
||||
"$requestId": {
|
||||
".read": "auth != null",
|
||||
".write": "auth != null"
|
||||
"$uid": {
|
||||
".write": "auth != null && auth.uid == $uid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1158,10 +1106,8 @@ interface PreviewResponse {
|
||||
```
|
||||
|
||||
**권한**:
|
||||
- 통계 쓰기: 본인만
|
||||
- 통계 읽기: 본인 + 팀 소유자
|
||||
- 미리보기 요청: 누구나 쓰기, 대상자만 읽기
|
||||
- 미리보기 응답: 누구나 읽기/쓰기 (requestId로 필터링)
|
||||
- 스냅샷 쓰기: 본인만
|
||||
- 스냅샷 읽기: 인증된 모든 사용자
|
||||
|
||||
**TypeScript**: `src/types/writingSession.ts`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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** |
|
||||
|
||||
167
TECH_STACK.md
167
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": "한 마리의 여우가..."
|
||||
},
|
||||
"previewRequests": {
|
||||
"{userId}": {
|
||||
"{requestId}": {
|
||||
"requestedBy": "admin_uid",
|
||||
"timestamp": 1234567890,
|
||||
"requestId": "req_xyz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"previewResponses": {
|
||||
"{requestId}": {
|
||||
"content": "현재 작성 중인 글...",
|
||||
"timestamp": 1234567890,
|
||||
"requestId": "req_xyz"
|
||||
}
|
||||
{
|
||||
"key": "blank_0",
|
||||
"label": "빈칸",
|
||||
"question": "여우는 무엇을 보았을까?",
|
||||
"text": "○○○○○"
|
||||
}
|
||||
],
|
||||
"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. 상태 관리 원칙
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user