2026-01-28 21:51:23 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Models\Documents\Document;
|
2026-02-11 22:18:17 +09:00
|
|
|
use App\Models\Documents\DocumentData;
|
2026-01-28 21:51:23 +09:00
|
|
|
use App\Models\DocumentTemplate;
|
2026-02-05 09:26:13 +09:00
|
|
|
use App\Models\Items\Item;
|
2026-01-28 21:51:23 +09:00
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Http\Response;
|
2026-02-05 09:26:13 +09:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-01-28 21:51:23 +09:00
|
|
|
use Illuminate\View\View;
|
|
|
|
|
|
|
|
|
|
class DocumentController extends Controller
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 문서 목록 페이지
|
|
|
|
|
*/
|
|
|
|
|
public function index(Request $request): View|Response
|
|
|
|
|
{
|
|
|
|
|
if ($request->header('HX-Request')) {
|
|
|
|
|
return response('', 200)->header('HX-Redirect', route('documents.index'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
// 템플릿 목록 (필터용)
|
|
|
|
|
$templates = $tenantId
|
|
|
|
|
? DocumentTemplate::where(function ($q) use ($tenantId) {
|
|
|
|
|
$q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
|
|
|
|
})->where('is_active', true)->orderBy('name')->get()
|
|
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
return view('documents.index', [
|
|
|
|
|
'templates' => $templates,
|
|
|
|
|
'statuses' => Document::STATUS_LABELS,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 문서 생성 페이지
|
|
|
|
|
*/
|
2026-02-01 20:37:01 +09:00
|
|
|
public function create(Request $request): View|Response
|
2026-01-28 21:51:23 +09:00
|
|
|
{
|
2026-02-01 20:37:01 +09:00
|
|
|
if ($request->header('HX-Request')) {
|
|
|
|
|
return response('', 200)->header('HX-Redirect', route('documents.create', $request->query()));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:51:23 +09:00
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
$templateId = $request->query('template_id');
|
|
|
|
|
|
|
|
|
|
// 템플릿 목록
|
|
|
|
|
$templates = $tenantId
|
|
|
|
|
? DocumentTemplate::where(function ($q) use ($tenantId) {
|
|
|
|
|
$q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
|
|
|
|
})->where('is_active', true)->orderBy('name')->get()
|
|
|
|
|
: collect();
|
|
|
|
|
|
|
|
|
|
// 선택된 템플릿
|
|
|
|
|
$template = $templateId
|
2026-02-04 08:38:00 +09:00
|
|
|
? DocumentTemplate::with(['approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', 'links.linkValues'])->find($templateId)
|
2026-01-28 21:51:23 +09:00
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
return view('documents.edit', [
|
|
|
|
|
'document' => null,
|
|
|
|
|
'template' => $template,
|
|
|
|
|
'templates' => $templates,
|
|
|
|
|
'isCreate' => true,
|
2026-02-05 09:26:13 +09:00
|
|
|
'linkedItemSpecs' => $template ? $this->getLinkedItemSpecs($template) : [],
|
2026-01-28 21:51:23 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 문서 수정 페이지
|
|
|
|
|
*/
|
2026-02-01 20:37:01 +09:00
|
|
|
public function edit(int $id): View|Response
|
2026-01-28 21:51:23 +09:00
|
|
|
{
|
2026-02-01 20:37:01 +09:00
|
|
|
if (request()->header('HX-Request')) {
|
|
|
|
|
return response('', 200)->header('HX-Redirect', route('documents.edit', $id));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:51:23 +09:00
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
$document = Document::with([
|
|
|
|
|
'template.approvalLines',
|
|
|
|
|
'template.basicFields',
|
|
|
|
|
'template.sections.items',
|
|
|
|
|
'template.columns',
|
2026-02-04 08:38:00 +09:00
|
|
|
'template.sectionFields',
|
|
|
|
|
'template.links.linkValues',
|
2026-01-28 21:51:23 +09:00
|
|
|
'approvals.user',
|
|
|
|
|
'data',
|
|
|
|
|
'attachments.file',
|
|
|
|
|
'creator',
|
|
|
|
|
])->where('tenant_id', $tenantId)->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
// 템플릿 목록 (변경용)
|
|
|
|
|
$templates = $tenantId
|
|
|
|
|
? DocumentTemplate::where(function ($q) use ($tenantId) {
|
|
|
|
|
$q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
|
|
|
|
})->where('is_active', true)->orderBy('name')->get()
|
|
|
|
|
: collect();
|
|
|
|
|
|
2026-02-11 22:18:17 +09:00
|
|
|
// 기본정보 bf_ 자동 backfill (show 안 거치고 바로 edit 진입 대비)
|
|
|
|
|
$this->resolveAndBackfillBasicFields($document);
|
|
|
|
|
|
2026-01-28 21:51:23 +09:00
|
|
|
return view('documents.edit', [
|
|
|
|
|
'document' => $document,
|
|
|
|
|
'template' => $document->template,
|
|
|
|
|
'templates' => $templates,
|
|
|
|
|
'isCreate' => false,
|
2026-02-05 09:26:13 +09:00
|
|
|
'linkedItemSpecs' => $this->getLinkedItemSpecs($document->template),
|
2026-01-28 21:51:23 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 20:37:01 +09:00
|
|
|
/**
|
|
|
|
|
* 문서 인쇄용 화면 (성적서 양식)
|
|
|
|
|
*/
|
|
|
|
|
public function print(int $id): View
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
$document = Document::with([
|
|
|
|
|
'template.approvalLines',
|
|
|
|
|
'template.basicFields',
|
|
|
|
|
'template.sections.items',
|
|
|
|
|
'template.columns',
|
2026-02-04 08:38:00 +09:00
|
|
|
'template.sectionFields',
|
|
|
|
|
'template.links.linkValues',
|
2026-02-01 20:37:01 +09:00
|
|
|
'approvals.user',
|
|
|
|
|
'data',
|
|
|
|
|
'creator',
|
|
|
|
|
])->where('tenant_id', $tenantId)->findOrFail($id);
|
|
|
|
|
|
2026-02-11 22:18:17 +09:00
|
|
|
// 연결된 작업지시서의 work_order_items 로드
|
|
|
|
|
$workOrderItems = collect();
|
|
|
|
|
if ($document->linkable_type === 'work_order' && $document->linkable_id) {
|
|
|
|
|
$workOrderItems = DB::table('work_order_items')
|
|
|
|
|
->where('work_order_id', $document->linkable_id)
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->get()
|
|
|
|
|
->map(function ($item) {
|
|
|
|
|
$item->options = json_decode($item->options, true) ?? [];
|
|
|
|
|
|
|
|
|
|
return $item;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본정보 bf_ 자동 backfill
|
|
|
|
|
$this->resolveAndBackfillBasicFields($document);
|
|
|
|
|
|
2026-02-01 20:37:01 +09:00
|
|
|
return view('documents.print', [
|
|
|
|
|
'document' => $document,
|
2026-02-11 22:18:17 +09:00
|
|
|
'workOrderItems' => $workOrderItems,
|
2026-02-01 20:37:01 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:51:23 +09:00
|
|
|
/**
|
|
|
|
|
* 문서 상세 페이지 (읽기 전용)
|
|
|
|
|
*/
|
|
|
|
|
public function show(int $id): View
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
|
|
|
|
|
|
$document = Document::with([
|
|
|
|
|
'template.approvalLines',
|
|
|
|
|
'template.basicFields',
|
|
|
|
|
'template.sections.items',
|
|
|
|
|
'template.columns',
|
2026-02-04 08:38:00 +09:00
|
|
|
'template.sectionFields',
|
|
|
|
|
'template.links.linkValues',
|
2026-01-28 21:51:23 +09:00
|
|
|
'approvals.user',
|
|
|
|
|
'data',
|
|
|
|
|
'attachments.file',
|
|
|
|
|
'creator',
|
|
|
|
|
'updater',
|
|
|
|
|
])->where('tenant_id', $tenantId)->findOrFail($id);
|
|
|
|
|
|
2026-02-11 22:18:17 +09:00
|
|
|
// 연결된 작업지시서의 work_order_items 로드
|
|
|
|
|
$workOrderItems = collect();
|
|
|
|
|
if ($document->linkable_type === 'work_order' && $document->linkable_id) {
|
|
|
|
|
$workOrderItems = DB::table('work_order_items')
|
|
|
|
|
->where('work_order_id', $document->linkable_id)
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->get()
|
|
|
|
|
->map(function ($item) {
|
|
|
|
|
$item->options = json_decode($item->options, true) ?? [];
|
|
|
|
|
|
|
|
|
|
return $item;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본정보 bf_ 자동 backfill
|
|
|
|
|
$this->resolveAndBackfillBasicFields($document);
|
|
|
|
|
|
2026-01-28 21:51:23 +09:00
|
|
|
return view('documents.show', [
|
|
|
|
|
'document' => $document,
|
2026-02-11 22:18:17 +09:00
|
|
|
'workOrderItems' => $workOrderItems,
|
2026-01-28 21:51:23 +09:00
|
|
|
]);
|
|
|
|
|
}
|
2026-02-05 09:26:13 +09:00
|
|
|
|
2026-02-11 22:18:17 +09:00
|
|
|
/**
|
|
|
|
|
* 기본정보(bf_) 레코드가 없으면 원본 데이터에서 resolve → document_data에 저장
|
|
|
|
|
* React resolveFieldValue와 동일 매핑 로직
|
|
|
|
|
*/
|
|
|
|
|
private function resolveAndBackfillBasicFields(Document $document): void
|
|
|
|
|
{
|
|
|
|
|
$basicFields = $document->template?->basicFields;
|
|
|
|
|
if (! $basicFields || $basicFields->isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bf_ 레코드가 하나라도 있으면 이미 저장된 것 → skip
|
|
|
|
|
$existingBfCount = $document->data
|
|
|
|
|
->filter(fn ($d) => str_starts_with($d->field_key, 'bf_'))
|
|
|
|
|
->count();
|
|
|
|
|
if ($existingBfCount > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 원본 데이터 로드: work_order + order
|
|
|
|
|
if ($document->linkable_type !== 'work_order' || ! $document->linkable_id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$workOrder = DB::table('work_orders')->find($document->linkable_id);
|
|
|
|
|
if (! $workOrder) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$workOrderItems = DB::table('work_order_items')
|
|
|
|
|
->where('work_order_id', $workOrder->id)
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->get()
|
|
|
|
|
->map(function ($item) {
|
|
|
|
|
$item->options = json_decode($item->options, true) ?? [];
|
|
|
|
|
|
|
|
|
|
return $item;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$order = $workOrder->sales_order_id
|
|
|
|
|
? DB::table('orders')->find($workOrder->sales_order_id)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
// 검사자 정보: work_order_items[0].options.inspection_data.inspected_by
|
|
|
|
|
$firstItem = $workOrderItems->first();
|
|
|
|
|
$inspectionData = $firstItem?->options['inspection_data'] ?? [];
|
|
|
|
|
$inspectedBy = $inspectionData['inspected_by'] ?? null;
|
|
|
|
|
$inspectedAt = $inspectionData['inspected_at'] ?? null;
|
|
|
|
|
$inspectorName = $inspectedBy
|
|
|
|
|
? DB::table('users')->where('id', $inspectedBy)->value('name')
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
// field_key → 값 매핑
|
|
|
|
|
$resolveMap = [
|
|
|
|
|
'product_name' => $firstItem?->item_name ?? '',
|
|
|
|
|
'specification' => $firstItem?->specification ?? '',
|
|
|
|
|
'lot_no' => $order?->order_no ?? '',
|
|
|
|
|
'lot_size' => $workOrderItems->count().' 개소',
|
|
|
|
|
'client' => $order?->client_name ?? '',
|
|
|
|
|
'site_name' => $workOrder->project_name ?? '',
|
|
|
|
|
'inspection_date' => $inspectedAt ? substr($inspectedAt, 0, 10) : now()->format('Y-m-d'),
|
|
|
|
|
'inspector' => $inspectorName ?? '',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// document_data에 bf_ 레코드 저장
|
|
|
|
|
$records = [];
|
|
|
|
|
foreach ($basicFields as $field) {
|
|
|
|
|
$value = $resolveMap[$field->field_key] ?? $field->default_value ?? '';
|
|
|
|
|
if ($value === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$records[] = [
|
|
|
|
|
'document_id' => $document->id,
|
|
|
|
|
'section_id' => null,
|
|
|
|
|
'column_id' => null,
|
|
|
|
|
'row_index' => 0,
|
|
|
|
|
'field_key' => 'bf_'.$field->id,
|
|
|
|
|
'field_value' => (string) $value,
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! empty($records)) {
|
|
|
|
|
DocumentData::insert($records);
|
|
|
|
|
// 메모리의 data relation도 갱신
|
|
|
|
|
$document->load('data');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 09:26:13 +09:00
|
|
|
/**
|
|
|
|
|
* 템플릿에 연결된 품목들의 규격 정보 (thickness, width, length) 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getLinkedItemSpecs(DocumentTemplate $template): array
|
|
|
|
|
{
|
|
|
|
|
$specs = [];
|
|
|
|
|
|
|
|
|
|
if (! $template->relationLoaded('links')) {
|
|
|
|
|
$template->load('links.linkValues');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($template->links as $link) {
|
|
|
|
|
if ($link->source_table !== 'items') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($link->linkValues as $lv) {
|
|
|
|
|
$item = Item::find($lv->linkable_id);
|
|
|
|
|
if (! $item) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$attrs = $item->attributes ?? [];
|
|
|
|
|
if (isset($attrs['thickness']) || isset($attrs['width']) || isset($attrs['length'])) {
|
|
|
|
|
$specs[] = [
|
|
|
|
|
'id' => $item->id,
|
|
|
|
|
'name' => $item->name,
|
|
|
|
|
'thickness' => $attrs['thickness'] ?? null,
|
|
|
|
|
'width' => $attrs['width'] ?? null,
|
|
|
|
|
'length' => $attrs['length'] ?? null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $specs;
|
|
|
|
|
}
|
2026-01-28 21:51:23 +09:00
|
|
|
}
|