From a1aa6036cf2bb2a726cf5f5fe7b73a5cdd81a2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 19 Feb 2026 13:51:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=EA=B3=A0=EA=B0=9D=EC=82=AC=EC=A0=95?= =?UTF-8?q?=EC=82=B0=20=ED=83=AD=20=EC=9E=AC=EC=84=A4=EA=B3=84=20(?= =?UTF-8?q?=EC=8B=A4=20=ED=85=8C=EB=84=8C=ED=8A=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B8=B0=EB=B0=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - customerTab() 메서드: SalesTenantManagement 기반 쿼리로 재작성 - getCustomerStats() private 메서드 추가 (총개발비/수금완료/미수금/개발진행/구독전환) - customer-tab.blade.php: Alpine.js CRUD → 순수 Blade 테이블로 전체 교체 - index.blade.php: 미사용 customerSettlementManager() Alpine 함수 제거 - 필터: 검색/개발상태/수금상태/담당파트너 4종 - 테이블: 고객사/파트너/매니저/개발비/1차/2차/구독료/개발상태 8열 Co-Authored-By: Claude Opus 4.6 --- .../Finance/SettlementController.php | 130 +++++++- .../views/finance/settlement/index.blade.php | 48 --- .../partials/customer-tab.blade.php | 282 +++++++++++------- 3 files changed, 310 insertions(+), 150 deletions(-) diff --git a/app/Http/Controllers/Finance/SettlementController.php b/app/Http/Controllers/Finance/SettlementController.php index 3d79c6da..c1146732 100644 --- a/app/Http/Controllers/Finance/SettlementController.php +++ b/app/Http/Controllers/Finance/SettlementController.php @@ -4,7 +4,9 @@ use App\Http\Controllers\Controller; use App\Models\Sales\SalesCommission; +use App\Models\Sales\SalesContractProduct; use App\Models\Sales\SalesPartner; +use App\Models\Sales\SalesTenantManagement; use App\Services\SalesCommissionService; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -169,7 +171,91 @@ public function consultingTab(Request $request): View */ public function customerTab(Request $request): View { - return view('finance.settlement.partials.customer-tab'); + $query = SalesTenantManagement::with([ + 'tenant', + 'tenantProspect', + 'salesPartner.user', + 'manager', + 'commissions', + ])->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING); + + // 필터: 검색 (회사명) + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->whereHas('tenant', fn ($tq) => $tq->where('company_name', 'like', "%{$search}%")) + ->orWhereHas('tenantProspect', fn ($pq) => $pq->where('company_name', 'like', "%{$search}%")); + }); + } + + // 필터: 개발 상태 + if ($hqStatus = $request->input('hq_status')) { + $query->where('hq_status', $hqStatus); + } + + // 필터: 담당 파트너 + if ($partnerId = $request->input('partner_id')) { + $query->where('sales_partner_id', $partnerId); + } + + // 필터: 수금 상태 + $paymentStatus = $request->input('payment_status'); + + $managements = $query->orderByDesc('id')->paginate(20)->withQueryString(); + + // 수금 상태 필터 (컬렉션 레벨) + if ($paymentStatus) { + $managements->setCollection( + $managements->getCollection()->filter(function ($mgmt) use ($paymentStatus) { + $depositPaid = $mgmt->deposit_status === 'paid'; + $balancePaid = $mgmt->balance_status === 'paid'; + return match ($paymentStatus) { + 'fully_paid' => $depositPaid && $balancePaid, + 'partial' => ($depositPaid || $balancePaid) && !($depositPaid && $balancePaid), + 'unpaid' => !$depositPaid && !$balancePaid, + default => true, + }; + }) + ); + } + + // 구독료 일괄 조회 (N+1 방지) + $tenantIds = $managements->getCollection() + ->pluck('tenant_id') + ->filter() + ->unique() + ->values() + ->toArray(); + + $subscriptionFees = []; + if (!empty($tenantIds)) { + $subscriptionFees = SalesContractProduct::whereIn('tenant_id', $tenantIds) + ->selectRaw('tenant_id, SUM(subscription_fee) as total_subscription_fee') + ->groupBy('tenant_id') + ->pluck('total_subscription_fee', 'tenant_id') + ->toArray(); + } + + // 파트너 목록 (필터용) + $partners = SalesPartner::with('user') + ->active() + ->orderBy('partner_code') + ->get(); + + // hqStatusLabels에서 pending 제외 + $hqStatusLabels = collect(SalesTenantManagement::$hqStatusLabels) + ->except(SalesTenantManagement::HQ_STATUS_PENDING) + ->toArray(); + + // 통계 카드 + $customerStats = $this->getCustomerStats(); + + return view('finance.settlement.partials.customer-tab', compact( + 'managements', + 'subscriptionFees', + 'partners', + 'hqStatusLabels', + 'customerStats', + )); } /** @@ -317,6 +403,48 @@ public function paymentStats(Request $request): View|Response )); } + /** + * 고객사정산 통계 데이터 + */ + private function getCustomerStats(): array + { + $baseQuery = SalesTenantManagement::where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING); + + // 총 개발비 + $totalFee = (clone $baseQuery)->sum('total_registration_fee'); + $totalCount = (clone $baseQuery)->count(); + + // 수금완료 (deposit + balance 모두 paid인 건의 합계) + $collectedAmount = (clone $baseQuery) + ->where('deposit_status', 'paid') + ->sum('deposit_amount') + + (clone $baseQuery) + ->where('balance_status', 'paid') + ->sum('balance_amount'); + + // 미수금 + $uncollectedAmount = $totalFee - $collectedAmount; + + // 개발 진행 중 (handover 제외) + $inProgressCount = (clone $baseQuery) + ->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_HANDOVER) + ->count(); + + // 구독 전환 (handover + tenant active) + $subscriptionCount = SalesTenantManagement::where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER) + ->whereHas('tenant', fn ($q) => $q->whereNull('deleted_at')) + ->count(); + + return [ + 'total_fee' => $totalFee, + 'total_count' => $totalCount, + 'collected_amount' => $collectedAmount, + 'uncollected_amount' => max(0, $uncollectedAmount), + 'in_progress_count' => $inProgressCount, + 'subscription_count' => $subscriptionCount, + ]; + } + /** * 통합 통계 데이터 */ diff --git a/resources/views/finance/settlement/index.blade.php b/resources/views/finance/settlement/index.blade.php index 6fa155c8..ccd7111d 100644 --- a/resources/views/finance/settlement/index.blade.php +++ b/resources/views/finance/settlement/index.blade.php @@ -369,54 +369,6 @@ function onTenantSelect(managementId) { }); } - // 고객사정산 탭 Alpine 컴포넌트 - function customerSettlementManager() { - return { - items: [], stats: { totalSales: 0, totalCommission: 0, totalNet: 0, settledAmount: 0 }, - loading: false, saving: false, showModal: false, modalMode: 'add', editingId: null, - searchTerm: '', filterStatus: 'all', - form: { period: '', customer: '', totalSales: 0, commission: 0, expense: 0, netAmount: 0, status: 'pending', settledDate: '', memo: '' }, - formatCurrency(val) { return Number(val || 0).toLocaleString(); }, - filteredItems() { - return this.items.filter(item => { - const matchSearch = !this.searchTerm || (item.customer || '').includes(this.searchTerm); - const matchStatus = this.filterStatus === 'all' || item.status === this.filterStatus; - return matchSearch && matchStatus; - }); - }, - async fetchData() { - this.loading = true; - try { - const res = await fetch('/finance/customer-settlements/list'); - const data = await res.json(); - if (data.success) { this.items = data.data; this.stats = data.stats; } - } finally { this.loading = false; } - }, - openModal(mode, item = null) { - this.modalMode = mode; this.editingId = item?.id || null; - this.form = item ? { ...item } : { period: new Date().toISOString().slice(0,7), customer: '', totalSales: 0, commission: 0, expense: 0, netAmount: 0, status: 'pending', settledDate: '', memo: '' }; - this.showModal = true; - }, - async saveItem() { - if (!this.form.customer) { alert('고객사를 입력해주세요.'); return; } - this.form.netAmount = (parseInt(this.form.totalSales)||0) - (parseInt(this.form.commission)||0) - (parseInt(this.form.expense)||0); - this.saving = true; - try { - const url = this.modalMode === 'add' ? '/finance/customer-settlements/store' : '/finance/customer-settlements/' + this.editingId; - const res = await fetch(url, { method: this.modalMode === 'add' ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify(this.form) }); - const data = await res.json(); - if (!res.ok) { alert(data.errors ? Object.values(data.errors).flat().join('\n') : data.message || '저장 실패'); return; } - this.showModal = false; this.fetchData(); - } finally { this.saving = false; } - }, - async deleteItem(id) { - if (!confirm('정말 삭제하시겠습니까?')) return; - await fetch('/finance/customer-settlements/' + id, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content } }); - this.fetchData(); - } - }; - } - // 구독관리 탭 Alpine 컴포넌트 function subscriptionManager() { return { diff --git a/resources/views/finance/settlement/partials/customer-tab.blade.php b/resources/views/finance/settlement/partials/customer-tab.blade.php index d6e10cbd..bfe137b9 100644 --- a/resources/views/finance/settlement/partials/customer-tab.blade.php +++ b/resources/views/finance/settlement/partials/customer-tab.blade.php @@ -1,44 +1,77 @@ -{{-- 고객사정산 탭 (Blade + Alpine.js) --}} -
+{{-- 고객사정산 탭 (실 테넌트 데이터 기반) --}} +
{{-- 통계 카드 --}} -
-
-

총 매출

-

0원

-
+
-

정산금액

-

0원

+

총 개발비

+

{{ number_format($customerStats['total_fee']) }}원

+

{{ $customerStats['total_count'] }}건

+
+
+

수금완료

+

{{ number_format($customerStats['collected_amount']) }}원

+
+
+

미수금

+

{{ number_format($customerStats['uncollected_amount']) }}원

+
+
+

개발 진행 중

+

{{ $customerStats['in_progress_count'] }}건

-

정산완료

-

0원

-
-
-

수수료 합계

-

0원

+

구독 전환

+

{{ $customerStats['subscription_count'] }}건

- {{-- 필터 + 등록 버튼 --}} -
-
-
+ {{-- 필터 --}} +
+
+
- +
- - + + @foreach ($hqStatusLabels as $value => $label) + + @endforeach
- -
+
+ + +
+
+ + +
+
+ +
+
{{-- 테이블 --}} @@ -47,89 +80,136 @@ - - - - - - - - + + + + + + + + - - - - + @empty + + + + @endforelse
정산월고객사매출액수수료비용정산금액상태관리고객사담당파트너담당매니저개발비 총액1차(계약금)2차(잔금)구독료개발상태
+ 개발이 시작된 고객사가 없습니다. +
-
- {{-- 모달 --}} -
-
-
-

- + @if ($managements->hasPages()) +
+ {{ $managements->links() }}
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 정산금액 (매출 - 수수료 - 비용) - -
-
-
-
-
-
-
-
- - -
-
+ @endif
-