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

- sam/docs/ 하위 62개 신규 파일을 루트로 이동 (contracts, features, guides, plans 등)
- sam/docs/ 하위 52개 변경 파일을 루트에 덮어쓰기 (brochure, rules 등)
- sam/ 폴더 전체 삭제 (docker, coocon 포함)
This commit is contained in:
2026-03-09 22:33:19 +09:00
parent bfcd6178ea
commit cc38b00c11
223 changed files with 178 additions and 32907 deletions

View File

@@ -1,369 +0,0 @@
# 방화셔터 백과사전 이미지 생성 프롬프트
> **작성일**: 2026-02-22
> **상태**: 확정
> **용도**: Google Gemini (Nano Banana Pro) 이미지 생성용
---
## 1. 개요
### 1.1 목적
MNG 아카데미 > 방화셔터 백과사전 페이지에 삽입할 기술 일러스트레이션을 AI 이미지 생성 도구(Google Gemini)로 제작하기 위한 프롬프트 모음이다.
### 1.2 사용 방법
1. Google Gemini (Nano Banana Pro 모델)에서 프롬프트를 입력한다
2. 생성된 이미지를 `mng/public/images/academy/fire-shutter/` 경로에 저장한다
3. Blade 뷰에서 `<img>` 태그로 참조한다
### 1.3 주의사항
- **화면 내 모든 라벨은 영어**로 작성되어 있다 (한글 텍스트는 AI 이미지 생성 시 깨짐 현상 발생)
- 전체 구성도, 설치 장면 등 넓은 이미지는 **16:9** 비율 권장
- 단면도, 부품 상세 등은 **1:1** 또는 **4:3** 비율 권장
- 생성 실패 시 프롬프트 앞에 `Detailed technical engineering illustration, clean white background, ` 를 추가한다
---
## 2. 프롬프트 목록
### 2.1 방화셔터 전체 구성도 (Full Component Diagram)
```
Technical illustration of a fire shutter (automatic fire-rated rolling shutter) installed in a building opening, cutaway side view showing all components with English labels.
Show these parts clearly labeled:
- Top: "CEILING SLAB" with "HEAD BOX / CASE" mounted below
- Inside head box: "SHAFT" with coiled steel slats, "BALANCE SPRING", "GEAR BOX", "MOTOR", "ELECTROMAGNETIC BRAKE", "BRACKET" on both sides
- Both sides: vertical "GUIDE RAIL" mounted on fireproof walls with "ANCHOR BOLTS"
- Center: multiple horizontal "STEEL SLATS" hanging down in interlocking pattern
- Bottom: "BOTTOM BAR" touching the floor with rubber seal
- Nearby wall: "MANUAL CONTROL BOX" with UP/STOP/DOWN buttons
- Ceiling: "SMOKE DETECTOR" and "HEAT DETECTOR"
- Wall-mounted: "FIRE SHUTTER CONTROLLER"
Style: Clean technical cutaway diagram, white background, professional engineering illustration, labeled with arrows pointing to each component. Color-coded: structural parts in gray/silver, electrical parts in blue, safety parts in red. Isometric or 3/4 perspective view.
```
---
### 2.2 슬랫 인터록킹 구조 (Slat Interlocking)
```
Technical cross-section illustration showing how fire shutter steel slats interlock with each other.
Show 3-4 slats connected in interlocking pattern:
- Each slat is a C-shaped or S-shaped profile made from 1.6mm EGI steel
- The curved edges of adjacent slats hook into each other, allowing flexibility while maintaining a continuous curtain surface
- One slat highlighted with dimension labels: "THICKNESS 1.6mm", "PITCH 75-100mm"
- Show the slight curved profile that allows the slat to wrap around the shaft when rolled up
- Arrow labeled "ROLLING DIRECTION"
Label each part: "SLAT", "INTERLOCKING JOINT", "EGI STEEL 1.6mm"
Style: Clean engineering cross-section diagram, white background, metallic silver color for steel. Include dimension lines. Zoomed-in detail view with magnified interlocking joint area in a callout circle.
```
---
### 2.3 가이드레일 단면도 (Guide Rail Cross-Section)
```
Technical cross-section illustration of a fire shutter guide rail mounted on a fireproof wall, viewed from top-down.
Show the C-channel shaped guide rail:
- C-channel profile, steel thickness 2.3mm+
- Inside the channel: slat edge sitting in the groove
- Smoke seal material strips on both sides of the channel, pressing against the slat
- Anchor bolts securing the guide rail to the concrete wall
- Wall shown as hatched concrete pattern
Labels with arrows:
- "GUIDE RAIL BODY (C-CHANNEL)"
- "SLAT EDGE"
- "SMOKE SEAL PACKING"
- "ANCHOR BOLT"
- "FIREPROOF WALL"
- "STEEL 2.3mm+"
Style: Clean technical cross-section, white background, steel parts in metallic gray, seal material in orange/red, wall in light brown hatched pattern. Include dimension annotations.
```
---
### 2.4 샤프트 어셈블리 (Shaft Assembly)
```
Technical illustration showing the inside of a fire shutter head box, exploded or cutaway view.
Show these components assembled on or around the shaft:
- Central pipe labeled "SHAFT" with slats attached, partially wound
- Left side: "BRACKET" steel plate bolted to wall, with "BEARING" supporting shaft end
- Right side: "GEAR BOX" and "MOTOR" mounted on bracket
- "ELECTROMAGNETIC BRAKE" attached to motor assembly
- "BALANCE SPRING" torsion spring visible inside the shaft
- "AUTO CLOSER" device mounted near the brake
- "LIMIT SWITCH" small switches with actuator arms
- "HEAD BOX CASE" shown as transparent or partially removed to reveal internals
- Wiring connections going down labeled "TO CONTROLLER"
Style: Exploded technical diagram or cutaway 3D illustration, white background, professional engineering style. Color-coded: mechanical parts in silver/gray, motor in dark blue, brake in red, spring in green. All labels in English with leader lines.
```
---
### 2.5 감속기+모터+브레이크 (Gear Box + Motor + Brake Assembly)
```
Technical illustration of a fire shutter drive unit assembly, showing three main components connected together.
Show them assembled in sequence with labels:
1. "MOTOR (220V)" - cylindrical body with power cables
2. "ELECTROMAGNETIC BRAKE" - disc-type brake between motor and gearbox, showing brake disc, coil, and spring
3. "WORM GEAR BOX" - rectangular housing with cutaway revealing the worm gear and worm wheel inside
Assembly order shown with arrows: MOTOR → BRAKE → GEAR BOX → "OUTPUT TO SHAFT"
Include rotation direction arrows
Small inset callout showing worm gear mechanism detail labeled: "WORM", "WORM WHEEL", "SELF-LOCKING"
Style: Technical exploded/assembly diagram, white background, metallic rendering, engineering illustration style.
```
---
### 2.6 연동제어기 시스템 (Controller System)
```
Technical schematic diagram showing the fire shutter interlock control system wiring and signal flow.
Layout (block diagram style):
- Top center: "FIRE ALARM PANEL" - rectangular box
- Left: "SMOKE DETECTOR (PHOTOELECTRIC)" - circular device on ceiling
- Right: "HEAT DETECTOR (FIXED TEMP.)" - circular device on ceiling
- Center: "FIRE SHUTTER CONTROLLER" - panel with LED indicators labeled "POWER", "PARTIAL CLOSE", "FULL CLOSE"
- Below controller: "AUTO CLOSER" connected to shutter mechanism
- Bottom left: "MANUAL CONTROL BOX" with "UP / STOP / DOWN" buttons
- Bottom: "FIRE SHUTTER" shown schematically
Signal flow arrows with labels:
- Smoke detector → Controller: "STAGE 1: PARTIAL CLOSE (1m gap)"
- Heat detector → Controller: "STAGE 2: FULL CLOSE (floor sealed)"
- Controller → Auto closer: "CLOSE COMMAND"
- Controller → Speaker icon: "ALARM OUTPUT"
- Controller ↔ Fire alarm panel: "STATUS SIGNAL"
Style: Clean schematic/block diagram, white background, professional electrical diagram style. Color coding: red for fire signals, blue for power, green for status. All labels in English.
```
---
### 2.7 2단계 폐쇄 시퀀스 (2-Stage Closure Sequence)
```
Technical illustration showing the two-stage closing sequence of an automatic fire shutter, presented as 3 side-by-side panels:
Panel 1 - Title: "NORMAL (OPEN)":
- Fire shutter fully open, rolled up inside head box
- People walking through the opening freely
- Smoke and heat detectors on ceiling shown in standby (green LED)
- Caption: "Shutter open, passage clear"
Panel 2 - Title: "STAGE 1: PARTIAL CLOSE":
- Smoke detector activated (red LED, smoke wisps shown)
- Shutter descended leaving about 1 meter gap from floor
- A person crouching to pass under the gap
- Alarm buzzer icon showing sound waves
- Caption: "Smoke detected → Partial close, 1m gap for evacuation"
Panel 3 - Title: "STAGE 2: FULL CLOSE":
- Heat detector activated (red LED, flames shown)
- Shutter fully closed to floor, bottom bar sealed against floor
- Fire and smoke on one side, clean air on other side
- Caption: "Heat detected → Full close, fire/smoke blocked"
Arrow at bottom labeled "TIME SEQUENCE →"
Style: Clean technical illustration with slight architectural rendering, sequential format left to right, white background. People as simple silhouettes. Fire/smoke rendered subtly.
```
---
### 2.8 롤포밍 공정 (Roll Forming Process)
```
Technical illustration showing the roll forming manufacturing process for fire shutter steel slats, production line viewed from the side.
Show the line from left to right with labels:
1. "UNCOILER" - Steel coil (EGI 1.6mm) being unrolled
2. "LEVELER" - Flattening rollers correcting coil curvature
3. "ROLL FORMING STATION" - 6-8 pairs of forming rollers progressively shaping the flat strip into C/S-shaped slat profile
4. "CUTTING STATION" - Flying shear cutting the formed strip to length
5. "FINISHED SLATS" - Slats stacked neatly on output table
Detail callout at top showing progressive cross-section shape changes: "FLAT → STAGE 1 → STAGE 2 → STAGE 3 → FINAL PROFILE"
Arrow at bottom: "MATERIAL FLOW →"
Label on coil: "EGI STEEL COIL 1.6mm"
Style: Technical factory/manufacturing illustration, clean white background, machinery in industrial gray/green, steel in silver. Side view. Directional arrows showing material flow.
```
---
### 2.9 현장 설치 (Field Installation)
```
Technical illustration showing fire shutter installation at a construction site, depicting key installation steps in a single scene.
Scene showing a large building opening (about 5m wide, 4m tall) with:
- Two workers on scaffolding installing the head box assembly at the top
- Brackets already bolted to both side walls near the ceiling
- Guide rails mounted vertically on walls with anchor bolts
- Shaft with wound slat curtain being lifted up to place on brackets
- Manual control box being mounted on adjacent wall
- Wiring conduits visible running from controller to head box
- Construction tools: level tool, drill, anchor bolts, wrenches
Labels with arrows pointing to activities:
- "BRACKET MOUNTING"
- "GUIDE RAIL ANCHORING"
- "SHAFT PLACEMENT"
- "ELECTRICAL WIRING"
- "LEVEL CHECK"
- "ANCHOR BOLT FIXING"
Style: Technical construction illustration, slightly warm tone, realistic building interior with exposed concrete. Workers wearing safety helmets and vests. Clean architectural illustration style. All text in English.
```
---
### 2.10 유지보수 점검 (Maintenance Inspection)
```
Technical illustration showing fire shutter maintenance inspection scene.
Show a maintenance technician inspecting a fire shutter:
- Technician with safety vest and hard hat, holding a tablet
- Fire shutter partially lowered (halfway) for testing
- Close-up callout bubbles showing key inspection points:
1. "SLAT CONDITION" - checking for deformation, rust
2. "SMOKE SEAL CHECK" - checking guide rail seal condition
3. "BOTTOM BAR PACKING" - checking floor seal
4. "MOTOR / BRAKE CHECK" - head box open, listening for sounds
5. "MANUAL BOX TEST" - pressing UP/STOP/DOWN buttons
6. "CONTROLLER STATUS" - checking LED indicators
Checklist overlay in corner:
☑ MOTOR OPEN/CLOSE TEST
☑ DETECTOR INTERLOCK TEST
☑ ALARM SOUND CHECK
☑ MANUAL OPERATION CHECK
☑ BOTTOM BAR SEAL CHECK
Style: Clean technical illustration, bright well-lit building interior, professional maintenance scene. Color callout bubbles with icons. All text in English.
```
---
### 2.11 강판형 vs 스크린형 (Steel Plate vs Screen Type)
```
Technical side-by-side comparison illustration of two types of fire shutters in similar building openings:
Left side - Title "STEEL PLATE TYPE":
- Steel slat fire shutter in partially closed position
- Opaque metallic surface of interlocking steel slats visible
- Heavier, industrial appearance with thick guide rails
- Bottom bar with rubber seal
- Callout: "EGI STEEL 1.6mm / HEAVY / OPAQUE / HIGH SEALING"
Right side - Title "SCREEN / FABRIC TYPE":
- Fabric fire shutter in partially closed position
- Semi-transparent woven silica fiber screen, you can faintly see light through it
- Lighter, sleeker with thin guide rails (11mm)
- Fabric gathered at top
- Callout: "SILICA FIBER / LIGHTWEIGHT / SEMI-TRANSPARENT / RAIL 11mm"
Center dividing line with "VS" label
Bottom comparison bar: "WEIGHT: Heavy vs Light | VISIBILITY: Opaque vs See-through | RAIL WIDTH: Wide vs 11mm"
Style: Clean technical comparison, white background, same scale, professional product comparison layout. All text in English.
```
---
### 2.12 주요 고장 유형 (Major Fault Types)
```
Technical illustration showing 6 common fire shutter failure types in a 2x3 grid layout, each in its own panel with a red problem highlight:
Panel 1 - "SLAT DERAILMENT":
- A slat edge coming out of the guide rail groove, curtain jammed
- Red circle on problem area
Panel 2 - "MOTOR BURNOUT":
- Motor with smoke marks, burnt wiring
- Overheat warning symbol
Panel 3 - "BRAKE PAD WEAR":
- Electromagnetic brake with worn disc pad
- Side comparison: "NEW" thick pad vs "WORN" thin pad
Panel 4 - "CONTROLLER MALFUNCTION":
- Controller panel with error LED, disconnected wires
- Broken signal path indicator
Panel 5 - "CLOSER SPEED FAULT":
- Shutter dropping fast, speedometer showing "0.15 m/s LIMIT EXCEEDED"
- Governor mechanism detail
Panel 6 - "SMOKE SEAL FAILURE":
- Smoke wisps leaking through guide rail gaps
- Comparison: "NEW SEAL" vs "DEGRADED SEAL"
Style: Technical diagnostic illustration, white background, bordered panels. Problem areas in red/orange highlight. Clean maintenance manual style. All titles and labels in English.
```
---
## 3. 이미지 파일 관리
### 3.1 저장 경로
```
mng/public/images/academy/fire-shutter/
├── 01-full-component-diagram.png
├── 02-slat-interlocking.png
├── 03-guide-rail-cross-section.png
├── 04-shaft-assembly.png
├── 05-gearbox-motor-brake.png
├── 06-controller-system.png
├── 07-two-stage-closure.png
├── 08-roll-forming-process.png
├── 09-field-installation.png
├── 10-maintenance-inspection.png
├── 11-steel-vs-screen-type.png
└── 12-major-fault-types.png
```
### 3.2 Blade 참조 예시
```html
<img src="{{ asset('images/academy/fire-shutter/01-full-component-diagram.png') }}"
alt="방화셔터 전체 구성도"
class="w-full rounded-lg shadow">
```
---
## 관련 문서
- `mng/resources/views/academy/fire-shutter.blade.php` - 방화셔터 백과사전 Blade 뷰
- `mng/app/Http/Controllers/AcademyController.php` - 아카데미 컨트롤러
---
**최종 업데이트**: 2026-02-22

View File

