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,
useTransition,
} from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
import { FoxEmail } from '@fox/core/components/fox-email';
import { FoxInput } from '@fox/core/components/fox-input';
import { FoxModal } from '@fox/core/components/fox-modal';
import {
foxPasswordValidator,
foxValidationMessage,
foxValidators,
} from '@fox/core/validation';
import { FoxChatCenteredDotsIcon } from '@fox/core/icons';
import { useFeedback } from '@/app/_hooks/use-feedback';
import { DEFAULT_ADMIN_ROLE_CODE } from '@/lib/domain/admin-member';
import { DEFAULT_MOBILE_PHONE_PREFIX } from '@/lib/domain/phone-number';
import {
ADMIN_EMAIL_MESSAGES,
ADMIN_LOGIN_ID_HELP_TEXT,
ADMIN_LOGIN_ID_MESSAGES,
ADMIN_LOGIN_ID_VALIDATORS,
ADMIN_PASSWORD_ERROR_TEXT,
ADMIN_PASSWORD_HELP_TEXT,
ADMIN_PASSWORD_POLICY,
INITIAL_ADMIN_MEMBER_FORM_STATE,
adminEmailValidators,
} from '@/lib/domain/admin-member-form';
import {
checkAdminLoginId,
createAdminMemberAction,
type LoginIdCheckResult,
} from '../_actions';
import { AdminMemberFormFields } from './admin-member-form-fields';
import styles from './admin-member-modal.module.scss';
import { submitFormAction } from '@/app/_hooks/submit-form-action';
interface AdminMemberCreateModalProps {
onClose: () => void;
}
const EMAIL_DOMAIN_OPTIONS = [
'naver.com',
'gmail.com',
'daum.net',
'hanmail.net',
'nate.com',
];
export function AdminMemberCreateModal({
onClose,
}: AdminMemberCreateModalProps) {
const { showToast } = useFeedback();
const [state, formAction, isPending] = useActionState(
createAdminMemberAction,
INITIAL_ADMIN_MEMBER_FORM_STATE
);
const formRef = useRef<HTMLFormElement>(null);
const [loginId, setLoginId] = useState('');
const [checkResult, setCheckResult] = useState<LoginIdCheckResult>({
status: 'idle',
});
const [isChecking, startChecking] = useTransition();
const [phoneNumber, setPhoneNumber] = useState(DEFAULT_MOBILE_PHONE_PREFIX);
const [emailValue, setEmailValue] = useState(`@${EMAIL_DOMAIN_OPTIONS[0]}`);
const [isEmailTouched, setIsEmailTouched] = useState(false);
const [roleCode, setRoleCode] = useState<string>(DEFAULT_ADMIN_ROLE_CODE);
const [menuCodes, setMenuCodes] = useState<string[]>([]);
useEffect(() => {
if (state.status === 'success') {
showToast({ variant: 'success', message: '관리자를 등록했습니다.' });
onClose();
}
}, [state, showToast, onClose]);
const isLoginIdVerified =
checkResult.status === 'available' && checkResult.loginId === loginId.trim();
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
const [emailLocal = '', emailDomain = ''] = emailValue.split('@');
const email = emailLocal && emailDomain ? emailValue : '';
const emailMessage =
errors.email ??
(isEmailTouched
? foxValidationMessage(
foxValidators.compose(adminEmailValidators(true))(email),
ADMIN_EMAIL_MESSAGES
)
: undefined);
const loginIdFailure =
checkResult.status === 'unavailable' || checkResult.status === 'failed'
? checkResult.message
: undefined;
const loginIdMessage =
loginIdFailure ??
errors.loginId ??
(isLoginIdVerified ? '사용할 수 있는 ID입니다.' : ADMIN_LOGIN_ID_HELP_TEXT);
return (
<FoxModal
open
onClose={onClose}
size="sm"
title="관리자 등록"
actions={
<FoxButtonGroup size="md">
<FoxButton type="default" size="md" label="취소" onAction={onClose} />
<FoxButton
type="primary"
size="md"
label="저장"
loading={isPending}
disabled={!isLoginIdVerified}
onAction={() => formRef.current?.requestSubmit()}
/>
</FoxButtonGroup>
}
>
<form ref={formRef} onSubmit={(event) => submitFormAction(event, formAction)} className={styles.formList}>
<FoxInput
size="md"
name="name"
label="이름"
requirement="required"
placeholder="이름을 입력하세요."
message={errors.name}
invalid={Boolean(errors.name)}
/>
<div className={styles.fieldWithAction}>
<FoxInput
size="md"
name="loginId"
label="ID"
requirement="required"
placeholder="관리자 ID를 입력하세요."
autoComplete="off"
value={loginId}
onChange={setLoginId}
message={loginIdMessage}
messageIcon={<FoxChatCenteredDotsIcon />}
invalid={Boolean(loginIdFailure) || Boolean(errors.loginId)}
validators={ADMIN_LOGIN_ID_VALIDATORS}
validationMessages={ADMIN_LOGIN_ID_MESSAGES}
/>
<FoxButton
type="secondary"
size="md"
fullWidth
label="중복 확인"
loading={isChecking}
disabled={loginId.trim().length === 0}
onAction={() =>
startChecking(async () => {
try {
setCheckResult(await checkAdminLoginId(loginId));
} catch {
setCheckResult({
status: 'failed',
message: '중복 확인에 실패했습니다. 잠시 후 다시 시도해 주세요.',
});
}
})
}
/>
</div>
<FoxInput
size="md"
type="password"
name="password"
label="비밀번호"
requirement="required"
placeholder="비밀번호를 입력하세요."
autoComplete="new-password"
message={errors.password ?? ADMIN_PASSWORD_HELP_TEXT}
messageIcon={<FoxChatCenteredDotsIcon />}
invalid={Boolean(errors.password)}
validators={[foxPasswordValidator({ ...ADMIN_PASSWORD_POLICY, required: true })]}
validationMessages={{ minlength: ADMIN_PASSWORD_ERROR_TEXT }}
/>
<AdminMemberFormFields
phoneNumber={phoneNumber}
onPhoneNumberChange={setPhoneNumber}
roleCode={roleCode}
onRoleCodeChange={setRoleCode}
menuCodes={menuCodes}
onMenuCodesChange={setMenuCodes}
errors={errors}
emailField={
<>
<FoxEmail
label="이메일"
requirement="required"
domainOptions={[...EMAIL_DOMAIN_OPTIONS]}
localPlaceholder="ID"
value={emailValue}
onChange={(next) => {
setEmailValue(next);
setIsEmailTouched(true);
}}
message={emailMessage}
invalid={Boolean(emailMessage)}
/>
<input type="hidden" name="email" value={email} />
</>
}
/>
{state.status === 'error' && state.message && (
<p role="alert">{state.message}</p>
)}
</form>
</FoxModal>
);
}