8.2 KiB
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 — 전달된 필드만 업데이트한다.
처리 순서
withApiHandler로 인증 자동 검사- 대상 maestro 존재 확인 (
findUnique) — 없으면 404 name이 포함된 경우: 동일 이름의 다른 마에스트로 존재 여부 확인 — 있으면 409- 변경 필드 추출 (전달값 vs 현재값 비교, 동일한 값이면 변경 대상에서 제외)
- 변경 필드가 없으면 200 +
{ changed: false }반환 (DB 업데이트 및 로그 기록 생략) - 트랜잭션:
maestro업데이트 +maestro_logINSERT (변경된 필드별로 각각 로그 기록) - 성공 응답: 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) |
제출 흐름
- 저장 버튼 클릭 → 확인 다이얼로그 표시
- 확인 →
PATCH /api/maestros/[id]호출, 저장 버튼 비활성화 - 성공 → 성공 토스트 +
router.refresh()로 화면 갱신 - 실패 → 실패 토스트 (이름 중복 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%) |
응답 형식
{
items: Array<{
playerID: number;
name: string;
enterCode: string;
accountType: number | null;
}>;
page: number;
pageSize: number;
totalCount: number;
totalPages: number;
q: string;
}
처리 순서
withApiHandler로 인증 검사- maestro 존재 확인 — 없으면 404
- 쿼리 파라미터 Zod 파싱
player테이블에서MaestroID조건 + 이름LIKE검색 + 페이지네이션 조회- 정렬:
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.tsmatcher 수정:api/auth→api/(API 라우트 전체를 미들웨어 제외 대상으로 변경)- 기존: API 라우트에 미들웨어가 개입하여 307 리다이렉트 반환
- 수정 후: API 라우트는
withApiHandler가 직접 401 처리