docs: Sync documentation from private repository

This commit is contained in:
Documentation Bot 2025-11-10 01:43:07 +00:00
parent 09db74ff4c
commit a3f6ecb5c4
5 changed files with 2164 additions and 13 deletions

1364
API_SPEC.md Normal file

File diff suppressed because it is too large Load Diff

772
DATA_MODELS.md Normal file
View File

@ -0,0 +1,772 @@
# 라온누리 - 데이터 모델 및 스키마
> 최종 업데이트: 2025-11-07
이 문서는 Firestore 데이터베이스 구조와 TypeScript 타입 정의를 설명합니다.
**참고**: API 타입 정의는 [API_SPEC.md](./API_SPEC.md)를 참조하세요.
---
## Firestore 컬렉션 구조
### 컬렉션 개요
```
firestore
├── teams/ # ✅ 팀 (팀 코드 시스템)
├── students/ # ✅ 학생 계정 (Anonymous Auth)
├── users/ # 🔜 사용자 프로필 및 진행 상황
├── writings/ # ✅ 작성한 글
├── topics/ # ✅ 글쓰기 주제
├── lessons/ # 🔜 학습 레슨
├── stickers/ # 🔜 스티커 마스터 데이터
└── userStickers/ # 🔜 사용자별 스티커 획득 기록
```
**범례**:
- ✅ 구현 완료
- 🔜 구현 예정
---
## 1. Team (팀) ✅
**컬렉션**: `teams/{teamId}`
### 스키마
```typescript
interface Team {
id: string; // 문서 ID
code: string; // 팀 코드 (예: "춤추는파란사자")
name: string; // 팀 이름 (예: "2학년 1반")
ownerId: string; // 팀 소유자 UID (정식 계정)
// 보안 설정
securityMode: 'simple' | 'normal' | 'open';
requirePin: boolean; // PIN 입력 필요 여부
allowAnonymousJoin: boolean; // 명단 없는 학생 자동 가입 허용
// 멤버
studentIds: string[]; // students 컬렉션 참조
// 타임스탬프
createdAt: Timestamp;
updatedAt: Timestamp;
isActive: boolean; // 활성 상태
}
```
### 보안 모드
| 모드 | requirePin | allowAnonymousJoin | 설명 |
|------|-----------|-------------------|------|
| `simple` | false | true | 팀 코드 + 이름 (초등 저학년) |
| `normal` | true | false | 팀 코드 + 이름 + PIN (보안 강화) |
| `open` | false | true | 누구나 자유롭게 참여 |
### 예시 데이터
```json
{
"id": "team_abc123",
"code": "춤추는파란사자",
"name": "2학년 1반",
"ownerId": "teacher_xyz",
"securityMode": "simple",
"requirePin": false,
"allowAnonymousJoin": true,
"studentIds": ["student_001", "student_002", "student_003"],
"createdAt": "2024-11-06T00:00:00Z",
"updatedAt": "2024-11-07T10:00:00Z",
"isActive": true
}
```
### 인덱스
- `code` (단일 필드, 고유)
- `ownerId` + `isActive` (복합 인덱스)
**TypeScript**: `src/types/team.ts`
---
## 2. Student (학생 계정) ✅
**컬렉션**: `students/{studentId}`
### 스키마
```typescript
interface Student {
id: string; // 문서 ID
firebaseUid: string; // Firebase Anonymous Auth UID
linkedUserId?: string; // 연결된 정식 계정 UID (1:1, 선택적)
// 기본 정보
name: string; // 학생 이름
// 보안
pinHash?: string; // SHA-256 해시된 PIN
// 팀 정보
teamIds: string[]; // 속한 팀 ID 배열 (다중 팀 지원)
// 메타데이터
isAnonymous: boolean; // true (Anonymous Auth)
createdAt: Timestamp;
lastLoginAt: Timestamp;
}
```
### 예시 데이터
```json
{
"id": "student_001",
"firebaseUid": "anon_xyz123",
"linkedUserId": "user_parent_abc",
"name": "김민지",
"pinHash": "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3",
"teamIds": ["team_abc123", "team_def456"],
"isAnonymous": true,
"createdAt": "2024-11-06T09:00:00Z",
"lastLoginAt": "2024-11-07T14:30:00Z"
}
```
### 인덱스
- `firebaseUid` (단일 필드, 고유)
- `linkedUserId` (단일 필드)
- `teamIds` (array-contains)
**TypeScript**: `src/types/student.ts`
---
## 3. User (사용자)
**컬렉션**: `users/{userId}` (🔜 구현 예정)
### 스키마
```typescript
interface User {
// Firebase Auth 정보
uid: string; // Firebase UID (문서 ID와 동일)
email: string; // 이메일
displayName: string; // 표시 이름
photoURL?: string; // 프로필 사진 URL (선택)
createdAt: Timestamp; // 가입일
updatedAt: Timestamp; // 최종 수정일
// 게임화 데이터
level: number; // 현재 레벨 (1부터 시작)
experience: number; // 현재 경험치 (XP)
totalStickers: number; // 획득한 스티커 총 개수
// 학습 통계
writingCount: number; // 작성한 글 수
completedLessons: string[]; // 완료한 레슨 ID 배열
currentStreak: number; // 연속 출석일
longestStreak: number; // 최장 연속 출석일
// 활동 기록
lastLoginAt: Timestamp; // 마지막 로그인 시간
lastWritingAt?: Timestamp; // 마지막 글 작성 시간
}
```
### 예시 데이터
```json
{
"uid": "abc123xyz",
"email": "student@example.com",
"displayName": "김학생",
"photoURL": "https://example.com/photo.jpg",
"createdAt": "2024-10-28T00:00:00Z",
"updatedAt": "2024-10-28T10:30:00Z",
"level": 5,
"experience": 450,
"totalStickers": 12,
"writingCount": 8,
"completedLessons": ["lesson_001", "lesson_002", "lesson_003"],
"currentStreak": 3,
"longestStreak": 7,
"lastLoginAt": "2024-10-28T10:00:00Z",
"lastWritingAt": "2024-10-27T15:30:00Z"
}
```
### 인덱스
- `email` (단일 필드)
- `level` (단일 필드, 내림차순)
- `writingCount` (단일 필드, 내림차순)
---
## 4. Writing (작성글) ✅
**컬렉션**: `writings/{writingId}`
### 스키마
```typescript
interface Writing {
id: string; // 문서 ID
userId: string; // 작성자 UID (TODO: studentId로 변경 예정)
topicId?: string | null; // 주제 ID (null은 자유 주제)
// 글 내용
title: string; // 제목
content: string; // 본문 내용 (HTML)
wordCount: number; // 단어 수
charCount: number; // 글자 수
// 상태
status: 'draft' | 'published'; // 임시저장/발행
// 타임스탬프
createdAt: Timestamp; // 최초 작성일
updatedAt: Timestamp; // 최종 수정일
}
```
**현재 구현**:
- ✅ CRUD 기능 완료 (WritingManager)
- 🔜 피드백 시스템 (향후 추가)
- 🔜 userId → studentId 마이그레이션 예정
### 예시 데이터
```json
{
"id": "writing_001",
"userId": "abc123xyz",
"topicId": "topic_daily_001",
"title": "내가 좋아하는 계절",
"content": "나는 가을을 제일 좋아해요. 왜냐하면...",
"wordCount": 120,
"status": "submitted",
"createdAt": "2024-10-27T14:00:00Z",
"updatedAt": "2024-10-27T15:00:00Z",
"submittedAt": "2024-10-27T15:00:00Z",
"feedback": {
"authorId": "parent_abc",
"authorName": "김엄마",
"content": "계절의 특징을 잘 표현했어요!",
"rating": 5,
"createdAt": "2024-10-27T20:00:00Z"
}
}
```
### 인덱스
- `userId` + `createdAt` (복합 인덱스, 내림차순)
- `topicId` + `createdAt` (복합 인덱스, 내림차순)
- `status` + `createdAt` (복합 인덱스)
---
## 5. Topic (글쓰기 주제) ✅
**컬렉션**: `topics/{topicId}`
### 스키마
```typescript
interface Topic {
id: string; // 문서 ID
title: string; // 주제 제목
description: string; // 주제 설명
// 분류
category: 'daily' | 'imagination' | 'emotion' | 'experience';
difficulty: 'easy' | 'medium' | 'hard';
// 소유자 정보
ownerType: 'system' | 'team' | 'personal';
ownerId: string; // 팀 주제: teamId, 개인 주제: userId
// 메타데이터
keywords: string[]; // 관련 키워드
examplePrompts: string[]; // 예시 질문/프롬프트
// 템플릿 (선택적)
titleTemplate?: string; // 제목 템플릿 (예: "{date} 일기")
contentTemplate?: string; // 내용 템플릿
// 통계
usageCount: number; // 사용된 횟수
// 타임스탬프
createdAt: Timestamp; // 생성일
updatedAt: Timestamp; // 수정일
// 관리자 정보
createdBy: string; // 작성자 UID
isActive: boolean; // 활성화 여부
}
```
**주제 소유 유형**:
- `system`: 시스템 기본 주제 (모든 사용자) - 미래 확장용
- `team`: 팀 주제 (팀 멤버만, `ownerId = teamId`)
- `personal`: 개인 주제 (작성자만)
**현재 구현**:
- ✅ CRUD 기능 완료 (TopicManager)
- ✅ 개인 주제 생성/수정/삭제
- ✅ 팀 주제 생성/수정/삭제
- ✅ 템플릿 처리 (클라이언트)
- ✅ TopicSelector에 팀/개인 배지 표시
### 카테고리 설명
| 카테고리 | 설명 | 예시 |
|---------|------|------|
| `daily` | 일상 경험 | "오늘 있었던 일", "가족과 함께한 시간" |
| `imagination` | 상상과 창작 | "만약 내가 슈퍼히어로라면", "꿈속에서의 모험" |
| `emotion` | 감정 표현 | "가장 행복했던 순간", "속상했던 경험" |
| `experience` | 특별한 경험 | "처음 해본 것", "여행에서의 추억" |
### 예시 데이터
```json
{
"id": "topic_daily_001",
"title": "내가 좋아하는 계절",
"description": "사계절 중 가장 좋아하는 계절과 그 이유를 써보아요.",
"category": "daily",
"difficulty": "easy",
"keywords": ["계절", "봄", "여름", "가을", "겨울", "날씨"],
"examplePrompts": [
"어떤 계절을 가장 좋아하나요?",
"그 계절에는 무엇을 하나요?",
"왜 그 계절이 좋은가요?"
],
"usageCount": 145,
"createdAt": "2024-10-01T00:00:00Z",
"updatedAt": "2024-10-01T00:00:00Z",
"createdBy": "admin_001",
"isActive": true
}
```
### 인덱스
- `category` + `difficulty` (복합 인덱스)
- `isActive` + `usageCount` (복합 인덱스, 내림차순)
---
## 6. Lesson (학습 레슨) 🔜
**컬렉션**: `lessons/{lessonId}` (구현 예정)
### 스키마
```typescript
interface Lesson {
id: string; // 문서 ID
title: string; // 레슨 제목
description: string; // 레슨 설명
// 분류
level: number; // 추천 레벨 (1-100)
category: string; // 카테고리 (예: "문단 쓰기", "비유 표현")
order: number; // 순서 (같은 카테고리 내)
// 콘텐츠
content: {
theory: string; // 이론 설명 (마크다운)
examples: string[]; // 예시 문장들
exercises: Exercise[]; // 연습 문제
};
// 선행 조건
requiredLessons?: string[]; // 선행 레슨 ID 배열
// 보상
reward: {
experience: number; // 획득 경험치
stickers?: string[]; // 획득 가능한 스티커 ID
};
// 통계
completionCount: number; // 완료한 학생 수
averageScore?: number; // 평균 점수 (0-100)
// 타임스탬프
createdAt: Timestamp;
updatedAt: Timestamp;
// 관리자
createdBy: string;
isActive: boolean;
}
interface Exercise {
id: string;
type: 'multiple_choice' | 'short_answer' | 'writing';
question: string;
options?: string[]; // 객관식인 경우
correctAnswer?: string | number;
explanation?: string; // 해설
}
```
### 예시 데이터
```json
{
"id": "lesson_001",
"title": "문장 부호 사용하기",
"description": "마침표, 물음표, 느낌표를 올바르게 사용하는 방법을 배워요.",
"level": 1,
"category": "기초 문법",
"order": 1,
"content": {
"theory": "# 문장 부호란?\n문장의 끝에는 마침표(.), 물음표(?), 느낌표(!)를 사용해요.",
"examples": [
"오늘은 날씨가 좋아요.",
"너는 어디에 가니?",
"와, 정말 멋지다!"
],
"exercises": [
{
"id": "ex_001",
"type": "multiple_choice",
"question": "다음 중 올바른 문장은?",
"options": [
"오늘은 날씨가 좋아요",
"오늘은 날씨가 좋아요.",
"오늘은 날씨가 좋아요?"
],
"correctAnswer": 1,
"explanation": "평서문은 마침표(.)로 끝나요."
}
]
},
"reward": {
"experience": 50,
"stickers": ["sticker_beginner_001"]
},
"completionCount": 234,
"averageScore": 87.5,
"createdAt": "2024-10-01T00:00:00Z",
"updatedAt": "2024-10-01T00:00:00Z",
"createdBy": "admin_001",
"isActive": true
}
```
### 인덱스
- `category` + `order` (복합 인덱스, 오름차순)
- `level` + `order` (복합 인덱스, 오름차순)
---
## 7. Sticker (스티커) 🔜
**컬렉션**: `stickers/{stickerId}` (구현 예정)
### 스키마
```typescript
interface Sticker {
id: string; // 문서 ID
name: string; // 스티커 이름
imageUrl: string; // 이미지 URL
description: string; // 설명
// 분류
category: 'learning' | 'challenge' | 'special'; // 카테고리
rarity: 'common' | 'rare' | 'epic' | 'legendary'; // 희귀도
// 획득 조건
unlockCondition: {
type: 'writing_count' | 'lesson_completion' | 'level' | 'streak' | 'manual';
value?: number; // 조건 값 (type에 따라 다름)
lessonId?: string; // lesson_completion인 경우
};
// 통계
unlockCount: number; // 획득한 사용자 수
// 타임스탬프
createdAt: Timestamp;
createdBy: string;
isActive: boolean;
}
```
### 희귀도 및 카테고리
| 희귀도 | 설명 | 예시 |
|-------|------|------|
| `common` | 일반 | 기본 달성 스티커 |
| `rare` | 레어 | 10개 글 작성 |
| `epic` | 에픽 | 50개 글 작성, 레벨 20 달성 |
| `legendary` | 전설 | 100개 글 작성, 모든 레슨 완료 |
| 카테고리 | 설명 |
|---------|------|
| `learning` | 학습 관련 (레슨 완료) |
| `challenge` | 도전 과제 (글 수, 연속 출석) |
| `special` | 특별 이벤트 |
### 예시 데이터
```json
{
"id": "sticker_beginner_001",
"name": "첫 글쓰기",
"imageUrl": "https://example.com/stickers/first_writing.png",
"description": "첫 번째 글을 완성했어요!",
"category": "challenge",
"rarity": "common",
"unlockCondition": {
"type": "writing_count",
"value": 1
},
"unlockCount": 856,
"createdAt": "2024-10-01T00:00:00Z",
"createdBy": "admin_001",
"isActive": true
}
```
### 인덱스
- `category` + `rarity` (복합 인덱스)
- `unlockCount` (단일 필드, 내림차순)
---
## 8. UserSticker (사용자 스티커) 🔜
**컬렉션**: `userStickers/{userStickerId}` (구현 예정)
### 스키마
```typescript
interface UserSticker {
id: string; // 문서 ID
userId: string; // 사용자 UID
stickerId: string; // 스티커 ID
unlockedAt: Timestamp; // 획득 시간
}
```
### 예시 데이터
```json
{
"id": "us_abc123_sticker001",
"userId": "abc123xyz",
"stickerId": "sticker_beginner_001",
"unlockedAt": "2024-10-27T15:30:00Z"
}
```
### 인덱스
- `userId` + `unlockedAt` (복합 인덱스, 내림차순)
- `userId` + `stickerId` (복합 인덱스, 중복 방지용)
---
## TypeScript 타입 정의 파일
### 데이터 모델 타입
모든 데이터 모델 타입은 `src/types/` 디렉토리에 정의됩니다.
```
src/types/
├── team.ts # ✅ Team 데이터 모델
├── student.ts # ✅ Student 데이터 모델
├── writing.ts # ✅ Writing 데이터 모델
├── topic.ts # ✅ Topic 데이터 모델
├── user.ts # 🔜 User 관련 타입 (예정)
├── lesson.ts # 🔜 Lesson 관련 타입 (예정)
├── sticker.ts # 🔜 Sticker 관련 타입 (예정)
└── api/ # ✅ API Request/Response 타입
├── team.ts # Team API 타입 (10개 엔드포인트)
├── student.ts # Student API 타입 (13개 엔드포인트)
├── writing.ts # Writing API 타입 (6개 엔드포인트)
└── topic.ts # Topic API 타입 (6개 엔드포인트)
```
### 사용 예시
```typescript
// 데이터 모델 import
import type { Team } from "@/types/team";
import type { Student } from "@/types/student";
import type { Writing } from "@/types/writing";
// API 타입 import
import type { CreateTeamRequest, CreateTeamResponse } from "@/types/api/team";
import type { GetStudentsByTeamResponse } from "@/types/api/student";
// Manager에서 사용
import { teamManager, studentManager } from "@/managers";
const teams = await teamManager.getMyTeams(); // Team[] 반환
```
### API 공통 타입
`src/types/api.ts`에 정의된 공통 타입:
```typescript
// 표준 응답 형식
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
}
// 에러 형식
interface ApiError {
code: string;
message: string;
details?: any;
}
// HTTP Method Enum
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
```
---
## Firestore 보안 규칙
### 파일 위치
- `firestore.rules` (프로젝트 루트)
### 기본 규칙 예시
**참고**: 실제 구현 시 Next.js API Routes/Server Actions에서 권한 체크 수행
```javascript
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 팀: 읽기는 모두 가능, 쓰기는 서버에서만
match /teams/{teamId} {
allow read: if request.auth != null;
allow write: if false; // API에서만 쓰기 (권한 체크)
}
// 학생: 본인 또는 연결된 계정만 읽기, 쓰기는 서버에서만
match /students/{studentId} {
allow read: if request.auth != null && (
request.auth.uid == resource.data.firebaseUid ||
request.auth.uid == resource.data.linkedUserId
);
allow write: if false; // API에서만 쓰기
}
// 사용자는 자신의 문서만 읽고 쓸 수 있음
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// 작성글: 작성자만 읽고 쓸 수 있음
match /writings/{writingId} {
allow read: if request.auth != null &&
resource.data.userId == request.auth.uid;
allow write: if false; // API에서만 쓰기
}
// 주제: 모든 인증된 사용자가 읽을 수 있음
match /topics/{topicId} {
allow read: if request.auth != null;
allow write: if false; // API에서만 쓰기
}
// 레슨: 모든 인증된 사용자가 읽을 수 있음
match /lessons/{lessonId} {
allow read: if request.auth != null;
allow write: if false;
}
// 스티커: 모든 인증된 사용자가 읽을 수 있음
match /stickers/{stickerId} {
allow read: if request.auth != null;
allow write: if false;
}
// 사용자 스티커: 자신의 것만 읽을 수 있음
match /userStickers/{userStickerId} {
allow read: if request.auth != null &&
resource.data.userId == request.auth.uid;
allow write: if false; // 서버에서만 부여
}
}
}
```
**보안 참고사항**:
- 모든 쓰기 작업은 Next.js API Routes에서 수행
- API에서 Firebase Admin SDK 사용하여 Firestore 접근
- 클라이언트 매니저는 API 호출만 수행 (Firestore 직접 접근 안 함)
---
## 관련 문서
- [API_SPEC.md](./API_SPEC.md) - API 명세서 (35개 엔드포인트)
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조
- [TECH_STACK.md](./TECH_STACK.md) - 기술 스택
- [ROADMAP.md](./ROADMAP.md) - 개발 로드맵
- [CLAUDE.md](./CLAUDE.md) - Claude Code 개발 가이드
---
© 2024 BlueNovaLab. All rights reserved.

