docs: Sync documentation from private repository

This commit is contained in:
Documentation Bot 2026-06-04 23:04:17 +00:00
parent df55a605e2
commit 4832f932e9
4 changed files with 420 additions and 6 deletions

201
AGENTS.md Normal file
View File

@ -0,0 +1,201 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
---
## 프로젝트 개요
**라온누리 (Raonnuri)** - 초등학생 대상 창작 글쓰기 교육 플랫폼
**Tech Stack**: Next.js 16, React 19, Chakra UI v3, Firebase Auth, TypeScript
---
## 개발 명령어
```bash
npm run dev # 개발 서버 (포트 3001, webpack 사용)
npm run build # 프로덕션 빌드
npm run lint # ESLint 실행
npx tsc --noEmit # 타입 체크 (빌드 전 필수)
```
**주의**:
- Dev server는 포트 3001 사용
- React Compiler 활성화
- 커밋 전 타입 체크 필수 (`npx tsc --noEmit`)
---
## 핵심 아키텍처
### Manager Pattern (필수)
**모든 데이터 작업은 Manager 사용 (서비스 직접 호출 금지)**
```typescript
import { teamManager, userManager } from "@/managers";
// ✅ Good
const teams = await teamManager.getMyTeams();
const users = await userManager.getUsersByTeam(teamId);
// ❌ Bad - 서비스 직접 호출 금지
```
**Available Managers**:
- `teamManager` - Team CRUD, member management, security level
- `userManager` - User CRUD
- `draftManager` - localStorage + Realtime DB 하이브리드
- `writingSessionManager` - Real-time monitoring
- `writingManager`, `topicManager`, `feedManager`
**상세**: `API_SPEC.md` 참조
---
### Data Model 핵심 원칙
**Firebase Auth = Single Source of Truth** (이름, 이메일, 사진)
- Firestore Users 컬렉션: `uid`, `createdAt`, `lastLoginAt`, `settings`만 저장
- UI User 객체: Firebase Auth + Firestore 자동 결합
- 팀별 닉네임: `team.members[uid].nickname` (User 아님)
- 멤버 확인: `uid in team.members`
**상세**: `DATA_MODELS.md` 참조
---
## 필수 개발 규칙
### 다국어 지원 (i18n)
**모든 새로운 UI 및 페이지는 다국어 지원이 필수입니다.**
```typescript
// ✅ Good
import {useTranslations} from "next-intl";
import {useRouter} from "@/i18n/routing";
const t = useTranslations('newPage');
return <h1>{t('title')}</h1>;
// ❌ Bad
return <h1>새 페이지</h1>; // 하드코딩 금지!
```
**상세**: `I18N_GUIDE.md` 참조
---
### 스타일 가이드
**버튼에는 항상 `colorPalette="brand"` 사용**:
```tsx
// ✅ Good
<Button colorPalette="brand">클릭</Button>
// ❌ Bad
<Button>클릭</Button> // 기본 gray 사용 금지
```
**Semantic 토큰 우선 사용** (숫자/하드코딩 금지):
- `color="fg"` / `color="fg.muted"`
- `bg="bg"` / `bg="brand.subtle"`
- `borderColor="border.muted"`
**Icon 규칙**: react-icons 필수 (이모티콘 금지)
- ✅ `react-icons/lu` (Lucide) - 메인 아이콘 세트
- ❌ 이모티콘 (🌍📝) - 사용 금지
**상세**: `STYLE_GUIDE.md` 참조
---
### API 개발 규칙
**RESTful 원칙**: HTTP Method로 동작 구분
```
POST /api/resource → 생성/추가
DELETE /api/resource → 삭제/제거
PUT /api/resource → 전체 수정
GET /api/resource → 조회
```
**Firebase Admin SDK**: `adminFbClient` 싱글톤 사용
```typescript
// ✅ Good
import {adminFbClient} from "@/lib/firebase-admin";
const doc = await adminFbClient.collection('users').doc(uid).get();
// ❌ Bad
import {getFirestore} from "firebase-admin/firestore";
const db = getFirestore(); // 초기화 문제 발생 가능
```
**API 응답**: 헬퍼 함수 필수
```typescript
import {successResponse, validationErrorResponse} from "@/lib/api-response";
// ✅ Good
return successResponse({ data });
return validationErrorResponse("에러 메시지");
// ❌ Bad
return NextResponse.json({success: false, ...});
```
**상세**: `API_SPEC.md` 참조
---
### 보안
**초대 링크 기반 팀 참여**: 디스코드 스타일 초대 시스템 사용
- 모든 유저는 로그인 필수 (익명 인증 삭제됨)
- 팀 참여는 초대 링크를 통해서만 가능
- 닫힌 팀 → 초대 안 만들면 됨
**XSS 방지**: 백엔드 자동 sanitize
- `src/lib/server/writing.ts`에서 모든 HTML 자동 세탁
- 프론트엔드 별도 처리 불필요
**상세**: `SECURITY.md` 참조
---
## Documentation Requirements
**모든 새 기능/페이지는 다음 3개 파일 업데이트 필수**:
1. `PROJECT_STRUCTURE.md` - 페이지 구조, 컴포넌트 목록
2. `ROADMAP.md` - 완료 작업, 예정 작업
3. `TECH_STACK.md` - 아키텍처 패턴, 플로우 다이어그램
---
## 참조 문서
### 개발 가이드
- **`API_SPEC.md`** - API 명세서, RESTful 원칙, Firebase Admin SDK
- **`STYLE_GUIDE.md`** - Color 사용법, Icon 규칙, Chakra UI 테마
- **`FRONTEND_DESIGN_PATTERNS.md`** - 컴포넌트 디자인 철학, 인터랙션/시각/애니메이션 패턴
- **`I18N_GUIDE.md`** - 다국어 지원 가이드 (필수)
- **`DEVELOPMENT_GUIDE.md`** - AI Delta 전송, Draft 저장, Firebase Functions
### 프로젝트 문서
- **`PROJECT_STRUCTURE.md`** - 프로젝트 구조
- **`TECH_STACK.md`** - 기술 스택, 아키텍처
- **`DATA_MODELS.md`** - 데이터베이스 스키마
- **`SECURITY.md`** - 보안 정책, 3단계 보안 레벨
- **`ROADMAP.md`** - 개발 로드맵
---
© 2024 BlueNovaLab. All rights reserved.

View File

