tenantId(); $query = Document::query() ->where('tenant_id', $tenantId) ->with([ 'template:id,name,category', 'creator:id,name', ]); // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 템플릿 필터 if (! empty($params['template_id'])) { $query->where('template_id', $params['template_id']); } // 검색 (문서번호, 제목) if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('document_no', 'like', "%{$search}%") ->orWhere('title', 'like', "%{$search}%"); }); } // 날짜 범위 필터 if (! empty($params['from_date'])) { $query->whereDate('created_at', '>=', $params['from_date']); } if (! empty($params['to_date'])) { $query->whereDate('created_at', '<=', $params['to_date']); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 문서 상세 조회 */ public function show(int $id): Document { $tenantId = $this->tenantId(); return Document::query() ->where('tenant_id', $tenantId) ->with([ 'template:id,name,category,title', 'template.approvalLines', 'template.basicFields', 'template.sections.items', 'template.columns', 'approvals.user:id,name', 'data', 'attachments.file', 'creator:id,name', 'updater:id,name', ]) ->findOrFail($id); } // ========================================================================= // 문서 생성/수정/삭제 // ========================================================================= /** * 문서 생성 */ public function create(array $data): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 문서번호 생성 $documentNo = $this->generateDocumentNo($tenantId, $data['template_id']); // 문서 생성 $document = Document::create([ 'tenant_id' => $tenantId, 'template_id' => $data['template_id'], 'document_no' => $documentNo, 'title' => $data['title'], 'status' => Document::STATUS_DRAFT, 'linkable_type' => $data['linkable_type'] ?? null, 'linkable_id' => $data['linkable_id'] ?? null, 'created_by' => $userId, 'updated_by' => $userId, ]); // 결재선 생성 if (! empty($data['approvers'])) { $this->createApprovals($document, $data['approvers'], $userId); } // 문서 데이터 저장 if (! empty($data['data'])) { $this->saveDocumentData($document, $data['data']); } // 첨부파일 연결 if (! empty($data['attachments'])) { $this->attachFiles($document, $data['attachments'], $userId); } return $document->fresh([ 'template:id,name,category', 'approvals.user:id,name', 'data', 'attachments.file', 'creator:id,name', ]); }); } /** * 문서 수정 */ public function update(int $id, array $data): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 수정 가능 상태 확인 if (! $document->canEdit()) { throw new BadRequestHttpException(__('error.document.not_editable')); } // 기본 정보 수정 $document->fill([ 'title' => $data['title'] ?? $document->title, 'linkable_type' => $data['linkable_type'] ?? $document->linkable_type, 'linkable_id' => $data['linkable_id'] ?? $document->linkable_id, 'updated_by' => $userId, ]); // 반려 상태에서 수정 시 DRAFT로 변경 if ($document->status === Document::STATUS_REJECTED) { $document->status = Document::STATUS_DRAFT; $document->submitted_at = null; $document->completed_at = null; } $document->save(); // 결재선 수정 if (isset($data['approvers'])) { $document->approvals()->delete(); if (! empty($data['approvers'])) { $this->createApprovals($document, $data['approvers'], $userId); } } // 문서 데이터 수정 if (isset($data['data'])) { $document->data()->delete(); if (! empty($data['data'])) { $this->saveDocumentData($document, $data['data']); } } // 첨부파일 수정 if (isset($data['attachments'])) { $document->attachments()->delete(); if (! empty($data['attachments'])) { $this->attachFiles($document, $data['attachments'], $userId); } } return $document->fresh([ 'template:id,name,category', 'approvals.user:id,name', 'data', 'attachments.file', 'creator:id,name', ]); }); } /** * 문서 삭제 */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // DRAFT 상태만 삭제 가능 if ($document->status !== Document::STATUS_DRAFT) { throw new BadRequestHttpException(__('error.document.not_deletable')); } $document->deleted_by = $userId; $document->save(); $document->delete(); return true; }); } // ========================================================================= // 결재 처리 // ========================================================================= /** * 결재 요청 (DRAFT → PENDING) */ public function submit(int $id): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 결재 요청 가능 상태 확인 if (! $document->canSubmit()) { throw new BadRequestHttpException(__('error.document.not_submittable')); } // 결재선 존재 확인 $approvalCount = $document->approvals()->count(); if ($approvalCount === 0) { throw new BadRequestHttpException(__('error.document.approvers_required')); } $document->status = Document::STATUS_PENDING; $document->submitted_at = now(); $document->updated_by = $userId; $document->save(); return $document->fresh([ 'template:id,name,category', 'approvals.user:id,name', 'creator:id,name', ]); }); } /** * 결재 승인 */ public function approve(int $id, ?string $comment = null): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $comment, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 결재 가능 상태 확인 if (! $document->canApprove()) { throw new BadRequestHttpException(__('error.document.not_approvable')); } // 현재 사용자의 대기 중인 결재 단계 찾기 $myApproval = $document->approvals() ->where('user_id', $userId) ->where('status', DocumentApproval::STATUS_PENDING) ->orderBy('step') ->first(); if (! $myApproval) { throw new BadRequestHttpException(__('error.document.not_your_turn')); } // 순차 결재 확인 (이전 단계가 완료되었는지) $pendingBefore = $document->approvals() ->where('step', '<', $myApproval->step) ->where('status', DocumentApproval::STATUS_PENDING) ->exists(); if ($pendingBefore) { throw new BadRequestHttpException(__('error.document.not_your_turn')); } // 승인 처리 $myApproval->status = DocumentApproval::STATUS_APPROVED; $myApproval->comment = $comment; $myApproval->acted_at = now(); $myApproval->updated_by = $userId; $myApproval->save(); // 모든 결재 완료 확인 $allApproved = ! $document->approvals() ->where('status', DocumentApproval::STATUS_PENDING) ->exists(); if ($allApproved) { $document->status = Document::STATUS_APPROVED; $document->completed_at = now(); } $document->updated_by = $userId; $document->save(); return $document->fresh([ 'template:id,name,category', 'approvals.user:id,name', 'creator:id,name', ]); }); } /** * 결재 반려 */ public function reject(int $id, string $comment): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $comment, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 결재 가능 상태 확인 if (! $document->canApprove()) { throw new BadRequestHttpException(__('error.document.not_approvable')); } // 현재 사용자의 대기 중인 결재 단계 찾기 $myApproval = $document->approvals() ->where('user_id', $userId) ->where('status', DocumentApproval::STATUS_PENDING) ->orderBy('step') ->first(); if (! $myApproval) { throw new BadRequestHttpException(__('error.document.not_your_turn')); } // 순차 결재 확인 $pendingBefore = $document->approvals() ->where('step', '<', $myApproval->step) ->where('status', DocumentApproval::STATUS_PENDING) ->exists(); if ($pendingBefore) { throw new BadRequestHttpException(__('error.document.not_your_turn')); } // 반려 처리 $myApproval->status = DocumentApproval::STATUS_REJECTED; $myApproval->comment = $comment; $myApproval->acted_at = now(); $myApproval->updated_by = $userId; $myApproval->save(); // 문서 반려 상태로 변경 $document->status = Document::STATUS_REJECTED; $document->completed_at = now(); $document->updated_by = $userId; $document->save(); return $document->fresh([ 'template:id,name,category', 'approvals.user:id,name', 'creator:id,name', ]); }); } /** * 결재 취소/회수 (작성자만) */ public function cancel(int $id): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $document = Document::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 취소 가능 상태 확인 if (! $document->canCancel()) { throw new BadRequestHttpException(__('error.document.not_cancellable')); } // 작성자만 취소 가능 if ($document->created_by !== $userId) { throw new BadRequestHttpException(__('error.document.only_creator_can_cancel')); } $document->status = Document::STATUS_CANCELLED; $document->completed_at = now(); $document->updated_by = $userId; $document->save(); return $document->fresh([ 'template:id,name,category', 'creator:id,name', ]); }); } // ========================================================================= // FQC 일괄생성 (제품검사) // ========================================================================= /** * 수주 개소별 제품검사 문서 일괄생성 * * Order의 OrderItem 수만큼 Document를 DRAFT 상태로 생성. * 기본필드(납품명, 제품명, 발주처, LOT NO, 로트크기) 자동매핑. */ public function bulkCreateFqc(array $data): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $templateId = $data['template_id']; $orderId = $data['order_id']; // 템플릿 존재 확인 $template = DocumentTemplate::where('tenant_id', $tenantId) ->where('is_active', true) ->findOrFail($templateId); // 수주 + 개소 조회 $order = \App\Models\Orders\Order::where('tenant_id', $tenantId) ->with('items') ->findOrFail($orderId); if ($order->items->isEmpty()) { throw new BadRequestHttpException(__('error.document.no_order_items')); } // 이미 생성된 문서 확인 (중복 방지) $existingLinkableIds = Document::where('tenant_id', $tenantId) ->where('template_id', $templateId) ->where('linkable_type', \App\Models\Orders\OrderItem::class) ->whereIn('linkable_id', $order->items->pluck('id')) ->pluck('linkable_id') ->toArray(); $itemsToCreate = $order->items->reject(function ($item) use ($existingLinkableIds) { return in_array($item->id, $existingLinkableIds); }); if ($itemsToCreate->isEmpty()) { throw new BadRequestHttpException(__('error.document.already_created')); } // 템플릿의 기본필드 로드 (bf_{id} 형식으로 저장하기 위해) $template = DocumentTemplate::with('basicFields')->find($templateId); return DB::transaction(function () use ($itemsToCreate, $order, $templateId, $tenantId, $userId, $template) { $documents = []; foreach ($itemsToCreate as $orderItem) { // 문서번호 생성 $documentNo = $this->generateDocumentNo($tenantId, $templateId); // 개소 식별 문자열 $locationLabel = trim("{$orderItem->floor_code}-{$orderItem->symbol_code}"); $specLabel = $orderItem->specification ?? ''; $titleSuffix = $specLabel ? "{$locationLabel} ({$specLabel})" : $locationLabel; // Document 생성 $document = Document::create([ 'tenant_id' => $tenantId, 'template_id' => $templateId, 'document_no' => $documentNo, 'title' => "제품검사 - {$titleSuffix}", 'status' => Document::STATUS_DRAFT, 'linkable_type' => \App\Models\Orders\OrderItem::class, 'linkable_id' => $orderItem->id, 'created_by' => $userId, 'updated_by' => $userId, ]); // 기본필드 자동매핑 (bf_{id} 형식, mng show.blade.php 호환) $resolveMap = [ 'product_name' => $orderItem->item_name, 'client' => $order->client_name, 'lot_no' => $order->order_no, 'lot_size' => '1 EA', 'site_name' => $order->site_name ?? '', ]; if ($template && $template->basicFields) { foreach ($template->basicFields as $field) { $value = $resolveMap[$field->field_key] ?? ''; // field_key가 없는 필드는 라벨 매칭 if (! $value && ! $field->field_key) { if (str_contains($field->label, '납품')) { $value = $order->site_name ?? $order->order_no; } } if ($value) { DocumentData::create([ 'document_id' => $document->id, 'section_id' => null, 'column_id' => null, 'row_index' => 0, 'field_key' => "bf_{$field->id}", 'field_value' => (string) $value, ]); } } } $documents[] = $document; } return [ 'created_count' => count($documents), 'skipped_count' => count($existingLinkableIds ?? []), 'total_items' => count($documents) + count($existingLinkableIds ?? []), 'documents' => collect($documents)->map(fn ($doc) => [ 'id' => $doc->id, 'document_no' => $doc->document_no, 'title' => $doc->title, 'status' => $doc->status, 'linkable_id' => $doc->linkable_id, ])->toArray(), ]; }); } /** * 수주 개소별 FQC 진행현황 조회 */ public function fqcStatus(int $orderId, int $templateId): array { $tenantId = $this->tenantId(); $order = \App\Models\Orders\Order::where('tenant_id', $tenantId) ->with('items') ->findOrFail($orderId); // 해당 수주의 FQC 문서 조회 $documents = Document::where('tenant_id', $tenantId) ->where('template_id', $templateId) ->where('linkable_type', \App\Models\Orders\OrderItem::class) ->whereIn('linkable_id', $order->items->pluck('id')) ->with('data') ->get() ->keyBy('linkable_id'); $items = $order->items->map(function ($orderItem) use ($documents) { $doc = $documents->get($orderItem->id); // 종합판정 값 추출 $judgement = null; if ($doc) { $judgementData = $doc->data->firstWhere('field_key', 'footer_judgement'); $judgement = $judgementData?->field_value; } return [ 'order_item_id' => $orderItem->id, 'floor_code' => $orderItem->floor_code, 'symbol_code' => $orderItem->symbol_code, 'specification' => $orderItem->specification, 'item_name' => $orderItem->item_name, 'document_id' => $doc?->id, 'document_no' => $doc?->document_no, 'status' => $doc?->status ?? 'NONE', 'judgement' => $judgement, ]; }); // 통계 $total = $items->count(); $created = $items->where('status', '!=', 'NONE')->count(); $approved = $items->where('status', 'APPROVED')->count(); $passed = $items->where('judgement', '합격')->count(); $failed = $items->where('judgement', '불합격')->count(); return [ 'order_id' => $orderId, 'order_no' => $order->order_no, 'total' => $total, 'created' => $created, 'approved' => $approved, 'passed' => $passed, 'failed' => $failed, 'pending' => $total - $created, 'items' => $items->toArray(), ]; } // ========================================================================= // Resolve/Upsert (React 연동용) // ========================================================================= /** * 문서 Resolve (category + item_id로 템플릿 + 기존 문서 조회) * * React에서 문서 작성 시: * 1. category + item_id로 해당 품목이 연결된 템플릿 조회 * 2. 기존 문서가 있으면 그 문서를, 없으면 빈 폼 + is_new=true 반환 */ public function resolve(array $params): array { $tenantId = $this->tenantId(); $category = $params['category']; $itemId = $params['item_id']; // 1. common_codes에서 category 유효성 확인 (tenant 우선, 없으면 global) $validCategory = DB::table('common_codes') ->where('code_group', 'document_category') ->where('code', $category) ->where('is_active', true) ->where(function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId) ->orWhereNull('tenant_id'); }) ->orderByRaw('tenant_id IS NULL') // tenant 우선 ->first(); if (! $validCategory) { throw new BadRequestHttpException(__('error.document.invalid_category')); } // 2. category에 매칭되는 템플릿 + 해당 item_id가 연결된 것 조회 // category 필드 값은 기존 데이터가 "수입검사", "품질검사" 등 한글 또는 // common_code의 code와 매핑되어야 함 // 우선 code_name 매핑: incoming_inspection → 수입검사 $categoryMapping = [ 'incoming_inspection' => '수입검사', 'quality_inspection' => '품질검사', 'outgoing_inspection' => '출하검사', ]; $categoryName = $categoryMapping[$category] ?? $category; $baseQuery = DocumentTemplate::query() ->where('tenant_id', $tenantId) ->where('is_active', true) ->where(function ($q) use ($itemId) { $q->whereJsonContains('linked_item_ids', (int) $itemId) ->orWhereJsonContains('linked_item_ids', (string) $itemId); }) ->with([ 'approvalLines', 'basicFields', 'sections.items', 'columns', 'sectionFields', ]); // 1차: category 매칭 + item_id $template = (clone $baseQuery) ->where(function ($q) use ($category, $categoryName) { $q->where('category', $category) ->orWhere('category', $categoryName) ->orWhere('category', 'LIKE', "%{$categoryName}%"); }) ->first(); // 2차: category 무관, item_id 연결만으로 fallback if (! $template) { $template = $baseQuery->first(); } if (! $template) { throw new NotFoundHttpException(__('error.document.template_not_found')); } // 3. 기존 문서 조회 (template + item_id, 수정 가능한 상태만) $existingDocument = Document::query() ->where('tenant_id', $tenantId) ->where('template_id', $template->id) ->where('linkable_type', 'item') ->where('linkable_id', $itemId) ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) ->with(['data', 'attachments.file', 'approvals.user:id,name']) ->first(); // 4. 품목 정보 조회 (auto-highlight용 속성 포함) $item = DB::table('items') ->where('id', $itemId) ->where('tenant_id', $tenantId) ->select('id', 'code', 'name', 'attributes') ->first(); if (! $item) { throw new NotFoundHttpException(__('error.item.not_found')); } // 5. 응답 구성 return [ 'is_new' => $existingDocument === null, 'template' => $this->formatTemplateForReact($template), 'document' => $existingDocument ? $this->formatDocumentForReact($existingDocument) : null, 'item' => [ 'id' => $item->id, 'code' => $item->code, 'name' => $item->name, 'attributes' => $item->attributes ? json_decode($item->attributes, true) : null, ], ]; } /** * 문서 Upsert (INSERT if new, UPDATE if exists) * * React에서 문서 저장 시: * - 기존 문서가 있으면 update * - 없으면 create */ public function upsert(array $data): Document { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId) { $templateId = $data['template_id']; $itemId = $data['item_id']; // 기존 문서 조회 (수정 가능한 상태만) $existingDocument = Document::query() ->where('tenant_id', $tenantId) ->where('template_id', $templateId) ->where('linkable_type', 'item') ->where('linkable_id', $itemId) ->whereIn('status', [Document::STATUS_DRAFT, Document::STATUS_REJECTED]) ->first(); if ($existingDocument) { // UPDATE: 기존 update 로직 재사용 return $this->update($existingDocument->id, [ 'title' => $data['title'] ?? $existingDocument->title, 'linkable_type' => 'item', 'linkable_id' => $itemId, 'data' => $data['data'] ?? [], 'attachments' => $data['attachments'] ?? [], ]); } // CREATE: 기존 create 로직 재사용 return $this->create([ 'template_id' => $templateId, 'title' => $data['title'] ?? '', 'linkable_type' => 'item', 'linkable_id' => $itemId, 'data' => $data['data'] ?? [], 'attachments' => $data['attachments'] ?? [], ]); }); } /** * 템플릿을 React 응답용으로 포맷 */ public function formatTemplateForReact(DocumentTemplate $template): array { // common_codes에서 inspection_method 코드 목록 조회 (code → name 매핑) $tenantId = $this->tenantId(); $methodCodes = DB::table('common_codes') ->where('code_group', 'inspection_method') ->where('is_active', true) ->where(function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId) ->orWhereNull('tenant_id'); }) ->orderByRaw('tenant_id IS NULL') // tenant 우선 ->pluck('name', 'code') ->toArray(); return [ 'id' => $template->id, 'name' => $template->name, 'category' => $template->category, 'title' => $template->title, 'company_name' => $template->company_name, 'company_address' => $template->company_address, 'company_contact' => $template->company_contact, 'footer_remark_label' => $template->footer_remark_label, 'footer_judgement_label' => $template->footer_judgement_label, 'footer_judgement_options' => $template->footer_judgement_options, 'approval_lines' => $template->approvalLines->map(fn ($line) => [ 'id' => $line->id, 'name' => $line->name, 'dept' => $line->dept, 'role' => $line->role, 'user_id' => $line->user_id, 'sort_order' => $line->sort_order, ])->toArray(), 'basic_fields' => $template->basicFields->map(fn ($field) => [ 'id' => $field->id, 'field_key' => $field->field_key, 'label' => $field->label, 'field_type' => $field->field_type, 'default_value' => $field->default_value, 'sort_order' => $field->sort_order, ])->toArray(), 'section_fields' => $template->sectionFields->map(fn ($field) => [ 'id' => $field->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, ])->toArray(), 'sections' => $template->sections->map(fn ($section) => [ 'id' => $section->id, 'name' => $section->title, 'title' => $section->title, 'image_path' => $section->image_path, 'sort_order' => $section->sort_order, 'items' => $section->items->map(function ($item) use ($methodCodes) { // method 코드를 한글 이름으로 변환 $methodName = $item->method ? ($methodCodes[$item->method] ?? $item->method) : null; return [ 'id' => $item->id, 'field_values' => $item->field_values ?? [], // 레거시 필드도 포함 (하위 호환) 'category' => $item->category, 'item' => $item->item, 'standard' => $item->standard, 'standard_criteria' => $item->standard_criteria, 'tolerance' => $item->tolerance, 'method' => $item->method, 'method_name' => $methodName, // 검사방식 한글 이름 추가 'measurement_type' => $item->measurement_type, 'frequency' => $item->frequency, 'frequency_n' => $item->frequency_n, 'frequency_c' => $item->frequency_c, 'regulation' => $item->regulation, 'sort_order' => $item->sort_order, ]; })->toArray(), ])->toArray(), 'columns' => $template->columns->map(fn ($col) => [ 'id' => $col->id, 'label' => $col->label, 'column_type' => $col->column_type, 'sub_labels' => $col->sub_labels, 'group_name' => $col->group_name, 'width' => $col->width, 'sort_order' => $col->sort_order, ])->toArray(), ]; } /** * 문서를 React 응답용으로 포맷 */ private function formatDocumentForReact(Document $document): array { return [ 'id' => $document->id, 'document_no' => $document->document_no, 'title' => $document->title, 'status' => $document->status, 'linkable_type' => $document->linkable_type, 'linkable_id' => $document->linkable_id, 'submitted_at' => $document->submitted_at?->toIso8601String(), 'completed_at' => $document->completed_at?->toIso8601String(), 'created_at' => $document->created_at?->toIso8601String(), 'data' => $document->data->map(fn ($d) => [ 'section_id' => $d->section_id, 'column_id' => $d->column_id, 'row_index' => $d->row_index, 'field_key' => $d->field_key, 'field_value' => $d->field_value, ])->toArray(), 'attachments' => $document->attachments->map(fn ($a) => [ 'id' => $a->id, 'file_id' => $a->file_id, 'attachment_type' => $a->attachment_type, 'description' => $a->description, 'file' => $a->file ? [ 'id' => $a->file->id, 'original_name' => $a->file->original_name ?? $a->file->display_name ?? $a->file->stored_name, 'display_name' => $a->file->display_name, 'file_path' => $a->file->file_path, 'file_size' => $a->file->file_size, 'mime_type' => $a->file->mime_type, ] : null, ])->toArray(), 'approvals' => $document->approvals->map(fn ($ap) => [ 'id' => $ap->id, 'user_id' => $ap->user_id, 'user_name' => $ap->user?->name, 'step' => $ap->step, 'role' => $ap->role, 'status' => $ap->status, 'comment' => $ap->comment, 'acted_at' => $ap->acted_at?->toIso8601String(), ])->toArray(), ]; } // ========================================================================= // 헬퍼 메서드 // ========================================================================= /** * 문서번호 생성 */ private function generateDocumentNo(int $tenantId, int $templateId): string { $prefix = 'DOC'; $date = now()->format('Ymd'); // 오늘 생성된 문서 중 마지막 번호 조회 $lastNumber = Document::query() ->where('tenant_id', $tenantId) ->where('document_no', 'like', "{$prefix}-{$date}-%") ->orderByDesc('document_no') ->value('document_no'); if ($lastNumber) { $sequence = (int) substr($lastNumber, -4) + 1; } else { $sequence = 1; } return sprintf('%s-%s-%04d', $prefix, $date, $sequence); } /** * 결재선 생성 */ private function createApprovals(Document $document, array $approvers, int $userId): void { foreach ($approvers as $index => $approver) { DocumentApproval::create([ 'document_id' => $document->id, 'user_id' => $approver['user_id'], 'step' => $index + 1, 'role' => $approver['role'] ?? '승인', 'status' => DocumentApproval::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); } } /** * 문서 데이터 저장 */ private function saveDocumentData(Document $document, array $dataItems): void { foreach ($dataItems as $item) { DocumentData::create([ 'document_id' => $document->id, 'section_id' => $item['section_id'] ?? null, 'column_id' => $item['column_id'] ?? null, 'row_index' => $item['row_index'] ?? 0, 'field_key' => $item['field_key'], 'field_value' => $item['field_value'] ?? null, ]); } } /** * 첨부파일 연결 */ private function attachFiles(Document $document, array $attachments, int $userId): void { foreach ($attachments as $attachment) { DocumentAttachment::create([ 'document_id' => $document->id, 'file_id' => $attachment['file_id'], 'attachment_type' => $attachment['attachment_type'] ?? DocumentAttachment::TYPE_GENERAL, 'description' => $attachment['description'] ?? null, 'created_by' => $userId, ]); } } }