# 결재 양식 기술 명세

> **작성일**: 2026-03-06
> **상태**: Phase 2 구현 완료
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md)

---

## 1. 개요

### 1.1 목적

SAM MNG 결재관리의 **기안함 양식** 기술 명세. 각 양식의 필드 구조, JSON Content 데이터 형식, UI 인터랙션, 특수 로직을 정의한다.

### 1.2 양식 목록

| 코드 | 양식명 | 분류 | Blade 파일 | 설명 |
|------|--------|------|------------|------|
| `BUSINESS_DRAFT` | 업무기안서 | 일반 | (body 편집기) | 일반 업무 보고·요청 |
| `leave` | 휴가신청 | 인사/근태 | `_leave-form.blade.php` | 연차, 휴가, 근태 신청 |
| `attendance_request` | 근태신청 | 인사/근태 | `_leave-form.blade.php` | 외근, 출장, 조퇴 등 |
| `reason_report` | 사유서 | 인사/근태 | `_leave-form.blade.php` | 지각, 결근 등 사유 소명 |
| `resignation` | 사직서 | 인사/근태 | `_resignation-form.blade.php` | 퇴직 서류 |
| `employment_cert` | 재직증명서 | 증명서 | `_certificate-form.blade.php` | 재직 증명 발급 (PDF) |
| `career_cert` | 경력증명서 | 증명서 | `_career-cert-form.blade.php` | 경력 증명 발급 (PDF) |
| `appointment_cert` | 위촉증명서 | 증명서 | `_appointment-cert-form.blade.php` | 위촉/임명 증명 발급 (PDF) |
| `pr_expense` | 지출품의서 | 품의 | `_purchase-request-form.blade.php` | 지출 전 사전 승인 |
| `pr_contract` | 계약체결품의서 | 품의 | `_purchase-request-form.blade.php` | 계약 체결 전 승인 |
| `pr_purchase` | 구매품의서 | 품의 | `_purchase-request-form.blade.php` | 물품 구매 전 승인 |
| `pr_trip` | 출장품의서 | 품의 | `_purchase-request-form.blade.php` | 출장 계획 승인 |
| `pr_settlement` | 비용정산품의서 | 품의 | `_purchase-request-form.blade.php` | 비용 정산 승인 |
| `expense` | 지출결의서 | 재무 | `_expense-form.blade.php` | 법인카드/송금/자동이체 지출 |

### 1.3 공통 구조

모든 양식은 동일한 패턴으로 동작한다:

```
양식 선택 (form_id)
    ↓
양식별 Blade 파셜 렌더링 (create.blade.php 내 조건부 display)
    ↓
사용자 입력 → Alpine.js / JavaScript 인터랙션
    ↓
getFormData() → JSON content 생성
    ↓
ApprovalService::createApproval() → Approval.content (JSON 컬럼) 저장
```

### 1.4 양식 선택 UI (2단계 분류 + 설명 카드)

양식 선택은 **2단계 드롭다운 + 설명 카드** 레이아웃으로 구성된다.

```
┌─────────────────────────────────────────────────────────────────────────┐
│ 양식 *                                                                 │
│ ┌──── 30% ────────┐ ┌─────────────── 70% ───────────────────────────┐  │
│ │ 📋 품의     ▼   │ │ ┌─────────────────────────────────────────┐  │  │
│ │                  │ │ │ 📋 지출품의서                            │  │  │
│ │ 지출품의서   ▼   │ │ │ 지출이 발생하기 전 사전 승인을 받는      │  │  │
│ │                  │ │ │ 문서입니다. 예산 범위 내에서 지출 항목과  │  │  │
│ │                  │ │ │ 금액을 기재하여 사전에 승락을 받습니다.   │  │  │
│ └──────────────────┘ │ └─────────────────────────────────────────┘  │  │
│                      └─────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
```

#### 1단계: 분류 선택 (`form_category`)

| 분류 | 아이콘 | 포함 양식 |
|------|--------|----------|
| 일반 | 📄 | 업무기안서 |
| 인사/근태 | 👤 | 휴가신청, 근태신청, 사유서, 사직서 |
| 증명서 | 📜 | 재직증명서, 경력증명서, 위촉증명서 |
| 품의 | 📋 | 지출품의서, 계약체결품의서, 구매품의서, 출장품의서, 비용정산품의서 |
| 재무 | 💰 | 지출결의서 |

#### 2단계: 양식 선택 (`form_id`)

- 1단계 분류 선택 시 해당 분류에 속하지 않는 양식은 `display:none` + `disabled`
- 분류 내 첫 번째 양식 자동 선택