@ -1,10 +1,25 @@
# 라온누리 - 프로젝트 구조 # 라온누리 - 프로젝트 구조
> 최종 업데이트: 2026-03-27 (학생/교사 로그인 분리) > 최종 업데이트: 2026-06-04 (스튜디오 플로우 UI 통합 리디자인)
초등학생을 위한 창작 글쓰기 교육 플랫폼 초등학생을 위한 창작 글쓰기 교육 플랫폼
**최신 업데이트** (2026-03-27): **최신 업데이트** (2026-06-02):
- **스튜디오 6단계 선형 글쓰기 플로우** (`/write/studio`) - "그림으로 시작 → 글 완성"을 한 페이지에서 진행하는 신규 라우트
- **단일 페이지 상태머신**: `page.tsx` 오케스트레이터(useReducer)가 단일 진실 공급원(StudioFlowState)을 보유, 7개 스테이지 컴포넌트를 순차 렌더
- **6단계 흐름**: S0 안내(intro) → S1 업로드(upload) → S2 1차 작문(firstWriting) → S3 보상① 인터랙션(rewardInteraction) → S4 2차 작문(secondWriting) → S5 보상② 이펙트(rewardEffect) → S6 완료(complete)
- **S1 키워드 추출**: 종이 그림 촬영/업로드 → AI가 그림에서 키워드 5개 추출 (`POST /api/studio/analyze`)
- **S2 1차 작문**: 그림 + 키워드 5개 + 역할별 자유 3문장(상황/감정/이유, 한 문장씩 자유 작성) (학생 단독, AI 없음), 문장 구조는 플랫폼 비고정 — 선생님 직접 교육 + 역할별 예문·전구 안내로 지도(주제 연동 시 주제 데이터로 대체 예정), S2→S3 전환 시 draft Writing 생성(createWriting) + 이미지 업로드(uploadUserImage)
- **S3/S5 보상**: 공용 `RewardCanvasEditor`로 인터랙션(움직임) 1개 / 스프라이트 이펙트 1개 부여 (영역 1개 제한, `useDistortionEditor` 기반)
- **S4 글 다듬기**: AI 띄어쓰기 교정 + 의미 단위 낱말 분해 (`POST /api/studio/segment`) → 낱말 교체·자리 삽입 추천 (`POST /api/studio/suggest`), 낱말 칩 탭/+자리로 편집 (SentenceTokenEditor)
- **S6 완료**: 1회차 수업 종료 → draft를 발행(published)으로 마감 (`POST /api/studio/complete`)
- **공용 컴포넌트**: `StageStepper`(6단계 가로 진행 표시기, 점수/숫자 미표시), `RewardCanvasEditor`(인터랙션/이펙트 공용)
- **데이터**: writings 컬렉션에 `keywords`, `studioResult`(sentencesV1/V2, blanks, grantedInteraction/Effect, completedAt) 저장, 보상은 distortionAreas/spriteEffectAreas, status draft→published, rewardType `participation`
- **진입점**: `/write` 모드 선택 화면에 스튜디오 진입 카드 추가
- **다국어 지원**: `studio` 네임스페이스, `pageTitles.writeStudio` (ko/en/ja)
- **레거시 공존**: 기존 글쓰기 라우트(`/write/fill-in-blank`, `/write/image`, `/write/easy`)와 병행 운영, 추후 제거 예정
**이전 업데이트** (2026-03-27):
- **학생/교사 역할 분리** - 로그인/가입 시 학생/교사 탭 선택, 역할별 홈 페이지 분리 - **학생/교사 역할 분리** - 로그인/가입 시 학생/교사 탭 선택, 역할별 홈 페이지 분리
- **역할 시스템**: `UserRole` (student/teacher), Firebase Auth Custom Claims, Firestore `role` 필드 - **역할 시스템**: `UserRole` (student/teacher), Firebase Auth Custom Claims, Firestore `role` 필드
- **교사 승인 워크플로우**: 가입 → 승인 대기 → 관리자 승인/거절 → 로그인 가능 - **교사 승인 워크플로우**: 가입 → 승인 대기 → 관리자 승인/거절 → 로그인 가능
@ -546,7 +561,8 @@
| **교사 승인 대기** | `/[locale]/teacher/pending-approval` | 교사 승인 대기 화면 | 승인 대기/거절 상태, 상태 확인, 로그아웃 | ✅ 완료 | | **교사 승인 대기** | `/[locale]/teacher/pending-approval` | 교사 승인 대기 화면 | 승인 대기/거절 상태, 상태 확인, 로그아웃 | ✅ 완료 |
| **내 글 모음** | `/[locale]/writings` | 🆕 **전체 글 목록 페이지** | 🆕 **사용자의 모든 글 표시 (Grid)**<br>🆕 **정렬 Select (최신순/오래된순)**<br>🆕 **WritingCard 컴포넌트 사용**<br>🆕 **Empty state 처리**<br>🆕 **전체 번역 완료** (ko/en/ja) | ✅ 완료 | | **내 글 모음** | `/[locale]/writings` | 🆕 **전체 글 목록 페이지** | 🆕 **사용자의 모든 글 표시 (Grid)**<br>🆕 **정렬 Select (최신순/오래된순)**<br>🆕 **WritingCard 컴포넌트 사용**<br>🆕 **Empty state 처리**<br>🆕 **전체 번역 완료** (ko/en/ja) | ✅ 완료 |
| **글 상세보기** | `/[locale]/writing/[writingId]` | 🆕 **Server Component 기반 상세 페이지** | 🆕 **SEO 최적화** (서버에서 HTML 생성)<br>🆕 **SNS 공유 미리보기** (카카오톡/페이스북)<br>🆕 **서버 데이터 로딩** (Firebase Admin SDK)<br>제목, 내용, 생성된 이미지 표시<br>주제 및 팀 정보 Badge<br>프롬프트 Collapsible<br>🆕 **CommentList** (Client Component)<br>🆕 **BackButton** 공통 컴포넌트 사용 | ✅ 완료 | | **글 상세보기** | `/[locale]/writing/[writingId]` | 🆕 **Server Component 기반 상세 페이지** | 🆕 **SEO 최적화** (서버에서 HTML 생성)<br>🆕 **SNS 공유 미리보기** (카카오톡/페이스북)<br>🆕 **서버 데이터 로딩** (Firebase Admin SDK)<br>제목, 내용, 생성된 이미지 표시<br>주제 및 팀 정보 Badge<br>프롬프트 Collapsible<br>🆕 **CommentList** (Client Component)<br>🆕 **BackButton** 공통 컴포넌트 사용 | ✅ 완료 |
| **글쓰기** | `/[locale]/write` | Tiptap 기반 순수 텍스트 에디터 + 주제 선택 | 주제 선택 (자유 주제/개인 주제/팀 주제)<br>🆕 **글 수정 기능 (URL params ?id=xxx)**<br>🆕 **수정 모드 배지 표시**<br>🆕 **주제 변경 경고** (작성 중 내용 초기화 알림, 임시 저장 안내)<br>제목 입력 (Editable), 순수 텍스트 에디터 (포맷팅 없음)<br>🆕 **다중 글조각 관리** (최대 10개), "저장된 글조각" 버튼<br>🆕 **강화된 자동 저장** (2초 debounce, 저장 상태 표시: 저장 중/저장됨)<br>🆕 **저장 플로우 개편** (저장 → 백그라운드 분석 → `/imageUpload` 리다이렉트)<br>🆕 **GenerateImageDialog 제거** (새 플로우로 대체)<br>템플릿 미리채우기 (제목/내용), Firestore 저장<br>비로그인도 접근 가능 (저장 시 로그인 유도) | ✅ 완료 | | **글쓰기** | `/[locale]/write` | Tiptap 기반 순수 텍스트 에디터 + 주제 선택 | 주제 선택 (자유 주제/개인 주제/팀 주제)<br>🆕 **글 수정 기능 (URL params ?id=xxx)**<br>🆕 **수정 모드 배지 표시**<br>🆕 **주제 변경 경고** (작성 중 내용 초기화 알림, 임시 저장 안내)<br>제목 입력 (Editable), 순수 텍스트 에디터 (포맷팅 없음)<br>🆕 **다중 글조각 관리** (최대 10개), "저장된 글조각" 버튼<br>🆕 **강화된 자동 저장** (2초 debounce, 저장 상태 표시: 저장 중/저장됨)<br>🆕 **저장 플로우 개편** (저장 → 백그라운드 분석 → `/imageUpload` 리다이렉트)<br>🆕 **GenerateImageDialog 제거** (새 플로우로 대체)<br>🆕 **스튜디오 진입 카드** (`/write/studio` 신규 플로우 진입)<br>템플릿 미리채우기 (제목/내용), Firestore 저장<br>비로그인도 접근 가능 (저장 시 로그인 유도) | ✅ 완료 |
| **스튜디오 글쓰기** | `/[locale]/write/studio` | 🆕 **그림으로 시작하는 6단계 선형 글쓰기 플로우 (신규)** | 🆕 **단일 페이지 상태머신** (useReducer 오케스트레이터, StudioFlowState 단일 진실 공급원)<br>🆕 **6단계**: S0 안내 → S1 업로드(키워드 5개 추출) → S2 1차 작문(역할별 자유 3문장 + 예문·전구 안내, 학생 단독) → S3 보상① 인터랙션 → S4 글 다듬기(낱말 분해 + 교체·삽입 추천) → S5 보상② 이펙트 → S6 완료(발행)<br>🆕 **StageStepper** 진행 표시기 (점수/숫자 미표시)<br>🆕 **RewardCanvasEditor** 공용 (인터랙션/이펙트, 영역 1개 제한)<br>🆕 **AI 엔드포인트 4개**: analyze / segment / suggest / complete<br>🆕 **다국어 지원** (studio namespace, ko/en/ja)<br>레거시 라우트와 병행 운영 (추후 제거 예정) | ✅ 완료 |
| **이미지 업로드/선택** | `/[locale]/imageUpload` | 🆕 **이미지 소스 선택 페이지 (글 저장 후 자동 이동)** | 🆕 **2가지 선택지**: AI 생성 (장면 추출 + 선택 + 생성) / 직접 업로드<br>🆕 **드래그앤드롭 파일 업로드** (미리보기, 5MB 제한, JPEG/PNG/WebP)<br>🆕 **Canvas API 클라이언트 사이드 리사이즈** (1920x1080 최대, 85% 품질)<br>🆕 **Firebase Storage 업로드** (WritingManager.uploadUserImage)<br>🆕 **이미 이미지 있으면 자동으로 `/interaction` 리다이렉트**<br>🆕 **다국어 지원** (imageUpload namespace, ko/en/ja) | ✅ 완료 | | **이미지 업로드/선택** | `/[locale]/imageUpload` | 🆕 **이미지 소스 선택 페이지 (글 저장 후 자동 이동)** | 🆕 **2가지 선택지**: AI 생성 (장면 추출 + 선택 + 생성) / 직접 업로드<br>🆕 **드래그앤드롭 파일 업로드** (미리보기, 5MB 제한, JPEG/PNG/WebP)<br>🆕 **Canvas API 클라이언트 사이드 리사이즈** (1920x1080 최대, 85% 품질)<br>🆕 **Firebase Storage 업로드** (WritingManager.uploadUserImage)<br>🆕 **이미 이미지 있으면 자동으로 `/interaction` 리다이렉트**<br>🆕 **다국어 지원** (imageUpload namespace, ko/en/ja) | ✅ 완료 |
| **인터랙션 편집** | `/[locale]/interaction` | 🆕 **이미지 왜곡 편집 페이지 (이미지 생성/업로드 후 이동)** | 🆕 **7:3 그리드 레이아웃** (이미지 70%, 컨트롤러 30%)<br>🆕 **Sticky Header** (InteractionHeader, 스크롤 시 상단 고정)<br>🆕 **framer-motion 애니메이션** (모드 전환, 영역 선택 부드러운 전환)<br>🆕 **영역 목록 일반 모드 표시** (CustomAreaList, 모든 모드에서 접근 가능)<br>🆕 **에디터 버튼 우측 배치** (추가/삭제/숨기기, 가로 3개)<br>🆕 **컨트롤러 내부 스크롤** (커스텀 스크롤바, 높이 80dvh)<br>🆕 **반응형 이미지** (aspectRatio 유지, 찌그러짐 해결)<br>왜곡 영역 편집 (EditorCanvas, DistortionArea)<br>모션 프리셋 선택 (horizontal, vertical, rotate, pulse 등)<br>물리 설정 (stiffness, damping, mass)<br>에디터/인터랙션 모드 전환 (Switch)<br>저장 시 Writing.distortionAreas 업데이트 | ✅ 완료 | | **인터랙션 편집** | `/[locale]/interaction` | 🆕 **이미지 왜곡 편집 페이지 (이미지 생성/업로드 후 이동)** | 🆕 **7:3 그리드 레이아웃** (이미지 70%, 컨트롤러 30%)<br>🆕 **Sticky Header** (InteractionHeader, 스크롤 시 상단 고정)<br>🆕 **framer-motion 애니메이션** (모드 전환, 영역 선택 부드러운 전환)<br>🆕 **영역 목록 일반 모드 표시** (CustomAreaList, 모든 모드에서 접근 가능)<br>🆕 **에디터 버튼 우측 배치** (추가/삭제/숨기기, 가로 3개)<br>🆕 **컨트롤러 내부 스크롤** (커스텀 스크롤바, 높이 80dvh)<br>🆕 **반응형 이미지** (aspectRatio 유지, 찌그러짐 해결)<br>왜곡 영역 편집 (EditorCanvas, DistortionArea)<br>모션 프리셋 선택 (horizontal, vertical, rotate, pulse 등)<br>물리 설정 (stiffness, damping, mass)<br>에디터/인터랙션 모드 전환 (Switch)<br>저장 시 Writing.distortionAreas 업데이트 | ✅ 완료 |
| **공개 팀 목록** | `/[locale]/team/all` | 🆕 **공개 팀 둘러보기** | 🆕 **공개된 팀 목록 표시** (isPublic=true)<br>🆕 **TeamCard 그리드** (글래스모피즘)<br>🆕 **페이지네이션** (커서 기반)<br>🆕 **Navbar "공개 팀" 메뉴** | ✅ 완료 | | **공개 팀 목록** | `/[locale]/team/all` | 🆕 **공개 팀 둘러보기** | 🆕 **공개된 팀 목록 표시** (isPublic=true)<br>🆕 **TeamCard 그리드** (글래스모피즘)<br>🆕 **페이지네이션** (커서 기반)<br>🆕 **Navbar "공개 팀" 메뉴** | ✅ 완료 |
@ -700,6 +716,7 @@
| **ColorMode** | `color-mode.tsx` | 다크모드 토글 | ✅ 완료 | | **ColorMode** | `color-mode.tsx` | 다크모드 토글 | ✅ 완료 |
| **Toaster** | `toaster.tsx` | 토스트 알림 | ✅ 완료 | | **Toaster** | `toaster.tsx` | 토스트 알림 | ✅ 완료 |
| **Tooltip** | `tooltip.tsx` | 툴팁 | ✅ 완료 | | **Tooltip** | `tooltip.tsx` | 툴팁 | ✅ 완료 |
| **StageStepper** | `StageStepper.tsx` | 🆕 **스튜디오 6단계 가로 진행 표시기** (현재/완료 단계 brand 강조, 점수·숫자 미표시, 모바일은 점만 표시) | ✅ 완료 |
--- ---
@ -729,6 +746,15 @@
| **VisibilitySelector** | `VisibilitySelector.tsx` | 🆕 **공개 범위 선택** (PUBLIC/TEAM/PRIVATE, RadioCard 기반) | ✅ 완료 | | **VisibilitySelector** | `VisibilitySelector.tsx` | 🆕 **공개 범위 선택** (PUBLIC/TEAM/PRIVATE, RadioCard 기반) | ✅ 완료 |
| **VisibilityBadge** | `VisibilityBadge.tsx` | 🆕 **공개 범위 배지** (아이콘 + 라벨, 색상별 구분) | ✅ 완료 | | **VisibilityBadge** | `VisibilityBadge.tsx` | 🆕 **공개 범위 배지** (아이콘 + 라벨, 색상별 구분) | ✅ 완료 |
| **InteractiveImageViewer** | `InteractiveImageViewer.tsx` | 🆕 **인터랙티브 이미지 뷰어** (왜곡 효과, 애니메이션, 오디오 반응) | ✅ 완료 | | **InteractiveImageViewer** | `InteractiveImageViewer.tsx` | 🆕 **인터랙티브 이미지 뷰어** (왜곡 효과, 애니메이션, 오디오 반응) | ✅ 완료 |
| **RewardCanvasEditor** | `studio/RewardCanvasEditor.tsx` | 🆕 **스튜디오 보상 편집기** (인터랙션 S3 / 이펙트 S5 공용, `useDistortionEditor` 기반, 영역 1개 제한, onApplied 콜백) | ✅ 완료 |
| **StudioCard** | `studio/StudioCard.tsx` | 🆕 **스튜디오 공용 콘텐츠 카드** (tone default/muted/brand, cascade index, 단계 전반 통일 카드) | ✅ 완료 |
| **StudioStageHeader** | `studio/StudioStageHeader.tsx` | 🆕 **스튜디오 단계 헤더** (원형 아이콘 + 제목 + 부제, 전 단계 공용) | ✅ 완료 |
| **RolePill** | `studio/RolePill.tsx` | 🆕 **역할 식별 Pill** (상황/마음/까닭 아이콘+라벨, S2·S4 공용, `studioRoleMeta.ts` 메타 기반) | ✅ 완료 |
| **StudioImageCard** | `studio/StudioImageCard.tsx` | 🆕 **고정 비율 이미지 카드** (선택적 children, 예: 키워드 칩) | ✅ 완료 |
| **RewardBanner** | `studio/RewardBanner.tsx` | 🆕 **보상 축하 배너** (S3/S5 보상 단계 공용) | ✅ 완료 |
| **SentenceTokenEditor** | `studio/SentenceTokenEditor.tsx` | 🆕 **S4 낱말 칩 편집기** (낱말 탭→교체 Popover, +자리→삽입 Popover, 갱신 낱말 그라디언트, `sentenceTokens.ts` 조립/폴백 유틸) | ✅ 완료 |
| **studioMotion** | `studio/studioMotion.ts` | 🆕 **스튜디오 공용 애니메이션** (studioFadeIn/Pop/Breathe/EmptyBreathe @keyframes + STUDIO_REDUCED_MOTION prefers-reduced-motion 게이트) | ✅ 완료 |
| **studioRoleMeta** | `studio/studioRoleMeta.ts` | 🆕 **역할 메타 정의** (상황/마음/까닭 아이콘·라벨, RolePill 공용) | ✅ 완료 |
| **ImageDropzone** | `ImageDropzone.tsx` | 🆕 **범용 이미지 드롭존** (드래그앤드롭 업로드, 미리보기, 4:3 크롭 통합, 호버 오버레이, 삭제 버튼, AspectRatio 지원, 커스텀 라벨 — 글쓰기/팀 커버 이미지 공용) | ✅ 완료 | | **ImageDropzone** | `ImageDropzone.tsx` | 🆕 **범용 이미지 드롭존** (드래그앤드롭 업로드, 미리보기, 4:3 크롭 통합, 호버 오버레이, 삭제 버튼, AspectRatio 지원, 커스텀 라벨 — 글쓰기/팀 커버 이미지 공용) | ✅ 완료 |
| **ImageCropper** | `ImageCropper.tsx` | 🆕 **이미지 크롭 모달** (react-cropper 기반, 4:3 고정 비율, 회전/확대/축소, 최소 400x300) | ✅ 완료 | | **ImageCropper** | `ImageCropper.tsx` | 🆕 **이미지 크롭 모달** (react-cropper 기반, 4:3 고정 비율, 회전/확대/축소, 최소 400x300) | ✅ 완료 |
| **CameraCaptureDialog** | `CameraCaptureDialog.tsx` | 🆕 **카메라 촬영 다이얼로그** (GlassDialog 기반, getUserMedia 카메라 스트림, 4:3 프레임 가이드 오버레이, 전/후면 카메라 전환, 촬영 후 ImageCropper 연동, 권한/미지원 에러 처리, 다국어 지원) | ✅ 완료 | | **CameraCaptureDialog** | `CameraCaptureDialog.tsx` | 🆕 **카메라 촬영 다이얼로그** (GlassDialog 기반, getUserMedia 카메라 스트림, 4:3 프레임 가이드 오버레이, 전/후면 카메라 전환, 촬영 후 ImageCropper 연동, 권한/미지원 에러 처리, 다국어 지원) | ✅ 완료 |
@ -847,6 +873,31 @@
--- ---
### 📁 `src/app/[locale]/write/studio/` - 🆕 스튜디오 6단계 플로우
| 파일 | 경로 | 설명 | 상태 |
|------|------|------|------|
| **StudioPage** | `page.tsx` | 🆕 **오케스트레이터** (useReducer 상태머신, StudioFlowState 단일 진실 공급원, goNext 부수효과 처리, AI 호출, draft 생성/발행) | ✅ 완료 |
| **Flow 계약** | `_flow/types.ts` | 🆕 **StudioFlowState / StudioStageProps** (오케스트레이터↔스테이지 prop 시그니처), `deriveBlankValues` | ✅ 완료 |
| **IntroStage** | `_stages/IntroStage.tsx` | 🆕 **S0 안내** (오프라인에서 종이에 그림 그리기 안내) | ✅ 완료 |
| **UploadStage** | `_stages/UploadStage.tsx` | 🆕 **S1 업로드** (그림 촬영/업로드 → AI 키워드 5개 추출) | ✅ 완료 |
| **FirstWritingStage** | `_stages/FirstWritingStage.tsx` | 🆕 **S2 1차 작문** (그림 + 키워드 5개 + 역할별 자유 3문장 + 전구 안내 + 예문, 학생 단독·AI 없음) | ✅ 완료 |
| **RewardInteractionStage** | `_stages/RewardInteractionStage.tsx` | 🆕 **S3 보상①** (인터랙션/움직임 1개 부여, RewardCanvasEditor) | ✅ 완료 |
| **SecondWritingStage** | `_stages/SecondWritingStage.tsx` | 🆕 **S4 글 다듬기** (낱말 분해 + 교체·삽입 추천, SentenceTokenEditor) | ✅ 완료 |
| **RewardEffectStage** | `_stages/RewardEffectStage.tsx` | 🆕 **S5 보상②** (스프라이트 이펙트 1개 부여, RewardCanvasEditor) | ✅ 완료 |
| **CompletionStage** | `_stages/CompletionStage.tsx` | 🆕 **S6 완료** (1회차 수업 종료, complete API 호출 → 발행) | ✅ 완료 |
**주요 기능**:
- ✅ **단일 페이지 선형 상태머신** (useReducer, 단계 전환 부수효과는 오케스트레이터 goNext에서 처리)
- ✅ **역할별 자유 3문장**: 상황/감정/이유를 한 문장씩 자유 작성 (고정 빈칸 템플릿 제거, 선생님 직접 교육 + 예문·전구 안내로 지도)
- ✅ **S2→S3 전환**: 모든 입력 채움 검증 → draft Writing 생성(createWriting) + 이미지 업로드(uploadUserImage), 1차 문장을 2차 작문 초기값으로 시드
- ✅ **S3/S5 보상**: 공용 RewardCanvasEditor (영역 1개 제한), 적용 시 distortionAreas/spriteEffectAreas 저장 후 자동 전진
- ✅ **AI 엔드포인트 4개**: `/api/studio/analyze`(키워드), `/api/studio/segment`(S4 분해), `/api/studio/suggest`(S4 교체·삽입 추천), `/api/studio/complete`(발행)
- ✅ **데이터 저장**: writings.keywords + writings.studioResult (sentencesV1/V2, blanks, grantedInteraction/Effect, completedAt), status draft→published, rewardType `participation`
- ✅ **다국어 지원** (studio namespace, pageTitles.writeStudio, ko/en/ja)
---
### 📁 `src/extensions/` - Tiptap Extensions ### 📁 `src/extensions/` - Tiptap Extensions
| Extension | 파일명 | 설명 | 상태 | | Extension | 파일명 | 설명 | 상태 |
@ -1065,6 +1116,7 @@ firebase functions:log --only cleanupExpiredReservations
| **Pattern Analysis** | `patternAnalysisService.ts` | 🆕 **글 작성 패턴 분석 서비스** (10개 글 종합 분석, AI 평가) | ✅ 완료 | | **Pattern Analysis** | `patternAnalysisService.ts` | 🆕 **글 작성 패턴 분석 서비스** (10개 글 종합 분석, AI 평가) | ✅ 완료 |
| **Spelling Check** | `spellingService.ts` | **맞춤법 검사 서비스** (Gemini 기반, 초등학생 눈높이) | ✅ 완료 | | **Spelling Check** | `spellingService.ts` | **맞춤법 검사 서비스** (Gemini 기반, 초등학생 눈높이) | ✅ 완료 |
| **Writing Assistance** | `writingAssistanceService.ts` | 🆕 **AI 글쓰기 도우미 서비스** (4단계 힌트 생성, 주제 맥락 활용, 서버 캐싱) | ✅ 완료 | | **Writing Assistance** | `writingAssistanceService.ts` | 🆕 **AI 글쓰기 도우미 서비스** (4단계 힌트 생성, 주제 맥락 활용, 서버 캐싱) | ✅ 완료 |
| **Studio Service** | `studioService.ts` | 🆕 **스튜디오 플로우 AI 서비스** (extractStudioKeywords: 그림→키워드 5개, segmentSentences: 띄어쓰기 교정+낱말 분해, suggestReplacements: 교체·삽입 추천, _ai 사용량 메타 전달) | ✅ 완료 |
| **Region Health** | `regionHealthManager.ts` | **Region 과부하 상태 추적** (자동 복구, 1분 TTL) | ✅ 완료 | | **Region Health** | `regionHealthManager.ts` | **Region 과부하 상태 추적** (자동 복구, 1분 TTL) | ✅ 완료 |
| ~~**Team Service**~~ | ~~`teamService.ts`~~ | ~~팀 CRUD~~ | ⚠️ Deprecated (TeamManager로 이동) | | ~~**Team Service**~~ | ~~`teamService.ts`~~ | ~~팀 CRUD~~ | ⚠️ Deprecated (TeamManager로 이동) |
| ~~**Student Service**~~ | ~~`studentService.ts`~~ | ~~학생 CRUD, PIN 해시/검증~~ | ⚠️ Deprecated (UserManager로 대체) | | ~~**Student Service**~~ | ~~`studentService.ts`~~ | ~~학생 CRUD, PIN 해시/검증~~ | ⚠️ Deprecated (UserManager로 대체) |
@ -1085,7 +1137,8 @@ firebase functions:log --only cleanupExpiredReservations
| **Plan 타입** | `plan.ts` | 🆕 **플랜 시스템 타입** (PlanType/BillingCycle/PlanSource/AIFeatureType Enum, UserPlan, Organization, AIUsage, PlanLimits, EffectivePlan, AIFeatureCheckResult) | ✅ 완료 | | **Plan 타입** | `plan.ts` | 🆕 **플랜 시스템 타입** (PlanType/BillingCycle/PlanSource/AIFeatureType Enum, UserPlan, Organization, AIUsage, PlanLimits, EffectivePlan, AIFeatureCheckResult) | ✅ 완료 |
| **Team 타입** | `team.ts` | 팀 데이터 모델 (members Map), 🆕 **AIAssistanceConfig** (AI 도우미 설정) | ✅ 완료 | | **Team 타입** | `team.ts` | 팀 데이터 모델 (members Map), 🆕 **AIAssistanceConfig** (AI 도우미 설정) | ✅ 완료 |
| **FirestoreUser 타입** | `firestoreUser.ts` | **FirestoreUser** (DB 저장용, 🆕 **plan/organizationId 필드 추가**, 🆕 **loginId/isSystemAccount/createdByTeacher 필드**), **User** (UI용) 분리 | ✅ 완료 | | **FirestoreUser 타입** | `firestoreUser.ts` | **FirestoreUser** (DB 저장용, 🆕 **plan/organizationId 필드 추가**, 🆕 **loginId/isSystemAccount/createdByTeacher 필드**), **User** (UI용) 분리 | ✅ 완료 |
| **Writing 타입** | `writing.ts` | 글 데이터 모델, 🆕 **WritingAnalysis** (AI 분석 결과, contentHash 기반 재사용), **SpellingError** (맞춤법 오류), 🆕 **AIAssistanceRecord** (AI 도움 이력), 🆕 **GeneratedImage** (AI 생성 이미지) | ✅ 완료 | | **Writing 타입** | `writing.ts` | 글 데이터 모델, 🆕 **WritingAnalysis** (AI 분석 결과, contentHash 기반 재사용), **SpellingError** (맞춤법 오류), 🆕 **AIAssistanceRecord** (AI 도움 이력), 🆕 **GeneratedImage** (AI 생성 이미지), 🆕 **keywords?·studioResult?** (스튜디오 플로우 필드) | ✅ 완료 |
| **Studio 타입** | `studio.ts` | 🆕 **스튜디오 6단계 플로우 타입** (StudioStage, StudioSentenceKey, StudioBlank, StudioScaffold, StudioToken, StudioResult, Analyze/Segment/Suggest/Complete Request·Response, STUDIO_STAGE_ORDER/SENTENCE_ORDER) | ✅ 완료 |
| **Scene 타입** | `scene.ts` | 🆕 **장면 데이터 모델** (Scene, SceneExtractionResponse) | ✅ 완료 | | **Scene 타입** | `scene.ts` | 🆕 **장면 데이터 모델** (Scene, SceneExtractionResponse) | ✅ 완료 |
| **Draft 타입** | `draft.ts` | 글조각 데이터 모델 (Draft, DraftListItem, **AnalysisHistoryItem**, **syncStatus**: 'local'\|'synced'\|'syncing') | ✅ 완료 | | **Draft 타입** | `draft.ts` | 글조각 데이터 모델 (Draft, DraftListItem, **AnalysisHistoryItem**, **syncStatus**: 'local'\|'synced'\|'syncing') | ✅ 완료 |
| **WritingPattern 타입** | `writingPattern.ts` | **글 작성 패턴 분석** 데이터 모델 (WritingPatternAnalysis) | ✅ 완료 | | **WritingPattern 타입** | `writingPattern.ts` | **글 작성 패턴 분석** 데이터 모델 (WritingPatternAnalysis) | ✅ 완료 |
@ -1146,6 +1199,9 @@ firebase functions:log --only cleanupExpiredReservations
| **Writing Assistance** | `writingAssistance.ts` | 🆕 **AI 글쓰기 도우미 프롬프트** (4단계 × 3개 언어, 주제 맥락 활용) | ✅ 완료 | | **Writing Assistance** | `writingAssistance.ts` | 🆕 **AI 글쓰기 도우미 프롬프트** (4단계 × 3개 언어, 주제 맥락 활용) | ✅ 완료 |
| **Scene Extraction** | `sceneExtraction.ts` | 🆕 **AI 장면 추출 프롬프트** (3~5개 장면 분리, Response Schema, ko/en/ja) | ✅ 완료 | | **Scene Extraction** | `sceneExtraction.ts` | 🆕 **AI 장면 추출 프롬프트** (3~5개 장면 분리, Response Schema, ko/en/ja) | ✅ 완료 |
| **Prompt Optimization** | `promptOptimization.ts` | 🆕 **프롬프트 최적화 프롬프트** (원문 → 핵심 키워드 배열 추출, Imagen 최적화) | ✅ 완료 | | **Prompt Optimization** | `promptOptimization.ts` | 🆕 **프롬프트 최적화 프롬프트** (원문 → 핵심 키워드 배열 추출, Imagen 최적화) | ✅ 완료 |
| **Studio Templates** | `studio/templates.ts` | 🆕 **스튜디오 역할별 자유 슬롯** (상황/감정/이유 1슬롯씩, STUDIO_BLANK_IDS, getStudioBlanks, composeStudioSentences, studioBlankId) | ✅ 완료 |
| **Studio Segmentation** | `studio/segmentation.ts` | 🆕 **S4 분해 프롬프트** (의미 단위 낱말 덩어리 + 띄어쓰기 교정, ko/en/ja) | ✅ 완료 |
| **Studio Suggestion** | `studio/suggestion.ts` | 🆕 **S4 추천 프롬프트** (낱말 교체 + 자리 삽입 추천, ko/en/ja) | ✅ 완료 |
--- ---
@ -1157,6 +1213,10 @@ firebase functions:log --only cleanupExpiredReservations
| **패턴 분석** | `/api/analyze-pattern` | POST | **글 작성 패턴 분석** (3가지 타입, contentHash 기반 3단계 캐싱, 변경 감지) | ✅ 완료 | | **패턴 분석** | `/api/analyze-pattern` | POST | **글 작성 패턴 분석** (3가지 타입, contentHash 기반 3단계 캐싱, 변경 감지) | ✅ 완료 |
| **맞춤법 검사** | `/api/spelling/check` | POST | **Gemini 기반 맞춤법 검사** (초등학생 눈높이) | ✅ 완료 | | **맞춤법 검사** | `/api/spelling/check` | POST | **Gemini 기반 맞춤법 검사** (초등학생 눈높이) | ✅ 완료 |
| **AI 글쓰기 도우미** | `/api/writing-assistance` | POST | 🆕 **AI 힌트 생성** (4단계, 주제 맥락, 팀 설정 검증) | ✅ 완료 | | **AI 글쓰기 도우미** | `/api/writing-assistance` | POST | 🆕 **AI 힌트 생성** (4단계, 주제 맥락, 팀 설정 검증) | ✅ 완료 |
| **스튜디오 그림 분석** | `/api/studio/analyze` | POST | 🆕 **S1 그림 키워드 추출** (analyzeImage 재사용, 키워드 5개 + 분석 결과, 플랜 검증) | ✅ 완료 |
| **스튜디오 분해** | `/api/studio/segment` | POST | 🆕 **S4 띄어쓰기 교정 + 낱말 분해** (의미 단위 낱말 덩어리, 무결성 가드) | ✅ 완료 |
| **스튜디오 추천** | `/api/studio/suggest` | POST | 🆕 **S4 교체·삽입 추천** (낱말 교체 + 자리 삽입, Response Schema) | ✅ 완료 |
| **스튜디오 완료** | `/api/studio/complete` | POST | 🆕 **S6 발행** (draft→published, studioResult 저장, HTML sanitize, AI 호출 없음, 주제 사용량 증가) | ✅ 완료 |
| **글 생성** | `/api/writing` | POST | 🆕 **글 생성** (wordCount/charCount 자동 계산, 🆕 **analysis 저장**) | ✅ 완료 | | **글 생성** | `/api/writing` | POST | 🆕 **글 생성** (wordCount/charCount 자동 계산, 🆕 **analysis 저장**) | ✅ 완료 |
| **글 조회** | `/api/writing/[id]` | GET | 🆕 **글 조회** (작성자만 접근) | ✅ 완료 | | **글 조회** | `/api/writing/[id]` | GET | 🆕 **글 조회** (작성자만 접근) | ✅ 완료 |
| **글 분석** | `/api/writing/[id]/analyze` | POST | 🆕 **글 분석 실행** (서버 데이터 기반, 결과 저장) | ✅ 완료 | | **글 분석** | `/api/writing/[id]/analyze` | POST | 🆕 **글 분석 실행** (서버 데이터 기반, 결과 저장) | ✅ 완료 |

