'use client'; import { useActionState, useEffect, useState, useTransition } from 'react'; import { Button } from '@/components/ui/button'; import { Field } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; import { Modal } from '@/components/ui/modal'; import { useFeedback } from '@/app/_hooks/use-feedback'; import { ADMIN_LOGIN_ID_HELP_TEXT, ADMIN_PASSWORD_HELP_TEXT, INITIAL_ADMIN_MEMBER_FORM_STATE, } from '@/lib/domain/admin-member-form'; import { checkAdminLoginId, createAdminMemberAction, type LoginIdCheckResult, } from '../_actions'; import { AdminMemberFormFields, FieldError, } from './admin-member-form-fields'; interface AdminMemberCreateModalProps { onClose: () => void; } const FORM_ID = 'admin-member-create-form'; /** * 관리자 등록 팝업(시안 ADM_ADM_102_p). * * **ID는 중복 확인을 통과해야 저장할 수 있다**(시안 ①). 확인은 등록 폼과 별개의 서버 호출인데, * 폼 안에 폼을 중첩할 수 없어 `useActionState` 대신 값을 직접 넘기는 Server Action * (`checkAdminLoginId`)을 `useTransition`으로 호출한다. 확인 후 사용자가 ID를 고칠 수 있으므로 * "확인에 성공한 ID"와 "지금 입력창의 값"이 같을 때만 통과로 본다 — 확인만 받아 두고 다른 ID로 * 바꿔 저장하는 경로를 막기 위해서다. 그래도 최종 방어선은 Server Action의 재확인이다. * * 저장 버튼은 footer 슬롯에서 `form={FORM_ID}` 속성으로 폼과 연결한다 — 네이티브 HTML의 `form` * 속성은 버튼이 실제 DOM상 form의 자손이 아니어도 같은 문서 안에서 id만 일치하면 그 form을 * 제출한다. * * 비밀번호는 형식만 검증하고 저장되지 않는다 — 백엔드에 등록 API가 없어 지금은 mock 저장소가 * 받는데, 평문 비밀번호를 메모리에 들고 있지 않기로 했기 때문이다(Repository·mock 주석 참조). */ export function AdminMemberCreateModal({ onClose, }: AdminMemberCreateModalProps) { const { showToast } = useFeedback(); const [state, formAction, isPending] = useActionState( createAdminMemberAction, INITIAL_ADMIN_MEMBER_FORM_STATE ); const [loginId, setLoginId] = useState(''); const [checkResult, setCheckResult] = useState({ status: 'idle', }); const [isChecking, startChecking] = useTransition(); useEffect(() => { if (state.status === 'success') { showToast({ variant: 'success', message: '관리자를 등록했습니다.' }); onClose(); } }, [state, showToast, onClose]); const isLoginIdVerified = checkResult.status === 'available' && checkResult.loginId === loginId.trim(); function handleCheckLoginId() { startChecking(async () => { setCheckResult(await checkAdminLoginId(loginId)); }); } const errors = state.status === 'error' ? (state.errors ?? {}) : {}; return ( } >

* 는 필수 항목입니다.

setLoginId(event.target.value)} placeholder="관리자 ID를 입력하세요." autoComplete="off" />

{ADMIN_LOGIN_ID_HELP_TEXT}

{checkResult.status === 'unavailable' && ( )} {isLoginIdVerified && (

사용할 수 있는 ID입니다.

)}

{ADMIN_PASSWORD_HELP_TEXT}

{state.status === 'error' && state.message && (

{state.message}

)}
); }