From 1deeafc4de481130b7738dee991d4e3871818aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Wed, 4 Mar 2026 10:42:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[expense,loan]=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EC=83=81=EC=84=B8=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B0=80=EC=A7=80=EA=B8=89=EA=B8=88=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B6=84=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ExpectedExpenseController/Service: dashboardDetail에 start_date/end_date/search 파라미터 추가 - Loan 모델: category 상수 및 라벨 정의 (카드/경조사/상품권/접대비) - LoanService: dashboard에 category_breakdown 집계 추가 - 마이그레이션: loans 테이블 category 컬럼 추가 Co-Authored-By: Claude Opus 4.6 --- .../Api/V1/ExpectedExpenseController.php | 7 ++- app/Models/Tenants/Loan.php | 40 +++++++++++++ app/Services/ExpectedExpenseService.php | 60 ++++++++++++------- app/Services/LoanService.php | 56 ++++++++++++++++- ..._04_100000_add_category_to_loans_table.php | 32 ++++++++++ 5 files changed, 167 insertions(+), 28 deletions(-) create mode 100644 database/migrations/2026_03_04_100000_add_category_to_loans_table.php diff --git a/app/Http/Controllers/Api/V1/ExpectedExpenseController.php b/app/Http/Controllers/Api/V1/ExpectedExpenseController.php index 14e6c6c..e51b431 100644 --- a/app/Http/Controllers/Api/V1/ExpectedExpenseController.php +++ b/app/Http/Controllers/Api/V1/ExpectedExpenseController.php @@ -128,13 +128,16 @@ public function summary(Request $request) /** * 대시보드 상세 조회 (CEO 대시보드 당월 예상 지출내역 모달용) * - * @param Request $request transaction_type 쿼리 파라미터 (purchase, card, bill, null=전체) + * @param Request $request transaction_type (purchase, card, bill, null=전체), start_date, end_date, search */ public function dashboardDetail(Request $request) { $transactionType = $request->query('transaction_type'); + $startDate = $request->query('start_date'); + $endDate = $request->query('end_date'); + $search = $request->query('search'); - $data = $this->service->dashboardDetail($transactionType); + $data = $this->service->dashboardDetail($transactionType, $startDate, $endDate, $search); return ApiResponse::success($data, __('message.fetched')); } diff --git a/app/Models/Tenants/Loan.php b/app/Models/Tenants/Loan.php index 542a3ad..7cf7eac 100644 --- a/app/Models/Tenants/Loan.php +++ b/app/Models/Tenants/Loan.php @@ -36,6 +36,37 @@ class Loan extends Model self::STATUS_PARTIAL, ]; + /** + * 카테고리 상수 (D1.7 기획서) + */ + public const CATEGORY_CARD = 'card'; // 카드 + + public const CATEGORY_CONGRATULATORY = 'congratulatory'; // 경조사 + + public const CATEGORY_GIFT_CERTIFICATE = 'gift_certificate'; // 상품권 + + public const CATEGORY_ENTERTAINMENT = 'entertainment'; // 접대비 + + /** + * 카테고리 목록 + */ + public const CATEGORIES = [ + self::CATEGORY_CARD, + self::CATEGORY_CONGRATULATORY, + self::CATEGORY_GIFT_CERTIFICATE, + self::CATEGORY_ENTERTAINMENT, + ]; + + /** + * 카테고리 라벨 매핑 + */ + public const CATEGORY_LABELS = [ + self::CATEGORY_CARD => '카드', + self::CATEGORY_CONGRATULATORY => '경조사', + self::CATEGORY_GIFT_CERTIFICATE => '상품권', + self::CATEGORY_ENTERTAINMENT => '접대비', + ]; + /** * 인정이자율 (연도별) */ @@ -71,6 +102,7 @@ class Loan extends Model 'settlement_date', 'settlement_amount', 'status', + 'category', 'withdrawal_id', 'created_by', 'updated_by', @@ -137,6 +169,14 @@ public function getStatusLabelAttribute(): string }; } + /** + * 카테고리 라벨 + */ + public function getCategoryLabelAttribute(): string + { + return self::CATEGORY_LABELS[$this->category] ?? $this->category ?? '카드'; + } + /** * 미정산 잔액 */ diff --git a/app/Services/ExpectedExpenseService.php b/app/Services/ExpectedExpenseService.php index da717d0..4998ddc 100644 --- a/app/Services/ExpectedExpenseService.php +++ b/app/Services/ExpectedExpenseService.php @@ -304,34 +304,41 @@ public function summary(array $params): array * 대시보드 상세 조회 (CEO 대시보드 당월 예상 지출내역 모달용) * * @param string|null $transactionType 거래유형 필터 (purchase, card, bill, null=전체) - * @return array{ - * summary: array{ - * total_amount: float, - * previous_month_amount: float, - * change_rate: float, - * remaining_balance: float, - * item_count: int - * }, - * monthly_trend: array, - * vendor_distribution: array, - * items: array, - * footer_summary: array - * } + * @param string|null $startDate 조회 시작일 (null이면 당월 1일) + * @param string|null $endDate 조회 종료일 (null이면 당월 말일) + * @param string|null $search 검색어 (거래처명, 적요) */ - public function dashboardDetail(?string $transactionType = null): array - { + public function dashboardDetail( + ?string $transactionType = null, + ?string $startDate = null, + ?string $endDate = null, + ?string $search = null + ): array { $tenantId = $this->tenantId(); - $currentMonthStart = now()->startOfMonth()->toDateString(); - $currentMonthEnd = now()->endOfMonth()->toDateString(); - $previousMonthStart = now()->subMonth()->startOfMonth()->toDateString(); - $previousMonthEnd = now()->subMonth()->endOfMonth()->toDateString(); - // 기본 쿼리 빌더 (transaction_type 필터 적용) - $baseQuery = function () use ($tenantId, $transactionType) { + // 날짜 범위: 파라미터 우선, 없으면 당월 기본값 + $currentMonthStart = $startDate ?? now()->startOfMonth()->toDateString(); + $currentMonthEnd = $endDate ?? now()->endOfMonth()->toDateString(); + + // 전월 대비: 조회 기간과 동일한 길이의 이전 기간 계산 + $startCarbon = \Carbon\Carbon::parse($currentMonthStart); + $endCarbon = \Carbon\Carbon::parse($currentMonthEnd); + $daysDiff = $startCarbon->diffInDays($endCarbon) + 1; + $previousMonthStart = $startCarbon->copy()->subDays($daysDiff)->toDateString(); + $previousMonthEnd = $startCarbon->copy()->subDay()->toDateString(); + + // 기본 쿼리 빌더 (transaction_type + search 필터 적용) + $baseQuery = function () use ($tenantId, $transactionType, $search) { $query = ExpectedExpense::query()->where('tenant_id', $tenantId); if ($transactionType) { $query->where('transaction_type', $transactionType); } + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('client_name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } return $query; }; @@ -361,10 +368,10 @@ public function dashboardDetail(?string $transactionType = null): array // 2. 월별 추이 (최근 7개월) $monthlyTrend = $this->getMonthlyTrend($tenantId, $transactionType); - // 3. 거래처별 분포 (당월, 상위 5개) + // 3. 거래처별 분포 (조회 기간, 상위 5개) $vendorDistribution = $this->getVendorDistribution($tenantId, $transactionType, $currentMonthStart, $currentMonthEnd); - // 4. 지출예상 목록 (당월, 지급일 순) + // 4. 지출예상 목록 (조회 기간, 지급일 순) $itemsQuery = ExpectedExpense::query() ->select([ 'expected_expenses.id', @@ -385,6 +392,13 @@ public function dashboardDetail(?string $transactionType = null): array $itemsQuery->where('expected_expenses.transaction_type', $transactionType); } + if ($search) { + $itemsQuery->where(function ($q) use ($search) { + $q->where('expected_expenses.client_name', 'like', "%{$search}%") + ->orWhere('expected_expenses.description', 'like', "%{$search}%"); + }); + } + $items = $itemsQuery ->orderBy('expected_expenses.expected_payment_date', 'asc') ->get() diff --git a/app/Services/LoanService.php b/app/Services/LoanService.php index 036ae41..56b905a 100644 --- a/app/Services/LoanService.php +++ b/app/Services/LoanService.php @@ -365,7 +365,8 @@ public function calculateInterest(int $year, ?int $userId = null): array /** * 가지급금 대시보드 데이터 * - * CEO 대시보드 카드/가지급금 관리 섹션(cm2) 모달용 데이터 제공 + * CEO 대시보드 카드/가지급금 관리 섹션 데이터 제공 + * D1.7: category_breakdown 추가 (카드/경조사/상품권/접대비 분류) * * @return array{ * summary: array{ @@ -373,6 +374,11 @@ public function calculateInterest(int $year, ?int $userId = null): array * recognized_interest: float, * outstanding_count: int * }, + * category_breakdown: array, * loans: array * } */ @@ -388,7 +394,10 @@ public function dashboard(): array $interestData = $this->calculateInterest($currentYear); $recognizedInterest = $interestData['summary']['total_recognized_interest'] ?? 0; - // 3. 가지급금 목록 (최근 10건, 미정산 우선) + // 3. 카테고리별 집계 (D1.7) + $categoryBreakdown = $this->getCategoryBreakdown($tenantId); + + // 4. 가지급금 목록 (최근 10건, 미정산 우선) $loans = Loan::query() ->where('tenant_id', $tenantId) ->with(['user:id,name,email', 'withdrawal']) @@ -404,7 +413,7 @@ public function dashboard(): array 'id' => $loan->id, 'loan_date' => $loan->loan_date->format('Y-m-d'), 'user_name' => $loan->user?->name ?? '미지정', - 'category' => $loan->withdrawal_id ? '카드' : '계좌', + 'category' => $loan->category_label, 'amount' => (float) $loan->amount, 'status' => $loan->status, 'content' => $loan->purpose ?? '', @@ -418,10 +427,51 @@ public function dashboard(): array 'recognized_interest' => (float) $recognizedInterest, 'outstanding_count' => (int) $summaryData['outstanding_count'], ], + 'category_breakdown' => $categoryBreakdown, 'loans' => $loans, ]; } + /** + * 카테고리별 가지급금 집계 + * + * @return array + */ + private function getCategoryBreakdown(int $tenantId): array + { + // 기본값: 4개 카테고리 모두 0으로 초기화 + $breakdown = []; + foreach (Loan::CATEGORIES as $category) { + $breakdown[$category] = [ + 'outstanding_amount' => 0.0, + 'total_count' => 0, + 'unverified_count' => 0, + ]; + } + + // 카테고리별 미정산 집계 + $stats = Loan::query() + ->where('tenant_id', $tenantId) + ->whereIn('status', [Loan::STATUS_OUTSTANDING, Loan::STATUS_PARTIAL]) + ->selectRaw('category, COUNT(*) as total_count, SUM(amount - COALESCE(settlement_amount, 0)) as outstanding_amount') + ->selectRaw('SUM(CASE WHEN purpose IS NULL OR purpose = \'\' THEN 1 ELSE 0 END) as unverified_count') + ->groupBy('category') + ->get(); + + foreach ($stats as $stat) { + $cat = $stat->category ?? Loan::CATEGORY_CARD; + if (isset($breakdown[$cat])) { + $breakdown[$cat] = [ + 'outstanding_amount' => (float) $stat->outstanding_amount, + 'total_count' => (int) $stat->total_count, + 'unverified_count' => (int) $stat->unverified_count, + ]; + } + } + + return $breakdown; + } + /** * 세금 시뮬레이션 데이터 * diff --git a/database/migrations/2026_03_04_100000_add_category_to_loans_table.php b/database/migrations/2026_03_04_100000_add_category_to_loans_table.php new file mode 100644 index 0000000..70c279c --- /dev/null +++ b/database/migrations/2026_03_04_100000_add_category_to_loans_table.php @@ -0,0 +1,32 @@ +string('category', 30) + ->default('card') + ->after('status') + ->comment('카테고리: card, congratulatory, gift_certificate, entertainment'); + + $table->index(['tenant_id', 'category'], 'idx_tenant_category'); + }); + } + + public function down(): void + { + Schema::table('loans', function (Blueprint $table) { + $table->dropIndex('idx_tenant_category'); + $table->dropColumn('category'); + }); + } +}; \ No newline at end of file