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 { FoxFileUpload } from '@fox/core/components/fox-file-upload';
import { FoxFormLabel } from '@fox/core/components/fox-form-label';
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
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 { FoxTextArea } from '@fox/core/components/fox-text-area';
import { FoxToggleSwitch } from '@fox/core/components/fox-toggle-switch';
import { submitFormAction } from '@/app/_hooks/submit-form-action';
import { useFeedback } from '@/app/_hooks/use-feedback';
import type { CommonCode } from '@/lib/domain/common-code';
import {
BOARD_CATEGORY_OPTIONS,
INQUIRY_TYPE_OPTIONS,
type BoardPost,
type BoardType,
} from '@/lib/domain/board-post';
import {
CONTENT_MAX_LENGTH,
INITIAL_BOARD_POST_FORM_STATE,
TITLE_MAX_LENGTH,
} from '@/lib/domain/board-post-form';
import { createBoardPostAction, updateBoardPostAction } from '../_actions';
import styles from './board-post-form.module.scss';
interface BoardPostFormModalProps {
boardType: BoardType;
mode: 'create' | 'edit';
post?: BoardPost;
/** 공통코드 `BBS_FAQ_CD` — FAQ의 유형 선택지. 다른 게시판에서는 쓰이지 않는다. */
faqTypes?: readonly CommonCode[];
onClose: () => void;
}
const BOARD_TITLES: Record<BoardType, string> = {
notice: '공지사항',
inquiry: '1:1문의',
faq: 'FAQ',
};
const STATIC_TYPE_OPTIONS: Partial<
Record<BoardType, readonly { value: string; label: string }[]>
> = {
inquiry: INQUIRY_TYPE_OPTIONS,
};
export function BoardPostFormModal({
boardType,
mode,
post,
faqTypes = [],
onClose,
}: BoardPostFormModalProps) {
const isCreate = mode === 'create';
const { showToast } = useFeedback();
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction, isPending] = useActionState(
isCreate ? createBoardPostAction : updateBoardPostAction,
INITIAL_BOARD_POST_FORM_STATE
);
const [categoryCode, setCategoryCode] = useState(
post?.categoryCode || BOARD_CATEGORY_OPTIONS[0].value
);
const [typeCode, setTypeCode] = useState(post?.typeCode ?? '');
const [isPinned, setIsPinned] = useState(post?.isPinned ?? false);
const [isVisible, setIsVisible] = useState(post?.isVisible ?? true);
const [isPushEnabled, setIsPushEnabled] = useState(post?.isPushEnabled ?? false);
useEffect(() => {
if (state.status === 'success') {
showToast({
variant: 'success',
message: isCreate ? '게시물을 등록했습니다.' : '게시물을 수정했습니다.',
});
onClose();
}
}, [state, showToast, onClose, isCreate]);
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
const typeOptions =
boardType === 'faq'
? faqTypes.map((code) => ({ value: code.code, label: code.label }))
: STATIC_TYPE_OPTIONS[boardType];
const hasNoticeOnlyFields = boardType === 'notice';
return (
<FoxModal
open
size="md"
title={`${BOARD_TITLES[boardType]} ${isCreate ? '신규 등록' : '수정'}`}
onClose={onClose}
actions={
<>
<FoxButton type="default" size="md" label="취소" onAction={onClose} />
<FoxButton
type="primary"
size="md"
label={isPending ? '저장 중...' : isCreate ? '등록' : '수정'}
disabled={isPending}
onAction={() => formRef.current?.requestSubmit()}
/>
</>
}
>
<form
ref={formRef}
onSubmit={(event) => submitFormAction(event, formAction)}
className={styles.fields}
>
<input type="hidden" name="boardType" value={boardType} />
{!isCreate && post && <input type="hidden" name="id" value={post.id} />}
<div className={styles.field}>
<FoxSelect
size="md"
label="구분"
options={BOARD_CATEGORY_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={categoryCode}
onValueChange={setCategoryCode}
error={Boolean(errors.categoryCode)}
hint={errors.categoryCode}
/>
<input type="hidden" name="categoryCode" value={categoryCode} />
</div>
{typeOptions && typeOptions.length > 0 && (
<div className={styles.field}>
<FoxSelect
size="md"
label="유형"
options={typeOptions.map((option) => ({
value: option.value,
label: option.label,
}))}
value={typeCode}
onValueChange={setTypeCode}
placeholder="유형을 선택하세요."
error={Boolean(errors.typeCode)}
hint={errors.typeCode}
/>
<input type="hidden" name="typeCode" value={typeCode} />
</div>
)}
<div className={styles.field}>
<FoxInput
size="md"
name="title"
label="제목"
requirement="required"
defaultValue={post?.title ?? ''}
placeholder="제목을 입력하세요."
invalid={Boolean(errors.title)}
message={errors.title}
maxLength={TITLE_MAX_LENGTH}
/>
</div>
<div className={styles.field}>
<FoxTextArea
name="content"
label="내용"
requirement="required"
defaultValue={post?.content ?? ''}
placeholder="내용을 입력하세요."
rows={8}
counter
maxLength={CONTENT_MAX_LENGTH}
invalid={Boolean(errors.content)}
message={errors.content}
/>
</div>
<div className={styles.field}>
<FoxFileUpload
mode="default"
name="attachment"
label="첨부파일"
selectLabel="파일선택"
placeholder="파일을 선택해 주세요."
/>
<input
type="hidden"
name="attachmentId"
defaultValue={post?.attachmentId ?? ''}
/>
</div>
{hasNoticeOnlyFields && (
<div className={styles.field}>
<FoxFormLabel as="span">상단고정</FoxFormLabel>
<FoxToggleSwitch
size="md"
name="isPinned"
value="Y"
label={isPinned ? '고정' : '미고정'}
checked={isPinned}
onChange={setIsPinned}
/>
</div>
)}
{hasNoticeOnlyFields && (
<div className={styles.field}>
<FoxFormLabel as="span">게시기간</FoxFormLabel>
<div className={styles.period}>
<FoxInput
size="md"
type="date"
name="startDate"
aria-label="게시 시작일"
defaultValue={post?.startDate ?? ''}
invalid={Boolean(errors.startDate)}
/>
<span aria-hidden="true">~</span>
<FoxInput
size="md"
type="date"
name="endDate"
aria-label="게시 종료일"
defaultValue={post?.endDate ?? ''}
invalid={Boolean(errors.endDate)}
/>
</div>
{(errors.startDate ?? errors.endDate) && (
<FoxHelperText
type="danger"
message={errors.startDate ?? errors.endDate ?? ''}
/>
)}
</div>
)}
<div className={styles.field}>
<FoxFormLabel as="span">사용여부</FoxFormLabel>
<FoxToggleSwitch
size="md"
name="isVisible"
value="Y"
label={isVisible ? '노출' : '미노출'}
checked={isVisible}
onChange={setIsVisible}
/>
</div>
{hasNoticeOnlyFields && (
<div className={styles.field}>
<FoxFormLabel as="span">앱푸쉬 설정</FoxFormLabel>
<FoxToggleSwitch
size="md"
name="isPushEnabled"
value="Y"
label={isPushEnabled ? '발송' : '미발송'}
checked={isPushEnabled}
onChange={setIsPushEnabled}
/>
</div>
)}
{state.status === 'error' && state.message && (
<FoxHelperText type="danger" message={state.message} />
)}
</form>
</FoxModal>
);
}