- block-editor.blade.php: 3패널 UI (Palette + Canvas + Properties) - Alpine.js blockEditor() 컴포넌트 (CRUD, Undo/Redo, SortableJS) - 기본 Block 6종: heading, paragraph, table, columns, divider, spacer - 폼 필드 Block 7종: text, number, date, select, checkbox, textarea, signature - BlockRendererService: JSON → HTML 렌더링 서비스 - 컨트롤러 분기: builder_type = 'block' → 블록 빌더 뷰 - 라우트 추가: block-create, block-edit - API store/update에 schema JSON 처리 추가 - index 페이지에 블록 빌더 진입 버튼 추가 - 목록에 builder_type 뱃지 표시
918 lines
36 KiB
PHP
918 lines
36 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\DocumentTemplate;
|
|
use App\Models\DocumentTemplateApprovalLine;
|
|
use App\Models\DocumentTemplateBasicField;
|
|
use App\Models\DocumentTemplateColumn;
|
|
use App\Models\DocumentTemplateLink;
|
|
use App\Models\DocumentTemplateLinkValue;
|
|
use App\Models\DocumentTemplateSection;
|
|
use App\Models\DocumentTemplateSectionField;
|
|
use App\Models\DocumentTemplateSectionItem;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\View\View;
|
|
|
|
class DocumentTemplateApiController extends Controller
|
|
{
|
|
/**
|
|
* 목록 조회 (HTMX 테이블)
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
$query = DocumentTemplate::query()
|
|
->withCount(['sections', 'columns']);
|
|
|
|
// 선택된 테넌트 필터
|
|
$tenantId = session('selected_tenant_id');
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
}
|
|
|
|
// 슈퍼관리자 휴지통 조회
|
|
$showTrashed = $request->filled('trashed') && auth()->user()?->is_super_admin;
|
|
if ($showTrashed) {
|
|
$query->onlyTrashed();
|
|
}
|
|
|
|
// 검색 (양식명, 제목, 분류, 연결 품목)
|
|
if ($search = $request->input('search')) {
|
|
// 연결 품목 검색을 위해 품목명으로 ID 조회
|
|
$matchingItemIds = \App\Models\Items\Item::where('name', 'like', "%{$search}%")
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
$query->where(function ($q) use ($search, $matchingItemIds) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('title', 'like', "%{$search}%")
|
|
->orWhere('category', 'like', "%{$search}%");
|
|
|
|
// 연결 품목 검색 (JSON 배열에서 매칭)
|
|
foreach ($matchingItemIds as $itemId) {
|
|
$q->orWhereJsonContains('linked_item_ids', $itemId);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 카테고리 필터
|
|
if ($category = $request->input('category')) {
|
|
$query->where('category', $category);
|
|
}
|
|
|
|
// 활성 상태 필터
|
|
if ($request->filled('is_active') && ! $showTrashed) {
|
|
$query->where('is_active', $request->boolean('is_active'));
|
|
}
|
|
|
|
$templates = $query->orderBy('updated_at', 'desc')
|
|
->paginate($request->input('per_page', 10));
|
|
|
|
// 연결된 품목 ID 수집 및 품목명 조회
|
|
$allItemIds = $templates->pluck('linked_item_ids')->flatten()->filter()->unique()->values()->toArray();
|
|
$itemNames = [];
|
|
if (! empty($allItemIds)) {
|
|
$itemNames = \App\Models\Items\Item::whereIn('id', $allItemIds)
|
|
->pluck('name', 'id')
|
|
->toArray();
|
|
}
|
|
|
|
// 분류 코드 → 이름 매핑
|
|
$tenantId = session('selected_tenant_id');
|
|
$categoryNames = DB::table('common_codes')
|
|
->where('code_group', 'document_category')
|
|
->where('is_active', true)
|
|
->where(function ($q) use ($tenantId) {
|
|
$q->whereNull('tenant_id');
|
|
if ($tenantId) {
|
|
$q->orWhere('tenant_id', $tenantId);
|
|
}
|
|
})
|
|
->pluck('name', 'code')
|
|
->toArray();
|
|
|
|
return view('document-templates.partials.table', compact('templates', 'showTrashed', 'itemNames', 'categoryNames'));
|
|
}
|
|
|
|
/**
|
|
* 단일 조회
|
|
*/
|
|
public function show(int $id): JsonResponse
|
|
{
|
|
$template = DocumentTemplate::with([
|
|
'approvalLines',
|
|
'basicFields',
|
|
'sections.items',
|
|
'columns',
|
|
'sectionFields',
|
|
'links.linkValues',
|
|
])->findOrFail($id);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $template,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 생성
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:100',
|
|
'category' => 'nullable|string|max:50',
|
|
'builder_type' => 'nullable|string|in:legacy,block',
|
|
'title' => 'nullable|string|max:200',
|
|
'company_name' => 'nullable|string|max:100',
|
|
'company_address' => 'nullable|string|max:255',
|
|
'company_contact' => 'nullable|string|max:100',
|
|
'footer_remark_label' => 'nullable|string|max:50',
|
|
'footer_judgement_label' => 'nullable|string|max:50',
|
|
'footer_judgement_options' => 'nullable|array',
|
|
'schema' => 'nullable|array',
|
|
'page_config' => 'nullable|array',
|
|
'is_active' => 'boolean',
|
|
'linked_item_ids' => 'nullable|array',
|
|
'linked_item_ids.*' => 'integer',
|
|
'linked_process_id' => 'nullable|integer',
|
|
// 관계 데이터
|
|
'approval_lines' => 'nullable|array',
|
|
'basic_fields' => 'nullable|array',
|
|
'sections' => 'nullable|array',
|
|
'columns' => 'nullable|array',
|
|
'section_fields' => 'nullable|array',
|
|
'template_links' => 'nullable|array',
|
|
]);
|
|
|
|
// 동일 분류 내 연결품목 중복 검증
|
|
$category = $validated['category'] ?? null;
|
|
$newItemIds = $this->extractLinkedItemIds($validated);
|
|
$duplicateError = $this->checkLinkedItemDuplicates($category, $newItemIds);
|
|
if ($duplicateError) {
|
|
return $duplicateError;
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$template = DocumentTemplate::create([
|
|
'tenant_id' => session('selected_tenant_id'),
|
|
'name' => $validated['name'],
|
|
'category' => $validated['category'] ?? null,
|
|
'builder_type' => $validated['builder_type'] ?? 'legacy',
|
|
'title' => $validated['title'] ?? null,
|
|
'company_name' => $validated['company_name'] ?? '경동기업',
|
|
'company_address' => $validated['company_address'] ?? null,
|
|
'company_contact' => $validated['company_contact'] ?? null,
|
|
'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용',
|
|
'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정',
|
|
'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'],
|
|
'schema' => $validated['schema'] ?? null,
|
|
'page_config' => $validated['page_config'] ?? null,
|
|
'is_active' => $validated['is_active'] ?? true,
|
|
'linked_item_ids' => $validated['linked_item_ids'] ?? null,
|
|
'linked_process_id' => $validated['linked_process_id'] ?? null,
|
|
]);
|
|
|
|
// 관계 데이터 저장
|
|
$this->saveRelations($template, $validated);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '문서양식이 생성되었습니다.',
|
|
'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '생성 중 오류가 발생했습니다: '.$e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수정
|
|
*/
|
|
public function update(Request $request, int $id): JsonResponse
|
|
{
|
|
$template = DocumentTemplate::findOrFail($id);
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:100',
|
|
'category' => 'nullable|string|max:50',
|
|
'builder_type' => 'nullable|string|in:legacy,block',
|
|
'title' => 'nullable|string|max:200',
|
|
'company_name' => 'nullable|string|max:100',
|
|
'company_address' => 'nullable|string|max:255',
|
|
'company_contact' => 'nullable|string|max:100',
|
|
'footer_remark_label' => 'nullable|string|max:50',
|
|
'footer_judgement_label' => 'nullable|string|max:50',
|
|
'footer_judgement_options' => 'nullable|array',
|
|
'schema' => 'nullable|array',
|
|
'page_config' => 'nullable|array',
|
|
'is_active' => 'boolean',
|
|
'linked_item_ids' => 'nullable|array',
|
|
'linked_item_ids.*' => 'integer',
|
|
'linked_process_id' => 'nullable|integer',
|
|
// 관계 데이터
|
|
'approval_lines' => 'nullable|array',
|
|
'basic_fields' => 'nullable|array',
|
|
'sections' => 'nullable|array',
|
|
'columns' => 'nullable|array',
|
|
'section_fields' => 'nullable|array',
|
|
'template_links' => 'nullable|array',
|
|
]);
|
|
|
|
// 동일 분류 내 연결품목 중복 검증
|
|
$category = $validated['category'] ?? null;
|
|
$newItemIds = $this->extractLinkedItemIds($validated);
|
|
$duplicateError = $this->checkLinkedItemDuplicates($category, $newItemIds, $template->id);
|
|
if ($duplicateError) {
|
|
return $duplicateError;
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$updateData = [
|
|
'name' => $validated['name'],
|
|
'category' => $validated['category'] ?? null,
|
|
'title' => $validated['title'] ?? null,
|
|
'company_name' => $validated['company_name'] ?? null,
|
|
'company_address' => $validated['company_address'] ?? null,
|
|
'company_contact' => $validated['company_contact'] ?? null,
|
|
'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용',
|
|
'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정',
|
|
'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'],
|
|
'is_active' => $validated['is_active'] ?? true,
|
|
'linked_item_ids' => $validated['linked_item_ids'] ?? null,
|
|
'linked_process_id' => $validated['linked_process_id'] ?? null,
|
|
];
|
|
|
|
// 블록 빌더 전용 필드
|
|
if (isset($validated['builder_type'])) {
|
|
$updateData['builder_type'] = $validated['builder_type'];
|
|
}
|
|
if (array_key_exists('schema', $validated)) {
|
|
$updateData['schema'] = $validated['schema'];
|
|
}
|
|
if (array_key_exists('page_config', $validated)) {
|
|
$updateData['page_config'] = $validated['page_config'];
|
|
}
|
|
|
|
$template->update($updateData);
|
|
|
|
// 관계 데이터 저장 (기존 데이터 삭제 후 재생성)
|
|
$this->saveRelations($template, $validated, true);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '문서양식이 수정되었습니다.',
|
|
'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues']),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '수정 중 오류가 발생했습니다: '.$e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 삭제 (소프트 삭제)
|
|
*/
|
|
public function destroy(int $id): JsonResponse
|
|
{
|
|
$template = DocumentTemplate::findOrFail($id);
|
|
$template->update(['deleted_by' => auth()->id()]);
|
|
$template->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '문서양식이 삭제되었습니다.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 영구삭제 (슈퍼관리자 전용)
|
|
*/
|
|
public function forceDestroy(int $id): JsonResponse
|
|
{
|
|
if (! auth()->user()?->is_super_admin) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '슈퍼관리자만 영구 삭제할 수 있습니다.',
|
|
], 403);
|
|
}
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
$template = DocumentTemplate::withTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->findOrFail($id);
|
|
|
|
// 이 양식을 참조하는 문서가 있는지 확인 (소프트삭제 포함)
|
|
$documentCount = \App\Models\Documents\Document::withTrashed()
|
|
->where('template_id', $template->id)
|
|
->count();
|
|
|
|
if ($documentCount > 0) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => "이 양식을 사용한 문서가 {$documentCount}건 있어 영구 삭제할 수 없습니다. 문서를 먼저 삭제해주세요.",
|
|
], 422);
|
|
}
|
|
|
|
// 관련 데이터도 영구삭제
|
|
$template->approvalLines()->delete();
|
|
$template->basicFields()->delete();
|
|
$template->sections()->each(function ($section) {
|
|
$section->items()->delete();
|
|
$section->delete();
|
|
});
|
|
$template->columns()->delete();
|
|
$template->sectionFields()->delete();
|
|
$template->links()->delete(); // cascade로 linkValues도 삭제
|
|
$template->forceDelete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '문서양식이 영구 삭제되었습니다.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 삭제된 문서양식 복원 (슈퍼관리자 전용)
|
|
*/
|
|
public function restore(int $id): JsonResponse
|
|
{
|
|
if (! auth()->user()?->is_super_admin) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '슈퍼관리자만 복원할 수 있습니다.',
|
|
], 403);
|
|
}
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
$template = DocumentTemplate::onlyTrashed()
|
|
->where('tenant_id', $tenantId)
|
|
->findOrFail($id);
|
|
|
|
$template->update(['deleted_by' => null]);
|
|
$template->restore();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '문서양식이 복원되었습니다.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 활성 상태 토글
|
|
*/
|
|
public function toggleActive(int $id): JsonResponse
|
|
{
|
|
$template = DocumentTemplate::findOrFail($id);
|
|
$template->update(['is_active' => ! $template->is_active]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => $template->is_active ? '활성화되었습니다.' : '비활성화되었습니다.',
|
|
'is_active' => $template->is_active,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 양식 복제
|
|
*/
|
|
public function duplicate(Request $request, int $id): JsonResponse
|
|
{
|
|
$source = DocumentTemplate::with([
|
|
'approvalLines',
|
|
'basicFields',
|
|
'sections.items',
|
|
'columns',
|
|
'sectionFields',
|
|
'links.linkValues',
|
|
])->findOrFail($id);
|
|
|
|
$newName = $request->input('name', $source->name.' (복사)');
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$newTemplate = DocumentTemplate::create([
|
|
'tenant_id' => $source->tenant_id,
|
|
'name' => $newName,
|
|
'category' => $source->category,
|
|
'builder_type' => $source->builder_type ?? 'legacy',
|
|
'title' => $source->title,
|
|
'company_name' => $source->company_name,
|
|
'company_address' => $source->company_address,
|
|
'company_contact' => $source->company_contact,
|
|
'footer_remark_label' => $source->footer_remark_label,
|
|
'footer_judgement_label' => $source->footer_judgement_label,
|
|
'footer_judgement_options' => $source->footer_judgement_options,
|
|
'schema' => $source->schema,
|
|
'page_config' => $source->page_config,
|
|
'is_active' => false,
|
|
'linked_item_ids' => null, // 연결품목은 복사하지 않음 (중복 방지)
|
|
'linked_process_id' => null,
|
|
]);
|
|
|
|
foreach ($source->approvalLines as $line) {
|
|
DocumentTemplateApprovalLine::create([
|
|
'template_id' => $newTemplate->id,
|
|
'name' => $line->name,
|
|
'dept' => $line->dept,
|
|
'role' => $line->role,
|
|
'user_id' => $line->user_id,
|
|
'sort_order' => $line->sort_order,
|
|
]);
|
|
}
|
|
|
|
foreach ($source->basicFields as $field) {
|
|
DocumentTemplateBasicField::create([
|
|
'template_id' => $newTemplate->id,
|
|
'label' => $field->label,
|
|
'field_type' => $field->field_type,
|
|
'default_value' => $field->default_value,
|
|
'sort_order' => $field->sort_order,
|
|
]);
|
|
}
|
|
|
|
foreach ($source->sections as $section) {
|
|
$newSection = DocumentTemplateSection::create([
|
|
'template_id' => $newTemplate->id,
|
|
'title' => $section->title,
|
|
'image_path' => $section->image_path,
|
|
'sort_order' => $section->sort_order,
|
|
]);
|
|
|
|
foreach ($section->items as $item) {
|
|
DocumentTemplateSectionItem::create([
|
|
'section_id' => $newSection->id,
|
|
'category' => $item->category,
|
|
'item' => $item->item,
|
|
'standard' => $item->standard,
|
|
'tolerance' => $item->tolerance,
|
|
'standard_criteria' => $item->standard_criteria,
|
|
'method' => $item->method,
|
|
'measurement_type' => $item->measurement_type,
|
|
'frequency_n' => $item->frequency_n,
|
|
'frequency_c' => $item->frequency_c,
|
|
'frequency' => $item->frequency,
|
|
'regulation' => $item->regulation,
|
|
'sort_order' => $item->sort_order,
|
|
]);
|
|
}
|
|
}
|
|
|
|
foreach ($source->columns as $col) {
|
|
DocumentTemplateColumn::create([
|
|
'template_id' => $newTemplate->id,
|
|
'label' => $col->label,
|
|
'width' => $col->width,
|
|
'column_type' => $col->column_type,
|
|
'group_name' => $col->group_name,
|
|
'sub_labels' => $col->sub_labels,
|
|
'sort_order' => $col->sort_order,
|
|
]);
|
|
}
|
|
|
|
// 검사 기준서 동적 필드 복제
|
|
foreach ($source->sectionFields as $field) {
|
|
DocumentTemplateSectionField::create([
|
|
'template_id' => $newTemplate->id,
|
|
'field_key' => $field->field_key,
|
|
'label' => $field->label,
|
|
'field_type' => $field->field_type,
|
|
'options' => $field->options,
|
|
'width' => $field->width,
|
|
'is_required' => $field->is_required,
|
|
'sort_order' => $field->sort_order,
|
|
]);
|
|
}
|
|
|
|
// 외부 키 매핑 복제 (구조만 복사, 연결 값은 제외 - 중복 방지)
|
|
foreach ($source->links as $link) {
|
|
DocumentTemplateLink::create([
|
|
'template_id' => $newTemplate->id,
|
|
'link_key' => $link->link_key,
|
|
'label' => $link->label,
|
|
'link_type' => $link->link_type,
|
|
'source_table' => $link->source_table,
|
|
'search_params' => $link->search_params,
|
|
'display_fields' => $link->display_fields,
|
|
'is_required' => $link->is_required,
|
|
'sort_order' => $link->sort_order,
|
|
]);
|
|
// linkValues는 복사하지 않음 (동일 분류 내 연결품목 중복 방지)
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "'{$newName}' 양식이 복제되었습니다.",
|
|
'data' => $newTemplate->load(['approvalLines', 'basicFields', 'sections.items', 'columns']),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '복제 중 오류가 발생했습니다: '.$e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지 업로드 (API 파일 저장소 연동)
|
|
*/
|
|
public function uploadImage(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'image' => 'required|image|max:5120', // 5MB
|
|
]);
|
|
|
|
$apiBaseUrl = config('services.api.base_url');
|
|
$apiKey = config('services.api.key');
|
|
|
|
if (empty($apiBaseUrl)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'API 서버 URL이 설정되지 않았습니다.',
|
|
], 500);
|
|
}
|
|
|
|
// API 토큰 교환
|
|
$tokenService = new \App\Services\ApiTokenService;
|
|
$userId = auth()->id();
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$tokenResult = $tokenService->exchangeToken($userId, $tenantId);
|
|
if (! $tokenResult['success']) {
|
|
Log::error('[DocumentTemplate] API 토큰 교환 실패', [
|
|
'error' => $tokenResult['error'] ?? '',
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'API 인증 실패',
|
|
], 500);
|
|
}
|
|
|
|
// API 파일 업로드 호출
|
|
$image = $request->file('image');
|
|
|
|
try {
|
|
$response = Http::withToken($tokenResult['data']['access_token'])
|
|
->withHeaders(['X-API-KEY' => $apiKey])
|
|
->attach('file', file_get_contents($image->getRealPath()), $image->getClientOriginalName())
|
|
->post("{$apiBaseUrl}/api/v1/files/upload");
|
|
|
|
if ($response->successful() && $response->json('success')) {
|
|
$filePath = $response->json('data.file_path');
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'path' => $filePath,
|
|
'url' => "{$apiBaseUrl}/storage/tenants/{$filePath}",
|
|
]);
|
|
}
|
|
|
|
Log::error('[DocumentTemplate] API 파일 업로드 실패', [
|
|
'status' => $response->status(),
|
|
'body' => $response->json(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'API 파일 업로드 실패',
|
|
], 500);
|
|
} catch (\Exception $e) {
|
|
Log::error('[DocumentTemplate] API 파일 업로드 예외', [
|
|
'exception' => $e->getMessage(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '파일 업로드 중 오류가 발생했습니다.',
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공통코드 그룹 조회 (JSON)
|
|
*/
|
|
public function getCommonCodes(string $group): JsonResponse
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$codes = \App\Models\Products\CommonCode::query()
|
|
->where('code_group', $group)
|
|
->where('is_active', true)
|
|
->where(function ($q) use ($tenantId) {
|
|
$q->whereNull('tenant_id');
|
|
if ($tenantId) {
|
|
$q->orWhere('tenant_id', $tenantId);
|
|
}
|
|
})
|
|
->orderBy('sort_order')
|
|
->get(['code', 'name']);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $codes,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 관계 데이터 저장 (ID 보존 upsert 방식)
|
|
*
|
|
* sections/columns는 기존 ID를 보존하여 document_data 참조가 깨지지 않도록 함.
|
|
* 나머지(approval_lines, basic_fields 등)는 단순 관계라 삭제→재생성.
|
|
*/
|
|
private function saveRelations(DocumentTemplate $template, array $data, bool $deleteExisting = false): void
|
|
{
|
|
// 단순 관계: 삭제 → 재생성 (document_data가 참조하지 않는 테이블)
|
|
if ($deleteExisting) {
|
|
$template->approvalLines()->delete();
|
|
$template->basicFields()->delete();
|
|
$template->sectionFields()->delete();
|
|
$template->links()->delete();
|
|
}
|
|
|
|
// 결재라인
|
|
if (! empty($data['approval_lines'])) {
|
|
foreach ($data['approval_lines'] as $index => $line) {
|
|
DocumentTemplateApprovalLine::create([
|
|
'template_id' => $template->id,
|
|
'name' => $line['name'] ?? '',
|
|
'dept' => $line['dept'] ?? '',
|
|
'role' => $line['role'] ?? '',
|
|
'user_id' => $line['user_id'] ?? null,
|
|
'sort_order' => $index,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 기본 필드
|
|
if (! empty($data['basic_fields'])) {
|
|
foreach ($data['basic_fields'] as $index => $field) {
|
|
DocumentTemplateBasicField::create([
|
|
'template_id' => $template->id,
|
|
'label' => $field['label'] ?? '',
|
|
'field_key' => $field['field_key'] ?? null,
|
|
'field_type' => $field['field_type'] ?? 'text',
|
|
'default_value' => $field['default_value'] ?? '',
|
|
'sort_order' => $index,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 섹션 및 항목: ID 보존 upsert (document_data.section_id 참조 보호)
|
|
if (isset($data['sections'])) {
|
|
$incomingIds = collect($data['sections'])->pluck('id')->filter()->toArray();
|
|
// 요청에 없는 섹션만 삭제 (cascade로 items도 삭제)
|
|
if ($deleteExisting) {
|
|
$template->sections()->whereNotIn('id', $incomingIds)->each(function ($section) {
|
|
$section->items()->delete();
|
|
$section->delete();
|
|
});
|
|
}
|
|
|
|
foreach ($data['sections'] as $sIndex => $section) {
|
|
$sectionData = [
|
|
'title' => $section['title'] ?? '',
|
|
'image_path' => $section['image_path'] ?? null,
|
|
'sort_order' => $sIndex,
|
|
];
|
|
|
|
if (! empty($section['id']) && $template->sections()->where('id', $section['id'])->exists()) {
|
|
$existing = $template->sections()->where('id', $section['id'])->first();
|
|
$existing->update($sectionData);
|
|
$savedSection = $existing;
|
|
} else {
|
|
$savedSection = DocumentTemplateSection::create(array_merge(
|
|
['template_id' => $template->id],
|
|
$sectionData
|
|
));
|
|
}
|
|
|
|
// 섹션 항목 upsert
|
|
if (isset($section['items'])) {
|
|
$itemIncomingIds = collect($section['items'])->pluck('id')->filter()->toArray();
|
|
$savedSection->items()->whereNotIn('id', $itemIncomingIds)->delete();
|
|
|
|
foreach ($section['items'] as $iIndex => $item) {
|
|
$itemData = [
|
|
'category' => $item['category'] ?? '',
|
|
'item' => $item['item'] ?? '',
|
|
'standard' => $item['standard'] ?? '',
|
|
'tolerance' => $item['tolerance'] ?? null,
|
|
'standard_criteria' => $item['standard_criteria'] ?? null,
|
|
'method' => $item['method'] ?? '',
|
|
'measurement_type' => $item['measurement_type'] ?? null,
|
|
'frequency_n' => $item['frequency_n'] ?? null,
|
|
'frequency_c' => $item['frequency_c'] ?? null,
|
|
'frequency' => $item['frequency'] ?? '',
|
|
'regulation' => $item['regulation'] ?? '',
|
|
'field_values' => $item['field_values'] ?? null,
|
|
'sort_order' => $iIndex,
|
|
];
|
|
|
|
if (! empty($item['id']) && $savedSection->items()->where('id', $item['id'])->exists()) {
|
|
$savedSection->items()->where('id', $item['id'])->update($itemData);
|
|
} else {
|
|
DocumentTemplateSectionItem::create(array_merge(
|
|
['section_id' => $savedSection->id],
|
|
$itemData
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 컬럼: ID 보존 upsert (document_data.column_id 참조 보호)
|
|
if (isset($data['columns'])) {
|
|
$incomingIds = collect($data['columns'])->pluck('id')->filter()->toArray();
|
|
if ($deleteExisting) {
|
|
$template->columns()->whereNotIn('id', $incomingIds)->delete();
|
|
}
|
|
|
|
foreach ($data['columns'] as $index => $column) {
|
|
$colData = [
|
|
'label' => $column['label'] ?? '',
|
|
'width' => $column['width'] ?? '100px',
|
|
'column_type' => $column['column_type'] ?? 'text',
|
|
'group_name' => $column['group_name'] ?? null,
|
|
'sub_labels' => $column['sub_labels'] ?? null,
|
|
'sort_order' => $index,
|
|
];
|
|
|
|
if (! empty($column['id']) && $template->columns()->where('id', $column['id'])->exists()) {
|
|
$template->columns()->where('id', $column['id'])->update($colData);
|
|
} else {
|
|
DocumentTemplateColumn::create(array_merge(
|
|
['template_id' => $template->id],
|
|
$colData
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 검사 기준서 동적 필드 정의
|
|
if (! empty($data['section_fields'])) {
|
|
foreach ($data['section_fields'] as $index => $field) {
|
|
DocumentTemplateSectionField::create([
|
|
'template_id' => $template->id,
|
|
'field_key' => $field['field_key'] ?? '',
|
|
'label' => $field['label'] ?? '',
|
|
'field_type' => $field['field_type'] ?? 'text',
|
|
'options' => $field['options'] ?? null,
|
|
'width' => $field['width'] ?? '100px',
|
|
'is_required' => $field['is_required'] ?? false,
|
|
'sort_order' => $index,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// 외부 키 매핑 + 연결 값
|
|
if (! empty($data['template_links'])) {
|
|
foreach ($data['template_links'] as $index => $link) {
|
|
$newLink = DocumentTemplateLink::create([
|
|
'template_id' => $template->id,
|
|
'link_key' => $link['link_key'] ?? '',
|
|
'label' => $link['label'] ?? '',
|
|
'link_type' => $link['link_type'] ?? 'single',
|
|
'source_table' => $link['source_table'] ?? '',
|
|
'search_params' => $link['search_params'] ?? null,
|
|
'display_fields' => $link['display_fields'] ?? null,
|
|
'is_required' => $link['is_required'] ?? false,
|
|
'sort_order' => $index,
|
|
]);
|
|
|
|
// 연결 값 저장
|
|
if (! empty($link['values'])) {
|
|
foreach ($link['values'] as $vIndex => $value) {
|
|
DocumentTemplateLinkValue::create([
|
|
'template_id' => $template->id,
|
|
'link_id' => $newLink->id,
|
|
'linkable_id' => $value['linkable_id'] ?? $value['id'] ?? $value,
|
|
'sort_order' => $vIndex,
|
|
'created_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 요청 데이터에서 연결 품목 ID 추출 (레거시 + 신규 방식)
|
|
*/
|
|
private function extractLinkedItemIds(array $data): array
|
|
{
|
|
$itemIds = collect($data['linked_item_ids'] ?? []);
|
|
|
|
if (! empty($data['template_links'])) {
|
|
foreach ($data['template_links'] as $link) {
|
|
if (($link['source_table'] ?? '') === 'items' && ! empty($link['values'])) {
|
|
foreach ($link['values'] as $value) {
|
|
$id = $value['linkable_id'] ?? $value['id'] ?? (is_numeric($value) ? $value : null);
|
|
if ($id) {
|
|
$itemIds->push((int) $id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $itemIds->filter()->unique()->values()->all();
|
|
}
|
|
|
|
/**
|
|
* 동일 분류 내 연결품목 중복 검증 → 중복 시 JsonResponse 반환
|
|
*/
|
|
private function checkLinkedItemDuplicates(?string $category, array $newItemIds, ?int $excludeTemplateId = null): ?JsonResponse
|
|
{
|
|
if (empty($newItemIds) || empty($category)) {
|
|
return null;
|
|
}
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = DocumentTemplate::where('tenant_id', $tenantId)
|
|
->where('category', $category);
|
|
|
|
if ($excludeTemplateId) {
|
|
$query->where('id', '!=', $excludeTemplateId);
|
|
}
|
|
|
|
$otherTemplates = $query->get(['id', 'name', 'linked_item_ids']);
|
|
|
|
$duplicates = [];
|
|
|
|
foreach ($otherTemplates as $template) {
|
|
// 레거시 linked_item_ids
|
|
$otherItemIds = collect($template->linked_item_ids ?? []);
|
|
|
|
// 신규 link_values (source_table = 'items')
|
|
$linkValueItemIds = DocumentTemplateLinkValue::where('template_id', $template->id)
|
|
->whereIn('linkable_id', $newItemIds)
|
|
->whereHas('link', fn ($q) => $q->where('source_table', 'items'))
|
|
->pluck('linkable_id');
|
|
|
|
$otherItemIds = $otherItemIds->merge($linkValueItemIds)->unique();
|
|
$overlap = $otherItemIds->intersect($newItemIds);
|
|
|
|
if ($overlap->isNotEmpty()) {
|
|
$duplicates[] = [
|
|
'template_name' => $template->name,
|
|
'item_ids' => $overlap->values()->toArray(),
|
|
];
|
|
}
|
|
}
|
|
|
|
if (empty($duplicates)) {
|
|
return null;
|
|
}
|
|
|
|
// 품목명 조회
|
|
$allDupItemIds = collect($duplicates)->pluck('item_ids')->flatten()->unique();
|
|
$itemNames = \App\Models\Items\Item::whereIn('id', $allDupItemIds)
|
|
->pluck('name', 'id')
|
|
->toArray();
|
|
|
|
$messages = [];
|
|
foreach ($duplicates as $dup) {
|
|
$names = collect($dup['item_ids'])
|
|
->map(fn ($id) => $itemNames[$id] ?? "ID:{$id}")
|
|
->join(', ');
|
|
$messages[] = "'{$dup['template_name']}' 양식에 이미 연결된 품목: {$names}";
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '동일 분류 내 연결품목이 중복됩니다.',
|
|
'errors' => ['linked_items' => $messages],
|
|
], 422);
|
|
}
|
|
}
|