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, 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 { formatPhoneNumber } from '@/lib/domain/phone-number';
import {
EMPTY_FIELD_PLACEHOLDER,
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줄 구성을 그대로 따랐다.
*
* 사용여부는 이 팝업에서 유일하게 수정 가능한 항목이다. 토글을 넘기면 화면이 바로 반응하고,
* 저장을 누르면 `PUT /api/v1/mngr/user/{userId}/{useYn}`로 나간다.
*
* 저장 버튼은 `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 formattedGradeClass = formatGradeClassNumber(member);
const gradeClassNumber =
formattedGradeClass === EMPTY_FIELD_PLACEHOLDER ? null : formattedGradeClass;
// 저장에 성공하면 닫는다. 목록은 Server Action의 revalidatePath가 다시 그린다.
useEffect(() => {
if (state.status === 'success') {
onClose();
}
}, [state, onClose]);
const items: FoxDescriptionItem[] = [
{ key: 'name', term: '이름', description: member.name },
{ key: 'loginId', term: 'ID', description: member.loginId },
{
key: 'phoneNumber',
term: '휴대전화 번호',
description: formatOptionalValue(formatPhoneNumber(member.phoneNumber)),
},
{
key: 'email',
term: '이메일',
description: formatOptionalValue(member.email),
},
{
key: 'birthDate',
term: '생년월일',
description: formatOptionalValue(member.birthDate),
},
{
key: 'guardian',
term: '보호자 이름 / 연락처',
description: joinPair(
member.guardianName,
formatPhoneNumber(member.guardianPhoneNumber)
),
},
{
key: 'school',
term: '학교 / 학년·반·번호',
description: joinPair(member.schoolName, gradeClassNumber),
},
{
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>
);
}