Files
sam-manage/app/Services/ItemFieldSeedingService.php

649 lines
21 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Services;
use App\Models\ItemField;
use App\Models\SystemFieldDefinition;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* 품목기준 필드 시딩 서비스
*
* 테넌트별 시스템 필드 시딩 커스텀 필드 관리
* DB 기반 시스템 필드 정의 사용 (system_field_definitions 테이블)
*/
class ItemFieldSeedingService
{
/**
* 최근 오류 목록 (세션 저장용)
*/
protected array $errors = [];
/**
* 테이블별 시딩 상태 조회
*/
public function getSeedingStatus(int $tenantId): array
{
$statuses = [];
$sourceTables = SystemFieldDefinition::getSourceTableOptions();
foreach ($sourceTables as $sourceTable => $label) {
$totalCount = SystemFieldDefinition::getFieldCountFor($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;
}
/**
* 특정 소스 테이블의 시딩 가능한 필드 목록 조회
*
* @param bool $onlyDefault true면 기본 시딩 대상만, false면 전체
*/
public function getAvailableFields(string $sourceTable, bool $onlyDefault = false): Collection
{
if ($onlyDefault) {
return SystemFieldDefinition::getSeedDefaultFieldsFor($sourceTable);
}
return SystemFieldDefinition::getFieldsFor($sourceTable);
}
/**
* 단일 테이블 시스템 필드 시딩
*
* @param array|null $fieldIds 시딩할 필드 ID 목록 (null이면 is_seed_default=true인 것만)
*/
public function seedTable(int $tenantId, string $sourceTable, ?array $fieldIds = null): array
{
// 필드 ID가 지정되면 해당 필드만, 아니면 기본 시딩 대상만
if ($fieldIds !== null) {
$fields = SystemFieldDefinition::whereIn('id', $fieldIds)
->where('source_table', $sourceTable)
->where('is_active', true)
->orderBy('order_no')
->get();
} else {
$fields = SystemFieldDefinition::getSeedDefaultFieldsFor($sourceTable);
}
if ($fields->isEmpty()) {
return [
'success' => false,
'message' => '시딩할 필드가 없습니다.',
'seeded_count' => 0,
];
}
$seededCount = 0;
$skippedCount = 0;
$errorCount = 0;
$this->errors = [];
foreach ($fields as $fieldDef) {
try {
// 이미 존재하는지 확인 (field_key + source_table 조합)
$exists = ItemField::where('tenant_id', $tenantId)
->where('source_table', $sourceTable)
->where('field_key', $fieldDef->field_key)
->exists();
if ($exists) {
$skippedCount++;
continue;
}
// DB 유니크 제약조건 충돌 방지: tenant_id + field_key 조합 체크
$existsGlobal = ItemField::where('tenant_id', $tenantId)
->where('field_key', $fieldDef->field_key)
->exists();
if ($existsGlobal) {
// 다른 source_table에 같은 field_key가 있으면 고유한 키로 변환
$fieldKey = "{$sourceTable}_{$fieldDef->field_key}";
// 변환된 키도 이미 존재하는지 확인
$existsTransformed = ItemField::where('tenant_id', $tenantId)
->where('field_key', $fieldKey)
->exists();
if ($existsTransformed) {
$skippedCount++;
continue;
}
} else {
$fieldKey = $fieldDef->field_key;
}
// 필드 생성
ItemField::create([
'tenant_id' => $tenantId,
'group_id' => 1, // 품목관리 그룹
'field_key' => $fieldKey,
'field_name' => $fieldDef->field_name,
'field_type' => $fieldDef->field_type,
'order_no' => $fieldDef->order_no ?? 0,
'is_required' => $fieldDef->is_required ?? false,
'default_value' => $fieldDef->default_value ?? null,
'options' => $fieldDef->options ?? null,
'source_table' => $sourceTable,
'source_column' => $fieldDef->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' => $fieldDef->field_key,
'field_name' => $fieldDef->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' => $fieldDef->field_key,
'error' => $e->getMessage(),
]);
} catch (\Exception $e) {
$errorCount++;
$this->errors[] = [
'field_key' => $fieldDef->field_key,
'field_name' => $fieldDef->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' => $fieldDef->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;
$sourceTables = array_keys(SystemFieldDefinition::getSourceTableOptions());
foreach ($sourceTables 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;
$sourceTables = array_keys(SystemFieldDefinition::getSourceTableOptions());
foreach ($sourceTables 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,
];
}
/**
* 필드 목록 조회 (시스템 + 커스텀)
*
* @param array $filters source_table, field_type, field_category (system|custom), search, per_page
*/
public function getFields(int $tenantId, array $filters = []): \Illuminate\Contracts\Pagination\LengthAwarePaginator
{
$query = ItemField::withTrashed()->where('tenant_id', $tenantId);
// 필드 유형 필터 (system=is_common:1, custom=is_common:0)
if (! empty($filters['field_category'])) {
if ($filters['field_category'] === 'system') {
$query->where('is_common', true);
} elseif ($filters['field_category'] === 'custom') {
$query->where('is_common', false);
}
}
// 소스 테이블 필터
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}%");
});
}
// 페이지당 항목 수 (기본 20개)
$perPage = ! empty($filters['per_page']) ? (int) $filters['per_page'] : 20;
// 시스템 필드 우선 정렬 (is_common DESC), 그 다음 source_table, order_no
return $query->orderByDesc('is_common')
->orderBy('source_table')
->orderBy('order_no')
->paginate($perPage)
->withQueryString();
}
/**
* 커스텀 필드 목록 조회 (기존 호환성 유지)
*
* @deprecated Use getFields() instead
*/
public function getCustomFields(int $tenantId, array $filters = []): Collection
{
// 기존 호환성 유지: 커스텀 필드만 반환
$filters['field_category'] = 'custom';
return $this->getFields($tenantId, $filters);
}
/**
* 커스텀 필드 추가
*/
public function createCustomField(int $tenantId, array $data): array
{
$sourceTable = $data['source_table'];
$fieldKey = $data['field_key'];
// 시스템 예약어 체크
$systemFieldKeys = SystemFieldDefinition::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' => '필드를 찾을 수 없습니다.',
];
}
$field->forceDelete();
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,
];
}
/**
* 소프트 삭제된 커스텀 필드 복원
*/
public function restoreCustomField(int $tenantId, int $fieldId): array
{
$field = ItemField::withTrashed()
->where('tenant_id', $tenantId)
->where('id', $fieldId)
->first();
if (! $field) {
return [
'success' => false,
'message' => '필드를 찾을 수 없습니다.',
];
}
if (is_null($field->deleted_at)) {
return [
'success' => false,
'message' => '이미 활성화된 필드입니다.',
];
}
$field->restore();
return [
'success' => true,
'message' => '필드가 복원되었습니다.',
];
}
/**
* 커스텀 필드 영구 삭제
*/
public function forceDeleteCustomField(int $tenantId, int $fieldId): array
{
$field = ItemField::withTrashed()
->where('tenant_id', $tenantId)
->where('id', $fieldId)
->first();
if (! $field) {
return [
'success' => false,
'message' => '필드를 찾을 수 없습니다.',
];
}
$field->forceDelete();
return [
'success' => true,
'message' => '필드가 영구 삭제되었습니다.',
];
}
/**
* 삭제된 필드 일괄 영구 삭제 (휴지통 비우기)
*/
public function purgeDeletedFields(int $tenantId): array
{
// 삭제된 필드(soft deleted) 조회
$deletedFields = ItemField::onlyTrashed()
->where('tenant_id', $tenantId)
->get();
if ($deletedFields->isEmpty()) {
return [
'success' => true,
'message' => '삭제된 필드가 없습니다.',
'purged_count' => 0,
];
}
$purgedCount = 0;
foreach ($deletedFields as $field) {
$field->forceDelete();
$purgedCount++;
}
return [
'success' => true,
'message' => "{$purgedCount}개의 삭제된 필드가 영구 삭제되었습니다.",
'purged_count' => $purgedCount,
];
}
}