Files
sam-manage/app/Services/ItemFieldSeedingService.php
hskwon 767db6f513 feat: 품목 필드 관리 및 UI 개선
- ItemFieldController API 수정
- ItemFieldSeedingService 로직 개선
- Flow Tester 상세 화면 개선
- 레이아웃 및 프로젝트 상세 화면 수정
- 테이블 정렬 JS 추가
2025-12-12 08:51:54 +09:00

535 lines
18 KiB
PHP

<?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,
];
}
/**
* 필드 목록 조회 (시스템 + 커스텀)
*
* @param array $filters source_table, field_type, field_category (system|custom), search
*/
public function getFields(int $tenantId, array $filters = []): Collection
{
$query = ItemField::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}%");
});
}
// 시스템 필드 우선 정렬 (is_common DESC), 그 다음 source_table, order_no
return $query->orderByDesc('is_common')
->orderBy('source_table')
->orderBy('order_no')
->get();
}
/**
* 커스텀 필드 목록 조회 (기존 호환성 유지)
*
* @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 = 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,
];
}
}