- POST /api/admin/fund-schedules/copy 엔드포인트 추가 - FundScheduleService에 copySchedulesToMonth() 메서드 추가 - 월 네비게이션 옆 일정복사 버튼 및 모달 UI 구현 - 날짜 조정 로직 (31일→28/29/30일) 포함 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
313 lines
10 KiB
PHP
313 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Finance\FundSchedule;
|
|
use App\Services\FundScheduleService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
|
|
class FundScheduleController extends Controller
|
|
{
|
|
public function __construct(
|
|
private FundScheduleService $fundScheduleService
|
|
) {}
|
|
|
|
/**
|
|
* 일정 목록 조회
|
|
*/
|
|
public function index(Request $request): JsonResponse|Response
|
|
{
|
|
$schedules = $this->fundScheduleService->getSchedules(
|
|
$request->all(),
|
|
$request->integer('per_page', 15)
|
|
);
|
|
|
|
// HTMX 요청인 경우 HTML 반환
|
|
if ($request->header('HX-Request')) {
|
|
return response(view('finance.fund-schedules.partials.table', compact('schedules')));
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $schedules->items(),
|
|
'meta' => [
|
|
'current_page' => $schedules->currentPage(),
|
|
'last_page' => $schedules->lastPage(),
|
|
'per_page' => $schedules->perPage(),
|
|
'total' => $schedules->total(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 캘린더용 월별 일정 조회
|
|
*/
|
|
public function calendar(Request $request): JsonResponse|Response
|
|
{
|
|
$year = $request->integer('year', now()->year);
|
|
$month = $request->integer('month', now()->month);
|
|
|
|
$calendarData = $this->fundScheduleService->getCalendarData($year, $month);
|
|
$summary = $this->fundScheduleService->getMonthlySummary($year, $month);
|
|
|
|
// HTMX 요청인 경우 캘린더 HTML 반환
|
|
if ($request->header('HX-Request')) {
|
|
return response(view('finance.fund-schedules.partials.calendar', compact('year', 'month', 'calendarData', 'summary')));
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'year' => $year,
|
|
'month' => $month,
|
|
'schedules' => $calendarData,
|
|
'summary' => $summary,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 일정 상세 조회
|
|
*/
|
|
public function show(int $id): JsonResponse
|
|
{
|
|
$schedule = $this->fundScheduleService->getScheduleById($id);
|
|
|
|
if (! $schedule) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '일정을 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $schedule,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 일정 생성
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'title' => 'required|string|max:200',
|
|
'description' => 'nullable|string',
|
|
'schedule_type' => 'required|in:income,expense',
|
|
'scheduled_date' => 'required|date',
|
|
'amount' => 'required|numeric|min:0',
|
|
'currency' => 'nullable|string|max:3',
|
|
'related_bank_account_id' => 'nullable|integer|exists:bank_accounts,id',
|
|
'counterparty' => 'nullable|string|max:200',
|
|
'category' => 'nullable|string|max:50',
|
|
'status' => 'nullable|in:pending,completed,cancelled',
|
|
'is_recurring' => 'nullable|boolean',
|
|
'recurrence_rule' => 'nullable|in:daily,weekly,monthly,yearly',
|
|
'recurrence_end_date' => 'nullable|date|after:scheduled_date',
|
|
'memo' => 'nullable|string',
|
|
]);
|
|
|
|
$schedule = $this->fundScheduleService->createSchedule($validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '일정이 등록되었습니다.',
|
|
'data' => $schedule,
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* 일정 수정
|
|
*/
|
|
public function update(Request $request, int $id): JsonResponse
|
|
{
|
|
$schedule = $this->fundScheduleService->getScheduleById($id);
|
|
|
|
if (! $schedule) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '일정을 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'title' => 'sometimes|required|string|max:200',
|
|
'description' => 'nullable|string',
|
|
'schedule_type' => 'sometimes|required|in:income,expense',
|
|
'scheduled_date' => 'sometimes|required|date',
|
|
'amount' => 'sometimes|required|numeric|min:0',
|
|
'currency' => 'nullable|string|max:3',
|
|
'related_bank_account_id' => 'nullable|integer|exists:bank_accounts,id',
|
|
'counterparty' => 'nullable|string|max:200',
|
|
'category' => 'nullable|string|max:50',
|
|
'status' => 'nullable|in:pending,completed,cancelled',
|
|
'is_recurring' => 'nullable|boolean',
|
|
'recurrence_rule' => 'nullable|in:daily,weekly,monthly,yearly',
|
|
'recurrence_end_date' => 'nullable|date',
|
|
'memo' => 'nullable|string',
|
|
]);
|
|
|
|
$schedule = $this->fundScheduleService->updateSchedule($schedule, $validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '일정이 수정되었습니다.',
|
|
'data' => $schedule,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 일정 삭제 (Soft Delete)
|
|
*/
|
|
public function destroy(Request $request, int $id): JsonResponse|Response
|
|
{
|
|
$schedule = $this->fundScheduleService->getScheduleById($id);
|
|
|
|
if (! $schedule) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '일정을 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
$this->fundScheduleService->deleteSchedule($schedule);
|
|
|
|
// HTMX 요청인 경우 갱신된 캘린더 반환
|
|
if ($request->header('HX-Request')) {
|
|
$year = $request->integer('year', now()->year);
|
|
$month = $request->integer('month', now()->month);
|
|
$calendarData = $this->fundScheduleService->getCalendarData($year, $month);
|
|
$summary = $this->fundScheduleService->getMonthlySummary($year, $month);
|
|
return response(view('finance.fund-schedules.partials.calendar', compact('year', 'month', 'calendarData', 'summary')));
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '일정이 삭제되었습니다.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 상태 변경
|
|
*/
|
|
public function updateStatus(Request $request, int $id): JsonResponse|Response
|
|
{
|
|
$schedule = $this->fundScheduleService->getScheduleById($id);
|
|
|
|
if (! $schedule) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '일정을 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'status' => 'required|in:pending,completed,cancelled',
|
|
'completed_amount' => 'nullable|numeric|min:0',
|
|
'completed_date' => 'nullable|date',
|
|
]);
|
|
|
|
if ($validated['status'] === FundSchedule::STATUS_COMPLETED) {
|
|
$schedule = $this->fundScheduleService->markAsCompleted(
|
|
$schedule,
|
|
$validated['completed_amount'] ?? null,
|
|
$validated['completed_date'] ?? null
|
|
);
|
|
} elseif ($validated['status'] === FundSchedule::STATUS_CANCELLED) {
|
|
$schedule = $this->fundScheduleService->markAsCancelled($schedule);
|
|
} else {
|
|
$schedule = $this->fundScheduleService->updateStatus($schedule, $validated['status']);
|
|
}
|
|
|
|
// HTMX 요청인 경우 갱신된 캘린더 반환
|
|
if ($request->header('HX-Request')) {
|
|
$year = $request->integer('year', now()->year);
|
|
$month = $request->integer('month', now()->month);
|
|
$calendarData = $this->fundScheduleService->getCalendarData($year, $month);
|
|
$summary = $this->fundScheduleService->getMonthlySummary($year, $month);
|
|
return response(view('finance.fund-schedules.partials.calendar', compact('year', 'month', 'calendarData', 'summary')));
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '상태가 변경되었습니다.',
|
|
'data' => $schedule,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 월별 일정 복사
|
|
*/
|
|
public function copy(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'source_year' => 'required|integer|min:2000|max:2100',
|
|
'source_month' => 'required|integer|min:1|max:12',
|
|
'target_year' => 'required|integer|min:2000|max:2100',
|
|
'target_month' => 'required|integer|min:1|max:12',
|
|
]);
|
|
|
|
if ($validated['source_year'] === $validated['target_year']
|
|
&& $validated['source_month'] === $validated['target_month']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '원본 월과 대상 월이 동일합니다.',
|
|
], 422);
|
|
}
|
|
|
|
$copiedCount = $this->fundScheduleService->copySchedulesToMonth(
|
|
$validated['source_year'],
|
|
$validated['source_month'],
|
|
$validated['target_year'],
|
|
$validated['target_month']
|
|
);
|
|
|
|
if ($copiedCount === 0) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '복사할 일정이 없습니다.',
|
|
], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "{$copiedCount}건의 일정이 복사되었습니다.",
|
|
'data' => ['copied_count' => $copiedCount],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 월별 요약 통계
|
|
*/
|
|
public function summary(Request $request): JsonResponse
|
|
{
|
|
$year = $request->integer('year', now()->year);
|
|
$month = $request->integer('month', now()->month);
|
|
|
|
$summary = $this->fundScheduleService->getMonthlySummary($year, $month);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $summary,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 예정 일정 조회
|
|
*/
|
|
public function upcoming(Request $request): JsonResponse
|
|
{
|
|
$days = $request->integer('days', 30);
|
|
$schedules = $this->fundScheduleService->getUpcomingSchedules($days);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $schedules,
|
|
]);
|
|
}
|
|
}
|