diff --git a/API_SPEC.md b/API_SPEC.md index 53428f0..8e6700c 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -2,7 +2,19 @@ 라온누리 서버 API 명세서 -## ⚠️ 최신 변경사항 (2025-11-12) +## ⚠️ 최신 변경사항 (2025-11-26) + +### 🔗 익명 계정 연결 기능 +- **POST /api/auth/merge-account**: 익명 계정 데이터를 정식 계정으로 마이그레이션 + - Firestore 데이터 이전 (writings, topics, comments, userReactions, teams) + - Realtime DB 데이터 이전 (drafts, monitoring, previewRequests) + - 원자성 보장 (Firestore Batch, Realtime DB Transaction) + - 병합 완료 후 통계 반환 +- **서비스 레이어**: `src/services/firebaseAuth.ts` (mergeAndLoginWithEmail, mergeAndLoginWithGoogle) +- **상태 관리**: `src/store/authStore.ts` (mergeWithEmail, mergeWithGoogle 액션) +- **UI 통합**: LoginForm/SignupForm mode prop, LoginDialog link 모드 + +## ⚠️ 변경사항 (2025-11-12) ### ✅ Writing API 구현 완료 - **POST /api/writing**: 글 생성 (서버에서 wordCount/charCount 자동 계산) @@ -849,6 +861,65 @@ await teamManager.removeMember(teamId, currentUser.uid); --- +## Auth API + +### POST `/auth/merge-account` - 익명 계정 데이터 병합 +실제 URL: `POST /api/auth/merge-account` + +**인증**: 필수 (정식 계정으로 로그인된 상태) + +**Request**: +```typescript +{ + anonymousUid: string; // 병합할 익명 계정의 UID +} +``` + +**Response**: +```typescript +{ + success: true, + data: { + mergedCounts: { + writings: number; // 이전된 글 개수 + topics: number; // 이전된 주제 개수 + comments: number; // 이전된 댓글 개수 + userReactions: number; // 이전된 반응 개수 + teamMemberships: number; // 이전된 팀 멤버십 개수 + drafts: number; // 이전된 초안 개수 + monitoring: number; // 이전된 모니터링 세션 개수 + previewRequests: number; // 이전된 미리보기 요청 개수 + } + } +} +``` + +**동작**: +1. **Firestore 데이터 마이그레이션** (Batch 사용): + - `writings` 컬렉션: userId 업데이트 + - `topics` 컬렉션: ownerId 업데이트 (개인 주제만) + - `comments` 컬렉션: authorId 업데이트 + - `userReactions` 컬렉션: userId 업데이트 + - `teams` 컬렉션: members 키 변경 (anonymousUid → targetUid) + +2. **Realtime DB 데이터 마이그레이션** (Transaction 사용): + - `drafts/{anonymousUid}` → `drafts/{targetUid}` 이동 + - `monitoring/{topicId}/{anonymousUid}` → `monitoring/{topicId}/{targetUid}` 이동 + - `previewRequests/{topicId}/{anonymousUid}` → `previewRequests/{topicId}/{targetUid}` 이동 + +**제약사항**: +- 익명 계정과 정식 계정은 다른 UID여야 함 +- 익명 계정 데이터는 병합 후 자동 삭제되지 않음 (수동 정리 필요) + +**에러 코드**: +- `400`: anonymousUid 누락 +- `401`: 인증되지 않은 요청 +- `500`: 마이그레이션 실패 + +**캐싱**: 없음 (일회성 작업) + +--- + ## User API **중요**: User vs FirestoreUser 구분 diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 7e280e9..4159595 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -275,10 +275,10 @@ | 컴포넌트 | 파일명 | 설명 | 상태 | |---------|--------|------|------| | **AuthInitializer** | `AuthInitializer.tsx` | Firebase 인증 상태 초기화 | ✅ 완료 | -| **LoginDialog** | `LoginDialog.tsx` | 로그인/회원가입 다이얼로그 (일반/팀 코드 모드 전환) | ✅ 완료 | +| **LoginDialog** | `LoginDialog.tsx` | 로그인/회원가입 다이얼로그 (auth/team/link 모드 지원) | ✅ 완료 | | **StudentLoginFlow** | `StudentLoginFlow.tsx` | 팀 코드 학생 로그인 플로우 (3단계) | ✅ 완료 | -| **LoginForm** | `LoginForm.tsx` | 로그인 폼 컴포넌트 | ✅ 완료 | -| **SignupForm** | `SignupForm.tsx` | 회원가입 폼 컴포넌트 | ✅ 완료 | +| **LoginForm** | `LoginForm.tsx` | 로그인 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 | +| **SignupForm** | `SignupForm.tsx` | 회원가입 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 | | **SocialLoginButton** | `SocialLoginButton.tsx` | 소셜 로그인 버튼 | ✅ 완료 | | **UserProfileButton** | `UserProfileButton.tsx` | 사용자 프로필/메뉴 버튼 | ✅ 완료 | @@ -290,6 +290,12 @@ - ✅ HIBP API 연동 (유출된 비밀번호 차단) - ✅ 폼 검증 및 에러 애니메이션 - ✅ Google OAuth 로그인 +- ✅ 익명 계정 연결 기능 + - ✅ 기존 폼 재사용 (LoginForm/SignupForm mode prop) + - ✅ 신규 계정 생성 (linkWithCredential) + - ✅ 기존 계정 병합 (API 데이터 마이그레이션) + - ✅ 3개 소셜 로그인 버튼 표시 (Naver/Kakao/Google) + - ✅ 용어 통일 ("병합" → "연결", "익명" → "임시") - ✅ 팀 코드 기반 사용자 로그인 (Anonymous Auth - 단순화됨) - ✅ 한글 팀 코드 ("춤추는 파란 사자" 형식) - ✅ 사용자 이름 입력 (2단계) @@ -756,6 +762,7 @@ firebase functions:log --only cleanupExpiredReservations | **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/auth/merge-account` | POST | 🆕 **익명 계정 데이터 병합** (Firestore + Realtime DB 마이그레이션, Batch/Transaction, 통계 반환) | ✅ 완료 | | **사용자 관리** | `/api/user` | GET, POST, PUT | 사용자 조회/생성/업데이트 | ✅ 완료 | **서버 레이어** (`src/lib/server/`): @@ -783,8 +790,10 @@ firebase functions:log --only cleanupExpiredReservations - ✅ **isAuthenticated** - 로그인 여부 (익명 포함) - ✅ **user.isAnonymous** - 익명/정식 계정 구분 - ✅ **loginAsUser()** - 팀 코드 로그인 (PIN 제거) -- ✅ **linkWithEmail()** - 이메일 계정 연결 -- ✅ **linkWithGoogle()** - Google 계정 연결 +- ✅ **linkWithEmail()** - 이메일 계정 연결 (신규 계정 생성) +- ✅ **linkWithGoogle()** - Google 계정 연결 (신규 계정 생성) +- ✅ **mergeWithEmail()** - 기존 이메일 계정과 병합 (데이터 마이그레이션) +- ✅ **mergeWithGoogle()** - 기존 Google 계정과 병합 (데이터 마이그레이션) - ❌ ~~currentStudent~~, ~~ownedStudents~~, ~~switchStudent()~~ 제거 (복잡도 감소) --- diff --git a/ROADMAP.md b/ROADMAP.md index c35a411..95c681b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -115,6 +115,7 @@ | **글 상세 페이지 댓글 기능 연결** | **CommentList 컴포넌트를 writing/[writingId]/page.tsx에 연결, 댓글 보기/작성/답글/삭제 기능 활성화** | **2025-11-26** | | **미사용 코드 정리** | **koreanWordList.ts 삭제 (AI 분석으로 대체), InteractiveImage.tsx 삭제 (InteractiveImageViewer로 대체), AllowListManager.tsx 삭제 (미통합), 관련 문서 업데이트** | **2025-11-26** | | **팀 페이지 리팩토링** | **useTeamData 훅 생성 (팀 + 멤버 로딩, 권한 체크, refreshMembers), 팀 페이지 중복 코드 제거 (~200줄 절감), 각 하위 컴포넌트 자체 로딩 스켈레톤 활용** | **2025-11-26** | +| **익명 계정 연결 기능** | **POST /api/auth/merge-account API (Firestore + Realtime DB 데이터 마이그레이션), mergeAndLoginWithEmail/mergeAndLoginWithGoogle 함수 (firebaseAuth.ts), mergeWithEmail/mergeWithGoogle 액션 (authStore.ts), LoginForm/SignupForm mode prop 추가 ('auth'\|'link'), LoginDialog link 모드 지원 (3개 소셜 버튼 표시), LinkAccountFlow 삭제 (기존 폼 재사용), 용어 변경 ("병합" → "연결", "익명" → "임시"), 다국어 지원 (linkAccountDescription, naverMerge/kakaoMerge/googleMerge, mergeButton/linkButton, ko/en/ja 15개 키)** | **2025-11-26** | ### 🚧 진행 중 diff --git a/TECH_STACK.md b/TECH_STACK.md index 1400652..73defaf 100644 --- a/TECH_STACK.md +++ b/TECH_STACK.md @@ -422,10 +422,11 @@ const nickname = teamManager.getMemberNickname(team, uid, user?.name); ├─> isLoading └─> 액션: ├─> login/signup/loginWithGoogle (기존) - ├─> **loginAsStudent(classCode, name, pin?)** - 팀 코드 로그인 - ├─> **switchStudent(student)** - 학생 전환 - ├─> **linkCurrentStudentWithEmail()** - 계정 연결 - └─> **linkCurrentStudentWithGoogle()** - Google 계정 연결 + ├─> **loginAsUser(teamCode, name)** - 팀 코드 로그인 (익명 계정 생성) + ├─> **linkWithEmail(email, password)** - 신규 이메일 계정 생성 (linkWithCredential) + ├─> **linkWithGoogle()** - 신규 Google 계정 생성 (linkWithCredential) + ├─> **mergeWithEmail(email, password)** - 기존 이메일 계정과 병합 (API 데이터 마이그레이션) + └─> **mergeWithGoogle()** - 기존 Google 계정과 병합 (API 데이터 마이그레이션) 3. 인증 기반 라우팅 ├─> 랜딩 페이지 (/) @@ -786,15 +787,23 @@ const [selectedPartIndex, setSelectedPartIndex] = useState(null); ├─> authStore.currentStudent 설정 └─> /team/[teamId] 멤버 페이지로 이동 -2. 정식 계정 연결 (선택적, 학부모/고학년) - ├─> 설정 → "내 계정 만들기" - ├─> 이메일 회원가입 또는 Google 로그인 - ├─> linkWithCredential() 호출 - │ └─ Anonymous(anon123) → Email(user456) 전환 - ├─> Firestore 연결: - │ ├─ students/studentDoc.linkedUserId = user456 - │ └─ users/user456.ownedStudentIds = [studentDoc] - └─> 이후 user456으로 로그인 가능 (currentStudent 자동 설정) +2. 정식 계정 연결 (선택적) + ├─> UserProfileButton → "계정 연결하기" + ├─> LoginDialog (link 모드) + ├─> 2가지 시나리오: + │ ├─ **신규 계정 생성** (linkWithCredential): + │ │ ├─ 이메일 회원가입 또는 Google 로그인 + │ │ ├─ linkWithCredential() 호출 + │ │ └─ Anonymous(anon123) → Email(user456) 전환 (UID 유지) + │ │ + │ └─ **기존 계정 병합** (API 데이터 마이그레이션): + │ ├─ 이메일 로그인 또는 Google 로그인 + │ ├─ POST /api/auth/merge-account 호출 + │ ├─ Firestore 데이터 이전 (writings, topics, comments, teams) + │ ├─ Realtime DB 데이터 이전 (drafts, monitoring) + │ └─ 익명 계정(anon123) → 정식 계정(user456)으로 데이터 이전 + │ + └─> 이후 user456으로 로그인 가능 (모든 데이터 통합) 3. 정식 계정 로그인 (학생 자동 선택) ├─> user456으로 로그인