page.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. 'use client';
  2. import './style.scss';
  3. import { useState, useEffect, useCallback } from 'react';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import { useStudioContext } from '@/app/studio/context';
  6. import type { CrewListResponse, CrewItem } from '@/types/response/crew/list';
  7. const EMPTY_FORM = {
  8. name: '',
  9. description: '',
  10. minAmount: '' as string|number,
  11. isActive: true
  12. };
  13. export default function CrewConfigPage() {
  14. const { channelID } = useStudioContext();
  15. const [items, setItems] = useState<CrewItem[]>([]);
  16. const [loading, setLoading] = useState(true);
  17. const [error, setError] = useState('');
  18. const [modal, setModal] = useState<{ open: boolean; editing: CrewItem|null }>({ open: false, editing: null });
  19. const [form, setForm] = useState(EMPTY_FORM);
  20. const [saving, setSaving] = useState(false);
  21. useEffect(() => {
  22. if (error) { alert(error); setError(''); }
  23. }, [error]);
  24. const fetchList = useCallback(() => {
  25. if (!channelID) {
  26. setLoading(false);
  27. return;
  28. }
  29. setLoading(true);
  30. fetchApi<CrewListResponse>(`/api/studio/crew/list/${channelID}`).then(res => {
  31. setItems(res.data?.list ?? []);
  32. })
  33. .catch(err => setError(err.message))
  34. .finally(() => setLoading(false));
  35. }, [channelID]);
  36. useEffect(() => { fetchList(); }, [fetchList]);
  37. const openAdd = () => {
  38. setForm(EMPTY_FORM);
  39. setModal({ open: true, editing: null });
  40. };
  41. const openEdit = (item: CrewItem) => {
  42. setForm({
  43. name: item.name,
  44. description: item.description ?? '',
  45. minAmount: item.minAmount ?? '',
  46. isActive: item.isActive
  47. });
  48. setModal({ open: true, editing: item });
  49. };
  50. const closeModal = () => setModal({ open: false, editing: null });
  51. const handleSave = async () => {
  52. if (!form.name.trim()) {
  53. alert('크루명을 입력해 주세요.');
  54. return;
  55. }
  56. setSaving(true);
  57. try {
  58. const body = {
  59. channelID,
  60. id: modal.editing?.id ?? undefined,
  61. name: form.name,
  62. description: form.description || undefined,
  63. minAmount: form.minAmount !== '' ? Number(form.minAmount) : undefined,
  64. isActive: form.isActive
  65. };
  66. const res = await fetchApi('/api/studio/crew/save', {
  67. method: 'POST',
  68. headers: { 'Content-Type': 'application/json' },
  69. body: JSON.stringify(body)
  70. });
  71. closeModal();
  72. fetchList();
  73. } catch (err: unknown) {
  74. setError(err instanceof Error ? err.message : '저장에 실패했습니다.');
  75. } finally {
  76. setSaving(false);
  77. }
  78. };
  79. return (
  80. <div className="studio-page">
  81. <div className="studio-page__header">
  82. <h1 className="studio-page__title">크루 후원 설정</h1>
  83. <button type="button" className="studio-page__btn studio-page__btn--primary" onClick={openAdd}>
  84. + 크루 추가
  85. </button>
  86. </div>
  87. <div className="studio-page__table-wrap">
  88. {loading ? (
  89. <p className="studio-page__empty">준비 중...</p>
  90. ) : (
  91. <table className="studio-page__table">
  92. <thead>
  93. <tr>
  94. <th>크루명</th>
  95. <th>설명</th>
  96. <th>최소 후원금</th>
  97. <th>멤버 수</th>
  98. <th>활성</th>
  99. <th>작업</th>
  100. </tr>
  101. </thead>
  102. <tbody>
  103. {items.length === 0 ? (
  104. <tr>
  105. <td colSpan={6} className="studio-page__empty">
  106. 등록된 크루가 없습니다.
  107. </td>
  108. </tr>
  109. ) : (
  110. items.map(item => (
  111. <tr key={item.id}>
  112. <td className="crew-config__name">{item.name}</td>
  113. <td className="crew-config__desc">{item.description ?? '-'}</td>
  114. <td>{item.minAmount ? `${item.minAmount.toLocaleString()}원` : '-'}</td>
  115. <td>{item.memberCount}명</td>
  116. <td>
  117. <span className={`studio-page__badge studio-page__badge--${item.isActive ? 'active' : 'inactive'}`}>
  118. {item.isActive ? '활성' : '비활성'}
  119. </span>
  120. </td>
  121. <td>
  122. <div className="studio-page__actions">
  123. <button type="button" className="studio-page__btn" onClick={() => openEdit(item)}>수정</button>
  124. </div>
  125. </td>
  126. </tr>
  127. ))
  128. )}
  129. </tbody>
  130. </table>
  131. )}
  132. </div>
  133. {modal.open && (
  134. <div className="studio-modal">
  135. <div className="studio-modal__overlay" onClick={closeModal} />
  136. <div className="studio-modal__box">
  137. <h2 className="studio-modal__title">{modal.editing ? '크루 수정' : '크루 추가'}</h2>
  138. <div className="studio-modal__field">
  139. <label>크루명 *</label>
  140. <input type="text" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} title='크루명' />
  141. </div>
  142. <div className="studio-modal__field">
  143. <label>설명</label>
  144. <input type="text" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} title='설명' />
  145. </div>
  146. <div className="studio-modal__field">
  147. <label>최소 후원금 (원)</label>
  148. <input type="number" min={0} placeholder="미설정" value={form.minAmount} onChange={e => setForm(f => ({ ...f, minAmount: e.target.value }))} />
  149. <p className="studio-modal__hint">이 금액 이상 후원한 회원만 크루에 가입할 수 있습니다.</p>
  150. </div>
  151. <div className="studio-modal__field">
  152. <label className="studio-modal__checkbox-label">
  153. <input type="checkbox" checked={form.isActive} onChange={e => setForm(f => ({ ...f, isActive: e.target.checked }))} />
  154. 활성화
  155. </label>
  156. </div>
  157. <div className="studio-modal__footer">
  158. <button type="button" className="studio-page__btn" onClick={closeModal}>취소</button>
  159. <button type="button" className="studio-page__btn studio-page__btn--primary" onClick={handleSave} disabled={saving}>
  160. {saving ? '저장 중...' : '저장'}
  161. </button>
  162. </div>
  163. </div>
  164. </div>
  165. )}
  166. </div>
  167. );
  168. }