From 8e135672a1e8f4515569361340b032b13d69a4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 11 Feb 2026 09:33:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=BC=EB=B0=98=EC=A0=84=ED=91=9C?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=97=90=20=EA=B3=84=EC=A0=95=EA=B3=BC?= =?UTF-8?q?=EB=AA=A9=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=9D=B4?= =?UTF-8?q?=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 계좌입출금내역에서 제거된 계정과목 설정 기능을 일반전표입력 페이지로 이관 - JournalEntryController에 계정과목 CRUD 메서드 추가 - 계정과목 CRUD 라우트 추가 (journal-entries/account-codes/*) - AccountCodeSettingsModal 컴포넌트 추가 - 페이지 헤더에 계정과목 설정 버튼 추가 Co-Authored-By: Claude Opus 4.6 --- .../Finance/JournalEntryController.php | 99 +++++++++ .../views/finance/journal-entries.blade.php | 196 +++++++++++++++++- routes/web.php | 4 + 3 files changed, 296 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Finance/JournalEntryController.php b/app/Http/Controllers/Finance/JournalEntryController.php index 358f353c..0f92d811 100644 --- a/app/Http/Controllers/Finance/JournalEntryController.php +++ b/app/Http/Controllers/Finance/JournalEntryController.php @@ -644,4 +644,103 @@ public function deleteBankJournal(int $id): JsonResponse 'message' => '분개가 삭제되었습니다.', ]); } + + /** + * 계정과목 전체 목록 (활성/비활성 포함) + */ + public function accountCodesAll(): JsonResponse + { + $codes = AccountCode::getAll(); + + return response()->json([ + 'success' => true, + 'data' => $codes, + ]); + } + + /** + * 계정과목 추가 + */ + public function accountCodeStore(Request $request): JsonResponse + { + $validated = $request->validate([ + 'code' => 'required|string|max:10', + 'name' => 'required|string|max:100', + 'category' => 'nullable|string|max:50', + ]); + + if (AccountCode::where('code', $validated['code'])->exists()) { + return response()->json([ + 'success' => false, + 'error' => '이미 존재하는 계정과목 코드입니다.', + ], 422); + } + + $maxSort = AccountCode::max('sort_order') ?? 0; + + $accountCode = AccountCode::create([ + 'tenant_id' => 1, + 'code' => $validated['code'], + 'name' => $validated['name'], + 'category' => $validated['category'] ?? null, + 'sort_order' => $maxSort + 1, + 'is_active' => true, + ]); + + return response()->json([ + 'success' => true, + 'message' => '계정과목이 추가되었습니다.', + 'data' => $accountCode, + ]); + } + + /** + * 계정과목 수정 + */ + public function accountCodeUpdate(Request $request, int $id): JsonResponse + { + $accountCode = AccountCode::find($id); + if (!$accountCode) { + return response()->json(['success' => false, 'error' => '계정과목을 찾을 수 없습니다.'], 404); + } + + $validated = $request->validate([ + 'code' => 'sometimes|string|max:10', + 'name' => 'sometimes|string|max:100', + 'category' => 'nullable|string|max:50', + 'is_active' => 'sometimes|boolean', + ]); + + if (isset($validated['code']) && $validated['code'] !== $accountCode->code) { + if (AccountCode::where('code', $validated['code'])->where('id', '!=', $id)->exists()) { + return response()->json(['success' => false, 'error' => '이미 존재하는 계정과목 코드입니다.'], 422); + } + } + + $accountCode->update($validated); + + return response()->json([ + 'success' => true, + 'message' => '계정과목이 수정되었습니다.', + 'data' => $accountCode, + ]); + } + + /** + * 계정과목 삭제 + */ + public function accountCodeDestroy(int $id): JsonResponse + { + $accountCode = AccountCode::find($id); + if (!$accountCode) { + return response()->json(['success' => false, 'error' => '계정과목을 찾을 수 없습니다.'], 404); + } + + $accountCode->delete(); + + return response()->json([ + 'success' => true, + 'message' => '계정과목이 삭제되었습니다.', + ]); + } } diff --git a/resources/views/finance/journal-entries.blade.php b/resources/views/finance/journal-entries.blade.php index 7ff2ad95..c952ffda 100644 --- a/resources/views/finance/journal-entries.blade.php +++ b/resources/views/finance/journal-entries.blade.php @@ -53,6 +53,7 @@ const ArrowDownCircle = createIcon('arrow-down-circle'); const ArrowUpCircle = createIcon('arrow-up-circle'); const RefreshCw = createIcon('refresh-cw'); +const Settings = createIcon('settings'); const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); @@ -187,6 +188,179 @@ className={`px-3 py-1.5 text-xs cursor-pointer ${index === highlightIndex ? 'bg- ); }; +// ============================================================ +// AccountCodeSettingsModal (계정과목 설정) +// ============================================================ +const AccountCodeSettingsModal = ({ isOpen, onClose, onUpdate }) => { + const [codes, setCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [newCode, setNewCode] = useState(''); + const [newName, setNewName] = useState(''); + const [newCategory, setNewCategory] = useState(''); + const [filter, setFilter] = useState(''); + const [categoryFilter, setCategoryFilter] = useState(''); + + const categories = ['자산', '부채', '자본', '수익', '비용']; + + useEffect(() => { if (isOpen) loadCodes(); }, [isOpen]); + + const loadCodes = async () => { + setLoading(true); + try { + const res = await fetch('/finance/journal-entries/account-codes/all'); + const data = await res.json(); + if (data.success) setCodes(data.data || []); + } catch (err) { + notify('계정과목 로드 실패', 'error'); + } finally { + setLoading(false); + } + }; + + const handleAdd = async () => { + if (!newCode.trim() || !newName.trim()) { notify('코드와 이름을 입력해주세요.', 'warning'); return; } + try { + const res = await fetch('/finance/journal-entries/account-codes', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF_TOKEN }, + body: JSON.stringify({ code: newCode.trim(), name: newName.trim(), category: newCategory || null }), + }); + const data = await res.json(); + if (data.success) { + notify('계정과목이 추가되었습니다.', 'success'); + setNewCode(''); setNewName(''); setNewCategory(''); + loadCodes(); onUpdate(); + } else { + notify(data.error || '추가 실패', 'error'); + } + } catch (err) { notify('추가 실패: ' + err.message, 'error'); } + }; + + const handleToggleActive = async (item) => { + try { + const res = await fetch(`/finance/journal-entries/account-codes/${item.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF_TOKEN }, + body: JSON.stringify({ is_active: !item.is_active }), + }); + const data = await res.json(); + if (data.success) { loadCodes(); onUpdate(); } + } catch (err) { notify('변경 실패', 'error'); } + }; + + const handleDelete = async (item) => { + if (!confirm(`"${item.code} ${item.name}" 계정과목을 삭제하시겠습니까?`)) return; + try { + const res = await fetch(`/finance/journal-entries/account-codes/${item.id}`, { + method: 'DELETE', + headers: { 'X-CSRF-TOKEN': CSRF_TOKEN }, + }); + const data = await res.json(); + if (data.success) { notify('삭제되었습니다.', 'success'); loadCodes(); onUpdate(); } + else { notify(data.error || '삭제 실패', 'error'); } + } catch (err) { notify('삭제 실패: ' + err.message, 'error'); } + }; + + const filteredCodes = codes.filter(c => { + const matchText = filter === '' || c.code.includes(filter) || c.name.includes(filter); + const matchCategory = categoryFilter === '' || c.category === categoryFilter; + return matchText && matchCategory; + }); + + if (!isOpen) return null; + + return ( +
+
+
+

계정과목 설정

+ +
+
+
+
+ + setNewCode(e.target.value)} placeholder="예: 101" + className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none" /> +
+
+ + setNewName(e.target.value)} placeholder="예: 현금" + className="w-full px-3 py-2 border border-stone-200 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 outline-none" /> +
+
+ + +
+ +
+
+
+ setFilter(e.target.value)} placeholder="코드 또는 이름 검색..." + className="flex-1 px-3 py-2 border border-stone-200 rounded-lg text-sm" /> + + {filteredCodes.length}개 +
+
+ {loading ? ( +
로딩 중...
+ ) : ( + + + + + + + + + + + + {filteredCodes.map(item => ( + + + + + + + + ))} + +
코드계정과목명분류상태작업
{item.code}{item.name} + {item.category || '-'} + + + + +
+ )} +
+
+ +
+
+
+ ); +}; + // ============================================================ // AddTradingPartnerModal // ============================================================ @@ -1042,6 +1216,7 @@ className={`px-6 py-2 text-sm font-medium rounded-lg flex items-center gap-1 tra function App() { const [accountCodes, setAccountCodes] = useState([]); const [tradingPartners, setTradingPartners] = useState([]); + const [showSettingsModal, setShowSettingsModal] = useState(false); const fetchMasterData = async () => { try { @@ -1067,9 +1242,18 @@ function App() { return (
{/* 페이지 헤더 */} -
-

