From c0af888bed78aad1e6f2315bdcdee94301eb4c0f Mon Sep 17 00:00:00 2001 From: hskwon Date: Fri, 19 Dec 2025 15:57:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=206.1=20=EC=95=85=EC=84=B1?= =?UTF-8?q?=EC=B1=84=EA=B6=8C=20=EC=B6=94=EC=8B=AC=EA=B4=80=EB=A6=AC=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 3개: bad_debts, bad_debt_documents, bad_debt_memos - 모델 3개: BadDebt, BadDebtDocument, BadDebtMemo - BadDebtService: CRUD, 요약 통계, 서류/메모 관리 - API 엔드포인트 11개 (목록, 등록, 상세, 수정, 삭제, 토글, 서류/메모 CRUD) - Swagger 문서 작성 완료 --- LOGICAL_RELATIONSHIPS.md | 34 +- .../Controllers/Api/V1/BadDebtController.php | 142 ++++++ .../BadDebt/StoreBadDebtDocumentRequest.php | 46 ++ .../V1/BadDebt/StoreBadDebtMemoRequest.php | 41 ++ .../V1/BadDebt/StoreBadDebtRequest.php | 55 +++ .../V1/BadDebt/UpdateBadDebtRequest.php | 53 +++ app/Models/BadDebts/BadDebt.php | 158 +++++++ app/Models/BadDebts/BadDebtDocument.php | 58 +++ app/Models/BadDebts/BadDebtMemo.php | 32 ++ app/Services/BadDebtService.php | 307 +++++++++++++ app/Swagger/v1/BadDebtApi.php | 433 ++++++++++++++++++ ...25_12_19_160001_create_bad_debts_table.php | 47 ++ ...160002_create_bad_debt_documents_table.php | 38 ++ ..._19_160003_create_bad_debt_memos_table.php | 37 ++ routes/api.php | 18 + 15 files changed, 1498 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/V1/BadDebtController.php create mode 100644 app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php create mode 100644 app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php create mode 100644 app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php create mode 100644 app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php create mode 100644 app/Models/BadDebts/BadDebt.php create mode 100644 app/Models/BadDebts/BadDebtDocument.php create mode 100644 app/Models/BadDebts/BadDebtMemo.php create mode 100644 app/Services/BadDebtService.php create mode 100644 app/Swagger/v1/BadDebtApi.php create mode 100644 database/migrations/2025_12_19_160001_create_bad_debts_table.php create mode 100644 database/migrations/2025_12_19_160002_create_bad_debt_documents_table.php create mode 100644 database/migrations/2025_12_19_160003_create_bad_debt_memos_table.php diff --git a/LOGICAL_RELATIONSHIPS.md b/LOGICAL_RELATIONSHIPS.md index aa2857d..8307bd9 100644 --- a/LOGICAL_RELATIONSHIPS.md +++ b/LOGICAL_RELATIONSHIPS.md @@ -1,10 +1,31 @@ # 논리적 데이터베이스 관계 문서 -> **자동 생성**: 2025-12-19 10:11:32 +> **자동 생성**: 2025-12-19 15:55:41 > **소스**: Eloquent 모델 관계 분석 ## 📊 모델별 관계 현황 +### bad_debts +**모델**: `App\Models\BadDebts\BadDebt` + +- **client()**: belongsTo → `clients` +- **assignedUser()**: belongsTo → `users` +- **creator()**: belongsTo → `users` +- **documents()**: hasMany → `bad_debt_documents` +- **memos()**: hasMany → `bad_debt_memos` + +### bad_debt_documents +**모델**: `App\Models\BadDebts\BadDebtDocument` + +- **badDebt()**: belongsTo → `bad_debts` +- **file()**: belongsTo → `files` + +### bad_debt_memos +**모델**: `App\Models\BadDebts\BadDebtMemo` + +- **badDebt()**: belongsTo → `bad_debts` +- **creator()**: belongsTo → `users` + ### boards **모델**: `App\Models\Boards\Board` @@ -231,6 +252,10 @@ ### user_tenants - **user()**: belongsTo → `users` - **tenant()**: belongsTo → `tenants` +### notification_settings +**모델**: `App\Models\NotificationSetting` + + ### clients **모델**: `App\Models\Orders\Client` @@ -546,3 +571,10 @@ ### withdrawals - **bankAccount()**: belongsTo → `bank_accounts` +### user_invitations +**모델**: `App\Models\UserInvitation` + +- **tenant()**: belongsTo → `tenants` +- **role()**: belongsTo → `roles` +- **inviter()**: belongsTo → `users` + diff --git a/app/Http/Controllers/Api/V1/BadDebtController.php b/app/Http/Controllers/Api/V1/BadDebtController.php new file mode 100644 index 0000000..0c728c7 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BadDebtController.php @@ -0,0 +1,142 @@ +only([ + 'client_id', + 'status', + 'is_active', + 'search', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $badDebts = $this->service->index($params); + + return ApiResponse::success($badDebts, __('message.fetched')); + } + + /** + * 악성채권 요약 통계 + */ + public function summary(Request $request) + { + $params = $request->only(['client_id']); + + $summary = $this->service->summary($params); + + return ApiResponse::success($summary, __('message.fetched')); + } + + /** + * 악성채권 등록 + */ + public function store(StoreBadDebtRequest $request) + { + $badDebt = $this->service->store($request->validated()); + + return ApiResponse::success($badDebt, __('message.created'), [], 201); + } + + /** + * 악성채권 상세 + */ + public function show(int $id) + { + $badDebt = $this->service->show($id); + + return ApiResponse::success($badDebt, __('message.fetched')); + } + + /** + * 악성채권 수정 + */ + public function update(int $id, UpdateBadDebtRequest $request) + { + $badDebt = $this->service->update($id, $request->validated()); + + return ApiResponse::success($badDebt, __('message.updated')); + } + + /** + * 악성채권 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::success(null, __('message.deleted')); + } + + /** + * 설정 토글 (is_active) + */ + public function toggle(int $id) + { + $badDebt = $this->service->toggle($id); + + return ApiResponse::success($badDebt, __('message.updated')); + } + + /** + * 서류 첨부 + */ + public function addDocument(int $id, StoreBadDebtDocumentRequest $request) + { + $document = $this->service->addDocument($id, $request->validated()); + + return ApiResponse::success($document, __('message.created'), [], 201); + } + + /** + * 서류 삭제 + */ + public function removeDocument(int $id, int $documentId) + { + $this->service->removeDocument($id, $documentId); + + return ApiResponse::success(null, __('message.deleted')); + } + + /** + * 메모 추가 + */ + public function addMemo(int $id, StoreBadDebtMemoRequest $request) + { + $memo = $this->service->addMemo($id, $request->validated()); + + return ApiResponse::success($memo, __('message.created'), [], 201); + } + + /** + * 메모 삭제 + */ + public function removeMemo(int $id, int $memoId) + { + $this->service->removeMemo($id, $memoId); + + return ApiResponse::success(null, __('message.deleted')); + } +} diff --git a/app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php b/app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php new file mode 100644 index 0000000..4948185 --- /dev/null +++ b/app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php @@ -0,0 +1,46 @@ +|string> + */ + public function rules(): array + { + return [ + 'document_type' => ['required', 'string', Rule::in(array_keys(BadDebtDocument::DOCUMENT_TYPES))], + 'file_id' => ['required', 'integer', 'exists:files,id'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'document_type.required' => __('validation.required', ['attribute' => '서류유형']), + 'document_type.in' => __('validation.in', ['attribute' => '서류유형']), + 'file_id.required' => __('validation.required', ['attribute' => '파일']), + 'file_id.exists' => __('validation.exists', ['attribute' => '파일']), + ]; + } +} diff --git a/app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php b/app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php new file mode 100644 index 0000000..fd6ed23 --- /dev/null +++ b/app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php @@ -0,0 +1,41 @@ +|string> + */ + public function rules(): array + { + return [ + 'content' => ['required', 'string', 'max:5000'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'content.required' => __('validation.required', ['attribute' => '메모 내용']), + 'content.max' => __('validation.max.string', ['attribute' => '메모 내용', 'max' => 5000]), + ]; + } +} diff --git a/app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php b/app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php new file mode 100644 index 0000000..0884fd6 --- /dev/null +++ b/app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php @@ -0,0 +1,55 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['required', 'integer', 'exists:clients,id'], + 'debt_amount' => ['required', 'numeric', 'min:0'], + 'status' => ['nullable', 'string', Rule::in(array_keys(BadDebt::STATUSES))], + 'overdue_days' => ['nullable', 'integer', 'min:0'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + 'occurred_at' => ['nullable', 'date'], + 'closed_at' => ['nullable', 'date', 'after_or_equal:occurred_at'], + 'is_active' => ['nullable', 'boolean'], + 'options' => ['nullable', 'array'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'client_id.required' => __('validation.required', ['attribute' => '거래처']), + 'client_id.exists' => __('validation.exists', ['attribute' => '거래처']), + 'debt_amount.required' => __('validation.required', ['attribute' => '채권금액']), + 'debt_amount.numeric' => __('validation.numeric', ['attribute' => '채권금액']), + 'status.in' => __('validation.in', ['attribute' => '상태']), + 'closed_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '발생일']), + ]; + } +} diff --git a/app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php b/app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php new file mode 100644 index 0000000..f627233 --- /dev/null +++ b/app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php @@ -0,0 +1,53 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['sometimes', 'integer', 'exists:clients,id'], + 'debt_amount' => ['sometimes', 'numeric', 'min:0'], + 'status' => ['sometimes', 'string', Rule::in(array_keys(BadDebt::STATUSES))], + 'overdue_days' => ['sometimes', 'integer', 'min:0'], + 'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'], + 'occurred_at' => ['nullable', 'date'], + 'closed_at' => ['nullable', 'date', 'after_or_equal:occurred_at'], + 'is_active' => ['sometimes', 'boolean'], + 'options' => ['nullable', 'array'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'client_id.exists' => __('validation.exists', ['attribute' => '거래처']), + 'debt_amount.numeric' => __('validation.numeric', ['attribute' => '채권금액']), + 'status.in' => __('validation.in', ['attribute' => '상태']), + 'closed_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '발생일']), + ]; + } +} diff --git a/app/Models/BadDebts/BadDebt.php b/app/Models/BadDebts/BadDebt.php new file mode 100644 index 0000000..a811897 --- /dev/null +++ b/app/Models/BadDebts/BadDebt.php @@ -0,0 +1,158 @@ + 'decimal:2', + 'overdue_days' => 'integer', + 'is_active' => 'boolean', + 'options' => 'array', + 'occurred_at' => 'date', + 'closed_at' => 'date', + ]; + + /** + * 상태 상수 + */ + public const STATUS_COLLECTING = 'collecting'; + + public const STATUS_LEGAL_ACTION = 'legal_action'; + + public const STATUS_RECOVERED = 'recovered'; + + public const STATUS_BAD_DEBT = 'bad_debt'; + + /** + * 상태 목록 + */ + public const STATUSES = [ + self::STATUS_COLLECTING => '추심중', + self::STATUS_LEGAL_ACTION => '법적조치', + self::STATUS_RECOVERED => '회수완료', + self::STATUS_BAD_DEBT => '대손처리', + ]; + + /** + * 거래처 관계 + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** + * 담당자 관계 + */ + 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 documents(): HasMany + { + return $this->hasMany(BadDebtDocument::class); + } + + /** + * 메모 관계 + */ + public function memos(): HasMany + { + return $this->hasMany(BadDebtMemo::class)->orderBy('created_at', 'desc'); + } + + /** + * 상태 라벨 속성 + */ + public function getStatusLabelAttribute(): string + { + return self::STATUSES[$this->status] ?? $this->status; + } + + /** + * 활성 스코프 + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * 상태별 스코프 + */ + public function scopeStatus($query, string $status) + { + return $query->where('status', $status); + } + + /** + * 추심중 스코프 + */ + public function scopeCollecting($query) + { + return $query->where('status', self::STATUS_COLLECTING); + } + + /** + * 법적조치 스코프 + */ + public function scopeLegalAction($query) + { + return $query->where('status', self::STATUS_LEGAL_ACTION); + } + + /** + * 회수완료 스코프 + */ + public function scopeRecovered($query) + { + return $query->where('status', self::STATUS_RECOVERED); + } + + /** + * 대손처리 스코프 + */ + public function scopeBadDebt($query) + { + return $query->where('status', self::STATUS_BAD_DEBT); + } +} diff --git a/app/Models/BadDebts/BadDebtDocument.php b/app/Models/BadDebts/BadDebtDocument.php new file mode 100644 index 0000000..2a84c93 --- /dev/null +++ b/app/Models/BadDebts/BadDebtDocument.php @@ -0,0 +1,58 @@ + '사업자등록증', + self::TYPE_TAX_INVOICE => '세금계산서', + self::TYPE_ADDITIONAL => '추가서류', + ]; + + /** + * 악성채권 관계 + */ + public function badDebt(): BelongsTo + { + return $this->belongsTo(BadDebt::class); + } + + /** + * 파일 관계 + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } + + /** + * 서류 유형 라벨 속성 + */ + public function getDocumentTypeLabelAttribute(): string + { + return self::DOCUMENT_TYPES[$this->document_type] ?? $this->document_type; + } +} diff --git a/app/Models/BadDebts/BadDebtMemo.php b/app/Models/BadDebts/BadDebtMemo.php new file mode 100644 index 0000000..56ee02f --- /dev/null +++ b/app/Models/BadDebts/BadDebtMemo.php @@ -0,0 +1,32 @@ +belongsTo(BadDebt::class); + } + + /** + * 작성자 관계 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/app/Services/BadDebtService.php b/app/Services/BadDebtService.php new file mode 100644 index 0000000..996e77b --- /dev/null +++ b/app/Services/BadDebtService.php @@ -0,0 +1,307 @@ +tenantId(); + + $query = BadDebt::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name,client_code', 'assignedUser:id,name']); + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 활성화 필터 + if (isset($params['is_active'])) { + $query->where('is_active', $params['is_active']); + } + + // 검색어 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->whereHas('client', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('client_code', 'like', "%{$search}%"); + }); + }); + } + + // 정렬 + $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 summary(array $params = []): array + { + $tenantId = $this->tenantId(); + + $query = BadDebt::query() + ->where('tenant_id', $tenantId); + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 전체 합계 + $totalAmount = (clone $query)->sum('debt_amount'); + + // 상태별 합계 + $collectingAmount = (clone $query)->collecting()->sum('debt_amount'); + $legalActionAmount = (clone $query)->legalAction()->sum('debt_amount'); + $recoveredAmount = (clone $query)->recovered()->sum('debt_amount'); + $badDebtAmount = (clone $query)->badDebt()->sum('debt_amount'); + + return [ + 'total_amount' => (float) $totalAmount, + 'collecting_amount' => (float) $collectingAmount, + 'legal_action_amount' => (float) $legalActionAmount, + 'recovered_amount' => (float) $recoveredAmount, + 'bad_debt_amount' => (float) $badDebtAmount, + ]; + } + + /** + * 악성채권 상세 조회 + */ + public function show(int $id): BadDebt + { + $tenantId = $this->tenantId(); + + return BadDebt::query() + ->where('tenant_id', $tenantId) + ->with([ + 'client', + 'assignedUser:id,name', + 'creator:id,name', + 'documents.file', + 'memos.creator:id,name', + ]) + ->findOrFail($id); + } + + /** + * 악성채권 등록 + */ + public function store(array $data): BadDebt + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + $badDebt = new BadDebt; + $badDebt->tenant_id = $tenantId; + $badDebt->client_id = $data['client_id']; + $badDebt->debt_amount = $data['debt_amount']; + $badDebt->status = $data['status'] ?? BadDebt::STATUS_COLLECTING; + $badDebt->overdue_days = $data['overdue_days'] ?? 0; + $badDebt->assigned_user_id = $data['assigned_user_id'] ?? null; + $badDebt->occurred_at = $data['occurred_at'] ?? null; + $badDebt->closed_at = $data['closed_at'] ?? null; + $badDebt->is_active = $data['is_active'] ?? true; + $badDebt->options = $data['options'] ?? null; + $badDebt->created_by = $userId; + $badDebt->updated_by = $userId; + $badDebt->save(); + + return $badDebt->load(['client:id,name,client_code', 'assignedUser:id,name']); + }); + } + + /** + * 악성채권 수정 + */ + public function update(int $id, array $data): BadDebt + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + if (isset($data['client_id'])) { + $badDebt->client_id = $data['client_id']; + } + if (isset($data['debt_amount'])) { + $badDebt->debt_amount = $data['debt_amount']; + } + if (isset($data['status'])) { + $badDebt->status = $data['status']; + } + if (isset($data['overdue_days'])) { + $badDebt->overdue_days = $data['overdue_days']; + } + if (array_key_exists('assigned_user_id', $data)) { + $badDebt->assigned_user_id = $data['assigned_user_id']; + } + if (array_key_exists('occurred_at', $data)) { + $badDebt->occurred_at = $data['occurred_at']; + } + if (array_key_exists('closed_at', $data)) { + $badDebt->closed_at = $data['closed_at']; + } + if (isset($data['is_active'])) { + $badDebt->is_active = $data['is_active']; + } + if (array_key_exists('options', $data)) { + $badDebt->options = $data['options']; + } + + $badDebt->updated_by = $userId; + $badDebt->save(); + + return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']); + }); + } + + /** + * 악성채권 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $badDebt->deleted_by = $userId; + $badDebt->save(); + $badDebt->delete(); + + return true; + }); + } + + /** + * 설정 토글 (is_active) + */ + public function toggle(int $id): BadDebt + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $badDebt->is_active = ! $badDebt->is_active; + $badDebt->updated_by = $userId; + $badDebt->save(); + + return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']); + }); + } + + /** + * 서류 첨부 + */ + public function addDocument(int $id, array $data): BadDebtDocument + { + $tenantId = $this->tenantId(); + + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $document = new BadDebtDocument; + $document->bad_debt_id = $badDebt->id; + $document->document_type = $data['document_type']; + $document->file_id = $data['file_id']; + $document->save(); + + return $document->load('file'); + } + + /** + * 서류 삭제 + */ + public function removeDocument(int $id, int $documentId): bool + { + $tenantId = $this->tenantId(); + + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $document = BadDebtDocument::query() + ->where('bad_debt_id', $badDebt->id) + ->findOrFail($documentId); + + return $document->delete(); + } + + /** + * 메모 추가 + */ + public function addMemo(int $id, array $data): BadDebtMemo + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $memo = new BadDebtMemo; + $memo->bad_debt_id = $badDebt->id; + $memo->content = $data['content']; + $memo->created_by = $userId; + $memo->save(); + + return $memo->load('creator:id,name'); + } + + /** + * 메모 삭제 + */ + public function removeMemo(int $id, int $memoId): bool + { + $tenantId = $this->tenantId(); + + $badDebt = BadDebt::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $memo = BadDebtMemo::query() + ->where('bad_debt_id', $badDebt->id) + ->findOrFail($memoId); + + return $memo->delete(); + } +} diff --git a/app/Swagger/v1/BadDebtApi.php b/app/Swagger/v1/BadDebtApi.php new file mode 100644 index 0000000..8d70136 --- /dev/null +++ b/app/Swagger/v1/BadDebtApi.php @@ -0,0 +1,433 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->unsignedBigInteger('client_id')->comment('거래처 ID'); + $table->decimal('debt_amount', 15, 2)->default(0)->comment('채권 금액'); + $table->string('status', 20)->default('collecting')->comment('상태: collecting(추심중), legal_action(법적조치), recovered(회수완료), bad_debt(대손처리)'); + $table->integer('overdue_days')->default(0)->comment('연체일수'); + $table->unsignedBigInteger('assigned_user_id')->nullable()->comment('담당자 ID'); + $table->date('occurred_at')->nullable()->comment('악성채권 발생일'); + $table->date('closed_at')->nullable()->comment('악성채권 종료일'); + $table->boolean('is_active')->default(true)->comment('설정 활성화 여부'); + $table->json('options')->nullable()->comment('추가 옵션 JSON'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('client_id'); + $table->index('status'); + $table->index('is_active'); + $table->index(['tenant_id', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bad_debts'); + } +}; diff --git a/database/migrations/2025_12_19_160002_create_bad_debt_documents_table.php b/database/migrations/2025_12_19_160002_create_bad_debt_documents_table.php new file mode 100644 index 0000000..05dba85 --- /dev/null +++ b/database/migrations/2025_12_19_160002_create_bad_debt_documents_table.php @@ -0,0 +1,38 @@ +id(); + $table->unsignedBigInteger('bad_debt_id')->comment('악성채권 ID'); + $table->string('document_type', 50)->comment('서류 유형: business_license(사업자등록증), tax_invoice(세금계산서), additional(추가서류)'); + $table->unsignedBigInteger('file_id')->comment('파일 ID'); + $table->timestamps(); + + $table->index('bad_debt_id'); + $table->index('document_type'); + + $table->foreign('bad_debt_id') + ->references('id') + ->on('bad_debts') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bad_debt_documents'); + } +}; diff --git a/database/migrations/2025_12_19_160003_create_bad_debt_memos_table.php b/database/migrations/2025_12_19_160003_create_bad_debt_memos_table.php new file mode 100644 index 0000000..71c7f4b --- /dev/null +++ b/database/migrations/2025_12_19_160003_create_bad_debt_memos_table.php @@ -0,0 +1,37 @@ +id(); + $table->unsignedBigInteger('bad_debt_id')->comment('악성채권 ID'); + $table->text('content')->comment('메모 내용'); + $table->unsignedBigInteger('created_by')->nullable()->comment('작성자 ID'); + $table->timestamps(); + + $table->index('bad_debt_id'); + + $table->foreign('bad_debt_id') + ->references('id') + ->on('bad_debts') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bad_debt_memos'); + } +}; diff --git a/routes/api.php b/routes/api.php index 9b789f2..7caf4e6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,6 +9,7 @@ use App\Http\Controllers\Api\V1\ApprovalFormController; use App\Http\Controllers\Api\V1\ApprovalLineController; use App\Http\Controllers\Api\V1\AttendanceController; +use App\Http\Controllers\Api\V1\BadDebtController; use App\Http\Controllers\Api\V1\BankAccountController; use App\Http\Controllers\Api\V1\BarobillSettingController; use App\Http\Controllers\Api\V1\BoardController; @@ -515,6 +516,23 @@ Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status'); }); + // Bad Debt API (악성채권 추심관리) + Route::prefix('bad-debts')->group(function () { + Route::get('', [BadDebtController::class, 'index'])->name('v1.bad-debts.index'); + Route::post('', [BadDebtController::class, 'store'])->name('v1.bad-debts.store'); + Route::get('/summary', [BadDebtController::class, 'summary'])->name('v1.bad-debts.summary'); + Route::get('/{id}', [BadDebtController::class, 'show'])->whereNumber('id')->name('v1.bad-debts.show'); + Route::put('/{id}', [BadDebtController::class, 'update'])->whereNumber('id')->name('v1.bad-debts.update'); + Route::delete('/{id}', [BadDebtController::class, 'destroy'])->whereNumber('id')->name('v1.bad-debts.destroy'); + Route::patch('/{id}/toggle', [BadDebtController::class, 'toggle'])->whereNumber('id')->name('v1.bad-debts.toggle'); + // 서류 + Route::post('/{id}/documents', [BadDebtController::class, 'addDocument'])->whereNumber('id')->name('v1.bad-debts.documents.store'); + Route::delete('/{id}/documents/{documentId}', [BadDebtController::class, 'removeDocument'])->whereNumber(['id', 'documentId'])->name('v1.bad-debts.documents.destroy'); + // 메모 + Route::post('/{id}/memos', [BadDebtController::class, 'addMemo'])->whereNumber('id')->name('v1.bad-debts.memos.store'); + Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy'); + }); + // Report API (보고서) Route::prefix('reports')->group(function () { Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');