feat: 보고서(Reports) API 구현
- 일일 일보 조회/엑셀 다운로드 API 추가 - 지출 예상 내역서 조회/엑셀 다운로드 API 추가 - ReportService: 전일/당일 잔액 계산, 월별 지출 예상 집계 - Laravel Excel을 이용한 엑셀 내보내기 구현 - Swagger 문서 작성 완료
This commit is contained in:
95
app/Exports/DailyReportExport.php
Normal file
95
app/Exports/DailyReportExport.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromArray;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
|
class DailyReportExport implements FromArray, ShouldAutoSize, WithHeadings, WithStyles, WithTitle
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $report
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시트 제목
|
||||||
|
*/
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '일일일보';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 정의
|
||||||
|
*/
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['일일 일보 - '.$this->report['date']],
|
||||||
|
[],
|
||||||
|
['전일 잔액', number_format($this->report['previous_balance']).'원'],
|
||||||
|
['당일 입금액', number_format($this->report['daily_deposit']).'원'],
|
||||||
|
['당일 출금액', number_format($this->report['daily_withdrawal']).'원'],
|
||||||
|
['당일 잔액', number_format($this->report['current_balance']).'원'],
|
||||||
|
[],
|
||||||
|
['구분', '거래처명', '계정과목', '입금액', '출금액', '적요'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 배열
|
||||||
|
*/
|
||||||
|
public function array(): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($this->report['details'] as $detail) {
|
||||||
|
$rows[] = [
|
||||||
|
$detail['type_label'],
|
||||||
|
$detail['client_name'],
|
||||||
|
$detail['account_code'],
|
||||||
|
$detail['deposit_amount'] > 0 ? number_format($detail['deposit_amount']) : '',
|
||||||
|
$detail['withdrawal_amount'] > 0 ? number_format($detail['withdrawal_amount']) : '',
|
||||||
|
$detail['description'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 합계 행 추가
|
||||||
|
$rows[] = [];
|
||||||
|
$rows[] = [
|
||||||
|
'합계',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
number_format($this->report['daily_deposit']),
|
||||||
|
number_format($this->report['daily_withdrawal']),
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스타일 정의
|
||||||
|
*/
|
||||||
|
public function styles(Worksheet $sheet): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true, 'size' => 14]],
|
||||||
|
3 => ['font' => ['bold' => true]],
|
||||||
|
4 => ['font' => ['bold' => true]],
|
||||||
|
5 => ['font' => ['bold' => true]],
|
||||||
|
6 => ['font' => ['bold' => true]],
|
||||||
|
8 => [
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'E0E0E0'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
121
app/Exports/ExpenseEstimateExport.php
Normal file
121
app/Exports/ExpenseEstimateExport.php
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromArray;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||||
|
|
||||||
|
class ExpenseEstimateExport implements FromArray, ShouldAutoSize, WithHeadings, WithStyles, WithTitle
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly array $report
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시트 제목
|
||||||
|
*/
|
||||||
|
public function title(): string
|
||||||
|
{
|
||||||
|
return '지출예상내역서';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 정의
|
||||||
|
*/
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['지출 예상 내역서 - '.$this->report['year_month']],
|
||||||
|
[],
|
||||||
|
['예상 지출 합계', number_format($this->report['total_estimate']).'원'],
|
||||||
|
['계좌 잔액', number_format($this->report['account_balance']).'원'],
|
||||||
|
['예상 잔액', number_format($this->report['expected_balance']).'원'],
|
||||||
|
[],
|
||||||
|
['예상 지급일', '품목', '지출금액', '거래처', '계좌'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 배열
|
||||||
|
*/
|
||||||
|
public function array(): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($this->report['items'] as $item) {
|
||||||
|
$rows[] = [
|
||||||
|
$item['expected_date'],
|
||||||
|
$item['item_name'],
|
||||||
|
number_format($item['amount']),
|
||||||
|
$item['client_name'],
|
||||||
|
$item['account_name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빈 줄 추가
|
||||||
|
$rows[] = [];
|
||||||
|
|
||||||
|
// 월별 합계
|
||||||
|
$rows[] = ['[월별 합계]', '', '', '', ''];
|
||||||
|
|
||||||
|
foreach ($this->report['monthly_summary']['by_month'] as $month) {
|
||||||
|
$rows[] = [
|
||||||
|
$month['month'].' 계',
|
||||||
|
'',
|
||||||
|
number_format($month['total']),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최종 합계
|
||||||
|
$rows[] = [];
|
||||||
|
$rows[] = [
|
||||||
|
'지출 합계',
|
||||||
|
'',
|
||||||
|
number_format($this->report['monthly_summary']['total_expense']),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
$rows[] = [
|
||||||
|
'계좌 잔액',
|
||||||
|
'',
|
||||||
|
number_format($this->report['monthly_summary']['account_balance']),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
$rows[] = [
|
||||||
|
'최종 차액',
|
||||||
|
'',
|
||||||
|
number_format($this->report['monthly_summary']['final_difference']),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스타일 정의
|
||||||
|
*/
|
||||||
|
public function styles(Worksheet $sheet): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
1 => ['font' => ['bold' => true, 'size' => 14]],
|
||||||
|
3 => ['font' => ['bold' => true]],
|
||||||
|
4 => ['font' => ['bold' => true]],
|
||||||
|
5 => ['font' => ['bold' => true]],
|
||||||
|
7 => [
|
||||||
|
'font' => ['bold' => true],
|
||||||
|
'fill' => [
|
||||||
|
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||||
|
'startColor' => ['rgb' => 'E0E0E0'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Http/Controllers/Api/V1/ReportController.php
Normal file
61
app/Http/Controllers/Api/V1/ReportController.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Exports\DailyReportExport;
|
||||||
|
use App\Exports\ExpenseEstimateExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\V1\Report\DailyReportRequest;
|
||||||
|
use App\Http\Requests\V1\Report\ExpenseEstimateRequest;
|
||||||
|
use App\Http\Responses\ApiResponse;
|
||||||
|
use App\Services\ReportService;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ReportService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일일 일보 조회
|
||||||
|
*/
|
||||||
|
public function daily(DailyReportRequest $request)
|
||||||
|
{
|
||||||
|
$report = $this->service->dailyReport($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $report);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일일 일보 엑셀 다운로드
|
||||||
|
*/
|
||||||
|
public function dailyExport(DailyReportRequest $request)
|
||||||
|
{
|
||||||
|
$report = $this->service->dailyReport($request->validated());
|
||||||
|
$filename = 'daily_report_'.$report['date'].'.xlsx';
|
||||||
|
|
||||||
|
return Excel::download(new DailyReportExport($report), $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지출 예상 내역서 조회
|
||||||
|
*/
|
||||||
|
public function expenseEstimate(ExpenseEstimateRequest $request)
|
||||||
|
{
|
||||||
|
$report = $this->service->expenseEstimate($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(__('message.fetched'), $report);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지출 예상 내역서 엑셀 다운로드
|
||||||
|
*/
|
||||||
|
public function expenseEstimateExport(ExpenseEstimateRequest $request)
|
||||||
|
{
|
||||||
|
$report = $this->service->expenseEstimate($request->validated());
|
||||||
|
$filename = 'expense_estimate_'.$report['year_month'].'.xlsx';
|
||||||
|
|
||||||
|
return Excel::download(new ExpenseEstimateExport($report), $filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Http/Requests/V1/Report/DailyReportRequest.php
Normal file
36
app/Http/Requests/V1/Report/DailyReportRequest.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Report;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DailyReportRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => ['nullable', 'date', 'date_format:Y-m-d'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom attribute names
|
||||||
|
*/
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => __('validation.attributes.date'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Http/Requests/V1/Report/ExpenseEstimateRequest.php
Normal file
46
app/Http/Requests/V1/Report/ExpenseEstimateRequest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Report;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ExpenseEstimateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'year_month' => ['nullable', 'regex:/^\d{4}-\d{2}$/'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom attribute names
|
||||||
|
*/
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'year_month' => __('validation.attributes.year_month'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'year_month.regex' => __('validation.date_format', ['format' => 'YYYY-MM']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
222
app/Services/ReportService.php
Normal file
222
app/Services/ReportService.php
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Deposit;
|
||||||
|
use App\Models\Tenants\Purchase;
|
||||||
|
use App\Models\Tenants\Withdrawal;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class ReportService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 일일 일보 조회
|
||||||
|
*
|
||||||
|
* @param array $params [date: 기준일]
|
||||||
|
*/
|
||||||
|
public function dailyReport(array $params): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$date = Carbon::parse($params['date'] ?? now()->toDateString());
|
||||||
|
$previousDate = $date->copy()->subDay();
|
||||||
|
|
||||||
|
// 전일 잔액 계산 (기준일 전일까지의 모든 입출금 합계)
|
||||||
|
$previousBalance = $this->calculateBalanceUntilDate($tenantId, $previousDate);
|
||||||
|
|
||||||
|
// 당일 입금 합계
|
||||||
|
$dailyDeposit = Deposit::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('deposit_date', $date)
|
||||||
|
->sum('amount');
|
||||||
|
|
||||||
|
// 당일 출금 합계
|
||||||
|
$dailyWithdrawal = Withdrawal::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('withdrawal_date', $date)
|
||||||
|
->sum('amount');
|
||||||
|
|
||||||
|
// 당일 잔액
|
||||||
|
$currentBalance = $previousBalance + $dailyDeposit - $dailyWithdrawal;
|
||||||
|
|
||||||
|
// 상세 내역 (입출금 통합)
|
||||||
|
$details = $this->getDailyDetails($tenantId, $date);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'date' => $date->toDateString(),
|
||||||
|
'previous_balance' => (float) $previousBalance,
|
||||||
|
'daily_deposit' => (float) $dailyDeposit,
|
||||||
|
'daily_withdrawal' => (float) $dailyWithdrawal,
|
||||||
|
'current_balance' => (float) $currentBalance,
|
||||||
|
'details' => $details,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지출 예상 내역서 조회
|
||||||
|
*
|
||||||
|
* @param array $params [year_month: YYYY-MM 형식]
|
||||||
|
*/
|
||||||
|
public function expenseEstimate(array $params): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$yearMonth = $params['year_month'] ?? now()->format('Y-m');
|
||||||
|
[$year, $month] = explode('-', $yearMonth);
|
||||||
|
|
||||||
|
$startDate = Carbon::createFromDate($year, $month, 1)->startOfMonth();
|
||||||
|
$endDate = $startDate->copy()->endOfMonth();
|
||||||
|
|
||||||
|
// 미결제 매입 내역 조회 (지출 예상)
|
||||||
|
$items = Purchase::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereIn('status', ['draft', 'confirmed'])
|
||||||
|
->whereNull('withdrawal_id')
|
||||||
|
->whereBetween('purchase_date', [$startDate, $endDate])
|
||||||
|
->with(['client:id,name'])
|
||||||
|
->orderBy('purchase_date')
|
||||||
|
->get()
|
||||||
|
->map(function ($purchase) {
|
||||||
|
return [
|
||||||
|
'id' => $purchase->id,
|
||||||
|
'expected_date' => $purchase->purchase_date->toDateString(),
|
||||||
|
'item_name' => $purchase->description ?? __('message.report.purchase'),
|
||||||
|
'amount' => (float) $purchase->total_amount,
|
||||||
|
'client_name' => $purchase->client?->name ?? '',
|
||||||
|
'account_name' => '',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 예상 지출 합계
|
||||||
|
$totalEstimate = $items->sum('amount');
|
||||||
|
|
||||||
|
// 계좌 잔액 (대표 계좌 기준, 없으면 모든 계좌 합산)
|
||||||
|
$accountBalance = $this->getAccountBalance($tenantId);
|
||||||
|
|
||||||
|
// 예상 잔액
|
||||||
|
$expectedBalance = $accountBalance - $totalEstimate;
|
||||||
|
|
||||||
|
// 월별 합계
|
||||||
|
$monthlySummary = $this->getMonthlySummary($tenantId, $startDate);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'year_month' => $yearMonth,
|
||||||
|
'total_estimate' => (float) $totalEstimate,
|
||||||
|
'account_balance' => (float) $accountBalance,
|
||||||
|
'expected_balance' => (float) $expectedBalance,
|
||||||
|
'items' => $items->values()->toArray(),
|
||||||
|
'monthly_summary' => $monthlySummary,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 날짜까지의 잔액 계산
|
||||||
|
*/
|
||||||
|
private function calculateBalanceUntilDate(int $tenantId, Carbon $date): float
|
||||||
|
{
|
||||||
|
$totalDeposits = Deposit::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('deposit_date', '<=', $date)
|
||||||
|
->sum('amount');
|
||||||
|
|
||||||
|
$totalWithdrawals = Withdrawal::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('withdrawal_date', '<=', $date)
|
||||||
|
->sum('amount');
|
||||||
|
|
||||||
|
return (float) ($totalDeposits - $totalWithdrawals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일일 상세 내역 조회 (입출금 통합)
|
||||||
|
*/
|
||||||
|
private function getDailyDetails(int $tenantId, Carbon $date): array
|
||||||
|
{
|
||||||
|
// 입금 내역
|
||||||
|
$deposits = Deposit::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('deposit_date', $date)
|
||||||
|
->with(['client:id,name'])
|
||||||
|
->get()
|
||||||
|
->map(function ($deposit) {
|
||||||
|
return [
|
||||||
|
'type' => 'deposit',
|
||||||
|
'type_label' => __('message.report.deposit'),
|
||||||
|
'client_name' => $deposit->client?->name ?? $deposit->client_name ?? '',
|
||||||
|
'account_code' => $deposit->account_code ?? '',
|
||||||
|
'deposit_amount' => (float) $deposit->amount,
|
||||||
|
'withdrawal_amount' => 0,
|
||||||
|
'description' => $deposit->description ?? '',
|
||||||
|
'payment_method' => $deposit->payment_method,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 출금 내역
|
||||||
|
$withdrawals = Withdrawal::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereDate('withdrawal_date', $date)
|
||||||
|
->with(['client:id,name'])
|
||||||
|
->get()
|
||||||
|
->map(function ($withdrawal) {
|
||||||
|
return [
|
||||||
|
'type' => 'withdrawal',
|
||||||
|
'type_label' => __('message.report.withdrawal'),
|
||||||
|
'client_name' => $withdrawal->client?->name ?? $withdrawal->client_name ?? '',
|
||||||
|
'account_code' => $withdrawal->account_code ?? '',
|
||||||
|
'deposit_amount' => 0,
|
||||||
|
'withdrawal_amount' => (float) $withdrawal->amount,
|
||||||
|
'description' => $withdrawal->description ?? '',
|
||||||
|
'payment_method' => $withdrawal->payment_method,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $deposits->concat($withdrawals)->values()->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계좌 잔액 조회
|
||||||
|
* 입출금 내역 기반으로 계산
|
||||||
|
*/
|
||||||
|
private function getAccountBalance(int $tenantId): float
|
||||||
|
{
|
||||||
|
return $this->calculateBalanceUntilDate($tenantId, Carbon::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 월별 지출 예상 합계
|
||||||
|
*/
|
||||||
|
private function getMonthlySummary(int $tenantId, Carbon $baseDate): array
|
||||||
|
{
|
||||||
|
$summary = [];
|
||||||
|
|
||||||
|
// 기준월부터 3개월간 집계
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
$targetDate = $baseDate->copy()->addMonths($i);
|
||||||
|
$startOfMonth = $targetDate->copy()->startOfMonth();
|
||||||
|
$endOfMonth = $targetDate->copy()->endOfMonth();
|
||||||
|
|
||||||
|
$total = Purchase::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->whereIn('status', ['draft', 'confirmed'])
|
||||||
|
->whereNull('withdrawal_id')
|
||||||
|
->whereBetween('purchase_date', [$startOfMonth, $endOfMonth])
|
||||||
|
->sum('total_amount');
|
||||||
|
|
||||||
|
$summary[] = [
|
||||||
|
'month' => $targetDate->format('Y/m'),
|
||||||
|
'total' => (float) $total,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 합계
|
||||||
|
$grandTotal = collect($summary)->sum('total');
|
||||||
|
|
||||||
|
// 계좌 잔액
|
||||||
|
$accountBalance = $this->getAccountBalance($tenantId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'by_month' => $summary,
|
||||||
|
'total_expense' => $grandTotal,
|
||||||
|
'account_balance' => $accountBalance,
|
||||||
|
'final_difference' => $accountBalance - $grandTotal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
212
app/Swagger/v1/ReportApi.php
Normal file
212
app/Swagger/v1/ReportApi.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="Reports", description="보고서 관리")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="DailyReport",
|
||||||
|
* type="object",
|
||||||
|
* description="일일 일보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="date", type="string", format="date", example="2025-01-15", description="기준일"),
|
||||||
|
* @OA\Property(property="previous_balance", type="number", format="float", example=10000000, description="전일 잔액"),
|
||||||
|
* @OA\Property(property="daily_deposit", type="number", format="float", example=5000000, description="당일 입금액"),
|
||||||
|
* @OA\Property(property="daily_withdrawal", type="number", format="float", example=3000000, description="당일 출금액"),
|
||||||
|
* @OA\Property(property="current_balance", type="number", format="float", example=12000000, description="당일 잔액"),
|
||||||
|
* @OA\Property(property="details", type="array", description="상세 내역",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="type", type="string", enum={"deposit","withdrawal"}, description="유형"),
|
||||||
|
* @OA\Property(property="type_label", type="string", example="입금", description="유형 라벨"),
|
||||||
|
* @OA\Property(property="client_name", type="string", example="(주)테스트", description="거래처명"),
|
||||||
|
* @OA\Property(property="account_code", type="string", example="401", description="계정과목"),
|
||||||
|
* @OA\Property(property="deposit_amount", type="number", format="float", example=1000000, description="입금액"),
|
||||||
|
* @OA\Property(property="withdrawal_amount", type="number", format="float", example=0, description="출금액"),
|
||||||
|
* @OA\Property(property="description", type="string", example="1월 매출 입금", description="적요"),
|
||||||
|
* @OA\Property(property="payment_method", type="string", example="transfer", description="결제수단")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="ExpenseEstimate",
|
||||||
|
* type="object",
|
||||||
|
* description="지출 예상 내역서",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="year_month", type="string", example="2025-01", description="기준 연월"),
|
||||||
|
* @OA\Property(property="total_estimate", type="number", format="float", example=15000000, description="예상 지출 합계"),
|
||||||
|
* @OA\Property(property="account_balance", type="number", format="float", example=20000000, description="계좌 잔액"),
|
||||||
|
* @OA\Property(property="expected_balance", type="number", format="float", example=5000000, description="예상 잔액"),
|
||||||
|
* @OA\Property(property="items", type="array", description="지출 예상 내역",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="매입 ID"),
|
||||||
|
* @OA\Property(property="expected_date", type="string", format="date", example="2025-01-20", description="예상 지급일"),
|
||||||
|
* @OA\Property(property="item_name", type="string", example="원자재 구매", description="품목"),
|
||||||
|
* @OA\Property(property="amount", type="number", format="float", example=5000000, description="지출금액"),
|
||||||
|
* @OA\Property(property="client_name", type="string", example="(주)공급사", description="거래처"),
|
||||||
|
* @OA\Property(property="account_name", type="string", example="법인통장", description="계좌")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="monthly_summary", type="object", description="월별 합계",
|
||||||
|
* @OA\Property(property="by_month", type="array",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
* type="object",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="month", type="string", example="2025/01", description="월"),
|
||||||
|
* @OA\Property(property="total", type="number", format="float", example=5000000, description="합계")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="total_expense", type="number", format="float", example=15000000, description="지출 합계"),
|
||||||
|
* @OA\Property(property="account_balance", type="number", format="float", example=20000000, description="계좌 잔액"),
|
||||||
|
* @OA\Property(property="final_difference", type="number", format="float", example=5000000, description="최종 차액")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ReportApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/reports/daily",
|
||||||
|
* tags={"Reports"},
|
||||||
|
* summary="일일 일보 조회",
|
||||||
|
* description="매일 전일의 입출금 및 매출 매입 현황을 자동 집계합니다.",
|
||||||
|
* security={{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="date",
|
||||||
|
* in="query",
|
||||||
|
* description="기준일 (YYYY-MM-DD, 기본값: 오늘)",
|
||||||
|
* required=false,
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", format="date", example="2025-01-15")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/DailyReport")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=422, description="유효성 검사 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function daily() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/reports/daily/export",
|
||||||
|
* tags={"Reports"},
|
||||||
|
* summary="일일 일보 엑셀 다운로드",
|
||||||
|
* description="일일 일보를 엑셀 파일로 다운로드합니다.",
|
||||||
|
* security={{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="date",
|
||||||
|
* in="query",
|
||||||
|
* description="기준일 (YYYY-MM-DD, 기본값: 오늘)",
|
||||||
|
* required=false,
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", format="date", example="2025-01-15")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="엑셀 파일",
|
||||||
|
*
|
||||||
|
* @OA\MediaType(
|
||||||
|
* mediaType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", format="binary")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=422, description="유효성 검사 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function dailyExport() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/reports/expense-estimate",
|
||||||
|
* tags={"Reports"},
|
||||||
|
* summary="지출 예상 내역서 조회",
|
||||||
|
* description="예상 지출 금액 및 일정을 조회합니다.",
|
||||||
|
* security={{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="year_month",
|
||||||
|
* in="query",
|
||||||
|
* description="기준 연월 (YYYY-MM, 기본값: 이번달)",
|
||||||
|
* required=false,
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", example="2025-01")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/ExpenseEstimate")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=422, description="유효성 검사 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function expenseEstimate() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/reports/expense-estimate/export",
|
||||||
|
* tags={"Reports"},
|
||||||
|
* summary="지출 예상 내역서 엑셀 다운로드",
|
||||||
|
* description="지출 예상 내역서를 엑셀 파일로 다운로드합니다.",
|
||||||
|
* security={{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="year_month",
|
||||||
|
* in="query",
|
||||||
|
* description="기준 연월 (YYYY-MM, 기본값: 이번달)",
|
||||||
|
* required=false,
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", example="2025-01")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="엑셀 파일",
|
||||||
|
*
|
||||||
|
* @OA\MediaType(
|
||||||
|
* mediaType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
*
|
||||||
|
* @OA\Schema(type="string", format="binary")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=422, description="유효성 검사 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function expenseEstimateExport() {}
|
||||||
|
}
|
||||||
@@ -238,4 +238,15 @@
|
|||||||
'deleted' => '현장이 삭제되었습니다.',
|
'deleted' => '현장이 삭제되었습니다.',
|
||||||
'active_fetched' => '활성화된 현장 목록을 조회했습니다.',
|
'active_fetched' => '활성화된 현장 목록을 조회했습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 보고서 관리
|
||||||
|
'report' => [
|
||||||
|
'fetched' => '보고서를 조회했습니다.',
|
||||||
|
'daily_fetched' => '일일 일보를 조회했습니다.',
|
||||||
|
'expense_estimate_fetched' => '지출 예상 내역서를 조회했습니다.',
|
||||||
|
'exported' => '보고서가 다운로드되었습니다.',
|
||||||
|
'deposit' => '입금',
|
||||||
|
'withdrawal' => '출금',
|
||||||
|
'purchase' => '매입',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -51,20 +51,21 @@
|
|||||||
use App\Http\Controllers\Api\V1\QuoteController;
|
use App\Http\Controllers\Api\V1\QuoteController;
|
||||||
use App\Http\Controllers\Api\V1\RefreshController;
|
use App\Http\Controllers\Api\V1\RefreshController;
|
||||||
use App\Http\Controllers\Api\V1\RegisterController;
|
use App\Http\Controllers\Api\V1\RegisterController;
|
||||||
|
use App\Http\Controllers\Api\V1\ReportController;
|
||||||
use App\Http\Controllers\Api\V1\RoleController;
|
use App\Http\Controllers\Api\V1\RoleController;
|
||||||
use App\Http\Controllers\Api\V1\RolePermissionController;
|
use App\Http\Controllers\Api\V1\RolePermissionController;
|
||||||
use App\Http\Controllers\Api\V1\SaleController;
|
use App\Http\Controllers\Api\V1\SaleController;
|
||||||
use App\Http\Controllers\Api\V1\SiteController;
|
use App\Http\Controllers\Api\V1\SiteController;
|
||||||
use App\Http\Controllers\Api\V1\TenantController;
|
use App\Http\Controllers\Api\V1\TenantController;
|
||||||
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
|
||||||
// 설계 전용 (디자인 네임스페이스)
|
// 설계 전용 (디자인 네임스페이스)
|
||||||
|
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
||||||
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
||||||
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
||||||
use App\Http\Controllers\Api\V1\UserController;
|
use App\Http\Controllers\Api\V1\UserController;
|
||||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
|
||||||
// 모델셋 관리 (견적 시스템)
|
// 모델셋 관리 (견적 시스템)
|
||||||
|
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||||
use App\Http\Controllers\Api\V1\WithdrawalController;
|
use App\Http\Controllers\Api\V1\WithdrawalController;
|
||||||
use App\Http\Controllers\Api\V1\WorkSettingController;
|
use App\Http\Controllers\Api\V1\WorkSettingController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@@ -340,6 +341,14 @@
|
|||||||
Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm');
|
Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Report API (보고서)
|
||||||
|
Route::prefix('reports')->group(function () {
|
||||||
|
Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');
|
||||||
|
Route::get('/daily/export', [ReportController::class, 'dailyExport'])->name('v1.reports.daily.export');
|
||||||
|
Route::get('/expense-estimate', [ReportController::class, 'expenseEstimate'])->name('v1.reports.expense-estimate');
|
||||||
|
Route::get('/expense-estimate/export', [ReportController::class, 'expenseEstimateExport'])->name('v1.reports.expense-estimate.export');
|
||||||
|
});
|
||||||
|
|
||||||
// Permission API
|
// Permission API
|
||||||
Route::prefix('permissions')->group(function () {
|
Route::prefix('permissions')->group(function () {
|
||||||
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스
|
Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스
|
||||||
|
|||||||
Reference in New Issue
Block a user