#### 설명 카드 (`formDescriptions`)

- 양식 선택 시 우측에 해당 양식의 아이콘/제목/설명 텍스트 표시
- 14종 전체 양식에 대한 설명 정의 (create/edit 공통)
- 색상: 양식별 Tailwind 테마 색상 (`border-*-200 bg-*-50`)

#### 핵심 JavaScript 함수

| 함수 | 설명 |
|------|------|
| `buildCategoryOptions()` | 사용 가능한 카테고리만 `form_category` 옵션으로 생성 |
| `filterFormsByCategory(cat)` | 선택된 분류 외 양식 옵션 숨김/비활성화 |
| `selectCategoryByFormId(formId)` | formId로 카테고리 역산하여 자동 선택 |
| `updateFormDescription(formId)` | 설명 카드 DOM 업데이트 |

### 1.5 파일 구조

```
resources/views/approvals/
├── create.blade.php                         ← 기안 작성 (2단계 양식 선택 + 설명 카드 + 동적 폼)
├── edit.blade.php                           ← 기안 수정 (create와 동일한 2단계 선택 구조)
├── show.blade.php                           ← 상세 조회 (양식별 조회 컴포넌트)
└── partials/
    ├── _leave-form.blade.php                ← 휴가신청 폼
    ├── _expense-form.blade.php              ← 지출결의서 폼
    ├── _expense-show.blade.php              ← 지출결의서 조회
    ├── _purchase-request-form.blade.php     ← 품의서 5종 통합 폼 (Alpine.js)
    ├── _purchase-request-show.blade.php     ← 품의서 5종 통합 조회
    ├── _certificate-form.blade.php          ← 재직증명서 폼
    ├── _certificate-show.blade.php          ← 재직증명서 조회
    ├── _career-cert-form.blade.php          ← 경력증명서 폼
    ├── _career-cert-show.blade.php          ← 경력증명서 조회
    ├── _appointment-cert-form.blade.php     ← 위촉증명서 폼
    ├── _appointment-cert-show.blade.php     ← 위촉증명서 조회
    ├── _resignation-form.blade.php          ← 사직서 폼
    ├── _resignation-show.blade.php          ← 사직서 조회
    ├── _approval-stamp-table.blade.php      ← 결재 도장 테이블
    └── _approval-line-editor.blade.php      ← 결재선 편집기
```

---

## 2. 휴가신청 (leave)

### 2.1 폼 필드

| 필드 ID | 라벨 | 타입 | 필수 | 기본값 | 설명 |
|---------|------|------|------|--------|------|
| `leave-user-id` | 신청자 | select | 필수 | `auth()->id()` | 활성 사원 목록 |
| `leave-type` | 유형 | select | 필수 | - | 휴가/근태신청/사유서 |
| `leave-start-date` | 시작일 | date | 필수 | - | `YYYY-MM-DD` |
| `leave-end-date` | 종료일 | date | 필수 | - | `YYYY-MM-DD` |
| `leave-reason` | 사유 | textarea | 선택 | - | 자유 텍스트 |

### 2.2 Content JSON

```json
{
    "user_id": "10",
    "leave_type": "연차",
    "start_date": "2026-03-06",
    "end_date": "2026-03-07",
    "reason": "개인 사유"
}
```

### 2.3 특수 로직

- **자동 선택**: 로그인 사용자가 기본 선택 (`auth()->id()`)
- **직원 목록**: `$employees` Props로 전달 (활성 사원만)
- **단순 구조**: Alpine.js 없이 Blade 폼으로 구현

---

## 3. 지출결의서 (expense)

### 3.1 폼 구조 (Alpine.js 기반)

```javascript
x-data="expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData)"
```

### 3.2 기본 정보 필드

| 필드 | 라벨 | 타입 | 필수 | 기본값 |
|------|------|------|------|--------|
| `expense_type` | 지출형식 | radio | 필수 | `corporate_card` |
| `tax_invoice` | 세금계산서 | radio | 필수 | `normal` |
| `write_date` | 작성일자 | date | 선택 | 오늘 |
| `approval_date` | 결재일자 | date | 선택 | 오늘 |
| `department` | 부서 | text | 선택 | `경리부` |
| `writer_name` | 이름 | text | 선택 | 인증 사용자명 |

### 3.3 지출형식별 선택

| 지출형식 | 코드 | 연결 데이터 |
|---------|------|------------|
| 법인카드 | `corporate_card` | `$cards` → `selected_card` |
| 송금 | `transfer` | `$accounts` → `selected_account` |
| 자동이체 출금 | `auto_transfer` | `$accounts` → `selected_account` |
| 현금/가지급정산 | `cash_advance` | 없음 |

