diff --git a/app/Http/Controllers/Finance/TradingPartnerController.php b/app/Http/Controllers/Finance/TradingPartnerController.php new file mode 100644 index 00000000..8641b078 --- /dev/null +++ b/app/Http/Controllers/Finance/TradingPartnerController.php @@ -0,0 +1,178 @@ +input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('manager', 'like', "%{$search}%"); + }); + } + + if ($type = $request->input('type')) { + if ($type !== 'all') { + $query->where('type', $type); + } + } + + if ($category = $request->input('category')) { + if ($category !== 'all') { + $query->where('category', $category); + } + } + + if ($status = $request->input('status')) { + if ($status !== 'all') { + $query->where('status', $status); + } + } + + $partners = $query->orderBy('created_at', 'desc') + ->get() + ->map(function ($partner) { + return [ + 'id' => $partner->id, + 'name' => $partner->name, + 'type' => $partner->type, + 'category' => $partner->category, + 'bizNo' => $partner->biz_no, + 'bankAccount' => $partner->bank_account, + 'contact' => $partner->contact, + 'email' => $partner->email, + 'manager' => $partner->manager, + 'managerPhone' => $partner->manager_phone, + 'status' => $partner->status, + 'memo' => $partner->memo, + ]; + }); + + $allPartners = TradingPartner::forTenant($tenantId); + $stats = [ + 'total' => (clone $allPartners)->count(), + 'vendor' => (clone $allPartners)->where('type', 'vendor')->count(), + 'freelancer' => (clone $allPartners)->where('type', 'freelancer')->count(), + 'active' => (clone $allPartners)->where('status', 'active')->count(), + ]; + + return response()->json([ + 'success' => true, + 'data' => $partners, + 'stats' => $stats, + ]); + } + + public function store(Request $request): JsonResponse + { + $request->validate([ + 'name' => 'required|string|max:100', + 'type' => 'required|in:vendor,freelancer', + 'category' => 'required|string|max:50', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $partner = TradingPartner::create([ + 'tenant_id' => $tenantId, + 'name' => $request->input('name'), + 'type' => $request->input('type', 'vendor'), + 'category' => $request->input('category', '기타'), + 'biz_no' => $request->input('bizNo'), + 'bank_account' => $request->input('bankAccount'), + 'contact' => $request->input('contact'), + 'email' => $request->input('email'), + 'manager' => $request->input('manager'), + 'manager_phone' => $request->input('managerPhone'), + 'status' => $request->input('status', 'active'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '거래처가 등록되었습니다.', + 'data' => [ + 'id' => $partner->id, + 'name' => $partner->name, + 'type' => $partner->type, + 'category' => $partner->category, + 'bizNo' => $partner->biz_no, + 'bankAccount' => $partner->bank_account, + 'contact' => $partner->contact, + 'email' => $partner->email, + 'manager' => $partner->manager, + 'managerPhone' => $partner->manager_phone, + 'status' => $partner->status, + 'memo' => $partner->memo, + ], + ]); + } + + public function update(Request $request, int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $partner = TradingPartner::forTenant($tenantId)->findOrFail($id); + + $request->validate([ + 'name' => 'required|string|max:100', + 'type' => 'required|in:vendor,freelancer', + 'category' => 'required|string|max:50', + ]); + + $partner->update([ + 'name' => $request->input('name'), + 'type' => $request->input('type'), + 'category' => $request->input('category'), + 'biz_no' => $request->input('bizNo'), + 'bank_account' => $request->input('bankAccount'), + 'contact' => $request->input('contact'), + 'email' => $request->input('email'), + 'manager' => $request->input('manager'), + 'manager_phone' => $request->input('managerPhone'), + 'status' => $request->input('status', 'active'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '거래처가 수정되었습니다.', + 'data' => [ + 'id' => $partner->id, + 'name' => $partner->name, + 'type' => $partner->type, + 'category' => $partner->category, + 'bizNo' => $partner->biz_no, + 'bankAccount' => $partner->bank_account, + 'contact' => $partner->contact, + 'email' => $partner->email, + 'manager' => $partner->manager, + 'managerPhone' => $partner->manager_phone, + 'status' => $partner->status, + 'memo' => $partner->memo, + ], + ]); + } + + public function destroy(int $id): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + $partner = TradingPartner::forTenant($tenantId)->findOrFail($id); + $partner->delete(); + + return response()->json([ + 'success' => true, + 'message' => '거래처가 삭제되었습니다.', + ]); + } +} diff --git a/app/Models/Finance/TradingPartner.php b/app/Models/Finance/TradingPartner.php new file mode 100644 index 00000000..b9c11c81 --- /dev/null +++ b/app/Models/Finance/TradingPartner.php @@ -0,0 +1,38 @@ +where('status', 'active'); + } + + public function scopeForTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } +} diff --git a/resources/views/finance/partners.blade.php b/resources/views/finance/partners.blade.php index 50f3c8b1..481cc46b 100644 --- a/resources/views/finance/partners.blade.php +++ b/resources/views/finance/partners.blade.php @@ -9,6 +9,7 @@ @endpush @section('content') +
@endsection @@ -47,13 +48,9 @@ const Hammer = createIcon('hammer'); function PartnersManagement() { - const [partners, setPartners] = useState([ - { id: 1, name: 'AWS Korea', type: 'vendor', category: '클라우드', bizNo: '111-22-33333', contact: '1544-1234', email: 'support@aws.amazon.com', manager: '김AWS', managerPhone: '', status: 'active', memo: '클라우드 인프라' }, - { id: 2, name: '(주)외주개발', type: 'vendor', category: '외주개발', bizNo: '222-33-44444', contact: '02-5555-6666', email: 'contact@outsource.co.kr', manager: '박개발', managerPhone: '010-5555-6666', status: 'active', memo: '프론트엔드 전문' }, - { id: 3, name: '한국타이어', type: 'vendor', category: '차량관리', bizNo: '333-44-55555', contact: '1588-0000', email: 'service@hankook.com', manager: '', managerPhone: '', status: 'active', memo: '' }, - { id: 4, name: '삼성화재', type: 'vendor', category: '보험', bizNo: '444-55-66666', contact: '1588-5114', email: 'insurance@samsung.com', manager: '이보험', managerPhone: '010-7777-8888', status: 'active', memo: '법인차량 보험' }, - { id: 5, name: '김개발 프리랜서', type: 'freelancer', category: '외주개발', bizNo: '', contact: '', email: 'kim.dev@gmail.com', manager: '김개발', managerPhone: '010-1111-2222', status: 'active', memo: '백엔드 개발자' }, - ]); + const [partners, setPartners] = useState([]); + const [stats, setStats] = useState({ total: 0, vendor: 0, freelancer: 0, active: 0 }); + const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [filterType, setFilterType] = useState('all'); @@ -62,6 +59,9 @@ function PartnersManagement() { const [showModal, setShowModal] = useState(false); const [modalMode, setModalMode] = useState('add'); const [editingItem, setEditingItem] = useState(null); + const [saving, setSaving] = useState(false); + + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); const types = [{ value: 'vendor', label: '공급업체' }, { value: 'freelancer', label: '프리랜서' }]; const categories = ['클라우드', '외주개발', '차량관리', '보험', '사무용품', '마케팅', '법률/회계', '기타']; @@ -81,31 +81,76 @@ function PartnersManagement() { }; const [formData, setFormData] = useState(initialFormState); + const fetchPartners = async () => { + setLoading(true); + try { + const res = await fetch('/finance/partners/list'); + const data = await res.json(); + if (data.success) { + setPartners(data.data); + setStats(data.stats); + } + } catch (err) { + console.error('거래처 조회 실패:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchPartners(); }, []); + const filteredPartners = partners.filter(item => { const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) || - item.manager.toLowerCase().includes(searchTerm.toLowerCase()); + (item.manager || '').toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType === 'all' || item.type === filterType; const matchesCategory = filterCategory === 'all' || item.category === filterCategory; return matchesSearch && matchesType && matchesCategory; }); - const totalPartners = partners.length; - const vendorCount = partners.filter(p => p.type === 'vendor').length; - const freelancerCount = partners.filter(p => p.type === 'freelancer').length; - const activeCount = partners.filter(p => p.status === 'active').length; - const handleAdd = () => { setModalMode('add'); setFormData(initialFormState); setShowModal(true); }; const handleEdit = (item) => { setModalMode('edit'); setEditingItem(item); setFormData({ ...item }); setShowModal(true); }; - const handleSave = () => { + const handleSave = async () => { if (!formData.name) { alert('거래처명을 입력해주세요.'); return; } - if (modalMode === 'add') { - setPartners(prev => [{ id: Date.now(), ...formData }, ...prev]); - } else { - setPartners(prev => prev.map(item => item.id === editingItem.id ? { ...item, ...formData } : item)); + setSaving(true); + try { + const url = modalMode === 'add' ? '/finance/partners/store' : `/finance/partners/${editingItem.id}`; + const res = await fetch(url, { + method: modalMode === 'add' ? 'POST' : 'PUT', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify(formData), + }); + const data = await res.json(); + if (!res.ok) { + const errors = data.errors ? Object.values(data.errors).flat().join('\n') : data.message; + alert(errors || '저장에 실패했습니다.'); + return; + } + setShowModal(false); + setEditingItem(null); + fetchPartners(); + } catch (err) { + console.error('저장 실패:', err); + alert('저장에 실패했습니다.'); + } finally { + setSaving(false); + } + }; + const handleDelete = async (id) => { + if (!confirm('정말 삭제하시겠습니까?')) return; + try { + const res = await fetch(`/finance/partners/${id}`, { + method: 'DELETE', + headers: { 'X-CSRF-TOKEN': csrfToken }, + }); + if (res.ok) { + setShowModal(false); + fetchPartners(); + } + } catch (err) { + console.error('삭제 실패:', err); + alert('삭제에 실패했습니다.'); } - setShowModal(false); setEditingItem(null); }; - const handleDelete = (id) => { if (confirm('정말 삭제하시겠습니까?')) { setPartners(prev => prev.filter(item => item.id !== id)); setShowModal(false); } }; const handleDownload = () => { const rows = [['거래처 관리'], [], ['거래처명', '유형', '분류', '사업자번호', '연락처', '이메일', '담당자', '상태'], @@ -136,19 +181,19 @@ function PartnersManagement() {{totalPartners}개
+{stats.total}개
{vendorCount}개
+{stats.vendor}개
{freelancerCount}개
+{stats.freelancer}개
{activeCount}개
+{stats.active}개