View File

@ -1,6 +1,6 @@
# 라온누리 - 개발 로드맵 # 라온누리 - 개발 로드맵
> 최종 업데이트: 2026-03-27 (학생/교사 로그인 분리) > 최종 업데이트: 2026-06-04 (S2 첫 글쓰기 자유 작성 전환)
초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획 초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획
@ -167,6 +167,9 @@
| **학생 일괄 생성 (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** | | **학생 일괄 생성 (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** | | **학생/교사 로그인 분리** | **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** |
| **스튜디오 6단계 선형 글쓰기 플로우** | **"그림으로 시작 → 글 완성" 신규 라우트 /write/studio (기존 fill-in-blank/image/easy 라우트와 병행 운영, 추후 제거 예정), 단일 페이지 상태머신 (page.tsx 오케스트레이터 useReducer, StudioFlowState 단일 진실 공급원, _flow/types.ts에 StudioStageProps 계약 정의), 7개 스테이지 컴포넌트 (_stages/Intro·Upload·FirstWriting·RewardInteraction·SecondWriting·RewardEffect·CompletionStage), StageStepper 진행 표시기 (점수·숫자 미표시, 참여 기반), 6단계: S0 안내(종이 그림)→S1 업로드(AI 키워드 5개 추출)→S2 1차 작문(3문장 빈칸: 상황 2칸/감정/이유, 학생 단독 AI 없음)→S3 보상① 인터랙션→S4 2차 작문(AI 추천 표현 종류별)→S5 보상② 이펙트→S6 완료(발행), S2→S3 전환 시 draft Writing 생성(createWriting)+이미지 업로드(uploadUserImage), 공용 RewardCanvasEditor (인터랙션/이펙트 공용, 영역 1개 제한, useDistortionEditor 기반), AI 엔드포인트 3개 (POST /api/studio/analyze·recommend-expressions·complete), studioService.ts (extractStudioKeywords·recommendExpressions), 프롬프트 studio/templates.ts·expressionRecommendation.ts, 타입 studio.ts (StudioStage/SentenceKey/Blank/Result 등), writing.ts에 keywords?·studioResult? 추가, writings 컬렉션에 keywords·studioResult(sentencesV1/V2·blanks·grantedInteraction/Effect·completedAt) 저장, 보상은 distortionAreas/spriteEffectAreas, status draft→published, rewardType participation, /write 진입 카드 추가, 다국어 지원 (studio namespace, pageTitles.writeStudio, ko/en/ja)** | **2026-06-02** |
| **스튜디오 플로우 UI 통합 리디자인** | **공용 프리미티브 + 단계별 일관 레이아웃, S2 빈칸 입력 버그 수정 (src/components/writing/studio/에 표현 전용 공용 프리미티브 정리: StudioCard·StudioStageHeader·RolePill+studioRoleMeta·FillBlankSentence·StudioImageCard·RewardBanner·ExpressionChip·studioMotion, 전 단계가 하나의 카드/헤더/애니메이션 시스템 공유, FillBlankSentence 고정 폭 빈칸 입력 + 빈칸별 힌트로 입력 늘어남 버그 수정, studio namespace i18n 키 추가 roles.{situation,emotion,reason}·firstWriting.blankStub·rewardInteraction.tip·rewardEffect.tip·reward.{controlsTitle,dragHint,applyButton}·secondWriting.contextTitle, ko/en/ja)** | **2026-06-04** |
| **S2 첫 글쓰기 자유 작성 전환** | **고정 빈칸 템플릿("나는 ___ 때 ___ 일이 있었습니다" 등) 제거 → 선생님 직접 교육 방식. 역할(상황/감정/이유)별 자유 Textarea 1문장 + 전구 안내(guides) + 예문 2개(examples)를 FirstWritingStage가 직접 렌더, FillBlankSentence 컴포넌트 삭제, templates.ts 토큰 템플릿 제거하고 역할당 1슬롯 모델로 단순화 (STUDIO_BLANK_IDS 3개·getStudioBlanks·composeStudioSentences(values)·studioBlankId), StudioTemplateToken/StudioSentenceTemplate 타입 제거, 다운스트림(sentencesV1/V2·S4 분해) 불변, 예문/안내는 현재 i18n 하드코딩(주제 연동 시 주제 데이터로 변수화 예정), firstWriting i18n 재구성 freePlaceholder·examplesLabel·guides.{situation,emotion,reason}·examples.{role}.{0,1} (ko/en/ja). 부수: TECH_STACK 스튜디오 섹션을 이미 반영된 S4 segment/suggest 파이프라인 기준으로 정정** | **2026-06-04** |
### 🚧 진행 중 ### 🚧 진행 중

View File

@ -1,6 +1,6 @@
# 라온누리 - 기술 스택 및 개발 환경 # 라온누리 - 기술 스택 및 개발 환경
> 최종 업데이트: 2025-12-23 (이미지 4:3 비율 표준화) > 최종 업데이트: 2026-06-04 (스튜디오 플로우 UI 통합 리디자인)
--- ---
@ -2638,6 +2638,156 @@ interface BulkCreateResult {
--- ---
### 27. 스튜디오 6단계 선형 글쓰기 플로우 (Studio Flow)
#### 핵심 개념
**목적**: "그림으로 시작 → 글 완성"을 하나의 페이지에서 진행하는 1회차 수업형 선형 플로우.
기존 글쓰기 라우트(`/write/fill-in-blank`, `/write/image`, `/write/easy`)와 **병행 운영**하는 신규 라우트(`/write/studio`)이며, 레거시 라우트는 추후 제거 예정이다.
**설계 원칙**:
- **점수/제한 없음**: 참여 기반 플로우 (StageStepper에 숫자 미표시, `rewardType: 'participation'`)
- **AI는 아동 행동 이후에만**: S1 키워드 추출은 업로드 후, S4 추천 표현은 학생 1차 작문 후 실행
- **단일 진실 공급원**: 오케스트레이터가 전역 상태(StudioFlowState)를 소유, 스테이지는 props만 받는다
#### 단일 페이지 상태머신 아키텍처
```
┌──────────────────────────────────────────────────────────┐
│ 오케스트레이터 src/app/[locale]/write/studio/page.tsx │
│ - useReducer(flowReducer, StudioFlowState) ← 단일 진실 │
│ - goNext(): 현재 단계별 부수효과 실행 후 advance() │
│ - AI 호출 / draft 생성 / 보상 저장 / 발행 모두 여기서 처리 │
│ - StageStepper(현재 단계) + AnimatePresence(스테이지 전환) │
└──────────────────────────────────────────────────────────┘
│ Pick<StudioStageProps, ...> 으로 필요한 props만 전달
┌──────────────────────────────────────────────────────────┐
│ 스테이지 컴포넌트 src/app/[locale]/write/studio/_stages/ │
│ IntroStage · UploadStage · FirstWritingStage · │
│ RewardInteractionStage · SecondWritingStage · │
│ RewardEffectStage · CompletionStage │
│ (상태/부수효과 없음 — 입력 수집 + 콜백 호출만) │
└──────────────────────────────────────────────────────────┘
│ 보상 단계(S3/S5)는 공용 편집기 사용
┌──────────────────────────────────────────────────────────┐
│ 공용 컴포넌트 │
│ src/components/ui/StageStepper.tsx (6단계 진행 표시기) │
│ src/components/writing/studio/RewardCanvasEditor.tsx │
│ - mode: "interaction" | "effect" 로 S3/S5 공용 │
│ - useDistortionEditor 기반, 영역 1개 제한 (AREA_CAP=1) │
└──────────────────────────────────────────────────────────┘
```
**계약 파일**: `src/app/[locale]/write/studio/_flow/types.ts``StudioFlowState`(전역 상태) / `StudioStageProps`(스테이지 prop 시그니처) / `deriveBlankValues()` 헬퍼.
#### S0 → S6 단계 흐름
```
S0 intro 오프라인에서 종이에 그림 그리기 안내 (앱 외부 활동)
↓ goNext()
S1 upload 그림 촬영/업로드 → AI 키워드 5개 추출
↓ runAnalyze() → POST /api/studio/analyze
S2 firstWriting 그림 + 키워드 5개 + 역할별 자유 3문장 (학생 단독, AI 없음)
· situation: 상황 — 언제/어디서 무슨 일이 있었는지 (자유 1문장)
· emotion: 감정 — 그때 어떤 마음이었는지 (자유 1문장)
· reason: 이유 — 왜 그런 마음이 들었는지 (자유 1문장)
· 문장 구조를 플랫폼이 고정하지 않음(선생님 직접 교육). 역할별 예문 + 전구 안내로만 지도. 주제 연동 시 예문/안내는 주제 데이터로 대체 예정(현재 i18n 하드코딩)
↓ createDraft(): 입력 검증 → writingManager.createWriting(status:'draft')
↓ + uploadUserImage() + 1차 문장을 2차 작문 초기값으로 시드
S3 rewardInteraction 인터랙션(움직임) 1개 부여 → 적용 (RewardCanvasEditor, 영역 1개)
↓ onInteractionApplied() → updateDistortionAreas()
S4 secondWriting 3문장 다듬기 — AI 띄어쓰기 교정 + 낱말 분해 → 낱말 교체·자리 삽입 추천 (낱말 칩 편집)
↓ 진입 시 fetchSegmentation() → POST /api/studio/segment (분해 후) fetchSuggestions() → POST /api/studio/suggest
S5 rewardEffect 스프라이트 이펙트 1개 부여 → 적용 (RewardCanvasEditor, 영역 1개)
↓ onEffectApplied() → updateDistortionAreas()
S6 complete 1회차 수업 종료 → onComplete() → POST /api/studio/complete (발행)
```
#### AI 엔드포인트 4개
| API | 경로 | AI | 역할 |
|-----|------|----|------|
| **분석** | `POST /api/studio/analyze` | ✅ | 그림 분석(analyzeImage 재사용) → 키워드 5개 + ImageAnalysisResult |
| **분해** | `POST /api/studio/segment` | ✅ | S4: 3문장 → 띄어쓰기 교정 + 의미 단위 낱말 분해 (낱말 텍스트 그대로, 글자 변조 금지 가드) |
| **추천** | `POST /api/studio/suggest` | ✅ | S4: 분해된 낱말 → 낱말 교체 추천 + 자리 삽입 추천 (분해와 분리된 별도 호출) |
| **완료** | `POST /api/studio/complete` | ❌ | draft→published 발행, studioResult 저장, HTML sanitize |
**서비스 레이어**: `src/services/studioService.ts`
- `extractStudioKeywords(imageBase64, mimeType, locale)` — imageAnalysisService.analyzeImage 재사용, suggestedKeywords→(부족 시)objects 순으로 정확히 5개 확정 (패딩 없음)
- `segmentSentences(sentences, locale)` — AI가 의미 단위 낱말 덩어리로 반환 → 결정적 분해/조립, `stripSpaces` 무결성 가드 통과 시 채택, 실패 시 공백 분리 폴백
- `suggestReplacements(sentences, keywords, locale)` — generateContent + Response Schema, 문장별 `{replacements, insertions}`(교체·삽입) 클램프/정제, 실패 시 빈 추천 폴백
- 사용량 메타데이터(`_ai`)는 라우트로 그대로 전달 → 라우트가 logAIUsage 기록
**프롬프트**: `src/prompts/studio/templates.ts`(역할별 자유 슬롯 — STUDIO_BLANK_IDS/getStudioBlanks/composeStudioSentences/studioBlankId), `src/prompts/studio/segmentation.ts`·`suggestion.ts`(ko/en/ja)
#### 데이터 / 영속화 모델
draft를 먼저 만들고(S2→S3) 단계별로 갱신한 뒤 S6에서 발행하는 점진적 저장 모델.
```typescript
// src/types/writing.ts (스튜디오 모드에서만 사용)
interface Writing {
// ...기존 필드
keywords?: string[]; // 🆕 그림에서 추출한 키워드 5개
studioResult?: StudioResult; // 🆕 스튜디오 플로우 결과
}
// src/types/studio.ts
interface StudioResult {
keywords: string[];
sentencesV1: Record<StudioSentenceKey, string>; // 1차 작문 (학생 단독)
sentencesV2: Record<StudioSentenceKey, string>; // 2차 작문 (AI 도움, 최종 본문)
blanks: StudioBlank[];
grantedInteraction: boolean; // S3 보상 적용 여부
grantedEffect: boolean; // S5 보상 적용 여부
completedAt: string; // ISO 8601
}
```
**저장 위치별 정리**:
- **writings 컬렉션**: `keywords`, `studioResult`(sentencesV1/V2·blanks·grantedInteraction/Effect·completedAt)
- **보상**: 인터랙션/이펙트는 `distortionAreas` / `spriteEffectAreas` (S3/S5에서 클라이언트가 저장, complete API는 건드리지 않음)
- **상태 전이**: `status` draft → published (최초 발행 시 주제 usageCount 1회 증가)
- **보상 분류**: `rewardType: 'participation'`
- **하드닝**: complete API에서 본문 HTML sanitize + studioResult 자유 입력값은 태그 제거 후 평문 저장
#### 진입점 및 i18n
- **진입 카드**: `/write` 모드 선택 화면 상단에 스튜디오 진입 카드 추가 (`router.push("/write/studio")`)
- **다국어**: `studio` 네임스페이스(stepper/actions/error/단계별 키), `pageTitles.writeStudio` (ko/en/ja)
- **접근 제어**: `useRequireAiAccess()` + `useAiIpCheck()` (AI 사용 가능 검증)
#### 공용 프리미티브 / 통합 비주얼 언어 (2026-06-04)
전 단계가 **하나의 카드/헤더/애니메이션 시스템**을 공유하도록 `src/components/writing/studio/`에 표현 전용(presentational) 공용 프리미티브를 정리했다. 스테이지는 레이아웃 코드를 직접 들지 않고 이 프리미티브만 조합한다.
- **`StudioCard`** — 단계 공용 콘텐츠 카드 (tone default/muted/brand, cascade index 진입 애니메이션)
- **`StudioStageHeader`** — 원형 아이콘 + 제목 + 부제 헤더 (전 단계 통일)
- **`RolePill` + `studioRoleMeta`** — 상황/마음/까닭 역할 식별(아이콘+라벨), S2·S4 공용
- **`StudioImageCard`** — 고정 비율 이미지 카드 (선택적 children, 예: 키워드 칩)
- **`RewardBanner`** — S3/S5 보상 축하 배너 공용
- **`SentenceTokenEditor` + `sentenceTokens`** — S4 낱말 칩 편집기 (낱말 탭→교체 Popover, +자리→삽입 Popover, 갱신 낱말 그라디언트 표시) + 토큰 조립/폴백 유틸
- **`studioMotion`** — 공용 `@keyframes`(studioFadeIn/Pop/Breathe/EmptyBreathe) + `STUDIO_REDUCED_MOTION`(prefers-reduced-motion 게이트)
- **S2 자유 작성**: 고정 빈칸 템플릿(FillBlankSentence) 제거 → FirstWritingStage가 역할별 자유 Textarea + 전구 안내 + 예문을 직접 렌더
- **i18n 추가**: `studio` 네임스페이스에 `roles.{situation,emotion,reason}`, `firstWriting.{freePlaceholder,examplesLabel,guides.*,examples.*}`, `rewardInteraction.tip`, `rewardEffect.tip`, `reward.{controlsTitle,dragHint,applyButton}`, `secondWriting.*`(낱말 편집 키) (ko/en/ja)
#### 참고 파일
- `src/app/[locale]/write/studio/page.tsx` - 오케스트레이터 (useReducer 상태머신)
- `src/app/[locale]/write/studio/_flow/types.ts` - StudioFlowState / StudioStageProps
- `src/app/[locale]/write/studio/_stages/*.tsx` - 7개 스테이지 컴포넌트
- `src/components/ui/StageStepper.tsx` - 6단계 진행 표시기
- `src/components/writing/studio/RewardCanvasEditor.tsx` - 인터랙션/이펙트 공용 편집기
- `src/components/writing/studio/{StudioCard,StudioStageHeader,RolePill,StudioImageCard,RewardBanner,SentenceTokenEditor}.tsx` + `{sentenceTokens,studioMotion,studioRoleMeta}.ts` - 공용 프리미티브 (통합 비주얼 언어)
- `src/services/studioService.ts` - extractStudioKeywords / segmentSentences / suggestReplacements
- `src/app/api/studio/{analyze,segment,suggest,complete}/route.ts` - AI 엔드포인트 4개
- `src/prompts/studio/{templates.ts,segmentation.ts,suggestion.ts}` - 프롬프트
- `src/types/studio.ts` - 플로우 타입, `src/types/writing.ts` - keywords?/studioResult?
---
## 참고 문서 ## 참고 문서
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조 - [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조