**법인카드 선택 시 저장 구조:**

```json
{
    "selected_card": {
        "id": 1,
        "card_name": "삼성카드",
        "card_company": "삼성",
        "card_number_last4": "1234",
        "card_holder_name": "홍길동"
    }
}
```

**계좌 선택 시 저장 구조:**

```json
{
    "selected_account": {
        "id": 1,
        "bank_name": "국민은행",
        "account_number": "123-456-789012",
        "account_holder": "주일기업"
    }
}
```

### 3.4 세금계산서 옵션

| 옵션 | 코드 |
|------|------|
| 일반 | `normal` |
| 이월발행 | `deferred` |
| 없음 | `none` |

### 3.5 내역 테이블

**동적 rows** (`.items` 배열):

| 필드 | 라벨 | 타입 | 설명 |
|------|------|------|------|
| `date` | 일자 | date | `YYYY-MM-DD` |
| `description` | 적요 | text | 지출 설명 |
| `amount` | 금액 | number | 콤마 제거 정수 |
| `vendor` | 거래처 | text | Autocomplete 검색 |
| `vendor_id` | 거래처 ID | hidden | API 연결 ID |
| `vendor_biz_no` | 사업자번호 | hidden | 자동 채움 |
| `bank` | 은행명 | text | 수동 입력 |
| `account_no` | 계좌번호 | text | 수동 입력 |
| `depositor` | 예금주 | text | 수동 입력 |
| `remark` | 비고 | text | 메모 |

### 3.6 Content JSON (전체)

```json
{
    "expense_type": "corporate_card",
    "tax_invoice": "normal",
    "write_date": "2026-03-06",
    "approval_date": "2026-03-06",
    "department": "경리부",
    "writer_name": "홍길동",
    "items": [
        {
            "date": "2026-03-05",
            "description": "사무용품 구매",
            "amount": 150000,
            "vendor": "오피스디포",
            "vendor_id": 123,
            "vendor_biz_no": "123-45-67890",
            "bank": "",
            "account_no": "",
            "depositor": "",
            "remark": ""
        }
    ],
    "total_amount": 150000,
    "attachment_memo": "영수증 첨부",
    "selected_card": { ... },
    "selected_account": null
}
```

### 3.7 특수 기능

#### 거래처 검색 (Autocomplete)

```
입력 → 250ms 디바운싱 → API 호출 → 드롭다운 렌더링

API: /barobill/tax-invoice/search-partners?keyword=...
키보드: ↑↓(네비게이션), Enter(선택), Esc(닫기)
마우스: 항목 클릭(선택)
```

#### 금액 입력 포맷팅

```
입력 시: 콤마 제거 → 정수 저장 (parseMoney)
표시 시: 콤마 포맷 (formatMoney)
합계: totalAmount getter → footer 실시간 업데이트
```

#### 파일 업로드

```
드래그 앤 드롭 + 파일 입력
최대: 20MB
형식: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, jpeg, png, gif, zip, rar
API: POST /api/admin/approvals/upload-file
진행률: XHR 업로드 이벤트
```

#### 카드/계좌 연동

```
카드 선택 → 모든 내역 행에 "결제카드" 자동 표시
계좌 선택 → 모든 내역 행에 "은행/계좌/예금주" 자동 채움
```

### 3.8 조회 화면 (_expense-show.blade.php)

| 섹션 | 내용 |
|------|------|
| 기본 정보 | 지출형식, 세금계산서, 작성일, 결재일, 부서, 이름 |
| 선택 카드/계좌 | 유색 박스로 표시 |
| 내역 테이블 | 읽기 전용, `number_format()` 금액 |
| 첨부서류 메모 | `whitespace-pre-wrap` |
| 첨부파일 목록 | 다운로드 링크 + 파일 크기 |

---

## 4. 증명서 양식 공통

### 4.1 공통 패턴

모든 증명서 양식은 동일한 패턴을 따른다:

```
사원 선택 → loadXxxInfo(userId) → API 호출 → 읽기 전용 필드 자동 채움
                                                     ↓
                                              일부 필드만 수정 가능
                                                     ↓
                                              미리보기 모달 (인쇄 가능)
```

### 4.2 공통 함수

