feat: [bending] 절곡품 전용 테이블 분리 API
- bending_items 전용 테이블 생성 (items.options → 정규 컬럼 승격) - bending_models 전용 테이블 생성 (가이드레일/케이스/하단마감재 통합) - bending_data JSON 통합 (별도 테이블 → bending_items.bending_data 컬럼) - bending_item_mappings 테이블 DROP (bending_items.code에 흡수) - BendingItemService/BendingCodeService → BendingItem 모델 전환 - GuiderailModelService component 이미지 자동 복사 - ItemsFileController bending_items/bending_models 폴백 지원 - Swagger 스키마 업데이트
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-03-17 14:05:31
|
||||
> **자동 생성**: 2026-03-19 16:29:38
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -88,6 +88,17 @@ ### hometax_invoice_journals
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
- **invoice()**: belongsTo → `hometax_invoices`
|
||||
|
||||
### bending_data_rows
|
||||
**모델**: `App\Models\BendingDataRow`
|
||||
|
||||
- **bendingItem()**: belongsTo → `bending_items`
|
||||
|
||||
### bending_items
|
||||
**모델**: `App\Models\BendingItem`
|
||||
|
||||
- **bendingData()**: hasMany → `bending_data`
|
||||
- **files()**: hasMany → `files`
|
||||
|
||||
### biddings
|
||||
**모델**: `App\Models\Bidding\Bidding`
|
||||
|
||||
@@ -723,11 +734,6 @@ ### process_steps
|
||||
|
||||
- **process()**: belongsTo → `processes`
|
||||
|
||||
### bending_item_mappings
|
||||
**모델**: `App\Models\Production\BendingItemMapping`
|
||||
|
||||
- **item()**: belongsTo → `items`
|
||||
|
||||
### work_orders
|
||||
**모델**: `App\Models\Production\WorkOrder`
|
||||
|
||||
|
||||
334
app/Console/Commands/BendingCleanReimport.php
Normal file
334
app/Console/Commands/BendingCleanReimport.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
// bending_data는 bending_items.bending_data JSON 컬럼에 저장
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* 클린 재이관: bending_items/bending_data 전체 삭제 → chandj.bending 직접 이관
|
||||
* 기존 R2 파일도 삭제 처리
|
||||
*
|
||||
* 실행: php artisan bending:clean-reimport [--dry-run] [--tenant_id=287]
|
||||
*/
|
||||
class BendingCleanReimport extends Command
|
||||
{
|
||||
protected $signature = 'bending:clean-reimport
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 실행하지 않고 미리보기만}
|
||||
{--legacy-img-path=/tmp/bending_img : 레거시 이미지 경로}';
|
||||
|
||||
protected $description = 'bending_items 클린 재이관 (chandj.bending 직접)';
|
||||
|
||||
private int $tenantId;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$legacyImgPath = $this->option('legacy-img-path');
|
||||
|
||||
// 1. 현재 상태
|
||||
$biCount = BendingItem::where('tenant_id', $this->tenantId)->count();
|
||||
$bdCount = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('bending_data')->count();
|
||||
$fileCount = File::where('field_key', 'bending_diagram')
|
||||
->where(function ($q) {
|
||||
$q->where('document_type', 'bending_item')
|
||||
->orWhere('document_type', '1');
|
||||
})->count();
|
||||
|
||||
$this->info("현재: bending_items={$biCount}, bending_data={$bdCount}, files={$fileCount}");
|
||||
|
||||
// chandj 유효 건수
|
||||
$chandjRows = DB::connection('chandj')->table('bending')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('is_deleted')->orWhere('is_deleted', 0);
|
||||
})
|
||||
->orderBy('num')
|
||||
->get();
|
||||
|
||||
$this->info("chandj 이관 대상: {$chandjRows->count()}건");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->preview($chandjRows);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (! $this->confirm("기존 데이터 전체 삭제 후 chandj에서 재이관합니다. 계속?")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($chandjRows) {
|
||||
// 2. 기존 파일 DB 레코드만 삭제 (R2 파일은 유지)
|
||||
$this->deleteFileRecords();
|
||||
|
||||
// 3. 기존 데이터 삭제
|
||||
BendingItem::where('tenant_id', $this->tenantId)->forceDelete();
|
||||
$this->info("기존 데이터 삭제 완료");
|
||||
|
||||
// 4. chandj에서 직접 이관
|
||||
$success = 0;
|
||||
$bdTotal = 0;
|
||||
|
||||
foreach ($chandjRows as $row) {
|
||||
try {
|
||||
$bi = $this->importItem($row);
|
||||
$bd = $this->importBendingData($bi, $row);
|
||||
$bdTotal += $bd;
|
||||
$success++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" ❌ #{$row->num} {$row->itemName}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("이관 완료: {$success}/{$chandjRows->count()}건, 전개도 {$bdTotal}행");
|
||||
});
|
||||
|
||||
// 5. 이미지 이관
|
||||
$this->importImages($legacyImgPath);
|
||||
|
||||
// 6. 최종 검증
|
||||
$this->verify();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function importItem(object $row): BendingItem
|
||||
{
|
||||
$code = $this->generateCode($row);
|
||||
|
||||
$bi = BendingItem::create([
|
||||
'tenant_id' => $this->tenantId,
|
||||
'code' => $code,
|
||||
'legacy_code' => "CHANDJ-{$row->num}",
|
||||
'legacy_bending_id' => $row->num,
|
||||
'item_name' => $row->itemName ?: "부품#{$row->num}",
|
||||
'item_sep' => $this->clean($row->item_sep),
|
||||
'item_bending' => $this->clean($row->item_bending),
|
||||
'material' => $this->clean($row->material),
|
||||
'item_spec' => $this->clean($row->item_spec),
|
||||
'model_name' => $this->clean($row->model_name ?? null),
|
||||
'model_UA' => $this->clean($row->model_UA ?? null),
|
||||
'rail_width' => $this->toNum($row->rail_width ?? null),
|
||||
'exit_direction' => $this->clean($row->exit_direction ?? null),
|
||||
'box_width' => $this->toNum($row->box_width ?? null),
|
||||
'box_height' => $this->toNum($row->box_height ?? null),
|
||||
'front_bottom' => $this->toNum($row->front_bottom_width ?? null),
|
||||
'options' => $this->buildOptions($row),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
|
||||
$this->line(" ✅ #{$row->num} → {$bi->id} ({$row->itemName}) [{$code}]");
|
||||
|
||||
return $bi;
|
||||
}
|
||||
|
||||
private function importBendingData(BendingItem $bi, object $row): int
|
||||
{
|
||||
$inputs = json_decode($row->inputList ?? '[]', true) ?: [];
|
||||
if (empty($inputs)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rates = json_decode($row->bendingrateList ?? '[]', true) ?: [];
|
||||
$sums = json_decode($row->sumList ?? '[]', true) ?: [];
|
||||
$colors = json_decode($row->colorList ?? '[]', true) ?: [];
|
||||
$angles = json_decode($row->AList ?? '[]', true) ?: [];
|
||||
|
||||
$data = [];
|
||||
$count = count($inputs);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$data[] = [
|
||||
'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),
|
||||
];
|
||||
}
|
||||
|
||||
$bi->update(['bending_data' => $data]);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function deleteFileRecords(): void
|
||||
{
|
||||
$count = File::where('field_key', 'bending_diagram')
|
||||
->where('document_type', 'bending_item')
|
||||
->forceDelete();
|
||||
|
||||
$this->info("파일 레코드 삭제: {$count}건 (R2 파일은 유지)");
|
||||
}
|
||||
|
||||
private function importImages(string $legacyImgPath): void
|
||||
{
|
||||
$chandjMap = DB::connection('chandj')->table('bending')
|
||||
->whereNotNull('imgdata')
|
||||
->where('imgdata', '!=', '')
|
||||
->pluck('imgdata', 'num');
|
||||
|
||||
$items = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->get();
|
||||
|
||||
$uploaded = 0;
|
||||
$notFound = 0;
|
||||
|
||||
foreach ($items as $bi) {
|
||||
$imgFile = $chandjMap[$bi->legacy_bending_id] ?? null;
|
||||
if (! $imgFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = "{$legacyImgPath}/{$imgFile}";
|
||||
if (! file_exists($filePath)) {
|
||||
$notFound++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$extension = pathinfo($imgFile, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/%s/%s', $this->tenantId, date('Y'), date('m'));
|
||||
$r2Path = $directory . '/' . $storedName;
|
||||
|
||||
Storage::disk('r2')->put($r2Path, file_get_contents($filePath));
|
||||
|
||||
File::create([
|
||||
'tenant_id' => $this->tenantId,
|
||||
'display_name' => $imgFile,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $r2Path,
|
||||
'file_size' => filesize($filePath),
|
||||
'mime_type' => mime_content_type($filePath),
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'bending_diagram',
|
||||
'document_id' => $bi->id,
|
||||
'document_type' => 'bending_item',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => 1,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
|
||||
$uploaded++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" ⚠️ 이미지 업로드 실패: #{$bi->legacy_bending_id} — {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("이미지 업로드: {$uploaded}건" . ($notFound > 0 ? " (파일없음 {$notFound}건)" : ''));
|
||||
}
|
||||
|
||||
private function generateCode(object $row): string
|
||||
{
|
||||
$bending = $row->item_bending ?? '';
|
||||
$sep = $row->item_sep ?? '';
|
||||
$material = $row->material ?? '';
|
||||
$name = $row->itemName ?? '';
|
||||
|
||||
$prodCode = match (true) {
|
||||
$bending === '케이스' => 'C',
|
||||
$bending === '하단마감재' && str_contains($sep, '철재') => 'T',
|
||||
$bending === '하단마감재' => 'B',
|
||||
$bending === '가이드레일' => 'R',
|
||||
$bending === '마구리' => 'X',
|
||||
$bending === 'L-BAR' => 'L',
|
||||
$bending === '연기차단재' => 'G',
|
||||
default => 'Z',
|
||||
};
|
||||
|
||||
$specCode = match (true) {
|
||||
str_contains($name, '전면') => 'F',
|
||||
str_contains($name, '린텔') => 'L',
|
||||
str_contains($name, '점검') => 'P',
|
||||
str_contains($name, '후면') => 'B',
|
||||
str_contains($name, '상부') || str_contains($name, '덮개') => 'X',
|
||||
str_contains($name, '본체') => 'M',
|
||||
str_contains($name, 'C형') || str_contains($name, '-C') => 'C',
|
||||
str_contains($name, 'D형') || str_contains($name, '-D') => 'D',
|
||||
str_contains($name, '마감') && str_contains($material, 'SUS') => 'S',
|
||||
str_contains($name, '하장바') && str_contains($material, 'SUS') => 'S',
|
||||
str_contains($name, '하장바') && str_contains($material, 'EGI') => 'E',
|
||||
str_contains($name, '보강') => 'H',
|
||||
str_contains($name, '절단') => 'T',
|
||||
str_contains($name, '비인정') => 'N',
|
||||
str_contains($name, '밑면') => 'P',
|
||||
str_contains($material, 'SUS') => 'S',
|
||||
str_contains($material, 'EGI') => 'E',
|
||||
default => 'Z',
|
||||
};
|
||||
|
||||
$date = $row->registration_date ?? now()->format('Y-m-d');
|
||||
$dateCode = date('ymd', strtotime($date));
|
||||
|
||||
$base = "{$prodCode}{$specCode}{$dateCode}";
|
||||
|
||||
// 중복 방지 일련번호
|
||||
$seq = 1;
|
||||
while (BendingItem::where('tenant_id', $this->tenantId)
|
||||
->where('code', $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT))
|
||||
->exists()) {
|
||||
$seq++;
|
||||
}
|
||||
|
||||
return $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function buildOptions(object $row): ?array
|
||||
{
|
||||
$opts = [];
|
||||
if (! empty($row->memo)) $opts['memo'] = $row->memo;
|
||||
if (! empty($row->author)) $opts['author'] = $row->author;
|
||||
if (! empty($row->search_keyword)) $opts['search_keyword'] = $row->search_keyword;
|
||||
if (! empty($row->registration_date)) $opts['registration_date'] = (string) $row->registration_date;
|
||||
|
||||
return empty($opts) ? null : $opts;
|
||||
}
|
||||
|
||||
private function verify(): void
|
||||
{
|
||||
$bi = BendingItem::where('tenant_id', $this->tenantId)->count();
|
||||
$bd = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('bending_data')->count();
|
||||
$mapped = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->distinct('legacy_bending_id')
|
||||
->count('legacy_bending_id');
|
||||
$files = File::where('field_key', 'bending_diagram')->count();
|
||||
|
||||
$this->newLine();
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
$this->info("📊 최종 결과");
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
$this->info(" bending_items: {$bi}건");
|
||||
$this->info(" bending_data: {$bd}행");
|
||||
$this->info(" chandj 매핑: {$mapped}건");
|
||||
$this->info(" 파일: {$files}건 (이미지 재업로드 필요)");
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
}
|
||||
|
||||
private function preview($rows): void
|
||||
{
|
||||
$grouped = $rows->groupBy(fn ($r) => ($r->item_bending ?: '미분류') . '/' . ($r->item_sep ?: '미분류'));
|
||||
$this->table(['분류', '건수'], $grouped->map(fn ($g, $k) => [$k, $g->count()])->values());
|
||||
}
|
||||
|
||||
private function clean(?string $v): ?string
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : trim($v);
|
||||
}
|
||||
|
||||
private function toNum(mixed $v): ?float
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : (float) $v;
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,9 @@ private function resolveOptions(string $code, string $name, array $existing): ?a
|
||||
$new[$key] = $value;
|
||||
}
|
||||
}
|
||||
if (empty($existing['item_name'])) {
|
||||
$new['item_name'] = $name;
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
@@ -222,10 +225,54 @@ private function resolveOptions(string $code, string $name, array $existing): ?a
|
||||
// 한글 패턴별 추가 파싱
|
||||
$this->parseKoreanPattern($code, $patternPrefix, $existing, $new);
|
||||
|
||||
// item_name 폴백: options에 없으면 items.name 사용
|
||||
if (empty($existing['item_name']) && empty($new['item_name'])) {
|
||||
$new['item_name'] = $name;
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
}
|
||||
|
||||
// 패턴 C: BD-LEGACY-NUM → chandj.bending에서 직접 조회
|
||||
if (preg_match('/^BD-LEGACY-(\d+)$/', $code, $m)) {
|
||||
$chandjNum = (int) $m[1];
|
||||
$chandjRow = DB::connection('chandj')->table('bending')
|
||||
->where('num', $chandjNum)
|
||||
->first();
|
||||
|
||||
if ($chandjRow) {
|
||||
$fields = [
|
||||
'item_name' => $chandjRow->itemName ?? $chandjRow->item_name ?? null,
|
||||
'item_sep' => $chandjRow->item_sep ?? null,
|
||||
'item_bending' => $chandjRow->item_bending ?? null,
|
||||
'material' => $chandjRow->material ?? null,
|
||||
'item_spec' => $chandjRow->item_spec ?? null,
|
||||
'model_name' => $chandjRow->model_name ?? null,
|
||||
'model_UA' => $chandjRow->model_UA ?? null,
|
||||
'rail_width' => $chandjRow->rail_width ?? null,
|
||||
'search_keyword' => $chandjRow->search_keyword ?? null,
|
||||
'legacy_bending_num' => $chandjNum,
|
||||
];
|
||||
foreach ($fields as $key => $value) {
|
||||
if (! empty($value) && empty($existing[$key])) {
|
||||
$new[$key] = $value;
|
||||
}
|
||||
}
|
||||
// item_name 폴백: chandj에도 없으면 items.name 사용
|
||||
if (empty($new['item_name']) && empty($existing['item_name'])) {
|
||||
$new['item_name'] = $name;
|
||||
}
|
||||
} else {
|
||||
// chandj에 없으면 items.name으로 폴백
|
||||
if (empty($existing['item_name'])) {
|
||||
$new['item_name'] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
101
app/Console/Commands/BendingFillSamItemIds.php
Normal file
101
app/Console/Commands/BendingFillSamItemIds.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use Illuminate\Console\Attributes\AsCommand;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* 모델(GUIDERAIL/SHUTTERBOX/BOTTOMBAR) components에 sam_item_id 일괄 채우기
|
||||
* legacy_bending_num → SAM BENDING item ID 매핑
|
||||
*/
|
||||
#[AsCommand(name: 'bending:fill-sam-item-ids', description: '모델 components의 sam_item_id 일괄 매핑')]
|
||||
class BendingFillSamItemIds extends Command
|
||||
{
|
||||
protected $signature = 'bending:fill-sam-item-ids
|
||||
{--tenant_id=287 : Target tenant ID}
|
||||
{--dry-run : 실제 저장 없이 미리보기}';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info('=== sam_item_id 일괄 매핑 ===');
|
||||
$this->info('Mode: ' . ($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||
|
||||
// 1. legacy_bending_num → SAM item ID 매핑 테이블 구축
|
||||
$bendingItems = Item::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_category', 'BENDING')
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
$legacyMap = [];
|
||||
foreach ($bendingItems as $item) {
|
||||
$legacyNum = $item->getOption('legacy_bending_num');
|
||||
if ($legacyNum !== null) {
|
||||
$legacyMap[(string) $legacyNum] = $item->id;
|
||||
}
|
||||
}
|
||||
$this->info("BENDING items: {$bendingItems->count()}건, legacy_bending_num 매핑: " . count($legacyMap) . '건');
|
||||
|
||||
// 2. 모델 items의 components 순회
|
||||
$models = Item::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereIn('item_category', ['GUIDERAIL_MODEL', 'SHUTTERBOX_MODEL', 'BOTTOMBAR_MODEL'])
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
$this->info("모델: {$models->count()}건");
|
||||
|
||||
$updated = 0;
|
||||
$mapped = 0;
|
||||
$notFound = 0;
|
||||
|
||||
foreach ($models as $model) {
|
||||
$components = $model->getOption('components', []);
|
||||
if (empty($components)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changed = false;
|
||||
foreach ($components as &$comp) {
|
||||
// 이미 sam_item_id가 있으면 스킵
|
||||
if (! empty($comp['sam_item_id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$legacyNum = $comp['legacy_bending_num'] ?? null;
|
||||
if ($legacyNum === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$samId = $legacyMap[(string) $legacyNum] ?? null;
|
||||
if ($samId) {
|
||||
$comp['sam_item_id'] = $samId;
|
||||
$changed = true;
|
||||
$mapped++;
|
||||
} else {
|
||||
$notFound++;
|
||||
$this->warn(" [{$model->id}] legacy_bending_num={$legacyNum} → SAM ID 없음 ({$comp['itemName']})");
|
||||
}
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
if ($changed && ! $dryRun) {
|
||||
$model->setOption('components', $components);
|
||||
$model->save();
|
||||
$updated++;
|
||||
} elseif ($changed) {
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('');
|
||||
$this->info("결과: 모델 {$updated}건 업데이트, 컴포넌트 {$mapped}건 매핑, {$notFound}건 미매핑");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -2,168 +2,114 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Attributes\AsCommand;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* 레거시 5130 절곡 이미지 → SAM R2 + files 테이블 마이그레이션
|
||||
* 레거시 이미지 → R2 업로드 + bending_items 연결
|
||||
*
|
||||
* 소스: https://5130.codebridge-x.com/bending/img/{imgdata}
|
||||
* 대상: R2 저장 + files 테이블 + items.options 업데이트
|
||||
* 실행: php artisan bending:import-images [--dry-run] [--tenant_id=287]
|
||||
*/
|
||||
#[AsCommand(name: 'bending:import-images', description: '레거시 절곡 이미지 → R2 마이그레이션')]
|
||||
class BendingImportImages extends Command
|
||||
{
|
||||
protected $signature = 'bending:import-images
|
||||
{--tenant_id=287 : Target tenant ID}
|
||||
{--dry-run : 실제 저장 없이 미리보기}
|
||||
{--source=https://5130.codebridge-x.com/bending/img : 이미지 소스 URL}';
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 미리보기}
|
||||
{--legacy-path=/home/kkk/sam/5130/bending/img : 레거시 이미지 경로}';
|
||||
|
||||
private int $uploaded = 0;
|
||||
|
||||
private int $skipped = 0;
|
||||
|
||||
private int $failed = 0;
|
||||
protected $description = '레거시 절곡품 이미지 → R2 업로드 + bending_items 연결';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$sourceBase = rtrim($this->option('source'), '/');
|
||||
$legacyPath = $this->option('legacy-path');
|
||||
|
||||
$this->info('=== 레거시 절곡 이미지 → R2 마이그레이션 ===');
|
||||
$this->info('Source: '.$sourceBase);
|
||||
$this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||
$this->newLine();
|
||||
$items = BendingItem::where('tenant_id', $tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->get();
|
||||
|
||||
// 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')
|
||||
$chandjMap = DB::connection('chandj')->table('bending')
|
||||
->whereNotNull('imgdata')
|
||||
->where('imgdata', '!=', '')
|
||||
->pluck('imgdata', 'num');
|
||||
|
||||
$this->info("chandj 이미지: {$chandjImages->count()}건");
|
||||
$this->newLine();
|
||||
$this->info("bending_items: {$items->count()}건 / chandj imgdata: {$chandjMap->count()}건");
|
||||
|
||||
foreach ($items as $item) {
|
||||
$opts = json_decode($item->options ?? '{}', true) ?: [];
|
||||
$legacyNum = $opts['legacy_bending_num'] ?? null;
|
||||
$uploaded = 0;
|
||||
$skipped = 0;
|
||||
$notFound = 0;
|
||||
$errors = 0;
|
||||
|
||||
if (! $legacyNum || ! isset($chandjImages[$legacyNum])) {
|
||||
foreach ($items as $bi) {
|
||||
$imgFile = $chandjMap[$bi->legacy_bending_id] ?? null;
|
||||
if (! $imgFile) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 파일이 연결되어 있으면 스킵
|
||||
$existingFile = File::where('tenant_id', $tenantId)
|
||||
->where('document_type', '1')
|
||||
->where('document_id', $item->id)
|
||||
$filePath = "{$legacyPath}/{$imgFile}";
|
||||
if (! file_exists($filePath)) {
|
||||
$this->warn(" ⚠️ 파일 없음: {$imgFile} (#{$bi->legacy_bending_id})");
|
||||
$notFound++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = File::where('document_type', 'bending_item')
|
||||
->where('document_id', $bi->id)
|
||||
->where('field_key', 'bending_diagram')
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($existingFile) {
|
||||
$this->skipped++;
|
||||
|
||||
if ($existing) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$imgFilename = $chandjImages[$legacyNum];
|
||||
$imageUrl = "{$sourceBase}/{$imgFilename}";
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(" ✅ {$item->code} ← {$imgFilename}");
|
||||
$this->uploaded++;
|
||||
|
||||
$this->line(" [DRY] #{$bi->legacy_bending_id} → {$bi->id} ({$bi->item_name}) ← {$imgFile}");
|
||||
$uploaded++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미지 다운로드
|
||||
try {
|
||||
$response = Http::withoutVerifying()->timeout(15)->get($imageUrl);
|
||||
$extension = pathinfo($imgFile, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/%s/%s', $tenantId, date('Y'), date('m'));
|
||||
$r2Path = $directory . '/' . $storedName;
|
||||
|
||||
if (! $response->successful()) {
|
||||
$this->warn(" ❌ {$item->code}: HTTP {$response->status()} ({$imageUrl})");
|
||||
$this->failed++;
|
||||
Storage::disk('r2')->put($r2Path, file_get_contents($filePath));
|
||||
|
||||
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([
|
||||
File::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'display_name' => $imgFilename,
|
||||
'display_name' => $imgFile,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $filePath,
|
||||
'file_size' => strlen($imageContent),
|
||||
'mime_type' => $mimeType,
|
||||
'file_path' => $r2Path,
|
||||
'file_size' => filesize($filePath),
|
||||
'mime_type' => mime_content_type($filePath),
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'bending_diagram',
|
||||
'document_id' => $item->id,
|
||||
'document_type' => '1',
|
||||
'document_id' => $bi->id,
|
||||
'document_type' => 'bending_item',
|
||||
'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->line(" ✅ #{$bi->legacy_bending_id} → {$bi->id} ({$bi->item_name}) ← {$imgFile}");
|
||||
$uploaded++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" ❌ #{$bi->legacy_bending_id}: {$e->getMessage()}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
$this->info("업로드: {$this->uploaded}건 | 스킵(이미 있음): {$this->skipped}건 | 실패: {$this->failed}건");
|
||||
$this->info("완료: 업로드 {$uploaded}, 스킵 {$skipped}, 파일없음 {$notFound}, 오류 {$errors}");
|
||||
|
||||
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',
|
||||
};
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
227
app/Console/Commands/BendingImportMissing.php
Normal file
227
app/Console/Commands/BendingImportMissing.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\BendingDataRow;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* chandj.bending 누락분 → bending_items + bending_data 직접 이관
|
||||
*
|
||||
* 실행: php artisan bending:import-missing [--dry-run] [--tenant_id=287]
|
||||
*/
|
||||
class BendingImportMissing extends Command
|
||||
{
|
||||
protected $signature = 'bending:import-missing
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 실행하지 않고 미리보기만}';
|
||||
|
||||
protected $description = 'chandj.bending 누락분 → bending_items 직접 이관';
|
||||
|
||||
private int $tenantId;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$existingNums = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->pluck('legacy_bending_id')
|
||||
->toArray();
|
||||
|
||||
$missing = DB::connection('chandj')->table('bending')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('is_deleted')->orWhere('is_deleted', 0);
|
||||
})
|
||||
->whereNotIn('num', $existingNums)
|
||||
->orderBy('num')
|
||||
->get();
|
||||
|
||||
$this->info("누락분: {$missing->count()}건 (이미 매핑: " . count($existingNums) . "건)");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->preview($missing);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$bdCount = 0;
|
||||
$errors = 0;
|
||||
|
||||
DB::transaction(function () use ($missing, &$success, &$bdCount, &$errors) {
|
||||
foreach ($missing as $row) {
|
||||
try {
|
||||
$bi = $this->importItem($row);
|
||||
$bd = $this->importBendingData($bi, $row);
|
||||
$bdCount += $bd;
|
||||
$success++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" ❌ #{$row->num} {$row->itemName}: {$e->getMessage()}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("완료: {$success}건 이관, {$bdCount}건 전개도, {$errors}건 오류");
|
||||
|
||||
return $errors > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function importItem(object $row): BendingItem
|
||||
{
|
||||
$code = $this->generateCode($row);
|
||||
|
||||
$bi = BendingItem::create([
|
||||
'tenant_id' => $this->tenantId,
|
||||
'code' => $code,
|
||||
'legacy_code' => "CHANDJ-{$row->num}",
|
||||
'legacy_bending_id' => $row->num,
|
||||
'item_name' => $row->itemName ?: "부품#{$row->num}",
|
||||
'item_sep' => $this->clean($row->item_sep),
|
||||
'item_bending' => $this->clean($row->item_bending),
|
||||
'material' => $this->clean($row->material),
|
||||
'item_spec' => $this->clean($row->item_spec),
|
||||
'model_name' => $this->clean($row->model_name ?? null),
|
||||
'model_UA' => $this->clean($row->model_UA ?? null),
|
||||
'rail_width' => $this->toNum($row->rail_width ?? null),
|
||||
'exit_direction' => $this->clean($row->exit_direction ?? null),
|
||||
'box_width' => $this->toNum($row->box_width ?? null),
|
||||
'box_height' => $this->toNum($row->box_height ?? null),
|
||||
'front_bottom' => $this->toNum($row->front_bottom_width ?? null),
|
||||
'options' => $this->buildOptions($row),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
|
||||
$this->line(" ✅ #{$row->num} → {$bi->id} ({$row->itemName}) [{$code}]");
|
||||
|
||||
return $bi;
|
||||
}
|
||||
|
||||
private function importBendingData(BendingItem $bi, object $row): int
|
||||
{
|
||||
$inputs = json_decode($row->inputList ?? '[]', true) ?: [];
|
||||
if (empty($inputs)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rates = json_decode($row->bendingrateList ?? '[]', true) ?: [];
|
||||
$sums = json_decode($row->sumList ?? '[]', true) ?: [];
|
||||
$colors = json_decode($row->colorList ?? '[]', true) ?: [];
|
||||
$angles = json_decode($row->AList ?? '[]', true) ?: [];
|
||||
|
||||
$count = count($inputs);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$input = (float) ($inputs[$i] ?? 0);
|
||||
$rate = (string) ($rates[$i] ?? '');
|
||||
$afterRate = ($rate !== '') ? $input + (float) $rate : $input;
|
||||
|
||||
BendingDataRow::create([
|
||||
'bending_item_id' => $bi->id,
|
||||
'sort_order' => $i + 1,
|
||||
'input' => $input,
|
||||
'rate' => $rate !== '' ? $rate : null,
|
||||
'after_rate' => $afterRate,
|
||||
'sum' => (float) ($sums[$i] ?? 0),
|
||||
'color' => (bool) ($colors[$i] ?? false),
|
||||
'a_angle' => (bool) ($angles[$i] ?? false),
|
||||
]);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function generateCode(object $row): string
|
||||
{
|
||||
$bending = $row->item_bending ?? '';
|
||||
$sep = $row->item_sep ?? '';
|
||||
$material = $row->material ?? '';
|
||||
$name = $row->itemName ?? '';
|
||||
|
||||
$prodCode = match (true) {
|
||||
$bending === '케이스' => 'C',
|
||||
$bending === '하단마감재' && str_contains($sep, '철재') => 'T',
|
||||
$bending === '하단마감재' => 'B',
|
||||
$bending === '가이드레일' && str_contains($sep, '철재') => 'R',
|
||||
$bending === '가이드레일' => 'R',
|
||||
$bending === '마구리' => 'X',
|
||||
$bending === 'L-BAR' => 'L',
|
||||
$bending === '연기차단재' => 'G',
|
||||
default => 'Z',
|
||||
};
|
||||
|
||||
$specCode = match (true) {
|
||||
str_contains($name, '전면') => 'F',
|
||||
str_contains($name, '린텔') => 'L',
|
||||
str_contains($name, '점검') => 'P',
|
||||
str_contains($name, '후면') => 'B',
|
||||
str_contains($name, '상부') || str_contains($name, '덮개') => 'X',
|
||||
str_contains($name, '본체') => 'M',
|
||||
str_contains($name, 'C형') || str_contains($name, '-C') => 'C',
|
||||
str_contains($name, 'D형') || str_contains($name, '-D') => 'D',
|
||||
str_contains($name, '마감') && str_contains($material, 'SUS') => 'S',
|
||||
str_contains($material, 'SUS') => 'S',
|
||||
str_contains($material, 'EGI') => 'E',
|
||||
default => 'Z',
|
||||
};
|
||||
|
||||
$date = $row->registration_date ?? now()->format('Y-m-d');
|
||||
$dateCode = date('ymd', strtotime($date));
|
||||
|
||||
$base = "{$prodCode}{$specCode}{$dateCode}";
|
||||
|
||||
// 중복 방지 일련번호
|
||||
$seq = 1;
|
||||
while (BendingItem::where('tenant_id', $this->tenantId)
|
||||
->where('code', $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT))
|
||||
->whereNull('length_code')
|
||||
->exists()) {
|
||||
$seq++;
|
||||
}
|
||||
|
||||
return $seq === 1 ? $base : "{$base}-" . str_pad($seq, 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function buildOptions(object $row): ?array
|
||||
{
|
||||
$opts = [];
|
||||
if (! empty($row->memo)) $opts['memo'] = $row->memo;
|
||||
if (! empty($row->author)) $opts['author'] = $row->author;
|
||||
if (! empty($row->search_keyword)) $opts['search_keyword'] = $row->search_keyword;
|
||||
if (! empty($row->registration_date)) $opts['registration_date'] = (string) $row->registration_date;
|
||||
|
||||
return empty($opts) ? null : $opts;
|
||||
}
|
||||
|
||||
private function preview($missing): void
|
||||
{
|
||||
$grouped = $missing->groupBy(fn ($r) => ($r->item_bending ?: '미분류') . '/' . ($r->item_sep ?: '미분류'));
|
||||
$this->table(['분류', '건수'], $grouped->map(fn ($g, $k) => [$k, $g->count()])->values());
|
||||
|
||||
$this->newLine();
|
||||
$headers = ['num', 'itemName', 'item_sep', 'item_bending', 'material', 'has_bd'];
|
||||
$rows = $missing->take(15)->map(fn ($r) => [
|
||||
$r->num,
|
||||
mb_substr($r->itemName ?? '', 0, 25),
|
||||
$r->item_sep ?? '-',
|
||||
$r->item_bending ?? '-',
|
||||
mb_substr($r->material ?? '-', 0, 12),
|
||||
! empty(json_decode($r->inputList ?? '[]', true)) ? '✅' : '❌',
|
||||
]);
|
||||
$this->table($headers, $rows);
|
||||
}
|
||||
|
||||
private function clean(?string $v): ?string
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : trim($v);
|
||||
}
|
||||
|
||||
private function toNum(mixed $v): ?float
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : (float) $v;
|
||||
}
|
||||
}
|
||||
145
app/Console/Commands/BendingModelCopyImages.php
Normal file
145
app/Console/Commands/BendingModelCopyImages.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\BendingModel;
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* 모델 component별 이미지 복사 (기초관리 원본 → 독립 복사본)
|
||||
*
|
||||
* component.source_num → bending_items.legacy_bending_id → 원본 이미지
|
||||
* → R2에 복사 → 새 file 레코드 → component.image_file_id 업데이트
|
||||
*
|
||||
* 실행: php artisan bending:model-copy-images [--dry-run] [--tenant_id=287]
|
||||
*/
|
||||
class BendingModelCopyImages extends Command
|
||||
{
|
||||
protected $signature = 'bending:model-copy-images
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 미리보기}';
|
||||
|
||||
protected $description = '모델 component별 이미지를 기초관리에서 복사';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
// bending_items의 legacy_bending_id → 이미지 파일 매핑
|
||||
$itemImageMap = [];
|
||||
$items = BendingItem::where('tenant_id', $tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->get();
|
||||
|
||||
foreach ($items as $bi) {
|
||||
$file = File::where('document_type', 'bending_item')
|
||||
->where('document_id', $bi->id)
|
||||
->where('field_key', 'bending_diagram')
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($file) {
|
||||
$itemImageMap[$bi->legacy_bending_id] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("기초관리 이미지 매핑: " . count($itemImageMap) . "건");
|
||||
|
||||
$models = BendingModel::where('tenant_id', $tenantId)
|
||||
->whereNotNull('components')
|
||||
->get();
|
||||
|
||||
$copied = 0;
|
||||
$skipped = 0;
|
||||
$noSource = 0;
|
||||
|
||||
foreach ($models as $model) {
|
||||
$components = $model->components;
|
||||
if (empty($components)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updated = false;
|
||||
foreach ($components as $idx => &$comp) {
|
||||
// 이미 image_file_id가 있으면 skip
|
||||
if (! empty($comp['image_file_id'])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// source_num으로 기초관리 이미지 찾기
|
||||
$sourceNum = $comp['num'] ?? $comp['source_num'] ?? null;
|
||||
if (! $sourceNum) {
|
||||
$noSource++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceFile = $itemImageMap[(int) $sourceNum] ?? null;
|
||||
if (! $sourceFile || ! $sourceFile->file_path) {
|
||||
$noSource++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(" [DRY] model#{$model->id} comp[{$idx}] ← bending#{$sourceNum} file#{$sourceFile->id}");
|
||||
$copied++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// R2에서 파일 복사
|
||||
try {
|
||||
$newFile = $this->copyFile($sourceFile, $model->id, $tenantId);
|
||||
$comp['image_file_id'] = $newFile->id;
|
||||
$updated = true;
|
||||
$copied++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" ⚠️ 복사 실패: model#{$model->id} comp[{$idx}] — {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
if ($updated && ! $dryRun) {
|
||||
$model->components = $components;
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("완료: 복사 {$copied}건, 스킵 {$skipped}건, 소스없음 {$noSource}건");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function copyFile(File $source, int $modelId, int $tenantId): File
|
||||
{
|
||||
$extension = pathinfo($source->stored_name, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/model-parts/%s/%s', $tenantId, date('Y'), date('m'));
|
||||
$newPath = $directory . '/' . $storedName;
|
||||
|
||||
// R2 파일 복사
|
||||
$content = Storage::disk('r2')->get($source->file_path);
|
||||
Storage::disk('r2')->put($newPath, $content);
|
||||
|
||||
// 새 파일 레코드 생성
|
||||
return File::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'display_name' => $source->display_name,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $source->file_size,
|
||||
'mime_type' => $source->mime_type,
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'component_image',
|
||||
'document_id' => $modelId,
|
||||
'document_type' => 'bending_model',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => 1,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
387
app/Console/Commands/BendingModelImport.php
Normal file
387
app/Console/Commands/BendingModelImport.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\BendingModel;
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* chandj guiderail/bottombar/shutterbox → bending_models 직접 이관
|
||||
* + 기존 assembly_image 파일 매핑 보존
|
||||
* + component별 이미지 복사 (기초관리 원본 → 독립 복사본)
|
||||
*
|
||||
* 실행: php artisan bending:model-import [--dry-run] [--tenant_id=287]
|
||||
*/
|
||||
class BendingModelImport extends Command
|
||||
{
|
||||
protected $signature = 'bending:model-import
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 미리보기}
|
||||
{--legacy-path=/tmp/legacy_5130 : 레거시 5130 경로}';
|
||||
|
||||
protected $description = 'chandj 절곡품 모델 3종 → bending_models 이관 (이미지 포함)';
|
||||
|
||||
private int $tenantId;
|
||||
private string $legacyPath;
|
||||
private array $itemImageMap = [];
|
||||
private array $itemIdMap = [];
|
||||
private array $modelImageMap = []; // "type:model_name:finishing_type" → image path
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$this->legacyPath = $this->option('legacy-path');
|
||||
|
||||
// 기초관리 이미지 매핑 + 모델 JSON 이미지 로드
|
||||
$this->buildItemImageMap();
|
||||
$this->loadModelImageJsons();
|
||||
|
||||
// 기존 데이터 삭제 (assembly_image 파일 매핑 보존)
|
||||
$existing = BendingModel::where('tenant_id', $this->tenantId)->count();
|
||||
$oldFileMap = [];
|
||||
if ($existing > 0 && ! $dryRun) {
|
||||
$oldFileMap = $this->buildOldFileMap();
|
||||
|
||||
// component_image 삭제 (재생성할 거니까)
|
||||
File::where('document_type', 'bending_model')
|
||||
->where('field_key', 'component_image')
|
||||
->whereNull('deleted_at')
|
||||
->forceDelete();
|
||||
|
||||
BendingModel::where('tenant_id', $this->tenantId)->forceDelete();
|
||||
$this->info("기존 bending_models {$existing}건 삭제 (component_image 재생성)");
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
|
||||
// 1. guiderail
|
||||
$guiderails = DB::connection('chandj')->table('guiderail')
|
||||
->where(function ($q) { $q->whereNull('is_deleted')->orWhere('is_deleted', ''); })
|
||||
->orderBy('num')->get();
|
||||
$this->info("\n=== 가이드레일: {$guiderails->count()}건 ===");
|
||||
foreach ($guiderails as $row) {
|
||||
if ($dryRun) { $this->line(" [DRY] #{$row->num} {$row->model_name}"); continue; }
|
||||
$this->importModel($row, BendingModel::TYPE_GUIDERAIL, "GR-{$row->num}", $this->buildGuiderailData($row));
|
||||
$total++;
|
||||
}
|
||||
|
||||
// 2. shutterbox
|
||||
$shutterboxes = DB::connection('chandj')->table('shutterbox')
|
||||
->where(function ($q) { $q->whereNull('is_deleted')->orWhere('is_deleted', ''); })
|
||||
->orderBy('num')->get();
|
||||
$this->info("\n=== 케이스: {$shutterboxes->count()}건 ===");
|
||||
foreach ($shutterboxes as $row) {
|
||||
if ($dryRun) { $this->line(" [DRY] #{$row->num} {$row->exit_direction}"); continue; }
|
||||
$this->importModel($row, BendingModel::TYPE_SHUTTERBOX, "SB-{$row->num}", $this->buildShutterboxData($row));
|
||||
$total++;
|
||||
}
|
||||
|
||||
// 3. bottombar
|
||||
$bottombars = DB::connection('chandj')->table('bottombar')
|
||||
->where(function ($q) { $q->whereNull('is_deleted')->orWhere('is_deleted', ''); })
|
||||
->orderBy('num')->get();
|
||||
$this->info("\n=== 하단마감재: {$bottombars->count()}건 ===");
|
||||
foreach ($bottombars as $row) {
|
||||
if ($dryRun) { $this->line(" [DRY] #{$row->num} {$row->model_name}"); continue; }
|
||||
$this->importModel($row, BendingModel::TYPE_BOTTOMBAR, "BB-{$row->num}", $this->buildBottombarData($row));
|
||||
$total++;
|
||||
}
|
||||
|
||||
// assembly_image 파일 매핑 업데이트
|
||||
if (! $dryRun && ! empty($oldFileMap)) {
|
||||
$this->remapAssemblyImages($oldFileMap);
|
||||
}
|
||||
|
||||
// 최종 결과
|
||||
$this->newLine();
|
||||
$final = BendingModel::where('tenant_id', $this->tenantId)->count();
|
||||
$assemblyFiles = File::where('document_type', 'bending_model')->where('field_key', 'assembly_image')->whereNull('deleted_at')->count();
|
||||
$compFiles = File::where('document_type', 'bending_model')->where('field_key', 'component_image')->whereNull('deleted_at')->count();
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
$this->info("모델: {$final}건 / 조립도: {$assemblyFiles}건 / 부품이미지: {$compFiles}건");
|
||||
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function importModel(object $row, string $type, string $code, array $data): void
|
||||
{
|
||||
$components = json_decode($row->bending_components ?? '[]', true) ?: [];
|
||||
$materialSummary = json_decode($row->material_summary ?? '{}', true) ?: [];
|
||||
|
||||
// component별 이미지 복사
|
||||
$components = $this->copyComponentImages($components);
|
||||
|
||||
$bm = BendingModel::create(array_merge($data, [
|
||||
'tenant_id' => $this->tenantId,
|
||||
'model_type' => $type,
|
||||
'code' => $code,
|
||||
'legacy_num' => $row->num,
|
||||
'components' => $components,
|
||||
'material_summary' => $materialSummary,
|
||||
'registration_date' => $row->registration_date ?? null,
|
||||
'author' => $this->clean($row->author ?? null),
|
||||
'remark' => $this->clean($row->remark ?? null),
|
||||
'search_keyword' => $this->clean($row->search_keyword ?? null),
|
||||
'is_active' => true,
|
||||
'created_by' => 1,
|
||||
]));
|
||||
|
||||
// assembly_image 업로드 (JSON 파일에서)
|
||||
$this->uploadAssemblyImage($bm, $type, $data);
|
||||
|
||||
$compCount = count($components);
|
||||
$imgCount = collect($components)->whereNotNull('image_file_id')->count();
|
||||
$hasAssembly = File::where('document_type', 'bending_model')->where('document_id', $bm->id)->where('field_key', 'assembly_image')->exists();
|
||||
$this->line(" ✅ #{$row->num} → {$bm->id} ({$data['name']}) [부품:{$compCount} 이미지:{$imgCount} 조립도:" . ($hasAssembly ? 'Y' : 'N') . ']');
|
||||
}
|
||||
|
||||
private function copyComponentImages(array $components): array
|
||||
{
|
||||
foreach ($components as &$comp) {
|
||||
$sourceNum = $comp['num'] ?? $comp['source_num'] ?? null;
|
||||
if (! $sourceNum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// sam_item_id 매핑 (원본수정 링크용)
|
||||
$samItemId = $this->itemIdMap[(int) $sourceNum] ?? null;
|
||||
if ($samItemId) {
|
||||
$comp['sam_item_id'] = $samItemId;
|
||||
}
|
||||
|
||||
$sourceFile = $this->itemImageMap[(int) $sourceNum] ?? null;
|
||||
if (! $sourceFile || ! $sourceFile->file_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$extension = pathinfo($sourceFile->stored_name, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/model-parts/%s/%s', $this->tenantId, date('Y'), date('m'));
|
||||
$newPath = $directory . '/' . $storedName;
|
||||
|
||||
$content = Storage::disk('r2')->get($sourceFile->file_path);
|
||||
Storage::disk('r2')->put($newPath, $content);
|
||||
|
||||
$newFile = File::create([
|
||||
'tenant_id' => $this->tenantId,
|
||||
'display_name' => $sourceFile->display_name,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $sourceFile->file_size,
|
||||
'mime_type' => $sourceFile->mime_type,
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'component_image',
|
||||
'document_id' => 0, // 모델 생성 전이므로 임시, 나중에 update 불필요 (component JSON에 ID 저장)
|
||||
'document_type' => 'bending_model',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => 1,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
|
||||
$comp['image_file_id'] = $newFile->id;
|
||||
} catch (\Throwable $e) {
|
||||
// 복사 실패 시 무시
|
||||
}
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
private function buildItemImageMap(): void
|
||||
{
|
||||
$items = BendingItem::where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('legacy_bending_id')
|
||||
->get();
|
||||
|
||||
foreach ($items as $bi) {
|
||||
$file = File::where('document_type', 'bending_item')
|
||||
->where('document_id', $bi->id)
|
||||
->where('field_key', 'bending_diagram')
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
$this->itemIdMap[$bi->legacy_bending_id] = $bi->id;
|
||||
|
||||
if ($file) {
|
||||
$this->itemImageMap[$bi->legacy_bending_id] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("기초관리 매핑: " . count($this->itemIdMap) . "건 (이미지: " . count($this->itemImageMap) . "건)");
|
||||
}
|
||||
|
||||
private function buildOldFileMap(): array
|
||||
{
|
||||
return File::where('document_type', 'bending_model')
|
||||
->where('field_key', 'assembly_image')
|
||||
->whereNull('deleted_at')
|
||||
->get()
|
||||
->mapWithKeys(function ($file) {
|
||||
$bm = BendingModel::find($file->document_id);
|
||||
return $bm ? [$bm->legacy_num => $file->document_id] : [];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
private function remapAssemblyImages(array $oldFileMap): void
|
||||
{
|
||||
$remapped = 0;
|
||||
$newModels = BendingModel::where('tenant_id', $this->tenantId)->get()->keyBy('legacy_num');
|
||||
|
||||
foreach ($oldFileMap as $legacyNum => $oldDocId) {
|
||||
$newBm = $newModels[$legacyNum] ?? null;
|
||||
if ($newBm && $oldDocId !== $newBm->id) {
|
||||
File::where('document_type', 'bending_model')
|
||||
->where('field_key', 'assembly_image')
|
||||
->where('document_id', $oldDocId)
|
||||
->whereNull('deleted_at')
|
||||
->update(['document_id' => $newBm->id]);
|
||||
$remapped++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("조립도 매핑 업데이트: {$remapped}건");
|
||||
}
|
||||
|
||||
private function loadModelImageJsons(): void
|
||||
{
|
||||
$jsonFiles = [
|
||||
'guiderail' => $this->legacyPath . '/guiderail/guiderail.json',
|
||||
'shutterbox' => $this->legacyPath . '/shutterbox/shutterbox.json',
|
||||
'bottombar' => $this->legacyPath . '/bottombar/bottombar.json',
|
||||
];
|
||||
|
||||
foreach ($jsonFiles as $type => $path) {
|
||||
if (! file_exists($path)) {
|
||||
continue;
|
||||
}
|
||||
$items = json_decode(file_get_contents($path), true) ?: [];
|
||||
foreach ($items as $item) {
|
||||
$key = $this->makeImageKey($type, $item);
|
||||
if ($key && ! empty($item['image'])) {
|
||||
$this->modelImageMap[$key] = $item['image'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("모델 이미지 매핑: " . count($this->modelImageMap) . "건");
|
||||
}
|
||||
|
||||
private function makeImageKey(string $type, array $item): ?string
|
||||
{
|
||||
if ($type === 'guiderail') {
|
||||
return "GR:{$item['model_name']}:{$item['check_type']}:{$item['finishing_type']}";
|
||||
}
|
||||
if ($type === 'shutterbox') {
|
||||
return "SB:{$item['exit_direction']}:{$item['box_width']}x{$item['box_height']}";
|
||||
}
|
||||
if ($type === 'bottombar') {
|
||||
return "BB:{$item['model_name']}:{$item['finishing_type']}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function uploadAssemblyImage(BendingModel $bm, string $type, array $data): void
|
||||
{
|
||||
$key = match ($type) {
|
||||
BendingModel::TYPE_GUIDERAIL => "GR:{$data['model_name']}:{$data['check_type']}:{$data['finishing_type']}",
|
||||
BendingModel::TYPE_SHUTTERBOX => "SB:{$data['exit_direction']}:" . intval($data['box_width'] ?? 0) . 'x' . intval($data['box_height'] ?? 0),
|
||||
BendingModel::TYPE_BOTTOMBAR => "BB:{$data['model_name']}:{$data['finishing_type']}",
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $key) return;
|
||||
|
||||
$imagePath = $this->modelImageMap[$key] ?? null;
|
||||
if (! $imagePath) return;
|
||||
|
||||
// /bottombar/images/xxx.png → legacy-path/bottombar/images/xxx.png
|
||||
$localPath = $this->legacyPath . $imagePath;
|
||||
if (! file_exists($localPath)) return;
|
||||
|
||||
try {
|
||||
$extension = pathinfo($localPath, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/models/%s/%s', $this->tenantId, date('Y'), date('m'));
|
||||
$r2Path = $directory . '/' . $storedName;
|
||||
|
||||
Storage::disk('r2')->put($r2Path, file_get_contents($localPath));
|
||||
|
||||
File::create([
|
||||
'tenant_id' => $this->tenantId,
|
||||
'display_name' => basename($imagePath),
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $r2Path,
|
||||
'file_size' => filesize($localPath),
|
||||
'mime_type' => mime_content_type($localPath),
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'assembly_image',
|
||||
'document_id' => $bm->id,
|
||||
'document_type' => 'bending_model',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => 1,
|
||||
'created_by' => 1,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" ⚠️ 조립도 업로드 실패: {$bm->name} — {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 모델별 데이터 빌드 ──
|
||||
|
||||
private function buildGuiderailData(object $row): array
|
||||
{
|
||||
return [
|
||||
'name' => trim("{$row->model_name} {$row->firstitem} {$row->check_type} {$row->finishing_type}"),
|
||||
'model_name' => $this->clean($row->model_name),
|
||||
'model_UA' => $this->clean($row->model_UA),
|
||||
'item_sep' => $this->clean($row->firstitem),
|
||||
'finishing_type' => $this->clean($row->finishing_type),
|
||||
'check_type' => $this->clean($row->check_type),
|
||||
'rail_width' => $this->toNum($row->rail_width),
|
||||
'rail_length' => $this->toNum($row->rail_length),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildShutterboxData(object $row): array
|
||||
{
|
||||
return [
|
||||
'name' => trim("케이스 {$row->exit_direction} {$row->box_width}x{$row->box_height}"),
|
||||
'exit_direction' => $this->clean($row->exit_direction),
|
||||
'front_bottom_width' => $this->toNum($row->front_bottom_width ?? null),
|
||||
'rail_width' => $this->toNum($row->rail_width ?? null),
|
||||
'box_width' => $this->toNum($row->box_width),
|
||||
'box_height' => $this->toNum($row->box_height),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildBottombarData(object $row): array
|
||||
{
|
||||
return [
|
||||
'name' => trim("{$row->model_name} {$row->firstitem} {$row->finishing_type} {$row->bar_width}x{$row->bar_height}"),
|
||||
'model_name' => $this->clean($row->model_name),
|
||||
'model_UA' => $this->clean($row->model_UA),
|
||||
'item_sep' => $this->clean($row->firstitem),
|
||||
'finishing_type' => $this->clean($row->finishing_type),
|
||||
'bar_width' => $this->toNum($row->bar_width),
|
||||
'bar_height' => $this->toNum($row->bar_height),
|
||||
];
|
||||
}
|
||||
|
||||
private function clean(?string $v): ?string
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : trim($v);
|
||||
}
|
||||
|
||||
private function toNum(mixed $v): ?float
|
||||
{
|
||||
return ($v === null || $v === '' || $v === 'null') ? null : (float) $v;
|
||||
}
|
||||
}
|
||||
199
app/Console/Commands/MigrateBendingItemsToNewTable.php
Normal file
199
app/Console/Commands/MigrateBendingItemsToNewTable.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\Items\Item;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* items(BENDING) + options JSON → bending_items + bending_data 이관
|
||||
*
|
||||
* 실행: php artisan bending:migrate-to-new-table
|
||||
* 롤백: php artisan bending:migrate-to-new-table --rollback
|
||||
*/
|
||||
class MigrateBendingItemsToNewTable extends Command
|
||||
{
|
||||
protected $signature = 'bending:migrate-to-new-table
|
||||
{--tenant_id=287 : 테넌트 ID}
|
||||
{--dry-run : 실행하지 않고 미리보기만}
|
||||
{--rollback : bending_items/bending_data 전체 삭제}';
|
||||
|
||||
protected $description = 'items(BENDING) → bending_items + bending_data 테이블 이관';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$tenantId = (int) $this->option('tenant_id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$rollback = $this->option('rollback');
|
||||
|
||||
if ($rollback) {
|
||||
return $this->rollback($tenantId);
|
||||
}
|
||||
|
||||
// 이미 이관된 데이터 확인
|
||||
$existingCount = BendingItem::where('tenant_id', $tenantId)->count();
|
||||
if ($existingCount > 0) {
|
||||
$this->warn("이미 bending_items에 {$existingCount}건 존재합니다.");
|
||||
if (! $this->confirm('기존 데이터 삭제 후 재이관하시겠습니까?')) {
|
||||
return 0;
|
||||
}
|
||||
$this->rollback($tenantId);
|
||||
}
|
||||
|
||||
// items(BENDING) 조회
|
||||
$items = Item::where('item_category', 'BENDING')
|
||||
->where('tenant_id', $tenantId)
|
||||
->get();
|
||||
|
||||
$this->info("이관 대상: {$items->count()}건");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->previewItems($items);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$errors = 0;
|
||||
$bdCount = 0;
|
||||
|
||||
DB::transaction(function () use ($items, $tenantId, &$success, &$errors, &$bdCount) {
|
||||
foreach ($items as $item) {
|
||||
try {
|
||||
$bi = $this->migrateItem($item, $tenantId);
|
||||
$bdRows = $this->migrateBendingData($bi, $item);
|
||||
$bdCount += $bdRows;
|
||||
$success++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" ❌ {$item->code}: {$e->getMessage()}");
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("완료: {$success}건 이관, {$bdCount}건 전개도 행, {$errors}건 오류");
|
||||
|
||||
return $errors > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function migrateItem(Item $item, int $tenantId): BendingItem
|
||||
{
|
||||
$opts = $item->options ?? [];
|
||||
|
||||
// item_name: options.item_name → name 폴백
|
||||
$itemName = $opts['item_name'] ?? null;
|
||||
if (empty($itemName) || $itemName === 'null') {
|
||||
$itemName = $item->name;
|
||||
}
|
||||
|
||||
$bi = BendingItem::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'code' => $item->code,
|
||||
'legacy_code' => $item->code,
|
||||
'legacy_bending_id' => $opts['legacy_bending_num'] ?? null,
|
||||
// 정규 컬럼 (options에서 승격)
|
||||
'item_name' => $itemName,
|
||||
'item_sep' => $this->cleanNull($opts['item_sep'] ?? null),
|
||||
'item_bending' => $this->cleanNull($opts['item_bending'] ?? null),
|
||||
'material' => $this->cleanNull($opts['material'] ?? null),
|
||||
'item_spec' => $this->cleanNull($opts['item_spec'] ?? null),
|
||||
'model_name' => $this->cleanNull($opts['model_name'] ?? null),
|
||||
'model_UA' => $this->cleanNull($opts['model_UA'] ?? null),
|
||||
'rail_width' => $this->toDecimal($opts['rail_width'] ?? null),
|
||||
'exit_direction' => $this->cleanNull($opts['exit_direction'] ?? null),
|
||||
'box_width' => $this->toDecimal($opts['box_width'] ?? null),
|
||||
'box_height' => $this->toDecimal($opts['box_height'] ?? null),
|
||||
'front_bottom' => $this->toDecimal($opts['front_bottom_width'] ?? $opts['front_bottom'] ?? null),
|
||||
'inspection_door' => $this->cleanNull($opts['inspection_door'] ?? null),
|
||||
// 비정형 속성
|
||||
'options' => $this->buildMetaOptions($opts),
|
||||
'is_active' => $item->is_active,
|
||||
'created_by' => $item->created_by,
|
||||
'updated_by' => $item->updated_by,
|
||||
]);
|
||||
|
||||
$this->line(" ✅ {$item->code} → bending_items#{$bi->id} ({$itemName})");
|
||||
|
||||
return $bi;
|
||||
}
|
||||
|
||||
private function migrateBendingData(BendingItem $bi, Item $item): int
|
||||
{
|
||||
$opts = $item->options ?? [];
|
||||
$bendingData = $opts['bendingData'] ?? [];
|
||||
|
||||
if (empty($bendingData) || ! is_array($bendingData)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// bending_items.bending_data JSON 컬럼에 저장
|
||||
$bi->update(['bending_data' => $bendingData]);
|
||||
|
||||
return count($bendingData);
|
||||
}
|
||||
|
||||
private function rollback(int $tenantId): int
|
||||
{
|
||||
$biCount = BendingItem::where('tenant_id', $tenantId)->count();
|
||||
BendingItem::where('tenant_id', $tenantId)->forceDelete();
|
||||
$this->info("롤백 완료: bending_items {$biCount}건 삭제");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function previewItems($items): void
|
||||
{
|
||||
$headers = ['code', 'name', 'item_name(opts)', 'item_sep', 'material', 'has_bd'];
|
||||
$rows = $items->take(20)->map(function ($item) {
|
||||
$opts = $item->options ?? [];
|
||||
return [
|
||||
$item->code,
|
||||
mb_substr($item->name, 0, 20),
|
||||
mb_substr($opts['item_name'] ?? '(NULL)', 0, 20),
|
||||
$opts['item_sep'] ?? '-',
|
||||
$opts['material'] ?? '-',
|
||||
! empty($opts['bendingData']) ? '✅' : '❌',
|
||||
];
|
||||
});
|
||||
$this->table($headers, $rows);
|
||||
|
||||
$nullNameCount = $items->filter(fn ($i) => empty(($i->options ?? [])['item_name']))->count();
|
||||
$hasBdCount = $items->filter(fn ($i) => ! empty(($i->options ?? [])['bendingData']))->count();
|
||||
$this->info("item_name NULL: {$nullNameCount}건 (name 필드로 폴백)");
|
||||
$this->info("bendingData 있음: {$hasBdCount}건");
|
||||
}
|
||||
|
||||
private function cleanNull(?string $value): ?string
|
||||
{
|
||||
if ($value === null || $value === 'null' || $value === '') {
|
||||
return null;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function toDecimal(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === 'null' || $value === '') {
|
||||
return null;
|
||||
}
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* options에 남길 비정형 속성만 추출
|
||||
*/
|
||||
private function buildMetaOptions(array $opts): ?array
|
||||
{
|
||||
$metaKeys = ['search_keyword', 'registration_date', 'author', 'memo', 'parent_num', 'modified_by'];
|
||||
$meta = [];
|
||||
foreach ($metaKeys as $key) {
|
||||
$val = $opts[$key] ?? null;
|
||||
if ($val !== null && $val !== 'null' && $val !== '') {
|
||||
$meta[$key] = $val;
|
||||
}
|
||||
}
|
||||
return empty($meta) ? null : $meta;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ 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']);
|
||||
$params = $request->only(['item_category', 'item_sep', 'model_UA', 'check_type', 'model_name', 'exit_direction', 'search', 'page', 'size']);
|
||||
$paginator = $this->service->list($params);
|
||||
$paginator->getCollection()->transform(fn ($item) => (new GuiderailModelResource($item))->resolve());
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Item\ItemFileUploadRequest;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\BendingModel;
|
||||
use App\Models\Items\Item;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -53,12 +55,13 @@ public function index(int $id, Request $request)
|
||||
$fieldKey = $request->input('field_key');
|
||||
|
||||
// 품목 존재 확인
|
||||
$this->getItemById($id, $tenantId);
|
||||
$owner = $this->getItemById($id, $tenantId);
|
||||
$docType = $this->getDocumentType($owner);
|
||||
|
||||
// 파일 조회
|
||||
$query = File::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('document_type', self::ITEM_GROUP_ID)
|
||||
->where('document_type', $docType)
|
||||
->where('document_id', $id);
|
||||
|
||||
// 특정 field_key만 조회
|
||||
@@ -94,7 +97,8 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
||||
$existingFileId = $validated['file_id'] ?? null;
|
||||
|
||||
// 품목 존재 확인
|
||||
$this->getItemById($id, $tenantId);
|
||||
$owner = $this->getItemById($id, $tenantId);
|
||||
$docType = $this->getDocumentType($owner);
|
||||
|
||||
$replaced = false;
|
||||
|
||||
@@ -102,7 +106,7 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
||||
if ($existingFileId) {
|
||||
$existingFile = File::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('document_type', self::ITEM_GROUP_ID)
|
||||
->where('document_type', $docType)
|
||||
->where('document_id', $id)
|
||||
->where('id', $existingFileId)
|
||||
->first();
|
||||
@@ -142,7 +146,7 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
||||
'file_type' => $fileType, // 파일 형식 (image, document, excel, archive)
|
||||
'field_key' => $fieldKey, // 비즈니스 용도 (drawing, certificate 등)
|
||||
'document_id' => $id,
|
||||
'document_type' => self::ITEM_GROUP_ID, // group_id
|
||||
'document_type' => $docType,
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => $userId,
|
||||
'created_by' => $userId,
|
||||
@@ -175,12 +179,13 @@ public function delete(int $id, mixed $fileId, Request $request)
|
||||
$tenantId = app('tenant_id');
|
||||
|
||||
// 품목 존재 확인
|
||||
$this->getItemById($id, $tenantId);
|
||||
$owner = $this->getItemById($id, $tenantId);
|
||||
$docType = $this->getDocumentType($owner);
|
||||
|
||||
// 파일 조회
|
||||
$file = File::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('document_type', self::ITEM_GROUP_ID)
|
||||
->where('document_type', $docType)
|
||||
->where('document_id', $id)
|
||||
->where('id', $fileId)
|
||||
->first();
|
||||
@@ -200,19 +205,51 @@ public function delete(int $id, mixed $fileId, Request $request)
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 품목 조회 (통합 items 테이블)
|
||||
* ID로 품목 조회 (items → bending_items 폴백)
|
||||
*/
|
||||
private function getItemById(int $id, int $tenantId): Item
|
||||
private function getItemById(int $id, int $tenantId): Item|BendingItem|BendingModel
|
||||
{
|
||||
$item = Item::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $item) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
if ($item) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
return $item;
|
||||
// bending_items 폴백
|
||||
$bendingItem = BendingItem::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if ($bendingItem) {
|
||||
return $bendingItem;
|
||||
}
|
||||
|
||||
// bending_models 폴백
|
||||
$bendingModel = BendingModel::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->find($id);
|
||||
|
||||
if ($bendingModel) {
|
||||
return $bendingModel;
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 유형에 따른 document_type 반환
|
||||
*/
|
||||
private function getDocumentType(Item|BendingItem|BendingModel $item): string
|
||||
{
|
||||
if ($item instanceof BendingItem) {
|
||||
return 'bending_item';
|
||||
}
|
||||
if ($item instanceof BendingModel) {
|
||||
return 'bending_model';
|
||||
}
|
||||
return self::ITEM_GROUP_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ public function rules(): array
|
||||
'material' => 'nullable|string',
|
||||
'model_UA' => 'nullable|string|in:인정,비인정',
|
||||
'model_name' => 'nullable|string',
|
||||
'legacy_bending_num' => 'nullable|integer',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
'size' => 'nullable|integer|min:1|max:200',
|
||||
|
||||
@@ -14,9 +14,10 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => 'required|string|max:100|unique:items,code',
|
||||
'name' => 'required|string|max:200',
|
||||
'unit' => 'nullable|string|max:20',
|
||||
'code' => [
|
||||
'required', 'string', 'max:50',
|
||||
\Illuminate\Validation\Rule::unique('bending_items', 'code')->where('tenant_id', request()->header('X-TENANT-ID', app()->bound('tenant_id') ? app('tenant_id') : 1)),
|
||||
],
|
||||
'item_name' => 'required|string|max:50',
|
||||
'item_sep' => 'required|in:스크린,철재',
|
||||
'item_bending' => 'required|string|max:50',
|
||||
|
||||
@@ -12,61 +12,61 @@ public function toArray(Request $request): array
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'code' => $this->code,
|
||||
'name' => $this->name,
|
||||
'item_type' => $this->item_type,
|
||||
'item_category' => $this->item_category,
|
||||
'unit' => $this->unit,
|
||||
'is_active' => $this->is_active,
|
||||
// options → 최상위로 노출
|
||||
'item_name' => $this->getOption('item_name'),
|
||||
'item_sep' => $this->getOption('item_sep'),
|
||||
'item_bending' => $this->getOption('item_bending'),
|
||||
'item_spec' => $this->getOption('item_spec'),
|
||||
'material' => $this->getOption('material'),
|
||||
'model_name' => $this->getOption('model_name'),
|
||||
'model_UA' => $this->getOption('model_UA'),
|
||||
'legacy_code' => $this->legacy_code,
|
||||
// 정규 컬럼 직접 참조
|
||||
'item_name' => $this->item_name,
|
||||
'item_sep' => $this->item_sep,
|
||||
'item_bending' => $this->item_bending,
|
||||
'item_spec' => $this->item_spec,
|
||||
'material' => $this->material,
|
||||
'model_name' => $this->model_name,
|
||||
'model_UA' => $this->model_UA,
|
||||
'rail_width' => $this->rail_width ? (int) $this->rail_width : null,
|
||||
// 케이스 전용
|
||||
'exit_direction' => $this->exit_direction,
|
||||
'front_bottom' => $this->front_bottom ? (int) $this->front_bottom : null,
|
||||
'box_width' => $this->box_width ? (int) $this->box_width : null,
|
||||
'box_height' => $this->box_height ? (int) $this->box_height : null,
|
||||
'inspection_door' => $this->inspection_door,
|
||||
// 원자재 길이
|
||||
'length_code' => $this->length_code,
|
||||
'length_mm' => $this->length_mm,
|
||||
// 전개도 (JSON 컬럼)
|
||||
'bendingData' => $this->bending_data,
|
||||
// 비정형 속성 (options)
|
||||
'search_keyword' => $this->getOption('search_keyword'),
|
||||
'rail_width' => $this->getOption('rail_width'),
|
||||
'registration_date' => $this->getOption('registration_date'),
|
||||
'author' => $this->getOption('author'),
|
||||
'memo' => $this->getOption('memo'),
|
||||
// 케이스 전용
|
||||
'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'),
|
||||
// 전개도
|
||||
'bendingData' => $this->getOption('bendingData'),
|
||||
// PREFIX 관련
|
||||
'prefix' => $this->getOption('prefix'),
|
||||
'length_code' => $this->getOption('length_code'),
|
||||
'length_mm' => $this->getOption('length_mm'),
|
||||
'registration_date' => $this->getOption('registration_date'),
|
||||
// 이미지
|
||||
'image_file_id' => $this->getImageFileId(),
|
||||
// 추적
|
||||
'legacy_bending_num' => $this->getOption('legacy_bending_num'),
|
||||
'legacy_bending_id' => $this->legacy_bending_id,
|
||||
'legacy_bending_num' => $this->legacy_bending_id, // MNG2 호환
|
||||
'modified_by' => $this->getOption('modified_by'),
|
||||
// MNG2 호환 (items 기반 필드명)
|
||||
'name' => $this->item_name,
|
||||
'front_bottom_width' => $this->front_bottom ? (int) $this->front_bottom : null,
|
||||
'item_type' => 'PT',
|
||||
'item_category' => 'BENDING',
|
||||
'unit' => 'EA',
|
||||
// 계산값
|
||||
'width_sum' => $this->getWidthSum(),
|
||||
'bend_count' => $this->getBendCount(),
|
||||
'width_sum' => $this->width_sum,
|
||||
'bend_count' => $this->bend_count,
|
||||
// 메타
|
||||
'is_active' => $this->is_active,
|
||||
'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 getWidthSum(): ?int
|
||||
private function getImageFileId(): ?int
|
||||
{
|
||||
$data = $this->getOption('bendingData', []);
|
||||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
$last = end($data);
|
||||
$file = $this->files()
|
||||
->where('field_key', 'bending_diagram')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
return isset($last['sum']) ? (int) $last['sum'] : null;
|
||||
}
|
||||
|
||||
private function getBendCount(): int
|
||||
{
|
||||
$data = $this->getOption('bendingData', []);
|
||||
|
||||
return count(array_filter($data, fn ($d) => ($d['rate'] ?? '') !== ''));
|
||||
return $file?->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ class GuiderailModelResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$components = $this->getOption('components', []);
|
||||
$materialSummary = $this->getOption('material_summary');
|
||||
$components = $this->components ?? [];
|
||||
$materialSummary = $this->material_summary;
|
||||
|
||||
// material_summary가 없으면 components에서 계산
|
||||
if (empty($materialSummary) && ! empty($components)) {
|
||||
$materialSummary = $this->calcMaterialSummary($components);
|
||||
}
|
||||
@@ -21,29 +20,34 @@ public function toArray(Request $request): array
|
||||
'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'),
|
||||
// MNG2 호환
|
||||
'item_type' => 'FG',
|
||||
'item_category' => $this->model_type,
|
||||
// 모델 속성 (정규 컬럼)
|
||||
'model_name' => $this->model_name,
|
||||
'check_type' => $this->check_type,
|
||||
'rail_width' => $this->rail_width ? (int) $this->rail_width : null,
|
||||
'rail_length' => $this->rail_length ? (int) $this->rail_length : null,
|
||||
'finishing_type' => $this->finishing_type,
|
||||
'item_sep' => $this->item_sep,
|
||||
'model_UA' => $this->model_UA,
|
||||
'search_keyword' => $this->search_keyword,
|
||||
'author' => $this->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'),
|
||||
'registration_date' => $this->registration_date?->format('Y-m-d'),
|
||||
// 케이스 전용
|
||||
'exit_direction' => $this->exit_direction,
|
||||
'front_bottom_width' => $this->front_bottom_width ? (int) $this->front_bottom_width : null,
|
||||
'box_width' => $this->box_width ? (int) $this->box_width : null,
|
||||
'box_height' => $this->box_height ? (int) $this->box_height : null,
|
||||
// 하단마감재 전용
|
||||
'bar_width' => $this->bar_width ? (int) $this->bar_width : null,
|
||||
'bar_height' => $this->bar_height ? (int) $this->bar_height : null,
|
||||
// 수정자
|
||||
'modified_by' => $this->getOption('modified_by'),
|
||||
// 이미지
|
||||
'image_file_id' => $this->getImageFileId(),
|
||||
// 부품 조합
|
||||
'components' => $components,
|
||||
'material_summary' => $materialSummary,
|
||||
@@ -54,18 +58,36 @@ public function toArray(Request $request): array
|
||||
];
|
||||
}
|
||||
|
||||
private function getImageFileId(): ?int
|
||||
{
|
||||
$file = \App\Models\Commons\File::where('document_id', $this->id)
|
||||
->where('document_type', 'bending_model')
|
||||
->where('field_key', 'assembly_image')
|
||||
->whereNull('deleted_at')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $file) {
|
||||
$file = $this->files()
|
||||
->where('field_key', 'bending_diagram')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
return $file?->id;
|
||||
}
|
||||
|
||||
private function calcMaterialSummary(array $components): array
|
||||
{
|
||||
$summary = [];
|
||||
foreach ($components as $comp) {
|
||||
$material = $comp['material'] ?? null;
|
||||
$widthSum = $comp['width_sum'] ?? 0;
|
||||
$widthSum = $comp['widthsum'] ?? $comp['width_sum'] ?? 0;
|
||||
$qty = $comp['quantity'] ?? 1;
|
||||
if ($material && $widthSum) {
|
||||
$summary[$material] = ($summary[$material] ?? 0) + ($widthSum * $qty);
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
|
||||
136
app/Models/BendingItem.php
Normal file
136
app/Models/BendingItem.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 절곡 기초관리 마스터
|
||||
*
|
||||
* code: {제품Code}{종류Code}{YYMMDD} (예: CP260319 = 케이스 점검구)
|
||||
* bending_data: 전개도 JSON 배열 [{no, input, rate, sum, color, aAngle}]
|
||||
*/
|
||||
class BendingItem extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'bending_items';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'code',
|
||||
'legacy_code',
|
||||
'legacy_bending_id',
|
||||
'item_name',
|
||||
'item_sep',
|
||||
'item_bending',
|
||||
'material',
|
||||
'item_spec',
|
||||
'model_name',
|
||||
'model_UA',
|
||||
'rail_width',
|
||||
'exit_direction',
|
||||
'box_width',
|
||||
'box_height',
|
||||
'front_bottom',
|
||||
'inspection_door',
|
||||
'length_code',
|
||||
'length_mm',
|
||||
'bending_data',
|
||||
'options',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'bending_data' => 'array',
|
||||
'options' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'rail_width' => 'decimal:2',
|
||||
'box_width' => 'decimal:2',
|
||||
'box_height' => 'decimal:2',
|
||||
'front_bottom' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected $hidden = ['deleted_at'];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 관계
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function files(): HasMany
|
||||
{
|
||||
return $this->hasMany(File::class, 'document_id')
|
||||
->where('document_type', 'bending_item');
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// options 헬퍼
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->options, $key, $default);
|
||||
}
|
||||
|
||||
public function setOption(string $key, mixed $value): self
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
data_set($options, $key, $value);
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 계산 accessor
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function getWidthSumAttribute(): ?float
|
||||
{
|
||||
$data = $this->bending_data ?? [];
|
||||
if (empty($data)) {
|
||||
return null;
|
||||
}
|
||||
$last = end($data);
|
||||
|
||||
return isset($last['sum']) ? (float) $last['sum'] : null;
|
||||
}
|
||||
|
||||
public function getBendCountAttribute(): int
|
||||
{
|
||||
$data = $this->bending_data ?? [];
|
||||
|
||||
return count(array_filter($data, fn ($d) => ($d['rate'] ?? '') !== ''));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// LOT 코드 테이블
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
public const PROD_CODES = [
|
||||
'R' => '가이드레일(벽면형)',
|
||||
'S' => '가이드레일(측면형)',
|
||||
'C' => '케이스',
|
||||
'B' => '하단마감재(스크린)',
|
||||
'T' => '하단마감재(철재)',
|
||||
'L' => 'L-Bar',
|
||||
'G' => '연기차단재',
|
||||
];
|
||||
|
||||
public const SPEC_CODES = [
|
||||
'R' => ['M' => '본체', 'T' => '본체(철재)', 'C' => 'C형', 'D' => 'D형', 'S' => 'SUS 마감재'],
|
||||
'S' => ['M' => '본체디딤', 'T' => '본체(철재)', 'C' => 'C형', 'D' => 'D형', 'S' => 'SUS 마감재①', 'U' => 'SUS 마감재②'],
|
||||
'C' => ['F' => '전면부', 'P' => '점검구', 'L' => '린텔부', 'B' => '후면코너부'],
|
||||
'B' => ['S' => 'SUS', 'E' => 'EGI'],
|
||||
'T' => ['S' => 'SUS', 'E' => 'EGI'],
|
||||
'L' => ['A' => '스크린용'],
|
||||
'G' => ['I' => '화이바원단'],
|
||||
];
|
||||
}
|
||||
68
app/Models/BendingModel.php
Normal file
68
app/Models/BendingModel.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BendingModel extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'bending_models';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'model_type', 'code', 'name', 'legacy_code', 'legacy_num',
|
||||
'model_name', 'model_UA', 'item_sep', 'finishing_type', 'author', 'remark',
|
||||
'check_type', 'rail_width', 'rail_length',
|
||||
'exit_direction', 'front_bottom_width', 'box_width', 'box_height',
|
||||
'bar_width', 'bar_height',
|
||||
'components', 'material_summary',
|
||||
'search_keyword', 'registration_date', 'options',
|
||||
'is_active', 'created_by', 'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'components' => 'array',
|
||||
'material_summary' => 'array',
|
||||
'options' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'registration_date' => 'date',
|
||||
'rail_width' => 'decimal:2',
|
||||
'rail_length' => 'decimal:2',
|
||||
'front_bottom_width' => 'decimal:2',
|
||||
'box_width' => 'decimal:2',
|
||||
'box_height' => 'decimal:2',
|
||||
'bar_width' => 'decimal:2',
|
||||
'bar_height' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected $hidden = ['deleted_at'];
|
||||
|
||||
public function files(): HasMany
|
||||
{
|
||||
return $this->hasMany(File::class, 'document_id')
|
||||
->where('document_type', 'bending_model');
|
||||
}
|
||||
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->options, $key, $default);
|
||||
}
|
||||
|
||||
public function setOption(string $key, mixed $value): self
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
data_set($options, $key, $value);
|
||||
$this->options = $options;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public const TYPE_GUIDERAIL = 'GUIDERAIL_MODEL';
|
||||
public const TYPE_SHUTTERBOX = 'SHUTTERBOX_MODEL';
|
||||
public const TYPE_BOTTOMBAR = 'BOTTOMBAR_MODEL';
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BendingItemMapping extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'bending_item_mappings';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'prod_code',
|
||||
'spec_code',
|
||||
'length_code',
|
||||
'item_id',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function item(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Item::class);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Production\BendingItemMapping;
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\Orders\Order;
|
||||
|
||||
class BendingCodeService extends Service
|
||||
{
|
||||
@@ -127,28 +128,38 @@ public function getCodeMap(): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭다운 선택 조합 → items 테이블 품목 매핑 조회
|
||||
* 드롭다운 선택 조합 → bending_items 품목 매핑 조회
|
||||
*
|
||||
* legacy_code 패턴: BD-{prod}{spec}-{length} (예: BD-CP-30)
|
||||
*/
|
||||
public function resolveItem(string $prodCode, string $specCode, string $lengthCode): ?array
|
||||
{
|
||||
$mapping = BendingItemMapping::where('tenant_id', $this->tenantId())
|
||||
->where('prod_code', $prodCode)
|
||||
->where('spec_code', $specCode)
|
||||
// 1차: code + length_code로 조회 (신규 LOT 체계)
|
||||
$item = BendingItem::where('tenant_id', $this->tenantId())
|
||||
->where('code', 'like', "{$prodCode}{$specCode}%")
|
||||
->where('length_code', $lengthCode)
|
||||
->where('is_active', true)
|
||||
->with('item:id,code,name,attributes,unit')
|
||||
->first();
|
||||
|
||||
if (! $mapping || ! $mapping->item) {
|
||||
// 2차: legacy_code 폴백
|
||||
if (! $item) {
|
||||
$legacyCode = "BD-{$prodCode}{$specCode}-{$lengthCode}";
|
||||
$item = BendingItem::where('tenant_id', $this->tenantId())
|
||||
->where('legacy_code', $legacyCode)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'item_id' => $mapping->item->id,
|
||||
'item_code' => $mapping->item->code,
|
||||
'item_name' => $mapping->item->name,
|
||||
'specification' => $mapping->item->specification,
|
||||
'unit' => $mapping->item->unit ?? 'EA',
|
||||
'item_id' => $item->id,
|
||||
'item_code' => $item->code,
|
||||
'item_name' => $item->item_name,
|
||||
'specification' => $item->item_spec,
|
||||
'unit' => 'EA',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,80 +2,102 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\BendingItem;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class BendingItemService extends Service
|
||||
{
|
||||
public function list(array $params): LengthAwarePaginator
|
||||
{
|
||||
return Item::where('item_category', 'BENDING')
|
||||
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('options->item_sep', $v))
|
||||
->when($params['item_bending'] ?? null, fn ($q, $v) => $q->where('options->item_bending', $v))
|
||||
->when($params['material'] ?? null, fn ($q, $v) => $q->where('options->material', 'like', "%{$v}%"))
|
||||
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('options->model_UA', $v))
|
||||
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('options->model_name', $v))
|
||||
return BendingItem::query()
|
||||
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('item_sep', $v))
|
||||
->when($params['item_bending'] ?? null, fn ($q, $v) => $q->where('item_bending', $v))
|
||||
->when($params['material'] ?? null, fn ($q, $v) => $q->where('material', 'like', "%{$v}%"))
|
||||
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('model_UA', $v))
|
||||
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('model_name', $v))
|
||||
->when($params['legacy_bending_num'] ?? $params['legacy_bending_id'] ?? null, fn ($q, $v) => $q->where('legacy_bending_id', (int) $v))
|
||||
->when($params['search'] ?? null, fn ($q, $v) => $q->where(
|
||||
fn ($q2) => $q2
|
||||
->where('name', 'like', "%{$v}%")
|
||||
->where('item_name', 'like', "%{$v}%")
|
||||
->orWhere('code', 'like', "%{$v}%")
|
||||
->orWhere('options->search_keyword', 'like', "%{$v}%")
|
||||
->orWhere('options->item_spec', 'like', "%{$v}%")
|
||||
->orWhere('item_spec', 'like', "%{$v}%")
|
||||
->orWhere('legacy_code', 'like', "%{$v}%")
|
||||
))
|
||||
->orderBy('code')
|
||||
->orderByDesc('id')
|
||||
->paginate($params['size'] ?? 50);
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
$items = Item::where('item_category', 'BENDING')
|
||||
->select('options')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'item_sep' => $items->pluck('options.item_sep')->filter()->unique()->sort()->values(),
|
||||
'item_bending' => $items->pluck('options.item_bending')->filter()->unique()->sort()->values(),
|
||||
'material' => $items->pluck('options.material')->filter()->unique()->sort()->values(),
|
||||
'model_UA' => $items->pluck('options.model_UA')->filter()->unique()->sort()->values(),
|
||||
'model_name' => $items->pluck('options.model_name')->filter()->unique()->sort()->values(),
|
||||
'item_sep' => BendingItem::whereNotNull('item_sep')->distinct()->pluck('item_sep')->sort()->values(),
|
||||
'item_bending' => BendingItem::whereNotNull('item_bending')->distinct()->pluck('item_bending')->sort()->values(),
|
||||
'material' => BendingItem::whereNotNull('material')->distinct()->pluck('material')->sort()->values(),
|
||||
'model_UA' => BendingItem::whereNotNull('model_UA')->distinct()->pluck('model_UA')->sort()->values(),
|
||||
'model_name' => BendingItem::whereNotNull('model_name')->distinct()->pluck('model_name')->sort()->values(),
|
||||
];
|
||||
}
|
||||
|
||||
public function find(int $id): Item
|
||||
public function find(int $id): BendingItem
|
||||
{
|
||||
return Item::where('item_category', 'BENDING')->findOrFail($id);
|
||||
return BendingItem::findOrFail($id);
|
||||
}
|
||||
|
||||
public function create(array $data): Item
|
||||
public function create(array $data): BendingItem
|
||||
{
|
||||
$options = $this->buildOptions($data);
|
||||
|
||||
return Item::create([
|
||||
return BendingItem::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'item_type' => 'PT',
|
||||
'item_category' => 'BENDING',
|
||||
'code' => $data['code'],
|
||||
'name' => $data['name'],
|
||||
'unit' => $data['unit'] ?? 'EA',
|
||||
'options' => $options,
|
||||
'legacy_code' => $data['legacy_code'] ?? null,
|
||||
'legacy_bending_id' => $data['legacy_bending_id'] ?? null,
|
||||
'item_name' => $data['item_name'] ?? $data['name'] ?? '',
|
||||
'item_sep' => $data['item_sep'] ?? null,
|
||||
'item_bending' => $data['item_bending'] ?? null,
|
||||
'material' => $data['material'] ?? null,
|
||||
'item_spec' => $data['item_spec'] ?? null,
|
||||
'model_name' => $data['model_name'] ?? null,
|
||||
'model_UA' => $data['model_UA'] ?? null,
|
||||
'rail_width' => $data['rail_width'] ?? null,
|
||||
'exit_direction' => $data['exit_direction'] ?? null,
|
||||
'box_width' => $data['box_width'] ?? null,
|
||||
'box_height' => $data['box_height'] ?? null,
|
||||
'front_bottom' => $data['front_bottom'] ?? $data['front_bottom_width'] ?? null,
|
||||
'inspection_door' => $data['inspection_door'] ?? null,
|
||||
'length_code' => $data['length_code'] ?? null,
|
||||
'length_mm' => $data['length_mm'] ?? null,
|
||||
'bending_data' => $data['bendingData'] ?? null,
|
||||
'options' => $this->buildOptions($data),
|
||||
'is_active' => true,
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): Item
|
||||
public function update(int $id, array $data): BendingItem
|
||||
{
|
||||
$item = Item::where('item_category', 'BENDING')->findOrFail($id);
|
||||
$item = BendingItem::findOrFail($id);
|
||||
|
||||
if (isset($data['code'])) {
|
||||
$item->code = $data['code'];
|
||||
$columns = [
|
||||
'code', 'item_name', 'item_sep', 'item_bending',
|
||||
'material', 'item_spec', 'model_name', 'model_UA',
|
||||
'rail_width', 'exit_direction', 'box_width', 'box_height',
|
||||
'front_bottom', 'inspection_door', 'length_code', 'length_mm',
|
||||
];
|
||||
foreach ($columns as $col) {
|
||||
if (array_key_exists($col, $data)) {
|
||||
$item->{$col} = $data[$col];
|
||||
}
|
||||
}
|
||||
if (isset($data['name'])) {
|
||||
$item->name = $data['name'];
|
||||
if (array_key_exists('front_bottom_width', $data) && ! array_key_exists('front_bottom', $data)) {
|
||||
$item->front_bottom = $data['front_bottom_width'];
|
||||
}
|
||||
|
||||
$optionKeys = self::OPTION_KEYS;
|
||||
foreach ($optionKeys as $key) {
|
||||
// 전개도 (JSON 직접 저장)
|
||||
if (array_key_exists('bendingData', $data)) {
|
||||
$item->bending_data = $data['bendingData'];
|
||||
}
|
||||
|
||||
// 비정형 속성
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
$item->setOption($key, $data[$key]);
|
||||
}
|
||||
@@ -89,14 +111,14 @@ public function update(int $id, array $data): Item
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$item = Item::where('item_category', 'BENDING')->findOrFail($id);
|
||||
$item = BendingItem::findOrFail($id);
|
||||
$item->deleted_by = $this->apiUserId();
|
||||
$item->save();
|
||||
|
||||
return $item->delete();
|
||||
}
|
||||
|
||||
private function buildOptions(array $data): array
|
||||
private function buildOptions(array $data): ?array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
@@ -105,15 +127,11 @@ private function buildOptions(array $data): array
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
return empty($options) ? null : $options;
|
||||
}
|
||||
|
||||
private const OPTION_KEYS = [
|
||||
'item_name', 'item_sep', 'item_bending', 'item_spec',
|
||||
'material', 'model_name', 'model_UA', 'search_keyword',
|
||||
'rail_width', 'registration_date', 'author', 'memo',
|
||||
'parent_num', 'exit_direction', 'front_bottom_width',
|
||||
'box_width', 'box_height', 'bendingData',
|
||||
'prefix', 'length_code', 'length_mm',
|
||||
'search_keyword', 'registration_date', 'author', 'memo',
|
||||
'parent_num', 'modified_by',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\BendingModel;
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class GuiderailModelService extends Service
|
||||
{
|
||||
@@ -11,75 +13,109 @@ class GuiderailModelService extends Service
|
||||
|
||||
public function list(array $params): LengthAwarePaginator
|
||||
{
|
||||
return Item::whereIn('item_category', self::CATEGORIES)
|
||||
->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))
|
||||
return BendingModel::whereIn('model_type', self::CATEGORIES)
|
||||
->when($params['item_category'] ?? null, fn ($q, $v) => $q->where('model_type', $v))
|
||||
->when($params['item_sep'] ?? null, fn ($q, $v) => $q->where('item_sep', $v))
|
||||
->when($params['model_UA'] ?? null, fn ($q, $v) => $q->where('model_UA', $v))
|
||||
->when($params['check_type'] ?? null, fn ($q, $v) => $q->where('check_type', $v))
|
||||
->when($params['model_name'] ?? null, fn ($q, $v) => $q->where('model_name', $v))
|
||||
->when($params['exit_direction'] ?? null, fn ($q, $v) => $q->where('exit_direction', $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}%")
|
||||
->orWhere('model_name', 'like', "%{$v}%")
|
||||
->orWhere('search_keyword', 'like', "%{$v}%")
|
||||
))
|
||||
->orderBy('code')
|
||||
->orderByDesc('id')
|
||||
->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(),
|
||||
'item_sep' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('item_sep')->distinct()->pluck('item_sep')->sort()->values(),
|
||||
'model_UA' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('model_UA')->distinct()->pluck('model_UA')->sort()->values(),
|
||||
'check_type' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('check_type')->distinct()->pluck('check_type')->sort()->values(),
|
||||
'model_name' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('model_name')->distinct()->pluck('model_name')->sort()->values(),
|
||||
'finishing_type' => BendingModel::whereIn('model_type', self::CATEGORIES)->whereNotNull('finishing_type')->distinct()->pluck('finishing_type')->sort()->values(),
|
||||
];
|
||||
}
|
||||
|
||||
public function find(int $id): Item
|
||||
public function find(int $id): BendingModel
|
||||
{
|
||||
return Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
return BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
}
|
||||
|
||||
public function create(array $data): Item
|
||||
public function create(array $data): BendingModel
|
||||
{
|
||||
$options = $this->buildOptions($data);
|
||||
// component 이미지 복사 (기초관리 원본 → 독립 복사본)
|
||||
if (! empty($data['components'])) {
|
||||
$data['components'] = $this->copyComponentImages($data['components']);
|
||||
}
|
||||
|
||||
return Item::create([
|
||||
return BendingModel::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'item_type' => 'FG',
|
||||
'item_category' => $data['item_category'] ?? 'GUIDERAIL_MODEL',
|
||||
'model_type' => $data['item_category'] ?? 'GUIDERAIL_MODEL',
|
||||
'code' => $data['code'],
|
||||
'name' => $data['name'],
|
||||
'unit' => 'SET',
|
||||
'options' => $options,
|
||||
'model_name' => $data['model_name'] ?? null,
|
||||
'model_UA' => $data['model_UA'] ?? null,
|
||||
'item_sep' => $data['item_sep'] ?? null,
|
||||
'finishing_type' => $data['finishing_type'] ?? null,
|
||||
'check_type' => $data['check_type'] ?? null,
|
||||
'rail_width' => $data['rail_width'] ?? null,
|
||||
'rail_length' => $data['rail_length'] ?? null,
|
||||
'exit_direction' => $data['exit_direction'] ?? null,
|
||||
'front_bottom_width' => $data['front_bottom_width'] ?? null,
|
||||
'box_width' => $data['box_width'] ?? null,
|
||||
'box_height' => $data['box_height'] ?? null,
|
||||
'bar_width' => $data['bar_width'] ?? null,
|
||||
'bar_height' => $data['bar_height'] ?? null,
|
||||
'components' => $data['components'] ?? null,
|
||||
'material_summary' => $data['material_summary'] ?? null,
|
||||
'search_keyword' => $data['search_keyword'] ?? null,
|
||||
'author' => $data['author'] ?? null,
|
||||
'memo' => $data['memo'] ?? null,
|
||||
'registration_date' => $data['registration_date'] ?? null,
|
||||
'options' => $this->buildOptions($data),
|
||||
'is_active' => true,
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): Item
|
||||
public function update(int $id, array $data): BendingModel
|
||||
{
|
||||
$item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
$item = BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
|
||||
if (isset($data['code'])) {
|
||||
$item->code = $data['code'];
|
||||
}
|
||||
if (isset($data['name'])) {
|
||||
$item->name = $data['name'];
|
||||
}
|
||||
$columns = [
|
||||
'code', 'name', 'model_name', 'model_UA', 'item_sep', 'finishing_type',
|
||||
'check_type', 'rail_width', 'rail_length',
|
||||
'exit_direction', 'front_bottom_width', 'box_width', 'box_height',
|
||||
'bar_width', 'bar_height',
|
||||
'components', 'material_summary',
|
||||
'search_keyword', 'author', 'registration_date',
|
||||
];
|
||||
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
$item->setOption($key, $data[$key]);
|
||||
foreach ($columns as $col) {
|
||||
if (array_key_exists($col, $data)) {
|
||||
// components 저장 시 이미지 복사
|
||||
if ($col === 'components' && ! empty($data[$col])) {
|
||||
$item->{$col} = $this->copyComponentImages($data[$col]);
|
||||
} else {
|
||||
$item->{$col} = $data[$col];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// memo → options
|
||||
if (array_key_exists('memo', $data)) {
|
||||
$item->setOption('memo', $data['memo']);
|
||||
}
|
||||
if (array_key_exists('modified_by', $data)) {
|
||||
$item->setOption('modified_by', $data['modified_by']);
|
||||
}
|
||||
|
||||
$item->updated_by = $this->apiUserId();
|
||||
$item->save();
|
||||
|
||||
@@ -88,33 +124,79 @@ public function update(int $id, array $data): Item
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$item = Item::whereIn('item_category', self::CATEGORIES)->findOrFail($id);
|
||||
$item = BendingModel::whereIn('model_type', self::CATEGORIES)->findOrFail($id);
|
||||
$item->deleted_by = $this->apiUserId();
|
||||
$item->save();
|
||||
|
||||
return $item->delete();
|
||||
}
|
||||
|
||||
private function buildOptions(array $data): array
|
||||
/**
|
||||
* component의 image_file_id가 bending_item 원본이면 복사본 생성
|
||||
*/
|
||||
private function copyComponentImages(array $components): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
if (isset($data[$key])) {
|
||||
$options[$key] = $data[$key];
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
foreach ($components as &$comp) {
|
||||
$fileId = $comp['image_file_id'] ?? null;
|
||||
if (! $fileId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source = File::find($fileId);
|
||||
if (! $source || ! $source->file_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 component_image면 복사 불필요 (이미 독립 복사본)
|
||||
if ($source->field_key === 'component_image') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// bending_item 원본이면 복사
|
||||
try {
|
||||
$extension = pathinfo($source->stored_name, PATHINFO_EXTENSION);
|
||||
$storedName = bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
$directory = sprintf('%d/bending/model-parts/%s/%s', $tenantId, date('Y'), date('m'));
|
||||
$newPath = $directory . '/' . $storedName;
|
||||
|
||||
Storage::disk('r2')->put($newPath, Storage::disk('r2')->get($source->file_path));
|
||||
|
||||
$newFile = File::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'display_name' => $source->display_name,
|
||||
'stored_name' => $storedName,
|
||||
'file_path' => $newPath,
|
||||
'file_size' => $source->file_size,
|
||||
'mime_type' => $source->mime_type,
|
||||
'file_type' => 'image',
|
||||
'field_key' => 'component_image',
|
||||
'document_id' => 0,
|
||||
'document_type' => 'bending_model',
|
||||
'is_temp' => false,
|
||||
'uploaded_by' => $this->apiUserId(),
|
||||
'created_by' => $this->apiUserId(),
|
||||
]);
|
||||
|
||||
$comp['image_file_id'] = $newFile->id;
|
||||
} catch (\Throwable $e) {
|
||||
// 복사 실패 시 원본 ID 유지
|
||||
}
|
||||
}
|
||||
unset($comp);
|
||||
|
||||
return $options;
|
||||
return $components;
|
||||
}
|
||||
|
||||
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',
|
||||
];
|
||||
private function buildOptions(array $data): ?array
|
||||
{
|
||||
$opts = [];
|
||||
foreach (['memo', 'modified_by'] as $key) {
|
||||
if (! empty($data[$key])) {
|
||||
$opts[$key] = $data[$key];
|
||||
}
|
||||
}
|
||||
return empty($opts) ? null : $opts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,47 +3,52 @@
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="BendingItem", description="절곡품 기초관리")
|
||||
* @OA\Tag(name="BendingItem", description="절곡품 기초관리 (bending_items 전용 테이블)")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BendingItem",
|
||||
* @OA\Property(property="id", type="integer", example=15862),
|
||||
* @OA\Property(property="code", type="string", example="BD-BE-30"),
|
||||
* @OA\Property(property="name", type="string", example="하단마감재(스크린) EGI 3000mm"),
|
||||
* @OA\Property(property="item_type", type="string", example="PT"),
|
||||
* @OA\Property(property="item_category", type="string", example="BENDING"),
|
||||
* @OA\Property(property="unit", type="string", example="EA"),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="item_name", type="string", example="하단마감재"),
|
||||
* @OA\Property(property="id", type="integer", example=431),
|
||||
* @OA\Property(property="code", type="string", example="RS260319", description="LOT 코드: {제품Code}{종류Code}{YYMMDD}"),
|
||||
* @OA\Property(property="legacy_code", type="string", nullable=true, example="BD-RS-30", description="이전 코드 (items 기반)"),
|
||||
* @OA\Property(property="item_name", type="string", example="SUS마감재"),
|
||||
* @OA\Property(property="item_sep", type="string", enum={"스크린","철재"}),
|
||||
* @OA\Property(property="item_bending", type="string", example="하단마감재"),
|
||||
* @OA\Property(property="item_spec", type="string", nullable=true, example="60*40"),
|
||||
* @OA\Property(property="material", type="string", example="EGI 1.55T"),
|
||||
* @OA\Property(property="item_bending", type="string", example="가이드레일"),
|
||||
* @OA\Property(property="item_spec", type="string", nullable=true, example="120*70"),
|
||||
* @OA\Property(property="material", type="string", example="SUS 1.2T"),
|
||||
* @OA\Property(property="model_name", type="string", nullable=true, example="KSS01"),
|
||||
* @OA\Property(property="model_UA", type="string", nullable=true, enum={"인정","비인정"}),
|
||||
* @OA\Property(property="rail_width", type="number", nullable=true),
|
||||
* @OA\Property(property="exit_direction", type="string", nullable=true, description="출구방향 (케이스 전용)"),
|
||||
* @OA\Property(property="front_bottom", type="number", nullable=true, description="전면밑 (케이스 전용)"),
|
||||
* @OA\Property(property="box_width", type="number", nullable=true, description="박스폭 (케이스 전용)"),
|
||||
* @OA\Property(property="box_height", type="number", nullable=true, description="박스높이 (케이스 전용)"),
|
||||
* @OA\Property(property="inspection_door", type="string", nullable=true, description="점검구 (케이스 전용)"),
|
||||
* @OA\Property(property="length_code", type="string", nullable=true, example="30", description="원자재 길이코드"),
|
||||
* @OA\Property(property="length_mm", type="integer", nullable=true, example=3000, description="원자재 길이(mm)"),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, description="전개도 데이터 (bending_data 테이블)", @OA\Items(
|
||||
* @OA\Property(property="no", type="integer", description="열 순서"),
|
||||
* @OA\Property(property="input", type="number", description="입력 치수"),
|
||||
* @OA\Property(property="rate", type="string", description="연신율: 빈값/'-1'(하향)/'1'(상향)"),
|
||||
* @OA\Property(property="sum", type="number", description="누적 합계"),
|
||||
* @OA\Property(property="color", type="boolean", description="음영 마킹"),
|
||||
* @OA\Property(property="aAngle", type="boolean", description="A각 표시")
|
||||
* )),
|
||||
* @OA\Property(property="search_keyword", type="string", nullable=true),
|
||||
* @OA\Property(property="rail_width", type="integer", nullable=true),
|
||||
* @OA\Property(property="registration_date", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="author", type="string", nullable=true),
|
||||
* @OA\Property(property="memo", type="string", nullable=true),
|
||||
* @OA\Property(property="exit_direction", type="string", nullable=true),
|
||||
* @OA\Property(property="front_bottom_width", type="integer", nullable=true),
|
||||
* @OA\Property(property="box_width", type="integer", nullable=true),
|
||||
* @OA\Property(property="box_height", type="integer", nullable=true),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, @OA\Items(
|
||||
* @OA\Property(property="no", type="integer"),
|
||||
* @OA\Property(property="input", type="number"),
|
||||
* @OA\Property(property="rate", type="string"),
|
||||
* @OA\Property(property="sum", type="number"),
|
||||
* @OA\Property(property="color", type="boolean"),
|
||||
* @OA\Property(property="aAngle", type="boolean")
|
||||
* )),
|
||||
* @OA\Property(property="prefix", type="string", nullable=true, example="BE"),
|
||||
* @OA\Property(property="length_code", type="string", nullable=true, example="30"),
|
||||
* @OA\Property(property="length_mm", type="integer", nullable=true, example=3000),
|
||||
* @OA\Property(property="legacy_bending_num", type="integer", nullable=true, description="레거시 chandj bending.num (운영 전 삭제 예정)"),
|
||||
* @OA\Property(property="width_sum", type="integer", nullable=true, example=193),
|
||||
* @OA\Property(property="bend_count", type="integer", example=5),
|
||||
* @OA\Property(property="registration_date", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="image_file_id", type="integer", nullable=true, description="절곡 도면 이미지 파일 ID"),
|
||||
* @OA\Property(property="legacy_bending_id", type="integer", nullable=true, description="chandj.bending.num 참조"),
|
||||
* @OA\Property(property="legacy_bending_num", type="integer", nullable=true, description="MNG2 호환 (=legacy_bending_id)"),
|
||||
* @OA\Property(property="modified_by", type="string", nullable=true),
|
||||
* @OA\Property(property="width_sum", type="number", nullable=true, example=203, description="폭합계 (전개도 마지막 sum)"),
|
||||
* @OA\Property(property="bend_count", type="integer", example=5, description="절곡 횟수"),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="name", type="string", description="MNG2 호환 (=item_name)"),
|
||||
* @OA\Property(property="front_bottom_width", type="number", nullable=true, description="MNG2 호환 (=front_bottom)"),
|
||||
* @OA\Property(property="item_type", type="string", example="PT", description="MNG2 호환 (고정값)"),
|
||||
* @OA\Property(property="item_category", type="string", example="BENDING", description="MNG2 호환 (고정값)"),
|
||||
* @OA\Property(property="unit", type="string", example="EA", description="MNG2 호환 (고정값)"),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||
* )
|
||||
@@ -55,14 +60,22 @@ class BendingItemApi
|
||||
* path="/api/v1/bending-items",
|
||||
* tags={"BendingItem"},
|
||||
* summary="절곡품 목록 조회",
|
||||
* description="bending_items 테이블에서 검색. 정규 컬럼 인덱스로 빠른 검색 지원.",
|
||||
* @OA\Parameter(name="item_sep", in="query", required=false, @OA\Schema(type="string", enum={"스크린","철재"})),
|
||||
* @OA\Parameter(name="item_bending", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="material", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="model_UA", in="query", required=false, @OA\Schema(type="string", enum={"인정","비인정"})),
|
||||
* @OA\Parameter(name="search", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="model_name", in="query", required=false, @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="legacy_bending_num", in="query", required=false, @OA\Schema(type="integer"), description="chandj.bending.num으로 검색"),
|
||||
* @OA\Parameter(name="search", in="query", required=false, @OA\Schema(type="string"), description="item_name, code, item_spec, legacy_code 통합 검색"),
|
||||
* @OA\Parameter(name="page", in="query", required=false, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="size", in="query", required=false, @OA\Schema(type="integer", default=50)),
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(
|
||||
* @OA\Property(property="data", type="object",
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/BendingItem")),
|
||||
* @OA\Property(property="total", type="integer")
|
||||
* )
|
||||
* ))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
@@ -72,7 +85,16 @@ public function index() {}
|
||||
* path="/api/v1/bending-items/filters",
|
||||
* tags={"BendingItem"},
|
||||
* summary="절곡품 필터 옵션 조회",
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* description="item_sep, item_bending, material, model_UA, model_name 고유값 목록",
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(
|
||||
* @OA\Property(property="data", type="object",
|
||||
* @OA\Property(property="item_sep", type="array", @OA\Items(type="string")),
|
||||
* @OA\Property(property="item_bending", type="array", @OA\Items(type="string")),
|
||||
* @OA\Property(property="material", type="array", @OA\Items(type="string")),
|
||||
* @OA\Property(property="model_UA", type="array", @OA\Items(type="string")),
|
||||
* @OA\Property(property="model_name", type="array", @OA\Items(type="string"))
|
||||
* )
|
||||
* ))
|
||||
* )
|
||||
*/
|
||||
public function filters() {}
|
||||
@@ -82,8 +104,11 @@ public function filters() {}
|
||||
* path="/api/v1/bending-items/{id}",
|
||||
* tags={"BendingItem"},
|
||||
* summary="절곡품 상세 조회",
|
||||
* description="전개도 데이터(bendingData) 포함",
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* @OA\Response(response=200, description="성공", @OA\JsonContent(
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BendingItem")
|
||||
* ))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
@@ -94,14 +119,33 @@ public function show() {}
|
||||
* tags={"BendingItem"},
|
||||
* summary="절곡품 등록",
|
||||
* @OA\RequestBody(@OA\JsonContent(
|
||||
* required={"code","name","item_name","item_sep","item_bending","material"},
|
||||
* @OA\Property(property="code", type="string"),
|
||||
* @OA\Property(property="name", type="string"),
|
||||
* @OA\Property(property="item_name", type="string"),
|
||||
* required={"code","item_name","item_sep","item_bending","material"},
|
||||
* @OA\Property(property="code", type="string", example="RM260319", description="LOT 코드"),
|
||||
* @OA\Property(property="item_name", type="string", example="본체"),
|
||||
* @OA\Property(property="item_sep", type="string", enum={"스크린","철재"}),
|
||||
* @OA\Property(property="item_bending", type="string"),
|
||||
* @OA\Property(property="material", type="string"),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, @OA\Items(type="object"))
|
||||
* @OA\Property(property="item_bending", type="string", example="가이드레일"),
|
||||
* @OA\Property(property="material", type="string", example="EGI 1.55T"),
|
||||
* @OA\Property(property="item_spec", type="string", nullable=true, example="120*70"),
|
||||
* @OA\Property(property="model_name", type="string", nullable=true, example="KSS01"),
|
||||
* @OA\Property(property="model_UA", type="string", nullable=true, enum={"인정","비인정"}),
|
||||
* @OA\Property(property="rail_width", type="number", nullable=true),
|
||||
* @OA\Property(property="exit_direction", type="string", nullable=true),
|
||||
* @OA\Property(property="front_bottom", type="number", nullable=true),
|
||||
* @OA\Property(property="box_width", type="number", nullable=true),
|
||||
* @OA\Property(property="box_height", type="number", nullable=true),
|
||||
* @OA\Property(property="inspection_door", type="string", nullable=true),
|
||||
* @OA\Property(property="length_code", type="string", nullable=true, example="30"),
|
||||
* @OA\Property(property="length_mm", type="integer", nullable=true, example=3000),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, @OA\Items(
|
||||
* @OA\Property(property="no", type="integer"),
|
||||
* @OA\Property(property="input", type="number"),
|
||||
* @OA\Property(property="rate", type="string"),
|
||||
* @OA\Property(property="sum", type="number"),
|
||||
* @OA\Property(property="color", type="boolean"),
|
||||
* @OA\Property(property="aAngle", type="boolean")
|
||||
* )),
|
||||
* @OA\Property(property="memo", type="string", nullable=true),
|
||||
* @OA\Property(property="author", type="string", nullable=true)
|
||||
* )),
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* )
|
||||
@@ -116,9 +160,18 @@ public function store() {}
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\RequestBody(@OA\JsonContent(
|
||||
* @OA\Property(property="code", type="string"),
|
||||
* @OA\Property(property="name", type="string"),
|
||||
* @OA\Property(property="memo", type="string"),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, @OA\Items(type="object"))
|
||||
* @OA\Property(property="item_name", type="string"),
|
||||
* @OA\Property(property="item_sep", type="string", enum={"스크린","철재"}),
|
||||
* @OA\Property(property="item_bending", type="string"),
|
||||
* @OA\Property(property="material", type="string"),
|
||||
* @OA\Property(property="item_spec", type="string", nullable=true),
|
||||
* @OA\Property(property="model_name", type="string", nullable=true),
|
||||
* @OA\Property(property="model_UA", type="string", nullable=true),
|
||||
* @OA\Property(property="rail_width", type="number", nullable=true),
|
||||
* @OA\Property(property="length_code", type="string", nullable=true),
|
||||
* @OA\Property(property="length_mm", type="integer", nullable=true),
|
||||
* @OA\Property(property="bendingData", type="array", nullable=true, description="전체 교체", @OA\Items(type="object")),
|
||||
* @OA\Property(property="memo", type="string", nullable=true)
|
||||
* )),
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* )
|
||||
@@ -130,6 +183,7 @@ public function update() {}
|
||||
* path="/api/v1/bending-items/{id}",
|
||||
* tags={"BendingItem"},
|
||||
* summary="절곡품 삭제",
|
||||
* description="bending_data(전개도)도 함께 삭제",
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Response(response=200, description="성공")
|
||||
* )
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 절곡 기초관리 전용 테이블
|
||||
*
|
||||
* 기존: items(item_category='BENDING') + options JSON
|
||||
* 변경: bending_items 전용 테이블 + 정규 컬럼
|
||||
*
|
||||
* 이유:
|
||||
* - options JSON 검색 불가 (하장바 등 레거시 검색 누락)
|
||||
* - 인덱싱/정렬/NULL 관리 불가
|
||||
* - bending_item_mappings 테이블 흡수 (code에 통합)
|
||||
*
|
||||
* code 체계: {제품Code}{종류Code}{YYMMDD}
|
||||
* 예: CP260319 = 케이스(C) 점검구(P) 2026-03-19
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bending_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
|
||||
// 코드 체계 (LOT 코드 = 제품Code + 종류Code + YYMMDD)
|
||||
$table->string('code', 50)->comment('LOT: {제품}{종류}{YYMMDD} 예: CP260319');
|
||||
$table->string('legacy_code', 50)->nullable()->comment('이전 BD-LEGACY-* / BD-{품명}-* 코드');
|
||||
$table->unsignedInteger('legacy_bending_id')->nullable()->comment('chandj.bending.id 참조');
|
||||
|
||||
// 기본 정보 (기존 options에서 정규 컬럼으로 승격)
|
||||
$table->string('item_name', 100)->comment('품명');
|
||||
$table->string('item_sep', 20)->nullable()->comment('대분류: 스크린/철재');
|
||||
$table->string('item_bending', 50)->nullable()->comment('중분류: 가이드레일/케이스/하단마감재');
|
||||
$table->string('material', 50)->nullable()->comment('재질: SUS 1.2T / EGI 1.55T');
|
||||
$table->string('item_spec', 100)->nullable()->comment('규격: 120*70');
|
||||
$table->string('model_name', 50)->nullable()->comment('소속 모델: KSS01');
|
||||
$table->string('model_UA', 20)->nullable()->comment('인정여부: 인정/비인정');
|
||||
|
||||
// 절곡 전용 속성
|
||||
$table->decimal('rail_width', 10, 2)->nullable()->comment('레일폭');
|
||||
$table->string('exit_direction', 20)->nullable()->comment('출구방향 (케이스 전용)');
|
||||
$table->decimal('box_width', 10, 2)->nullable()->comment('박스폭 (케이스 전용)');
|
||||
$table->decimal('box_height', 10, 2)->nullable()->comment('박스높이 (케이스 전용)');
|
||||
$table->decimal('front_bottom', 10, 2)->nullable()->comment('전면밑 (케이스 전용)');
|
||||
$table->string('inspection_door', 20)->nullable()->comment('점검구 (케이스 전용)');
|
||||
|
||||
// 메타 (비정형 속성만 — 검색/필터 대상 아닌 것)
|
||||
$table->json('options')->nullable()->comment('memo, author, search_keyword, modified_by 등');
|
||||
$table->boolean('is_active')->default(true)->comment('활성 상태');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 인덱스
|
||||
$table->index('tenant_id', 'idx_bi_tenant');
|
||||
$table->index('item_name', 'idx_bi_item_name');
|
||||
$table->index('item_sep', 'idx_bi_item_sep');
|
||||
$table->index('item_bending', 'idx_bi_item_bending');
|
||||
$table->index('material', 'idx_bi_material');
|
||||
$table->index('model_name', 'idx_bi_model_name');
|
||||
$table->index('code', 'idx_bi_code');
|
||||
$table->index('legacy_code', 'idx_bi_legacy_code');
|
||||
$table->unique(['tenant_id', 'code', 'deleted_at'], 'uk_bi_tenant_code');
|
||||
|
||||
// 외래키
|
||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||||
});
|
||||
|
||||
\DB::statement("ALTER TABLE bending_items COMMENT = '절곡 기초관리 마스터 (items 테이블에서 분리)'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bending_items');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 절곡 전개도 데이터 테이블
|
||||
*
|
||||
* 기존: items.options.bendingData JSON 배열
|
||||
* [{no:1, input:10, rate:"", sum:10, color:true, aAngle:false}, ...]
|
||||
*
|
||||
* 변경: bending_data 정규 테이블 (bending_items 1:N)
|
||||
*
|
||||
* 이유:
|
||||
* - JSON 배열은 개별 행 수정/검색 불가
|
||||
* - sort_order로 열 순서 보장
|
||||
* - 정규 컬럼으로 타입 안전성 확보
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bending_data', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bending_item_id')->comment('FK → bending_items.id');
|
||||
$table->smallInteger('sort_order')->unsigned()->comment('열 순서 (1,2,3,...)');
|
||||
|
||||
// 전개도 행 데이터
|
||||
$table->decimal('input', 10, 2)->default(0)->comment('입력 치수');
|
||||
$table->string('rate', 10)->nullable()->comment('연신율: ""(없음), "-1"(하향), "1"(상향)');
|
||||
$table->decimal('after_rate', 10, 2)->nullable()->comment('연신율 적용 후 값 (input + rate)');
|
||||
$table->decimal('sum', 10, 2)->nullable()->comment('누적 합계');
|
||||
$table->boolean('color')->default(false)->comment('음영 마킹 (파란/노란 배경)');
|
||||
$table->boolean('a_angle')->default(false)->comment('A각 표시');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// 인덱스
|
||||
$table->index('bending_item_id', 'idx_bd_bending_item');
|
||||
$table->unique(['bending_item_id', 'sort_order'], 'uk_bd_item_order');
|
||||
|
||||
// 외래키
|
||||
$table->foreign('bending_item_id')
|
||||
->references('id')
|
||||
->on('bending_items')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
\DB::statement("ALTER TABLE bending_data COMMENT = '절곡 전개도 행 데이터 (bending_items 1:N)'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bending_data');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* bending_data 별도 테이블 → bending_items.bending_data JSON 컬럼으로 통합
|
||||
*
|
||||
* 전개도 데이터는 항상 통째로 읽고/쓰기 하므로 JSON이 적합
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. bending_items에 bending_data JSON 컬럼 추가
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->json('bending_data')->nullable()->after('inspection_door')
|
||||
->comment('전개도 데이터 [{no, input, rate, sum, color, aAngle}, ...]');
|
||||
});
|
||||
|
||||
// 2. bending_data 테이블 → bending_items.bending_data JSON으로 이관
|
||||
$items = DB::table('bending_data')
|
||||
->select('bending_item_id')
|
||||
->distinct()
|
||||
->pluck('bending_item_id');
|
||||
|
||||
foreach ($items as $itemId) {
|
||||
$rows = DB::table('bending_data')
|
||||
->where('bending_item_id', $itemId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$json = $rows->map(fn ($r) => [
|
||||
'no' => $r->sort_order,
|
||||
'input' => (float) $r->input,
|
||||
'rate' => $r->rate ?? '',
|
||||
'sum' => $r->sum !== null ? (float) $r->sum : null,
|
||||
'color' => (bool) $r->color,
|
||||
'aAngle' => (bool) $r->a_angle,
|
||||
])->values()->toArray();
|
||||
|
||||
DB::table('bending_items')
|
||||
->where('id', $itemId)
|
||||
->update(['bending_data' => json_encode($json)]);
|
||||
}
|
||||
|
||||
// 3. bending_data 테이블 DROP
|
||||
Schema::dropIfExists('bending_data');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// bending_data 테이블 재생성
|
||||
Schema::create('bending_data', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bending_item_id');
|
||||
$table->smallInteger('sort_order')->unsigned();
|
||||
$table->decimal('input', 10, 2)->default(0);
|
||||
$table->string('rate', 10)->nullable();
|
||||
$table->decimal('after_rate', 10, 2)->nullable();
|
||||
$table->decimal('sum', 10, 2)->nullable();
|
||||
$table->boolean('color')->default(false);
|
||||
$table->boolean('a_angle')->default(false);
|
||||
$table->timestamps();
|
||||
$table->index('bending_item_id', 'idx_bd_bending_item');
|
||||
$table->unique(['bending_item_id', 'sort_order'], 'uk_bd_item_order');
|
||||
$table->foreign('bending_item_id')->references('id')->on('bending_items')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// bending_items.bending_data 컬럼 삭제
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->dropColumn('bending_data');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* bending_items.bending_data JSON → bending_data 별도 테이블로 분리
|
||||
*
|
||||
* 전개도 데이터는 행 수 가변 + 정규화가 적합
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. bending_data 테이블 재생성
|
||||
Schema::create('bending_data', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bending_item_id')->comment('FK → bending_items.id');
|
||||
$table->smallInteger('sort_order')->unsigned()->comment('열 순서 (1,2,3,...)');
|
||||
|
||||
$table->decimal('input', 10, 2)->default(0)->comment('입력 치수');
|
||||
$table->string('rate', 10)->nullable()->comment('연신율: ""(없음), "-1"(하향), "1"(상향)');
|
||||
$table->decimal('after_rate', 10, 2)->nullable()->comment('연신율 적용 후 값');
|
||||
$table->decimal('sum', 10, 2)->nullable()->comment('누적 합계');
|
||||
$table->boolean('color')->default(false)->comment('음영 마킹');
|
||||
$table->boolean('a_angle')->default(false)->comment('A각 표시');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('bending_item_id', 'idx_bd_bending_item');
|
||||
$table->unique(['bending_item_id', 'sort_order'], 'uk_bd_item_order');
|
||||
$table->foreign('bending_item_id')->references('id')->on('bending_items')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// 2. bending_items.bending_data JSON → bending_data rows 분해
|
||||
$items = DB::table('bending_items')
|
||||
->whereNotNull('bending_data')
|
||||
->select('id', 'bending_data')
|
||||
->get();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$rows = json_decode($item->bending_data, true);
|
||||
if (empty($rows) || ! is_array($rows)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($rows as $i => $row) {
|
||||
$input = (float) ($row['input'] ?? 0);
|
||||
$rate = $row['rate'] ?? '';
|
||||
$afterRate = ($rate !== '' && $rate !== null) ? $input + (float) $rate : $input;
|
||||
|
||||
DB::table('bending_data')->insert([
|
||||
'bending_item_id' => $item->id,
|
||||
'sort_order' => $row['no'] ?? ($i + 1),
|
||||
'input' => $input,
|
||||
'rate' => $rate !== '' ? $rate : null,
|
||||
'after_rate' => $afterRate,
|
||||
'sum' => $row['sum'] ?? null,
|
||||
'color' => (bool) ($row['color'] ?? false),
|
||||
'a_angle' => (bool) ($row['aAngle'] ?? $row['a_angle'] ?? false),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. bending_items.bending_data JSON 컬럼 삭제
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->dropColumn('bending_data');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// bending_items에 bending_data JSON 컬럼 복원
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->json('bending_data')->nullable()->after('inspection_door');
|
||||
});
|
||||
|
||||
// bending_data rows → JSON 복원
|
||||
$itemIds = DB::table('bending_data')->distinct()->pluck('bending_item_id');
|
||||
foreach ($itemIds as $itemId) {
|
||||
$rows = DB::table('bending_data')
|
||||
->where('bending_item_id', $itemId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$json = $rows->map(fn ($r) => [
|
||||
'no' => $r->sort_order,
|
||||
'input' => (float) $r->input,
|
||||
'rate' => $r->rate ?? '',
|
||||
'sum' => $r->sum !== null ? (float) $r->sum : null,
|
||||
'color' => (bool) $r->color,
|
||||
'aAngle' => (bool) $r->a_angle,
|
||||
])->values()->toArray();
|
||||
|
||||
DB::table('bending_items')->where('id', $itemId)->update([
|
||||
'bending_data' => json_encode($json),
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::dropIfExists('bending_data');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* bending_item_mappings 테이블 제거
|
||||
*
|
||||
* LOT 코드 매핑이 bending_items.legacy_code로 흡수됨
|
||||
* BendingCodeService.resolveItem()도 bending_items 직접 조회로 변경됨
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::dropIfExists('bending_item_mappings');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::create('bending_item_mappings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('prod_code', 2);
|
||||
$table->string('spec_code', 2);
|
||||
$table->string('length_code', 2);
|
||||
$table->unsignedBigInteger('item_id');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_id', 'prod_code', 'spec_code', 'length_code'], 'bim_tenant_prod_spec_length_unique');
|
||||
$table->index(['tenant_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* bending_items에 length_code, length_mm 컬럼 추가
|
||||
* 유니크 인덱스를 (tenant_id, code, length_code, deleted_at)로 변경
|
||||
*
|
||||
* 같은 제품+종류(RS)라도 원자재 길이(24,30,35...)가 다르면 별도 품목
|
||||
* code = RS260319, length_code = 30 → RS260319 + 3000mm
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 컬럼 추가
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->string('length_code', 5)->nullable()->after('inspection_door')->comment('원자재 길이코드: 24,30,35,40,42,43 등');
|
||||
$table->integer('length_mm')->nullable()->after('length_code')->comment('원자재 길이(mm): 2438,3000,3500...');
|
||||
});
|
||||
|
||||
// 2. items.options에서 length_code/length_mm 복사
|
||||
DB::statement("
|
||||
UPDATE bending_items bi
|
||||
JOIN items i ON i.code = bi.legacy_code AND i.item_category = 'BENDING' AND i.tenant_id = bi.tenant_id
|
||||
SET bi.length_code = JSON_UNQUOTE(JSON_EXTRACT(i.options, '$.length_code')),
|
||||
bi.length_mm = CAST(JSON_UNQUOTE(JSON_EXTRACT(i.options, '$.length_mm')) AS UNSIGNED)
|
||||
WHERE JSON_EXTRACT(i.options, '$.length_code') IS NOT NULL
|
||||
");
|
||||
|
||||
// 3. legacy_code에서 length_code 추출 (BD-RS-30 → 30)
|
||||
DB::statement("
|
||||
UPDATE bending_items
|
||||
SET length_code = SUBSTRING(legacy_code, -2)
|
||||
WHERE legacy_code REGEXP '^BD-[A-Z]{2}-[0-9]{2}$'
|
||||
AND length_code IS NULL
|
||||
");
|
||||
|
||||
// 4. code에서 -XX 접미사 제거 (RS260319-30 → RS260319)
|
||||
DB::statement("
|
||||
UPDATE bending_items
|
||||
SET code = SUBSTRING(code, 1, LENGTH(code) - 3)
|
||||
WHERE code REGEXP '^[A-Z]{2}[0-9]{6}-[0-9]{2}$'
|
||||
");
|
||||
|
||||
// 5. 유니크 인덱스 변경
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->dropUnique('uk_bi_tenant_code');
|
||||
$table->unique(['tenant_id', 'code', 'length_code', 'deleted_at'], 'uk_bi_tenant_code_length');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->dropUnique('uk_bi_tenant_code_length');
|
||||
$table->unique(['tenant_id', 'code', 'deleted_at'], 'uk_bi_tenant_code');
|
||||
$table->dropColumn(['length_code', 'length_mm']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 절곡품 모델 전용 테이블 (가이드레일/케이스/하단마감재 통합)
|
||||
*
|
||||
* 기존: items (item_category = GUIDERAIL_MODEL / SHUTTERBOX_MODEL / BOTTOMBAR_MODEL) + options JSON
|
||||
* 변경: bending_models 전용 테이블 + 정규 컬럼
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bending_models', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->string('model_type', 30)->comment('GUIDERAIL_MODEL / SHUTTERBOX_MODEL / BOTTOMBAR_MODEL');
|
||||
$table->string('code', 100);
|
||||
$table->string('name', 255);
|
||||
$table->string('legacy_code', 100)->nullable();
|
||||
$table->unsignedInteger('legacy_num')->nullable()->comment('chandj 원본 num');
|
||||
|
||||
// 공통
|
||||
$table->string('model_name', 50)->nullable();
|
||||
$table->string('model_UA', 20)->nullable();
|
||||
$table->string('item_sep', 20)->nullable();
|
||||
$table->string('finishing_type', 20)->nullable();
|
||||
$table->string('author', 50)->nullable();
|
||||
$table->text('remark')->nullable();
|
||||
|
||||
// 가이드레일
|
||||
$table->string('check_type', 30)->nullable();
|
||||
$table->decimal('rail_width', 10, 2)->nullable();
|
||||
$table->decimal('rail_length', 10, 2)->nullable();
|
||||
|
||||
// 케이스
|
||||
$table->string('exit_direction', 20)->nullable();
|
||||
$table->decimal('front_bottom_width', 10, 2)->nullable();
|
||||
$table->decimal('box_width', 10, 2)->nullable();
|
||||
$table->decimal('box_height', 10, 2)->nullable();
|
||||
|
||||
// 하단마감재
|
||||
$table->decimal('bar_width', 10, 2)->nullable();
|
||||
$table->decimal('bar_height', 10, 2)->nullable();
|
||||
|
||||
// 부품 조합
|
||||
$table->json('components')->nullable();
|
||||
$table->json('material_summary')->nullable();
|
||||
|
||||
// 메타
|
||||
$table->string('search_keyword', 100)->nullable();
|
||||
$table->date('registration_date')->nullable();
|
||||
$table->json('options')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedBigInteger('updated_by')->nullable();
|
||||
$table->unsignedBigInteger('deleted_by')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('tenant_id', 'idx_bm_tenant');
|
||||
$table->index('model_type', 'idx_bm_type');
|
||||
$table->index('model_name', 'idx_bm_model_name');
|
||||
$table->index('item_sep', 'idx_bm_item_sep');
|
||||
$table->unique(['tenant_id', 'code', 'deleted_at'], 'uk_bm_tenant_code');
|
||||
$table->foreign('tenant_id')->references('id')->on('tenants');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bending_models');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* bending_data 테이블 → bending_items.bending_data JSON 컬럼으로 통합
|
||||
*
|
||||
* 전개도 데이터는 항상 통째로 읽기/쓰기, 개별 행 검색 없음 → JSON이 적합
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. bending_items에 bending_data JSON 컬럼 추가
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->json('bending_data')->nullable()->after('inspection_door')
|
||||
->comment('전개도 [{no, input, rate, sum, color, aAngle}]');
|
||||
});
|
||||
|
||||
// 2. bending_data rows → JSON 변환
|
||||
$itemIds = DB::table('bending_data')->distinct()->pluck('bending_item_id');
|
||||
|
||||
foreach ($itemIds as $itemId) {
|
||||
$rows = DB::table('bending_data')
|
||||
->where('bending_item_id', $itemId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$json = $rows->map(fn ($r) => [
|
||||
'no' => $r->sort_order,
|
||||
'input' => (float) $r->input,
|
||||
'rate' => $r->rate ?? '',
|
||||
'sum' => $r->sum !== null ? (float) $r->sum : null,
|
||||
'color' => (bool) $r->color,
|
||||
'aAngle' => (bool) $r->a_angle,
|
||||
])->values()->toArray();
|
||||
|
||||
DB::table('bending_items')
|
||||
->where('id', $itemId)
|
||||
->update(['bending_data' => json_encode($json)]);
|
||||
}
|
||||
|
||||
// 3. bending_data 테이블 DROP
|
||||
Schema::dropIfExists('bending_data');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::create('bending_data', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bending_item_id');
|
||||
$table->smallInteger('sort_order')->unsigned();
|
||||
$table->decimal('input', 10, 2)->default(0);
|
||||
$table->string('rate', 10)->nullable();
|
||||
$table->decimal('after_rate', 10, 2)->nullable();
|
||||
$table->decimal('sum', 10, 2)->nullable();
|
||||
$table->boolean('color')->default(false);
|
||||
$table->boolean('a_angle')->default(false);
|
||||
$table->timestamps();
|
||||
$table->index('bending_item_id', 'idx_bd_bending_item');
|
||||
$table->unique(['bending_item_id', 'sort_order'], 'uk_bd_item_order');
|
||||
$table->foreign('bending_item_id')->references('id')->on('bending_items')->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::table('bending_items', function (Blueprint $table) {
|
||||
$table->dropColumn('bending_data');
|
||||
});
|
||||
}
|
||||
};
|
||||
463
storage/api-docs/api-docs-v1.json
Executable file → Normal file
463
storage/api-docs/api-docs-v1.json
Executable file → Normal file
@@ -9442,6 +9442,7 @@
|
||||
"BendingItem"
|
||||
],
|
||||
"summary": "절곡품 목록 조회",
|
||||
"description": "bending_items 테이블에서 검색. 정규 컬럼 인덱스로 빠른 검색 지원.",
|
||||
"operationId": "c497d5bfebed3fb08cd4d5be9224c795",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -9484,9 +9485,27 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "model_name",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "legacy_bending_num",
|
||||
"in": "query",
|
||||
"description": "chandj.bending.num으로 검색",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"description": "item_name, code, item_spec, legacy_code 통합 검색",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
@@ -9512,7 +9531,30 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "성공"
|
||||
"description": "성공",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BendingItem"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9528,7 +9570,6 @@
|
||||
"schema": {
|
||||
"required": [
|
||||
"code",
|
||||
"name",
|
||||
"item_name",
|
||||
"item_sep",
|
||||
"item_bending",
|
||||
@@ -9536,13 +9577,13 @@
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"description": "LOT 코드",
|
||||
"type": "string",
|
||||
"example": "RM260319"
|
||||
},
|
||||
"item_name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "본체"
|
||||
},
|
||||
"item_sep": {
|
||||
"type": "string",
|
||||
@@ -9552,17 +9593,99 @@
|
||||
]
|
||||
},
|
||||
"item_bending": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "가이드레일"
|
||||
},
|
||||
"material": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "EGI 1.55T"
|
||||
},
|
||||
"item_spec": {
|
||||
"type": "string",
|
||||
"example": "120*70",
|
||||
"nullable": true
|
||||
},
|
||||
"model_name": {
|
||||
"type": "string",
|
||||
"example": "KSS01",
|
||||
"nullable": true
|
||||
},
|
||||
"model_UA": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"인정",
|
||||
"비인정"
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"rail_width": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"exit_direction": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"front_bottom": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"box_width": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"box_height": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"inspection_door": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"length_code": {
|
||||
"type": "string",
|
||||
"example": "30",
|
||||
"nullable": true
|
||||
},
|
||||
"length_mm": {
|
||||
"type": "integer",
|
||||
"example": 3000,
|
||||
"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
|
||||
},
|
||||
"memo": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9583,10 +9706,55 @@
|
||||
"BendingItem"
|
||||
],
|
||||
"summary": "절곡품 필터 옵션 조회",
|
||||
"description": "item_sep, item_bending, material, model_UA, model_name 고유값 목록",
|
||||
"operationId": "f5dd325adc791e1b8cab40b9fa2fb77d",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "성공"
|
||||
"description": "성공",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"properties": {
|
||||
"item_sep": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"item_bending": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"material": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"model_UA": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"model_name": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9597,6 +9765,7 @@
|
||||
"BendingItem"
|
||||
],
|
||||
"summary": "절곡품 상세 조회",
|
||||
"description": "전개도 데이터(bendingData) 포함",
|
||||
"operationId": "d364f4d4cf76bcce7167561b73216382",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -9610,7 +9779,19 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "성공"
|
||||
"description": "성공",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/BendingItem"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9638,18 +9819,57 @@
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"item_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"memo": {
|
||||
"item_sep": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"스크린",
|
||||
"철재"
|
||||
]
|
||||
},
|
||||
"item_bending": {
|
||||
"type": "string"
|
||||
},
|
||||
"material": {
|
||||
"type": "string"
|
||||
},
|
||||
"item_spec": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"model_name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"model_UA": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"rail_width": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"length_code": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"length_mm": {
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"bendingData": {
|
||||
"description": "전체 교체",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"memo": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9668,6 +9888,7 @@
|
||||
"BendingItem"
|
||||
],
|
||||
"summary": "절곡품 삭제",
|
||||
"description": "bending_data(전개도)도 함께 삭제",
|
||||
"operationId": "46dcd93439505ae5ffb1dd2c8c1e5685",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -64680,35 +64901,22 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"example": 15862
|
||||
"example": 431
|
||||
},
|
||||
"code": {
|
||||
"description": "LOT 코드: {제품Code}{종류Code}{YYMMDD}",
|
||||
"type": "string",
|
||||
"example": "BD-BE-30"
|
||||
"example": "RS260319"
|
||||
},
|
||||
"name": {
|
||||
"legacy_code": {
|
||||
"description": "이전 코드 (items 기반)",
|
||||
"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
|
||||
"example": "BD-RS-30",
|
||||
"nullable": true
|
||||
},
|
||||
"item_name": {
|
||||
"type": "string",
|
||||
"example": "하단마감재"
|
||||
"example": "SUS마감재"
|
||||
},
|
||||
"item_sep": {
|
||||
"type": "string",
|
||||
@@ -64719,16 +64927,16 @@
|
||||
},
|
||||
"item_bending": {
|
||||
"type": "string",
|
||||
"example": "하단마감재"
|
||||
"example": "가이드레일"
|
||||
},
|
||||
"item_spec": {
|
||||
"type": "string",
|
||||
"example": "60*40",
|
||||
"example": "120*70",
|
||||
"nullable": true
|
||||
},
|
||||
"material": {
|
||||
"type": "string",
|
||||
"example": "EGI 1.55T"
|
||||
"example": "SUS 1.2T"
|
||||
},
|
||||
"model_name": {
|
||||
"type": "string",
|
||||
@@ -64743,19 +64951,85 @@
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"rail_width": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"exit_direction": {
|
||||
"description": "출구방향 (케이스 전용)",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"front_bottom": {
|
||||
"description": "전면밑 (케이스 전용)",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"box_width": {
|
||||
"description": "박스폭 (케이스 전용)",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"box_height": {
|
||||
"description": "박스높이 (케이스 전용)",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"inspection_door": {
|
||||
"description": "점검구 (케이스 전용)",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"length_code": {
|
||||
"description": "원자재 길이코드",
|
||||
"type": "string",
|
||||
"example": "30",
|
||||
"nullable": true
|
||||
},
|
||||
"length_mm": {
|
||||
"description": "원자재 길이(mm)",
|
||||
"type": "integer",
|
||||
"example": 3000,
|
||||
"nullable": true
|
||||
},
|
||||
"bendingData": {
|
||||
"description": "전개도 데이터 (bending_data 테이블)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"no": {
|
||||
"description": "열 순서",
|
||||
"type": "integer"
|
||||
},
|
||||
"input": {
|
||||
"description": "입력 치수",
|
||||
"type": "number"
|
||||
},
|
||||
"rate": {
|
||||
"description": "연신율: 빈값/'-1'(하향)/'1'(상향)",
|
||||
"type": "string"
|
||||
},
|
||||
"sum": {
|
||||
"description": "누적 합계",
|
||||
"type": "number"
|
||||
},
|
||||
"color": {
|
||||
"description": "음영 마킹",
|
||||
"type": "boolean"
|
||||
},
|
||||
"aAngle": {
|
||||
"description": "A각 표시",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"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
|
||||
@@ -64764,78 +65038,69 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"exit_direction": {
|
||||
"registration_date": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"nullable": true
|
||||
},
|
||||
"front_bottom_width": {
|
||||
"image_file_id": {
|
||||
"description": "절곡 도면 이미지 파일 ID",
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"box_width": {
|
||||
"legacy_bending_id": {
|
||||
"description": "chandj.bending.num 참조",
|
||||
"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 (운영 전 삭제 예정)",
|
||||
"description": "MNG2 호환 (=legacy_bending_id)",
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"modified_by": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"width_sum": {
|
||||
"type": "integer",
|
||||
"example": 193,
|
||||
"description": "폭합계 (전개도 마지막 sum)",
|
||||
"type": "number",
|
||||
"example": 203,
|
||||
"nullable": true
|
||||
},
|
||||
"bend_count": {
|
||||
"description": "절곡 횟수",
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
},
|
||||
"is_active": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"name": {
|
||||
"description": "MNG2 호환 (=item_name)",
|
||||
"type": "string"
|
||||
},
|
||||
"front_bottom_width": {
|
||||
"description": "MNG2 호환 (=front_bottom)",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"item_type": {
|
||||
"description": "MNG2 호환 (고정값)",
|
||||
"type": "string",
|
||||
"example": "PT"
|
||||
},
|
||||
"item_category": {
|
||||
"description": "MNG2 호환 (고정값)",
|
||||
"type": "string",
|
||||
"example": "BENDING"
|
||||
},
|
||||
"unit": {
|
||||
"description": "MNG2 호환 (고정값)",
|
||||
"type": "string",
|
||||
"example": "EA"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
@@ -94607,7 +94872,7 @@
|
||||
},
|
||||
{
|
||||
"name": "BendingItem",
|
||||
"description": "절곡품 기초관리"
|
||||
"description": "절곡품 기초관리 (bending_items 전용 테이블)"
|
||||
},
|
||||
{
|
||||
"name": "Bidding",
|
||||
|
||||
0
storage/api-docs/api-docs.json
Executable file → Normal file
0
storage/api-docs/api-docs.json
Executable file → Normal file
Reference in New Issue
Block a user