import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, } from '@/components/ui/table'; import { formatGradeClassNumber, formatOptionalValue, type StudentMember, } from '@/lib/domain/student-member'; import { StudentRowActions } from './student-row-actions'; interface StudentTableProps { items: StudentMember[]; page: number; pageSize: number; } const COLUMN_HEADERS = [ '번호', '회원코드', '이름', '아이디', '휴대전화번호', '이메일', '역할', '학교', '학년/반/번호', '보호자이름', '보호자연락처', '가입일', '관리', ]; /** * 학생 회원 목록 테이블 — 조회 전용(등록/수정/삭제 없음). "번호"는 저장된 값이 아니라 * 현재 페이지 기준 표시 순번(= (page-1)*pageSize + 행 인덱스 + 1)이다. 개인정보 마스킹은 * 기획 시안에서 명시적으로 해제 지시가 있어 원본 값을 그대로 노출한다. * * 백엔드가 아직 주지 않는 항목(휴대전화·이메일·학교·학년/반/번호·보호자·가입일)은 열을 그대로 * 유지한 채 `-`로 표시한다 — 백엔드가 필드를 추가하면 Repository 매핑만 늘리면 이 파일은 * 그대로 값이 채워진다. * * "관리" 열은 행별 조회 팝업 트리거(StudentRowActions)에 위임한다 — 상호작용이 필요한 것은 * 그 셀뿐이라 이 테이블 자체는 Server Component로 유지하고 최말단만 클라이언트 경계로 뗀다 * (§2.4 "'use client'는 최말단에만"). */ export function StudentTable({ items, page, pageSize }: StudentTableProps) { return ( {COLUMN_HEADERS.map((header) => ( {header} ))} {items.map((member, index) => ( {(page - 1) * pageSize + index + 1} {member.memberCode} {member.name} {member.loginId} {formatOptionalValue(member.phoneNumber)} {formatOptionalValue(member.email)} {member.role} {formatOptionalValue(member.schoolName)} {formatGradeClassNumber(member)} {formatOptionalValue(member.guardianName)} {formatOptionalValue(member.guardianPhoneNumber)} {formatOptionalValue(member.joinedAt)} ))}
); }