| 함수 | 설명 |
|------|------|
| `loadXxxInfo(userId)` | 사원 선택 시 인적/재직 정보 로드 |
| `openXxxPreview()` | 미리보기 모달 열기 |
| `printXxxPreview()` | 미리보기 인쇄 (`window.print()`) |
| `closeXxxPreview()` | 미리보기 닫기 |
| `onXxxPurposeChange()` | 용도 선택 시 직접입력 필드 표시 |

### 4.3 조회 화면 공통

- 읽기 전용 필드 표시
- PDF 다운로드: `route('api.admin.approvals.cert-pdf', $approval->id)`

---

## 5. 재직증명서 (employment_cert)

### 5.1 폼 필드

| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `cert-name` | 성명 | text | readonly | DB 자동 채움 |
| | `cert-resident` | 주민등록번호 | text | readonly | DB 자동 채움 |
| | `cert-address` | 주소 | text | editable | 직접 입력 |
| 재직사항 | `cert-company` | 회사명 | text | readonly | DB 자동 채움 |
| | `cert-business-num` | 사업자번호 | text | readonly | DB 자동 채움 |
| | `cert-department` | 근무부서 | text | readonly | DB 자동 채움 |
| | `cert-position` | 직급 | text | readonly | DB 자동 채움 |
| | `cert-hire-date` | 재직기간 | text | readonly | DB 자동 채움 |
| 발급정보 | `cert-purpose-select` | 사용용도 | select | editable | 드롭다운 선택 |
| | (custom) | 기타 용도 | text | editable | "기타" 선택 시 표시 |
| | `cert-issue-date` | 발급일 | text | readonly | `now()->format('Y-m-d')` |

### 5.2 Content JSON

```json
{
    "name": "홍길동",
    "resident_number": "900101-1XXXXXX",
    "address": "서울특별시 강남구",
    "company_name": "(주)코드브릿지엑스",
    "business_num": "123-45-67890",
    "department": "개발팀",
    "position": "과장",
    "hire_date": "2020-03-01",
    "purpose": "은행 제출용",
    "issue_date": "2026-03-06"
}
```

---

## 6. 경력증명서 (career_cert)

### 6.1 폼 필드 (재직증명서 대비 추가/변경)

| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `cc-birth-date` | 생년월일 | text | readonly | DB 자동 채움 |
| 경력사항 | `cc-ceo-name` | 대표자 | text | readonly | DB 자동 채움 |
| | `cc-phone` | 대표전화 | text | readonly | DB 자동 채움 |
| | `cc-company-address` | 소재지 | text | readonly | DB 자동 채움 |
| | `cc-department` | 소속부서 | text | readonly | DB 자동 채움 |
| | `cc-position` | 직위/직급 | text | readonly | DB 자동 채움 |
| | `cc-hire-date` | 근무기간 시작 | text | readonly | DB 자동 채움 |
| | `cc-resign-date` | 근무기간 종료 | date | editable | 직접 입력 |
| | `cc-job-description` | 담당업무 | text | editable | 직접 입력 |
| 발급정보 | 용도 | select | editable | + "이직 제출용" 옵션 |

### 6.2 Content JSON

```json
{
    "name": "홍길동",
    "birth_date": "1990-01-01",
    "address": "서울특별시 강남구",
    "company_name": "(주)코드브릿지엑스",
    "business_num": "123-45-67890",
    "ceo_name": "김대표",
    "phone": "02-1234-5678",
    "company_address": "서울특별시 강남구 테헤란로",
    "department": "개발팀",
    "position": "과장",
    "hire_date": "2020-03-01",
    "resign_date": "2026-02-28",
    "job_description": "웹 애플리케이션 개발",
    "purpose": "이직 제출용",
    "issue_date": "2026-03-06"
}
```

---

## 7. 위촉증명서 (appointment_cert)

### 7.1 폼 필드

| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `ac-name` | 성명 | text | readonly | DB 자동 채움 |
| | `ac-resident` | 주민등록번호 | text | readonly | DB 자동 채움 |
| | `ac-department` | 소속 | text | readonly | DB 자동 채움 |
| | `ac-phone` | 연락처 | text | editable | 직접 입력 |
| 위촉정보 | `ac-hire-date` | 위촉기간 시작 | text | readonly | DB 자동 채움 |
| | `ac-resign-date` | 위촉기간 종료 | date | editable | 직접 입력 |
| | `ac-contract-type` | 계약자격 | text | editable | 직접 입력 |
| 발급정보 | `ac-purpose-select` | 용도 | select | editable | 드롭다운 선택 |
| | `ac-issue-date` | 발급일 | text | readonly | 자동 설정 |
| (숨김) | `ac-company-name` | 회사명 | hidden | - | 미리보기용 |
| | `ac-ceo-name` | 대표자명 | hidden | - | 미리보기용 |