일반전표입력

-

계좌입출금내역을 기반으로 분개 전표를 생성합니다

+
+
+

일반전표입력

+

계좌입출금내역을 기반으로 분개 전표를 생성합니다

+
+
+ + setShowSettingsModal(false)} + onUpdate={fetchMasterData} + />
); } diff --git a/routes/web.php b/routes/web.php index 3b853390..2f40bc0e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -856,6 +856,10 @@ Route::get('/list', [\App\Http\Controllers\Finance\JournalEntryController::class, 'index'])->name('list'); Route::get('/next-entry-no', [\App\Http\Controllers\Finance\JournalEntryController::class, 'nextEntryNo'])->name('next-entry-no'); Route::get('/account-codes', [\App\Http\Controllers\Finance\JournalEntryController::class, 'accountCodes'])->name('account-codes'); + Route::get('/account-codes/all', [\App\Http\Controllers\Finance\JournalEntryController::class, 'accountCodesAll'])->name('account-codes.all'); + Route::post('/account-codes', [\App\Http\Controllers\Finance\JournalEntryController::class, 'accountCodeStore'])->name('account-codes.store'); + Route::put('/account-codes/{id}', [\App\Http\Controllers\Finance\JournalEntryController::class, 'accountCodeUpdate'])->name('account-codes.update'); + Route::delete('/account-codes/{id}', [\App\Http\Controllers\Finance\JournalEntryController::class, 'accountCodeDestroy'])->name('account-codes.destroy'); Route::get('/trading-partners', [\App\Http\Controllers\Finance\JournalEntryController::class, 'tradingPartners'])->name('trading-partners'); // 은행거래 기반 분개