@@ -1,298 +0,0 @@
# 결재관리 시스템
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **프로젝트**: SAM MNG (관리자 웹)
> **우선순위**: 🔴 필수
---
## 1. 개요
### 1.1 목적
SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보류, 전결, 참조까지 기업 결재 프로세스를 디지털화한다.
### 1.2 문서 구조
| 문서 | 설명 |
|------|------|
| **README.md** (이 문서) | 시스템 전체 개요, 아키텍처, 상태 관리 |
| [form-types.md](form-types.md) | 양식별 필드/JSON 구조/인터랙션 기술 명세 |
| [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) |
| [api-reference.md](api-reference.md) | API 엔드포인트 명세 |
| [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 |
| [db-changes-and-model-sync.md](db-changes-and-model-sync.md) | DB 변경사항 및 API/MNG 모델 동기화 현황 |
### 1.3 구현 현황
| Phase | 범위 | 상태 |
|-------|------|------|
| **Phase 1** | 순차결재, 기안/상신/승인/반려/회수 | ✅ 완료 |
| **Phase 2** | 보류/해제, 전결, 참조 열람 추적, 복사 재기안 | ✅ 완료 |
| **Phase 3** | 병렬결재, 위임(대결), 알림 | 미착수 |
| **Phase 4** | ERP 연동, 결재 통계, 관리자 설정 | 미착수 |
---
## 2. 아키텍처
### 2.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 뷰 | Blade + HTMX + Alpine.js | 동적 UI, 부분 렌더링 |
| API | Laravel Controller + Service | JSON API (내부용) |
| 모델 | Eloquent ORM | Multi-tenant 스코프 |
| DB | MySQL 8.0 | API 프로젝트에서 마이그레이션 관리 |
### 2.2 프로젝트 분리
```
API (/home/aweso/sam/api)
├── database/migrations/ ← 모든 결재 테이블 마이그레이션
MNG (/home/aweso/sam/mng)
├── app/Models/Approvals/ ← 모델 (Approval, ApprovalStep, ApprovalForm, ApprovalLine, ApprovalDelegation)
├── app/Services/ ← ApprovalService (비즈니스 로직)
├── app/Http/Controllers/ ← ApprovalController (웹), ApprovalApiController (API)
├── resources/views/approvals/ ← Blade 뷰
└── routes/ ← 웹 라우트 + API 라우트
```
### 2.3 핵심 클래스
```
ApprovalService
├── 목록 조회: getMyDrafts(), getPendingForMe(), getCompletedByMe(), getReferencesForMe()
├── CRUD: createApproval(), updateApproval(), deleteApproval(), getApproval()
├── 워크플로우: submit(), approve(), reject(), cancel(), hold(), releaseHold(), preDecide(), copyForRedraft()
├── 참조: markAsRead()
└── 유틸: getBadgeCounts(), getApprovalLines(), getApprovalForms(), saveApprovalSteps()
```
---
## 3. 데이터베이스
### 3.1 테이블 관계
```
approval_forms (결재 양식)
│ 1:N
approvals (결재 문서)
│ 1:N │ N:1 (self)
▼ ▼
approval_steps (결재 단계) approvals (parent_doc_id → 원본 문서)
approval_lines (결재선 템플릿) ← approvals.line_id 참조
approval_delegations (위임 설정) ← Phase 3 준비
```
### 3.2 approvals (결재 문서)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT | 테넌트 격리 |
| `document_number` | VARCHAR | `APR-YYMMDD-001` 형식 |
| `form_id` | BIGINT FK | 양식 |
| `line_id` | BIGINT FK NULL | 결재선 템플릿 |
| `title` | VARCHAR(200) | 제목 |
| `content` | JSON | 양식 필드 데이터 |
| `body` | TEXT NULL | 본문 |
| `status` | VARCHAR(20) | 문서 상태 (6가지) |
| `is_urgent` | BOOLEAN | 긴급 여부 |
| `drafter_id` | BIGINT FK | 기안자 |
| `department_id` | BIGINT FK NULL | 기안 부서 |
| `current_step` | INT | 현재 결재 단계 번호 |
| `drafted_at` | TIMESTAMP NULL | 상신 일시 |
| `completed_at` | TIMESTAMP NULL | 완료 일시 |
| `recall_reason` | TEXT NULL | 회수 사유 |
| `parent_doc_id` | BIGINT FK NULL | 재기안 원본 문서 |
| `attachments` | JSON NULL | 첨부파일 |
### 3.3 approval_steps (결재 단계)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `approval_id` | BIGINT FK | 결재 문서 |
| `step_order` | INT | 순서 (1, 2, 3...) |
| `step_type` | VARCHAR | `approval`, `agreement`, `reference` |
| `parallel_group` | INT NULL | 병렬 그룹 (Phase 3) |
| `approver_id` | BIGINT FK | 결재자 |
| `acted_by` | BIGINT FK NULL | 실제 처리자 (대결 시) |
| `approver_name` | VARCHAR | 결재자명 스냅샷 |
| `approver_department` | VARCHAR | 부서 스냅샷 |
| `approver_position` | VARCHAR | 직급 스냅샷 |
| `status` | VARCHAR(20) | 단계 상태 (5가지) |
| `approval_type` | VARCHAR(20) | `normal`, `pre_decided`, `delegated` |
| `comment` | TEXT NULL | 결재 의견 |
| `acted_at` | TIMESTAMP NULL | 처리 일시 |
| `is_read` | BOOLEAN | 참조 열람 여부 |
| `read_at` | TIMESTAMP NULL | 열람 일시 |
### 3.4 approval_delegations (위임 설정, Phase 3)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | |
| `delegator_id` | BIGINT FK | 위임자 |
| `delegate_id` | BIGINT FK | 대리인 |
| `start_date` | DATE | 위임 시작일 |
| `end_date` | DATE | 위임 종료일 |
| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) |
| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 |
| `is_active` | BOOLEAN | 활성 여부 |
| `reason` | VARCHAR(200) | 위임 사유 |
---
## 4. 상태 관리
### 4.1 문서 상태 (6가지)
| 상태 | 코드 | 라벨 | 색상 | 설명 |
|------|------|------|------|------|
| 임시저장 | `draft` | 임시저장 | gray | 작성 중, 미상신 |
| 진행 | `pending` | 진행 | blue | 결재선 순환 중 |
| 완료 | `approved` | 완료 | green | 최종 승인 |
| 반려 | `rejected` | 반려 | red | 결재자가 반려 |
| 회수 | `cancelled` | 회수 | yellow | 기안자가 회수 |
| 보류 | `on_hold` | 보류 | amber | 결재자가 보류 |
### 4.2 단계 상태 (5가지)
| 상태 | 코드 | 라벨 | 아이콘 | 설명 |
|------|------|------|--------|------|
| 대기 | `pending` | 대기 | 숫자 | 차례 아직 아님 |
| 승인 | `approved` | 승인 | ✓ (녹색) | 승인 완료 |
| 반려 | `rejected` | 반려 | ✗ (적색) | 반려 |
| 건너뜀 | `skipped` | 건너뜀 | — (회색) | 전결/회수로 소멸 |
| 보류 | `on_hold` | 보류 | ⏸ (노란) | 보류 중 |
### 4.3 결재 유형 (approval_type)
| 유형 | 코드 | 아이콘 | 설명 |
|------|------|--------|------|
| 일반결재 | `normal` | ✓ | 기본 승인 |
| 전결 | `pre_decided` | ⚡ (남색) | 이후 단계 모두 건너뛰고 즉시 완료 |
| 대결 | `delegated` | — | 대리인이 처리 (Phase 3) |
### 4.4 참여자 역할 (step_type)
| 역할 | 코드 | 의사결정 | 설명 |
|------|------|---------|------|
| 결재 | `approval` | ✅ 있음 | 승인/반려/보류/전결 가능 |
| 합의 | `agreement` | ✅ 있음 | 타부서 동의 (승인/반려 가능) |
| 참조 | `reference` | ❌ 없음 | 열람만 가능, 열람 추적 |
### 4.5 상태 전이 다이어그램
```
┌─────────────────────────────┐
│ │
┌────────┐ submit() │ ┌─────────┐ │
│ draft │────────────→│ │ pending │ │
└────────┘ │ └────┬────┘ │
▲ │ │ │
│ │ ┌────┼─────────┬───────┐ │
│ (수정 후 재상신) │ │ │ │ │ │
│ │ │ approve() reject() hold()│
│ │ │ │ │ │ │
│ │ │ ▼ ▼ ▼ │
│ │ │ 다음 step rejected on_hold│
│ │ │ 또는 │ │ │
│ │ │ approved │ releaseHold()
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ └────┼────────┼───────┘ │
│ │ │ │ │
│ │ preDecide() │ │
│ │ → approved │ │
│ │ │ │ cancel() │
│ │ │ │ │ │
│ │ ▼ │ ▼ │
│ │ ┌─────────┐ │ ┌──────────┐
│ │ │approved │ │ │cancelled │
│ │ └─────────┘ │ └──────────┘
│ │ │ │ │
│ │ │ │ │
│ │ copyForRedraft() │
│ │ │ │ │
└───────────────────┼───────┴────────┘ │
(새 draft 생성) │ │
│ copyForRedraft() │
│◀──────────────────────┘
└─────────────────────────────┘
```
---
## 5. 권한 매트릭스
### 5.1 누가 무엇을 할 수 있는가
| 액션 | 대상자 | 조건 |
|------|--------|------|
| **기안 작성** | 모든 사용자 | — |
| **수정** | 기안자 | `draft` 또는 `rejected` |
| **삭제** | 기안자 | `draft`만 |
| **상신** | 기안자 | `draft` 또는 `rejected`, 결재선 1명 이상 |
| **승인** | 현재 결재자 | `pending`, 자신이 현재 차례 |
| **반려** | 현재 결재자 | `pending`, 사유 필수 |
| **보류** | 현재 결재자 | `pending`, 사유 필수 |
| **보류 해제** | 보류한 결재자 | `on_hold`, 자신이 보류한 건 |
| **전결** | 현재 결재자 | `pending`, 이후 모든 단계 건너뜀 |
| **회수** | 기안자 | `pending` 또는 `on_hold`, 첫 결재자 미처리 |
| **복사 재기안** | 기안자 | `approved`, `rejected`, `cancelled` |
| **참조 열람** | 참조자 | `reference` step 보유 |
### 5.2 회수 가능 조건 상세
```
회수(cancel) 가능 여부 판단:
1. 문서 상태가 pending 또는 on_hold인가? → 아니면 불가
2. 요청자가 기안자(drafter_id)인가? → 아니면 불가
3. 첫 번째 결재자(approval/agreement)의 상태가 pending 또는 on_hold인가?
→ 이미 approved/rejected이면 불가 (첫 결재자가 이미 처리)
```
---
## 6. 메뉴 구조
```
결재관리
├── 기안함 /approval-mgmt/drafts ← 내가 기안한 문서
├── 결재 대기함 /approval-mgmt/pending ← 내가 결재해야 할 문서
├── 처리 완료함 /approval-mgmt/completed ← 내가 결재한 문서
└── 참조함 /approval-mgmt/references ← 참조 문서 (열람 추적)
```
### 추가 페이지
| URL | 설명 |
|-----|------|
| `/approval-mgmt/create` | 기안 작성 |
| `/approval-mgmt/{id}` | 상세 조회 |
| `/approval-mgmt/{id}/edit` | 기안 수정 |
---
## 7. 관련 문서
- [결재 양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조, 인터랙션
- [결재관리 워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [API 명세](api-reference.md) — 엔드포인트 목록 및 요청/응답 예시
- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작
- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,594 +0,0 @@
# 결재관리 API 명세
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **Base URL**: `/api/admin/approvals`
> **미들웨어**: `web`, `auth`, `hq.member`
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [UI 화면](ui-screens.md)
---
## 1. 개요
모든 API는 JSON 응답을 반환한다. 인증은 세션 기반이며, CSRF 토큰이 필요하다.
### 1.1 공통 응답 형식
**성공:**
```json
{
"success": true,
"message": "처리 메시지",
"data": { ... }
}
```
**실패 (400):**
```json
{
"success": false,
"message": "에러 메시지"
}
```
### 1.2 공통 헤더
```
Content-Type: application/json
Accept: application/json
X-CSRF-TOKEN: {csrf_token}
```
---
## 2. 목록 조회 API
### 2.1 기안함
내가 기안한 문서 목록을 조회한다.
```
GET /api/admin/approvals/drafts
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `status` | string | 상태 필터 (`draft`, `pending`, `approved`, `rejected`, `cancelled`, `on_hold`) |
| `is_urgent` | boolean | 긴급 문서만 |
| `date_from` | date | 시작일 (YYYY-MM-DD) |
| `date_to` | date | 종료일 (YYYY-MM-DD) |
| `per_page` | int | 페이지당 건수 (기본 15) |
| `page` | int | 페이지 번호 |
**응답:** Laravel 페이지네이션 형식
```json
{
"data": [
{
"id": 1,
"document_number": "APR-260228-001",
"title": "휴가 신청",
"status": "pending",
"is_urgent": false,
"form": { "id": 1, "name": "휴가신청서" },
"steps": [...],
"created_at": "2026-02-28T10:00:00",
"drafted_at": "2026-02-28T10:05:00"
}
],
"current_page": 1,
"last_page": 3,
"per_page": 15,
"total": 42
}
```
---
### 2.2 결재 대기함
내가 현재 결재해야 할 문서 목록을 조회한다.
```
GET /api/admin/approvals/pending
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `is_urgent` | boolean | 긴급 문서만 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
> 현재 사용자가 결재 차례인 문서만 표시된다. 이미 승인/반려한 문서는 표시되지 않는다.
---
### 2.3 처리 완료함
내가 승인 또는 반려한 문서 목록을 조회한다.
```
GET /api/admin/approvals/completed
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `status` | string | 상태 필터 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
---
### 2.4 참조함
내가 참조자로 지정된 문서 목록을 조회한다.
```
GET /api/admin/approvals/references
```
**Query Parameters:**
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목/문서번호 검색 |
| `is_read` | string | 열람 상태 필터 (`true`=열람완료, `false`=미열람) |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
---
## 3. CRUD API
### 3.1 상세 조회
```
GET /api/admin/approvals/{id}
```
**응답:**
```json
{
"success": true,
"data": {
"id": 1,
"tenant_id": 1,
"document_number": "APR-260228-001",
"form_id": 1,
"line_id": null,
"title": "휴가 신청",
"content": {},
"body": "2월 27일~28일 연차 사용 신청합니다.",
"status": "pending",
"is_urgent": false,
"drafter_id": 10,
"department_id": 3,
"current_step": 2,
"drafted_at": "2026-02-28T10:05:00",
"completed_at": null,
"recall_reason": null,
"parent_doc_id": null,
"form": { "id": 1, "name": "휴가신청서" },
"drafter": { "id": 10, "name": "홍길동" },
"line": null,
"steps": [
{
"id": 1,
"step_order": 1,
"step_type": "approval",
"approver_id": 20,
"approver_name": "김과장",
"approver_department": "경영지원팀",
"approver_position": "과장",
"status": "approved",
"approval_type": "normal",
"comment": "승인합니다.",
"acted_at": "2026-02-28T11:00:00",
"is_read": false,
"read_at": null
},
{
"id": 2,
"step_order": 2,
"step_type": "approval",
"approver_id": 30,
"approver_name": "박부장",
"approver_department": "경영지원팀",
"approver_position": "부장",
"status": "pending",
"approval_type": "normal",
"comment": null,
"acted_at": null,
"is_read": false,
"read_at": null
}
]
}
}
```
---
### 3.2 생성 (임시저장)
```
POST /api/admin/approvals
```
**Request Body:**
```json
{
"form_id": 1,
"title": "휴가 신청",
"body": "2월 27일~28일 연차 사용",
"is_urgent": false,
"steps": [
{ "user_id": 20, "step_type": "approval" },
{ "user_id": 30, "step_type": "approval" },
{ "user_id": 40, "step_type": "reference" }
]
}
```
**Validation:**
| 필드 | 규칙 |
|------|------|
| `form_id` | required, exists:approval_forms,id |
| `title` | required, string, max:200 |
| `body` | nullable, string |
| `is_urgent` | boolean |
| `steps` | nullable, array |
| `steps.*.user_id` | required_with:steps, exists:users,id |
| `steps.*.step_type` | required_with:steps, in:approval,agreement,reference |
**응답 (201):**
```json
{
"success": true,
"message": "결재 문서가 저장되었습니다.",
"data": { ... }
}
```
---
### 3.3 수정
```
PUT /api/admin/approvals/{id}
```
> `draft` 또는 `rejected` 상태에서만 수정 가능
**Request Body:** (생성과 동일, 모든 필드 선택)
**Validation:**
| 필드 | 규칙 |
|------|------|
| `title` | sometimes, string, max:200 |
| `body` | nullable, string |
| `is_urgent` | boolean |
| `steps` | nullable, array |
---
### 3.4 삭제
```
DELETE /api/admin/approvals/{id}
```
> `draft` 상태에서만 삭제 가능
**응답:**
```json
{
"success": true,
"message": "결재 문서가 삭제되었습니다."
}
```
---
## 4. 워크플로우 API
### 4.1 상신
```
POST /api/admin/approvals/{id}/submit
```
> 기안자가 `draft`/`rejected` 문서를 결재 요청한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "결재가 상신되었습니다.", "data": {...} }`
---
### 4.2 승인
```
POST /api/admin/approvals/{id}/approve
```
> 현재 결재자가 승인한다.
**Request Body:**
```json
{
"comment": "승인합니다." // 선택
}
```
**응답:** `{ "success": true, "message": "승인되었습니다.", "data": {...} }`
---
### 4.3 반려
```
POST /api/admin/approvals/{id}/reject
```
> 현재 결재자가 반려한다. 사유 필수.
**Request Body:**
```json
{
"comment": "예산 초과로 반려합니다." // 필수
}
```
**Validation:** `comment` — required, string, max:1000
**응답:** `{ "success": true, "message": "반려되었습니다.", "data": {...} }`
---
### 4.4 회수
```
POST /api/admin/approvals/{id}/cancel
```
> 기안자가 `pending`/`on_hold` 문서를 회수한다. 첫 결재자 미처리 시에만 가능.
**Request Body:**
```json
{
"recall_reason": "내용 수정 필요" // 선택
}
```
**응답:** `{ "success": true, "message": "결재가 회수되었습니다.", "data": {...} }`
---
### 4.5 보류
```
POST /api/admin/approvals/{id}/hold
```
> 현재 결재자가 결재를 보류한다. 사유 필수.
**Request Body:**
```json
{
"comment": "추가 자료 검토 필요" // 필수
}
```
**Validation:** `comment` — required, string, max:1000
**응답:** `{ "success": true, "message": "보류되었습니다.", "data": {...} }`
---
### 4.6 보류 해제
```
POST /api/admin/approvals/{id}/release-hold
```
> 보류한 결재자가 보류를 해제한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "보류가 해제되었습니다.", "data": {...} }`
---
### 4.7 전결
```
POST /api/admin/approvals/{id}/pre-decide
```
> 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인한다.
**Request Body:**
```json
{
"comment": "전결 처리합니다." // 선택
}
```
**응답:** `{ "success": true, "message": "전결 처리되었습니다.", "data": {...} }`
---
### 4.8 복사 재기안
```
POST /api/admin/approvals/{id}/copy
```
> 기안자가 `approved`/`rejected`/`cancelled` 문서를 복사하여 새 draft를 생성한다.
**Request Body:** 없음
**응답:**
```json
{
"success": true,
"message": "문서가 복사되었습니다.",
"data": {
"id": 15,
"document_number": "APR-260228-003",
"parent_doc_id": 1,
"status": "draft",
...
}
}
```
> 응답의 `data.id`를 사용하여 `/approval-mgmt/{id}/edit`로 이동한다.
---
### 4.9 참조 열람 추적
```
POST /api/admin/approvals/{id}/mark-read
```
> 참조자가 문서를 열람했음을 기록한다.
**Request Body:** 없음
**응답:** `{ "success": true, "message": "열람 처리되었습니다." }`
---
## 5. 유틸리티 API
### 5.1 결재선 템플릿 목록
```
GET /api/admin/approvals/lines
```
**응답:**
```json
{
"success": true,
"data": [
{ "id": 1, "name": "일반 결재선", "steps": [...] }
]
}
```
---
### 5.2 양식 목록
```
GET /api/admin/approvals/forms
```
**응답:**
```json
{
"success": true,
"data": [
{ "id": 1, "name": "휴가신청서", "is_active": true }
]
}
```
---
### 5.3 미처리 건수 (뱃지)
```
GET /api/admin/approvals/badge-counts
```
**응답:**
```json
{
"success": true,
"data": {
"pending": 3,
"draft": 1,
"reference_unread": 5
}
}
```
| 필드 | 설명 |
|------|------|
| `pending` | 내가 결재해야 할 문서 수 |
| `draft` | 내 임시저장 문서 수 |
| `reference_unread` | 미열람 참조 문서 수 |
---
## 6. 라우트 전체 목록
| Method | Path | 컨트롤러 메서드 | 이름 | 설명 |
|--------|------|---------------|------|------|
| GET | `/drafts` | `drafts` | `drafts` | 기안함 |
| GET | `/pending` | `pending` | `pending` | 결재 대기함 |
| GET | `/completed` | `completed` | `completed` | 처리 완료함 |
| GET | `/references` | `references` | `references` | 참조함 |
| GET | `/lines` | `lines` | `lines` | 결재선 템플릿 |
| GET | `/forms` | `forms` | `forms` | 양식 목록 |
| GET | `/badge-counts` | `badgeCounts` | `badge-counts` | 뱃지 건수 |
| POST | `/` | `store` | `store` | 생성 |
| GET | `/{id}` | `show` | `show` | 상세 |
| PUT | `/{id}` | `update` | `update` | 수정 |
| DELETE | `/{id}` | `destroy` | `destroy` | 삭제 |
| POST | `/{id}/submit` | `submit` | `submit` | 상신 |
| POST | `/{id}/approve` | `approve` | `approve` | 승인 |
| POST | `/{id}/reject` | `reject` | `reject` | 반려 |
| POST | `/{id}/cancel` | `cancel` | `cancel` | 회수 |
| POST | `/{id}/hold` | `hold` | `hold` | 보류 |
| POST | `/{id}/release-hold` | `releaseHold` | `release-hold` | 보류 해제 |
| POST | `/{id}/pre-decide` | `preDecide` | `pre-decide` | 전결 |
| POST | `/{id}/copy` | `copyForRedraft` | `copy` | 복사 재기안 |
| POST | `/{id}/mark-read` | `markAsRead` | `mark-read` | 열람 추적 |
---
## 관련 문서
- [README.md](README.md) — 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [UI 화면 구성](ui-screens.md) — 화면별 동작
---
**최종 업데이트**: 2026-02-28

View File

@@ -1,286 +0,0 @@
# 결재관리 DB 변경사항 및 API 모델 동기화 현황
> **작성일**: 2026-03-09
> **상태**: 조사 완료
> **관련**: [README.md](README.md) | [API 명세](api-reference.md)
---
## 1. 개요
### 1.1 목적
2026-02-27 ~ 2026-03-05 기간에 결재관리 테이블에 대규모 컬럼 추가가 이루어졌다. 이 문서는 변경된 DB 스키마와 API/MNG 프로젝트 간 모델 동기화 상태를 기록한다.
### 1.2 핵심 발견
- 마이그레이션 **15개** 실행 (API 프로젝트에서 관리)
- MNG 모델: ✅ 모든 신규 컬럼 반영 완료
- API 모델: ❌ **`$fillable`/`$casts` 미반영** — 오류 원인 가능성
---
## 2. 마이그레이션 변경 타임라인
### 2.1 Phase 2 확장 (2026-02-27)
| 마이그레이션 파일 | 대상 테이블 | 작업 |
|------------------|-----------|------|
| `add_columns_to_approvals_table` | `approvals` | `line_id`, `body`, `is_urgent`, `department_id` 추가 |
| `add_columns_to_approval_steps_table` | `approval_steps` | `approver_name`, `approver_department`, `approver_position` 추가 |
| `add_phase2_columns_to_approval_steps_table` | `approval_steps` | `parallel_group`, `acted_by`, `approval_type` 추가 |
| `add_phase2_columns_to_approvals_table` | `approvals` | `recall_reason`, `parent_doc_id` 추가 |
| `create_approval_delegations_table` | `approval_delegations` | 위임 테이블 신규 생성 |
| `add_linkable_to_approvals_table` | `approvals` | `linkable_type`, `linkable_id` 추가 (다형성) |
### 2.2 도메인 연동 (2026-02-28)
| 마이그레이션 파일 | 대상 테이블 | 작업 |
|------------------|-----------|------|
| `add_approval_id_to_leaves_table` | `leaves` | `approval_id` FK 추가 |
| `insert_leave_approval_form` | `approval_forms` | 휴가신청 양식 데이터 등록 |
### 2.3 양식 확장 (2026-03-03 ~ 03-04)
| 마이그레이션 파일 | 대상 테이블 | 작업 |
|------------------|-----------|------|
| `insert_attendance_approval_forms` | `approval_forms` | 근태신청, 사유서 양식 등록 |
| `add_body_template_to_approval_forms` | `approval_forms` | `body_template` 컬럼 추가 |
| `insert_expense_approval_form` | `approval_forms` | 지출결의서 양식 + body_template 등록 |
| `update_expense_approval_form_body_template` | `approval_forms` | 지출결의서 body_template 고도화 |
### 2.4 추적 기능 (2026-03-05)
| 마이그레이션 파일 | 대상 테이블 | 작업 |
|------------------|-----------|------|
| `add_drafter_read_at_to_approvals_table` | `approvals` | `drafter_read_at` 추가 |
| `add_resubmit_count_to_approvals_table` | `approvals` | `resubmit_count` 추가 |
| `add_rejection_history_to_approvals_table` | `approvals` | `rejection_history` 추가 |
---
## 3. 추가된 컬럼 상세
### 3.1 `approvals` 테이블 (11개 컬럼 추가)
| 컬럼 | 타입 | 기본값 | 추가일 | 용도 |
|------|------|--------|--------|------|
| `line_id` | BIGINT FK NULL | NULL | 02-27 | 결재선 템플릿 참조 |
| `body` | LONGTEXT NULL | NULL | 02-27 | 문서 본문 HTML |
| `is_urgent` | BOOLEAN | false | 02-27 | 긴급 여부 |
| `department_id` | BIGINT NULL | NULL | 02-27 | 기안 부서 |
| `recall_reason` | TEXT NULL | NULL | 02-27 | 회수 사유 |
| `parent_doc_id` | BIGINT FK NULL | NULL | 02-27 | 재기안 원본 문서 |
| `linkable_type` | VARCHAR NULL | NULL | 02-27 | 다형성 모델 타입 |
| `linkable_id` | BIGINT NULL | NULL | 02-27 | 다형성 모델 ID |
| `drafter_read_at` | TIMESTAMP NULL | NULL | 03-05 | 기안자 열람 시각 |
| `resubmit_count` | TINYINT UNSIGNED | 0 | 03-05 | 재상신 횟수 |
| `rejection_history` | JSON NULL | NULL | 03-05 | 반려 이력 배열 |
### 3.2 `approval_steps` 테이블 (6개 컬럼 추가)
| 컬럼 | 타입 | 기본값 | 추가일 | 용도 |
|------|------|--------|--------|------|
| `approver_name` | VARCHAR(50) NULL | NULL | 02-27 | 결재자명 스냅샷 |
| `approver_department` | VARCHAR(100) NULL | NULL | 02-27 | 결재자 부서 스냅샷 |
| `approver_position` | VARCHAR(50) NULL | NULL | 02-27 | 결재자 직급 스냅샷 |
| `parallel_group` | INT NULL | NULL | 02-27 | 병렬 결재 그룹 (Phase 3) |
| `acted_by` | BIGINT FK NULL | NULL | 02-27 | 실제 처리자 (대결) |
| `approval_type` | VARCHAR(20) | 'normal' | 02-27 | normal/pre_decided/delegated |
### 3.3 `approval_forms` 테이블 (1개 컬럼 추가)
| 컬럼 | 타입 | 기본값 | 추가일 | 용도 |
|------|------|--------|--------|------|
| `body_template` | TEXT NULL | NULL | 03-04 | HTML 양식 렌더링 템플릿 |
### 3.4 `approval_delegations` 테이블 (신규 생성)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `delegator_id` | BIGINT FK | 위임자 |
| `delegate_id` | BIGINT FK | 대리인 |
| `start_date` | DATE | 위임 시작일 |
| `end_date` | DATE | 위임 종료일 |
| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) |
| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 |
| `is_active` | BOOLEAN | 활성 여부 |
| `reason` | VARCHAR(200) | 위임 사유 |
---
## 4. API/MNG 모델 동기화 현황
### 4.1 Approval 모델 비교
| 항목 | MNG (`mng/app/Models/Approvals/Approval.php`) | API (`api/app/Models/Tenants/Approval.php`) |
|------|:---:|:---:|
| `line_id` in $fillable | ✅ | ❌ |
| `body` in $fillable | ✅ | ❌ |
| `is_urgent` in $fillable/$casts | ✅ boolean | ❌ |
| `department_id` in $fillable | ✅ | ❌ |
| `recall_reason` in $fillable | ✅ | ❌ |
| `parent_doc_id` in $fillable | ✅ | ❌ |
| `linkable_type/id` in $fillable | ✅ | ✅ |
| `drafter_read_at` in $fillable/$casts | ✅ datetime | ❌ |
| `resubmit_count` in $fillable/$casts | ✅ integer | ❌ |
| `rejection_history` in $fillable/$casts | ✅ array | ❌ |
### 4.2 ApprovalStep 모델 비교
| 항목 | MNG | API |
|------|:---:|:---:|
| `approver_name` in $fillable | ✅ | ❌ |
| `approver_department` in $fillable | ✅ | ❌ |
| `approver_position` in $fillable | ✅ | ❌ |
| `parallel_group` in $fillable | ✅ | ❌ |
| `acted_by` in $fillable | ✅ | ❌ |
| `approval_type` in $fillable | ✅ | ❌ |
### 4.3 ApprovalForm 모델 비교
| 항목 | MNG | API |
|------|:---:|:---:|
| `body_template` in $fillable | ✅ | ❌ |
### 4.4 ApprovalDelegation 모델
| 항목 | MNG | API |
|------|:---:|:---:|
| 모델 파일 존재 | ✅ | ❌ 미생성 |
---
## 5. 오류 영향 분석
### 5.1 API 모델 미반영으로 인한 잠재적 오류
API 프로젝트의 모델 `$fillable`에 신규 컬럼이 누락되어, API 엔드포인트를 통한 결재 문서 처리 시 다음 오류가 발생할 수 있다:
| 증상 | 원인 | 영향 범위 |
|------|------|----------|
| `create()`/`update()` 시 신규 필드 저장 안 됨 | `$fillable` 미포함 → mass assignment 차단 | API v1 결재 CRUD |
| JSON 필드(`rejection_history`) 문자열로 반환 | `$casts` 미정의 → 타입 변환 안 됨 | API 응답 파싱 오류 |
| `drafter_read_at` 날짜 비교 실패 | `$casts` datetime 미정의 → Carbon 미변환 | 열람 추적 기능 |
| `is_urgent` 비교 오류 | `$casts` boolean 미정의 → 문자열 비교 | 긴급 필터링 |
| 위임(delegation) 기능 완전 불가 | 모델 자체 미생성 | Phase 3 기능 전체 |
### 5.2 MNG는 정상
MNG 프로젝트의 모델은 모든 신규 컬럼이 `$fillable`, `$casts`, `$attributes`에 반영되어 있으며, `ApprovalService`에서 정상 사용 중이다.
```
MNG 정상 동작 확인 기능:
✅ 반려 이력 저장 (rejection_history)
✅ 재상신 횟수 추적 (resubmit_count)
✅ 기안자 열람 추적 (drafter_read_at)
✅ 결재자 스냅샷 저장 (approver_name/department/position)
✅ 전결 처리 (approval_type = pre_decided)
✅ 회수 사유 기록 (recall_reason)
```
---
## 6. 수정 필요 파일 목록
### 6.1 API 모델 업데이트 필요
| 파일 | 수정 내용 |
|------|----------|
| `api/app/Models/Tenants/Approval.php` | `$fillable`에 9개 필드, `$casts`에 4개 필드 추가 |
| `api/app/Models/Tenants/ApprovalStep.php` | `$fillable`에 6개 필드 추가 |
| `api/app/Models/Tenants/ApprovalForm.php` | `$fillable``body_template` 추가 |
| `api/app/Models/Tenants/ApprovalDelegation.php` | 모델 파일 신규 생성 |
### 6.2 Approval.php 수정 상세
**`$fillable` 추가 필요:**
```php
'line_id',
'body',
'is_urgent',
'department_id',
'recall_reason',
'parent_doc_id',
'drafter_read_at',
'resubmit_count',
'rejection_history',
```
**`$casts` 추가 필요:**
```php
'drafter_read_at' => 'datetime',
'resubmit_count' => 'integer',
'rejection_history' => 'array',
'is_urgent' => 'boolean',
```
### 6.3 ApprovalStep.php 수정 상세
**`$fillable` 추가 필요:**
```php
'approver_name',
'approver_department',
'approver_position',
'parallel_group',
'acted_by',
'approval_type',
```
### 6.4 ApprovalForm.php 수정 상세
**`$fillable` 추가 필요:**
```php
'body_template',
```
---
## 7. 연관 테이블 참조 변경
결재 시스템과 연동된 다른 테이블의 변경사항:
| 테이블 | 추가 컬럼 | 추가일 | 용도 |
|--------|----------|--------|------|
| `leaves` | `approval_id` (BIGINT FK) | 02-28 | 휴가 ↔ 결재 연동 |
| `purchases` | `approval_id` (BIGINT FK) | (기존) | 구매 ↔ 결재 연동 |
---
## 8. 등록된 결재 양식 (13종)
2026-02-28 ~ 03-07 기간에 마이그레이션으로 등록된 양식:
| 코드 | 양식명 | 카테고리 | 등록일 |
|------|--------|---------|--------|
| `leave` | 휴가신청서 | request | 02-28 |
| `attendance_request` | 근태신청서 | request | 03-03 |
| `reason_report` | 사유서 | request | 03-03 |
| `expense` | 지출결의서 | expense | 03-04 |
| `employment_cert` | 재직증명서 | request | 03-05 |
| `career_cert` | 경력증명서 | request | 03-05 |
| `appointment_cert` | 위촉증명서 | request | 03-05 |
| `resignation` | 사직서 | request | 03-06 |
| `seal_usage` | 사용인감계 | request | 03-06 |
| `delegation` | 위임장 | request | 03-06 |
| `board_minutes` | 이사회의사록 | request | 03-06 |
| `quotation` | 견적서 | request | 03-06 |
| `official_letter` | 공문서 | request | 03-07 |
---
## 관련 문서
- [결재관리 시스템 개요](README.md) — 아키텍처, 상태 관리, 권한
- [API 명세](api-reference.md) — 20개 엔드포인트 상세
- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름
- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획
---
**최종 업데이트**: 2026-03-09

View File

@@ -1,999 +0,0 @@
# 결재 양식 기술 명세
> **작성일**: 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

View File

@@ -1,381 +0,0 @@
# 결재관리 UI 화면 구성
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **기술**: Blade + HTMX + Alpine.js + Tailwind CSS
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md)
---
## 1. 개요
결재관리 화면은 MNG(관리자 웹)에서 Blade 템플릿으로 구현되며, API 호출은 `fetch()`를 사용한다.
### 1.1 파일 구조
```
resources/views/approvals/
├── drafts.blade.php ← 기안함 (목록)
├── pending.blade.php ← 결재 대기함 (목록)
├── completed.blade.php ← 처리 완료함 (목록)
├── references.blade.php ← 참조함 (목록)
├── create.blade.php ← 기안 작성
├── edit.blade.php ← 기안 수정
├── show.blade.php ← 상세 조회 + 결재 처리
└── partials/
├── _status-badge.blade.php ← 상태 뱃지 컴포넌트
└── _step-progress.blade.php ← 결재 단계 진행 표시
```
---
## 2. 목록 화면
### 2.1 기안함 (`/approval-mgmt/drafts`)
내가 기안한 모든 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 기안함 [+ 새 기안] │
├──────────────────────────────────────────────────────────┤
│ [검색] [상태 필터 ▼] [긴급만 □] [날짜 범위] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 양식 │ 상태 │ 기안일 │
│ APR-260228-001│ 휴가 신청 │ 휴가서 │ 🟢완료 │ 02-28 │
│ APR-260228-002│ 출장 보고 │ 출장서 │ 🔵진행 │ 02-28 │
│ APR-260227-001│ 경비 청구 │ 경비서 │ ⬜임시 │ 02-27 │
├──────────────────────────────────────────────────────────┤
│ [◀ 이전] 1 / 3 [다음 ▶] │
└──────────────────────────────────────────────────────────┘
```
**상태 필터:** 전체, 임시저장, 진행, 완료, 반려, 회수, 보류
---
### 2.2 결재 대기함 (`/approval-mgmt/pending`)
내가 현재 결재해야 할 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 결재 대기함 [뱃지: 3건] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 기안자 │ 양식 │ 상신일 │
│ 🔴 APR-260..│ 긴급 승인 │ 홍길동 │ 구매서 │ 02-28 │
│ APR-260..│ 휴가 신청 │ 김영희 │ 휴가서 │ 02-27 │
└──────────────────────────────────────────────────────────┘
```
> 긴급 문서는 🔴 아이콘과 함께 상단에 표시
---
### 2.3 참조함 (`/approval-mgmt/references`)
내가 참조자로 지정된 문서를 표시한다.
**UI 구성:**
```
┌──────────────────────────────────────────────────────────┐
│ 참조함 │
├──────────────────────────────────────────────────────────┤
│ [전체] [미열람 (5)] [열람완료] │
├──────────────────────────────────────────────────────────┤
│ 문서번호 │ 제목 │ 기안자 │ 상태 │ 열람 │
│ APR-260228-001│ 회의록 │ 박부장 │ 🟢완료 │ ❌미열람│
│ APR-260227-003│ 인사발령 │ 이팀장 │ 🔵진행 │ ✅열람 │
└──────────────────────────────────────────────────────────┘
```
**열람 추적:**
- 문서 클릭 시 `mark-read` API가 자동 호출된다
- 미열람/열람완료 탭으로 필터링 가능
- 미열람 건수가 뱃지로 표시된다
---
## 3. 상세 화면 (`/approval-mgmt/{id}`)
### 3.1 전체 레이아웃
```
┌──────────────────────────────────────────────────────────┐
│ 결재 상세 [수정] [목록으로] │
│ APR-260228-001 │
├──────────────────────────────────────────────────────────┤
│ │
│ 상태: [🔵 진행] [🔴 긴급] │
│ 양식: 휴가신청서 기안자: 홍길동 │
│ 기안일: 2026-02-28 10:05 완료일: - │
│ 원본 문서: APR-260225-003 (재기안 시 표시) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 회수 사유 (cancelled 상태에서만) │ │
│ │ 내용 수정이 필요하여 회수합니다. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 제목: 2월 연차 사용 신청 │
│ 본문: 2월 27일~28일 연차 사용합니다... │
│ │
├──────────────────────────────────────────────────────────┤
│ │
│ 결재 진행 │
│ ┌────────────────────────────────────────────────┐ │
│ │ [결재 단계 프로그레스 바] │ │
│ │ ✓김과장(승인) → ●박부장(대기) → ③이사(대기) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ 결재 의견 │
│ ┌────────────────────────────────────────────────┐ │
│ │ ✓ 김과장 2026-02-28 11:00 │ │
│ │ 승인합니다. │ │
│ └────────────────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────┤
│ │
│ 결재 처리 (현재 결재자에게만 표시) │
│ [결재 의견 textarea] │
│ [승인] [반려] [보류] [전결] │
│ │
├──────────────────────────────────────────────────────────┤
│ 보류 해제 (on_hold + 보류한 본인에게만) │
│ [보류 해제] │
├──────────────────────────────────────────────────────────┤
│ 회수 (기안자 + pending/on_hold) │
│ [회수 사유 textarea] │
│ [결재 회수] │
├──────────────────────────────────────────────────────────┤
│ 복사 재기안 (기안자 + approved/rejected/cancelled) │
│ [복사하여 재기안] │
└──────────────────────────────────────────────────────────┘
```
### 3.2 조건부 섹션 표시
| 섹션 | 표시 조건 |
|------|----------|
| **수정 버튼** | 기안자 + `draft`/`rejected` |
| **회수 사유** | `cancelled` + `recall_reason` 존재 |
| **원본 문서 링크** | `parent_doc_id` 존재 (재기안 문서) |
| **결재 처리** | `pending` + 현재 결재자 |
| **보류 해제** | `on_hold` + 보류한 본인 |
| **회수** | 기안자 + `pending`/`on_hold` |
| **복사 재기안** | 기안자 + `approved`/`rejected`/`cancelled` |
---
## 4. 파셜 컴포넌트
### 4.1 상태 뱃지 (`_status-badge.blade.php`)
문서 상태를 색상 뱃지로 표시한다.
| 상태 | 라벨 | 스타일 |
|------|------|--------|
| `draft` | 임시저장 | `bg-gray-100 text-gray-700` |
| `pending` | 진행 | `bg-blue-100 text-blue-700` |
| `approved` | 완료 | `bg-green-100 text-green-700` |
| `rejected` | 반려 | `bg-red-100 text-red-700` |
| `cancelled` | 회수 | `bg-yellow-100 text-yellow-700` |
| `on_hold` | 보류 | `bg-amber-100 text-amber-700` |
---
### 4.2 결재 단계 프로그레스 (`_step-progress.blade.php`)
결재선의 각 단계를 가로 프로그레스 바로 표시한다.
**단계 아이콘:**
| 상태 | 아이콘 | 배경색 | 텍스트색 |
|------|--------|--------|---------|
| `approved` (normal) | ✓ | `bg-green-500` | white |
| `approved` (pre_decided) | ⚡ | `bg-indigo-500` | white |
| `rejected` | ✗ | `bg-red-500` | white |
| `on_hold` | ⏸ | `bg-amber-400` | white |
| `skipped` | — | `bg-gray-300` | gray |
| `pending` (현재 차례) | 번호 | `bg-blue-500` | white |
| `pending` (대기) | 번호 | `bg-gray-200` | gray |
**레이아웃:**
```
┌──────────────────────────────────────────────────────┐
│ │
│ ✓ ──── ⚡ ──── — ──── — ──── ● ──── 3 │
│ 김과장 박부장 이사장 팀장 최대리 참조자 │
│ 경영팀 경영팀 대표실 개발팀 개발팀 인사팀 │
│ (승인) (전결) (건너뜀)(건너뜀)(대기) (참조) │
│ │
└──────────────────────────────────────────────────────┘
```
**특수 표시:**
- **전결** step: ⚡ 아이콘 + "전결" 라벨 (남색)
- **보류** step: ⏸ 아이콘 + "보류" 라벨 (노란색)
- **건너뜀** step: 이름에 취소선 (line-through)
- **참조** step: 별도 구분 없이 동일 프로그레스 바에 표시
- **연결선**: 단계 사이 가로선 (`border-t-2`)
---
## 5. 결재 처리 인터랙션
### 5.1 승인
```
[승인 버튼 클릭]
→ confirm("승인하시겠습니까?")
→ POST /api/admin/approvals/{id}/approve
body: { comment: "의견 텍스트" }
→ 성공 시: 토스트("승인되었습니다") + 페이지 리로드
```
### 5.2 반려
```
[반려 버튼 클릭]
→ comment 빈 값 체크 → 경고 토스트("반려 시 사유를 입력해주세요")
→ confirm("반려하시겠습니까?")
→ POST /api/admin/approvals/{id}/reject
body: { comment: "사유" }
→ 성공 시: 토스트("반려되었습니다") + 페이지 리로드
```
### 5.3 보류
```
[보류 버튼 클릭]
→ comment 빈 값 체크 → 경고 토스트("보류 사유를 입력해주세요")
→ confirm("이 결재를 보류하시겠습니까?")
→ POST /api/admin/approvals/{id}/hold
body: { comment: "사유" }
→ 성공 시: 토스트("보류되었습니다") + 페이지 리로드
```
### 5.4 전결
```
[전결 버튼 클릭]
→ confirm("전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.")
→ POST /api/admin/approvals/{id}/pre-decide
body: { comment: "의견(선택)" }
→ 성공 시: 토스트("전결 처리되었습니다") + 페이지 리로드
```
### 5.5 보류 해제
```
[보류 해제 버튼 클릭]
→ confirm("보류를 해제하시겠습니까?")
→ POST /api/admin/approvals/{id}/release-hold
→ 성공 시: 토스트("보류가 해제되었습니다") + 페이지 리로드
```
### 5.6 회수
```
[결재 회수 버튼 클릭]
→ confirm("결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.")
→ POST /api/admin/approvals/{id}/cancel
body: { recall_reason: "사유(선택)" }
→ 성공 시: 토스트("결재가 회수되었습니다") + 페이지 리로드
```
### 5.7 복사 재기안
```
[복사하여 재기안 버튼 클릭]
→ confirm("이 문서를 복사하여 새 결재를 작성하시겠습니까?")
→ POST /api/admin/approvals/{id}/copy
→ 성공 시: 토스트("문서가 복사되었습니다")
→ /approval-mgmt/{newId}/edit로 이동
```
---
## 6. 결재 의견 표시
상세 페이지에서 결재 의견이 있는 step을 카드 형태로 표시한다.
```
┌──────────────────────────────────────┐
│ ✓ 김과장 2026-02-28 11:00 │
│ 승인합니다. │
├──────────────────────────────────────┤
│ ⚡ 박부장 (전결) 2026-02-28 14:00 │
│ 전결 처리합니다. │
├──────────────────────────────────────┤
│ ⏸ 이사장 (보류) 2026-02-28 15:00 │
│ 추가 자료 검토 필요 │
├──────────────────────────────────────┤
│ ✗ 팀장 2026-02-28 16:00 │
│ 예산 초과로 반려합니다. │
└──────────────────────────────────────┘
```
**아이콘 색상:**
- ✓ 승인: 녹색 (`bg-green-100 text-green-600`)
- ⚡ 전결: 남색 (`bg-indigo-100 text-indigo-600`)
- ⏸ 보류: 노란색 (`bg-amber-100 text-amber-600`)
- ✗ 반려: 적색 (`bg-red-100 text-red-600`)
---
## 7. 참조함 열람 추적 UI
### 7.1 탭 필터
```
[전체] [미열람 (5)] [열람완료]
```
- 탭 클릭 시 `is_read` 파라미터로 API 재호출
- 미열람 탭에 건수 뱃지 표시
### 7.2 열람 상태 표시
| 상태 | 표시 |
|------|------|
| 미열람 | `bg-red-100 text-red-700` "미열람" |
| 열람완료 | `bg-green-100 text-green-700` "열람완료" |
### 7.3 자동 열람 처리
문서 행 클릭 시:
1. `mark-read` API 호출 (비동기)
2. 상세 페이지로 이동
---
## 8. 버튼 스타일 가이드
| 버튼 | 색상 | Tailwind 클래스 |
|------|------|----------------|
| 승인 | 녹색 | `bg-green-600 hover:bg-green-700` |
| 반려 | 적색 | `bg-red-600 hover:bg-red-700` |
| 보류 | 노란색 | `bg-amber-500 hover:bg-amber-600` |
| 전결 | 남색 | `bg-indigo-600 hover:bg-indigo-700` |
| 보류 해제 | 노란색 | `bg-amber-500 hover:bg-amber-600` |
| 회수 | 노란색 | `bg-yellow-500 hover:bg-yellow-600` |
| 복사 재기안 | 회색 | `bg-gray-600 hover:bg-gray-700` |
| 수정 | 회색 | `bg-gray-600 hover:bg-gray-700` |
---
## 관련 문서
- [README.md](README.md) — 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름
- [API 명세](api-reference.md) — 엔드포인트별 요청/응답
---
**최종 업데이트**: 2026-02-28

View File

@@ -1,565 +0,0 @@
# 결재관리 워크플로우 상세
> **작성일**: 2026-02-28
> **상태**: Phase 2 구현 완료
> **관련**: [README.md](README.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md)
---
## 1. 개요
이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다.
모든 워크플로우는 `ApprovalService`에서 트랜잭션으로 처리된다.
### 1.1 용어 정의
| 용어 | 설명 |
|------|------|
| **기안자** | 결재 문서를 작성한 사람 (`drafter_id`) |
| **현재 결재자** | 결재선에서 현재 차례인 사람 (가장 작은 `step_order``pending` step) |
| **결재자** | `step_type``approval` 또는 `agreement`인 참여자 |
| **참조자** | `step_type``reference`인 참여자 (의사결정 권한 없음) |
| **전결** | 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인 |
---
## 2. 기안 작성 (createApproval)
### 2.1 흐름
```
사용자 → [양식 선택] → [제목/본문 입력] → [결재선 설정] → [임시저장]
새 Approval 생성
status = 'draft'
current_step = 0
```
### 2.2 조건
- 모든 로그인 사용자가 작성 가능
- `form_id` 필수 (양식 선택)
- 결재선(steps)은 저장 시 선택사항 (상신 시 필수)
### 2.3 처리 로직
1. 문서번호 자동 채번 (`APR-YYMMDD-001` 형식)
2. `numbering_sequences` 테이블로 일일 순번 관리
3. 결재선 설정 시 `approval_steps` 저장 + 사용자 정보 스냅샷 (이름, 부서, 직급)
4. `status = 'draft'`, `current_step = 0`
---
## 3. 상신 (submit)
### 3.1 흐름
```
기안자 → [상신 버튼] → 유효성 검사 → 결재선 검사 → 상신 완료
status = 'pending'
current_step = 1
drafted_at = now()
```
### 3.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `draft` 또는 `rejected` |
| 결재선 | 결재/합의 step 1명 이상 필수 |
| 요청자 | 기안자만 |
### 3.3 처리 로직
1. `isSubmittable()` 검증 → `draft` 또는 `rejected`인지 확인
2. 결재/합의 step 존재 확인
3. **반려 후 재상신인 경우**: 모든 step을 `pending`으로 초기화 (comment, acted_at도 초기화)
4. `status → pending`, `drafted_at → now()`, `current_step → 1`
### 3.4 반려 후 재상신
```
rejected 문서
├── 기안자가 내용 수정 (updateApproval)
└── 상신 (submit)
├── 모든 steps → pending (초기화)
├── status → pending
└── current_step → 1 (처음부터 다시)
```
> 반려 후 재상신 시 결재선이 초기화되므로, 이전 결재 의견(comment)은 사라진다.
---
## 4. 승인 (approve)
### 4.1 흐름
```
현재 결재자 → [의견 입력(선택)] → [승인 버튼]
┌──────────┴──────────┐
│ 현재 step │
│ status → 'approved' │
│ comment → (입력값) │
│ acted_at → now() │
└──────────┬──────────┘
┌─────────────────┴─────────────────┐
│ │
다음 pending step 있음 마지막 결재자
│ │
current_step 갱신 status → 'approved'
(다음 순서 결재자 대기) completed_at → now()
```
### 4.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `pending` |
| 요청자 | 현재 차례 결재자 (`approver_id === auth()->id()`) |
### 4.3 처리 로직
1. `isActionable()` 검증 → `pending` 상태인지 확인
2. `getCurrentApproverStep()` → 현재 차례 step 조회
3. 현재 step → `approved` + comment + acted_at
4. 다음 pending 결재/합의 step 조회
- **있으면**: `current_step` 갱신
- **없으면**: 문서 `approved` + `completed_at`
### 4.4 순차결재 순서 결정
```
step_order = 1 (결재) → step_order = 2 (합의) → step_order = 3 (결재)
│ │ │
1번째 승인 → 2번째 승인 → 3번째 승인 → 문서 완료
```
> 결재와 합의는 동일한 순차 흐름을 따른다. `step_order` 순서대로 처리된다.
---
## 5. 반려 (reject)
### 5.1 흐름
```
현재 결재자 → [반려 사유 입력(필수)] → [반려 버튼]
┌──────────┴──────────┐
│ 현재 step │
│ status → 'rejected' │
│ comment → (사유) │
│ acted_at → now() │
└──────────┬──────────┘
문서 status → 'rejected'
completed_at → now()
```
### 5.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `pending` |
| 요청자 | 현재 차례 결재자 |
| 반려 사유 | **필수** (빈 값 불가) |
### 5.3 처리 로직
1. `isActionable()` 검증
2. 현재 결재자 확인
3. 반려 사유 빈 값 체크
4. 현재 step → `rejected` + comment + acted_at
5. 문서 → `rejected` + completed_at
### 5.4 반려 후 가능한 동작
```
rejected 문서
├── 기안자가 수정 → 재상신 (submit)
│ └── 결재선 초기화, 처음부터 다시 진행
└── 기안자가 복사 재기안 (copyForRedraft)
└── 새 문서 생성 (draft), 원본은 그대로 유지
```
---
## 6. 회수 (cancel)
### 6.1 흐름
```
기안자 → [회수 사유 입력(선택)] → [회수 버튼]
┌──────────┴──────────┐
│ 회수 가능 여부 판단 │
│ (첫 결재자 미처리?) │
└──────────┬──────────┘
┌───────────┴───────────┐
│ │
첫 결재자 첫 결재자 이미
pending/on_hold 승인/반려
│ │
회수 진행 회수 불가
│ (에러 반환)
모든 pending/on_hold steps → 'skipped'
문서 status → 'cancelled'
recall_reason → (입력값)
completed_at → now()
```
### 6.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `pending` 또는 `on_hold` |
| 요청자 | 기안자만 (`drafter_id === auth()->id()`) |
| 첫 결재자 상태 | `pending` 또는 `on_hold` (이미 처리했으면 불가) |
### 6.3 회수 가능 판단 로직
```php
// 1단계: 문서 상태 확인
$approval->isCancellable() // pending 또는 on_hold
// 2단계: 기안자 확인
$approval->drafter_id === auth()->id()
// 3단계: 첫 결재자 상태 확인
$firstStep = steps.approvalOnly().orderBy('step_order').first()
$firstStep->status === 'pending' || 'on_hold' // 미처리 상태여야 함
```
### 6.4 처리 로직
1. `isCancellable()` 검증 → `pending` 또는 `on_hold`
2. 기안자 확인
3. 첫 번째 결재/합의 step의 상태 확인 → `pending`/`on_hold`이 아니면 거부
4. 모든 `pending`/`on_hold` steps → `skipped`
5. 문서 → `cancelled` + `recall_reason` + `completed_at`
---
## 7. 보류 (hold)
### 7.1 흐름
```
현재 결재자 → [보류 사유 입력(필수)] → [보류 버튼]
┌──────────┴──────────┐
│ 현재 step │
│ status → 'on_hold' │
│ comment → (사유) │
│ acted_at → now() │
└──────────┬──────────┘
문서 status → 'on_hold'
```
### 7.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `pending` (`isHoldable()`) |
| 요청자 | 현재 차례 결재자 |
| 보류 사유 | **필수** (빈 값 불가) |
### 7.3 처리 로직
1. `isHoldable()` 검증 → `pending` 상태인지 확인
2. `getCurrentApproverStep()` → 현재 차례 step 조회
3. 현재 결재자 확인 (`approver_id === auth()->id()`)
4. 보류 사유 빈 값 체크
5. 현재 step → `on_hold` + comment + acted_at
6. 문서 → `on_hold`
### 7.4 보류 상태의 영향
```
on_hold 상태에서:
├── 다른 결재자는 아무 동작 불가 (결재 흐름 중단)
├── 기안자는 회수 가능 (첫 결재자가 미처리 상태이면)
└── 보류한 결재자만 보류 해제 가능
```
---
## 8. 보류 해제 (releaseHold)
### 8.1 흐름
```
보류한 결재자 → [보류 해제 버튼]
┌──────────┴──────────┐
│ on_hold step │
│ status → 'pending' │
│ comment → null │
│ acted_at → null │
└──────────┬──────────┘
문서 status → 'pending'
(결재 흐름 재개)
```
### 8.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `on_hold` (`isHoldReleasable()`) |
| 요청자 | 보류한 본인만 (`on_hold` step의 `approver_id === auth()->id()`) |
### 8.3 처리 로직
1. `isHoldReleasable()` 검증 → `on_hold` 상태인지 확인
2. `on_hold` 상태인 step 조회
3. 해당 step의 `approver_id`가 현재 사용자인지 확인
4. step → `pending` + comment/acted_at 초기화
5. 문서 → `pending`
---
## 9. 전결 (preDecide)
### 9.1 흐름
```
현재 결재자 → [의견 입력(선택)] → [전결 버튼] → 확인 팝업
┌──────────┴──────────┐
│ 현재 step │
│ status → 'approved' │
│ approval_type → │
│ 'pre_decided' │
│ comment → (입력값) │
│ acted_at → now() │
└──────────┬──────────┘
이후 모든 pending
approval/agreement steps
→ status = 'skipped'
문서 status → 'approved'
completed_at → now()
```
### 9.2 조건
| 조건 | 설명 |
|------|------|
| 문서 상태 | `pending` (`isActionable()`) |
| 요청자 | 현재 차례 결재자 |
### 9.3 처리 로직
1. `isActionable()` 검증
2. `getCurrentApproverStep()` → 현재 차례 step 조회
3. 현재 결재자 확인
4. 현재 step → `approved` + `approval_type = 'pre_decided'` + comment + acted_at
5. 이후 모든 pending 결재/합의 steps → `skipped`
6. 문서 → `approved` + `completed_at`
### 9.4 전결 예시
```
step_order=1 (이사장, 결재) → approved (normal)
step_order=2 (부장, 결재) → approved (pre_decided) ← 여기서 전결
step_order=3 (과장, 합의) → skipped (전결로 건너뜀)
step_order=4 (팀장, 결재) → skipped (전결로 건너뜀)
step_order=5 (참조자, 참조) → (참조는 영향 없음, 그대로 유지)
문서 → approved, completed_at = now()
```
> 전결은 결재/합의 step만 건너뛴다. 참조 step은 영향받지 않는다.
---
## 10. 복사 재기안 (copyForRedraft)
### 10.1 흐름
```
기안자 → [복사하여 재기안 버튼]
┌─────────────────────────────┐
│ 원본 문서에서 복사 │
│ ├── form_id │
│ ├── title │
│ ├── content (양식 데이터) │
│ ├── body │
│ ├── is_urgent │
│ ├── department_id │
│ └── 결재선 (모두 pending) │
└─────────────┬───────────────┘
새 문서 생성 (status = 'draft')
parent_doc_id = 원본.id
새 문서번호 채번
수정 페이지로 이동
(/approval-mgmt/{newId}/edit)
```
### 10.2 조건
| 조건 | 설명 |
|------|------|
| 원본 문서 상태 | `approved`, `rejected`, `cancelled` (`isCopyable()`) |
| 요청자 | 기안자만 (`drafter_id === auth()->id()`) |
### 10.3 처리 로직
1. `isCopyable()` 검증 → `approved`/`rejected`/`cancelled` 중 하나
2. 기안자 확인
3. 새 문서 생성:
- 새 문서번호 채번
- 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사
- `parent_doc_id = 원본.id`
- `status = 'draft'`, `current_step = 0`
4. 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두 `pending` 상태)
5. 새 문서의 edit 페이지로 리다이렉트
### 10.4 원본과의 관계
```
원본 문서 (approved/rejected/cancelled)
└── parent_doc_id로 연결
새 문서 (draft)
├── 상세 페이지에서 "원본 문서" 링크 표시
└── 기안자가 내용 수정 후 상신 가능
```
---
## 11. 참조 열람 추적 (markAsRead)
### 11.1 흐름
```
참조자 → [참조함 목록에서 문서 클릭]
├── markAsRead API 호출
│ ├── is_read → true
│ └── read_at → now()
└── 상세 페이지로 이동
```
### 11.2 조건
| 조건 | 설명 |
|------|------|
| 요청자 | 해당 문서의 참조자 (`step_type = 'reference'`) |
### 11.3 처리 로직
1. 현재 사용자의 참조 step 조회
2. `is_read = false`인 step → `is_read = true`, `read_at = now()`
3. 이미 열람한 경우 중복 업데이트 없음 (`where('is_read', false)`)
---
## 12. 전체 상태 전이 요약
```
┌───────────────────────────────────────────────────────────────────┐
│ │
│ draft ──submit()──→ pending ──approve()──→ (다음 step 또는) │
│ ▲ │ │ approved │
│ │ │ │ │
│ │ │ ├──reject()──→ rejected │
│ │ │ │ │ │
│ │ │ │ ├── 수정 → submit() │
│ │ │ │ │ (재상신, draft X) │
│ │ │ │ │ │
│ │ │ │ └── copyForRedraft() │
│ │ │ │ → 새 draft 생성 │
│ │ │ │ │
│ │ │ ├──hold()──→ on_hold │
│ │ │ │ │ │
│ │ │ │ ├── releaseHold() │
│ │ │ │ │ → pending 복원 │
│ │ │ │ │ │
│ │ │ │ └── cancel() (기안자) │
│ │ │ │ → cancelled │
│ │ │ │ │
│ │ │ ├──preDecide()──→ approved │
│ │ │ │ (이후 steps → skipped) │
│ │ │ │ │
│ │ │ └──cancel()──→ cancelled │
│ │ │ (기안자, 첫결재자 미처리 시) │
│ │ │ │ │
│ │ │ └── copyForRedraft() │
│ │ │ → 새 draft 생성 │
│ │ │ │
│ │ └── approved ──copyForRedraft() │
│ │ → 새 draft 생성 │
│ │ │
│ └── updateApproval() (draft/rejected 상태에서 수정) │
│ │
└───────────────────────────────────────────────────────────────────┘
```
---
## 13. 에러 케이스 정리
| 동작 | 에러 조건 | 에러 메시지 |
|------|----------|------------|
| submit | 상태가 draft/rejected 아님 | "상신할 수 없는 상태입니다." |
| submit | 결재선 없음 | "결재선을 설정해주세요." |
| approve | 상태가 pending 아님 | "승인할 수 없는 상태입니다." |
| approve | 현재 결재자 아님 | "현재 결재자가 아닙니다." |
| reject | 상태가 pending 아님 | "반려할 수 없는 상태입니다." |
| reject | 사유 미입력 | "반려 사유를 입력해주세요." |
| cancel | 상태가 pending/on_hold 아님 | "회수할 수 없는 상태입니다." |
| cancel | 기안자 아님 | "기안자만 회수할 수 있습니다." |
| cancel | 첫 결재자 이미 처리 | "첫 번째 결재자가 이미 처리하여 회수할 수 없습니다." |
| hold | 상태가 pending 아님 | "보류할 수 없는 상태입니다." |
| hold | 현재 결재자 아님 | "현재 결재자가 아닙니다." |
| hold | 사유 미입력 | "보류 사유를 입력해주세요." |
| releaseHold | 상태가 on_hold 아님 | "보류 해제할 수 없는 상태입니다." |
| releaseHold | 보류한 본인 아님 | "보류한 결재자만 해제할 수 있습니다." |
| preDecide | 상태가 pending 아님 | "전결할 수 없는 상태입니다." |
| preDecide | 현재 결재자 아님 | "현재 결재자가 아닙니다." |
| copyForRedraft | 상태가 approved/rejected/cancelled 아님 | "복사할 수 없는 상태입니다." |
| copyForRedraft | 기안자 아님 | "기안자만 복사할 수 있습니다." |
| update | 상태가 draft/rejected 아님 | "수정할 수 없는 상태입니다." |
| delete | 상태가 draft 아님 | "삭제할 수 없는 상태입니다." |
---
## 관련 문서
- [README.md](README.md) — 시스템 전체 개요
- [API 명세](api-reference.md) — 엔드포인트별 요청/응답
- [UI 화면 구성](ui-screens.md) — 화면별 동작
---
**최종 업데이트**: 2026-02-28

View File

@@ -1,410 +0,0 @@
# 바로빌 카카오톡 (알림톡/친구톡) 연동
> **문서 버전**: 1.1
> **작성일**: 2026-02-14
> **최종 수정**: 2026-02-27
> **상태**: 운영 중 (알림톡 + SMS + 환경별 분기 완료)
> **대상 프로젝트**: MNG
---
## 1. 개요
### 1.1 목적
바로빌(Barobill) 플랫폼의 카카오톡 알림톡/친구톡 API를 SAM에 연동하여,
고객사에 카카오톡 메시지를 자동 또는 수동으로 발송하는 기능을 제공한다.
### 1.2 사전 요구사항
| 항목 | 상태 | 설명 |
|------|------|------|
| 법인 명의 휴대폰 준비 | **완료** | 카카오톡 채널 가입에 법인 명의 번호 사용 |
| 카카오톡 채널 개설 | **완료** (2026-02-20) | 채널 ID: `@codebridge`, 채널명: (주)코드브릿지엑스 |
| 바로빌 카카오톡 서비스 신청 | **완료** (2026-02-20) | 바로빌 관리자 페이지에서 카카오톡 서비스 활성화 |
| 채널 연동 (바로빌↔카카오) | **완료** (2026-02-20) | 바로빌 관리 URL에서 채널 연동 처리 |
| 바로빌 파트너 과금 설정 | **완료** (2026-02-23) | 바로빌 측에서 파트너사 과금 설정 완료 |
| 알림톡 템플릿 v1 검수 | **완료** (2026-02-22) | `전자계약_서명요청`, `전자계약_리마인드` 2종 승인 |
| 알림톡 템플릿 v2 검수 | **완료** (2026-02-25) | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 |
| 알림톡 `전자계약_완료` | **완료** (2026-02-26) | 서명 완료 알림 발송용 템플릿 승인 |
| 역할 기반 알림 분기 | **완료** (2026-02-26) | 본사=이메일, 상대방=알림톡/SMS |
| 환경별 템플릿 분기 | **완료** (2026-02-27) | `_DEV` 접미사 개발 템플릿 등록 |
| DEV 템플릿 검수 | **심사 중** (2026-02-27 접수) | 개발서버용 3종 (`admin.codebridge-x.com`) |
> 상세 등록 가이드: [카카오톡 알림톡 채널 및 템플릿 등록 가이드](../../guides/카카오톡-알림톡-채널-템플릿-등록.md)
### 1.3 알림톡 vs 친구톡
| 구분 | 알림톡 | 친구톡 |
|------|--------|--------|
| **용도** | 정보성 메시지 (주문확인, 배송안내 등) | 광고성 메시지 (프로모션, 이벤트 등) |
| **수신 대상** | 모든 카카오톡 사용자 | 채널 친구 추가한 사용자만 |
| **템플릿** | 필수 (카카오 사전 검수) | 불필요 (자유 형식) |
| **광고 표시** | 불가 | 필수 (`(광고)` 표기) |
| **이미지 첨부** | 불가 | 가능 (이미지/와이드 이미지) |
| **비용** | 건당 약 8~9원 | 건당 약 15~20원 |
| **SMS 대체발송** | 설정 가능 | 설정 가능 |
---
## 2. 아키텍처
### 2.1 시스템 구조
```
SAM MNG (브라우저)
├─ [페이지] /barobill/kakaotalk/* ← Blade 뷰
│ KakaotalkController (페이지 렌더링)
├─ [API] /api/admin/barobill/kakaotalk/* ← AJAX 호출
│ BarobillKakaotalkController
└─ [전자계약] /esign/* ← 자동 발송
EsignApiController::sendAlimtalk()
└─ BarobillService (SOAP 클라이언트)
└─ 바로빌 KAKAOTALK.asmx (WSDL)
└─ 카카오톡 서버
```
### 2.2 바로빌 SOAP API 엔드포인트
| 환경 | WSDL URL |
|------|----------|
| **테스트** | `https://testws.baroservice.com/KAKAOTALK.asmx?WSDL` |
| **운영** | `https://ws.baroservice.com/KAKAOTALK.asmx?WSDL` |
---
## 3. 전자계약 알림톡 연동 (핵심)
### 3.1 발송 흐름
```
전자계약 생성 (E-Sign)
├─ [1단계] EsignApiController::sendAlimtalk()
│ │
│ ├─ 채널 ID 조회 (getKakaotalkChannelId)
│ ├─ 템플릿 본문 + 버튼 조회 (getTemplateData)
│ ├─ 변수 치환 (#{이름}, #{계약명}, #{기한})
│ └─ SendATKakaotalkEx 호출
├─ [2단계] 바로빌 접수 → SendKey 반환
├─ [3단계] 3초 대기 후 GetSendKakaotalk으로 전달 결과 확인
│ │
│ ├─ ResultCode = 1 → 성공
│ └─ ResultCode != 1 → 실패 (에러 반환)
└─ [이메일 폴백] 알림톡 실패 시 이메일로 자동 전환
```
### 3.2 등록된 템플릿 (v1 — 현재 운영)
**`전자계약_서명요청`**
```
안녕하세요, #{이름}님.
전자계약 서명 요청이 도착했습니다.
■ 계약명: #{계약명}
■ 서명 기한: #{기한}
아래 버튼을 눌러 계약서를 확인하고 서명해 주세요.
```
- 버튼: `계약서 확인하기` (WL)
- Url1/Url2: `https://mng.codebridge-x.com`
**`전자계약_리마인드`**
```
안녕하세요, #{이름}님.
아직 서명이 완료되지 않은 전자계약이 있습니다.
■ 계약명: #{계약명}
■ 서명 기한: #{기한}
기한 내에 서명을 완료해 주세요.
```
- 버튼: `계약서 확인하기` (WL)
- Url1/Url2: `https://mng.codebridge-x.com`
### 3.3 등록 예정 템플릿 (v2 — 심사 중)
> **2026-02-24 재등록**: 버튼 URL에 `#{토큰}` 변수를 포함하여 동적 서명 URL 지원
- Url1/Url2: `https://mng.codebridge-x.com/esign/sign/#{토큰}`
v2 승인 후 코드 변경 필요:
- `EsignApiController::sendAlimtalk()`에서 동적 `$signUrl`을 버튼 URL로 전달
- 현재 코드의 등록된 URL 그대로 사용 → 동적 URL 사용으로 전환
### 3.4 임시 우회: 로그인 페이지 서명 확인
v1 템플릿의 버튼 URL이 대시보드(`https://mng.codebridge-x.com`)로 고정되어 있어,
로그인 페이지에 전화번호 기반 서명 확인 기능을 추가하였다.
```
알림톡 버튼 클릭 → https://mng.codebridge-x.com → 로그인 페이지
└─ "전자계약 서명하기" 섹션
├─ 전화번호 입력
├─ POST /esign/verify-phone
└─ 대기 중인 계약 조회 → /esign/sign/{token} 리다이렉트
```
- 라우트: `POST /esign/verify-phone`
- 컨트롤러: `EsignPublicController::verifyPhone()`
- v2 템플릿 승인 후에도 유지 (비로그인 사용자 대응)
### 3.5 관련 파일
| 파일 | 역할 |
|------|------|
| `app/Http/Controllers/ESign/EsignApiController.php` | `sendAlimtalk()`, `getTemplateData()` |
| `app/Http/Controllers/ESign/EsignPublicController.php` | `verifyPhone()` 전화번호 확인 |
| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트, `sendATKakaotalkEx()` |
| `resources/views/auth/login.blade.php` | 로그인 페이지 서명 확인 UI |
| `routes/web.php` | `/esign/verify-phone` 라우트 |
---
## 4. 트러블슈팅 (실전 경험)
> **경고: 아래 내용은 실제 연동 과정에서 발견한 핵심 이슈다. 반드시 숙지할 것.**
### 4.1 바로빌 API 응답 구조
바로빌 SOAP 응답은 `stdClass` 객체로 반환된다. 배열이 아니므로 주의:
```php
// ❌ 잘못된 접근
$channels = $result['data']; // 배열이 아님
// ✅ 올바른 접근
$data = $result['data']; // stdClass
$channels = is_array($data->KakaotalkChannel)
? $data->KakaotalkChannel
: [$data->KakaotalkChannel]; // 1건이면 객체, N건이면 배열
```
### 4.2 SendKey vs ResultCode (2단계 검증 필수)
> **핵심**: 바로빌이 SendKey를 반환해도 **실제 카카오톡 전달이 실패할 수 있다.**
```
[1단계] SendATKakaotalkEx 호출
→ SendKey 반환 (예: BB_6648603713_AT_3044107_260224)
→ 이것은 "접수 성공"이지 "전달 성공"이 아님!
[2단계] 3초 후 GetSendKakaotalk(SendKey) 호출
→ ResultCode = 1: 전달 성공 ✅
→ ResultCode = 4: 템플릿 데이터 일치 오류 ❌
→ ResultCode != 1: 기타 실패 ❌
```
```php
// 반드시 2단계 검증 필요
if ($result['success'] && is_string($result['data'])) {
$sendKey = $result['data'];
sleep(3); // 카카오톡 전달 대기
$sendResult = $barobill->getSendKakaotalk($member->biz_no, $sendKey);
$resultCode = $sendResult['data']->ResultCode ?? null;
if ($resultCode != 1) {
// 실패 처리!
}
}
```
### 4.3 템플릿 URL 정확 일치 규칙
> **핵심**: 버튼 URL은 등록된 템플릿의 URL과 **정확히 일치**해야 한다. 1글자라도 다르면 실패.
| 등록된 URL | 전송 시 URL | 결과 |
|------------|------------|------|
| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com` | ResultCode=1 (성공) |
| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com/esign/sign/xxx` | ResultCode=4 (실패) |
| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com?sign=xxx` | ResultCode=4 (실패) |
| `https://mng.codebridge-x.com` | `https://mng.codebridge-x.com#sign=xxx` | ResultCode=4 (실패) |
- 경로 추가: 실패
- 쿼리 파라미터 추가: 실패
- URL 프래그먼트(#) 추가: 실패
- **동적 URL을 사용하려면 템플릿에 `#{변수}` 포함하여 재등록 필요**
### 4.4 SmsReply 오류 (-31325)
`SmsReply` 파라미터가 `'S'`(대체발송 사용)인데 `SmsSenderNum`이 비어있으면 `-31325` 오류 발생.
```php
// ❌ 오류 발생
'SmsReply' => empty($smsMessage) ? 'N' : 'S', // SmsSenderNum이 비어도 S로 설정
// ✅ 수정
'SmsReply' => (empty($smsMessage) || empty($smsSenderNum)) ? 'N' : 'S',
```
### 4.5 SOAP 파라미터 구조
바로빌 SOAP API의 파라미터 구조에 주의:
```php
// 올바른 구조
$params = [
'CorpNum' => $bizNo, // 사업자번호 (하이픈 포함: 123-45-67890)
'SenderID' => $barobillId, // 바로빌 계정 ID
'YellowId' => $channelId, // 카카오 채널 ID (@codebridge)
'TemplateName' => '전자계약_서명요청',
'SendDT' => '', // 즉시발송: 빈 문자열
'SmsReply' => 'N', // SMS 발신번호 없으면 반드시 'N'
'SmsSenderNum' => '',
'KakaotalkMessage' => [
'ReceiverName' => $name,
'ReceiverNum' => $phone, // 하이픈 없이: 01012345678
'Title' => '',
'Message' => $message, // 템플릿 변수 치환 완료된 본문
'SmsMessage' => '',
'SmsSubject' => '',
'Buttons' => ['KakaotalkButton' => $buttons], // 버튼 배열
],
];
```
### 4.6 에러 코드 정리
| 코드 | 메시지 | 원인 | 해결 |
|------|--------|------|------|
| 1 | 성공 | 정상 전달 | - |
| 4 | 템플릿 데이터 일치 오류 | 본문/버튼 URL이 등록 템플릿과 불일치 | 등록된 템플릿과 동일하게 전송 |
| -31325 | 대체문자 유형 오류 | SmsReply=S인데 SmsSenderNum 비어있음 | SmsReply를 N으로 설정 |
| 음수값 | 바로빌 API 오류 | 파라미터 오류 또는 서비스 미설정 | 바로빌 에러코드 문서 참조 |
---
## 5. 구현 현황
### 5.1 완료된 항목
| 구분 | 파일 | 설명 |
|------|------|------|
| SOAP 서비스 | `app/Services/Barobill/BarobillService.php` | kakaotalk SOAP 클라이언트 + 15개 API 메서드 |
| 전자계약 알림톡 | `app/Http/Controllers/ESign/EsignApiController.php` | `sendAlimtalk()`, `getTemplateData()` |
| 서명 확인 | `app/Http/Controllers/ESign/EsignPublicController.php` | `verifyPhone()` 전화번호 기반 서명 확인 |
| API 컨트롤러 | `app/Http/Controllers/Api/Admin/Barobill/BarobillKakaotalkController.php` | 15개 API 엔드포인트 |
| 페이지 컨트롤러 | `app/Http/Controllers/Barobill/KakaotalkController.php` | 6개 관리 페이지 |
| 로그인 페이지 | `resources/views/auth/login.blade.php` | 전자계약 서명하기 섹션 |
| 라우트 | `routes/web.php` | `/esign/verify-phone`, `/barobill/kakaotalk/*` |
| 메뉴 등록 | DB (menus 테이블) | 로컬/서버 모두 등록 완료 |
### 5.2 검증 완료 항목
| 항목 | 결과 | 날짜 |
|------|------|------|
| 채널 API 호출 | **성공** | 2026-02-22 |
| 템플릿 조회 | **성공** | 2026-02-22 |
| 알림톡 발송 (본문) | **성공** (ResultCode=1) | 2026-02-24 |
| 알림톡 버튼 URL | **성공** (등록된 URL 사용 시) | 2026-02-24 |
| 전달 결과 확인 (2단계) | **구현 완료** | 2026-02-24 |
| 로그인 페이지 서명 확인 | **성공** | 2026-02-24 |
### 5.3 완료된 추가 항목 (2026-02-26~27)
| 항목 | 상태 | 비고 |
|------|------|------|
| 템플릿 v2 승인 | **완료** | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 |
| `전자계약_완료` 템플릿 | **완료** | 서명 완료 알림 발송 — PDF 다운로드 버튼 |
| 역할 기반 알림 분기 | **완료** | 본사(creator)=이메일, 상대방(counterpart)=알림톡 |
| OTP SMS 발송 | **완료** | 상대방에게 SMS로 인증코드 발송 |
| 환경별 템플릿 분기 | **완료** | `resolveTemplateName()``_DEV` 접미사 자동 적용 |
| 서명 PDF 재생성 | **완료** | `downloadDocument()`에서 완료 계약 PDF 자동 재생성 |
> 상세 가이드: [전자계약 알림톡/SMS 환경별 설정 가이드](./esign-notification-guide.md)
### 5.4 대기 중인 항목
| 항목 | 상태 | 비고 |
|------|------|------|
| DEV 템플릿 검수 | **심사 중** | `admin.codebridge-x.com` 도메인 3종 |
| 친구톡 발송 | **대기** | 채널 친구 추가 후 가능 |
| 대량 발송 | **대기** | 단건 안정화 후 |
---
## 6. v2 템플릿 승인 후 코드 변경 가이드
### 6.1 변경 대상
`EsignApiController::sendAlimtalk()` (약 1059~1063행)
### 6.2 현재 코드 (v1)
```php
// 등록된 버튼 URL을 그대로 사용 (동적 URL 사용 시 템플릿 불일치 오류)
$buttons = ! empty($templateButtons) ? $templateButtons : [
['Name' => '계약서 확인하기', 'ButtonType' => 'WL',
'Url1' => 'https://mng.codebridge-x.com', 'Url2' => 'https://mng.codebridge-x.com'],
];
```
### 6.3 변경 코드 (v2 승인 후)
```php
// v2 템플릿: 버튼 URL에 동적 서명 URL 사용
$buttons = [
['Name' => '계약서 확인하기', 'ButtonType' => 'WL',
'Url1' => $signUrl, 'Url2' => $signUrl],
];
```
- `$signUrl`은 1033행에서 이미 생성됨: `config('app.url').'/esign/sign/'.$signer->access_token`
- `getTemplateData()`에서 등록된 버튼 조회는 더 이상 필요 없음 (제거 가능)
---
## 7. API 메서드 목록
### 7.1 BarobillService 카카오톡 메서드
| 메서드 | SOAP Action | 설명 |
|--------|-------------|------|
| `getKakaotalkChannels` | `GetKakaotalkChannels` | 채널 목록 조회 |
| `getKakaotalkChannelManagementUrl` | `GetKakaotalkChannelManagementURL` | 채널 관리 URL |
| `getKakaotalkTemplates` | `GetKakaotalkTemplates` | 템플릿 목록 조회 |
| `getKakaotalkTemplateManagementUrl` | `GetKakaotalkTemplateManagementURL` | 템플릿 관리 URL |
| `sendATKakaotalk` | `SendATKakaotalk` | 알림톡 단건 발송 |
| `sendATKakaotalkEx` | `SendATKakaotalkEx` | 알림톡 단건 발송 (버튼 포함) |
| `sendATKakaotalks` | `SendATKakaotalks` | 알림톡 대량 발송 |
| `sendFTKakaotalk` | `SendFTKakaotalk` | 친구톡 텍스트 단건 |
| `sendFTKakaotalks` | `SendFTKakaotalks` | 친구톡 텍스트 대량 |
| `sendFIKakaotalk` | `SendFIKakaotalk` | 친구톡 이미지 |
| `sendFWKakaotalk` | `SendFWKakaotalk` | 친구톡 와이드 이미지 |
| `getSendKakaotalk` | `GetSendKakaotalk` | 전송 결과 단건 조회 |
| `getSendKakaotalks` | `GetSendKakaotalks` | 전송 결과 다건 조회 |
| `cancelReservedKakaotalk` | `CancelReservedKakaotalk` | 예약 전송 취소 |
---
## 8. 참고 자료
- [바로빌 API 문서](https://dev.barobill.co.kr)
- [카카오비즈니스 채널 관리](https://business.kakao.com)
- [카카오 알림톡 가이드](https://kakaobusiness.gitbook.io)
- 바로빌 템플릿 관리: 로그인 후 `https://www.barobill.co.kr` → 카카오톡 템플릿 관리
---
## 변경 이력
| 날짜 | 버전 | 변경 내용 |
|------|------|----------|
| 2026-02-27 | 1.1 | 역할 기반 알림, OTP SMS, 환경별 템플릿 분기, 완료 알림톡 추가 |
| 2026-02-24 | 1.0 | 전자계약 알림톡 연동 완료, 트러블슈팅 문서화, v2 템플릿 가이드 추가 |
| 2026-02-14 | 0.2 | 전자계약(E-Sign) 알림톡 연동 활용 계획 추가 |
| 2026-02-14 | 0.1 | 초안 작성 - 코드 구현 완료, 실 서비스 연동 대기 |

View File

@@ -1,250 +0,0 @@
# 전자계약 알림톡/SMS 환경별 설정 가이드
> **작성일**: 2026-02-27
> **상태**: 운영 중
> **대상 프로젝트**: MNG
---
## 1. 개요
### 1.1 목적
전자계약(E-Sign) 시스템의 카카오톡 알림톡, SMS, 이메일 발송을 **3개 환경(로컬/개발/운영)**에서 올바르게 설정하고 테스트하기 위한 가이드이다.
### 1.2 핵심 원칙
- **역할 기반 알림**: 본사(creator)는 이메일, 상대방(counterpart)은 카카오톡/SMS
- **환경별 템플릿 분리**: 운영은 원본 템플릿, 개발은 `_DEV` 접미사 템플릿 사용
- **URL 자동 분기**: `config('app.url')`로 환경별 도메인 자동 적용
---
## 2. 환경별 설정
### 2.1 도메인 및 APP_URL
| 환경 | `APP_ENV` | `APP_URL` | 알림톡 버튼 URL 도메인 |
|------|-----------|-----------|----------------------|
| 로컬 (Docker) | `local` | `https://mng.sam.kr` | 로컬 — 알림톡 미사용 |
| 개발 서버 | `local` | `https://admin.codebridge-x.com` | `admin.codebridge-x.com` |
| 운영 서버 | `production` | `https://mng.codebridge-x.com` | `mng.codebridge-x.com` |
### 2.2 바로빌 서버 모드
`barobill_members.server_mode` 컬럼으로 바로빌 API 엔드포인트를 결정한다:
| server_mode | WSDL (카카오톡) | WSDL (SMS) | 용도 |
|-------------|----------------|------------|------|
| `test` | `testws.baroservice.com/KAKAOTALK.asmx` | `testws.baroservice.com/SMS.asmx` | 테스트 |
| `production` | `ws.baroservice.com/KAKAOTALK.asmx` | `ws.baroservice.com/SMS.asmx` | 실제 발송 |
> `server_mode`는 환경(로컬/개발/운영)과 독립적이다. 개발서버에서도 `production` 모드로 실제 발송 가능.
### 2.3 알림톡 템플릿 환경별 분기
코드에서 `resolveTemplateName()` 메서드가 `APP_ENV`에 따라 템플릿명을 자동 결정한다:
```php
private function resolveTemplateName(string $baseName): string
{
return $baseName . (app()->environment('production') ? '' : '_DEV');
}
```
| 기본 템플릿명 | 운영 (`production`) | 개발/로컬 (기타) |
|-------------|--------------------|--------------------|
| `전자계약_서명요청` | `전자계약_서명요청` | `전자계약_서명요청_DEV` |
| `전자계약_완료` | `전자계약_완료` | `전자계약_완료_DEV` |
| `전자계약_리마인드` | `전자계약_리마인드` | `전자계약_리마인드_DEV` |
---
## 3. 등록된 알림톡 템플릿
### 3.1 운영 템플릿 (mng.codebridge-x.com)
| 템플릿명 | 용도 | 상태 | 버튼 URL |
|---------|------|------|---------|
| `전자계약_서명요청` | 서명 요청 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` |
| `전자계약_완료` | 서명 완료 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` |
| `전자계약_리마인드` | 서명 독촉 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` |
### 3.2 개발 템플릿 (admin.codebridge-x.com)
| 템플릿명 | 용도 | 상태 | 버튼 URL |
|---------|------|------|---------|
| `전자계약_서명요청_DEV` | 서명 요청 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` |
| `전자계약_완료_DEV` | 서명 완료 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` |
| `전자계약_리마인드_DEV` | 서명 독촉 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` |
> 개발 템플릿 본문은 운영 템플릿과 동일하며, 버튼 URL 도메인만 다르다.
### 3.3 템플릿 변수
| 변수 | 용도 | 사용 템플릿 |
|------|------|-----------|
| `#{이름}` | 서명자 이름 | 서명요청, 완료, 리마인드 |
| `#{계약명}` | 계약 제목 | 서명요청, 완료, 리마인드 |
| `#{기한}` | 서명 기한 | 서명요청, 리마인드 |
| `#{완료일}` | 계약 완료일 | 완료 |
| `#{토큰}` | 서명자 액세스 토큰 | 버튼 URL |
---
## 4. 역할 기반 알림 흐름
### 4.1 전체 흐름
```
① 계약 발송 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡
② OTP 인증 ─→ 본사: 이메일 / 상대방: SMS
③ 다음 서명자 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡
④ 서명 완료 ─→ 본사: 이메일(PDF) / 상대방: 카카오톡(PDF 다운로드)
```
### 4.2 역할 판별
```php
$isCounterpart = $signer->role === EsignSigner::ROLE_COUNTERPART;
```
| 역할 | 상수 | 알림톡 | SMS(OTP) | 이메일 |
|------|------|--------|----------|--------|
| 본사 (creator) | `ROLE_CREATOR` | ❌ | ❌ | ✅ 항상 |
| 상대방 (counterpart) | `ROLE_COUNTERPART` | ✅ 우선 | ✅ OTP만 | ✅ 폴백 |
### 4.3 이메일 폴백 조건
상대방(counterpart)에게도 이메일을 보내는 경우:
- 전화번호가 없을 때 (`$signer->phone` 없음)
- 알림톡 발송 실패 시 (`$alimtalkFailed = true`)
- 발송 방식이 `email` 또는 `both`일 때
### 4.4 완료 알림 특수 처리
완료 알림톡 버튼은 **서명 페이지가 아닌 문서 다운로드 URL**로 강제 변경된다:
```php
// sendCompletionAlimtalk() 내부
$documentUrl = config('app.url') . '/esign/sign/' . $signer->access_token . '/api/document';
// 버튼 URL 강제 변경 (서명페이지 → 문서 다운로드)
if (str_contains($btn[$urlKey], '/esign/sign/') && !str_contains($btn[$urlKey], '/api/document')) {
$btn[$urlKey] = $documentUrl;
}
```
---
## 5. SMS (OTP 인증)
### 5.1 발송 조건
상대방(counterpart)이 `alimtalk` 또는 `both` 발송 방식이고 전화번호가 있을 때 SMS로 OTP 발송:
```php
if (in_array($sendMethod, ['alimtalk', 'both'])
&& $signer->phone
&& $signer->role === EsignSigner::ROLE_COUNTERPART) {
$this->sendOtpViaSms($contract, $signer, $otpCode);
}
```
### 5.2 SMS 발송 파라미터
| 항목 | 값 |
|------|-----|
| API | `BarobillService::sendSMSMessage()` |
| 발신번호 | `barobill_members.manager_hp` |
| 수신번호 | `esign_signers.phone` |
| 메시지 | `[SAM] 전자계약 인증코드: {코드} (5분 이내 입력)` |
| OTP 유효시간 | 5분 |
| 최대 시도 | 5회 |
### 5.3 SMS 실패 시 이메일 폴백
SMS 발송 실패 → 이메일 OTP 폴백 → 이메일도 없으면 500 에러 반환.
---
## 6. 바로빌 템플릿 등록 절차
### 6.1 관리자 페이지
```
https://www.barobill.co.kr 로그인 → 카카오톡 → 템플릿관리
```
### 6.2 DEV 템플릿 등록 시 주의사항
1. **본문**: 운영 템플릿과 **완전히 동일** (1글자도 다르면 안 됨)
2. **버튼 URL**: 도메인만 `admin.codebridge-x.com`으로 변경
3. **템플릿명**: 운영 이름 + `_DEV` 접미사 (예: `전자계약_서명요청_DEV`)
4. **검수 기간**: 영업일 기준 2~3일
### 6.3 새 템플릿 추가 시 체크리스트
- [ ] 바로빌에서 운영용 + 개발용 2개 등록
- [ ] 코드에서 `resolveTemplateName('기본명')`으로 호출
- [ ] 본문의 변수 치환 로직 추가 (str_replace)
- [ ] 버튼 URL의 `#{토큰}` 치환 확인
- [ ] 2단계 검증 (SendKey → GetSendKakaotalk) 포함
---
## 7. 관련 파일
| 파일 | 역할 |
|------|------|
| `app/Http/Controllers/ESign/EsignApiController.php` | 계약 발송, `sendAlimtalk()`, `resolveTemplateName()` |
| `app/Http/Controllers/ESign/EsignPublicController.php` | OTP SMS, 완료 알림톡, `sendCompletionAlimtalk()` |
| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트 (`sendATKakaotalkEx`, `sendSMSMessage`) |
| `app/Models/ESign/EsignSigner.php` | `ROLE_CREATOR`, `ROLE_COUNTERPART` 상수 |
| `app/Mail/EsignCompletedMail.php` | 완료 이메일 (PDF 다운로드 링크) |
| `app/Services/ESign/PdfSignatureService.php` | 서명 PDF 합성 (`mergeSignatures`) |
---
## 8. 트러블슈팅
### 8.1 환경별 템플릿 미스매치
**증상**: `ResultCode=4` (템플릿 데이터 일치 오류)
**원인**: 개발서버에서 운영용 템플릿(`전자계약_서명요청`)으로 발송 시 버튼 URL 도메인 불일치
**해결**: DEV 템플릿 등록 후 `APP_ENV``production`이 아닌지 확인
### 8.2 서명 PDF 누락 (이메일)
**증상**: 완료 이메일의 다운로드 링크가 서명 없는 초안 PDF 반환
**원인**: `mergeSignatures()` 실패 → `signed_file_path` 미설정 → preview PDF 폴백
**해결**: `downloadDocument()`가 완료 상태에서 자동 재생성 시도. 로그에서 trace 확인:
```bash
# 개발서버 로그 확인
ssh pro@114.203.209.83 "tail -100 /home/webservice/mng/storage/logs/laravel.log | grep 'PDF 서명'"
```
**주요 실패 원인**:
- `storage/fonts/Pretendard-Regular.ttf` 폰트 파일 누락
- FPDI/TCPDF 패키지 미설치 → `composer install` 필요
- `storage/app/esign/{tenant_id}/signed/` 디렉토리 권한 문제
### 8.3 MNG 모델 상수 누락
**증상**: `Undefined constant App\Models\ESign\EsignSigner::ROLE_COUNTERPART`
**원인**: API 프로젝트와 MNG 프로젝트의 모델이 독립적 — API에만 상수 정의됨
**해결**: MNG `EsignSigner.php`에도 동일한 상수 추가 (2026-02-26 핫픽스 완료)
---
## 관련 문서
- [바로빌 카카오톡 연동 README](./README.md) — SOAP API 전체 연동 가이드
- [E-Sign 기술 설계](../../projects/e-sign/technical-design.md) — 전자계약 아키텍처
- [E-Sign API 명세](../../projects/e-sign/api-specification.md) — API 엔드포인트
- [알림톡 연동 계획](../../plans/esign-alimtalk-integration.md) — 초기 계획 (구현 완료)
---
**최종 업데이트**: 2026-02-27

View File

@@ -1,173 +0,0 @@
# 명함신청 관리
> **작성일**: 2026-02-25
> **상태**: 구현 완료
---
## 1. 개요
### 1.1 목적
영업파트너가 명함을 신청하면 본사에서 제작소에 의뢰하고, 완료 후 처리하는 3단계 워크플로우를 제공한다.
### 1.2 워크플로우
```
요청(pending) ──제작의뢰──→ 제작중(ordered) ──처리완료──→ 완료(processed)
노랑 파랑 초록
```
### 1.3 메뉴 구조
| 메뉴 | URL | 대상 | 설명 |
|------|-----|------|------|
| 파트너 명함신청 | `/sales/business-cards` | 모든 사용자 | 신청폼 + 내 이력 |
| 명함신청 처리 | `/sales/business-cards/manage` | 관리자 전용 | 3단계 처리 + 뱃지 |
---
## 2. 테이블 구조
### 2.1 `business_card_requests`
| 필드 | 타입 | 설명 |
|------|------|------|
| `id` | bigint | PK |
| `tenant_id` | bigint | 테넌트 ID |
| `user_id` | bigint | 신청자 ID |
| `name` | varchar(50) | 성함 |
| `phone` | varchar(20) | 전화번호 |
| `title` | varchar(50) | 직함 (nullable) |
| `email` | varchar(100) | 이메일 (nullable) |
| `quantity` | int | 수량 (기본 100) |
| `memo` | text | 비고 (nullable) |
| `status` | varchar(20) | 상태: `pending`, `ordered`, `processed` |
| `ordered_by` | bigint | 제작의뢰 처리자 ID (nullable) |
| `ordered_at` | timestamp | 제작의뢰 일시 (nullable) |
| `processed_by` | bigint | 처리완료 처리자 ID (nullable) |
| `processed_at` | timestamp | 처리완료 일시 (nullable) |
| `process_memo` | text | 처리 메모 (nullable) |
| `created_at` | timestamp | 생성일 |
| `updated_at` | timestamp | 수정일 |
**인덱스**: `(tenant_id, status)`, `user_id`
---
## 3. 상태 전이
```
pending ──→ ordered ──→ processed
│ ▲
└── (역방향 전이 없음) ──┘
```
| 상태 | 라벨 | 색상 | 설명 |
|------|------|------|------|
| `pending` | 요청 | 노랑 | 파트너가 신청, 관리자 확인 대기 |
| `ordered` | 제작의뢰 | 파랑 | 관리자가 제작소에 의뢰 |
| `processed` | 처리완료 | 초록 | 제작 완료, 전달 완료 |
---
## 4. API 엔드포인트
| Method | Path | 이름 | 설명 |
|--------|------|------|------|
| GET | `/sales/business-cards` | `sales.business-cards.index` | 파트너 명함신청 (신청폼 + 이력) |
| POST | `/sales/business-cards` | `sales.business-cards.store` | 신청 등록 |
| GET | `/sales/business-cards/manage` | `sales.business-cards.manage` | 관리자 처리 화면 |
| POST | `/sales/business-cards/{id}/order` | `sales.business-cards.order` | 제작의뢰 (관리자) |
| POST | `/sales/business-cards/{id}/process` | `sales.business-cards.process` | 처리완료 (관리자) |
---
## 5. 파일 구조
### 5.1 API 프로젝트
| 파일 | 설명 |
|------|------|
| `database/migrations/2026_02_24_100000_create_business_card_requests_table.php` | 테이블 생성 |
| `database/migrations/2026_02_25_100000_add_ordered_columns_to_business_card_requests_table.php` | ordered 컬럼 추가 |
### 5.2 MNG 프로젝트
| 파일 | 설명 |
|------|------|
| `app/Models/Sales/BusinessCardRequest.php` | 모델 (상태 상수, 스코프, 헬퍼) |
| `app/Services/Sales/BusinessCardRequestService.php` | 서비스 (CRUD, 통계, 뱃지) |
| `app/Http/Controllers/Sales/BusinessCardRequestController.php` | 컨트롤러 |
| `app/Providers/ViewServiceProvider.php` | 사이드바 뱃지 연동 |
| `routes/web.php` | 라우트 5개 |
| `resources/views/sales/business-cards/admin-index.blade.php` | 관리자 뷰 |
| `resources/views/sales/business-cards/partner-index.blade.php` | 파트너 뷰 |
---
## 6. 화면 구성
### 6.1 파트너 명함신청 (`partner-index`)
```
┌─ 회사 정보 안내 (코드브릿지엑스) ──────────────┐
├─ 신청 폼 ─────────────────────────────────────┤
│ 성함* │ 직함 │ 전화번호* │ 이메일 │
│ 수량 │ 메모 │ [명함 신청하기] │
├─ 내 신청 이력 ────────────────────────────────┤
│ 신청일 │ 성함 │ 직함 │ 전화번호 │ 수량 │ 상태 │
│ (요청=노랑, 제작중=파랑, 처리완료=초록) │
└───────────────────────────────────────────────┘
```
- 로그인 사용자 정보(name, phone, email)로 자동 채움
- 관리자도 동일한 화면 접근 가능
### 6.2 명함신청 처리 (`admin-index`)
```
┌─ 통계 ──────────────────────────────────────┐
│ 신규요청(노랑) │ 제작의뢰(파랑) │ 오늘처리(초록) │ 전체 │
├─────────────────┬───────────────────────────┤
│ 신규 요청 │ 제작 중 │
│ [제작의뢰] 버튼 │ 의뢰일 + [처리완료] 버튼 │
├─────────────────┴───────────────────────────┤
│ 처리 완료 이력 (하단 스크롤 테이블) │
└─────────────────────────────────────────────┘
```
- 사이드바 뱃지: 요청 + 제작의뢰 합산 건수 표시
- 처리 버튼 클릭 시 `showConfirm()` 확인 다이얼로그
---
## 7. 뱃지 연동
`ViewServiceProvider`에서 `BusinessCardRequestService::getPendingCount()`를 호출하여 사이드바 메뉴 뱃지에 대기 건수를 표시한다.
- **카운트 기준**: `pending` + `ordered` 합산
- **표시 위치**: "명함신청 처리" 메뉴 (`sales.business-cards.manage`)
- **0건일 때**: 뱃지 미표시
---
## 8. 메뉴 등록 정보
| ID | parent_id | 이름 | URL | sort_order |
|----|-----------|------|-----|------------|
| 15507 | 15456 | 파트너 명함신청 | `/sales/business-cards` | 5 |
| 15508 | 15456 | 명함신청 처리 | `/sales/business-cards/manage` | 6 |
> 영업파트너에게는 "파트너 명함신청"만 보이도록 메뉴 권한 설정 필요
---
## 관련 문서
- 참고 패턴: `api/app/Models/CompanyRequest.php` (상태 관리 모델)
- 참고 뷰: `mng/resources/views/sales/managers/approvals.blade.php` (2분할 레이아웃)
---
**최종 업데이트**: 2026-02-25

View File

@@ -1,284 +0,0 @@
# 신용평가 시스템 (쿠콘 연동)
> **작성일**: 2026-03-02
> **상태**: 운영중
---
## 1. 개요
### 1.1 목적
SAM에서 거래처/협력업체의 **기업 신용정보를 조회**하여, 거래 안전성을 사전 판단하는 시스템이다.
### 1.2 핵심 원칙
- **쿠콘(KooCon/나이스평가정보)** API로 기업 신용정보 7개 항목 조회
- **국세청 공공데이터포털** API로 사업자등록 상태(영업/휴업/폐업) 확인
- 모든 조회 결과는 DB에 원본 저장 (감사 추적용)
- 테넌트별 월 5건 무료, 초과 시 건당 2,000원 과금
---
## 2. 시스템 구조
### 2.1 전체 흐름
```
사용자 (SAM MNG)
CreditController::search()
├──▶ CooconService::getAllCreditInfo()
│ ├── OA08: 기업 기본정보
│ ├── OA12: 신용요약정보
│ ├── OA13: 단기연체정보
│ ├── OA14: 신용도판단정보 (KCI)
│ ├── OA15: 신용도판단정보 (CB)
│ ├── OA16: 당좌거래정지정보
│ └── OA17: 법정관리/워크아웃
├──▶ NtsBusinessService::getBusinessStatus()
│ └── 국세청 사업자등록 상태 조회
└──▶ CreditInquiry::createFromApiResponse()
└── DB에 조회 이력 저장
```
### 2.2 파트너 구조
| 역할 | 대상 | 설명 |
|------|------|------|
| **API 제공사** | 쿠콘(KooCon) / 나이스평가정보 | 기업 신용정보 API 플랫폼 |
| **파트너사** | (주)코드브릿지엑스 | API 키 보유, 쿠콘과 직접 계약 |
| **이용사** | 각 테넌트 (주일, 경동 등) | SAM을 통해 신용조회 실행 |
---
## 3. 쿠콘(KooCon) API
### 3.1 API 엔드포인트
| 환경 | URL |
|------|-----|
| 테스트 | `https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp` |
| 운영 | `https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp` |
### 3.2 인증 방식
- **API_KEY**: 쿠콘에서 발급받은 인증키 (DB `coocon_configs` 테이블에서 관리)
- **API_ID**: 조회할 API 식별자 (OA08~OA17)
- **TR_SEQ**: 거래일련번호 (중복 방지용, `YmdHis` + 마이크로초 6자리)
### 3.3 요청 형식
```json
{
"API_KEY": "발급받은_API_키",
"API_ID": "OA12",
"TR_SEQ": "20260302173000123456",
"COMPANY_KEY": "1234567890"
}
```
- **Method**: POST
- **Content-Type**: application/json
- **Timeout**: 30초
### 3.4 API 목록
| API ID | 상수명 | 설명 | 데이터 출처 |
|--------|--------|------|------------|
| `OA08` | `API_COMPANY_INFO` | 기업 기본정보 | 나이스평가정보 |
| `OA12` | `API_CREDIT_SUMMARY` | 신용요약정보 (이슈 건수 요약) | 나이스평가정보 |
| `OA13` | `API_SHORT_TERM_OVERDUE` | 단기연체정보 | 한국신용정보원 |
| `OA14` | `API_NEGATIVE_INFO_KCI` | 신용도판단정보 (KCI) | 한국신용정보원 + 공공정보 |
| `OA15` | `API_NEGATIVE_INFO_CB` | 신용도판단정보 (CB) | 신용정보사 |
| `OA16` | `API_SUSPENSION_INFO` | 당좌거래정지정보 | 금융결제원 |
| `OA17` | `API_WORKOUT_INFO` | 법정관리/워크아웃정보 | 법원 |
### 3.5 응답 형식
```json
{
"RSLT_CD": "00000000",
"RSLT_MSG": "정상처리되었습니다.",
"RSLT_DATA": { ... }
}
```
- `RSLT_CD === '00000000'`: 성공
- 기타 값: 에러 (에러 메시지는 `RSLT_MSG`에 포함)
---
## 4. 국세청 사업자등록 조회 API
### 4.1 API 정보
| 항목 | 값 |
|------|------|
| URL | `https://api.odcloud.kr/api/nts-businessman/v1/status` |
| 인증 | serviceKey (쿼리 파라미터) |
| 출처 | 공공데이터포털 |
### 4.2 상태 코드
| 코드 | 상태 | 설명 |
|------|------|------|
| `01` | 계속사업자 | 정상 영업 중 |
| `02` | 휴업자 | 영업 중지 |
| `03` | 폐업자 | 사업 종료 |
---
## 5. 데이터베이스
### 5.1 `coocon_configs` — API 설정
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `name` | VARCHAR(100) | 설정 이름 |
| `environment` | ENUM('test', 'production') | 환경 |
| `api_key` | VARCHAR(100) | 쿠콘 API 키 |
| `base_url` | VARCHAR(255) | API 기본 URL |
| `description` | TEXT | 설명 |
| `is_active` | BOOLEAN | 활성화 여부 |
> **규칙**: 환경당 1개만 활성화 가능. 새 설정 활성화 시 기존 설정은 자동 비활성화.
### 5.2 `credit_inquiries` — 조회 이력
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 |
| `inquiry_key` | VARCHAR(32) UNIQUE | 조회 고유키 |
| `company_key` | VARCHAR(20) | 사업자번호/법인번호 |
| `company_name` | VARCHAR | 업체명 |
| `user_id` | BIGINT FK | 조회자 |
| `inquired_at` | TIMESTAMP | 조회 일시 |
| `nts_status` | VARCHAR(20) | 국세청 상태 |
| `nts_status_code` | VARCHAR(2) | 국세청 상태코드 |
| `short_term_overdue_cnt` | UINT | 단기연체 건수 |
| `negative_info_kci_cnt` | UINT | KCI 건수 |
| `negative_info_pb_cnt` | UINT | 공공정보 건수 |
| `negative_info_cb_cnt` | UINT | CB 건수 |
| `suspension_info_cnt` | UINT | 당좌거래정지 건수 |
| `workout_cnt` | UINT | 법정관리/워크아웃 건수 |
| `raw_*` | JSON | 각 API 원본 응답 (7개 + NTS) |
| `status` | ENUM | success / partial / failed |
---
## 6. 과금 정책
| 항목 | 값 |
|------|------|
| 월 무료 할당량 | **5건** |
| 초과 건당 요금 | **2,000원** |
| 계산식 | `max(0, (조회건수 - 5)) × 2,000` |
### 요금 예시
| 월 조회 건수 | 무료 | 유료 | 요금 |
|-------------|------|------|------|
| 3건 | 3 | 0 | 0원 |
| 5건 | 5 | 0 | 0원 |
| 10건 | 5 | 5 | 10,000원 |
| 20건 | 5 | 15 | 30,000원 |
---
## 7. 환경 설정
### 7.1 테스트/운영 분리
| 환경 | API URL | 설명 |
|------|---------|------|
| 테스트 | `dev2.coocon.co.kr:8443` | 개발/검증용 (과금 없음) |
| 운영 | `sgw.coocon.co.kr` | 실 서비스 (과금 발생) |
- `coocon_configs` 테이블에서 환경별로 별도 설정 관리
- 각 환경에서 `is_active=true`인 설정 1개만 사용
### 7.2 필요한 설정
| 항목 | 관리 위치 | 설명 |
|------|----------|------|
| 쿠콘 API 키 | DB (`coocon_configs`) | 쿠콘에서 발급 |
| 쿠콘 API URL | DB (`coocon_configs`) | 환경별 URL |
| 국세청 API 키 | 코드 내 하드코딩 | 공공데이터포털 발급 |
---
## 8. MNG 라우트
| Method | Path | 설명 |
|--------|------|------|
| GET | `/credit/inquiry` | 조회 이력 목록 |
| POST | `/credit/inquiry/search` | 신용정보 조회 실행 |
| POST | `/credit/inquiry/test` | API 연결 테스트 |
| GET | `/credit/inquiry/{key}/raw` | 원본 데이터 조회 |
| GET | `/credit/inquiry/{key}/report` | 리포트 조회 |
| DELETE | `/credit/inquiry/{id}` | 이력 삭제 |
| GET | `/credit/usage` | 조회회수 집계 |
| GET | `/credit/settings` | 설정 관리 |
| POST | `/credit/settings` | 설정 생성 |
| PUT | `/credit/settings/{id}` | 설정 수정 |
| DELETE | `/credit/settings/{id}` | 설정 삭제 |
| POST | `/credit/settings/{id}/toggle` | 활성화 토글 |
---
## 9. 에러 코드
### 9.1 쿠콘 API
| 코드 | 설명 |
|------|------|
| `NO_CONFIG` | API 설정 없음 |
| `HTTP_ERROR` | HTTP 통신 오류 |
| `EXCEPTION` | 예외 발생 |
| `RSLT_CD ≠ 00000000` | 쿠콘 API 에러 (RSLT_MSG 참조) |
### 9.2 국세청 API
| 코드 | 설명 |
|------|------|
| `INVALID_FORMAT` | 사업자번호 형식 오류 |
| `NOT_FOUND` | 조회 결과 없음 |
| `HTTP_ERROR` | HTTP 통신 오류 |
---
## 10. 관련 파일
### MNG 프로젝트
| 구분 | 경로 |
|------|------|
| 컨트롤러 | `app/Http/Controllers/Credit/CreditController.php` |
| 컨트롤러 | `app/Http/Controllers/Credit/CreditUsageController.php` |
| 서비스 | `app/Services/Coocon/CooconService.php` |
| 서비스 | `app/Services/Nts/NtsBusinessService.php` |
| 모델 | `app/Models/Coocon/CooconConfig.php` |
| 모델 | `app/Models/Credit/CreditInquiry.php` |
| 뷰 | `resources/views/credit/inquiry/index.blade.php` |
| 뷰 | `resources/views/credit/usage/index.blade.php` |
| 뷰 | `resources/views/credit/settings/index.blade.php` |
### API 프로젝트 (마이그레이션)
| 경로 |
|------|
| `database/migrations/2026_01_22_192637_create_coocon_configs_table.php` |
| `database/migrations/2026_01_22_201143_create_credit_inquiries_table.php` |
| `database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php` |
| `database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php` |
---
**최종 업데이트**: 2026-03-02

View File

@@ -1,122 +0,0 @@
# 문서관리 시스템 (Document Management)
> **상태**: API 완전 구현
> **최종 갱신**: 2026-02-27
---
## 1. 개요
EAV(Entity-Attribute-Value) 패턴 기반의 동적 문서 관리 시스템. 문서 서식(Template)을 정의하면 해당 서식에 따라 문서를 생성·결재·관리할 수 있다. 제품 검사(FQC), 공정 검사 등 다양한 문서 유형을 하나의 시스템으로 처리한다.
**핵심 기능:**
- 문서 서식(Template) 관리: 결재선, 기본필드, 섹션, 컬럼 정의
- EAV 기반 동적 데이터 저장 (section_id + column_id + row_index + field_key)
- 결재 워크플로우: 작성 → 검토 → 승인 (다단계)
- FQC(제품검사) 일괄 생성 및 진행 현황
- 첨부파일 관리 (서명, 이미지, 참조 문서)
---
## 2. 모델
### 서식 (Template) 계층
| 모델 | 설명 |
|------|------|
| `DocumentTemplate` | 서식 마스터 (이름, 카테고리, 회사 정보, 활성 여부) |
| `DocumentTemplateApprovalLine` | 결재선 (이름, 부서, 역할, 순서) |
| `DocumentTemplateBasicField` | 기본 필드 (라벨, 유형, 기본값) |
| `DocumentTemplateSection` | 섹션 (제목, 이미지, 순서) |
| `DocumentTemplateSectionField` | 섹션 필드 (field_key, 유형, 옵션, 필수 여부) |
| `DocumentTemplateColumn` | 컬럼 (라벨, 너비, 유형, 하위 라벨) |
| `DocumentTemplateLink` | 서식 간 연결 |
### 문서 (Document) 계층
| 모델 | 설명 | Traits |
|------|------|--------|
| `Document` | 문서 인스턴스 (서식 기반, 상태, 연결 대상) | BelongsToTenant, Auditable, SoftDeletes |
| `DocumentApproval` | 결재 기록 (단계, 역할, 상태, 코멘트) | BelongsToTenant |
| `DocumentData` | EAV 데이터 (section + column + row + field_key → value) | BelongsToTenant |
| `DocumentAttachment` | 첨부파일 (유형: general, signature, image, reference) | BelongsToTenant |
**문서 상태 흐름:**
```
DRAFT → PENDING → APPROVED
→ REJECTED → DRAFT (재작성)
→ CANCELLED
```
**컬럼 유형:** text, check, complex, select, measurement
---
## 3. 서비스
| 서비스 | 주요 메서드 |
|--------|-----------|
| `DocumentService` | list, show, create, update, destroy, submit, approve, reject, cancel, bulkCreateFqc, fqcStatus, resolve, upsert, formatTemplateForReact |
| `DocumentTemplateService` | list, show |
---
## 4. API 엔드포인트
### 서식 조회 (읽기 전용)
| HTTP | URI | 설명 |
|------|-----|------|
| GET | `/v1/document-templates` | 서식 목록 |
| GET | `/v1/document-templates/{id}` | 서식 상세 (필드·컬럼·섹션 포함) |
### 문서 CRUD + 워크플로우
| HTTP | URI | 설명 |
|------|-----|------|
| GET | `/v1/documents` | 문서 목록 (필터: status, template_id, 날짜, 검색) |
| POST | `/v1/documents` | 문서 생성 |
| GET | `/v1/documents/{id}` | 문서 상세 |
| PATCH | `/v1/documents/{id}` | 문서 수정 |
| DELETE | `/v1/documents/{id}` | 문서 삭제 |
| POST | `/v1/documents/{id}/submit` | 결재 요청 |
| POST | `/v1/documents/{id}/approve` | 승인 |
| POST | `/v1/documents/{id}/reject` | 반려 |
| POST | `/v1/documents/{id}/cancel` | 취소/회수 |
### 특수 기능
| HTTP | URI | 설명 |
|------|-----|------|
| POST | `/v1/documents/bulk-create-fqc` | FQC 일괄 생성 |
| GET | `/v1/documents/fqc-status` | FQC 진행 현황 |
| GET | `/v1/documents/resolve` | 카테고리+item_id로 문서 조회 |
| POST | `/v1/documents/upsert` | 생성 또는 업데이트 |
---
## 5. FormRequest
| Request | 주요 검증 |
|---------|----------|
| `StoreRequest` | template_id (필수, exists), title, approvers[], data[] (EAV), attachments[] |
| `UpdateRequest` | title, data[] (EAV), attachments[] |
| `IndexRequest` | status, template_id, search, 날짜 범위, 정렬 |
| `BulkCreateFqcRequest` | order_id, template_id, item_count |
| `ResolveRequest` | category, item_id |
| `ApproveRequest` | comment (선택) |
| `RejectRequest` | comment (필수) |
---
## 관련 문서
- [MNG 문서관리 시스템 상세](mng-document-system.md) — MNG 화면 구성, 탭별 기능, 서식 빌더, EAV 저장 패턴 상세
- [MNG 문서양식관리](mng-document-template.md) — 서식 생성/편집, Legacy/Block Builder, 프리셋, 연결품목 관리
- [DB 스키마 — 문서/전자서명](../../system/database/documents.md)
- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 적용
- Swagger: `/api-docs` → Documents 섹션
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,738 +0,0 @@
# 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

@@ -1,826 +0,0 @@
# 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

View File

@@ -1,129 +0,0 @@
# 주일기업 기획 메뉴
> **작성일**: 2026-03-06
> **상태**: 운영 중
> **프로젝트**: SAM MNG (관리자 웹)
> **라우트 접두사**: `/juil`
---
## 1. 개요
### 1.1 목적
블라인드/스크린 제조업체의 현장 관리를 위한 기획 도구 모음. 견적부터 공사, 준공까지의 업무 흐름과 현장 기록(사진대지), 회의 기록(STT/AI 요약)을 제공한다.
### 1.2 문서 구조
| 문서 | 설명 |
|------|------|
| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 아키텍처 |
| [construction-photos.md](construction-photos.md) | 공사현장 사진대지 기술 명세 |
| [meeting-minutes.md](meeting-minutes.md) | 회의록 작성 기술 명세 (STT/AI 통합) |
| [planning-views.md](planning-views.md) | 견적/프로젝트/워크플로우 화면 명세 |
### 1.3 하위 메뉴 구조
```
주일기업 기획
├── 견적/입찰/공사관리 /juil/estimate
├── 프로젝트관리/기성청구 /juil/project
├── 업무 Workflow /juil/workflow
├── 공사현장 사진대지 /juil/construction-photos
└── 회의록 작성 /juil/meeting-minutes
```
---
## 2. 아키텍처
### 2.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 뷰 | Blade + React (인라인) + Babel | 브라우저 트랜스파일 React 컴포넌트 |
| API | Laravel Controller + Service | JSON API (AJAX) |
| 모델 | Eloquent ORM | Multi-tenant (BelongsToTenant) |
| 파일 저장 | Google Cloud Storage | 사진, 오디오 파일 |
| AI | Gemini API (Vertex AI) | 요약, 화자 분리 |
| STT | Google Speech-to-Text V1/V2 + Web Speech API | 음성 인식 |
### 2.2 프로젝트 파일 구조
```
mng/
├── app/Http/Controllers/
│ ├── PlanningController.php ← 견적/프로젝트/워크플로우
│ ├── ConstructionSitePhotoController.php ← 사진대지 CRUD + 파일 관리
│ └── MeetingMinuteController.php ← 회의록 CRUD + AI 기능
├── app/Services/
│ ├── ConstructionSitePhotoService.php ← 사진대지 비즈니스 로직
│ └── MeetingMinuteService.php ← 회의록 + AI 통합 로직
├── app/Models/
│ ├── ConstructionSitePhoto.php ← 사진대지 모델
│ ├── ConstructionSitePhotoRow.php ← 사진 행 모델
│ ├── MeetingMinute.php ← 회의록 모델
│ └── MeetingMinuteSegment.php ← 회의 세그먼트 모델
└── resources/views/juil/
├── estimate.blade.php ← 견적/입찰/공사관리
├── project.blade.php ← 프로젝트관리/기성청구
├── workflow.blade.php ← 업무 Workflow
├── construction-photos.blade.php ← 사진대지 SPA
└── meeting-minutes.blade.php ← 회의록 SPA
```
### 2.3 기능별 구현 현황
| 기능 | 구현 방식 | 백엔드 | DB |
|------|----------|--------|-----|
| 견적/입찰/공사관리 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 |
| 프로젝트관리/기성청구 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 |
| 업무 Workflow | React 뷰 (정적 데이터) | PlanningController (뷰 반환만) | 없음 |
| 공사현장 사진대지 | React SPA + API | Controller + Service | 2 테이블 |
| 회의록 작성 | React SPA + API | Controller + Service + AI | 2 테이블 |
---
## 3. 외부 서비스 의존성
| 서비스 | 용도 | 추적 |
|--------|------|------|
| **Google Cloud Storage** | 사진/오디오 파일 저장 | `AiTokenHelper::saveGcsStorageUsage()` |
| **Google Speech-to-Text V2 (Chirp2)** | 자동 화자 분리 (최우선) | `AiTokenHelper::saveSttUsage()` |
| **Google Speech-to-Text V1** | 화자 분리 (V2 실패 시 폴백) | `AiTokenHelper::saveSttUsage()` |
| **Gemini API (Vertex AI)** | 요약 생성 + 화자 재분배 | `AiTokenHelper::saveGeminiUsage()` |
| **Web Speech API** | 브라우저 음성 입력 (현장명/설명) | `logSttUsage()` |
### 3.1 도메인 용어 힌트 (STT 정확도 향상)
```
블라인드, 스크린, 롤스크린, 허니콤, 버티컬,
원단, 바텀레일, 헤드레일, 브라켓,
주일, 경동, 주일블라인드, 경동블라인드,
수주, 발주, 납기, 출하, 재고, 원가, 단가,
SAM, ERP, MES
```
---
## 4. HTMX 전체 페이지 로드 규칙
모든 `/juil/*` 페이지는 React 인라인 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 컨트롤러 메서드에서 HTMX 요청 감지 시 **HX-Redirect로 전체 페이지 리로드를 강제**한다.
```php
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('juil.estimate'));
}
```
---
## 5. 관련 문서
- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 행 구조, 음성 입력
- [회의록 작성](meeting-minutes.md) — STT/화자분리/AI 요약, 오디오 녹음
- [견적/프로젝트/워크플로우](planning-views.md) — React 뷰 구성, 업무 프로세스 정의
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,275 +0,0 @@
# 공사현장 사진대지
> **작성일**: 2026-03-06
> **상태**: 운영 중
> **라우트**: `/juil/construction-photos`
> **관련**: [README.md](README.md) | [회의록](meeting-minutes.md) | [뷰 화면](planning-views.md)
---
## 1. 개요
건설/시공 현장의 작업 과정을 **작업전/작업중/작업후** 3단계 사진으로 기록하고 관리하는 기능. Google Cloud Storage에 사진을 저장하며, 음성 입력(Web Speech API)으로 현장명과 설명을 입력할 수 있다.
---
## 2. 라우트
```
/juil/construction-photos
├── GET / → index (목록 페이지)
├── GET /list → list (JSON 목록)
├── POST / → store (새 사진대지 등록)
├── POST /log-stt-usage → logSttUsage (STT 시간 기록)
├── GET /{id} → show (상세 조회)
├── PUT /{id} → update (메타데이터 수정)
├── DELETE /{id} → destroy (삭제)
├── POST /{id}/rows → addRow (행 추가)
├── DELETE /{id}/rows/{rowId} → deleteRow (행 삭제)
├── POST /{id}/rows/{rowId}/upload → uploadPhoto (사진 업로드)
├── DELETE /{id}/rows/{rowId}/photo/{type} → deletePhoto (사진 삭제)
└── GET /{id}/rows/{rowId}/download/{type} → downloadPhoto (다운로드)
```
---
## 3. 데이터베이스
### 3.1 construction_site_photos (사진대지)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `user_id` | BIGINT FK | 등록자 |
| `site_name` | VARCHAR(200) | 현장명 (필수) |
| `work_date` | DATE | 작업일자 (필수) |
| `description` | TEXT NULL | 설명 |
| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 |
**인덱스**: `tenant_id`, `user_id`, `(tenant_id, work_date)`
### 3.2 construction_site_photo_rows (사진 행)
각 사진대지는 1개 이상의 행을 가지며, 각 행에 3개 타입(before/during/after) 사진 저장.
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `construction_site_photo_id` | BIGINT FK | 부모 (cascade delete) |
| `sort_order` | INT | 정렬 순서 (0부터) |
| `before_photo_path` | VARCHAR(500) NULL | 작업전 GCS 경로 |
| `before_photo_gcs_uri` | VARCHAR(500) NULL | 작업전 GCS URI |
| `before_photo_size` | INT UNSIGNED NULL | 작업전 파일크기 (bytes) |
| `during_photo_path` | VARCHAR(500) NULL | 작업중 GCS 경로 |
| `during_photo_gcs_uri` | VARCHAR(500) NULL | 작업중 GCS URI |
| `during_photo_size` | INT UNSIGNED NULL | 작업중 파일크기 (bytes) |
| `after_photo_path` | VARCHAR(500) NULL | 작업후 GCS 경로 |
| `after_photo_gcs_uri` | VARCHAR(500) NULL | 작업후 GCS URI |
| `after_photo_size` | INT UNSIGNED NULL | 작업후 파일크기 (bytes) |
### 3.3 테이블 관계
```
construction_site_photos
│ 1:N
construction_site_photo_rows (sort_order ASC)
├── before_photo_* (작업전)
├── during_photo_* (작업중)
└── after_photo_* (작업후)
```
---
## 4. API 명세
### 4.1 목록 조회
```
GET /juil/construction-photos/list
```
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 현장명 검색 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `per_page` | int | 페이지당 건수 |
### 4.2 생성
```
POST /juil/construction-photos
```
| 필드 | 규칙 | 설명 |
|------|------|------|
| `site_name` | required, max:200 | 현장명 |
| `work_date` | required, date | 작업일자 |
| `description` | nullable, max:2000 | 설명 |
> 생성 시 빈 행 1개 자동 추가
### 4.3 사진 업로드
```
POST /juil/construction-photos/{id}/rows/{rowId}/upload
```
| 필드 | 규칙 | 설명 |
|------|------|------|
| `type` | required, in:before,during,after | 사진 타입 |
| `photo` | required, image, mimes:jpeg,jpg,png,webp, max:10240 | 최대 10MB |
### 4.4 사진 다운로드
```
GET /juil/construction-photos/{id}/rows/{rowId}/download/{type}?inline=1
```
| 파라미터 | 설명 |
|---------|------|
| `inline=1` | 브라우저 표시 (미지정 시 다운로드) |
---
## 5. GCS 저장 구조
### 5.1 경로 패턴
```
construction-site-photos/{tenant_id}/{photo_id}/{row_id}_{timestamp}_{type}.{ext}
```
**예시:**
```
construction-site-photos/1/42/15_1709723456_before.jpg
construction-site-photos/1/42/15_1709723456_during.jpg
construction-site-photos/1/42/15_1709723456_after.png
```
### 5.2 업로드 흐름
```
클라이언트 (Canvas 이미지 압축: 1920px, quality 80%)
FormData (multipart) 전송
컨트롤러: uploadPhoto()
서비스: uploadPhoto()
├── 기존 사진 있으면 GCS에서 삭제
├── GCS에 업로드
├── DB에 path + uri + size 저장
└── AiTokenHelper::saveGcsStorageUsage() 호출
응답: { success, data: Photo with rows }
```
### 5.3 삭제 흐름
```
사진 삭제: GCS 파일 삭제 → DB 필드 null
행 삭제: 행 내 모든 사진 GCS 삭제 → 행 삭제 → sort_order 재정렬
사진대지 삭제: 모든 행의 모든 사진 GCS 삭제 → soft delete
```
---
## 6. 음성 입력 (Web Speech API)
### 6.1 VoiceInputButton 컴포넌트
현장명, 설명 필드에 음성으로 텍스트 입력 가능.
```javascript
// Web Speech Recognition 설정
recognition.lang = 'ko-KR';
recognition.continuous = true;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
```
### 6.2 인식 상태
| 상태 | 표시 | 설명 |
|------|------|------|
| interim (미확정) | 이탤릭 + 회색 | 인식 중간 결과, 2초 후 소실 |
| final (확정) | 일반체 + 진한색 | 확정 텍스트, 영구 저장 |
### 6.3 사용량 추적
```
STT 사용 종료 시:
duration = Math.max(1, (Date.now() - startTime) / 1000)
POST /juil/construction-photos/log-stt-usage
body: { duration_seconds }
AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', seconds)
```
---
## 7. UI 구성 (React)
### 7.1 사진 타입별 색상
| 타입 | 라벨 | 배경색 | 뱃지색 |
|------|------|--------|--------|
| `before` | 작업전 | `bg-blue-50` | `bg-blue-100 text-blue-800` |
| `during` | 작업중 | `bg-yellow-50` | `bg-yellow-100 text-yellow-800` |
| `after` | 작업후 | `bg-green-50` | `bg-green-100 text-green-800` |
### 7.2 행 관리
- **행 추가**: sort_order 자동 계산 (마지막 + 1)
- **행 삭제**: 최소 1개 행 유지 필수
- **행별 사진**: 각 행에 3개 타입 사진 독립 업로드/삭제
---
## 8. 모델 메서드
### 8.1 ConstructionSitePhoto
```php
user() # BelongsTo User (등록자)
rows() # HasMany Row (sort_order ASC)
getPhotoCount(): int # 전체 사진 개수 (모든 행의 사진 합계)
```
### 8.2 ConstructionSitePhotoRow
```php
constructionSitePhoto() # BelongsTo 부모
hasPhoto(string $type): bool # 특정 타입 사진 존재 여부
getPhotoCount(): int # 이 행의 사진 개수 (0~3)
```
### 8.3 ConstructionSitePhotoService
```php
getList(array $filters) # 검색/필터 목록 (페이지네이션)
create(array $data) # 생성 + 빈 행 1개 자동 추가
update(ConstructionSitePhoto, array $data) # 메타데이터만 수정
delete(ConstructionSitePhoto) # GCS 전체 삭제 → soft delete
uploadPhoto(Row, UploadedFile, string $type) # GCS 업로드 + DB 기록
deletePhotoByType(Row, string $type) # 특정 타입 GCS 삭제
addRow(ConstructionSitePhoto) # 행 추가 (sort_order 자동)
deleteRow(Row) # 행 내 GCS 삭제 → 행 삭제 → 재정렬
```
---
## 관련 문서
- [README.md](README.md) — 기획 메뉴 전체 개요
- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록
- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,456 +0,0 @@
# 회의록 작성
> **작성일**: 2026-03-06
> **상태**: 운영 중
> **라우트**: `/juil/meeting-minutes`
> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [뷰 화면](planning-views.md)
---
## 1. 개요
음성으로 회의 내용을 기록하고, **Google STT(화자 분리)** + **Gemini AI(요약/결정사항/액션아이템)** 로 자동 정리하는 회의록 시스템. 브라우저 MediaRecorder로 녹음하고, GCS에 오디오를 저장하며, 세그먼트(화자별 발화)를 관리한다.
---
## 2. 라우트
```
/juil/meeting-minutes
├── GET / → index (목록 페이지)
├── GET /list → list (JSON 목록)
├── POST / → store (새 회의록 생성)
├── POST /log-stt-usage → logSttUsage (STT 시간 기록)
├── GET /{id} → show (상세 조회 + segments)
├── PUT /{id} → update (메타데이터 수정)
├── DELETE /{id} → destroy (삭제)
├── POST /{id}/segments → saveSegments (세그먼트 저장)
├── POST /{id}/upload-audio → uploadAudio (오디오 업로드)
├── POST /{id}/summarize → summarize (AI 요약 생성)
├── POST /{id}/diarize → diarize (자동 화자 분리)
└── GET /{id}/download-audio → downloadAudio (오디오 다운로드)
```
---
## 3. 데이터베이스
### 3.1 meeting_minutes (회의록)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `user_id` | BIGINT FK | 작성자 |
| `title` | VARCHAR(300) | 제목 (기본: "무제 회의록") |
| `folder` | VARCHAR(100) NULL | 폴더 분류 |
| `participants` | JSON NULL | 참여자 목록 배열 |
| `meeting_date` | DATE | 회의 날짜 |
| `meeting_time` | TIME NULL | 회의 시작 시간 |
| `duration_seconds` | INT UNSIGNED | 녹음 총 시간(초) |
| `audio_file_path` | VARCHAR(500) NULL | 오디오 GCS 경로 |
| `audio_gcs_uri` | VARCHAR(500) NULL | 오디오 GCS URI |
| `audio_file_size` | BIGINT UNSIGNED NULL | 오디오 파일 크기 (bytes) |
| `full_transcript` | LONGTEXT NULL | 전체 트랜스크립트 |
| `summary` | LONGTEXT NULL | AI 요약 |
| `decisions` | JSON NULL | 결정사항 배열 |
| `action_items` | JSON NULL | 액션아이템 배열 |
| `status` | VARCHAR(20) | 상태 (5가지) |
| `stt_language` | VARCHAR(10) | STT 언어 (기본: ko-KR) |
| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 |
**인덱스**: `tenant_id`, `user_id`, `(tenant_id, meeting_date)`, `status`
### 3.2 meeting_minute_segments (세그먼트)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `meeting_minute_id` | BIGINT FK | 회의록 (cascade delete) |
| `segment_order` | INT UNSIGNED | 순서 |
| `speaker_name` | VARCHAR(100) | 화자 이름 (기본: "화자 1") |
| `speaker_label` | VARCHAR(20) NULL | 화자 라벨/번호 |
| `text` | TEXT | 발화 텍스트 |
| `start_time_ms` | INT UNSIGNED | 시작 시간 (ms, 기본: 0) |
| `end_time_ms` | INT UNSIGNED NULL | 종료 시간 (ms) |
| `is_manual_speaker` | BOOLEAN | 수동 화자 전환 여부 (기본: true) |
**인덱스**: `meeting_minute_id`, `(meeting_minute_id, segment_order)`
### 3.3 테이블 관계
```
meeting_minutes
│ 1:N
meeting_minute_segments (segment_order ASC)
├── speaker_name (화자명)
├── text (발화 내용)
└── start_time_ms / end_time_ms (타임스탬프)
```
---
## 4. 상태 관리
### 4.1 상태값
| 상태 | 코드 | 색상 | 설명 |
|------|------|------|------|
| 초안 | `DRAFT` | 회색 | 생성 직후, 편집 가능 |
| 녹음중 | `RECORDING` | 빨강 | (클라이언트 상태) |
| 처리중 | `PROCESSING` | 노랑 | AI 요약/화자분리 처리 중 |
| 완료 | `COMPLETED` | 초록 | AI 처리 완료 |
| 실패 | `FAILED` | 빨강 | AI 처리 실패 |
### 4.2 상태 전이
```
DRAFT
↓ [오디오 업로드, 세그먼트 추가]
DRAFT (계속 편집)
↓ [summarize() 호출]
PROCESSING
COMPLETED (성공) 또는 FAILED (실패)
DRAFT
↓ [diarize() 호출 → 화자 분리]
DRAFT (세그먼트 갱신, 상태 유지)
```
---
## 5. API 명세
### 5.1 목록 조회
```
GET /juil/meeting-minutes/list
```
| 파라미터 | 타입 | 설명 |
|---------|------|------|
| `search` | string | 제목 검색 |
| `date_from` | date | 시작일 |
| `date_to` | date | 종료일 |
| `status` | string | 상태 필터 |
| `per_page` | int | 페이지당 건수 |
### 5.2 생성
```
POST /juil/meeting-minutes
```
| 필드 | 규칙 | 설명 |
|------|------|------|
| `title` | nullable, max:300 | 제목 (미입력 시 "무제 회의록") |
| `folder` | nullable, max:100 | 폴더 분류 |
| `participants` | nullable, array | 참여자 목록 |
| `meeting_date` | required, date | 회의 날짜 |
| `meeting_time` | nullable | 회의 시간 |
| `stt_language` | nullable, max:10 | STT 언어 (기본: ko-KR) |
### 5.3 세그먼트 저장
```
POST /juil/meeting-minutes/{id}/segments
```
```json
{
"segments": [
{
"speaker_name": "김과장",
"speaker_label": "1",
"text": "블라인드 납기일 확인 필요합니다.",
"start_time_ms": 0,
"end_time_ms": 5000,
"is_manual_speaker": true
}
]
}
```
> **전처리**: 빈 텍스트 필터링, 언더스코어 노이즈 제거, 다중 공백 정규화
> **자동 생성**: `full_transcript` = `[화자명] 발화텍스트\n...` 형식
### 5.4 오디오 업로드
```
POST /juil/meeting-minutes/{id}/upload-audio
```
| 필드 | 규칙 | 설명 |
|------|------|------|
| `audio` | required, file | webm/mp3 등 |
| `duration_seconds` | required, integer, min:1 | 녹음 시간(초) |
### 5.5 AI 요약 생성
```
POST /juil/meeting-minutes/{id}/summarize
```
**요청**: 없음 (서버에서 `full_transcript` 사용)
**응답 예시:**
```json
{
"success": true,
"message": "AI 요약이 완료되었습니다.",
"data": {
"summary": "블라인드 납품 일정과 현장 설치 계획을 논의했습니다...",
"decisions": [
"납품일을 3월 15일로 확정",
"현장 실측은 3월 10일 진행"
],
"action_items": [
{
"assignee": "김과장",
"task": "거래처에 납기 확인 연락",
"deadline": "2026-03-08"
}
],
"status": "COMPLETED"
}
}
```
### 5.6 자동 화자 분리
```
POST /juil/meeting-minutes/{id}/diarize
```
| 필드 | 설명 | 기본값 |
|------|------|--------|
| `min_speakers` | 최소 화자 수 | 2 |
| `max_speakers` | 최대 화자 수 | 6 |
**응답:**
```json
{
"success": true,
"message": "자동 화자 분리가 완료되었습니다. (3명 감지)",
"data": { /* Meeting with segments */ },
"speaker_count": 3
}
```
---
## 6. AI 통합 상세
### 6.1 화자 분리 (Diarization) 3단계 폴백
```
[1단계] Google STT V2 (Chirp2) ← 최우선
│ speechToTextWithDiarizationAuto()
│ 최신 모델, 높은 정확도
│ 도메인 용어 힌트 포함
↓ (실패 시)
[2단계] Google STT V1 (latest_long) ← 폴백
│ 안정적이지만 약간 덜 정확
↓ (1명만 인식 시)
[3단계] Gemini AI 화자 재분배
splitSpeakersWithGemini()
대화 맥락/호칭/질답 패턴/어투 변화 분석
2명 이상으로 재분배
```
### 6.2 요약 생성 (Gemini API)
```
입력: full_transcript (전체 트랜스크립트)
Gemini API 호출
├── 모드 1: Vertex AI (projectId, region, JWT)
└── 모드 2: Google AI Studio (API key) ← 폴백
│ Temperature: 0.3 (결정적)
│ Max tokens: 4096
출력 JSON:
{
"summary": "3-5문장 요약",
"decisions": ["결정사항 1", "..."],
"action_items": [
{ "assignee": "담당자", "task": "할일", "deadline": "기한" }
],
"keywords": ["키워드1", "..."]
}
```
### 6.3 Gemini 화자 재분배
Google STT가 1명만 인식할 때 Gemini로 대화 맥락 분석:
```
입력: 단일 화자 트랜스크립트 + 예상 화자 수
Gemini 프롬프트:
- 대화 맥락 분석 (호칭, 질답, 어투 변화)
- 지정된 수의 화자로 분리
출력: 화자별 세그먼트 배열
→ DB 세그먼트 교체
```
---
## 7. 오디오 관리 (GCS)
### 7.1 GCS 경로 패턴
```
meeting-minutes/{tenant_id}/{meeting_id}/{timestamp}.webm
```
### 7.2 녹음 흐름
```
브라우저 MediaRecorder API
├── navigator.mediaDevices.getUserMedia({ audio: true })
├── new MediaRecorder(stream)
├── recorder.ondataavailable → webm 블롭 수집
└── 녹음 종료 → FormData로 업로드
POST /{id}/upload-audio
├── GCS 업로드
├── DB: audio_file_path, audio_gcs_uri, audio_file_size, duration_seconds
└── AiTokenHelper::saveGcsStorageUsage()
```
### 7.3 다운로드
```
GET /{id}/download-audio
→ GCS에서 파일 콘텐츠 다운로드
→ Content-Disposition: attachment; filename="{title}.webm"
```
---
## 8. 세그먼트 처리 로직
### 8.1 저장 시 전처리
```php
// 1. 빈 텍스트 필터링
trim($segment['text']) !== ''
// 2. 언더스코어 노이즈 제거
str_replace('_', '', $text)
// 3. 다중 공백 정규화
preg_replace('/\s{2,}/', ' ', $text)
```
### 8.2 전체 트랜스크립트 자동 생성
```
[김과장] 블라인드 납기일 확인 필요합니다.
[박부장] 3월 15일로 확정합시다.
[김과장] 네, 거래처에 연락하겠습니다.
```
### 8.3 화자 분리 결과 세그먼트 변환
```
Google STT 결과 → MeetingMinuteSegment 변환:
{
segment_order: 순서,
speaker_name: "화자 N",
speaker_label: "N",
text: 발화 텍스트,
start_time_ms: 시작시간,
end_time_ms: 종료시간,
is_manual_speaker: false // 자동 분리
}
```
---
## 9. UI 구성 (React)
### 9.1 화자 색상
| 화자 | 배경색 | 뱃지색 |
|------|--------|--------|
| 화자 1 | `bg-blue-50` | `bg-blue-100 text-blue-800` |
| 화자 2 | `bg-green-50` | `bg-green-100 text-green-800` |
| 화자 3 | `bg-purple-50` | `bg-purple-100 text-purple-800` |
| 화자 4 | `bg-orange-50` | `bg-orange-100 text-orange-800` |
### 9.2 지원 언어
| 코드 | 라벨 |
|------|------|
| `ko-KR` | 한국어 |
| `en-US` | English |
| `ja-JP` | 日本語 |
| `zh-CN` | 中文 |
---
## 10. 사용량 추적
| 추적 항목 | 레이블 | Helper |
|----------|--------|--------|
| Web Speech API 사용 | `회의록-음성인식` | `AiTokenHelper::saveSttUsage()` |
| Google STT V1 화자 분리 | `회의록-화자분리` | `AiTokenHelper::saveSttUsage()` |
| Google STT V2 화자 분리 | `회의록-화자분리(Chirp2)` | `AiTokenHelper::saveSttUsage()` |
| GCS 오디오 저장 | `회의록-GCS저장` | `AiTokenHelper::saveGcsStorageUsage()` |
| Gemini 요약/분리 | `회의록-AI요약` | `AiTokenHelper::saveGeminiUsage()` |
---
## 11. 모델 메서드
### 11.1 MeetingMinute
```php
user() # BelongsTo User
segments() # HasMany Segment (segment_order ASC)
getFormattedDurationAttribute() # "H:MM:SS" 또는 "MM:SS"
```
**Cast**: `participants`, `decisions`, `action_items` → array, `meeting_date` → date
### 11.2 MeetingMinuteService
```php
# CRUD
getList(array $filters) # 검색/필터 목록
create(array $data) # 생성 (DRAFT)
update(MeetingMinute, array $data) # 수정
delete(MeetingMinute) # GCS 삭제 → soft delete
# 세그먼트
saveSegments(MeetingMinute, array $segments) # 전처리 + 저장 + 트랜스크립트 생성
uploadAudio(MeetingMinute, UploadedFile, int $seconds) # GCS 업로드
logSttUsage(int $seconds) # STT 사용량 기록
# AI
generateSummary(MeetingMinute) # Gemini 요약 생성
processDiarization(MeetingMinute, int $min, int $max) # 3단계 화자 분리
splitSpeakersWithGemini(string $text, int $expected) # Gemini 화자 재분배
```
---
## 관련 문서
- [README.md](README.md) — 기획 메뉴 전체 개요
- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력
- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,222 +0,0 @@
# 견적/프로젝트/워크플로우 화면 명세
> **작성일**: 2026-03-06
> **상태**: 뷰 구현 완료 (목데이터 기반, API 미연동)
> **라우트**: `/juil/estimate`, `/juil/project`, `/juil/workflow`
> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [회의록](meeting-minutes.md)
---
## 1. 개요
3개 화면 모두 **React 인라인 컴포넌트**(Babel 브라우저 트랜스파일)로 구현. 현재는 정적/목데이터 기반이며, 향후 API 연동 예정. PlanningController에서 뷰만 반환한다.
---
## 2. 견적/입찰/공사관리 (/juil/estimate)
### 2.1 개요
블라인드/스크린 설치 프로젝트의 견적서 작성, 입찰 관리, 공사 진행 현황을 한 화면에서 관리.
### 2.2 데이터 구조 (initialEstimates)
```javascript
{
id: "string",
name: "프로젝트명",
client: "고객사명",
status: "견적중|입찰|계약|공사중|준공",
amount: number, // 금액
startDate: "YYYY-MM-DD",
endDate: "YYYY-MM-DD",
manager: "담당자명",
items: [ // 품목 내역
{ name: "품목명", quantity: number, unitPrice: number }
]
}
```
### 2.3 공사관리 정보 (initialConstructionData)
```javascript
{
id: "string",
estimateId: "string", // 연결 견적
siteName: "현장명",
address: "현장 주소",
progress: number, // 진행률 (0~100)
workers: number, // 투입 인원
safetyChecks: [ // 안전점검
{ date: "YYYY-MM-DD", result: "합격|불합격", inspector: "점검자" }
]
}
```
### 2.4 상태별 배지 색상
| 상태 | 색상 |
|------|------|
| 견적중 | 파랑 |
| 입찰 | 보라 |
| 계약 | 초록 |
| 공사중 | 주황 |
| 준공 | 회색 |
### 2.5 SAM 연계
- 견적서 작성 시 SAM 견적 시스템(`features/quotes/`) 데이터 활용 가능
- 향후 `/juil/estimate` ↔ SAM 견적 API 연동 계획
---
## 3. 프로젝트관리/기성청구 (/juil/project)
### 3.1 개요
계약된 프로젝트의 현장 관리, 발주/청구/인건비 상태 추적, 기성 청구 관리.
### 3.2 데이터 구조 (initialProjects)
```javascript
{
id: "string",
name: "프로젝트명",
client: "발주처",
contractAmount: number, // 계약금액
status: "진행중|완료|보류",
sites: [ // 현장 목록
{
name: "현장명",
address: "주소",
progress: number // 진행률
}
],
orders: [ // 발주 내역
{
vendor: "거래처",
amount: number,
status: "발주|납품|정산"
}
],
claims: [ // 기성 청구
{
round: number, // 차수
amount: number, // 청구금액
claimDate: "YYYY-MM-DD",
status: "청구|승인|입금"
}
],
laborCosts: [ // 인건비
{
month: "YYYY-MM",
amount: number,
workers: number
}
]
}
```
### 3.3 금액 포맷 함수
```javascript
fmt(amount) // 1,234,567 (쉼표 포맷)
fmtBillion(amount) // 12.3억 (억 단위 축약)
```
---
## 4. 업무 Workflow (/juil/workflow)
### 4.1 개요
블라인드/스크린 사업의 전체 업무 프로세스를 단계별로 시각화. 각 프로세스에 담당 부서, 산출물, 서브스텝을 정의.
### 4.2 프로세스 데이터 구조
```javascript
{
id: "S1-1", // 프로세스 ID
phase: "영업", // Phase 명
name: "정보 수집", // 프로세스 이름
icon: "icon-name", // 아이콘
dept: "영업팀", // 담당 부서
color: "#3B82F6", // 테마 색상
description: "프로세스 설명",
documents: [ // 산출물 목록
"현장조사서", "고객요구사항서"
],
subSteps: [ // 상세 서브스텝
{
name: "서브스텝명",
description: "상세 설명",
responsible: "담당자/팀",
output: "산출물"
}
]
}
```
### 4.3 업무 Phase 목록
| Phase | ID 범위 | 설명 |
|-------|---------|------|
| **영업** | S1-1 ~ S1-4 | 정보 수집 → 현장 실측 → 고객 미팅 → 프로젝트 검토 |
| **견적서 작성** | S2-1 ~ S2-4 | 물량 산출 → 단가 산정 → 견적가 산출 → 견적서 작성/검토 |
| **입찰** | S3-* | 입찰 준비 → 제출 → 결과 확인 |
| **계약** | S4-* | 계약 협상 → 계약 체결 |
| **공사** | S5-* | 자재 발주 → 시공 → 현장 관리 |
| **준공** | S6-* | 검수 → 하자보수 → 준공 정산 |
### 4.4 SAM 연계 포인트
```javascript
// 견적서 작성 Phase에서 SAM 견적 화면으로 연결
{ samLink: '/juil/estimate', label: '견적서 작성 바로가기' }
```
---
## 5. 공통 특징
### 5.1 HTMX 전체 페이지 로드
3개 화면 모두 React 컴포넌트 사용하므로 HTMX 부분 로드 불가:
```php
// PlanningController의 모든 메서드
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('juil.xxx'));
}
return view('juil.xxx');
```
### 5.2 현재 구현 상태
| 항목 | 상태 |
|------|------|
| UI 화면 | 구현 완료 (React 인라인) |
| 목데이터 | 블레이드에 하드코딩 |
| API 연동 | 미연동 (향후 계획) |
| DB 테이블 | 미생성 (향후 계획) |
| CRUD 기능 | 뷰 조회만 (생성/수정/삭제 미구현) |
### 5.3 향후 개발 방향
1. 견적/프로젝트 DB 테이블 설계 (API 프로젝트)
2. API 엔드포인트 구현
3. React 컴포넌트 API 연동
4. SAM 견적 시스템과 데이터 동기화
---
## 관련 문서
- [README.md](README.md) — 기획 메뉴 전체 개요
- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력
- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록
- [견적 시스템](../quotes/README.md) — SAM 견적 관리 (BOM, 10단계 로직)
---
**최종 업데이트**: 2026-03-06

View File

@@ -1,110 +0,0 @@
# R&D 메뉴
> **작성일**: 2026-03-08
> **상태**: 운영 중
> **프로젝트**: SAM MNG (관리자 웹)
> **라우트 접두사**: `/rd`
---
## 1. 개요
### 1.1 목적
R&D 메뉴는 SAM 플랫폼의 **연구개발 및 내부 도구** 모음이다. AI 견적, 조직도 관리, 기획디자인(스토리보드 에디터), 안전점검 등 실험적이거나 내부 운영 목적의 기능을 제공한다.
### 1.2 문서 구조
| 문서 | 설명 |
|------|------|
| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 컨트롤러 매핑 |
| [planning-design.md](planning-design.md) | 기획디자인 스토리보드 에디터 기술 명세 |
| [design-insight.md](design-insight.md) | 디자인 인사이트 UI/UX 연구 도구 (100종 패턴, AI 프롬프트) |
### 1.3 하위 메뉴 구조
```
R&D
├── 대시보드 /rd
├── 조직도 관리 /rd/org-chart
├── 중대재해처벌법 점검 /rd/safety-audit
├── AI 견적 /rd/ai-quotation
│ ├── 목록 /rd/ai-quotation
│ ├── 생성 /rd/ai-quotation/create
│ ├── 상세 /rd/ai-quotation/{id}
│ ├── 편집 /rd/ai-quotation/{id}/edit
│ └── 문서 /rd/ai-quotation/{id}/document
├── 기획디자인 /rd/planning-design
└── 디자인 인사이트 /rd/design-insight
```
---
## 2. 아키텍처
### 2.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 뷰 | Blade + Alpine.js | 반응형 SPA (서버 렌더링 없음) |
| 컨트롤러 | `RdController` | 모든 R&D 라우트 처리 |
| 서비스 | `AiQuotationService` | AI 견적 비즈니스 로직 |
| 모델 | `AiQuotation`, `Employee`, `Department`, `Tenant` | Multi-tenant |
| 저장 | localStorage (기획디자인), DB (견적/조직도) | 용도별 분리 |
### 2.2 컨트롤러 구조
**파일**: `app/Http/Controllers/RdController.php`
| 메서드 | 라우트 | 설명 |
|--------|--------|------|
| `index()` | `GET /rd` | R&D 대시보드 |
| `orgChart()` | `GET /rd/org-chart` | 조직도 관리 |
| `orgChartAssign()` | `POST /rd/org-chart/assign` | 직원 부서 배치 |
| `orgChartUnassign()` | `POST /rd/org-chart/unassign` | 직원 부서 해제 |
| `orgChartReorder()` | `POST /rd/org-chart/reorder` | 직원 순서/이동 |
| `orgChartReorderDepts()` | `POST /rd/org-chart/reorder-depts` | 부서 순서 변경 |
| `orgChartToggleHide()` | `POST /rd/org-chart/toggle-hide` | 부서 숨기기/표시 |
| `safetyAudit()` | `GET /rd/safety-audit` | 중대재해처벌법 점검 |
| `quotations()` | `GET /rd/ai-quotation` | AI 견적 목록 |
| `createQuotation()` | `GET /rd/ai-quotation/create` | AI 견적 생성 폼 |
| `showQuotation()` | `GET /rd/ai-quotation/{id}` | AI 견적 상세 |
| `editQuotation()` | `GET /rd/ai-quotation/{id}/edit` | AI 견적 편집 |
| `documentQuotation()` | `GET /rd/ai-quotation/{id}/document` | AI 견적 문서 |
| `planningDesign()` | `GET /rd/planning-design` | 기획디자인 |
| `designInsight()` | `GET /rd/design-insight` | 디자인 인사이트 |
### 2.3 HTMX 전체 페이지 로드 규칙
모든 `/rd/*` 페이지는 Alpine.js 또는 React 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 메서드에서 `HX-Request` 감지 시 `HX-Redirect`로 전체 페이지 로드를 강제한다.
```php
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.planning-design'));
}
```
---
## 3. 기능별 구현 현황
| 기능 | 구현 방식 | 백엔드 | DB | 상태 |
|------|----------|--------|-----|------|
| R&D 대시보드 | Blade | AiQuotationService | ai_quotations | 운영 중 |
| 조직도 관리 | Blade + Alpine.js | RdController (직접 쿼리) | employees, departments | 운영 중 |
| 중대재해처벌법 점검 | Blade (정적) | 없음 | 없음 | 운영 중 |
| AI 견적 | Blade + Alpine.js | AiQuotationService | ai_quotations | 운영 중 |
| **기획디자인** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** |
| **디자인 인사이트** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** |
---
## 4. 관련 문서
- [기획디자인 스토리보드 에디터](planning-design.md) — 블록 에디터, 서식, 인쇄, 내보내기
- [디자인 인사이트](design-insight.md) — UI/UX 연구 도구 (100종 패턴, AI 프롬프트 복사)
- [조직도 관리 기술문서](../../projects/org-chart/) — 조직도 시스템 상세 (별도 프로젝트 문서)
---
**최종 업데이트**: 2026-03-08

View File

@@ -1,246 +0,0 @@
# 디자인 인사이트 — UI/UX 연구 도구
> **작성일**: 2026-03-08
> **상태**: 운영 중
> **라우트**: `GET /rd/design-insight`
> **뷰**: `resources/views/rd/design-insight/index.blade.php`
---
## 1. 개요
### 1.1 목적
디자인 인사이트는 UI/UX 패턴을 **수집·분석·비교**하는 연구 도구이다. 외부 서비스의 UI 레퍼런스를 카드 형태로 정리하고, CSS 와이어프레임 미리보기와 AI 프롬프트 생성 기능으로 디자인 의사결정을 지원한다.
### 1.2 핵심 기능
| 기능 | 설명 |
|------|------|
| 카드 관리 | 레퍼런스/분석/패턴/Before-After 4종 카드 CRUD |
| 프로젝트 관리 | 다중 프로젝트, localStorage 저장 |
| CSS 와이어프레임 | 100종 UI 패턴의 순수 CSS 미니 와이어프레임 |
| 프리셋 템플릿 | 인기 UI 패턴 100종 원클릭 불러오기 |
| AI 프롬프트 복사 | 카드 정보를 AI용 구조화 프롬프트로 변환·복사 |
| 3종 뷰 | 보드(카테고리별)/갤러리(그리드)/리스트 뷰 |
| JSON 내보내기/가져오기 | 프로젝트 데이터 백업/복원 |
---
## 2. 아키텍처
### 2.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 뷰 | Blade + Alpine.js | 단일 파일 SPA |
| 컨트롤러 | `RdController::designInsight()` | HX-Redirect 패턴 |
| 저장 | localStorage | `di_projects`, `di_current` 키 사용 |
| 백엔드 | 없음 | 서버 API 호출 없이 클라이언트 단독 동작 |
| 스타일 | 커스텀 CSS 변수 | `--di-*` 접두사 (Tailwind 미사용) |
### 2.2 데이터 구조
```json
{
"id": "di_1709000000_abc123",
"title": "프로젝트명",
"cards": [
{
"id": "di_1709000001_def456",
"type": "pattern",
"title": "KPI 대시보드",
"category": "dashboard",
"rating": 5,
"tags": ["대시보드", "KPI", "통계"],
"memo": "레퍼런스 설명",
"guidelines": "디자인 가이드라인",
"usedIn": ["Stripe", "Shopify"],
"components": [
{ "name": "KPI 요약 카드", "required": true },
{ "name": "필터 영역", "required": false }
],
"image": null,
"createdAt": "2026-03-08T00:00:00.000Z"
}
],
"createdAt": "2026-03-08T00:00:00.000Z",
"updatedAt": "2026-03-08T00:00:00.000Z"
}
```
---
## 3. 카드 유형 (4종)
| 코드 | 라벨 | 용도 |
|------|------|------|
| `reference` | 레퍼런스 | 외부 서비스 UI 스크린샷 수집 |
| `analysis` | 분석 | CRAP 원칙 등 UX 분석 (8가지 디자인 원칙 평가) |
| `pattern` | 패턴 | 재사용 가능한 UI 패턴 정의 |
| `comparison` | Before/After | 개선 전후 비교 (이미지 2장) |
---
## 4. 카테고리 (8종)
| 코드 | 라벨 | 아이콘 |
|------|------|--------|
| `dashboard` | 대시보드 | 📊 |
| `list` | 목록 | 📋 |
| `form` | 상세/폼 | 📝 |
| `modal` | 모달/팝업 | 💬 |
| `navigation` | 네비게이션 | 🧭 |
| `auth` | 로그인 | 🔐 |
| `report` | 보고서 | 📄 |
| `etc` | 기타 | 📎 |
---
## 5. CSS 와이어프레임 시스템
### 5.1 동작 원리
`getWireframe(card)` 함수가 카드의 `title``tags`를 키워드 매칭하여 해당 패턴에 맞는 순수 CSS/HTML 미니 와이어프레임을 반환한다.
```javascript
getWireframe(card) {
const t = (card.title || '').toLowerCase();
const tags = (card.tags || []).join(' ').toLowerCase();
const key = t + ' ' + tags;
if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `...`;
// ... 100종 패턴 매칭
return `기본 와이어프레임 (매칭 안 됨)`;
}
```
### 5.2 프리셋 100종 분포
| 카테고리 | 패턴 수 | 대표 패턴 |
|---------|---------|----------|
| 대시보드 | 10 | KPI, 실시간 모니터링, 게이지/미터, 히트맵, 퍼널 |
| 목록 | 10 | 데이터 테이블, 칸반 보드, 마스터-디테일, 피벗 테이블 |
| 상세/폼 | 16 | 프로필, 설정, 위지윅 에디터, 멀티스텝 폼, 태그 입력 |
| 모달/팝업 | 10 | 확인 다이얼로그, 라이트박스, 바텀시트, 컨텍스트 메뉴 |
| 네비게이션 | 10 | 사이드바, 탭, 브레드크럼, FAB, 앵커 스크롤 |
| 로그인 | 8 | 로그인 폼, 회원가입, 비밀번호 재설정, RBAC, API 키 |
| 보고서 | 9 | 인쇄용 보고서, 간트 차트, 조직도, 워터폴, 리포트 빌더 |
| 기타 | 27 | 댓글, 에러 페이지, FAQ, 캐러셀, 파일 매니저 등 |
---
## 6. AI 프롬프트 복사 기능
### 6.1 목적
카드에 정리된 UI 패턴 정보를 **AI가 이해할 수 있는 구조화된 마크다운 프롬프트**로 변환하여 클립보드에 복사한다. 복사한 프롬프트를 AI(Claude, ChatGPT 등)에 붙여넣으면 해당 스타일로 코드를 생성할 수 있다.
### 6.2 UI 위치
카드 상세 모달 상단, **편집** 버튼 왼쪽에 보라색 `✨ AI 프롬프트` 버튼으로 배치.
### 6.3 프롬프트 구조
`copyAiPrompt(card)` 함수가 카드 데이터를 다음 구조로 변환한다:
```markdown
## UI 패턴 구현 요청
아래 UI/UX 패턴 레퍼런스를 참고하여, 동일한 스타일과 구조로 코드를 작성해 주세요.
---
### 패턴 정보
- **패턴명**: {title}
- **카테고리**: {category label}
- **완성도 평점**: ★★★☆☆ ({rating}/5)
- **키워드**: {tags}
### 레퍼런스 설명
{memo}
### 실제 사용처 (벤치마킹 대상)
- {usedIn[0]}
- {usedIn[1]}
### 필수 구성 요소
**필수 (반드시 포함)**:
- ✅ {required component}
**선택 (권장)**:
- ○ {optional component}
### 디자인 가이드라인
{guidelines}
### 개선 제안
{suggestion}
### 기대 효과
{effect}
---
### 구현 요구사항
1. **기술 스택**: HTML + Tailwind CSS (또는 프로젝트에 맞는 프레임워크)
2. **반응형**: 모바일/태블릿/데스크톱 대응
3. **접근성**: 시맨틱 태그, ARIA 라벨, 키보드 네비게이션
4. **인터랙션**: hover, focus, active 상태 포함
5. **위 구성 요소를 모두 포함**하되, 실제 서비스처럼 자연스러운 더미 데이터 사용
6. **위 가이드라인을 충실히 반영**하여 UX 완성도를 높일 것
```
### 6.4 포함 필드 매핑
| 카드 필드 | 프롬프트 섹션 | 조건 |
|----------|-------------|------|
| `title` | 패턴 정보 > 패턴명 | 항상 |
| `category` | 패턴 정보 > 카테고리 | 항상 (라벨로 변환) |
| `rating` | 패턴 정보 > 평점 | 항상 (별점으로 변환) |
| `tags` | 패턴 정보 > 키워드 | 태그가 있을 때만 |
| `memo` | 레퍼런스 설명 | 값이 있을 때만 |
| `usedIn` | 실제 사용처 | 배열이 비어있지 않을 때만 |
| `components` | 필수 구성 요소 | 배열이 비어있지 않을 때만 |
| `guidelines` | 디자인 가이드라인 | 값이 있을 때만 |
| `suggestion` | 개선 제안 | 값이 있을 때만 |
| `effect` | 기대 효과 | 값이 있을 때만 |
| `principles` | UX 원칙 | `type === 'analysis'`일 때만 |
---
## 7. 뷰 모드 (3종)
| 모드 | 설명 | 정렬 |
|------|------|------|
| `board` | 카테고리별 컬럼 그룹핑 | 카테고리 → 생성순 |
| `gallery` | 그리드 갤러리 (와이어프레임 강조) | 필터 순 |
| `list` | 테이블형 리스트 | 필터 순 |
---
## 8. 파일 구조
```
resources/views/rd/design-insight/
└── index.blade.php # 단일 파일 SPA (~6,200줄)
├── <style> # 커스텀 CSS (~700줄)
├── HTML 템플릿 # Toolbar, 사이드바, 카드, 모달 (~1,300줄)
├── Alpine.js x-data # 상태 관리, CRUD, 필터 (~2,000줄)
├── getWireframe() # CSS 와이어프레임 100종 (~2,000줄)
└── loadPresetTemplates() # 프리셋 100종 데이터 (~1,200줄)
```
---
## 관련 문서
- [R&D 메뉴 개요](README.md) — 전체 R&D 메뉴 구조
- [기획디자인 스토리보드 에디터](planning-design.md) — 유사한 단일 파일 SPA 패턴
- [디자인 인사이트 기획서](../../plans/design-insight-menu-plan.md) — 초기 기획 문서
---
**최종 업데이트**: 2026-03-08

View File

@@ -1,366 +0,0 @@
# 기획디자인 — 스토리보드 에디터
> **작성일**: 2026-03-08
> **상태**: 운영 중
> **경로**: `/rd/planning-design`
> **뷰 파일**: `resources/views/rd/planning-design/index.blade.php`
---
## 1. 개요
### 1.1 목적
ERP 화면 기획서(스토리보드)를 **브라우저 내에서 직접 설계**하는 Notion/Figma 스타일의 블록 에디터. 별도 소프트웨어 없이 화면 와이어프레임, Description, 메뉴 트리를 작성하고 HTML 내보내기 및 인쇄까지 지원한다.
### 1.2 핵심 특징
| 항목 | 설명 |
|------|------|
| **프레임워크** | Alpine.js 단일 파일 SPA (서버 API 없음) |
| **저장 방식** | localStorage (`pc_projects` 키) |
| **블록 에디터** | 자유 배치 캔버스 (absolute positioning) |
| **서식 시스템** | 블록별 글자색/배경색/크기/굵기/정렬/z-index |
| **내보내기** | HTML 파일 다운로드 + 좌표 기반 인쇄 (A4 Landscape) |
---
## 2. 화면 구조
```
┌──────────────────────────────────────────────────────────┐
│ 프로젝트 툴바 (프로젝트명, 저장, 내보내기, 인쇄) │
├──────────────────────────────────────────────────────────┤
│ 블록 툴바 (H1, H2, 텍스트, 테이블, 콜아웃, ... 번호마커) │
├──────────────────────────────────────────────────────────┤
│ 문서 정보 바 (단위업무명, 버전, 페이지 네비) │
├────┬─────────────────────────────────────────────────────┤
│ │ 페이지 헤더 (경로, 화면명, 화면ID) │
│ 메 │ ┌─────────────────────────────────────────────────┐ │
│ 뉴 │ │ 캔버스 (자유 배치 블록 영역) │ │
│ 트 │ │ ┌──────┐ ┌────────────┐ ┌──────┐ │ │
│ 리 │ │ │ H1 │ │ 테이블 │ │ 01 │ │ │
│ │ │ └──────┘ └────────────┘ └──────┘ │ │
│ ◀▶ │ │ │ │
│ 리 │ └─────────────────────────────────────────────────┘ │
│ 사 ├─────────────────────────────────────────────────────┤
│ 이 │ Description 패널 (기능 설명 목록, 번호 뱃지 D&D) │
│ 저 │ 01 로그인 후 3초 내 전사 현황 표시 │
│ │ 02 실시간 수주 데이터 연동 │
├────┴─────────────────────────────────────────────────────┤
│ [플로팅 서식 툴바] B I ☰ A □ 13px ▲▼ ↺ │
│ [우클릭 컨텍스트 메뉴] 복제/잘라내기/삭제/색상/정렬/레이어 │
└──────────────────────────────────────────────────────────┘
```
### 2.1 리사이저
| 경계 | 방향 | 범위 |
|------|------|------|
| 메뉴 ↔ 캔버스 | 좌우 (col-resize) | 80px ~ 400px |
| 캔버스 ↔ Description | 상하 (row-resize) | 60px ~ 500px |
---
## 3. 데이터 구조
### 3.1 프로젝트 (localStorage: `pc_projects`)
```json
{
"sb": {
"docInfo": {
"projectName": "SAM ERP",
"unitTask": "품질관리",
"version": "D1.0"
},
"menuTree": [
{ "name": "대시보드", "children": [] },
{ "name": "품질관리", "children": [
{ "name": "제품검사관리" }
]}
],
"pages": [ /* Page[] */ ],
"currentPageIndex": 0
}
}
```
### 3.2 페이지 (Page)
```json
{
"id": "sp_1709856000000_a1b2",
"path": "품질관리 > 제품검사관리",
"screenName": "제품검사 목록",
"screenId": "QM-001",
"blocks": [ /* Block[] */ ],
"descriptions": [
{ "text": "검색 조건 입력 후 조회 버튼 클릭" }
]
}
```
### 3.3 블록 (Block)
```json
{
"id": "blk_1709856000000_x3y",
"type": "text",
"content": "텍스트 내용",
"x": 16,
"y": 100,
"w": 340,
"h": 50,
"style": {
"fontColor": "#ef4444",
"bgColor": "#fef2f2",
"fontSize": 14,
"bold": true,
"italic": false,
"textAlign": "center",
"zIndex": 11
}
}
```
### 3.4 블록 유형
| type | 설명 | 기본 크기 (W x H) | 고유 속성 |
|------|------|-------------------|----------|
| `heading` | 제목 (H1) | 400 x 40 | `content` |
| `heading2` | 소제목 (H2) | 350 x 36 | `content` |
| `text` | 텍스트 | 340 x 50 | `content` |
| `divider` | 구분선 | 400 x 20 | — |
| `callout` | 콜아웃 박스 | 240 x 60 | `content`, `icon` |
| `code` | 코드 블록 | 400 x 80 | `content` |
| `table` | 테이블 | 500 x 140 | `cols[]`, `rows[][]` |
| `button` | 버튼 모형 | 240 x 50 | `content`, `color` |
| `input` | 입력필드 모형 | 240 x 70 | `label`, `placeholder` |
| `select` | 셀렉트 모형 | 240 x 70 | `label`, `placeholder` |
| `card` | 카드 모형 | 300 x 90 | `title`, `content` |
| `badges` | 뱃지 모음 | 350 x 50 | `items[{text, color, textColor}]` |
| `todo` | 체크리스트 | 300 x 80 | `items[{text, checked}]` |
| `image` | 이미지 | 400 x 200 | `src` (base64 Data URL) |
| `marker` | 번호 마커 | 32 x 32 | `content` (01, 02, ...) |
### 3.5 블록 스타일 (style 객체)
| 속성 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| `fontColor` | string | — | 글자색 (CSS color) |
| `bgColor` | string | — | 배경색 (CSS color) |
| `fontSize` | number | 13 | 글자 크기 (px) |
| `bold` | boolean | false | 굵게 |
| `italic` | boolean | false | 기울임 |
| `textAlign` | string | `'left'` | 정렬 (`left`, `center`, `right`) |
| `zIndex` | number | 10 | 레이어 순서 (높을수록 앞) |
---
## 4. 기능 상세
### 4.1 블록 조작
| 기능 | 조작 방법 |
|------|----------|
| 블록 추가 | 상단 블록 툴바에서 유형 클릭 |
| 블록 선택 | 클릭 (단일), 올가미 드래그 (다중) |
| 블록 이동 | 드래그 앤 드롭 (단일/다중) |
| 블록 리사이즈 | 선택 후 우측/하단/우하단 핸들 드래그 |
| 블록 편집 | 더블클릭 → contenteditable 활성화 |
| 이미지 업로드 | 이미지 블록 더블클릭 → 파일 선택 (드래그 충돌 방지) |
| 블록 복제 | 우상단 ⧉ 버튼 또는 Ctrl+C → Ctrl+V |
| 블록 삭제 | 우상단 × 버튼 또는 Delete 키 |
| 블록 잘라내기 | Ctrl+X |
### 4.2 다중 선택 (올가미)
| 단계 | 동작 |
|------|------|
| 1 | 빈 캔버스 영역에서 마우스 드래그 시작 |
| 2 | 보라색 점선 사각형(lasso rect) 표시 |
| 3 | 마우스 놓으면 사각형과 겹치는 블록 전부 선택 (주황 테두리) |
| 4 | 선택된 블록 그룹을 드래그/복사/삭제 가능 |
| Ctrl+A | 전체 블록 선택 |
### 4.3 서식 설정
#### 플로팅 서식 툴바 (블록 선택 시 위에 나타남)
```
[ B ] [ I ] | [ ☰ ] [ ≡ ] [ ≡ ] | [ A▾ ] [ □▾ ] | [ 13px▾ ] | [ ▲ ] [ ▼ ] | [ ↺ ]
굵게 기울임 좌 가운데 우 글자색 배경색 크기 앞으로 뒤로 초기화
```
- 글자색 / 배경색: 클릭 시 12색 팔레트 드롭다운
- 글자 크기: 10px ~ 24px 선택 드롭다운
- 앞으로/뒤로: z-index 증감 (레이어 순서)
#### 우클릭 컨텍스트 메뉴
블록에서 마우스 오른쪽 버튼 클릭 시 표시:
| 메뉴 항목 | 단축키 | 설명 |
|----------|--------|------|
| 복제 | Ctrl+C → V | 블록 복사 + 즉시 붙여넣기 |
| 잘라내기 | Ctrl+X | 클립보드에 복사 후 삭제 |
| 삭제 | Del | 블록 삭제 |
| 글자색 ▸ | — | 12색 팔레트 서브메뉴 |
| 배경색 ▸ | — | 12색 팔레트 서브메뉴 |
| 왼쪽/가운데/오른쪽 정렬 | — | 텍스트 정렬 |
| 앞으로 가져오기 | — | z-index +1 |
| 뒤로 보내기 | — | z-index -1 |
| 굵게 / 기울임 | — | 토글 |
| 서식 초기화 | — | 모든 style 속성 제거 |
### 4.4 키보드 단축키
| 단축키 | 기능 |
|--------|------|
| `Ctrl+Z` | 실행 취소 (Undo) |
| `Ctrl+Y` | 다시 실행 (Redo) |
| `Ctrl+C` | 블록 복사 (단일/다중) |
| `Ctrl+V` | 블록 붙여넣기 |
| `Ctrl+X` | 블록 잘라내기 |
| `Ctrl+A` | 전체 블록 선택 |
| `Ctrl+S` | 프로젝트 저장 |
| `Delete` / `Backspace` | 선택된 블록 삭제 |
### 4.5 번호 마커 (Description 연동)
| 입력 방식 | 설명 |
|----------|------|
| 블록 툴바 입력 | 번호 입력 후 "번호" 버튼 클릭 → 캔버스에 마커 추가 (자동 증가) |
| Description 드래그 | Description 번호 뱃지를 캔버스로 드래그 앤 드롭 |
### 4.6 페이지 관리
| 기능 | 설명 |
|------|------|
| 페이지 추가 | + 버튼으로 새 페이지 생성 |
| 페이지 복사 | 현재 페이지 통째로 복제 (블록 ID 재생성) |
| 페이지 삭제 | 마지막 1페이지는 삭제 불가 |
| 페이지 이동 | ◀ ▶ 버튼으로 전환 |
### 4.7 Undo/Redo
- 최대 50단계 히스토리 유지
- 블록 추가/삭제/이동/리사이즈/서식 변경 모두 스냅샷 저장
- 히스토리 분기: 중간 상태에서 새 작업 시 이후 히스토리 폐기
### 4.8 작업 영역 극대화
| 기능 | 설명 |
|------|------|
| 사이드바 접기 | 좌측 패널 탭 바의 `<<` 버튼 → 메뉴트리 패널 숨김 |
| 사이드바 펼치기 | 좌측 가장자리 `>` 버튼 → 메뉴트리 패널 복원 |
| Description 접기 | Description 영역 상단 토글 바 클릭 → 패널 숨김 |
| 캔버스 폭 자동 확장 | 사이드바 접힘 시 페이지 폭 1100px → 1400px |
| 패딩 축소 | sb-editor padding 24px → 12px |
### 4.9 템플릿 시스템
| 유형 | 설명 |
|------|------|
| 프리셋 | 검색+목록, 상세 폼, CRUD, 대시보드 카드 등 기본 레이아웃 |
| 커스텀 | 현재 페이지 블록을 템플릿으로 저장 (localStorage: `sb_custom_templates`) |
---
## 5. 내보내기 & 인쇄
### 5.1 HTML 내보내기 (`sbExportHtml`)
- 모든 페이지를 단일 HTML 파일로 출력
- 블록은 **좌표 기반 absolute positioning**으로 배치 (WYSIWYG)
- 블록 스타일(글자색, 배경색, 크기 등) 반영
- `@media print` CSS 포함 → 브라우저 인쇄 지원
### 5.2 인쇄 미리보기 (`sbPrintPreview`)
- 새 창에서 인쇄 미리보기 HTML 생성
- A4 Landscape, 8mm 마진
- `window.print()` 자동 호출
- 페이지별 `page-break-after: always`
---
## 6. CSS 스타일 상속 구조
블록 컨테이너(`.sb-block`)에 인라인 스타일로 서식을 적용하고, 자식 요소에 `inherit` 규칙을 적용하여 상속한다.
```css
/* 부모에 color가 설정된 경우 자식에게 상속 */
.sb-block[style*="color"] .sb-blk-text,
.sb-block[style*="color"] .sb-blk-heading { color: inherit; }
/* font-size, font-weight, font-style, text-align 동일 패턴 */
```
> **배경**: 자식 요소(`.sb-blk-text`, `.sb-blk-heading` 등)에 하드코딩된 `color: #334155` 등이 있어, 단순히 부모에 `color`를 설정하면 CSS 우선순위에 의해 무시된다. `[style*="color"]` attribute selector로 부모에 인라인 스타일이 존재할 때만 `inherit`를 활성화한다.
---
## 7. 기술적 주의사항
### 7.1 저장 용량
- localStorage 제한: 브라우저별 약 5~10MB
- 이미지를 base64 Data URL로 저장하므로 다수의 이미지 사용 시 용량 초과 가능
- 향후 서버 저장(DB) 전환 검토 필요
### 7.2 Alpine.js 반응성
- 블록 데이터는 Alpine.js 반응형 객체로 관리
- `style` 속성은 `sbEnsureStyle(blk)`로 객체 초기화 후 속성 설정
- 배열 조작(`splice`, `push`)은 Alpine.js가 자동 감지
### 7.3 좌표 시스템
| 항목 | 단위 | 기준 |
|------|------|------|
| `blk.x`, `blk.y` | px | 캔버스 좌상단 (0,0) |
| `blk.w`, `blk.h` | px | 블록 폭/높이 |
| 드래그 계산 | clientX/Y + scroll offset | 뷰포트 → 캔버스 좌표 변환 |
| 올가미 | 캔버스 내부 좌표 | scroll 보정 포함 |
---
## 8. 파일 구조
```
mng/
├── app/Http/Controllers/
│ └── RdController.php ← planningDesign() 메서드 (L308)
└── resources/views/rd/planning-design/
└── index.blade.php ← 전체 CSS + HTML + Alpine.js (~4,430줄)
```
> **단일 파일 구조**: 이 기능은 서버 API가 없으며, 모든 CSS/HTML/JS가 `index.blade.php` 하나에 포함된다. Alpine.js `x-data` 객체 내에 모든 상태와 메서드가 정의되어 있다.
---
## 9. 향후 확장 가능성
| 기능 | 설명 | 우선순위 |
|------|------|---------|
| 스냅/그리드 정렬 | 블록 간 자석 가이드라인 | 높음 |
| 그룹핑 | 여러 블록을 하나의 그룹으로 묶기 | 중간 |
| 레이어 패널 | z-index 순서를 시각적으로 관리 | 중간 |
| DB 저장 | localStorage → DB 전환 (협업 지원) | 높음 |
| PDF 내보내기 | 직접 PDF 생성 | 낮음 |
| 리치 텍스트 | 블록 내 부분 텍스트 서식 (인라인 B/I/색상) | 중간 |
| 스냅샷/버전 관리 | 명시적 버전 저장 및 비교 | 낮음 |
| 이미지 드래그 업로드 | 외부 이미지를 캔버스로 드래그 앤 드롭 | 낮음 |
---
## 10. 관련 문서
- [R&D 메뉴 개요](README.md) — R&D 전체 메뉴 구조
- [MNG 구조](../../system/mng-structure.md) — MNG 관리자 패널 전체 구조
---
**최종 업데이트**: 2026-03-08