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 { useState, useActionState } from 'react';
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
import { FoxInput } from '@fox/core/components/fox-input';
import { FoxPageHeader } from '@fox/core/components/fox-page-header';
import { FoxRadio } from '@fox/core/components/fox-radio';
import { FoxRadioGroup } from '@fox/core/components/fox-radio-group';
import { FoxSelect } from '@fox/core/components/fox-select';
import { FoxTextArea } from '@fox/core/components/fox-text-area';
import { submitFormAction } from '@/app/_hooks/submit-form-action';
import {
ANSWER_MESSAGE_ETC,
ANSWER_MESSAGE_OPTIONS,
QUIZ_ANSWER_OPTIONS,
QUIZ_LEVEL_OPTIONS,
type QuizItem,
type GradesBySchool,
} from '@/lib/domain/content';
import {
ANSWER_MESSAGE_MAX_LENGTH,
INITIAL_CONTENT_FORM_STATE,
MESSAGE_MAX_LENGTH,
QUESTION_MAX_LENGTH,
} from '@/lib/domain/content-form';
import { contentBreadcrumb, contentListPath } from '@/lib/domain/content-query';
import type { CommonCode } from '@/lib/domain/common-code';
import { createQuizAction, updateQuizAction } from '../../_actions';
import { ContentFormActions } from '../../_components/content-form-actions';
import { ContentFormRow } from '../../_components/content-form-row';
import { ContentGradeField } from '../../_components/content-grade-field';
import { ContentKeywordField } from '../../_components/content-keyword-field';
import { ContentVisibilityField } from '../../_components/content-visibility-field';
import styles from '../../_components/content-form.module.scss';
interface QuizFormProps {
schools: readonly CommonCode[];
gradesBySchool: GradesBySchool;
item?: QuizItem;
}
/**
* 콘텐츠 등록·수정 (금융OX퀴즈) — 시안 5402:12360.
*
* 정답여부는 O·X 두 줄이고 각 줄이 「사용자에게 노출할 문구」를 갖는다. 두 문구 모두
* 백엔드로 보낸다 — 정답이 아닌 쪽에도 보여 줄 문구가 필요하기 때문이다(사용자 확정).
* 「기타」를 고르면 문구를 직접 적는다. 앞의 라디오는 어느 쪽이 정답인지만 정한다.
*/
export function QuizForm({ schools, gradesBySchool, item }: QuizFormProps) {
const isEdit = item !== undefined;
const [state, formAction, isPending] = useActionState(
isEdit ? updateQuizAction : createQuizAction,
INITIAL_CONTENT_FORM_STATE
);
const [answer, setAnswer] = useState(item?.answer || QUIZ_ANSWER_OPTIONS[0]);
// O·X 각각의 노출 문구. 정답이 아닌 쪽도 보내므로 두 벌을 따로 들고 있는다.
const [messages, setMessages] = useState<
Record<string, { code: string; text: string }>
>(() =>
Object.fromEntries(
QUIZ_ANSWER_OPTIONS.map((option) => [
option,
{ code: ANSWER_MESSAGE_OPTIONS[0].value, text: '' },
])
)
);
const patchMessage = (
option: string,
patch: Partial<{ code: string; text: string }>
) =>
setMessages((current) => ({
...current,
[option]: { ...current[option], ...patch },
}));
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
return (
<>
<FoxPageHeader
title={`콘텐츠 ${isEdit ? '수정' : '등록'} (금융OX퀴즈)`}
breadcrumb={contentBreadcrumb('quiz', isEdit ? '수정' : '등록')}
/>
<form
onSubmit={(event) => submitFormAction(event, formAction)}
className={styles.form}
>
{isEdit && <input type="hidden" name="quizId" value={item.quizId} />}
{/* 화면에 입력 칸이 없는 값이라 그대로 돌려보내 지워지지 않게 한다(도메인 주석 참조). */}
{isEdit && (
<ContentFormRow label="콘텐츠ID">
<FoxInput size="md" value={item.quizId} readOnly />
</ContentFormRow>
)}
<ContentFormRow label="학년선택" required>
<ContentGradeField
schools={schools}
gradesBySchool={gradesBySchool}
defaultSchoolCode={item?.schoolCode}
defaultGrades={item?.grades}
schoolError={errors.schoolCode}
gradesError={errors.grades}
/>
</ContentFormRow>
<ContentFormRow label="난이도" required>
<FoxRadioGroup name="level" size="md" label="난이도" orientation="horizontal">
{QUIZ_LEVEL_OPTIONS.map((option) => (
<FoxRadio
key={option.value}
value={option.value}
label={option.label}
defaultChecked={(item?.level || QUIZ_LEVEL_OPTIONS[1].value) === option.value}
/>
))}
</FoxRadioGroup>
{errors.level && <FoxHelperText type="danger" message={errors.level} />}
</ContentFormRow>
<ContentFormRow label="키워드">
<ContentKeywordField
defaultKeywords={
item ? item.keywords : undefined
}
error={errors.keywords}
/>
</ContentFormRow>
<ContentFormRow label="질문" required>
<FoxTextArea
name="question"
aria-label="질문"
defaultValue={item?.question ?? ''}
rows={3}
placeholder="질문을 입력하세요."
maxLength={QUESTION_MAX_LENGTH}
counter
invalid={Boolean(errors.question)}
message={errors.question}
/>
</ContentFormRow>
<ContentFormRow label="정답여부" required>
<div className={styles.answerLines}>
{QUIZ_ANSWER_OPTIONS.map((option) => (
<div key={option} className={styles.inline}>
<FoxRadio
name="answer"
size="md"
value={option}
label={option}
checked={answer === option}
onChange={setAnswer}
/>
<div className={styles.answerMessage}>
<FoxSelect
size="md"
ariaLabel={`${option} 노출 문구`}
options={ANSWER_MESSAGE_OPTIONS.map((message) => ({
value: message.value,
label: message.label,
}))}
value={messages[option].code}
onValueChange={(code) => patchMessage(option, { code })}
/>
</div>
{messages[option].code === ANSWER_MESSAGE_ETC && (
<div className={styles.answerMessage}>
<FoxInput
size="md"
aria-label={`${option}에 노출할 문구`}
value={messages[option].text}
onChange={(text) => patchMessage(option, { text })}
placeholder="내용을 입력하세요."
maxLength={ANSWER_MESSAGE_MAX_LENGTH}
/>
</div>
)}
<input
type="hidden"
name={`answerMessage${option}Code`}
value={messages[option].code}
/>
<input
type="hidden"
name={`answerMessage${option}Text`}
value={messages[option].text}
/>
</div>
))}
</div>
{(errors.answer ||
errors.answerMessageOText ||
errors.answerMessageXText) && (
<FoxHelperText
type="danger"
message={
errors.answer ??
errors.answerMessageOText ??
errors.answerMessageXText ??
''
}
/>
)}
</ContentFormRow>
<ContentFormRow label="해설" required>
<FoxTextArea
name="explanation"
aria-label="해설"
defaultValue={item?.explanation ?? ''}
rows={4}
placeholder="해설(전달 메시지)을 입력하세요."
maxLength={MESSAGE_MAX_LENGTH}
counter
invalid={Boolean(errors.explanation)}
message={errors.explanation}
/>
</ContentFormRow>
<ContentFormRow label="공개 설정" required>
<ContentVisibilityField defaultVisible={item?.isVisible ?? false} />
</ContentFormRow>
{state.status === 'error' && state.message && (
<FoxHelperText type="danger" message={state.message} />
)}
<ContentFormActions
submitLabel={isEdit ? '수정' : '등록'}
listHref={contentListPath('quiz')}
pending={isPending}
/>
</form>
</>
);
}