### 7.2 Content JSON

```json
{
    "name": "홍길동",
    "resident_number": "900101-1XXXXXX",
    "department": "기술자문팀",
    "phone": "010-1234-5678",
    "hire_date": "2024-01-01",
    "resign_date": "2026-12-31",
    "contract_type": "기술자문위원",
    "purpose": "관공서 제출용",
    "issue_date": "2026-03-06"
}
```

---

## 8. 사직서 (resignation)

### 8.1 폼 필드

| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 필수 |
|------|---------|------|------|------|------|
| 인적사항 | `rg-department` | 소속 | text | readonly | - |
| | `rg-position` | 직위 | text | readonly | - |
| | `rg-name` | 성명 | text | readonly | - |
| | `rg-resident` | 주민등록번호 | text | readonly | - |
| | `rg-hire-date` | 입사일 | text | readonly | - |
| | `rg-resign-date` | 퇴사(예정)일 | date | editable | 필수 |
| | `rg-address` | 주소 | text | editable | - |
| 사직사유 | `rg-reason-select` | 사유 | select | editable | 필수 |
| | (custom) | 기타 사유 | text | editable | - |
| 제출일 | `rg-issue-date` | 제출일 | text | readonly | - |

### 8.2 사직사유 옵션

| 옵션 |
|------|
| 일신상의 사유 |
| 가사 사정 |
| 건강상의 이유 |
| 진학/학업 |
| 이직 |
| 기타 (직접입력) |

### 8.3 Content JSON

```json
{
    "department": "개발팀",
    "position": "대리",
    "name": "홍길동",
    "resident_number": "900101-1XXXXXX",
    "hire_date": "2020-03-01",
    "resign_date": "2026-04-01",
    "address": "서울특별시 강남구",
    "reason": "이직",
    "issue_date": "2026-03-06"
}
```

---

## 9. 품의서 5종 공통 (_purchase-request-form/show)

### 9.1 통합 Alpine.js 컴포넌트

품의서 5종은 **단일 Blade 파일**(`_purchase-request-form.blade.php`)에서 `prType` 프로퍼티로 동적 전환된다.

```javascript
x-data="purchaseRequestForm(initialData, authUserName, initialFiles)"
```

#### 타입 전환 메커니즘

```
create.blade.php → switchFormMode()
    ↓
code.startsWith('pr_') 감지
    ↓
#purchase-request-form-container display: block
    ↓
setTimeout(50ms) → _x_dataStack[0].setPrType(code)
    ↓
Alpine.js x-if 분기 → 해당 폼 렌더링
```

#### prType 코드 및 라벨

| prType | 라벨 | 색상 |
|--------|------|------|
| `pr_expense` | 지출품의서 | `bg-orange-50 text-orange-700` |
| `pr_contract` | 계약체결품의서 | `bg-purple-50 text-purple-700` |
| `pr_purchase` | 구매품의서 | `bg-blue-50 text-blue-700` |
| `pr_trip` | 출장품의서 | `bg-green-50 text-green-700` |
| `pr_settlement` | 비용정산품의서 | `bg-teal-50 text-teal-700` |

### 9.2 공통 필드 (모든 품의서)

| 필드 | 라벨 | 타입 | 기본값 |
|------|------|------|--------|
| `write_date` | 작성일자 | date | 오늘 |
| `department` | 요청부서 | text | - |
| `writer_name` | 요청자 | text | 인증 사용자명 |
| `attachment_memo` | 첨부서류 메모 | textarea | - |
| `files` | 파일 업로드 | file[] | - |

### 9.3 공통 함수

| 함수 | 설명 |
|------|------|
| `setPrType(type)` | 외부에서 prType 설정 (switchFormMode에서 호출) |
| `getFormData()` | prType별 다른 JSON 구조 반환 (base에 `pr_type` 포함) |
| `addItem()` | 내역 행 추가 |
| `removeItem(index)` | 내역 행 삭제 |
| `formatMoney(val)` | 숫자 → 콤마 포맷 |
| `parseMoney(str)` | 콤마 문자열 → 정수 |
| `prVendorSearch(target, fieldName)` | 범용 거래처 Autocomplete 검색 |

### 9.4 조회 화면 분기 (show.blade.php)

```php
// show.blade.php에서 pr_ prefix로 분기
@if(str_starts_with($approval->form?->code ?? '', 'pr_'))
    @include('approvals.partials._purchase-request-show', ['content' => $content])
@endif
```

