page.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. 'use client';
  2. import { useState, useEffect, useMemo } from 'react';
  3. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  4. import { faCircleCheck, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
  5. import { fetchApi } from '@/lib/utils/client';
  6. import type { SettlementAccountResponse, SettlementAccountItem } from '@/types/response/settlement/account';
  7. import { BANK_LIST } from '../constants';
  8. import Loading from '@/app/component/Loading';
  9. const MAX_ACCOUNTS = 8;
  10. export default function SettlementAccountPage() {
  11. const [loading, setLoading] = useState(true);
  12. const [submitting, setSubmitting] = useState(false);
  13. const [accounts, setAccounts] = useState<SettlementAccountItem[]>([]);
  14. const [mode, setMode] = useState<'list'|'add'|'edit'>('list');
  15. const [editTarget, setEditTarget] = useState<SettlementAccountItem|null>(null);
  16. const [bankCode, setBankCode] = useState('');
  17. const [accountNumber, setAccountNumber] = useState('');
  18. const [accountHolder, setAccountHolder] = useState('');
  19. const fetchAccounts = () => {
  20. setLoading(true);
  21. fetchApi<SettlementAccountResponse>('/api/studio/settlement/account')
  22. .then(res => {
  23. if (res.data) {
  24. setAccounts(res.data.accounts);
  25. }
  26. })
  27. .catch(() => {})
  28. .finally(() => setLoading(false));
  29. };
  30. useEffect(() => {
  31. fetchAccounts();
  32. }, []);
  33. const canSubmit = useMemo(() => {
  34. return (
  35. bankCode !== '' &&
  36. /^\d{7,16}$/.test(accountNumber) &&
  37. accountHolder.trim().length >= 2 &&
  38. !submitting
  39. );
  40. }, [bankCode, accountNumber, accountHolder, submitting]);
  41. const resetForm = () => {
  42. setBankCode('');
  43. setAccountNumber('');
  44. setAccountHolder('');
  45. setEditTarget(null);
  46. };
  47. const handleAdd = () => {
  48. resetForm();
  49. setMode('add');
  50. };
  51. const handleEdit = (item: SettlementAccountItem) => {
  52. setEditTarget(item);
  53. setBankCode(item.bankCode);
  54. setAccountNumber('');
  55. setAccountHolder(item.accountHolder);
  56. setMode('edit');
  57. };
  58. const handleCancel = () => {
  59. resetForm();
  60. setMode('list');
  61. };
  62. const handleDelete = async (item: SettlementAccountItem) => {
  63. if (!confirm(`${item.bankName} ${item.accountNumber} 계좌를 삭제하시겠습니까?`)) {
  64. return;
  65. }
  66. try {
  67. await fetchApi(`/api/studio/settlement/account/${item.id}`, { method: 'DELETE' });
  68. fetchAccounts();
  69. } catch (err: unknown) {
  70. alert(err instanceof Error ? err.message : '계좌 삭제에 실패했습니다.');
  71. }
  72. };
  73. const handleSubmit = async () => {
  74. if (!canSubmit) {
  75. return;
  76. }
  77. setSubmitting(true);
  78. try {
  79. await fetchApi('/api/studio/settlement/account', {
  80. method: 'POST',
  81. body: {
  82. accountID: editTarget?.id ?? null,
  83. bankCode,
  84. accountNumber,
  85. accountHolder,
  86. },
  87. });
  88. alert(editTarget ? '계좌가 수정되었습니다.' : '계좌가 등록되었습니다.');
  89. resetForm();
  90. setMode('list');
  91. fetchAccounts();
  92. } catch (err: unknown) {
  93. alert(err instanceof Error ? err.message : '계좌 등록에 실패했습니다.');
  94. } finally {
  95. setSubmitting(false);
  96. }
  97. };
  98. if (loading) {
  99. return <Loading />;
  100. }
  101. return (
  102. <div className="studio-page settlement">
  103. <div className="studio-page__header">
  104. <h1 className="studio-page__title">계좌 관리</h1>
  105. </div>
  106. {/* 계좌 추가/수정 폼 */}
  107. {mode !== 'list' && (
  108. <div className="settlement__account-box">
  109. <p className="settlement__account-form-title">
  110. {mode === 'edit' ? '계좌 수정' : '계좌 추가'}
  111. </p>
  112. <div className="settlement__form">
  113. <div className="settlement__field">
  114. <label className="settlement__label" htmlFor="bank-select">은행</label>
  115. <select
  116. id="bank-select"
  117. className="settlement__select"
  118. value={bankCode}
  119. onChange={e => setBankCode(e.target.value)}
  120. >
  121. <option value="">은행을 선택하세요</option>
  122. {BANK_LIST.map(bank => (
  123. <option key={bank.code} value={bank.code}>{bank.name}</option>
  124. ))}
  125. </select>
  126. </div>
  127. <div className="settlement__field">
  128. <label className="settlement__label" htmlFor="account-number">계좌번호</label>
  129. <input
  130. id="account-number"
  131. type="text"
  132. inputMode="numeric"
  133. className="settlement__input"
  134. placeholder="숫자만 입력 (7~16자리)"
  135. value={accountNumber}
  136. onChange={e => setAccountNumber(e.target.value.replace(/\D/g, ''))}
  137. maxLength={16}
  138. />
  139. </div>
  140. <div className="settlement__field">
  141. <label className="settlement__label" htmlFor="account-holder">예금주</label>
  142. <input
  143. id="account-holder"
  144. type="text"
  145. className="settlement__input"
  146. placeholder="예금주명을 입력하세요"
  147. value={accountHolder}
  148. onChange={e => setAccountHolder(e.target.value)}
  149. />
  150. </div>
  151. <ul className="settlement__notice">
  152. <li>본인 명의 계좌만 등록 가능합니다.</li>
  153. <li>출금 시 등록된 계좌로 입금됩니다.</li>
  154. </ul>
  155. <div className="settlement__account-actions">
  156. <button
  157. type="button"
  158. className="settlement__btn settlement__btn--primary"
  159. disabled={!canSubmit}
  160. onClick={handleSubmit}
  161. >
  162. {submitting ? '저장 중...' : (mode === 'edit' ? '수정하기' : '등록하기')}
  163. </button>
  164. <button
  165. type="button"
  166. className="settlement__btn settlement__btn--cancel"
  167. onClick={handleCancel}
  168. >
  169. 취소
  170. </button>
  171. </div>
  172. </div>
  173. </div>
  174. )}
  175. {/* 계좌 목록 */}
  176. {mode === 'list' && (
  177. <>
  178. <div className="settlement__account-header">
  179. <span className="settlement__account-count">등록 계좌 {accounts.length}/{MAX_ACCOUNTS}</span>
  180. {accounts.length < MAX_ACCOUNTS && (
  181. <button
  182. type="button"
  183. className="settlement__btn settlement__btn--primary"
  184. onClick={handleAdd}
  185. >
  186. + 계좌 추가
  187. </button>
  188. )}
  189. </div>
  190. {accounts.length === 0 ? (
  191. <div className="settlement__account-empty">
  192. 등록된 계좌가 없습니다. 출금을 위해 계좌를 등록해 주세요.
  193. </div>
  194. ) : (
  195. <div className="settlement__account-list">
  196. {accounts.map(item => (
  197. <div key={item.id} className="settlement__account-card">
  198. <div className="settlement__account-info">
  199. <span className="settlement__account-bank">
  200. {item.bankName} {item.accountNumber}
  201. </span>
  202. <span className="settlement__account-detail">
  203. 예금주: {item.accountHolder}
  204. </span>
  205. <span className={`settlement__account-status${item.isVerified ? ' settlement__account-status--verified' : ' settlement__account-status--unverified'}`}>
  206. {item.isVerified ? (
  207. <>
  208. <FontAwesomeIcon icon={faCircleCheck} />
  209. 인증 완료
  210. </>
  211. ) : (
  212. <>
  213. <FontAwesomeIcon icon={faTriangleExclamation} />
  214. 미인증
  215. </>
  216. )}
  217. <span className="settlement__account-detail">
  218. · 등록일 {item.registeredAt.slice(0, 10).replace(/-/g, '.')}
  219. </span>
  220. </span>
  221. </div>
  222. <div className="settlement__account-actions">
  223. <button
  224. type="button"
  225. className="settlement__btn"
  226. onClick={() => handleEdit(item)}
  227. >
  228. 수정
  229. </button>
  230. <button
  231. type="button"
  232. className="settlement__btn settlement__btn--danger"
  233. onClick={() => handleDelete(item)}
  234. >
  235. 삭제
  236. </button>
  237. </div>
  238. </div>
  239. ))}
  240. </div>
  241. )}
  242. </>
  243. )}
  244. </div>
  245. );
  246. }