2025-11-10 02:18:27 +00:00

26 KiB

API Specification

라온누리 서버 API 명세서

⚠️ 최신 변경사항 (2025-11-10)

🔐 5단계 보안 레벨 시스템

팀 생성 시 5가지 보안 레벨 선택 가능:

  • Level 1 (OPEN): 완전 개방, 닉네임 공유 로그인
  • Level 2 (NAME_LIST): 명단 기반, allowedNames 체크
  • Level 3 (AUTH_REQUIRED): 로그인 필수, 정식 계정 누구나
  • Level 4 (EMAIL_LIST): 이메일 화이트리스트, allowedEmails 체크
  • Level 5 (CLOSED): 닫힌 팀, 신규 가입 불가

📦 User 타입 분리

  • FirestoreUser: DB 저장용 (최소 데이터만)
  • User: UI 사용용 (Firebase Auth + Firestore 결합)
  • 이름, 이메일, 사진 등은 Firebase Auth가 Single Source of Truth

🏷️ 닉네임 저장 위치 변경

  • 기존: users.nicknames[teamId]
  • 신규: team.members[uid].nickname

🎯 RESTful API 설계 규칙

HTTP Method로 동작 구분 (경로가 아님):

✅ POST   /api/resource  → 생성/추가
✅ DELETE /api/resource  → 삭제/제거
✅ PUT    /api/resource  → 전체 수정
✅ GET    /api/resource  → 조회

❌ POST /api/resource/add     → 사용 금지
❌ POST /api/resource/remove  → 사용 금지

개요

  • Base URL: /api (환경변수 NEXT_PUBLIC_API_URL로 설정 가능, 기본값 /api)
  • 엔드포인트: /team, /user 등 (Base URL에 /api 포함됨)
  • 실제 호출 URL: {BASE_URL}{endpoint}/api/team, /api/user
  • 인증: Firebase ID Token을 Authorization: Bearer {token} 헤더로 전달
  • 응답 형식: 모든 API는 ApiResponse<T> 형식 반환
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: ApiError;
}

interface ApiError {
  code: string;
  message: string;
  details?: any;
}

Team API

1. POST /team - 팀 생성

실제 URL: POST /api/team

인증: 필수 (정식 계정)

Request:

