diff --git a/app/Http/Controllers/Api/V1/BankAccountController.php b/app/Http/Controllers/Api/V1/BankAccountController.php new file mode 100644 index 0000000..9105eb7 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BankAccountController.php @@ -0,0 +1,108 @@ +only([ + 'search', + 'status', + 'assigned_user_id', + 'is_primary', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $accounts = $this->service->index($params); + + return ApiResponse::handle(__('message.fetched'), $accounts); + } + + /** + * 계좌 등록 + */ + public function store(StoreBankAccountRequest $request) + { + $account = $this->service->store($request->validated()); + + return ApiResponse::handle(__('message.created'), $account, 201); + } + + /** + * 계좌 상세 + */ + public function show(int $id) + { + $account = $this->service->show($id); + + return ApiResponse::handle(__('message.fetched'), $account); + } + + /** + * 계좌 수정 + */ + public function update(int $id, UpdateBankAccountRequest $request) + { + $account = $this->service->update($id, $request->validated()); + + return ApiResponse::handle(__('message.updated'), $account); + } + + /** + * 계좌 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::handle(__('message.deleted')); + } + + /** + * 계좌 상태 토글 (사용/정지) + */ + public function toggle(int $id) + { + $account = $this->service->toggleStatus($id); + + return ApiResponse::handle(__('message.updated'), $account); + } + + /** + * 대표계좌 설정 + */ + public function setPrimary(int $id) + { + $account = $this->service->setPrimary($id); + + return ApiResponse::handle(__('message.updated'), $account); + } + + /** + * 활성 계좌 목록 (셀렉트박스용) + */ + public function active() + { + $accounts = $this->service->getActiveAccounts(); + + return ApiResponse::handle(__('message.fetched'), $accounts); + } +} diff --git a/app/Http/Controllers/Api/V1/CardController.php b/app/Http/Controllers/Api/V1/CardController.php new file mode 100644 index 0000000..80308c6 --- /dev/null +++ b/app/Http/Controllers/Api/V1/CardController.php @@ -0,0 +1,97 @@ +only([ + 'search', + 'status', + 'assigned_user_id', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $cards = $this->service->index($params); + + return ApiResponse::handle(__('message.fetched'), $cards); + } + + /** + * 카드 등록 + */ + public function store(StoreCardRequest $request) + { + $card = $this->service->store($request->validated()); + + return ApiResponse::handle(__('message.created'), $card, 201); + } + + /** + * 카드 상세 + */ + public function show(int $id) + { + $card = $this->service->show($id); + + return ApiResponse::handle(__('message.fetched'), $card); + } + + /** + * 카드 수정 + */ + public function update(int $id, UpdateCardRequest $request) + { + $card = $this->service->update($id, $request->validated()); + + return ApiResponse::handle(__('message.updated'), $card); + } + + /** + * 카드 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::handle(__('message.deleted')); + } + + /** + * 카드 상태 토글 (사용/정지) + */ + public function toggle(int $id) + { + $card = $this->service->toggleStatus($id); + + return ApiResponse::handle(__('message.updated'), $card); + } + + /** + * 활성 카드 목록 (셀렉트박스용) + */ + public function active() + { + $cards = $this->service->getActiveCards(); + + return ApiResponse::handle(__('message.fetched'), $cards); + } +} diff --git a/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php b/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php new file mode 100644 index 0000000..cea8b9a --- /dev/null +++ b/app/Http/Requests/V1/BankAccount/StoreBankAccountRequest.php @@ -0,0 +1,53 @@ + ['required', 'string', 'max:10'], + 'bank_name' => ['required', 'string', 'max:50'], + 'account_number' => ['required', 'string', 'max:30', 'regex:/^[\d-]+$/'], + 'account_holder' => ['required', 'string', 'max:50'], + 'account_name' => ['required', 'string', 'max:100'], + 'status' => ['nullable', 'string', 'in:active,inactive'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + 'is_primary' => ['nullable', 'boolean'], + ]; + } + + public function messages(): array + { + return [ + 'bank_code.required' => __('validation.required', ['attribute' => __('validation.attributes.bank_code')]), + 'bank_name.required' => __('validation.required', ['attribute' => __('validation.attributes.bank_name')]), + 'account_number.required' => __('validation.required', ['attribute' => __('validation.attributes.account_number')]), + 'account_number.regex' => __('validation.account_number_format'), + 'account_holder.required' => __('validation.required', ['attribute' => __('validation.attributes.account_holder')]), + 'account_name.required' => __('validation.required', ['attribute' => __('validation.attributes.account_name')]), + ]; + } + + public function attributes(): array + { + return [ + 'bank_code' => __('validation.attributes.bank_code'), + 'bank_name' => __('validation.attributes.bank_name'), + 'account_number' => __('validation.attributes.account_number'), + 'account_holder' => __('validation.attributes.account_holder'), + 'account_name' => __('validation.attributes.account_name'), + 'status' => __('validation.attributes.status'), + 'assigned_user_id' => __('validation.attributes.assigned_user_id'), + 'is_primary' => __('validation.attributes.is_primary'), + ]; + } +} diff --git a/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php b/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php new file mode 100644 index 0000000..45afdb6 --- /dev/null +++ b/app/Http/Requests/V1/BankAccount/UpdateBankAccountRequest.php @@ -0,0 +1,46 @@ + ['sometimes', 'string', 'max:10'], + 'bank_name' => ['sometimes', 'string', 'max:50'], + 'account_number' => ['sometimes', 'string', 'max:30', 'regex:/^[\d-]+$/'], + 'account_holder' => ['sometimes', 'string', 'max:50'], + 'account_name' => ['sometimes', 'string', 'max:100'], + 'status' => ['sometimes', 'string', 'in:active,inactive'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + ]; + } + + public function messages(): array + { + return [ + 'account_number.regex' => __('validation.account_number_format'), + ]; + } + + public function attributes(): array + { + return [ + 'bank_code' => __('validation.attributes.bank_code'), + 'bank_name' => __('validation.attributes.bank_name'), + 'account_number' => __('validation.attributes.account_number'), + 'account_holder' => __('validation.attributes.account_holder'), + 'account_name' => __('validation.attributes.account_name'), + 'status' => __('validation.attributes.status'), + 'assigned_user_id' => __('validation.attributes.assigned_user_id'), + ]; + } +} diff --git a/app/Http/Requests/V1/Card/StoreCardRequest.php b/app/Http/Requests/V1/Card/StoreCardRequest.php new file mode 100644 index 0000000..20123e9 --- /dev/null +++ b/app/Http/Requests/V1/Card/StoreCardRequest.php @@ -0,0 +1,53 @@ + ['required', 'string', 'max:50'], + 'card_number' => ['required', 'string', 'regex:/^\d{13,19}$/'], + 'expiry_date' => ['required', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'], + 'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'], + 'card_name' => ['required', 'string', 'max:100'], + 'status' => ['nullable', 'string', 'in:active,inactive'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + ]; + } + + public function messages(): array + { + return [ + 'card_company.required' => __('validation.required', ['attribute' => __('validation.attributes.card_company')]), + 'card_number.required' => __('validation.required', ['attribute' => __('validation.attributes.card_number')]), + 'card_number.regex' => __('validation.card_number_format'), + 'expiry_date.required' => __('validation.required', ['attribute' => __('validation.attributes.expiry_date')]), + 'expiry_date.regex' => __('validation.expiry_date_format'), + 'card_password.size' => __('validation.card_password_format'), + 'card_password.regex' => __('validation.card_password_format'), + 'card_name.required' => __('validation.required', ['attribute' => __('validation.attributes.card_name')]), + ]; + } + + public function attributes(): array + { + return [ + 'card_company' => __('validation.attributes.card_company'), + 'card_number' => __('validation.attributes.card_number'), + 'expiry_date' => __('validation.attributes.expiry_date'), + 'card_password' => __('validation.attributes.card_password'), + 'card_name' => __('validation.attributes.card_name'), + 'status' => __('validation.attributes.status'), + 'assigned_user_id' => __('validation.attributes.assigned_user_id'), + ]; + } +} diff --git a/app/Http/Requests/V1/Card/UpdateCardRequest.php b/app/Http/Requests/V1/Card/UpdateCardRequest.php new file mode 100644 index 0000000..2506ab6 --- /dev/null +++ b/app/Http/Requests/V1/Card/UpdateCardRequest.php @@ -0,0 +1,49 @@ + ['sometimes', 'string', 'max:50'], + 'card_number' => ['sometimes', 'string', 'regex:/^\d{13,19}$/'], + 'expiry_date' => ['sometimes', 'string', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'], + 'card_password' => ['nullable', 'string', 'size:2', 'regex:/^\d{2}$/'], + 'card_name' => ['sometimes', 'string', 'max:100'], + 'status' => ['sometimes', 'string', 'in:active,inactive'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + ]; + } + + public function messages(): array + { + return [ + 'card_number.regex' => __('validation.card_number_format'), + 'expiry_date.regex' => __('validation.expiry_date_format'), + 'card_password.size' => __('validation.card_password_format'), + 'card_password.regex' => __('validation.card_password_format'), + ]; + } + + public function attributes(): array + { + return [ + 'card_company' => __('validation.attributes.card_company'), + 'card_number' => __('validation.attributes.card_number'), + 'expiry_date' => __('validation.attributes.expiry_date'), + 'card_password' => __('validation.attributes.card_password'), + 'card_name' => __('validation.attributes.card_name'), + 'status' => __('validation.attributes.status'), + 'assigned_user_id' => __('validation.attributes.assigned_user_id'), + ]; + } +} diff --git a/app/Models/Tenants/BankAccount.php b/app/Models/Tenants/BankAccount.php new file mode 100644 index 0000000..5a686d9 --- /dev/null +++ b/app/Models/Tenants/BankAccount.php @@ -0,0 +1,130 @@ + 'boolean', + ]; + + protected $attributes = [ + 'status' => 'active', + 'is_primary' => false, + ]; + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + /** + * 담당자 + */ + public function assignedUser(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_user_id'); + } + + /** + * 생성자 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * 수정자 + */ + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + // ========================================================================= + // 헬퍼 메서드 + // ========================================================================= + + /** + * 마스킹된 계좌번호 조회 + */ + public function getMaskedAccountNumber(): string + { + $length = strlen($this->account_number); + if ($length <= 4) { + return $this->account_number; + } + + $visibleEnd = substr($this->account_number, -4); + $maskedPart = str_repeat('*', $length - 4); + + return $maskedPart.$visibleEnd; + } + + /** + * 활성 상태 여부 + */ + public function isActive(): bool + { + return $this->status === 'active'; + } + + /** + * 상태 토글 + */ + public function toggleStatus(): void + { + $this->status = $this->status === 'active' ? 'inactive' : 'active'; + } + + /** + * 대표계좌로 설정 + */ + public function setAsPrimary(): void + { + $this->is_primary = true; + } +} diff --git a/app/Models/Tenants/Card.php b/app/Models/Tenants/Card.php new file mode 100644 index 0000000..46c9c6f --- /dev/null +++ b/app/Models/Tenants/Card.php @@ -0,0 +1,156 @@ + 'active', + ]; + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + /** + * 담당자 + */ + public function assignedUser(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_user_id'); + } + + /** + * 생성자 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * 수정자 + */ + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + // ========================================================================= + // 암호화/복호화 메서드 + // ========================================================================= + + /** + * 카드번호 암호화 설정 + */ + public function setCardNumber(string $cardNumber): void + { + $this->card_number_encrypted = Crypt::encryptString($cardNumber); + $this->card_number_last4 = substr($cardNumber, -4); + } + + /** + * 카드번호 복호화 조회 + */ + public function getDecryptedCardNumber(): string + { + return Crypt::decryptString($this->card_number_encrypted); + } + + /** + * 카드 비밀번호 암호화 설정 + */ + public function setCardPassword(?string $password): void + { + $this->card_password_encrypted = $password + ? Crypt::encryptString($password) + : null; + } + + /** + * 카드 비밀번호 복호화 조회 + */ + public function getDecryptedCardPassword(): ?string + { + return $this->card_password_encrypted + ? Crypt::decryptString($this->card_password_encrypted) + : null; + } + + // ========================================================================= + // 헬퍼 메서드 + // ========================================================================= + + /** + * 마스킹된 카드번호 조회 + */ + public function getMaskedCardNumber(): string + { + return '****-****-****-'.$this->card_number_last4; + } + + /** + * 활성 상태 여부 + */ + public function isActive(): bool + { + return $this->status === 'active'; + } + + /** + * 상태 토글 + */ + public function toggleStatus(): void + { + $this->status = $this->status === 'active' ? 'inactive' : 'active'; + } +} diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php new file mode 100644 index 0000000..55669e8 --- /dev/null +++ b/app/Services/BankAccountService.php @@ -0,0 +1,244 @@ +tenantId(); + + $query = BankAccount::query() + ->where('tenant_id', $tenantId); + + // 검색 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('account_name', 'like', "%{$search}%") + ->orWhere('bank_name', 'like', "%{$search}%") + ->orWhere('account_holder', 'like', "%{$search}%") + ->orWhere('account_number', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 담당자 필터 + if (! empty($params['assigned_user_id'])) { + $query->where('assigned_user_id', $params['assigned_user_id']); + } + + // 대표계좌만 필터 + if (isset($params['is_primary']) && $params['is_primary']) { + $query->where('is_primary', true); + } + + // 정렬: 대표계좌 먼저, 그 다음 생성일순 + $query->orderByDesc('is_primary') + ->orderBy($params['sort_by'] ?? 'created_at', $params['sort_dir'] ?? 'desc'); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 계좌 상세 조회 + */ + public function show(int $id): BankAccount + { + $tenantId = $this->tenantId(); + + return BankAccount::query() + ->where('tenant_id', $tenantId) + ->with(['assignedUser:id,name']) + ->findOrFail($id); + } + + /** + * 계좌 등록 + */ + public function store(array $data): BankAccount + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + // 첫 번째 계좌인 경우 자동으로 대표계좌 설정 + $isFirst = BankAccount::where('tenant_id', $tenantId)->count() === 0; + $isPrimary = $data['is_primary'] ?? $isFirst; + + // 대표계좌로 설정 시 기존 대표계좌 해제 + if ($isPrimary) { + BankAccount::where('tenant_id', $tenantId) + ->where('is_primary', true) + ->update(['is_primary' => false]); + } + + $account = BankAccount::create([ + 'tenant_id' => $tenantId, + 'bank_code' => $data['bank_code'], + 'bank_name' => $data['bank_name'], + 'account_number' => $data['account_number'], + 'account_holder' => $data['account_holder'], + 'account_name' => $data['account_name'], + 'status' => $data['status'] ?? 'active', + 'assigned_user_id' => $data['assigned_user_id'] ?? null, + 'is_primary' => $isPrimary, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + + return $account; + }); + } + + /** + * 계좌 수정 + */ + public function update(int $id, array $data): BankAccount + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $account = BankAccount::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $account->fill([ + 'bank_code' => $data['bank_code'] ?? $account->bank_code, + 'bank_name' => $data['bank_name'] ?? $account->bank_name, + 'account_number' => $data['account_number'] ?? $account->account_number, + 'account_holder' => $data['account_holder'] ?? $account->account_holder, + 'account_name' => $data['account_name'] ?? $account->account_name, + 'status' => $data['status'] ?? $account->status, + 'assigned_user_id' => $data['assigned_user_id'] ?? $account->assigned_user_id, + 'updated_by' => $userId, + ]); + + $account->save(); + + return $account->fresh(); + }); + } + + /** + * 계좌 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $account = BankAccount::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + // 대표계좌 삭제 시 다른 계좌를 대표로 설정 + if ($account->is_primary) { + $nextPrimary = BankAccount::where('tenant_id', $tenantId) + ->where('id', '!=', $id) + ->where('status', 'active') + ->first(); + + if ($nextPrimary) { + $nextPrimary->is_primary = true; + $nextPrimary->save(); + } + } + + $account->deleted_by = $userId; + $account->save(); + $account->delete(); + + return true; + }); + } + + /** + * 계좌 상태 토글 (사용/정지) + */ + public function toggleStatus(int $id): BankAccount + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $account = BankAccount::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $account->toggleStatus(); + $account->updated_by = $userId; + $account->save(); + + return $account; + }); + } + + /** + * 대표계좌 설정 + */ + public function setPrimary(int $id): BankAccount + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + // 기존 대표계좌 해제 + BankAccount::where('tenant_id', $tenantId) + ->where('is_primary', true) + ->update(['is_primary' => false]); + + // 새 대표계좌 설정 + $account = BankAccount::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $account->is_primary = true; + $account->updated_by = $userId; + $account->save(); + + return $account; + }); + } + + /** + * 활성 계좌 목록 조회 (셀렉트박스용) + */ + public function getActiveAccounts(): array + { + $tenantId = $this->tenantId(); + + return BankAccount::query() + ->where('tenant_id', $tenantId) + ->where('status', 'active') + ->orderByDesc('is_primary') + ->orderBy('account_name') + ->get(['id', 'account_name', 'bank_name', 'account_number', 'is_primary']) + ->map(function ($account) { + return [ + 'id' => $account->id, + 'account_name' => $account->account_name, + 'bank_name' => $account->bank_name, + 'display_number' => $account->getMaskedAccountNumber(), + 'is_primary' => $account->is_primary, + ]; + }) + ->toArray(); + } +} diff --git a/app/Services/CardService.php b/app/Services/CardService.php new file mode 100644 index 0000000..fd14373 --- /dev/null +++ b/app/Services/CardService.php @@ -0,0 +1,201 @@ +tenantId(); + + $query = Card::query() + ->where('tenant_id', $tenantId); + + // 검색 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('card_name', 'like', "%{$search}%") + ->orWhere('card_company', 'like', "%{$search}%") + ->orWhere('card_number_last4', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 담당자 필터 + if (! empty($params['assigned_user_id'])) { + $query->where('assigned_user_id', $params['assigned_user_id']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'created_at'; + $sortDir = $params['sort_dir'] ?? 'desc'; + $query->orderBy($sortBy, $sortDir); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 카드 상세 조회 + */ + public function show(int $id): Card + { + $tenantId = $this->tenantId(); + + return Card::query() + ->where('tenant_id', $tenantId) + ->with(['assignedUser:id,name']) + ->findOrFail($id); + } + + /** + * 카드 등록 + */ + public function store(array $data): Card + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + $card = new Card; + $card->tenant_id = $tenantId; + $card->card_company = $data['card_company']; + $card->setCardNumber($data['card_number']); + $card->expiry_date = $data['expiry_date']; + $card->card_name = $data['card_name']; + $card->status = $data['status'] ?? 'active'; + $card->assigned_user_id = $data['assigned_user_id'] ?? null; + $card->created_by = $userId; + $card->updated_by = $userId; + + if (! empty($data['card_password'])) { + $card->setCardPassword($data['card_password']); + } + + $card->save(); + + return $card; + }); + } + + /** + * 카드 수정 + */ + public function update(int $id, array $data): Card + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $card = Card::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + if (isset($data['card_company'])) { + $card->card_company = $data['card_company']; + } + if (isset($data['card_number'])) { + $card->setCardNumber($data['card_number']); + } + if (isset($data['expiry_date'])) { + $card->expiry_date = $data['expiry_date']; + } + if (isset($data['card_name'])) { + $card->card_name = $data['card_name']; + } + if (isset($data['status'])) { + $card->status = $data['status']; + } + if (array_key_exists('assigned_user_id', $data)) { + $card->assigned_user_id = $data['assigned_user_id']; + } + if (isset($data['card_password'])) { + $card->setCardPassword($data['card_password']); + } + + $card->updated_by = $userId; + $card->save(); + + return $card->fresh(); + }); + } + + /** + * 카드 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $card = Card::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $card->deleted_by = $userId; + $card->save(); + $card->delete(); + + return true; + }); + } + + /** + * 카드 상태 토글 (사용/정지) + */ + public function toggleStatus(int $id): Card + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $card = Card::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $card->toggleStatus(); + $card->updated_by = $userId; + $card->save(); + + return $card; + }); + } + + /** + * 활성 카드 목록 조회 (셀렉트박스용) + */ + public function getActiveCards(): array + { + $tenantId = $this->tenantId(); + + return Card::query() + ->where('tenant_id', $tenantId) + ->where('status', 'active') + ->orderBy('card_name') + ->get(['id', 'card_name', 'card_company', 'card_number_last4']) + ->map(function ($card) { + return [ + 'id' => $card->id, + 'card_name' => $card->card_name, + 'card_company' => $card->card_company, + 'display_number' => '****-'.$card->card_number_last4, + ]; + }) + ->toArray(); + } +} diff --git a/app/Swagger/v1/BankAccountApi.php b/app/Swagger/v1/BankAccountApi.php new file mode 100644 index 0000000..af9ec54 --- /dev/null +++ b/app/Swagger/v1/BankAccountApi.php @@ -0,0 +1,353 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('card_company', 50)->comment('카드사'); + $table->text('card_number_encrypted')->comment('암호화된 카드번호'); + $table->string('card_number_last4', 4)->comment('카드번호 끝 4자리'); + $table->string('expiry_date', 5)->comment('유효기간 (MM/YY)'); + $table->text('card_password_encrypted')->nullable()->comment('암호화된 비밀번호 앞2자리'); + $table->string('card_name', 100)->comment('카드 별칭'); + $table->string('status', 20)->default('active')->comment('상태: active/inactive'); + $table->unsignedBigInteger('assigned_user_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', 'idx_cards_tenant'); + $table->index('status', 'idx_cards_status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cards'); + } +}; diff --git a/database/migrations/2025_12_17_120001_create_bank_accounts_table.php b/database/migrations/2025_12_17_120001_create_bank_accounts_table.php new file mode 100644 index 0000000..2048f40 --- /dev/null +++ b/database/migrations/2025_12_17_120001_create_bank_accounts_table.php @@ -0,0 +1,43 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('bank_code', 10)->comment('은행 코드'); + $table->string('bank_name', 50)->comment('은행명'); + $table->string('account_number', 30)->comment('계좌번호'); + $table->string('account_holder', 50)->comment('예금주'); + $table->string('account_name', 100)->comment('계좌 별칭'); + $table->string('status', 20)->default('active')->comment('상태: active/inactive'); + $table->unsignedBigInteger('assigned_user_id')->nullable()->comment('담당자 ID'); + $table->boolean('is_primary')->default(false)->comment('대표계좌 여부'); + $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', 'idx_bank_accounts_tenant'); + $table->index('status', 'idx_bank_accounts_status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bank_accounts'); + } +}; diff --git a/routes/api.php b/routes/api.php index 15172e8..fe867c5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -49,6 +49,8 @@ use App\Http\Controllers\Api\V1\RegisterController; use App\Http\Controllers\Api\V1\RoleController; 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\SiteController; use App\Http\Controllers\Api\V1\TenantController; use App\Http\Controllers\Api\V1\TenantFieldSettingController; @@ -269,6 +271,29 @@ Route::delete('/{id}', [SiteController::class, 'destroy'])->whereNumber('id')->name('v1.sites.destroy'); }); + // Card API (카드 관리) + Route::prefix('cards')->group(function () { + Route::get('', [CardController::class, 'index'])->name('v1.cards.index'); + Route::post('', [CardController::class, 'store'])->name('v1.cards.store'); + Route::get('/active', [CardController::class, 'active'])->name('v1.cards.active'); + Route::get('/{id}', [CardController::class, 'show'])->whereNumber('id')->name('v1.cards.show'); + Route::put('/{id}', [CardController::class, 'update'])->whereNumber('id')->name('v1.cards.update'); + Route::delete('/{id}', [CardController::class, 'destroy'])->whereNumber('id')->name('v1.cards.destroy'); + Route::patch('/{id}/toggle', [CardController::class, 'toggle'])->whereNumber('id')->name('v1.cards.toggle'); + }); + + // BankAccount API (계좌 관리) + Route::prefix('bank-accounts')->group(function () { + Route::get('', [BankAccountController::class, 'index'])->name('v1.bank-accounts.index'); + Route::post('', [BankAccountController::class, 'store'])->name('v1.bank-accounts.store'); + Route::get('/active', [BankAccountController::class, 'active'])->name('v1.bank-accounts.active'); + Route::get('/{id}', [BankAccountController::class, 'show'])->whereNumber('id')->name('v1.bank-accounts.show'); + Route::put('/{id}', [BankAccountController::class, 'update'])->whereNumber('id')->name('v1.bank-accounts.update'); + Route::delete('/{id}', [BankAccountController::class, 'destroy'])->whereNumber('id')->name('v1.bank-accounts.destroy'); + Route::patch('/{id}/toggle', [BankAccountController::class, 'toggle'])->whereNumber('id')->name('v1.bank-accounts.toggle'); + Route::patch('/{id}/set-primary', [BankAccountController::class, 'setPrimary'])->whereNumber('id')->name('v1.bank-accounts.set-primary'); + }); + // Permission API Route::prefix('permissions')->group(function () { Route::get('departments/{dept_id}/menu-matrix', [PermissionController::class, 'deptMenuMatrix'])->name('v1.permissions.deptMenuMatrix'); // 부서별 권한 메트릭스