feat: [calendar,vat] 캘린더 CRUD 및 부가세 상세 조회 API 추가
- CalendarController/Service: 일정 등록/수정/삭제 API 추가 - VatController/Service: getDetail() 상세 조회 (요약, 참조테이블, 미발행 목록, 신고기간 옵션) - 라우트: POST/PUT/DELETE /calendar/schedules, GET /vat/detail 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,4 +51,56 @@ public function summary(Request $request)
|
||||
);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 등록
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:200',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'start_date' => 'required|date_format:Y-m-d',
|
||||
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||
'start_time' => 'nullable|date_format:H:i',
|
||||
'end_time' => 'nullable|date_format:H:i',
|
||||
'is_all_day' => 'boolean',
|
||||
'color' => 'nullable|string|max:20',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($validated) {
|
||||
return $this->calendarService->createSchedule($validated);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 수정
|
||||
*/
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:200',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'start_date' => 'required|date_format:Y-m-d',
|
||||
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||
'start_time' => 'nullable|date_format:H:i',
|
||||
'end_time' => 'nullable|date_format:H:i',
|
||||
'is_all_day' => 'boolean',
|
||||
'color' => 'nullable|string|max:20',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($id, $validated) {
|
||||
return $this->calendarService->updateSchedule($id, $validated);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->calendarService->deleteSchedule($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,18 @@ public function summary(Request $request): JsonResponse
|
||||
return $this->vatService->getSummary($periodType, $year, $period);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 부가세 상세 조회 (모달용)
|
||||
*/
|
||||
public function detail(Request $request): JsonResponse
|
||||
{
|
||||
$periodType = $request->query('period_type', 'quarter');
|
||||
$year = $request->query('year') ? (int) $request->query('year') : null;
|
||||
$period = $request->query('period') ? (int) $request->query('period') : null;
|
||||
|
||||
return ApiResponse::handle(function () use ($periodType, $year, $period) {
|
||||
return $this->vatService->getDetail($periodType, $year, $period);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,78 @@ private function getLeaveSchedules(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 등록
|
||||
*/
|
||||
public function createSchedule(array $data): array
|
||||
{
|
||||
$schedule = Schedule::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'start_date' => $data['start_date'],
|
||||
'end_date' => $data['end_date'],
|
||||
'start_time' => $data['start_time'] ?? null,
|
||||
'end_time' => $data['end_time'] ?? null,
|
||||
'is_all_day' => $data['is_all_day'] ?? true,
|
||||
'type' => Schedule::TYPE_EVENT,
|
||||
'color' => $data['color'] ?? null,
|
||||
'is_active' => true,
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $schedule->id,
|
||||
'title' => $schedule->title,
|
||||
'start_date' => $schedule->start_date?->format('Y-m-d'),
|
||||
'end_date' => $schedule->end_date?->format('Y-m-d'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 수정
|
||||
*/
|
||||
public function updateSchedule(int $id, array $data): array
|
||||
{
|
||||
$schedule = Schedule::where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
|
||||
$schedule->update([
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'start_date' => $data['start_date'],
|
||||
'end_date' => $data['end_date'],
|
||||
'start_time' => $data['start_time'] ?? null,
|
||||
'end_time' => $data['end_time'] ?? null,
|
||||
'is_all_day' => $data['is_all_day'] ?? true,
|
||||
'color' => $data['color'] ?? null,
|
||||
'updated_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $schedule->id,
|
||||
'title' => $schedule->title,
|
||||
'start_date' => $schedule->start_date?->format('Y-m-d'),
|
||||
'end_date' => $schedule->end_date?->format('Y-m-d'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 삭제 (소프트 삭제)
|
||||
*/
|
||||
public function deleteSchedule(int $id): array
|
||||
{
|
||||
$schedule = Schedule::where('tenant_id', $this->tenantId())
|
||||
->findOrFail($id);
|
||||
|
||||
$schedule->update(['deleted_by' => $this->apiUserId()]);
|
||||
$schedule->delete();
|
||||
|
||||
return [
|
||||
'id' => $schedule->id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 범용 일정 조회 (본사 공통 + 테넌트 일정)
|
||||
*/
|
||||
|
||||
@@ -237,6 +237,139 @@ private function getPeriodLabel(int $year, string $periodType, int $period): str
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 부가세 상세 조회 (모달용)
|
||||
*
|
||||
* @param string|null $periodType 기간 타입 (quarter|half|year)
|
||||
* @param int|null $year 연도
|
||||
* @param int|null $period 기간 번호
|
||||
* @return array
|
||||
*/
|
||||
public function getDetail(?string $periodType = 'quarter', ?int $year = null, ?int $period = null): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$now = Carbon::now();
|
||||
|
||||
$year = $year ?? $now->year;
|
||||
$periodType = $periodType ?? 'quarter';
|
||||
$period = $period ?? $this->getCurrentPeriod($periodType, $now);
|
||||
|
||||
[$startDate, $endDate] = $this->getPeriodDateRange($year, $periodType, $period);
|
||||
$periodLabel = $this->getPeriodLabel($year, $periodType, $period);
|
||||
|
||||
$validStatuses = [TaxInvoice::STATUS_ISSUED, TaxInvoice::STATUS_SENT];
|
||||
|
||||
// 매출 공급가액 + 세액
|
||||
$salesData = TaxInvoice::where('tenant_id', $tenantId)
|
||||
->where('direction', TaxInvoice::DIRECTION_SALES)
|
||||
->whereIn('status', $validStatuses)
|
||||
->whereBetween('issue_date', [$startDate, $endDate])
|
||||
->selectRaw('COALESCE(SUM(supply_amount), 0) as supply_amount, COALESCE(SUM(tax_amount), 0) as tax_amount')
|
||||
->first();
|
||||
|
||||
// 매입 공급가액 + 세액
|
||||
$purchasesData = TaxInvoice::where('tenant_id', $tenantId)
|
||||
->where('direction', TaxInvoice::DIRECTION_PURCHASES)
|
||||
->whereIn('status', $validStatuses)
|
||||
->whereBetween('issue_date', [$startDate, $endDate])
|
||||
->selectRaw('COALESCE(SUM(supply_amount), 0) as supply_amount, COALESCE(SUM(tax_amount), 0) as tax_amount')
|
||||
->first();
|
||||
|
||||
$salesSupplyAmount = (int) ($salesData->supply_amount ?? 0);
|
||||
$salesTaxAmount = (int) ($salesData->tax_amount ?? 0);
|
||||
$purchasesSupplyAmount = (int) ($purchasesData->supply_amount ?? 0);
|
||||
$purchasesTaxAmount = (int) ($purchasesData->tax_amount ?? 0);
|
||||
$estimatedPayment = $salesTaxAmount - $purchasesTaxAmount;
|
||||
|
||||
// 신고기간 옵션 생성
|
||||
$periodOptions = $this->generatePeriodOptions($year, $periodType, $period);
|
||||
|
||||
// 부가세 요약 테이블 (direction + invoice_type 별 GROUP BY)
|
||||
$referenceTable = TaxInvoice::where('tenant_id', $tenantId)
|
||||
->whereIn('status', $validStatuses)
|
||||
->whereBetween('issue_date', [$startDate, $endDate])
|
||||
->selectRaw("
|
||||
direction,
|
||||
invoice_type,
|
||||
COALESCE(SUM(supply_amount), 0) as supply_amount,
|
||||
COALESCE(SUM(tax_amount), 0) as tax_amount
|
||||
")
|
||||
->groupBy('direction', 'invoice_type')
|
||||
->get()
|
||||
->map(fn ($row) => [
|
||||
'direction' => $row->direction,
|
||||
'direction_label' => $row->direction === TaxInvoice::DIRECTION_SALES ? '매출' : '매입',
|
||||
'invoice_type' => $row->invoice_type,
|
||||
'invoice_type_label' => match ($row->invoice_type) {
|
||||
TaxInvoice::TYPE_TAX_INVOICE => '전자세금계산서',
|
||||
TaxInvoice::TYPE_INVOICE => '계산서',
|
||||
TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서',
|
||||
default => $row->invoice_type,
|
||||
},
|
||||
'supply_amount' => (int) $row->supply_amount,
|
||||
'tax_amount' => (int) $row->tax_amount,
|
||||
])
|
||||
->toArray();
|
||||
|
||||
// 미발행/미수취 세금계산서 목록 (status=draft)
|
||||
$unissuedInvoices = TaxInvoice::where('tenant_id', $tenantId)
|
||||
->where('status', TaxInvoice::STATUS_DRAFT)
|
||||
->orderBy('issue_date', 'desc')
|
||||
->limit(100)
|
||||
->get()
|
||||
->map(fn ($invoice) => [
|
||||
'id' => $invoice->id,
|
||||
'direction' => $invoice->direction,
|
||||
'direction_label' => $invoice->direction === TaxInvoice::DIRECTION_SALES ? '매출' : '매입',
|
||||
'issue_date' => $invoice->issue_date,
|
||||
'vendor_name' => $invoice->direction === TaxInvoice::DIRECTION_SALES
|
||||
? ($invoice->buyer_corp_name ?? '-')
|
||||
: ($invoice->supplier_corp_name ?? '-'),
|
||||
'tax_amount' => (int) $invoice->tax_amount,
|
||||
'status' => $invoice->direction === TaxInvoice::DIRECTION_SALES ? '미발행' : '미수취',
|
||||
])
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'period_label' => $periodLabel,
|
||||
'period_options' => $periodOptions,
|
||||
'summary' => [
|
||||
'sales_supply_amount' => $salesSupplyAmount,
|
||||
'sales_tax_amount' => $salesTaxAmount,
|
||||
'purchases_supply_amount' => $purchasesSupplyAmount,
|
||||
'purchases_tax_amount' => $purchasesTaxAmount,
|
||||
'estimated_payment' => (int) abs($estimatedPayment),
|
||||
'is_refund' => $estimatedPayment < 0,
|
||||
],
|
||||
'reference_table' => $referenceTable,
|
||||
'unissued_invoices' => $unissuedInvoices,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 신고기간 드롭다운 옵션 생성
|
||||
* 현재 기간 포함 최근 8개 기간
|
||||
*/
|
||||
private function generatePeriodOptions(int $currentYear, string $periodType, int $currentPeriod): array
|
||||
{
|
||||
$options = [];
|
||||
$year = $currentYear;
|
||||
$period = $currentPeriod;
|
||||
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$label = $this->getPeriodLabel($year, $periodType, $period);
|
||||
$value = "{$year}-{$periodType}-{$period}";
|
||||
$options[] = ['value' => $value, 'label' => $label];
|
||||
|
||||
// 이전 기간으로 이동
|
||||
$prev = $this->getPreviousPeriod($year, $periodType, $period);
|
||||
$year = $prev['year'];
|
||||
$period = $prev['period'];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이전 기간 계산
|
||||
*
|
||||
|
||||
@@ -200,9 +200,13 @@
|
||||
|
||||
// Calendar API (CEO 대시보드 캘린더)
|
||||
Route::get('/calendar/schedules', [CalendarController::class, 'summary'])->name('v1.calendar.schedules');
|
||||
Route::post('/calendar/schedules', [CalendarController::class, 'store'])->name('v1.calendar.schedules.store');
|
||||
Route::put('/calendar/schedules/{id}', [CalendarController::class, 'update'])->whereNumber('id')->name('v1.calendar.schedules.update');
|
||||
Route::delete('/calendar/schedules/{id}', [CalendarController::class, 'destroy'])->whereNumber('id')->name('v1.calendar.schedules.destroy');
|
||||
|
||||
// Vat API (CEO 대시보드 부가세 현황)
|
||||
Route::get('/vat/summary', [VatController::class, 'summary'])->name('v1.vat.summary');
|
||||
Route::get('/vat/detail', [VatController::class, 'detail'])->name('v1.vat.detail');
|
||||
|
||||
// Entertainment API (CEO 대시보드 접대비 현황)
|
||||
Route::get('/entertainment/summary', [EntertainmentController::class, 'summary'])->name('v1.entertainment.summary');
|
||||
|
||||
Reference in New Issue
Block a user