`_purchase-request-show.blade.php`에서 `$content['pr_type']`으로 5종 분기 렌더링.

---

## 10. 지출품의서 (pr_expense)

### 10.1 추가 필드

| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `expense_category` | 지출항목 | text | 선택 |
| `usage_date` | 사용일자 | date | 선택 |
| `purpose` | 사용목적 | textarea | 필수 |

### 10.2 내역 테이블

| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `description` | 항목 | text |
| `amount` | 금액 | number (콤마 포맷) |
| `remark` | 비고 | text |

### 10.3 Content JSON

```json
{
    "pr_type": "pr_expense",
    "write_date": "2026-03-06",
    "department": "개발팀",
    "writer_name": "홍길동",
    "expense_category": "사무용품",
    "usage_date": "2026-03-05",
    "purpose": "업무용 모니터 구매",
    "items": [
        { "description": "27인치 모니터", "amount": 350000, "remark": "LG전자" }
    ],
    "total_amount": 350000,
    "attachment_memo": "견적서 첨부"
}
```

---

## 11. 계약체결품의서 (pr_contract)

### 11.1 추가 필드

| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `contract_party` | 계약상대방 | text + Autocomplete | 필수 |
| `contract_party_biz_no` | 사업자번호 | text (자동) | - |
| `contract_content` | 계약내용 | textarea | 필수 |
| `contract_period_start` | 계약기간 시작 | date | 선택 |
| `contract_period_end` | 계약기간 종료 | date | 선택 |
| `contract_amount` | 계약금액 | number (콤마) | 필수 |
| `contract_conditions` | 주요조건 | textarea | 선택 |

### 11.2 Content JSON

```json
{
    "pr_type": "pr_contract",
    "write_date": "2026-03-06",
    "department": "경영지원팀",
    "writer_name": "홍길동",
    "contract_party": "(주)에이비씨",
    "contract_party_biz_no": "123-45-67890",
    "contract_content": "연간 IT 유지보수 계약",
    "contract_period_start": "2026-04-01",
    "contract_period_end": "2027-03-31",
    "contract_amount": 12000000,
    "contract_conditions": "월 1회 정기점검, 장애 발생 시 4시간 내 대응",
    "attachment_memo": "계약서 초안 첨부"
}
```

### 11.3 특수 로직

- **거래처 검색**: `prVendorSearch(formData, 'contract_party')` — 계약상대방 필드에 Autocomplete 적용
- 선택 시 `contract_party_biz_no` 자동 채움

---

## 12. 구매품의서 (pr_purchase)

### 12.1 추가 필드

| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `vendor` | 납품업체 | text + Autocomplete | 선택 |
| `vendor_biz_no` | 사업자번호 | text (자동) | - |
| `delivery_date` | 납품예정일 | date | 선택 |
| `delivery_location` | 납품장소 | text | 선택 |

### 12.2 내역 테이블

| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `name` | 품목 | text |
| `spec` | 규격 | text |
| `quantity` | 수량 | number |
| `unit_price` | 단가 | number (콤마) |
| `amount` | 금액 | number (자동: 수량×단가) |
| `remark` | 비고 | text |

### 12.3 Content JSON

```json
{
    "pr_type": "pr_purchase",
    "write_date": "2026-03-06",
    "department": "생산팀",
    "writer_name": "홍길동",
    "vendor": "(주)공급사",
    "vendor_biz_no": "987-65-43210",
    "delivery_date": "2026-03-20",
    "delivery_location": "본사 1층 창고",
    "items": [
        { "name": "A4용지", "spec": "80g 500매", "quantity": 10, "unit_price": 25000, "amount": 250000, "remark": "" }
    ],
    "total_amount": 250000,
    "attachment_memo": ""
}
```

### 12.4 특수 로직

- **금액 자동 계산**: `quantity × unit_price → amount` (x-effect 반응)
- **거래처 검색**: `prVendorSearch(formData, 'vendor')` — 납품업체 필드에 Autocomplete 적용

---

## 13. 출장품의서 (pr_trip)

### 13.1 추가 필드

| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `destination` | 출장지 | text | 필수 |
| `trip_period_start` | 출장기간 시작 | date | 필수 |
| `trip_period_end` | 출장기간 종료 | date | 필수 |
| `trip_purpose` | 출장목적 | textarea | 필수 |

### 13.2 일정표 (items)

| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `date` | 일자 | date |
| `schedule` | 일정 | text |
| `remark` | 비고 | text |

### 13.3 경비 내역 (expenses)

