diff --git a/DATA_MODELS.md b/DATA_MODELS.md index 72e1507..e55d986 100644 --- a/DATA_MODELS.md +++ b/DATA_MODELS.md @@ -115,36 +115,86 @@ interface Team { --- -## 2. User (사용자) +## 2. User (사용자) ✅ -**컬렉션**: `users/{userId}` (🔜 구현 예정) +**컬렉션**: `users/{userId}` -### 스키마 +### 설계 원칙 + +- **Firebase Auth = Single Source of Truth**: 이름(`displayName`), 이메일(`email`), 프로필 사진(`photoURL`)은 Firebase Auth에서 관리 +- **Firestore에는 앱 전용 메타데이터만 저장** (FirestoreUser) +- **UI User 객체**: Firebase Auth + Firestore 자동 결합 (`User extends Partial`) + +### FirestoreUser 스키마 (Firestore 저장용) ```typescript -interface User { - // Firebase Auth 정보 - uid: string; // Firebase UID (문서 ID와 동일) - email: string; // 이메일 - displayName: string; // 표시 이름 - photoURL?: string; // 프로필 사진 URL (선택) +interface FirestoreUser { + uid: string; // Firebase Auth UID (문서 ID와 동일) + + // 타임스탬프 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; // 마지막 글 작성 시간 + + // 역할 + role?: UserRole; // 'student' | 'teacher' (기본값: 'student') + teacherApproval?: TeacherApproval; // 교사 역할인 경우 승인 정보 + + // 설정 + settings?: { + theme?: "light" | "dark"; + language?: string; + weeklyGoal?: { + count: number; // 1-10, 기본값: 3 + updatedAt: Timestamp; + }; + }; + + // 플랜 및 크레딧 + plan?: UserPlan; // 사용자 플랜 (기본값: FREE) + organizationId?: string; // 소속 조직 ID (School Plan) + aiCredits?: number; // AI 크레딧 잔액 + + // 커리큘럼 + subscribedCurriculumIds?: string[]; // 구독한 커리큘럼 ID 배열 + + // 관리자 + isAdmin?: boolean; + + // 개인 정보 + birthYear?: number; // 생년 (나이 계산용, 예: 2015) + + // 시스템 계정 (교사 대리 생성) + loginId?: string; // 로그인용 아이디 (시스템 계정만) + isSystemAccount?: boolean; // 교사가 대리 생성한 계정 + createdByTeacher?: string; // 생성한 교사 uid +} +``` + +### User 스키마 (UI 표시용 — Firebase Auth + Firestore 결합) + +```typescript +interface User extends Partial { + uid: string; // 항상 존재 (Firebase Auth UID) + + // Firebase Auth 전용 필드 + email?: string; + name?: string; // Firebase Auth displayName + photoURL?: string | null; +} +``` + +### 역할 관련 타입 + +```typescript +type UserRole = "student" | "teacher"; +type TeacherApprovalStatus = "pending" | "approved" | "rejected"; + +interface TeacherApproval { + status: TeacherApprovalStatus; + requestedAt: Timestamp; + reviewedAt?: Timestamp; // 승인/거절 시간 + reviewedBy?: string; // 승인/거절한 관리자 uid + rejectionReason?: string; // 거절 사유 } ``` @@ -153,31 +203,25 @@ interface User { ```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" + "role": "student", + "birthYear": 2015, + "settings": { + "language": "ko", + "weeklyGoal": { + "count": 3, + "updatedAt": "2024-11-01T00:00:00Z" + } + } } ``` ### 인덱스 -- `email` (단일 필드) -- `level` (단일 필드, 내림차순) -- `writingCount` (단일 필드, 내림차순) +- `uid` (문서 ID) +- `role` (단일 필드) +- `loginId` (단일 필드, 시스템 계정 조회용) --- diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 6cead78..9da81f3 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -1,10 +1,22 @@ # 라온누리 - 프로젝트 구조 -> 최종 업데이트: 2026-03-19 (학생 일괄 생성 및 loginId 시스템) +> 최종 업데이트: 2026-03-27 (학생/교사 로그인 분리) 초등학생을 위한 창작 글쓰기 교육 플랫폼 -**최신 업데이트** (2026-03-19): +**최신 업데이트** (2026-03-27): +- **학생/교사 역할 분리** - 로그인/가입 시 학생/교사 탭 선택, 역할별 홈 페이지 분리 + - **역할 시스템**: `UserRole` (student/teacher), Firebase Auth Custom Claims, Firestore `role` 필드 + - **교사 승인 워크플로우**: 가입 → 승인 대기 → 관리자 승인/거절 → 로그인 가능 + - **LoginDialog 탭 UI**: 학생/교사 탭 전환, 탭별 SignupForm 분기 (SignupForm/TeacherSignupForm) + - **역할별 홈 분리**: `/student/home` (학생), `/teacher/home` (교사), `/home` (리다이렉터) + - **교사 승인 대기 화면**: `TeacherPendingApprovalScreen` (승인 대기/거절 상태 표시) + - **관리자 교사 승인 UI**: `/admin/teacher-approvals` (대기 목록, 승인/거절 버튼, 거절 사유) + - **라우트 가드**: `useRequireAuth({ requiredRole })` 옵션 추가 + - **서버 유틸**: `custom-claims.ts`, `teacher-approval.ts`, `withTeacherAuth()`, `withAdminAuth()` + - **마이그레이션**: `scripts/migrate-user-roles.ts` (기존 유저 → student + Custom Claims) + +**이전 업데이트** (2026-03-19): - **학생 일괄 생성 (Bulk Student Creation)** - CSV 기반 학생 계정 일괄 생성, 템플릿 ID/비밀번호, 로그인 카드 인쇄 - **`BulkMemberCreator`** (`src/components/team/BulkMemberCreator.tsx`): 4단계 UI (입력→미리보기→생성중→결과), 로그인 카드 인쇄 기능 - **`POST /api/team/[teamId]/bulk-create-members`**: 소유자 전용 학생 일괄 생성 API @@ -527,8 +539,11 @@ | 페이지 | 경로 | 설명 | 주요 기능 | 다국어 | |-------|------|------|---------|--------| -| **랜딩 페이지** | `/[locale]` | 서비스 소개 및 홍보 (비로그인 전용) | Hero, Features, How It Works, CTA, Footer
로그인 시 `/home`으로 자동 리다이렉트
🆕 **전체 번역 완료** (사이트명, 태그라인, 모든 섹션) | ✅ 완료 | -| **유저 홈** | `/[locale]/home` | 인증된 사용자 대시보드 | 환영 메시지, 빠른 시작 대시보드, **최근 활동 (최근 글 3개 표시)**
비로그인 시 `/`로 자동 리다이렉트
정식 계정은 "내 팀" 카드 추가 표시
🆕 **WritingCard Grid, "모두 보기" 버튼**
🆕 **전체 번역 완료** (웰컴 메시지, 모든 액션 카드) | ✅ 완료 | +| **랜딩 페이지** | `/[locale]` | 서비스 소개 및 홍보 (비로그인 전용) | Hero, Features, How It Works, CTA, Footer
로그인 시 역할별 홈으로 자동 리다이렉트 | ✅ 완료 | +| **홈 리다이렉터** | `/[locale]/home` | 역할별 홈으로 리다이렉트 | student → `/student/home`, teacher → `/teacher/home` | ✅ 완료 | +| **학생 홈** | `/[locale]/student/home` | 학생 대시보드 | 환영 메시지, 빠른 시작, 최근 활동, 알림 | ✅ 완료 | +| **교사 홈** | `/[locale]/teacher/home` | 교사 대시보드 | 팀 관리, 학생 글쓰기 확인 | ✅ 완료 | +| **교사 승인 대기** | `/[locale]/teacher/pending-approval` | 교사 승인 대기 화면 | 승인 대기/거절 상태, 상태 확인, 로그아웃 | ✅ 완료 | | **내 글 모음** | `/[locale]/writings` | 🆕 **전체 글 목록 페이지** | 🆕 **사용자의 모든 글 표시 (Grid)**
🆕 **정렬 Select (최신순/오래된순)**
🆕 **WritingCard 컴포넌트 사용**
🆕 **Empty state 처리**
🆕 **전체 번역 완료** (ko/en/ja) | ✅ 완료 | | **글 상세보기** | `/[locale]/writing/[writingId]` | 🆕 **Server Component 기반 상세 페이지** | 🆕 **SEO 최적화** (서버에서 HTML 생성)
🆕 **SNS 공유 미리보기** (카카오톡/페이스북)
🆕 **서버 데이터 로딩** (Firebase Admin SDK)
제목, 내용, 생성된 이미지 표시
주제 및 팀 정보 Badge
프롬프트 Collapsible
🆕 **CommentList** (Client Component)
🆕 **BackButton** 공통 컴포넌트 사용 | ✅ 완료 | | **글쓰기** | `/[locale]/write` | Tiptap 기반 순수 텍스트 에디터 + 주제 선택 | 주제 선택 (자유 주제/개인 주제/팀 주제)
🆕 **글 수정 기능 (URL params ?id=xxx)**
🆕 **수정 모드 배지 표시**
🆕 **주제 변경 경고** (작성 중 내용 초기화 알림, 임시 저장 안내)
제목 입력 (Editable), 순수 텍스트 에디터 (포맷팅 없음)
🆕 **다중 글조각 관리** (최대 10개), "저장된 글조각" 버튼
🆕 **강화된 자동 저장** (2초 debounce, 저장 상태 표시: 저장 중/저장됨)
🆕 **저장 플로우 개편** (저장 → 백그라운드 분석 → `/imageUpload` 리다이렉트)
🆕 **GenerateImageDialog 제거** (새 플로우로 대체)
템플릿 미리채우기 (제목/내용), Firestore 저장
비로그인도 접근 가능 (저장 시 로그인 유도) | ✅ 완료 | @@ -560,7 +575,9 @@ | **LoginDialog** | `LoginDialog.tsx` | 로그인/회원가입 다이얼로그 (auth/team/link 모드 지원) | ✅ 완료 | | **StudentLoginFlow** | `StudentLoginFlow.tsx` | 학생 로그인 플로우 | ✅ 완료 | | **LoginForm** | `LoginForm.tsx` | 로그인 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 | -| **SignupForm** | `SignupForm.tsx` | 회원가입 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 | +| **SignupForm** | `SignupForm.tsx` | 학생 회원가입 폼 (birthYear 포함) | ✅ 완료 | +| **TeacherSignupForm** | `TeacherSignupForm.tsx` | 교사 회원가입 폼 (birthYear 없음, role 고정 teacher) | ✅ 완료 | +| **TeacherPendingApprovalScreen** | `TeacherPendingApprovalScreen.tsx` | 교사 승인 대기/거절 화면 | ✅ 완료 | | **SocialLoginButton** | `SocialLoginButton.tsx` | 소셜 로그인 버튼 | ✅ 완료 | | **UserProfileButton** | `UserProfileButton.tsx` | 사용자 프로필/메뉴 버튼 (설정 메뉴 통합) | ✅ 완료 | diff --git a/ROADMAP.md b/ROADMAP.md index 78bae38..b3ec3ee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 라온누리 - 개발 로드맵 -> 최종 업데이트: 2026-03-19 (학생 일괄 생성 및 loginId 시스템) +> 최종 업데이트: 2026-03-27 (학생/교사 로그인 분리) 초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획 @@ -166,6 +166,8 @@ | **이메일 기반 대기 초대 시스템** | **이메일로 팀 초대 전송, 대기 초대 Firestore 모델 (PendingInvite 타입), 서버 레이어 (pending-invite.ts), API Routes (POST/GET/DELETE /api/pending-invite, 수락/거절 /api/pending-invite/[id]/respond), EmailInviteSection UI 컴포넌트, 내 대기 초대 조회 (/api/pending-invite/my)** | **2026-03-19** | | **학생 일괄 생성 (Bulk Student Creation)** | **CSV 기반 학생 계정 일괄 생성, 템플릿 ID/PW 시스템 ({teamCode}{number} 등), loginId 간편 로그인 (@ 없는 ID → 시스템 이메일 변환), BulkMemberCreator 4단계 UI (입력→미리보기→생성중→결과), 로그인 카드 인쇄 기능, POST /api/team/[teamId]/bulk-create-members (소유자 전용), POST /api/auth/resolve-login-id, 서버 레이어 bulk-member.ts, FirestoreUser loginId/isSystemAccount/createdByTeacher 필드 추가, authStore loginId 로그인 지원, LoginForm 이메일/loginId 겸용, 다국어 지원 (ko/en/ja)** | **2026-03-19** | +| **학생/교사 로그인 분리** | **UserRole (student/teacher) 타입, Custom Claims 기반 역할 관리, LoginDialog 학생/교사 탭 UI, 학생/교사 별도 SignupForm, 교사 승인 워크플로우 (가입→대기→관리자 승인/거절), withTeacherAuth/withAdminAuth 서버 래퍼, 역할별 홈 페이지 분리 (/student/home, /teacher/home), useRequireAuth requiredRole 옵션, 관리자 교사 승인 UI (/admin/teacher-approvals), TeacherApprovalManager 싱글톤, 마이그레이션 스크립트 (기존 유저 → student)** | **2026-03-27** | + ### 🚧 진행 중 | 항목 | 설명 | 진행률 | diff --git a/TECH_STACK.md b/TECH_STACK.md index d02a127..e1cccfc 100644 --- a/TECH_STACK.md +++ b/TECH_STACK.md @@ -172,6 +172,17 @@ NEXT_PUBLIC_API_URL=/api | **네이버** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) | | **카카오** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) | +### 역할 기반 인증 (Custom Claims) + +| 역할 | Custom Claim | 설명 | +|------|-------------|------| +| **학생** | `role: "student"` | 기본값, 가입 즉시 활성화 | +| **교사** | `role: "teacher"` | 관리자 승인 필요 (`teacherApproval.status`) | +| **관리자** | `isAdmin: true` | Firestore `isAdmin` + Custom Claims | + +**인증 래퍼**: `withAuth()`, `withTeacherAuth()`, `withAdminAuth()` (`src/lib/auth.ts`) +**Custom Claims 헬퍼**: `setUserRole()`, `getUserRole()`, `setAdminClaim()` (`src/lib/server/custom-claims.ts`) + ### Firestore 데이터베이스 상세한 데이터베이스 스키마와 모델 정의는 [DATA_MODELS.md](./DATA_MODELS.md) 문서를 참조하세요.