feat: 품목 파일 업로드 API 구현 (절곡도, 시방서, 인정서)
- Products 테이블에 9개 파일 관련 필드 추가
- bending_diagram, bending_details (JSON)
- specification_file, specification_file_name
- certification_file, certification_file_name
- certification_number, certification_start_date, certification_end_date
- ItemsFileController 구현 (Code-based API)
- POST /items/{code}/files - 파일 업로드
- DELETE /items/{code}/files/{type} - 파일 삭제
- 파일 타입: bending_diagram, specification, certification
- ItemsFileUploadRequest 검증
- 파일 타입별 MIME 검증 (이미지/문서)
- 파일 크기 제한 (10MB/20MB)
- 인증 정보 및 절곡 상세 정보 검증
- Swagger 문서 작성 (ItemsFileApi.php)
- 업로드/삭제 API 스펙
- 스키마: ItemFileUploadResponse, ItemFileDeleteResponse
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-11-14 11:26:35
|
||||
> **자동 생성**: 2025-11-17 13:00:34
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -358,6 +358,11 @@ ### tenant_option_values
|
||||
|
||||
- **group()**: belongsTo → `tenant_option_groups`
|
||||
|
||||
### tenant_stat_fields
|
||||
**모델**: `App\Models\Tenants\TenantStatField`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### tenant_user_profiles
|
||||
**모델**: `App\Models\Tenants\TenantUserProfile`
|
||||
|
||||
|
||||
174
app/Http/Controllers/Api/V1/ItemsFileController.php
Normal file
174
app/Http/Controllers/Api/V1/ItemsFileController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ItemsFileUploadRequest;
|
||||
use App\Http\Responses\ApiResponse;
|
||||
use App\Models\Products\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* 품목 파일 관리 컨트롤러
|
||||
*
|
||||
* Code-based 파일 업로드/삭제 API
|
||||
* - 절곡도 (bending_diagram)
|
||||
* - 시방서 (specification)
|
||||
* - 인정서 (certification)
|
||||
*/
|
||||
class ItemsFileController extends Controller
|
||||
{
|
||||
/**
|
||||
* 파일 업로드
|
||||
*
|
||||
* POST /api/v1/items/{code}/files
|
||||
*/
|
||||
public function upload(string $code, ItemsFileUploadRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code, $request) {
|
||||
$product = $this->getProductByCode($code);
|
||||
$validated = $request->validated();
|
||||
$fileType = $request->route('type') ?? $validated['type'];
|
||||
$file = $validated['file'];
|
||||
|
||||
// 파일 저장 경로: items/{code}/{type}/{filename}
|
||||
$directory = sprintf('items/%s/%s', $code, $fileType);
|
||||
$filePath = Storage::disk('public')->putFile($directory, $file);
|
||||
$fileUrl = Storage::disk('public')->url($filePath);
|
||||
$originalName = $file->getClientOriginalName();
|
||||
|
||||
// Product 모델 업데이트
|
||||
$updateData = $this->buildUpdateData($fileType, $filePath, $originalName, $validated);
|
||||
$product->update($updateData);
|
||||
|
||||
return [
|
||||
'file_type' => $fileType,
|
||||
'file_url' => $fileUrl,
|
||||
'file_path' => $filePath,
|
||||
'file_name' => $originalName,
|
||||
'product' => $product->fresh(),
|
||||
];
|
||||
}, __('message.file.uploaded'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 삭제
|
||||
*
|
||||
* DELETE /api/v1/items/{code}/files/{type}
|
||||
*/
|
||||
public function delete(string $code, string $type, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code, $type) {
|
||||
$product = $this->getProductByCode($code);
|
||||
|
||||
// 파일 타입 검증
|
||||
if (! in_array($type, ['bending_diagram', 'specification', 'certification'])) {
|
||||
throw new \InvalidArgumentException(__('error.file.invalid_file_type'));
|
||||
}
|
||||
|
||||
// 파일 경로 가져오기
|
||||
$filePath = $this->getFilePath($product, $type);
|
||||
|
||||
if ($filePath) {
|
||||
// 물리적 파일 삭제
|
||||
Storage::disk('public')->delete($filePath);
|
||||
}
|
||||
|
||||
// DB 필드 null 처리
|
||||
$updateData = $this->buildDeleteData($type);
|
||||
$product->update($updateData);
|
||||
|
||||
return [
|
||||
'file_type' => $type,
|
||||
'deleted' => (bool) $filePath,
|
||||
'product' => $product->fresh(),
|
||||
];
|
||||
}, __('message.file.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드로 Product 조회
|
||||
*/
|
||||
private function getProductByCode(string $code): Product
|
||||
{
|
||||
$tenantId = app('tenant_id');
|
||||
$product = Product::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $code)
|
||||
->first();
|
||||
|
||||
if (! $product) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 타입별 업데이트 데이터 구성
|
||||
*/
|
||||
private function buildUpdateData(string $fileType, string $filePath, string $originalName, array $validated): array
|
||||
{
|
||||
$updateData = match ($fileType) {
|
||||
'bending_diagram' => [
|
||||
'bending_diagram' => $filePath,
|
||||
'bending_details' => $validated['bending_details'] ?? null,
|
||||
],
|
||||
'specification' => [
|
||||
'specification_file' => $filePath,
|
||||
'specification_file_name' => $originalName,
|
||||
],
|
||||
'certification' => [
|
||||
'certification_file' => $filePath,
|
||||
'certification_file_name' => $originalName,
|
||||
'certification_number' => $validated['certification_number'] ?? null,
|
||||
'certification_start_date' => $validated['certification_start_date'] ?? null,
|
||||
'certification_end_date' => $validated['certification_end_date'] ?? null,
|
||||
],
|
||||
default => throw new \InvalidArgumentException(__('error.file.invalid_file_type')),
|
||||
};
|
||||
|
||||
return $updateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 타입별 삭제 데이터 구성
|
||||
*/
|
||||
private function buildDeleteData(string $fileType): array
|
||||
{
|
||||
return match ($fileType) {
|
||||
'bending_diagram' => [
|
||||
'bending_diagram' => null,
|
||||
'bending_details' => null,
|
||||
],
|
||||
'specification' => [
|
||||
'specification_file' => null,
|
||||
'specification_file_name' => null,
|
||||
],
|
||||
'certification' => [
|
||||
'certification_file' => null,
|
||||
'certification_file_name' => null,
|
||||
'certification_number' => null,
|
||||
'certification_start_date' => null,
|
||||
'certification_end_date' => null,
|
||||
],
|
||||
default => throw new \InvalidArgumentException(__('error.file.invalid_file_type')),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Product에서 파일 경로 가져오기
|
||||
*/
|
||||
private function getFilePath(Product $product, string $fileType): ?string
|
||||
{
|
||||
return match ($fileType) {
|
||||
'bending_diagram' => $product->bending_diagram,
|
||||
'specification' => $product->specification_file,
|
||||
'certification' => $product->certification_file,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
109
app/Http/Requests/ItemsFileUploadRequest.php
Normal file
109
app/Http/Requests/ItemsFileUploadRequest.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* 품목 파일 업로드 요청 검증
|
||||
*
|
||||
* 지원 파일 타입:
|
||||
* - bending_diagram: 절곡도 (이미지)
|
||||
* - specification: 시방서 (문서)
|
||||
* - certification: 인정서 (문서)
|
||||
*/
|
||||
class ItemsFileUploadRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$fileType = $this->route('type') ?? $this->input('type');
|
||||
|
||||
$rules = [
|
||||
'type' => ['sometimes', 'required', 'string', 'in:bending_diagram,specification,certification'],
|
||||
];
|
||||
|
||||
// 파일 타입별 검증 규칙
|
||||
if ($fileType === 'bending_diagram') {
|
||||
$rules['file'] = [
|
||||
'required',
|
||||
'file',
|
||||
'mimes:jpg,jpeg,png,gif,bmp,svg,webp',
|
||||
'max:10240', // 10MB
|
||||
];
|
||||
} else {
|
||||
// specification, certification - 문서 파일
|
||||
$rules['file'] = [
|
||||
'required',
|
||||
'file',
|
||||
'mimes:pdf,doc,docx,xls,xlsx,hwp',
|
||||
'max:20480', // 20MB
|
||||
];
|
||||
}
|
||||
|
||||
// 인증 정보 (certification 타입일 때만)
|
||||
if ($fileType === 'certification') {
|
||||
$rules['certification_number'] = ['sometimes', 'nullable', 'string', 'max:50'];
|
||||
$rules['certification_start_date'] = ['sometimes', 'nullable', 'date'];
|
||||
$rules['certification_end_date'] = ['sometimes', 'nullable', 'date', 'after_or_equal:certification_start_date'];
|
||||
}
|
||||
|
||||
// 절곡 상세 정보 (bending_diagram 타입일 때만)
|
||||
if ($fileType === 'bending_diagram') {
|
||||
$rules['bending_details'] = ['sometimes', 'nullable', 'array'];
|
||||
$rules['bending_details.*.angle'] = ['required_with:bending_details', 'numeric'];
|
||||
$rules['bending_details.*.length'] = ['required_with:bending_details', 'numeric'];
|
||||
$rules['bending_details.*.type'] = ['required_with:bending_details', 'string'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'type' => '파일 타입',
|
||||
'file' => '파일',
|
||||
'certification_number' => '인증번호',
|
||||
'certification_start_date' => '인증 시작일',
|
||||
'certification_end_date' => '인증 종료일',
|
||||
'bending_details' => '절곡 상세 정보',
|
||||
'bending_details.*.angle' => '절곡 각도',
|
||||
'bending_details.*.length' => '절곡 길이',
|
||||
'bending_details.*.type' => '절곡 타입',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'type.in' => '파일 타입은 bending_diagram, specification, certification 중 하나여야 합니다.',
|
||||
'file.required' => '파일을 선택해주세요.',
|
||||
'file.mimes' => '허용되지 않는 파일 형식입니다.',
|
||||
'file.max' => '파일 크기가 너무 큽니다.',
|
||||
'certification_end_date.after_or_equal' => '인증 종료일은 시작일 이후여야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,20 @@ class Product extends Model
|
||||
'safety_stock', 'lead_time', 'is_variable_size',
|
||||
'product_category', 'part_type',
|
||||
'attributes_archive',
|
||||
// 파일 필드
|
||||
'bending_diagram', 'bending_details',
|
||||
'specification_file', 'specification_file_name',
|
||||
'certification_file', 'certification_file_name',
|
||||
'certification_number', 'certification_start_date', 'certification_end_date',
|
||||
'created_by', 'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'attributes' => 'array',
|
||||
'attributes_archive' => 'array',
|
||||
'bending_details' => 'array',
|
||||
'certification_start_date' => 'date',
|
||||
'certification_end_date' => 'date',
|
||||
'is_sellable' => 'boolean',
|
||||
'is_purchasable' => 'boolean',
|
||||
'is_producible' => 'boolean',
|
||||
|
||||
@@ -60,6 +60,7 @@ public static function cloneGlobalMenusForTenant(int $tenantId): array
|
||||
* 테넌트를 위한 기본 메뉴 구조 생성 (구버전 - 하위 호환성 유지)
|
||||
*
|
||||
* @deprecated Use cloneGlobalMenusForTenant() instead
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @return array 생성된 메뉴 ID 목록
|
||||
*/
|
||||
|
||||
179
app/Swagger/v1/ItemsFileApi.php
Normal file
179
app/Swagger/v1/ItemsFileApi.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="Items Files",
|
||||
* description="품목 파일 관리 API (절곡도, 시방서, 인정서)"
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ItemFileUploadResponse",
|
||||
* type="object",
|
||||
* required={"file_type", "file_url", "file_path", "file_name"},
|
||||
*
|
||||
* @OA\Property(property="file_type", type="string", enum={"bending_diagram", "specification", "certification"}, example="bending_diagram", description="파일 타입"),
|
||||
* @OA\Property(property="file_url", type="string", format="uri", example="http://api.sam.kr/storage/items/P-001/bending_diagram/abc123.jpg", description="파일 URL"),
|
||||
* @OA\Property(property="file_path", type="string", example="items/P-001/bending_diagram/abc123.jpg", description="파일 경로"),
|
||||
* @OA\Property(property="file_name", type="string", example="절곡도_V1.jpg", description="원본 파일명"),
|
||||
* @OA\Property(property="product", ref="#/components/schemas/Product", description="업데이트된 품목 정보")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="ItemFileDeleteResponse",
|
||||
* type="object",
|
||||
* required={"file_type", "deleted"},
|
||||
*
|
||||
* @OA\Property(property="file_type", type="string", enum={"bending_diagram", "specification", "certification"}, example="bending_diagram", description="파일 타입"),
|
||||
* @OA\Property(property="deleted", type="boolean", example=true, description="파일 삭제 여부"),
|
||||
* @OA\Property(property="product", ref="#/components/schemas/Product", description="업데이트된 품목 정보")
|
||||
* )
|
||||
*/
|
||||
class ItemsFileApi
|
||||
{
|
||||
/**
|
||||
* 파일 업로드
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/items/{code}/files",
|
||||
* summary="품목 파일 업로드",
|
||||
* description="품목에 파일을 업로드합니다 (절곡도/시방서/인정서)",
|
||||
* operationId="uploadItemFile",
|
||||
* tags={"Items Files"},
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(
|
||||
* name="code",
|
||||
* in="path",
|
||||
* required=true,
|
||||
* description="품목 코드",
|
||||
*
|
||||
* @OA\Schema(type="string", example="P-001")
|
||||
* ),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
*
|
||||
* @OA\Schema(
|
||||
* required={"type", "file"},
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="type",
|
||||
* type="string",
|
||||
* enum={"bending_diagram", "specification", "certification"},
|
||||
* description="파일 타입",
|
||||
* example="bending_diagram"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="file",
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="업로드할 파일 (절곡도: jpg,png,gif,svg / 문서: pdf,doc,docx,xls,xlsx,hwp)"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="bending_details",
|
||||
* type="array",
|
||||
* description="절곡 상세 정보 (bending_diagram 타입일 때만)",
|
||||
* @OA\Items(
|
||||
* type="object",
|
||||
* required={"angle", "length", "type"},
|
||||
* @OA\Property(property="angle", type="number", format="float", example=90, description="절곡 각도"),
|
||||
* @OA\Property(property="length", type="number", format="float", example=100.5, description="절곡 길이"),
|
||||
* @OA\Property(property="type", type="string", example="V형", description="절곡 타입")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="certification_number",
|
||||
* type="string",
|
||||
* description="인증번호 (certification 타입일 때만)",
|
||||
* example="CERT-2025-001"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="certification_start_date",
|
||||
* type="string",
|
||||
* format="date",
|
||||
* description="인증 시작일 (certification 타입일 때만)",
|
||||
* example="2025-01-01"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="certification_end_date",
|
||||
* type="string",
|
||||
* format="date",
|
||||
* description="인증 종료일 (certification 타입일 때만)",
|
||||
* example="2026-12-31"
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="파일 업로드 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="파일이 업로드되었습니다."),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ItemFileUploadResponse")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function upload() {}
|
||||
|
||||
/**
|
||||
* 파일 삭제
|
||||
*
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/items/{code}/files/{type}",
|
||||
* summary="품목 파일 삭제",
|
||||
* description="품목의 파일을 삭제합니다",
|
||||
* operationId="deleteItemFile",
|
||||
* tags={"Items Files"},
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(
|
||||
* name="code",
|
||||
* in="path",
|
||||
* required=true,
|
||||
* description="품목 코드",
|
||||
*
|
||||
* @OA\Schema(type="string", example="P-001")
|
||||
* ),
|
||||
*
|
||||
* @OA\Parameter(
|
||||
* name="type",
|
||||
* in="path",
|
||||
* required=true,
|
||||
* description="파일 타입",
|
||||
*
|
||||
* @OA\Schema(type="string", enum={"bending_diagram", "specification", "certification"}, example="bending_diagram")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="파일 삭제 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string", example="파일이 삭제되었습니다."),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/ItemFileDeleteResponse")
|
||||
* )
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="존재하지 않는 URI 또는 데이터", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function delete() {}
|
||||
}
|
||||
@@ -31,4 +31,4 @@ public function down(): void
|
||||
$table->dropIndex('menus_deleted_at_idx');
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 품목 파일 필드 추가
|
||||
*
|
||||
* 프론트엔드 요구사항 (BACKEND_DEVELOPMENT_ROADMAP_V2.md):
|
||||
* - 절곡도 (bending_diagram)
|
||||
* - 시방서 (specification_file, specification_file_name)
|
||||
* - 인정서 (certification_file, certification_file_name)
|
||||
* - 인증 정보 (certification_number, certification_start_date, certification_end_date)
|
||||
*
|
||||
* 파일 저장 방식:
|
||||
* - Storage::putFile('items/{code}/{type}', $file)
|
||||
* - URL 저장: Storage::url($path) 또는 relative path
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
// ================================================
|
||||
// 절곡도 (Bending Diagram)
|
||||
// ================================================
|
||||
if (! Schema::hasColumn('products', 'bending_diagram')) {
|
||||
$table->string('bending_diagram', 255)
|
||||
->nullable()
|
||||
->after('part_type')
|
||||
->comment('절곡도 파일 경로 (이미지 URL)');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('products', 'bending_details')) {
|
||||
$table->json('bending_details')
|
||||
->nullable()
|
||||
->after('bending_diagram')
|
||||
->comment('절곡 상세 정보 (BendingDetail[])');
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 시방서 (Specification File)
|
||||
// ================================================
|
||||
if (! Schema::hasColumn('products', 'specification_file')) {
|
||||
$table->string('specification_file', 255)
|
||||
->nullable()
|
||||
->after('bending_details')
|
||||
->comment('시방서 파일 경로');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('products', 'specification_file_name')) {
|
||||
$table->string('specification_file_name', 255)
|
||||
->nullable()
|
||||
->after('specification_file')
|
||||
->comment('시방서 원본 파일명');
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 인정서 (Certification File)
|
||||
// ================================================
|
||||
if (! Schema::hasColumn('products', 'certification_file')) {
|
||||
$table->string('certification_file', 255)
|
||||
->nullable()
|
||||
->after('specification_file_name')
|
||||
->comment('인정서 파일 경로');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('products', 'certification_file_name')) {
|
||||
$table->string('certification_file_name', 255)
|
||||
->nullable()
|
||||
->after('certification_file')
|
||||
->comment('인정서 원본 파일명');
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// 인증 정보 (Certification Info)
|
||||
// ================================================
|
||||
if (! Schema::hasColumn('products', 'certification_number')) {
|
||||
$table->string('certification_number', 50)
|
||||
->nullable()
|
||||
->after('certification_file_name')
|
||||
->comment('인증번호');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('products', 'certification_start_date')) {
|
||||
$table->date('certification_start_date')
|
||||
->nullable()
|
||||
->after('certification_number')
|
||||
->comment('인증 시작일');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('products', 'certification_end_date')) {
|
||||
$table->date('certification_end_date')
|
||||
->nullable()
|
||||
->after('certification_start_date')
|
||||
->comment('인증 종료일');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$columns = [
|
||||
'certification_end_date',
|
||||
'certification_start_date',
|
||||
'certification_number',
|
||||
'certification_file_name',
|
||||
'certification_file',
|
||||
'specification_file_name',
|
||||
'specification_file',
|
||||
'bending_details',
|
||||
'bending_diagram',
|
||||
];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn('products', $column)) {
|
||||
$table->dropColumn($column);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -268,4 +268,4 @@ public function run(): void
|
||||
|
||||
$this->command->info('✅ 글로벌 메뉴 템플릿 생성 완료 (약 60개 메뉴 항목)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,12 @@
|
||||
Route::get('/categories', [\App\Http\Controllers\Api\V1\ItemsBomController::class, 'listCategories'])->name('v1.items.bom.categories'); // 카테고리 목록
|
||||
});
|
||||
|
||||
// Items Files (Code-based File Upload API)
|
||||
Route::prefix('items/{code}/files')->group(function () {
|
||||
Route::post('', [\App\Http\Controllers\Api\V1\ItemsFileController::class, 'upload'])->name('v1.items.files.upload'); // 파일 업로드
|
||||
Route::delete('/{type}', [\App\Http\Controllers\Api\V1\ItemsFileController::class, 'delete'])->name('v1.items.files.delete'); // 파일 삭제 (type: bending_diagram|specification|certification)
|
||||
});
|
||||
|
||||
// BOM (product_components: ref_type=PRODUCT|MATERIAL)
|
||||
Route::prefix('products/{id}/bom')->group(function () {
|
||||
Route::post('/', [ProductBomItemController::class, 'replace'])->name('v1.products.bom.replace');
|
||||
|
||||
Reference in New Issue
Block a user