| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- 'use client';
- import type { GoalConfigItem } from '@/types/response/donation/goalConfig';
- import { Checkbox } from '@/components/ui/checkbox';
- import { FONT_OPTIONS } from '../../alert/constants';
- import { GOAL_STYLES } from '../types';
- import type { FormState } from '../types';
- import {
- GOAL_BAR_HEIGHT_MIN, GOAL_BAR_HEIGHT_MAX,
- FONT_SIZE_MIN, FONT_SIZE_MAX, COLOR_HEX_MAX_LENGTH
- } from '@/constants/donation';
- type Props = {
- form: FormState;
- editingItem: GoalConfigItem|null;
- saving: boolean;
- onFormChange: <K extends keyof FormState>(field: K, value: FormState[K]) => void;
- onSave: () => void;
- onCancel: () => void;
- };
- export default function GoalFormPanel({
- form,
- editingItem,
- saving,
- onFormChange,
- onSave,
- onCancel
- }: Props) {
- // ── 색상 그룹 렌더러 ─────────────────────────────
- const renderColorField = (
- label: string,
- id: string,
- colorField: 'barColor'|'barBackgroundColor'|'titleFontColor'|'amountFontColor',
- ) => (
- <div className="goal-config__field">
- <label htmlFor={id} className="goal-config__field-label">{label}</label>
- <div className="goal-config__color-wrap">
- <input
- id={id}
- type="color"
- className="goal-config__color-input"
- value={form[colorField]}
- onChange={e => onFormChange(colorField, e.target.value)}
- />
- <input
- type="text"
- className="goal-config__input goal-config__color-text"
- value={form[colorField]}
- onChange={e => onFormChange(colorField, e.target.value)}
- maxLength={COLOR_HEX_MAX_LENGTH}
- title={label}
- />
- </div>
- </div>
- );
- // ── 폰트 그룹 렌더러 ─────────────────────────────
- const renderFontGroup = (
- label: string,
- prefix: string,
- familyField: 'titleFontFamily'|'amountFontFamily',
- sizeField: 'titleFontSizePx'|'amountFontSizePx',
- colorField: 'titleFontColor'|'amountFontColor',
- defaultSize: number
- ) => (
- <div className="goal-config__font-group">
- <div className="goal-config__font-group-title">{label}</div>
- <div className="goal-config__font-grid">
- <div className="goal-config__field">
- <label htmlFor={`${prefix}-family`} className="goal-config__field-label">글꼴</label>
- <select
- id={`${prefix}-family`}
- className="goal-config__select"
- value={form[familyField] ?? ''}
- onChange={e => onFormChange(familyField, e.target.value || null)}
- >
- {FONT_OPTIONS.map(f => (
- <option key={f.value} value={f.value}>{f.label}</option>
- ))}
- </select>
- </div>
- <div className="goal-config__field">
- <label htmlFor={`${prefix}-size`} className="goal-config__field-label">크기 (px)</label>
- <input
- id={`${prefix}-size`}
- type="number"
- className="goal-config__input"
- min={FONT_SIZE_MIN}
- max={FONT_SIZE_MAX}
- value={form[sizeField]}
- onChange={e => onFormChange(sizeField, parseInt(e.target.value) || defaultSize)}
- title="Font Size"
- />
- </div>
- <div className="goal-config__field">
- <label htmlFor={`${prefix}-color`} className="goal-config__field-label">색상</label>
- <div className="goal-config__color-wrap">
- <input
- id={`${prefix}-color`}
- type="color"
- className="goal-config__color-input"
- value={form[colorField]}
- onChange={e => onFormChange(colorField, e.target.value)}
- />
- <input
- type="text"
- className="goal-config__input goal-config__color-text"
- value={form[colorField]}
- onChange={e => onFormChange(colorField, e.target.value)}
- maxLength={COLOR_HEX_MAX_LENGTH}
- title="Font Color"
- />
- </div>
- </div>
- </div>
- </div>
- );
- return (
- <main className="goal-config__form-panel">
- {/* ── 기본 설정 ────────────────────────────── */}
- <section className="goal-config__section">
- <h3 className="goal-config__section-title">기본 설정</h3>
- <div className="goal-config__section-body">
- <div className="goal-config__field">
- <label htmlFor="goal-title" className="goal-config__field-label">제목</label>
- <input
- id="goal-title"
- type="text"
- className="goal-config__input"
- value={form.title}
- onChange={e => onFormChange('title', e.target.value)}
- placeholder="후원 목표"
- />
- </div>
- <div className="goal-config__field">
- <label htmlFor="goal-style" className="goal-config__field-label">스타일</label>
- <select
- id="goal-style"
- className="goal-config__select"
- value={form.style}
- onChange={e => onFormChange('style', parseInt(e.target.value))}
- >
- {GOAL_STYLES.map(s => (
- <option key={s.value} value={s.value}>{s.label}</option>
- ))}
- </select>
- </div>
- <div className="goal-config__field-row">
- <div className="goal-config__field">
- <label htmlFor="goal-startAmount" className="goal-config__field-label">시작금액 (원)</label>
- <input
- id="goal-startAmount"
- type="number"
- className="goal-config__input"
- min={0}
- value={form.startAmount}
- onChange={e => onFormChange('startAmount', parseInt(e.target.value) || 0)}
- />
- </div>
- <div className="goal-config__field">
- <label htmlFor="goal-targetAmount" className="goal-config__field-label">목표금액 (원)</label>
- <input
- id="goal-targetAmount"
- type="number"
- className="goal-config__input"
- min={1}
- value={form.targetAmount}
- onChange={e => onFormChange('targetAmount', parseInt(e.target.value) || 0)}
- />
- </div>
- </div>
- <div className="goal-config__field-row">
- <div className="goal-config__field">
- <label htmlFor="goal-startAt" className="goal-config__field-label">시작일시</label>
- <input
- id="goal-startAt"
- type="text"
- className="goal-config__input"
- value={form.startAt ?? ''}
- onChange={e => onFormChange('startAt', e.target.value)}
- placeholder="2026.03.30 14:00"
- maxLength={16}
- />
- </div>
- <div className="goal-config__field">
- <label htmlFor="goal-endAt" className="goal-config__field-label">종료일시</label>
- <input
- id="goal-endAt"
- type="text"
- className="goal-config__input"
- value={form.endAt ?? ''}
- onChange={e => onFormChange('endAt', e.target.value)}
- placeholder="2026.03.31 14:00"
- maxLength={16}
- />
- </div>
- </div>
- <div className="goal-config__field">
- <label className="goal-config__checkbox-label">
- <Checkbox
- checked={form.isShowPercent}
- onCheckedChange={v => onFormChange('isShowPercent', !!v)}
- />
- 퍼센트 표시
- </label>
- </div>
- <div className="goal-config__field">
- <label className="goal-config__checkbox-label">
- <Checkbox
- checked={form.isActive}
- onCheckedChange={v => onFormChange('isActive', !!v)}
- />
- 활성화
- </label>
- </div>
- </div>
- </section>
- {/* ── 바 설정 ─────────────────────────────── */}
- <section className="goal-config__section">
- <h3 className="goal-config__section-title">바 설정</h3>
- <div className="goal-config__section-body">
- <div className="goal-config__field-row">
- {renderColorField('바 색상', 'goal-barColor', 'barColor')}
- {renderColorField('바 배경색', 'goal-barBgColor', 'barBackgroundColor')}
- </div>
- <div className="goal-config__field">
- <label htmlFor="goal-barHeight" className="goal-config__field-label">바 높이 (px)</label>
- <input
- id="goal-barHeight"
- type="number"
- className="goal-config__input"
- min={GOAL_BAR_HEIGHT_MIN}
- max={GOAL_BAR_HEIGHT_MAX}
- value={form.barHeightPx}
- onChange={e => onFormChange('barHeightPx', parseInt(e.target.value) || 24)}
- />
- </div>
- </div>
- </section>
- {/* ── 폰트 ────────────────────────────────── */}
- <section className="goal-config__section">
- <h3 className="goal-config__section-title">폰트</h3>
- <div className="goal-config__section-body">
- {renderFontGroup('제목', 'font-title', 'titleFontFamily', 'titleFontSizePx', 'titleFontColor', 16)}
- {renderFontGroup('현황', 'font-amount', 'amountFontFamily', 'amountFontSizePx', 'amountFontColor', 14)}
- </div>
- </section>
- {/* ── 하단 버튼 ────────────────────────────── */}
- <div className="goal-config__form-footer flex-1 w-full sm:justify-end gap-2">
- <button type="button" className="goal-config__btn flex-1 sm:flex-none" onClick={onCancel}>
- 취소
- </button>
- <button
- type="button"
- className="goal-config__btn goal-config__btn--primary flex-1 sm:flex-none"
- onClick={onSave}
- disabled={saving}
- >
- {saving ? '저장 중...' : (editingItem ? '수정하기' : '등록하기')}
- </button>
- </div>
- </main>
- );
- }
|