feat: 경동기업 품목 기준 데이터 배포용 시더 구현
- ExportItemMasterDataCommand: tenant_id=287 데이터를 JSON으로 추출 - KyungdongItemMasterSeeder: JSON 기반 DELETE+재삽입 시더 - Phase 1: item_pages/sections/fields + entity_relationships - Phase 2: categories(depth순) + items(배치500건) - Phase 3: item_details + prices - ID 매핑으로 환경별 충돌 없음, 트랜잭션 안전 - 8개 JSON 데이터 파일 포함 (총 약 1.5MB) - .gitignore에 시더 데이터 예외 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -153,4 +153,7 @@ _ide_helper_models.php
|
||||
!**/data/
|
||||
# 그리고 .gitkeep은 예외로 추적
|
||||
!**/data/.gitkeep
|
||||
# 시더 데이터 JSON은 추적
|
||||
!database/seeders/data/kyungdong/
|
||||
!database/seeders/data/kyungdong/**
|
||||
storage/secrets/
|
||||
|
||||
202
app/Console/Commands/ExportItemMasterDataCommand.php
Normal file
202
app/Console/Commands/ExportItemMasterDataCommand.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 경동기업(tenant_id=287) 품목 기준 데이터를 JSON으로 추출
|
||||
*
|
||||
* 추출 대상: item_pages, item_sections, item_fields,
|
||||
* entity_relationships, categories, items, item_details, prices
|
||||
*
|
||||
* 사용법: php artisan kyungdong:export-item-master
|
||||
*/
|
||||
class ExportItemMasterDataCommand extends Command
|
||||
{
|
||||
protected $signature = 'kyungdong:export-item-master';
|
||||
|
||||
protected $description = '경동기업(tenant_id=287) 품목 기준 데이터를 JSON 파일로 추출';
|
||||
|
||||
private const TENANT_ID = 287;
|
||||
|
||||
private string $outputPath;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->outputPath = database_path('seeders/data/kyungdong');
|
||||
|
||||
if (! is_dir($this->outputPath)) {
|
||||
mkdir($this->outputPath, 0755, true);
|
||||
}
|
||||
|
||||
$this->info('경동기업 품목 기준 데이터 추출 시작 (tenant_id=' . self::TENANT_ID . ')');
|
||||
$this->newLine();
|
||||
|
||||
$this->exportItemPages();
|
||||
$this->exportItemSections();
|
||||
$this->exportItemFields();
|
||||
$this->exportEntityRelationships();
|
||||
$this->exportCategories();
|
||||
$this->exportItems();
|
||||
$this->exportItemDetails();
|
||||
$this->exportPrices();
|
||||
|
||||
$this->newLine();
|
||||
$this->info('추출 완료! 경로: ' . $this->outputPath);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function exportItemPages(): void
|
||||
{
|
||||
$rows = DB::table('item_pages')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('item_pages.json', $rows);
|
||||
$this->info(" item_pages: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportItemSections(): void
|
||||
{
|
||||
$rows = DB::table('item_sections')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('item_sections.json', $rows);
|
||||
$this->info(" item_sections: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportItemFields(): void
|
||||
{
|
||||
$rows = DB::table('item_fields')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('item_fields.json', $rows);
|
||||
$this->info(" item_fields: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportEntityRelationships(): void
|
||||
{
|
||||
// 참조 대상이 실제 존재하는 것만 추출
|
||||
$validPageIds = DB::table('item_pages')->where('tenant_id', self::TENANT_ID)->whereNull('deleted_at')->pluck('id');
|
||||
$validSectionIds = DB::table('item_sections')->where('tenant_id', self::TENANT_ID)->whereNull('deleted_at')->pluck('id');
|
||||
$validFieldIds = DB::table('item_fields')->where('tenant_id', self::TENANT_ID)->whereNull('deleted_at')->pluck('id');
|
||||
$validBomIds = DB::table('item_bom_items')->where('tenant_id', self::TENANT_ID)->whereNull('deleted_at')->pluck('id');
|
||||
|
||||
$validIds = [
|
||||
'page' => $validPageIds->flip(),
|
||||
'section' => $validSectionIds->flip(),
|
||||
'field' => $validFieldIds->flip(),
|
||||
'bom' => $validBomIds->flip(),
|
||||
];
|
||||
|
||||
$rows = DB::table('entity_relationships')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->get()
|
||||
->filter(function ($row) use ($validIds) {
|
||||
$parentValid = isset($validIds[$row->parent_type][$row->parent_id]);
|
||||
$childValid = isset($validIds[$row->child_type][$row->child_id]);
|
||||
|
||||
return $parentValid && $childValid;
|
||||
})
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('entity_relationships.json', $rows);
|
||||
$this->info(" entity_relationships: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportCategories(): void
|
||||
{
|
||||
$rows = DB::table('categories')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->orderByRaw('COALESCE(parent_id, 0), sort_order, id')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('categories.json', $rows);
|
||||
$this->info(" categories: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportItems(): void
|
||||
{
|
||||
$rows = DB::table('items')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('items.json', $rows);
|
||||
$this->info(" items: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportItemDetails(): void
|
||||
{
|
||||
$itemIds = DB::table('items')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id');
|
||||
|
||||
$rows = DB::table('item_details')
|
||||
->whereIn('item_id', $itemIds)
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('item_details.json', $rows);
|
||||
$this->info(" item_details: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
private function exportPrices(): void
|
||||
{
|
||||
$rows = DB::table('prices')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn ($row) => $this->addOriginalId($row))
|
||||
->toArray();
|
||||
|
||||
$this->writeJson('prices.json', $rows);
|
||||
$this->info(" prices: " . count($rows) . "건");
|
||||
}
|
||||
|
||||
/**
|
||||
* _original_id 추가 + id 제거
|
||||
*/
|
||||
private function addOriginalId(object $row): array
|
||||
{
|
||||
$data = (array) $row;
|
||||
$data['_original_id'] = $data['id'];
|
||||
unset($data['id']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function writeJson(string $filename, array $data): void
|
||||
{
|
||||
$path = $this->outputPath . '/' . $filename;
|
||||
file_put_contents(
|
||||
$path,
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
}
|
||||
}
|
||||
621
database/seeders/Kyungdong/KyungdongItemMasterSeeder.php
Normal file
621
database/seeders/Kyungdong/KyungdongItemMasterSeeder.php
Normal file
@@ -0,0 +1,621 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\Kyungdong;
|
||||
|
||||
use Database\Seeders\DummyDataSeeder;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 경동기업(tenant_id=287) 품목 기준 데이터 배포용 시더
|
||||
*
|
||||
* DELETE + 재삽입 방식. AUTO_INCREMENT ID 사용으로 환경별 충돌 없음.
|
||||
* 종속 테이블은 새 ID로 자동 매핑.
|
||||
*
|
||||
* 실행: php artisan db:seed --class="Database\Seeders\Kyungdong\KyungdongItemMasterSeeder"
|
||||
*/
|
||||
class KyungdongItemMasterSeeder extends Seeder
|
||||
{
|
||||
private int $tenantId;
|
||||
|
||||
private int $userId;
|
||||
|
||||
private string $dataPath;
|
||||
|
||||
/** @var array<int,int> 원본ID → 새ID */
|
||||
private array $pageMap = [];
|
||||
|
||||
private array $sectionMap = [];
|
||||
|
||||
private array $fieldMap = [];
|
||||
|
||||
private array $categoryMap = [];
|
||||
|
||||
/** @var array<string,int> code → 새ID */
|
||||
private array $itemMap = [];
|
||||
|
||||
/** 기대 건수 (검증용) */
|
||||
private array $expectedCounts = [];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->tenantId = DummyDataSeeder::TENANT_ID;
|
||||
$this->userId = DummyDataSeeder::USER_ID;
|
||||
$this->dataPath = database_path('seeders/data/kyungdong');
|
||||
|
||||
$this->command->info('=== 경동기업 품목 기준 데이터 시더 시작 ===');
|
||||
$this->command->info(" tenant_id: {$this->tenantId}");
|
||||
$this->command->newLine();
|
||||
|
||||
DB::transaction(function () {
|
||||
$this->cleanup();
|
||||
|
||||
$this->command->info('--- Phase 1: 독립 테이블 ---');
|
||||
$this->seedItemPages();
|
||||
$this->seedItemSections();
|
||||
$this->seedItemFields();
|
||||
$this->seedEntityRelationships();
|
||||
$this->updateDisplayConditions();
|
||||
|
||||
$this->command->info('--- Phase 2: 기반 테이블 ---');
|
||||
$this->seedCategories();
|
||||
$this->seedItems();
|
||||
|
||||
$this->command->info('--- Phase 3: 종속 테이블 ---');
|
||||
$this->seedItemDetails();
|
||||
$this->seedPrices();
|
||||
});
|
||||
|
||||
$this->verify();
|
||||
$this->printSummary();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// CLEANUP
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function cleanup(): void
|
||||
{
|
||||
$this->command->info('CLEANUP: tenant_id=' . $this->tenantId . ' 데이터 삭제');
|
||||
|
||||
// FK 역순 삭제
|
||||
// prices - tenant_id로 직접 삭제
|
||||
$c = DB::table('prices')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" prices: {$c}건 삭제");
|
||||
|
||||
// item_details - items를 통해 삭제
|
||||
$itemIds = DB::table('items')->where('tenant_id', $this->tenantId)->pluck('id');
|
||||
$c = DB::table('item_details')->whereIn('item_id', $itemIds)->delete();
|
||||
$this->command->info(" item_details: {$c}건 삭제");
|
||||
|
||||
// items
|
||||
$c = DB::table('items')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" items: {$c}건 삭제");
|
||||
|
||||
// categories - 자식 먼저 삭제
|
||||
$catIds = DB::table('categories')->where('tenant_id', $this->tenantId)->pluck('id');
|
||||
// 자식 (parent_id가 있는 것) 먼저
|
||||
DB::table('categories')
|
||||
->where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('parent_id')
|
||||
->delete();
|
||||
$c = DB::table('categories')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" categories: " . count($catIds) . "건 삭제");
|
||||
|
||||
// entity_relationships
|
||||
$c = DB::table('entity_relationships')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" entity_relationships: {$c}건 삭제");
|
||||
|
||||
// item_fields
|
||||
$c = DB::table('item_fields')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" item_fields: {$c}건 삭제");
|
||||
|
||||
// item_sections
|
||||
$c = DB::table('item_sections')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" item_sections: {$c}건 삭제");
|
||||
|
||||
// item_pages
|
||||
$c = DB::table('item_pages')->where('tenant_id', $this->tenantId)->delete();
|
||||
$this->command->info(" item_pages: {$c}건 삭제");
|
||||
|
||||
$this->command->newLine();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// PHASE 1: 독립 테이블
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function seedItemPages(): void
|
||||
{
|
||||
$rows = $this->loadJson('item_pages.json');
|
||||
$this->expectedCounts['item_pages'] = count($rows);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$originalId = $row['_original_id'];
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
$newId = DB::table('item_pages')->insertGetId($data);
|
||||
$this->pageMap[$originalId] = $newId;
|
||||
}
|
||||
|
||||
$this->command->info(" item_pages: " . count($rows) . "건 삽입");
|
||||
}
|
||||
|
||||
private function seedItemSections(): void
|
||||
{
|
||||
$rows = $this->loadJson('item_sections.json');
|
||||
$this->expectedCounts['item_sections'] = count($rows);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$originalId = $row['_original_id'];
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
$newId = DB::table('item_sections')->insertGetId($data);
|
||||
$this->sectionMap[$originalId] = $newId;
|
||||
}
|
||||
|
||||
$this->command->info(" item_sections: " . count($rows) . "건 삽입");
|
||||
}
|
||||
|
||||
private function seedItemFields(): void
|
||||
{
|
||||
$rows = $this->loadJson('item_fields.json');
|
||||
$this->expectedCounts['item_fields'] = count($rows);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$originalId = $row['_original_id'];
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
$newId = DB::table('item_fields')->insertGetId($data);
|
||||
$this->fieldMap[$originalId] = $newId;
|
||||
}
|
||||
|
||||
$this->command->info(" item_fields: " . count($rows) . "건 삽입");
|
||||
}
|
||||
|
||||
private function seedEntityRelationships(): void
|
||||
{
|
||||
$rows = $this->loadJson('entity_relationships.json');
|
||||
$this->expectedCounts['entity_relationships'] = count($rows);
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
// parent_id 매핑
|
||||
$data['parent_id'] = $this->mapEntityId($data['parent_type'], $data['parent_id']);
|
||||
// child_id 매핑
|
||||
$data['child_id'] = $this->mapEntityId($data['child_type'], $data['child_id']);
|
||||
|
||||
if ($data['parent_id'] === null || $data['child_id'] === null) {
|
||||
$this->command->warn(" entity_relationships: 매핑 실패 건 스킵 (parent_type={$row['parent_type']}, child_type={$row['child_type']})");
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('entity_relationships')->insert($data);
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
$this->expectedCounts['entity_relationships'] = $inserted;
|
||||
$this->command->info(" entity_relationships: {$inserted}건 삽입");
|
||||
}
|
||||
|
||||
/**
|
||||
* item_fields의 display_condition 내 targetFieldIds를 새 ID로 치환
|
||||
*/
|
||||
private function updateDisplayConditions(): void
|
||||
{
|
||||
if (empty($this->fieldMap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = DB::table('item_fields')
|
||||
->where('tenant_id', $this->tenantId)
|
||||
->whereNotNull('display_condition')
|
||||
->get(['id', 'display_condition']);
|
||||
|
||||
$updated = 0;
|
||||
foreach ($fields as $field) {
|
||||
$condition = json_decode($field->display_condition, true);
|
||||
if (! is_array($condition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$changed = false;
|
||||
$condition = $this->remapDisplayCondition($condition, $changed);
|
||||
|
||||
if ($changed) {
|
||||
DB::table('item_fields')
|
||||
->where('id', $field->id)
|
||||
->update(['display_condition' => json_encode($condition)]);
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated > 0) {
|
||||
$this->command->info(" display_condition 후처리: {$updated}건 업데이트");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* display_condition 내 targetFieldIds 재귀적 치환
|
||||
*/
|
||||
private function remapDisplayCondition(array $condition, bool &$changed): array
|
||||
{
|
||||
if (isset($condition['targetFieldIds']) && is_array($condition['targetFieldIds'])) {
|
||||
$newIds = [];
|
||||
foreach ($condition['targetFieldIds'] as $oldId) {
|
||||
if (isset($this->fieldMap[$oldId])) {
|
||||
$newIds[] = $this->fieldMap[$oldId];
|
||||
$changed = true;
|
||||
} else {
|
||||
$newIds[] = $oldId;
|
||||
}
|
||||
}
|
||||
$condition['targetFieldIds'] = $newIds;
|
||||
}
|
||||
|
||||
// targetFieldId (단일) 처리
|
||||
if (isset($condition['targetFieldId']) && isset($this->fieldMap[$condition['targetFieldId']])) {
|
||||
$condition['targetFieldId'] = $this->fieldMap[$condition['targetFieldId']];
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// conditions 배열 재귀 처리
|
||||
if (isset($condition['conditions']) && is_array($condition['conditions'])) {
|
||||
foreach ($condition['conditions'] as $i => $sub) {
|
||||
$condition['conditions'][$i] = $this->remapDisplayCondition($sub, $changed);
|
||||
}
|
||||
}
|
||||
|
||||
return $condition;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// PHASE 2: 기반 테이블
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function seedCategories(): void
|
||||
{
|
||||
$rows = $this->loadJson('categories.json');
|
||||
$this->expectedCounts['categories'] = count($rows);
|
||||
|
||||
// depth 순 삽입을 위해 parent_id 없는 것 먼저
|
||||
$roots = array_filter($rows, fn ($r) => empty($r['parent_id']));
|
||||
$children = array_filter($rows, fn ($r) => ! empty($r['parent_id']));
|
||||
|
||||
// 루트 카테고리 삽입
|
||||
foreach ($roots as $row) {
|
||||
$originalId = $row['_original_id'];
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$data['parent_id'] = null;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
$newId = DB::table('categories')->insertGetId($data);
|
||||
$this->categoryMap[$originalId] = $newId;
|
||||
}
|
||||
|
||||
// 자식 카테고리 삽입 (여러 depth 처리를 위해 최대 5회 반복)
|
||||
$remaining = $children;
|
||||
for ($pass = 0; $pass < 5 && ! empty($remaining); $pass++) {
|
||||
$nextRemaining = [];
|
||||
foreach ($remaining as $row) {
|
||||
$originalId = $row['_original_id'];
|
||||
$parentOriginalId = $row['parent_id'];
|
||||
|
||||
if (! isset($this->categoryMap[$parentOriginalId])) {
|
||||
$nextRemaining[] = $row;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$data['parent_id'] = $this->categoryMap[$parentOriginalId];
|
||||
$this->setAuditFields($data);
|
||||
|
||||
$newId = DB::table('categories')->insertGetId($data);
|
||||
$this->categoryMap[$originalId] = $newId;
|
||||
}
|
||||
$remaining = $nextRemaining;
|
||||
}
|
||||
|
||||
if (! empty($remaining)) {
|
||||
$this->command->warn(" categories: " . count($remaining) . "건 매핑 실패 (depth 초과)");
|
||||
}
|
||||
|
||||
$this->command->info(" categories: " . count($this->categoryMap) . "건 삽입");
|
||||
}
|
||||
|
||||
private function seedItems(): void
|
||||
{
|
||||
$rows = $this->loadJson('items.json');
|
||||
$this->expectedCounts['items'] = count($rows);
|
||||
|
||||
$chunks = array_chunk($rows, 500);
|
||||
$total = 0;
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
$insertData = [];
|
||||
foreach ($chunk as $row) {
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
|
||||
// category_id 매핑
|
||||
if (! empty($data['category_id']) && isset($this->categoryMap[$data['category_id']])) {
|
||||
$data['category_id'] = $this->categoryMap[$data['category_id']];
|
||||
}
|
||||
|
||||
$insertData[] = $data;
|
||||
}
|
||||
|
||||
DB::table('items')->insert($insertData);
|
||||
$total += count($insertData);
|
||||
}
|
||||
|
||||
// code로 itemMap 구축
|
||||
$this->itemMap = DB::table('items')
|
||||
->where('tenant_id', $this->tenantId)
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id', 'code')
|
||||
->toArray();
|
||||
|
||||
$this->command->info(" items: {$total}건 삽입 (itemMap: " . count($this->itemMap) . "건)");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// PHASE 3: 종속 테이블
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function seedItemDetails(): void
|
||||
{
|
||||
$rows = $this->loadJson('item_details.json');
|
||||
$this->expectedCounts['item_details'] = count($rows);
|
||||
|
||||
// items JSON에서 원본 item_id → code 매핑 구축
|
||||
$itemsJson = $this->loadJson('items.json');
|
||||
$originalIdToCode = [];
|
||||
foreach ($itemsJson as $item) {
|
||||
$originalIdToCode[$item['_original_id']] = $item['code'];
|
||||
}
|
||||
|
||||
$insertData = [];
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $this->stripMeta($row);
|
||||
$originalItemId = $data['item_id'];
|
||||
|
||||
// 원본 item_id → code → 새 item_id
|
||||
$code = $originalIdToCode[$originalItemId] ?? null;
|
||||
if ($code === null || ! isset($this->itemMap[$code])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['item_id'] = $this->itemMap[$code];
|
||||
$insertData[] = $data;
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
foreach (array_chunk($insertData, 500) as $chunk) {
|
||||
DB::table('item_details')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if ($skipped > 0) {
|
||||
$this->command->warn(" item_details: {$skipped}건 매핑 실패 스킵");
|
||||
}
|
||||
|
||||
$this->expectedCounts['item_details'] = count($insertData);
|
||||
$this->command->info(" item_details: " . count($insertData) . "건 삽입");
|
||||
}
|
||||
|
||||
private function seedPrices(): void
|
||||
{
|
||||
$rows = $this->loadJson('prices.json');
|
||||
$this->expectedCounts['prices'] = count($rows);
|
||||
|
||||
// items JSON에서 원본 item_id → code 매핑 구축
|
||||
$itemsJson = $this->loadJson('items.json');
|
||||
$originalIdToCode = [];
|
||||
foreach ($itemsJson as $item) {
|
||||
$originalIdToCode[$item['_original_id']] = $item['code'];
|
||||
}
|
||||
|
||||
$insertData = [];
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$data = $this->stripMeta($row);
|
||||
$data['tenant_id'] = $this->tenantId;
|
||||
$this->setAuditFields($data);
|
||||
$originalItemId = $data['item_id'];
|
||||
|
||||
// 원본 item_id → code → 새 item_id
|
||||
$code = $originalIdToCode[$originalItemId] ?? null;
|
||||
if ($code === null || ! isset($this->itemMap[$code])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data['item_id'] = $this->itemMap[$code];
|
||||
$insertData[] = $data;
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
foreach (array_chunk($insertData, 500) as $chunk) {
|
||||
DB::table('prices')->insert($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if ($skipped > 0) {
|
||||
$this->command->warn(" prices: {$skipped}건 매핑 실패 스킵");
|
||||
}
|
||||
|
||||
$this->expectedCounts['prices'] = count($insertData);
|
||||
$this->command->info(" prices: " . count($insertData) . "건 삽입");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// VERIFY & SUMMARY
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function verify(): void
|
||||
{
|
||||
$this->command->newLine();
|
||||
$this->command->info('=== 검증 ===');
|
||||
|
||||
$tables = [
|
||||
'item_pages' => fn () => DB::table('item_pages')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
'item_sections' => fn () => DB::table('item_sections')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
'item_fields' => fn () => DB::table('item_fields')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
'entity_relationships' => fn () => DB::table('entity_relationships')->where('tenant_id', $this->tenantId)->count(),
|
||||
'categories' => fn () => DB::table('categories')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
'items' => fn () => DB::table('items')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
'item_details' => fn () => DB::table('item_details')->whereIn('item_id', DB::table('items')->where('tenant_id', $this->tenantId)->pluck('id'))->count(),
|
||||
'prices' => fn () => DB::table('prices')->where('tenant_id', $this->tenantId)->whereNull('deleted_at')->count(),
|
||||
];
|
||||
|
||||
$allOk = true;
|
||||
foreach ($tables as $table => $countFn) {
|
||||
$actual = $countFn();
|
||||
$expected = $this->expectedCounts[$table] ?? '?';
|
||||
$status = ($actual == $expected) ? 'OK' : 'MISMATCH';
|
||||
|
||||
if ($status === 'MISMATCH') {
|
||||
$allOk = false;
|
||||
$this->command->error(" {$table}: {$actual}건 (기대: {$expected}) [{$status}]");
|
||||
} else {
|
||||
$this->command->info(" {$table}: {$actual}건 [{$status}]");
|
||||
}
|
||||
}
|
||||
|
||||
// entity_relationships 참조 무결성 검증
|
||||
$this->verifyRelationshipIntegrity();
|
||||
|
||||
if ($allOk) {
|
||||
$this->command->newLine();
|
||||
$this->command->info('모든 검증 통과!');
|
||||
}
|
||||
}
|
||||
|
||||
private function verifyRelationshipIntegrity(): void
|
||||
{
|
||||
$relationships = DB::table('entity_relationships')
|
||||
->where('tenant_id', $this->tenantId)
|
||||
->get();
|
||||
|
||||
$orphans = 0;
|
||||
foreach ($relationships as $rel) {
|
||||
$parentExists = $this->entityExists($rel->parent_type, $rel->parent_id);
|
||||
$childExists = $this->entityExists($rel->child_type, $rel->child_id);
|
||||
|
||||
if (! $parentExists || ! $childExists) {
|
||||
$orphans++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($orphans > 0) {
|
||||
$this->command->warn(" entity_relationships 참조 무결성: {$orphans}건 고아 레코드");
|
||||
} else {
|
||||
$this->command->info(" entity_relationships 참조 무결성: OK");
|
||||
}
|
||||
}
|
||||
|
||||
private function entityExists(string $type, int $id): bool
|
||||
{
|
||||
return match ($type) {
|
||||
'page' => DB::table('item_pages')->where('id', $id)->exists(),
|
||||
'section' => DB::table('item_sections')->where('id', $id)->exists(),
|
||||
'field' => DB::table('item_fields')->where('id', $id)->exists(),
|
||||
'bom' => DB::table('item_bom_items')->where('id', $id)->exists(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function printSummary(): void
|
||||
{
|
||||
$this->command->newLine();
|
||||
$this->command->info('=== 경동기업 품목 기준 데이터 시더 완료 ===');
|
||||
$this->command->info(" 매핑 현황:");
|
||||
$this->command->info(" pageMap: " . count($this->pageMap) . "건");
|
||||
$this->command->info(" sectionMap: " . count($this->sectionMap) . "건");
|
||||
$this->command->info(" fieldMap: " . count($this->fieldMap) . "건");
|
||||
$this->command->info(" categoryMap: " . count($this->categoryMap) . "건");
|
||||
$this->command->info(" itemMap: " . count($this->itemMap) . "건");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// HELPERS
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private function loadJson(string $filename): array
|
||||
{
|
||||
$path = $this->dataPath . '/' . $filename;
|
||||
|
||||
if (! file_exists($path)) {
|
||||
$this->command->error("JSON 파일 없음: {$path}");
|
||||
$this->command->error("먼저 php artisan kyungdong:export-item-master 를 실행하세요.");
|
||||
|
||||
throw new \RuntimeException("JSON 파일 없음: {$path}");
|
||||
}
|
||||
|
||||
return json_decode(file_get_contents($path), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* _original_id, id, created_at, updated_at, deleted_at 등 메타 제거
|
||||
*/
|
||||
private function stripMeta(array $row): array
|
||||
{
|
||||
unset(
|
||||
$row['_original_id'],
|
||||
$row['id'],
|
||||
$row['created_at'],
|
||||
$row['updated_at'],
|
||||
$row['deleted_at'],
|
||||
);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function setAuditFields(array &$data): void
|
||||
{
|
||||
$now = now();
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
|
||||
if (isset($data['created_by'])) {
|
||||
$data['created_by'] = $this->userId;
|
||||
}
|
||||
if (isset($data['updated_by'])) {
|
||||
$data['updated_by'] = $this->userId;
|
||||
}
|
||||
|
||||
// deleted_by, deleted_at는 항상 제거
|
||||
unset($data['deleted_by'], $data['deleted_at']);
|
||||
}
|
||||
|
||||
/**
|
||||
* entity type에 따라 원본 ID를 새 ID로 매핑
|
||||
*/
|
||||
private function mapEntityId(string $type, int $originalId): ?int
|
||||
{
|
||||
return match ($type) {
|
||||
'page' => $this->pageMap[$originalId] ?? null,
|
||||
'section' => $this->sectionMap[$originalId] ?? null,
|
||||
'field' => $this->fieldMap[$originalId] ?? null,
|
||||
default => $originalId, // bom 등은 그대로
|
||||
};
|
||||
}
|
||||
}
|
||||
1298
database/seeders/data/kyungdong/categories.json
Normal file
1298
database/seeders/data/kyungdong/categories.json
Normal file
File diff suppressed because it is too large
Load Diff
1352
database/seeders/data/kyungdong/entity_relationships.json
Normal file
1352
database/seeders/data/kyungdong/entity_relationships.json
Normal file
File diff suppressed because it is too large
Load Diff
4118
database/seeders/data/kyungdong/item_details.json
Normal file
4118
database/seeders/data/kyungdong/item_details.json
Normal file
File diff suppressed because it is too large
Load Diff
2180
database/seeders/data/kyungdong/item_fields.json
Normal file
2180
database/seeders/data/kyungdong/item_fields.json
Normal file
File diff suppressed because it is too large
Load Diff
82
database/seeders/data/kyungdong/item_pages.json
Normal file
82
database/seeders/data/kyungdong/item_pages.json
Normal file
@@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"page_name": "소모품 등록",
|
||||
"item_type": "CS",
|
||||
"source_table": "items",
|
||||
"absolute_path": "\/소모품관리\/소모품 등록",
|
||||
"is_active": 1,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 1025
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"page_name": "원자재 등록",
|
||||
"item_type": "RM",
|
||||
"source_table": "items",
|
||||
"absolute_path": "\/원자재관리\/원자재 등록",
|
||||
"is_active": 1,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 1026
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"page_name": "부자재 등록",
|
||||
"item_type": "SM",
|
||||
"source_table": "items",
|
||||
"absolute_path": "\/부자재관리\/부자재 등록",
|
||||
"is_active": 1,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 1027
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"page_name": "부품 등록",
|
||||
"item_type": "PT",
|
||||
"source_table": "items",
|
||||
"absolute_path": "\/부품관리\/부품 등록",
|
||||
"is_active": 1,
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 1028
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"page_name": "제품 등록",
|
||||
"item_type": "FG",
|
||||
"source_table": "items",
|
||||
"absolute_path": "\/제품관리\/제품 등록",
|
||||
"is_active": 1,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 1029
|
||||
}
|
||||
]
|
||||
189
database/seeders/data/kyungdong/item_sections.json
Normal file
189
database/seeders/data/kyungdong/item_sections.json
Normal file
@@ -0,0 +1,189 @@
|
||||
[
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "기본정보",
|
||||
"type": "fields",
|
||||
"order_no": 0,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 107
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "기본 정보",
|
||||
"type": "fields",
|
||||
"order_no": 0,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 108
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "기본 정보",
|
||||
"type": "fields",
|
||||
"order_no": 0,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 109
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "기본 정보",
|
||||
"type": "fields",
|
||||
"order_no": 0,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 110
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "조립 부품 정보",
|
||||
"type": "fields",
|
||||
"order_no": 1,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 111
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "절곡 부품",
|
||||
"type": "fields",
|
||||
"order_no": 2,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 112
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "구매 부품",
|
||||
"type": "fields",
|
||||
"order_no": 3,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 113
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "측면 규격 및 길이",
|
||||
"type": "fields",
|
||||
"order_no": 4,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 114
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "BOM",
|
||||
"type": "fields",
|
||||
"order_no": 5,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 115
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "부품 구성 (BOM)",
|
||||
"type": "bom",
|
||||
"order_no": 6,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 116
|
||||
},
|
||||
{
|
||||
"tenant_id": 287,
|
||||
"group_id": 1,
|
||||
"title": "기본 정보",
|
||||
"type": "fields",
|
||||
"order_no": 0,
|
||||
"is_template": 0,
|
||||
"is_default": 0,
|
||||
"description": null,
|
||||
"created_by": 1,
|
||||
"updated_by": null,
|
||||
"deleted_by": null,
|
||||
"created_at": "2026-02-04 22:20:41",
|
||||
"updated_at": "2026-02-04 22:20:41",
|
||||
"deleted_at": null,
|
||||
"_original_id": 117
|
||||
}
|
||||
]
|
||||
17942
database/seeders/data/kyungdong/items.json
Normal file
17942
database/seeders/data/kyungdong/items.json
Normal file
File diff suppressed because it is too large
Load Diff
21842
database/seeders/data/kyungdong/prices.json
Normal file
21842
database/seeders/data/kyungdong/prices.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user