feat:자금계획일정 추가

This commit is contained in:
김보곤
2026-01-20 20:21:06 +09:00
parent 75a4a2b766
commit acad251eec
25 changed files with 6455 additions and 35 deletions

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Finance\BankAccount;
use App\Services\BankAccountService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class BankAccountController extends Controller
{
public function __construct(
private BankAccountService $bankAccountService
) {}
/**
* 계좌 목록 조회
*/
public function index(Request $request): JsonResponse|Response
{
$accounts = $this->bankAccountService->getAccounts(
$request->all(),
$request->integer('per_page', 15)
);
// HTMX 요청인 경우 HTML 반환
if ($request->header('HX-Request')) {
return response(view('finance.accounts.partials.table', compact('accounts')));
}
return response()->json([
'success' => true,
'data' => $accounts->items(),
'meta' => [
'current_page' => $accounts->currentPage(),
'last_page' => $accounts->lastPage(),
'per_page' => $accounts->perPage(),
'total' => $accounts->total(),
],
]);
}
/**
* 계좌 상세 조회
*/
public function show(int $id): JsonResponse
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'data' => $account,
]);
}
/**
* 계좌 생성
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'bank_code' => 'nullable|string|max:10',
'bank_name' => 'required|string|max:50',
'account_number' => 'required|string|max:30',
'account_holder' => 'required|string|max:50',
'account_name' => 'nullable|string|max:100',
'account_type' => 'nullable|string|max:30',
'balance' => 'nullable|numeric|min:0',
'currency' => 'nullable|string|max:3',
'opened_at' => 'nullable|date',
'branch_name' => 'nullable|string|max:100',
'memo' => 'nullable|string',
'status' => 'nullable|string|in:active,inactive',
'is_primary' => 'nullable|boolean',
'sort_order' => 'nullable|integer|min:0',
]);
// 기본값 설정
$validated['status'] = $validated['status'] ?? 'active';
$validated['account_name'] = $validated['account_name'] ?? $validated['bank_name'] . ' 계좌';
$account = $this->bankAccountService->createAccount($validated);
return response()->json([
'success' => true,
'message' => '계좌가 등록되었습니다.',
'data' => $account,
], 201);
}
/**
* 계좌 수정
*/
public function update(Request $request, int $id): JsonResponse
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$validated = $request->validate([
'bank_code' => 'nullable|string|max:10',
'bank_name' => 'sometimes|required|string|max:50',
'account_number' => 'sometimes|required|string|max:30',
'account_holder' => 'nullable|string|max:50',
'account_name' => 'nullable|string|max:100',
'account_type' => 'nullable|string|max:30',
'balance' => 'nullable|numeric|min:0',
'currency' => 'nullable|string|max:3',
'opened_at' => 'nullable|date',
'branch_name' => 'nullable|string|max:100',
'memo' => 'nullable|string',
'status' => 'nullable|string|in:active,inactive',
'is_primary' => 'nullable|boolean',
'sort_order' => 'nullable|integer|min:0',
]);
$account = $this->bankAccountService->updateAccount($account, $validated);
return response()->json([
'success' => true,
'message' => '계좌가 수정되었습니다.',
'data' => $account,
]);
}
/**
* 계좌 삭제 (Soft Delete)
*/
public function destroy(Request $request, int $id): JsonResponse|Response
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$this->bankAccountService->deleteAccount($account);
// HTMX 요청인 경우 갱신된 테이블 반환
if ($request->header('HX-Request')) {
$accounts = $this->bankAccountService->getAccounts($request->all(), $request->integer('per_page', 15));
return response(view('finance.accounts.partials.table', compact('accounts')));
}
return response()->json([
'success' => true,
'message' => '계좌가 삭제되었습니다.',
]);
}
/**
* 계좌 복원
*/
public function restore(Request $request, int $id): JsonResponse|Response
{
$account = $this->bankAccountService->getAccountById($id, withTrashed: true);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$this->bankAccountService->restoreAccount($account);
// HTMX 요청인 경우 갱신된 테이블 반환
if ($request->header('HX-Request')) {
$accounts = $this->bankAccountService->getAccounts($request->all(), $request->integer('per_page', 15));
return response(view('finance.accounts.partials.table', compact('accounts')));
}
return response()->json([
'success' => true,
'message' => '계좌가 복원되었습니다.',
]);
}
/**
* 계좌 영구 삭제
*/
public function forceDelete(Request $request, int $id): JsonResponse|Response
{
$account = $this->bankAccountService->getAccountById($id, withTrashed: true);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$this->bankAccountService->forceDeleteAccount($account);
// HTMX 요청인 경우 갱신된 테이블 반환
if ($request->header('HX-Request')) {
$accounts = $this->bankAccountService->getAccounts($request->all(), $request->integer('per_page', 15));
return response(view('finance.accounts.partials.table', compact('accounts')));
}
return response()->json([
'success' => true,
'message' => '계좌가 영구 삭제되었습니다.',
]);
}
/**
* 활성/비활성 토글
*/
public function toggleActive(Request $request, int $id): JsonResponse|Response
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$account = $this->bankAccountService->toggleActive($account);
// HTMX 요청인 경우 갱신된 테이블 반환
if ($request->header('HX-Request')) {
$accounts = $this->bankAccountService->getAccounts($request->all(), $request->integer('per_page', 15));
return response(view('finance.accounts.partials.table', compact('accounts')));
}
return response()->json([
'success' => true,
'message' => $account->status === 'active' ? '계좌가 활성화되었습니다.' : '계좌가 비활성화되었습니다.',
'data' => $account,
]);
}
/**
* 일괄 삭제
*/
public function bulkDelete(Request $request): JsonResponse
{
$validated = $request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer|exists:bank_accounts,id',
]);
$count = $this->bankAccountService->bulkDelete($validated['ids']);
return response()->json([
'success' => true,
'message' => "{$count}개의 계좌가 삭제되었습니다.",
]);
}
/**
* 일괄 복원
*/
public function bulkRestore(Request $request): JsonResponse
{
$validated = $request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer',
]);
$count = $this->bankAccountService->bulkRestore($validated['ids']);
return response()->json([
'success' => true,
'message' => "{$count}개의 계좌가 복원되었습니다.",
]);
}
/**
* 일괄 영구 삭제
*/
public function bulkForceDelete(Request $request): JsonResponse
{
$validated = $request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer',
]);
$count = $this->bankAccountService->bulkForceDelete($validated['ids']);
return response()->json([
'success' => true,
'message' => "{$count}개의 계좌가 영구 삭제되었습니다.",
]);
}
/**
* 모든 계좌 목록 (드롭다운용)
*/
public function all(): JsonResponse
{
$accounts = $this->bankAccountService->getAllAccounts();
return response()->json([
'success' => true,
'data' => $accounts,
]);
}
/**
* 요약 통계
*/
public function summary(): JsonResponse
{
$summary = $this->bankAccountService->getSummary();
return response()->json([
'success' => true,
'data' => $summary,
]);
}
/**
* 계좌의 거래내역 조회
*/
public function transactions(Request $request, int $id): JsonResponse|Response
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
return response()->json([
'success' => false,
'message' => '계좌를 찾을 수 없습니다.',
], 404);
}
$transactions = $this->bankAccountService->getTransactions(
$id,
$request->all(),
$request->integer('per_page', 20)
);
// HTMX 요청인 경우 HTML 반환
if ($request->header('HX-Request')) {
return response(view('finance.accounts.partials.transactions-table', compact('account', 'transactions')));
}
return response()->json([
'success' => true,
'data' => [
'account' => $account,
'transactions' => $transactions->items(),
],
'meta' => [
'current_page' => $transactions->currentPage(),
'last_page' => $transactions->lastPage(),
'per_page' => $transactions->perPage(),
'total' => $transactions->total(),
],
]);
}
}

