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:
강영보
2026-03-19 19:54:23 +09:00
parent 623298dd82
commit c29090a0b8
32 changed files with 3114 additions and 490 deletions

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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']);
});
}
};

View File

@@ -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');
}
};

View File

@@ -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');
});
}
};