View File

@ -1,9 +1,15 @@
# 라온누리 - 프로젝트 구조 # 라온누리 - 프로젝트 구조
> 최종 업데이트: 2025-11-07 (아키텍처 단순화 - Users 컬렉션 전환) > 최종 업데이트: 2025-11-10 (5단계 보안 레벨 시스템, User 타입 최소화)
초등학생을 위한 창작 글쓰기 교육 플랫폼 초등학생을 위한 창작 글쓰기 교육 플랫폼
**주요 업데이트** (2025-11-10):
- 🔐 5단계 보안 레벨 시스템 (팀별 보안 정책 선택)
- 📦 User 타입 분리 (FirestoreUser / User)
- 🏷️ 닉네임 저장 위치 변경 (team.members[uid].nickname)
- 🗑️ memberUids optional (Object.keys(members) 사용)
--- ---
## 페이지 구조 ## 페이지 구조

View File

@ -1,6 +1,6 @@
# 라온누리 - 개발 로드맵 # 라온누리 - 개발 로드맵
> 최종 업데이트: 2025-11-07 (그룹 기능 제거, 팀 시스템 개선) > 최종 업데이트: 2025-11-10 (5단계 보안 레벨 시스템, User 타입 최소화)
초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획 초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획
@ -53,6 +53,10 @@
| **UserManager 개선** | **getUser() 404 시 null 반환, 팀 코드 로그인 시 신규 사용자 생성 플로우 개선** | **2025-11-07** | | **UserManager 개선** | **getUser() 404 시 null 반환, 팀 코드 로그인 시 신규 사용자 생성 플로우 개선** | **2025-11-07** |
| **TopicSelector UI 개선** | **드롭다운 메뉴에 팀/개인 주제 배지 표시, 선택 전에도 주제 타입 확인 가능** | **2025-11-07** | | **TopicSelector UI 개선** | **드롭다운 메뉴에 팀/개인 주제 배지 표시, 선택 전에도 주제 타입 확인 가능** | **2025-11-07** |
| **T_ prefix 제거** | **팀 주제 ownerId에서 T_ prefix 제거, ownerId = teamId 직접 사용** | **2025-11-07** | | **T_ prefix 제거** | **팀 주제 ownerId에서 T_ prefix 제거, ownerId = teamId 직접 사용** | **2025-11-07** |
| **5단계 보안 레벨 시스템** | **TeamSecurityLevel enum (1-5), 팀별 보안 정책 선택, 명단 관리 API** | **2025-11-10** |
| **User 타입 최소화** | **FirestoreUser/User 분리, Firebase Auth를 Single Source of Truth로, 데이터 중복 제거** | **2025-11-10** |
| **닉네임 저장 위치 변경** | **users.nicknames → team.members[uid].nickname 이동, TeamMember 타입 확장** | **2025-11-10** |
| **authStore DB 연동** | **combineUserData로 Firebase Auth + Firestore 자동 결합, userManager.getUser() 활용** | **2025-11-10** |
### 🚧 진행 중 ### 🚧 진행 중

