diff --git a/app/Http/Controllers/Api/V1/MaterialController.php b/app/Http/Controllers/Api/V1/MaterialController.php index cbbed0a..0a96d90 100644 --- a/app/Http/Controllers/Api/V1/MaterialController.php +++ b/app/Http/Controllers/Api/V1/MaterialController.php @@ -4,15 +4,18 @@ use Illuminate\Http\Request; use App\Http\Controllers\Controller; -use App\Services\MeterialService; +use App\Services\MaterialService; use App\Helpers\ApiResponse; -class MaterialController +class MaterialController extends Controller { + + public function __construct(private MaterialService $service) {} + public function index(Request $request) { return ApiResponse::handle(function () use ($request) { - return MeterialService::getMeterials($request); + return $this->service->getMaterials($request->all()); }, '제품 목록 조회'); } @@ -20,7 +23,7 @@ public function index(Request $request) public function store(Request $request) { return ApiResponse::handle(function () use ($request) { - return MeterialService::setMeterial($request); + return $this->service->setMaterial($request->all()); }, '제품 등록'); } @@ -28,7 +31,7 @@ public function store(Request $request) public function show(Request $request, int $id) { return ApiResponse::handle(function () use ($id) { - return MeterialService::getMeterial($id); + return $this->service->getMaterial($id); }, '특정제품 상세 조회'); } @@ -36,7 +39,7 @@ public function show(Request $request, int $id) public function update(Request $request, int $id) { return ApiResponse::handle(function () use ($id) { - return MeterialService::updateMeterial($id); + return $this->service->updateMaterial($id); }, '제품 수정'); } @@ -44,7 +47,7 @@ public function update(Request $request, int $id) public function destroy(Request $request, int $id) { return ApiResponse::handle(function () use ($id) { - return MeterialService::destoryMeterial($id); + return $this->service->destroyMaterial($id); }, '제품 삭제'); } } diff --git a/app/Models/Materials/Material.php b/app/Models/Materials/Material.php index a832e22..0aaf330 100644 --- a/app/Models/Materials/Material.php +++ b/app/Models/Materials/Material.php @@ -5,6 +5,8 @@ use App\Models\Commons\File; use App\Models\Commons\Tag; use App\Models\Qualitys\Lot; +use App\Traits\ModelTrait; +use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,20 +15,13 @@ */ class Material extends Model { - use SoftDeletes; + use SoftDeletes, ModelTrait, BelongsToTenant; - // 자재(품목) 마스터 - protected $fillable = [ - 'name', // 자재명(품명) - 'specification', // 규격 - 'material_code', // 자재코드 - 'unit', // 단위 - 'is_inspection', // 검사대상 여부(Y/N) - 'search_tag', // 검색 태그 - 'remarks', // 비고 + protected $casts = [ + 'attributes' => 'array', + 'options' => 'array', ]; - // 자재 입고 내역 public function receipts() { diff --git a/app/Services/MaterialService.php b/app/Services/MaterialService.php new file mode 100644 index 0000000..246e44c --- /dev/null +++ b/app/Services/MaterialService.php @@ -0,0 +1,297 @@ +fails()) { + return ['error' => $v->errors()->first(), 'code' => 422]; + } + return $v->validated(); + } + + /** 목록 */ + public function getMaterials(array $params) + { + $tenantId = $this->tenantId(); + + $p = $this->v($params, [ + 'q' => 'nullable|string|max:100', + 'category' => 'nullable|integer|min:1', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer|min:1|max:200', + ]); + if (isset($p['error'])) return $p; + + $q = Material::query() + ->where('tenant_id', $tenantId); // SoftDeletes가 있으면 기본적으로 deleted_at IS NULL + + if (!empty($p['category'])) { + $q->where('category_id', (int)$p['category']); + } + if (!empty($p['q'])) { + $kw = '%' . $p['q'] . '%'; + $q->where(function ($w) use ($kw) { + $w->where('item_name', 'like', $kw) + ->orWhere('name', 'like', $kw) + ->orWhere('material_code', 'like', $kw) + ->orWhere('search_tag', 'like', $kw); + }); + } + + $q->orderByDesc('id'); + + $perPage = $p['per_page'] ?? 20; + $page = $p['page'] ?? null; + + return $q->paginate($perPage, ['*'], 'page', $page); + } + + /** 단건 조회 */ + public function getMaterial(int $id) + { + $tenantId = $this->tenantId(); + + /** @var Material|null $row */ + $row = Material::query() + ->where('tenant_id', $tenantId) + ->find($id); + + if (!$row) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + + // 모델에서 casts가 없을 수 있으니 안전하게 배열화 + $row->attributes = is_array($row->attributes) ? $row->attributes : ($row->attributes ? json_decode($row->attributes, true) : null); + $row->options = is_array($row->options) ? $row->options : ($row->options ? json_decode($row->options, true) : null); + + return $row; + } + + /** 등록 */ + public function setMaterial(array $params) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $p = $this->v($params, [ + 'category_id' => 'nullable|integer|min:1', + 'name' => 'required|string|max:100', + 'unit' => 'required|string|max:10', + 'is_inspection' => 'nullable|in:Y,N', + 'search_tag' => 'nullable|string', + 'remarks' => 'nullable|string', + 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map + 'options' => 'nullable|array', // [{label,value,unit}] 또는 map + 'material_code' => 'nullable|string|max:50', + 'specification' => 'nullable|string|max:100', + ]); + if (isset($p['error'])) return $p; + + // 기존 normalizeAttributes 사용(그대로), options는 새 normalizeOptions 사용 + $attributes = $this->normalizeAttributes($p['attributes'] ?? null); + $options = $this->normalizeOptions($p['options'] ?? null); + + $itemName = $this->buildItemName($p['name'], $attributes); + $specText = $p['specification'] ?? $this->buildSpecText($attributes); + + $m = new Material(); + $m->tenant_id = $tenantId; + $m->category_id = $p['category_id'] ?? null; + $m->name = $p['name']; + $m->item_name = $itemName; + $m->specification = $specText; + $m->material_code = $p['material_code'] ?? null; + $m->unit = $p['unit']; + $m->is_inspection = $p['is_inspection'] ?? 'N'; + $m->search_tag = $p['search_tag'] ?? null; + $m->remarks = $p['remarks'] ?? null; + $m->attributes = $attributes ?? null; + $m->options = $options ?? null; + $m->created_by = $userId ?? 0; + $m->updated_by = $userId ?? null; + $m->save(); + + return $this->getMaterial($m->id); + } + + /** 수정 */ + public function updateMaterial(int $id, array $params = []) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + /** @var Material|null $exists */ + $exists = Material::query()->where('tenant_id', $tenantId)->find($id); + if (!$exists) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + + $p = $this->v($params, [ + 'category_id' => 'nullable|integer|min:1', + 'name' => 'nullable|string|max:100', + 'unit' => 'nullable|string|max:10', + 'is_inspection' => 'nullable|in:Y,N', + 'search_tag' => 'nullable|string', + 'remarks' => 'nullable|string', + 'attributes' => 'nullable|array', // [{label,value,unit}] 또는 map + 'options' => 'nullable|array', // [{label,value,unit}] 또는 map + 'material_code' => 'nullable|string|max:50', + 'specification' => 'nullable|string|max:100', + ]); + if (isset($p['error'])) return $p; + + $currentAttrs = is_array($exists->attributes) ? $exists->attributes + : ($exists->attributes ? json_decode($exists->attributes, true) : null); + $currentOpts = is_array($exists->options) ? $exists->options + : ($exists->options ? json_decode($exists->options, true) : null); + + // 변경 점만 정규화 + $attrs = array_key_exists('attributes', $p) + ? $this->normalizeAttributes($p['attributes']) + : $currentAttrs; + $opts = array_key_exists('options', $p) + ? $this->normalizeOptions($p['options']) + : $currentOpts; + + $baseName = array_key_exists('name', $p) ? ($p['name'] ?? $exists->name) : $exists->name; + + $exists->category_id = $p['category_id'] ?? $exists->category_id; + $exists->name = $baseName; + $exists->item_name = $this->buildItemName($baseName, $attrs); + $exists->specification = array_key_exists('specification', $p) + ? ($p['specification'] ?? null) + : ($exists->specification ?: $this->buildSpecText($attrs)); + $exists->material_code = $p['material_code'] ?? $exists->material_code; + $exists->unit = $p['unit'] ?? $exists->unit; + $exists->is_inspection = $p['is_inspection'] ?? $exists->is_inspection; + $exists->search_tag = $p['search_tag'] ?? $exists->search_tag; + $exists->remarks = $p['remarks'] ?? $exists->remarks; + + if (array_key_exists('attributes', $p)) $exists->attributes = $attrs; + if (array_key_exists('options', $p)) $exists->options = $opts; + + $exists->updated_by = $userId ?? $exists->updated_by; + $exists->save(); + + return $this->getMaterial($exists->id); + } + + /** 삭제(소프트) */ + public function destroyMaterial(int $id) + { + $tenantId = $this->tenantId(); + + /** @var Material|null $row */ + $row = Material::query() + ->where('tenant_id', $tenantId) + ->find($id); + + if (!$row) return ['error' => '자재를 찾을 수 없습니다.', 'code' => 404]; + + $row->delete(); + + return ['id' => $id, 'deleted_at' => now()->toDateTimeString()]; + } + + /* ------------------------- + 헬퍼: 규격/품목명 빌더 + attributes 예시: + [ + {"label":"두께","value":"10","unit":"T"}, + {"label":"길이","value":"150","unit":"CM"} + ] + → item_name: "철판 10T 150CM" + → specification: "두께 10T, 길이 150CM" + ------------------------- */ + + private function normalizeAttributes(null|array $attrs): ?array + { + if (!$attrs) return null; + + $out = []; + foreach ($attrs as $a) { + if (!is_array($a)) continue; + $label = trim((string)($a['label'] ?? '')); + $value = trim((string)($a['value'] ?? '')); + $unit = trim((string)($a['unit'] ?? '')); + if ($label === '' && $value === '' && $unit === '') continue; + + $out[] = ['label' => $label, 'value' => $value, 'unit' => $unit]; + } + return $out ?: null; + } + + private function buildItemName(string $name, ?array $attrs): string + { + if (!$attrs || count($attrs) === 0) return $name; + + $parts = []; + foreach ($attrs as $a) { + $value = (string)($a['value'] ?? ''); + $unit = (string)($a['unit'] ?? ''); + $chunk = trim($value . $unit); + if ($chunk !== '') $parts[] = $chunk; + } + return trim($name . ' ' . implode(' ', $parts)); + } + + private function buildSpecText(?array $attrs): ?string + { + if (!$attrs || count($attrs) === 0) return null; + + $parts = []; + foreach ($attrs as $a) { + $label = (string)($a['label'] ?? ''); + $value = (string)($a['value'] ?? ''); + $unit = (string)($a['unit'] ?? ''); + $valueWithUnit = trim($value . $unit); + + if ($label !== '' && $valueWithUnit !== '') { + $parts[] = "{$label} {$valueWithUnit}"; + } elseif ($valueWithUnit !== '') { + $parts[] = $valueWithUnit; + } + } + return $parts ? implode(', ', $parts) : null; + } + + /** + * options 입력을 [{label,value,unit}] 형태로 정규화. + * - 이미 리스트(triple)면 그대로 정규화 + * - 맵({"키":"값"})이면 [{label:키, value:값, unit:""}...]로 변환 + */ + private function normalizeOptions(null|array $in): ?array + { + if (!$in) return null; + + // 연관 맵 형태인지 간단 판별 + $isAssoc = array_keys($in) !== range(0, count($in) - 1); + + if ($isAssoc) { + $out = []; + foreach ($in as $k => $v) { + $label = trim((string)$k); + $value = is_scalar($v) ? trim((string)$v) : json_encode($v, JSON_UNESCAPED_UNICODE); + if ($label === '' && $value === '') continue; + $out[] = ['label' => $label, 'value' => $value, 'unit' => '']; + } + return $out ?: null; + } + + // 리스트(triple) 정규화 + $out = []; + foreach ($in as $a) { + if (!is_array($a)) continue; + $label = trim((string)($a['label'] ?? '')); + $value = trim((string)($a['value'] ?? '')); + $unit = trim((string)($a['unit'] ?? '')); + if ($label === '' && $value === '' && $unit === '') continue; + $out[] = ['label' => $label, 'value' => $value, 'unit' => $unit]; + } + return $out ?: null; + } +} diff --git a/app/Services/MeterialService.php b/app/Services/MeterialService.php deleted file mode 100644 index 6bb51a9..0000000 --- a/app/Services/MeterialService.php +++ /dev/null @@ -1,42 +0,0 @@ -get(); - } - - public static function setMeterial() - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function getMeterial(int $id) - { - $query = Material::find($id); - return $query->get(); - } - - public static function updateMeterial(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } - - public static function destoryMeterial(int $id) - { - $query = DB::table('COM_CODE') - ->select(['CODE_TP_ID', 'CODE_ID', 'CODE_VAL', 'CODE_DESC', 'USE_YN']); - return $query->get(); - } -} diff --git a/app/Swagger/v1/MaterialApi.php b/app/Swagger/v1/MaterialApi.php index d0f5381..254dbbe 100644 --- a/app/Swagger/v1/MaterialApi.php +++ b/app/Swagger/v1/MaterialApi.php @@ -2,6 +2,297 @@ namespace App\Swagger\v1; +/** + * @OA\Tag( + * name="Material", + * description="자재 관리(목록/등록/조회/수정/삭제)" + * ) + */ +/** + * 공통 응답 스키마 + */ +/** + * @OA\Schema( + * schema="ApiResponse", + * type="object", + * required={"success","message"}, + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="자재 목록 조회 성공"), + * @OA\Property(property="data", nullable=true) + * ) + * + * @OA\Schema( + * schema="ErrorResponse", + * type="object", + * required={"success","message"}, + * @OA\Property(property="success", type="boolean", example=false), + * @OA\Property(property="message", type="string", example="자재 등록 실패"), + * @OA\Property(property="data", type="null", example=null) + * ) + */ - class MaterialApi {} +/** + * 자재/요청 스키마 + */ +/** + * @OA\Schema( + * schema="MaterialAttribute", + * type="object", + * description="규격 정보 한 항목", + * @OA\Property(property="label", type="string", example="두께"), + * @OA\Property(property="value", type="string", example="10"), + * @OA\Property(property="unit", type="string", example="T") + * ) + * + * @OA\Schema( + * schema="Material", + * type="object", + * description="자재 상세", + * required={"id","tenant_id","name","item_name","unit"}, + * @OA\Property(property="id", type="integer", example=101), + * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="category_id", type="integer", nullable=true, example=3), + * @OA\Property(property="name", type="string", example="철판"), + * @OA\Property(property="item_name", type="string", example="철판 10T 150CM"), + * @OA\Property(property="specification", type="string", nullable=true, example="두께 10T, 길이 150CM"), + * @OA\Property(property="material_code", type="string", nullable=true, example=null), + * @OA\Property(property="unit", type="string", example="EA"), + * @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, example="N"), + * @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 판재, 금속"), + * @OA\Property(property="remarks", type="string", nullable=true, example="비고 메모"), + * @OA\Property( + * property="attributes", + * type="array", + * nullable=true, + * @OA\Items(ref="#/components/schemas/MaterialAttribute") + * ), + * @OA\Property( + * property="options", + * type="object", + * nullable=true, + * example={"manufacturer":"ACME","color":"SILVER"} + * ), + * @OA\Property(property="created_by", type="integer", example=12), + * @OA\Property(property="updated_by", type="integer", nullable=true, example=12), + * @OA\Property(property="created_at", type="string", format="date-time", example="2025-08-21 10:00:00"), + * @OA\Property(property="updated_at", type="string", format="date-time", example="2025-08-21 10:00:00") + * ) + * + * @OA\Schema( + * schema="MaterialBrief", + * type="object", + * description="자재 요약", + * required={"id","name","item_name","unit"}, + * @OA\Property(property="id", type="integer", example=101), + * @OA\Property(property="category_id", type="integer", nullable=true, example=3), + * @OA\Property(property="name", type="string", example="철판"), + * @OA\Property(property="item_name", type="string", example="철판 10T 150CM"), + * @OA\Property(property="unit", type="string", example="EA") + * ) + * + * @OA\Schema( + * schema="MaterialList", + * type="array", + * @OA\Items(ref="#/components/schemas/MaterialBrief") + * ) + * + * @OA\Schema( + * schema="MaterialIndexParams", + * type="object", + * @OA\Property(property="q", type="string", nullable=true, example="알루미늄"), + * @OA\Property(property="category", type="integer", nullable=true, example=3), + * @OA\Property(property="page", type="integer", nullable=true, example=1), + * @OA\Property(property="per_page", type="integer", nullable=true, example=20) + * ) + * + * @OA\Schema( + * schema="MaterialCreateRequest", + * type="object", + * required={"name","unit"}, + * @OA\Property(property="category_id", type="integer", nullable=true, example=3), + * @OA\Property(property="name", type="string", example="철판"), + * @OA\Property(property="unit", type="string", example="EA"), + * @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, example="N"), + * @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 판재, 금속"), + * @OA\Property(property="remarks", type="string", nullable=true, example="비고 메모"), + * @OA\Property( + * property="attributes", + * type="array", + * nullable=true, + * @OA\Items(ref="#/components/schemas/MaterialAttribute") + * ), + * @OA\Property( + * property="options", + * type="object", + * nullable=true, + * example={"manufacturer":"ACME","color":"SILVER"} + * ), + * @OA\Property(property="material_code", type="string", nullable=true, example=null), + * @OA\Property(property="specification", type="string", nullable=true, example=null) + * ) + * + * @OA\Schema( + * schema="MaterialUpdateRequest", + * type="object", + * @OA\Property(property="category_id", type="integer", nullable=true, example=3), + * @OA\Property(property="name", type="string", nullable=true, example="철판(개선)"), + * @OA\Property(property="unit", type="string", nullable=true, example="KG"), + * @OA\Property(property="is_inspection", type="string", enum={"Y","N"}, nullable=true, example="Y"), + * @OA\Property(property="search_tag", type="string", nullable=true, example="철판, 금속"), + * @OA\Property(property="remarks", type="string", nullable=true, example="비고 변경"), + * @OA\Property( + * property="attributes", + * type="array", + * nullable=true, + * @OA\Items(ref="#/components/schemas/MaterialAttribute") + * ), + * @OA\Property( + * property="options", + * type="object", + * nullable=true, + * example={"manufacturer":"ACME","color":"BLACK"} + * ), + * @OA\Property(property="material_code", type="string", nullable=true, example="MAT-0002"), + * @OA\Property(property="specification", type="string", nullable=true, example="두께 12T, 길이 180CM") + * ) + */ + +class MaterialApi +{ + /** + * @OA\Get( + * path="/api/v1/materials", + * summary="자재 목록 조회", + * description="자재 목록을 페이징으로 반환합니다. (q로 코드/이름/태그 검색, category로 분류 필터)", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\Parameter(name="q", in="query", required=false, @OA\Schema(type="string"), description="검색어(이름/코드/태그 등)"), + * @OA\Parameter(name="category", in="query", required=false, @OA\Schema(type="integer"), description="카테고리 ID"), + * @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer", example=1)), + * @OA\Parameter(name="per_page", in="query", required=false, @OA\Schema(type="integer", example=20)), + * @OA\Response( + * response=200, description="자재 목록 조회 성공", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property( + * property="data", + * type="object", + * @OA\Property(property="current_page", type="integer", example=1), + * @OA\Property(property="per_page", type="integer", example=20), + * @OA\Property(property="total", type="integer", example=2), + * @OA\Property(property="data", ref="#/components/schemas/MaterialList") + * )) + * } + * ) + * ) + * ) + */ + public function index() {} + + /** + * @OA\Post( + * path="/api/v1/materials", + * summary="자재 등록", + * description="자재를 등록합니다. item_name/specification은 name+attributes를 기반으로 서버에서 자동 생성됩니다.", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialCreateRequest")), + * @OA\Response( + * response=200, description="자재 등록 성공", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) + * } + * ) + * ), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function store() {} + + /** + * @OA\Get( + * path="/api/v1/materials/{id}", + * summary="자재 단건 조회", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * @OA\Response( + * response=200, description="자재 단건 조회 성공", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) + * } + * ) + * ), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function show() {} + + /** + * @OA\Put( + * path="/api/v1/materials/{id}", + * summary="자재 수정(전체)", + * description="자재를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")), + * @OA\Response( + * response=200, description="자재 수정 성공", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) + * } + * ) + * ), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updatePut() {} + + /** + * @OA\Patch( + * path="/api/v1/materials/{id}", + * summary="자재 부분 수정", + * description="자재의 일부 필드를 수정합니다. name/attributes 변경 시 item_name/specification이 서버에서 재계산됩니다.", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/MaterialUpdateRequest")), + * @OA\Response( + * response=200, description="자재 수정 성공", + * @OA\JsonContent( + * allOf={ + * @OA\Schema(ref="#/components/schemas/ApiResponse"), + * @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Material")) + * } + * ) + * ), + * @OA\Response(response=404, description="데이터 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), + * @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")) + * ) + */ + public function updatePatch() {} + + /** + * @OA\Delete( + * path="/api/v1/materials/{id}", + * summary="자재 삭제", + * description="자재를 소프트 삭제합니다.", + * tags={"Material"}, + * security={{"ApiKeyAuth": {}},{"BearerAuth": {}}}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer", example=101)), + * @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() {} +} diff --git a/database/migrations/2025_08_21_000100_create_materials_table.php b/database/migrations/2025_08_21_000100_create_materials_table.php new file mode 100644 index 0000000..ff10f3a --- /dev/null +++ b/database/migrations/2025_08_21_000100_create_materials_table.php @@ -0,0 +1,49 @@ +unsignedBigInteger('category_id') + ->nullable() + ->after('tenant_id') + ->comment('카테고리 ID') + ->index(); + } + + // item_name + if (!Schema::hasColumn('materials', 'item_name')) { + $table->string('item_name', 255) + ->nullable() + ->after('name') + ->comment('품목명 (자재명+규격정보)'); + } + }); + + // 필요 시 FK RAW SQL로 추가(선택) + // try { + // DB::statement('ALTER TABLE materials + // ADD CONSTRAINT materials_category_id_foreign + // FOREIGN KEY (category_id) REFERENCES categories(id) + // ON DELETE SET NULL'); + // } catch (\Throwable $e) {} + } + + public function down(): void + { + // FK 제거 시도 + try { DB::statement('ALTER TABLE materials DROP FOREIGN KEY materials_category_id_foreign'); } catch (\Throwable $e) {} + + // 컬럼 제거 + try { DB::statement('ALTER TABLE materials DROP COLUMN category_id'); } catch (\Throwable $e) {} + try { DB::statement('ALTER TABLE materials DROP COLUMN item_name'); } catch (\Throwable $e) {} + } +};