{
  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:

{
  success: true,
  data: {
    teamId: string;
    team: Team;           // 생성된 팀 객체
  }
}

권한: 현재 로그인한 사용자가 자동으로 팀 소유자가 됨


2. GET /team/:id - 팀 조회

실제 URL: GET /api/team/:id

인증: 선택적 (공개 팀은 인증 없이 조회 가능)

Response:

{
  success: true,
  data: {
    team: Team;
  }
}

캐싱: 클라이언트에서 5분간 캐싱


3. POST /team/search - 팀 코드로 조회

실제 URL: POST /api/team/search

인증: 선택적

Request:

{
  code: string;           // 팀 코드 (정규화됨)
}

Response:

{
  success: true,
  data: {
    team: Team | null;    // 팀이 없으면 null
  }
}

캐싱: 클라이언트에서 1분간 캐싱


4. GET /team/list - 내 팀 목록

실제 URL: GET /api/team/list

인증: 필수 (정식 계정)

Response:

{
  success: true,
  data: {
    teams: Team[];
  }
}

권한: 로그인한 사용자가 소유한 팀 + 참여한 팀 모두 조회 (중복 제거)

백엔드 로직:

  1. getTeamsByOwner(uid) - 소유한 팀 조회
  2. getTeamsByMember(uid) - 참여한 팀 조회 (memberUids array-contains)
  3. 두 결과를 병합하고 중복 제거하여 반환

캐싱: 클라이언트에서 1분간 캐싱


5. PUT /team/:id - 팀 정보 수정

실제 URL: PUT /api/team/:id

인증: 필수

Request:

{
  teamId: string;
  data: {
    name?: string;
    securityMode?: "simple" | "normal" | "open";
    requirePin?: boolean;
    allowAnonymousJoin?: boolean;
    isActive?: boolean;
  }
}

Response:

{
  success: true,
  data: {
    team: Team;
  }
}

권한: 팀 소유자만 수정 가능

캐시 무효화: 해당 팀, 팀 목록


6. DELETE /team/:id - 팀 삭제 (Soft Delete)

실제 URL: DELETE /api/team/:id

인증: 필수

Request:

{
  teamId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 팀 소유자만 삭제 가능

캐시 무효화: 해당 팀, 팀 목록


7. POST /team/add-student - 팀에 학생 추가

실제 URL: POST /api/team/add-student

인증: 필수 (내부 사용 - StudentManager에서 호출)

Request:

{
  teamId: string;
  studentId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

캐시 무효화: 해당 팀


8. POST /team/remove-student - 팀에서 학생 제거

실제 URL: POST /api/team/remove-student

인증: 필수

Request:

{
  teamId: string;
  studentId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 팀 소유자만 제거 가능 (서버에서 검증)

캐시 무효화: 해당 팀


9. POST /team/generate-code - 고유 팀 코드 생성

실제 URL: POST /api/team/generate-code

인증: 선택적

Request:

{
  maxAttempts?: number;   // 기본값: 10
}

Response:

{
  success: true,
  data: {
    code: string;         // 생성된 고유 팀 코드
  }
}

10. POST /team/check-code - 팀 코드 존재 여부

실제 URL: POST /api/team/check-code

인증: 선택적

Request:

{
  code: string;
}

Response:

{
  success: true,
  data: {
    exists: boolean;
  }
}

🆕 11. POST /team/:teamId/security-level - 보안 레벨 변경

실제 URL: POST /api/team/:teamId/security-level

인증: 필수 (팀 소유자만)

Request:

{
  securityLevel: 1 | 2 | 3 | 4 | 5;
  autoPopulateList?: boolean;  // 기본값: true
}

Response:

{
  success: true,
  data: {
    team: Team;  // 업데이트된 팀
  }
}

autoPopulateList 동작:

  • true: 기존 멤버를 자동으로 명단에 추가
    • Level 2로 변경 → 기존 멤버 이름을 allowedNames에 추가
    • Level 4로 변경 → 기존 정식 계정 이메일을 allowedEmails에 추가
  • false: 명단을 자동 생성하지 않음 (수동 관리)

캐시 무효화: 해당 팀, 팀 목록


🆕 12. POST/DELETE /team/:teamId/allowed-names - 허용 이름 관리 (Level 2)

실제 URL:

  • POST /api/team/:teamId/allowed-names - 이름 추가
  • DELETE /api/team/:teamId/allowed-names - 이름 제거

인증: 필수 (팀 소유자만)

Request (POST):

{
  name: string;  // 추가할 이름
}

Request (DELETE):

{
  name: string;  // 제거할 이름
}

Response:

{
  success: true,
  data: {
    allowedNames: string[];  // 업데이트된 명단
  }
}

참고: RESTful 원칙에 따라 HTTP Method로 동작 구분

캐시 무효화: 해당 팀


🆕 13. POST/DELETE /team/:teamId/allowed-emails - 허용 이메일 관리 (Level 4)

실제 URL:

  • POST /api/team/:teamId/allowed-emails - 이메일 추가
  • DELETE /api/team/:teamId/allowed-emails - 이메일 제거

인증: 필수 (팀 소유자만)

Request (POST):

{
  email: string;  // 추가할 이메일 (소문자로 자동 변환)
}

Request (DELETE):

{
  email: string;  // 제거할 이메일
}

Response:

{
  success: true,
  data: {
    allowedEmails: string[];  // 업데이트된 이메일 목록
  }
}

유효성 검사: 이메일 형식 검증 (/^[^\s@]+@[^\s@]+\.[^\s@]+$/)

캐시 무효화: 해당 팀


User API

중요: User vs FirestoreUser 구분

  • FirestoreUser: DB 저장용 (uid, createdAt, lastLoginAt, settings만)
  • User: API 응답/UI용 (Firebase Auth + FirestoreUser 결합)

1. POST /user - 사용자 생성

실제 URL: POST /api/user

인증: 필수

Request:

{
  uid: string;           // Firebase Auth UID
  displayName: string;   // Firebase Auth displayName 설정용
  teamId: string;        // 최초 가입 팀
  isAnonymous: boolean;  // 익명 여부
}

Response:

{
  success: true,
  data: {
    user: User;          // Firebase Auth + Firestore 결합된 완전한 User
  }
}

부수 효과:

  • Firestore에 FirestoreUser 생성 (최소 데이터)
  • 팀에 멤버 추가 (team.members[uid])
  • Firebase Auth displayName 설정

캐시 무효화: 팀별 사용자 목록


2. GET /student/:id - 학생 조회

실제 URL: GET /api/student/:id

인증: 선택적

Response:

{
  success: true,
  data: {
    student: Student;
  }
}

캐싱: 클라이언트에서 5분간 캐싱


3. POST /student/by-uid - Firebase UID로 학생 조회

실제 URL: POST /api/student/by-uid

인증: 필수 (Anonymous Auth)

Request:

{
  firebaseUid: string;
}

Response:

{
  success: true,
  data: {
    student: Student | null;
  }
}

용도: 로그인 시 기존 학생 확인

캐싱: 클라이언트에서 5분간 캐싱


4. POST /student/by-team - 팀별 학생 목록

실제 URL: POST /api/student/by-team

인증: 선택적

Request:

{
  teamId: string;
}

Response:

{
  success: true,
  data: {
    students: Student[];
  }
}

캐싱: 클라이언트에서 30초간 캐싱 (자주 변경)


5. POST /student/find-by-name - 이름으로 학생 찾기

실제 URL: POST /api/student/find-by-name

인증: 선택적

Request:

{
  teamId: string;
  name: string;
}

Response:

{
  success: true,
  data: {
    student: Student | null;
  }
}

용도: 학생 로그인 시 기존 학생 확인


6. PUT /student/:id - 학생 정보 수정

실제 URL: PUT /api/student/:id

인증: 필수

Request:

{
  studentId: string;
  data: {
    name?: string;
    pinHash?: string;
    linkedUserId?: string;
  }
}

Response:

{
  success: true,
  data: {
    student: Student;
  }
}

캐시 무효화: 해당 학생, 팀별 학생 목록


7. POST /student/kick - 팀에서 학생 강퇴

실제 URL: POST /api/student/kick

인증: 필수

Request:

{
  studentId: string;
  teamId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 팀 소유자만 강퇴 가능

부수 효과:

  • student.teamIds에서 팀 제거
  • team.studentIds에서 학생 제거

캐시 무효화: 해당 학생, 팀, 팀별 학생 목록


8. POST /student/update-pin - PIN 변경

실제 URL: POST /api/student/update-pin

인증: 필수

Request:

{
  studentId: string;
  newPin: string;         // 평문 (4자리 숫자)
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

검증: 서버에서 PIN 형식 검증 (4자리 숫자)

보안: PIN은 SHA-256 해시로 저장

캐시 무효화: 해당 학생


9. POST /student/validate-pin - PIN 검증

실제 URL: POST /api/student/validate-pin

인증: 선택적

Request:

{
  studentId: string;
  pin: string;
}

Response:

{
  success: true,
  data: {
    valid: boolean;
  }
}

용도: 학생 로그인 시 PIN 확인


10. POST /student/link - 정식 계정 연결

실제 URL: POST /api/student/link

인증: 필수 (정식 계정)

Request:

{
  studentId: string;
  // userId는 Authorization 헤더에서 추출
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 현재 로그인한 사용자와 연결

검증:

  • 학생이 이미 다른 계정과 연결되어 있으면 에러

캐시 무효화: 해당 학생, 내 학생 목록


11. POST /student/unlink - 정식 계정 연결 해제

실제 URL: POST /api/student/unlink

인증: 필수

Request:

{
  studentId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

캐시 무효화: 해당 학생, 내 학생 목록


12. GET /student/my-students - 내 학생 목록

실제 URL: GET /api/student/my-students

인증: 필수 (정식 계정)

Response:

{
  success: true,
  data: {
    students: Student[];
  }
}

권한: 로그인한 사용자가 소유한 학생만 조회 (linkedUserId 기준)

캐싱: 클라이언트에서 1분간 캐싱


13. POST /student/update-last-login - 마지막 로그인 시간 업데이트

실제 URL: POST /api/student/update-last-login

인증: 선택적

Request:

{
  studentId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

참고: 실패해도 클라이언트는 에러를 무시 (크리티컬하지 않음)


Writing API

1. POST /writing - 글 생성

실제 URL: POST /api/writing

인증: 필수

Request:

{
  title: string;
  content: string;
  status?: "draft" | "published";
  topicId?: string | null;
}

Response:

{
  success: true,
  data: {
    writingId: string;
    writing: Writing;
  }
}

부수 효과: 서버에서 wordCount, charCount 자동 계산

캐시 무효화: 사용자 글 목록, 최근 글


2. GET /writing/:id - 글 조회

실제 URL: GET /api/writing/:id

인증: 선택적

Response:

{
  success: true,
  data: {
    writing: Writing | null;
  }
}

캐싱: 클라이언트에서 5분간 캐싱


3. POST /writing/user - 사용자의 글 목록

실제 URL: POST /api/writing/user

인증: 필수

Request:

{
  userId?: string;  // 없으면 현재 사용자
}

Response:

{
  success: true,
  data: {
    writings: Writing[];
  }
}

캐싱: 클라이언트에서 1분간 캐싱


4. POST /writing/recent - 최근 글

실제 URL: POST /api/writing/recent

인증: 필수

Request:

{
  limit?: number;  // 기본값: 5
}

Response:

{
  success: true,
  data: {
    writings: Writing[];
  }
}

캐싱: 클라이언트에서 30초간 캐싱


5. PUT /writing/:id - 글 수정

실제 URL: PUT /api/writing/:id

인증: 필수

Request:

{
  writingId: string;
  data: {
    title?: string;
    content?: string;
    status?: "draft" | "published";
    topicId?: string | null;
    wordCount?: number;
    charCount?: number;
  }
}

Response:

{
  success: true,
  data: {
    writing: Writing;
  }
}

권한: 작성자만 수정 가능

캐시 무효화: 해당 글, 사용자 글 목록, 최근 글


6. DELETE /writing/:id - 글 삭제

실제 URL: DELETE /api/writing/:id

인증: 필수

Request:

{
  writingId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 작성자만 삭제 가능

캐시 무효화: 해당 글, 사용자 글 목록, 최근 글


Topic API

1. POST /topic/available - 사용 가능한 주제 목록

실제 URL: POST /api/topic/available

인증: 필수

Request:

{
  teamIds?: string[];    // 팀 주제를 가져올 팀 ID 목록
}

Response:

{
  success: true,
  data: {
    topics: Topic[];
  }
}

백엔드 로직:

  1. 개인 주제: ownerType === PERSONAL && ownerId === currentUserId
  2. 팀 주제: ownerType === TEAM && ownerId in [teamId1, teamId2, ...]
    • 클라이언트에서 teamIds: ["team1", "team2"] 전달
    • 서버에서 필터링
  3. 모든 주제를 병합하여 반환

캐싱: 클라이언트에서 5분간 캐싱

참고: 그룹(Group) 기능은 제거되었습니다. 팀(Team) 기능만 사용합니다.


2. GET /topic/:id - 주제 조회

실제 URL: GET /api/topic/:id

인증: 선택적

Response:

{
  success: true,
  data: {
    topic: Topic | null;
  }
}

캐싱: 클라이언트에서 5분간 캐싱


3. POST /topic - 개인 주제 생성

실제 URL: POST /api/topic

인증: 필수

Request:

{
  title: string;
  description: string;
  category: "daily" | "imagination" | "emotion" | "experience";
  difficulty: "easy" | "medium" | "hard";
  keywords?: string[];
  examplePrompts?: string[];
  titleTemplate?: string;
  contentTemplate?: string;
}

Response:

{
  success: true,
  data: {
    topicId: string;
    topic: Topic;
  }
}

권한: 로그인한 사용자가 자동으로 소유자가 됨

캐시 무효화: 주제 목록


4. PUT /topic/:id - 개인 주제 수정

실제 URL: PUT /api/topic/:id

인증: 필수

Request:

{
  topicId: string;
  data: {
    title?: string;
    description?: string;
    category?: "daily" | "imagination" | "emotion" | "experience";
    difficulty?: "easy" | "medium" | "hard";
    keywords?: string[];
    examplePrompts?: string[];
    titleTemplate?: string;
    contentTemplate?: string;
    isActive?: boolean;
  }
}

Response:

{
  success: true,
  data: {
    topic: Topic;
  }
}

권한: 주제 소유자만 수정 가능

캐시 무효화: 해당 주제, 주제 목록


5. DELETE /topic/:id - 개인 주제 삭제

실제 URL: DELETE /api/topic/:id

인증: 필수

Request:

{
  topicId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한: 주제 소유자만 삭제 가능

캐시 무효화: 해당 주제, 주제 목록


6. POST /topic/increment-usage - 주제 사용 횟수 증가

실제 URL: POST /api/topic/increment-usage

인증: 선택적

Request:

{
  topicId: string;
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

참고: 실패해도 클라이언트는 에러를 무시 (크리티컬하지 않음)


7. POST /topic/team - 팀 주제 생성

실제 URL: POST /api/topic/team

인증: 필수 (팀 소유자만)

Request:

{
  teamId: string;      // 팀 ID (ownerId = teamId)
  title: string;
  description: string;
  category: "daily" | "imagination" | "emotion" | "experience";
  difficulty: "easy" | "medium" | "hard";
  keywords?: string[];
  examplePrompts?: string[];
  titleTemplate?: string;
  contentTemplate?: string;
}

Response:

{
  success: true,
  data: {
    topicId: string;
    topic: Topic;       // ownerType: TEAM, ownerId: teamId
  }
}

권한:

  • 로그인한 사용자가 팀 소유자인지 확인 (team.ownerId === currentUserId)
  • 팀이 활성화 상태인지 확인 (team.isActive === true)

백엔드 처리:


const topic = {
  ...requestData,
  ownerType: TopicOwnerType.TEAM,
  ownerId: teamId, 
  createdBy: currentUserId,
  usageCount: 0,
  isActive: true,
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp(),
};

캐시 무효화: 주제 목록, 팀 주제 목록


8. GET /topic/team/:teamId - 팀 주제 목록 조회

실제 URL: GET /api/topic/team/:teamId

인증: 선택적 (공개 조회 가능)

Response:

{
  success: true,
  data: {
    topics: Topic[];    // ownerType === TEAM && ownerId === teamId
  }
}

백엔드 로직:


const topics = await db.collection('topics')
	.where('ownerType', '==', TopicOwnerType.TEAM)
	.where('ownerId', '==', teamId)
	.where('isActive', '==', true)
	.orderBy('createdAt', 'desc')
	.get();

캐싱: 클라이언트에서 5분간 캐싱


9. PUT /topic/team/:id - 팀 주제 수정

실제 URL: PUT /api/topic/team/:id

인증: 필수 (팀 소유자만)

Request:

{
  topicId: string;
  teamId: string;      // 소유권 검증용
  data: {
    title?: string;
    description?: string;
    category?: "daily" | "imagination" | "emotion" | "experience";
    difficulty?: "easy" | "medium" | "hard";
    keywords?: string[];
    examplePrompts?: string[];
    titleTemplate?: string;
    contentTemplate?: string;
    isActive?: boolean;
  }
}

Response:

{
  success: true,
  data: {
    topic: Topic;
  }
}

권한 검증:

  1. 주제가 팀 주제인지 확인: topic.ownerType === TEAM && isTeamOwnerId(topic.ownerId)
  2. 요청한 teamId와 주제의 teamId 일치 확인: extractTeamId(topic.ownerId) === teamId
  3. 현재 사용자가 팀 소유자인지 확인: team.ownerId === currentUserId

캐시 무효화: 해당 주제, 주제 목록, 팀 주제 목록


10. DELETE /topic/team/:id - 팀 주제 삭제

실제 URL: DELETE /api/topic/team/:id

인증: 필수 (팀 소유자만)

Request:

{
  topicId: string;
  teamId: string;      // 소유권 검증용
}

Response:

{
  success: true,
  data: {
    success: true;
  }
}

권한 검증: PUT과 동일

캐시 무효화: 해당 주제, 주제 목록, 팀 주제 목록

참고: Soft delete 방식 (isActive: false로 설정)


에러 코드

코드 설명
UNAUTHORIZED 인증 필요
FORBIDDEN 권한 없음
NOT_FOUND 리소스 없음
VALIDATION_ERROR 유효성 검사 실패
TEAM_CODE_INVALID 팀 코드 형식 오류
TEAM_INACTIVE 비활성화된 팀
PIN_REQUIRED PIN 입력 필요
PIN_INVALID PIN 불일치
STUDENT_NAME_DUPLICATE 팀 내 이름 중복
ALREADY_EXISTS 리소스 중복
INTERNAL_ERROR 서버 오류

구현 노트

Next.js API Routes 구현 예시

// app/api/team/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyIdToken } from '@/lib/auth';
import { createTeam } from '@/services/teamService';

export async function POST(request: NextRequest) {
  try {
    // 1. 인증 확인
    const token = request.headers.get('Authorization')?.replace('Bearer ', '');
    const decoded = await verifyIdToken(token);

    if (!decoded) {
      return NextResponse.json({
        success: false,
        error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' }
      }, { status: 401 });
    }

    // 2. Request 파싱
    const body = await request.json();

    // 3. 유효성 검사
    if (!body.name || !body.code) {
      return NextResponse.json({
        success: false,
        error: { code: 'VALIDATION_ERROR', message: '필수 필드가 누락되었습니다.' }
      }, { status: 400 });
    }

    // 4. 비즈니스 로직 실행
    const teamId = await createTeam({
      ...body,
      ownerId: decoded.uid
    });

    // 5. 응답
    return NextResponse.json({
      success: true,
      data: { teamId, team: {...} }
    });

  } catch (error: any) {
    return NextResponse.json({
      success: false,
      error: { code: 'INTERNAL_ERROR', message: error.message }
    }, { status: 500 });
  }
}

Server Actions 구현 예시

// app/actions/team.ts
'use server'

import { auth } from '@/lib/auth';
import { createTeam as createTeamFirestore } from '@/services/teamService';
import type { ApiResponse } from '@/types/api';
import type { CreateTeamRequest, CreateTeamResponse } from '@/types/api/team';

export async function createTeam(data: CreateTeamRequest): Promise<ApiResponse<CreateTeamResponse>> {
  try {
    const session = await auth();

    if (!session) {
      return {
        success: false,
        error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' }
      };
    }

    const teamId = await createTeamFirestore({
      ...data,
      ownerId: session.uid
    });

    return {
      success: true,
      data: { teamId, team: {...} }
    };
  } catch (error: any) {
    return {
      success: false,
      error: { code: 'INTERNAL_ERROR', message: error.message }
    };
  }
}

보안 고려사항

  1. 인증 토큰 검증: 모든 쓰기 작업은 Firebase ID Token 검증 필수
  2. 권한 체크: 팀 소유자 확인 (ownerId === decoded.uid)
  3. 입력 검증: 모든 입력값 sanitization 및 validation
  4. Rate Limiting: Redis로 API 호출 횟수 제한 (선택적)
  5. PIN 보안: PIN은 평문으로 받아 서버에서 SHA-256 해시로 저장

Redis 캐싱 전략 (서버 사이드)

캐싱 대상

  • 팀 정보: redis:team:{teamId} - TTL 5분
  • 팀 코드 조회: redis:team:code:{code} - TTL 1분
  • 학생 정보: redis:student:{studentId} - TTL 5분
  • 팀별 학생 목록: redis:students:team:{teamId} - TTL 30초

캐시 무효화

  • 팀 생성/수정/삭제 시: 해당 팀 + 팀 목록
  • 학생 생성/수정 시: 해당 학생 + 팀별 학생 목록
  • 강퇴 시: 학생 + 팀 + 팀별 학생 목록

클라이언트 캐싱 전략

매니저 레벨에서 in-memory 캐싱 (SingletonManager):

  • 조회 작업: 캐싱 활성화 (GET 요청)
  • 변경 작업: 캐싱 안 함 (POST/PUT/DELETE)
  • 캐시 무효화: 변경 작업 시 관련 캐시 자동 무효화

개발 순서

  1. API 타입 정의 (src/types/api.ts, src/types/api/team.ts, src/types/api/student.ts)
  2. BaseManager에 authenticatedFetch, callApi 구현
  3. BaseManager에 클라이언트 캐싱 메서드 구현
  4. TeamManager, StudentManager를 API 호출 방식으로 전환
  5. Next.js API Routes 또는 Server Actions 구현
  6. Redis 캐싱 구현 (선택적)
  7. Rate Limiting 구현 (선택적)