| 필드 | 라벨 | 타입 |
|------|------|------|
| `transport` | 교통비 | number (콤마) |
| `accommodation` | 숙박비 | number (콤마) |
| `meals` | 식비 | number (콤마) |
| `others` | 기타 | number (콤마) |
| (자동) | 합계 | number (합산) |

### 13.4 Content JSON

```json
{
    "pr_type": "pr_trip",
    "write_date": "2026-03-06",
    "department": "영업팀",
    "writer_name": "홍길동",
    "destination": "부산 해운대",
    "trip_period_start": "2026-03-10",
    "trip_period_end": "2026-03-11",
    "trip_purpose": "거래처 방문 및 현장 점검",
    "items": [
        { "date": "2026-03-10", "schedule": "거래처 미팅", "remark": "오전 10시" },
        { "date": "2026-03-11", "schedule": "현장 점검 및 복귀", "remark": "" }
    ],
    "expenses": {
        "transport": 120000,
        "accommodation": 80000,
        "meals": 40000,
        "others": 0
    },
    "total_amount": 240000,
    "attachment_memo": ""
}
```

### 13.5 조회 화면 특수 구조

- **일정표**: 테이블 형태로 일자/일정/비고 렌더링
- **경비 카드**: 교통비/숙박비/식비/기타 4개 항목 + 합계를 카드 그리드로 표시

---

## 14. 비용정산품의서 (pr_settlement)

### 14.1 추가 필드

| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `settlement_period_start` | 정산기간 시작 | date | 선택 |
| `settlement_period_end` | 정산기간 종료 | date | 선택 |
| `payment_method` | 지급방법 | radio | 필수 |

### 14.2 지급방법 옵션

| 값 | 라벨 |
|----|------|
| `corporate_card` | 법인카드 사용 |
| `personal_advance` | 개인 선지출 (환급 요청) |

### 14.3 내역 테이블

| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `date` | 사용일자 | date |
| `description` | 항목 | text |
| `amount` | 금액 | number (콤마) |
| `remark` | 비고 | text |

### 14.4 Content JSON

```json
{
    "pr_type": "pr_settlement",
    "write_date": "2026-03-06",
    "department": "개발팀",
    "writer_name": "홍길동",
    "settlement_period_start": "2026-02-01",
    "settlement_period_end": "2026-02-28",
    "payment_method": "personal_advance",
    "items": [
        { "date": "2026-02-15", "description": "택시비", "amount": 25000, "remark": "야근 귀가" },
        { "date": "2026-02-20", "description": "회의 다과", "amount": 15000, "remark": "팀 미팅" }
    ],
    "total_amount": 40000,
    "attachment_memo": "영수증 첨부"
}
```

### 14.5 조회 화면 특수 구조

- **지급방법 표시**: `corporate_card` → "법인카드 사용", `personal_advance` → "개인 선지출 (환급 요청)"
- 해당 라벨을 뱃지 형태로 표시

---

## 15. 결재 도장 테이블 (_approval-stamp-table.blade.php)

### 15.1 구조

전통 한글 결재 양식의 도장 테이블을 구현한다.

```
┌──────┬────────┬────────┬────────┐
│      │ 과장   │ 부장   │ 이사   │  ← 1행: 직급 헤더
│ 결재 ├────────┼────────┼────────┤
│      │ [승인] │ [대기] │ [대기] │  ← 2행: 서명/도장 영역
│      ├────────┼────────┼────────┤
│      │ 김과장 │ 박부장 │ 이이사 │  ← 3행: 이름 + 처리일
│      │ 03/06  │        │        │
└──────┴────────┴────────┴────────┘
```

### 15.2 상태별 표시

| 상태 | approval_type | 표시 | 색상 |
|------|---------------|------|------|
| 승인 | `normal` | 빨간 원형 "승인" | `bg-red-500` |
| 전결 | `pre_decided` | 파란 원형 "전결" | `bg-blue-500` |
| 반려 | - | 빨간 원형 "반려" | `bg-red-500` |
| 보류 | - | 주황 원형 "보류" | `bg-amber-500` |
| 건너뜀 | - | 회색 "-" | `bg-gray-300` |

---

## 16. 결재선 편집기 (_approval-line-editor.blade.php)

### 16.1 2패널 구조

