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:
2026-02-04 22:47:03 +09:00
parent a6fc537a02
commit fa07e5b58a
11 changed files with 49829 additions and 0 deletions

3
.gitignore vendored
View File

@@ -153,4 +153,7 @@ _ide_helper_models.php
!**/data/
# 그리고 .gitkeep은 예외로 추적
!**/data/.gitkeep
# 시더 데이터 JSON은 추적
!database/seeders/data/kyungdong/
!database/seeders/data/kyungdong/**
storage/secrets/

View 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)
);
}
}

View 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 등은 그대로
};
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}
]

View 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
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff