where('tenant_id', $tenantId) ->orderBy('created_at', 'desc'); // 상태 필터 if ($request->filled('status')) { $query->where('status', $request->status); } // 템플릿 필터 if ($request->filled('template_id')) { $query->where('template_id', $request->template_id); } // 검색 if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('document_no', 'like', "%{$search}%") ->orWhere('title', 'like', "%{$search}%"); }); } // 날짜 범위 필터 if ($request->filled('date_from')) { $query->whereDate('created_at', '>=', $request->date_from); } if ($request->filled('date_to')) { $query->whereDate('created_at', '<=', $request->date_to); } $documents = $query->paginate($request->input('per_page', 15)); return response()->json($documents); } /** * 문서 상세 조회 */ public function show(int $id): JsonResponse { $tenantId = session('selected_tenant_id'); $document = Document::with([ 'template.approvalLines', 'template.basicFields', 'template.sections.items', 'template.columns', 'approvals.user', 'data', 'attachments.file', 'creator', 'updater', ])->where('tenant_id', $tenantId)->findOrFail($id); return response()->json([ 'success' => true, 'data' => $document, ]); } /** * 문서 생성 */ public function store(Request $request): JsonResponse { $tenantId = session('selected_tenant_id'); $userId = auth()->id(); $request->validate([ 'template_id' => 'required|exists:document_templates,id', 'title' => 'required|string|max:255', 'data' => 'nullable|array', 'data.*.field_key' => 'required|string', 'data.*.field_value' => 'nullable|string', 'data.*.section_id' => 'nullable|integer', 'data.*.column_id' => 'nullable|integer', 'data.*.row_index' => 'nullable|integer', ]); try { DB::beginTransaction(); // 문서 번호 생성 $documentNo = $this->generateDocumentNo($tenantId, $request->template_id); $document = Document::create([ 'tenant_id' => $tenantId, 'template_id' => $request->template_id, 'document_no' => $documentNo, 'title' => $request->title, 'status' => Document::STATUS_DRAFT, 'created_by' => $userId, 'updated_by' => $userId, ]); // 결재라인 초기화 (템플릿의 approvalLines 기반) $template = DocumentTemplate::with('approvalLines')->find($request->template_id); if ($template && $template->approvalLines->isNotEmpty()) { foreach ($template->approvalLines as $line) { DocumentApproval::create([ 'document_id' => $document->id, 'user_id' => $userId, 'step' => $line->sort_order + 1, 'role' => $line->role ?? $line->name, 'status' => DocumentApproval::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); } } // 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터) $this->saveDocumentData($document, $request->input('data', [])); DB::commit(); return response()->json([ 'success' => true, 'message' => '문서가 저장되었습니다.', 'data' => $document->fresh(['template', 'data', 'approvals']), ], 201); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => '문서 생성 중 오류가 발생했습니다: '.$e->getMessage(), ], 500); } } /** * 문서 수정 */ public function update(int $id, Request $request): JsonResponse { $tenantId = session('selected_tenant_id'); $userId = auth()->id(); $document = Document::where('tenant_id', $tenantId)->findOrFail($id); // 작성중 또는 반려 상태에서만 수정 가능 if (! in_array($document->status, [Document::STATUS_DRAFT, Document::STATUS_REJECTED])) { return response()->json([ 'success' => false, 'message' => '현재 상태에서는 수정할 수 없습니다.', ], 422); } $request->validate([ 'title' => 'sometimes|required|string|max:255', 'data' => 'nullable|array', 'data.*.field_key' => 'required|string', 'data.*.field_value' => 'nullable|string', 'data.*.section_id' => 'nullable|integer', 'data.*.column_id' => 'nullable|integer', 'data.*.row_index' => 'nullable|integer', ]); $document->update([ 'title' => $request->input('title', $document->title), 'updated_by' => $userId, ]); // 문서 데이터 업데이트 (기존 삭제 후 재저장) if ($request->has('data')) { $document->data()->delete(); $this->saveDocumentData($document, $request->input('data', [])); } return response()->json([ 'success' => true, 'message' => '문서가 수정되었습니다.', 'data' => $document->fresh(['template', 'data']), ]); } /** * 문서 삭제 (소프트 삭제) */ public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id'); $document = Document::where('tenant_id', $tenantId)->findOrFail($id); // 작성중 상태에서만 삭제 가능 if ($document->status !== Document::STATUS_DRAFT) { return response()->json([ 'success' => false, 'message' => '작성중 상태의 문서만 삭제할 수 있습니다.', ], 422); } $document->delete(); return response()->json([ 'success' => true, 'message' => '문서가 삭제되었습니다.', ]); } /** * 결재 제출 (DRAFT → PENDING) */ public function submit(int $id): JsonResponse { $tenantId = session('selected_tenant_id'); $userId = auth()->id(); $document = Document::where('tenant_id', $tenantId)->findOrFail($id); if ($document->status !== Document::STATUS_DRAFT && $document->status !== Document::STATUS_REJECTED) { return response()->json([ 'success' => false, 'message' => '작성중 또는 반려 상태의 문서만 제출할 수 있습니다.', ], 422); } $document->update([ 'status' => Document::STATUS_PENDING, 'submitted_at' => now(), 'updated_by' => $userId, ]); // 결재라인 상태 초기화 (반려 후 재제출 시) $document->approvals()->update([ 'status' => DocumentApproval::STATUS_PENDING, 'comment' => null, 'acted_at' => null, ]); return response()->json([ 'success' => true, 'message' => '결재가 제출되었습니다.', 'data' => $document->fresh(['approvals']), ]); } /** * 결재 승인 (단계별) */ public function approve(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id'); $userId = auth()->id(); $document = Document::where('tenant_id', $tenantId)->findOrFail($id); if ($document->status !== Document::STATUS_PENDING) { return response()->json([ 'success' => false, 'message' => '결재중 상태의 문서만 승인할 수 있습니다.', ], 422); } // 현재 단계의 미처리 결재 찾기 $pendingApproval = $document->approvals() ->where('status', DocumentApproval::STATUS_PENDING) ->orderBy('step') ->first(); if (! $pendingApproval) { return response()->json([ 'success' => false, 'message' => '승인 대기 중인 결재 단계가 없습니다.', ], 422); } $pendingApproval->update([ 'user_id' => $userId, 'status' => DocumentApproval::STATUS_APPROVED, 'comment' => $request->input('comment'), 'acted_at' => now(), 'updated_by' => $userId, ]); // 모든 결재가 완료되었는지 확인 $remainingPending = $document->approvals() ->where('status', DocumentApproval::STATUS_PENDING) ->count(); if ($remainingPending === 0) { $document->update([ 'status' => Document::STATUS_APPROVED, 'completed_at' => now(), 'updated_by' => $userId, ]); } return response()->json([ 'success' => true, 'message' => $remainingPending === 0 ? '최종 승인되었습니다.' : '승인되었습니다. (다음 단계 대기)', 'data' => $document->fresh(['approvals']), ]); } /** * 결재 반려 */ public function reject(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id'); $userId = auth()->id(); $request->validate([ 'comment' => 'required|string|max:500', ]); $document = Document::where('tenant_id', $tenantId)->findOrFail($id); if ($document->status !== Document::STATUS_PENDING) { return response()->json([ 'success' => false, 'message' => '결재중 상태의 문서만 반려할 수 있습니다.', ], 422); } // 현재 단계 결재에 반려 기록 $pendingApproval = $document->approvals() ->where('status', DocumentApproval::STATUS_PENDING) ->orderBy('step') ->first(); if ($pendingApproval) { $pendingApproval->update([ 'user_id' => $userId, 'status' => DocumentApproval::STATUS_REJECTED, 'comment' => $request->input('comment'), 'acted_at' => now(), 'updated_by' => $userId, ]); } $document->update([ 'status' => Document::STATUS_REJECTED, 'completed_at' => now(), 'updated_by' => $userId, ]); return response()->json([ 'success' => true, 'message' => '문서가 반려되었습니다.', 'data' => $document->fresh(['approvals']), ]); } /** * 문서 데이터 저장 (기본필드 + 섹션 테이블 데이터) */ private function saveDocumentData(Document $document, array $dataItems): void { foreach ($dataItems as $item) { if (empty($item['field_key'])) { continue; } // 빈 값도 저장 (섹션 데이터 편집 시 빈값으로 클리어 가능) 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'] ?? '', ]); } } /** * 문서 번호 생성 * 형식: {카테고리prefix}-{YYMMDD}-{순번} * 예: IQC-260131-01, PRD-260131-01 */ private function generateDocumentNo(int $tenantId, int $templateId): string { $template = DocumentTemplate::find($templateId); $prefix = $this->getCategoryPrefix($template?->category); $date = now()->format('ymd'); $lastDocument = Document::where('tenant_id', $tenantId) ->where('document_no', 'like', "{$prefix}-{$date}-%") ->orderBy('id', 'desc') ->first(); $sequence = 1; if ($lastDocument) { $parts = explode('-', $lastDocument->document_no); if (count($parts) >= 3) { $sequence = (int) end($parts) + 1; } } return sprintf('%s-%s-%02d', $prefix, $date, $sequence); } /** * 카테고리별 문서번호 prefix * 카테고리가 '품질/수입검사' 등 슬래시 포함 시 상위 카테고리 기준 */ private function getCategoryPrefix(?string $category): string { if (! $category) { return 'DOC'; } // 상위 카테고리 추출 (슬래시 포함 시) $mainCategory = str_contains($category, '/') ? explode('/', $category)[0] : $category; return match ($mainCategory) { '품질' => 'IQC', '생산' => 'PRD', '영업' => 'SLS', '구매' => 'PUR', default => 'DOC', }; } }