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 ( +
| 코드 | +계정과목명 | +분류 | +상태 | +작업 | +
|---|---|---|---|---|
| {item.code} | +{item.name} | ++ {item.category || '-'} + | ++ + | ++ + | +
계좌입출금내역을 기반으로 분개 전표를 생성합니다
+계좌입출금내역을 기반으로 분개 전표를 생성합니다
+