From 17799c47de24e66553b9182d6712c52caa32d560 Mon Sep 17 00:00:00 2001 From: hskwon Date: Wed, 17 Dec 2025 21:47:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=202.4=20=EC=9E=85=EA=B8=88/=EC=B6=9C?= =?UTF-8?q?=EA=B8=88=20=EA=B4=80=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마이그레이션: deposits, withdrawals 테이블 생성 - 모델: Deposit, Withdrawal (BelongsToTenant, SoftDeletes) - 서비스: DepositService, WithdrawalService (CRUD + summary) - 컨트롤러: DepositController, WithdrawalController - FormRequest: Store/Update 검증 클래스 - Swagger: 입금/출금 API 문서 (12개 엔드포인트) - 라우트: /v1/deposits, /v1/withdrawals 등록 --- .../Controllers/Api/V1/DepositController.php | 97 ++++++ .../Api/V1/WithdrawalController.php | 97 ++++++ .../V1/Deposit/StoreDepositRequest.php | 54 +++ .../V1/Deposit/UpdateDepositRequest.php | 43 +++ .../V1/Withdrawal/StoreWithdrawalRequest.php | 54 +++ .../V1/Withdrawal/UpdateWithdrawalRequest.php | 43 +++ app/Models/Tenants/Deposit.php | 92 +++++ app/Models/Tenants/Withdrawal.php | 92 +++++ app/Services/DepositService.php | 228 +++++++++++++ app/Services/WithdrawalService.php | 228 +++++++++++++ app/Swagger/v1/DepositApi.php | 313 ++++++++++++++++++ app/Swagger/v1/WithdrawalApi.php | 313 ++++++++++++++++++ ...025_12_17_130000_create_deposits_table.php | 40 +++ ..._12_17_130001_create_withdrawals_table.php | 40 +++ routes/api.php | 22 ++ 15 files changed, 1756 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/DepositController.php create mode 100644 app/Http/Controllers/Api/V1/WithdrawalController.php create mode 100644 app/Http/Requests/V1/Deposit/StoreDepositRequest.php create mode 100644 app/Http/Requests/V1/Deposit/UpdateDepositRequest.php create mode 100644 app/Http/Requests/V1/Withdrawal/StoreWithdrawalRequest.php create mode 100644 app/Http/Requests/V1/Withdrawal/UpdateWithdrawalRequest.php create mode 100644 app/Models/Tenants/Deposit.php create mode 100644 app/Models/Tenants/Withdrawal.php create mode 100644 app/Services/DepositService.php create mode 100644 app/Services/WithdrawalService.php create mode 100644 app/Swagger/v1/DepositApi.php create mode 100644 app/Swagger/v1/WithdrawalApi.php create mode 100644 database/migrations/2025_12_17_130000_create_deposits_table.php create mode 100644 database/migrations/2025_12_17_130001_create_withdrawals_table.php diff --git a/app/Http/Controllers/Api/V1/DepositController.php b/app/Http/Controllers/Api/V1/DepositController.php new file mode 100644 index 0000000..3e82d3e --- /dev/null +++ b/app/Http/Controllers/Api/V1/DepositController.php @@ -0,0 +1,97 @@ +only([ + 'search', + 'start_date', + 'end_date', + 'client_id', + 'payment_method', + 'bank_account_id', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $deposits = $this->service->index($params); + + return ApiResponse::handle(__('message.fetched'), $deposits); + } + + /** + * 입금 등록 + */ + public function store(StoreDepositRequest $request) + { + $deposit = $this->service->store($request->validated()); + + return ApiResponse::handle(__('message.created'), $deposit, 201); + } + + /** + * 입금 상세 + */ + public function show(int $id) + { + $deposit = $this->service->show($id); + + return ApiResponse::handle(__('message.fetched'), $deposit); + } + + /** + * 입금 수정 + */ + public function update(int $id, UpdateDepositRequest $request) + { + $deposit = $this->service->update($id, $request->validated()); + + return ApiResponse::handle(__('message.updated'), $deposit); + } + + /** + * 입금 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::handle(__('message.deleted')); + } + + /** + * 입금 요약 (기간별 합계) + */ + public function summary(Request $request) + { + $params = $request->only([ + 'start_date', + 'end_date', + 'client_id', + 'payment_method', + ]); + + $summary = $this->service->summary($params); + + return ApiResponse::handle(__('message.fetched'), $summary); + } +} diff --git a/app/Http/Controllers/Api/V1/WithdrawalController.php b/app/Http/Controllers/Api/V1/WithdrawalController.php new file mode 100644 index 0000000..d12c078 --- /dev/null +++ b/app/Http/Controllers/Api/V1/WithdrawalController.php @@ -0,0 +1,97 @@ +only([ + 'search', + 'start_date', + 'end_date', + 'client_id', + 'payment_method', + 'bank_account_id', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $withdrawals = $this->service->index($params); + + return ApiResponse::handle(__('message.fetched'), $withdrawals); + } + + /** + * 출금 등록 + */ + public function store(StoreWithdrawalRequest $request) + { + $withdrawal = $this->service->store($request->validated()); + + return ApiResponse::handle(__('message.created'), $withdrawal, 201); + } + + /** + * 출금 상세 + */ + public function show(int $id) + { + $withdrawal = $this->service->show($id); + + return ApiResponse::handle(__('message.fetched'), $withdrawal); + } + + /** + * 출금 수정 + */ + public function update(int $id, UpdateWithdrawalRequest $request) + { + $withdrawal = $this->service->update($id, $request->validated()); + + return ApiResponse::handle(__('message.updated'), $withdrawal); + } + + /** + * 출금 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::handle(__('message.deleted')); + } + + /** + * 출금 요약 (기간별 합계) + */ + public function summary(Request $request) + { + $params = $request->only([ + 'start_date', + 'end_date', + 'client_id', + 'payment_method', + ]); + + $summary = $this->service->summary($params); + + return ApiResponse::handle(__('message.fetched'), $summary); + } +} diff --git a/app/Http/Requests/V1/Deposit/StoreDepositRequest.php b/app/Http/Requests/V1/Deposit/StoreDepositRequest.php new file mode 100644 index 0000000..0fff63b --- /dev/null +++ b/app/Http/Requests/V1/Deposit/StoreDepositRequest.php @@ -0,0 +1,54 @@ + ['required', 'date'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'amount' => ['required', 'numeric', 'min:0'], + 'payment_method' => ['required', 'string', 'in:cash,transfer,card,check'], + 'account_code' => ['nullable', 'string', 'max:20'], + 'description' => ['nullable', 'string', 'max:1000'], + 'reference_type' => ['nullable', 'string', 'max:50'], + 'reference_id' => ['nullable', 'integer'], + ]; + } + + public function messages(): array + { + return [ + 'deposit_date.required' => __('validation.required', ['attribute' => __('validation.attributes.deposit_date')]), + 'amount.required' => __('validation.required', ['attribute' => __('validation.attributes.amount')]), + 'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]), + 'payment_method.required' => __('validation.required', ['attribute' => __('validation.attributes.payment_method')]), + 'payment_method.in' => __('validation.in', ['attribute' => __('validation.attributes.payment_method')]), + ]; + } + + public function attributes(): array + { + return [ + 'deposit_date' => __('validation.attributes.deposit_date'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + 'amount' => __('validation.attributes.amount'), + 'payment_method' => __('validation.attributes.payment_method'), + 'account_code' => __('validation.attributes.account_code'), + 'description' => __('validation.attributes.description'), + ]; + } +} diff --git a/app/Http/Requests/V1/Deposit/UpdateDepositRequest.php b/app/Http/Requests/V1/Deposit/UpdateDepositRequest.php new file mode 100644 index 0000000..57f1f0e --- /dev/null +++ b/app/Http/Requests/V1/Deposit/UpdateDepositRequest.php @@ -0,0 +1,43 @@ + ['sometimes', 'date'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'amount' => ['sometimes', 'numeric', 'min:0'], + 'payment_method' => ['sometimes', 'string', 'in:cash,transfer,card,check'], + 'account_code' => ['nullable', 'string', 'max:20'], + 'description' => ['nullable', 'string', 'max:1000'], + 'reference_type' => ['nullable', 'string', 'max:50'], + 'reference_id' => ['nullable', 'integer'], + ]; + } + + public function attributes(): array + { + return [ + 'deposit_date' => __('validation.attributes.deposit_date'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + 'amount' => __('validation.attributes.amount'), + 'payment_method' => __('validation.attributes.payment_method'), + 'account_code' => __('validation.attributes.account_code'), + 'description' => __('validation.attributes.description'), + ]; + } +} diff --git a/app/Http/Requests/V1/Withdrawal/StoreWithdrawalRequest.php b/app/Http/Requests/V1/Withdrawal/StoreWithdrawalRequest.php new file mode 100644 index 0000000..1aebdba --- /dev/null +++ b/app/Http/Requests/V1/Withdrawal/StoreWithdrawalRequest.php @@ -0,0 +1,54 @@ + ['required', 'date'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'amount' => ['required', 'numeric', 'min:0'], + 'payment_method' => ['required', 'string', 'in:cash,transfer,card,check'], + 'account_code' => ['nullable', 'string', 'max:20'], + 'description' => ['nullable', 'string', 'max:1000'], + 'reference_type' => ['nullable', 'string', 'max:50'], + 'reference_id' => ['nullable', 'integer'], + ]; + } + + public function messages(): array + { + return [ + 'withdrawal_date.required' => __('validation.required', ['attribute' => __('validation.attributes.withdrawal_date')]), + 'amount.required' => __('validation.required', ['attribute' => __('validation.attributes.amount')]), + 'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]), + 'payment_method.required' => __('validation.required', ['attribute' => __('validation.attributes.payment_method')]), + 'payment_method.in' => __('validation.in', ['attribute' => __('validation.attributes.payment_method')]), + ]; + } + + public function attributes(): array + { + return [ + 'withdrawal_date' => __('validation.attributes.withdrawal_date'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + 'amount' => __('validation.attributes.amount'), + 'payment_method' => __('validation.attributes.payment_method'), + 'account_code' => __('validation.attributes.account_code'), + 'description' => __('validation.attributes.description'), + ]; + } +} diff --git a/app/Http/Requests/V1/Withdrawal/UpdateWithdrawalRequest.php b/app/Http/Requests/V1/Withdrawal/UpdateWithdrawalRequest.php new file mode 100644 index 0000000..a15536e --- /dev/null +++ b/app/Http/Requests/V1/Withdrawal/UpdateWithdrawalRequest.php @@ -0,0 +1,43 @@ + ['sometimes', 'date'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'amount' => ['sometimes', 'numeric', 'min:0'], + 'payment_method' => ['sometimes', 'string', 'in:cash,transfer,card,check'], + 'account_code' => ['nullable', 'string', 'max:20'], + 'description' => ['nullable', 'string', 'max:1000'], + 'reference_type' => ['nullable', 'string', 'max:50'], + 'reference_id' => ['nullable', 'integer'], + ]; + } + + public function attributes(): array + { + return [ + 'withdrawal_date' => __('validation.attributes.withdrawal_date'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + 'amount' => __('validation.attributes.amount'), + 'payment_method' => __('validation.attributes.payment_method'), + 'account_code' => __('validation.attributes.account_code'), + 'description' => __('validation.attributes.description'), + ]; + } +} diff --git a/app/Models/Tenants/Deposit.php b/app/Models/Tenants/Deposit.php new file mode 100644 index 0000000..69ee158 --- /dev/null +++ b/app/Models/Tenants/Deposit.php @@ -0,0 +1,92 @@ + 'date', + 'amount' => 'decimal:2', + 'client_id' => 'integer', + 'bank_account_id' => 'integer', + 'reference_id' => 'integer', + ]; + + /** + * 결제수단 목록 + */ + public const PAYMENT_METHODS = [ + 'cash' => '현금', + 'transfer' => '계좌이체', + 'card' => '카드', + 'check' => '수표', + ]; + + /** + * 거래처 관계 + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** + * 입금 계좌 관계 + */ + public function bankAccount(): BelongsTo + { + return $this->belongsTo(BankAccount::class); + } + + /** + * 생성자 관계 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'created_by'); + } + + /** + * 거래처명 조회 (회원/비회원 통합) + */ + public function getDisplayClientNameAttribute(): string + { + if ($this->client) { + return $this->client->name; + } + + return $this->client_name ?? ''; + } + + /** + * 결제수단 라벨 + */ + public function getPaymentMethodLabelAttribute(): string + { + return self::PAYMENT_METHODS[$this->payment_method] ?? $this->payment_method; + } +} diff --git a/app/Models/Tenants/Withdrawal.php b/app/Models/Tenants/Withdrawal.php new file mode 100644 index 0000000..7d5e1c4 --- /dev/null +++ b/app/Models/Tenants/Withdrawal.php @@ -0,0 +1,92 @@ + 'date', + 'amount' => 'decimal:2', + 'client_id' => 'integer', + 'bank_account_id' => 'integer', + 'reference_id' => 'integer', + ]; + + /** + * 결제수단 목록 + */ + public const PAYMENT_METHODS = [ + 'cash' => '현금', + 'transfer' => '계좌이체', + 'card' => '카드', + 'check' => '수표', + ]; + + /** + * 거래처 관계 + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** + * 출금 계좌 관계 + */ + public function bankAccount(): BelongsTo + { + return $this->belongsTo(BankAccount::class); + } + + /** + * 생성자 관계 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'created_by'); + } + + /** + * 거래처명 조회 (회원/비회원 통합) + */ + public function getDisplayClientNameAttribute(): string + { + if ($this->client) { + return $this->client->name; + } + + return $this->client_name ?? ''; + } + + /** + * 결제수단 라벨 + */ + public function getPaymentMethodLabelAttribute(): string + { + return self::PAYMENT_METHODS[$this->payment_method] ?? $this->payment_method; + } +} diff --git a/app/Services/DepositService.php b/app/Services/DepositService.php new file mode 100644 index 0000000..f574c7b --- /dev/null +++ b/app/Services/DepositService.php @@ -0,0 +1,228 @@ +tenantId(); + + $query = Deposit::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name']); + + // 검색어 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('client_name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%") + ->orWhereHas('client', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); + }); + } + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('deposit_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('deposit_date', '<=', $params['end_date']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 결제수단 필터 + if (! empty($params['payment_method'])) { + $query->where('payment_method', $params['payment_method']); + } + + // 계좌 필터 + if (! empty($params['bank_account_id'])) { + $query->where('bank_account_id', $params['bank_account_id']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'deposit_date'; + $sortDir = $params['sort_dir'] ?? 'desc'; + $query->orderBy($sortBy, $sortDir); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 입금 상세 조회 + */ + public function show(int $id): Deposit + { + $tenantId = $this->tenantId(); + + return Deposit::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'creator:id,name']) + ->findOrFail($id); + } + + /** + * 입금 등록 + */ + public function store(array $data): Deposit + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + $deposit = new Deposit; + $deposit->tenant_id = $tenantId; + $deposit->deposit_date = $data['deposit_date']; + $deposit->client_id = $data['client_id'] ?? null; + $deposit->client_name = $data['client_name'] ?? null; + $deposit->bank_account_id = $data['bank_account_id'] ?? null; + $deposit->amount = $data['amount']; + $deposit->payment_method = $data['payment_method']; + $deposit->account_code = $data['account_code'] ?? null; + $deposit->description = $data['description'] ?? null; + $deposit->reference_type = $data['reference_type'] ?? null; + $deposit->reference_id = $data['reference_id'] ?? null; + $deposit->created_by = $userId; + $deposit->updated_by = $userId; + $deposit->save(); + + return $deposit->load(['client:id,name', 'bankAccount:id,bank_name,account_name']); + }); + } + + /** + * 입금 수정 + */ + public function update(int $id, array $data): Deposit + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $deposit = Deposit::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + if (isset($data['deposit_date'])) { + $deposit->deposit_date = $data['deposit_date']; + } + if (array_key_exists('client_id', $data)) { + $deposit->client_id = $data['client_id']; + } + if (array_key_exists('client_name', $data)) { + $deposit->client_name = $data['client_name']; + } + if (array_key_exists('bank_account_id', $data)) { + $deposit->bank_account_id = $data['bank_account_id']; + } + if (isset($data['amount'])) { + $deposit->amount = $data['amount']; + } + if (isset($data['payment_method'])) { + $deposit->payment_method = $data['payment_method']; + } + if (array_key_exists('account_code', $data)) { + $deposit->account_code = $data['account_code']; + } + if (array_key_exists('description', $data)) { + $deposit->description = $data['description']; + } + if (array_key_exists('reference_type', $data)) { + $deposit->reference_type = $data['reference_type']; + } + if (array_key_exists('reference_id', $data)) { + $deposit->reference_id = $data['reference_id']; + } + + $deposit->updated_by = $userId; + $deposit->save(); + + return $deposit->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name']); + }); + } + + /** + * 입금 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $deposit = Deposit::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $deposit->deleted_by = $userId; + $deposit->save(); + $deposit->delete(); + + return true; + }); + } + + /** + * 입금 요약 (기간별 합계) + */ + public function summary(array $params): array + { + $tenantId = $this->tenantId(); + + $query = Deposit::query() + ->where('tenant_id', $tenantId); + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('deposit_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('deposit_date', '<=', $params['end_date']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 결제수단 필터 + if (! empty($params['payment_method'])) { + $query->where('payment_method', $params['payment_method']); + } + + // 전체 합계 + $total = (clone $query)->sum('amount'); + $count = (clone $query)->count(); + + // 결제수단별 합계 + $byPaymentMethod = (clone $query) + ->select('payment_method', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count')) + ->groupBy('payment_method') + ->get() + ->keyBy('payment_method') + ->toArray(); + + return [ + 'total_amount' => (float) $total, + 'total_count' => $count, + 'by_payment_method' => $byPaymentMethod, + ]; + } +} diff --git a/app/Services/WithdrawalService.php b/app/Services/WithdrawalService.php new file mode 100644 index 0000000..6e9dd04 --- /dev/null +++ b/app/Services/WithdrawalService.php @@ -0,0 +1,228 @@ +tenantId(); + + $query = Withdrawal::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name']); + + // 검색어 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('client_name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%") + ->orWhereHas('client', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); + }); + } + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('withdrawal_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('withdrawal_date', '<=', $params['end_date']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 결제수단 필터 + if (! empty($params['payment_method'])) { + $query->where('payment_method', $params['payment_method']); + } + + // 계좌 필터 + if (! empty($params['bank_account_id'])) { + $query->where('bank_account_id', $params['bank_account_id']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'withdrawal_date'; + $sortDir = $params['sort_dir'] ?? 'desc'; + $query->orderBy($sortBy, $sortDir); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 출금 상세 조회 + */ + public function show(int $id): Withdrawal + { + $tenantId = $this->tenantId(); + + return Withdrawal::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'creator:id,name']) + ->findOrFail($id); + } + + /** + * 출금 등록 + */ + public function store(array $data): Withdrawal + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + $withdrawal = new Withdrawal; + $withdrawal->tenant_id = $tenantId; + $withdrawal->withdrawal_date = $data['withdrawal_date']; + $withdrawal->client_id = $data['client_id'] ?? null; + $withdrawal->client_name = $data['client_name'] ?? null; + $withdrawal->bank_account_id = $data['bank_account_id'] ?? null; + $withdrawal->amount = $data['amount']; + $withdrawal->payment_method = $data['payment_method']; + $withdrawal->account_code = $data['account_code'] ?? null; + $withdrawal->description = $data['description'] ?? null; + $withdrawal->reference_type = $data['reference_type'] ?? null; + $withdrawal->reference_id = $data['reference_id'] ?? null; + $withdrawal->created_by = $userId; + $withdrawal->updated_by = $userId; + $withdrawal->save(); + + return $withdrawal->load(['client:id,name', 'bankAccount:id,bank_name,account_name']); + }); + } + + /** + * 출금 수정 + */ + public function update(int $id, array $data): Withdrawal + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $withdrawal = Withdrawal::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + if (isset($data['withdrawal_date'])) { + $withdrawal->withdrawal_date = $data['withdrawal_date']; + } + if (array_key_exists('client_id', $data)) { + $withdrawal->client_id = $data['client_id']; + } + if (array_key_exists('client_name', $data)) { + $withdrawal->client_name = $data['client_name']; + } + if (array_key_exists('bank_account_id', $data)) { + $withdrawal->bank_account_id = $data['bank_account_id']; + } + if (isset($data['amount'])) { + $withdrawal->amount = $data['amount']; + } + if (isset($data['payment_method'])) { + $withdrawal->payment_method = $data['payment_method']; + } + if (array_key_exists('account_code', $data)) { + $withdrawal->account_code = $data['account_code']; + } + if (array_key_exists('description', $data)) { + $withdrawal->description = $data['description']; + } + if (array_key_exists('reference_type', $data)) { + $withdrawal->reference_type = $data['reference_type']; + } + if (array_key_exists('reference_id', $data)) { + $withdrawal->reference_id = $data['reference_id']; + } + + $withdrawal->updated_by = $userId; + $withdrawal->save(); + + return $withdrawal->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name']); + }); + } + + /** + * 출금 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $withdrawal = Withdrawal::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $withdrawal->deleted_by = $userId; + $withdrawal->save(); + $withdrawal->delete(); + + return true; + }); + } + + /** + * 출금 요약 (기간별 합계) + */ + public function summary(array $params): array + { + $tenantId = $this->tenantId(); + + $query = Withdrawal::query() + ->where('tenant_id', $tenantId); + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('withdrawal_date', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('withdrawal_date', '<=', $params['end_date']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 결제수단 필터 + if (! empty($params['payment_method'])) { + $query->where('payment_method', $params['payment_method']); + } + + // 전체 합계 + $total = (clone $query)->sum('amount'); + $count = (clone $query)->count(); + + // 결제수단별 합계 + $byPaymentMethod = (clone $query) + ->select('payment_method', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count')) + ->groupBy('payment_method') + ->get() + ->keyBy('payment_method') + ->toArray(); + + return [ + 'total_amount' => (float) $total, + 'total_count' => $count, + 'by_payment_method' => $byPaymentMethod, + ]; + } +} diff --git a/app/Swagger/v1/DepositApi.php b/app/Swagger/v1/DepositApi.php new file mode 100644 index 0000000..c14ae77 --- /dev/null +++ b/app/Swagger/v1/DepositApi.php @@ -0,0 +1,313 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->date('deposit_date')->comment('입금일'); + $table->unsignedBigInteger('client_id')->nullable()->comment('거래처 ID'); + $table->string('client_name', 100)->nullable()->comment('비회원 거래처명'); + $table->unsignedBigInteger('bank_account_id')->nullable()->comment('입금 계좌 ID'); + $table->decimal('amount', 15, 2)->comment('금액'); + $table->string('payment_method', 20)->comment('결제수단: cash/transfer/card/check'); + $table->string('account_code', 20)->nullable()->comment('계정과목'); + $table->text('description')->nullable()->comment('적요'); + $table->string('reference_type', 50)->nullable()->comment('참조 유형: sales/receivable/etc'); + $table->unsignedBigInteger('reference_id')->nullable()->comment('참조 ID'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->softDeletes(); + $table->timestamps(); + + $table->index(['tenant_id', 'deposit_date'], 'idx_tenant_date'); + $table->index('client_id', 'idx_client'); + $table->index('payment_method', 'idx_payment_method'); + }); + } + + public function down(): void + { + Schema::dropIfExists('deposits'); + } +}; diff --git a/database/migrations/2025_12_17_130001_create_withdrawals_table.php b/database/migrations/2025_12_17_130001_create_withdrawals_table.php new file mode 100644 index 0000000..ad6be30 --- /dev/null +++ b/database/migrations/2025_12_17_130001_create_withdrawals_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->date('withdrawal_date')->comment('출금일'); + $table->unsignedBigInteger('client_id')->nullable()->comment('거래처 ID'); + $table->string('client_name', 100)->nullable()->comment('비회원 거래처명'); + $table->unsignedBigInteger('bank_account_id')->nullable()->comment('출금 계좌 ID'); + $table->decimal('amount', 15, 2)->comment('금액'); + $table->string('payment_method', 20)->comment('결제수단: cash/transfer/card/check'); + $table->string('account_code', 20)->nullable()->comment('계정과목'); + $table->text('description')->nullable()->comment('적요'); + $table->string('reference_type', 50)->nullable()->comment('참조 유형: purchase/payable/payroll/etc'); + $table->unsignedBigInteger('reference_id')->nullable()->comment('참조 ID'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->softDeletes(); + $table->timestamps(); + + $table->index(['tenant_id', 'withdrawal_date'], 'idx_tenant_date'); + $table->index('client_id', 'idx_client'); + $table->index('payment_method', 'idx_payment_method'); + }); + } + + public function down(): void + { + Schema::dropIfExists('withdrawals'); + } +}; diff --git a/routes/api.php b/routes/api.php index fe867c5..b27c7da 100644 --- a/routes/api.php +++ b/routes/api.php @@ -51,6 +51,8 @@ use App\Http\Controllers\Api\V1\RolePermissionController; use App\Http\Controllers\Api\V1\BankAccountController; use App\Http\Controllers\Api\V1\CardController; +use App\Http\Controllers\Api\V1\DepositController; +use App\Http\Controllers\Api\V1\WithdrawalController; use App\Http\Controllers\Api\V1\SiteController; use App\Http\Controllers\Api\V1\TenantController; use App\Http\Controllers\Api\V1\TenantFieldSettingController; @@ -294,6 +296,26 @@ Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary'); }); + // Deposit API (입금 관리) + Route::prefix('deposits')->group(function () { + Route::get('', [DepositController::class, 'index'])->name('v1.deposits.index'); + Route::post('', [DepositController::class, 'store'])->name('v1.deposits.store'); + Route::get('/summary', [DepositController::class, 'summary'])->name('v1.deposits.summary'); + Route::get('/{id}', [DepositController::class, 'show'])->whereNumber('id')->name('v1.deposits.show'); + Route::put('/{id}', [DepositController::class, 'update'])->whereNumber('id')->name('v1.deposits.update'); + Route::delete('/{id}', [DepositController::class, 'destroy'])->whereNumber('id')->name('v1.deposits.destroy'); + }); + + // Withdrawal API (출금 관리) + Route::prefix('withdrawals')->group(function () { + Route::get('', [WithdrawalController::class, 'index'])->name('v1.withdrawals.index'); + Route::post('', [WithdrawalController::class, 'store'])->name('v1.withdrawals.store'); + Route::get('/summary', [WithdrawalController::class, 'summary'])->name('v1.withdrawals.summary'); + Route::get('/{id}', [WithdrawalController::class, 'show'])->whereNumber('id')->name('v1.withdrawals.show'); + Route::put('/{id}', [WithdrawalController::class, 'update'])->whereNumber('id')->name('v1.withdrawals.update'); + Route::delete('/{id}', [WithdrawalController::class, 'destroy'])->whereNumber('id')->name('v1.withdrawals.destroy'); + }); + // Permission API Route::prefix('permissions')->group(function () { Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스