Phase 10. 마에스트로 정보 수정 및 학생 목록 페이지네이션 적용
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# Phase 10. 마에스트로 정보 수정 및 학생 목록 고도화
|
||||
|
||||
## 목표
|
||||
|
||||
`/maestros/[maestroId]` 상세 화면에서 마에스트로 정보를 직접 수정할 수 있는 폼을 추가하고, 학생 목록을 서버 사이드 페이지네이션과 이름 검색이 가능한 형태로 완성한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 마에스트로 상세 화면 수정 폼 구현
|
||||
|
||||
### 사전 조사 결과 (기존 mouse-typing 코드 확인 완료)
|
||||
|
||||
| 조사 항목 | 파일 위치 | 결과 |
|
||||
|---|---|---|
|
||||
| 이름 중복 검사 기준 | `server/maestro/update_maestro_info.php` | **중복 검사 없음** — 이름을 그대로 UPDATE. chocoadmin에서 신규 추가. |
|
||||
| 이메일 변경 처리 | `server/maestro/update_maestro_info.php` | 변경 시 이메일 안내 메일 발송 + `maestro_log` 기록. 메일은 Phase 11에서 구현. |
|
||||
| 입장코드 수정 컬럼 | `server/maestro/set_allow_edit_entercode.php` | `maestro.AllowEditEnterCode`: `0`(비허용) / `1`(허용). 로그 기록 없음. |
|
||||
| 상태/계정유형 변경 로그 | `server/admin/` 전체 | 기존 관리자에서 상태/계정유형/사용가능일을 직접 수정하는 기능 없음. 신규 기능이므로 신규 로그 타입 정의. |
|
||||
|
||||
정의한 신규 로그 타입 (기존 코드에 없음):
|
||||
|
||||
| Type | 대상 필드 | Remark 형식 |
|
||||
|---|---|---|
|
||||
| `update_maestro_status` | `ActivateStatus` | `{prev} -> {new}` (숫자) |
|
||||
| `update_maestro_account_type` | `AccountType` | `{prev} -> {new}` (숫자) |
|
||||
| `update_maestro_available_date` | `AvailableActivateDateTime` | `{YYYY-MM-DD} -> {YYYY-MM-DD}` |
|
||||
| `update_maestro_allow_enter_code` | `AllowEditEnterCode` | `{prev} -> {new}` (0 or 1) |
|
||||
|
||||
### API 구현: `PATCH /api/maestros/[id]` ✅
|
||||
|
||||
파일: `app/api/maestros/[id]/route.ts` (기존 GET과 동일 파일에 PATCH 추가)
|
||||
서비스 레이어: `lib/maestros.ts`에 `updateMaestroSchema`, `UpdateMaestroPayload`, `updateMaestro()` 추가
|
||||
|
||||
**요청 스키마 (Zod)**
|
||||
|
||||
| 필드 | 타입 | 규칙 |
|
||||
|---|---|---|
|
||||
| `name` | `string` | 1자 이상, trim |
|
||||
| `email` | `string` | 이메일 형식 |
|
||||
| `activateStatus` | `0 \| 1 \| 2 \| 100` | `ACTIVATE_STATUSES` 상수 |
|
||||
| `accountType` | `1 \| 2 \| 3 \| 4 \| 5` | 유료 타입만 허용 (`PaidAccountType`) |
|
||||
| `availableActivateDateTime` | `string` | ISO datetime 또는 `YYYY-MM-DD` 형식 |
|
||||
| `allowEditEnterCode` | `0 \| 1` | 정수 |
|
||||
|
||||
모든 필드는 optional — 전달된 필드만 업데이트한다.
|
||||
|
||||
**처리 순서**
|
||||
|
||||
1. `withApiHandler`로 인증 자동 검사
|
||||
2. 대상 maestro 존재 확인 (`findUnique`) — 없으면 404
|
||||
3. `name`이 포함된 경우: 동일 이름의 다른 마에스트로 존재 여부 확인 — 있으면 409
|
||||
4. 변경 필드 추출 (전달값 vs 현재값 비교, 동일한 값이면 변경 대상에서 제외)
|
||||
5. 변경 필드가 없으면 200 + `{ changed: false }` 반환 (DB 업데이트 및 로그 기록 생략)
|
||||
6. 트랜잭션: `maestro` 업데이트 + `maestro_log` INSERT (변경된 필드별로 각각 로그 기록)
|
||||
7. 성공 응답: 200 + `{ changed: true }`
|
||||
|
||||
**응답 정책**
|
||||
|
||||
| 상황 | HTTP | 비고 |
|
||||
|---|---|---|
|
||||
| 변경 없음 | 200 | `{ changed: false }` |
|
||||
| 성공 | 200 | `{ changed: true }` |
|
||||
| 이름 중복 | 409 | 자기 자신은 중복으로 처리하지 않음 |
|
||||
| 존재하지 않는 마에스트로 | 404 | |
|
||||
| 유효하지 않은 값 | 400 | Zod 파싱 실패 |
|
||||
| 인증 없음 | 401 | `withApiHandler` 처리 |
|
||||
|
||||
### UI 구현: 수정 폼 ✅
|
||||
|
||||
파일: `app/(admin)/maestros/[maestroId]/MaestroEditForm.tsx` (신규 Client Component)
|
||||
|
||||
현재 상세 페이지(`page.tsx`)의 "기본 정보" 섹션을 읽기 전용 표시에서 편집 가능한 폼으로 교체한다.
|
||||
|
||||
**필드 구성**
|
||||
|
||||
| 필드 | 컴포넌트 | 비고 |
|
||||
|---|---|---|
|
||||
| 이름 | `<Input type="text">` | |
|
||||
| 이메일 | `<Input type="email">` | |
|
||||
| 상태 | `<Select>` | `ACTIVATE_STATUS_LABELS` 기반 |
|
||||
| 계정유형 | `<Select>` | `ACCOUNT_TYPE_LABELS` 기반, 유료 타입만 |
|
||||
| 사용 가능일 | `<Input type="datetime-local">` | |
|
||||
| 입장코드 수정 | `<Select>` | 허용(1) / 비허용(0) |
|
||||
|
||||
**제출 흐름**
|
||||
|
||||
1. 저장 버튼 클릭 → 확인 다이얼로그 표시
|
||||
2. 확인 → `PATCH /api/maestros/[id]` 호출, 저장 버튼 비활성화
|
||||
3. 성공 → 성공 토스트 + `router.refresh()`로 화면 갱신
|
||||
4. 실패 → 실패 토스트 (이름 중복 409 응답 시 "이미 사용 중인 이름입니다." 별도 메시지)
|
||||
|
||||
---
|
||||
|
||||
## 2. 학생 목록 구현
|
||||
|
||||
### API 구현: `GET /api/maestros/[id]/students` ✅
|
||||
|
||||
파일: `app/api/maestros/[id]/students/route.ts` (신규)
|
||||
서비스 레이어: `lib/maestros.ts`에 `studentSearchSchema`, `MaestroStudentsResult`, `getMaestroStudents()` 추가
|
||||
|
||||
**쿼리 파라미터**
|
||||
|
||||
| 파라미터 | 타입 | 기본값 | 설명 |
|
||||
|---|---|------|---|
|
||||
| `page` | number | `1` | 페이지 번호 |
|
||||
| `pageSize` | `10 \| 20 \| 50` | `10` | 페이지 크기 |
|
||||
| `q` | string | `""` | 이름 검색 (`LIKE %q%`) |
|
||||
|
||||
**응답 형식**
|
||||
|
||||
```ts
|
||||
{
|
||||
items: Array<{
|
||||
playerID: number;
|
||||
name: string;
|
||||
enterCode: string;
|
||||
accountType: number | null;
|
||||
}>;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
q: string;
|
||||
}
|
||||
```
|
||||
|
||||
**처리 순서**
|
||||
|
||||
1. `withApiHandler`로 인증 검사
|
||||
2. maestro 존재 확인 — 없으면 404
|
||||
3. 쿼리 파라미터 Zod 파싱
|
||||
4. `player` 테이블에서 `MaestroID` 조건 + 이름 `LIKE` 검색 + 페이지네이션 조회
|
||||
5. 정렬: `PlayerID DESC`
|
||||
|
||||
### UI 구현: 학생 목록 ✅
|
||||
|
||||
현재 "학생 목록 요약" 섹션(최근 20명 고정)을 서버 사이드 페이지네이션 목록으로 교체한다.
|
||||
|
||||
파일: `app/(admin)/maestros/[maestroId]/StudentsSection.tsx` (신규 Client Component)
|
||||
|
||||
**구성 요소**
|
||||
|
||||
- 이름 검색 입력 (`<Input>`) + 검색 버튼
|
||||
- 전체 학생 수 표시 (`전체 N명`)
|
||||
- 학생 목록 테이블 (`PlayerID`, `이름`, `입장코드`)
|
||||
- 페이지네이션 컨트롤 — 마에스트로 목록과 동일한 슬라이딩 윈도우 형식 (현재 페이지 ±2, 처음/끝 이동, ±5 그룹 이동)
|
||||
- 빈 목록 상태: "등록된 학생이 없습니다."
|
||||
- 검색 결과 없음 상태: "검색 결과가 없습니다."
|
||||
|
||||
**동작**
|
||||
|
||||
- 검색어 입력 후 Enter 또는 검색 버튼 클릭으로 조회 (실시간 debounce 없음)
|
||||
- 검색어 변경 시 page를 1로 초기화
|
||||
- 페이지 이동은 URL query string 없이 컴포넌트 state로 관리
|
||||
|
||||
---
|
||||
|
||||
## 3. 검증 ✅
|
||||
|
||||
| 항목 | 방법 | 기대 결과 | 실행 결과 |
|
||||
|---|---|---|---|
|
||||
| 이름 중복 저장 실패 | 다른 마에스트로와 동일한 이름으로 저장 시도 | 409, "이미 사용 중인 이름입니다." | ✅ 409 + `{"message":"이미 사용 중인 이름입니다."}` |
|
||||
| 이름 자기 자신과 동일 | 현재 이름 그대로 저장 | 변경 없음 처리 또는 중복 아님으로 정상 처리 | ✅ 200 + `{"changed":false}` |
|
||||
| 정보 수정 성공 후 DB 반영 | 이름 / 이메일 / 상태 / 계정유형 / 사용 가능일 / 입장코드 수정 변경 후 DB 직접 조회 | 변경값 반영 확인 | ✅ 모든 필드 DB 반영 확인 |
|
||||
| maestro_log 기록 확인 | 정보 수정 성공 후 `maestro_log` 조회 | 변경 필드별 `Type`, `Remark` 형식이 §1 정의와 일치 | ✅ 6개 Type 모두 정확한 형식으로 기록됨 |
|
||||
| 변경 없는 저장 | 현재 값 그대로 저장 시도 | `{ changed: false }` 응답, `maestro_log` 미기록 | ✅ 200 + `{"changed":false}`, 로그 없음 |
|
||||
| 학생 목록 페이지네이션 | 학생 수가 pageSize 초과인 마에스트로에서 다음 페이지 이동 | 정확한 레코드 표시, `totalCount` 일치 | ✅ 823명 마에스트로: totalPages=83, page=2 정상 |
|
||||
| 학생 목록 이름 검색 | 존재하는 이름 일부로 검색 | 해당 학생만 필터링 | ✅ `q=최건` → totalCount=1, 해당 학생만 반환 |
|
||||
| 학생 목록 검색 결과 없음 | 존재하지 않는 이름으로 검색 | 빈 상태 메시지 표시 | ✅ totalCount=0, items=[] |
|
||||
| 비인증 접근 차단 | 세션 없이 `PATCH /api/maestros/[id]`, `GET /api/maestros/[id]/students` 호출 | 401 응답 | ✅ 401 + `{"message":"Unauthorized"}` |
|
||||
|
||||
### 부가 수정 사항
|
||||
|
||||
- `proxy.ts` matcher 수정: `api/auth` → `api/` (API 라우트 전체를 미들웨어 제외 대상으로 변경)
|
||||
- 기존: API 라우트에 미들웨어가 개입하여 307 리다이렉트 반환
|
||||
- 수정 후: API 라우트는 `withApiHandler`가 직접 401 처리
|
||||
Reference in New Issue
Block a user