```
┌─────────────────────┬─────────────────────┐
│ 인원 목록            │ 결재선               │
│                     │                     │
│ [검색 input]         │ [템플릿 선택 ▼]      │
│                     │                     │
│ ▼ 개발팀             │ ① 김과장 (결재) [✗]  │
│   홍길동 과장 [+]    │ ② 박부장 (합의) [✗]  │
│   김영희 대리 [+]    │ ③ 이대리 (참조) [✗]  │
│                     │                     │
│ ▼ 경영지원팀          │ (드래그로 순서 변경)  │
│   박부장 부장 [+]    │                     │
│                     │                     │
├─────────────────────┴─────────────────────┤
│ 결재: 1명  합의: 1명  참조: 1명  합계: 3명   │
└───────────────────────────────────────────┘
```

### 16.2 기능

| 기능 | 설명 |
|------|------|
| **인원 검색** | 이름/부서 실시간 검색 |
| **부서별 접기** | 부서 헤더 클릭으로 인원 접기/펼치기 |
| **드래그 정렬** | SortableJS로 결재선 순서 변경 |
| **유형 선택** | 각 단계별 approval/agreement/reference 선택 |
| **템플릿 로드** | 저장된 결재선 템플릿 드롭다운 |

### 16.3 데이터 소스

```
API: /api/admin/tenant-users/list

응답:
[
    {
        "department_id": 1,
        "department_name": "개발팀",
        "users": [
            { "id": 10, "name": "홍길동", "position": "과장", "job_title": "팀장" }
        ]
    }
]
```

### 16.4 Hidden Inputs (form 전송)

```html
<input type="hidden" name="steps[0][user_id]" value="10">
<input type="hidden" name="steps[0][step_type]" value="approval">
<input type="hidden" name="steps[1][user_id]" value="20">
<input type="hidden" name="steps[1][step_type]" value="agreement">
```

---

## 17. ApprovalForm 모델

### 17.1 테이블 스키마

| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `name` | VARCHAR | 양식명 (예: "휴가신청서") |
| `code` | VARCHAR UNIQUE | 양식 코드 (예: `leave`) |
| `category` | ENUM | `request`, `expense`, `certificate`, `expense_estimate` |
| `template` | JSON | 필드 정의 메타데이터 |
| `body_template` | LONGTEXT NULL | HTML 본문 템플릿 |
| `is_active` | BOOLEAN | 활성 여부 |

### 17.2 카테고리

#### DB 카테고리 (ApprovalForm.category)

| 카테고리 | 설명 | 양식 코드 |
|---------|------|----------|
| `request` | 신청서 | `leave`, `attendance_request`, `reason_report` |
| `expense` | 지출결의서 | `expense` |
| `certificate` | 증명서/서류 | `employment_cert`, `career_cert`, `appointment_cert`, `resignation` |
| `expense_estimate` | 품의서 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` |

#### UI 분류 (formCategoryMap — 2단계 선택용)

| UI 분류 | 양식 코드 |
|---------|----------|
| 일반 | `BUSINESS_DRAFT` |
| 인사/근태 | `leave`, `attendance_request`, `reason_report`, `resignation` |
| 증명서 | `employment_cert`, `career_cert`, `appointment_cert` |
| 품의 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` |
| 재무 | `expense` |

> **참고**: DB 카테고리와 UI 분류는 별도 매핑이다. DB는 `approval_forms.category` ENUM이고, UI 분류는 JavaScript `formCategoryMap` 객체로 정의된다.

---

## 18. 양식별 저장/조회 흐름

### 18.1 저장 (create/update)

```
사용자 입력
    ↓
getFormData() (JavaScript)
    ↓
POST /api/admin/approvals
    body: { form_id, title, content: {...}, body, steps: [...] }
    ↓
ApprovalService::createApproval()
    ↓
Approval.content = JSON encode → DB 저장
```

### 18.2 조회 (show)

```
GET /approval-mgmt/{id}
    ↓
ApprovalController::show()
    ↓
Blade: show.blade.php
    ↓
양식 코드별 분기:
    leave → (본문에 인라인 표시)
    expense → @include('_expense-show')
    pr_* → @include('_purchase-request-show')  ← str_starts_with 매칭
    employment_cert → @include('_certificate-show')
    career_cert → @include('_career-cert-show')
    appointment_cert → @include('_appointment-cert-show')
    resignation → @include('_resignation-show')
```

> **품의서 분기**: `str_starts_with($approval->form?->code ?? '', 'pr_')` 조건으로 5종 모두 단일 include로 처리. `_purchase-request-show.blade.php` 내부에서 `$content['pr_type']`으로 세부 분기.

---

## 관련 문서

- [README.md](README.md) — 결재관리 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름
- [API 명세](api-reference.md) — 엔드포인트별 요청/응답
- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작

---

**최종 업데이트**: 2026-03-06
