501 lines
16 KiB
PHP
501 lines
16 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');
|
|
|
|
// 슈퍼관리자 휴지통 조회
|
|
$showTrashed = $request->filled('trashed') && auth()->user()?->is_super_admin;
|
|
|
|
$query = Document::with(['template', 'creator'])
|
|
->where('tenant_id', $tenantId)
|
|
->orderBy('created_at', 'desc');
|
|
|
|
if ($showTrashed) {
|
|
$query->onlyTrashed();
|
|
}
|
|
|
|
// 상태 필터
|
|
if ($request->filled('status')) {
|
|
$query->where('status', $request->status);
|
|
}
|
|
|
|
// 양식분류 필터
|
|
if ($request->filled('category')) {
|
|
$category = $request->category;
|
|
$query->whereHas('template', function ($q) use ($category) {
|
|
$q->where('category', $category);
|
|
});
|
|
}
|
|
|
|
// 템플릿 필터
|
|
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);
|
|
|
|
$document->update(['deleted_by' => auth()->id()]);
|
|
$document->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');
|
|
|
|
$document = Document::withTrashed()->where('tenant_id', $tenantId)->findOrFail($id);
|
|
|
|
// 관련 데이터도 영구삭제
|
|
$document->data()->delete();
|
|
$document->approvals()->delete();
|
|
$document->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');
|
|
|
|
$document = Document::onlyTrashed()->where('tenant_id', $tenantId)->findOrFail($id);
|
|
$document->update(['deleted_by' => null]);
|
|
$document->restore();
|
|
|
|
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',
|
|
};
|
|
}
|
|
}
|