File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useActionState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Field } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal';
import { RadioGroup } from '@/components/ui/radio-group';
import {
formatGradeClassNumber,
formatOptionalValue,
type StudentMember,
} from '@/lib/domain/student-member';
import {
updateStudentActiveStatus,
type UpdateStudentActiveStatusState,
} from '../_actions';
interface StudentDetailModalProps {
member: StudentMember;
onClose: () => void;
}
const ACTIVE_STATUS_OPTIONS = [
{ value: 'true', label: '활성' },
{ value: 'false', label: '비활성' },
];
const INITIAL_STATE: UpdateStudentActiveStatusState = { status: 'idle' };
const DETAIL_FORM_ID = 'student-active-status-form';
/**
* 사용여부 수정 가능 여부의 단일 토글. 백엔드에 사용여부 값도 변경 API도 없어 현재는 false다 —
* 변경 API가 생기면 이 상수를 true로 되돌리고 `_actions.ts`의 미지원 처리를 실제 구현으로
* 바꾸면 된다(라디오·저장 버튼의 비활성이 함께 풀린다).
*/
const IS_ACTIVE_STATUS_EDITABLE = false;
const ACTIVE_STATUS_UNSUPPORTED_NOTE =
'사용여부 변경은 백엔드 API 준비 후 제공됩니다.';
/**
* 학생 회원 조회 팝업 — 모든 항목이 readOnly다. 데이터는 목록 행에서 이미 갖고 있는 member를
* props로 그대로 받으므로 재조회하지 않는다(백엔드에 단건 조회 API도 아직 없다).
* 백엔드가 주지 않는 항목은 `-`로 표시한다.
*
* 사용여부는 원래 이 팝업에서 유일하게 수정 가능한 항목이었지만, 백엔드 목록 응답에 값이 없고
* 변경 API도 없어 지금은 라디오·저장 버튼을 비활성으로 둔다(`_actions.ts`의 미지원 처리와 짝).
* 폼·Server Action 배선은 그대로 남겨 두었으므로 변경 API가 생기면 `disabled`만 걷어내면 된다.
* 저장(Server Action) 성공 시 자동으로 닫히는 동작도 그대로다.
*
* Modal이 내부적으로 portal을 쓸 수도 있어(구현 미확정 — components/ui/modal.tsx는 design
* 레인 소관) 저장 버튼은 footer 슬롯에서 `form={DETAIL_FORM_ID}` 속성으로 폼과 연결한다.
* 네이티브 HTML의 `form` 속성은 버튼이 실제 DOM상 form의 자손이 아니어도 같은 문서 안에서
* id만 일치하면 그 form을 제출한다 — footer가 body와 다른 DOM 위치에 렌더돼도 안전하다.
*/
export function StudentDetailModal({ member, onClose }: StudentDetailModalProps) {
const [state, formAction, isPending] = useActionState(
updateStudentActiveStatus,
INITIAL_STATE
);
useEffect(() => {
if (state.status === 'success') {
onClose();
}
}, [state, onClose]);
const gradeClassNumber = formatGradeClassNumber(member);
return (
<Modal
title="학생 회원 조회"
onClose={onClose}
footer={
<>
<Button type="button" variant="ghost" onClick={onClose}>
취소
</Button>
<Button
type="submit"
form={DETAIL_FORM_ID}
variant="primary"
disabled={isPending || !IS_ACTIVE_STATUS_EDITABLE}
title={
IS_ACTIVE_STATUS_EDITABLE
? undefined
: ACTIVE_STATUS_UNSUPPORTED_NOTE
}
>
{isPending ? '저장 중...' : '저장'}
</Button>
</>
}
>
<form id={DETAIL_FORM_ID} action={formAction} className="flex flex-col gap-4">
<input type="hidden" name="id" value={member.id} />
<p className="text-body-sm text-foreground-muted">
본 화면의 모든 항목은 조회 전용입니다.
</p>
<Field label="이름">
<Input value={member.name} readOnly />
</Field>
<Field label="ID">
<Input value={member.loginId} readOnly />
</Field>
<Field label="휴대전화 번호">
<Input value={formatOptionalValue(member.phoneNumber)} readOnly />
</Field>
<Field label="이메일">
<Input value={formatOptionalValue(member.email)} readOnly />
</Field>
<Field label="생년월일">
<Input value={formatOptionalValue(member.birthDate)} readOnly />
</Field>
<Field label="보호자 이름">
<Input value={formatOptionalValue(member.guardianName)} readOnly />
</Field>
<Field label="보호자 연락처">
<Input
value={formatOptionalValue(member.guardianPhoneNumber)}
readOnly
/>
</Field>
<Field label="학교">
<Input value={formatOptionalValue(member.schoolName)} readOnly />
</Field>
<Field label="학년/반/번호">
<Input value={gradeClassNumber} readOnly />
</Field>
<Field label="사용여부">
<RadioGroup
name="isActive"
options={ACTIVE_STATUS_OPTIONS}
defaultValue={
member.isActive === null
? undefined
: String(member.isActive)
}
disabled={!IS_ACTIVE_STATUS_EDITABLE}
/>
</Field>
{!IS_ACTIVE_STATUS_EDITABLE && (
<p className="text-body-sm text-foreground-muted">
{ACTIVE_STATUS_UNSUPPORTED_NOTE}
</p>
)}
{state.status === 'error' && (
<p className="text-body-sm text-danger">{state.error}</p>
)}
<p className="text-body-sm text-foreground-muted">
회원 가입일 : {formatOptionalValue(member.joinedAt)}
</p>
</form>
</Modal>
);
}