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
File name
Commit message
Commit date
'use client';
import { useActionState, useEffect, useRef, useState } from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxInput } from '@fox/core/components/fox-input';
import { FoxModal } from '@fox/core/components/fox-modal';
import { FoxSelect } from '@fox/core/components/fox-select';
import { submitFormAction } from '@/app/_hooks/submit-form-action';
import { useFeedback } from '@/app/_hooks/use-feedback';
import {
ACTIVE_OPTIONS,
NEW_WINDOW_OPTIONS,
type AdminMenu,
type AdminMenuNode,
} from '@/lib/domain/admin-menu';
import {
ADMIN_MENU_DESCRIPTION_MESSAGES,
ADMIN_MENU_NAME_MESSAGES,
ADMIN_MENU_URL_MESSAGES,
DESCRIPTION_VALIDATORS,
INITIAL_ADMIN_MENU_FORM_STATE,
NAME_VALIDATORS,
URL_VALIDATORS,
} from '@/lib/domain/admin-menu-form';
import { createAdminMenuAction, updateAdminMenuAction } from '../_actions';
import styles from './admin-menu-modal.module.scss';
export type AdminMenuRole = { id: string; name: string };
interface AdminMenuModalProps {
/** 등록이면 상위가 될 메뉴, 수정이면 대상의 상위. 최상위 메뉴 수정 시에는 없다. */
parent: AdminMenu | null;
/** 없으면 등록 팝업이다. */
menu?: AdminMenuNode;
roles: AdminMenuRole[];
nextSortOrder: number;
onClose: () => void;
}
export function AdminMenuModal({
parent,
menu,
roles,
nextSortOrder,
onClose,
}: AdminMenuModalProps) {
const isEdit = menu !== undefined;
const { showToast } = useFeedback();
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction, isPending] = useActionState(
isEdit ? updateAdminMenuAction : createAdminMenuAction,
INITIAL_ADMIN_MENU_FORM_STATE
);
const [roleId, setRoleId] = useState(menu?.roleId ?? roles[0]?.id ?? '');
const [isTopActive, setIsTopActive] = useState(menu?.isTopActive ?? false);
const [opensNewWindow, setOpensNewWindow] = useState(true);
const [isActive, setIsActive] = useState(menu?.isActive ?? true);
useEffect(() => {
if (state.status === 'success') {
showToast({
variant: 'success',
message: isEdit ? '메뉴를 수정했습니다.' : '메뉴를 등록했습니다.',
});
onClose();
}
}, [state, showToast, onClose, isEdit]);
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
return (
<FoxModal
open
size="sm"
title={isEdit ? '수정' : '신규 등록'}
onClose={onClose}
actions={
<>
<FoxButton type="default" size="md" label="취소" onAction={onClose} />
<FoxButton
type="primary"
size="md"
label={isEdit ? '수정' : '저장'}
loading={isPending}
onAction={() => formRef.current?.requestSubmit()}
/>
</>
}
>
<form
ref={formRef}
onSubmit={(event) => submitFormAction(event, formAction)}
className={styles.fields}
>
{isEdit ? (
<input type="hidden" name="menuId" value={menu.id} />
) : (
<input type="hidden" name="parentId" value={parent?.id ?? ''} />
)}
{parent && (
<FoxInput
size="md"
label="상위메뉴명"
requirement="required"
value={`[${parent.id}] ${parent.name}`}
readOnly
/>
)}
<FoxInput
size="md"
name="name"
label="메뉴명"
requirement="required"
placeholder="메뉴명을 입력하세요."
defaultValue={menu?.name ?? ''}
invalid={Boolean(errors.name)}
message={errors.name}
validators={NAME_VALIDATORS}
validationMessages={ADMIN_MENU_NAME_MESSAGES}
/>
<FoxInput
size="md"
type="number"
name="sortOrder"
label="메뉴순번"
requirement="required"
min={1}
step={1}
defaultValue={menu?.sortOrder ?? nextSortOrder}
invalid={Boolean(errors.sortOrder)}
message={errors.sortOrder}
/>
<div>
<FoxSelect
size="md"
label="메뉴권한"
requirement="required"
options={roles.map((role) => ({ value: role.id, label: role.name }))}
placeholder="메뉴권한을 선택하세요."
value={roleId}
onValueChange={setRoleId}
error={Boolean(errors.roleId)}
hint={errors.roleId}
/>
<input type="hidden" name="roleId" value={roleId} />
</div>
<div>
<FoxSelect
size="md"
label="최상위권한활성"
requirement="required"
options={ACTIVE_OPTIONS.map((option) => ({ ...option }))}
value={String(isTopActive)}
onValueChange={(value) => setIsTopActive(value === 'true')}
/>
<input type="hidden" name="isTopActive" value={String(isTopActive)} />
</div>
<FoxInput
size="md"
name="url"
label="메뉴링크"
requirement="required"
placeholder="메뉴링크를 입력하세요."
defaultValue={menu?.url ?? ''}
invalid={Boolean(errors.url)}
message={errors.url}
validators={URL_VALIDATORS}
validationMessages={ADMIN_MENU_URL_MESSAGES}
/>
{/* 이슈: 백엔드에 대응 컬럼이 없어 저장되지 않는다(기획대로 화면에만 둔다). */}
<div>
<FoxSelect
size="md"
label="새창여부"
requirement="required"
options={NEW_WINDOW_OPTIONS.map((option) => ({ ...option }))}
value={String(opensNewWindow)}
onValueChange={(value) => setOpensNewWindow(value === 'true')}
/>
<input
type="hidden"
name="opensNewWindow"
value={String(opensNewWindow)}
/>
</div>
<FoxInput
size="md"
name="description"
label="메뉴설명"
requirement="required"
placeholder="메뉴설명을 입력하세요."
defaultValue={menu?.description ?? ''}
invalid={Boolean(errors.description)}
message={errors.description}
validators={DESCRIPTION_VALIDATORS}
validationMessages={ADMIN_MENU_DESCRIPTION_MESSAGES}
/>
<div>
<FoxSelect
size="md"
label="활성여부"
requirement="required"
options={ACTIVE_OPTIONS.map((option) => ({ ...option }))}
value={String(isActive)}
onValueChange={(value) => setIsActive(value === 'true')}
/>
<input type="hidden" name="isActive" value={String(isActive)} />
</div>
{state.status === 'error' && state.message && (
<p role="alert">{state.message}</p>
)}
</form>
</FoxModal>
);
}