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, useRef, useState } from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import {
FoxDescriptionList,
type FoxDescriptionItem,
} from '@fox/core/components/fox-description-list';
import { FoxModal } from '@fox/core/components/fox-modal';
import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
import {
formatGradeClassNumber,
formatOptionalValue,
type StudentMember,
} from '@/lib/domain/student-member';
import {
updateStudentActiveStatus,
type UpdateStudentActiveStatusState,
} from '../_actions';
import styles from './student-detail-modal.module.scss';
interface StudentDetailModalProps {
member: StudentMember;
onClose: () => void;
}
const INITIAL_STATE: UpdateStudentActiveStatusState = { status: 'idle' };
const DETAIL_FORM_ID = 'student-active-status-form';
/** 시안은 두 값을 " / "로 묶는다("홍라애 / 010-2222-1234"). 둘 다 없으면 `-`다. */
function joinPair(first: string | null, second: string | null) {
const parts = [first, second].filter(
(value): value is string => value !== null && value !== ''
);
return parts.length > 0 ? parts.join(' / ') : '-';
}
/**
* 학생 회원 조회 팝업 — 시안: 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) modal 5227:809
*
* 모든 항목이 조회 전용이다. 데이터는 목록 행이 이미 갖고 있는 member를 그대로 받으므로
* 재조회하지 않는다(백엔드에 단건 조회 API도 아직 없다). 백엔드가 주지 않는 값은 `-`다.
*
* 시안은 보호자(이름/연락처)와 학교(학교/학년·반·번호)를 각각 한 줄로 묶는다 — 종전처럼
* 항목을 따로 두지 않고 시안의 8줄 구성을 그대로 따랐다.
*
* 사용여부는 이 팝업에서 유일하게 수정 가능한 항목이지만, 백엔드 목록 응답에 값이 없고 변경
* API도 없어 토글·저장 버튼을 비활성으로 둔다(`_actions.ts`의 미지원 처리와 짝). 폼·Server
* Action 배선은 남겨 두었으므로 API가 생기면 `IS_ACTIVE_STATUS_EDITABLE`만 되돌리면 된다.
*
* 저장 버튼은 `form={DETAIL_FORM_ID}`로 폼과 이어 둔다 — FoxModal이 버튼을 `foot` 슬롯에
* 그려 폼의 자손이 아니게 되는데, 네이티브 `form` 속성은 같은 문서 안에서 id만 맞으면 그
* 폼을 제출한다.
*/
export function StudentDetailModal({ member, onClose }: StudentDetailModalProps) {
const [state, formAction, isPending] = useActionState(
updateStudentActiveStatus,
INITIAL_STATE
);
const formRef = useRef<HTMLFormElement>(null);
// 토글은 화면에서 바로 반응해야 하므로 이 컴포넌트가 상태를 갖는다 — prop에서 계산하면
// 스위치를 넘겨도 라벨이 그대로다. 저장이 되지 않으므로 이 값은 화면에만 머문다.
const [isActive, setIsActive] = useState(member.isActive ?? false);
const items: FoxDescriptionItem[] = [
{ key: 'name', term: '이름', description: member.name },
{ key: 'loginId', term: 'ID', description: member.loginId },
{
key: 'phoneNumber',
term: '휴대전화 번호',
description: formatOptionalValue(member.phoneNumber),
},
{
key: 'email',
term: '이메일',
description: formatOptionalValue(member.email),
},
{
key: 'birthDate',
term: '생년월일',
description: formatOptionalValue(member.birthDate),
},
{
key: 'guardian',
term: '보호자 이름 / 연락처',
description: joinPair(member.guardianName, member.guardianPhoneNumber),
},
{
key: 'school',
term: '학교 / 학년·반·번호',
description: joinPair(
member.schoolName,
formatGradeClassNumber(member) === '-'
? null
: formatGradeClassNumber(member)
),
},
{
key: 'isActive',
term: '사용여부',
description: (
<FoxToggleSwitch
name="isActive"
value="true"
label={isActive ? '활성' : '비활성'}
checked={isActive}
onChange={(checked) => setIsActive(checked)}
/>
),
},
];
return (
<FoxModal
open
size="sm"
title="학생 회원 조회"
onClose={onClose}
utility={
state.status === 'error' ? (
<p className={styles.error} role="alert">
{state.error}
</p>
) : undefined
}
actions={
<>
<FoxButton
type="default"
size="md"
label="취소"
onAction={onClose}
/>
{/* FoxButton에는 폼을 가리키는 prop이 없다(네이티브 `form` 속성 미노출). 버튼이
foot 슬롯에 그려져 폼의 자손이 아니므로 ref로 직접 제출한다. */}
<FoxButton
type="primary"
size="md"
label={isPending ? '저장 중...' : '저장'}
disabled={isPending}
onAction={() => formRef.current?.requestSubmit()}
/>
</>
}
>
<form
ref={formRef}
id={DETAIL_FORM_ID}
action={formAction}
className={styles.form}
>
<input type="hidden" name="id" value={member.id} />
<FoxDescriptionList items={items} />
</form>
</FoxModal>
);
}