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 { 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 { 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 { useFeedback } from '@/app/_hooks/use-feedback';
import {
DESCRIPTION_MAX_LENGTH,
INITIAL_POINT_STANDARD_FORM_STATE,
} from '@/lib/domain/point-standard-form';
import type { PointStandard } from '@/lib/domain/point-standard';
import { formatCommonCode, type CommonCode } from '@/lib/domain/common-code';
import {
createPointStandardAction,
updatePointStandardAction,
} from '../_actions';
import styles from './point-standard-form.module.scss';
interface PointStandardFormModalProps {
/** 수정 대상. 없으면 등록이다. */
standard?: PointStandard;
/** 공통코드 `CODE_GROUP.pointEvent` — 등록의 이벤트 선택지(기획 ① "등록된 코드중에서 선택"). */
eventCodes: readonly CommonCode[];
onClose: () => void;
}
export function PointStandardFormModal({
standard,
eventCodes,
onClose,
}: PointStandardFormModalProps) {
const isCreate = standard === undefined;
const { showToast } = useFeedback();
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction, isPending] = useActionState(
isCreate ? createPointStandardAction : updatePointStandardAction,
INITIAL_POINT_STANDARD_FORM_STATE
);
const [eventCode, setEventCode] = useState(
standard?.eventCode ?? eventCodes[0]?.code ?? ''
);
const [isDuplicable, setIsDuplicable] = useState(standard?.isDuplicable ?? true);
const [isActive, setIsActive] = useState(standard?.isActive ?? true);
useEffect(() => {
if (state.status === 'success') {
showToast({
variant: 'success',
message: isCreate
? '포인트 기준을 등록했습니다.'
: '포인트 기준을 수정했습니다.',
});
onClose();
}
}, [state, showToast, onClose, isCreate]);
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
return (
<FoxModal
open
size="md"
title={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}
>
<p className={styles.required}>* 는 필수 항목입니다.</p>
<input type="hidden" name="id" value={standard?.id ?? ''} />
{/* 수정에서는 화면이 읽기 전용이지만 백엔드 UPDATE가 EVENT_CD를 덮어써서 그대로 돌려보낸다. */}
<input type="hidden" name="eventCode" value={eventCode} />
<div className={styles.field}>
{isCreate ? (
// 기획 ① 등록된 코드중에서 선택.
<FoxSelect
size="md"
label="이벤트"
requirement="required"
options={eventCodes.map((code) => ({
value: code.code,
label: code.label,
}))}
placeholder="이벤트를 선택하세요."
value={eventCode}
onValueChange={setEventCode}
error={Boolean(errors.eventCode)}
hint={errors.eventCode}
/>
) : (
// 기획 ① 이벤트행은 수정불가(Read Only). 보이는 것은 코드명이고 저장되는 값은
// 위 hidden의 코드값이다.
<FoxInput
size="md"
label="이벤트"
requirement="required"
value={formatCommonCode(eventCodes, eventCode)}
readOnly
state="view"
/>
)}
</div>
<div className={styles.field}>
<FoxFormLabel as="span" requirement="required">
포인트
</FoxFormLabel>
<div className={styles.numberRow}>
<div className={styles.numberField}>
<FoxInput
size="md"
name="point"
type="number"
inputMode="numeric"
min={0}
aria-label="포인트"
defaultValue={String(standard?.point ?? 0)}
invalid={Boolean(errors.point)}
/>
</div>
<span className={styles.numberNote}>포인트</span>
</div>
{errors.point && <FoxHelperText type="danger" message={errors.point} />}
</div>
<div className={styles.field}>
<FoxFormLabel as="span" requirement="required">
제한포인트
</FoxFormLabel>
<div className={styles.numberRow}>
<div className={styles.numberField}>
<FoxInput
size="md"
name="limitPoint"
type="number"
inputMode="numeric"
min={0}
aria-label="제한포인트"
defaultValue={String(standard?.limitPoint ?? 0)}
invalid={Boolean(errors.limitPoint)}
/>
</div>
<span className={styles.numberNote}>
'0' 일경우 일 누적 포인트에 대한 제한이 없습니다.
</span>
</div>
{errors.limitPoint && (
<FoxHelperText type="danger" message={errors.limitPoint} />
)}
</div>
<div className={styles.field}>
<FoxFormLabel as="span">중복 여부</FoxFormLabel>
<FoxRadioGroup name="isDuplicable" size="md" label="중복 여부">
<FoxRadio
value="Y"
label="중복 가능"
checked={isDuplicable}
onChange={() => setIsDuplicable(true)}
/>
<FoxRadio
value="N"
label="중복 불가"
checked={!isDuplicable}
onChange={() => setIsDuplicable(false)}
/>
</FoxRadioGroup>
</div>
<div className={styles.field}>
<FoxTextArea
name="description"
label="이벤트 설명"
requirement="required"
defaultValue={standard?.description ?? ''}
placeholder="이벤트 설명을 입력하세요."
rows={4}
counter
maxLength={DESCRIPTION_MAX_LENGTH}
invalid={Boolean(errors.description)}
message={errors.description}
/>
</div>
<div className={styles.field}>
<FoxFormLabel as="span">사용여부</FoxFormLabel>
<FoxRadioGroup name="isActive" size="md" label="사용여부">
<FoxRadio
value="Y"
label="사용"
checked={isActive}
onChange={() => setIsActive(true)}
/>
<FoxRadio
value="N"
label="사용안함"
checked={!isActive}
onChange={() => setIsActive(false)}
/>
</FoxRadioGroup>
</div>
{state.status === 'error' && state.message && (
<FoxHelperText type="danger" message={state.message} />
)}
</form>
</FoxModal>
);
}