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 { useId, type ReactNode } from 'react';
import { FoxChip } from '@fox/core/components/fox-chip';
import { FoxChipArea } from '@fox/core/components/fox-chip-area';
import { FoxFormLabel } from '@fox/core/components/fox-form-label';
import { FoxPhoneNumber } from '@fox/core/components/fox-phone-number';
import { FoxSelect } from '@fox/core/components/fox-select';
import {
ADMIN_MENU_OPTIONS,
ADMIN_ROLE_OPTIONS,
} from '@/lib/domain/admin-member';
import type { AdminMemberFormErrors } from '@/lib/domain/admin-member-form';
import { MOBILE_PHONE_PREFIXES } from '@/lib/domain/phone-number';
import styles from './admin-member-modal.module.scss';
interface AdminMemberFormFieldsProps {
/** 숫자만 담긴 휴대전화번호. 하이픈은 화면이 그리고 값에는 넣지 않는다. */
phoneNumber: string;
onPhoneNumberChange: (phoneNumber: string) => void;
roleCode: string;
onRoleCodeChange: (roleCode: string) => void;
menuCodes: string[];
onMenuCodesChange: (menuCodes: string[]) => void;
/**
* 휴대전화 번호와 역할 선택 **사이**에 들어가는 이메일 칸. 두 시안이 이 자리에 서로 다른 것을
* 놓아(등록은 도메인 셀렉트가 붙은 form-email, 수정은 단일 input) 슬롯으로 받는다 — 자리 순서는
* 두 시안이 같으므로 여기가 갖고, 무엇을 놓을지는 각 팝업이 정한다.
*/
emailField: ReactNode;
errors: AdminMemberFormErrors;
}
/**
* 등록·수정 팝업이 공유하는 입력 항목 — 휴대전화 번호 / (이메일 슬롯) / 역할 선택 / 메뉴 선택.
* 두 시안(ADM_ADM_102_p 5227:2035 / ADM_ADM_103_p 5227:3351)에서 자리와 순서가 같은 부분이다.
* 이름·ID·비밀번호·잠김여부는 두 시안이 서로 달라 각 팝업이 직접 그린다.
*
* 세 칸 모두 @fox의 제어 위젯이라 값이 FormData에 실리지 않는다(`FoxSelect`는 네이티브 select가
* 아니라 버튼+리스트박스, `FoxPhoneNumber`·칩도 마찬가지다). 그래서 값은 호출부가 들고, 제출용
* hidden input을 여기서 함께 낸다 — 폼 제출 규약(Server Action + FormData)을 그대로 두기 위해서다.
*
* ⚠️ **메뉴 선택은 저장되지 않는다.** 백엔드에 관리자별 메뉴 권한이 테이블·VO·SQL 어디에도 없다
* (`/api/v1/common/menu`는 개인 북마크용이다). 시안대로 자리는 유지하되 **필수 검증은 걸지 않는다**
* (사용자 확정) — 저장되지도 않는 값 때문에 등록이 막히면 안 되기 때문이다. 권한 API가 생기면
* 여기는 그대로 두고 Server Action이 값을 넘기기만 하면 된다.
*/
export function AdminMemberFormFields({
phoneNumber,
onPhoneNumberChange,
roleCode,
onRoleCodeChange,
menuCodes,
onMenuCodesChange,
emailField,
errors,
}: AdminMemberFormFieldsProps) {
const phoneLabelId = useId();
const menuLabelId = useId();
const allMenuValues = ADMIN_MENU_OPTIONS.map((option) => option.value);
const isAllSelected = menuCodes.length === allMenuValues.length;
function toggleMenu(value: string) {
onMenuCodesChange(
menuCodes.includes(value)
? menuCodes.filter((code) => code !== value)
: [...menuCodes, value]
);
}
return (
<>
{/* 칸이 셋으로 나뉘어 있어 `<label htmlFor>`가 가리킬 대상이 하나가 아니다 — 이름 연결은
감싼 group이 aria-labelledby로 한다(FoxFormLabel의 `as="span"`이 그 용도다). */}
<div className={styles.field} role="group" aria-labelledby={phoneLabelId}>
<FoxFormLabel as="span" id={phoneLabelId} requirement="required">
휴대전화 번호
</FoxFormLabel>
<FoxPhoneNumber
type="unit"
// 넘기지 않으면 앞자리 셀렉트가 빈 목록이라 아무것도 고를 수 없다(상수 주석 참조).
prefixOptions={[...MOBILE_PHONE_PREFIXES]}
value={phoneNumber}
onChange={onPhoneNumberChange}
message={errors.phoneNumber}
invalid={Boolean(errors.phoneNumber)}
/>
</div>
<input type="hidden" name="phoneNumber" value={phoneNumber} />
{emailField}
<FoxSelect
size="md"
label="역할 선택"
options={ADMIN_ROLE_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={roleCode}
onValueChange={onRoleCodeChange}
error={Boolean(errors.roleCode)}
hint={errors.roleCode}
/>
<input type="hidden" name="roleCode" value={roleCode} />
<div className={styles.field} role="group" aria-labelledby={menuLabelId}>
<FoxFormLabel as="span" id={menuLabelId} requirement="required">
메뉴 선택
</FoxFormLabel>
<FoxChipArea size="md" wrap labelledBy={menuLabelId}>
{/* 시안의 첫 칩. 개별 메뉴가 아니라 나머지를 한 번에 켜고 끄는 조각이다. */}
<FoxChip
type="check"
label="전체"
checked={isAllSelected}
onCheckedChange={(checked) =>
onMenuCodesChange(checked ? allMenuValues : [])
}
/>
{ADMIN_MENU_OPTIONS.map((option) => (
<FoxChip
key={option.value}
type="check"
label={option.label}
checked={menuCodes.includes(option.value)}
onCheckedChange={() => toggleMenu(option.value)}
/>
))}
</FoxChipArea>
</div>
{menuCodes.map((code) => (
<input key={code} type="hidden" name="menuCodes" value={code} />
))}
</>
);
}