From 7083057d590f2329b6317adc1bafa8b7ed050c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=98=81=EB=B3=B4?= Date: Tue, 17 Mar 2026 12:50:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[bending]=20=EC=A0=88=EA=B3=A1=ED=92=88?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20API=20=EC=99=84=EC=84=B1=20+=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GuiderailModelController/Service/Resource: 가이드레일/케이스/하단마감재 통합 CRUD - item_category 필터 (GUIDERAIL_MODEL/SHUTTERBOX_MODEL/BOTTOMBAR_MODEL) - BendingItemResource: legacy_bending_num 노출 추가 - ApiKeyMiddleware: guiderail-models, files 화이트리스트 추가 - Swagger: BendingItemApi, GuiderailModelApi 문서 (케이스/하단마감재 필드 포함) - 마이그레이션 커맨드 5개: GuiderailImportLegacy, BendingProductImportLegacy, BendingImportImages, BendingModelImportImages, BendingModelImportAssemblyImages - 데이터: GR 20건 + SB 30건 + BB 10건 + 이미지 473건 R2 업로드 --- app/Console/Commands/BendingImportImages.php | 169 ++++ app/Console/Commands/BendingImportLegacy.php | 10 +- .../BendingModelImportAssemblyImages.php | 192 +++++ .../Commands/BendingModelImportImages.php | 160 ++++ .../Commands/BendingProductImportLegacy.php | 200 +++++ .../Commands/GuiderailImportLegacy.php | 136 +++ .../Api/V1/GuiderailModelController.php | 89 ++ app/Http/Middleware/ApiKeyMiddleware.php | 5 + .../Resources/Api/V1/BendingItemResource.php | 2 + .../Api/V1/GuiderailModelResource.php | 71 ++ app/Services/GuiderailModelService.php | 120 +++ app/Swagger/v1/BendingItemApi.php | 138 +++ app/Swagger/v1/GuiderailModelApi.php | 125 +++ routes/api/v1/production.php | 11 + storage/api-docs/api-docs-v1.json | 814 +++++++++++++++++- 15 files changed, 2236 insertions(+), 6 deletions(-) create mode 100644 app/Console/Commands/BendingImportImages.php create mode 100644 app/Console/Commands/BendingModelImportAssemblyImages.php create mode 100644 app/Console/Commands/BendingModelImportImages.php create mode 100644 app/Console/Commands/BendingProductImportLegacy.php create mode 100644 app/Console/Commands/GuiderailImportLegacy.php create mode 100644 app/Http/Controllers/Api/V1/GuiderailModelController.php create mode 100644 app/Http/Resources/Api/V1/GuiderailModelResource.php create mode 100644 app/Services/GuiderailModelService.php create mode 100644 app/Swagger/v1/BendingItemApi.php create mode 100644 app/Swagger/v1/GuiderailModelApi.php diff --git a/app/Console/Commands/BendingImportImages.php b/app/Console/Commands/BendingImportImages.php new file mode 100644 index 00000000..6626ce58 --- /dev/null +++ b/app/Console/Commands/BendingImportImages.php @@ -0,0 +1,169 @@ +option('tenant_id'); + $dryRun = $this->option('dry-run'); + $sourceBase = rtrim($this->option('source'), '/'); + + $this->info('=== 레거시 절곡 이미지 → R2 마이그레이션 ==='); + $this->info('Source: '.$sourceBase); + $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); + $this->newLine(); + + // 1. BENDING 아이템에서 legacy_bending_num이 있는 것 조회 + $items = DB::table('items') + ->where('tenant_id', $tenantId) + ->where('item_category', 'BENDING') + ->whereNull('deleted_at') + ->get(['id', 'code', 'options']); + + $this->info("BENDING 아이템: {$items->count()}건"); + + // legacy_bending_num → chandj imgdata 매핑 + $chandjImages = DB::connection('chandj')->table('bending') + ->whereNull('is_deleted') + ->whereNotNull('imgdata') + ->where('imgdata', '!=', '') + ->pluck('imgdata', 'num'); + + $this->info("chandj 이미지: {$chandjImages->count()}건"); + $this->newLine(); + + foreach ($items as $item) { + $opts = json_decode($item->options ?? '{}', true) ?: []; + $legacyNum = $opts['legacy_bending_num'] ?? null; + + if (! $legacyNum || ! isset($chandjImages[$legacyNum])) { + continue; + } + + // 이미 파일이 연결되어 있으면 스킵 + $existingFile = File::where('tenant_id', $tenantId) + ->where('document_type', '1') + ->where('document_id', $item->id) + ->where('field_key', 'bending_diagram') + ->whereNull('deleted_at') + ->first(); + + if ($existingFile) { + $this->skipped++; + + continue; + } + + $imgFilename = $chandjImages[$legacyNum]; + $imageUrl = "{$sourceBase}/{$imgFilename}"; + + if ($dryRun) { + $this->line(" ✅ {$item->code} ← {$imgFilename}"); + $this->uploaded++; + + continue; + } + + // 이미지 다운로드 + try { + $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); + + if (! $response->successful()) { + $this->warn(" ❌ {$item->code}: HTTP {$response->status()} ({$imageUrl})"); + $this->failed++; + + continue; + } + + $imageContent = $response->body(); + $mimeType = $response->header('Content-Type', 'image/png'); + $extension = $this->getExtension($imgFilename, $mimeType); + + // R2 저장 + $storedName = bin2hex(random_bytes(8)).'.'.$extension; + $year = date('Y'); + $month = date('m'); + $directory = sprintf('%d/items/%s/%s', $tenantId, $year, $month); + $filePath = $directory.'/'.$storedName; + + Storage::disk('r2')->put($filePath, $imageContent); + + // files 테이블 저장 + $file = File::create([ + 'tenant_id' => $tenantId, + 'display_name' => $imgFilename, + 'stored_name' => $storedName, + 'file_path' => $filePath, + 'file_size' => strlen($imageContent), + 'mime_type' => $mimeType, + 'file_type' => 'image', + 'field_key' => 'bending_diagram', + 'document_id' => $item->id, + 'document_type' => '1', + 'is_temp' => false, + 'uploaded_by' => 1, + 'created_by' => 1, + ]); + + $this->line(" ✅ {$item->code} ← {$imgFilename} → file_id={$file->id}"); + $this->uploaded++; + } catch (\Exception $e) { + $this->error(" ❌ {$item->code}: {$e->getMessage()}"); + $this->failed++; + } + } + + $this->newLine(); + $this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + $this->info("업로드: {$this->uploaded}건 | 스킵(이미 있음): {$this->skipped}건 | 실패: {$this->failed}건"); + + if ($dryRun) { + $this->info('🔍 DRY-RUN 완료.'); + } + + return self::SUCCESS; + } + + private function getExtension(string $filename, string $mimeType): string + { + $ext = pathinfo($filename, PATHINFO_EXTENSION); + if ($ext) { + return strtolower($ext); + } + + return match ($mimeType) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + default => 'png', + }; + } +} diff --git a/app/Console/Commands/BendingImportLegacy.php b/app/Console/Commands/BendingImportLegacy.php index 6a1b6aff..733c0fe1 100644 --- a/app/Console/Commands/BendingImportLegacy.php +++ b/app/Console/Commands/BendingImportLegacy.php @@ -32,14 +32,14 @@ class BendingImportLegacy extends Command 'RE' => ['item_bending' => '가이드레일', 'itemName_like' => '%마감%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'], 'RM' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'], 'RC' => ['item_bending' => '가이드레일', 'itemName_like' => '%C형%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'], - 'RD' => ['item_bending' => '가이드레일', 'itemName_like' => '%D형%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'], + 'RD' => ['item_bending' => '가이드레일', 'itemName_like' => '%벽면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'], 'RT' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '130*75'], // 가이드레일 (측면) — 벽면과 같은 전개도 'SS' => ['same_as' => 'RS'], 'SU' => ['same_as' => 'RS'], 'SM' => ['same_as' => 'RM'], 'SC' => ['same_as' => 'RC'], - 'SD' => ['same_as' => 'RD'], + 'SD' => ['item_bending' => '가이드레일', 'itemName_like' => '%측면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*120'], 'ST' => ['same_as' => 'RT'], 'SE' => ['same_as' => 'RE'], // 하단마감재 @@ -231,13 +231,13 @@ private function matchKoreanPattern(string $code, $chandjRows): ?object ]); } - // BD-케이스-650*550 + // BD-케이스-650*550 → chandj에서 itemName에 "650*550" 포함된 전면판 매칭 if (preg_match('/^BD-케이스-(\d+)\*(\d+)$/', $code, $m)) { $spec = $m[1].'*'.$m[2]; return $chandjRows->first(function ($r) use ($spec) { - return $r->item_bending === '케이스' - && (str_contains($r->itemName, $spec) || $r->item_spec === $spec); + return (str_contains($r->itemName, $spec) || $r->item_spec === $spec) + && str_contains($r->itemName, '전면'); }); } diff --git a/app/Console/Commands/BendingModelImportAssemblyImages.php b/app/Console/Commands/BendingModelImportAssemblyImages.php new file mode 100644 index 00000000..7d9690c6 --- /dev/null +++ b/app/Console/Commands/BendingModelImportAssemblyImages.php @@ -0,0 +1,192 @@ +option('tenant_id'); + $dryRun = $this->option('dry-run'); + $sourceBase = rtrim($this->option('source'), '/'); + + $this->info('=== 결합형태 이미지 → R2 마이그레이션 ==='); + + // 3개 JSON 파일 순차 처리 + $jsonConfigs = [ + ['file' => 'guiderail/guiderail.json', 'category' => 'GUIDERAIL_MODEL', 'imageBase' => ''], + ['file' => 'shutterbox/shutterbox.json', 'category' => 'SHUTTERBOX_MODEL', 'imageBase' => ''], + ['file' => 'bottombar/bottombar.json', 'category' => 'BOTTOMBAR_MODEL', 'imageBase' => ''], + ]; + + $uploaded = 0; + $skipped = 0; + $failed = 0; + + foreach ($jsonConfigs as $jsonConfig) { + $jsonPath = base_path('../5130/' . $jsonConfig['file']); + if (! file_exists($jsonPath)) { + $resp = Http::withoutVerifying()->get("{$sourceBase}/{$jsonConfig['file']}"); + $assemblyData = $resp->successful() ? $resp->json() : []; + } else { + $assemblyData = json_decode(file_get_contents($jsonPath), true) ?: []; + } + + $this->info("--- {$jsonConfig['category']} ({$jsonConfig['file']}): " . count($assemblyData) . '건 ---'); + + foreach ($assemblyData as $entry) { + $imagePath = $entry['image'] ?? ''; + if (! $imagePath) { + continue; + } + + // SAM 코드 생성 (카테고리별) + $code = $this->buildCode($entry, $jsonConfig['category']); + if (! $code) { + continue; + } + + $samItem = DB::table('items') + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->where('item_category', $jsonConfig['category']) + ->whereNull('deleted_at') + ->first(['id', 'code', 'options']); + + if (! $samItem) { + $this->warn(" ⚠️ {$code}: SAM 모델 없음"); + $failed++; + + continue; + } + + // 이미 이미지 있으면 스킵 + $existing = File::where('tenant_id', $tenantId) + ->where('document_id', $samItem->id) + ->where('field_key', 'assembly_image') + ->whereNull('deleted_at') + ->first(); + + if ($existing) { + $skipped++; + + continue; + } + + $imageUrl = "{$sourceBase}{$imagePath}"; + + if ($dryRun) { + $this->line(" ✅ {$code} ← {$imagePath}"); + $uploaded++; + + continue; + } + + try { + $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); + + if (! $response->successful()) { + $this->warn(" ❌ {$code}: HTTP {$response->status()}"); + $failed++; + + continue; + } + + $content = $response->body(); + $ext = pathinfo($imagePath, PATHINFO_EXTENSION) ?: 'png'; + $storedName = bin2hex(random_bytes(8)).'.'.strtolower($ext); + $directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m')); + $filePath = $directory.'/'.$storedName; + + Storage::disk('r2')->put($filePath, $content); + + $file = File::create([ + 'tenant_id' => $tenantId, + 'display_name' => basename($imagePath), + 'stored_name' => $storedName, + 'file_path' => $filePath, + 'file_size' => strlen($content), + 'mime_type' => $response->header('Content-Type', 'image/png'), + 'file_type' => 'image', + 'field_key' => 'assembly_image', + 'document_id' => $samItem->id, + 'document_type' => '1', + 'is_temp' => false, + 'uploaded_by' => 1, + 'created_by' => 1, + ]); + + $this->line(" ✅ {$code} ← {$imagePath} → file_id={$file->id}"); + $uploaded++; + } catch (\Exception $e) { + $this->error(" ❌ {$code}: {$e->getMessage()}"); + $failed++; + } + } + + } // end foreach jsonConfigs + + $this->newLine(); + $this->info("업로드: {$uploaded}건 | 스킵: {$skipped}건 | 실패: {$failed}건"); + + return self::SUCCESS; + } + + private function buildCode(array $entry, string $category): ?string + { + if ($category === 'GUIDERAIL_MODEL') { + $modelName = $entry['model_name'] ?? ''; + $checkType = $entry['check_type'] ?? ''; + $finishType = $entry['finishing_type'] ?? ''; + if (! $modelName) { + return null; + } + $finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI'; + + return "GR-{$modelName}-{$checkType}-{$finish}"; + } + + if ($category === 'SHUTTERBOX_MODEL') { + $w = $entry['box_width'] ?? ''; + $h = $entry['box_height'] ?? ''; + $exit = $entry['exit_direction'] ?? ''; + $exitShort = match ($exit) { + '양면 점검구' => '양면', + '밑면 점검구' => '밑면', + '후면 점검구' => '후면', + default => $exit, + }; + + return "SB-{$w}*{$h}-{$exitShort}"; + } + + if ($category === 'BOTTOMBAR_MODEL') { + $modelName = $entry['model_name'] ?? ''; + $finishType = $entry['finishing_type'] ?? ''; + if (! $modelName) { + return null; + } + $finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI'; + + return "BB-{$modelName}-{$finish}"; + } + + return null; + } +} diff --git a/app/Console/Commands/BendingModelImportImages.php b/app/Console/Commands/BendingModelImportImages.php new file mode 100644 index 00000000..5d6d5627 --- /dev/null +++ b/app/Console/Commands/BendingModelImportImages.php @@ -0,0 +1,160 @@ +option('tenant_id'); + $dryRun = $this->option('dry-run'); + $sourceBase = rtrim($this->option('source'), '/'); + + $this->info('=== 절곡품 모델 부품 이미지 → R2 마이그레이션 ==='); + $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); + $this->newLine(); + + // chandj에서 원본 imgdata 조회 + $chandjTables = [ + 'GUIDERAIL_MODEL' => 'guiderail', + 'SHUTTERBOX_MODEL' => 'shutterbox', + 'BOTTOMBAR_MODEL' => 'bottombar', + ]; + + foreach ($chandjTables as $category => $table) { + $this->info("--- {$category} ({$table}) ---"); + + $chandjRows = DB::connection('chandj')->table($table)->whereNull('is_deleted')->get(); + $samItems = DB::table('items')->where('tenant_id', $tenantId) + ->where('item_category', $category)->whereNull('deleted_at') + ->get(['id', 'code', 'options']); + + // legacy_num → chandj row 매핑 + $chandjMap = []; + foreach ($chandjRows as $row) { + $chandjMap[$row->num] = $row; + } + + foreach ($samItems as $samItem) { + $opts = json_decode($samItem->options, true) ?? []; + $legacyNum = $opts['legacy_num'] ?? $opts['legacy_guiderail_num'] ?? null; + + if (! $legacyNum || ! isset($chandjMap[$legacyNum])) { + continue; + } + + $chandjRow = $chandjMap[$legacyNum]; + $chandjComps = json_decode($chandjRow->bending_components ?? '[]', true) ?: []; + $components = $opts['components'] ?? []; + $updated = false; + + foreach ($components as $idx => &$comp) { + // chandj component에서 imgdata 찾기 + $chandjComp = $chandjComps[$idx] ?? null; + $imgdata = $chandjComp['imgdata'] ?? null; + + if (! $imgdata || ! empty($comp['image_file_id'])) { + continue; + } + + $imageUrl = "{$sourceBase}/{$imgdata}"; + + if ($dryRun) { + $this->line(" ✅ {$samItem->code} #{$idx} ← {$imgdata}"); + $this->uploaded++; + $updated = true; + + continue; + } + + try { + $response = Http::withoutVerifying()->timeout(15)->get($imageUrl); + + if (! $response->successful()) { + $this->warn(" ❌ {$samItem->code} #{$idx}: HTTP {$response->status()}"); + $this->failed++; + + continue; + } + + $imageContent = $response->body(); + $extension = pathinfo($imgdata, PATHINFO_EXTENSION) ?: 'png'; + $storedName = bin2hex(random_bytes(8)).'.'.strtolower($extension); + $directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m')); + $filePath = $directory.'/'.$storedName; + + Storage::disk('r2')->put($filePath, $imageContent); + + $file = File::create([ + 'tenant_id' => $tenantId, + 'display_name' => $imgdata, + 'stored_name' => $storedName, + 'file_path' => $filePath, + 'file_size' => strlen($imageContent), + 'mime_type' => $response->header('Content-Type', 'image/png'), + 'file_type' => 'image', + 'field_key' => 'bending_component_image', + 'document_id' => $samItem->id, + 'document_type' => '1', + 'is_temp' => false, + 'uploaded_by' => 1, + 'created_by' => 1, + ]); + + $comp['image_file_id'] = $file->id; + $comp['imgdata'] = $imgdata; + $updated = true; + $this->uploaded++; + $this->line(" ✅ {$samItem->code} #{$idx} {$comp['itemName']} ← {$imgdata} → file_id={$file->id}"); + } catch (\Exception $e) { + $this->error(" ❌ {$samItem->code} #{$idx}: {$e->getMessage()}"); + $this->failed++; + } + } + unset($comp); + + // components 업데이트 + if ($updated && ! $dryRun) { + $opts['components'] = $components; + DB::table('items')->where('id', $samItem->id)->update([ + 'options' => json_encode($opts, JSON_UNESCAPED_UNICODE), + 'updated_at' => now(), + ]); + } + } + } + + $this->newLine(); + $this->info("업로드: {$this->uploaded}건 | 스킵: {$this->skipped}건 | 실패: {$this->failed}건"); + if ($dryRun) { + $this->info('🔍 DRY-RUN 완료.'); + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/BendingProductImportLegacy.php b/app/Console/Commands/BendingProductImportLegacy.php new file mode 100644 index 00000000..39110e1d --- /dev/null +++ b/app/Console/Commands/BendingProductImportLegacy.php @@ -0,0 +1,200 @@ +option('tenant_id'); + $dryRun = $this->option('dry-run'); + + $this->info('=== 케이스/하단마감재 모델 → SAM 임포트 ==='); + $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); + $this->newLine(); + + // 케이스 (shutterbox) + $this->info('--- 케이스 (shutterbox) ---'); + $cases = DB::connection('chandj')->table('shutterbox')->whereNull('is_deleted')->get(); + $this->info("chandj shutterbox: {$cases->count()}건"); + $caseCreated = $this->importItems($cases, 'SHUTTERBOX_MODEL', $tenantId, $dryRun); + + $this->newLine(); + + // 하단마감재 (bottombar) + $this->info('--- 하단마감재 (bottombar) ---'); + $bars = DB::connection('chandj')->table('bottombar')->whereNull('is_deleted')->get(); + $this->info("chandj bottombar: {$bars->count()}건"); + $barCreated = $this->importItems($bars, 'BOTTOMBAR_MODEL', $tenantId, $dryRun); + + $this->newLine(); + $this->info("결과: 케이스 {$caseCreated}건 + 하단마감재 {$barCreated}건"); + if ($dryRun) { + $this->info('🔍 DRY-RUN 완료.'); + } + + return self::SUCCESS; + } + + private function importItems($rows, string $category, int $tenantId, bool $dryRun): int + { + $created = 0; + $skipped = 0; + + foreach ($rows as $row) { + $code = $this->buildCode($row, $category); + $name = $this->buildName($row, $category); + + $existing = DB::table('items') + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->whereNull('deleted_at') + ->first(); + + if ($existing) { + $skipped++; + + continue; + } + + $components = $this->convertComponents(json_decode($row->bending_components ?? '[]', true) ?: []); + $materialSummary = json_decode($row->material_summary ?? '{}', true) ?: []; + + $options = $this->buildOptions($row, $category, $components, $materialSummary); + + if (! $dryRun) { + DB::table('items')->insert([ + 'tenant_id' => $tenantId, + 'code' => $code, + 'name' => $name, + 'item_type' => 'FG', + 'item_category' => $category, + 'unit' => 'SET', + 'options' => json_encode($options, JSON_UNESCAPED_UNICODE), + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + $created++; + $this->line(" ✅ {$code} ({$name}) — 부품 ".count($components).'개'); + } + + $this->info(" 생성: {$created}건 | 스킵: {$skipped}건"); + + return $created; + } + + private function buildCode(object $row, string $category): string + { + if ($category === 'SHUTTERBOX_MODEL') { + $size = ($row->box_width ?? ''). + '*'.($row->box_height ?? ''); + $exit = match ($row->exit_direction ?? '') { + '양면 점검구' => '양면', + '밑면 점검구' => '밑면', + '후면 점검구' => '후면', + default => $row->exit_direction ?? '', + }; + + return "SB-{$size}-{$exit}"; + } + + // BOTTOMBAR_MODEL + $model = $row->model_name ?? 'UNKNOWN'; + $finish = str_contains($row->finishing_type ?? '', 'SUS') ? 'SUS' : 'EGI'; + + return "BB-{$model}-{$finish}"; + } + + private function buildName(object $row, string $category): string + { + if ($category === 'SHUTTERBOX_MODEL') { + return "케이스 {$row->box_width}*{$row->box_height} {$row->exit_direction}"; + } + + return "하단마감재 {$row->model_name} {$row->firstitem}"; + } + + private function buildOptions(object $row, string $category, array $components, array $materialSummary): array + { + $base = [ + 'author' => $row->author ?? null, + 'registration_date' => $row->registration_date ?? null, + 'search_keyword' => $row->search_keyword ?? null, + 'memo' => $row->remark ?? null, + 'components' => $components, + 'material_summary' => $materialSummary, + 'source' => 'chandj_'.(strtolower($category)), + 'legacy_num' => $row->num, + ]; + + if ($category === 'SHUTTERBOX_MODEL') { + return array_merge($base, [ + 'box_width' => (int) ($row->box_width ?? 0), + 'box_height' => (int) ($row->box_height ?? 0), + 'exit_direction' => $row->exit_direction ?? null, + 'front_bottom_width' => $row->front_bottom_width ?? null, + 'rail_width' => $row->rail_width ?? null, + ]); + } + + // BOTTOMBAR_MODEL + return array_merge($base, [ + 'model_name' => $row->model_name ?? null, + 'item_sep' => $row->firstitem ?? null, + 'model_UA' => $row->model_UA ?? null, + 'finishing_type' => $row->finishing_type ?? null, + 'bar_width' => $row->bar_width ?? null, + 'bar_height' => $row->bar_height ?? null, + ]); + } + + private function convertComponents(array $legacyComps): array + { + return array_map(function ($c, $idx) { + $inputs = $c['inputList'] ?? []; + $rates = $c['bendingrateList'] ?? []; + $sums = $c['sumList'] ?? []; + $colors = $c['colorList'] ?? []; + $angles = $c['AList'] ?? []; + + $bendingData = []; + for ($i = 0; $i < count($inputs); $i++) { + $bendingData[] = [ + 'no' => $i + 1, + 'input' => (float) ($inputs[$i] ?? 0), + 'rate' => (string) ($rates[$i] ?? ''), + 'sum' => (float) ($sums[$i] ?? 0), + 'color' => (bool) ($colors[$i] ?? false), + 'aAngle' => (bool) ($angles[$i] ?? false), + ]; + } + + $lastSum = ! empty($sums) ? (float) end($sums) : ($c['widthsum'] ?? 0); + + return [ + 'orderNumber' => $idx + 1, + 'itemName' => $c['itemName'] ?? '', + 'material' => $c['material'] ?? '', + 'quantity' => (int) ($c['quantity'] ?? 1), + 'width_sum' => (float) $lastSum, + 'bendingData' => $bendingData, + 'legacy_bending_num' => $c['source_num'] ?? $c['num'] ?? null, + ]; + }, $legacyComps, array_keys($legacyComps)); + } +} diff --git a/app/Console/Commands/GuiderailImportLegacy.php b/app/Console/Commands/GuiderailImportLegacy.php new file mode 100644 index 00000000..f3d92c35 --- /dev/null +++ b/app/Console/Commands/GuiderailImportLegacy.php @@ -0,0 +1,136 @@ +option('tenant_id'); + $dryRun = $this->option('dry-run'); + + $this->info('=== chandj guiderail → SAM 임포트 ==='); + $this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE')); + + $rows = DB::connection('chandj')->table('guiderail')->whereNull('is_deleted')->get(); + $this->info("chandj guiderail: {$rows->count()}건"); + + $created = 0; + $skipped = 0; + + foreach ($rows as $row) { + $finish = str_contains($row->finishing_type ?? '', 'SUS') ? 'SUS' : 'EGI'; + $code = 'GR-'.($row->model_name ?? 'UNKNOWN').'-'.($row->check_type ?? '').'-'.$finish; + $name = implode(' ', array_filter([$row->model_name, $row->check_type, $row->finishing_type])); + + // 중복 확인 + $existing = DB::table('items') + ->where('tenant_id', $tenantId) + ->where('code', $code) + ->whereNull('deleted_at') + ->first(); + + if ($existing) { + $skipped++; + continue; + } + + // components 변환 + $legacyComps = json_decode($row->bending_components ?? '[]', true) ?: []; + $components = array_map(fn ($c) => $this->convertComponent($c), $legacyComps); + + $materialSummary = json_decode($row->material_summary ?? '{}', true) ?: []; + + $options = [ + 'model_name' => $row->model_name, + 'check_type' => $row->check_type, + 'rail_width' => (int) $row->rail_width, + 'rail_length' => (int) $row->rail_length, + 'finishing_type' => $row->finishing_type, + 'item_sep' => $row->firstitem, + 'model_UA' => $row->model_UA, + 'search_keyword' => $row->search_keyword, + 'author' => $row->author, + 'registration_date' => $row->registration_date, + 'memo' => $row->remark, + 'components' => $components, + 'material_summary' => $materialSummary, + 'source' => 'chandj_guiderail', + 'legacy_guiderail_num' => $row->num, + ]; + + if (! $dryRun) { + DB::table('items')->insert([ + 'tenant_id' => $tenantId, + 'code' => $code, + 'name' => $name, + 'item_type' => 'FG', + 'item_category' => 'GUIDERAIL_MODEL', + 'unit' => 'SET', + 'options' => json_encode($options, JSON_UNESCAPED_UNICODE), + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + $created++; + $this->line(" ✅ {$code} ({$name}) — {$row->firstitem}/{$row->model_UA} — 부품 ".count($components).'개'); + } + + $this->newLine(); + $this->info("생성: {$created}건 | 스킵(중복): {$skipped}건"); + + if ($dryRun) { + $this->info('🔍 DRY-RUN 완료.'); + } + + return self::SUCCESS; + } + + private function convertComponent(array $c): array + { + $inputs = $c['inputList'] ?? []; + $rates = $c['bendingrateList'] ?? []; + $sums = $c['sumList'] ?? []; + $colors = $c['colorList'] ?? []; + $angles = $c['AList'] ?? []; + + // bendingData 형식으로 변환 + $bendingData = []; + for ($i = 0; $i < count($inputs); $i++) { + $bendingData[] = [ + 'no' => $i + 1, + 'input' => (float) ($inputs[$i] ?? 0), + 'rate' => (string) ($rates[$i] ?? ''), + 'sum' => (float) ($sums[$i] ?? 0), + 'color' => (bool) ($colors[$i] ?? false), + 'aAngle' => (bool) ($angles[$i] ?? false), + ]; + } + + $lastSum = ! empty($sums) ? (float) end($sums) : 0; + + return [ + 'orderNumber' => $c['orderNumber'] ?? null, + 'itemName' => $c['itemName'] ?? '', + 'material' => $c['material'] ?? '', + 'quantity' => (int) ($c['quantity'] ?? 1), + 'width_sum' => $lastSum, + 'bendingData' => $bendingData, + 'legacy_bending_num' => $c['num'] ?? null, + ]; + } +} diff --git a/app/Http/Controllers/Api/V1/GuiderailModelController.php b/app/Http/Controllers/Api/V1/GuiderailModelController.php new file mode 100644 index 00000000..a5cb2728 --- /dev/null +++ b/app/Http/Controllers/Api/V1/GuiderailModelController.php @@ -0,0 +1,89 @@ +bound('tenant_id') || ! app('tenant_id')) { + $tenantId = (int) ($request->header('X-TENANT-ID') ?: 287); + app()->instance('tenant_id', $tenantId); + } + if (! app()->bound('api_user') || ! app('api_user')) { + app()->instance('api_user', 1); + } + } + + public function index(Request $request): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle(function () use ($request) { + $params = $request->only(['item_category', 'item_sep', 'model_UA', 'check_type', 'model_name', 'search', 'page', 'size']); + $paginator = $this->service->list($params); + $paginator->getCollection()->transform(fn ($item) => (new GuiderailModelResource($item))->resolve()); + + return $paginator; + }, __('message.fetched')); + } + + public function filters(Request $request): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle( + fn () => $this->service->filters(), + __('message.fetched') + ); + } + + public function show(Request $request, int $id): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle( + fn () => new GuiderailModelResource($this->service->find($id)), + __('message.fetched') + ); + } + + public function store(Request $request): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle( + fn () => new GuiderailModelResource($this->service->create($request->all())), + __('message.created') + ); + } + + public function update(Request $request, int $id): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle( + fn () => new GuiderailModelResource($this->service->update($id, $request->all())), + __('message.updated') + ); + } + + public function destroy(Request $request, int $id): JsonResponse + { + $this->ensureContext($request); + + return ApiResponse::handle( + fn () => $this->service->delete($id), + __('message.deleted') + ); + } +} diff --git a/app/Http/Middleware/ApiKeyMiddleware.php b/app/Http/Middleware/ApiKeyMiddleware.php index 3c1ce956..70a5b4b6 100644 --- a/app/Http/Middleware/ApiKeyMiddleware.php +++ b/app/Http/Middleware/ApiKeyMiddleware.php @@ -127,6 +127,11 @@ public function handle(Request $request, Closure $next) 'api/v1/app/*', // 앱 버전 확인/다운로드 (API Key만 필요) 'api/v1/bending-items', // 절곡품 목록 (MNG에서 API Key만으로 접근) 'api/v1/bending-items/*', // 절곡품 상세/필터 + 'api/v1/guiderail-models', // 절곡품 모델 목록 + 'api/v1/guiderail-models/*', // 절곡품 모델 상세/필터 + 'api/v1/items/*/files', // 품목 파일 업로드/조회 + 'api/v1/files/*/view', // 파일 인라인 보기 (MNG 이미지 표시) + 'api/v1/files/*/download', // 파일 다운로드 ]; // 현재 라우트 확인 (경로 또는 이름) diff --git a/app/Http/Resources/Api/V1/BendingItemResource.php b/app/Http/Resources/Api/V1/BendingItemResource.php index 76336e58..e2f78016 100644 --- a/app/Http/Resources/Api/V1/BendingItemResource.php +++ b/app/Http/Resources/Api/V1/BendingItemResource.php @@ -41,6 +41,8 @@ public function toArray(Request $request): array 'prefix' => $this->getOption('prefix'), 'length_code' => $this->getOption('length_code'), 'length_mm' => $this->getOption('length_mm'), + // 추적 + 'legacy_bending_num' => $this->getOption('legacy_bending_num'), // 계산값 'width_sum' => $this->getWidthSum(), 'bend_count' => $this->getBendCount(), diff --git a/app/Http/Resources/Api/V1/GuiderailModelResource.php b/app/Http/Resources/Api/V1/GuiderailModelResource.php new file mode 100644 index 00000000..5e54912f --- /dev/null +++ b/app/Http/Resources/Api/V1/GuiderailModelResource.php @@ -0,0 +1,71 @@ +getOption('components', []); + $materialSummary = $this->getOption('material_summary'); + + // material_summary가 없으면 components에서 계산 + if (empty($materialSummary) && ! empty($components)) { + $materialSummary = $this->calcMaterialSummary($components); + } + + return [ + 'id' => $this->id, + 'code' => $this->code, + 'name' => $this->name, + 'item_type' => $this->item_type, + 'item_category' => $this->item_category, + 'is_active' => $this->is_active, + // 모델 속성 + 'model_name' => $this->getOption('model_name'), + 'check_type' => $this->getOption('check_type'), + 'rail_width' => $this->getOption('rail_width'), + 'rail_length' => $this->getOption('rail_length'), + 'finishing_type' => $this->getOption('finishing_type'), + 'item_sep' => $this->getOption('item_sep'), + 'model_UA' => $this->getOption('model_UA'), + 'search_keyword' => $this->getOption('search_keyword'), + 'author' => $this->getOption('author'), + 'memo' => $this->getOption('memo'), + 'registration_date' => $this->getOption('registration_date'), + // 케이스(SHUTTERBOX_MODEL) 전용 + 'exit_direction' => $this->getOption('exit_direction'), + 'front_bottom_width' => $this->getOption('front_bottom_width'), + 'box_width' => $this->getOption('box_width'), + 'box_height' => $this->getOption('box_height'), + // 하단마감재(BOTTOMBAR_MODEL) 전용 + 'bar_width' => $this->getOption('bar_width'), + 'bar_height' => $this->getOption('bar_height'), + // 부품 조합 + 'components' => $components, + 'material_summary' => $materialSummary, + 'component_count' => count($components), + // 메타 + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } + + private function calcMaterialSummary(array $components): array + { + $summary = []; + foreach ($components as $comp) { + $material = $comp['material'] ?? null; + $widthSum = $comp['width_sum'] ?? 0; + $qty = $comp['quantity'] ?? 1; + if ($material && $widthSum) { + $summary[$material] = ($summary[$material] ?? 0) + ($widthSum * $qty); + } + } + + return $summary; + } +} diff --git a/app/Services/GuiderailModelService.php b/app/Services/GuiderailModelService.php new file mode 100644 index 00000000..a8cb639c --- /dev/null +++ b/app/Services/GuiderailModelService.php @@ -0,0 +1,120 @@ +when($params['item_category'] ?? null, fn ($q, $v) => $q->where('item_category', $v)) + ->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('options->item_sep', $v)) + ->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('options->model_UA', $v)) + ->when($params['check_type'] ?? null, fn ($q, $v) => $q->where('options->check_type', $v)) + ->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('options->model_name', $v)) + ->when($params['search'] ?? null, fn ($q, $v) => $q->where( + fn ($q2) => $q2 + ->where('name', 'like', "%{$v}%") + ->orWhere('code', 'like', "%{$v}%") + ->orWhere('options->model_name', 'like', "%{$v}%") + ->orWhere('options->search_keyword', 'like', "%{$v}%") + )) + ->orderBy('code') + ->paginate($params['size'] ?? 50); + } + + public function filters(): array + { + $items = Item::whereIn('item_category', self::CATEGORIES)->select('options')->get(); + + return [ + 'item_sep' => $items->pluck('options.item_sep')->filter()->unique()->sort()->values(), + 'model_UA' => $items->pluck('options.model_UA')->filter()->unique()->sort()->values(), + 'check_type' => $items->pluck('options.check_type')->filter()->unique()->sort()->values(), + 'model_name' => $items->pluck('options.model_name')->filter()->unique()->sort()->values(), + 'finishing_type' => $items->pluck('options.finishing_type')->filter()->unique()->sort()->values(), + ]; + } + + public function find(int $id): Item + { + return Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id); + } + + public function create(array $data): Item + { + $options = $this->buildOptions($data); + + return Item::create([ + 'tenant_id' => $this->tenantId(), + 'item_type' => 'FG', + 'item_category' => $data['item_category'] ?? 'GUIDERAIL_MODEL', + 'code' => $data['code'], + 'name' => $data['name'], + 'unit' => 'SET', + 'options' => $options, + 'is_active' => true, + 'created_by' => $this->apiUserId(), + ]); + } + + public function update(int $id, array $data): Item + { + $item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id); + + if (isset($data['code'])) { + $item->code = $data['code']; + } + if (isset($data['name'])) { + $item->name = $data['name']; + } + + foreach (self::OPTION_KEYS as $key) { + if (array_key_exists($key, $data)) { + $item->setOption($key, $data[$key]); + } + } + + $item->updated_by = $this->apiUserId(); + $item->save(); + + return $item; + } + + public function delete(int $id): bool + { + $item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id); + $item->deleted_by = $this->apiUserId(); + $item->save(); + + return $item->delete(); + } + + private function buildOptions(array $data): array + { + $options = []; + foreach (self::OPTION_KEYS as $key) { + if (isset($data[$key])) { + $options[$key] = $data[$key]; + } + } + + return $options; + } + + private const OPTION_KEYS = [ + 'model_name', 'check_type', 'rail_width', 'rail_length', + 'finishing_type', 'item_sep', 'model_UA', 'search_keyword', + 'author', 'memo', 'registration_date', + 'components', 'material_summary', + // 케이스(SHUTTERBOX_MODEL) 전용 + 'exit_direction', 'front_bottom_width', 'box_width', 'box_height', + // 하단마감재(BOTTOMBAR_MODEL) 전용 + 'bar_width', 'bar_height', + ]; +} diff --git a/app/Swagger/v1/BendingItemApi.php b/app/Swagger/v1/BendingItemApi.php new file mode 100644 index 00000000..bb794e38 --- /dev/null +++ b/app/Swagger/v1/BendingItemApi.php @@ -0,0 +1,138 @@ +whereNumber('id')->name('v1.bending-items.destroy'); }); +// Guiderail Model API (절곡품 모델 관리) +Route::prefix('guiderail-models')->group(function () { + Route::get('', [GuiderailModelController::class, 'index'])->name('v1.guiderail-models.index'); + Route::get('/filters', [GuiderailModelController::class, 'filters'])->name('v1.guiderail-models.filters'); + Route::post('', [GuiderailModelController::class, 'store'])->name('v1.guiderail-models.store'); + Route::get('/{id}', [GuiderailModelController::class, 'show'])->whereNumber('id')->name('v1.guiderail-models.show'); + Route::put('/{id}', [GuiderailModelController::class, 'update'])->whereNumber('id')->name('v1.guiderail-models.update'); + Route::delete('/{id}', [GuiderailModelController::class, 'destroy'])->whereNumber('id')->name('v1.guiderail-models.destroy'); +}); + // Production Order API (생산지시 조회) Route::prefix('production-orders')->group(function () { Route::get('', [ProductionOrderController::class, 'index'])->name('v1.production-orders.index'); diff --git a/storage/api-docs/api-docs-v1.json b/storage/api-docs/api-docs-v1.json index cb709d1f..602224c7 100755 --- a/storage/api-docs/api-docs-v1.json +++ b/storage/api-docs/api-docs-v1.json @@ -11,7 +11,7 @@ "servers": [ { "url": "https://api.sam.kr/", - "description": "SAM관리시스템 API 서버" + "description": "SAM API 서버" } ], "paths": { @@ -9436,6 +9436,256 @@ ] } }, + "/api/v1/bending-items": { + "get": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 목록 조회", + "operationId": "c497d5bfebed3fb08cd4d5be9224c795", + "parameters": [ + { + "name": "item_sep", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "스크린", + "철재" + ] + } + }, + { + "name": "item_bending", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "material", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "model_UA", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "인정", + "비인정" + ] + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + }, + "post": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 등록", + "operationId": "8c9e50c74611ec24e621ceb665031059", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": [ + "code", + "name", + "item_name", + "item_sep", + "item_bending", + "material" + ], + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "item_name": { + "type": "string" + }, + "item_sep": { + "type": "string", + "enum": [ + "스크린", + "철재" + ] + }, + "item_bending": { + "type": "string" + }, + "material": { + "type": "string" + }, + "bendingData": { + "type": "array", + "items": { + "type": "object" + }, + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "성공" + } + } + } + }, + "/api/v1/bending-items/filters": { + "get": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 필터 옵션 조회", + "operationId": "f5dd325adc791e1b8cab40b9fa2fb77d", + "responses": { + "200": { + "description": "성공" + } + } + } + }, + "/api/v1/bending-items/{id}": { + "get": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 상세 조회", + "operationId": "d364f4d4cf76bcce7167561b73216382", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + }, + "put": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 수정", + "operationId": "9ce1223d6528c5f926f6726c2b1f65f2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "memo": { + "type": "string" + }, + "bendingData": { + "type": "array", + "items": { + "type": "object" + }, + "nullable": true + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "성공" + } + } + }, + "delete": { + "tags": [ + "BendingItem" + ], + "summary": "절곡품 삭제", + "operationId": "46dcd93439505ae5ffb1dd2c8c1e5685", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + } + }, "/api/v1/biddings": { "get": { "tags": [ @@ -26052,6 +26302,243 @@ ] } }, + "/api/v1/guiderail-models": { + "get": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 목록 (가이드레일/케이스/하단마감재 통합)", + "operationId": "f06bc491c0801ca7ec8676381ac5dc53", + "parameters": [ + { + "name": "item_category", + "in": "query", + "description": "필수: 카테고리 구분 (없으면 60건 전부 반환)", + "required": true, + "schema": { + "type": "string", + "enum": [ + "GUIDERAIL_MODEL", + "SHUTTERBOX_MODEL", + "BOTTOMBAR_MODEL" + ] + } + }, + { + "name": "item_sep", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "model_UA", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "check_type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "model_name", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + }, + "post": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 등록", + "operationId": "6b4f759362fa6db48357bab171175f4f", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": [ + "code", + "name" + ], + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "model_name": { + "type": "string" + }, + "check_type": { + "type": "string" + }, + "components": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "성공" + } + } + } + }, + "/api/v1/guiderail-models/filters": { + "get": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 필터 옵션", + "operationId": "85422fba3619fcb7fac3db2a1dc20bb1", + "responses": { + "200": { + "description": "성공" + } + } + } + }, + "/api/v1/guiderail-models/{id}": { + "get": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 상세 (부품 조합 포함)", + "operationId": "6c24a276c41bef2fb211b4d6d5a3c45a", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + }, + "put": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 수정", + "operationId": "0284e070bc05779f643d8579c584de23", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "model_name": { + "type": "string" + }, + "components": { + "type": "array", + "items": { + "type": "object" + } + }, + "material_summary": { + "type": "object" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "성공" + } + } + }, + "delete": { + "tags": [ + "GuiderailModel" + ], + "summary": "절곡품 모델 삭제", + "operationId": "3468308d63c19adb4325812d9636bf5a", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "성공" + } + } + } + }, "/api/v1/internal/exchange-token": { "post": { "tags": [ @@ -64189,6 +64676,177 @@ }, "type": "object" }, + "BendingItem": { + "properties": { + "id": { + "type": "integer", + "example": 15862 + }, + "code": { + "type": "string", + "example": "BD-BE-30" + }, + "name": { + "type": "string", + "example": "하단마감재(스크린) EGI 3000mm" + }, + "item_type": { + "type": "string", + "example": "PT" + }, + "item_category": { + "type": "string", + "example": "BENDING" + }, + "unit": { + "type": "string", + "example": "EA" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "item_name": { + "type": "string", + "example": "하단마감재" + }, + "item_sep": { + "type": "string", + "enum": [ + "스크린", + "철재" + ] + }, + "item_bending": { + "type": "string", + "example": "하단마감재" + }, + "item_spec": { + "type": "string", + "example": "60*40", + "nullable": true + }, + "material": { + "type": "string", + "example": "EGI 1.55T" + }, + "model_name": { + "type": "string", + "example": "KSS01", + "nullable": true + }, + "model_UA": { + "type": "string", + "enum": [ + "인정", + "비인정" + ], + "nullable": true + }, + "search_keyword": { + "type": "string", + "nullable": true + }, + "rail_width": { + "type": "integer", + "nullable": true + }, + "registration_date": { + "type": "string", + "format": "date", + "nullable": true + }, + "author": { + "type": "string", + "nullable": true + }, + "memo": { + "type": "string", + "nullable": true + }, + "exit_direction": { + "type": "string", + "nullable": true + }, + "front_bottom_width": { + "type": "integer", + "nullable": true + }, + "box_width": { + "type": "integer", + "nullable": true + }, + "box_height": { + "type": "integer", + "nullable": true + }, + "bendingData": { + "type": "array", + "items": { + "properties": { + "no": { + "type": "integer" + }, + "input": { + "type": "number" + }, + "rate": { + "type": "string" + }, + "sum": { + "type": "number" + }, + "color": { + "type": "boolean" + }, + "aAngle": { + "type": "boolean" + } + }, + "type": "object" + }, + "nullable": true + }, + "prefix": { + "type": "string", + "example": "BE", + "nullable": true + }, + "length_code": { + "type": "string", + "example": "30", + "nullable": true + }, + "length_mm": { + "type": "integer", + "example": 3000, + "nullable": true + }, + "legacy_bending_num": { + "description": "레거시 chandj bending.num (운영 전 삭제 예정)", + "type": "integer", + "nullable": true + }, + "width_sum": { + "type": "integer", + "example": 193, + "nullable": true + }, + "bend_count": { + "type": "integer", + "example": 5 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "type": "object" + }, "Bidding": { "properties": { "id": { @@ -73925,6 +74583,152 @@ }, "type": "object" }, + "GuiderailModel": { + "properties": { + "id": { + "type": "integer" + }, + "code": { + "type": "string", + "example": "GR-KSS01-벽면형-SUS" + }, + "name": { + "type": "string", + "example": "KSS01 벽면형 SUS마감" + }, + "item_category": { + "type": "string", + "enum": [ + "GUIDERAIL_MODEL", + "SHUTTERBOX_MODEL", + "BOTTOMBAR_MODEL" + ] + }, + "model_name": { + "type": "string", + "example": "KSS01", + "nullable": true + }, + "check_type": { + "type": "string", + "enum": [ + "벽면형", + "측면형" + ], + "nullable": true + }, + "rail_width": { + "type": "integer", + "example": 70, + "nullable": true + }, + "rail_length": { + "type": "integer", + "example": 120, + "nullable": true + }, + "finishing_type": { + "type": "string", + "enum": [ + "SUS마감", + "EGI마감" + ], + "nullable": true + }, + "item_sep": { + "type": "string", + "enum": [ + "스크린", + "철재" + ], + "nullable": true + }, + "model_UA": { + "type": "string", + "enum": [ + "인정", + "비인정" + ], + "nullable": true + }, + "exit_direction": { + "description": "케이스 점검구 방향", + "type": "string", + "nullable": true + }, + "front_bottom_width": { + "description": "케이스 전면밑", + "type": "integer", + "nullable": true + }, + "box_width": { + "description": "케이스 너비", + "type": "integer", + "nullable": true + }, + "box_height": { + "description": "케이스 높이", + "type": "integer", + "nullable": true + }, + "bar_width": { + "description": "하단마감재 폭", + "type": "integer", + "nullable": true + }, + "bar_height": { + "description": "하단마감재 높이", + "type": "integer", + "nullable": true + }, + "components": { + "type": "array", + "items": { + "properties": { + "orderNumber": { + "type": "integer" + }, + "itemName": { + "type": "string" + }, + "material": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "width_sum": { + "type": "number" + }, + "image_file_id": { + "description": "부품 이미지 → /api/v1/files/{id}/view", + "type": "integer", + "nullable": true + }, + "bendingData": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "type": "object" + } + }, + "material_summary": { + "type": "object", + "example": { + "SUS 1.2T": 406, + "EGI 1.55T": 398 + } + }, + "component_count": { + "type": "integer", + "example": 4 + } + }, + "type": "object" + }, "ExchangeTokenRequest": { "required": [ "user_id", @@ -93801,6 +94605,10 @@ "name": "BarobillSettings", "description": "바로빌 설정 관리" }, + { + "name": "BendingItem", + "description": "절곡품 기초관리" + }, { "name": "Bidding", "description": "입찰관리 API" @@ -93905,6 +94713,10 @@ "name": "Folder", "description": "폴더 관리" }, + { + "name": "GuiderailModel", + "description": "절곡품 모델 관리 (가이드레일/케이스/하단마감재)" + }, { "name": "ItemMaster", "description": "품목기준관리 API"