refactor: [structure] sam/ 하위 문서를 docs 루트로 재배치

- .gitignore를 sam/ 기반에서 루트 기반으로 변경
- sam/docs/ 하위 문서를 루트로 이동 (contracts, features, guides, plans 등)
- sam/ 폴더 삭제 (docker, coocon 포함)
This commit is contained in:
2026-03-09 22:53:07 +09:00
parent cc38b00c11
commit 7a969b9d57
64 changed files with 18723 additions and 15 deletions

View File

@@ -0,0 +1,738 @@
# MNG 문서관리 시스템 상세 기술 명세
> **작성일**: 2026-03-06
> **상태**: 운영 중
> **프로젝트**: SAM MNG (관리자 웹)
> **관련**: [README.md](README.md) (API 명세)
---
## 1. 개요
### 1.1 목적
블라인드/스크린 제조 현장의 **검사 성적서, 작업일지, 수입검사 기록** 등 품질/생산 문서를 전자화하여 관리하는 시스템. 문서 양식(Template)을 정의하면 EAV 패턴으로 데이터를 동적 저장하며, 다단계 결재 워크플로우를 지원한다.
### 1.2 핵심 특징
| 특징 | 설명 |
|------|------|
| **EAV 패턴** | 양식별로 다른 필드를 하나의 `document_data` 테이블에 저장 |
| **2가지 양식 빌더** | 레거시 빌더 (DB 정규화) + 블록 빌더 (A4 JSON 스키마) |
| **결재 워크플로우** | 작성 → 검토 → 승인 (다단계 순차 결재) |
| **자동 데이터 매핑** | 작업지시서/수주 데이터에서 기본필드 자동 채움 |
| **다형성 연결** | work_order, sales_order 등 다양한 모델과 연결 |
| **자재 LOT 추적** | 검사 문서에서 투입 자재의 LOT 이력 조회 |
### 1.3 문서 구조
| 문서 | 설명 |
|------|------|
| [README.md](README.md) | API 엔드포인트, 모델 요약, FormRequest |
| **이 문서** | MNG 화면별 상세, 동작원리, 데이터 흐름 |
---
## 2. 메뉴/탭 구조
```
생산 관리
└── 문서관리
├── 문서 목록 /documents ← 문서 검색/필터/관리
├── 새 문서 작성 /documents/create ← 템플릿 선택 → 폼 입력
├── 문서 상세 /documents/{id} ← 읽기 전용 + 결재 현황
├── 문서 수정 /documents/{id}/edit ← DRAFT/REJECTED만
├── 인쇄 /documents/{id}/print ← 성적서 인쇄용
└── 문서양식 관리
├── 양식 목록 /document-templates ← 양식 검색/관리
├── 새 양식 (레거시) /document-templates/create ← 레거시 빌더
├── 양식 수정 /document-templates/{id}/edit ← 자동 빌더 판별
├── 양식 디자이너 /document-templates/block-create ← 블록 빌더
└── 블록 수정 /document-templates/{id}/block-edit ← 블록 빌더 수정
```
---
## 3. 파일 구조
```
mng/
├── app/Http/Controllers/
│ ├── DocumentController.php ← 문서 CRUD 화면
│ └── DocumentTemplateController.php ← 양식 관리 화면
├── app/Models/Documents/
│ ├── Document.php ← 문서 모델
│ ├── DocumentApproval.php ← 결재 단계
│ ├── DocumentData.php ← EAV 데이터
│ ├── DocumentTemplate.php ← 양식 마스터
│ └── ... (기타 템플릿 관련 모델)
└── resources/views/
├── documents/
│ ├── index.blade.php ← 문서 목록
│ ├── edit.blade.php ← 문서 작성/수정
│ ├── show.blade.php ← 문서 상세
│ └── print.blade.php ← 인쇄 전용
└── document-templates/
├── index.blade.php ← 양식 목록
├── edit.blade.php ← 레거시 빌더
├── block-editor.blade.php ← 블록 빌더
└── partials/
├── block-palette.blade.php ← 블록 타입 목록
├── block-canvas.blade.php ← 편집 캔버스
└── block-properties.blade.php ← 속성 패널
```
---
## 4. 데이터베이스 아키텍처
### 4.1 테이블 관계도
```
document_templates (양식 마스터)
├── 1:N → document_template_approval_lines (결재선 정의)
├── 1:N → document_template_basic_fields (기본필드 정의)
├── 1:N → document_template_sections (섹션 정의)
│ └── 1:N → document_template_section_items (검사항목)
├── 1:N → document_template_columns (테이블 컬럼 정의)
├── 1:N → document_template_section_fields (섹션 필드)
├── 1:N → document_template_links (외부 연결 정의)
│ └── 1:N → document_template_link_values (템플릿 레벨 연결값)
└── 1:N → documents (문서 인스턴스)
├── 1:N → document_approvals (결재 진행)
├── 1:N → document_data (EAV 필드값)
├── 1:N → document_attachments (첨부파일)
└── 1:N → document_links (문서 레벨 연결)
```
### 4.2 documents (문서)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `template_id` | BIGINT FK | 사용 양식 |
| `document_no` | VARCHAR UNIQUE | 문서번호 (자동 채번) |
| `title` | VARCHAR | 문서 제목 |
| `status` | VARCHAR(20) | 상태 (5가지) |
| `linkable_type` | VARCHAR NULL | 다형성 모델 타입 |
| `linkable_id` | BIGINT NULL | 다형성 모델 ID |
| `submitted_at` | TIMESTAMP NULL | 결재 요청 일시 |
| `completed_at` | TIMESTAMP NULL | 결재 완료 일시 |
| `created_by` | BIGINT FK | 작성자 |
| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 |
**인덱스**: `(tenant_id, status)`, `document_no`, `(linkable_type, linkable_id)`
### 4.3 document_data (EAV 필드값)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `document_id` | BIGINT FK | 소속 문서 |
| `section_id` | BIGINT FK NULL | 소속 섹션 (NULL=기본필드) |
| `column_id` | BIGINT FK NULL | 소속 컬럼 (테이블 데이터용) |
| `row_index` | INT | 테이블 행 번호 (기본: 0) |
| `field_key` | VARCHAR | 필드 식별자 (`bf_1`, `cf_2`, `col_3`) |
| `field_value` | TEXT NULL | 실제 값 |
**인덱스**: `(document_id, section_id)`, `(document_id, field_key)`
### 4.4 document_approvals (결재)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `document_id` | BIGINT FK | 소속 문서 |
| `user_id` | BIGINT FK | 결재자 |
| `step` | INT | 결재 순서 (1, 2, 3...) |
| `role` | VARCHAR | 역할 (작성, 검토, 승인) |
| `status` | VARCHAR(20) | PENDING / APPROVED / REJECTED |
| `comment` | TEXT NULL | 결재 의견 |
| `acted_at` | TIMESTAMP NULL | 처리 일시 |
**인덱스**: `(document_id, step)`, `(user_id, status)`
### 4.5 document_attachments (첨부파일)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `document_id` | BIGINT FK | 소속 문서 |
| `file_id` | BIGINT FK | File 모델 연결 |
| `attachment_type` | VARCHAR | `general`, `signature`, `image`, `reference` |
| `description` | VARCHAR NULL | 설명 |
| `created_by` | BIGINT FK | 업로드자 |
---
## 5. 양식(Template) 시스템
### 5.1 두 가지 빌더 방식
| 방식 | 필드명 | 저장 구조 | UI | 상태 |
|------|--------|----------|-----|------|
| **레거시 빌더** | `builder_type = null` | 정규화 테이블들 | `edit.blade.php` | 기존 양식용 |
| **블록 빌더** | `builder_type = 'block'` | `schema` JSON | `block-editor.blade.php` | 신규 양식용 |
**자동 판별 로직:**
```php
// DocumentTemplateController::edit()
if ($template->isBlockBuilder()) {
return $this->blockEdit($id); // block-editor.blade.php
} else {
return view('document-templates.edit'); // 레거시
}
```
### 5.2 양식 마스터 (document_templates)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `name` | VARCHAR | 양식명 (예: "제품검사 성적서") |
| `category` | VARCHAR | 분류 (common_codes 기반) |
| `title` | VARCHAR NULL | 문서 제목 템플릿 |
| `company_name` | VARCHAR NULL | 회사명 |
| `company_address` | VARCHAR NULL | 회사 주소 |
| `company_contact` | VARCHAR NULL | 연락처 |
| `footer_remark_label` | VARCHAR NULL | 비고란 라벨 |
| `footer_judgement_label` | VARCHAR NULL | 판정란 라벨 |
| `footer_judgement_options` | JSON NULL | 판정 선택지 (적합/부적합) |
| `builder_type` | VARCHAR NULL | `block` 또는 NULL |
| `schema` | JSON NULL | 블록 빌더 JSON 스키마 |
| `page_config` | JSON NULL | 페이지 설정 (A4, 여백 등) |
| `is_active` | BOOLEAN | 활성 여부 |
### 5.3 레거시 빌더 구성 요소
#### 결재선 (document_template_approval_lines)
```
step 1: 작성 (작성자 본인)
step 2: 검토 (팀장)
step 3: 승인 (부장)
```
| 컬럼 | 설명 |
|------|------|
| `name` | 라벨 (작성, 검토, 승인) |
| `dept` | 부서 |
| `role` | 역할 |
| `sort_order` | 순서 |
#### 기본필드 (document_template_basic_fields)
문서 상단의 고정 필드 영역.
| 컬럼 | 설명 |
|------|------|
| `label` | 필드 라벨 (품명, LOT NO, 납기일 등) |
| `field_key` | 식별자 (EAV 저장 시 사용) |
| `field_type` | 입력 타입 (text, date, number, item_search) |
| `default_value` | 기본값 |
| `sort_order` | 순서 |
**EAV 저장 시 field_key 패턴:**
```
bf_1 → 기본필드 ID 1 (예: 품명)
bf_2 → 기본필드 ID 2 (예: LOT NO)
bf_3 → 기본필드 ID 3 (예: 납기일)
```
#### 섹션 (document_template_sections)
검사 기준서의 섹션 단위.
| 컬럼 | 설명 |
|------|------|
| `title` | 섹션 제목 (예: "겉모양 검사", "치수 검사") |
| `image_path` | 도해 이미지 경로 (검사 부위 도면) |
| `sort_order` | 순서 |
#### 검사항목 (document_template_section_items)
각 섹션 내의 개별 검사항목.
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `category` | VARCHAR | 구분 (겉모양, 치수, 재질) |
| `item` | VARCHAR | 검사항목명 |
| `standard` | VARCHAR | 검사기준 (100mm ±5mm) |
| `tolerance` | JSON NULL | 허용오차 (min/max) |
| `standard_criteria` | VARCHAR NULL | 판정기준 |
| `method` | VARCHAR | 검사방법 (육안, 측정) |
| `measurement_type` | VARCHAR NULL | 측정 유형 |
| `frequency_n` | INT NULL | 검사건수 N |
| `frequency_c` | INT NULL | 합격건수 C |
| `frequency` | VARCHAR NULL | 검사빈도 텍스트 |
| `field_values` | JSON NULL | 확장 필드 (마이그레이션 없이 추가) |
#### 테이블 컬럼 (document_template_columns)
검사 데이터 테이블의 컬럼 정의.
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `label` | VARCHAR | 컬럼 라벨 |
| `width` | INT NULL | 너비 (px) |
| `column_type` | VARCHAR | `text`, `check`, `complex`, `measurement`, `select` |
| `group_name` | VARCHAR NULL | 상단 병합 헤더명 |
| `sub_labels` | JSON NULL | complex 타입 하위 라벨 |
| `sort_order` | INT | 순서 |
**컬럼 타입 상세:**
| 타입 | 설명 | 예시 |
|------|------|------|
| `text` | 단순 텍스트 입력 | 비고, 메모 |
| `check` | 체크박스 (합격/부적합) | 외관 검사 합격 여부 |
| `complex` | 여러 서브필드 조합 | 측정값 + 단위 + 판정 |
| `measurement` | 수치 입력 | 길이: 100.5mm |
| `select` | 드롭다운 선택 | 판정: 합격/불합격/보류 |
#### 외부 연결 (document_template_links)
템플릿에서 외부 테이블 데이터를 참조하기 위한 정의.
| 컬럼 | 설명 |
|------|------|
| `link_key` | 연결 식별자 |
| `label` | 화면 라벨 |
| `link_type` | `single` (1개 선택) / `multiple` (다중 선택) |
| `source_table` | 소스 테이블 (`items`, `processes`, `users`) |
| `search_params` | API 검색 추가 조건 (JSON) |
| `display_fields` | 표시 필드 (title, subtitle) |
| `is_required` | 필수 여부 |
### 5.4 블록 빌더 구조
**페이지 설정 (page_config):**
```json
{
"size": "A4",
"orientation": "portrait",
"margin": {
"top": 20,
"right": 15,
"bottom": 20,
"left": 15
}
}
```
**스키마 (schema):**
블록 배열로 레이아웃 정의. 드래그앤드롭으로 편집.
```json
{
"blocks": [
{ "type": "text", "x": 0, "y": 0, "width": 100, "content": "검사 성적서" },
{ "type": "table", "x": 0, "y": 50, "columns": [...], "rows": [...] },
{ "type": "image", "x": 200, "y": 100, "src": "..." }
]
}
```
**블록 빌더 UI (3패널):**
```
┌──────────┬────────────────────┬──────────┐
│ 블록 │ │ 속성 │
│ 팔레트 │ A4 캔버스 │ 패널 │
│ │ │ │
│ [텍스트] │ ┌──────────────┐ │ 너비: _ │
│ [이미지] │ │ 드래그앤드롭 │ │ 높이: _ │
│ [표] │ │ 블록 배치 │ │ 색상: _ │
│ [선] │ │ │ │ 폰트: _ │
│ [도형] │ └──────────────┘ │ │
└──────────┴────────────────────┴──────────┘
```
---
## 6. EAV 데이터 저장 패턴
### 6.1 핵심 개념
하나의 `document_data` 테이블에 **모든 양식의 모든 필드값**을 저장. 양식이 다르면 field_key가 다르고, 같은 양식이라도 섹션/행이 다르면 section_id/row_index로 구분.
### 6.2 저장 구조
```
document_data 레코드 예시:
기본필드 (상단 고정 영역):
┌─────────────┬────────────┬───────────┬───────────┬───────────┬─────────────┐
│ document_id │ section_id │ column_id │ row_index │ field_key │ field_value │
├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤
│ 42 │ NULL │ NULL │ 0 │ bf_1 │ 블라인드A │ ← 품명
│ 42 │ NULL │ NULL │ 0 │ bf_2 │ LOT-2026-001│ ← LOT NO
│ 42 │ NULL │ NULL │ 0 │ bf_3 │ 2026-03-15 │ ← 납기일
├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤
테이블 데이터 (섹션별 검사 결과):
│ 42 │ 10 │ 20 │ 0 │ col_20 │ 합격 │ ← 섹션10, 컬럼20, 1행
│ 42 │ 10 │ 20 │ 1 │ col_20 │ 부적합 │ ← 섹션10, 컬럼20, 2행
│ 42 │ 10 │ 21 │ 0 │ col_21 │ 100.5 │ ← 섹션10, 컬럼21, 1행
└─────────────┴────────────┴───────────┴───────────┴───────────┴─────────────┘
```
### 6.3 field_key 네이밍 규칙
| 접두사 | 의미 | 예시 |
|--------|------|------|
| `bf_` | 기본필드 (BasicField) | `bf_1`, `bf_2` |
| `cf_` | 섹션필드 (SectionField) | `cf_5`, `cf_6` |
| `col_` | 컬럼 데이터 | `col_20`, `col_21` |
### 6.4 데이터 조회 패턴
```php
// 기본필드 값 조회
$data = DocumentData::where('document_id', $id)
->whereNull('section_id')
->get()
->keyBy('field_key');
$productName = $data['bf_1']->field_value;
// 섹션별 테이블 데이터 조회
$rows = DocumentData::where('document_id', $id)
->where('section_id', $sectionId)
->get()
->groupBy('row_index');
```
---
## 7. 결재 워크플로우
### 7.1 상태 전이
```
DRAFT (작성중)
├── submit() → PENDING (결재중)
│ │
│ ├── approve() [step 1] → 다음 step 대기
│ ├── approve() [step 2] → 다음 step 대기
│ ├── approve() [마지막] → APPROVED (승인)
│ │
│ └── reject() → REJECTED (반려)
│ │
│ └── edit → submit() → PENDING (재요청)
└── cancel() → CANCELLED (취소)
```
### 7.2 상태값 및 라벨
| 코드 | 라벨 | 색상 | 편집 가능 |
|------|------|------|----------|
| `DRAFT` | 작성중 | gray | 예 |
| `PENDING` | 결재중 | yellow | 아니오 |
| `APPROVED` | 승인 | green | 아니오 |
| `REJECTED` | 반려 | red | 예 (수정 후 재요청) |
| `CANCELLED` | 취소 | gray | 아니오 |
### 7.3 결재 단계 (Approval)
```
DocumentTemplateApprovalLine (양식 정의)
↓ (문서 생성 시 복사)
DocumentApproval (문서별 결재 레코드)
step 1: 작성 → PENDING → 결재자 승인 → APPROVED
step 2: 검토 → PENDING → 결재자 승인 → APPROVED
step 3: 승인 → PENDING → 결재자 승인 → APPROVED → 문서 전체 APPROVED
```
### 7.4 결재 판단 메서드
```php
// Document 모델
canEdit() // DRAFT 또는 REJECTED
canSubmit() // DRAFT 또는 REJECTED
canApprove() // PENDING (현재 결재자만)
canCancel() // DRAFT 또는 PENDING (작성자만)
```
---
## 8. 자동 데이터 매핑
### 8.1 개요
문서 작성/수정 시, 연결된 작업지시서(work_order)/수주(order) 데이터에서 기본필드를 **자동으로 채움**. 사용자 입력 부담을 줄이고 데이터 정확성을 보장.
### 8.2 검사 성적서 매핑 (field_key 기반)
| field_key | 라벨 | 소스 |
|-----------|------|------|
| `product_name` | 품명 | `workOrderItem.item_name` |
| `specification` | 규격 | `workOrderItem.specification` |
| `lot_no` | LOT NO | `order.order_no` |
| `lot_size` | LOT 크기 | `"N 개소"` (개소 수 기반) |
| `client` | 발주처 | `order.client_name` |
| `site_name` | 현장명 | `workOrder.project_name` |
| `inspection_date` | 검사일 | `workOrderItem.options.inspection_data.inspected_at` |
| `inspector` | 검사자 | 검사자 이름 |
### 8.3 작업일지 매핑 (label 기반)
| label 포함 문자열 | 소스 |
|------------------|------|
| `발주처` | `order.client_name` |
| `현장명` | `workOrder.project_name` |
| `작업일자` | `now()` |
| `LOT NO`, `LOT` | `order.order_no` |
| `납기일`, `납기` | `order.delivery_date` |
| `작업지시번호` | `workOrder.work_order_no` |
| `수주일` | `order.received_at` 또는 `order.created_at` |
### 8.4 자동 매핑 흐름
```
문서 작성/수정 페이지 로드
DocumentController::edit()
resolveAndBackfillBasicFields($template, $document)
linkable_type 확인 (work_order? order?)
field_key 또는 label 매칭
DB에 값이 없으면 → 소스 데이터에서 resolve
뷰에 자동 채움된 값 전달
```
---
## 9. 자재 LOT 추적
### 9.1 개요
검사 성적서에서 해당 작업지시의 **투입 자재 LOT 이력**을 조회. `stock_transactions` 테이블의 OUT(투입)/IN(취소) 트랜잭션을 상쇄하여 순수 투입량을 계산.
### 9.2 추적 구조
```
work_orders (작업지시)
├── stock_transactions (재고 트랜잭션)
│ ├── OUT (투입): qty < 0
│ └── IN (취소/반납): qty > 0
│ → 순수 투입량 = ABS(SUM(qty)) where qty < 0
└── work_order_material_inputs (개소별 투입자재)
└── stock_lots (LOT 정보) JOIN
```
### 9.3 표시 내용
| 항목 | 설명 |
|------|------|
| 자재명 | 투입된 원자재/부자재 이름 |
| LOT 번호 | 자재의 LOT 식별 번호 |
| 투입 수량 | OUT 트랜잭션 합계 (절대값) |
| 투입일 | 트랜잭션 일시 |
---
## 10. 화면별 상세
### 10.1 문서 목록 (/documents)
**필터 항목:**
| 필터 | 타입 | 설명 |
|------|------|------|
| 검색 | text | 문서번호 또는 제목 |
| 상태 | dropdown | DRAFT, PENDING, APPROVED, REJECTED, CANCELLED, 휴지통(admin) |
| 양식분류 | dropdown | category |
| 템플릿 | dropdown | template_id |
| 날짜 범위 | date | created_at (from ~ to) |
**목록 테이블 컬럼:**
```
문서번호 | 제목 | 양식 | 상태 | 작성자 | 작성일 | 결재현황
```
### 10.2 문서 작성/수정 (/documents/create, /documents/{id}/edit)
**폼 구성:**
```
┌──────────────────────────────────────────────┐
│ 템플릿 선택 (읽기전용) │
│ 제목 (필수) │
├──────────────────────────────────────────────┤
│ 기본 필드 (template.basicFields) │
│ ┌─────────────────┬─────────────────┐ │
│ │ 품명: [자동채움] │ LOT NO: [자동] │ │
│ │ 납기일: [날짜] │ 발주처: [자동] │ │
│ └─────────────────┴─────────────────┘ │
├──────────────────────────────────────────────┤
│ 섹션 1: 겉모양 검사 │
│ ┌──────────────────────────────────────┐ │
│ │ 도해 이미지 (있으면) │ │
│ ├──────┬──────┬──────┬──────┬──────┤ │
│ │ 구분 │ 항목 │ 기준 │ 결과1│ 결과2│ │
│ ├──────┼──────┼──────┼──────┼──────┤ │
│ │ 치수 │ 길이 │±5mm │ [ ] │ [ ] │ │
│ │ 외관 │ 흠집 │ 없음 │ [✓] │ [✓] │ │
│ ├──────┴──────┴──────┴──────┴──────┤ │
│ │ [+ 행 추가] [행 삭제] │ │
│ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ 외부 연결 (template.links) │
│ 품목 선택: [검색 드롭다운] │
├──────────────────────────────────────────────┤
│ 첨부파일 │
│ [일반 문서] [서명 이미지] [검사 사진] [참고 자료] │
├──────────────────────────────────────────────┤
│ [임시저장] [결재 요청] │
└──────────────────────────────────────────────┘
```
### 10.3 문서 상세 (/documents/{id})
**읽기 전용 표시:**
```
┌──────────────────────────────────────────────┐
│ 문서번호: DOC-260306-001 상태: [🟢 승인] │
│ 제목: 블라인드A 검사 성적서 │
├──────────────────────────────────────────────┤
│ 기본 필드 (읽기 전용) │
├──────────────────────────────────────────────┤
│ 검사 데이터 테이블 (읽기 전용) │
├──────────────────────────────────────────────┤
│ 결재 현황 │
│ ┌────────┬────────┬────────┐ │
│ │ 작성 │ 검토 │ 승인 │ │
│ │ 홍길동 │ 김과장 │ 박부장 │ │
│ │ ✓승인 │ ✓승인 │ ●대기 │ │
│ └────────┴────────┴────────┘ │
├──────────────────────────────────────────────┤
│ 자재 투입 LOT (작업지시 연결 시) │
│ ┌────────┬──────────┬──────┬──────┐ │
│ │ 자재명 │ LOT 번호 │ 수량 │ 투입일│ │
│ └────────┴──────────┴──────┴──────┘ │
├──────────────────────────────────────────────┤
│ 첨부파일 목록 │
├──────────────────────────────────────────────┤
│ [수정] [인쇄] [결재 승인] [결재 반려] │
└──────────────────────────────────────────────┘
```
### 10.4 인쇄 (/documents/{id}/print)
성적서 형식의 인쇄 전용 화면. `window.print()` 호출. 작업지시 관련 자재(work_order_items) 데이터 포함.
### 10.5 양식 목록 (/document-templates)
**필터:**
- 검색: 양식명, 제목, 분류
- 카테고리: common_codes 기반 + 기존 데이터 폴백
- 활성 상태: 활성 / 비활성 / 휴지통(admin)
**HTMX**: 필터 변경 시 테이블 영역만 부분 로드
---
## 11. 첨부파일 유형
| 유형 | 코드 | 용도 | 예시 |
|------|------|------|------|
| 일반 문서 | `general` | PDF, 엑셀 등 | 규격서, 보고서 |
| 서명 이미지 | `signature` | 검사 완료 서명 | 검사자 서명 사진 |
| 검사 사진 | `image` | 검사 증빙 사진 | 불량 부위 촬영 |
| 참고 자료 | `reference` | 참고용 문서 | KS 규격, 작업 지침 |
---
## 12. API 연동 (MNG → API)
MNG 뷰에서 데이터 저장/삭제는 **API 서버를 호출**하여 처리. GET 요청(뷰 렌더링)은 MNG 컨트롤러가 직접 처리.
| 작업 | MNG (GET 요청) | API (POST/PUT/DELETE) |
|------|---------------|----------------------|
| 목록 조회 | `DocumentController::index()` | `GET /v1/documents` |
| 상세 조회 | `DocumentController::show()` | `GET /v1/documents/{id}` |
| 생성 | 폼 표시만 | `POST /v1/documents` |
| 수정 | 폼 표시만 | `PATCH /v1/documents/{id}` |
| 삭제 | - | `DELETE /v1/documents/{id}` |
| 결재 요청 | - | `POST /v1/documents/{id}/submit` |
| 승인 | - | `POST /v1/documents/{id}/approve` |
| 반려 | - | `POST /v1/documents/{id}/reject` |
---
## 13. 카테고리 해결 로직
양식 카테고리는 **common_codes 테이블**에서 조회하되, 없으면 **기존 데이터에서 추출**하여 폴백.
```php
// DocumentTemplateController::getCategories()
$categories = CommonCode::where('group', 'document_category')
->orderBy('sort_order')
->get();
if ($categories->isEmpty()) {
// 폴백: 기존 템플릿의 category 값에서 중복 제거
$categories = DocumentTemplate::distinct('category')
->pluck('category')
->filter();
}
```
---
## 14. 검사항목 확장 (field_values JSON)
`document_template_section_items.field_values` JSON 컬럼으로 마이그레이션 없이 새 필드를 추가할 수 있다.
```json
{
"custom_field_1": "추가 기준값",
"min_value": 95.0,
"max_value": 105.0,
"unit": "mm"
}
```
> options JSON 컬럼 정책(`docs/standards/options-column-policy.md`) 준용
---
## 15. HTMX 전체 페이지 로드 규칙
문서관리 페이지들은 JavaScript를 사용하므로 HTMX 부분 로드 시 스크립트 미실행 문제가 있다. 컨트롤러에서 HX-Request 감지 시 **HX-Redirect로 전체 페이지 리로드 강제**.
```php
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('documents.index'));
}
```
---
## 관련 문서
- [README.md](README.md) — API 엔드포인트, 모델 요약, FormRequest
- [DB 스키마 — 문서/전자서명](../../system/database/documents.md) — 테이블 상세
- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 참고
- [결재관리 시스템](../approvals/README.md) — 별도 결재 시스템 (문서관리와 독립)
---
**최종 업데이트**: 2026-03-06

