diff --git a/app/Http/Controllers/Api/V1/ClassificationController.php b/app/Http/Controllers/Api/V1/ClassificationController.php new file mode 100644 index 0000000..23983f8 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ClassificationController.php @@ -0,0 +1,39 @@ + $this->service->index($request->all()), '분류 목록 조회'); + } + + public function show(string $id) + { + return ApiResponse::handle(fn() => $this->service->show((int)$id), '분류 단건 조회'); + } + + public function store(Request $request) + { + return ApiResponse::handle(fn() => $this->service->store($request->all()), '분류 생성'); + } + + public function update(string $id, Request $request) + { + return ApiResponse::handle(fn() => $this->service->update((int)$id, $request->all()), '분류 수정'); + } + + public function destroy(string $id) + { + return ApiResponse::handle(fn() => $this->service->destroy((int)$id), '분류 삭제'); + } + +} diff --git a/app/Models/Commons/Classification.php b/app/Models/Commons/Classification.php new file mode 100644 index 0000000..1d49960 --- /dev/null +++ b/app/Models/Commons/Classification.php @@ -0,0 +1,41 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + protected $hidden = ['deleted_at']; + + // 스코프 + public function scopeGroup($q, string $group) + { + return $q->where('group', $group); + } + + public function scopeActive($q) + { + return $q->where('is_active', 1); + } +} diff --git a/app/Services/ClassificationService.php b/app/Services/ClassificationService.php new file mode 100644 index 0000000..0baa77e --- /dev/null +++ b/app/Services/ClassificationService.php @@ -0,0 +1,123 @@ +tenantId(); + + $page = (int)($params['page'] ?? 1); + $size = (int)($params['size'] ?? 20); + $q = trim((string)($params['q'] ?? '')); + $group = $params['group'] ?? null; + $only = $params['only_active'] ?? null; + + $query = Classification::query()->where('tenant_id', $tenantId); + + if ($q !== '') { + $query->where(function ($w) use ($q) { + $w->where('name', 'like', "%{$q}%") + ->orWhere('code', 'like', "%{$q}%"); + }); + } + if (!is_null($group) && $group !== '') { + $query->where('group', $group); + } + if (!is_null($only)) { + $query->where('is_active', (int)!!$only); + } + + $query->orderBy('group')->orderBy('code')->orderBy('id'); + + return $query->paginate($size, ['*'], 'page', $page); + } + + /** 단건 */ + public function show(int $id) + { + $tenantId = $this->tenantId(); + $row = Classification::where('tenant_id', $tenantId)->find($id); + if (!$row) throw new NotFoundHttpException(__('error.not_found')); + return $row; + } + + /** 생성 */ + public function store(array $params) + { + $tenantId = $this->tenantId(); + $uid = $this->apiUserId(); + + $v = Validator::make($params, [ + 'group' => 'required|string|max:50', + 'code' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'is_active' => 'nullable|boolean', + ]); + if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + + // 그룹 내 코드 중복 체크(tenant 기준) + $data = $v->validated(); + $exists = Classification::where('tenant_id', $tenantId) + ->where('group', $data['group']) + ->where('code', $data['code']) + ->exists(); + if ($exists) throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + + $data['tenant_id'] = $tenantId; + $data['is_active'] = (int)($data['is_active'] ?? 1); + + return Classification::create($data); + } + + /** 수정 */ + public function update(int $id, array $params) + { + $tenantId = $this->tenantId(); + + $row = Classification::where('tenant_id', $tenantId)->find($id); + if (!$row) throw new NotFoundHttpException(__('error.not_found')); + + $v = Validator::make($params, [ + 'group' => 'sometimes|required|string|max:50', + 'code' => 'sometimes|required|string|max:50', + 'name' => 'sometimes|required|string|max:100', + 'is_active' => 'nullable|boolean', + ]); + if ($v->fails()) throw new BadRequestHttpException($v->errors()->first()); + + $payload = $v->validated(); + + // group 또는 code 변경 시 유니크 검사 + $newGroup = $payload['group'] ?? $row->group; + $newCode = $payload['code'] ?? $row->code; + $dupe = Classification::where('tenant_id', $tenantId) + ->where('group', $newGroup) + ->where('code', $newCode) + ->where('id', '!=', $row->id) + ->exists(); + if ($dupe) throw new BadRequestHttpException(__('validation.unique', ['attribute' => 'code'])); + + $row->update($payload); + return $row->refresh(); + } + + /** 삭제(soft) */ + public function destroy(int $id) + { + $tenantId = $this->tenantId(); + + $row = Classification::where('tenant_id', $tenantId)->find($id); + if (!$row) throw new NotFoundHttpException(__('error.not_found')); + + $row->delete(); + return 'success'; + } +} diff --git a/app/Swagger/v1/ClassificationApi.php b/app/Swagger/v1/ClassificationApi.php new file mode 100644 index 0000000..1efbe9c --- /dev/null +++ b/app/Swagger/v1/ClassificationApi.php @@ -0,0 +1,178 @@ +bigIncrements('id')->comment('PK'); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('group', 50)->comment('분류 그룹 키 (예: product_type, payment_method)'); + $table->string('code', 50)->comment('분류 코드 (그룹 내 고유)'); + $table->string('name', 100)->comment('분류명'); + $table->boolean('is_active')->default(true)->comment('활성여부(1=활성,0=비활성)'); + $table->timestamps(); + $table->softDeletes()->comment('소프트삭제 시각'); + + // 고유/검색 인덱스 + $table->unique(['tenant_id', 'group', 'code'], 'uniq_tenant_group_code'); // code는 그룹 내 유니크 + $table->index(['tenant_id', 'group', 'is_active'], 'idx_tenant_group_active'); + + // FK (모델링 단계에서 생성, 운영 전 필요 시 제거) + $table->foreign('tenant_id') + ->references('id')->on('tenants') + ->cascadeOnUpdate() + ->restrictOnDelete(); + }); + + // 테이블 주석 + DB::statement("ALTER TABLE `classifications` COMMENT='분류 관리(코드 테이블; 평면 구조)';"); + } + + public function down(): void + { + Schema::dropIfExists('classifications'); + } +}; diff --git a/routes/api.php b/routes/api.php index ecbc5cc..fca7902 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ name('v1.categories.destroy'); // 삭제(soft) }); + // Product API + Route::prefix('classifications')->group(function () { + Route::get ('', [ClassificationController::class, 'index'])->name('v1.classifications.index'); // 목록 + Route::post ('', [ClassificationController::class, 'store'])->name('v1.classifications.store'); // 생성 + Route::get ('/{id}', [ClassificationController::class, 'show'])->whereNumber('id')->name('v1.classifications.show'); // 단건 + Route::patch ('/{id}', [ClassificationController::class, 'update'])->whereNumber('id')->name('v1.classifications.update'); // 수정 + Route::delete('/{id}', [ClassificationController::class, 'destroy'])->whereNumber('id')->name('v1.classifications.destroy'); // 삭제 + }); + }); });