feat:문서양식관리 기능 추가 및 권한 토글 개선
- 문서양식관리 CRUD 기능 구현 (생산관리 > 문서양식관리) - 결재라인, 섹션, 컬럼 동적 관리 (Vanilla JS) - 섹션별 이미지 업로드 기능 - SortableJS 드래그앤드롭 순서 변경 - 문서 미리보기 모달 - document_type 글로벌 코드 추가 (품질, 생산, 영업, 구매, 일반, 기타) - 역할/부서 권한 토글 시 페이지 새로고침 방지 (hx-swap="none") Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
320
app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php
Normal file
320
app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Models\DocumentTemplateApprovalLine;
|
||||
use App\Models\DocumentTemplateBasicField;
|
||||
use App\Models\DocumentTemplateColumn;
|
||||
use App\Models\DocumentTemplateSection;
|
||||
use App\Models\DocumentTemplateSectionItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DocumentTemplateApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* 목록 조회 (HTMX 테이블)
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = DocumentTemplate::query()
|
||||
->withCount(['sections', 'columns']);
|
||||
|
||||
// 검색
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('title', 'like', "%{$search}%")
|
||||
->orWhere('category', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 카테고리 필터
|
||||
if ($category = $request->input('category')) {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
// 활성 상태 필터
|
||||
if ($request->has('is_active') && $request->input('is_active') !== '') {
|
||||
$query->where('is_active', $request->boolean('is_active'));
|
||||
}
|
||||
|
||||
$templates = $query->orderBy('updated_at', 'desc')
|
||||
->paginate($request->input('per_page', 10));
|
||||
|
||||
return view('document-templates.partials.table', compact('templates'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$template = DocumentTemplate::with([
|
||||
'approvalLines',
|
||||
'basicFields',
|
||||
'sections.items',
|
||||
'columns',
|
||||
])->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $template,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'category' => 'nullable|string|max:50',
|
||||
'title' => 'nullable|string|max:200',
|
||||
'company_name' => 'nullable|string|max:100',
|
||||
'company_address' => 'nullable|string|max:255',
|
||||
'company_contact' => 'nullable|string|max:100',
|
||||
'footer_remark_label' => 'nullable|string|max:50',
|
||||
'footer_judgement_label' => 'nullable|string|max:50',
|
||||
'footer_judgement_options' => 'nullable|array',
|
||||
'is_active' => 'boolean',
|
||||
// 관계 데이터
|
||||
'approval_lines' => 'nullable|array',
|
||||
'basic_fields' => 'nullable|array',
|
||||
'sections' => 'nullable|array',
|
||||
'columns' => 'nullable|array',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$template = DocumentTemplate::create([
|
||||
'tenant_id' => session('selected_tenant_id'),
|
||||
'name' => $validated['name'],
|
||||
'category' => $validated['category'] ?? null,
|
||||
'title' => $validated['title'] ?? null,
|
||||
'company_name' => $validated['company_name'] ?? '경동기업',
|
||||
'company_address' => $validated['company_address'] ?? null,
|
||||
'company_contact' => $validated['company_contact'] ?? null,
|
||||
'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용',
|
||||
'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정',
|
||||
'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'],
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
// 관계 데이터 저장
|
||||
$this->saveRelations($template, $validated);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '문서양식이 생성되었습니다.',
|
||||
'data' => $template->load(['approvalLines', 'basicFields', 'sections.items', 'columns']),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '생성 중 오류가 발생했습니다: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$template = DocumentTemplate::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'category' => 'nullable|string|max:50',
|
||||
'title' => 'nullable|string|max:200',
|
||||
'company_name' => 'nullable|string|max:100',
|
||||
'company_address' => 'nullable|string|max:255',
|
||||
'company_contact' => 'nullable|string|max:100',
|
||||
'footer_remark_label' => 'nullable|string|max:50',
|
||||
'footer_judgement_label' => 'nullable|string|max:50',
|
||||
'footer_judgement_options' => 'nullable|array',
|
||||
'is_active' => 'boolean',
|
||||
// 관계 데이터
|
||||
'approval_lines' => 'nullable|array',
|
||||
'basic_fields' => 'nullable|array',
|
||||
'sections' => 'nullable|array',
|
||||
'columns' => 'nullable|array',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$template->update([
|
||||
'name' => $validated['name'],
|
||||
'category' => $validated['category'] ?? null,
|
||||
'title' => $validated['title'] ?? null,
|
||||
'company_name' => $validated['company_name'] ?? null,
|
||||
'company_address' => $validated['company_address'] ?? null,
|
||||
'company_contact' => $validated['company_contact'] ?? null,
|
||||
'footer_remark_label' => $validated['footer_remark_label'] ?? '부적합 내용',
|
||||
'footer_judgement_label' => $validated['footer_judgement_label'] ?? '종합판정',
|
||||
'footer_judgement_options' => $validated['footer_judgement_options'] ?? ['적합', '부적합'],
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
// 관계 데이터 저장 (기존 데이터 삭제 후 재생성)
|
||||
$this->saveRelations($template, $validated, true);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '문서양식이 수정되었습니다.',
|
||||
'data' => $template->fresh(['approvalLines', 'basicFields', 'sections.items', 'columns']),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '수정 중 오류가 발생했습니다: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$template = DocumentTemplate::findOrFail($id);
|
||||
$template->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '문서양식이 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 상태 토글
|
||||
*/
|
||||
public function toggleActive(int $id): JsonResponse
|
||||
{
|
||||
$template = DocumentTemplate::findOrFail($id);
|
||||
$template->update(['is_active' => ! $template->is_active]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $template->is_active ? '활성화되었습니다.' : '비활성화되었습니다.',
|
||||
'is_active' => $template->is_active,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 업로드
|
||||
*/
|
||||
public function uploadImage(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required|image|max:5120', // 5MB
|
||||
]);
|
||||
|
||||
$path = $request->file('image')->store('document-templates', 'public');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
'url' => asset('storage/'.$path),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계 데이터 저장
|
||||
*/
|
||||
private function saveRelations(DocumentTemplate $template, array $data, bool $deleteExisting = false): void
|
||||
{
|
||||
// 기존 데이터 삭제 (수정 시)
|
||||
if ($deleteExisting) {
|
||||
$template->approvalLines()->delete();
|
||||
$template->basicFields()->delete();
|
||||
// sections는 cascade로 items도 함께 삭제됨
|
||||
$template->sections()->delete();
|
||||
$template->columns()->delete();
|
||||
}
|
||||
|
||||
// 결재라인
|
||||
if (! empty($data['approval_lines'])) {
|
||||
foreach ($data['approval_lines'] as $index => $line) {
|
||||
DocumentTemplateApprovalLine::create([
|
||||
'template_id' => $template->id,
|
||||
'name' => $line['name'] ?? '',
|
||||
'dept' => $line['dept'] ?? '',
|
||||
'role' => $line['role'] ?? '',
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 필드
|
||||
if (! empty($data['basic_fields'])) {
|
||||
foreach ($data['basic_fields'] as $index => $field) {
|
||||
DocumentTemplateBasicField::create([
|
||||
'template_id' => $template->id,
|
||||
'label' => $field['label'] ?? '',
|
||||
'field_type' => $field['field_type'] ?? 'text',
|
||||
'default_value' => $field['default_value'] ?? '',
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 섹션 및 항목
|
||||
if (! empty($data['sections'])) {
|
||||
foreach ($data['sections'] as $sIndex => $section) {
|
||||
$newSection = DocumentTemplateSection::create([
|
||||
'template_id' => $template->id,
|
||||
'title' => $section['title'] ?? '',
|
||||
'image_path' => $section['image_path'] ?? null,
|
||||
'sort_order' => $sIndex,
|
||||
]);
|
||||
|
||||
if (! empty($section['items'])) {
|
||||
foreach ($section['items'] as $iIndex => $item) {
|
||||
DocumentTemplateSectionItem::create([
|
||||
'section_id' => $newSection->id,
|
||||
'category' => $item['category'] ?? '',
|
||||
'item' => $item['item'] ?? '',
|
||||
'standard' => $item['standard'] ?? '',
|
||||
'method' => $item['method'] ?? '',
|
||||
'frequency' => $item['frequency'] ?? '',
|
||||
'regulation' => $item['regulation'] ?? '',
|
||||
'sort_order' => $iIndex,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 컬럼
|
||||
if (! empty($data['columns'])) {
|
||||
foreach ($data['columns'] as $index => $column) {
|
||||
DocumentTemplateColumn::create([
|
||||
'template_id' => $template->id,
|
||||
'label' => $column['label'] ?? '',
|
||||
'width' => $column['width'] ?? '100px',
|
||||
'column_type' => $column['column_type'] ?? 'text',
|
||||
'group_name' => $column['group_name'] ?? null,
|
||||
'sub_labels' => $column['sub_labels'] ?? null,
|
||||
'sort_order' => $index,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class CommonCodeController extends Controller
|
||||
'bad_debt_progress' => '대손진행',
|
||||
'height_construction_cost' => '높이시공비',
|
||||
'width_construction_cost' => '폭시공비',
|
||||
'document_type' => '문서분류',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
133
app/Http/Controllers/DocumentTemplateController.php
Normal file
133
app/Http/Controllers/DocumentTemplateController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Models\Products\CommonCode;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DocumentTemplateController extends Controller
|
||||
{
|
||||
/**
|
||||
* 문서양식 목록 페이지
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('document-templates.index', [
|
||||
'documentTypes' => $this->getDocumentTypes(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서양식 생성 페이지
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('document-templates.edit', [
|
||||
'template' => null,
|
||||
'templateData' => null,
|
||||
'isCreate' => true,
|
||||
'documentTypes' => $this->getDocumentTypes(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서양식 수정 페이지
|
||||
*/
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$template = DocumentTemplate::with([
|
||||
'approvalLines',
|
||||
'basicFields',
|
||||
'sections.items',
|
||||
'columns',
|
||||
])->findOrFail($id);
|
||||
|
||||
// JavaScript용 데이터 변환
|
||||
$templateData = $this->prepareTemplateData($template);
|
||||
|
||||
return view('document-templates.edit', [
|
||||
'template' => $template,
|
||||
'templateData' => $templateData,
|
||||
'isCreate' => false,
|
||||
'documentTypes' => $this->getDocumentTypes(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서분류 목록 조회 (글로벌 + 테넌트)
|
||||
*/
|
||||
private function getDocumentTypes(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return CommonCode::query()
|
||||
->where(function ($query) use ($tenantId) {
|
||||
$query->whereNull('tenant_id');
|
||||
if ($tenantId) {
|
||||
$query->orWhere('tenant_id', $tenantId);
|
||||
}
|
||||
})
|
||||
->where('code_group', 'document_type')
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->pluck('name', 'code')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaScript용 템플릿 데이터 준비
|
||||
*/
|
||||
private function prepareTemplateData(DocumentTemplate $template): array
|
||||
{
|
||||
return [
|
||||
'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,
|
||||
'is_active' => $template->is_active,
|
||||
'approval_lines' => $template->approvalLines->map(function ($l) {
|
||||
return [
|
||||
'id' => $l->id,
|
||||
'name' => $l->name,
|
||||
'dept' => $l->dept,
|
||||
'role' => $l->role,
|
||||
];
|
||||
})->toArray(),
|
||||
'sections' => $template->sections->map(function ($s) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'title' => $s->title,
|
||||
'image_path' => $s->image_path,
|
||||
'items' => $s->items->map(function ($i) {
|
||||
return [
|
||||
'id' => $i->id,
|
||||
'category' => $i->category,
|
||||
'item' => $i->item,
|
||||
'standard' => $i->standard,
|
||||
'method' => $i->method,
|
||||
'frequency' => $i->frequency,
|
||||
'regulation' => $i->regulation,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
})->toArray(),
|
||||
'columns' => $template->columns->map(function ($c) {
|
||||
return [
|
||||
'id' => $c->id,
|
||||
'label' => $c->label,
|
||||
'width' => $c->width,
|
||||
'column_type' => $c->column_type,
|
||||
'group_name' => $c->group_name,
|
||||
'sub_labels' => $c->sub_labels,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user