File name
Commit message
Commit date
File name
Commit message
Commit date
09-04
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:
member.guardians.length === 0
? EMPTY_FIELD_PLACEHOLDER
: member.guardians.map((guardian, index) => (
<div key={index}>
{joinPair(guardian.name, formatPhoneNumber(guardian.phoneNumber))}
</div>
)),
},
{
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
dismissible={false}
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>
);
}