Files
sam-manage/app/Http/Controllers/Api/Admin/DocumentApiController.php
권혁성 7635373a45 feat:문서관리 Phase 1.3~2.1 구현 (시드데이터, 복제, 문서생성)
- Phase 1.3: EGI/SUS 수입검사 시드 데이터 생성 (IncomingInspectionTemplateSeeder)
- Phase 1.5: 양식 복제 기능 (duplicate API, 테이블 버튼, JS)
- Phase 2.1: 문서 생성 보완
  - 문서번호 카테고리별 prefix (IQC/PRD/SLS/PUR-YYMMDD-순번)
  - 결재라인 초기화 (template.approvalLines → document_approvals)
  - 기본필드 뷰 속성 수정 (field_type, Str::slug field_key)
  - store()에 DB 트랜잭션 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 04:32:35 +09:00

282 lines
8.8 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Documents\Document;
use App\Models\Documents\DocumentApproval;
use App\Models\Documents\DocumentData;
use App\Models\DocumentTemplate;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DocumentApiController extends Controller
{
/**
* 문서 목록 조회
*/
public function index(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id');
$query = Document::with(['template', 'creator'])
->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}%");
});
}
$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',
]);
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,
]);
}
}
// 문서 데이터 저장
if ($request->filled('data')) {
foreach ($request->data as $item) {
if (! empty($item['field_value'])) {
DocumentData::create([
'document_id' => $document->id,
'field_key' => $item['field_key'],
'field_value' => $item['field_value'],
]);
}
}
}
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',
]);
$document->update([
'title' => $request->input('title', $document->title),
'updated_by' => $userId,
]);
// 문서 데이터 업데이트
if ($request->has('data')) {
// 기존 데이터 삭제
$document->data()->delete();
// 새 데이터 저장
foreach ($request->data as $item) {
if (! empty($item['field_value'])) {
DocumentData::create([
'document_id' => $document->id,
'field_key' => $item['field_key'],
'field_value' => $item['field_value'],
]);
}
}
}
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' => '문서가 삭제되었습니다.',
]);
}
/**
* 문서 번호 생성
* 형식: {카테고리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',
};
}
}