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[];
}
}
권한: 로그인한 사용자가 소유한 팀 + 참여한 팀 모두 조회 (중복 제거)
백엔드 로직:
getTeamsByOwner(uid)- 소유한 팀 조회getTeamsByMember(uid)- 참여한 팀 조회 (memberUidsarray-contains)- 두 결과를 병합하고 중복 제거하여 반환
캐싱: 클라이언트에서 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에 추가
- Level 2로 변경 → 기존 멤버 이름을
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[];
}
}
백엔드 로직:
- 개인 주제:
ownerType === PERSONAL && ownerId === currentUserId - 팀 주제:
ownerType === TEAM && ownerId in [teamId1, teamId2, ...]- 클라이언트에서
teamIds: ["team1", "team2"]전달 - 서버에서 필터링
- 클라이언트에서
- 모든 주제를 병합하여 반환
캐싱: 클라이언트에서 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;
}
}
권한 검증:
- 주제가 팀 주제인지 확인:
topic.ownerType === TEAM && isTeamOwnerId(topic.ownerId) - 요청한 teamId와 주제의 teamId 일치 확인:
extractTeamId(topic.ownerId) === teamId - 현재 사용자가 팀 소유자인지 확인:
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 }
};
}
}
보안 고려사항
- 인증 토큰 검증: 모든 쓰기 작업은 Firebase ID Token 검증 필수
- 권한 체크: 팀 소유자 확인 (ownerId === decoded.uid)
- 입력 검증: 모든 입력값 sanitization 및 validation
- Rate Limiting: Redis로 API 호출 횟수 제한 (선택적)
- 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)
- 캐시 무효화: 변경 작업 시 관련 캐시 자동 무효화
개발 순서
- ✅ API 타입 정의 (
src/types/api.ts,src/types/api/team.ts,src/types/api/student.ts) - ✅ BaseManager에
authenticatedFetch,callApi구현 - ✅ BaseManager에 클라이언트 캐싱 메서드 구현
- ✅ TeamManager, StudentManager를 API 호출 방식으로 전환
- ⏳ Next.js API Routes 또는 Server Actions 구현
- ⏳ Redis 캐싱 구현 (선택적)
- ⏳ Rate Limiting 구현 (선택적)