View File

@@ -0,0 +1,826 @@
# MNG 문서양식관리 (Document Template Management)
> **작성일**: 2026-03-06
> **상태**: 운영 중
> **라우트**: `/document-templates`
> **관련**: [README.md](README.md) | [MNG 문서관리](mng-document-system.md)
---
## 1. 개요
문서관리 시스템에서 사용하는 **서식(Template)**을 생성, 편집, 복제, 관리하는 기능. 검사 성적서, 작업지시서 등 다양한 문서 양식을 정의하며, 2가지 빌더 타입을 지원한다.
| 빌더 | builder_type | UI 명칭 | 설명 |
|------|-------------|---------|------|
| **Legacy Builder** | `legacy` 또는 null | 새 양식 | 탭 기반 폼 UI (순수 JavaScript) |
| **Block Builder** | `block` | 양식 디자이너 | WYSIWYG 캔버스 편집기 (Alpine.js + SortableJS) |
> **명칭 변경 이력**: Block Builder의 UI 표시 명칭이 '블록 빌더' → '양식 디자이너'로 변경됨 (2026-02-28)
**핵심 기능:**
- 결재선, 기본필드, 검사 기준서, 테이블 컬럼 정의
- EAV 데이터 구조의 서식 스키마 관리
- 양식 복제 (연결품목 제외)
- 프리셋 자동 제안 (카테고리별)
- 소프트 삭제 + 휴지통 관리 (슈퍼어드민)
---
## 2. 라우트
### 2.1 웹 라우트 (페이지)
```
GET /document-templates → index (목록)
GET /document-templates/create → create (Legacy 신규 생성)
GET /document-templates/block-create → blockCreate (양식 디자이너 신규 생성)
GET /document-templates/{id}/edit → edit (Legacy 편집)
GET /document-templates/{id}/block-edit → blockEdit (양식 디자이너 편집)
```
### 2.2 API 라우트 (CRUD + 기능)
```
Prefix: /api/admin/document-templates (HQ 관리자 전용)
GET / → index (HTMX 테이블)
POST / → store (생성)
GET /{id} → show (상세 조회)
PUT /{id} → update (수정)
DELETE /{id} → destroy (소프트 삭제)
DELETE /{id}/force → forceDestroy (영구삭제, 슈퍼어드민)
POST /{id}/restore → restore (복원, 슈퍼어드민)
POST /{id}/toggle-active → toggleActive (활성 토글)
POST /{id}/duplicate → duplicate (복제)
POST /upload-image → uploadImage (이미지 업로드)
GET /admin/common-codes/{group} → getCommonCodes (공통코드 조회)
```
---
## 3. 모델 구조
### 3.1 모델 관계도
```
DocumentTemplate (서식 마스터)
├── 1:N DocumentTemplateApprovalLine (결재선)
├── 1:N DocumentTemplateBasicField (기본필드)
├── 1:N DocumentTemplateSection (섹션/기준서)
│ └── 1:N DocumentTemplateSectionItem (섹션 항목)
├── 1:N DocumentTemplateSectionField (섹션 필드)
├── 1:N DocumentTemplateColumn (테이블 컬럼)
└── 1:N DocumentTemplateLink (연결 설정)
└── 1:N DocumentTemplateLinkValue (연결 값)
```
### 3.2 DocumentTemplate 핵심 필드
```php
// 기본 정보
builder_type // 'legacy' | 'block'
name // 양식명
category // 분류명
title // 문서 제목
// 회사 정보
company_name // 회사명
company_address // 회사 주소
company_contact // 회사 연락처
// 하단 설정
footer_remark_label // 비고 라벨
footer_judgement_label // 판정 라벨
footer_judgement_options // array - 판정 선택지
// Block Builder 전용
schema // array - 블록 스키마 (JSON)
page_config // array - 페이지 설정 (A4/A3, 여백 등)
// 연결 (레거시)
linked_item_ids // array - 연결 품목 ID 목록
linked_process_id // int - 연결 공정 ID
// 상태
is_active // boolean - 활성 여부
deleted_at // timestamp - 소프트 삭제
deleted_by // int - 삭제자
```
**Helper 메서드:**
```php
isBlockBuilder(): bool // builder_type === 'block'
isLegacyBuilder(): bool // builder_type !== 'block'
```
### 3.3 DocumentTemplateApprovalLine (결재선)
| 필드 | 타입 | 설명 |
|------|------|------|
| `template_id` | FK | 서식 ID |
| `name` | string | 결재자 이름/직책 |
| `department` | string | 부서 |
| `role` | string | 역할 (작성/검토/승인) |
| `sort_order` | int | 순서 |
### 3.4 DocumentTemplateBasicField (기본필드)
| 필드 | 타입 | 설명 |
|------|------|------|
| `template_id` | FK | 서식 ID |
| `field_key` | string | 필드 키 (bf_ 접두사) |
| `label` | string | 라벨 |
| `field_type` | string | text, date, select 등 |
| `default_value` | string | 기본값 |
| `is_required` | boolean | 필수 여부 |
| `sort_order` | int | 순서 |
| `options` | array | 선택지 (select 타입) |
### 3.5 DocumentTemplateSection (섹션/검사 기준서)
| 필드 | 타입 | 설명 |
|------|------|------|
| `template_id` | FK | 서식 ID |
| `title` | string | 섹션 제목 |
| `image_path` | string | 섹션 이미지 경로 |
| `sort_order` | int | 순서 |
**하위 관계:**
```
Section 1:N SectionItem
├── category // 카테고리 (그룹핑)
├── name // 항목명
├── standard // 기준
├── tolerance_type // 공차 유형 (symmetric/asymmetric/range/limit)
├── tolerance_plus // +공차
├── tolerance_minus // -공차
├── reference_value // 기준값
├── method // 검사방법
├── measurement_type // 측정유형
└── frequency // 검사주기
```
### 3.6 DocumentTemplateColumn (테이블 컬럼)
| 필드 | 타입 | 설명 |
|------|------|------|
| `template_id` | FK | 서식 ID |
| `label` | string | 컬럼 라벨 |
| `group_name` | string | 그룹명 (다단계 "/" 구분) |
| `width` | int | 컬럼 너비 |
| `column_type` | string | text, check, complex, select, measurement |
| `sub_labels` | array | complex 타입 하위 라벨 |
| `sort_order` | int | 순서 |
### 3.7 DocumentTemplateLink (연결 설정)
| 필드 | 타입 | 설명 |
|------|------|------|
| `template_id` | FK | 서식 ID |
| `link_key` | string | 연결 키 |
| `label` | string | 라벨 |
| `link_type` | string | `single` / `multiple` |
| `source_table` | string | `items` / `processes` / `users` |
| `search_params` | array | 검색 파라미터 |
| `display_fields` | array | 표시 필드 |
| `is_required` | boolean | 필수 여부 |
| `sort_order` | int | 순서 |
**하위 관계:**
```
Link 1:N LinkValue
├── link_id // FK → Link
├── linkable_id // 연결 엔티티 ID
└── (source_table에 따라 items/processes/users 참조)
```
**레거시 호환 처리:**
```php
// 신규 links가 있으면 사용
if ($template->links->isNotEmpty()) {
// template_links + link_values 사용
}
// 레거시만 있으면 가상 엔트리 생성
if (!empty($template->linked_item_ids)) {
return [['link_key' => 'items', 'values' => [...]]]
}
```
---
## 4. 컨트롤러 상세
### 4.1 DocumentTemplateController (웹)
| 메서드 | 동작 |
|--------|------|
| `index()` | HTMX 요청 → HX-Redirect 반환 (전체 페이지 로드 강제) |
| `create()` | Legacy 신규 생성 폼 렌더링 |
| `edit($id)` | Legacy 편집. 양식 디자이너 타입이면 `blockEdit`으로 자동 리다이렉트 |
| `blockCreate()` | 양식 디자이너 신규 생성 (빈 캔버스) |
| `blockEdit($id)` | 양식 디자이너 편집 (스키마 로드) |
**공통 데이터 준비:**
```php
// 현재 테넌트 조회
$tenantId = getCurrentTenant(); // 세션의 selected_tenant_id
// 카테고리 목록 = common_codes + 기존 템플릿 카테고리
$categories = getCategories();
// 기본필드 키 옵션
$basicFieldKeys = getBasicFieldKeys(); // common_codes 'doc_template_basic_field'
```
### 4.2 DocumentTemplateApiController (API)
#### `index()` — HTMX 테이블 조회
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 양식명/분류 검색 |
| `category` | string | 분류 필터 |
| `is_active` | string | `1` / `0` / `TRASHED` (휴지통) |
```php
// 휴지통 모드 (슈퍼어드민 전용)
if ($isActive === 'TRASHED') {
$query->onlyTrashed();
}
```
#### `store()` / `update()` — 생성/수정
```
요청 데이터
검증 (직접 validate, FormRequest 미사용)
연결품목 중복 검증 (checkLinkedItemDuplicates)
DB::transaction 시작
Template 생성/수정
saveRelations() — 관계 데이터 upsert
DB::transaction 완료
JSON 응답
```
#### `duplicate()` — 양식 복제
```php
$source = DocumentTemplate::with([...all relationships...]);
$newTemplate = DocumentTemplate::create([
...원본 데이터,
'name' => request('name', '원본 (복사)'),
'is_active' => false, // 비활성으로 생성
'linked_item_ids' => null, // 연결품목 제외
'linked_process_id' => null, // 연결공정 제외
]);
// 각 관계 데이터 복사 (approvalLines, basicFields, sections, columns...)
// linkValues는 복사 안 함 (동일 분류 내 중복 방지)
```
#### `forceDestroy()` — 영구삭제
```php
// 사전 검사: 참조하는 문서 존재 여부
$documentCount = Document::withTrashed()
->where('template_id', $id)
->count();
if ($documentCount > 0) {
return 422; // "이 양식을 사용한 문서 {count}건이 있어 삭제 불가"
}
```
#### `uploadImage()` — 이미지 업로드
```
요청 (multipart)
ApiTokenService::exchangeToken($userId, $tenantId)
API /files/upload 호출 (Bearer 토큰)
응답: file_path (1/temp/2026/02/xxx.jpg)
최종 URL: http://api.sam.kr/storage/tenants/{file_path}
```
---
## 5. 저장 메커니즘 (saveRelations)
### 5.1 upsert 전략
| 관계 | 방식 | 이유 |
|------|------|------|
| approvalLines | 전체 삭제 → 재생성 | ID 참조 없음 |
| basicFields | 전체 삭제 → 재생성 | ID 참조 없음 |
| **sections** | **ID 보존 upsert** | document_data가 section_id 참조 |
| **sectionItems** | **ID 보존 upsert** | section 하위 항목 |
| **columns** | **ID 보존 upsert** | document_data가 column_id 참조 |
| sectionFields | 전체 삭제 → 재생성 | ID 참조 없음 |
| links + linkValues | 전체 삭제 → 재생성 | ID 참조 없음 |
### 5.2 ID 보존 upsert 로직
```php
// 1. 요청 ID 수집
$incomingIds = collect($data['sections'])->pluck('id')->filter();
// 2. 요청에 없는 항목 삭제
$template->sections()
->whereNotIn('id', $incomingIds)
->each(function($s) {
$s->items()->delete();
$s->delete();
});
// 3. 각 항목 upsert
foreach ($data['sections'] as $section) {
if (!empty($section['id']) && $existing = $template->sections()->find($section['id'])) {
$existing->update($sectionData); // 기존: update
} else {
DocumentTemplateSection::create([...]); // 신규: create
}
}
```
> **ID 보존이 필수인 이유**: `document_data` 테이블이 `section_id`, `column_id`를 FK로 참조한다. 양식 수정 시 ID가 변경되면 기존 문서 데이터와의 매핑이 깨진다.
---
## 6. 화면 구성
### 6.1 목록 화면 (`index.blade.php`)
```
┌─────────────────────────────────────────────────┐
│ 문서양식관리 │
│ [+ 새 양식] [+ 양식 디자이너] │
├─────────────────────────────────────────────────┤
│ 필터: [검색어] [분류 ▼] [활성/비활성/휴지통 ▼] │
├─────────────────────────────────────────────────┤
│ # │ 양식명 │ 분류 │ 활성 │ 수정일 │ 액션 │
│ 1 │ FQC... │ 검사 │ ✅ │ 03-06 │ 편집 복제 삭제 │
│ 2 │ 수입... │ 검사 │ ✅ │ 03-05 │ 편집 복제 삭제 │
│ ...│ │ │ │ │ │
└─────────────────────────────────────────────────┘
```
**HTMX 테이블 로드:**
```html
<div id="template-table"
hx-get="/api/admin/document-templates"
hx-trigger="load, filterSubmit from:body"
hx-swap="innerHTML">
</div>
```
**액션 버튼:**
- **편집**: 새 양식 → `/document-templates/{id}/edit`, 양식 디자이너 → `/document-templates/{id}/block-edit`
- **복제**: `duplicateTemplate(id)` — 이름 입력 모달 후 POST
- **삭제**: `confirmDelete(id)` — 확인 후 DELETE
- **미리보기**: `previewTemplate(id)` — 모달 표시
- **활성 토글**: `toggleActive(id)` — POST toggle-active
- **복원/영구삭제**: 휴지통 모드에서만 표시 (슈퍼어드민)
### 6.2 Legacy Builder 편집 화면 (`edit.blade.php`)
**4개 탭 구조:**
```
┌─────────────────────────────────────────────────────┐
│ [기본정보] [기본필드] [검사 기준서] [테이블 컬럼] │
├─────────────────────────────────────────────────────┤
│ │
│ (각 탭 콘텐츠) │
│ │
├─────────────────────────────────────────────────────┤
│ [미리보기] [저장] [취소] │
└─────────────────────────────────────────────────────┘
```
#### 탭 1: 기본정보
| 필드 | 설명 |
|------|------|
| 양식명 | 서식 이름 (필수) |
| 제목 | 문서 제목 |
| 분류 | 카테고리 (common_codes + 기존값) |
| 회사명 | 문서 헤더 회사명 |
| 회사 주소/연락처 | 문서 헤더 |
| 활성 | 체크박스 |
| 결재선 | 동적 행 추가/삭제 (이름, 부서, 역할) |
#### 탭 2: 기본필드
| 항목 | 설명 |
|------|------|
| 필드 키 | `bf_` 접두사 (common_codes에서 선택) |
| 라벨 | 표시 라벨 |
| 필드 타입 | text, date, select 등 |
| 기본값 | 문서 생성 시 자동 입력 |
| 필수 여부 | 체크박스 |
#### 탭 3: 검사 기준서
```
┌──────────────────────────────────────────────────┐
│ 섹션 1: [제목 입력] [이미지 업로드] [+ 항목 추가] │
│ ┌──────────────────────────────────────────────┐ │
│ │ 카테고리 │ 항목 │ 기준 │ 공차 │ 기준값 │ ... │ │
│ │ 외관 │ 색상 │ 기준 │ ±0.5 │ 5.0 │ ... │ │
│ │ 외관 │ 흠집 │ 무 │ │ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 섹션 2: [제목 입력] [이미지 업로드] [+ 항목 추가] │
│ ... │
│ [+ 섹션 추가] │
└──────────────────────────────────────────────────┘
```
**공차 유형:**
| 유형 | 입력 | 표시 예 |
|------|------|--------|
| `symmetric` | ± 값 | ±0.5 |
| `asymmetric` | +값, -값 | +0.3 / -0.2 |
| `range` | 최소~최대 | 4.5 ~ 5.5 |
| `limit` | 상한 또는 하한 | ≤ 10 |
#### 탭 4: 테이블 컬럼
| 항목 | 설명 |
|------|------|
| 라벨 | 컬럼 헤더 |
| 그룹명 | 다단계 그룹 ("/" 구분) |
| 너비 | 컬럼 너비 (px 또는 %) |
| 컬럼 타입 | text, check, complex, select, measurement |
| 하위 라벨 | complex 타입 시 sub_labels |
**자동 컬럼 생성:**
```
[기준서에서 자동 생성] 버튼 클릭
검사 기준서 섹션의 항목들을 분석
카테고리 그룹별 컬럼 자동 생성
measurement_type에 따라 컬럼 타입 결정
```
### 6.3 양식 디자이너 편집 화면 (`block-editor.blade.php`)
**3패널 레이아웃:**
```
┌──────────┬──────────────────────────┬───────────┐
│ 팔레트 │ 캔버스 │ 속성 패널 │
│ (220px) │ (flex: 1) │ (300px) │
│ │ │ │
│ 기본: │ ┌──────────────────────┐ │ 선택 블록: │
│ □ 제목 │ │ [제목 블록] │ │ │
│ □ 문단 │ │ [문단 블록] │ │ 제목: ... │
│ □ 테이블 │ │ [테이블 블록] │ │ 크기: ... │
│ □ 컬럼 │ │ [입력 필드 블록] │ │ 정렬: ... │
│ □ 구분선 │ │ │ │ │
│ □ 여백 │ └──────────────────────┘ │ │
│ │ │ │
│ 폼: │ │ │
│ □ 텍스트 │ │ │
│ □ 숫자 │ │ │
│ □ 날짜 │ │ │
│ □ 선택 │ │ │
│ □ 체크 │ │ │
│ □ 텍스트영역│ │ │
│ □ 서명 │ │ │
└──────────┴──────────────────────────┴───────────┘
```
**블록 타입 (15개):**
| 분류 | 타입 | 설명 |
|------|------|------|
| 기본 | `heading` | 제목 (h1~h6) |
| 기본 | `paragraph` | 문단 텍스트 |
| 기본 | `table` | 테이블 (행/열 편집) |
| 기본 | `columns` | 다단 컬럼 레이아웃 |
| 기본 | `divider` | 구분선 |
| 기본 | `spacer` | 여백 |
| 폼 | `text_field` | 텍스트 입력 |
| 폼 | `number_field` | 숫자 입력 |
| 폼 | `date_field` | 날짜 입력 |
| 폼 | `select_field` | 선택 드롭다운 |
| 폼 | `checkbox_field` | 체크박스 |
| 폼 | `textarea_field` | 긴 텍스트 입력 |
| 폼 | `signature_field` | 서명 영역 |
**Alpine.js 상태 관리:**
```javascript
blockEditor(initialSchema, templateId) {
blocks: [], // 블록 배열
selectedBlockId: null, // 현재 선택 블록
history: [], // Undo/Redo 스택 (최대 50)
historyIndex: -1,
pageConfig: { // 페이지 설정
size: 'A4', // A4 / A3
orientation: 'portrait', // portrait / landscape
margins: { top, right, bottom, left }
},
templateName: '',
category: ''
}
```
**키보드 단축키:**
| 단축키 | 기능 |
|--------|------|
| `Ctrl+Z` / `Cmd+Z` | Undo |
| `Ctrl+Shift+Z` / `Cmd+Shift+Z` | Redo |
| `Ctrl+S` / `Cmd+S` | 저장 |
**SortableJS:**
- 캔버스 내 블록 드래그-앤-드롭 정렬
- 팔레트에서 캔버스로 블록 추가
---
## 7. 미리보기 시스템
### 7.1 Legacy Builder 미리보기
```javascript
buildDocumentPreviewHtml(data)
├── 결재란 테이블 (역할별 )
├── 기본필드 (2 15:35:15:35 비율)
├── 섹션별 이미지 (title + image 또는 placeholder)
├── 검사 데이터 테이블
├── 다단계 그룹 헤더 (group_name "/" 구분)
├── sub_labels (complex 컬럼)
├── 항목 (카테고리 그룹핑)
└── 측정치 (measurement_type별 렌더)
└── 비고/종합판정 섹션
```
### 7.2 양식 디자이너 미리보기
```javascript
buildBlockPreviewHtml(data)
├── 블록 타입별 HTML 렌더링
├── 필드 placeholder 표시
└── A4/A3 레이아웃 시뮬레이션
```
### 7.3 이미지 URL 처리
```javascript
_previewImageUrl(imagePath)
├── http(s):// 시작 → 그대로 사용
├── /^\d+\// 패턴 → API tenant storage URL 생성
http://api.sam.kr/storage/tenants/{imagePath}
└── 기타 MNG local storage (/storage/{imagePath})
```
---
## 8. 분류(Category) 관리
### 8.1 소스 (우선순위)
1. **common_codes** (code_group = `document_category`, is_active = true)
- tenant_id가 있는 것 우선 (테넌트 전용)
- tenant_id가 null인 것도 포함 (공통)
- code 기준 중복 제거 (테넌트 우선)
2. **기존 템플릿의 category** (common_codes에 없는 값)
- 기존 이름 그대로 추가
### 8.2 연동 공통코드 그룹
| 그룹 | 용도 |
|------|------|
| `document_category` | 문서 분류 |
| `doc_template_basic_field` | 기본필드 키 옵션 |
| `doc_inspection_method` | 검사방법 |
| `doc_measurement_type` | 측정유형 |
---
## 9. 프리셋 시스템
### 9.1 테이블
```
document_template_field_presets
├── name // 프리셋 이름
├── category // 대상 카테고리
├── description // 설명
└── field_definitions // array - 필드 정의 목록
[{ field_key, label, field_type, options, ... }]
```
### 9.2 동작
```
분류(Category) 변경
매칭 프리셋 검색
기존 section_fields가 비어있으면
"'{category}' 카테고리에 맞는 프리셋을 적용할까요?" 확인
승인 시 field_definitions 자동 적용
```
> **주의**: 초기 로드 시에는 제안하지 않음. 분류 변경 시에만 제안.
---
## 10. 연결품목 중복 검증
### 10.1 규칙
같은 category 내 서로 다른 템플릿이 동일한 items를 연결할 수 없다.
### 10.2 검증 로직
```php
checkLinkedItemDuplicates($templateId, $category, $itemIds)
// 1. 같은 category의 다른 템플릿 조회
$otherTemplates = DocumentTemplate::where('category', $category)
->where('id', '!=', $templateId)
->get();
// 2. 각 템플릿의 연결품목 수집
foreach ($otherTemplates as $other) {
// 레거시: linked_item_ids (JSON 배열)
// 신규: template_links → linkValues (source_table = 'items')
$existingItemIds = ...;
}
// 3. 교집합 검사
$duplicates = array_intersect($itemIds, $existingItemIds);
if (!empty($duplicates)) {
return 422; // 중복 항목 목록과 함께 오류 반환
}
```
---
## 11. JavaScript 상태 관리 (Legacy Builder)
### 11.1 templateState 객체
```javascript
const templateState = {
// 기본정보
id, name, category, title,
company_name, company_address, company_contact,
footer_remark_label, footer_judgement_label,
footer_judgement_options,
is_active,
// 관계 데이터
approval_lines: [], // 결재선
basic_fields: [], // 기본필드
sections: [], // 섹션 + items
columns: [], // 테이블 컬럼
section_fields: [], // 섹션 필드
template_links: [], // 연결 설정 + values
};
```
### 11.2 저장 흐름
```
사용자 입력 (Blade 폼)
templateState 객체 갱신
saveTemplate() 호출
fetch POST/PUT /api/admin/document-templates
DocumentTemplateApiController::store/update()
검증 → 중복 검사 → DB 트랜잭션 → saveRelations()
JSON 응답
showToast() 메시지
htmx.trigger('#template-table', 'filterSubmit') → 테이블 새로고침
```
---
## 12. 양식 디자이너(Block Builder) vs 새 양식(Legacy Builder) 비교
| 항목 | 양식 디자이너 | 새 양식 |
|------|:------------:|:-------------:|
| builder_type | `block` | `legacy` 또는 null |
| 편집 UI | WYSIWYG 캔버스 (Alpine.js) | 탭 폼 (순수 JavaScript) |
| 데이터 저장 | `schema` JSON 컬럼 | 관계 테이블 (7개) |
| Undo/Redo | 히스토리 스택 (최대 50) | 불가 |
| 블록 타입 | 15개 (기본 6 + 폼 7 + 기타 2) | N/A |
| 드래그-앤-드롭 | SortableJS | 불가 |
| 페이지 설정 | A4/A3, 여백, 방향 | 없음 |
| 복제 | 스키마 JSON 복사 | 각 관계 데이터 개별 복사 |
| 미리보기 함수 | `buildBlockPreviewHtml()` | `buildDocumentPreviewHtml()` |
| 적합 용도 | 자유 레이아웃 문서 | 정형화된 검사 성적서 |
---
## 13. 권한 및 보안
### 13.1 미들웨어
- **웹 라우트**: 일반 인증 (auth)
- **API 라우트**: HQ 관리자 미들웨어 (`admin` prefix)
### 13.2 슈퍼어드민 전용 기능
| 기능 | 엔드포인트 |
|------|-----------|
| 영구삭제 | `DELETE /{id}/force` |
| 복원 | `POST /{id}/restore` |
| 휴지통 조회 | `GET /?is_active=TRASHED` |
### 13.3 삭제 보호
- 소프트 삭제: `deleted_at` + `deleted_by` 기록
- 영구삭제 전 참조 문서 검사 (Document 테이블)
- 참조 문서가 있으면 영구삭제 불가 (422 응답)
---
## 14. API 프로젝트 연동
### 14.1 API 서비스
```php
// DocumentTemplateService (API)
list(array $params): LengthAwarePaginator
// 필터: is_active, category, search
show(int $id): DocumentTemplate
// 전체 관계 로드 (approvalLines, basicFields, sections, columns...)
```
### 14.2 API 엔드포인트
```
GET /v1/document-templates → index (목록)
GET /v1/document-templates/{id} → show (상세)
```
> API는 **읽기 전용**. 서식 생성/수정은 MNG에서만 수행.
---
## 15. 주요 파일 경로
| 기능 | 경로 |
|------|------|
| 웹 컨트롤러 | `mng/app/Http/Controllers/DocumentTemplateController.php` |
| API 컨트롤러 | `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` |
| 모델 (8개) | `mng/app/Models/DocumentTemplate*.php` |
| 뷰 - 목록 | `mng/resources/views/document-templates/index.blade.php` |
| 뷰 - Legacy 편집 | `mng/resources/views/document-templates/edit.blade.php` |
| 뷰 - 양식 디자이너 | `mng/resources/views/document-templates/block-editor.blade.php` |
| 뷰 - 테이블 | `mng/resources/views/document-templates/partials/table.blade.php` |
| 뷰 - 미리보기 | `mng/resources/views/document-templates/partials/preview-modal.blade.php` |
| API 서비스 | `api/app/Services/DocumentTemplateService.php` |
| API 모델 | `api/app/Models/Documents/DocumentTemplate*.php` |
---
## 관련 문서
- [README.md](README.md) — 문서관리 시스템 개요 (API 중심)
- [MNG 문서관리](mng-document-system.md) — 문서 생성/편집/결재 (서식을 사용하는 측)
- [DB 스키마 — 문서](../../system/database/documents.md)
---
**최종 업데이트**: 2026-03-06