View File

@@ -0,0 +1,271 @@
<?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 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,
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Services\BankAccountService;
use Illuminate\Contracts\View\View;
class BankAccountController extends Controller
{
public function __construct(
private BankAccountService $bankAccountService
) {}
/**
* 계좌 목록 페이지
*/
public function index(): View
{
$summary = $this->bankAccountService->getSummary();
return view('finance.accounts.index', [
'summary' => $summary,
]);
}
/**
* 계좌 등록 페이지
*/
public function create(): View
{
return view('finance.accounts.create');
}
/**
* 계좌 수정 페이지
*/
public function edit(int $id): View
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
abort(404, '계좌를 찾을 수 없습니다.');
}
return view('finance.accounts.edit', [
'account' => $account,
]);
}
/**
* 계좌 상세/거래내역 페이지
*/
public function show(int $id): View
{
$account = $this->bankAccountService->getAccountById($id);
if (! $account) {
abort(404, '계좌를 찾을 수 없습니다.');
}
return view('finance.accounts.show', [
'account' => $account,
]);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Models\Finance\BankAccount;
use App\Models\Finance\FundSchedule;
use App\Services\FundScheduleService;
use Illuminate\Contracts\View\View;
class FundScheduleController extends Controller
{
public function __construct(
private FundScheduleService $fundScheduleService
) {}
/**
* 자금계획일정 목록 (캘린더 뷰)
*/
public function index(): View
{
// 현재 연월
$year = (int) request('year', now()->year);
$month = (int) request('month', now()->month);
// 월별 일정 데이터
$calendarData = $this->fundScheduleService->getCalendarData($year, $month);
// 월별 요약
$summary = $this->fundScheduleService->getMonthlySummary($year, $month);
// 계좌 목록 (필터용)
$accounts = BankAccount::active()->ordered()->get(['id', 'bank_name', 'account_number']);
return view('finance.fund-schedules.index', compact(
'year',
'month',
'calendarData',
'summary',
'accounts'
));
}
/**
* 자금계획일정 등록 폼
*/
public function create(): View
{
$accounts = BankAccount::active()->ordered()->get(['id', 'bank_name', 'account_number', 'account_name']);
$types = FundSchedule::getTypeOptions();
$statuses = FundSchedule::getStatusOptions();
$recurrenceOptions = FundSchedule::getRecurrenceOptions();
// 기본 날짜 (쿼리스트링에서)
$defaultDate = request('date', now()->toDateString());
return view('finance.fund-schedules.create', compact(
'accounts',
'types',
'statuses',
'recurrenceOptions',
'defaultDate'
));
}
/**
* 자금계획일정 수정 폼
*/
public function edit(int $id): View
{
$schedule = $this->fundScheduleService->getScheduleById($id);
if (! $schedule) {
abort(404, '일정을 찾을 수 없습니다.');
}
$accounts = BankAccount::active()->ordered()->get(['id', 'bank_name', 'account_number', 'account_name']);
$types = FundSchedule::getTypeOptions();
$statuses = FundSchedule::getStatusOptions();
$recurrenceOptions = FundSchedule::getRecurrenceOptions();
return view('finance.fund-schedules.edit', compact(
'schedule',
'accounts',
'types',
'statuses',
'recurrenceOptions'
));
}
/**
* 자금계획일정 상세
*/
public function show(int $id): View
{
$schedule = $this->fundScheduleService->getScheduleById($id);
if (! $schedule) {
abort(404, '일정을 찾을 수 없습니다.');
}
return view('finance.fund-schedules.show', compact('schedule'));
}
}