품목기준 필드 관리 기능 구현

- ItemField 모델 및 SystemFieldDefinitions 상수 클래스 추가
- ItemFieldSeedingService: 시스템 필드 시딩/초기화/커스텀 필드 CRUD
- ItemFieldController (API): HTMX 기반 시딩 상태, 커스텀 필드 관리
- 커스텀 필드 수정 기능 (시스템 필드는 source_table/field_key 수정 불가)
- 레거시 데이터 표시 개선: 소스 테이블 비어있으면 '미지정' 배지
- 필드 키 정책 변경: 숫자로 시작 허용 (영문/숫자/밑줄)
- AI 문의하기: 시딩 오류 보고서 생성 기능
- 사이드바에 품목기준 필드 관리 메뉴 추가
This commit is contained in:
2025-12-09 23:13:27 +09:00
parent 36daf862b1
commit c1bd7ab4d3
12 changed files with 2234 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Constants;
/**
* 테이블별 시스템 필드 정의
*
* 시딩 시 사용되는 테이블별 고정 컬럼 필드 정의입니다.
*/
class SystemFieldDefinitions
{
/**
* products 테이블 시스템 필드 정의
*/
public const PRODUCTS = [
['field_key' => 'code', 'field_name' => '품목코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1],
['field_key' => 'name', 'field_name' => '품목명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2],
['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 3],
['field_key' => 'product_type', 'field_name' => '제품유형', 'field_type' => 'dropdown', 'order_no' => 4, 'options' => [
['label' => '완제품', 'value' => 'FG'],
['label' => '부품', 'value' => 'PT'],
]],
['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 5],
['field_key' => 'is_sellable', 'field_name' => '판매가능', 'field_type' => 'checkbox', 'order_no' => 6, 'default_value' => 'true'],
['field_key' => 'is_purchasable', 'field_name' => '구매가능', 'field_type' => 'checkbox', 'order_no' => 7],
['field_key' => 'is_producible', 'field_name' => '생산가능', 'field_type' => 'checkbox', 'order_no' => 8, 'default_value' => 'true'],
['field_key' => 'is_active', 'field_name' => '활성화', 'field_type' => 'checkbox', 'order_no' => 9, 'default_value' => 'true'],
['field_key' => 'certification_number', 'field_name' => '인증번호', 'field_type' => 'textbox', 'order_no' => 10],
['field_key' => 'certification_start_date', 'field_name' => '인증시작일', 'field_type' => 'date', 'order_no' => 11],
['field_key' => 'certification_end_date', 'field_name' => '인증만료일', 'field_type' => 'date', 'order_no' => 12],
];
/**
* materials 테이블 시스템 필드 정의
*/
public const MATERIALS = [
['field_key' => 'material_code', 'field_name' => '자재코드', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 1],
['field_key' => 'name', 'field_name' => '자재명', 'field_type' => 'textbox', 'is_required' => true, 'order_no' => 2],
['field_key' => 'item_name', 'field_name' => '품목명', 'field_type' => 'textbox', 'order_no' => 3],
['field_key' => 'specification', 'field_name' => '규격', 'field_type' => 'textbox', 'order_no' => 4],
['field_key' => 'unit', 'field_name' => '단위', 'field_type' => 'dropdown', 'is_required' => true, 'order_no' => 5],
['field_key' => 'category_id', 'field_name' => '카테고리', 'field_type' => 'dropdown', 'order_no' => 6],
['field_key' => 'is_inspection', 'field_name' => '검수필요', 'field_type' => 'checkbox', 'order_no' => 7],
['field_key' => 'search_tag', 'field_name' => '검색태그', 'field_type' => 'textarea', 'order_no' => 8],
];
/**
* product_components 테이블 (BOM) 시스템 필드 정의
*/
public const PRODUCT_COMPONENTS = [
['field_key' => 'ref_type', 'field_name' => '참조유형', 'field_type' => 'dropdown', 'order_no' => 1, 'options' => [
['label' => '제품', 'value' => 'product'],
['label' => '자재', 'value' => 'material'],
]],
['field_key' => 'ref_id', 'field_name' => '참조품목', 'field_type' => 'dropdown', 'order_no' => 2],
['field_key' => 'quantity', 'field_name' => '수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3],
['field_key' => 'formula', 'field_name' => '계산공식', 'field_type' => 'textbox', 'order_no' => 4],
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 5],
];
/**
* material_inspections 테이블 시스템 필드 정의
*/
public const MATERIAL_INSPECTIONS = [
['field_key' => 'inspection_date', 'field_name' => '검수일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1],
['field_key' => 'inspector_id', 'field_name' => '검수자', 'field_type' => 'dropdown', 'order_no' => 2],
['field_key' => 'status', 'field_name' => '검수상태', 'field_type' => 'dropdown', 'order_no' => 3, 'options' => [
['label' => '대기', 'value' => 'pending'],
['label' => '진행중', 'value' => 'in_progress'],
['label' => '완료', 'value' => 'completed'],
['label' => '불합격', 'value' => 'rejected'],
]],
['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 4],
['field_key' => 'quantity', 'field_name' => '검수수량', 'field_type' => 'number', 'order_no' => 5],
['field_key' => 'passed_quantity', 'field_name' => '합격수량', 'field_type' => 'number', 'order_no' => 6],
['field_key' => 'rejected_quantity', 'field_name' => '불합격수량', 'field_type' => 'number', 'order_no' => 7],
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 8],
];
/**
* material_receipts 테이블 시스템 필드 정의
*/
public const MATERIAL_RECEIPTS = [
['field_key' => 'receipt_date', 'field_name' => '입고일', 'field_type' => 'date', 'is_required' => true, 'order_no' => 1],
['field_key' => 'lot_no', 'field_name' => 'LOT번호', 'field_type' => 'textbox', 'order_no' => 2],
['field_key' => 'quantity', 'field_name' => '입고수량', 'field_type' => 'number', 'is_required' => true, 'order_no' => 3],
['field_key' => 'unit_price', 'field_name' => '단가', 'field_type' => 'number', 'order_no' => 4],
['field_key' => 'total_price', 'field_name' => '금액', 'field_type' => 'number', 'order_no' => 5],
['field_key' => 'supplier_id', 'field_name' => '공급업체', 'field_type' => 'dropdown', 'order_no' => 6],
['field_key' => 'warehouse_id', 'field_name' => '입고창고', 'field_type' => 'dropdown', 'order_no' => 7],
['field_key' => 'po_number', 'field_name' => '발주번호', 'field_type' => 'textbox', 'order_no' => 8],
['field_key' => 'invoice_number', 'field_name' => '송장번호', 'field_type' => 'textbox', 'order_no' => 9],
['field_key' => 'note', 'field_name' => '비고', 'field_type' => 'textarea', 'order_no' => 10],
];
/**
* 소스 테이블 목록
*/
public const SOURCE_TABLES = [
'products' => '제품',
'materials' => '자재',
'product_components' => 'BOM',
'material_inspections' => '자재검수',
'material_receipts' => '자재입고',
];
/**
* 소스 테이블별 필드 정의 가져오기
*/
public static function getFieldsFor(string $sourceTable): array
{
return match ($sourceTable) {
'products' => self::PRODUCTS,
'materials' => self::MATERIALS,
'product_components' => self::PRODUCT_COMPONENTS,
'material_inspections' => self::MATERIAL_INSPECTIONS,
'material_receipts' => self::MATERIAL_RECEIPTS,
default => [],
};
}
/**
* 전체 테이블 필드 수 조회
*/
public static function getTotalFieldCount(string $sourceTable): int
{
return count(self::getFieldsFor($sourceTable));
}
/**
* 모든 소스 테이블의 시스템 필드 키 목록
*/
public static function getAllSystemFieldKeys(string $sourceTable): array
{
$fields = self::getFieldsFor($sourceTable);
return array_column($fields, 'field_key');
}
}

View File

@@ -0,0 +1,366 @@
<?php
namespace App\Http\Controllers\Api\Admin;
use App\Constants\SystemFieldDefinitions;
use App\Http\Controllers\Controller;
use App\Services\ItemFieldSeedingService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
* 품목기준 필드 관리 API 컨트롤러 (HTMX)
*/
class ItemFieldController extends Controller
{
public function __construct(
private ItemFieldSeedingService $service
) {}
/**
* 시딩 상태 조회 (HTMX partial)
*/
public function seedingStatus(Request $request): View
{
$tenantId = session('selected_tenant_id');
Log::info('seedingStatus called', [
'tenantId' => $tenantId,
'session' => session()->all(),
]);
if (! $tenantId || $tenantId === 'all') {
return view('item-fields.partials.seeding-status', [
'statuses' => [],
'error' => '테넌트를 선택해주세요.',
]);
}
$statuses = $this->service->getSeedingStatus($tenantId);
Log::info('seedingStatus result', [
'statuses_count' => count($statuses),
'statuses' => $statuses,
]);
return view('item-fields.partials.seeding-status', compact('statuses'));
}
/**
* 단일 테이블 시딩
*/
public function seed(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$sourceTable = $request->input('source_table');
if (! $sourceTable) {
return response()->json([
'success' => false,
'message' => '소스 테이블을 지정해주세요.',
], 400);
}
$result = $this->service->seedTable($tenantId, $sourceTable);
return response()->json($result);
}
/**
* 전체 테이블 시딩
*/
public function seedAll(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$result = $this->service->seedAll($tenantId);
return response()->json($result);
}
/**
* 단일 테이블 초기화
*/
public function reset(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$sourceTable = $request->input('source_table');
if (! $sourceTable) {
return response()->json([
'success' => false,
'message' => '소스 테이블을 지정해주세요.',
], 400);
}
$result = $this->service->resetTable($tenantId, $sourceTable);
return response()->json($result);
}
/**
* 전체 테이블 초기화
*/
public function resetAll(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$result = $this->service->resetAll($tenantId);
return response()->json($result);
}
/**
* 커스텀 필드 목록 (HTMX partial)
*/
public function customFields(Request $request): View
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return view('item-fields.partials.custom-fields', [
'fields' => collect([]),
'sourceTables' => SystemFieldDefinitions::SOURCE_TABLES,
'error' => '테넌트를 선택해주세요.',
]);
}
$fields = $this->service->getCustomFields($tenantId, $request->all());
return view('item-fields.partials.custom-fields', [
'fields' => $fields,
'sourceTables' => SystemFieldDefinitions::SOURCE_TABLES,
]);
}
/**
* 커스텀 필드 추가
*/
public function storeCustomField(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$validated = $request->validate([
'source_table' => 'required|string',
'field_key' => 'required|string|max:100|regex:/^[a-zA-Z0-9_]+$/',
'field_name' => 'required|string|max:255',
'field_type' => 'required|string|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
'options' => 'nullable|array',
]);
$result = $this->service->createCustomField($tenantId, $validated);
return response()->json($result, $result['success'] ? 200 : 400);
}
/**
* 커스텀 필드 수정
*/
public function updateCustomField(Request $request, int $id): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$validated = $request->validate([
'source_table' => 'nullable|string',
'field_key' => 'nullable|string|max:100|regex:/^[a-zA-Z0-9_]+$/',
'field_name' => 'required|string|max:255',
'field_type' => 'required|string|in:textbox,number,dropdown,checkbox,date,textarea',
'is_required' => 'nullable|boolean',
'default_value' => 'nullable|string',
'options' => 'nullable|array',
]);
$result = $this->service->updateCustomField($tenantId, $id, $validated);
return response()->json($result, $result['success'] ? 200 : 400);
}
/**
* 커스텀 필드 삭제
*/
public function destroyCustomField(int $id): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$result = $this->service->deleteCustomField($tenantId, $id);
return response()->json($result, $result['success'] ? 200 : 400);
}
/**
* 커스텀 필드 일괄 삭제
*/
public function destroyCustomFields(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
if (! $tenantId || $tenantId === 'all') {
return response()->json([
'success' => false,
'message' => '테넌트를 선택해주세요.',
], 400);
}
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'required|integer',
]);
$result = $this->service->deleteCustomFields($tenantId, $validated['ids']);
return response()->json($result);
}
/**
* 소스 테이블 목록 (JSON)
*/
public function sourceTables(): JsonResponse
{
return response()->json([
'success' => true,
'data' => SystemFieldDefinitions::SOURCE_TABLES,
]);
}
/**
* 오류 로그 조회 (HTMX partial)
*/
public function errorLogs(): View
{
$errorLogs = $this->service->getErrorLogs();
return view('item-fields.partials.error-logs', compact('errorLogs'));
}
/**
* 오류 로그 초기화
*/
public function clearErrorLogs(): JsonResponse
{
$this->service->clearErrorLogs();
return response()->json([
'success' => true,
'message' => '오류 로그가 초기화되었습니다.',
]);
}
/**
* AI 문의용 오류 보고서 생성
*/
public function generateErrorReport(): JsonResponse
{
$tenantId = session('selected_tenant_id');
$errorLogs = $this->service->getErrorLogs();
if (empty($errorLogs)) {
return response()->json([
'success' => false,
'message' => '저장된 오류 로그가 없습니다.',
]);
}
$latestError = $errorLogs[0];
// 보고서 생성
$report = $this->buildErrorReport($latestError, $tenantId);
return response()->json([
'success' => true,
'report' => $report,
'error_count' => count($latestError['errors'] ?? []),
]);
}
/**
* 오류 보고서 텍스트 생성
*/
protected function buildErrorReport(array $errorLog, ?int $tenantId): string
{
$errors = $errorLog['errors'] ?? [];
$sourceTable = $errorLog['source_table'] ?? 'unknown';
$timestamp = $errorLog['timestamp'] ?? now()->toDateTimeString();
$report = "## 품목기준 필드 시딩 오류 보고서\n\n";
$report .= "### 기본 정보\n";
$report .= "- **발생 시각**: {$timestamp}\n";
$report .= "- **테넌트 ID**: {$tenantId}\n";
$report .= "- **소스 테이블**: {$sourceTable}\n";
$report .= '- **오류 수**: '.count($errors)."\n\n";
$report .= "### 오류 상세\n";
foreach ($errors as $index => $error) {
$num = $index + 1;
$report .= "\n#### 오류 #{$num}\n";
$report .= "- **필드 키**: `{$error['field_key']}`\n";
$report .= "- **필드명**: {$error['field_name']}\n";
$report .= "- **오류 코드**: {$error['error_code']}\n";
$report .= "- **오류 메시지**:\n```\n{$error['error_message']}\n```\n";
}
$report .= "\n### 환경 정보\n";
$report .= "- **애플리케이션**: MNG (품목기준 필드 관리)\n";
$report .= "- **DB 테이블**: item_fields\n";
$report .= "- **관련 파일**: `app/Services/ItemFieldSeedingService.php`\n";
$report .= "\n### 요청 사항\n";
$report .= "위 오류의 원인을 분석하고 해결 방법을 제시해 주세요.\n";
return $report;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use Illuminate\View\View;
/**
* 품목기준 필드 관리 컨트롤러 (Blade 화면)
*/
class ItemFieldController extends Controller
{
/**
* 필드 관리 메인 화면
*/
public function index(): View
{
return view('item-fields.index');
}
}

89
app/Models/ItemField.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
namespace App\Models;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 품목기준 필드 모델
*
* API DB의 item_fields 테이블을 참조합니다.
*/
class ItemField extends Model
{
use BelongsToTenant, SoftDeletes;
protected $fillable = [
'tenant_id',
'group_id',
'field_name',
'field_key',
'field_type',
'order_no',
'is_required',
'default_value',
'placeholder',
'display_condition',
'validation_rules',
'options',
'properties',
'category',
'description',
'is_common',
'is_active',
'is_locked',
'locked_by',
'locked_at',
'created_by',
'updated_by',
'deleted_by',
// 내부용 매핑 컬럼
'source_table',
'source_column',
'storage_type',
'json_path',
];
protected $casts = [
'group_id' => 'integer',
'order_no' => 'integer',
'is_required' => 'boolean',
'is_common' => 'boolean',
'is_active' => 'boolean',
'is_locked' => 'boolean',
'display_condition' => 'array',
'validation_rules' => 'array',
'options' => 'array',
'properties' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
'locked_at' => 'datetime',
];
/**
* 시스템 필드 여부 확인 (DB 컬럼과 매핑된 필드)
*/
public function isSystemField(): bool
{
return $this->storage_type === 'column' && ! is_null($this->source_column);
}
/**
* 컬럼 저장 방식 여부 확인
*/
public function isColumnStorage(): bool
{
return $this->storage_type === 'column';
}
/**
* JSON 저장 방식 여부 확인
*/
public function isJsonStorage(): bool
{
return $this->storage_type === 'json';
}
}

View File

@@ -0,0 +1,509 @@
<?php
namespace App\Services;
use App\Constants\SystemFieldDefinitions;
use App\Models\ItemField;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* 품목기준 필드 시딩 서비스
*
* 테넌트별 시스템 필드 시딩 및 커스텀 필드 관리
*/
class ItemFieldSeedingService
{
/**
* 최근 오류 목록 (세션 저장용)
*/
protected array $errors = [];
/**
* 테이블별 시딩 상태 조회
*/
public function getSeedingStatus(int $tenantId): array
{
$statuses = [];
foreach (SystemFieldDefinitions::SOURCE_TABLES as $sourceTable => $label) {
$totalCount = SystemFieldDefinitions::getTotalFieldCount($sourceTable);
// 현재 등록된 시스템 필드 수
$currentCount = ItemField::where('tenant_id', $tenantId)
->where('source_table', $sourceTable)
->where('storage_type', 'column')
->count();
$statuses[$sourceTable] = [
'label' => $label,
'source_table' => $sourceTable,
'total_count' => $totalCount,
'current_count' => $currentCount,
'is_complete' => $currentCount >= $totalCount,
'status' => $currentCount >= $totalCount ? 'complete' : ($currentCount > 0 ? 'partial' : 'empty'),
];
}
return $statuses;
}
/**
* 단일 테이블 시스템 필드 시딩
*/
public function seedTable(int $tenantId, string $sourceTable): array
{
$fields = SystemFieldDefinitions::getFieldsFor($sourceTable);
if (empty($fields)) {
return [
'success' => false,
'message' => '알 수 없는 소스 테이블입니다.',
'seeded_count' => 0,
];
}
$seededCount = 0;
$skippedCount = 0;
$errorCount = 0;
$this->errors = [];
foreach ($fields as $fieldData) {
try {
// 이미 존재하는지 확인 (field_key + source_table 조합)
$exists = ItemField::where('tenant_id', $tenantId)
->where('source_table', $sourceTable)
->where('field_key', $fieldData['field_key'])
->exists();
if ($exists) {
$skippedCount++;
continue;
}
// DB 유니크 제약조건 충돌 방지: tenant_id + field_key 조합 체크
// (DB에 source_table 없이 유니크 키가 있는 경우를 대비)
$existsGlobal = ItemField::where('tenant_id', $tenantId)
->where('field_key', $fieldData['field_key'])
->exists();
if ($existsGlobal) {
// 다른 source_table에 같은 field_key가 있으면 고유한 키로 변환
$fieldKey = "{$sourceTable}_{$fieldData['field_key']}";
// 변환된 키도 이미 존재하는지 확인
$existsTransformed = ItemField::where('tenant_id', $tenantId)
->where('field_key', $fieldKey)
->exists();
if ($existsTransformed) {
$skippedCount++;
continue;
}
} else {
$fieldKey = $fieldData['field_key'];
}
// 필드 생성
ItemField::create([
'tenant_id' => $tenantId,
'group_id' => 1, // 품목관리 그룹
'field_key' => $fieldKey,
'field_name' => $fieldData['field_name'],
'field_type' => $fieldData['field_type'],
'order_no' => $fieldData['order_no'] ?? 0,
'is_required' => $fieldData['is_required'] ?? false,
'default_value' => $fieldData['default_value'] ?? null,
'options' => $fieldData['options'] ?? null,
'source_table' => $sourceTable,
'source_column' => $fieldData['field_key'], // 원본 컬럼명 유지
'storage_type' => 'column',
'is_active' => true,
'is_common' => true,
'created_by' => auth()->id(),
]);
$seededCount++;
} catch (\Illuminate\Database\QueryException $e) {
$errorCount++;
$errorInfo = [
'field_key' => $fieldData['field_key'],
'field_name' => $fieldData['field_name'],
'source_table' => $sourceTable,
'error_code' => $e->getCode(),
'error_message' => $e->getMessage(),
'sql_state' => $e->errorInfo[0] ?? null,
'timestamp' => now()->toDateTimeString(),
];
$this->errors[] = $errorInfo;
Log::error('ItemField 시딩 오류', [
'tenant_id' => $tenantId,
'source_table' => $sourceTable,
'field_key' => $fieldData['field_key'],
'error' => $e->getMessage(),
]);
} catch (\Exception $e) {
$errorCount++;
$this->errors[] = [
'field_key' => $fieldData['field_key'],
'field_name' => $fieldData['field_name'],
'source_table' => $sourceTable,
'error_code' => 'UNKNOWN',
'error_message' => $e->getMessage(),
'timestamp' => now()->toDateTimeString(),
];
Log::error('ItemField 시딩 예외', [
'tenant_id' => $tenantId,
'source_table' => $sourceTable,
'field_key' => $fieldData['field_key'],
'error' => $e->getMessage(),
]);
}
}
// 오류가 있으면 세션에 저장
if (! empty($this->errors)) {
$this->saveErrorsToSession($tenantId, $sourceTable);
}
$success = $errorCount === 0;
$message = $success
? "시딩 완료: {$seededCount}개 추가, {$skippedCount}개 건너뜀"
: "시딩 부분 완료: {$seededCount}개 추가, {$skippedCount}개 건너뜀, {$errorCount}개 오류";
return [
'success' => $success,
'message' => $message,
'seeded_count' => $seededCount,
'skipped_count' => $skippedCount,
'error_count' => $errorCount,
'errors' => $this->errors,
'has_errors' => $errorCount > 0,
];
}
/**
* 오류 정보를 세션에 저장
*/
protected function saveErrorsToSession(int $tenantId, string $sourceTable): void
{
$errorLog = [
'tenant_id' => $tenantId,
'source_table' => $sourceTable,
'errors' => $this->errors,
'timestamp' => now()->toDateTimeString(),
'user_id' => auth()->id(),
'user_name' => auth()->user()?->name,
];
// 기존 오류 로그 가져오기
$existingLogs = session('item_field_errors', []);
// 최신 오류를 앞에 추가 (최대 10개 유지)
array_unshift($existingLogs, $errorLog);
$existingLogs = array_slice($existingLogs, 0, 10);
session(['item_field_errors' => $existingLogs]);
}
/**
* 저장된 오류 로그 조회
*/
public function getErrorLogs(): array
{
return session('item_field_errors', []);
}
/**
* 오류 로그 초기화
*/
public function clearErrorLogs(): void
{
session()->forget('item_field_errors');
}
/**
* 전체 테이블 시스템 필드 시딩
*/
public function seedAll(int $tenantId): array
{
$results = [];
$totalSeeded = 0;
$totalSkipped = 0;
foreach (array_keys(SystemFieldDefinitions::SOURCE_TABLES) as $sourceTable) {
$result = $this->seedTable($tenantId, $sourceTable);
$results[$sourceTable] = $result;
$totalSeeded += $result['seeded_count'];
$totalSkipped += $result['skipped_count'] ?? 0;
}
return [
'success' => true,
'message' => "전체 시딩 완료: {$totalSeeded}개 추가, {$totalSkipped}개 건너뜀",
'total_seeded' => $totalSeeded,
'total_skipped' => $totalSkipped,
'details' => $results,
];
}
/**
* 단일 테이블 시스템 필드 초기화 (삭제 후 재시딩)
*/
public function resetTable(int $tenantId, string $sourceTable): array
{
// 시스템 필드만 삭제 (커스텀 필드는 유지)
$deletedCount = ItemField::where('tenant_id', $tenantId)
->where('source_table', $sourceTable)
->where('storage_type', 'column')
->forceDelete();
// 재시딩
$seedResult = $this->seedTable($tenantId, $sourceTable);
return [
'success' => true,
'message' => "초기화 완료: {$deletedCount}개 삭제, {$seedResult['seeded_count']}개 재등록",
'deleted_count' => $deletedCount,
'seeded_count' => $seedResult['seeded_count'],
];
}
/**
* 전체 테이블 시스템 필드 초기화
*/
public function resetAll(int $tenantId): array
{
$results = [];
$totalDeleted = 0;
$totalSeeded = 0;
foreach (array_keys(SystemFieldDefinitions::SOURCE_TABLES) as $sourceTable) {
$result = $this->resetTable($tenantId, $sourceTable);
$results[$sourceTable] = $result;
$totalDeleted += $result['deleted_count'];
$totalSeeded += $result['seeded_count'];
}
return [
'success' => true,
'message' => "전체 초기화 완료: {$totalDeleted}개 삭제, {$totalSeeded}개 재등록",
'total_deleted' => $totalDeleted,
'total_seeded' => $totalSeeded,
'details' => $results,
];
}
/**
* 커스텀 필드 목록 조회
*/
public function getCustomFields(int $tenantId, array $filters = []): Collection
{
$query = ItemField::where('tenant_id', $tenantId)
->where('storage_type', 'json');
// 소스 테이블 필터
if (! empty($filters['source_table'])) {
$query->where('source_table', $filters['source_table']);
}
// 필드 타입 필터
if (! empty($filters['field_type'])) {
$query->where('field_type', $filters['field_type']);
}
// 검색
if (! empty($filters['search'])) {
$search = $filters['search'];
$query->where(function ($q) use ($search) {
$q->where('field_key', 'like', "%{$search}%")
->orWhere('field_name', 'like', "%{$search}%");
});
}
return $query->orderBy('source_table')
->orderBy('order_no')
->get();
}
/**
* 커스텀 필드 추가
*/
public function createCustomField(int $tenantId, array $data): array
{
$sourceTable = $data['source_table'];
$fieldKey = $data['field_key'];
// 시스템 예약어 체크
$systemFieldKeys = SystemFieldDefinitions::getAllSystemFieldKeys($sourceTable);
if (in_array($fieldKey, $systemFieldKeys)) {
return [
'success' => false,
'message' => "\"{$fieldKey}\"은(는) 시스템 예약어로 사용할 수 없습니다.",
];
}
// 기존 필드 키 중복 체크
$exists = ItemField::where('tenant_id', $tenantId)
->where('source_table', $sourceTable)
->where('field_key', $fieldKey)
->exists();
if ($exists) {
return [
'success' => false,
'message' => "\"{$fieldKey}\"는 이미 사용 중인 필드 키입니다.",
];
}
// 커스텀 필드 생성
$field = ItemField::create([
'tenant_id' => $tenantId,
'group_id' => 1,
'field_key' => $fieldKey,
'field_name' => $data['field_name'],
'field_type' => $data['field_type'],
'order_no' => $data['order_no'] ?? 0,
'is_required' => $data['is_required'] ?? false,
'default_value' => $data['default_value'] ?? null,
'options' => $data['options'] ?? null,
'source_table' => $sourceTable,
'source_column' => null,
'storage_type' => 'json',
'json_path' => "attributes.{$fieldKey}",
'is_active' => true,
'is_common' => false,
'created_by' => auth()->id(),
]);
return [
'success' => true,
'message' => '커스텀 필드가 추가되었습니다.',
'field' => $field,
];
}
/**
* 커스텀 필드 수정
*/
public function updateCustomField(int $tenantId, int $fieldId, array $data): array
{
$field = ItemField::where('tenant_id', $tenantId)
->where('id', $fieldId)
->first();
if (! $field) {
return [
'success' => false,
'message' => '필드를 찾을 수 없습니다.',
];
}
// 시스템 필드(storage_type = column)의 경우 일부 필드만 수정 가능
$isSystemField = $field->storage_type === 'column';
// field_key 변경 시 중복 체크 (시스템 필드는 field_key 변경 불가)
if (! $isSystemField && ! empty($data['field_key']) && $data['field_key'] !== $field->field_key) {
$exists = ItemField::where('tenant_id', $tenantId)
->where('field_key', $data['field_key'])
->where('id', '!=', $fieldId)
->exists();
if ($exists) {
return [
'success' => false,
'message' => "\"{$data['field_key']}\"는 이미 사용 중인 필드 키입니다.",
];
}
}
// 업데이트 데이터 준비
$updateData = [
'field_name' => $data['field_name'],
'field_type' => $data['field_type'],
'is_required' => $data['is_required'] ?? false,
'default_value' => $data['default_value'] ?? null,
'options' => $data['options'] ?? null,
'updated_by' => auth()->id(),
];
// 커스텀 필드만 source_table, field_key 변경 가능
if (! $isSystemField) {
if (isset($data['source_table'])) {
$updateData['source_table'] = $data['source_table'];
}
if (isset($data['field_key'])) {
$updateData['field_key'] = $data['field_key'];
$updateData['json_path'] = "attributes.{$data['field_key']}";
}
}
$field->update($updateData);
return [
'success' => true,
'message' => '커스텀 필드가 수정되었습니다.',
'field' => $field->fresh(),
];
}
/**
* 커스텀 필드 삭제
*/
public function deleteCustomField(int $tenantId, int $fieldId): array
{
$field = ItemField::where('tenant_id', $tenantId)
->where('id', $fieldId)
->first();
if (! $field) {
return [
'success' => false,
'message' => '필드를 찾을 수 없습니다.',
];
}
// 시스템 필드는 삭제 불가
if ($field->storage_type === 'column') {
return [
'success' => false,
'message' => '시스템 필드는 삭제할 수 없습니다. 초기화 기능을 사용하세요.',
];
}
$field->update(['deleted_by' => auth()->id()]);
$field->delete();
return [
'success' => true,
'message' => '커스텀 필드가 삭제되었습니다.',
];
}
/**
* 커스텀 필드 일괄 삭제
*/
public function deleteCustomFields(int $tenantId, array $fieldIds): array
{
$deletedCount = 0;
foreach ($fieldIds as $fieldId) {
$result = $this->deleteCustomField($tenantId, $fieldId);
if ($result['success']) {
$deletedCount++;
}
}
return [
'success' => true,
'message' => "{$deletedCount}개의 커스텀 필드가 삭제되었습니다.",
'deleted_count' => $deletedCount,
];
}
}