fix : 분류 API
This commit is contained in:
39
app/Http/Controllers/Api/V1/ClassificationController.php
Normal file
39
app/Http/Controllers/Api/V1/ClassificationController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ClassificationService;
|
||||
use App\Helpers\ApiResponse;
|
||||
|
||||
class ClassificationController extends Controller
|
||||
{
|
||||
public function __construct(private ClassificationService $service) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(fn() => $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), '분류 삭제');
|
||||
}
|
||||
|
||||
}
|
||||
41
app/Models/Commons/Classification.php
Normal file
41
app/Models/Commons/Classification.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Classification extends Model
|
||||
{
|
||||
use SoftDeletes, ModelTrait, BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'group',
|
||||
'code',
|
||||
'name',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => '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);
|
||||
}
|
||||
}
|
||||
123
app/Services/ClassificationService.php
Normal file
123
app/Services/ClassificationService.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Commons\Classification;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ClassificationService extends Service
|
||||
{
|
||||
/** 목록(검색/페이징) */
|
||||
public function index(array $params)
|
||||
{
|
||||
$tenantId = $this->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';
|
||||
}
|
||||
}
|
||||
178
app/Swagger/v1/ClassificationApi.php
Normal file
178
app/Swagger/v1/ClassificationApi.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Classification", description="분류 관리 (평면 구조 / 코드 테이블)")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Classification",
|
||||
* type="object",
|
||||
* required={"id","group","name"},
|
||||
* @OA\Property(property="id", type="integer", example=11),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="group", type="string", example="product_type"),
|
||||
* @OA\Property(property="code", type="string", nullable=true, example="OUTER"),
|
||||
* @OA\Property(property="name", type="string", example="아우터"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example=null),
|
||||
* @OA\Property(property="is_active", type="integer", example=1),
|
||||
* @OA\Property(property="sort_order", type="integer", example=10),
|
||||
* @OA\Property(property="created_at", type="string", example="2025-08-22 10:00:00"),
|
||||
* @OA\Property(property="updated_at", type="string", example="2025-08-22 10:10:00")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ClassificationPagination",
|
||||
* type="object",
|
||||
* description="라라벨 LengthAwarePaginator 기본 구조",
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
* @OA\Items(ref="#/components/schemas/Classification")
|
||||
* ),
|
||||
* @OA\Property(property="first_page_url", type="string", example="/api/v1/classifications?page=1"),
|
||||
* @OA\Property(property="from", type="integer", example=1),
|
||||
* @OA\Property(property="last_page", type="integer", example=3),
|
||||
* @OA\Property(property="last_page_url", type="string", example="/api/v1/classifications?page=3"),
|
||||
* @OA\Property(
|
||||
* property="links",
|
||||
* type="array",
|
||||
* @OA\Items(type="object",
|
||||
* @OA\Property(property="url", type="string", nullable=true, example=null),
|
||||
* @OA\Property(property="label", type="string", example="« Previous"),
|
||||
* @OA\Property(property="active", type="boolean", example=false)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="next_page_url", type="string", nullable=true, example="/api/v1/classifications?page=2"),
|
||||
* @OA\Property(property="path", type="string", example="/api/v1/classifications"),
|
||||
* @OA\Property(property="per_page", type="integer", example=20),
|
||||
* @OA\Property(property="prev_page_url", type="string", nullable=true, example=null),
|
||||
* @OA\Property(property="to", type="integer", example=20),
|
||||
* @OA\Property(property="total", type="integer", example=60)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ClassificationCreateRequest",
|
||||
* type="object",
|
||||
* required={"group","name"},
|
||||
* @OA\Property(property="group", type="string", maxLength=50, example="product_type"),
|
||||
* @OA\Property(property="code", type="string", nullable=true, maxLength=50, example="OUTER"),
|
||||
* @OA\Property(property="name", type="string", maxLength=100, example="아우터"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, maxLength=255),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="sort_order", type="integer", example=0)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ClassificationUpdateRequest",
|
||||
* type="object",
|
||||
* @OA\Property(property="group", type="string", maxLength=50),
|
||||
* @OA\Property(property="code", type="string", nullable=true, maxLength=50),
|
||||
* @OA\Property(property="name", type="string", maxLength=100),
|
||||
* @OA\Property(property="description", type="string", nullable=true, maxLength=255),
|
||||
* @OA\Property(property="is_active", type="boolean"),
|
||||
* @OA\Property(property="sort_order", type="integer")
|
||||
* )
|
||||
*/
|
||||
class ClassificationApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/classifications",
|
||||
* tags={"Classification"},
|
||||
* summary="분류 목록",
|
||||
* description="그룹/검색/활성여부 조건으로 분류 목록을 페이징 반환합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)),
|
||||
* @OA\Parameter(name="size", in="query", @OA\Schema(type="integer", example=20)),
|
||||
* @OA\Parameter(name="q", in="query", description="코드/이름 검색", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="group", in="query", description="분류 그룹 키", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="only_active", in="query", @OA\Schema(type="boolean")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/ClassificationPagination"))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/classifications/{id}",
|
||||
* tags={"Classification"},
|
||||
* summary="분류 단건 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="조회 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification"))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/classifications",
|
||||
* tags={"Classification"},
|
||||
* summary="분류 생성",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClassificationCreateRequest")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="생성 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification"))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/classifications/{id}",
|
||||
* tags={"Classification"},
|
||||
* summary="분류 수정",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/ClassificationUpdateRequest")),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
* @OA\JsonContent(allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Classification"))
|
||||
* })
|
||||
* ),
|
||||
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/classifications/{id}",
|
||||
* tags={"Classification"},
|
||||
* summary="분류 삭제(soft)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('classifications', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\V1\ClassificationController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\V1\CommonController;
|
||||
@@ -238,5 +239,14 @@
|
||||
Route::delete('/{id}', [CategoryController::class, 'destroy'])->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'); // 삭제
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user