feat: [expense,loan] 대시보드 상세 필터 및 가지급금 카테고리 분류
- ExpectedExpenseController/Service: dashboardDetail에 start_date/end_date/search 파라미터 추가 - Loan 모델: category 상수 및 라벨 정의 (카드/경조사/상품권/접대비) - LoanService: dashboard에 category_breakdown 집계 추가 - 마이그레이션: loans 테이블 category 컬럼 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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 ?? '카드';
|
||||
}
|
||||
|
||||
/**
|
||||
* 미정산 잔액
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<string, array{
|
||||
* outstanding_amount: float,
|
||||
* total_count: int,
|
||||
* unverified_count: int
|
||||
* }>,
|
||||
* 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<string, array{outstanding_amount: float, total_count: int, unverified_count: int}>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금 시뮬레이션 데이터
|
||||
*
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 가지급금 카테고리 컬럼 추가
|
||||
* D1.7 기획서: 카드/경조사/상품권/접대비 4개 카테고리 분류
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('loans', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user