docs: Sync documentation from private repository
This commit is contained in:
parent
204114b769
commit
7d37a6c89e
313
API_SPEC.md
313
API_SPEC.md
@ -12,20 +12,17 @@
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ 올바른 방식 (RESTful)
|
// ✅ 올바른 방식 (RESTful)
|
||||||
POST /api/team/:id/allowed-names // 이름 추가
|
POST /api/team/:id/members // 멤버 추가
|
||||||
DELETE /api/team/:id/allowed-names // 이름 제거
|
DELETE /api/team/:id/members // 멤버 제거
|
||||||
|
|
||||||
POST /api/team/:id/allowed-emails // 이메일 추가
|
|
||||||
DELETE /api/team/:id/allowed-emails // 이메일 제거
|
|
||||||
|
|
||||||
// ❌ 잘못된 방식 (경로로 구분)
|
// ❌ 잘못된 방식 (경로로 구분)
|
||||||
POST /api/team/:id/allowed-names/add // ❌
|
POST /api/team/:id/members/add // ❌
|
||||||
POST /api/team/:id/allowed-names/remove // ❌
|
POST /api/team/:id/members/remove // ❌
|
||||||
```
|
```
|
||||||
|
|
||||||
**API Route 파일 구조**:
|
**API Route 파일 구조**:
|
||||||
```typescript
|
```typescript
|
||||||
// src/app/api/team/[teamId]/allowed-names/route.ts
|
// src/app/api/team/[teamId]/members/route.ts
|
||||||
|
|
||||||
export async function POST(request: NextRequest, context: RouteContext) {
|
export async function POST(request: NextRequest, context: RouteContext) {
|
||||||
// 추가 로직
|
// 추가 로직
|
||||||
@ -550,18 +547,7 @@ patternAnalyses/{contentHash}
|
|||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
name: string; // 팀 이름
|
name: string; // 팀 이름
|
||||||
code: string; // 팀 코드 (고유)
|
|
||||||
securityLevel: 1 | 2 | 3 | 4 | 5; // 🆕 5단계 보안 레벨
|
|
||||||
allowedNames?: string[]; // 🆕 Level 2용 (명단 기반)
|
|
||||||
allowedEmails?: string[]; // 🆕 Level 4용 (이메일 화이트리스트)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 보안 레벨:
|
|
||||||
// 1: OPEN (완전 개방, 닉네임 공유 로그인)
|
|
||||||
// 2: NAME_LIST (명단 기반, 등록된 이름만)
|
|
||||||
// 3: AUTH_REQUIRED (로그인 필수, 정식 계정 누구나)
|
|
||||||
// 4: EMAIL_LIST (이메일 화이트리스트)
|
|
||||||
// 5: CLOSED (닫힌 팀, 신규 가입 불가)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response**:
|
**Response**:
|
||||||
@ -598,33 +584,7 @@ patternAnalyses/{contentHash}
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. POST `/team/search` - 팀 코드로 조회
|
### 3. GET `/team/list` - 내 팀 목록
|
||||||
실제 URL: `POST /api/team/search`
|
|
||||||
|
|
||||||
**인증**: 선택적
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
code: string; // 팀 코드 (정규화됨)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
team: Team | null; // 팀이 없으면 null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**캐싱**: 클라이언트에서 1분간 캐싱
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. GET `/team/list` - 내 팀 목록
|
|
||||||
실제 URL: `GET /api/team/list`
|
실제 URL: `GET /api/team/list`
|
||||||
|
|
||||||
**인증**: 필수 (정식 계정)
|
**인증**: 필수 (정식 계정)
|
||||||
@ -797,99 +757,7 @@ patternAnalyses/{contentHash}
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. POST `/team/generate-code` - 고유 팀 코드 생성
|
### 🆕 10. POST `/team/remove-member` - 팀 멤버 제거/나가기
|
||||||
실제 URL: `POST /api/team/generate-code`
|
|
||||||
|
|
||||||
**인증**: 선택적
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
maxAttempts?: number; // 기본값: 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
code: string; // 생성된 고유 팀 코드
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 11. POST `/team/check-code` - 팀 코드 존재 여부
|
|
||||||
실제 URL: `POST /api/team/check-code`
|
|
||||||
|
|
||||||
**인증**: 선택적
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
exists: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🆕 12. POST `/team/get-custom-token` - Custom Token 생성 (Level 1 재로그인)
|
|
||||||
실제 URL: `POST /api/team/get-custom-token`
|
|
||||||
|
|
||||||
**설명**: Level 1 (OPEN) 팀에서 같은 닉네임으로 재로그인 시 사용. 익명 계정만 허용.
|
|
||||||
|
|
||||||
**인증**: 불필요 (공개 API)
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
uid: string; // 로그인하려는 사용자의 Firebase UID
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
// 성공 (익명 계정)
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
customToken: string; // Firebase Custom Token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 실패 (정식 계정)
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: "해당 이름은 다른 사용자가 사용 중입니다."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**보안**:
|
|
||||||
- ✅ 익명 계정 검증: Firebase Admin SDK로 `providerData` 확인
|
|
||||||
- ✅ 정식 계정 차단: Google/Email 계정은 Custom Token 발급 불가
|
|
||||||
- ✅ 탈취 방지: 정식 계정 이름으로 타인이 로그인 불가
|
|
||||||
|
|
||||||
**사용 흐름**:
|
|
||||||
1. 클라이언트에서 `team.members` 검색 → 같은 닉네임 발견
|
|
||||||
2. 해당 UID로 Custom Token 요청
|
|
||||||
3. 익명 계정이면 Token 발급, 정식 계정이면 403
|
|
||||||
4. Token으로 `signInWithCustomToken()` 호출
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🆕 13. POST `/team/remove-member` - 팀 멤버 제거/나가기
|
|
||||||
실제 URL: `POST /api/team/remove-member`
|
실제 URL: `POST /api/team/remove-member`
|
||||||
|
|
||||||
**설명**: 팀에서 멤버를 제거합니다. 소유자는 다른 멤버를 강퇴할 수 있고, 일반 멤버는 본인을 제거(팀 나가기)할 수 있습니다.
|
**설명**: 팀에서 멤버를 제거합니다. 소유자는 다른 멤버를 강퇴할 수 있고, 일반 멤버는 본인을 제거(팀 나가기)할 수 있습니다.
|
||||||
@ -929,111 +797,6 @@ await teamManager.removeMember(teamId, currentUser.uid);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🆕 14. POST `/team/:teamId/security-level` - 보안 레벨 변경
|
|
||||||
실제 URL: `POST /api/team/:teamId/security-level`
|
|
||||||
|
|
||||||
**인증**: 필수 (팀 소유자만)
|
|
||||||
|
|
||||||
**Request**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
securityLevel: 1 | 2 | 3 | 4 | 5;
|
|
||||||
autoPopulateList?: boolean; // 기본값: true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
team: Team; // 업데이트된 팀
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**autoPopulateList 동작**:
|
|
||||||
- `true`: 기존 멤버를 자동으로 명단에 추가
|
|
||||||
- Level 2로 변경 → 기존 멤버 이름을 `allowedNames`에 추가
|
|
||||||
- Level 4로 변경 → 기존 정식 계정 이메일을 `allowedEmails`에 추가
|
|
||||||
- `false`: 명단을 자동 생성하지 않음 (수동 관리)
|
|
||||||
|
|
||||||
**캐시 무효화**: 해당 팀, 팀 목록
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🆕 15. POST/DELETE `/team/:teamId/allowed-names` - 허용 이름 관리 (Level 2)
|
|
||||||
실제 URL:
|
|
||||||
- `POST /api/team/:teamId/allowed-names` - 이름 추가
|
|
||||||
- `DELETE /api/team/:teamId/allowed-names` - 이름 제거
|
|
||||||
|
|
||||||
**인증**: 필수 (팀 소유자만)
|
|
||||||
|
|
||||||
**Request (POST)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
name: string; // 추가할 이름
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Request (DELETE)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
name: string; // 제거할 이름
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
allowedNames: string[]; // 업데이트된 명단
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**참고**: RESTful 원칙에 따라 HTTP Method로 동작 구분
|
|
||||||
|
|
||||||
**캐시 무효화**: 해당 팀
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🆕 16. POST/DELETE `/team/:teamId/allowed-emails` - 허용 이메일 관리 (Level 4)
|
|
||||||
실제 URL:
|
|
||||||
- `POST /api/team/:teamId/allowed-emails` - 이메일 추가
|
|
||||||
- `DELETE /api/team/:teamId/allowed-emails` - 이메일 제거
|
|
||||||
|
|
||||||
**인증**: 필수 (팀 소유자만)
|
|
||||||
|
|
||||||
**Request (POST)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
email: string; // 추가할 이메일 (소문자로 자동 변환)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Request (DELETE)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
email: string; // 제거할 이메일
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
allowedEmails: string[]; // 업데이트된 이메일 목록
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**유효성 검사**: 이메일 형식 검증 (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`)
|
|
||||||
|
|
||||||
**캐시 무효화**: 해당 팀
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🆕 17. POST `/team/:teamId/cover-image` - 팀 커버 이미지 업로드
|
### 🆕 17. POST `/team/:teamId/cover-image` - 팀 커버 이미지 업로드
|
||||||
@ -1096,65 +859,6 @@ 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 API
|
||||||
|
|
||||||
**중요**: User vs FirestoreUser 구분
|
**중요**: User vs FirestoreUser 구분
|
||||||
@ -1172,7 +876,6 @@ await teamManager.removeMember(teamId, currentUser.uid);
|
|||||||
uid: string; // Firebase Auth UID
|
uid: string; // Firebase Auth UID
|
||||||
displayName: string; // Firebase Auth displayName 설정용
|
displayName: string; // Firebase Auth displayName 설정용
|
||||||
teamId: string; // 최초 가입 팀
|
teamId: string; // 최초 가입 팀
|
||||||
isAnonymous: boolean; // 익명 여부
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1913,7 +1616,6 @@ const topics = await db.collection('topics')
|
|||||||
| `FORBIDDEN` | 권한 없음 |
|
| `FORBIDDEN` | 권한 없음 |
|
||||||
| `NOT_FOUND` | 리소스 없음 |
|
| `NOT_FOUND` | 리소스 없음 |
|
||||||
| `VALIDATION_ERROR` | 유효성 검사 실패 |
|
| `VALIDATION_ERROR` | 유효성 검사 실패 |
|
||||||
| `TEAM_CODE_INVALID` | 팀 코드 형식 오류 |
|
|
||||||
| `TEAM_INACTIVE` | 비활성화된 팀 |
|
| `TEAM_INACTIVE` | 비활성화된 팀 |
|
||||||
| `PIN_REQUIRED` | PIN 입력 필요 |
|
| `PIN_REQUIRED` | PIN 입력 필요 |
|
||||||
| `PIN_INVALID` | PIN 불일치 |
|
| `PIN_INVALID` | PIN 불일치 |
|
||||||
@ -2034,7 +1736,6 @@ export async function createTeam(data: CreateTeamRequest): Promise<ApiResponse<C
|
|||||||
|
|
||||||
### 캐싱 대상
|
### 캐싱 대상
|
||||||
- 팀 정보: `redis:team:{teamId}` - TTL 5분
|
- 팀 정보: `redis:team:{teamId}` - TTL 5분
|
||||||
- 팀 코드 조회: `redis:team:code:{code}` - TTL 1분
|
|
||||||
- 학생 정보: `redis:student:{studentId}` - TTL 5분
|
- 학생 정보: `redis:student:{studentId}` - TTL 5분
|
||||||
- 팀별 학생 목록: `redis:students:team:{teamId}` - TTL 30초
|
- 팀별 학생 목록: `redis:students:team:{teamId}` - TTL 30초
|
||||||
|
|
||||||
|
|||||||
134
DATA_MODELS.md
134
DATA_MODELS.md
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
firestore
|
firestore
|
||||||
├── teams/ # ✅ 팀 (팀 코드 시스템)
|
├── teams/ # ✅ 팀
|
||||||
├── users/ # ✅ 사용자 프로필 및 메타데이터
|
├── users/ # ✅ 사용자 프로필 및 메타데이터
|
||||||
├── writings/ # ✅ 작성한 글
|
├── writings/ # ✅ 작성한 글
|
||||||
├── topics/ # ✅ 글쓰기 주제
|
├── topics/ # ✅ 글쓰기 주제
|
||||||
@ -37,8 +37,6 @@ realtime-db
|
|||||||
│ └── {userId}/
|
│ └── {userId}/
|
||||||
├── previewResponses/ # 🆕 미리보기 응답
|
├── previewResponses/ # 🆕 미리보기 응답
|
||||||
│ └── {requestId}/
|
│ └── {requestId}/
|
||||||
└── teamCodeReservations/ # 🆕 팀 코드 예약 (Race Condition 방지)
|
|
||||||
└── {code-with-hyphens}/ # 예: "춤추는-파란-사자"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**범례**:
|
**범례**:
|
||||||
@ -47,57 +45,6 @@ realtime-db
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Realtime Database 데이터 모델
|
|
||||||
|
|
||||||
### teamCodeReservations (팀 코드 예약) ✅
|
|
||||||
|
|
||||||
**경로**: `teamCodeReservations/{code-with-hyphens}`
|
|
||||||
|
|
||||||
**목적**: 팀 코드 생성 시 Race Condition 방지 (Atomic 예약)
|
|
||||||
|
|
||||||
**스키마**:
|
|
||||||
```typescript
|
|
||||||
interface TeamCodeReservation {
|
|
||||||
userId: string; // 예약한 사용자 UID
|
|
||||||
createdAt: number; // 예약 시각 (timestamp)
|
|
||||||
expiresAt: number; // 만료 시각 (createdAt + 5분)
|
|
||||||
locale?: string; // 생성 언어 (ko, en, ja)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**예시 데이터**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"teamCodeReservations": {
|
|
||||||
"춤추는-파란-사자": {
|
|
||||||
"userId": "abc123...",
|
|
||||||
"createdAt": 1700000000000,
|
|
||||||
"expiresAt": 1700000300000,
|
|
||||||
"locale": "ko"
|
|
||||||
},
|
|
||||||
"dancing-blue-lion": {
|
|
||||||
"userId": "def456...",
|
|
||||||
"createdAt": 1700000050000,
|
|
||||||
"expiresAt": 1700000350000,
|
|
||||||
"locale": "en"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**특징**:
|
|
||||||
- ✅ **Atomic 예약**: Transaction으로 동시 요청 처리
|
|
||||||
- ✅ **5분 TTL**: 자동 만료 (팀 생성 안 하면 해제)
|
|
||||||
- ✅ **자동 정리**: cleanupExpiredReservations() 함수
|
|
||||||
- ✅ **언어별 코드**: ko/en/ja 각각 다른 단어 사용
|
|
||||||
|
|
||||||
**Security Rules**:
|
|
||||||
- `.read`: 모두 허용 (중복 체크)
|
|
||||||
- `.write`: 본인만 수정 가능
|
|
||||||
- `.validate`: userId, createdAt, expiresAt 필수 + TTL 검증
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Team (팀) ✅
|
## 1. Team (팀) ✅
|
||||||
|
|
||||||
**컬렉션**: `teams/{teamId}`
|
**컬렉션**: `teams/{teamId}`
|
||||||
@ -107,17 +54,9 @@ interface TeamCodeReservation {
|
|||||||
```typescript
|
```typescript
|
||||||
interface Team {
|
interface Team {
|
||||||
id: string; // 문서 ID
|
id: string; // 문서 ID
|
||||||
code: string; // 팀 코드 (예: "춤추는 파란 사자")
|
|
||||||
name: string; // 팀 이름 (예: "2학년 1반")
|
name: string; // 팀 이름 (예: "2학년 1반")
|
||||||
ownerId: string; // 팀 소유자 UID
|
ownerId: string; // 팀 소유자 UID
|
||||||
|
|
||||||
// 보안 레벨 (5단계 시스템)
|
|
||||||
securityLevel: TeamSecurityLevel; // 1~5 (OPEN, NAME_LIST, AUTH_REQUIRED, EMAIL_LIST, CLOSED)
|
|
||||||
|
|
||||||
// 명단 관리
|
|
||||||
allowedNames?: string[]; // Level 2용: 허용된 이름 목록
|
|
||||||
allowedEmails?: string[]; // Level 4용: 허용된 이메일 목록
|
|
||||||
|
|
||||||
// AI 글쓰기 도우미 설정
|
// AI 글쓰기 도우미 설정
|
||||||
aiAssistanceConfig?: AIAssistanceConfig;
|
aiAssistanceConfig?: AIAssistanceConfig;
|
||||||
|
|
||||||
@ -139,16 +78,6 @@ interface Team {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 보안 레벨 (5단계)
|
|
||||||
|
|
||||||
| Level | Enum | 이름 | 익명 허용 | 가입 제한 | 주요 사용처 |
|
|
||||||
|-------|------|------|-----------|-----------|------------|
|
|
||||||
| **1** | `OPEN` | 완전 개방 | ✅ | 닉네임 공유 로그인 | 공개 워크샵, 체험 수업 |
|
|
||||||
| **2** | `NAME_LIST` | 명단 기반 | ✅ | `allowedNames` 체크 | 저학년 반 (익명이지만 통제) |
|
|
||||||
| **3** | `AUTH_REQUIRED` | 로그인 필수 | ❌ | 정식 계정 누구나 | 고학년 반 (구글 계정) ⭐ 추천 |
|
|
||||||
| **4** | `EMAIL_LIST` | 이메일 제한 | ❌ | `allowedEmails` 체크 | 특정 학생만 (전학생 차단) |
|
|
||||||
| **5** | `CLOSED` | 닫힌 팀 | ❌ | 신규 가입 차단 | 졸업반, 종료된 프로젝트 |
|
|
||||||
|
|
||||||
### 예시 데이터
|
### 예시 데이터
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -158,7 +87,6 @@ interface Team {
|
|||||||
"name": "2학년 1반",
|
"name": "2학년 1반",
|
||||||
"ownerId": "user_xyz",
|
"ownerId": "user_xyz",
|
||||||
|
|
||||||
"securityLevel": 3,
|
|
||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"allowPublicWritings": false,
|
"allowPublicWritings": false,
|
||||||
"description": "창의적인 글쓰기를 배우는 우리 반입니다!",
|
"description": "창의적인 글쓰기를 배우는 우리 반입니다!",
|
||||||
@ -187,65 +115,7 @@ interface Team {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Student (학생 계정) ✅
|
## 2. User (사용자)
|
||||||
|
|
||||||
**컬렉션**: `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}` (🔜 구현 예정)
|
**컬렉션**: `users/{userId}` (🔜 구현 예정)
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,6 @@ export { cleanupExpiredReservations, cleanupExpiredDrafts };
|
|||||||
|
|
||||||
| 함수 | 타입 | 실행 주기 | 설명 |
|
| 함수 | 타입 | 실행 주기 | 설명 |
|
||||||
|------|------|----------|------|
|
|------|------|----------|------|
|
||||||
| `cleanupExpiredReservations` | Scheduled | 매 시간 | 팀 코드 예약 정리 |
|
|
||||||
| `cleanupExpiredDrafts` | Scheduled | 매일 새벽 3시 | 180일+ drafts 정리 |
|
| `cleanupExpiredDrafts` | Scheduled | 매일 새벽 3시 | 180일+ drafts 정리 |
|
||||||
| `generateDailyInspirations` | Scheduled | 매일 새벽 | AI 영감 자동 생성 |
|
| `generateDailyInspirations` | Scheduled | 매일 새벽 | AI 영감 자동 생성 |
|
||||||
| `generateInspirationsManual` | HTTP | 수동 호출 | 관리자 수동 영감 생성 |
|
| `generateInspirationsManual` | HTTP | 수동 호출 | 관리자 수동 영감 생성 |
|
||||||
@ -196,79 +195,6 @@ interface Inspiration {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 팀 코드 예약 시스템
|
|
||||||
|
|
||||||
### Race Condition 방지
|
|
||||||
|
|
||||||
**Realtime DB Transaction** 사용:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/lib/server/teamCodeReservation.ts
|
|
||||||
export async function generateAndReserveTeamCode(
|
|
||||||
userId: string,
|
|
||||||
locale: string = "ko"
|
|
||||||
): Promise<string> {
|
|
||||||
const maxAttempts = 10;
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
||||||
const code = generateTeamCode(locale);
|
|
||||||
const codeRef = realtimeDb.ref(`teamCodeReservations/${code}`);
|
|
||||||
|
|
||||||
// Atomic 예약
|
|
||||||
const result = await codeRef.transaction((current) => {
|
|
||||||
if (current !== null) return; // 이미 예약됨
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
expiresAt: Date.now() + 5 * 60 * 1000, // 5분 TTL
|
|
||||||
locale,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.committed) {
|
|
||||||
return code; // 성공
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("팀 코드 생성 실패");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 예약 해제
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 새 코드 받기 시 이전 예약 해제
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
const { previousCode } = await request.json();
|
|
||||||
|
|
||||||
if (previousCode) {
|
|
||||||
await releaseTeamCodeReservation(previousCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCode = await generateAndReserveTeamCode(userId);
|
|
||||||
return successResponse({ code: newCode });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 실패 시 반환
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 팀 생성 실패 시에도 예약 해제
|
|
||||||
try {
|
|
||||||
const team = await createTeam({ code, ... });
|
|
||||||
await releaseTeamCodeReservation(code);
|
|
||||||
return successResponse({ team });
|
|
||||||
} catch (error) {
|
|
||||||
await releaseTeamCodeReservation(code); // catch 블록에서도 해제
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**참조**: `src/lib/server/teamCodeReservation.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## XSS 보안
|
## XSS 보안
|
||||||
|
|
||||||
### 백엔드 자동 세탁
|
### 백엔드 자동 세탁
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
### 2.1 랜딩 페이지 (`/`)
|
### 2.1 랜딩 페이지 (`/`)
|
||||||
**목적:** 비로그인 사용자에게 서비스 정보를 제공하고 회원가입 및 로그인을 지원합니다.
|
**목적:** 비로그인 사용자에게 서비스 정보를 제공하고 회원가입 및 로그인을 지원합니다.
|
||||||
**주요 기능:**
|
**주요 기능:**
|
||||||
* **메인 섹션:** 서비스 소개 문구와 로그인/회원가입 버튼, 팀 코드 입력(학생용) 버튼을 제공합니다.
|
* **메인 섹션:** 서비스 소개 문구와 로그인/회원가입 버튼을 제공합니다.
|
||||||
* **로그인:** 개인 계정 로그인과 단체(팀) 로그인을 분리하여 지원합니다.
|
* **로그인:** 개인 계정 로그인과 단체(팀) 로그인을 분리하여 지원합니다.
|
||||||
* **기능 안내:** 글쓰기, 습관 관리, 보상 시스템 등 주요 기능을 아이콘과 텍스트로 설명합니다.
|
* **기능 안내:** 글쓰기, 습관 관리, 보상 시스템 등 주요 기능을 아이콘과 텍스트로 설명합니다.
|
||||||
* **이용 가이드:** 서비스 사용 절차를 단계별로 안내합니다.
|
* **이용 가이드:** 서비스 사용 절차를 단계별로 안내합니다.
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
1. npm run dev
|
1. npm run dev
|
||||||
2. Level 1 팀 생성 (완전 개방)
|
2. Level 1 팀 생성 (완전 개방)
|
||||||
3. 로그아웃 (또는 시크릿 모드)
|
3. 로그아웃 (또는 시크릿 모드)
|
||||||
4. 팀 코드 입력 → "철수" 입력
|
4. 초대 링크로 팀 참여 → "철수" 입력
|
||||||
5. 예상: 새 익명 계정 생성 ✅
|
5. 예상: 새 익명 계정 생성 ✅
|
||||||
6. 확인: /team/{teamId} 페이지에서 멤버 확인
|
6. 확인: /team/{teamId} 페이지에서 멤버 확인
|
||||||
```
|
```
|
||||||
@ -63,7 +63,7 @@
|
|||||||
#### 시나리오 B: 재로그인 (핵심)
|
#### 시나리오 B: 재로그인 (핵심)
|
||||||
```
|
```
|
||||||
1. 로그아웃
|
1. 로그아웃
|
||||||
2. 동일 팀 코드 → "철수" 입력
|
2. 동일 초대 링크 → "철수" 입력
|
||||||
3. 예상: Custom Token으로 기존 계정 로그인 ✅
|
3. 예상: Custom Token으로 기존 계정 로그인 ✅
|
||||||
4. 확인: 이전 글이 그대로 있는지 확인
|
4. 확인: 이전 글이 그대로 있는지 확인
|
||||||
```
|
```
|
||||||
@ -71,7 +71,7 @@
|
|||||||
#### 시나리오 C: 다른 브라우저 (중요)
|
#### 시나리오 C: 다른 브라우저 (중요)
|
||||||
```
|
```
|
||||||
1. 다른 브라우저 또는 시크릿 모드
|
1. 다른 브라우저 또는 시크릿 모드
|
||||||
2. 동일 팀 코드 → "철수" 입력
|
2. 동일 초대 링크 → "철수" 입력
|
||||||
3. 예상: 동일 계정으로 로그인 ✅
|
3. 예상: 동일 계정으로 로그인 ✅
|
||||||
4. 확인: 크로스 디바이스 로그인 작동
|
4. 확인: 크로스 디바이스 로그인 작동
|
||||||
```
|
```
|
||||||
@ -249,7 +249,7 @@ export default function Error({
|
|||||||
```
|
```
|
||||||
시나리오: 초등 2학년 반 (25명)
|
시나리오: 초등 2학년 반 (25명)
|
||||||
1. 선생님이 팀 생성 (Level 2, 명단 등록)
|
1. 선생님이 팀 생성 (Level 2, 명단 등록)
|
||||||
2. 학생들 팀 코드로 로그인
|
2. 학생들 초대 링크로 팀 참여
|
||||||
3. 팀 주제 "오늘의 날씨" 작성
|
3. 팀 주제 "오늘의 날씨" 작성
|
||||||
4. 선생님 실시간 모니터링
|
4. 선생님 실시간 모니터링
|
||||||
5. 피드백 수집
|
5. 피드백 수집
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
# 라온누리 - 프로젝트 구조
|
# 라온누리 - 프로젝트 구조
|
||||||
|
|
||||||
> 최종 업데이트: 2026-03-09 (글쓰기 플로우 단순화)
|
> 최종 업데이트: 2026-03-19 (학생 일괄 생성 및 loginId 시스템)
|
||||||
|
|
||||||
초등학생을 위한 창작 글쓰기 교육 플랫폼
|
초등학생을 위한 창작 글쓰기 교육 플랫폼
|
||||||
|
|
||||||
**최신 업데이트** (2026-03-09):
|
**최신 업데이트** (2026-03-19):
|
||||||
|
- **학생 일괄 생성 (Bulk Student Creation)** - CSV 기반 학생 계정 일괄 생성, 템플릿 ID/비밀번호, 로그인 카드 인쇄
|
||||||
|
- **`BulkMemberCreator`** (`src/components/team/BulkMemberCreator.tsx`): 4단계 UI (입력→미리보기→생성중→결과), 로그인 카드 인쇄 기능
|
||||||
|
- **`POST /api/team/[teamId]/bulk-create-members`**: 소유자 전용 학생 일괄 생성 API
|
||||||
|
- **`POST /api/auth/resolve-login-id`**: loginId→email 변환 API (인증 불필요)
|
||||||
|
- **loginId 시스템**: 학생용 간편 로그인 ID (@ 없는 ID → 시스템 이메일로 자동 변환)
|
||||||
|
- **서버 레이어**: `src/lib/server/bulk-member.ts` (템플릿 처리, loginId 생성, Firebase Auth 계정 생성)
|
||||||
|
|
||||||
|
**업데이트** (2026-03-09):
|
||||||
- **글쓰기 플로우 단순화** - 학생의 글쓰기 도달 경로를 최대 2클릭으로 단축
|
- **글쓰기 플로우 단순화** - 학생의 글쓰기 도달 경로를 최대 2클릭으로 단축
|
||||||
- **`/write` 자동 리다이렉트**: 커리큘럼 파라미터(`topicId`, `startType`, `helpLevel`, `teamId`)를 감지하여 적절한 에디터 페이지로 자동 이동
|
- **`/write` 자동 리다이렉트**: 커리큘럼 파라미터(`topicId`, `startType`, `helpLevel`, `teamId`)를 감지하여 적절한 에디터 페이지로 자동 이동
|
||||||
- **`NextAssignmentCard`** (`src/components/home/NextAssignmentCard.tsx`): 홈 페이지에 다음 과제 카드 표시, 커리큘럼 진행 상황 기반
|
- **`NextAssignmentCard`** (`src/components/home/NextAssignmentCard.tsx`): 홈 페이지에 다음 과제 카드 표시, 커리큘럼 진행 상황 기반
|
||||||
@ -85,10 +93,10 @@
|
|||||||
- **주요 기능**: 부모님 대시보드, 선생님 첨삭 시스템, 글 포트폴리오, 협업 글쓰기 등
|
- **주요 기능**: 부모님 대시보드, 선생님 첨삭 시스템, 글 포트폴리오, 협업 글쓰기 등
|
||||||
|
|
||||||
**업데이트** (2025-12-12):
|
**업데이트** (2025-12-12):
|
||||||
- 🔒 **팀 보안 설정 통합**
|
- 🔒 **팀 설정 통합**
|
||||||
- **TeamSecuritySettingsDialog 확장**: 공개설정(isPublic) + 보안레벨(securityLevel) 통합
|
- **TeamSettingsDialog**: 공개설정(isPublic) 통합
|
||||||
- **공개 설정 스위치**: 보안 단계 선택 아래에 배치 (팀 공개, 글 공개 허용, 팀 설명, 커버 이미지)
|
- **공개 설정 스위치**: 팀 공개, 글 공개 허용, 팀 설명, 커버 이미지
|
||||||
- **검색 가능/불가능 태그**: TeamInfoCard 보안 정보에 isPublic 기반 배지 추가
|
- **검색 가능/불가능 태그**: TeamInfoCard에 isPublic 기반 배지 추가
|
||||||
- **TeamPublicSettings 제거**: 팀 관리 페이지에서 분리된 공개 설정 섹션 제거
|
- **TeamPublicSettings 제거**: 팀 관리 페이지에서 분리된 공개 설정 섹션 제거
|
||||||
- **다국어 지원**: searchable/notSearchable 키 추가 (ko/en/ja)
|
- **다국어 지원**: searchable/notSearchable 키 추가 (ko/en/ja)
|
||||||
- 👑 **팀 소유자 이전 기능**
|
- 👑 **팀 소유자 이전 기능**
|
||||||
@ -96,9 +104,6 @@
|
|||||||
- **TeamMemberEditor 컴포넌트**: 멤버 메뉴에 "소유자 이전" 옵션 추가
|
- **TeamMemberEditor 컴포넌트**: 멤버 메뉴에 "소유자 이전" 옵션 추가
|
||||||
- **소유자 표시**: 멤버 목록에서 owner 역할 배지 표시
|
- **소유자 표시**: 멤버 목록에서 owner 역할 배지 표시
|
||||||
- **다국어 지원**: transferOwnership, transferDialogTitle, transferConfirm 등 (ko/en/ja)
|
- **다국어 지원**: transferOwnership, transferDialogTitle, transferConfirm 등 (ko/en/ja)
|
||||||
- 🛡️ **SecurityLevelSelector 개선**
|
|
||||||
- **검색 가능 여부 표시**: 각 보안 레벨별 검색 가능 아이콘 (LuSearch/LuSearchX)
|
|
||||||
- **searchable 속성 추가**: SecurityLevelOption 타입 확장
|
|
||||||
|
|
||||||
**업데이트** (2025-12-10):
|
**업데이트** (2025-12-10):
|
||||||
- 📉 **플랜 다운그레이드 선택 옵션**
|
- 📉 **플랜 다운그레이드 선택 옵션**
|
||||||
@ -158,11 +163,6 @@
|
|||||||
- **Props**: isOwner, onClick 추가
|
- **Props**: isOwner, onClick 추가
|
||||||
- **적용 범위**: `/team` (내 팀 목록), `/team/all` (공개 팀 목록)
|
- **적용 범위**: `/team` (내 팀 목록), `/team/all` (공개 팀 목록)
|
||||||
- **인라인 함수 제거**: renderTeamCard → TeamCard 컴포넌트로 전환
|
- **인라인 함수 제거**: renderTeamCard → TeamCard 컴포넌트로 전환
|
||||||
- 🔒 **공개 팀 코드 보안 강화**
|
|
||||||
- **Team.code**: `string` → `string?` (optional)
|
|
||||||
- **서버 API**: getPublicTeams() 전체 코드 제거, GET /api/team/[teamId]/public 멤버 아닌 경우 제거
|
|
||||||
- **TeamCard**: 코드 없으면 숨김 (조건부 렌더링)
|
|
||||||
- **타입 안전성**: StudentLoginFlow, TeamInfoCard, firebaseAuth.ts 수정
|
|
||||||
|
|
||||||
**업데이트** (2025-12-04):
|
**업데이트** (2025-12-04):
|
||||||
- 🔀 **글쓰기 페이지 라우트 분리**
|
- 🔀 **글쓰기 페이지 라우트 분리**
|
||||||
@ -387,7 +387,7 @@
|
|||||||
- **개선된 언어 전환**: LocaleSwitcher 드롭다운 메뉴 (국기 이모지 🇰🇷 🇺🇸 🇯🇵, 현재 언어 체크 표시)
|
- **개선된 언어 전환**: LocaleSwitcher 드롭다운 메뉴 (국기 이모지 🇰🇷 🇺🇸 🇯🇵, 현재 언어 체크 표시)
|
||||||
- **번역 파일**: `messages/ko.json`, `messages/en.json`, `messages/ja.json` (각 407줄, 220+ 키)
|
- **번역 파일**: `messages/ko.json`, `messages/en.json`, `messages/ja.json` (각 407줄, 220+ 키)
|
||||||
- **완성된 페이지**: Navbar, Landing, Home, Team(전체), Write 페이지 번역 완료
|
- **완성된 페이지**: Navbar, Landing, Home, Team(전체), Write 페이지 번역 완료
|
||||||
- **완성된 컴포넌트**: LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog, SecurityLevelSelector 번역 완료
|
- **완성된 컴포넌트**: LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog 번역 완료
|
||||||
- **어린이 친화적**: 일본어는 한자 최소화, ひらがな 우선 사용
|
- **어린이 친화적**: 일본어는 한자 최소화, ひらがな 우선 사용
|
||||||
- **타입 안전**: useTranslations 훅으로 타입 체크
|
- **타입 안전**: useTranslations 훅으로 타입 체크
|
||||||
- **쿠키 저장**: 사용자가 선택한 언어 기억 (`NEXT_LOCALE`)
|
- **쿠키 저장**: 사용자가 선택한 언어 기억 (`NEXT_LOCALE`)
|
||||||
@ -499,7 +499,7 @@
|
|||||||
- 💾 강화된 자동 저장 (2초 debounce, 저장 상태 표시)
|
- 💾 강화된 자동 저장 (2초 debounce, 저장 상태 표시)
|
||||||
- 🎨 테마 슬롯 레시피 추가 (Dialog, Select 자동 배경색)
|
- 🎨 테마 슬롯 레시피 추가 (Dialog, Select 자동 배경색)
|
||||||
- 📋 TopicSelector Dialog 리디자인 (glassmorphism, 탭 기반 그룹핑, 미리보기 7:3 레이아웃)
|
- 📋 TopicSelector Dialog 리디자인 (glassmorphism, 탭 기반 그룹핑, 미리보기 7:3 레이아웃)
|
||||||
- 🔐 5단계 보안 레벨 시스템 (팀별 보안 정책 선택)
|
- 🔐 초대 링크 기반 팀 참가 시스템
|
||||||
- 📦 User 타입 분리 (FirestoreUser / User)
|
- 📦 User 타입 분리 (FirestoreUser / User)
|
||||||
- 🏷️ 닉네임 저장 위치 변경 (team.members[uid].nickname)
|
- 🏷️ 닉네임 저장 위치 변경 (team.members[uid].nickname)
|
||||||
- 🗑️ memberUids optional (Object.keys(members) 사용)
|
- 🗑️ memberUids optional (Object.keys(members) 사용)
|
||||||
@ -534,12 +534,11 @@
|
|||||||
| **글쓰기** | `/[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>템플릿 미리채우기 (제목/내용), Firestore 저장<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]/test` | 팀 코드 시스템 테스트 페이지 | 팀 코드 생성/검증 테스트<br>팀/학생 생성 테스트<br>학생 로그인 테스트<br>authStore 상태 확인 | 🔜 예정 |
|
|
||||||
| **공개 팀 목록** | `/[locale]/team/all` | 🆕 **공개 팀 둘러보기** | 🆕 **공개된 팀 목록 표시** (isPublic=true)<br>🆕 **TeamCard 그리드** (글래스모피즘)<br>🆕 **페이지네이션** (커서 기반)<br>🆕 **Navbar "공개 팀" 메뉴** | ✅ 완료 |
|
| **공개 팀 목록** | `/[locale]/team/all` | 🆕 **공개 팀 둘러보기** | 🆕 **공개된 팀 목록 표시** (isPublic=true)<br>🆕 **TeamCard 그리드** (글래스모피즘)<br>🆕 **페이지네이션** (커서 기반)<br>🆕 **Navbar "공개 팀" 메뉴** | ✅ 완료 |
|
||||||
| **내 팀 목록** | `/[locale]/team` | 내가 만든/참여한 팀 목록 (정식 계정 전용) | 팀 카드 그리드, 팀 정보 (코드, 멤버 수, 보안 설정)<br>"새 팀 만들기" 버튼 | ✅ 완료 |
|
| **내 팀 목록** | `/[locale]/team` | 내가 만든/참여한 팀 목록 (정식 계정 전용) | 팀 카드 그리드, 팀 정보 (코드, 멤버 수, 보안 설정)<br>"새 팀 만들기" 버튼 | ✅ 완료 |
|
||||||
| **팀 생성** | `/[locale]/team/create` | 새 팀 만들기 (정식 계정 전용) | 팀 이름 입력, 팀 코드 자동 생성<br>🆕 **5단계 보안 레벨 선택** (RadioCard, 애니메이션)<br>🆕 **명단 관리 (Level 2/4)**: TagsInput으로 Enter/쉼표 입력<br>생성 후 `/team/[teamId]`로 이동 | ✅ 완료 |
|
| **팀 생성** | `/[locale]/team/create` | 새 팀 만들기 (정식 계정 전용) | 팀 이름 입력<br>생성 후 `/team/[teamId]`로 이동 | ✅ 완료 |
|
||||||
| **팀 상세 (통합)** | `/[locale]/team/[teamId]` | 🆕 **멤버/공개 뷰 통합 페이지** | 🆕 **멤버인 경우**: 멤버 목록, 관리/나가기 버튼<br>🆕 **비멤버 + 공개 팀**: 팀 정보, 공개 글 목록, 참여 버튼<br>🆕 **비멤버 + 비공개 팀**: 접근 불가 메시지<br>팀 코드 로그인 후 기본 이동 페이지 | ✅ 완료 |
|
| **팀 상세 (통합)** | `/[locale]/team/[teamId]` | 🆕 **멤버/공개 뷰 통합 페이지** | 🆕 **멤버인 경우**: 멤버 목록, 관리/나가기 버튼<br>🆕 **비멤버 + 공개 팀**: 팀 정보, 공개 글 목록, 참여 버튼<br>🆕 **비멤버 + 비공개 팀**: 접근 불가 메시지<br>초대 링크 참여 후 기본 이동 페이지 | ✅ 완료 |
|
||||||
| **팀 관리** | `/[locale]/team/[teamId]/manage` | 팀 관리 페이지 (소유자 전용) | 팀 정보 및 코드, 보안 설정 표시<br>🆕 **팀 공개 설정** (isPublic, allowPublicWritings, description)<br>**팀 주제 관리 (생성/삭제)**<br>🆕 **주제별 학생 분석** (TopicMemberAnalysisSection)<br>멤버 목록 및 관리<br>🆕 **멤버 메뉴 - 팀 내 글 분석** (by-team)<br>"멤버 페이지 보기" 버튼 | ✅ 완료 |
|
| **팀 관리** | `/[locale]/team/[teamId]/manage` | 팀 관리 페이지 (소유자 전용) | 팀 정보 및 코드<br>🆕 **팀 공개 설정** (isPublic, allowPublicWritings, description)<br>**팀 주제 관리 (생성/삭제)**<br>🆕 **주제별 학생 분석** (TopicMemberAnalysisSection)<br>멤버 목록 및 관리<br>🆕 **멤버 메뉴 - 팀 내 글 분석** (by-team)<br>"멤버 페이지 보기" 버튼 | ✅ 완료 |
|
||||||
|
|
||||||
### 🚧 구현 예정
|
### 🚧 구현 예정
|
||||||
|
|
||||||
@ -559,7 +558,7 @@
|
|||||||
|---------|--------|------|------|
|
|---------|--------|------|------|
|
||||||
| **AuthInitializer** | `AuthInitializer.tsx` | Firebase 인증 상태 초기화 | ✅ 완료 |
|
| **AuthInitializer** | `AuthInitializer.tsx` | Firebase 인증 상태 초기화 | ✅ 완료 |
|
||||||
| **LoginDialog** | `LoginDialog.tsx` | 로그인/회원가입 다이얼로그 (auth/team/link 모드 지원) | ✅ 완료 |
|
| **LoginDialog** | `LoginDialog.tsx` | 로그인/회원가입 다이얼로그 (auth/team/link 모드 지원) | ✅ 완료 |
|
||||||
| **StudentLoginFlow** | `StudentLoginFlow.tsx` | 팀 코드 학생 로그인 플로우 (3단계) | ✅ 완료 |
|
| **StudentLoginFlow** | `StudentLoginFlow.tsx` | 학생 로그인 플로우 | ✅ 완료 |
|
||||||
| **LoginForm** | `LoginForm.tsx` | 로그인 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 |
|
| **LoginForm** | `LoginForm.tsx` | 로그인 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 |
|
||||||
| **SignupForm** | `SignupForm.tsx` | 회원가입 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 |
|
| **SignupForm** | `SignupForm.tsx` | 회원가입 폼 컴포넌트 (mode: auth\|link) | ✅ 완료 |
|
||||||
| **SocialLoginButton** | `SocialLoginButton.tsx` | 소셜 로그인 버튼 | ✅ 완료 |
|
| **SocialLoginButton** | `SocialLoginButton.tsx` | 소셜 로그인 버튼 | ✅ 완료 |
|
||||||
@ -573,20 +572,9 @@
|
|||||||
- ✅ HIBP API 연동 (유출된 비밀번호 차단)
|
- ✅ HIBP API 연동 (유출된 비밀번호 차단)
|
||||||
- ✅ 폼 검증 및 에러 애니메이션
|
- ✅ 폼 검증 및 에러 애니메이션
|
||||||
- ✅ Google OAuth 로그인
|
- ✅ Google OAuth 로그인
|
||||||
- ✅ 익명 계정 연결 기능
|
- ✅ 초대 링크 기반 팀 참여 (정식 계정 필수)
|
||||||
- ✅ 기존 폼 재사용 (LoginForm/SignupForm mode prop)
|
|
||||||
- ✅ 신규 계정 생성 (linkWithCredential)
|
|
||||||
- ✅ 기존 계정 병합 (API 데이터 마이그레이션)
|
|
||||||
- ✅ 3개 소셜 로그인 버튼 표시 (Naver/Kakao/Google)
|
|
||||||
- ✅ 용어 통일 ("병합" → "연결", "익명" → "임시")
|
|
||||||
- ✅ 팀 코드 기반 사용자 로그인 (Anonymous Auth - 단순화됨)
|
|
||||||
- ✅ 한글 팀 코드 ("춤추는 파란 사자" 형식)
|
|
||||||
- ✅ 사용자 이름 입력 (2단계)
|
|
||||||
- ❌ PIN 인증 제거 (복잡도 감소)
|
|
||||||
- ✅ UID 기반 통합 인증 (currentStudent 제거)
|
|
||||||
- ✅ 팀별 닉네임 시스템
|
- ✅ 팀별 닉네임 시스템
|
||||||
- ✅ 로그인/회원가입 페이드 전환 애니메이션
|
- ✅ 로그인/회원가입 페이드 전환 애니메이션
|
||||||
- ✅ Anonymous ↔ 정식 계정 연결 (UID 유지)
|
|
||||||
- 🔜 네이버 로그인 (준비 중)
|
- 🔜 네이버 로그인 (준비 중)
|
||||||
- 🔜 카카오 로그인 (준비 중)
|
- 🔜 카카오 로그인 (준비 중)
|
||||||
|
|
||||||
@ -872,14 +860,16 @@
|
|||||||
|
|
||||||
| 컴포넌트 | 파일명 | 설명 | 상태 |
|
| 컴포넌트 | 파일명 | 설명 | 상태 |
|
||||||
|---------|--------|------|------|
|
|---------|--------|------|------|
|
||||||
| **TeamCard** | `TeamCard.tsx` | 🆕 **팀 카드 컴포넌트** (내 팀/공개 팀 공용, 커버 이미지 140px, 팀 설명 2줄, 팀 코드 조건부 표시, isOwner/onClick props) | ✅ 완료 |
|
| **TeamCard** | `TeamCard.tsx` | 🆕 **팀 카드 컴포넌트** (내 팀/공개 팀 공용, 커버 이미지 140px, 팀 설명 2줄, isOwner/onClick props) | ✅ 완료 |
|
||||||
| ~~TeamCoverImageUploader~~ | 삭제됨 | ImageDropzone로 통합 (범용 이미지 업로드 컴포넌트) | ✅ 완료 |
|
| ~~TeamCoverImageUploader~~ | 삭제됨 | ImageDropzone로 통합 (범용 이미지 업로드 컴포넌트) | ✅ 완료 |
|
||||||
| **TeamTopicManager** | `TeamTopicManager.tsx` | 팀 주제 목록 및 생성/삭제 UI | ✅ 완료 |
|
| **TeamTopicManager** | `TeamTopicManager.tsx` | 팀 주제 목록 및 생성/삭제 UI | ✅ 완료 |
|
||||||
| **TeamAISettings** | `TeamAISettings.tsx` | 🆕 **팀 AI 설정 컴포넌트** (2단계 계층: 팀 AI 마스터 스위치 + AI 글쓰기 도우미, 플랜 제한 체크, 3가지 상태 피드백, VStack 레이아웃) | ✅ 완료 |
|
| **TeamAISettings** | `TeamAISettings.tsx` | 🆕 **팀 AI 설정 컴포넌트** (2단계 계층: 팀 AI 마스터 스위치 + AI 글쓰기 도우미, 플랜 제한 체크, 3가지 상태 피드백, VStack 레이아웃) | ✅ 완료 |
|
||||||
| **AIConfigDialog** | `AIConfigDialog.tsx` | 🆕 **AI 도우미 고급 설정 Dialog** (Slider, 커스텀 CheckboxCard) | ✅ 완료 |
|
| **AIConfigDialog** | `AIConfigDialog.tsx` | 🆕 **AI 도우미 고급 설정 Dialog** (Slider, 커스텀 CheckboxCard) | ✅ 완료 |
|
||||||
| **SecurityLevelSelector** | `SecurityLevelSelector.tsx` | 5단계 보안 레벨 선택 (RadioCard, framer-motion 애니메이션) | ✅ 완료 |
|
| ~~SecurityLevelSelector~~ | 삭제됨 | 초대 링크 시스템으로 대체 | ✅ 완료 |
|
||||||
| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | 🆕 **주제별 학생 글쓰기 분석** (Accordion, 주제별 학생 목록, 글 개수, by-topic 분석 연동) | ✅ 완료 |
|
| **TopicMemberAnalysisSection** | `TopicMemberAnalysisSection.tsx` | 🆕 **주제별 학생 글쓰기 분석** (Accordion, 주제별 학생 목록, 글 개수, by-topic 분석 연동) | ✅ 완료 |
|
||||||
| **LiveWritingMonitor** | `LiveWritingMonitor.tsx` | 🆕 **실시간 글쓰기 모니터링** (Firebase Realtime DB, 5초 주기) | ✅ 완료 |
|
| **LiveWritingMonitor** | `LiveWritingMonitor.tsx` | 🆕 **실시간 글쓰기 모니터링** (Firebase Realtime DB, 5초 주기) | ✅ 완료 |
|
||||||
|
| **EmailInviteSection** | `EmailInviteSection.tsx` | 🆕 **이메일 초대 관리 UI** (이메일로 팀 초대 전송, 대기 초대 목록, 수락/거절 상태 표시) | ✅ 완료 |
|
||||||
|
| **BulkMemberCreator** | `BulkMemberCreator.tsx` | 🆕 **학생 일괄 생성 UI** (4단계: CSV 입력→미리보기→생성중→결과, 템플릿 ID/PW, 로그인 카드 인쇄) | ✅ 완료 |
|
||||||
|
|
||||||
**주요 기능**:
|
**주요 기능**:
|
||||||
- ✅ **AIConfigDialog** (2025-11-17):
|
- ✅ **AIConfigDialog** (2025-11-17):
|
||||||
@ -914,8 +904,6 @@
|
|||||||
- **마지막 통계 유지**: 나간 학생도 통계 표시
|
- **마지막 통계 유지**: 나간 학생도 통계 표시
|
||||||
- 실시간 구독/구독 해제 (useEffect cleanup)
|
- 실시간 구독/구독 해제 (useEffect cleanup)
|
||||||
- 시각적 구분 (색상별 테두리, 아바타, 배지)
|
- 시각적 구분 (색상별 테두리, 아바타, 배지)
|
||||||
- ✅ SecurityLevelSelector: RadioCard 기반, 선택 시 추가 UI 애니메이션으로 표시
|
|
||||||
- ✅ 그라데이션 배경 (RadioCard와 자연스럽게 연결)
|
|
||||||
- ✅ react-icons/lu 사용 (이모티콘 제거)
|
- ✅ react-icons/lu 사용 (이모티콘 제거)
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -972,7 +960,7 @@
|
|||||||
| Manager | 파일명 | 설명 | 상태 |
|
| Manager | 파일명 | 설명 | 상태 |
|
||||||
|---------|--------|------|------|
|
|---------|--------|------|------|
|
||||||
| **ManagerBase** | `ManagerBase.ts` | 공통 기능 (authenticatedFetch, API 호출, 캐싱) | ✅ 완료 |
|
| **ManagerBase** | `ManagerBase.ts` | 공통 기능 (authenticatedFetch, API 호출, 캐싱) | ✅ 완료 |
|
||||||
| **TeamManager** | `TeamManager.ts` | 팀 관련 API 호출 (생성, 조회, 수정, 삭제, 멤버 관리) | ✅ 완료 |
|
| **TeamManager** | `TeamManager.ts` | 팀 관련 API 호출 (생성, 조회, 수정, 삭제, 멤버 관리, 🆕 **bulkCreateMembers**) | ✅ 완료 |
|
||||||
| **UserManager** | `UserManager.ts` | 사용자 관련 API 호출 (생성, 조회, 수정, 닉네임 관리) **[NEW]** | ✅ 완료 |
|
| **UserManager** | `UserManager.ts` | 사용자 관련 API 호출 (생성, 조회, 수정, 닉네임 관리) **[NEW]** | ✅ 완료 |
|
||||||
| **DraftManager** | `DraftManager.ts` | 글조각 관리 (**localStorage + Realtime DB 하이브리드**, 기기 간 동기화, 최대 10개, CRUD, syncStatus) | ✅ 완료 |
|
| **DraftManager** | `DraftManager.ts` | 글조각 관리 (**localStorage + Realtime DB 하이브리드**, 기기 간 동기화, 최대 10개, CRUD, syncStatus) | ✅ 완료 |
|
||||||
| **WritingSessionManager** | `WritingSessionManager.ts` | 🆕 **실시간 글쓰기 세션 관리** (Firebase Realtime DB 작업) | ✅ 완료 |
|
| **WritingSessionManager** | `WritingSessionManager.ts` | 🆕 **실시간 글쓰기 세션 관리** (Firebase Realtime DB 작업) | ✅ 완료 |
|
||||||
@ -1021,7 +1009,6 @@ Firebase Cloud Functions로 구현된 백그라운드 작업 및 자동화 로
|
|||||||
|
|
||||||
| Function | 파일 | 타입 | 스케줄/Trigger | 설명 | 상태 |
|
| Function | 파일 | 타입 | 스케줄/Trigger | 설명 | 상태 |
|
||||||
|----------|------|------|----------------|------|------|
|
|----------|------|------|----------------|------|------|
|
||||||
| **cleanupExpiredReservations** | `cleanup.ts` | Scheduled | 매 시간 0분 | 만료된 팀 코드 예약 정리 (expiresAt < now) | ✅ 배포됨 |
|
|
||||||
| **cleanupExpiredDrafts** | `triggers/drafts.ts` | Scheduled | 매일 새벽 3시 | 180일 이상 오래된 drafts 정리 (Realtime DB) | ✅ 배포됨 |
|
| **cleanupExpiredDrafts** | `triggers/drafts.ts` | Scheduled | 매일 새벽 3시 | 180일 이상 오래된 drafts 정리 (Realtime DB) | ✅ 배포됨 |
|
||||||
| **onTeamDeleted** | `triggers/team.ts` | Firestore Trigger | teams/{teamId} 삭제 | 팀 주제, 모니터링 데이터 cascade 삭제 | ✅ 배포됨 |
|
| **onTeamDeleted** | `triggers/team.ts` | Firestore Trigger | teams/{teamId} 삭제 | 팀 주제, 모니터링 데이터 cascade 삭제 | ✅ 배포됨 |
|
||||||
| **onWritingCreated** | `triggers/writing.ts` | Firestore Trigger | writings/{writingId} 생성 | 글 생성 감지 (추후 백그라운드 분석) | ✅ 배포됨 |
|
| **onWritingCreated** | `triggers/writing.ts` | Firestore Trigger | writings/{writingId} 생성 | 글 생성 감지 (추후 백그라운드 분석) | ✅ 배포됨 |
|
||||||
@ -1062,7 +1049,7 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| **Spelling Check** | `spellingService.ts` | **맞춤법 검사 서비스** (Gemini 기반, 초등학생 눈높이) | ✅ 완료 |
|
| **Spelling Check** | `spellingService.ts` | **맞춤법 검사 서비스** (Gemini 기반, 초등학생 눈높이) | ✅ 완료 |
|
||||||
| **Writing Assistance** | `writingAssistanceService.ts` | 🆕 **AI 글쓰기 도우미 서비스** (4단계 힌트 생성, 주제 맥락 활용, 서버 캐싱) | ✅ 완료 |
|
| **Writing Assistance** | `writingAssistanceService.ts` | 🆕 **AI 글쓰기 도우미 서비스** (4단계 힌트 생성, 주제 맥락 활용, 서버 캐싱) | ✅ 완료 |
|
||||||
| **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로 대체) |
|
||||||
| **Level System** | `levelSystem.ts` | 레벨/경험치 계산 로직 | ❌ 미구현 |
|
| **Level System** | `levelSystem.ts` | 레벨/경험치 계산 로직 | ❌ 미구현 |
|
||||||
| **Sticker System** | `stickerSystem.ts` | 스티커 획득 조건 엔진 | ❌ 미구현 |
|
| **Sticker System** | `stickerSystem.ts` | 스티커 획득 조건 엔진 | ❌ 미구현 |
|
||||||
@ -1079,8 +1066,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), **TeamSecurityLevel Enum (1-5)**, 🆕 **AIAssistanceConfig** (AI 도우미 설정) | ✅ 완료 |
|
| **Team 타입** | `team.ts` | 팀 데이터 모델 (members Map), 🆕 **AIAssistanceConfig** (AI 도우미 설정) | ✅ 완료 |
|
||||||
| **FirestoreUser 타입** | `firestoreUser.ts` | **FirestoreUser** (DB 저장용, 🆕 **plan/organizationId 필드 추가**), **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 생성 이미지) | ✅ 완료 |
|
||||||
| **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') | ✅ 완료 |
|
||||||
@ -1090,11 +1077,14 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| ~~**Student 타입**~~ | ~~`student.ts`~~ | ~~학생 데이터 모델~~ | ⚠️ Deprecated (user.ts로 대체) |
|
| ~~**Student 타입**~~ | ~~`student.ts`~~ | ~~학생 데이터 모델~~ | ⚠️ Deprecated (user.ts로 대체) |
|
||||||
| **Topic 타입** | `topic.ts` | 주제 데이터 모델, TopicCategory/Difficulty/OwnerType Enum, 팀 주제 유틸 함수 | ✅ 완료 |
|
| **Topic 타입** | `topic.ts` | 주제 데이터 모델, TopicCategory/Difficulty/OwnerType Enum, 팀 주제 유틸 함수 | ✅ 완료 |
|
||||||
| **API 공통 타입** | `api.ts` | ApiResponse, ApiError, HttpMethod Enum | ✅ 완료 |
|
| **API 공통 타입** | `api.ts` | ApiResponse, ApiError, HttpMethod Enum | ✅ 완료 |
|
||||||
| **Team API 타입** | `api/team.ts` | 팀 API Request/Response (add/removeMember 추가, requirePin 제거), 🆕 **AI 설정 API 타입** (GetAIConfig, UpdateAIConfig, GetAIStats) | ✅ 완료 |
|
| **Team API 타입** | `api/team.ts` | 팀 API Request/Response (add/removeMember 추가, requirePin 제거), 🆕 **AI 설정 API 타입** (GetAIConfig, UpdateAIConfig, GetAIStats), 🆕 **BulkCreateMembersRequest/Response, ResolveLoginIdRequest/Response** | ✅ 완료 |
|
||||||
|
| **BulkMember 타입** | `bulkMember.ts` | 🆕 **학생 일괄 생성 타입** (StudentCSVRow, GeneratedCredential, BulkCreateResult) | ✅ 완료 |
|
||||||
| **User API 타입** | `api/user.ts` | 사용자 API Request/Response (닉네임 관리 포함) | ✅ 완료 |
|
| **User API 타입** | `api/user.ts` | 사용자 API Request/Response (닉네임 관리 포함) | ✅ 완료 |
|
||||||
| **Writing API 타입** | `api/writing.ts` | 글 API Request/Response, 🆕 **CreateWritingRequest.analysis** (AI 분석 결과 포함) | ✅ 완료 |
|
| **Writing API 타입** | `api/writing.ts` | 글 API Request/Response, 🆕 **CreateWritingRequest.analysis** (AI 분석 결과 포함) | ✅ 완료 |
|
||||||
| ~~**Student API 타입**~~ | ~~`api/student.ts`~~ | ~~학생 API Request/Response~~ | ⚠️ Deprecated (api/user.ts로 대체) |
|
| ~~**Student API 타입**~~ | ~~`api/student.ts`~~ | ~~학생 API Request/Response~~ | ⚠️ Deprecated (api/user.ts로 대체) |
|
||||||
| **Topic API 타입** | `api/topic.ts` | 주제 API Request/Response (9개 엔드포인트, 팀 주제 포함) | ✅ 완료 |
|
| **Topic API 타입** | `api/topic.ts` | 주제 API Request/Response (9개 엔드포인트, 팀 주제 포함) | ✅ 완료 |
|
||||||
|
| **PendingInvite 타입** | `pendingInvite.ts` | 🆕 **이메일 대기 초대 타입** (PendingInvite, 초대 상태/응답 타입) | ✅ 완료 |
|
||||||
|
|
||||||
|
|
||||||
**타입 테스트** (`src/types/__tests__/`):
|
**타입 테스트** (`src/types/__tests__/`):
|
||||||
- 🆕 `plan.test.ts` - PlanType/BillingCycle/PlanSource/AIFeatureType Enum 검증, 타입 호환성 테스트 (70개 통과)
|
- 🆕 `plan.test.ts` - PlanType/BillingCycle/PlanSource/AIFeatureType Enum 검증, 타입 호환성 테스트 (70개 통과)
|
||||||
@ -1105,7 +1095,7 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
|
|
||||||
| 유틸리티 | 파일명 | 설명 | 상태 |
|
| 유틸리티 | 파일명 | 설명 | 상태 |
|
||||||
|---------|--------|------|------|
|
|---------|--------|------|------|
|
||||||
| **Team Code Generator** | `teamCodeGenerator.ts` | 한글/영어/일본어 팀 코드 생성 (형용사 + 색깔 + 동물, locale 파라미터) | ✅ 완료 |
|
| ~~**Team Code Generator**~~ | ~~`teamCodeGenerator.ts`~~ | ~~삭제됨 (초대 링크 시스템으로 대체)~~ | ❌ 삭제 |
|
||||||
| **🆕 i18n 유틸리티** | `i18n.ts` | 서비스 레이어용 번역 함수 (detectLocale, t), nested key 지원, 파라미터 치환 | ✅ 완료 |
|
| **🆕 i18n 유틸리티** | `i18n.ts` | 서비스 레이어용 번역 함수 (detectLocale, t), nested key 지원, 파라미터 치환 | ✅ 완료 |
|
||||||
| **🆕 Validation** | `validation.ts` | 폼 검증 유틸리티 (이메일/비밀번호/이름/비밀번호 확인), 타입 안전, 다국어 에러 메시지 | ✅ 완료 |
|
| **🆕 Validation** | `validation.ts` | 폼 검증 유틸리티 (이메일/비밀번호/이름/비밀번호 확인), 타입 안전, 다국어 에러 메시지 | ✅ 완료 |
|
||||||
| **🆕 SSE Stream Processor** | `sseStreamProcessor.ts` | Server-Sent Events 스트리밍 처리 (계정 병합 진행률 표시) | ✅ 완료 |
|
| **🆕 SSE Stream Processor** | `sseStreamProcessor.ts` | Server-Sent Events 스트리밍 처리 (계정 병합 진행률 표시) | ✅ 완료 |
|
||||||
@ -1158,9 +1148,6 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| **사용자 글 목록** | `/api/writing/user` | POST | 🆕 **사용자 글 목록** (userId 선택적, 본인 글만) | ✅ 완료 |
|
| **사용자 글 목록** | `/api/writing/user` | POST | 🆕 **사용자 글 목록** (userId 선택적, 본인 글만) | ✅ 완료 |
|
||||||
| **최근 글** | `/api/writing/recent` | POST | 🆕 **최근 글** (limit 파라미터, 기본 5개) | ✅ 완료 |
|
| **최근 글** | `/api/writing/recent` | POST | 🆕 **최근 글** (limit 파라미터, 기본 5개) | ✅ 완료 |
|
||||||
| **팀 CRUD** | `/api/team` | GET, POST, PUT, DELETE | 팀 생성/조회/수정/삭제 | ✅ 완료 |
|
| **팀 CRUD** | `/api/team` | GET, POST, PUT, DELETE | 팀 생성/조회/수정/삭제 | ✅ 완료 |
|
||||||
| **팀 보안 레벨** | `/api/team/[teamId]/security-level` | POST | 보안 레벨 변경 | ✅ 완료 |
|
|
||||||
| **명단 관리** | `/api/team/[teamId]/allowed-names` | POST, DELETE | 허용 이름 추가/제거 | ✅ 완료 |
|
|
||||||
| **이메일 관리** | `/api/team/[teamId]/allowed-emails` | POST, DELETE | 허용 이메일 추가/제거 | ✅ 완료 |
|
|
||||||
| **팀 AI 설정** | `/api/team/[teamId]/ai-config` | GET, PUT | 🆕 **AI 도우미 설정 조회/업데이트** | ✅ 완료 |
|
| **팀 AI 설정** | `/api/team/[teamId]/ai-config` | GET, PUT | 🆕 **AI 도우미 설정 조회/업데이트** | ✅ 완료 |
|
||||||
| **AI 장면 추출** | `/api/extract-scenes` | POST | 🆕 **글에서 주요 장면 추출** (Gemini Flash, 3~5개 장면, 각 장면별 이미지 프롬프트 자동 생성) | ✅ 완료 |
|
| **AI 장면 추출** | `/api/extract-scenes` | POST | 🆕 **글에서 주요 장면 추출** (Gemini Flash, 3~5개 장면, 각 장면별 이미지 프롬프트 자동 생성) | ✅ 완료 |
|
||||||
| **AI 이미지 생성** | `/api/generate-image` | POST | 🆕 **장면 기반 이미지 생성** (🆕 **AI 프롬프트 최적화**, Imagen 3.0, Firebase Storage 저장, Writing 업데이트, **플랜 검증**) | ✅ 완료 |
|
| **AI 이미지 생성** | `/api/generate-image` | POST | 🆕 **장면 기반 이미지 생성** (🆕 **AI 프롬프트 최적화**, Imagen 3.0, Firebase Storage 저장, Writing 업데이트, **플랜 검증**) | ✅ 완료 |
|
||||||
@ -1168,20 +1155,26 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| **조직 관리** | `/api/organization` | GET, POST, PUT, DELETE | 🆕 **Organization CRUD** (School Plan 보유, 멤버 조회, 플랜 정보) | ✅ 완료 |
|
| **조직 관리** | `/api/organization` | GET, POST, PUT, DELETE | 🆕 **Organization CRUD** (School Plan 보유, 멤버 조회, 플랜 정보) | ✅ 완료 |
|
||||||
| **주제 CRUD** | `/api/topic` | GET, POST, PUT, DELETE | 주제 생성/조회/수정/삭제 (9개 엔드포인트) | ✅ 완료 |
|
| **주제 CRUD** | `/api/topic` | GET, POST, PUT, DELETE | 주제 생성/조회/수정/삭제 (9개 엔드포인트) | ✅ 완료 |
|
||||||
| **주제별 작성자** | `/api/topic/[topicId]/writers` | GET | 🆕 **주제로 글 쓴 학생 목록** (글 개수, Firebase Auth 결합, 글 개수 내림차순) | ✅ 완료 |
|
| **주제별 작성자** | `/api/topic/[topicId]/writers` | GET | 🆕 **주제로 글 쓴 학생 목록** (글 개수, Firebase Auth 결합, 글 개수 내림차순) | ✅ 완료 |
|
||||||
| **계정 병합** | `/api/auth/merge-account` | POST | 🆕 **익명 계정 데이터 병합** (Firestore + Realtime DB 마이그레이션, Batch/Transaction, 통계 반환) | ✅ 완료 |
|
|
||||||
| **사용자 관리** | `/api/user` | GET, POST, PUT | 사용자 조회/생성/업데이트 | ✅ 완료 |
|
| **사용자 관리** | `/api/user` | GET, POST, PUT | 사용자 조회/생성/업데이트 | ✅ 완료 |
|
||||||
|
| **대기 초대** | `/api/pending-invite` | POST, GET | 🆕 **이메일 대기 초대 생성/조회** | ✅ 완료 |
|
||||||
|
| **내 대기 초대** | `/api/pending-invite/my` | GET | 🆕 **내 대기 초대 목록 조회** | ✅ 완료 |
|
||||||
|
| **대기 초대 취소** | `/api/pending-invite/[id]` | DELETE | 🆕 **대기 초대 취소** | ✅ 완료 |
|
||||||
|
| **대기 초대 응답** | `/api/pending-invite/[id]/respond` | POST | 🆕 **초대 수락/거절** | ✅ 완료 |
|
||||||
|
| **학생 일괄 생성** | `/api/team/[teamId]/bulk-create-members` | POST | 🆕 **CSV 기반 학생 계정 일괄 생성** (소유자 전용, 템플릿 ID/PW, Firebase Auth 생성) | ✅ 완료 |
|
||||||
|
| **loginId 변환** | `/api/auth/resolve-login-id` | POST | 🆕 **loginId→시스템 이메일 변환** (인증 불필요, 로그인 시 사용) | ✅ 완료 |
|
||||||
|
|
||||||
**서버 레이어** (`src/lib/server/`):
|
**서버 레이어** (`src/lib/server/`):
|
||||||
- `team.ts` - 팀 Firestore CRUD, 🆕 **getTeamAIConfig/updateTeamAIConfig** (AI 설정 관리), 🆕 **generateUniqueTeamCode(locale)** (언어별 코드 생성)
|
- `team.ts` - 팀 Firestore CRUD, 🆕 **getTeamAIConfig/updateTeamAIConfig** (AI 설정 관리)
|
||||||
- `user.ts` - 사용자 Firestore CRUD
|
- `user.ts` - 사용자 Firestore CRUD
|
||||||
- `topic.ts` - 주제 Firestore CRUD
|
- `topic.ts` - 주제 Firestore CRUD
|
||||||
- 🆕 `writing.ts` - 글 Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner, 🆕 **getTopicWriters**)
|
- 🆕 `writing.ts` - 글 Firestore CRUD (createWriting, getWriting, updateWriting, deleteWriting, getUserWritings, getRecentWritings, isWritingOwner, 🆕 **getTopicWriters**)
|
||||||
- 🆕 `patternAnalysis.ts` - 패턴 분석 결과 Firestore 저장/조회 (contentHash 키, 영구 저장)
|
- 🆕 `patternAnalysis.ts` - 패턴 분석 결과 Firestore 저장/조회 (contentHash 키, 영구 저장)
|
||||||
- 🆕 `teamCodeReservation.ts` - **팀 코드 예약 시스템** (Realtime DB transaction, atomic 예약, 5분 TTL, race condition 방지)
|
|
||||||
- 🆕 `planLimits.ts` - **플랜별 제한 상수** (PLAN_LIMITS, 헬퍼 함수들)
|
- 🆕 `planLimits.ts` - **플랜별 제한 상수** (PLAN_LIMITS, 헬퍼 함수들)
|
||||||
- 🆕 `planResolver.ts` - **플랜 결정 로직** (getEffectivePlan, getGuestEffectivePlan, canUseAIFeature)
|
- 🆕 `planResolver.ts` - **플랜 결정 로직** (getEffectivePlan, getGuestEffectivePlan, canUseAIFeature)
|
||||||
- 🆕 `aiUsage.ts` - **AI 사용량 추적** (getAIUsage, incrementAIUsage, hashIpAddress, getUsageDocId)
|
- 🆕 `aiUsage.ts` - **AI 사용량 추적** (getAIUsage, incrementAIUsage, hashIpAddress, getUsageDocId)
|
||||||
- 🆕 `organization.ts` - **Organization CRUD** (getOrganization, createOrganization, updateOrganization, deleteOrganization, isOrganizationPlanValid)
|
- 🆕 `organization.ts` - **Organization CRUD** (getOrganization, createOrganization, updateOrganization, deleteOrganization, isOrganizationPlanValid)
|
||||||
|
- 🆕 `pending-invite.ts` - **대기 초대 Firestore CRUD** (이메일 기반 초대 생성/조회/수락/거절/취소)
|
||||||
|
- 🆕 `bulk-member.ts` - **학생 일괄 생성** (템플릿 처리, loginId 해석, Firebase Auth 계정 생성, 팀 멤버 추가)
|
||||||
|
|
||||||
**서버 레이어 테스트** (`src/lib/server/__tests__/`):
|
**서버 레이어 테스트** (`src/lib/server/__tests__/`):
|
||||||
- 🆕 `planLimits.test.ts` - 플랜별 제한 상수 검증, 헬퍼 함수 테스트
|
- 🆕 `planLimits.test.ts` - 플랜별 제한 상수 검증, 헬퍼 함수 테스트
|
||||||
@ -1201,17 +1194,10 @@ firebase functions:log --only cleanupExpiredReservations
|
|||||||
| **Notification Store** | `notificationStore.ts` | 알림 상태 | ❌ 미구현 |
|
| **Notification Store** | `notificationStore.ts` | 알림 상태 | ❌ 미구현 |
|
||||||
|
|
||||||
**Auth Store 아키텍처 (2025-11-07 단순화, 2025-12-09 리팩토링)**:
|
**Auth Store 아키텍처 (2025-11-07 단순화, 2025-12-09 리팩토링)**:
|
||||||
- ✅ **user** - Firebase Auth + Firestore 결합 사용자 (익명 + 정식 계정, 🆕 **실시간 구독**)
|
- ✅ **user** - Firebase Auth + Firestore 결합 사용자 (정식 계정, 🆕 **실시간 구독**)
|
||||||
- ✅ **isAuthenticated** - 로그인 여부 (익명 포함)
|
- ✅ **isAuthenticated** - 로그인 여부
|
||||||
- ✅ **isLoading** - 🆕 **초기값 `true`로 변경 (2025-11-28)** - Auth 초기화 완료 전 리다이렉트 방지
|
- ✅ **isLoading** - 🆕 **초기값 `true`로 변경 (2025-11-28)** - Auth 초기화 완료 전 리다이렉트 방지
|
||||||
- ✅ **user.isAnonymous** - 익명/정식 계정 구분
|
|
||||||
- ✅ **loginAsUser()** - 팀 코드 로그인 (PIN 제거)
|
|
||||||
- ✅ **linkWithEmail()** - 이메일 계정 연결 (신규 계정 생성)
|
|
||||||
- ✅ **linkWithGoogle()** - Google 계정 연결 (신규 계정 생성)
|
|
||||||
- ✅ **mergeWithEmail()** - 기존 이메일 계정과 병합 (데이터 마이그레이션, 🆕 **performMerge 헬퍼**)
|
|
||||||
- ✅ **mergeWithGoogle()** - 기존 Google 계정과 병합 (데이터 마이그레이션, 🆕 **performMerge 헬퍼**)
|
|
||||||
- ✅ **🆕 initializeAuth()** - Firestore `onSnapshot` 실시간 구독 (aiCredits/plan/settings 자동 업데이트)
|
- ✅ **🆕 initializeAuth()** - Firestore `onSnapshot` 실시간 구독 (aiCredits/plan/settings 자동 업데이트)
|
||||||
- ❌ ~~currentStudent~~, ~~ownedStudents~~, ~~switchStudent()~~ 제거 (복잡도 감소)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -1302,7 +1288,7 @@ project_w/
|
|||||||
│ └── favicon.ico
|
│ └── favicon.ico
|
||||||
│
|
│
|
||||||
├── components/ # React 컴포넌트
|
├── components/ # React 컴포넌트
|
||||||
│ ├── auth/ # ✅ 인증 (로그인, 프로필, 팀 코드)
|
│ ├── auth/ # ✅ 인증 (로그인, 프로필)
|
||||||
│ ├── landing/ # ✅ 랜딩 페이지 카드들 (다국어 지원)
|
│ ├── landing/ # ✅ 랜딩 페이지 카드들 (다국어 지원)
|
||||||
│ ├── navigation/ # ✅ 네비게이션 바 (다국어 지원)
|
│ ├── navigation/ # ✅ 네비게이션 바 (다국어 지원)
|
||||||
│ │ ├── Navbar.tsx # 다국어 링크 텍스트
|
│ │ ├── Navbar.tsx # 다국어 링크 텍스트
|
||||||
@ -1317,7 +1303,6 @@ project_w/
|
|||||||
│ │ └── CreateTopicDialog.tsx # ✅ 통합 주제 생성 (개인/팀 공용)
|
│ │ └── CreateTopicDialog.tsx # ✅ 통합 주제 생성 (개인/팀 공용)
|
||||||
│ ├── team/ # ✅ 팀 관련
|
│ ├── team/ # ✅ 팀 관련
|
||||||
│ │ ├── TeamTopicManager.tsx # ✅ 팀 주제 관리
|
│ │ ├── TeamTopicManager.tsx # ✅ 팀 주제 관리
|
||||||
│ │ ├── SecurityLevelSelector.tsx # 보안 레벨 선택 (RadioCard, 애니메이션)
|
|
||||||
│ │ ├── TopicMemberAnalysisSection.tsx # 주제별 학생 분석 (UI만)
|
│ │ ├── TopicMemberAnalysisSection.tsx # 주제별 학생 분석 (UI만)
|
||||||
│ │ └── LiveWritingMonitor.tsx # 🆕 실시간 글쓰기 모니터링 (Realtime DB)
|
│ │ └── LiveWritingMonitor.tsx # 🆕 실시간 글쓰기 모니터링 (Realtime DB)
|
||||||
│ └── [미구현]
|
│ └── [미구현]
|
||||||
@ -1345,7 +1330,7 @@ project_w/
|
|||||||
│ └── StickerManager.ts
|
│ └── StickerManager.ts
|
||||||
│
|
│
|
||||||
├── data/ # ✅ 정적 데이터
|
├── data/ # ✅ 정적 데이터
|
||||||
│ └── classCodeWords.ts # ✅ 한글 팀 코드 단어 (형용사, 색상, 동물)
|
│ └── classCodeWords.ts # ❌ 삭제 예정 (팀 코드 시스템 제거됨)
|
||||||
│
|
│
|
||||||
├── services/ # 데이터 레이어 (Firestore/API 호출)
|
├── services/ # 데이터 레이어 (Firestore/API 호출)
|
||||||
│ ├── firebaseAuth.ts # ✅ 인증 (이메일, Google, Anonymous)
|
│ ├── firebaseAuth.ts # ✅ 인증 (이메일, Google, Anonymous)
|
||||||
@ -1382,7 +1367,7 @@ project_w/
|
|||||||
└── utils/ # 유틸리티 함수
|
└── utils/ # 유틸리티 함수
|
||||||
├── passwordStrength.ts # ✅ 비밀번호 강도 계산
|
├── passwordStrength.ts # ✅ 비밀번호 강도 계산
|
||||||
├── passwordSecurity.ts # ✅ HIBP API 연동
|
├── passwordSecurity.ts # ✅ HIBP API 연동
|
||||||
└── classCodeGenerator.ts # ✅ 팀 코드 생성/검증/정규화
|
└── classCodeGenerator.ts # ❌ 삭제 예정 (팀 코드 시스템 제거됨)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -1397,24 +1382,13 @@ project_w/
|
|||||||
5. ✅ 인증 기반 라우팅 (`/`와 `/home` 분리)
|
5. ✅ 인증 기반 라우팅 (`/`와 `/home` 분리)
|
||||||
6. ✅ 글쓰기 페이지 (Tiptap 에디터, 저장 시 AI 분석, 분석 결과 DB 저장)
|
6. ✅ 글쓰기 페이지 (Tiptap 에디터, 저장 시 AI 분석, 분석 결과 DB 저장)
|
||||||
7. ✅ 주제 선택 및 관리 (자유/개인 주제, 템플릿)
|
7. ✅ 주제 선택 및 관리 (자유/개인 주제, 템플릿)
|
||||||
8. ✅ **팀 코드 시스템** (완료)
|
8. ✅ **초대 링크 시스템** (완료)
|
||||||
- ✅ 한글 팀 코드 생성 ("춤추는 파란 사자")
|
- ✅ 초대 링크 생성/관리 (만료 시간, 최대 사용 횟수)
|
||||||
- ✅ Anonymous Auth 학생 로그인
|
|
||||||
- ✅ 팀/학생 관리 서비스 (ownerId 기반)
|
|
||||||
- ✅ PIN 인증 (SHA-256)
|
|
||||||
- ✅ 정식 계정 연결 (linkWithCredential)
|
|
||||||
- ✅ currentStudent 중심 authStore 재설계
|
|
||||||
- ✅ 학생 로그인 UI (3단계 플로우, 이름 입력, 완료 화면)
|
|
||||||
- ✅ 팀 관리 UI (목록/생성/멤버 페이지/관리 페이지)
|
- ✅ 팀 관리 UI (목록/생성/멤버 페이지/관리 페이지)
|
||||||
- ✅ 용어 변경: "클래스" → "팀", "교사" → "소유자"
|
- ✅ 이메일 기반 대기 초대 시스템
|
||||||
9. 🚧 **다음**: 내가 쓴 글 목록
|
9. 🚧 **다음**: 내가 쓴 글 목록
|
||||||
|
|
||||||
### Phase 2: 팀 코드 UI 완성
|
### Phase 2: 학습 시스템
|
||||||
- 학생 로그인 UI (팀 코드 입력 → 이름 선택 → PIN)
|
|
||||||
- 팀 관리 (팀 생성, 멤버 관리)
|
|
||||||
- LoginDialog에 학생 탭 추가
|
|
||||||
|
|
||||||
### Phase 3: 학습 시스템
|
|
||||||
- `/learn` 학습 페이지
|
- `/learn` 학습 페이지
|
||||||
- 레슨 콘텐츠 및 진행 상황
|
- 레슨 콘텐츠 및 진행 상황
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@ -7,7 +7,7 @@
|
|||||||
라온누리는 초등학생들이 재미있게 글쓰기를 배울 수 있는 한국어 교육 플랫폼입니다.
|
라온누리는 초등학생들이 재미있게 글쓰기를 배울 수 있는 한국어 교육 플랫폼입니다.
|
||||||
|
|
||||||
### 주요 특징
|
### 주요 특징
|
||||||
- **팀 코드 시스템**: 초등 저학년도 쉽게 로그인 ("춤추는 파란 사자")
|
- **초대 링크 시스템**: 디스코드 스타일 초대 링크로 팀 참여
|
||||||
- **개인 맞춤 주제**: 자유 주제, 그룹 주제, 개인 주제 지원
|
- **개인 맞춤 주제**: 자유 주제, 그룹 주제, 개인 주제 지원
|
||||||
- **글쓰기 에디터**: 초등학생 친화적인 순수 텍스트 에디터
|
- **글쓰기 에디터**: 초등학생 친화적인 순수 텍스트 에디터
|
||||||
- 🔜 **레벨업 시스템**: 경험치와 스티커 보상 (예정)
|
- 🔜 **레벨업 시스템**: 경험치와 스티커 보상 (예정)
|
||||||
@ -19,8 +19,7 @@
|
|||||||
- **언어**: TypeScript
|
- **언어**: TypeScript
|
||||||
- **UI Library**: Chakra UI v3
|
- **UI Library**: Chakra UI v3
|
||||||
- **인증**: Firebase Authentication
|
- **인증**: Firebase Authentication
|
||||||
- Email/Password, Google OAuth (정식 계정)
|
- Email/Password, Google OAuth
|
||||||
- Anonymous Auth (학생 팀 코드 로그인)
|
|
||||||
- **데이터베이스**: Firestore
|
- **데이터베이스**: Firestore
|
||||||
- **상태 관리**: Zustand
|
- **상태 관리**: Zustand
|
||||||
- **비즈니스 로직**: Manager 패턴 (API 호출 + 클라이언트 캐싱)
|
- **비즈니스 로직**: Manager 패턴 (API 호출 + 클라이언트 캐싱)
|
||||||
@ -97,17 +96,10 @@ src/
|
|||||||
- Google OAuth 소셜 로그인
|
- Google OAuth 소셜 로그인
|
||||||
- 비밀번호 강도 체크 + HIBP API 유출 확인
|
- 비밀번호 강도 체크 + HIBP API 유출 확인
|
||||||
|
|
||||||
**학생 로그인** (초등 저학년):
|
|
||||||
- 팀 코드 기반 로그인 ("춤추는 파란 사자")
|
|
||||||
- Anonymous Auth 사용
|
|
||||||
- 이름만 입력 또는 이름 + PIN
|
|
||||||
- 정식 계정 연결 가능 (선택적)
|
|
||||||
|
|
||||||
### ✅ 팀 관리 시스템
|
### ✅ 팀 관리 시스템
|
||||||
|
|
||||||
- 팀 생성 (누구나 가능)
|
- 팀 생성 (누구나 가능)
|
||||||
- 한글 팀 코드 자동 생성 (10만 가지 조합)
|
- 초대 링크 생성 및 관리 (만료 시간, 최대 사용 횟수 설정)
|
||||||
- 보안 모드 3종: simple, normal, open
|
|
||||||
- 팀원 관리 (이름 수정, 강퇴)
|
- 팀원 관리 (이름 수정, 강퇴)
|
||||||
- 멤버 페이지 (팀 정보 및 멤버 목록)
|
- 멤버 페이지 (팀 정보 및 멤버 목록)
|
||||||
|
|
||||||
|
|||||||
17
ROADMAP.md
17
ROADMAP.md
@ -1,6 +1,6 @@
|
|||||||
# 라온누리 - 개발 로드맵
|
# 라온누리 - 개발 로드맵
|
||||||
|
|
||||||
> 최종 업데이트: 2026-03-09 (글쓰기 플로우 단순화)
|
> 최종 업데이트: 2026-03-19 (학생 일괄 생성 및 loginId 시스템)
|
||||||
|
|
||||||
초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획
|
초등학생을 위한 창작 글쓰기 교육 플랫폼 개발 계획
|
||||||
|
|
||||||
@ -53,14 +53,12 @@
|
|||||||
| **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** |
|
| ~~보안 레벨 시스템~~ | ~~TeamSecurityLevel enum, 팀별 보안 정책 선택, 명단 관리 API~~ (초대 링크 시스템으로 대체) | **2025-11-10** |
|
||||||
| **User 타입 최소화** | **FirestoreUser/User 분리, Firebase Auth를 Single Source of Truth로, 데이터 중복 제거** | **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** |
|
| **닉네임 저장 위치 변경** | **users.nicknames → team.members[uid].nickname 이동, TeamMember 타입 확장** | **2025-11-10** |
|
||||||
| **authStore DB 연동** | **combineUserData로 Firebase Auth + Firestore 자동 결합, userManager.getUser() 활용** | **2025-11-10** |
|
| **authStore DB 연동** | **combineUserData로 Firebase Auth + Firestore 자동 결합, userManager.getUser() 활용** | **2025-11-10** |
|
||||||
| **memberUids 제거** | **Team.memberUids 필드 제거, Object.keys(members) 사용, 데이터 중복 제거** | **2025-11-10** |
|
| **memberUids 제거** | **Team.memberUids 필드 제거, Object.keys(members) 사용, 데이터 중복 제거** | **2025-11-10** |
|
||||||
| **SecurityLevelSelector** | **RadioCard 기반 보안 레벨 선택 UI, framer-motion 애니메이션, 그라데이션 배경** | **2025-11-10** |
|
| ~~SecurityLevelSelector~~ | ~~RadioCard 기반 보안 레벨 선택 UI~~ (초대 링크 시스템으로 대체) | **2025-11-10** |
|
||||||
| **API Routes 구현** | **보안 레벨 변경, 명단 관리 API (RESTful 원칙), POST/DELETE 메서드 구분** | **2025-11-10** |
|
|
||||||
| **lib/server/team 확장** | **updateTeamSecurityLevel, add/removeAllowedName/Email, 자동 명단 생성** | **2025-11-10** |
|
|
||||||
| **react-icons 전환** | **이모티콘 → react-icons/lu로 전환 (LuGlobe, LuClipboardList 등)** | **2025-11-10** |
|
| **react-icons 전환** | **이모티콘 → react-icons/lu로 전환 (LuGlobe, LuClipboardList 등)** | **2025-11-10** |
|
||||||
| **다중 글조각 관리** | **DraftManager (localStorage + Realtime DB 하이브리드, 기기 간 동기화, 최대 10개), SavedDraftsDialog (syncStatus 배지), 2초 debounce 자동 저장** | **2025-11-10 / 2025-11-19** |
|
| **다중 글조각 관리** | **DraftManager (localStorage + Realtime DB 하이브리드, 기기 간 동기화, 최대 10개), SavedDraftsDialog (syncStatus 배지), 2초 debounce 자동 저장** | **2025-11-10 / 2025-11-19** |
|
||||||
| **테마 슬롯 레시피** | **Dialog, Select slot recipe 추가 (자동 배경색, border, shadow)** | **2025-11-10** |
|
| **테마 슬롯 레시피** | **Dialog, Select slot recipe 추가 (자동 배경색, border, shadow)** | **2025-11-10** |
|
||||||
@ -83,7 +81,7 @@
|
|||||||
| **패턴 분석 - 팀 소유자 기능** | **3가지 분석 타입 (self/by-team/by-topic), API 권한 체크, 팀 주제 필터링, WritingPatternDialog Props 확장, TopicMemberAnalysisSection (UI만), 팀 관리 페이지에 멤버 분석 메뉴** | **2025-11-12** |
|
| **패턴 분석 - 팀 소유자 기능** | **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** |
|
| **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** |
|
| **실시간 글쓰기 모니터링** | **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** |
|
||||||
| **다국어 지원 시스템 (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 + SecurityLevelSelector - 60+ 항목), Write 페이지 번역 (분석/저장 메시지, 버튼), 일본어 어린이 친화적 표현 (한자 최소화, ひらがな 우선), site.ts 텍스트를 번역 파일로 이동 (사이트명, 태그라인, 저작권), localeDetection: true 설정, NEXT_LOCALE 쿠키 저장, next.config.ts에 next-intl 플러그인 추가, i18n/routing.ts + i18n/request.ts 설정, 타입 체크 통과, CLAUDE.md에 다국어 필수 규칙 추가** | **2025-11-13** |
|
| **다국어 지원 시스템 (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** |
|
| **개별 글 분석 결과 저장** | **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** |
|
| **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** |
|
||||||
| **AI 설정 고급 UI 완성** | **AIConfigDialog 컴포넌트 (팀 관리 페이지), Slider 3개 (멈춤 감지/최대 힌트/쿨다운), 커스텀 힌트 레벨 카드 (반투명 배경 번호, 우상단 접힌 페이지 삼각형 인디케이터, CSS border trick, 직접 onClick 처리), SimpleGrid 2x2 레이아웃, 브랜드 컬러 상태 변화, Switch (AI 제안 그대로 사용 금지), 함수형 상태 업데이트 (클로저 해결), 다국어 지원 (ko/en/ja, team.manage.aiConfig namespace 28개 키)** | **2025-11-17** |
|
| **AI 설정 고급 UI 완성** | **AIConfigDialog 컴포넌트 (팀 관리 페이지), Slider 3개 (멈춤 감지/최대 힌트/쿨다운), 커스텀 힌트 레벨 카드 (반투명 배경 번호, 우상단 접힌 페이지 삼각형 인디케이터, CSS border trick, 직접 onClick 처리), SimpleGrid 2x2 레이아웃, 브랜드 컬러 상태 변화, Switch (AI 제안 그대로 사용 금지), 함수형 상태 업데이트 (클로저 해결), 다국어 지원 (ko/en/ja, team.manage.aiConfig namespace 28개 키)** | **2025-11-17** |
|
||||||
@ -123,7 +121,7 @@
|
|||||||
| **공개 팀 목록 페이지** | **/team/all 페이지 구현, TeamCard 컴포넌트 (글래스모피즘 스타일), 페이지네이션 (커서 기반 무한 스크롤), Navbar "공개 팀" 메뉴 추가, 공개 팀만 표시 (isPublic=true), 다국어 지원 (teams namespace, ko/en/ja)** | **2025-11-27** |
|
| **공개 팀 목록 페이지** | **/team/all 페이지 구현, TeamCard 컴포넌트 (글래스모피즘 스타일), 페이지네이션 (커서 기반 무한 스크롤), Navbar "공개 팀" 메뉴 추가, 공개 팀만 표시 (isPublic=true), 다국어 지원 (teams namespace, ko/en/ja)** | **2025-11-27** |
|
||||||
| **팀 상세 페이지 통합** | **/team/[teamId] 페이지 완전 재구성, 3가지 뷰 모드 (멤버 뷰/공개 뷰/접근 불가), 멤버 뷰: 멤버 목록 + 관리/나가기 버튼, 공개 뷰: 팀 정보 + 공개 글 목록 + 참여 버튼, PublicWritingCard 컴포넌트, 다국어 지원** | **2025-11-27** |
|
| **팀 상세 페이지 통합** | **/team/[teamId] 페이지 완전 재구성, 3가지 뷰 모드 (멤버 뷰/공개 뷰/접근 불가), 멤버 뷰: 멤버 목록 + 관리/나가기 버튼, 공개 뷰: 팀 정보 + 공개 글 목록 + 참여 버튼, PublicWritingCard 컴포넌트, 다국어 지원** | **2025-11-27** |
|
||||||
| **팀 커버 이미지 시스템** | **Team 타입 확장 (coverImage?: string), Firebase Storage 업로드 (adminStorage.bucket()), TeamCoverImageUploader 컴포넌트 (드래그앤드롭, 미리보기, AspectRatio 16:9, 5MB 제한 JPEG/PNG/WebP/GIF), POST/DELETE /api/team/[teamId]/cover-image (FormData 업로드, 기존 이미지 자동 삭제, makePublic, extractPathFromUrl 헬퍼), TeamManager 메서드 (uploadCoverImage, deleteCoverImage, fetch FormData, 캐시 무효화), 팀 생성 페이지 이미지 선택 (선택적), 팀 관리 페이지 즉시 업로드/삭제 (공개 설정 내), TeamCard 표시 (이미지 있으면 상단 140px, 그라데이션 오버레이), 다국어 지원 (team.coverImage namespace 16개 키, ko/en/ja)** | **2025-11-27** |
|
| **팀 커버 이미지 시스템** | **Team 타입 확장 (coverImage?: string), Firebase Storage 업로드 (adminStorage.bucket()), TeamCoverImageUploader 컴포넌트 (드래그앤드롭, 미리보기, AspectRatio 16:9, 5MB 제한 JPEG/PNG/WebP/GIF), POST/DELETE /api/team/[teamId]/cover-image (FormData 업로드, 기존 이미지 자동 삭제, makePublic, extractPathFromUrl 헬퍼), TeamManager 메서드 (uploadCoverImage, deleteCoverImage, fetch FormData, 캐시 무효화), 팀 생성 페이지 이미지 선택 (선택적), 팀 관리 페이지 즉시 업로드/삭제 (공개 설정 내), TeamCard 표시 (이미지 있으면 상단 140px, 그라데이션 오버레이), 다국어 지원 (team.coverImage namespace 16개 키, ko/en/ja)** | **2025-11-27** |
|
||||||
| **팀 멤버 아바타 표시 개선** | **Avatar 컴포넌트 도입 (photoURL 표시), 익명 계정 LuUser 아이콘 + gray 색상, 정식 계정 FaUserGraduate 아이콘 + teal 색상, "정식계정/익명" 텍스트 표시 제거, 닉네임과 실명이 다른 경우만 실명 이탤릭 표시** | **2025-11-27** |
|
| **팀 멤버 아바타 표시 개선** | **Avatar 컴포넌트 도입 (photoURL 표시), FaUserGraduate 아이콘 + teal 색상, 닉네임과 실명이 다른 경우만 실명 이탤릭 표시** | **2025-11-27** |
|
||||||
| **채점 시스템 전면 개편** | **0~1 품질 기반 점수 (5단계: 0, 0.25, 0.5, 0.75, 1.0), 가중 평균 0~100점 변환, 계층적 설정 (기본→팀→주제 우선순위), ScoringConfig/ScoringWeights/ScoringRubric 타입, scoringConfigService.ts (설정 병합), temperature=0 일관성, TeamScoringSettings 컴포넌트 (가중치 슬라이더), TeamRubricSettings 컴포넌트 (5단계 기준 편집, 아코디언 UI), 팀 관리 페이지 통합, 다국어 지원 (ko/en/ja)** | **2025-11-27** |
|
| **채점 시스템 전면 개편** | **0~1 품질 기반 점수 (5단계: 0, 0.25, 0.5, 0.75, 1.0), 가중 평균 0~100점 변환, 계층적 설정 (기본→팀→주제 우선순위), ScoringConfig/ScoringWeights/ScoringRubric 타입, scoringConfigService.ts (설정 병합), temperature=0 일관성, TeamScoringSettings 컴포넌트 (가중치 슬라이더), TeamRubricSettings 컴포넌트 (5단계 기준 편집, 아코디언 UI), 팀 관리 페이지 통합, 다국어 지원 (ko/en/ja)** | **2025-11-27** |
|
||||||
| **이미지 업로드/선택 플로우 개편** | **/imageUpload 페이지 신규 생성 (AI 생성/직접 업로드 선택), WritingManager.uploadUserImage() 메서드 추가 (클라이언트 사이드 Canvas API 리사이즈, 1920x1080 최대, 85% 품질, Firebase Storage 업로드), WritingManager.analyzeWritingBackground() 메서드 추가 (fire-and-forget 패턴), Write 페이지 저장 플로우 변경 (저장 → 백그라운드 분석 → /imageUpload 리다이렉트, GenerateImageDialog 제거), Interaction 페이지 리다이렉트 로직 추가 (이미지 없으면 /imageUpload로 자동 이동), authStore.isLoading 초기값 변경 (false → true, auth 초기화 완료 전 리다이렉트 방지), 드래그앤드롭 파일 업로드 지원, 파일 검증 (JPEG/PNG/WebP, 5MB 제한), 다국어 지원 (imageUpload namespace, ko/en/ja 15개 키)** | **2025-11-28** |
|
| **이미지 업로드/선택 플로우 개편** | **/imageUpload 페이지 신규 생성 (AI 생성/직접 업로드 선택), WritingManager.uploadUserImage() 메서드 추가 (클라이언트 사이드 Canvas API 리사이즈, 1920x1080 최대, 85% 품질, Firebase Storage 업로드), WritingManager.analyzeWritingBackground() 메서드 추가 (fire-and-forget 패턴), Write 페이지 저장 플로우 변경 (저장 → 백그라운드 분석 → /imageUpload 리다이렉트, GenerateImageDialog 제거), Interaction 페이지 리다이렉트 로직 추가 (이미지 없으면 /imageUpload로 자동 이동), authStore.isLoading 초기값 변경 (false → true, auth 초기화 완료 전 리다이렉트 방지), 드래그앤드롭 파일 업로드 지원, 파일 검증 (JPEG/PNG/WebP, 5MB 제한), 다국어 지원 (imageUpload namespace, ko/en/ja 15개 키)** | **2025-11-28** |
|
||||||
| **가격정책 페이지** | **/pricing 페이지 신규 생성 (4개 플랜: Free, Classroom, Academy, School), 월간/연간 결제 토글 (20% 할인), 기능 비교 테이블, FAQ 섹션, 다국어 지원 (pricing namespace, ko/en/ja 40개 키), PricingCard 컴포넌트, Navbar "요금제" 메뉴 추가** | **2025-11-28** |
|
| **가격정책 페이지** | **/pricing 페이지 신규 생성 (4개 플랜: Free, Classroom, Academy, School), 월간/연간 결제 토글 (20% 할인), 기능 비교 테이블, FAQ 섹션, 다국어 지원 (pricing namespace, ko/en/ja 40개 키), PricingCard 컴포넌트, Navbar "요금제" 메뉴 추가** | **2025-11-28** |
|
||||||
@ -152,7 +150,7 @@
|
|||||||
| **플랜 다운그레이드 선택 옵션** | **DowngradeOptionDialog 컴포넌트 (즉시 적용/다음 결제일 선택), downgradeMode 파라미터 추가 (immediate/scheduled), 즉시 다운그레이드 시 크레딧 전환, GET /api/user/plan/estimate-refund API (예상 환불 크레딧 조회), isDowngrade/isUpgrade 공용 유틸로 분리 (src/lib/planUtils.ts), Pricing 페이지 통합, 다국어 지원 (downgradeDialog namespace, ko/en/ja)** | **2025-12-10** |
|
| **플랜 다운그레이드 선택 옵션** | **DowngradeOptionDialog 컴포넌트 (즉시 적용/다음 결제일 선택), downgradeMode 파라미터 추가 (immediate/scheduled), 즉시 다운그레이드 시 크레딧 전환, GET /api/user/plan/estimate-refund API (예상 환불 크레딧 조회), isDowngrade/isUpgrade 공용 유틸로 분리 (src/lib/planUtils.ts), Pricing 페이지 통합, 다국어 지원 (downgradeDialog namespace, ko/en/ja)** | **2025-12-10** |
|
||||||
| **결제 주기 변경 기능** | **동일 플랜 결제 주기 변경 (월간↔연간), scheduledPlan.billingCycle 필드 추가, 다음 결제일부터 적용, 월간 30일/연간 365일 구독 기간, billingCycleChangeSuccess 토스트 알림, 다국어 지원** | **2025-12-10** |
|
| **결제 주기 변경 기능** | **동일 플랜 결제 주기 변경 (월간↔연간), scheduledPlan.billingCycle 필드 추가, 다음 결제일부터 적용, 월간 30일/연간 365일 구독 기간, billingCycleChangeSuccess 토스트 알림, 다국어 지원** | **2025-12-10** |
|
||||||
| **팀 소유자 이전 기능** | **POST /api/team/[teamId]/transfer-ownership API, TeamMemberEditor 소유자 이전 메뉴, 소유자 역할 배지 표시, 다국어 지원 (transferOwnership, transferConfirm 등 ko/en/ja)** | **2025-12-12** |
|
| **팀 소유자 이전 기능** | **POST /api/team/[teamId]/transfer-ownership API, TeamMemberEditor 소유자 이전 메뉴, 소유자 역할 배지 표시, 다국어 지원 (transferOwnership, transferConfirm 등 ko/en/ja)** | **2025-12-12** |
|
||||||
| **팀 보안 설정 통합** | **TeamSecuritySettingsDialog에 공개설정 통합 (isPublic 스위치, allowPublicWritings, description, coverImage), TeamInfoCard에 검색가능/불가능 태그 추가 (FaGlobe/FaEyeSlash), TeamPublicSettings 제거, SecurityLevelSelector searchable 속성 추가, 다국어 지원 (searchable/notSearchable ko/en/ja)** | **2025-12-12** |
|
| **팀 설정 통합** | **TeamSettingsDialog에 공개설정 통합 (isPublic 스위치, allowPublicWritings, description, coverImage), TeamInfoCard에 검색가능/불가능 태그 추가 (FaGlobe/FaEyeSlash), TeamPublicSettings 제거, 다국어 지원 (searchable/notSearchable ko/en/ja)** | **2025-12-12** |
|
||||||
| **이미지 생성 가능 여부 확인 API** | **POST /api/check-image-generation (팀/개인 글쓰기 분기 처리), 5가지 비활성화 사유 (PLAN_NOT_SUPPORTED, LIMIT_EXCEEDED, TEAM_AI_DISABLED, TEAM_LIMIT_EXCEEDED, DAILY_LIMIT_EXCEEDED), WritingManager.checkImageGenerationAvailability() 메서드 추가, CheckImageGenerationRequest/Result/DisableReason 타입, 다국어 지원 (imageUpload.disabledReasons namespace ko/en/ja)** | **2025-12-16** |
|
| **이미지 생성 가능 여부 확인 API** | **POST /api/check-image-generation (팀/개인 글쓰기 분기 처리), 5가지 비활성화 사유 (PLAN_NOT_SUPPORTED, LIMIT_EXCEEDED, TEAM_AI_DISABLED, TEAM_LIMIT_EXCEEDED, DAILY_LIMIT_EXCEEDED), WritingManager.checkImageGenerationAvailability() 메서드 추가, CheckImageGenerationRequest/Result/DisableReason 타입, 다국어 지원 (imageUpload.disabledReasons namespace ko/en/ja)** | **2025-12-16** |
|
||||||
| **계획된 기능 문서화** | **plannedFeature/ 디렉토리 신규 생성, 우선순위별 분류 (high/medium/low-priority.md), implementation-guide.md 구현 가이드, 주요 기능 (부모님 대시보드, 선생님 첨삭 시스템, 글 포트폴리오, 협업 글쓰기, 개인화 글감 AI, 독후감 템플릿, 음성 녹음, PDF 내보내기, 배지/업적 시스템)** | **2025-12-16** |
|
| **계획된 기능 문서화** | **plannedFeature/ 디렉토리 신규 생성, 우선순위별 분류 (high/medium/low-priority.md), implementation-guide.md 구현 가이드, 주요 기능 (부모님 대시보드, 선생님 첨삭 시스템, 글 포트폴리오, 협업 글쓰기, 개인화 글감 AI, 독후감 템플릿, 음성 녹음, PDF 내보내기, 배지/업적 시스템)** | **2025-12-16** |
|
||||||
| **커리큘럼 UX 개편** | **CurriculumPreviewModal (목록 클릭→모달로 상세 표시), 레슨별 콘텐츠 블록 상세 표시 (Accordion UI, theory/quiz/mission/writing_prompt), TeamSelectModal (커리큘럼 가져오기 시 팀 선택), POST /api/curriculum/[curriculumId]/copy (레슨 포함 전체 복사), /my-curriculums 페이지 (MyCurriculumsDashboard), CurriculumEditModal (제목/설명 수정), 권한 분리 (시스템/타팀→가져오기만, 내팀→편집 가능), 스켈레톤 로딩 UI, 다국어 지원 (preview.blockType namespace ko/en/ja)** | **2025-12-19** |
|
| **커리큘럼 UX 개편** | **CurriculumPreviewModal (목록 클릭→모달로 상세 표시), 레슨별 콘텐츠 블록 상세 표시 (Accordion UI, theory/quiz/mission/writing_prompt), TeamSelectModal (커리큘럼 가져오기 시 팀 선택), POST /api/curriculum/[curriculumId]/copy (레슨 포함 전체 복사), /my-curriculums 페이지 (MyCurriculumsDashboard), CurriculumEditModal (제목/설명 수정), 권한 분리 (시스템/타팀→가져오기만, 내팀→편집 가능), 스켈레톤 로딩 UI, 다국어 지원 (preview.blockType namespace ko/en/ja)** | **2025-12-19** |
|
||||||
@ -164,6 +162,9 @@
|
|||||||
| **글쓰기 플로우 단순화** | **/write 커리큘럼 파라미터 자동 리다이렉트, NextAssignmentCard (홈 다음 과제 카드), WritingModeSelector 2단계→1단계 단순화 (토글+카드), TopicSelector 인라인 변형 (variant="inline"), 전체 에디터 페이지 topicId searchParam 처리, 다국어 지원 (ko/en/ja)** | **2026-03-09** |
|
| **글쓰기 플로우 단순화** | **/write 커리큘럼 파라미터 자동 리다이렉트, NextAssignmentCard (홈 다음 과제 카드), WritingModeSelector 2단계→1단계 단순화 (토글+카드), TopicSelector 인라인 변형 (variant="inline"), 전체 에디터 페이지 topicId searchParam 처리, 다국어 지원 (ko/en/ja)** | **2026-03-09** |
|
||||||
| **ImageDropzone 범용화** | **ImageDropzone을 범용 이미지 업로드 컴포넌트로 리팩토링, TeamCoverImageUploader 삭제 및 통합, 새 props 추가 (currentImageUrl, onDelete, containerAspectRatio, acceptedTypes, maxFileSize, labels, showHoverOverlay, label, helperText), document.getElementById→useRef 전환, 팀 생성/공개설정/보안설정 3곳 마이그레이션, 기존 글쓰기 5곳은 변경 없음 (모든 새 props optional + 기본값 동일)** | **2026-02-26** |
|
| **ImageDropzone 범용화** | **ImageDropzone을 범용 이미지 업로드 컴포넌트로 리팩토링, TeamCoverImageUploader 삭제 및 통합, 새 props 추가 (currentImageUrl, onDelete, containerAspectRatio, acceptedTypes, maxFileSize, labels, showHoverOverlay, label, helperText), document.getElementById→useRef 전환, 팀 생성/공개설정/보안설정 3곳 마이그레이션, 기존 글쓰기 5곳은 변경 없음 (모든 새 props optional + 기본값 동일)** | **2026-02-26** |
|
||||||
| **ImageDropzone 카메라 촬영 기능** | **CameraCaptureDialog 컴포넌트 신규 생성 (GlassDialog 기반, getUserMedia 카메라 스트림, 4:3 프레임 가이드 오버레이, 전/후면 카메라 전환), ImageDropzone에 "카메라로 촬영" 버튼 추가 (파일 선택과 나란히 배치), 촬영 → ImageCropper 크롭 플로우 연동, 카메라 권한 거부/미지원 에러 처리, 다국어 지원 (camera namespace ko/en/ja 7개 키), 모든 사용처(글쓰기 5곳, 팀 3곳) 자동 활성화** | **2026-03-03** |
|
| **ImageDropzone 카메라 촬영 기능** | **CameraCaptureDialog 컴포넌트 신규 생성 (GlassDialog 기반, getUserMedia 카메라 스트림, 4:3 프레임 가이드 오버레이, 전/후면 카메라 전환), ImageDropzone에 "카메라로 촬영" 버튼 추가 (파일 선택과 나란히 배치), 촬영 → ImageCropper 크롭 플로우 연동, 카메라 권한 거부/미지원 에러 처리, 다국어 지원 (camera namespace ko/en/ja 7개 키), 모든 사용처(글쓰기 5곳, 팀 3곳) 자동 활성화** | **2026-03-03** |
|
||||||
|
| **익명 로그인 제거 및 초대 링크 시스템 전환** | **Anonymous Auth 제거, 보안 레벨 시스템 제거 (초대 링크로 대체), get-custom-token API 제거, allowed-names API 제거, allowed-emails API 제거, security-level API 제거, merge-account API 제거, allowedNames/allowedEmails/securityLevel 필드 제거, SecurityLevelSelector 컴포넌트 제거, isAnonymous 참조 제거, Student 모델 제거, 모든 사용자 정식 계정 필수** | **2026-03-18** |
|
||||||
|
| **이메일 기반 대기 초대 시스템** | **이메일로 팀 초대 전송, 대기 초대 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** |
|
||||||
|
|
||||||
### 🚧 진행 중
|
### 🚧 진행 중
|
||||||
|
|
||||||
|
|||||||
76
SECURITY.md
76
SECURITY.md
@ -77,7 +77,6 @@ if (data.content) {
|
|||||||
|
|
||||||
### Firebase Authentication
|
### Firebase Authentication
|
||||||
- **제공자**: Email/Password, Google OAuth
|
- **제공자**: Email/Password, Google OAuth
|
||||||
- **익명 로그인**: 팀 코드 기반 (Level 1 보안)
|
|
||||||
- **정식 계정**: Google 계정 연동, 이메일/비밀번호
|
- **정식 계정**: Google 계정 연동, 이메일/비밀번호
|
||||||
|
|
||||||
### API 인증
|
### API 인증
|
||||||
@ -256,75 +255,22 @@ npm update
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. 5단계 보안 레벨 시스템
|
## 13. 초대 링크 기반 팀 참가 시스템
|
||||||
|
|
||||||
라온누리는 팀별로 선택 가능한 5가지 보안 레벨을 제공합니다.
|
라온누리는 Discord 스타일의 초대 링크 시스템으로 팀 참가를 관리합니다. 모든 사용자는 정식 계정(로그인)이 필수입니다.
|
||||||
|
|
||||||
### 보안 레벨 개요
|
### 핵심 원칙
|
||||||
|
|
||||||
| Level | Enum | 이름 | 익명 허용 | 가입 제한 | 주요 사용처 |
|
- **모든 사용자**: 정식 계정 로그인 필수 (Google/이메일)
|
||||||
|-------|------|------|-----------|-----------|------------|
|
- **팀 참가**: 초대 링크를 통해서만 가능
|
||||||
| **1** | `OPEN` | 완전 개방 | ✅ | 닉네임 공유 로그인 | 공개 워크샵, 체험 수업 |
|
- **닫힌 팀**: 초대 링크를 생성하지 않으면 자연스럽게 닫힌 팀
|
||||||
| **2** | `NAME_LIST` | 명단 기반 | ✅ | `allowedNames` 체크 | 저학년 반 (익명이지만 통제) |
|
- **접근 제한**: 특정 사용자에게만 초대 링크를 공유하여 제한
|
||||||
| **3** | `AUTH_REQUIRED` | 로그인 필수 | ❌ | 정식 계정 누구나 | 고학년 반 (구글 계정) ⭐ 추천 |
|
|
||||||
| **4** | `EMAIL_LIST` | 이메일 제한 | ❌ | `allowedEmails` 체크 | 특정 학생만 (전학생 차단) |
|
|
||||||
| **5** | `CLOSED` | 닫힌 팀 | ❌ | 신규 가입 차단 | 졸업반, 종료된 프로젝트 |
|
|
||||||
|
|
||||||
### 핵심 동작
|
### 초대 링크 워크플로우
|
||||||
|
|
||||||
**Level 1 (OPEN)**:
|
1. 팀 소유자가 초대 링크 생성 (만료 시간, 최대 사용 횟수 설정 가능)
|
||||||
- 닉네임 중복 시 Custom Token으로 재로그인 (익명 계정만)
|
2. 초대 링크를 대상 사용자에게 공유
|
||||||
- 정식 계정 탈취 방지 (Custom Token API에서 익명 체크)
|
3. 사용자가 링크를 클릭하여 로그인 후 팀에 참가
|
||||||
- **보안 조치**: Firebase Admin SDK로 `providerData` 검증
|
|
||||||
- ✅ 익명 계정 (`providerData.length === 0`): Custom Token 발급
|
|
||||||
- ❌ 정식 계정 (Google/Email): 403 에러 반환
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// POST /api/team/get-custom-token
|
|
||||||
const userRecord = await adminAuth.getUser(uid);
|
|
||||||
if (userRecord.providerData.length > 0) {
|
|
||||||
// 정식 계정 → 거부
|
|
||||||
return forbiddenResponse("해당 이름은 다른 사용자가 사용 중입니다.");
|
|
||||||
}
|
|
||||||
// 익명 계정 → Custom Token 발급
|
|
||||||
const customToken = await adminAuth.createCustomToken(uid);
|
|
||||||
return successResponse({ customToken });
|
|
||||||
```
|
|
||||||
|
|
||||||
**Level 2 (NAME_LIST)**:
|
|
||||||
- `team.allowedNames` 배열 체크
|
|
||||||
- 익명 로그인 허용하되, 등록된 이름만 입장 가능
|
|
||||||
- 저학년 반에 적합 (익명이지만 통제)
|
|
||||||
|
|
||||||
**Level 3 (AUTH_REQUIRED)**:
|
|
||||||
- `user.isAnonymous === false` 체크
|
|
||||||
- 정식 계정(구글/이메일) 필수
|
|
||||||
- 고학년 반 권장
|
|
||||||
|
|
||||||
**Level 4 (EMAIL_LIST)**:
|
|
||||||
- `team.allowedEmails` 배열 체크
|
|
||||||
- 특정 학생만 접근 가능 (이메일 화이트리스트)
|
|
||||||
|
|
||||||
**Level 5 (CLOSED)**:
|
|
||||||
- `uid in team.members` 체크만
|
|
||||||
- 신규 가입 차단, 기존 멤버만 접근
|
|
||||||
|
|
||||||
### 레벨 변경 규칙
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await teamManager.updateSecurityLevel(teamId, 4, true);
|
|
||||||
// autoPopulateList: true → 기존 멤버 이메일/이름 자동 등록
|
|
||||||
// Level 3→4: 기존 멤버 이메일 자동 등록
|
|
||||||
// Level 2→4: 기존 익명 멤버는 유지, 신규는 정식 계정만
|
|
||||||
```
|
|
||||||
|
|
||||||
**API 엔드포인트**:
|
|
||||||
- `POST /api/team/:teamId/security-level` - 보안 레벨 변경
|
|
||||||
- `POST /api/team/:teamId/allowed-names` - 이름 추가 (Level 2)
|
|
||||||
- `DELETE /api/team/:teamId/allowed-names` - 이름 제거 (Level 2)
|
|
||||||
- `POST /api/team/:teamId/allowed-emails` - 이메일 추가 (Level 4)
|
|
||||||
- `DELETE /api/team/:teamId/allowed-emails` - 이메일 제거 (Level 4)
|
|
||||||
- `POST /api/team/get-custom-token` - Custom Token 생성 (Level 1)
|
|
||||||
|
|
||||||
**참조**: `DATA_MODELS.md`, `API_SPEC.md`
|
**참조**: `DATA_MODELS.md`, `API_SPEC.md`
|
||||||
|
|
||||||
|
|||||||
345
TECH_STACK.md
345
TECH_STACK.md
@ -168,7 +168,7 @@ NEXT_PUBLIC_API_URL=/api
|
|||||||
|-------|------|----------|------|
|
|-------|------|----------|------|
|
||||||
| **이메일/비밀번호** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (학부모/고학년) |
|
| **이메일/비밀번호** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (학부모/고학년) |
|
||||||
| **Google OAuth** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (소셜 로그인) |
|
| **Google OAuth** | ✅ 활성화 | Firebase Console > Authentication | 정식 계정 (소셜 로그인) |
|
||||||
| **Anonymous** | ✅ 활성화 | Firebase Console > Authentication | **학생 팀 코드 로그인** |
|
| **Anonymous** | ❌ 삭제됨 | - | - |
|
||||||
| **네이버** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) |
|
| **네이버** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) |
|
||||||
| **카카오** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) |
|
| **카카오** | 🔜 준비 중 | - | 정식 계정 (소셜 로그인) |
|
||||||
|
|
||||||
@ -210,14 +210,13 @@ Manager Layer (비즈니스 로직 + 클라이언트 캐싱)
|
|||||||
│ ├─> getMyTeams() → GET /team/list (소유+참여 팀, 1분 캐싱)
|
│ ├─> getMyTeams() → GET /team/list (소유+참여 팀, 1분 캐싱)
|
||||||
│ ├─> updateTeam() → PUT /team/:id
|
│ ├─> updateTeam() → PUT /team/:id
|
||||||
│ ├─> deleteTeam() → DELETE /team/:id
|
│ ├─> deleteTeam() → DELETE /team/:id
|
||||||
│ └─> generateUniqueTeamCode() → POST /team/generate-code
|
│ └─> bulkCreateMembers() → POST /team/:id/bulk-create-members
|
||||||
│
|
│
|
||||||
├─> UserManager (싱글톤)
|
├─> UserManager (싱글톤)
|
||||||
│ ├─> createUser() → POST /user
|
│ ├─> createUser() → POST /user
|
||||||
│ ├─> getUser() → GET /user/:id (Firebase Auth + Firestore 자동 결합, 5분 캐싱)
|
│ ├─> getUser() → GET /user/:id (Firebase Auth + Firestore 자동 결합, 5분 캐싱)
|
||||||
│ ├─> getUsersByTeam() → GET /user/by-team/:teamId (30초 캐싱)
|
│ ├─> getUsersByTeam() → GET /user/by-team/:teamId (30초 캐싱)
|
||||||
│ ├─> updateLastLogin() → POST /user/:uid/update-last-login
|
│ ├─> updateLastLogin() → POST /user/:uid/update-last-login
|
||||||
│ ├─> findUserByNickname() → POST /user/find-by-nickname (Level 1용)
|
|
||||||
│ └─> setUserNickname() → POST /user/:uid/nickname (DEPRECATED - 팀에서 관리)
|
│ └─> setUserNickname() → POST /user/:uid/nickname (DEPRECATED - 팀에서 관리)
|
||||||
│
|
│
|
||||||
├─> WritingManager (싱글톤)
|
├─> WritingManager (싱글톤)
|
||||||
@ -285,49 +284,35 @@ 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: "춤추는파란사자",
|
||||||
securityLevel: 2, // 1-5 (명단 기반)
|
|
||||||
allowedNames: ["홍길동", "김철수"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 보안 레벨 변경
|
// 닉네임 조회
|
||||||
await teamManager.updateSecurityLevel(teamId, 4, true); // Level 4, 자동 명단 생성
|
|
||||||
|
|
||||||
// 🆕 닉네임 조회
|
|
||||||
const nickname = teamManager.getMemberNickname(team, uid, user?.name);
|
const nickname = teamManager.getMemberNickname(team, uid, user?.name);
|
||||||
```
|
```
|
||||||
|
|
||||||
**참고 문서**:
|
**참고 문서**:
|
||||||
- [API_SPEC.md](./API_SPEC.md) - 전체 API 명세서
|
- [API_SPEC.md](./API_SPEC.md) - 전체 API 명세서
|
||||||
|
|
||||||
### 2. 인증 플로우 및 라우팅 (currentStudent 중심 아키텍처)
|
### 2. 인증 플로우 및 라우팅
|
||||||
|
|
||||||
```
|
```
|
||||||
1. AuthInitializer (클라이언트)
|
1. AuthInitializer (클라이언트)
|
||||||
└─> initializeAuth() 호출 (마운트 시)
|
└─> initializeAuth() 호출 (마운트 시)
|
||||||
└─> onAuthStateChanged 리스너 등록
|
└─> onAuthStateChanged 리스너 등록
|
||||||
├─> firebaseUser.isAnonymous ?
|
└─> Firebase Auth 사용자 → authStore.user 설정
|
||||||
│ └─> getStudentByFirebaseUid() → authStore.currentStudent 설정
|
|
||||||
└─> else ?
|
|
||||||
└─> getStudentsByUserId() → authStore.ownedStudents 설정
|
|
||||||
|
|
||||||
2. authStore (Zustand) - 재설계됨
|
2. authStore (Zustand) - 재설계됨
|
||||||
├─> **currentStudent** (Student | null) - 현재 활동 중인 학생 (필수)
|
├─> user (User | null) - Firebase Auth + Firestore 결합 사용자 (정식 계정)
|
||||||
├─> user (User | null) - 정식 계정 (선택적)
|
├─> isAuthenticated - 로그인 여부
|
||||||
├─> ownedStudents (Student[]) - 정식 계정이 소유한 학생들
|
|
||||||
├─> isAnonymous (boolean) - Anonymous Auth 여부
|
|
||||||
├─> isAuthenticated - 정식 계정 로그인 여부
|
|
||||||
├─> isLoading
|
├─> isLoading
|
||||||
└─> 액션:
|
└─> 액션:
|
||||||
├─> login/signup/loginWithGoogle (기존)
|
├─> login/signup/loginWithGoogle (기존)
|
||||||
├─> **loginAsUser(teamCode, name)** - 팀 코드 로그인 (익명 계정 생성)
|
├─> **mergeWithGoogle()** - 기존 Google 계정과 병합 (API 데이터 마이그레이션)
|
||||||
├─> **linkWithEmail(email, password)** - 신규 이메일 계정 생성 (linkWithCredential)
|
└─> **login() loginId 지원** - @ 없는 입력 시 resolve-login-id API로 시스템 이메일 변환 후 로그인
|
||||||
├─> **linkWithGoogle()** - 신규 Google 계정 생성 (linkWithCredential)
|
|
||||||
├─> **mergeWithEmail(email, password)** - 기존 이메일 계정과 병합 (API 데이터 마이그레이션)
|
|
||||||
└─> **mergeWithGoogle()** - 기존 Google 계정과 병합 (API 데이터 마이그레이션)
|
|
||||||
|
|
||||||
3. 인증 기반 라우팅
|
3. 인증 기반 라우팅
|
||||||
├─> 랜딩 페이지 (/)
|
├─> 랜딩 페이지 (/)
|
||||||
@ -352,6 +337,13 @@ const nickname = teamManager.getMemberNickname(team, uid, user?.name);
|
|||||||
├─> 팀 페이지 공통 데이터 로딩 (team + members)
|
├─> 팀 페이지 공통 데이터 로딩 (team + members)
|
||||||
├─> requiredAccess: "member" | "owner" 권한 체크
|
├─> requiredAccess: "member" | "owner" 권한 체크
|
||||||
└─> 반환: team, members, isLoadingTeam, isLoadingMembers, isOwner, refreshMembers
|
└─> 반환: team, members, isLoadingTeam, isLoadingMembers, isOwner, refreshMembers
|
||||||
|
|
||||||
|
6. 이메일 기반 대기 초대 플로우
|
||||||
|
└─> 팀 소유자가 이메일로 초대 전송
|
||||||
|
└─> PendingInvite Firestore 문서 생성
|
||||||
|
└─> 초대 대상 로그인 시 대기 초대 조회 (/api/pending-invite/my)
|
||||||
|
└─> 수락/거절 (/api/pending-invite/[id]/respond)
|
||||||
|
└─> 수락 시 팀 멤버 자동 추가
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 글쓰기 및 저장 로직 (Manager 패턴 적용)
|
### 3. 글쓰기 및 저장 로직 (Manager 패턴 적용)
|
||||||
@ -673,152 +665,7 @@ const [selectedPartIndex, setSelectedPartIndex] = useState<number | null>(null);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. 팀 코드 시스템 아키텍처 (초등 저학년 로그인 간소화)
|
### 6. 실시간 피드백 시스템 (3-Layer 아키텍처) + 맞춤법 검사
|
||||||
|
|
||||||
#### 핵심 개념
|
|
||||||
|
|
||||||
**문제**: 초등 저학년은 이메일/비밀번호 로그인이 어려움
|
|
||||||
**해결**: 팀 소유자가 팀 코드를 발급하고, 학생은 간단히 로그인
|
|
||||||
**아키텍처**: currentStudent 중심, 정식 계정은 선택사항
|
|
||||||
|
|
||||||
#### 계정 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
정식 계정 (User) - 선택적
|
|
||||||
├─ Firebase Auth: user456 (Email/Google)
|
|
||||||
├─ Firestore: users/user456
|
|
||||||
└─ ownedStudentIds: ["studentDoc1", "studentDoc2"]
|
|
||||||
│
|
|
||||||
├─> 학생 계정 1 (Student) - 독립적, 필수
|
|
||||||
│ ├─ Firebase Auth: anon123 (Anonymous)
|
|
||||||
│ ├─ Firestore: students/studentDoc1
|
|
||||||
│ ├─ linkedUserId: user456 (1:1 관계)
|
|
||||||
│ ├─ teamIds: ["team1", "team2"]
|
|
||||||
│ └─ 모든 활동 데이터(writings, topics)는 studentId로 기록
|
|
||||||
│
|
|
||||||
└─> 학생 계정 2 (Student)
|
|
||||||
└─ ... (동일 구조)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 팀 코드 생성
|
|
||||||
|
|
||||||
**한글 3단어 조합**: `[형용사/동사] + [색상] + [동물]`
|
|
||||||
|
|
||||||
```
|
|
||||||
예시: "춤추는 파란 사자"
|
|
||||||
|
|
||||||
조합 수:
|
|
||||||
- 형용사/동사: 100개
|
|
||||||
- 색상: 20개
|
|
||||||
- 동물: 50개
|
|
||||||
→ 총 100,000가지 조합 (10만 개)
|
|
||||||
|
|
||||||
특징:
|
|
||||||
✅ 기억하기 쉬움 (이미지 연상)
|
|
||||||
✅ 구두 전달 가능 (말로 쉽게 전달)
|
|
||||||
✅ 타이핑 오타 최소화 (자동완성 가능)
|
|
||||||
✅ 초등 저학년도 이해 가능
|
|
||||||
✅ 재미있고 친근함 (팀 정체성 형성)
|
|
||||||
```
|
|
||||||
|
|
||||||
**파일**: `src/data/classCodeWords.ts`, `src/utils/classCodeGenerator.ts`
|
|
||||||
|
|
||||||
#### 학생 로그인 플로우 (개선됨 - 2025-11-06)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 학생 로그인 (팀 코드 3단계)
|
|
||||||
Step 1: 팀 코드 입력
|
|
||||||
├─> "춤추는 파란 사자" 입력
|
|
||||||
├─> Firestore teams 조회
|
|
||||||
├─> 소유자 체크 (본인 팀이면 /manage로 리다이렉트)
|
|
||||||
└─> Step 2로 진행
|
|
||||||
|
|
||||||
Step 2: 이름 입력 (선택 → 입력으로 개선)
|
|
||||||
├─> 이름 직접 입력 (예: "김민지")
|
|
||||||
├─> PIN 필요하면 Step 3 (PIN)
|
|
||||||
└─> PIN 불필요하면 Step 3 (완료 화면)
|
|
||||||
|
|
||||||
Step 3: PIN 입력 또는 완료 화면
|
|
||||||
├─> [PIN 필요] 숫자 패드로 PIN 입력 → 검증 → 완료 화면
|
|
||||||
└─> [완료 화면] 참여/로그인 구분
|
|
||||||
├─> 신규: 🎉 "환영합니다! {이름}님, {팀명}에 참여했어요"
|
|
||||||
└─> 재로그인: 👋 "반가워요! {이름}님, 다시 돌아왔군요!"
|
|
||||||
|
|
||||||
2. 로그인 완료
|
|
||||||
├─> Anonymous Auth 유지/생성
|
|
||||||
├─> authStore.currentStudent 설정
|
|
||||||
└─> /team/[teamId] 멤버 페이지로 이동
|
|
||||||
|
|
||||||
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으로 로그인
|
|
||||||
├─> Firestore users/user456 조회
|
|
||||||
├─> ownedStudentIds로 students 조회
|
|
||||||
├─> 학생이 1명: 자동 선택
|
|
||||||
├─> 학생이 2명+: StudentPicker 표시 (누구로 활동할까요?)
|
|
||||||
└─> authStore.currentStudent 설정
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 보안 모드
|
|
||||||
|
|
||||||
| 모드 | 인증 단계 | 사용 사례 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| **simple** | 팀 코드 + 이름 | 교실 전용, 저학년 (1-2학년) |
|
|
||||||
| **normal** | 팀 코드 + 이름 + PIN | 가정 학습 포함, 고학년 (3-4학년) |
|
|
||||||
| **open** | 팀 코드 + 자유 가입 | 전학생, 체험 학생 허용 |
|
|
||||||
|
|
||||||
#### Rate Limiting (학생 친화적)
|
|
||||||
|
|
||||||
```
|
|
||||||
5회 실패: 💡 "어려우면 팀을 만든 사람에게 물어보세요!"
|
|
||||||
10회 실패: ⚠️ "입력을 확인해주세요. 띄어쓰기는 안 해도 괜찮아요!"
|
|
||||||
15회 실패: 🔒 "2분 후에 다시 시도해주세요. 팀 관리자에게 도움을 요청하세요."
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 데이터 흐름
|
|
||||||
|
|
||||||
```
|
|
||||||
모든 활동 데이터는 studentId로 기록:
|
|
||||||
|
|
||||||
writings/{writingId}
|
|
||||||
├─ studentId: "studentDoc1" ← 핵심! (userId 아님)
|
|
||||||
├─ title: "나의 하루"
|
|
||||||
└─ content: "..."
|
|
||||||
|
|
||||||
조회 시:
|
|
||||||
- 팀 코드 계정: getUserWritings(currentStudent.id)
|
|
||||||
- 정식 계정: ownedStudents.map(s => getUserWritings(s.id))
|
|
||||||
```
|
|
||||||
|
|
||||||
**참고 파일**:
|
|
||||||
- `src/managers/TeamManager.ts` - 팀 관련 API 호출 + 캐싱
|
|
||||||
- `src/managers/ManagerBase.ts` - API 호출 및 캐싱 공통 로직
|
|
||||||
- `src/services/firebaseAuth.ts:125-316` - 학생 로그인 로직
|
|
||||||
- `src/store/authStore.ts` - currentStudent 중심 상태 관리
|
|
||||||
- `src/types/api/team.ts` - 팀 API 타입 정의
|
|
||||||
- `src/types/api/student.ts` - 학생 API 타입 정의
|
|
||||||
- `API_SPEC.md` - 전체 API 명세서 (23개 엔드포인트)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 실시간 피드백 시스템 (3-Layer 아키텍처) + 맞춤법 검사
|
|
||||||
|
|
||||||
#### 핵심 개념
|
#### 핵심 개념
|
||||||
|
|
||||||
@ -1224,7 +1071,6 @@ Middleware (src/middleware.ts)
|
|||||||
"landing": {
|
"landing": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"cta": "지금 시작하기",
|
"cta": "지금 시작하기",
|
||||||
"teamCode": "팀 코드로 참여"
|
|
||||||
},
|
},
|
||||||
"features": {...},
|
"features": {...},
|
||||||
"howItWorks": {...}
|
"howItWorks": {...}
|
||||||
@ -1388,7 +1234,7 @@ return (
|
|||||||
| **Landing** | `/[locale]` | ✅ 완료 | Hero(사이트명, 태그라인, CTA 버튼), Features(4개 카드), Steps(3단계), CTA 섹션, Footer (총 20+ 항목) |
|
| **Landing** | `/[locale]` | ✅ 완료 | Hero(사이트명, 태그라인, CTA 버튼), Features(4개 카드), Steps(3단계), CTA 섹션, Footer (총 20+ 항목) |
|
||||||
| **Home** | `/[locale]/home` | ✅ 완료 | 웰컴 메시지, QuickStart(9개 액션 카드), RecentActivity (총 15+ 항목) |
|
| **Home** | `/[locale]/home` | ✅ 완료 | 웰컴 메시지, QuickStart(9개 액션 카드), RecentActivity (총 15+ 항목) |
|
||||||
| **Auth** | LoginDialog 등 | ✅ 완료 | LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog (총 50+ 항목) |
|
| **Auth** | LoginDialog 등 | ✅ 완료 | LoginDialog, LoginForm, SignupForm, UserProfileButton, StudentLoginFlow, SavedDraftsDialog (총 50+ 항목) |
|
||||||
| **Team** | `/[locale]/team/*` | ✅ 완료 | List, Create, Detail, Manage + SecurityLevelSelector (총 60+ 항목) |
|
| **Team** | `/[locale]/team/*` | ✅ 완료 | List, Create, Detail, Manage (총 60+ 항목) |
|
||||||
| **Write** | `/[locale]/write` | ✅ 완료 | 분석/저장 메시지, 버튼, 상태 표시 (총 20+ 항목) |
|
| **Write** | `/[locale]/write` | ✅ 완료 | 분석/저장 메시지, 버튼, 상태 표시 (총 20+ 항목) |
|
||||||
|
|
||||||
**총 번역 키**: 220개 이상 (ko.json, en.json)
|
**총 번역 키**: 220개 이상 (ko.json, en.json)
|
||||||
@ -2041,7 +1887,7 @@ aiUsage/{identifier}_{yearMonth}
|
|||||||
- User가 organizationId를 가지면 조직 플랜 자동 적용
|
- User가 organizationId를 가지면 조직 플랜 자동 적용
|
||||||
- 조직 플랜이 개인 플랜보다 우선
|
- 조직 플랜이 개인 플랜보다 우선
|
||||||
|
|
||||||
2. **익명 사용자 지원** 👤:
|
2. **비로그인 사용자 지원** 👤:
|
||||||
- IP 주소 해싱 (SHA-256, 16자)
|
- IP 주소 해싱 (SHA-256, 16자)
|
||||||
- Free 플랜 제한 자동 적용
|
- Free 플랜 제한 자동 적용
|
||||||
- 세션 간 사용량 추적
|
- 세션 간 사용량 추적
|
||||||
@ -2253,93 +2099,7 @@ if (data.aiEnabled !== undefined && data.aiEnabled !== team.aiEnabled) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 23. 공개 팀 코드 보안 강화
|
### 23. AI 크레딧 환불 시스템
|
||||||
|
|
||||||
#### 핵심 개념
|
|
||||||
|
|
||||||
**목적**: 공개 팀에서 팀 코드를 비멤버에게 노출하지 않아 무단 가입 방지
|
|
||||||
|
|
||||||
#### 보안 정책
|
|
||||||
|
|
||||||
```
|
|
||||||
멤버인 경우 → 팀 코드 표시 ✅
|
|
||||||
멤버 아닌 경우 → 팀 코드 숨김 🔒
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 구현 레이어
|
|
||||||
|
|
||||||
**1. 타입 레벨** (`src/types/team.ts`):
|
|
||||||
```typescript
|
|
||||||
interface Team {
|
|
||||||
code?: string; // optional로 변경 (공개 팀에서 제거 가능)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. 서버 API 레벨**:
|
|
||||||
|
|
||||||
a) **공개 팀 목록** (`src/lib/server/team.ts - getPublicTeams`):
|
|
||||||
```typescript
|
|
||||||
const teams = querySnapshot.docs.slice(0, limit).map((doc) => {
|
|
||||||
const data = doc.data();
|
|
||||||
const {code, ...teamDataWithoutCode} = data; // 🔒 코드 제거
|
|
||||||
return { id: doc.id, ...teamDataWithoutCode };
|
|
||||||
}) as Team[];
|
|
||||||
```
|
|
||||||
|
|
||||||
b) **개별 공개 팀 조회** (`src/app/api/team/[teamId]/public/route.ts`):
|
|
||||||
```typescript
|
|
||||||
const isMember = uid && uid in team.members;
|
|
||||||
const teamData = isMember ? team : (() => {
|
|
||||||
const {code, ...teamWithoutCode} = team; // 🔒 멤버 아니면 제거
|
|
||||||
return teamWithoutCode as typeof team;
|
|
||||||
})();
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. UI 컴포넌트 레벨** (`src/components/team/TeamCard.tsx`):
|
|
||||||
```tsx
|
|
||||||
{/* 팀 코드 (코드가 있을 때만 표시) */}
|
|
||||||
{team.code && (
|
|
||||||
<Box w="full" p={2} bg="bg.muted" borderRadius="md" textAlign="center">
|
|
||||||
<Text fontSize="sm" fontWeight="bold" color="brand.fg">
|
|
||||||
{team.code}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 타입 안전성 처리
|
|
||||||
|
|
||||||
**필수 코드 사용처**:
|
|
||||||
- `StudentLoginFlow.tsx`: 팀 코드 null 체크
|
|
||||||
- `TeamInfoCard.tsx`: 복사 함수 null 체크
|
|
||||||
- `firebaseAuth.ts`: Non-null assertion (`team.code!`)
|
|
||||||
|
|
||||||
#### 주요 특징
|
|
||||||
|
|
||||||
1. **계층적 보안** 🛡️:
|
|
||||||
- 서버에서 데이터 제거 (클라이언트 우회 불가)
|
|
||||||
- 타입 시스템으로 안전성 보장
|
|
||||||
- UI에서 조건부 렌더링
|
|
||||||
|
|
||||||
2. **멤버 우대** 👥:
|
|
||||||
- 팀 멤버는 모든 정보 접근 가능
|
|
||||||
- 공개 팀 페이지에서도 코드 확인
|
|
||||||
|
|
||||||
3. **사용자 경험** ✨:
|
|
||||||
- 코드 없어도 UI 깨지지 않음
|
|
||||||
- 내 팀 페이지는 항상 코드 표시
|
|
||||||
|
|
||||||
#### 참고 파일
|
|
||||||
|
|
||||||
**타입**: `src/types/team.ts` (Team.code?: string)
|
|
||||||
**서버**:
|
|
||||||
- `src/lib/server/team.ts` - getPublicTeams()
|
|
||||||
- `src/app/api/team/[teamId]/public/route.ts` - GET
|
|
||||||
**컴포넌트**: `src/components/team/TeamCard.tsx`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 24. AI 크레딧 환불 시스템
|
|
||||||
|
|
||||||
#### 핵심 개념
|
#### 핵심 개념
|
||||||
|
|
||||||
@ -2804,6 +2564,69 @@ Navbar (z-index: 1000)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 26. 학생 일괄 생성 시스템 (Bulk Student Creation + loginId)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 팀 소유자가 BulkMemberCreator UI에서 CSV 입력
|
||||||
|
└─> 학생 이름 목록 (줄바꿈 구분)
|
||||||
|
└─> ID/PW 템플릿 설정 (예: {name}{number} / {name}{number}!)
|
||||||
|
|
||||||
|
2. 4단계 UI 플로우
|
||||||
|
├─> Step 1: 입력 (CSV 텍스트 + 템플릿 설정)
|
||||||
|
├─> Step 2: 미리보기 (생성될 계정 목록 확인)
|
||||||
|
├─> Step 3: 생성 중 (Progress bar)
|
||||||
|
└─> Step 4: 결과 (성공/실패 목록 + 로그인 카드 인쇄)
|
||||||
|
|
||||||
|
3. API 플로우 (POST /api/team/[teamId]/bulk-create-members)
|
||||||
|
└─> 소유자 권한 검증
|
||||||
|
└─> 서버 레이어 (bulk-member.ts)
|
||||||
|
├─> 템플릿 처리: {number}, {name} 치환
|
||||||
|
├─> loginId 생성: 고유 loginId → 시스템 이메일 ({loginId}@raonnuri.system)
|
||||||
|
├─> Firebase Auth 계정 생성 (createUser)
|
||||||
|
├─> Firestore User 문서 생성 (loginId, isSystemAccount, createdByTeacher 필드)
|
||||||
|
└─> 팀 멤버 자동 추가
|
||||||
|
|
||||||
|
4. loginId 로그인 플로우
|
||||||
|
└─> 학생이 loginId (@ 없는 ID) 입력
|
||||||
|
└─> authStore.login()에서 @ 여부 감지
|
||||||
|
└─> POST /api/auth/resolve-login-id
|
||||||
|
└─> loginId → 시스템 이메일 변환
|
||||||
|
└─> Firebase Auth signInWithEmailAndPassword(시스템 이메일, 비밀번호)
|
||||||
|
|
||||||
|
5. 로그인 카드 인쇄
|
||||||
|
└─> 결과 화면에서 "인쇄" 버튼
|
||||||
|
└─> 카드별 학생 이름, loginId, 비밀번호 표시
|
||||||
|
└─> window.print() (인쇄 전용 CSS)
|
||||||
|
```
|
||||||
|
|
||||||
|
**주요 타입**:
|
||||||
|
```typescript
|
||||||
|
// src/types/bulkMember.ts
|
||||||
|
interface StudentCSVRow {
|
||||||
|
name: string;
|
||||||
|
// CSV에서 파싱된 학생 이름
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratedCredential {
|
||||||
|
name: string;
|
||||||
|
loginId: string;
|
||||||
|
password: string;
|
||||||
|
email: string; // 시스템 이메일 ({loginId}@raonnuri.system)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BulkCreateResult {
|
||||||
|
success: GeneratedCredential[];
|
||||||
|
failed: { name: string; error: string }[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**FirestoreUser 확장 필드**:
|
||||||
|
- `loginId` - 학생용 간편 로그인 ID (@ 없는 고유 ID)
|
||||||
|
- `isSystemAccount` - 교사가 일괄 생성한 시스템 계정 여부
|
||||||
|
- `createdByTeacher` - 생성한 교사의 UID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 참고 문서
|
## 참고 문서
|
||||||
|
|
||||||
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조
|
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 프로젝트 구조
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user