| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- 'use client';
- import './style.scss';
- import { useState, useEffect, useCallback } from 'react';
- import { useRouter } from 'next/navigation';
- import { fetchApi } from '@/lib/utils/client';
- import { useStudioContext } from '@/app/studio/context';
- import type { CrewListResponse, CrewItem } from '@/types/response/crew/list';
- import { Button } from '@/components/ui/button';
- import { Input } from '@/components/ui/input';
- import { Label } from '@/components/ui/label';
- import { Checkbox } from '@/components/ui/checkbox';
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
- function RequiredLabel({ htmlFor, children }: { htmlFor: string; children: React.ReactNode }) {
- return <Label htmlFor={htmlFor}><span className="text-destructive mr-0.5">*</span>{children}</Label>;
- }
- function MoneyInput({ id, value, onChange, placeholder }: { id: string; value: string|number; onChange: (v: string) => void; placeholder?: string }) {
- const [focused, setFocused] = useState(false);
- const raw = String(value);
- const display = focused || !raw ? raw : (Number(raw) ? Number(raw).toLocaleString() : raw);
- return (
- <Input
- id={id} type={focused ? 'number' : 'text'} min={0} placeholder={placeholder}
- value={display}
- onKeyDown={e => { if (!/^[0-9]$/.test(e.key) && !['Backspace','Delete','ArrowLeft','ArrowRight','Tab','Home','End'].includes(e.key)) e.preventDefault(); }}
- onCompositionStart={e => e.preventDefault()}
- onChange={e => onChange(e.target.value.replace(/[^0-9]/g, ''))}
- onFocus={() => setFocused(true)}
- onBlur={() => setFocused(false)}
- />
- );
- }
- const EMPTY_FORM = {
- name: '',
- description: '',
- minAmount: '' as string|number,
- isActive: true
- };
- export default function CrewConfigPage()
- {
- const router = useRouter();
- const { channelID } = useStudioContext();
- const [items, setItems] = useState<CrewItem[]>([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState('');
- const [modal, setModal] = useState<{ open: boolean; editing: CrewItem|null }>({ open: false, editing: null });
- const [form, setForm] = useState(EMPTY_FORM);
- const [saving, setSaving] = useState(false);
- useEffect(() => {
- if (error) { alert(error); setError(''); }
- }, [error]);
- const fetchList = useCallback(() => {
- if (!channelID) {
- setLoading(false);
- return;
- }
- setLoading(true);
- fetchApi<CrewListResponse>(`/api/studio/crew/list/${channelID}`).then(res => {
- setItems(res.data?.list ?? []);
- })
- .catch(err => setError(err.message))
- .finally(() => setLoading(false));
- }, [channelID]);
- useEffect(() => { fetchList(); }, [fetchList]);
- const openAdd = () => {
- setForm(EMPTY_FORM);
- setModal({ open: true, editing: null });
- };
- const openEdit = (item: CrewItem) => {
- setForm({
- name: item.name,
- description: item.description ?? '',
- minAmount: item.minAmount ?? '',
- isActive: item.isActive
- });
- setModal({ open: true, editing: item });
- };
- const closeModal = () => setModal({ open: false, editing: null });
- const handleSave = async () => {
- if (!form.name.trim()) {
- alert('크루명을 입력해 주세요.');
- return;
- }
- setSaving(true);
- try {
- await fetchApi('/api/studio/crew/save', {
- method: 'POST',
- body: {
- channelID,
- id: modal.editing?.id ?? undefined,
- name: form.name,
- description: form.description || undefined,
- minAmount: form.minAmount !== '' ? Number(form.minAmount) : undefined,
- isActive: form.isActive
- }
- });
- closeModal();
- fetchList();
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : '저장에 실패했습니다.');
- } finally {
- setSaving(false);
- }
- };
- return (
- <div className="studio-page crew-config">
- <div className="studio-page__header">
- <h1 className="studio-page__title">크루 후원 설정</h1>
- <Button onClick={openAdd}>+ 크루 추가</Button>
- </div>
- <div className="studio-page__table-wrap">
- {loading ? (
- <p className="studio-page__empty">준비 중...</p>
- ) : (
- <table className="studio-page__table">
- <thead>
- <tr>
- <th>크루명</th>
- <th>설명</th>
- <th>최소 후원금</th>
- <th>멤버 수</th>
- <th>활성</th>
- <th>작업</th>
- </tr>
- </thead>
- <tbody>
- {items.length === 0 ? (
- <tr>
- <td colSpan={6} className="studio-page__empty">
- 등록된 크루가 없습니다.
- </td>
- </tr>
- ) : (
- items.map(item => (
- <tr key={item.id}>
- <td className="crew-config__name">
- <button type="button" className="crew-config__link" onClick={() => router.push(`/studio/donation/crew/${item.id}`)}>
- {item.name}
- </button>
- </td>
- <td className="crew-config__desc">{item.description ?? '-'}</td>
- <td>{item.minAmount ? `${item.minAmount.toLocaleString()}원` : '-'}</td>
- <td>{item.memberCount}명</td>
- <td>
- <span className={`studio-page__badge studio-page__badge--${item.isActive ? 'active' : 'inactive'}`}>
- {item.isActive ? '활성' : '비활성'}
- </span>
- </td>
- <td>
- <div className="studio-page__actions">
- <Button variant="outline" size="sm" onClick={() => openEdit(item)}>수정</Button>
- <Button variant="outline" size="sm" onClick={() => router.push(`/studio/donation/crew/${item.id}`)}>관리</Button>
- </div>
- </td>
- </tr>
- ))
- )}
- </tbody>
- </table>
- )}
- </div>
- <Dialog open={modal.open} onOpenChange={open => { if (!open) closeModal(); }}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>{modal.editing ? '크루 수정' : '크루 추가'}</DialogTitle>
- </DialogHeader>
- <div className="space-y-4">
- <div className="space-y-2">
- <RequiredLabel htmlFor="crew-name">크루명</RequiredLabel>
- <Input id="crew-name" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} />
- </div>
- <div className="space-y-2">
- <Label htmlFor="crew-desc">설명</Label>
- <Input id="crew-desc" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} />
- </div>
- <div className="space-y-2">
- <Label htmlFor="crew-min">최소 후원금 (원)</Label>
- <MoneyInput id="crew-min" placeholder="미설정" value={form.minAmount} onChange={v => setForm(f => ({ ...f, minAmount: v }))} />
- <p className="text-xs text-muted-foreground">이 금액 이상 후원한 회원만 크루에 가입할 수 있습니다.</p>
- </div>
- <div className="flex items-center gap-2">
- <Checkbox id="crew-active" checked={form.isActive} onCheckedChange={v => setForm(f => ({ ...f, isActive: !!v }))} />
- <Label htmlFor="crew-active">활성화</Label>
- </div>
- </div>
- <DialogFooter className="flex flex-row justify-center gap-0 sm:justify-center">
- <Button variant="outline" className="flex-1 sm:flex-none" onClick={closeModal}>취소</Button>
- <Button className="flex-1 sm:flex-none" onClick={handleSave} disabled={saving}>{saving ? '저장 중...' : '저장'}</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </div>
- );
- }
|