| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { useRouter } from 'next/navigation';
- import { fetchApi } from '@/lib/utils/client';
- import { useStudioContext } from '@/app/studio/context';
- import { useCrewWidgetConfigContext } from '../context';
- import { CREW_WIDGET_THEMES, CREW_PERIODS, FONT_FAMILIES } from '../constants';
- import { type FormState, createEmptyForm, formatInput, parseInput } from '../types';
- import type { CrewWidgetConfigItem } from '@/types/response/crew/widgetConfig';
- import { Checkbox } from '@/components/ui/checkbox';
- /** 색상 입력 (color picker + hex text) */
- function ColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
- return (
- <div className="crew-widget-form__color-field">
- <input
- type="color"
- className="crew-widget-form__color-picker"
- value={value}
- onChange={e => onChange(e.target.value)}
- />
- <input
- type="text"
- className="crew-widget-form__input crew-widget-form__input--color-text"
- value={value}
- onChange={e => onChange(e.target.value)}
- maxLength={7}
- />
- </div>
- );
- }
- type Props = {
- editItem?: CrewWidgetConfigItem;
- form?: FormState;
- onFormChange?: (form: FormState) => void;
- onSaved?: () => void;
- onCancel?: () => void;
- channelID?: number;
- saving?: boolean;
- setSaving?: (v: boolean) => void;
- };
- export default function CrewWidgetFormPanel({ editItem, form: externalForm, onFormChange, onSaved, onCancel, channelID: directChannelID, saving: directSaving, setSaving: directSetSaving }: Props)
- {
- const router = useRouter();
- const studio = useStudioContext();
- const channelID = directChannelID ?? studio.channelID;
- // context 사용 가능 여부에 따라 분기
- let ctxSaving = false;
- let ctxSetSaving: (v: boolean) => void = () => {};
- let ctxFetchList: () => void = () => {};
- try {
- const ctx = useCrewWidgetConfigContext();
- ctxSaving = ctx.saving;
- ctxSetSaving = ctx.setSaving;
- ctxFetchList = ctx.fetchList;
- } catch {
- }
- const saving = directSaving ?? ctxSaving;
- const setSaving = directSetSaving ?? ctxSetSaving;
- const fetchList = ctxFetchList;
- const [internalForm, setInternalForm] = useState<FormState>(createEmptyForm());
- const form = externalForm ?? internalForm;
- useEffect(() => {
- if (!editItem) {
- return;
- }
- const data: FormState = {
- title: editItem.title,
- theme: editItem.theme,
- period: editItem.period,
- startAt: editItem.startAt,
- endAt: editItem.endAt,
- maxDisplayCount: editItem.maxDisplayCount,
- isShowAmount: editItem.isShowAmount,
- isShowDonationCount: editItem.isShowDonationCount,
- isShowContributionRate: editItem.isShowContributionRate,
- isShowMemberIcon: editItem.isShowMemberIcon,
- isActive: editItem.isActive,
- bgColor: editItem.bgColor,
- titleFontFamily: editItem.titleFontFamily,
- titleFontSizePx: editItem.titleFontSizePx,
- titleFontColor: editItem.titleFontColor,
- rank1FontFamily: editItem.rank1FontFamily,
- rank1FontSizePx: editItem.rank1FontSizePx,
- rank1FontColor: editItem.rank1FontColor,
- rank2FontFamily: editItem.rank2FontFamily,
- rank2FontSizePx: editItem.rank2FontSizePx,
- rank2FontColor: editItem.rank2FontColor,
- rank3FontFamily: editItem.rank3FontFamily,
- rank3FontSizePx: editItem.rank3FontSizePx,
- rank3FontColor: editItem.rank3FontColor,
- rowFontFamily: editItem.rowFontFamily,
- rowFontSizePx: editItem.rowFontSizePx,
- rowFontColor: editItem.rowFontColor
- };
- if (onFormChange) {
- onFormChange(data);
- } else {
- setInternalForm(data);
- }
- }, [editItem]);
- const set = (key: keyof FormState, value: FormState[keyof FormState]) => {
- const updater = (f: FormState): FormState => ({ ...f, [key]: value });
- if (onFormChange) {
- onFormChange(updater(form));
- } else {
- setInternalForm(updater);
- }
- };
- const handleSave = async () => {
- if (!form.title.trim()) {
- alert('제목을 입력해 주세요.');
- return;
- }
- if (form.period === 5) {
- if (!form.startAt || !form.endAt) {
- alert('사용자 지정 기간을 입력해 주세요.');
- return;
- }
- }
- setSaving(true);
- try {
- await fetchApi('/api/studio/crew/widget/config', {
- method: 'POST',
- body: { ...form, channelID, id: editItem?.id ?? undefined }
- });
- fetchList();
- if (onSaved) {
- onSaved();
- } else {
- router.push('/studio/donation/crew/widget/list');
- }
- } catch (err: unknown) {
- alert(err instanceof Error ? err.message : '저장에 실패했습니다.');
- } finally {
- setSaving(false);
- }
- };
- const renderFontFields = (prefix: string) =>
- {
- const familyKey = `${prefix}FontFamily` as keyof FormState;
- const sizeKey = `${prefix}FontSizePx` as keyof FormState;
- const colorKey = `${prefix}FontColor` as keyof FormState;
- return (
- <>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">글꼴</label>
- <select
- className="crew-widget-form__select"
- aria-label="글꼴"
- value={(form[familyKey] as string) ?? ''}
- onChange={e => set(familyKey, e.target.value || null)}
- >
- {FONT_FAMILIES.map(f => <option key={f.value} value={f.value}>{f.label}</option>)}
- </select>
- </div>
- <div className="crew-widget-form__row">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">크기(px)</label>
- <input
- type="number"
- className="crew-widget-form__input"
- min={10}
- max={48}
- value={form[sizeKey] as number}
- onChange={e => set(sizeKey, Number(e.target.value))}
- />
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">색상</label>
- <ColorInput
- value={form[colorKey] as string}
- onChange={v => set(colorKey, v)}
- />
- </div>
- </div>
- </>
- );
- };
- const renderFontDetails = (label: string, prefix: string) => {
- return (
- <details className="crew-widget-form__details">
- <summary className="crew-widget-form__details-summary">{label} 폰트 설정</summary>
- <div className="crew-widget-form__details-body">
- {renderFontFields(prefix)}
- </div>
- </details>
- );
- };
- return (
- <main className="crew-widget-form">
- {/* 기본 설정 */}
- <details className="crew-widget-form__section" open>
- <summary className="crew-widget-form__section-title">기본 설정</summary>
- <div className="crew-widget-form__section-body">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label"><span className="text-destructive mr-0.5">*</span> 제목</label>
- <input
- type="text"
- className="crew-widget-form__input"
- value={form.title}
- onChange={e => set('title', e.target.value)}
- maxLength={300}
- />
- </div>
- <div className="crew-widget-form__row">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">테마</label>
- <select
- className="crew-widget-form__select"
- aria-label="테마"
- value={form.theme}
- onChange={e => set('theme', Number(e.target.value))}
- >
- {CREW_WIDGET_THEMES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
- </select>
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">기간</label>
- <select
- className="crew-widget-form__select"
- aria-label="기간"
- value={form.period}
- onChange={e => set('period', Number(e.target.value))}
- >
- {CREW_PERIODS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
- </select>
- </div>
- </div>
- {form.period === 5 && (
- <div className="crew-widget-form__row">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">시작</label>
- <input
- type="text"
- className="crew-widget-form__input"
- placeholder="2026.04.12 14:00"
- maxLength={16}
- value={form.startAt ? formatInput(new Date(form.startAt)) : ''}
- onChange={e => set('startAt', parseInput(e.target.value) || null)}
- />
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">종료</label>
- <input
- type="text"
- className="crew-widget-form__input"
- placeholder="2026.04.13 14:00"
- maxLength={16}
- value={form.endAt ? formatInput(new Date(form.endAt)) : ''}
- onChange={e => set('endAt', parseInput(e.target.value) || null)}
- />
- </div>
- </div>
- )}
- <div className="crew-widget-form__row">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">최대 표시 수</label>
- <input
- type="number"
- className="crew-widget-form__input"
- min={1}
- max={20}
- value={form.maxDisplayCount}
- onChange={e => set('maxDisplayCount', Number(e.target.value))}
- />
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__field-label">배경 색상</label>
- <ColorInput
- value={form.bgColor}
- onChange={v => set('bgColor', v)}
- />
- </div>
- </div>
- </div>
- </details>
- {/* 표시 옵션 */}
- <details className="crew-widget-form__section" open>
- <summary className="crew-widget-form__section-title">표시 옵션</summary>
- <div className="crew-widget-form__section-body">
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__checkbox-label">
- <Checkbox checked={form.isShowAmount} onCheckedChange={v => set('isShowAmount', !!v)} />
- 후원 금액 표시
- </label>
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__checkbox-label">
- <Checkbox checked={form.isShowDonationCount} onCheckedChange={v => set('isShowDonationCount', !!v)} />
- 후원 건수 표시
- </label>
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__checkbox-label">
- <Checkbox checked={form.isShowContributionRate} onCheckedChange={v => set('isShowContributionRate', !!v)} />
- 기여율 표시
- </label>
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__checkbox-label">
- <Checkbox checked={form.isShowMemberIcon} onCheckedChange={v => set('isShowMemberIcon', !!v)} />
- 크루원 아이콘 표시
- </label>
- </div>
- <div className="crew-widget-form__field">
- <label className="crew-widget-form__checkbox-label">
- <Checkbox checked={form.isActive} onCheckedChange={v => set('isActive', !!v)} />
- 활성화
- </label>
- </div>
- </div>
- </details>
- {/* 제목 폰트 */}
- <details className="crew-widget-form__section" open>
- <summary className="crew-widget-form__section-title">제목 폰트</summary>
- <div className="crew-widget-form__section-body">
- {renderFontFields('title')}
- </div>
- </details>
- {/* 순위별 폰트 */}
- <details className="crew-widget-form__section" open>
- <summary className="crew-widget-form__section-title">순위별 폰트</summary>
- <div className="crew-widget-form__section-body">
- {renderFontDetails('1위', 'rank1')}
- {renderFontDetails('2위', 'rank2')}
- {renderFontDetails('3위', 'rank3')}
- {renderFontDetails('일반', 'row')}
- </div>
- </details>
- {/* 버튼 */}
- <div className="crew-widget-form__footer flex-1 w-full sm:justify-end gap-2">
- <button type="button" className="crew-widget-form__btn flex-1 sm:flex-none" onClick={() => onCancel ? onCancel() : router.push('/studio/donation/crew/widget/list')}>취소</button>
- <button type="button" className="crew-widget-form__btn crew-widget-form__btn--primary flex-1 sm:flex-none" onClick={handleSave} disabled={saving}>
- {saving ? '저장 중...' : '저장'}
- </button>
- </div>
- </main>
- );
- }
|