657 lines
33 KiB
Markdown
657 lines
33 KiB
Markdown
# chocoadmin 프로젝트 계획서
|
|
|
|
## 1. 프로젝트 개요
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 서비스명 | chocoadmin |
|
|
| 목적 | chocomae 웹 서비스(mouse-typing)의 관리자 기능을 독립 웹 서비스로 분리 |
|
|
| 기존 서비스 경로 | `/Users/jisangs/work/jinaju/git/web/mouse-typing` |
|
|
| 신규 서비스 경로 | `/Users/jisangs/work/jinaju/git/web/chocoadmin` |
|
|
| 기존 운영 환경 | AWS EC2 t3a.medium 인스턴스 1개 |
|
|
| 사용 DB | 기존 chocomae DB (MariaDB 10.11.13) 그대로 사용 |
|
|
|
|
---
|
|
|
|
## 2. 핵심 개발 방향
|
|
|
|
1. 기존 `chocomae` DB를 그대로 사용한다. DB 스키마는 임의로 변경하지 않는다.
|
|
2. 관리자 UI와 서버 로직만 신규 Next.js 서비스로 분리한다.
|
|
3. 기존 테이블 구조와 상태값을 그대로 존중하고, 기존 서비스의 비즈니스 규칙을 먼저 파악한 뒤 동일하게 구현한다.
|
|
4. 승인·취소·거절 같은 변경 액션은 반드시 서버 사이드에서 권한 검증 → 트랜잭션 → 로그 기록 순서로 처리한다.
|
|
5. 중복 처리 방지를 위해 모든 변경 API는 현재 상태를 조건으로 먼저 검사한다.
|
|
|
|
---
|
|
|
|
## 3. 기술 스택
|
|
|
|
| 구분 | 기술 |
|
|
|---|---|
|
|
| 런타임 | Node.js LTS |
|
|
| 프레임워크 | Next.js 16 (App Router) |
|
|
| 언어 | TypeScript |
|
|
| 패키지 매니저 | pnpm |
|
|
| UI / CSS | Tailwind CSS + shadcn/ui |
|
|
| Table | TanStack Table (React Table v8) |
|
|
| DB 클라이언트 | mysql2 (MariaDB 호환) |
|
|
| ORM | Prisma (기존 DB introspection 방식으로 활용) |
|
|
| 유효성 검증 | Zod |
|
|
| 인증 | NextAuth.js v5 (기존 `admin` 테이블 활용) |
|
|
| 이메일 발송 | Nodemailer (기존 `mouse-typing` SMTP 설정을 환경변수로 이관) |
|
|
| 환경변수 관리 | `.env.local` / `.env.production` |
|
|
|
|
### Prisma vs Drizzle ORM
|
|
|
|
두 가지 모두 MariaDB를 지원하나, 이 프로젝트에서는 **Prisma**를 우선 사용한다.
|
|
|
|
- 기존 DB를 `prisma db pull`(introspection)로 그대로 가져올 수 있어 초기 세팅이 빠름
|
|
- TypeScript 타입 자동 생성으로 컬럼명 오타를 컴파일 단계에서 잡을 수 있음
|
|
- 세밀한 SQL 제어가 필요한 구간은 `$queryRaw` / `$executeRaw`로 raw SQL 병행 가능
|
|
|
|
---
|
|
|
|
## 4. 개발 환경 및 배포 환경
|
|
|
|
### 4-1. 개발 환경
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 하드웨어 | 맥미니 |
|
|
| IDE | IntelliJ IDEA |
|
|
| 실행 방법 | IntelliJ 터미널에서 `pnpm dev` |
|
|
| 포트 | 3000 |
|
|
| DB (개발) | 맥미니 로컬 MariaDB (`localhost:3306`) |
|
|
| 환경변수 | `.env.local` |
|
|
|
|
### 4-2. 배포(서비스) 환경
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 플랫폼 | Synology NAS DS920+ (RAM 20GB) — Docker |
|
|
| 실행 방법 | Synology Container Manager (`docker compose up`) |
|
|
| DB (서비스) | AWS EC2 MariaDB (chocomae 서비스와 동일 인스턴스) |
|
|
| DB 접근 | AWS 보안그룹에서 NAS 외부 IP만 3306 포트 허용 |
|
|
| DB 계정 | chocoadmin 전용 최소 권한 계정으로 분리 |
|
|
| 환경변수 | `.env.production` 또는 Container Manager 환경변수 설정 |
|
|
|
|
### 4-3. Dockerfile / docker-compose (배포용)
|
|
|
|
```dockerfile
|
|
# Dockerfile
|
|
FROM node:22-alpine
|
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
WORKDIR /app
|
|
COPY package.json pnpm-lock.yaml ./
|
|
RUN pnpm install --frozen-lockfile
|
|
COPY . .
|
|
RUN pnpm build
|
|
CMD ["pnpm", "start"]
|
|
```
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
chocoadmin:
|
|
build: .
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
- NODE_ENV=production
|
|
- DATABASE_URL=mysql://USER:PASS@AWS_EC2_HOST:3306/chocomae
|
|
- NEXTAUTH_SECRET=...
|
|
- NEXTAUTH_URL=http://NAS_IP:3000
|
|
restart: unless-stopped
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 데이터베이스 주요 테이블
|
|
|
|
스키마: [`docs/db-reference/schema.sql`](./db-reference/schema.sql)
|
|
|
|
| 테이블 | 설명 | 주요 컬럼 |
|
|
|---|---|---|
|
|
| `maestro` | 마에스트로(강사) 계정 | `MaestroID`, `Name`, `Email`, `AccountType`, `ActivateStatus`, `AvailableActivateDateTime`, `PlayerCount` |
|
|
| `maestro_extension` | 연장 신청 | `MaestroExtensionID`, `MaestroID`, `AccountType`, `RequestedDateTime`, `Status` |
|
|
| `maestro_upgrade` | 업그레이드 신청 | `MaestroUpgradeID`, `MaestroID`, `RegisteredAccountType`, `RequestedAccountType`, `RequestedDateTime`, `Status` |
|
|
| `maestro_log` | 처리 이력 로그 | `MaestroLogID`, `MaestroID`, `Type`, `LogDateTime`, `Remark` |
|
|
| `admin` | 관리자 계정 | `AdminID`, `Name`, `Password`, `Email`, `AccountType`, `ActivateStatus` |
|
|
| `player` | 학생 계정 | `PlayerID`, `MaestroID`, `Name`, `EnterCode` |
|
|
|
|
### 상태값 코드표
|
|
|
|
**`maestro.AccountType`**
|
|
|
|
| 값 | 의미 |
|
|
|---|---|
|
|
| 1 | 20명 (1만원) |
|
|
| 2 | 50명 (2만원) |
|
|
| 3 | 100명 (3만원) |
|
|
| 4 | 500명 (4만원) |
|
|
| 5 | 1,000명 (5만원) |
|
|
|
|
**`maestro.ActivateStatus`**
|
|
|
|
| 값 | 의미 |
|
|
|---|---|
|
|
| 0 | 미승인 |
|
|
| 1 | 체험 |
|
|
| 2 | 활성화 |
|
|
| 100 | 등록취소 |
|
|
|
|
**`maestro_extension.Status` / `maestro_upgrade.Status`**
|
|
|
|
| 값 | 의미 |
|
|
|---|---|
|
|
| 1 | 요청 |
|
|
| 2 | 취소/거절 |
|
|
| 3 | 적용 완료 |
|
|
|
|
---
|
|
|
|
## 6. 애플리케이션 구조
|
|
|
|
```
|
|
chocoadmin/
|
|
├── app/
|
|
│ ├── (auth)/
|
|
│ │ └── login/
|
|
│ │ └── page.tsx # 관리자 로그인
|
|
│ ├── (admin)/
|
|
│ │ ├── layout.tsx # 공통 사이드바 + 헤더
|
|
│ │ ├── MobileNav.tsx # 모바일 슬라이드인 사이드바 (햄버거·오버레이·X 버튼)
|
|
│ │ ├── nav-config.ts # 내비게이션 항목 공유 설정
|
|
│ │ ├── maestros/
|
|
│ │ │ ├── page.tsx # 마에스트로 전체 목록
|
|
│ │ │ └── [maestroId]/
|
|
│ │ │ ├── page.tsx # 마에스트로 상세 정보 (server)
|
|
│ │ │ ├── MaestroEditForm.tsx # 기본 정보 수정 폼 (client)
|
|
│ │ │ ├── StudentsSection.tsx # 학생 목록 + 검색/페이지네이션 (client)
|
|
│ │ │ ├── ExtensionRequestsSection.tsx # 연장 신청 이력 + [연장] 버튼 (client)
|
|
│ │ │ └── UpgradeRequestsSection.tsx # 업그레이드 신청 이력 + 신청 UI (client)
|
|
│ │ ├── extension-requests/
|
|
│ │ │ └── page.tsx # 연장 신청 목록 + 승인/취소
|
|
│ │ └── upgrade-requests/
|
|
│ │ └── page.tsx # 업그레이드 신청 목록 + 승인/거절
|
|
│ └── api/
|
|
│ ├── auth/[...nextauth]/
|
|
│ ├── maestros/
|
|
│ │ ├── route.ts # GET: 목록
|
|
│ │ └── [id]/
|
|
│ │ ├── route.ts # GET: 상세, PATCH: 정보 수정
|
|
│ │ ├── students/route.ts # GET: 학생 목록
|
|
│ │ ├── extension-requests/route.ts # POST: 연장 신청 등록 (관리자 직접)
|
|
│ │ ├── upgrade-requests/route.ts # POST: 업그레이드 신청 등록 (관리자 직접)
|
|
│ │ └── reset-password/route.ts # POST: 비밀번호 123456 초기화
|
|
│ ├── extension-requests/
|
|
│ │ └── [id]/route.ts # PATCH: 승인/취소
|
|
│ └── upgrade-requests/
|
|
│ └── [id]/route.ts # PATCH: 승인/거절
|
|
├── components/
|
|
│ ├── ui/ # shadcn/ui 컴포넌트
|
|
│ ├── data-table/ # TanStack Table 공통 컴포넌트
|
|
│ ├── layout/ # 사이드바, 헤더
|
|
│ ├── forms/
|
|
│ └── dialogs/ # 승인/취소 확인 다이얼로그
|
|
├── lib/
|
|
│ ├── db.ts # Prisma Client 싱글톤
|
|
│ ├── auth.ts # NextAuth 설정
|
|
│ ├── mail.ts # chocomae SMTP 메일 발송 모듈
|
|
│ ├── validators/ # Zod 스키마
|
|
│ └── constants.ts # 상태값 상수
|
|
├── types/
|
|
│ └── index.ts
|
|
├── prisma/
|
|
│ └── schema.prisma # DB introspection 결과
|
|
├── docs/
|
|
│ └── db-reference/
|
|
│ └── schema.sql
|
|
├── .env.local # gitignore
|
|
├── .env.production # gitignore
|
|
├── Dockerfile
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 주요 화면 기능 명세
|
|
|
|
### 7-1. 공통 레이아웃
|
|
|
|
- 로그인 화면 (미인증 시 `/login` 리다이렉트)
|
|
- 좌측 사이드바 네비게이션
|
|
- 공통 확인 다이얼로그 (승인/취소/거절 전 확인)
|
|
- 처리 성공/실패 토스트 알림
|
|
|
|
### 7-2. 마에스트로 전체 목록 (`/maestros`)
|
|
|
|
- 전체 목록 조회 (서버 사이드 페이지네이션)
|
|
- 검색: 이름, 이메일, MaestroID
|
|
- 필터: ActivateStatus (미승인 / 체험 / 활성화 / 등록취소)
|
|
- 필터: AccountType (20명 / 50명 / 100명 / 500명 / 1,000명)
|
|
- 정렬: 가입일, 사용 가능일
|
|
- 컬럼: ID / 이름 / 이메일 / 계정 유형 / 상태 / 학생 수 / 사용 가능일 / 상세보기
|
|
|
|
### 7-3. 마에스트로 상세 정보 (`/maestros/[maestroId]`)
|
|
|
|
- 기본 계정 정보 (이름, 이메일, 계정 유형, 상태, 만료일, 약관 동의일)
|
|
- 기본 계정 정보 수정 및 저장
|
|
- 이름: 저장 시 다른 마에스트로 계정에서 이미 사용 중인지 검사하고, 중복이면 저장 실패 처리
|
|
- 이메일
|
|
- 상태: 콤보박스 (`maestro.ActivateStatus`)
|
|
- 계정유형: 콤보박스 (`maestro.AccountType`)
|
|
- 사용 가능일 (`maestro.AvailableActivateDateTime`)
|
|
- 입장코드 수정: 콤보박스 기반 입력/선택 UI
|
|
- 정보 수정 및 저장 성공 시 `maestro_log`에 처리 이력 기록
|
|
- 플레이어(학생) 전체 목록
|
|
- 서버 사이드 페이지네이션
|
|
- 이름 검색
|
|
- 연장 신청 이력 + [연장] 버튼
|
|
- 유료 요금제 계정에서만 버튼 활성화 (체험 계정 비활성)
|
|
- 클릭 시 현재 `AccountType`으로 `maestro_extension` INSERT (서버에서 DB 조회)
|
|
- 등록 후 이력 목록 즉시 갱신 (`router.refresh()`)
|
|
- 업그레이드 신청 이력 + 요금제 콤보박스 + [업그레이드] 버튼
|
|
- 콤보박스: 현재 요금제 이하 항목 비활성화, 상위 요금제만 선택 가능
|
|
- 클릭 시 선택한 `requestedAccountType`으로 `maestro_upgrade` INSERT (트랜잭션 내 서버 조회)
|
|
- 서버에서 `requestedAccountType > maestro.AccountType` 검증 (다운그레이드 차단)
|
|
- 등록 후 이력 목록 즉시 갱신 (`router.refresh()`)
|
|
- 관리자 처리 로그 (`maestro_log`)
|
|
|
|
### 7-4. 연장 신청 목록 (`/extension-requests`)
|
|
|
|
- 연장 신청 목록 조회 (기본 필터: Status=1 요청 건)
|
|
- 필터: 상태 (요청 / 취소 / 적용)
|
|
- 컬럼: 신청ID / 마에스트로명 / 계정유형 / 신청일시 / 상태 / 승인 / 취소
|
|
|
|
**승인 처리 순서:**
|
|
1. `maestro_extension.Status = 1` 인지 확인 (중복 처리 방지)
|
|
2. 트랜잭션 시작
|
|
3. `maestro_extension.Status → 3`
|
|
4. `maestro.AvailableActivateDateTime` 연장, `maestro.ActivateStatus → 2` (기존 서비스 로직 확인 후 동일 적용)
|
|
5. `maestro_log` 기록
|
|
6. 트랜잭션 커밋
|
|
7. 커밋 후 연장 완료 이메일 자동 발송
|
|
|
|
**연장 완료 이메일:**
|
|
- 기존 호출 위치: `/Users/jisangs/work/jinaju/git/web/mouse-typing/src/web/server/admin/extension_maestro.php:29`
|
|
- 제목: `[초코마에] {마에스트로명} - 마에스트로 계정 유효기간 연장 완료`
|
|
- 내용: 유효기간 +1년 연장 완료, 새 유효기간 안내
|
|
|
|
**취소 처리 순서:**
|
|
1. `maestro_extension.Status = 1` 인지 확인
|
|
2. 트랜잭션 시작
|
|
3. `maestro_extension.Status → 2`
|
|
4. `maestro_log` 기록
|
|
5. 트랜잭션 커밋
|
|
|
|
### 7-5. 업그레이드 신청 목록 (`/upgrade-requests`)
|
|
|
|
- 업그레이드 신청 목록 조회 (기본 필터: Status=1 요청 건)
|
|
- 필터: 상태 (요청 / 취소 / 적용)
|
|
- 컬럼: 신청ID / 마에스트로명 / 현재등급 / 요청등급 / 신청일시 / 상태 / 승인 / 거절
|
|
|
|
**승인 처리 순서:**
|
|
1. `maestro_upgrade.Status = 1` 인지 확인 (중복 처리 방지)
|
|
2. 트랜잭션 시작
|
|
3. `maestro_upgrade.Status → 3`
|
|
4. `maestro.AccountType → RequestedAccountType` 변경 (PlayerCount 갱신 정책은 기존 서비스 확인 후 동일 적용)
|
|
5. `maestro_log` 기록
|
|
6. 트랜잭션 커밋
|
|
7. 커밋 후 업그레이드 완료 이메일 자동 발송
|
|
|
|
**업그레이드 완료 이메일:**
|
|
- 기존 호출 위치: `/Users/jisangs/work/jinaju/git/web/mouse-typing/src/web/server/admin/upgrade_maestro.php:30`
|
|
- 제목: `[초코마에] {마에스트로명} - 마에스트로 계정의 요금제 업그레이드 완료 알림`
|
|
- 내용: 기존 요금제에서 상위 요금제로 변경 완료, 유효기간 오늘부터 1년
|
|
|
|
**거절 처리 순서:**
|
|
1. `maestro_upgrade.Status = 1` 인지 확인
|
|
2. 트랜잭션 시작
|
|
3. `maestro_upgrade.Status → 2`
|
|
4. `maestro_log` 기록
|
|
5. 트랜잭션 커밋
|
|
|
|
---
|
|
|
|
## 8. 보안 고려사항
|
|
|
|
1. 관리자 로그인은 NextAuth 서버 세션 기반으로 구현한다.
|
|
2. 기존 `admin.Password` 저장 방식(평문/해시/자체 암호화)을 먼저 확인한 뒤 동일 방식 또는 해시 업그레이드를 결정한다.
|
|
3. 승인·취소·거절 액션은 Next.js Server Action 또는 API Route에서 CSRF 보호를 적용한다.
|
|
4. DB 연결 정보는 `.env` 파일로 관리하며 Git에 커밋하지 않는다.
|
|
5. 배포 DB 계정은 chocoadmin 전용 계정으로 분리하고 최소 권한(SELECT + 필요한 테이블만 UPDATE)을 적용한다.
|
|
6. AWS EC2 보안그룹에서 MariaDB 포트(3306)를 NAS 외부 IP로만 허용한다.
|
|
7. 관리자 전용 서비스이므로 외부에 직접 노출하지 않고 VPN 또는 NAS 내부 포트로 접근하는 방식을 권장한다.
|
|
8. SMTP/API 인증 정보, 발송자 주소, BCC 주소는 환경변수 또는 배포 시크릿으로 관리하고 코드에 하드코딩하지 않는다.
|
|
|
|
---
|
|
|
|
## 9. 개발 단계 계획
|
|
|
|
### Phase 0. 기존 관리자 코드 조사
|
|
|
|
> 기존 서비스 비즈니스 규칙을 파악해 재현 실수를 방지한다.
|
|
|
|
- [x] `mouse-typing` 관리자 라우트와 처리 로직 분석 (`src/web/admin/`, `src/web/server/admin/`)
|
|
- [x] 연장 승인 시 `AvailableActivateDateTime` 계산 방식 확인 (몇 개월/년 연장인지)
|
|
- [x] 업그레이드 승인 시 `PlayerCount` 갱신 여부 확인
|
|
- [x] `admin.Password` 검증 방식 확인 (평문 / MD5 / bcrypt 등)
|
|
- [x] `maestro_log` 기록 형식(`Type`, `Remark`) 확인
|
|
- [x] 조사 결과를 `docs/business-rules.md`에 문서화
|
|
|
|
### Phase 1. 프로젝트 초기화
|
|
|
|
- [x] `pnpm create next-app` (Next.js 16, TypeScript, App Router, Tailwind CSS)
|
|
- [x] shadcn/ui 초기화 (`pnpm dlx shadcn@latest init`)
|
|
- [x] TanStack Table 설치 (`pnpm add @tanstack/react-table`)
|
|
- [x] Prisma 설치 및 MariaDB introspection (`pnpm add prisma`, `prisma db pull`)
|
|
- [x] Zod 설치 (`pnpm add zod`)
|
|
- [x] NextAuth.js v5 설치
|
|
- [x] ESLint / Prettier 설정
|
|
- [x] `.env.local` 환경변수 구조 정의
|
|
- [x] IntelliJ에서 `pnpm dev` 실행 확인
|
|
|
|
### Phase 2. DB 연결 및 타입 구성
|
|
|
|
- [x] Prisma Client 싱글톤 (`lib/db.ts`)
|
|
- [x] 상태값 상수 정의 (`lib/constants.ts`)
|
|
- [x] 공통 유틸 작성 (날짜 포맷, AccountType 레이블 변환 등)
|
|
- [x] 개발 DB에서 `maestro`, `maestro_extension`, `maestro_upgrade` 조회 확인
|
|
|
|
### Phase 3. 인증 및 공통 레이아웃
|
|
|
|
- [x] 로그인 페이지 (`/login`)
|
|
- [x] NextAuth Credentials Provider — `admin` 테이블 기반 검증
|
|
- [x] 인증 프록시 (`proxy.ts`) — 비로그인 시 `/login` 리다이렉트
|
|
- [x] 공통 레이아웃 (`(admin)/layout.tsx`) — 사이드바, 헤더
|
|
- [x] 로그아웃 기능
|
|
|
|
### Phase 4. 마에스트로 목록 및 상세
|
|
|
|
- [x] 마에스트로 목록 API (`GET /api/maestros`) — 검색·필터·페이지네이션 포함
|
|
- [x] TanStack Table 기반 목록 화면
|
|
- [x] 마에스트로 상세 API (`GET /api/maestros/[id]`)
|
|
- [x] 상세 화면 — 연장/업그레이드 이력, 학생 목록 요약, 처리 로그
|
|
|
|
### Phase 5. 연장 신청 관리
|
|
|
|
- [x] 연장 신청 목록 API + 화면
|
|
- [x] 승인/취소 확인 다이얼로그
|
|
- [x] 승인 API (`PATCH /api/extension-requests/[id]`) — 트랜잭션 + maestro_log
|
|
- [x] 취소 API — 트랜잭션 + maestro_log
|
|
|
|
### Phase 6. 업그레이드 신청 관리
|
|
|
|
- [x] 업그레이드 신청 목록 API + 화면
|
|
- [x] 승인/거절 확인 다이얼로그
|
|
- [x] 승인 API (`PATCH /api/upgrade-requests/[id]`) — 트랜잭션 + maestro_log
|
|
- [x] 거절 API — 트랜잭션 + maestro_log
|
|
|
|
### Phase 7. 배포 환경 구성
|
|
|
|
- [x] Dockerfile (프로덕션용 pnpm 빌드)
|
|
- [x] `docker-compose.yml` (Synology NAS Container Manager용)
|
|
- [x] AWS EC2 보안그룹 인바운드 설정 (NAS IP → 3306 허용)
|
|
- [x] Synology NAS에서 컨테이너 실행 확인 (production + stage 동시 운영 중)
|
|
- [x] 운영 DB 연결 및 변경 기능 확인
|
|
|
|
### Phase 8. 검증 및 안정화
|
|
|
|
- [x] 승인/취소/거절 트랜잭션 시나리오 테스트 절차 문서화 (`docs/phase/phase8.md`)
|
|
- [x] 중복 처리 방지 코드 확인 (이미 처리된 건 재처리 시 `409` 응답)
|
|
- [x] 비인증 접근 차단 코드 확인 (`proxy.ts`, API route `auth()` 검사)
|
|
- [x] Docker 재시작 후 정상 동작 확인 절차 문서화 (`docs/phase/phase8.md`)
|
|
|
|
### Phase 9. 실행 로그 및 DB 쿼리 로그
|
|
|
|
> local/stage에서는 개발과 디버깅에 충분한 상세 로그를 남기고, production에서는 운영에 필요한 수준으로 로그 양과 민감정보 노출을 제한한다.
|
|
|
|
- [x] 로그 정책 정의 문서 작성 (`docs/phase/phase9.md`)
|
|
- [x] 환경 구분: `local`, `stage`, `production`
|
|
- [x] 로그 레벨 구분: `debug`, `info`, `warn`, `error`
|
|
- [x] 로그 출력 형식: 사람이 읽기 쉬운 콘솔 출력 + 필요 시 JSON line 확장 가능 구조
|
|
- [x] 민감정보 마스킹 대상 정의: 비밀번호, 인증 토큰, 쿠키, DB 접속 문자열, secret, 개인정보성 필드
|
|
- [x] 서버 실행 로그 유틸 구현
|
|
- [x] 공통 logger 모듈 추가 (`lib/logger.ts`)
|
|
- [ ] request 단위 correlation id 생성 및 로그에 포함 (보류 — Edge Middleware 제약으로 헤더 주입 방식 필요, `docs/phase/phase9.md` 참고)
|
|
- [x] API route 시작/종료, 처리 시간, status code, action 결과 기록 (`lib/api-handler.ts` `withApiHandler` 래퍼로 공통 처리)
|
|
- [x] 인증 실패, 권한 없음, 유효성 검증 실패, 비즈니스 예외를 구분해 기록 (`ApiError` + `withApiHandler`)
|
|
- [x] DB query 로그 구현
|
|
- [x] Prisma query event 기반 query 로깅 적용 (`lib/db.ts` `$on("query", ...)`)
|
|
- [x] local/stage: query문, params, duration, 결과를 `sql-formatter`로 pretty 출력 (`$on` + `$extends.$allOperations`)
|
|
- [x] production: `LOG_LEVEL=info` 설정 시 debug 로그 전체 억제 — slow query만 warn으로 출력
|
|
- [x] production slow query threshold 설정 (`DB_SLOW_QUERY_MS`, 기본 1000ms)
|
|
- [ ] 결과 로그 최대 길이 제한 (`DB_LOG_RESULT_LIMIT`) 및 초과 시 truncate 표시 (미구현)
|
|
- [x] 환경변수 추가
|
|
- [x] `APP_ENV=local|stage|production`
|
|
- [x] `LOG_LEVEL=debug|info|warn|error`
|
|
- [ ] `DB_QUERY_LOG=off|summary|full` (미구현 — 현재는 `LOG_LEVEL`로 제어)
|
|
- [x] `DB_SLOW_QUERY_MS`
|
|
- [ ] `DB_LOG_RESULT_LIMIT` (미구현)
|
|
- [x] 주요 API에 로그 적용
|
|
- [x] 로그인 성공/실패 로그 (`auth.ts`)
|
|
- [x] 마에스트로 목록/상세 조회 로그 (`lib/maestros.ts`)
|
|
- [x] 연장 신청 조회/승인/취소 로그 (`lib/extension-requests.ts`, `app/api/extension-requests/[id]/route.ts`)
|
|
- [x] 업그레이드 신청 조회/승인/거절 로그 (`lib/upgrade-requests.ts`, `app/api/upgrade-requests/[id]/route.ts`)
|
|
- [x] 중복 처리 방지로 `409` 반환 시 warning 로그 (`withApiHandler`가 `ApiError` 캐치 후 warn 출력)
|
|
- [x] Docker/NAS 로그 운영 절차 문서화 (`docs/phase/phase9.md`)
|
|
- [x] `docker compose logs -f` 확인 방법
|
|
- [x] stage와 production의 권장 `.env` 로그 설정 예시
|
|
- [x] Nginx Proxy Manager access/error log와 애플리케이션 로그를 같이 확인하는 절차
|
|
- [x] 검증
|
|
- [x] local에서 query문과 결과 전체가 출력되는지 확인
|
|
- [x] stage에서 query문과 결과 전체가 출력되는지 확인
|
|
- [x] production 설정에서 결과 전체가 출력되지 않고 summary/slow query 중심으로 출력되는지 확인
|
|
- [x] 로그에 비밀번호, token, cookie, secret, DB password가 노출되지 않는지 확인 (`onQuery`에서 `PASSWORD(` 포함 쿼리는 params 치환 생략)
|
|
|
|
### Phase 10. 마에스트로 정보 수정 및 학생 목록 고도화
|
|
|
|
> `/maestros/[maestroId]` 상세 화면에서 관리자 계정 정보 수정과 학생 목록 조회를 완성한다.
|
|
|
|
- [x] 기존 `mouse-typing` 마에스트로 정보 수정 로직 조사
|
|
- [x] 이름 변경 시 중복 검사 기준 확인 (`maestro.Name`, 자기 자신 제외)
|
|
- [x] 이메일 변경 처리와 로그 형식 확인
|
|
- [x] 입장코드 수정 대상 컬럼과 기존 값 생성/검증 규칙 확인
|
|
- [x] `maestro_log.Type` / `Remark` 형식을 기존 서비스와 맞춤
|
|
- [x] 마에스트로 정보 수정 API 구현 (`PATCH /api/maestros/[id]`)
|
|
- [x] Zod 요청 스키마 정의: 이름, 이메일, 상태, 계정유형, 사용 가능일, 입장코드
|
|
- [x] 관리자 인증 및 권한 검증
|
|
- [x] 이름 중복 검사: 다른 계정이 사용 중이면 저장 실패 응답
|
|
- [x] 트랜잭션으로 `maestro` 정보 수정
|
|
- [x] 변경 전/후 값을 포함해 `maestro_log` 기록
|
|
- [x] 변경 없음, 유효하지 않은 상태값, 존재하지 않는 마에스트로에 대한 응답 정책 정리
|
|
- [x] 마에스트로 상세 화면 수정 폼 구현
|
|
- [x] 이름/이메일 입력
|
|
- [x] 상태 콤보박스
|
|
- [x] 계정유형 콤보박스
|
|
- [x] 사용 가능일 입력
|
|
- [x] 입장코드 수정 콤보박스
|
|
- [x] 저장 전 확인, 저장 중 비활성화, 성공/실패 토스트
|
|
- [x] 학생 목록 API 구현 (`GET /api/maestros/[id]/students`)
|
|
- [x] 전체 학생 목록 조회
|
|
- [x] 서버 사이드 페이지네이션
|
|
- [x] 이름 검색
|
|
- [x] 정렬 기준과 기본 page size 정의
|
|
- [x] 학생 목록 UI 구현
|
|
- [x] 이름 검색 입력
|
|
- [x] 페이지네이션 컨트롤
|
|
- [x] 빈 목록/검색 결과 없음 상태
|
|
- [x] 검증
|
|
- [x] 이름 중복 저장 실패 확인
|
|
- [x] 정보 수정 성공 시 DB 반영과 `maestro_log` 기록 확인
|
|
- [x] 학생 목록 페이지네이션/검색 동작 확인
|
|
- [x] 비인증 접근 차단 확인
|
|
|
|
### Phase 11. 이메일 발송 환경 및 자동 발송
|
|
|
|
> 기존 `chocomae` 서비스의 관리자 승인 완료 메일을 `chocoadmin`에서도 동일하게 발송한다.
|
|
> 상세 계획: `docs/phase/phase11.md`
|
|
|
|
- [x] 기존 이메일 발송 설정 이관 준비
|
|
- [x] 기존 서비스 파일 확인: `send_jinaju_mail.php` — SMTP gmail:465, SMTPS, 발송자/BCC 주소 확인
|
|
- [x] 기존 서비스 파일 확인: `mail_contents.php` — 연장/업그레이드 완료 이메일 제목·본문 확인
|
|
- [x] 기존 서비스 파일 확인: `mail_setting.php` — From, BCC, sender_name 확인
|
|
- [x] `.env.local`에 메일 환경변수 추가 (SMTP 비밀번호는 배포 시크릿으로 분리)
|
|
- [x] `nodemailer` 패키지 설치 (`pnpm add nodemailer && pnpm add -D @types/nodemailer`)
|
|
- [x] 메일 발송 모듈 구현 (`lib/mail.ts`)
|
|
- [x] `createTransporter()` — 환경변수 기반 SMTP transport
|
|
- [x] `sendMail()` — 공통 발송 함수 (`MAIL_SEND_ENABLED` 체크, From/BCC/ReplyTo 구성, 성공·실패 로그)
|
|
- [x] `sendExtensionDoneEmail()` — 연장 완료 이메일 템플릿
|
|
- [x] `sendUpgradeDoneEmail()` — 업그레이드 완료 이메일 템플릿 (체험→유료 / 유료→유료 내부 분기)
|
|
- [x] `lib/extension-requests.ts` 수정
|
|
- [x] `approveExtensionRequest` 반환값에 `maestroName`, `maestroEmail` 추가 (이미 `include: { maestro: true }` 있음 — 추가 쿼리 불필요)
|
|
- [x] `lib/upgrade-requests.ts` 수정
|
|
- [x] `getActionTarget`에 `include: { maestro: { select: { Name, Email } } }` 추가
|
|
- [x] `approveUpgradeRequest` 반환값에 `maestroName`, `maestroEmail`, `registeredActivateStatus`, `registeredAccountType`, `requestedAccountType` 추가
|
|
- [x] 승인 API에 이메일 자동 발송 연결
|
|
- [x] `app/api/extension-requests/[id]/route.ts` — 승인 후 `void sendExtensionDoneEmail(...)` 호출
|
|
- [x] `app/api/upgrade-requests/[id]/route.ts` — 승인 후 `void sendUpgradeDoneEmail(...)` 호출
|
|
- [x] 메일 발송 실패 정책 적용: 실패 시 `logger.error`만 기록, 승인 결과(200)는 그대로 반환 (롤백 없음)
|
|
- [x] 검증
|
|
- [x] `MAIL_SEND_ENABLED=false`에서 승인 시 `send skipped` 로그 확인, 승인 결과 200 정상
|
|
- [x] `MAIL_SEND_ENABLED=true`에서 연장 승인 후 이메일 제목/본문/수신자/BCC 확인
|
|
- [ ] 업그레이드 승인 후 체험→유료 케이스: `0원 : 무료 체험 기간` 본문 확인
|
|
- [ ] 업그레이드 승인 후 유료→유료 케이스: `{prev_price}원` 본문 확인
|
|
- [x] SMTP 오류 시 승인 API 200 응답 + `error: send failed` 로그 확인
|
|
|
|
### Phase 12. 관리자 직접 연장/업그레이드 신청 등록
|
|
|
|
> 마에스트로 상세 화면에서 관리자가 직접 연장·업그레이드 신청을 DB에 등록할 수 있도록 한다. 메일 발송은 하지 않는다.
|
|
|
|
- [x] 연장 신청 등록 API 구현 (`POST /api/maestros/[id]/extension-requests`)
|
|
- [x] 서버에서 `maestro.AccountType` 직접 조회 — 클라이언트 값 신뢰 안 함
|
|
- [x] 유료 요금제 계정이 아닌 경우 400 반환 (체험 계정 차단)
|
|
- [x] `maestro_extension` INSERT (`AccountType`, `RequestedDateTime`, `Status=1`)
|
|
- [x] 업그레이드 신청 등록 API 구현 (`POST /api/maestros/[id]/upgrade-requests`)
|
|
- [x] 트랜잭션 내에서 `maestro` SELECT → `maestro_upgrade` INSERT (원자성 보장)
|
|
- [x] `requestedAccountType > maestro.AccountType` 검증 (다운그레이드 차단, 400 반환)
|
|
- [x] `RegisteredActivateStatus`, `RegisteredAccountType` 서버 DB 조회값으로 저장
|
|
- [x] 연장 신청 이력 클라이언트 컴포넌트 (`ExtensionRequestsSection.tsx`)
|
|
- [x] 섹션 우상단 [연장] 버튼
|
|
- [x] 체험 계정 (`AccountType` ∉ `PAID_ACCOUNT_TYPE_VALUES`) 시 버튼 비활성화
|
|
- [x] 등록 성공 후 `router.refresh()`로 이력 테이블 즉시 갱신
|
|
- [x] 업그레이드 신청 이력 클라이언트 컴포넌트 (`UpgradeRequestsSection.tsx`)
|
|
- [x] 섹션 우상단 요금제 콤보박스 + [업그레이드] 버튼
|
|
- [x] 현재 요금제 이하 옵션 `disabled`, 상위 요금제만 선택 가능
|
|
- [x] 선택 후 등록 성공 시 콤보박스 초기화 + `router.refresh()`
|
|
- [x] `PAID_ACCOUNT_TYPE_VALUES` 공용 상수로 추출 (`lib/constants.ts`)
|
|
- [x] 기존 `lib/maestros.ts`의 로컬 정의 제거 및 import로 교체
|
|
- [x] 두 신규 API route의 로컬 정의 제거 및 import로 교체
|
|
|
|
### Phase 13. 보안·정합성 수정
|
|
|
|
> AGENTS.md "Implemented phases" Phase 13 항목 참조.
|
|
|
|
- [x] 모든 항목 완료 (P0-1 ~ P2-8)
|
|
|
|
### Phase 14. 비밀번호 초기화 및 배포 스크립트
|
|
|
|
> 마에스트로 계정 비밀번호 초기화 기능을 추가한다.
|
|
> 완료 날짜: 2026-07-05
|
|
|
|
- [x] 비밀번호 초기화 API 구현 (`POST /api/maestros/[id]/reset-password`)
|
|
- [x] `resetMaestroPassword(maestroID)` 추가 (`lib/maestros.ts`)
|
|
- [x] `$transaction` 내에서 `UPDATE maestro SET Password = PASSWORD('123456')` 실행
|
|
- [x] `reset_maestro_password` 타입으로 `maestro_log` 기록
|
|
- [x] `withApiHandler`로 인증·에러 처리 공통화
|
|
- [x] 비밀번호 초기화 버튼 UI (`MaestroEditForm.tsx`)
|
|
- [x] `[암호 초기화]` 버튼을 `[저장]` 버튼 우측에 추가 (`variant="outline"`, `KeyRound` 아이콘)
|
|
- [x] `window.confirm`으로 `{마에스트로명} 계정의 비밀번호를 123456으로 초기화 하시겠습니까?` 확인
|
|
- [x] 성공 시 기존 메시지 영역에 `암호 초기화 완료` 표시
|
|
|
|
### Phase 15. Production/Stage 배포 환경 안정화
|
|
|
|
> Production 첫 배포 과정에서 발견된 이슈들을 해결하고 NAS 배포 자동화 스크립트를 추가한다.
|
|
> 완료 날짜: 2026-07-05
|
|
|
|
- [x] Production 배포 경로 생성 및 git pull 방식 배포 설정
|
|
- Production: `/volume1/docker/service/jinaju/chocoadmin` + `docker-compose.yml`
|
|
- Stage: 동일 경로, `-f docker-compose.stage.yml` 로 구분
|
|
- [x] **포트 충돌 수정** (`docker-compose.yml`)
|
|
- Gitea가 호스트 3000 포트를 선점 → `ports: "3000:3000"` → `expose: "3000"` 으로 변경
|
|
- [x] **Docker DNS alias 충돌 수정** (`docker-compose.stage.yml`)
|
|
- `services: chocoadmin:` → `services: stage-chocoadmin:` 으로 변경
|
|
- 동일 서비스명은 `proxy-network`에서 같은 DNS alias로 등록됨
|
|
- NPM의 `set $server "chocoadmin"` 이 두 컨테이너에 라운드로빈 → 트래픽이 production/stage 사이에 무작위 분산
|
|
- [x] **NextAuth v5 커스텀 쿠키 이름으로 production/stage 쿠키 분리**
|
|
- `auth.ts`: `cookies.sessionToken.name = "${APP_ENV}-chocoadmin.session-token"` 추가
|
|
- `proxy.ts`: `getToken({ cookieName })` 추가 — `auth.ts`에만 적용하면 middleware가 세션 토큰을 찾지 못해 `ERR_TOO_MANY_REDIRECTS` 발생
|
|
- `__Secure-` prefix 금지: Node.js 런타임은 Nginx에서 HTTP로 수신하므로 Secure 쿠키가 무시됨
|
|
- [x] **서버 액션 인라인 클로저 제거** — 로그아웃 액션을 `app/(admin)/actions.ts` 로 추출
|
|
- 인라인 `"use server"` 클로저는 빌드마다 새 ID 부여 → 재배포 후 `Failed to find Server Action`
|
|
- 코드베이스 전체 인라인 서버 액션 검토 및 동일 패턴 수정 완료
|
|
- [x] Production/Stage 동시 운영 정상 확인
|
|
- [x] 배포 자동화 스크립트 추가 (`scripts/`)
|
|
- [x] `scripts/deploy-stage.sh` — git pull → proxy-network 확인 → `docker compose -f docker-compose.stage.yml up -d --build --remove-orphans` → NPM reload
|
|
- [x] `scripts/deploy-production.sh` — `yes` 입력 확인 후 동일 5단계 실행 (`docker-compose.yml` 사용)
|
|
- [x] 각 스크립트는 NAS 해당 디렉토리에서 직접 실행
|
|
|
|
### Phase 16. 반응형 웹 디자인
|
|
|
|
> 모바일 화면(360~430px)에서도 관리자 패널을 정상 사용할 수 있도록 사이드바 내비게이션, 표 overflow, 페이지네이션 정렬을 개선한다.
|
|
> 완료 날짜: 2026-07-07
|
|
|
|
- [x] 모바일 사이드바
|
|
- [x] `app/(admin)/nav-config.ts` 생성 — navItems 배열을 `layout.tsx`와 `MobileNav.tsx`가 공유
|
|
- [x] `app/(admin)/MobileNav.tsx` 생성 — 햄버거 버튼(`md:hidden`), 반투명 오버레이(탭하면 닫힘), 슬라이드인 패널(X 버튼), ESC 키 지원, pathname 변경 시 자동 닫힘, body 스크롤 잠금, CSS `translate-x` 트랜지션 애니메이션
|
|
- [x] `app/(admin)/layout.tsx` 수정 — 헤더에 `<MobileNav />` 통합; 데스크탑 `<aside>`(`hidden md:block`)는 그대로 유지
|
|
- [x] 표 overflow 수정 (`ExtensionRequestsSection.tsx`, `UpgradeRequestsSection.tsx`)
|
|
- 원인: 두 섹션은 서버 props로 데이터를 받아 SSR 초기 렌더에 `min-w-[560px]` 표가 포함됨 → `section`에 `overflow` 미설정(`visible` 기본값)이어서 표가 섹션을 밀어냄 → `overflow-x-auto`가 확장된 너비를 그대로 가져가 스크롤 미발생. `StudentsSection`·`LogsSection`은 클라이언트 fetch 방식이라 SSR 초기 렌더에 표가 없어 동일 패턴으로도 정상 동작.
|
|
- [x] `<section>`에 `overflow-hidden` 추가 — BFC 확립으로 섹션 너비를 뷰포트 기준으로 고정
|
|
- [x] 섹션 헤더를 `flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between`으로 변경 — 모바일에서 세로 스택, 액션 버튼 접근성 확보
|
|
- [x] 페이지네이션 오른쪽 정렬 — `<nav>`에 `self-end sm:self-auto` 추가 (모바일 `flex-col`에서 오른쪽 정렬, 데스크탑 `flex-row`에서 기존 동작 유지)
|
|
- [x] `app/(admin)/extension-requests/page.tsx`
|
|
- [x] `app/(admin)/upgrade-requests/page.tsx`
|
|
- [x] `app/(admin)/maestros/page.tsx`
|
|
- [x] `app/(admin)/maestros/[maestroId]/StudentsSection.tsx`
|
|
- [x] `app/(admin)/maestros/[maestroId]/LogsSection.tsx`
|
|
|
|
---
|
|
|
|
## 10. MVP 범위
|
|
|
|
**1차 MVP (위 Phase 0~7):**
|
|
- 관리자 로그인
|
|
- 마에스트로 전체 목록 및 상세 정보 보기
|
|
- 연장 신청 목록 / 승인 / 취소
|
|
- 업그레이드 신청 목록 / 승인 / 거절
|
|
- 관리자 액션 로그 기록
|
|
- Docker 기반 NAS 배포
|
|
|
|
**MVP 이후 확장 후보:**
|
|
- 학생 목록 상세 관리
|
|
- 이메일 발송 이력/재발송 관리
|
|
- 데이터 내보내기 (CSV)
|
|
- 처리 통계 대시보드
|
|
- 감사 로그 전용 화면
|
|
- 관리자 계정 관리
|
|
|
|
---
|
|
|
|
## 11. 구현 전 확인이 필요한 사항
|
|
|
|
기존 `mouse-typing` 서비스 코드(`src/web/server/admin/`)에서 반드시 확인해야 할 비즈니스 규칙들이다.
|
|
|
|
1. 연장 승인 시 `AvailableActivateDateTime`을 정확히 얼마나 연장하는가? (1년? 계정 유형별 다름?)
|
|
: 계정 유형별로 차이 없고, 1년 연장
|
|
2. 업그레이드 승인 시 `PlayerCount`를 즉시 변경하는가, 아니면 `AccountType`만 변경하는가?
|
|
: AccountType만 변경
|
|
3. `admin.Password`는 어떤 방식으로 저장/검증되는가? (평문, MD5, bcrypt 등)
|
|
: MariaDB의 Password() 함수를 이용해서 저장/검증됨
|
|
4. `maestro_log`의 `Type` 컬럼에 어떤 값들이 사용되는가? (`Remark` 형식 포함)
|
|
: 다음과 같은 값들이 사용됨
|
|
* add_maestro
|
|
* update_maestro
|
|
* add_maestro
|
|
* register_maestro
|
|
* update_maestro_name
|
|
* extension_maestro
|
|
* upgrade_maestro
|
|
* update_maestro_email
|
|
5. 운영 MariaDB가 외부 IP 접속을 허용할 수 있는 네트워크 구조인가?
|
|
: 외부 IP 접속을 허용하고 있다
|
|
|
|
---
|
|
|
|
*작성일: 2026-05-26*
|