# 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`)의 "기본 정보" 섹션을 읽기 전용 표시에서 편집 가능한 폼으로 교체한다. **필드 구성** | 필드 | 컴포넌트 | 비고 | |---|---|---| | 이름 | `` | | | 이메일 | `` | | | 상태 | `` | `ACCOUNT_TYPE_LABELS` 기반, 유료 타입만 | | 사용 가능일 | `` | | | 입장코드 수정 | ``) + 검색 버튼 - 전체 학생 수 표시 (`전체 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 처리