View File

@ -262,13 +262,13 @@ Manager Layer (비즈니스 로직 + 클라이언트 캐싱)
│ ├─> deleteTeam() → DELETE /team/:id │ ├─> deleteTeam() → DELETE /team/:id
│ └─> generateUniqueTeamCode() → POST /team/generate-code │ └─> generateUniqueTeamCode() → POST /team/generate-code
├─> StudentManager (싱글톤) ├─> UserManager (싱글톤)
│ ├─> createStudent() → POST /student │ ├─> createUser() → POST /user
│ ├─> getStudent() → GET /student/:id (5분 캐싱) │ ├─> getUser() → GET /user/:id (Firebase Auth + Firestore 자동 결합, 5분 캐싱)
│ ├─> getStudentsByTeam() → POST /student/by-team (30초 캐싱) │ ├─> getUsersByTeam() → GET /user/by-team/:teamId (30초 캐싱)
│ ├─> kickStudentFromTeam() → POST /student/kick │ ├─> updateLastLogin() → POST /user/:uid/update-last-login
│ ├─> validateStudentPin() → POST /student/validate-pin │ ├─> findUserByNickname() → POST /user/find-by-nickname (Level 1용)
│ └─> linkStudentToUser() → POST /student/link │ └─> setUserNickname() → POST /user/:uid/nickname (DEPRECATED - 팀에서 관리)
├─> WritingManager (싱글톤) ├─> WritingManager (싱글톤)
│ ├─> createWriting() │ ├─> createWriting()
@ -335,14 +335,19 @@ import { teamManager } from "@/managers";
// 팀 목록 조회 - 소유한 팀 + 참여한 팀 (1분간 캐싱됨) // 팀 목록 조회 - 소유한 팀 + 참여한 팀 (1분간 캐싱됨)
const teams = await teamManager.getMyTeams(); const teams = await teamManager.getMyTeams();
// 팀 생성 (캐시 자동 무효화) // 🆕 팀 생성 (5단계 보안 레벨)
const teamId = await teamManager.createTeam({ const teamId = await teamManager.createTeam({
name: "우리반", name: "우리반",
code: "춤추는파란사자", code: "춤추는파란사자",
securityMode: "simple", securityLevel: 2, // 1-5 (명단 기반)
requirePin: false, allowedNames: ["홍길동", "김철수"]
allowAnonymousJoin: true
}); });
// 🆕 보안 레벨 변경
await teamManager.updateSecurityLevel(teamId, 4, true); // Level 4, 자동 명단 생성
// 🆕 닉네임 조회
const nickname = teamManager.getMemberNickname(team, uid, user?.name);
``` ```
**참고 문서**: **참고 문서**: