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

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

48
.gitignore vendored
View File

@@ -4,6 +4,9 @@
# 추적할 파일만 허용
!.gitignore
!CLAUDE.md
!INDEX.md
!README.md
!resources.md
# .claude 폴더 - 스킬/에이전트는 추적
!.claude/
@@ -14,22 +17,37 @@
!.claude/agents/
!.claude/agents/**
# sam 문서
!sam/
sam/*
!sam/docs/
!sam/docs/**
sam/docs/contracts/docx/backup/
# sam 배포/운영 문서
!sam/deploys/
!sam/deploys/**
!sam/front/
!sam/front/**
!sam/projects/
!sam/projects/**
# 문서 폴더 (루트 기준)
!assets/
!assets/**
!brochure/
!brochure/**
!changes/
!changes/**
!contracts/
!contracts/**
contracts/docx/backup/
!data/
!data/**
!dev/
!dev/**
!features/
!features/**
!frontend/
!frontend/**
!guides/
!guides/**
!plans/
!plans/**
!projects/
!projects/**
!requests/
!requests/**
!rules/
!rules/**
!system/
!system/**
# 기타
sam/sales
.DS_Store
_to_notion/

View File

@@ -0,0 +1,119 @@
# Gemini 모델 업그레이드: 2.0-flash → 2.5-flash
**날짜:** 2026-03-03
**작업자:** Claude Code
---
## 변경 개요
Google이 2026년 6월 1일부로 Gemini 2.0 Flash 모델 서비스를 종료한다는 통보를 받아, SAM 시스템 전체의 Gemini 모델을 `gemini-2.0-flash``gemini-2.5-flash`로 마이그레이션했다.
---
## 변경 사유
- Google의 공식 메일 통보: Gemini 2.0 Flash / 2.0 Flash-Lite → 2026-06-01 강제 종료
- 마이그레이션 경로: `gemini-2.0-flash``gemini-2.5-flash`
- API 키, Base URL 변경 없음 (모델명만 변경)
---
## 수정된 파일
### API 프로젝트 (`/home/aweso/sam/api`)
| 파일 | 변경 내용 |
|------|----------|
| `.env` | `GEMINI_MODEL=gemini-2.0-flash``gemini-2.5-flash` |
| `config/services.php` | fallback 기본값 `gemini-2.0-flash``gemini-2.5-flash` |
| `app/Services/AiReportService.php` | fallback 기본값 변경 |
### MNG 프로젝트 (`/home/aweso/sam/mng`)
| 파일 | 변경 내용 |
|------|----------|
| `.env` | `GEMINI_MODEL=gemini-2.0-flash``gemini-2.5-flash` |
| `config/services.php` | fallback 기본값 변경 |
| `app/Models/System/AiConfig.php` | `DEFAULT_MODELS['gemini']` 상수 + `getActiveGemini()` fallback 변경 |
| `app/Services/NotionService.php` | fallback 기본값 변경 |
| `resources/views/system/ai-config/index.blade.php` | UI placeholder, 기본값, JS defaultModels 변경 |
| `resources/views/google-cloud/ai-guide/index.blade.php` | 서비스 현황 테이블 모델명 7곳 변경 |
| `resources/views/academy/env-management.blade.php` | 환경변수 예시 테이블 변경 |
### 문서 (`/home/aweso/sam/docs`)
| 파일 | 변경 내용 |
|------|----------|
| `guides/ai-config-settings.md` | 기본 모델명 업데이트, 최종 업데이트 날짜 변경 |
| `guides/ai-management.md` | **신규** — AI 관리 종합 가이드 (아키텍처, 버전 이력, 온보딩) |
| `guides/ai-model-update-workflow.md` | **신규** — 모델 업데이트 표준 절차 (7단계 워크플로우) |
| `changes/20260303_gemini_model_upgrade.md` | **신규** — 이 변경 이력 문서 |
### 수정하지 않은 파일 (의도적)
| 파일 | 이유 |
|------|------|
| `api/database/migrations/2026_01_27_*.php` | 이미 실행된 마이그레이션 — 변경 시 DB 무결성 문제 |
| `api/database/migrations/2026_02_07_*.php` | 동일 |
| `api/database/migrations/2026_02_09_*.php` | 동일 |
| `mng/views/google-cloud/cloud-api-pricing/index.blade.php` | `2.0 → 2.5` 마이그레이션 안내 UI — 이전 모델명이 의도적 잔존 |
---
## 서버 .env 수정 필요 (배포 후)
| 환경 | 파일 | 변수 | 담당 |
|------|------|------|------|
| 개발서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 |
| 개발서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 |
| 운영서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 |
| 운영서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 |
수정 후 반드시 실행:
```bash
php artisan config:clear
```
---
## DB 단가 설정 필요
MNG `/system/ai-token-usage` → 단가 설정에서:
- 기존 `gemini-2.0-flash` 단가 → 비활성화
- 신규 `gemini-2.5-flash` 단가 추가:
- `input_price_per_million`: 0.15
- `output_price_per_million`: 0.60
- `exchange_rate`: 현재 환율
---
## 테스트 체크리스트
- [x] 로컬 .env 수정 완료
- [x] 코드 fallback 전체 변경 완료
- [ ] 로컬 연결 테스트 (MNG `/system/ai-config`)
- [ ] 개발서버 .env 수정 + config:clear
- [ ] 개발서버 연결 테스트
- [ ] 운영서버 .env 수정 + config:clear
- [ ] DB 단가 설정 (gemini-2.5-flash)
- [ ] 토큰 사용량 로그 확인 (새 모델명)
---
## 롤백 절차
문제 발생 시 `.env`만 되돌리면 즉시 복구:
```bash
# 모든 환경의 .env에서
GEMINI_MODEL=gemini-2.0-flash
php artisan config:clear
```
---
## 관련 문서
- [AI 관리 종합 가이드](../guides/ai-management.md)
- [모델 업데이트 워크플로우](../guides/ai-model-update-workflow.md)
- [AI 설정 기술문서](../guides/ai-config-settings.md)

View File

@@ -0,0 +1,165 @@
# 계좌 입출금내역 부분 월 조회 시 무한루프 크래시 수정
**날짜:** 2026-03-04
**작업자:** Claude Code
---
## 변경 개요
계좌 입출금내역 페이지에서 **날짜를 수동 입력**하여 조회 시 500 에러가 발생하는 문제를 수정했다.
편의 버튼(이번달, 지난달 등)은 항상 전체 월(1일~말일)을 사용하여 문제가 없었으나,
수동으로 날짜를 입력하면 **부분 월**(예: 12/01~12/18)이 되어 무한루프가 발생했다.
---
## 근본 원인
### `splitDateRangeMonthly()` 함수의 cursor 이동 버그
긴 기간 조회 시 바로빌 SOAP API의 한계로 인해 기간을 **월별 청크**로 분할하는 함수에서,
endDate가 **월 중간**일 때 cursor가 **같은 달 1일로 되돌아가** 무한루프가 발생했다.
```php
// ❌ 버그 코드 — endDate가 월 중간이면 무한루프
$cursor = $chunkEnd->copy()->addDay()->startOfMonth();
// 예시: endDate = 20251218
// chunkEnd = 20251218
// → addDay() = 20251219
// → startOfMonth() = 20251201 ← 같은 달 1일로 되돌아감!
// → while($cursor <= $end) 조건 여전히 true → 무한 반복
```
```php
// ✅ 수정 코드 — chunkStart 기준으로 다음 월로 이동
$cursor = $chunkStart->copy()->addMonth()->startOfMonth();
// 예시: startDate = 20251201
// chunkStart = 20251201
// → addMonth() = 20260101
// → startOfMonth() = 20260101 ← 다음 달로 정상 이동
// → while($cursor <= $end) 조건 false → 루프 종료
```
### 재현 조건
| 조건 | 결과 |
|------|------|
| 전체 월 (12/01~12/31) | 정상 — `addDay()` = 01/01 → `startOfMonth()` = 01/01 |
| 부분 월 (12/01~12/18) | **무한루프**`addDay()` = 12/19 → `startOfMonth()` = 12/01 |
| 다중 월 (12/01~02/18) | **무한루프** — 마지막 월이 부분 월이면 동일 증상 |
### 증상
- PHP 프로세스가 메모리 한도(256M/512M)에 도달하여 **Fatal Error로 크래시**
- Laravel 로그에 에러 기록 없음 (try-catch 밖에서 프로세스가 종료)
- 프론트엔드에 `서버 응답 오류 (500):` (빈 응답 본문)
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `app/Http/Controllers/Barobill/EaccountController.php` | `splitDateRangeMonthly()` cursor 이동 로직 수정 |
---
## 검증 결과
tinker에서 수정 전후 비교 테스트:
```
=== 수정 전 (버그): 20251201~20251218 ===
→ 같은 청크 무한 반복 (10회 제한으로 강제 중단)
=== 수정 후: 20251201~20251218 ===
→ [{start: 20251201, end: 20251218}] ← 1개 청크, 정상
=== 수정 후: 20251201~20260218 (다중 월) ===
→ [{20251201~20251231}, {20260101~20260131}, {20260201~20260218}] ← 3개 청크, 정상
=== 수정 후: 20251215~20251231 ===
→ [{start: 20251215, end: 20251231}] ← 1개 청크, 정상
```
---
## 동일 패턴 코드베이스 점검 결과
`sam/mng` 전체를 검색하여 유사 패턴을 점검했다:
| 파일 | 함수 | 패턴 | 위험도 |
|------|------|------|--------|
| `EaccountController.php` | `splitDateRangeMonthly()` | 월별 청크 분할 | ✅ 수정 완료 |
| `DashboardStatService.php` | `generateDateRange()` | `addDay()` 단순 증가 | 안전 |
| `InspectionCycle.php` | `getHolidayDates()` | `addDay()` 단순 증가 | 안전 |
| `CorporateCardController.php` | `getNextBusinessDay()` | `addDay()` 단순 증가 | 안전 |
| `PartitionManagementService.php` | `addPartitions()` | `for` 루프 (고정 횟수) | 안전 |
> **결론**: `EaccountController` 외에 동일 버그 패턴 없음.
> 다른 코드들은 모두 `addDay()` 단순 증가 패턴을 사용하여 무한루프 위험 없음.
---
## 교훈 및 방지 규칙
### R1. 날짜 cursor 이동 시 `chunkEnd` 기반 이동 금지
```php
// ❌ 위험: chunkEnd가 월 중간이면 startOfMonth()가 같은 달로 되돌림
$cursor = $chunkEnd->copy()->addDay()->startOfMonth();
// ✅ 안전: chunkStart 기준으로 항상 다음 월로 이동
$cursor = $chunkStart->copy()->addMonth()->startOfMonth();
```
### R2. 날짜 루프에 안전장치(max iterations) 추가 권장
```php
$maxIterations = 120; // 10년 = 120개월
$iterations = 0;
while ($cursor->lte($end) && $iterations < $maxIterations) {
// ... 청크 처리 ...
$iterations++;
}
if ($iterations >= $maxIterations) {
Log::error('날짜 분할 루프 안전장치 작동', compact('startDate', 'endDate'));
}
```
### R3. 부분 월 테스트 필수
날짜 범위를 분할하는 코드 작성/수정 시 반드시 다음 케이스를 테스트:
- [ ] 전체 월 (01일~말일)
- [ ] 부분 월 — 시작 (01일~중간)
- [ ] 부분 월 — 끝 (중간~말일)
- [ ] 다중 월 (마지막 월이 부분 월)
- [ ] 같은 날 (시작일 = 종료일)
---
## 부수 개선 사항
이 문제 조사 과정에서 추가로 발견/수정된 항목:
| 항목 | 내용 |
|------|------|
| WSDL 캐싱 | `WSDL_CACHE_NONE``WSDL_CACHE_BOTH` (4개 바로빌 컨트롤러 전체) |
| 소켓 타임아웃 | `default_socket_timeout` 60→120초 연장 |
| Shutdown handler | PHP Fatal Error 감지 시 Laravel 로그에 기록 |
| SOAP 호출 로깅 | 호출 시작/완료 시간 + 소요시간(ms) 기록 |
---
## 관련 문서
- `app/Http/Controllers/Barobill/EaccountController.php` — 바로빌 계좌 입출금내역
---
**최종 업데이트**: 2026-03-04

42
contracts/CHANGELOG.md Normal file
View File

@@ -0,0 +1,42 @@
# 계약서 개정이력
> **작성일**: 2026-02-22
> **관리 대상**: 전자계약 DOCX 4종
---
## v4.1 (2026-02-22)
**작성자**: 개발팀
**대상**: 고객사 서비스 이용계약서
- 제4조에 사용량 기반 추가 과금 조항(4.5) 추가
- 파일 저장 공간: 기본 100GB 초과 시 100GB당 100,000원/월
- AI 토큰: 월 100만 토큰 기본, 초과 시 1,000토큰 단위 실비 과금
- 제4조에 바로빌 부가 서비스 요금 조항(4.6) 추가
- 계좌조회, 카드내역, 세금계산서 발행 요금 명시
- 홈택스 매입/매출 조회는 회사 부담 명시
---
## v4.0 (2026-02-22)
**작성자**: 개발팀
- 계약서 버전 관리 시스템 도입
- DOCX → Markdown 미러링 체계 구축
- 4개 전자계약 문서에 개정이력 테이블 삽입
- 동기화 검증 스크립트 구축
### 대상 문서
| 파일 | 문서명 |
|------|--------|
| `01_고객_서비스이용계약서_v4_0_전자서명용.docx` | 고객사 서비스 이용계약서 |
| `비밀유지서약서.docx` | 비밀유지서약서 (NDA) |
| `영업파트너 위촉계약서.docx` | 영업파트너 위촉계약서 |
| `영업파트너 위촉계약서(단체용).docx` | 영업파트너 위촉계약서 (단체용) |
---
**최종 업데이트**: 2026-02-22 (v4.1)

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,458 @@
---
title: "고객사 서비스 이용계약서"
version: "v4.2"
date: "2026-02-24"
docx_file: "01_고객_서비스이용계약서_v4_0_전자서명용.docx"
---
# 고객사 서비스 이용계약서
Customer Service Agreement
계약번호:
계약일:
본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 간에 SAM 서비스 제공과 관련하여 다음과 같이 계약을 체결합니다.
## 제1조 (계약의 목적)
본 계약은 회사가 고객에게 SAM(Smart MES/ERP Solution) 서비스를 제공함에 있어 필요한 사항을 규정하고, 양측의 권리와 의무를 명확히 함을 목적으로 합니다.
## 제2조 (용어의 정의)
- **서비스**: 회사가 제공하는 SAM 클라우드 기반 MES/ERP 솔루션
- **SaaS**: Software as a Service (서비스형 소프트웨어)
- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것
- **액세스 제공**: 고객에게 서비스 사용 권한을 부여하는 것
- **검수 기간**: 서비스 게시 전 고객이 완성도를 확인하는 기간 (최대 1개월)
- **하자**: 계약서에 명시된 기능의 오류, 미구현, 성능 미달 등
- **하자담보 책임**: 서비스 게시 후 1년간 하자를 무상으로 수정하는 의무
## 제3조 (서비스 내용)
### 3.1 서비스 범위
회사는 다음의 서비스를 제공합니다:
- **맞춤형 개발**:
- 고객 요구사항에 맞춘 SAM 시스템 개발
- 개발 범위: [별첨 기획서 참조]
- 개발 기간: 계약일로부터 [ 3 ]개월
- **클라우드 제공** (SaaS):
- 연중무휴 24시간 접근 가능
- 자동 백업 및 보안
- **기술 지원**:
- 고객센터 운영 (평일 09:00~18:00)
- 이메일 지원 (24시간)
- 긴급 장애 대응
- **하자담보 책임** (1년):
- 서비스 게시일로부터 1년간 무상 수정
- 버그, 미구현 기능, 성능 개선 등
### 3.2 제공 방식
- 회사는 서비스를 **SaaS 방식**으로 제공합니다.
- 고객은 서비스에 대한 **사용 권한**만을 부여받으며, 소유권은 회사에 귀속됩니다.
- 소스코드는 제공되지 않습니다.
## 제4조 (비용 및 납부)
### 4.1 개발비
| 구분 | 금액 (부가세 별도) | 지급 시기 | 비고 |
| --- | --- | --- | --- |
| 1차 개발비 | 총 개발비의 50% | 계약 체결 시 | 착수금 |
| 2차 개발비 | 총 개발비의 50% | 서비스 게시일로부터 3일 이내 | 잔금 |
| 총 개발비 | [ ]원 | | |
### 4.2 월 구독료
| 구분 | 금액 (부가세 별도) | 지급 시기 | 비고 |
| --- | --- | --- | --- |
| 월 구독료 | 원 ~ | 매월 말일 | 후불제, 사용량 기준 청구 |
> ⚠️ 중요: - 월 구독료는 원이며, 영업 협상 및 개발 범위에 따라 증액될 수 있습니다.
- 계약 시 확정된 구독료: [ ]원/월
### 4.3 납부 방법
- **개발비**:
- 계좌이체 (세금계산서 발행)
- 입금 계좌: 기업은행 170-175519-04-011  (주)코드브릿지엑스
- **구독료**:
- CMS 자동이체 (권장)
- 또는 세금계산서 발행 후 계좌이체
### 4.4 잔금 지급 기한 [법률 검토 반영]
- **지급 기한**: 서비스 게시일로부터 **3일 이내**
- **사전 준비**: 회사는 영업 단계부터 납품 일정을 공유하여 고객이 미리 준비할 수 있도록 합니다.
- **미납 시 조치**: 제13조 참조
### 4.5 사용량 기반 추가 과금
기본 제공 한도 초과 시 다음과 같이 실비 과금됩니다.
| 항목 | 기본 제공 | 추가 과금 기준 |
| --- | --- | --- |
| 파일 저장 공간 | 100GB | 100GB당 100,000원/월 (부가세 별도) |
| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 과금 |
- **파일 저장 공간: **기본 100GB를 초과하는 경우 100GB 단위로 월 100,000원(부가세 별도)이 추가 과금됩니다.
- **AI 토큰: **월 100만 토큰 기본 제공되며, 초과 사용 시 1,000토큰 단위로 실비 과금됩니다.
- 미사용 잔여 토큰은 이월되지 않습니다. (매월 1일 갱신)
- 기본 제공량 80%, 100% 소진 시 자동 알림이 발송됩니다.
### 4.6 바로빌 부가 서비스 요금
고객이 선택적으로 이용하는 바로빌 연동 서비스의 요금은 다음과 같습니다.
| 서비스 | 과금 방식 | 기본 제공 | 추가 과금 |
| --- | --- | --- | --- |
| 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 |
| 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 |
| 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 |
- **바로빌 서비스 요금은 고객이 부담하며, 월 구독료와 별도로 청구됩니다.**
- 홈택스 매입/매출 조회 서비스(월 30,000원)는 회사가 부담합니다.
- 상기 금액은 부가세 별도입니다.
## 제5조 (마일스톤 및 진행 일정)
### 5.1 개발 단계 (5단계 통일)
| 단계 | 주요 활동 | 진행률 | 기간 | 납부 |
| --- | --- | --- | --- | --- |
| M1 | 요구사항 분석 및 기획 | 20% | [ 2 ]주 | 1차 개발비 (착수금 50%) |
| M2 | 설계 및 개발 착수 | 50% | [ 2 ]주 | - |
| M3 | 개발 진행 (50% 완료) | 60% | [ 2 ]주 | - |
| M4 | 개발 완료 및 테스트 | 80% | [ 2 ]주 | - |
| M5 | 검수 및 서비스 게시 | 100% | 최대 2주 | 2차 개발비 (잔금 50%) |
> ⚠️ 중요: - 5단계 마일스톤으로 통일 관리 - M5 검수 완료 후 서비스 게시 - 서비스 게시일로부터 3일 이내 잔금 납부
### 5.2 일정 조정
- 개발 일정은 고객의 협조에 따라 변동될 수 있습니다.
- 고객 귀책 사유로 인한 지연은 회사의 책임이 아닙니다.
- 불가항력으로 인한 지연 시 양측 협의하여 일정을 조정합니다.
## 제6조 (서비스 게시 및 검수)
### 6.1 서비스 게시
- 회사는 개발 완료 후 고객에게 **서비스 게시**를 통지합니다.
- **서비스 게시일**은 고객이 서비스에 접근 가능한 날짜를 의미합니다.
- 서비스 게시일부터 구독료가 발생합니다.
### 6.2 검수 기간
- 고객은 개발 완료 후 **최대 2주간 검수 기간**을 가집니다.
- 검수 기간은 서비스 게시 **전**에 이루어집니다.
- 검수 기간 중 발견된 하자는 회사가 무상으로 수정합니다.
### 6.3 검수 완료
- 고객이 서면으로 검수 완료를 통지하거나,
- 검수 기간 2주 종료 시점에 특별한 이의가 없으면 자동 승인으로 간주합니다.
- 검수 완료 후 서비스 게시일이 확정되고, 하자담보 책임 정책이 적용됩니다.
## 제7조 (하자담보 책임)
### 7.1 책임 기간
서비스 게시일로부터 1년 (소프트웨어산업진흥법 제16조, 민법 제667조)
### 7.2 하자담보 범위 (무상 처리)
| 항목 | 내용 | 예시 |
| --- | --- | --- |
| 버그 수정 | 소프트웨어 오류 | 계산 오류, 기능 미작동 |
| 미구현 기능 | 계약서에 명시된 기능 누락 | 약속된 기능 미구현 |
| 성능 개선 | 명시된 성능 기준 미달 | 속도 저하, 응답 지연 |
| UI/UX 수정 | 사용성 문제 | 버튼 미작동, 화면 깨짐 |
| 데이터 오류 | 데이터 손실 또는 오류 | 데이터 삭제, 중복 생성 |
| 보안 패치 | 보안 취약점 수정 | 해킹 방지, 암호화 |
### 7.3 제외 사항 (별도 비용)
| 항목 | 내용 | 예시 |
| --- | --- | --- |
| 신규 기능 개발 | 계약서에 없던 새 기능 | 새로운 모듈, 기능 확장 |
| 구조 변경 | 시스템 아키텍처 변경 | DB 구조, 프레임워크 교체 |
| 추가 모듈 | 새로운 모듈 개발 | 회계 모듈, 재고 모듈 |
| 기획 변경 | 초기 기획과 다른 요구사항 | 화면 구성, 프로세스 변경 |
| 교육/컨설팅 | 사용자 교육, 업무 컨설팅 | 직원 교육, 프로세스 개선 |
### 7.4 하자 처리 절차
| 단계 | 내용 | 기간 |
| --- | --- | --- |
| 1. 하자 신고 | 고객이 이메일로 하자 신고 | - |
| 2. 하자 확인 | 회사가 하자 여부 판정 | 3영업일 |
| 3. 수정 작업 | 하자 인정 시 무상 수정 | 7영업일 |
| 4. 검수 완료 | 고객이 수정 사항 확인 | - |
> ⚠️ 긴급 하자 (서비스 중단)는 24시간 이내 조치합니다.
### 7.5 책임 면제 사유
다음의 경우 하자담보 책임이 면제됩니다:
- **고객 귀책 사유**:
- 고객의 임의 수정 또는 변경
- 승인되지 않은 제3자 개입
- 사용 환경 미준수
- **불가항력**:
- 천재지변 (지진, 태풍 등)
- 전쟁, 테러, 전염병
- 정부 규제 또는 법령 변경
- **기간 만료**:
- 서비스 게시일로부터 1년 경과
## 제8조 (계약 해제 및 환불)
### 8.1 환불 정책 개요
고객의 임의 해제 권리와 회사의 투입 비용 보전의 균형을 고려하여 수립되었습니다.
### 8.2 단계별 환불
### Phase 1: 상담(인터뷰) 시작 전
- **환불율**: 100% (전액 환불)
- **조건**: 계약 후 상담(인터뷰) 배정 전
- **위약금**: 없음
- **임의 해제 가능**
### Phase 2: 상담(인터뷰) 시작 후, 개발 착수 전
| 진행 상황 | 환불율 | 공제 내역 |
| --- | --- | --- |
| M1: 기획안 작성 중 (50% 미만) | 80% | 상담매니저 및 기획/개발자 투입 비용 20% 공제 |
| M2: 기획안 완료 (50% 이상) | 50% | 상담매니저 및 기획/개발자 투입 비용 50% 공제 |
### Phase 3: 개발 진행 중 (5단계 마일스톤 기준)
| 마일스톤 | 진행률 | 청구 금액(개발비 대비) | 비고 |
| --- | --- | --- | --- |
| M3: 개발 진행 중 (50%) | 70% | 70% | 30% 환불 |
| M4: 개발 완료 및 테스트 | 90% | 90% | 10% 환불 |
| M5: 서비스 개시 완료 | 100% | 100% | 환불 불가 |
> ⚠️ 중요: 5단계 마일스톤으로 통일 관리
### Phase 4: 서비스 게시 후
- **환불율**: 0% (환불 불가)
- **개발비**: 전액 확정, 환불 불가
- **구독료**: 매월 말일 후불제이므로 사용한 만큼만 청구 (환불 개념 없음)
- **대신 제공**: 하자담보 책임 (1년) + 유지보수 (구독 기간 전체)
### 8.3 환불 불가 사유
- **고객 귀책 사유**:
- 협조 지연으로 인한 개발 지연
- 요구사항 변경으로 인한 추가 개발
- 승인 거부 또는 회피
- **약관 위반**:
- 허위 정보 제공
- 부정 사용 또는 재판매
- 회사 명예 훼손
### 8.4 할인 계약 해지 시 추가 조건
본 계약이 정상가 대비 할인 조건으로 체결된 경우, 다음 조건이 추가 적용된다.
- 발주자 귀책 해지 시 정상가(할인 전 금액) 기준으로 정산한다.
## 제9조 (구독 및 해지)
### 9.1 구독 시작
- **시작일**: 서비스 게시일 (검수 완료 후)
- **결제일**: 매월 말일
- **청구 방식**: 후불제 (해당 월 사용량 기준)
- **일할 계산**: (사용 일수 / 해당 월 일수) × 구독료
> ⚠️ 중요: - 계약 시 확정된 구독료 금액은 [ ]원/월입니다.
- 매월 말일에 해당 월 사용일수만큼만 후불 청구됩니다.
### 9.2 구독 해지
- 고객은 언제든지 구독을 해지할 수 있습니다. (위약금 없음)
- 해지 신청 후 30일간 데이터 백업 기간 제공
- 해지일로부터 30일 후 모든 데이터 완전 삭제
## 제10조 (유지보수 정책)
### 10.1 유지보수 개요
- **적용 대상**: 구독료를 정상 납부하는 고객
- **적용 기간**: 구독 기간 전체 (하자담보 책임 1년 이후에도 구독 중이면 계속 제공)
- **비용**: 월 구독료(500,000원)에 포함
### 10.2 하자담보 책임과의 차이
| 구분 | 하자담보 책임 (제7조) | 유지보수 (제9조의2) |
| --- | --- | --- |
| 기간 | 서비스 게시일로부터 1년 | 구독 기간 전체 |
| 근거 | 법적 의무 (소프트웨어산업진흥법) | 계약 조건 |
| 비용 | 무상 | 구독료에 포함 |
| 범위 | 하자(버그, 미구현 등) | 하자 + 일반 유지보수 |
### 10.3 유지보수 범위 (구독료에 포함)
> ✅ 무상 제공: - 모든 버그 수정 및 오류 처리 - 보안 패치 및 업데이트 - 성능 최적화 - 긴급 장애 대응 (24시간 이내) - 데이터 백업 및 복구 - 기술 지원 (고객센터, 이메일) - 플랫폼 업데이트 (프레임워크, 브라우저 호환성)
> ❌ 별도 비용: - 신규 기능 개발 - 커스터마이징 및 추가 개발 - 기획 변경 (화면 구성, 프로세스 변경) - 외부 시스템 연동 - 추가 교육 및 컨설팅
### 10.4 서비스 레벨 (SLA)
| 심각도 | 상황 | 응답 시간 | 해결 목표 |
| --- | --- | --- | --- |
| 긴급 (P0) | 서비스 완전 중단 | 1시간 | 24시간 |
| 높음 (P1) | 주요 기능 장애 | 4시간 | 3영업일 |
| 보통 (P2) | 일반 버그 | 1영업일 | 7영업일 |
| 낮음 (P3) | 문의/안내 | 1영업일 | 3영업일 |
### 10.5 정기 유지보수
- **월간**: 보안 패치, 백업 점검 (매월 첫째 주 일요일 새벽)
- **분기**: 성능 최적화 (분기 말 일요일 새벽)
- **반기**: 시스템 점검 (6월/12월 일요일 새벽)
> ⚠️ 모든 정기 점검은 최소 7일 전 사전 공지됩니다.
### 10.6 유지보수 신청
- **고객센터**: 02-6347-0005 (평일 09:00~18:00 )
- **이메일**: support@codebridge-x.com (24시간)
- **시스템 내**: SAM 시스템 내 고객지원 메뉴
### 10.7 유지보수 종료
다음의 경우 유지보수 서비스가 종료됩니다: 1. 구독 해지 시 2. 구독료 3개월 연속 미납 시 3. 중대한 약관 위반 시
## 제11조 (고객의 의무)
고객은 다음 사항을 준수해야 합니다:
- **정확한 정보 제공**: 허위 정보 제공 금지
- **협조 의무**: 개발에 필요한 자료 및 정보 제공
- **부정 사용 금지**: 서비스의 재판매, 재배포 금지
- **지적재산권 존중**: 무단 복제, 역설계 금지
## 제12조 (회사의 의무)
회사는 다음 사항을 준수합니다:
- **서비스 제공**: 계약서에 명시된 서비스 제공
- **하자담보 책임**: 1년간 하자 무상 수정
- **개인정보 보호**: 개인정보보호법 준수
- **기술 지원**: 고객센터 운영 및 기술 지원
## 제13조 (미입금 시 법적 조치)
### 13.1 개발비 미입금 절차
| 단계 | 시점 | 조치 내용 |
| --- | --- | --- |
| 1차 독촉 | 기한 경과 후 3일 | 이메일 및 SMS 발송 |
| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 |
| 추심등 | 기한 경과 후 14일 | 신용정보사 연체 등록, 법률대리인 위임 |
| 법적 조치 | 기한 경과 후 30일 | 지급명령 신청 또는 소송 제기 |
### 13.2 구독료 미입금 절차
| 단계 | 시점 | 조치 내용 |
| --- | --- | --- |
| 1차 실패 | 익일 | 재출금 |
| 2차 실패 | 기한 경과 후 3일 | 재출금 |
| 3차 실패 | 미수금 처리 | 서비스 접근 제한, 1차 독촉 |
| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 |
| 서비스 중단 | 기한 경과 후 14일 | 로그인 불가, 데이터 격리 |
| 강제 해지 | 기한 경과 후 30일 | 계약 해지, 법적 조치 검토 |
## 제14조 (개인정보 보호)
- 회사는 「개인정보 보호법」을 준수합니다.
- 고객의 개인정보는 서비스 제공 목적으로만 사용됩니다.
- 제3자에게 제공하지 않습니다. (법령 제외)
- 계약 종료 시 개인정보는 즉시 삭제됩니다. (법정 보관 의무 제외)
## 제15조 (지적재산권)
- **소유권**: 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다.
- **사용 권한**: 고객은 서비스 사용 권한만을 부여받습니다.
- **금지 사항**: 복제, 배포, 역설계, 재판매 금지
## 제16조 (손해배상)
- 회사 또는 고객이 본 계약을 위반하여 상대방에게 손해를 입힌 경우 배상 책임이 있습니다.
- 다만, 불가항력으로 인한 손해는 배상 책임에서 제외됩니다.
## 제17조 (불가항력)
다음의 사유로 서비스 제공이 불가능한 경우 회사는 책임을 지지 않습니다:
- 천재지변 (지진, 태풍, 홍수 등)
- 전쟁, 테러, 전염병
- 정부 규제 또는 법령 변경
- 제3자의 불법 행위 (해킹 등)
## 제18조 (분쟁 해결)
- 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다.
- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다.
## 제19조 (계약의 효력)
- 본 계약은 계약일로부터 효력이 발생합니다.
- 본 계약은 구독 해지 시까지 유효합니다.
## 제20조 (기타)
- 본 계약서는 2부 작성하여 회사와 고객이 각 1부씩 보관합니다.
- 본 계약의 해석 및 적용은 대한민국 법률을 준거법으로 합니다.
## 계약 당사자
### [회사]
상호: 주식회사 코드브릿지엑스
대표자: 이의찬
사업자등록번호: 664-86-03713
주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호
이메일: contact@codebridge-x.com
전화: 02-6347-0005
서명:
날짜:
### [고객]
상호:
대표자:
사업자등록번호:
주소:
이메일:
전화:
서명:
날짜:
## 별첨
### 별첨 1: 기획서
[별도 첨부]
### 별첨 2: 개발 일정표
[별도 첨부]
### 별첨 3: 기능 명세서
[별도 첨부]
주식회사 코드브릿지엑스
이메일: contact@codebridge-x.com
전화: 02-6347-0005
주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호

View File

@@ -0,0 +1,199 @@
---
title: "비밀유지서약서 (NDA)"
version: "v4.0"
date: "2026-02-22"
docx_file: "비밀유지서약서.docx"
---
# 비밀유지서약서 (NDA)
- **작성일**:
- **서약인 정보**
- **구분**:
- **인적 사항:**
상호(성명): _______________
대표자(본인): _______________
사업자등록번호(주민등록번호): ____________________
주소: ______________________________________________________________________
연락처: _______________
이메일: _______________
## 제1조 (목적)
- 본 서약서는 주식회사 코드브릿지(이하 “회사”)와의 업무 관계에서 알게 된 기밀 정보를
- 보호하기 위해 작성되었습니다.
## 제2조 (기밀 정보의 정의)
- 다음 각 호의 정보는 회사의 기밀 정보로 간주됩니다:
### 2.1 고객 정보
- 고객사 명단 (법인명, 대표자명, 연락처)
- 고객사 담당자 정보 (성명, 부서, 연락처, 이메일)
- 계약 내역 (가입비, 할인율, 구독료, 특약 사항)
- 고객사의 사업 정보 (매출, 직원 수, 거래처 등)
- 고객사가 회사에 요구한 개발 내역 및 기획 문서
### 2.2 영업 정보
- 가격 정책 (정가, 할인 정책, 최소 가입비)
- 수수료 정책 (비율, 지급 기준, 상계 방식)
- 영업 전략 및 마케팅 계획
- 잠재 고객 리스트
- 계약 체결 노하우 및 제안서 템플릿
### 2.3 기술 정보
- SAM 시스템의 소스코드
- 데이터베이스 구조 및 설계 문서
- 개발 프로세스 및 방법론
- 서버 인프라 구성 및 보안 정책
- API 키, 접속 정보, 관리자 권한
### 2.4 경영 정보
- 회사의 재무 정보 (매출, 이익, 원가)
- 조직도 및 인사 정보
- 사업 계획 및 전략
- 투자 유치 및 M&A 관련 정보
### 2.5 기타
- 회사가 **“기밀(Confidential)”** 또는 **“대외비”**로 표시한 모든 문서 및 정보
## 제3조 (기밀 유지 의무)
### 3.1 기본 의무
- 본인은 업무 수행 중 알게 된 모든 기밀 정보를:
- **외부에 누설하지 않습니다**
- **업무 목적 외에 사용하지 않습니다**
- **무단으로 복사, 복제, 전송하지 않습니다**
- **제3자에게 제공하거나 공개하지 않습니다**
### 3.2 정보 관리
- 기밀 문서는 안전한 장소에 보관
- 이메일, 메신저 등 전송 시 암호화
- 업무 종료 시 모든 기밀 자료 반환 또는 파기
- 개인 디바이스에 기밀 정보 저장 금지
### 3.3 제3자 접근 차단
- 가족, 친구 등 타인이 기밀 정보에 접근하지 못하도록 조치
- 공공장소(카페, 도서관 등)에서 기밀 정보 취급 금지
- 비밀번호 및 접속 정보 타인 공유 금지
## 제4조 (예외 사항)
- 다음의 정보는 기밀 정보에서 제외됩니다:
- 본인이 알기 전에 이미 공개된 정보
- 본인의 귀책사유 없이 공개된 정보
- 제3자로부터 적법하게 취득한 정보
- 본인이 독자적으로 개발한 정보
- 법원, 정부기관의 법적 요구에 따라 공개해야 하는 정보 (단, 회사에 사전 통지 필수)
## 제5조 (의무 기간)
### 5.1 기간
- 본 서약서의 기밀 유지 의무는:
- **계약 체결일로부터 효력 발생**
- **계약 종료 후 2년간 유지**
### 5.2 영구 보호
- 단, 다음 정보는 **영구적으로** 보호됩니다:
- 고객사 개인정보
- 회사의 영업 비밀 (부정경쟁방지법상 영업 비밀)
- 기술 정보 (특허, 저작권 대상)
## 제6조 (위반 시 책임)
### 6.1 민사 책임
- 본인이 본 서약을 위반하여 회사 또는 고객에게 손해를 입힌 경우:
- **실손해**** 배상**: 실제 발생한 손해 전액
- **징벌적 손해배상**: 실손해의 최대 3배 (악의적 유출 시)
- **법률 비용**: 소송 비용, 변호사 비용 등
### 6.2 형사 책임
- 다음의 경우 형사 고발 대상이 됩니다:
- **부정경쟁방지법** 위반 (영업 비밀 침해)
- **개인정보보호법** 위반 (고객 정보 유출)
- **정보통신망법** 위반 (기술 정보 침해)
- **형법** 위반 (업무상 배임)
- **※ 형사 처벌: 5년 이하 징역 또는 5천만원 이하 벌금**
### 6.3 계약 해지
- 회사는 본 서약 위반 시 즉시 계약을 해지할 수 있으며, 이미 지급한 수수료 또는
- 대금을 환수할 수 있습니다.
## 제7조 (자료 반환)
### 7.1 반환 대상
- 계약 종료 또는 요청 시 다음을 즉시 반환해야 합니다:
- 회사로부터 제공받은 모든 문서 (종이, 파일)
- 고객사 계약서 및 개인정보
- 가격표, 제안서, 템플릿 등 영업 자료
- USB, 하드디스크 등 저장 매체
### 7.2 파기 확인
- 반환 불가능한 파일(이메일, 클라우드 등)은 즉시 삭제하고, **삭제 확인서**를 회사에
- 제출해야 합니다.
## 제8조 (경업 금지)
### 8.1 경업 금지 기간
- 계약 종료 후 **6개월간** 다음 행위를 금지합니다:
- 회사의 고객에게 경쟁 제품 판매
- 회사의 기밀 정보를 이용한 유사 사업
- 회사 직원 또는 영업파트너를 스카우트
### 8.2 예외
- 단순히 경쟁사 제품을 판매하는 것은 허용되나, 회사의 기밀 정보를 활용해서는
- 안 됩니다.
## 제9조 (분쟁 해결)
### 9.1 관할 법원
- 본 서약과 관련된 분쟁은 회사 본사 소재지 관할 법원으로 합니다.
### 9.2 준거법
- 본 서약은 대한민국 법률에 따라 해석됩니다.
- **서약 확인**
- 본인은 위 내용을 충분히 이해하였으며, 이를 성실히 준수할 것을 서약합니다.
- **서약일**: ___________________
- **서약인**
상호(성명): _______________
대표자(본인): _______________
주민등록번호(또는 사업자등록번호): _______________
- **서명 또는 인**: _______________
- **수령인 (주식회사 ****코드브릿지엑스****)**
- 대표이사: 이의찬
- **확인****일**: ___________________
- **서명 또는 인**: _______________
- **참고: 관련 법률**
- **부정경쟁방지법 제2조 (영업비밀)**
- “영업비밀”이란 공공연히 알려져 있지 아니하고 독립된 경제적 가치를 가지는 것으로서,
- 비밀로 관리된 생산방법, 판매방법, 그 밖에 영업활동에 유용한 기술상 또는 경영상의
- 정보를 말한다.
- **부정경쟁방지법 제18조 (벌칙)**
- 영업비밀을 외국에서 사용하거나 외국에서 사용되게 할 목적으로 취득·사용 또는 제3자에게 누설한 자는 **15년 이하의 징역** 또는 **15억원 이하의 벌금**에 처한다.
- **※ 본 서약서는 2부를 작성하여 회사와 서약인이 각 1부씩 보관합니다.**
- **※ 서약 위반 시 민·형사상 책임을 질 수 있습니다.**

View File

@@ -0,0 +1,276 @@
---
title: "영업파트너 위촉계약서"
version: "v4.0"
date: "2026-02-22"
docx_file: "영업파트너 위촉계약서.docx"
---
# < 영업파트너 위촉계약서 >
# Sales Partner Engagement Agreement
- 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다.
## 제1조 (계약의 목적)
- 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를
- 명확히 함을 목적으로 합니다.
## 제2조 (용어의 정의)
- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너
- **관리자**: 판매자를 관리하고 지원하는 상급 영업파트너
- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용
- **수수료**: 파트너가 영업 활동의 대가로 받는 보상
- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것
## 제3조 (파트너의 역할 및 업무)
### 3.1 판매자의 역할
- 잠재 고객 발굴 및 초기 접촉
- SAM 서비스 소개 및 제안
- 고객과의 계약 체결 지원
- 계약 후 고객 관리 및 사후 지원
### 3.2 관리자의 역할
- 판매자 모집 및 관리
- 판매자 교육 및 지원
- 영업 전략 수립 및 실행
- 회사와 판매자 간 소통 중재
### 3.3 공통 의무
- 회사의 브랜드 이미지 유지
- 정확한 정보 제공
- 윤리적 영업 활동 준수
- 비밀 유지 의무
## 제4조 (수수료 정책)
### 4.1 수수료 비율
| 역할 | 수수료 비율 | 산정 기준 |
| --- | --- | --- |
| 판매자 | 개발비의 20% | 1차,2차 입금액 기준 |
| 관리자 | 개발비의 5% | 1차,2차 입금액 기준 |
### 4.2 수수료 산정 예시
- **총 개발비 80,000,000원 계약 시**
| 단계 | 고객 입금 | 판매자 수수료 (20%) | 관리자 수수료 (5%) |
| --- | --- | --- | --- |
| 1차 착수금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 |
| 2차 잔금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 |
| 총계 | 80,000,000원 | 16,000,000원 | 4,000,000원 |
- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다.
### 4.3 지급 시기
- **지급일**: 고객 입금일 **익월 10일**
- **지급 방식**: 계좌 이체
- **세금**: 3.3% 원천징수 (사업소득)
### 4.4 수수료 지급 조건
- 고객이 개발비를 실제로 입금한 경우에만 지급
- 계약 해지 또는 환불 시 수수료 미지급 또는 환수
- 파트너가 계약 위반 시 수수료 지급 보류
## 제5조 (수수료 정책 변경)
### 5.1 사전 고지 의무
- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다.
- 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다.
- 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다.
### 5.2 변경 효력
- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다.
- 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다.
- 진행 중인 계약은 최초 약정 조건을 유지합니다.
### 5.3 변경 예시
#### 예시 1: 수수료율 변경
- 고지일: 2026년 2월 1일
- 변경 내용: 판매자 수수료 20% → 18%
- 적용일: 2026년 3월 1일 이후 체결 계약부터
#### 예시 2: 수수료 정책 폐지
- 고지일: 2026년 2월 1일
- 변경 내용: 수수료 정책 완전 폐지
- 적용일: 2026년 3월 1일 이후 체결 계약부터
## 제6조 (계약 기간)
- 본 계약은 계약일로부터 **1년간** 유효합니다.
- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다.
- 자동 연장은 동일한 조건으로 반복됩니다.
## 제7조 (계약 해지)
### 7.1 일반 해지 (양방향)
- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다.
- **통지 방법**: 이메일 또는 등기우편
- **효력 발생**: 통지일로부터 30일 후
- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급
- **예시**:
- 통지일: 2026년 2월 1일
- 해지일: 2026년 3월 1일
- 2월 중 발생한 수수료는 3월 10일 정상 지급
### 7.2 즉시 해지 사유
- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다:
- **(1) 품위 유지 결격사유 발생 [신설]**
- 음주운전으로 적발된 경우
- 형사 범죄로 기소 또는 구속된 경우
- 사회적 물의를 일으킨 경우
- 기타 파트너로서의 품위를 훼손한 경우
- **(2) 계약 위반**
- 허위 정보 제공 또는 사기 행위
- 회사 명예 훼손 또는 영업 방해
- 비밀 유지 의무 위반
- 중대한 업무 태만
- **(3) 부정 행위**
- 고객으로부터 금품 수수
- 계약서 위조 또는 변조
- 회사 자산 횡령 또는 유용
### 7.3 즉시 해지 시 조치
- 미지급 수수료는 지급하지 않습니다.
- 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외)
- 진행 중인 계약은 회사가 직접 관리합니다.
## 제8조 (비밀 유지)
### 8.1 비밀 정보
- 다음 정보는 비밀로 유지되어야 합니다:
- 회사의 영업 전략 및 계획
- 고객 정보 (회사명, 담당자, 연락처 등)
- 수수료 정책 및 계약 조건
- 기술 정보 및 노하우
- 회사 내부 자료
### 8.2 비밀 유지 의무
- 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다.
- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다.
- 위반 시 손해배상 책임이 있습니다.
## 제9조 (지적재산권)
- SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다.
- 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다.
- 영업 활동에 필요한 자료는 회사가 제공합니다.
## 제10조 (세금 및 원천징수)
### 10.1 사업소득
- 파트너 수수료는 **사업소득**으로 간주됩니다.
### 10.2 원천징수
| 항목 | 비율 | 비고 |
| --- | --- | --- |
| 소득세 | 3.0% | |
| 지방소득세 | 0.3% | 소득세의 10% |
| 합계 | 3.3% | |
### 10.3 지급명세서
- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다.
## 제11조 (손해배상)
### 11.1 파트너의 귀책 사유
- 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다:
- 허위 정보 제공으로 계약 취소
- 고객과의 분쟁으로 회사 명예 훼손
- 비밀 유지 의무 위반
- 부정 행위
### 11.2 회사의 귀책 사유
- 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다.
## 제12조 (분쟁 해결)
- 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다.
- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다.
## 제13조 (기타 사항)
### 13.1 계약서 교부
- 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다.
### 13.2 통지
- 모든 통지는 다음의 연락처로 발송됩니다:
- **회사**:
- 이메일: admin@codebridge-x.com
- 전화: 02-6347-0005
- **파트너**:
- 이메일:
- 전화:
### 13.3 준거법
- 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다.
- **계약 당사자**
- **[회사]**
- **상호**: 주식회사 코드브릿지엑스
- **대표자**: 이의찬 (인)
- **사업자등록번호**: 664-86-03713
- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호
- **이메일**: admin@codebridge-x.com
- **전화**: 02-6347-0005
- **날짜**:
- **[파트너]**
- **상호/성명**:
- **대표자/본인**: (서명)
- **사업자등록번호**:
- **주소**:
- **이메일**:
- **전화**:
- **날짜**:
- **별첨**
#### 별첨 1: 수수료 정산표
| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 |
| --- | --- | --- | --- | --- | --- | --- |
| | | | | | | |
#### 별첨 2: 영업 활동 보고서
| 날짜 | 활동내용 | 고객사 | 진행 상황 |
| --- | --- | --- | --- |
| | | | |
- 첨부 서류
- 사업자등록증 사본 (사업자인 경우)
- 주민등록등본 사본 (개인인 경우)
- 통장 사본 (수수료 입금용)
- 비밀유지서약서
- **주식회사 코드브릿지엑스**
- 이메일: admin@codebridge-x.com
- 전화: 02-6347-0005
- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호

View File

@@ -0,0 +1,267 @@
---
title: "영업파트너 위촉계약서 (단체용)"
version: "v4.0"
date: "2026-02-22"
docx_file: "영업파트너 위촉계약서(단체용).docx"
---
# < 영업파트너 위촉계약서 >
# Sales Partner Engagement Agreement
- 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다.
## 제1조 (계약의 목적)
- 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를
- 명확히 함을 목적으로 합니다.
## 제2조 (용어의 정의)
- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너
- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용
- **수수료**: 파트너가 영업 활동의 대가로 받는 보상
- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것
## 제3조 (파트너의 역할 및 업무)
### 3.1 판매자의 역할
- 잠재 고객 발굴 및 초기 접촉
- SAM 서비스 소개 및 제안
- 고객과의 계약 체결 지원
- 계약 후 고객 관리 및 사후 지원
### 3.2 공통 의무
- 회사의 브랜드 이미지 유지
- 정확한 정보 제공
- 윤리적 영업 활동 준수
- 비밀 유지 의무
## 제4조 (수수료 정책)
### 4.1 수수료 비율
| 역할 | 수수료 비율 | 산정 기준 |
| --- | --- | --- |
| 판매자 | 개발비의 30% | 1차,2차 입금액 기준 |
### 4.2 수수료 산정 예시
- **총 개발비 80,000,000원 계약 시**
| 단계 | 고객 입금 | 판매자 수수료 (30%) |
| --- | --- | --- |
| 1차 착수금 (50%) | 40,000,000원 | 12,000,000원 |
| 2차 잔금 (50%) | 40,000,000원 | 12,000,000원 |
| 총계 | 80,000,000원 | 24,000,000원 |
- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다.
### 4.3 지급 시기
- **지급일**: 고객 입금일 **익월 10일**
- **지급 방식**: 계좌 이체
- **세금**: 사업소득일 경우 3.3% 원천징수
### 4.4 수수료 지급 조건
- 고객이 개발비를 실제로 입금한 경우에만 지급
- 계약 해지 또는 환불 시 수수료 미지급 또는 환수
- 파트너가 계약 위반 시 수수료 지급 보류
## 제5조 (수수료 정책 변경)
### 5.1 사전 고지 의무
- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다.
- 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다.
- 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다.
### 5.2 변경 효력
- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다.
- 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다.
- 진행 중인 계약은 최초 약정 조건을 유지합니다.
### 5.3 변경 예시
#### 예시 1: 수수료율 변경
- 고지일: 2026년 2월 1일
- 변경 내용: 판매자 수수료 20% → 18%
- 적용일: 2026년 3월 1일 이후 체결 계약부터
#### 예시 2: 수수료 정책 폐지
- 고지일: 2026년 2월 1일
- 변경 내용: 수수료 정책 완전 폐지
- 적용일: 2026년 3월 1일 이후 체결 계약부터
## 제6조 (계약 기간)
- 본 계약은 계약일로부터 **1년간** 유효합니다.
- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다.
- 자동 연장은 동일한 조건으로 반복됩니다.
## 제7조 (계약 해지)
### 7.1 일반 해지 (양방향)
- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다.
- **통지 방법**: 이메일 또는 등기우편
- **효력 발생**: 통지일로부터 30일 후
- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급
- **예시**:
- 통지일: 2026년 2월 1일
- 해지일: 2026년 3월 1일
- 2월 중 발생한 수수료는 3월 10일 정상 지급
### 7.2 즉시 해지 사유
- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다:
- **(1) 품위 유지 결격사유 발생 [신설]**
- 음주운전으로 적발된 경우
- 형사 범죄로 기소 또는 구속된 경우
- 사회적 물의를 일으킨 경우
- 기타 파트너로서의 품위를 훼손한 경우
- **(2) 계약 위반**
- 허위 정보 제공 또는 사기 행위
- 회사 명예 훼손 또는 영업 방해
- 비밀 유지 의무 위반
- 중대한 업무 태만
- **(3) 부정 행위**
- 고객으로부터 금품 수수
- 계약서 위조 또는 변조
- 회사 자산 횡령 또는 유용
### 7.3 즉시 해지 시 조치
- 미지급 수수료는 지급하지 않습니다.
- 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외)
- 진행 중인 계약은 회사가 직접 관리합니다.
## 제8조 (비밀 유지)
### 8.1 비밀 정보
- 다음 정보는 비밀로 유지되어야 합니다:
- 회사의 영업 전략 및 계획
- 고객 정보 (회사명, 담당자, 연락처 등)
- 수수료 정책 및 계약 조건
- 기술 정보 및 노하우
- 회사 내부 자료
### 8.2 비밀 유지 의무
- 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다.
- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다.
- 위반 시 손해배상 책임이 있습니다.
## 제9조 (지적재산권)
- SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다.
- 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다.
- 영업 활동에 필요한 자료는 회사가 제공합니다.
## 제10조 (세금 및 원천징수)
### 10.1 사업소득
- 파트너 수수료는 **사업소득**으로 간주됩니다.
### 10.2 원천징수
| 항목 | 비율 | 비고 |
| --- | --- | --- |
| 소득세 | 3.0% | |
| 지방소득세 | 0.3% | 소득세의 10% |
| 합계 | 3.3% | |
### 10.3 지급명세서
- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다.
## 제11조 (손해배상)
### 11.1 파트너의 귀책 사유
- 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다:
- 허위 정보 제공으로 계약 취소
- 고객과의 분쟁으로 회사 명예 훼손
- 비밀 유지 의무 위반
- 부정 행위
### 11.2 회사의 귀책 사유
- 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다.
## 제12조 (분쟁 해결)
- 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다.
- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다.
## 제13조 (기타 사항)
### 13.1 계약서 교부
- 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다.
### 13.2 통지
- 모든 통지는 다음의 연락처로 발송됩니다:
- **회사**:
- 이메일: admin@codebridge-x.com
- 전화: 02-6347-0005
- **파트너**:
- 이메일:
- 전화:
### 13.3 준거법
- 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다.
- **계약 당사자**
- **[회사]**
- **상호**: 주식회사 코드브릿지엑스
- **대표자**: 이의찬 (인)
- **사업자등록번호**: 664-86-03713
- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호
- **이메일**: admin@codebridge-x.com
- **전화**: 02-6347-0005
- **날짜**:
- **[파트너]**
- **상호/성명**:
- **대표자/본인**: (서명)
- **사업자등록번호**:
- **주소**:
- **이메일**:
- **전화**:
- **날짜**:
- **별첨**
#### 별첨 1: 수수료 정산표
| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 |
| --- | --- | --- | --- | --- | --- | --- |
| | | | | | | |
#### 별첨 2: 영업 활동 보고서
| 날짜 | 활동내용 | 고객사 | 진행 상황 |
| --- | --- | --- | --- |
| | | | |
- 첨부 서류
- 사업자등록증 사본 (사업자인 경우)
- 주민등록등본 사본 (개인인 경우)
- 통장 사본 (수수료 입금용)
- 비밀유지서약서
- **주식회사 코드브릿지엑스**
- 이메일: admin@codebridge-x.com
- 전화: 02-6347-0005
- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호

58
contracts/revisions.json Normal file
View File

@@ -0,0 +1,58 @@
{
"documents": {
"01-service-agreement": {
"title": "고객사 서비스 이용계약서",
"docx_file": "01_고객_서비스이용계약서_v4_0_전자서명용.docx",
"revisions": [
{
"version": "v4.0",
"date": "2026-02-22",
"author": "개발팀",
"description": "버전 관리 시스템 도입, 개정이력 추적 시작"
},
{
"version": "v4.1",
"date": "2026-02-22",
"author": "개발팀",
"description": "제4조에 사용량 기반 추가 과금(4.5) 및 바로빌 부가 서비스 요금(4.6) 조항 추가"
}
]
},
"02-nda": {
"title": "비밀유지서약서 (NDA)",
"docx_file": "비밀유지서약서.docx",
"revisions": [
{
"version": "v4.0",
"date": "2026-02-22",
"author": "개발팀",
"description": "버전 관리 시스템 도입, 개정이력 추적 시작"
}
]
},
"03-partner-agreement": {
"title": "영업파트너 위촉계약서",
"docx_file": "영업파트너 위촉계약서.docx",
"revisions": [
{
"version": "v4.0",
"date": "2026-02-22",
"author": "개발팀",
"description": "버전 관리 시스템 도입, 개정이력 추적 시작"
}
]
},
"04-partner-agreement-group": {
"title": "영업파트너 위촉계약서 (단체용)",
"docx_file": "영업파트너 위촉계약서(단체용).docx",
"revisions": [
{
"version": "v4.0",
"date": "2026-02-22",
"author": "개발팀",
"description": "버전 관리 시스템 도입, 개정이력 추적 시작"
}
]
}
}
}

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
DOCX → Markdown 추출 스크립트
4개 전자계약 DOCX 파일을 Markdown으로 변환한다.
- 서비스이용계약서: Heading 스타일 기반 매핑
- 나머지 3개: Bold 런 + 패턴 매칭으로 구조 유추
"""
import re
import sys
from datetime import date
from pathlib import Path
from docx import Document
# 경로 설정
BASE_DIR = Path(__file__).resolve().parent.parent
DOCX_DIR = BASE_DIR / "docx"
MD_DIR = BASE_DIR / "markdown"
# DOCX → Markdown 매핑
FILE_MAP = {
"01_고객_서비스이용계약서_v4_0_전자서명용.docx": {
"output": "01-service-agreement.md",
"title": "고객사 서비스 이용계약서",
"type": "styled",
},
"비밀유지서약서.docx": {
"output": "02-nda.md",
"title": "비밀유지서약서 (NDA)",
"type": "pattern",
},
"영업파트너 위촉계약서.docx": {
"output": "03-partner-agreement.md",
"title": "영업파트너 위촉계약서",
"type": "pattern",
},
"영업파트너 위촉계약서(단체용).docx": {
"output": "04-partner-agreement-group.md",
"title": "영업파트너 위촉계약서 (단체용)",
"type": "pattern",
},
}
def table_to_markdown(table):
"""DOCX 테이블을 Markdown 테이블로 변환"""
rows = []
for row in table.rows:
cells = [cell.text.strip().replace("\n", " ") for cell in row.cells]
rows.append(cells)
if not rows:
return ""
lines = []
# 헤더
lines.append("| " + " | ".join(rows[0]) + " |")
lines.append("| " + " | ".join(["---"] * len(rows[0])) + " |")
# 본문
for row in rows[1:]:
# 셀 개수 맞추기
while len(row) < len(rows[0]):
row.append("")
lines.append("| " + " | ".join(row[: len(rows[0])]) + " |")
return "\n".join(lines)
def get_paragraph_heading_level_styled(para):
"""스타일 기반 문서의 헤딩 레벨 판별 (서비스이용계약서)"""
style = para.style.name if para.style else ""
if style == "Heading 1":
return 1
elif style == "Heading 2":
return 2
elif style == "Heading 3":
return 3
return 0
def get_paragraph_heading_level_pattern(para):
"""패턴 매칭 기반 문서의 헤딩 레벨 판별 (비밀유지서약서, 영업파트너 위촉계약서)"""
text = para.text.strip()
has_bold = any(r.bold for r in para.runs if r.bold)
if not text or not has_bold:
return 0
# "제X조" 패턴 → ## (h2)
if re.match(r"^<?[ ]*제\d+조", text):
return 2
# "X.X " 패턴 (소제목) → ### (h3)
if re.match(r"^\d+\.\d+\s", text):
return 3
# 문서 제목 (첫 번째 bold 텍스트)
if re.match(r"^<?\s*(영업파트너|비밀유지서약서|Sales Partner)", text):
return 1
return 0
def is_list_item(para, doc_type):
"""리스트 아이템인지 판별"""
text = para.text.strip()
if not text:
return False
if doc_type == "styled":
style = para.style.name if para.style else ""
return style == "Compact"
# pattern 기반: bold가 아닌 일반 텍스트이면서 제X조나 X.X 패턴이 아닌 것
has_bold = any(r.bold for r in para.runs if r.bold)
if not has_bold and not re.match(r"^(제\d+조|<?|계약 당사자|\[)", text):
return True
return False
def extract_styled_doc(doc, file_info):
"""스타일 기반 문서 추출 (서비스이용계약서)"""
lines = []
table_positions = {}
# 테이블 위치 매핑: 문단 인덱스 기준으로 테이블이 어디에 삽입되는지 추적
body = doc.element.body
table_idx = 0
para_idx = 0
for child in body:
tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
if tag == "p":
para_idx += 1
elif tag == "tbl":
table_positions[para_idx] = table_idx
table_idx += 1
para_idx = 0
for child in body:
tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
if tag == "p":
para = doc.paragraphs[para_idx]
para_idx += 1
text = para.text.strip()
if not text:
lines.append("")
continue
style = para.style.name if para.style else ""
level = get_paragraph_heading_level_styled(para)
if level > 0:
lines.append("")
lines.append(f"{'#' * level} {text}")
lines.append("")
elif style == "Compact":
# Bold 런이 있으면 강조 리스트
has_bold = any(r.bold for r in para.runs if r.bold)
if has_bold:
# Bold 부분과 일반 부분 분리
parts = []
for run in para.runs:
if run.bold:
parts.append(f"**{run.text}**")
else:
parts.append(run.text)
combined = "".join(parts)
lines.append(f"- {combined}")
else:
# 들여쓰기된 하위 항목
lines.append(f" - {text}")
elif style in ("Body Text", "First Paragraph"):
# 본문 텍스트
if text.startswith("⚠️") or text.startswith("") or text.startswith(""):
lines.append("")
lines.append(f"> {text}")
lines.append("")
else:
lines.append(text)
else:
lines.append(text)
elif tag == "tbl":
if table_idx <= len(doc.tables):
current_table_idx = sum(
1
for c in list(body)[: list(body).index(child)]
if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl"
)
if current_table_idx < len(doc.tables):
lines.append("")
lines.append(table_to_markdown(doc.tables[current_table_idx]))
lines.append("")
return "\n".join(lines)
def extract_pattern_doc(doc, file_info):
"""패턴 매칭 기반 문서 추출 (비밀유지서약서, 영업파트너 위촉계약서)"""
lines = []
body = doc.element.body
para_idx = 0
for child in body:
tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
if tag == "p":
para = doc.paragraphs[para_idx]
para_idx += 1
text = para.text.strip()
if not text:
lines.append("")
continue
level = get_paragraph_heading_level_pattern(para)
has_bold = any(r.bold for r in para.runs if r.bold)
if level > 0:
lines.append("")
# 제목에서 < > 제거
clean_text = re.sub(r"^<\s*|\s*>$", "", text).strip()
lines.append(f"{'#' * level} {clean_text}")
lines.append("")
elif has_bold:
# Bold 텍스트는 강조 처리
parts = []
for run in para.runs:
if run.bold:
parts.append(f"**{run.text}**")
else:
parts.append(run.text)
combined = "".join(parts)
# (1), (2) 같은 번호 패턴
if re.match(r"^\*\*\(\d+\)", combined):
lines.append(f"- {combined}")
# "예시 N:", "Phase N:" 같은 패턴
elif re.match(r"^\*\*(예시|Phase|별첨)\s", combined):
lines.append("")
lines.append(f"#### {text}")
lines.append("")
else:
lines.append(f"- {combined}")
else:
# 일반 텍스트
# 빈칸 양식 (___) 유지
if "___" in text:
lines.append(text)
elif re.match(r"^(이메일|전화|주소|상호|대표|사업자|주민|연락처|날짜):", text):
lines.append(f"- {text}")
else:
lines.append(f" - {text}")
elif tag == "tbl":
current_table_idx = sum(
1
for c in list(body)[: list(body).index(child)]
if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl"
)
if current_table_idx < len(doc.tables):
lines.append("")
lines.append(table_to_markdown(doc.tables[current_table_idx]))
lines.append("")
return "\n".join(lines)
def add_frontmatter(content, file_info, docx_name):
"""YAML 프론트매터 추가"""
frontmatter = f"""---
title: "{file_info['title']}"
version: "v4.0"
date: "{date.today().isoformat()}"
docx_file: "{docx_name}"
---
"""
return frontmatter + "\n" + content
def extract_file(docx_name, file_info):
"""단일 DOCX 파일 추출"""
docx_path = DOCX_DIR / docx_name
if not docx_path.exists():
print(f" [SKIP] {docx_name} - 파일 없음")
return False
doc = Document(str(docx_path))
if file_info["type"] == "styled":
content = extract_styled_doc(doc, file_info)
else:
content = extract_pattern_doc(doc, file_info)
# 프론트매터 추가
content = add_frontmatter(content, file_info, docx_name)
# 연속 빈 줄 정리 (3줄 이상 → 2줄로)
content = re.sub(r"\n{3,}", "\n\n", content)
# 파일 저장
output_path = MD_DIR / file_info["output"]
output_path.write_text(content, encoding="utf-8")
print(f" [OK] {docx_name}{file_info['output']}")
return True
def main():
print("DOCX → Markdown 추출 시작")
print(f" DOCX 디렉토리: {DOCX_DIR}")
print(f" 출력 디렉토리: {MD_DIR}")
print()
MD_DIR.mkdir(parents=True, exist_ok=True)
success = 0
for docx_name, file_info in FILE_MAP.items():
if extract_file(docx_name, file_info):
success += 1
print(f"\n완료: {success}/{len(FILE_MAP)} 파일 변환됨")
return 0 if success == len(FILE_MAP) else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""
DOCX ↔ Markdown 동기화 검증 스크립트
DOCX에서 텍스트를 추출하고 Markdown 파일의 텍스트와 비교하여
불일치 항목을 리포트한다.
"""
import difflib
import re
import sys
from pathlib import Path
from docx import Document
BASE_DIR = Path(__file__).resolve().parent.parent
DOCX_DIR = BASE_DIR / "docx"
MD_DIR = BASE_DIR / "markdown"
# DOCX → Markdown 파일 매핑
FILE_MAP = {
"01_고객_서비스이용계약서_v4_0_전자서명용.docx": "01-service-agreement.md",
"비밀유지서약서.docx": "02-nda.md",
"영업파트너 위촉계약서.docx": "03-partner-agreement.md",
"영업파트너 위촉계약서(단체용).docx": "04-partner-agreement-group.md",
}
def extract_text_from_docx(docx_path):
"""DOCX에서 순수 텍스트만 추출 (개정이력 테이블 제외, 인터리빙 방식)"""
doc = Document(str(docx_path))
lines = []
from docx.oxml.ns import qn as _qn
body = doc.element.body
para_idx = 0
table_idx = 0
skip_revision = False
for child in body:
tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
if tag == "p":
if para_idx < len(doc.paragraphs):
text = doc.paragraphs[para_idx].text.strip()
para_idx += 1
if "개정이력" in text:
skip_revision = True
continue
if text:
skip_revision = False
lines.append(text)
elif tag == "tbl":
if table_idx < len(doc.tables):
table = doc.tables[table_idx]
table_idx += 1
# 개정이력 테이블 건너뛰기
if len(table.rows) > 0:
first_row_text = [cell.text.strip() for cell in table.rows[0].cells]
if "버전" in first_row_text and "날짜" in first_row_text:
skip_revision = False
continue
if skip_revision:
skip_revision = False
continue
for row in table.rows:
cells = [cell.text.strip() for cell in row.cells]
# 빈 셀만 있는 행 건너뛰기
if not any(cells):
continue
row_text = " | ".join(cells)
if row_text.strip():
lines.append(row_text)
return lines
def extract_text_from_markdown(md_path):
"""Markdown에서 순수 텍스트만 추출 (프론트매터, 마크업 제거)"""
content = md_path.read_text(encoding="utf-8")
lines = []
in_frontmatter = False
in_table = False
for line in content.split("\n"):
stripped = line.strip()
# YAML 프론트매터 건너뛰기
if stripped == "---":
in_frontmatter = not in_frontmatter
continue
if in_frontmatter:
continue
# 빈 줄 건너뛰기
if not stripped:
in_table = False
continue
# Markdown 마크업 제거
text = stripped
# 헤딩 마크업 제거
text = re.sub(r"^#{1,6}\s+", "", text)
# 리스트 마크업 제거
text = re.sub(r"^\s*[-*+]\s+", "", text)
# Bold/Italic 마크업 제거
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
text = re.sub(r"\*(.+?)\*", r"\1", text)
# 블록인용 제거
text = re.sub(r"^>\s*", "", text)
# 테이블 구분선 건너뛰기
if re.match(r"^\|[\s\-|]+\|$", text):
continue
# 테이블 행
if text.startswith("|") and text.endswith("|"):
# 파이프 제거하고 셀 텍스트 추출
cells = [c.strip() for c in text.strip("|").split("|")]
text = " | ".join(cells)
text = text.strip()
if text:
lines.append(text)
return lines
def normalize_text(text):
"""비교를 위한 텍스트 정규화"""
# 공백 정규화
text = re.sub(r"\s+", " ", text).strip()
# 특수문자 정규화
text = text.replace("\u00a0", " ") # non-breaking space
text = text.replace("\u3000", " ") # ideographic space
# 언더스코어 빈칸 정규화
text = re.sub(r"_{3,}", "___", text)
# Bold 마크업(**) 제거 (DOCX 텍스트에 리터럴 ** 포함되는 경우)
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
# 선행 리스트 마커 제거 (DOCX 텍스트가 "- "로 시작하는 경우)
text = re.sub(r"^-\s+", "", text)
return text
def compare_documents(docx_name, md_name):
"""두 문서의 텍스트를 비교"""
docx_path = DOCX_DIR / docx_name
md_path = MD_DIR / md_name
if not docx_path.exists():
return {"status": "error", "message": f"DOCX 파일 없음: {docx_name}"}
if not md_path.exists():
return {"status": "error", "message": f"Markdown 파일 없음: {md_name}"}
docx_lines = [normalize_text(l) for l in extract_text_from_docx(docx_path) if l.strip()]
md_lines = [normalize_text(l) for l in extract_text_from_markdown(md_path) if l.strip()]
# difflib로 비교
matcher = difflib.SequenceMatcher(None, docx_lines, md_lines)
ratio = matcher.ratio()
# 차이점 추출
diffs = []
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == "equal":
continue
elif tag == "replace":
for idx in range(max(i2 - i1, j2 - j1)):
docx_text = docx_lines[i1 + idx] if i1 + idx < i2 else "(없음)"
md_text = md_lines[j1 + idx] if j1 + idx < j2 else "(없음)"
diffs.append({
"type": "변경",
"docx": docx_text[:80],
"markdown": md_text[:80],
})
elif tag == "delete":
for idx in range(i1, i2):
diffs.append({
"type": "DOCX에만 존재",
"docx": docx_lines[idx][:80],
"markdown": "-",
})
elif tag == "insert":
for idx in range(j1, j2):
diffs.append({
"type": "Markdown에만 존재",
"docx": "-",
"markdown": md_lines[idx][:80],
})
return {
"status": "ok",
"similarity": round(ratio * 100, 1),
"docx_lines": len(docx_lines),
"md_lines": len(md_lines),
"diff_count": len(diffs),
"diffs": diffs[:20], # 상위 20개만
}
def main():
print("=" * 70)
print("DOCX ↔ Markdown 동기화 검증")
print("=" * 70)
all_ok = True
for docx_name, md_name in FILE_MAP.items():
print(f"\n{'' * 50}")
print(f"문서: {docx_name}")
print(f"{md_name}")
print(f"{'' * 50}")
result = compare_documents(docx_name, md_name)
if result["status"] == "error":
print(f" [ERROR] {result['message']}")
all_ok = False
continue
similarity = result["similarity"]
status_icon = "OK" if similarity >= 80 else "WARN" if similarity >= 60 else "FAIL"
print(f" 유사도: {similarity}% [{status_icon}]")
print(f" DOCX 라인: {result['docx_lines']}")
print(f" Markdown 라인: {result['md_lines']}")
print(f" 차이점: {result['diff_count']}")
if result["diffs"]:
print(f"\n 주요 차이점 (상위 {min(len(result['diffs']), 10)}개):")
for i, diff in enumerate(result["diffs"][:10]):
print(f" [{diff['type']}]")
if diff["docx"] != "-":
print(f" DOCX: {diff['docx']}")
if diff["markdown"] != "-":
print(f" MD: {diff['markdown']}")
if similarity < 80:
all_ok = False
print(f"\n{'=' * 70}")
if all_ok:
print("결과: 모든 문서 동기화 상태 양호")
else:
print("결과: 일부 문서에서 불일치 발견 - 확인 필요")
print(f"{'=' * 70}")
return 0 if all_ok else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,279 @@
-- ============================================================
-- 인터뷰 질문 마스터 데이터 SQL
-- 8개 도메인 × 16개 템플릿 × 80개 질문
--
-- 실행 방법:
-- 로컬: docker exec -i sam-mysql-1 mysql -u root -p samdb < docs/data/interview-master-questions.sql
-- 개발서버: mysql -u <user> -p samdb < interview-master-questions.sql
-- phpMyAdmin: SQL 탭에서 전체 복사 후 실행
--
-- 주의: 한 번만 실행할 것. 중복 실행 시 데이터가 중복됨.
-- ============================================================
SET NAMES utf8mb4;
SET @tenant_id = 1;
SET @user_id = 1;
SET @now = NOW();
-- ============================================================
-- 대분류: 제조업-방화셔터 (parent_id=null, 루트 카테고리)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, NULL, '제조업-방화셔터', '방화셔터 제조업 인터뷰', NULL, 1, 1, @user_id, @user_id, @now, @now);
SET @root_manufacturing = LAST_INSERT_ID();
-- ============================================================
-- Domain 1: 제품 분류 체계 (product_classification)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '제품 분류 체계', '제품 카테고리, 모델 코드, 분류 기준 파악', 'product_classification', 3, 1, @user_id, @user_id, @now, @now);
SET @cat_1 = LAST_INSERT_ID();
-- 템플릿 1.1: 제품 카테고리 구조
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_1, '제품 카테고리 구조', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_1_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_1_1, '귀사의 주요 제품군을 모두 나열해주세요', 'text', NULL, '쉼표 구분으로 제품군 나열', NULL, NULL, 'product_classification', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '각 제품군의 하위 모델명과 코드 체계를 알려주세요', 'table_input', '{"columns":["모델코드","모델명","비고"]}', '코드-이름 매핑 테이블', NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '제품을 분류하는 기준은 무엇인가요? (소재, 용도, 크기 등)', 'multi_select', '{"choices":["소재별","용도별","크기별","설치방식별","인증여부별"]}', NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '인증(인정) 제품과 비인증 제품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'product_classification', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '인증 제품의 경우 구성이 고정되나요?', 'checkbox', NULL, NULL, NULL, '{"question_index":3,"value":"있음"}', 'product_classification', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '카테고리별 제품 수는 대략 몇 개인가요?', 'number', NULL, NULL, '', NULL, 'product_classification', 0, 6, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '제품 코드 명명 규칙을 설명해주세요 (예: KSS01의 의미)', 'text', NULL, '코드 체계의 각 자릿수 의미', NULL, NULL, 'product_classification', 0, 7, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_1, '기존 시스템(ERP/엑셀)에서 사용하는 제품 분류 방식을 캡처하여 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'product_classification', 0, 8, 1, @user_id, @user_id, @now, @now);
-- 템플릿 1.2: 설치 유형별 분류
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_1, '설치 유형별 분류', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_1_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_1_2, '설치 유형(벽면형, 측면형, 혼합형 등)에 따라 견적이 달라지나요?', 'select', '{"choices":["예, 크게 달라짐","약간 달라짐","달라지지 않음"]}', NULL, NULL, NULL, 'product_classification', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_2, '각 설치 유형별로 어떤 부품이 달라지나요?', 'table_input', '{"columns":["설치유형","추가부품","제외부품","비고"]}', NULL, NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_1_2, '설치 유형에 따른 추가 비용 항목이 있나요?', 'text', NULL, NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 2: BOM 구조 (bom_structure)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, 'BOM 구조', '완제품-부품 관계, 부품 카테고리, BOM 레벨', 'bom_structure', 4, 1, @user_id, @user_id, @now, @now);
SET @cat_2 = LAST_INSERT_ID();
-- 템플릿 2.1: 완제품-부품 관계
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_2, '완제품-부품 관계', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_2_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_2_1, '대표 제품 1개의 완제품→부품 구성을 트리로 그려주세요', 'bom_tree', NULL, '최상위 제품부터 하위 부품까지 트리 구조', NULL, NULL, 'bom_structure', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_1, '모든 제품에 공통으로 들어가는 부품은 무엇인가요?', 'multi_select', '{"choices":["가이드레일","케이스","모터","제어기","브라켓","볼트/너트"]}', '직접 입력 가능', NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_1, '제품별로 선택적(옵션)인 부품은 무엇인가요?', 'table_input', '{"columns":["제품명","옵션부품","적용조건"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_1, 'BOM이 현재 엑셀로 관리되고 있나요? 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_1, '하위 부품의 단계(레벨)는 최대 몇 단계인가요?', 'number', NULL, NULL, '단계', NULL, 'bom_structure', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_1, '부품 수량이 고정인 것과 계산이 필요한 것을 구분해주세요', 'table_input', '{"columns":["부품명","고정/계산","고정수량 또는 계산식"]}', NULL, NULL, NULL, 'bom_structure', 0, 6, 1, @user_id, @user_id, @now, @now);
-- 템플릿 2.2: 부품 카테고리
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_2, '부품 카테고리', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_2_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_2_2, '부품을 카테고리로 분류하면 어떻게 나눠지나요? (본체, 절곡품, 전동부, 부자재 등)', 'text', NULL, '부품 분류 체계', NULL, NULL, 'bom_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_2, '각 카테고리에 속하는 부품 목록을 정리해주세요', 'table_input', '{"columns":["카테고리","부품명","규격"]}', NULL, NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_2, '외주 구매 부품과 자체 제작 부품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_2_2, '부자재(볼트, 너트, 패킹 등)는 별도 관리하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 3: 치수/변수 계산 (dimension_formula)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '치수/변수 계산', '오픈 사이즈→제작 사이즈 변환, 파생 변수 계산', 'dimension_formula', 5, 1, @user_id, @user_id, @now, @now);
SET @cat_3 = LAST_INSERT_ID();
-- 템플릿 3.1: 오픈 사이즈 → 제작 사이즈
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_3, '오픈 사이즈 → 제작 사이즈', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_3_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_3_1, '고객이 입력하는 기본 치수 항목은 무엇인가요? (폭, 높이, 깊이 등)', 'multi_select', '{"choices":["폭(W)","높이(H)","깊이(D)","두께(T)","지름(Ø)"]}', NULL, NULL, NULL, 'dimension_formula', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '오픈 사이즈에서 제작 사이즈로 변환할 때 더하는 마진값은?', 'formula_input', NULL, '예: W1 = W0 + 120, H1 = H0 + 50', 'mm', NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '제품 카테고리별로 마진값이 다른가요?', 'table_input', '{"columns":["제품카테고리","W 마진(mm)","H 마진(mm)","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '면적(㎡) 계산 공식을 알려주세요', 'formula_input', NULL, '예: area = W1 * H1 / 1000000', '', NULL, 'dimension_formula', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '중량(kg) 계산 공식을 알려주세요', 'formula_input', NULL, '예: weight = area * 단위중량(kg/㎡)', 'kg', NULL, 'dimension_formula', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '기타 파생 변수가 있나요? (예: 분할 개수, 절곡 길이 등)', 'table_input', '{"columns":["변수명","계산식","단위","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 6, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_1, '치수 계산에 사용하는 엑셀 수식을 캡처해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 7, 1, @user_id, @user_id, @now, @now);
-- 템플릿 3.2: 변수 의존 관계
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_3, '변수 의존 관계', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_3_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_3_2, '변수 간 의존 관계를 설명해주세요 (A는 B와 C로 계산)', 'text', NULL, '계산 순서와 변수 의존성', NULL, NULL, 'dimension_formula', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_2, '계산 순서가 중요한 변수가 있나요?', 'text', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_3_2, '단위는 mm, m, kg 중 어떤 것을 기본으로 사용하나요?', 'select', '{"choices":["mm","m","cm","혼용"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 4: 부품 구성 상세 (component_config)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '부품 구성 상세', '주요 부품별 규격, 선택 기준, 특수 구성', 'component_config', 6, 1, @user_id, @user_id, @now, @now);
SET @cat_4 = LAST_INSERT_ID();
-- 템플릿 4.1: 주요 부품별 상세
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_4, '주요 부품별 상세', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_4_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_4_1, '가이드레일의 표준 길이 규격은? (예: 1219, 2438, 3305mm)', 'table_input', '{"columns":["규격코드","길이(mm)","비고"]}', NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '가이드레일 길이 조합 규칙은? (어떤 길이를 몇 개 사용?)', 'text', NULL, '높이에 따른 가이드레일 조합 로직', NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '케이스(하우징) 크기별 규격과 부속품 차이를 설명해주세요', 'table_input', '{"columns":["케이스규격","적용조건","부속품"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '모터 용량 종류와 선택 기준은? (무게별? 면적별?)', 'table_input', '{"columns":["모터용량","적용범위(최소)","적용범위(최대)","단위"]}', '무게/면적 범위별 모터 매핑', NULL, NULL, 'component_config', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '모터 전압 옵션은? (380V, 220V 등)', 'multi_select', '{"choices":["380V","220V","110V","DC 24V"]}', NULL, NULL, NULL, 'component_config', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '제어기 종류와 선택 기준은? (노출형/매립형 등)', 'table_input', '{"columns":["제어기유형","적용조건","비고"]}', NULL, NULL, NULL, 'component_config', 0, 6, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '절곡품(판재 가공) 목록과 각각의 치수 결정 방식은?', 'table_input', '{"columns":["절곡품명","치수결정방식","재질","두께(mm)"]}', NULL, NULL, NULL, 'component_config', 0, 7, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_1, '부자재(볼트, 너트, 패킹 등) 목록과 수량 결정 방식은?', 'table_input', '{"columns":["부자재명","규격","수량결정방식","기본수량"]}', NULL, NULL, NULL, 'component_config', 0, 8, 1, @user_id, @user_id, @now, @now);
-- 템플릿 4.2: 특수 구성
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_4, '특수 구성', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_4_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_4_2, '연기차단재 등 특수 부품이 있나요? 적용 조건은?', 'text', NULL, NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_2, '보강재(샤프트, 파이프, 앵글 등) 사용 조건은?', 'table_input', '{"columns":["보강재명","규격","적용조건","수량"]}', NULL, NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_4_2, '고객 요청에 따라 추가/제외되는 옵션 부품은?', 'table_input', '{"columns":["옵션부품","추가/제외","추가비용","비고"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 5: 단가 체계 (pricing_structure)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '단가 체계', '단가 관리 방식, 계산 방식, 마진/LOSS율', 'pricing_structure', 7, 1, @user_id, @user_id, @now, @now);
SET @cat_5 = LAST_INSERT_ID();
-- 템플릿 5.1: 단가 관리 방식
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_5, '단가 관리 방식', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_5_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_5_1, '부품별 단가를 어디서 관리하나요? (엑셀, ERP, 구두 등)', 'select', '{"choices":["엑셀","ERP 시스템","구두/경험","기타"]}', NULL, NULL, NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, '단가표 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, '단가 변경 주기는? (월/분기/연 등)', 'select', '{"choices":["수시","월 단위","분기 단위","반기 단위","연 단위"]}', NULL, NULL, NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, '단가에 포함되는 항목은? (재료비만? 가공비 포함?)', 'multi_select', '{"choices":["재료비","가공비","운송비","설치비","마진"]}', NULL, NULL, NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, '고객별/거래처별 차등 단가가 있나요?', 'select', '{"choices":["있음 (등급별)","있음 (거래처별)","없음 (일괄 동일)"]}', NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, 'LOSS율(손실률)을 적용하나요? 적용 방식은?', 'formula_input', NULL, '예: 실제수량 = 계산수량 × (1 + LOSS율)', '%', NULL, 'pricing_structure', 0, 6, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_1, '마진율 설정 방식은? (일괄? 품목별?)', 'select', '{"choices":["일괄 마진율","품목별 마진율","카테고리별 마진율","고객별 마진율"]}', NULL, NULL, NULL, 'pricing_structure', 0, 7, 1, @user_id, @user_id, @now, @now);
-- 템플릿 5.2: 단가 계산 방식
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_5, '단가 계산 방식', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_5_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_5_2, '면적 기반 단가 품목은? (원/㎡)', 'table_input', '{"columns":["품목명","단가(원/㎡)","비고"]}', NULL, '원/㎡', NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_2, '중량 기반 단가 품목은? (원/kg)', 'table_input', '{"columns":["품목명","단가(원/kg)","비고"]}', NULL, '원/kg', NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_2, '수량 기반 단가 품목은? (원/EA)', 'table_input', '{"columns":["품목명","단가(원/EA)","비고"]}', NULL, '원/EA', NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_2, '길이 기반 단가 품목은? (원/m)', 'table_input', '{"columns":["품목명","단가(원/m)","비고"]}', NULL, '원/m', NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_5_2, '기타 특수 단가 계산 방식이 있나요?', 'text', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 6: 수량 수식 (quantity_formula)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '수량 수식', '부품별 수량 결정 규칙, 계산식, 검증', 'quantity_formula', 8, 1, @user_id, @user_id, @now, @now);
SET @cat_6 = LAST_INSERT_ID();
-- 템플릿 6.1: 수량 결정 규칙
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_6, '수량 결정 규칙', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_6_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_6_1, '고정 수량 부품 목록 (항상 1개, 2개 등)', 'table_input', '{"columns":["부품명","고정수량","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_1, '치수 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 슬랫수량 = CEIL(H1 / 슬랫피치)', NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_1, '면적 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 스크린수량 = area / 기준면적', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_1, '중량 기반 수량 계산 부품과 수식', 'formula_input', NULL, NULL, NULL, NULL, 'quantity_formula', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_1, '올림/내림/반올림 규칙이 있는 계산은?', 'table_input', '{"columns":["계산항목","올림/내림/반올림","소수점자릿수"]}', NULL, NULL, NULL, 'quantity_formula', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_1, '여유 수량(LOSS) 적용 품목과 비율은?', 'table_input', '{"columns":["품목명","LOSS율(%)","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 6, 1, @user_id, @user_id, @now, @now);
-- 템플릿 6.2: 수식 검증
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_6, '수식 검증', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_6_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_6_2, '실제 견적서에서 수량 계산 예시를 보여주세요 (W=3000, H=2500일 때)', 'table_input', '{"columns":["부품명","수식","계산결과","단위"]}', NULL, NULL, NULL, 'quantity_formula', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_2, '수식에 사용하는 함수가 있나요? (SUM, CEIL, ROUND 등)', 'multi_select', '{"choices":["CEIL (올림)","FLOOR (내림)","ROUND (반올림)","MAX","MIN","IF 조건문","SUM"]}', NULL, NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_6_2, '조건에 따라 수식이 달라지는 경우가 있나요?', 'text', NULL, '예: 폭이 3000 초과이면 분할 계산', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 7: 조건부 로직 (conditional_logic)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '조건부 로직', '범위/매핑 기반 부품 자동 선택 규칙', 'conditional_logic', 9, 1, @user_id, @user_id, @now, @now);
SET @cat_7 = LAST_INSERT_ID();
-- 템플릿 7.1: 범위 기반 선택
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_7, '범위 기반 선택', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_7_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_7_1, '무게 범위별 모터 용량 선택표를 작성해주세요', 'price_table', '{"columns":["범위 시작(kg)","범위 끝(kg)","모터용량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_7_1, '크기 범위별 부품 자동 선택 규칙이 있나요?', 'table_input', '{"columns":["조건(변수)","범위","선택부품","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_7_1, '브라켓 크기 결정 기준은?', 'table_input', '{"columns":["조건","범위","브라켓 규격"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now);
-- 템플릿 7.2: 매핑 기반 선택
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_7, '매핑 기반 선택', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_7_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_7_2, '제품 모델 → 기본 부품 세트 매핑표', 'table_input', '{"columns":["제품모델","기본부품1","기본부품2","기본부품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_7_2, '설치 유형 → 추가 부품 매핑표', 'table_input', '{"columns":["설치유형","추가부품","수량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_7_2, '제어기 유형 → 부속품 매핑표', 'table_input', '{"columns":["제어기유형","부속품1","부속품2","부속품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_7_2, '기타 조건부 자동 선택 규칙', 'text', NULL, '위 항목에 해당하지 않는 조건-결과 매핑', NULL, NULL, 'conditional_logic', 0, 4, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- Domain 8: 견적서 양식 (quote_format)
-- ============================================================
INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, NULL, @root_manufacturing, '견적서 양식', '출력 양식, 항목 그룹, 소계/합계 구조', 'quote_format', 10, 1, @user_id, @user_id, @now, @now);
SET @cat_8 = LAST_INSERT_ID();
-- 템플릿 8.1: 출력 양식
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_8, '출력 양식', 1, 1, @user_id, @user_id, @now, @now);
SET @tpl_8_1 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_8_1, '현재 사용 중인 견적서 양식을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'quote_format', 1, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '견적서에 표시되는 항목 그룹은? (재료비, 노무비, 설치비 등)', 'multi_select', '{"choices":["재료비","노무비","경비","설치비","운반비","이윤","부가세"]}', NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '소계/합계 계산 구조를 설명해주세요', 'text', NULL, '항목 그룹별 소계와 최종 합계의 관계', NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '할인 적용 방식은? (일괄? 항목별?)', 'select', '{"choices":["일괄 할인","항목별 할인","할인 없음","협의 할인"]}', NULL, NULL, NULL, 'quote_format', 0, 4, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '부가세 표시 방식은? (별도? 포함?)', 'select', '{"choices":["별도 표시","포함 표시","선택 가능"]}', NULL, NULL, NULL, 'quote_format', 0, 5, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '견적서에 표시하지 않는 내부 관리 항목은?', 'text', NULL, NULL, NULL, NULL, 'quote_format', 0, 6, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_1, '견적 번호 체계를 알려주세요', 'text', NULL, '예: Q-2026-001 형식', NULL, NULL, 'quote_format', 0, 7, 1, @user_id, @user_id, @now, @now);
-- 템플릿 8.2: 특수 요구사항
INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at)
VALUES (@tenant_id, @cat_8, '특수 요구사항', 2, 1, @user_id, @user_id, @now, @now);
SET @tpl_8_2 = LAST_INSERT_ID();
INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES
(@tenant_id, @tpl_8_2, '산출내역서(세부 내역)를 별도로 제공하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 1, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_2, '위치별(층/부호) 개별 산출이 필요한가요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now),
(@tenant_id, @tpl_8_2, '일괄 산출(여러 위치 합산)을 사용하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now);
-- ============================================================
-- 완료 확인
-- ============================================================
SELECT
(SELECT COUNT(*) FROM interview_categories WHERE interview_project_id IS NULL AND domain IS NOT NULL) AS master_categories,
(SELECT COUNT(*) FROM interview_templates t JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_templates,
(SELECT COUNT(*) FROM interview_questions q JOIN interview_templates t ON q.interview_template_id = t.id JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_questions;

View File

@@ -0,0 +1,316 @@
# 품질인정심사(QMS) API 연동 계획
> **작성일**: 2026-03-09
> **상태**: 계획 수립
> **URL**: `/quality/qms`
> **스토리보드**: 슬라이드 19~20
> **관련 문서**: `docs/features/quality-management/quality-certification-audit.md`
---
## 1. 현황 분석
### 1.1 프론트엔드 현황
| 항목 | 상태 | 비고 |
|------|------|------|
| `page.tsx` | ✅ 구현됨 | 14KB, 전체 페이지 레이아웃 |
| `types.ts` | ✅ 구현됨 | 95줄, 타입 정의 완료 |
| `mockData.ts` | ✅ 구현됨 | 543줄, 완전한 목업 데이터 |
| `components/` | ✅ 구현됨 | 12개 컴포넌트 + documents/ 7개 |
| `actions.ts` | ❌ 없음 | API 연동 0% |
프론트엔드는 UI가 완성되어 있으나 **100% 목업 데이터**로 동작 중.
### 1.2 백엔드 현황
| 영역 | 기존 API | 신규 필요 |
|------|----------|-----------|
| **1일차 (기준/매뉴얼 심사)** | ❌ 없음 | 모델, 마이그레이션, 서비스, 컨트롤러 전체 |
| **2일차 (로트 추적 심사)** | ⚠️ 부분 존재 | 기존 API 조합 + 서류 연결 API 신규 |
**기존 활용 가능 API:**
- `GET /quality/documents` — 품질관리서 목록 (2일차 1단계)
- `GET /quality/documents/{id}` — 품질관리서 상세 + 수주/개소 (2일차 2단계)
- `GET /quality/performance-reports` — 실적신고 (분기 필터 활용)
- `GET /inspections` — 수입검사/중간검사 성적서
- 출하/출고/납품 관련 기존 API
---
## 2. 작업 범위
### Phase 1: 2일차 (로트 추적 심사) API 연동
> **우선순위 높음** — 기존 API 활용 가능하여 빠르게 연동 가능
#### 2.1 Frontend — `actions.ts` 생성
```
react/src/app/[locale]/(protected)/quality/qms/actions.ts
```
| 액션 | 호출 API | 설명 |
|------|----------|------|
| `getQualityReports()` | `GET /quality/documents` | 품질관리서 목록 (분기 필터) |
| `getReportRoutes(reportId)` | `GET /quality/documents/{id}` | 수주코드 + 개소 목록 |
| `getRouteDocuments(routeId)` | 복합 조회 (아래 참조) | 개소별 관련 서류 8종 |
| `confirmUnitInspection(unitId)` | `PATCH /qms/lot-audit/confirm` | 개소 확인 완료 처리 |
#### 2.2 관련 서류 조회 로직
2일차 3단계 "관련 서류"는 개소(Location)에 연결된 8종 서류를 조합 조회:
| 서류 타입 | 데이터 소스 | 조회 방식 |
|-----------|-------------|-----------|
| 수입검사 성적서 | `inspections` (type=IQC) | 수주의 BOM 원자재 LOT 추적 |
| 수주서 | `orders` | 수주코드로 직접 조회 |
| 작업일지 | `work_orders` + 작업일지 | 수주 → 작업지시 → 작업일지 |
| 중간검사 성적서 | `inspections` (type=PQC) | 작업지시별 중간검사 |
| 납품확인서 | `shipments` | 출하 → 납품확인서 |
| 출고증 | `shipments` | 출하 → 출고증 |
| 제품검사 성적서 | `quality_document_locations` | 개소별 검사 문서 (EAV) |
| 품질관리서 | `quality_documents` | 품질관리서 원본 |
#### 2.3 Backend — 신규 API (최소)
```
GET /api/v1/qms/lot-audit/reports — 분기별 품질관리서 목록 (전용 뷰)
GET /api/v1/qms/lot-audit/reports/{id} — 수주코드 + 개소 + 완료 상태
GET /api/v1/qms/lot-audit/routes/{id}/documents — 개소별 8종 서류 조합 조회
PATCH /api/v1/qms/lot-audit/units/{id}/confirm — 확인 완료 처리
```
> 기존 `quality/documents` API를 래핑하여 QMS 전용 응답 형태로 가공하는 방식 권장.
> 8종 서류 조합 로직이 복잡하므로 **전용 서비스 메서드** 필요.
#### 2.4 DB 변경
| 변경 | 테이블 | 설명 |
|------|--------|------|
| 컬럼 추가 | `quality_document_locations` | `options` JSON에 `lot_audit_confirmed`, `lot_audit_confirmed_at` 추가 |
> 별도 테이블 없이 기존 개소(Location) 테이블의 `options` 활용 (컬럼 추가 정책 준수)
---
### Phase 2: 1일차 (기준/매뉴얼 심사) 백엔드 구축
> **작업량 많음** — 완전 신규 백엔드 구축 필요
#### 2.1 DB 설계 (신규 테이블)
```
audit_checklists (심사 점검표 마스터)
├── id, tenant_id
├── year, quarter
├── type: 'standard_manual' (1일차)
├── status: draft/in_progress/completed
├── options: JSON
├── created_by, timestamps, soft_delete
audit_checklist_categories (점검표 카테고리)
├── id, tenant_id
├── checklist_id (FK → audit_checklists)
├── title: '원재료 품질관리 기준'
├── sort_order
├── options: JSON
audit_checklist_items (점검표 세부 항목)
├── id, tenant_id
├── category_id (FK → audit_checklist_categories)
├── name: '수입검사 기준 확인'
├── description
├── is_completed: boolean
├── completed_at, completed_by
├── sort_order
├── options: JSON
audit_standard_documents (기준 문서)
├── id, tenant_id
├── checklist_item_id (FK → audit_checklist_items)
├── title, version, date
├── document_id (FK → documents, EAV)
├── options: JSON
```
#### 2.2 Backend 구현
| 파일 | 역할 |
|------|------|
| `api/app/Models/Qualitys/AuditChecklist.php` | 심사 점검표 모델 |
| `api/app/Models/Qualitys/AuditChecklistCategory.php` | 카테고리 모델 |
| `api/app/Models/Qualitys/AuditChecklistItem.php` | 세부 항목 모델 |
| `api/app/Models/Qualitys/AuditStandardDocument.php` | 기준 문서 모델 |
| `api/app/Services/AuditChecklistService.php` | 서비스 |
| `api/app/Http/Controllers/Api/V1/AuditChecklistController.php` | 컨트롤러 |
| `api/database/migrations/XXXX_create_audit_checklists_table.php` | 마이그레이션 (4테이블) |
#### 2.3 API 엔드포인트
```
GET /api/v1/qms/checklists — 점검표 목록 (연도/분기 필터)
POST /api/v1/qms/checklists — 점검표 생성
GET /api/v1/qms/checklists/{id} — 점검표 상세 (카테고리+항목+문서)
PUT /api/v1/qms/checklists/{id} — 점검표 수정
PATCH /api/v1/qms/checklists/{id}/complete — 점검표 완료 처리
PATCH /api/v1/qms/checklist-items/{id}/toggle — 항목 완료/미완료 토글
GET /api/v1/qms/checklist-items/{id}/documents — 항목별 기준 문서 조회
POST /api/v1/qms/checklist-items/{id}/documents — 기준 문서 연결
DELETE /api/v1/qms/checklist-items/{id}/documents/{docId} — 기준 문서 연결 해제
```
#### 2.4 Frontend — actions.ts 확장
| 액션 | 설명 |
|------|------|
| `getChecklists(year, quarter)` | 점검표 목록 |
| `getChecklistDetail(id)` | 점검표 상세 (카테고리+항목+문서) |
| `toggleChecklistItem(itemId)` | 항목 완료/미완료 토글 |
| `getCheckItemDocuments(itemId)` | 기준 문서 조회 |
| `confirmCheckItem(itemId)` | 기준/매뉴얼 확인 완료 |
---
### Phase 3: 프론트엔드 목업 → API 전환
#### 3.1 page.tsx 수정
- `mockData.ts` import 제거
- `actions.ts` import로 교체
- `useEffect`에서 API 호출
- 로딩/에러 상태 추가
#### 3.2 컴포넌트 수정
| 컴포넌트 | 변경 내용 |
|----------|-----------|
| `ReportList.tsx` | API 데이터 바인딩 |
| `RouteList.tsx` | API 데이터 바인딩 |
| `DocumentList.tsx` | 8종 서류 실제 조회 |
| `InspectionModal.tsx` | 실제 검사 문서 렌더링 |
| `Day1ChecklistPanel.tsx` | API 데이터 바인딩 |
| `Day1DocumentSection.tsx` | 기준 문서 API 조회 |
| `Day1DocumentViewer.tsx` | 실제 파일 미리보기 |
| `AuditProgressBar.tsx` | 실시간 진행률 계산 |
| `Filters.tsx` | 연도/분기 필터 API 연동 |
#### 3.3 mockData.ts 처리
- Phase 3 완료 후 `mockData.ts` 삭제
- 또는 `USE_MOCK` 플래그 패턴 적용 (개발 편의)
---
## 3. 데이터 매핑
### 3.1 InspectionReport ↔ QualityDocument
| 프론트 (InspectionReport) | 백엔드 (QualityDocument) |
|---------------------------|-------------------------|
| `id` | `quality_documents.id` |
| `code` | `quality_documents.code` (채번) |
| `siteName` | `quality_documents.site_name` |
| `item` | `quality_documents.options.product_type` 또는 인정특성 |
| `routeCount` | `quality_document_orders` COUNT |
| `totalRoutes` | `quality_document_locations` COUNT |
| `quarter` | `performance_reports.year` + `quarter` |
| `year` | `performance_reports.year` |
| `quarterNum` | `performance_reports.quarter` |
### 3.2 RouteItem ↔ QualityDocumentOrder
| 프론트 (RouteItem) | 백엔드 (QualityDocumentOrder) |
|--------------------|-------------------------------|
| `id` | `quality_document_orders.id` |
| `code` | `orders.order_code` |
| `date` | `orders.order_date` |
| `site` | `orders.site_name` |
| `locationCount` | `quality_document_locations` COUNT |
| `subItems` | `quality_document_locations` 변환 |
### 3.3 ChecklistCategory ↔ AuditChecklistCategory
| 프론트 (ChecklistCategory) | 백엔드 (AuditChecklistCategory) |
|---------------------------|--------------------------------|
| `id` | `audit_checklist_categories.id` |
| `title` | `audit_checklist_categories.title` |
| `subItems` | `audit_checklist_items` 관계 |
---
## 4. 일정 산정
| Phase | 작업 내용 | 예상 소요 |
|-------|----------|-----------|
| **Phase 1** | 2일차 API 연동 (기존 API 활용) | |
| ├ 1-1 | Backend: 전용 서비스 + 컨트롤러 + 라우트 | 1일 |
| ├ 1-2 | Backend: 8종 서류 조합 조회 로직 | 1일 |
| ├ 1-3 | Frontend: actions.ts 생성 + 목업 교체 | 1일 |
| └ 1-4 | 테스트 및 디버깅 | 0.5일 |
| **Phase 2** | 1일차 백엔드 구축 (완전 신규) | |
| ├ 2-1 | DB 설계 + 마이그레이션 (4테이블) | 0.5일 |
| ├ 2-2 | 모델 4개 + 관계 설정 | 0.5일 |
| ├ 2-3 | 서비스 + 컨트롤러 + 라우트 | 1일 |
| └ 2-4 | 초기 데이터 시딩 (점검표 마스터) | 0.5일 |
| **Phase 3** | 프론트엔드 전환 | |
| ├ 3-1 | 2일차 컴포넌트 API 바인딩 | 1일 |
| ├ 3-2 | 1일차 컴포넌트 API 바인딩 | 1일 |
| └ 3-3 | 통합 테스트 + mockData 정리 | 0.5일 |
**총 예상: ~8일**
---
## 5. 의존성 및 리스크
### 5.1 의존성
| 항목 | 의존 대상 | 상태 |
|------|-----------|------|
| 품질관리서 데이터 | `quality_documents` 실 데이터 | ✅ 운영 중 |
| 실적신고 데이터 | `performance_reports` 실 데이터 | ✅ 운영 중 |
| 수입검사 성적서 | `inspections` (IQC) | ✅ 운영 중 |
| 중간검사 성적서 | `inspections` (PQC) | ⚠️ 구현 중 |
| 작업일지 | `work_orders` 연결 | ✅ 운영 중 |
| 출하/납품 | `shipments` | ✅ 운영 중 |
| 기준 문서 파일 | EAV Document 시스템 | ✅ 운영 중 |
### 5.2 리스크
| 리스크 | 영향 | 완화 방안 |
|--------|------|-----------|
| 8종 서류 추적 로직 복잡 | Phase 1 지연 | 서류별 독립 조회 후 프론트에서 조합 |
| 1일차 점검표 초기 데이터 부재 | Phase 2 테스트 어려움 | 시더로 기본 점검표 생성 |
| 중간검사 미완성 | 2일차 일부 서류 누락 | 빈 상태로 표시, 추후 연동 |
---
## 6. 권장 진행 순서
```
Phase 1 (2일차 API 연동) — 3.5일
Phase 2 (1일차 백엔드 구축) — 2.5일
Phase 3 (프론트엔드 전환) — 2.5일
```
**Phase 1을 먼저 하는 이유:**
- 기존 API 활용으로 빠르게 실 데이터 확인 가능
- 로트 추적은 실적신고와 직접 연결되어 비즈니스 우선순위 높음
- Phase 2(1일차)는 독립적인 신규 개발이므로 나중에 진행 가능
---
## 관련 문서
- [품질인정심사 기능 문서](../../features/quality-management/quality-certification-audit.md)
- [제품검사 관리](../../features/quality-management/inspection-management.md)
- [생산실적신고](../../features/quality-management/performance-reports.md)
- [통합 개선 마스터 플랜](./integrated-master-plan.md)
---
**최종 업데이트**: 2026-03-09

View File

@@ -0,0 +1,369 @@
# 방화셔터 백과사전 이미지 생성 프롬프트
> **작성일**: 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

@@ -0,0 +1,298 @@
# 결재관리 시스템
> **작성일**: 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

@@ -0,0 +1,594 @@
# 결재관리 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

@@ -0,0 +1,286 @@
# 결재관리 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

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

@@ -0,0 +1,381 @@
# 결재관리 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

@@ -0,0 +1,565 @@
# 결재관리 워크플로우 상세
> **작성일**: 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

@@ -0,0 +1,250 @@
# 전자계약 알림톡/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

@@ -0,0 +1,173 @@
# 명함신청 관리
> **작성일**: 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

@@ -0,0 +1,284 @@
# 신용평가 시스템 (쿠콘 연동)
> **작성일**: 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

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

View File

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

129
features/planning/README.md Normal file
View File

@@ -0,0 +1,129 @@
# 주일기업 기획 메뉴
> **작성일**: 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

@@ -0,0 +1,275 @@
# 공사현장 사진대지
> **작성일**: 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

@@ -0,0 +1,456 @@
# 회의록 작성
> **작성일**: 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

@@ -0,0 +1,222 @@
# 견적/프로젝트/워크플로우 화면 명세
> **작성일**: 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

110
features/rd/README.md Normal file
View File

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

@@ -0,0 +1,246 @@
# 디자인 인사이트 — 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

@@ -0,0 +1,366 @@
# 기획디자인 — 스토리보드 에디터
> **작성일**: 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

View File

@@ -0,0 +1,325 @@
# AI 및 스토리지 설정 기술문서
> 최종 업데이트: 2026-03-03
## 개요
SAM MNG 시스템의 AI API 및 클라우드 스토리지(GCS) 설정을 관리하는 기능입니다.
관리자 UI에서 설정하거나, `.env` 환경변수로 설정할 수 있습니다.
**접근 경로**: 시스템 관리 > AI 설정 (`/system/ai-config`)
---
## 지원 Provider
### AI Provider
| Provider | 용도 | 기본 모델 |
|----------|------|----------|
| `gemini` | Google Gemini (명함 OCR, AI 어시스턴트) | gemini-2.5-flash |
| `claude` | Anthropic Claude | claude-sonnet-4-20250514 |
| `openai` | OpenAI GPT | gpt-4o |
### Storage Provider
| Provider | 용도 |
|----------|------|
| `gcs` | Google Cloud Storage (음성 녹음 백업) |
---
## 데이터베이스 구조
### 테이블: `ai_configs`
```sql
CREATE TABLE ai_configs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL, -- 설정 이름 (예: "Production Gemini")
provider VARCHAR(20) NOT NULL, -- gemini, claude, openai, gcs
api_key VARCHAR(255) NOT NULL, -- API 키 (GCS는 'gcs_service_account')
model VARCHAR(100) NOT NULL, -- 모델명 (GCS는 '-')
base_url VARCHAR(255) NULL, -- 커스텀 Base URL
description TEXT NULL, -- 설명
is_active BOOLEAN DEFAULT FALSE, -- 활성화 여부 (provider당 1개만)
options JSON NULL, -- 추가 옵션 (아래 참조)
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL -- Soft Delete
);
```
### options JSON 구조
**AI Provider (Gemini Vertex AI)**:
```json
{
"auth_type": "vertex_ai",
"project_id": "my-gcp-project",
"region": "us-central1",
"service_account_path": "/var/www/sales/apikey/google_service_account.json"
}
```
**AI Provider (API Key)**:
```json
{
"auth_type": "api_key"
}
```
**GCS Provider**:
```json
{
"bucket_name": "my-bucket-name",
"service_account_path": "/var/www/sales/apikey/google_service_account.json",
"service_account_json": { ... } // 또는 JSON 직접 입력
}
```
---
## 설정 우선순위
### GCS 설정 우선순위
```
1. DB 설정 (ai_configs 테이블의 활성화된 gcs provider)
↓ 없으면
2. 환경변수 (.env의 GCS_BUCKET_NAME, GCS_SERVICE_ACCOUNT_PATH)
↓ 없으면
3. 레거시 파일 (/sales/apikey/gcs_config.txt, google_service_account.json)
```
### AI 설정 우선순위
```
1. DB 설정 (ai_configs 테이블의 활성화된 provider)
↓ 없으면
2. 환경변수 (.env의 GEMINI_API_KEY 등)
↓ 없으면
3. 레거시 파일
```
---
## 환경변수 설정 (.env)
### GCS 설정
```env
# Google Cloud Storage (음성 녹음 백업)
GCS_BUCKET_NAME=your-bucket-name
GCS_SERVICE_ACCOUNT_PATH=/var/www/sales/apikey/google_service_account.json
GCS_USE_DB_CONFIG=true # false면 DB 설정 무시, .env만 사용
```
### AI 설정 (참고)
```env
# Google Gemini API
GEMINI_API_KEY=your-api-key
GEMINI_PROJECT_ID=your-project-id
```
---
## 관련 파일 목록
### 모델
| 파일 | 설명 |
|------|------|
| `app/Models/System/AiConfig.php` | AI 설정 Eloquent 모델 |
### 컨트롤러
| 파일 | 설명 |
|------|------|
| `app/Http/Controllers/System/AiConfigController.php` | CRUD + 연결 테스트 |
### 서비스
| 파일 | 설명 |
|------|------|
| `app/Services/GoogleCloudStorageService.php` | GCS 업로드/다운로드/삭제 |
| `app/Services/GeminiService.php` | Gemini API 호출 (명함 OCR 등) |
### 설정
| 파일 | 설명 |
|------|------|
| `config/gcs.php` | GCS 환경변수 설정 |
### 뷰
| 파일 | 설명 |
|------|------|
| `resources/views/system/ai-config/index.blade.php` | AI 설정 관리 페이지 |
### 라우트
```php
// routes/web.php
Route::prefix('system')->name('system.')->group(function () {
Route::resource('ai-config', AiConfigController::class)->except(['show', 'create', 'edit']);
Route::post('ai-config/{id}/toggle', [AiConfigController::class, 'toggle'])->name('ai-config.toggle');
Route::post('ai-config/test', [AiConfigController::class, 'test'])->name('ai-config.test');
Route::post('ai-config/test-gcs', [AiConfigController::class, 'testGcs'])->name('ai-config.test-gcs');
});
```
---
## 주요 메서드
### AiConfig 모델
```php
// Provider별 활성 설정 조회
AiConfig::getActiveGemini(); // ?AiConfig
AiConfig::getActiveClaude(); // ?AiConfig
AiConfig::getActiveGcs(); // ?AiConfig
AiConfig::getActive('openai'); // ?AiConfig
// GCS 전용 메서드
$config->getBucketName(); // ?string
$config->getServiceAccountJson(); // ?array
$config->getServiceAccountPath(); // ?string
$config->isGcs(); // bool
// Vertex AI 전용 메서드
$config->isVertexAi(); // bool
$config->getProjectId(); // ?string
$config->getRegion(); // string (기본: us-central1)
```
### GoogleCloudStorageService
```php
$gcs = new GoogleCloudStorageService();
// 사용 가능 여부
$gcs->isAvailable(); // bool
// 설정 소스 확인
$gcs->getConfigSource(); // 'db' | 'env' | 'legacy' | 'none'
// 파일 업로드
$gcsUri = $gcs->upload($localPath, $objectName); // 'gs://bucket/object' | null
// 서명된 다운로드 URL (60분 유효)
$url = $gcs->getSignedUrl($objectName, 60); // string | null
// 파일 삭제
$gcs->delete($objectName); // bool
```
---
## UI 구조
### 탭 구성
- **AI 설정 탭**: Gemini, Claude, OpenAI 설정 관리
- **스토리지 설정 탭**: GCS 설정 관리
### 기능
- 설정 추가/수정/삭제
- 활성화/비활성화 토글 (provider당 1개만 활성화)
- 연결 테스트
---
## 사용 예시
### GCS 업로드 (ConsultationController)
```php
use App\Services\GoogleCloudStorageService;
public function uploadAudio(Request $request)
{
// 파일 저장
$path = $file->store("tenant/consultations/{$tenantId}");
$fullPath = storage_path('app/' . $path);
// 10MB 이상이면 GCS에도 업로드
if ($file->getSize() > 10 * 1024 * 1024) {
$gcs = new GoogleCloudStorageService();
if ($gcs->isAvailable()) {
$gcsUri = $gcs->upload($fullPath, "consultations/{$tenantId}/" . basename($path));
}
}
}
```
### 명함 OCR (GeminiService)
```php
use App\Services\GeminiService;
$gemini = new GeminiService();
$result = $gemini->extractBusinessCard($imagePath);
```
---
## 배포 가이드
### 서버 최초 설정
1. `.env` 파일에 GCS 설정 추가:
```env
GCS_BUCKET_NAME=production-bucket
GCS_SERVICE_ACCOUNT_PATH=/var/www/sales/apikey/google_service_account.json
```
2. 서비스 계정 JSON 파일 배치:
```
/var/www/sales/apikey/google_service_account.json
```
3. 설정 캐시 갱신:
```bash
docker exec sam-mng-1 php artisan config:cache
```
### 이후 배포
- 코드 push만으로 동작 (설정 변경 불필요)
- UI에서 오버라이드하고 싶을 때만 DB 설정 사용
---
## 트러블슈팅
### GCS 업로드 실패
1. **설정 확인**:
```php
$gcs = new GoogleCloudStorageService();
dd($gcs->isAvailable(), $gcs->getConfigSource(), $gcs->getBucketName());
```
2. **로그 확인**:
```bash
docker exec sam-mng-1 tail -f storage/logs/laravel.log | grep GCS
```
3. **일반적인 원인**:
- 서비스 계정 파일 경로 오류
- 서비스 계정에 Storage 권한 없음
- 버킷 이름 오타
### AI API 연결 실패
1. **API 키 확인**: UI에서 "테스트" 버튼 클릭
2. **모델명 확인**: provider별 지원 모델 확인
3. **할당량 확인**: Google Cloud Console에서 API 할당량 확인
---
## 레거시 파일 위치 (참고)
Docker 컨테이너 내부 경로:
```
/var/www/sales/apikey/
├── gcs_config.txt # bucket_name=xxx
├── google_service_account.json # GCP 서비스 계정 키
└── gemini_api_key.txt # Gemini API 키 (레거시)
```
호스트 경로 (mng 기준):
```
../sales/apikey/
```

291
guides/ai-management.md Normal file
View File

@@ -0,0 +1,291 @@
# SAM AI 관리 종합 가이드
> **작성일**: 2026-03-03
> **상태**: 확정
> **대상 독자**: SAM 프로젝트에 투입되는 모든 개발자
---
## 1. 개요
### 1.1 목적
SAM 시스템에서 사용하는 AI 서비스(Google Gemini, Anthropic Claude, OpenAI)의 **설정 구조, 호출 흐름, 모델 관리, 비용 추적**을 한눈에 파악할 수 있는 종합 가이드다.
### 1.2 현재 상태 (2026-03-03)
| Provider | 모델 | 용도 | 상태 |
|----------|------|------|------|
| **Google Gemini** | `gemini-2.5-flash` | 명함 OCR, 사업자등록증 OCR, 재무 리포트, 회의 요약, 동영상 스크립트 | ✅ 운영 중 |
| Anthropic Claude | `claude-sonnet-4-20250514` | AI 재무 분석 (예비) | 🟡 코드 준비 |
| OpenAI | `gpt-4o` | 범용 AI (예비) | 🟡 코드 준비 |
| Google Cloud STT | Chirp 2 | 음성 녹음 → 텍스트 변환 | ✅ 운영 중 |
| Google Cloud Storage | Standard | 음성 파일 백업 | ✅ 운영 중 |
---
## 2. 아키텍처
### 2.1 설정 흐름도
```
┌─────────────────────────────────────────────────────────────┐
│ .env 파일 (환경별 4곳) │
│ ┌───────────────────────────────────────────┐ │
│ │ GEMINI_API_KEY=AIzaSy... │ │
│ │ GEMINI_MODEL=gemini-2.5-flash │ ← ① 핵심 │
│ │ GEMINI_BASE_URL=https://...googleapis... │ │
│ └───────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ config/services.php │
│ ┌───────────────────────────────────────────┐ │
│ │ 'gemini' => [ │ │
│ │ 'model' => env('GEMINI_MODEL', │ │
│ │ 'gemini-2.5-flash'), │ ← ② fallback│
│ │ ] │ │
│ └───────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ AiConfig::getActiveGemini() │
│ ┌───────────────────────────────────────────┐ │
│ │ config('services.gemini.model', │ │
│ │ 'gemini-2.5-flash') │ ← ③ fallback│
│ └───────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 서비스 클래스 (실제 API 호출) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ BusinessCardOcrService → AiConfig::getActiveGemini│ │
│ │ TradingPartnerOcrService → AiConfig::getActiveGemini│ │
│ │ AiReportService (API) → config('services.gemini')│ │
│ │ GeminiScriptService → AiConfig::getActiveGemini│ │
│ │ NotionService → AiConfig::getActiveGemini│ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
> **핵심**: `.env`의 `GEMINI_MODEL` 값만 바꾸면 전체 서비스가 새 모델을 사용한다.
### 2.2 프로젝트별 역할
| 프로젝트 | AI 관련 역할 |
|----------|-------------|
| **MNG** | 명함 OCR, 사업자등록증 OCR, 동영상 스크립트, AI 설정 관리 UI, 토큰 사용량 조회 |
| **API** | 재무 AI 리포트 생성, 토큰 사용량 저장, AI 가격 설정 |
| **React** | AI 기능 없음 (Google Maps만 사용) |
---
## 3. 환경변수 설정
### 3.1 Gemini AI 설정
```env
# ─── Google Gemini AI ───
GEMINI_API_KEY=AIzaSy... # Google AI Studio에서 발급
GEMINI_MODEL=gemini-2.5-flash # 사용할 모델명
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
GEMINI_PROJECT_ID=codebridge-chatbot # GCP 프로젝트 ID
```
### 3.2 환경별 .env 위치
| 환경 | API 경로 | MNG 경로 |
|------|---------|---------|
| 로컬 (Docker) | `/home/aweso/sam/api/.env` | `/home/aweso/sam/mng/.env` |
| 개발서버 | `/home/webservice/api/.env` | `/home/webservice/mng/.env` |
| 운영서버 | `/home/webservice/api/.env` | `/home/webservice/mng/.env` |
> API와 MNG는 같은 `GEMINI_MODEL` 값을 사용해야 한다.
---
## 4. 서비스별 AI 호출 매핑
### 4.1 MNG 프로젝트
| 서비스 | 파일 | 용도 | 설정 소스 |
|--------|------|------|----------|
| 명함 OCR | `app/Services/BusinessCardOcrService.php` | 명함 이미지 → 연락처 정보 추출 | `AiConfig::getActiveGemini()` |
| 사업자등록증 OCR | `app/Services/TradingPartnerOcrService.php` | 사업자등록증 → 거래처 정보 추출 | `AiConfig::getActiveGemini()` |
| 동영상 스크립트 | `app/Services/Video/GeminiScriptService.php` | 영상 자막/스크립트 생성 | `AiConfig::getActiveGemini()` |
| Notion 연동 | `app/Services/NotionService.php` | Notion 콘텐츠 AI 분석 | `AiConfig::getActiveGemini()` |
### 4.2 API 프로젝트
| 서비스 | 파일 | 용도 | 설정 소스 |
|--------|------|------|----------|
| 재무 AI 리포트 | `app/Services/AiReportService.php` | 지출/매출/매입 데이터 AI 분석 | `config('services.gemini')` |
### 4.3 공통 유틸
| 파일 | 역할 |
|------|------|
| `mng/app/Helpers/AiTokenHelper.php` | 토큰 사용량 저장 (Gemini, Claude, GCS, STT) |
| `mng/app/Models/System/AiConfig.php` | AI 설정 모델 (Provider별 활성 설정 조회) |
| `mng/app/Models/System/AiPricingConfig.php` | 토큰 단가 설정 (비용 계산) |
| `mng/app/Models/System/AiTokenUsage.php` | 토큰 사용 이력 |
---
## 5. 관리 화면
### 5.1 AI 설정 관리
- **URL**: `/system/ai-config`
- **기능**: Provider별 API 키, 모델명, Base URL 관리 / 연결 테스트 / 활성화 토글
- **컨트롤러**: `app/Http/Controllers/System/AiConfigController.php`
### 5.2 AI 토큰 사용량
- **URL**: `/system/ai-token-usage`
- **기능**: 일별/메뉴별 토큰 사용량 조회 / 비용(USD, KRW) 통계 / 단가 설정
- **컨트롤러**: `app/Http/Controllers/System/AiTokenUsageController.php`
### 5.3 Google Cloud AI 가이드
- **URL**: `/google-cloud/ai-guide`
- **기능**: SAM에서 사용하는 Google AI 서비스 전체 현황 / 아키텍처 다이어그램
---
## 6. 모델 변경 이력 (버전 관리)
| 날짜 | 변경 내용 | 이유 | 영향 범위 |
|------|----------|------|----------|
| 2026-01-27 | 최초 설정: `gemini-2.0-flash` | SAM AI 기능 도입 | 전체 |
| **2026-03-03** | **`gemini-2.0-flash``gemini-2.5-flash`** | **Google 2026-06-01 서비스 종료 예고** | **전체** |
### 2026-03-03 변경 상세
**배경**: Google이 2026년 6월 1일부로 Gemini 2.0 Flash 모델 서비스를 종료한다는 통보를 함.
**수정된 파일 (코드)**:
| 프로젝트 | 파일 | 변경 내용 |
|----------|------|----------|
| API | `config/services.php` | fallback 기본값 변경 |
| API | `app/Services/AiReportService.php` | fallback 기본값 변경 |
| MNG | `config/services.php` | fallback 기본값 변경 |
| MNG | `app/Models/System/AiConfig.php` | DEFAULT_MODELS 상수 + getActiveGemini() fallback 변경 |
| MNG | `app/Services/NotionService.php` | fallback 기본값 변경 |
| MNG | `resources/views/system/ai-config/index.blade.php` | UI placeholder/기본값 변경 |
| MNG | `resources/views/google-cloud/ai-guide/index.blade.php` | 서비스 현황 모델명 변경 |
| MNG | `resources/views/academy/env-management.blade.php` | 환경 변수 예시 변경 |
**수정된 .env (환경별)**:
| 환경 | 수정 대상 | 담당 |
|------|----------|------|
| 로컬 api/.env | `GEMINI_MODEL=gemini-2.5-flash` | ✅ 완료 |
| 로컬 mng/.env | `GEMINI_MODEL=gemini-2.5-flash` | ✅ 완료 |
| 개발서버 api, mng | `GEMINI_MODEL=gemini-2.5-flash` | 배포 후 SSH 수정 필요 |
| 운영서버 api, mng | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 수정 |
**수정하지 않은 파일 (의도적)**:
| 파일 | 이유 |
|------|------|
| `api/database/migrations/` (3개) | 이미 실행 완료된 마이그레이션 — 변경 금지 |
| `cloud-api-pricing/index.blade.php` | `2.0 → 2.5` 마이그레이션 안내 UI — 이전 모델명이 의도적 |
---
## 7. 모델 업데이트 워크플로우
> 상세 절차: [ai-model-update-workflow.md](./ai-model-update-workflow.md)
### 7.1 요약 (7단계)
```
① 사전 확인 — 새 모델명, API 호환성, 가격 확인
② 로컬 테스트 — .env 수정 → config:clear → 연결 테스트
③ 코드 업데이트 — fallback 기본값 5곳 + 뷰 파일
④ 개발서버 배포 — 코드 push + .env 수정 + 기능 테스트
⑤ 단가 설정 — MNG 관리 화면에서 새 모델 단가 추가
⑥ 운영서버 배포 — cherry-pick + .env 수정 (개발팀장)
⑦ 사후 모니터링 — 토큰 로그 확인 + 에러 로그 감시 (1일)
```
### 7.2 긴급 롤백
```bash
# .env만 이전 모델로 변경 (코드 배포 불필요)
GEMINI_MODEL=gemini-2.0-flash
php artisan config:clear
```
---
## 8. 비용 관리
### 8.1 토큰 단가 (ai_pricing_configs)
| 모델 | 입력 ($/1M tokens) | 출력 ($/1M tokens) | 비고 |
|------|-------------------|--------------------|------|
| gemini-2.5-flash | 0.15 | 0.60 | 2026-03-03~ |
| gemini-2.0-flash | 0.10 | 0.40 | ~2026-06-01 종료 예정 |
> 단가는 MNG `/system/ai-token-usage` → 단가 설정에서 관리
### 8.2 비용 계산 흐름
```
API 호출 → 응답의 usageMetadata에서 토큰 수 추출
AiTokenHelper::saveGeminiUsage()
ai_pricing_configs에서 단가 조회 (캐시 60분)
ai_token_usages 테이블에 기록 (토큰 수, USD, KRW)
```
---
## 9. 신규 개발자 온보딩 체크리스트
AI 관련 작업을 시작하기 전:
- [ ] 이 문서 전체 읽기
- [ ] 로컬 `.env``GEMINI_API_KEY`, `GEMINI_MODEL` 설정 확인
- [ ] MNG `/system/ai-config` 화면에서 연결 테스트 성공 확인
- [ ] AI 호출 서비스 파일 위치 파악 (섹션 4 참조)
- [ ] `AiConfig::getActiveGemini()` 사용법 이해
- [ ] `AiTokenHelper::saveGeminiUsage()` 로 토큰 추적하는 패턴 이해
### 새 AI 기능 추가 시
```php
// 1. AiConfig에서 활성 설정 가져오기
$config = AiConfig::getActiveGemini();
if (!$config) throw new \RuntimeException('Gemini API 설정이 없습니다.');
// 2. API 호출
$url = "{$config->base_url}/models/{$config->model}:generateContent?key={$config->api_key}";
$response = Http::timeout(30)->post($url, [ ... ]);
// 3. 토큰 사용량 기록
AiTokenHelper::saveGeminiUsage(
$response->json()['usageMetadata'] ?? [],
$config->model,
'메뉴명'
);
```
---
## 관련 문서
| 문서 | 경로 | 설명 |
|------|------|------|
| AI 설정 기술문서 | `docs/guides/ai-config-settings.md` | DB 구조, 메서드, 코드 예시 |
| 모델 업데이트 워크플로우 | `docs/guides/ai-model-update-workflow.md` | 모델 변경 시 Step-by-Step 절차 |
| 변경 이력 | `docs/changes/20260303_gemini_model_upgrade.md` | 2.0→2.5 마이그레이션 상세 기록 |
---
**최종 업데이트**: 2026-03-03

View File

@@ -0,0 +1,313 @@
# AI 모델 업데이트 워크플로우
> **작성일**: 2026-03-03
> **상태**: 확정
> **대상**: Google Gemini, Claude, OpenAI 등 AI 모델 버전 업데이트 시 적용
---
## 1. 개요
### 1.1 목적
AI 제공사(Google, Anthropic, OpenAI)가 모델을 업데이트하거나 기존 모델을 종료(deprecate)할 때, SAM 시스템 전체를 안전하게 마이그레이션하기 위한 표준 절차를 정의한다.
### 1.2 핵심 원칙
SAM의 AI 설정은 **환경변수 기반 아키텍처**로 설계되어 있어, 모델 변경 시 코드 수정이 최소화된다.
```
.env (GEMINI_MODEL=gemini-2.5-flash) ← ① 여기만 바꾸면 동작
config/services.php (env() + fallback 기본값) ← ② fallback도 업데이트 권장
AiConfig 모델 (DEFAULT_MODELS 상수) ← ③ 관리 화면 기본값
서비스 클래스 (config 또는 AiConfig 참조) ← 코드 수정 불필요
```
---
## 2. 수정 대상 전체 매핑
### 2.1 환경변수 (.env) — 🔴 필수
| 환경 | 파일 위치 | 변수명 |
|------|----------|--------|
| 로컬 API | `/home/aweso/sam/api/.env` | `GEMINI_MODEL` |
| 로컬 MNG | `/home/aweso/sam/mng/.env` | `GEMINI_MODEL` |
| 개발서버 API | `/home/webservice/api/.env` | `GEMINI_MODEL` |
| 개발서버 MNG | `/home/webservice/mng/.env` | `GEMINI_MODEL` |
| 운영서버 API | `/home/webservice/api/.env` | `GEMINI_MODEL` |
| 운영서버 MNG | `/home/webservice/mng/.env` | `GEMINI_MODEL` |
> **참고**: API와 MNG가 같은 `.env` 값을 사용하므로 둘 다 동일하게 변경해야 한다.
> **참고**: `GEMINI_API_KEY`와 `GEMINI_BASE_URL`은 모델 변경 시 대부분 그대로 유지된다.
### 2.2 코드 Fallback 기본값 — 🟡 권장
코드에 하드코딩된 fallback 기본값. `.env`가 정상 설정되어 있으면 동작에 영향 없지만, 유지보수를 위해 함께 업데이트한다.
| 파일 | 줄 | 현재 값 |
|------|---|---------|
| `api/config/services.php` | 46 | `env('GEMINI_MODEL', 'gemini-2.0-flash')` |
| `mng/config/services.php` | 40 | `env('GEMINI_MODEL', 'gemini-2.0-flash')` |
| `mng/app/Models/System/AiConfig.php` | 62 | `DEFAULT_MODELS['gemini'] = 'gemini-2.0-flash'` |
| `mng/app/Models/System/AiConfig.php` | 97 | `config('services.gemini.model', 'gemini-2.0-flash')` |
| `api/app/Services/AiReportService.php` | 326 | `config('services.gemini.model', 'gemini-2.0-flash')` |
### 2.3 데이터베이스 — 🟢 필요 시
| 테이블 | 변경 내용 | 변경 방법 |
|--------|----------|----------|
| `ai_configs` | 모델명 변경 | MNG 관리 화면 (`/system/ai-config`) |
| `ai_pricing_configs` | 새 모델 단가 추가 | MNG 관리 화면 (`/system/ai-token-usage` → 단가 설정) |
> `ai_token_usages` 테이블은 과거 기록이므로 수정 불필요. 새 호출부터 새 모델명으로 기록된다.
### 2.4 Google API Base URL — 보통 변경 불필요
| 변수 | 현재 값 | 비고 |
|------|---------|------|
| `GEMINI_BASE_URL` | `https://generativelanguage.googleapis.com/v1beta` | v1beta → v1 변경 시에만 수정 |
> Google이 API 버전을 올릴 때(v1beta → v1) Base URL도 변경 필요. 모델명만 바뀔 때는 그대로 유지.
---
## 3. 실행 절차 (Step-by-Step)
### Step 1: 사전 확인 (10분)
```bash
# 1. 새 모델명 확인 (Google AI Studio 또는 공식 문서)
# 예: gemini-2.0-flash → gemini-2.5-flash
# 2. API 호환성 확인
# - Base URL 변경 여부 (v1beta → v1 등)
# - 요청/응답 스키마 변경 여부
# - 가격 변경 여부
# 3. 현재 사용 중인 모델 확인
grep -r "GEMINI_MODEL" /home/aweso/sam/api/.env /home/aweso/sam/mng/.env
```
---
### Step 2: 로컬 환경 테스트 (15분)
```bash
# 1. 로컬 .env 수정
# api/.env
GEMINI_MODEL=gemini-2.5-flash
# mng/.env
GEMINI_MODEL=gemini-2.5-flash
# 2. 캐시 클리어 (config가 캐시되어 있을 수 있음)
docker exec sam-api-1 php artisan config:clear
docker exec sam-mng-1 php artisan config:clear
# 3. MNG 관리 화면에서 테스트
# http://mng.sam.kr/system/ai-config → "연결 테스트" 버튼
# 4. 실제 기능 테스트
# - 명함 OCR 테스트 (BusinessCardOcrService)
# - 사업자등록증 OCR 테스트 (TradingPartnerOcrService)
# - AI 리포트 생성 테스트 (AiReportService)
```
---
### Step 3: 코드 Fallback 업데이트 (5분)
```bash
# Claude Code에게 요청:
# "Gemini 모델 fallback 기본값을 gemini-2.5-flash로 업데이트해줘"
# 수정 대상 (5개 파일):
# 1. api/config/services.php
# 2. mng/config/services.php
# 3. mng/app/Models/System/AiConfig.php (DEFAULT_MODELS + getActiveGemini)
# 4. api/app/Services/AiReportService.php
```
---
### Step 4: 개발서버 배포 + 테스트 (10분)
```bash
# 1. 코드 커밋 & 푸시 (Jenkins 자동 배포)
# "개발서버 푸시"
# 2. 개발서버 .env 수정 (SSH 접속)
ssh pro@114.203.209.83
# API
cd /home/webservice/api
# .env 파일에서 GEMINI_MODEL 수정
php artisan config:clear
# MNG
cd /home/webservice/mng
# .env 파일에서 GEMINI_MODEL 수정
php artisan config:clear
# 3. 개발서버에서 기능 테스트
# https://admin.codebridge-x.com/system/ai-config → 연결 테스트
```
---
### Step 5: 단가 설정 업데이트 (5분)
```
# MNG 관리 화면 접속
# /system/ai-token-usage → 단가 설정 탭
# 기존 모델 단가 비활성화 (삭제하지 않음 — 과거 기록 참조용)
# 새 모델 단가 추가:
# - provider: gemini
# - model_name: gemini-2.5-flash
# - input_price_per_million: (Google 공식 가격)
# - output_price_per_million: (Google 공식 가격)
# - exchange_rate: (현재 환율)
# - is_active: true
```
---
### Step 6: 운영서버 배포 (10분)
```bash
# 1. 운영 코드 배포
# "운영서버 푸시"
# 2. 운영서버 .env 수정 (개발팀장이 직접)
# API: /home/webservice/api/.env → GEMINI_MODEL=gemini-2.5-flash
# MNG: /home/webservice/mng/.env → GEMINI_MODEL=gemini-2.5-flash
# php artisan config:clear (api, mng 각각)
# 3. 운영서버 기능 확인
```
---
### Step 7: 사후 모니터링 (1일)
```
# 1. AI 토큰 사용량 모니터링
# /system/ai-token-usage → 새 모델명으로 로그 기록되는지 확인
# 2. 에러 로그 모니터링
# API: storage/logs/laravel.log
# MNG: storage/logs/laravel.log
# 3. 기존 모델 종료일 전까지 롤백 준비
# .env의 GEMINI_MODEL만 이전 값으로 되돌리면 즉시 롤백
```
---
## 4. 체크리스트 (모델 업데이트 시)
### 사전 준비
- [ ] 새 모델명 확인 (공식 문서)
- [ ] API 호환성 확인 (Base URL, 스키마 변경 여부)
- [ ] 새 모델 가격 확인
### 로컬 테스트
- [ ] `api/.env``GEMINI_MODEL` 수정
- [ ] `mng/.env``GEMINI_MODEL` 수정
- [ ] Docker config:clear 실행
- [ ] MNG AI 설정 화면에서 연결 테스트 성공
- [ ] 명함 OCR / 사업자등록증 OCR / AI 리포트 테스트
### 코드 업데이트
- [ ] `api/config/services.php` fallback 기본값 변경
- [ ] `mng/config/services.php` fallback 기본값 변경
- [ ] `AiConfig.php` DEFAULT_MODELS 상수 변경
- [ ] `AiConfig::getActiveGemini()` fallback 변경
- [ ] `AiReportService.php` fallback 변경
- [ ] Git 커밋
### 개발서버
- [ ] 코드 푸시 (Jenkins 배포)
- [ ] `.env` 수정 (api, mng)
- [ ] `config:clear` 실행
- [ ] 연결 테스트 + 기능 테스트
### 단가 설정
- [ ] 기존 모델 단가 비활성화
- [ ] 새 모델 단가 추가 (input/output 가격, 환율)
### 운영서버
- [ ] 코드 푸시 (cherry-pick → main)
- [ ] `.env` 수정 (개발팀장 직접)
- [ ] `config:clear` 실행
- [ ] 기능 확인
### 사후 관리
- [ ] 토큰 사용량 로그에 새 모델명 확인
- [ ] 에러 로그 모니터링 (1일)
- [ ] 구 모델 종료일 전 롤백 가능 상태 유지
---
## 5. 긴급 롤백 절차
모델 변경 후 문제 발생 시:
```bash
# 1. .env만 이전 모델로 변경 (코드 수정 불필요)
GEMINI_MODEL=gemini-2.0-flash
# 2. 캐시 클리어
php artisan config:clear
# 3. 즉시 이전 모델로 복구됨
```
> `.env` 기반 아키텍처의 장점: 코드 배포 없이 환경변수만 변경하면 즉시 롤백 가능
---
## 6. FAQ
### Q: Base URL도 바꿔야 하나?
A: 대부분 **변경 불필요**. Google이 API 버전을 올릴 때(예: `v1beta``v1`)에만 `GEMINI_BASE_URL`도 함께 변경한다. 모델명만 바뀔 때는 `GEMINI_MODEL`만 수정하면 된다.
### Q: API 키도 바꿔야 하나?
A: **변경 불필요**. 동일 프로젝트 내에서 모델이 바뀌어도 API 키는 유지된다.
### Q: React 프로젝트도 수정해야 하나?
A: **불필요**. React는 Google Maps API Key만 사용하며, Gemini AI 호출 코드가 없다.
### Q: MNG 관리 화면에서만 바꾸면 안 되나?
A: 현재 구조상 `AiConfig::getActiveGemini()`**DB가 아니라 .env 값을 읽는다**. DB의 `ai_configs` 테이블은 관리 화면 표시/테스트용이며, 실제 API 호출은 `.env``config()` 경로를 따른다. 따라서 `.env` 수정이 필수다.
### Q: 향후 DB 기반으로 전환하면 더 편해지나?
A: 맞다. `AiConfig::getActiveGemini()`가 DB에서 활성 설정을 읽도록 리팩토링하면, MNG 관리 화면에서만 모델을 바꿔도 전체 적용된다. 서버 SSH 접속 없이 웹에서 즉시 변경 가능해진다. 이는 향후 개선 과제로 검토할 수 있다.
---
## 관련 문서
- [AI 설정 가이드](/home/aweso/sam/docs/guides/ai-config-settings.md)
- [서버 환경 비교](/home/aweso/sam/CLAUDE.md) — 실행 환경 섹션
---
**최종 업데이트**: 2026-03-03

View File

@@ -0,0 +1,387 @@
# HTML → PPTX 변환 도구 사용법
> **작성일**: 2026-03-01
> **상태**: 확정
---
## 1. 개요
HTML 슬라이드 파일을 PowerPoint(PPTX)로 변환하는 로컬 도구이다.
Playwright(브라우저 렌더링) + PptxGenJS(PPTX 생성)를 조합하여, HTML/CSS로 디자인한 슬라이드를 그대로 PPTX로 출력한다.
### 1.1 구성 요소
```
~/.claude/skills/pptx-skill/scripts/
├── html2pptx.js ← 핵심 변환 엔진 (HTML → PPTX)
└── node_modules/
├── pptxgenjs ← PPTX 파일 생성 라이브러리
├── playwright ← 브라우저 렌더링 (HTML 파싱)
├── sharp ← 이미지 처리
└── ...
```
### 1.2 사전 조건
| 항목 | 현재 설치 상태 |
|------|---------------|
| Node.js | v24.13.0 (`~/.nvm/versions/node/`) |
| html2pptx.js | `~/.claude/skills/pptx-skill/scripts/html2pptx.js` |
| pptxgenjs | 위 scripts/node_modules 안에 설치됨 |
| playwright | 위 scripts/node_modules 안에 설치됨 |
> 별도 `npm install`이 필요 없다. 이미 모든 의존성이 설치되어 있다.
---
## 2. 작업 흐름
```
① HTML 슬라이드 작성 (slides/ 폴더)
② 변환 스크립트 작성 (convert.cjs)
③ 터미널에서 실행: node convert.cjs
④ PPTX 파일 생성 완료
```
---
## 3. HTML 슬라이드 작성법
### 3.1 기본 템플릿
```html
<!DOCTYPE html>
<html>
<head>
<!-- 폰트: Pretendard (CDN) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
/* ⚠️ 반드시 width/height를 pt 단위로 지정 */
width: 720pt; height: 405pt; /* 16:9 가로형 */
font-family: 'Pretendard', sans-serif;
background: #0F2439;
padding: 32pt 40pt;
}
</style>
</head>
<body>
<!-- 여기에 슬라이드 내용 작성 -->
<h1 style="font-size: 24pt; color: #ffffff;">제목</h1>
<p style="font-size: 12pt; color: rgba(255,255,255,0.6);">본문 내용</p>
</body>
</html>
```
### 3.2 슬라이드 크기 (body width/height)
| 용도 | body 크기 | 변환 스크립트 layout |
|------|----------|---------------------|
| **16:9 가로형** (발표용) | `width: 720pt; height: 405pt;` | `width: 10, height: 5.625` |
| **9:16 세로형** (브로셔) | `width: 405pt; height: 720pt;` | `width: 5.625, height: 10` |
| **4:3 가로형** (구형) | `width: 720pt; height: 540pt;` | `width: 10, height: 7.5` |
> **중요**: HTML의 body 크기와 변환 스크립트의 layout 크기가 일치해야 한다. 불일치 시 에러 발생.
### 3.3 필수 규칙
#### 텍스트 줄바꿈 방지 (가장 중요)
브라우저와 PowerPoint의 폰트 렌더링 차이로, HTML에서 한 줄인 텍스트가 PPTX에서 두 줄로 넘어가는 문제가 발생한다.
```html
<!-- ✅ 한 줄짜리 텍스트에는 반드시 white-space: nowrap -->
<p style="white-space: nowrap; font-size: 10pt;">이 텍스트는 한 줄입니다</p>
<!-- ❌ nowrap 없으면 PPTX에서 줄바꿈될 수 있음 -->
<p style="font-size: 10pt;">이 텍스트는 한 줄입니다</p>
<!-- ✅ 의도적 멀티라인은 nowrap 불필요 -->
<p style="font-size: 10pt; line-height: 1.6;">
여러 줄로 의도된 텍스트입니다.<br>
이 경우 nowrap을 넣지 않는다.
</p>
```
#### 적용 대상
- 뱃지 텍스트 (01, UC-01 등)
- 카드 제목, 태그, 짧은 라벨
- 푸터 텍스트
- 단일행 `<p>` 태그 전부
#### 이미지 경로
```html
<!-- ✅ 절대 경로 사용 -->
<img src="/home/aweso/sam/docs/assets/bi/sam_bi_white.png" style="height: 24pt;">
<!-- ❌ 상대 경로는 동작하지 않을 수 있음 -->
<img src="../../assets/bi/sam_bi_white.png">
```
#### 스타일 작성
```html
<!-- ✅ 인라인 스타일 사용 (가장 안정적) -->
<div style="background: #2E86AB; padding: 8pt; border-radius: 6pt;">
<!-- ⚠️ <style> 태그의 클래스도 사용 가능하지만, 인라인이 더 안정적 -->
```
### 3.4 사용 가능한 CSS 속성
| 속성 | 지원 | 비고 |
|------|:----:|------|
| background (색상) | ✅ | 단색, rgba 모두 지원 |
| background (그라데이션) | ✅ | linear-gradient 지원 |
| border | ✅ | 색상, 두께, radius |
| border-radius | ✅ | px, pt 단위 |
| font-size, font-weight | ✅ | pt 단위 권장 |
| color | ✅ | hex, rgba |
| padding, margin | ✅ | pt 단위 권장 |
| display: flex | ✅ | gap, align-items 등 |
| white-space: nowrap | ✅ | 줄바꿈 방지 (필수) |
| opacity | ✅ | |
| box-shadow | ⚠️ | 부분 지원 |
| transform | ❌ | 미지원 |
| animation | ❌ | 미지원 |
---
## 4. 변환 스크립트 작성법
### 4.1 기본 구조 (복사해서 사용)
```javascript
const path = require('path');
// ① 패키지 경로 설정 (이 두 줄은 항상 동일)
module.paths.unshift(
path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')
);
const PptxGenJS = require('pptxgenjs');
const html2pptx = require(
path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')
);
async function main() {
const pres = new PptxGenJS();
// ② 레이아웃 설정 (HTML body 크기와 일치해야 함)
pres.defineLayout({ name: 'CUSTOM', width: 10, height: 5.625 });
pres.layout = 'CUSTOM';
// ③ HTML 파일 변환
await html2pptx('/절대경로/slides/slide-01.html', pres);
// ④ PPTX 출력
await pres.writeFile({ fileName: '/절대경로/output.pptx' });
console.log('완료!');
}
main().catch(console.error);
```
### 4.2 실전 예시: 여러 슬라이드 변환
```javascript
const path = require('path');
module.paths.unshift(
path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')
);
const PptxGenJS = require('pptxgenjs');
const html2pptx = require(
path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')
);
async function main() {
const pres = new PptxGenJS();
// 16:9 가로형
pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 });
pres.layout = 'CUSTOM_16x9';
const slidesDir = path.join(__dirname, 'slides');
// 변환할 HTML 파일 목록
const slides = [
'slide-01.html',
'slide-02.html',
'slide-03.html',
];
for (const file of slides) {
console.log(`Converting ${file} ...`);
try {
await html2pptx(path.join(slidesDir, file), pres);
} catch (err) {
console.error(`Error: ${err.message}`);
}
}
const outputPath = path.join(__dirname, 'my-presentation.pptx');
await pres.writeFile({ fileName: outputPath });
console.log(`\nPPTX created: ${outputPath}`);
}
main().catch(console.error);
```
### 4.3 실전 예시: 번호 기반 자동 변환 (18슬라이드)
```javascript
// slide-01.html ~ slide-18.html 자동 변환
const totalSlides = 18;
for (let i = 1; i <= totalSlides; i++) {
const num = String(i).padStart(2, '0');
const htmlFile = path.join(slidesDir, `slide-${num}.html`);
await html2pptx(htmlFile, pres);
}
```
---
## 5. 실행 방법
### 5.1 터미널에서 직접 실행
```bash
# 해당 폴더로 이동 후 실행
cd /home/aweso/sam/docs/brochure
node convert-2page.cjs
# 또는 절대 경로로 실행
node /home/aweso/sam/docs/brochure/convert-2page.cjs
```
### 5.2 실행 결과
```
Converting brochure-2page-front.html ...
Converting brochure-2page-back.html ...
PPTX created: /home/aweso/sam/docs/brochure/sam-brochure-2page.pptx
```
> 에러가 나면 HTML body 크기와 layout 설정 불일치가 가장 흔한 원인이다.
---
## 6. 프로젝트 내 기존 사용 사례
| 경로 | 슬라이드 수 | 레이아웃 | 용도 |
|------|:-----------:|:--------:|------|
| `docs/usecase/` | 18장 | 16:9 가로 | 방화셔터 제안서 |
| `docs/usecase/brochure/` | 1장 / 2장 | 9:16 세로 | 방화셔터 브로셔 |
| `docs/brochure/` | 1장 / 2장 | 9:16 세로 | SAM 범용 브로셔 |
| `docs/plans/slides/` | N장 | 16:9 가로 | 배포 계획 발표 |
| `docs/rules/slides/` | N장 | 16:9 가로 | 정책 규칙 문서 |
---
## 7. 폴더 구조 권장 패턴
새 PPTX를 만들 때 아래 구조를 따른다:
```
docs/my-document/
├── slides/ ← HTML 슬라이드 파일들
│ ├── slide-01.html
│ ├── slide-02.html
│ └── slide-03.html
├── convert.cjs ← 변환 스크립트
└── my-document.pptx ← 생성된 PPTX (node convert.cjs 실행 후)
```
---
## 8. 문제 해결
| 증상 | 원인 | 해결 |
|------|------|------|
| `Error: dimensions don't match` | HTML body 크기 ≠ layout 설정 | body의 width/height와 `defineLayout` 값 맞추기 |
| 텍스트가 PPTX에서 줄바꿈됨 | `white-space: nowrap` 미적용 | 단일행 `<p>` 태그에 nowrap 추가 |
| 이미지 안 보임 | 상대 경로 사용 | 절대 경로(`/home/aweso/...`)로 변경 |
| `Cannot find module 'pptxgenjs'` | module.paths 설정 누락 | 스크립트 상단 2줄(module.paths.unshift) 확인 |
| `Cannot find module 'playwright'` | Playwright 미설치 | `cd ~/.claude/skills/pptx-skill/scripts && npx playwright install chromium` |
| PPTX는 생성되지만 빈 슬라이드 | HTML 내용 없음 | HTML 파일을 브라우저로 열어 확인 |
---
## 9. 빠른 시작 (3분 가이드)
### Step 1: 폴더 만들기
```bash
mkdir -p /home/aweso/sam/docs/my-pptx/slides
```
### Step 2: HTML 슬라이드 만들기
`slides/slide-01.html` 파일 생성:
```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 720pt; height: 405pt;
font-family: 'Pretendard', sans-serif;
background: #0F2439;
padding: 60pt 80pt;
display: flex; flex-direction: column;
justify-content: center;
}
</style>
</head>
<body>
<p style="white-space: nowrap; font-size: 10pt; color: #10B981; margin-bottom: 12pt;">MY COMPANY</p>
<h1 style="font-size: 36pt; font-weight: 800; color: #ffffff;">발표 제목을 여기에</h1>
<p style="font-size: 14pt; color: rgba(255,255,255,0.5); margin-top: 16pt;">부제목 또는 설명</p>
</body>
</html>
```
### Step 3: 변환 스크립트 만들기
`convert.cjs` 파일 생성:
```javascript
const path = require('path');
module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules'));
const PptxGenJS = require('pptxgenjs');
const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js'));
async function main() {
const pres = new PptxGenJS();
pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 });
pres.layout = 'CUSTOM_16x9';
await html2pptx(path.join(__dirname, 'slides', 'slide-01.html'), pres);
await pres.writeFile({ fileName: path.join(__dirname, 'my-presentation.pptx') });
console.log('완료!');
}
main().catch(console.error);
```
### Step 4: 실행
```bash
cd /home/aweso/sam/docs/my-pptx
node convert.cjs
# → my-presentation.pptx 생성됨
```
---
**최종 업데이트**: 2026-03-01

View File

@@ -0,0 +1,247 @@
# SAM 서버 동작 원리 초보자 가이드
> **작성일**: 2026-02-22
> **대상**: SAM 프로젝트에 새로 합류한 개발자
---
## 1. 개요
### 1.1 이 문서의 목적
SAM 시스템에서 **웹 요청이 어떤 경로로 흐르는지**, **git push 후 서버에서 무슨 일이 일어나는지**를 설명한다.
설정값 나열이 아닌, **"왜 이런 구조인가"**에 초점을 맞춘다.
### 1.2 SAM 전체 구조
```
브라우저 → Nginx (SSL 종료, 도메인별 라우팅)
┌────┬───┴───┬─────┬─────┐
▼ ▼ ▼ ▼ ▼
MNG API React Sales 5130 ← 5개 서비스
(PHP)(PHP) (Node) (PHP) (PHP7.3)
└────┴───┬───┴─────┴─────┘
MySQL 8.0 ← 단일 DB 공유
```
---
## 2. 웹 요청의 여정: URL에서 화면까지
### 2.1 전체 흐름
`https://mng.sam.kr/orders` 접속 시:
```
브라우저 →① Nginx →② PHP-FPM →③ Laravel →④ MySQL
브라우저 ←────────────────────────────── ⑤ 응답
```
### 2.2 Step 1: 브라우저 → Nginx
Nginx는 **도메인 이름**을 보고 어떤 서비스로 보낼지 결정한다.
- `mng.sam.kr` → MNG 컨테이너의 PHP-FPM (포트 9000)
- `api.sam.kr` → API 컨테이너의 PHP-FPM (포트 9000)
- `dev.sam.kr` → React 컨테이너의 Node.js (포트 3000)
또한 HTTP(80) 요청을 HTTPS(443)로 리다이렉트하고, SSL 인증서를 처리한다.
이를 **SSL 종료**(SSL Termination)라 한다. 내부 통신은 암호화 없이 빠르게 진행된다.
### 2.3 Step 2: Nginx → PHP-FPM
Nginx는 PHP 코드를 직접 실행하지 못한다. 대신 **FastCGI 프로토콜**로 PHP-FPM에 요청을 전달한다.
```
Nginx: "이 PHP 파일을 실행해줘" → fastcgi_pass mng:9000
PHP-FPM: "결과 HTML이야" → Nginx → 브라우저
```
PHP-FPM은 여러 **워커 프로세스**를 미리 만들어 두고, 요청이 오면 빈 워커에 할당한다.
MNG의 경우 최대 20개 워커(`pm.max_children = 20`)가 동시에 요청을 처리할 수 있다.
### 2.4 Step 3: PHP-FPM → Laravel
PHP-FPM이 실행하는 진입점은 `public/index.php`다. 여기서 Laravel 프레임워크가 시작된다.
```
public/index.php
→ Bootstrap (설정 로드, 서비스 등록)
→ 미들웨어 (인증, 권한, 로깅)
→ 라우터 (URL → 컨트롤러 매핑)
→ 컨트롤러 (비즈니스 로직)
→ 뷰 렌더링 (Blade 템플릿 → HTML)
```
### 2.5 Step 4: Laravel → MySQL
컨트롤러에서 Eloquent ORM으로 DB를 조회한다. 예를 들어:
```php
// 코드: Order::where('status', 'active')->get();
// 실제 SQL: SELECT * FROM orders WHERE status = 'active' AND tenant_id = 1;
```
`tenant_id`는 글로벌 스코프로 자동 추가되어, 다른 테넌트의 데이터가 섞이지 않는다.
### 2.6 Step 5: 응답이 돌아오는 길
MySQL → Laravel(HTML 생성) → PHP-FPM → Nginx → 브라우저 순으로 돌아온다.
MNG는 HTMX를 사용하므로, 이후 상호작용은 **HTML 조각**(partial)만 주고받아 페이지 전체를 새로고침하지 않는다.
---
## 3. 각 구성 요소의 역할
| 구성 요소 | 역할 | 비유 |
|-----------|------|------|
| **Nginx** | 리버스 프록시, SSL, 정적 파일 | 안내 데스크 |
| **PHP-FPM** | PHP 워커 풀 관리 | 창구 직원 팀 |
| **Laravel** | MVC, 라우팅, 비즈니스 로직 | 업무 매뉴얼 |
| **MySQL** | 데이터 저장/조회 | 서류 보관실 |
| **Supervisor** | 프로세스 감시, 자동 재시작 | 관리 감독관 |
### 3.1 Supervisor가 관리하는 프로세스
각 컨테이너 안에서 Supervisor가 여러 프로세스를 관리한다.
**API 컨테이너** (`sam-api-1`):
- `php-fpm` — PHP 요청 처리
- `nginx` — 컨테이너 내부 웹서버
- `queue-worker` — 백그라운드 작업 (이메일, 알림 등)
- `scheduler` — 60초마다 예약 작업 실행 (`schedule:run`)
**MNG 컨테이너** (`sam-mng-1`):
- `php-fpm`, `nginx` — 위와 동일
- `queue-worker` x2 — 2개 워커가 병렬 처리
---
## 4. 로컬 환경 vs 서버 환경
### 4.1 비교
```
[로컬 - Docker] [서버 - Bare-metal]
┌───────────────┐ ┌───────────────┐
│ sam-nginx-1 │ │ Nginx │
├───────────────┤ ├───────────────┤
│ sam-mng-1 │ │ MNG (직접) │
│ sam-api-1 │ │ API (직접) │
├───────────────┤ ├───────────────┤
│ sam-mysql-1 │ │ MySQL (직접) │
└───────────────┘ └───────────────┘
네트워크: samnet 네트워크: localhost
```
### 4.2 핵심 차이
| 항목 | 로컬 (Docker) | 서버 (Bare-metal) |
|------|--------------|-------------------|
| **DB 접속** | `DB_HOST=sam-mysql-1` | `DB_HOST=127.0.0.1` |
| **코드 반영** | 볼륨 마운트 (실시간) | `git pull` 필요 |
| **명령 실행** | `docker exec sam-api-1 php artisan ...` | `php artisan ...` |
---
## 5. "git push하면 무슨 일이 일어나는가?"
### 5.1 배포 흐름 다이어그램
```
개발자 PC (WSL) Gitea 서버 운영 서버
┌──────────┐ push ┌──────────┐ pull ┌──────────┐
│ 코드 수정 │ ──────────→ │ 원격 │ ←───────── │ 서버에서 │
│ git add │ │ 저장소 │ │ 수동 pull │
│ git commit│ └──────────┘ └──────────┘
└──────────┘
```
> **주의**: 자동 배포(CI/CD)가 없다. 서버에서 **수동으로 `git pull`** 해야 반영된다.
### 5.2 PHP 앱 배포 (MNG, API)
```bash
# 서버에서 실행하는 명령 (개발팀장이 수행)
cd /home/webservice/api
git pull # ① 최신 코드 받기
composer install # ② 패키지 의존성 동기화
php artisan migrate # ③ DB 구조 변경 적용
php artisan config:clear # ④ 설정 캐시 초기화
```
**각 명령이 필요한 이유**:
| 명령 | 왜 필요한가 |
|------|------------|
| `git pull` | 코드를 최신 상태로 동기화 |
| `composer install` | 새로 추가된 PHP 패키지 설치 (`composer.json` 변경 시) |
| `php artisan migrate` | 새 테이블/컬럼 생성 등 DB 스키마 적용 (API만) |
| `php artisan config:clear` | `.env` 또는 `config/` 변경 시 캐시된 설정 갱신 |
### 5.3 React 앱 배포 (Next.js)
서버 스펙(2코어, 3.8GB RAM)으로는 Next.js 빌드가 메모리 부족으로 실패한다.
따라서 **로컬에서 빌드 → 결과물을 서버에 업로드**하는 방식을 사용한다.
```bash
# deploy.sh가 수행하는 5단계
① 로컬에서 npm run build # standalone 빌드
② tar.gz로 압축 # .next/standalone + static + public
③ scp로 서버 업로드 # 압축 파일 전송
④ 서버에서 압축 해제 + 시작 # node server.js (포트 3001)
⑤ 로컬 정리 # 임시 파일 삭제
```
---
## 6. SAM 도메인별 요청 경로
### 6.1 도메인 → 서비스 매핑
| 도메인 | 서비스 | 기술 스택 | 응답 형태 |
|--------|--------|-----------|-----------|
| `mng.sam.kr` | MNG | Laravel + Blade + HTMX | HTML (서버 렌더링) |
| `api.sam.kr` | API | Laravel | JSON |
| `dev.sam.kr` | React | Next.js | HTML (SSR/CSR) |
| `sales.sam.kr` | Sales | Laravel | HTML |
| `5130.sam.kr` | 5130 | PHP 7.3 (레거시) | HTML |
### 6.2 서비스별 요청 흐름
**MNG** (관리자 화면 — Blade + HTMX):
```
브라우저 → Nginx(:443) → MNG PHP-FPM(:9000) → Laravel → Blade HTML
이후 HTMX가 HTML 조각을 Ajax로 교체 (전체 새로고침 없음)
```
**API** (REST API — JSON 응답):
```
React/외부 → Nginx(:443) → API PHP-FPM(:9000) → Laravel → JSON
인증: Bearer 토큰 (Authorization 헤더)
```
**React** (Next.js — SSR + CSR):
```
브라우저 → Nginx(:443) → Node.js(:3000) → SSR HTML
이후 React 하이드레이션 → CSR (클라이언트 렌더링)
API 호출 시 → Next.js API Route 프록시 → api.sam.kr
```
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 |
| [system-overview.md](../architecture/system-overview.md) | 시스템 아키텍처 레퍼런스 |
| [production-deployment-plan.md](../plans/production-deployment-plan.md) | 운영 배포 계획 |
| [dev-commands.md](../quickstart/dev-commands.md) | 개발 명령어 모음 |
---
**최종 업데이트**: 2026-02-22

View File

@@ -0,0 +1,486 @@
# SAM 테이블 설계 가이드 — 비전문가용
> **작성일**: 2026-03-02
> **대상 독자**: 개발자, 기획자, 관리자 — 데이터베이스를 잘 모르는 분도 읽을 수 있습니다
> **관련 정책**: `standards/options-column-policy.md` (개발자 전용 상세 규칙)
---
## 1. 이 문서는 왜 필요한가?
SAM은 **여러 회사가 하나의 시스템을 공유**하는 구조입니다.
A회사, B회사, C회사가 모두 같은 프로그램을 쓰지만, 각 회사가 필요한 정보는 다릅니다.
이 문서는 SAM에서 **데이터를 어떻게 저장하는지**, 그 설계 철학을 누구나 이해할 수 있도록 설명합니다.
---
## 2. 기본 개념: 데이터베이스 테이블이란?
데이터베이스 테이블은 **엑셀 시트**와 같습니다.
```
"주문" 테이블 (= 엑셀 시트)
열(컬럼) → 주문번호 │ 고객명 │ 금액 │ 상태
─────────────────────────────────────────────────────────
행(레코드) 1 → ORD-001 │ 김철수 │ 500,000 │ 완료
행(레코드) 2 → ORD-002 │ 이영희 │ 300,000 │ 진행중
행(레코드) 3 → ORD-003 │ 박민수 │ 800,000 │ 대기
```
- **열(컬럼)** = 정보의 종류 (주문번호, 고객명, 금액...)
- **행(레코드)** = 실제 데이터 한 건 (주문 1건)
---
## 3. 문제: 회사마다 필요한 정보가 다르다
SAM은 여러 회사가 같은 테이블을 공유합니다.
```
같은 "주문" 테이블을 쓰는데...
🏭 A회사 (블라인드 제조)
→ "절곡 각도", "날개 수" 정보가 필요해요
🏭 B회사 (스크린 제조)
→ "메시 밀도", "소재 종류" 정보가 필요해요
🏭 C회사 (셔터 제조)
→ "날개 간격", "색상 코드" 정보가 필요해요
```
---
### 3.1 전통적인 해결 방법 (SAM은 이렇게 안 합니다)
필요할 때마다 엑셀에 열을 추가하는 것처럼, 테이블에 컬럼을 추가합니다.
```
"주문" 테이블 — 전통적 방식
주문번호 │ 고객명 │ 금액 │ 절곡각도 │ 날개수 │ 메시밀도 │ 소재 │ 날개간격 │ 색상코드
─────────────────────────────────────────────────────────────────────────────────
ORD-001 │ 김철수 │ 50만 │ 45도 │ 12개 │ (빈칸) │ (빈칸) │ (빈칸) │ (빈칸) ← A회사
ORD-002 │ 이영희 │ 30만 │ (빈칸) │ (빈칸)│ 18 │ 폴리 │ (빈칸) │ (빈칸) ← B회사
ORD-003 │ 박민수 │ 80만 │ (빈칸) │ (빈칸)│ (빈칸) │ (빈칸) │ 25mm │ #FF0000 ← C회사
```
**문제점:**
- 회사가 100개면? 열이 수백 개로 늘어남
- 각 회사는 자기 것 빼고 전부 빈칸
- 새 회사가 들어올 때마다 시스템 전체를 수정해야 함
- 열 추가 = 시스템 중단 위험이 있는 작업
---
### 3.2 SAM의 해결 방법: "메모칸(options)" 하나로 통합
**핵심 열만 남기고**, 나머지는 **메모칸 하나**에 자유롭게 적습니다.
```
"주문" 테이블 — SAM 방식
주문번호 │ 고객명 │ 금액 │ 상태 │ options (메모칸)
────────────────────────────────────────────────────────────────────────
ORD-001 │ 김철수 │ 50만 │ 완료 │ {"절곡각도": 45, "날개수": 12} ← A회사
ORD-002 │ 이영희 │ 30만 │ 진행 │ {"메시밀도": 18, "소재": "폴리에스터"} ← B회사
ORD-003 │ 박민수 │ 80만 │ 대기 │ {"날개간격": 25, "색상코드": "#FF0000"} ← C회사
ORD-004 │ 최지은 │ 40만 │ 대기 │ null ← 메모 없음
```
**`options`** = JSON이라는 형식의 메모칸. `{ }` 안에 자유롭게 정보를 넣을 수 있습니다.
---
## 4. 어떤 정보를 열(컬럼)로 만들고, 어떤 정보를 메모칸(options)에 넣나?
이것이 SAM 테이블 설계의 **가장 중요한 판단 기준**입니다.
### 4.1 판단 흐름 (5가지 질문)
새로운 정보를 저장해야 할 때, 아래 질문에 답합니다.
```
질문 1. 이 정보로 다른 테이블의 데이터를 연결(참조)하나?
예: 고객ID로 고객 테이블을 찾는다
→ YES: 일반 컬럼
질문 2. 이 정보로 자주 검색(필터)하나?
예: "완료" 상태인 주문만 보여줘
→ YES: 일반 컬럼
질문 3. 이 정보로 정렬하나?
예: 최신 주문부터 보여줘
→ YES: 일반 컬럼
질문 4. 이 정보가 절대 중복되면 안 되나?
예: 주문번호는 세상에 하나뿐이어야 한다
→ YES: 일반 컬럼
질문 5. 이 정보로 합계/평균을 계산하나?
예: 이번 달 매출 합계
→ YES: 일반 컬럼
질문 1~5 전부 NO → options 메모칸에 저장
```
### 4.2 실생활 예시로 비교
#### 예시 1: "주문" 테이블
| 정보 | 어디에 저장? | 이유 |
|------|:-----------:|------|
| 주문번호 | **일반 컬럼** | 중복 불가 + 검색 필수 |
| 고객 ID | **일반 컬럼** | 고객 테이블과 연결 |
| 금액 | **일반 컬럼** | 합계 계산 필요 |
| 상태 (진행/완료) | **일반 컬럼** | 필터(검색) 필수 |
| 생성일 | **일반 컬럼** | 정렬 필요 |
| 배송지 주소 | **options** | 부가 정보, 검색 안 함 |
| 수신자 이름 | **options** | 부가 정보 |
| 수신자 연락처 | **options** | 부가 정보 |
| 특이사항 메모 | **options** | 있어도 되고 없어도 됨 |
**실제 SAM 코드에서 주문(Order) 테이블:**
```
일반 컬럼: id, tenant_id, order_number, client_id, total_amount, status, created_at
options: {"shipping_cost_code":"착불", "receiver":"홍길동",
"receiver_contact":"010-1234-5678",
"shipping_address":"서울 강남구 역삼동 123"}
```
#### 예시 2: "입고검사" 테이블
| 정보 | 어디에 저장? | 이유 |
|------|:-----------:|------|
| 품목 ID | **일반 컬럼** | 품목 테이블과 연결 |
| 수량 | **일반 컬럼** | 합계 계산 |
| 입고일 | **일반 컬럼** | 정렬 + 검색 |
| 제조사 | **options** | 모든 입고에 있지는 않음 |
| 검사 결과 (합격/불합격) | **options** | 검사를 안 하는 회사도 있음 |
| 검사일 | **options** | 선택적 정보 |
**실제 SAM 코드에서 입고(Receiving) 테이블:**
```
일반 컬럼: id, tenant_id, item_id, quantity, received_at, status
options: {"manufacturer":"삼성전자",
"inspection_status":"합격",
"inspection_date":"2026-03-01"}
```
> 검사 결과가 options에 있는 이유: **모든 회사가 입고검사를 하는 것은 아닙니다.**
> A회사는 검사를 하고, B회사는 안 합니다. 이걸 일반 컬럼으로 만들면 B회사에겐 항상 빈칸입니다.
#### 예시 3: "공정" 테이블
| 정보 | 어디에 저장? | 이유 |
|------|:-----------:|------|
| 공정 코드 | **일반 컬럼** | 중복 불가 + 검색 |
| 공정명 | **일반 컬럼** | 검색 + 표시 |
| 담당 부서 | **일반 컬럼** | 필터 |
| 작업일지 필요 여부 | **options** | 회사별로 다름 |
| 검사 필요 여부 | **options** | 회사별로 다름 |
```
일반 컬럼: id, tenant_id, process_code, process_name, department
options: {"needs_work_log": true, "needs_inspection": false}
```
---
## 5. 메모칸(options)의 실제 모습: JSON이란?
`options`에 저장되는 데이터 형식은 **JSON**입니다.
JSON은 프로그래밍 세계의 "구조화된 메모장"이라고 생각하면 됩니다.
### 5.1 JSON 기본 문법
```
{ ← 시작
"키": "값", ← 문자(텍스트)
"이름": "홍길동",
"나이": 30, ← 숫자 (따옴표 없음)
"합격": true, ← 참/거짓 (따옴표 없음)
"메모": null ← 값 없음
} ← 끝
```
### 5.2 중첩(nested) — 메모 안의 메모
```
{
"배송": { ← 배송 관련 정보를 묶음
"주소": "서울 강남구 역삼동",
"수신자": "홍길동",
"연락처": "010-1234-5678"
},
"검사": { ← 검사 관련 정보를 묶음
"결과": "합격",
"검사일": "2026-03-01",
"검사자ID": 5
}
}
```
### 5.3 목록(배열) — 여러 개를 나열
```
{
"선택지": [ ← 대괄호 [ ] = 목록
{"label": "블라인드", "value": "blind"},
{"label": "스크린", "value": "screen"},
{"label": "셔터", "value": "shutter"}
]
}
```
> 이 형태는 드롭다운 메뉴의 선택지 목록을 저장할 때 사용합니다.
---
## 6. 멀티테넌시란? — 여러 회사가 하나의 시스템을 쓰는 구조
### 6.1 개념
```
┌──────────────────────────────────────────────┐
│ SAM 시스템 (하나의 프로그램) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ A회사 │ │ B회사 │ │ C회사 │ │
│ │ tenant=1 │ │ tenant=2 │ │ tenant=3 │ │
│ │ │ │ │ │ │ │
│ │ 블라인드 │ │ 스크린 │ │ 셔터 │ │
│ │ 제조 │ │ 제조 │ │ 제조 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 같은 테이블을 쓰지만, │
│ tenant_id로 데이터가 완전히 분리됨 │
└──────────────────────────────────────────────┘
```
### 6.2 tenant_id = 회사 식별 번호
모든 테이블의 모든 행에 `tenant_id`(회사 번호)가 붙어 있습니다.
```
"주문" 테이블
id │ tenant_id │ 주문번호 │ 금액 │ options
───────────────────────────────────────────────────────────
1 │ 1 │ ORD-001 │ 50만 │ {"절곡각도": 45} ← A회사 데이터
2 │ 1 │ ORD-002 │ 30만 │ {"절곡각도": 90} ← A회사 데이터
3 │ 2 │ ORD-001 │ 80만 │ {"메시밀도": 18} ← B회사 데이터
4 │ 3 │ ORD-001 │ 40만 │ {"날개간격": 25} ← C회사 데이터
```
**A회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 1`인 데이터만 보여줌
**B회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 2`인 데이터만 보여줌
> A회사는 B회사의 데이터를 절대 볼 수 없습니다. 시스템이 자동으로 차단합니다.
### 6.3 options + tenant_id = 강력한 조합
이 두 가지가 합쳐지면:
```
같은 테이블, 같은 컬럼 구조인데
✅ 회사마다 다른 데이터 (tenant_id로 분리)
✅ 회사마다 다른 속성 (options로 유연하게)
✅ 시스템 수정 없이 확장 가능
```
---
## 7. SAM 테이블의 표준 구조
SAM에서 새 테이블을 만들면 항상 이 구조를 따릅니다.
```
┌─────────────────────────────────────────────────────────────────┐
│ SAM 표준 테이블 구조 │
│ │
│ ① 식별자 │
│ id — 자동 생성 번호 (1, 2, 3...) │
│ tenant_id — 어느 회사의 데이터인지 │
│ │
│ ② 핵심 정보 (검색/정렬/연결에 쓰는 것만) │
│ code — 코드 (중복 불가) │
│ status — 상태 (검색용) │
│ is_active — 사용 여부 │
│ sort_order — 표시 순서 │
│ (+ FK 컬럼들) — 다른 테이블 연결 │
│ │
│ ③ 메모칸 │
│ options — 나머지 전부 (JSON) │
│ │
│ ④ 감사 기록 (자동) │
│ created_by — 누가 만들었나 │
│ updated_by — 누가 수정했나 │
│ deleted_by — 누가 삭제했나 │
│ created_at — 언제 만들었나 │
│ updated_at — 언제 수정했나 │
│ deleted_at — 언제 삭제했나 (휴지통 개념) │
└─────────────────────────────────────────────────────────────────┘
```
### 영역별 설명
| 영역 | 역할 | 비유 |
|------|------|------|
| ① 식별자 | "이 데이터가 누구 것인지" 구분 | 우편물의 받는 사람 + 주소 |
| ② 핵심 정보 | 검색, 정렬, 집계에 꼭 필요한 정보 | 엑셀의 고정 열 |
| ③ options | 회사마다 다른 부가 정보 | 엑셀의 "비고" 칸 (자유 서식) |
| ④ 감사 기록 | 언제 누가 뭘 했는지 자동 추적 | CCTV 기록 |
---
## 8. 실제 SAM에서 options를 쓰는 테이블들 (22개)
현재 SAM에서 options 메모칸을 사용하는 주요 테이블입니다.
| 테이블 | 한글명 | options에 저장하는 정보 예시 |
|--------|--------|--------------------------|
| `orders` | 주문 | 배송지, 수신자, 연락처, 담당자 |
| `quotes` | 견적 | 견적 요약, 비용 항목, 가격 조정 |
| `receivings` | 입고 | 제조사, 검사 결과, 검사일 |
| `work_orders` | 작업지시 | 절곡 정보 (bending_info) |
| `work_order_items` | 작업지시 항목 | 작업 결과, 양품/불량 수량, LOT번호 |
| `processes` | 공정 | 작업일지 필요 여부, 검사 필요 여부 |
| `order_nodes` | 주문 노드 | 위치, 구역, 층, 실 (트리 구조) |
| `products` | 제품 | 동적 옵션 (라벨, 값, 단위) |
| `items` | 품목 | 품목별 동적 속성 |
| `materials` | 자재 | 자재 추가 속성 |
| `menus` | 메뉴 | 섹션, 메뉴 타입, 필요 권한 |
| `users` | 사용자 | 개인 설정/환경설정 |
| `tenants` | 회사(테넌트) | 회사 규모, 업종 |
| `document_template_section_fields` | 문서 양식 필드 | 선택지 목록, API 경로 |
| `item_fields` | 품목 필드 정의 | 필드별 세부 설정 |
---
## 9. 자주 묻는 질문 (FAQ)
### Q1. options에 넣으면 검색이 안 되나요?
**아닙니다.** MySQL 8.0은 JSON 내부도 검색할 수 있습니다.
```
일반 컬럼 검색: "상태가 '완료'인 주문 찾아줘" → 매우 빠름
options 검색: "제조사가 '삼성'인 입고 찾아줘" → 가능하지만 조금 느림
```
다만, **매일 수천 번 검색하는 정보**라면 일반 컬럼으로 승격하는 것이 맞습니다.
가끔 검색하는 정보라면 options로 충분합니다.
### Q2. options에 아무 정보나 마음대로 넣을 수 있나요?
기술적으로는 가능하지만, 개발팀 내부에서 **어떤 키를 쓸지 미리 약속**합니다.
```
✅ 약속된 키: {"manufacturer": "삼성", "inspection_status": "합격"}
❌ 멋대로: {"asdf": 123, "temp_data": "???"}
```
코드에서 상수로 정의하여 일관성을 유지합니다.
### Q3. 전통적 방식보다 뭐가 좋은 건가요?
| 비교 항목 | 전통적 방식 (열 추가) | SAM 방식 (options JSON) |
|----------|:------------------:|:---------------------:|
| 새 정보 추가 시 | 시스템 수정 필요 | 코드만 변경 |
| 다른 회사에 영향 | 있음 (전체 구조 변경) | 없음 |
| 빈칸(null) 낭비 | 많음 | 없음 |
| 검색 속도 | 빠름 | 조금 느림 (충분히 실용적) |
| 유연성 | 낮음 | 높음 |
| 시스템 중단 위험 | 있음 (대형 테이블 수정 시) | 없음 |
### Q4. 그럼 모든 정보를 options에 넣으면 되지 않나요?
**아닙니다.** 핵심 정보는 반드시 일반 컬럼으로 만들어야 합니다.
```
❌ 나쁜 예: 모든 것을 options에
id │ tenant_id │ options
──────────────────────────────────────────────────────────────
1 │ 1 │ {"주문번호":"ORD-001", "금액":500000, "상태":"완료", ...}
→ 주문번호 검색 느림, 금액 합계 계산 불가, 중복 방지 불가
```
```
✅ 좋은 예: 핵심은 컬럼, 부가는 options
id │ tenant_id │ order_number │ amount │ status │ options
──────────────────────────────────────────────────────────────
1 │ 1 │ ORD-001 │ 500000 │ 완료 │ {"배송지":"서울..."}
→ 검색 빠름, 합계 가능, 중복 방지 가능, 부가 정보도 유연
```
### Q5. options 데이터는 화면에서 어떻게 보이나요?
사용자 화면에서는 options 안에 있는지, 일반 컬럼인지 **구분할 수 없습니다**.
프로그램이 자동으로 꺼내서 보여줍니다.
```
화면에 보이는 모습:
┌─────────────────────────────────┐
│ 입고 상세 정보 │
│ │
│ 품목: SUS304 스틸 │ ← 일반 컬럼
│ 수량: 100개 │ ← 일반 컬럼
│ 입고일: 2026-03-01 │ ← 일반 컬럼
│ 제조사: 삼성전자 │ ← options에서 꺼냄
│ 검사결과: 합격 │ ← options에서 꺼냄
│ 검사일: 2026-03-01 │ ← options에서 꺼냄
└─────────────────────────────────┘
사용자는 어디에 저장되어 있는지 알 필요 없음!
```
---
## 10. 한 장 요약
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ SAM 테이블 설계 = "핵심만 컬럼, 나머진 메모칸(options)" │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ tenant_id → 어느 회사 것인지 (자동 격리) │ │
│ │ 핵심 컬럼들 → 검색/정렬/연결/집계에 쓰는 필수 정보 │ │
│ │ options → 나머지 전부 (회사마다 다른 부가 정보) │ │
│ │ 감사 컬럼들 → 누가/언제 만들고/수정하고/삭제했는지 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 이렇게 하면: │
│ ✅ 회사 추가해도 테이블 구조 안 바꿈 │
│ ✅ 새 정보 추가해도 시스템 수정 최소화 │
│ ✅ 회사마다 다른 정보를 유연하게 저장 │
│ ✅ 데이터 보안 (회사 간 완전 분리) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 관련 문서
| 문서 | 설명 | 대상 |
|------|------|------|
| [options-column-policy.md](../standards/options-column-policy.md) | 개발자용 상세 정책 (코드 규칙, 마이그레이션 패턴) | 개발자 |
| [database/README.md](../system/database/README.md) | DB 스키마 전체 현황 (220개 모델) | 개발자 |
| [PROJECT_DEVELOPMENT_POLICY.md](PROJECT_DEVELOPMENT_POLICY.md) | 개발 공통 정책 (테이블 생성 절차) | 개발자 |
| [system/overview.md](../system/overview.md) | SAM 시스템 전체 아키텍처 | 전체 |
---
**최종 업데이트**: 2026-03-02

View File

@@ -0,0 +1,737 @@
# SAM General Rule Storyboard D1.0
> **작성일**: 2026-01-16
> **버전**: D1.0
> **원본**: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` (43페이지)
> **상태**: PC 섹션 정리 완료
---
## 1. 문서 이력
| 날짜 | 버전 | 주요 내용 | 세부 내용 |
|------|------|----------|----------|
| 2026-01-15 | D0.9 | 초안 | General Rule - PC, 태블릿, 모바일 UIUX 공통 작성 |
| 2026-01-16 | D1.0 | 작성 | PC 섹션 정리 33p |
---
## 2. 인터랙션 (Interaction)
> **페이지**: 4
사용자 입력 제스처 및 적용 여부를 정의한다.
| Type | 제스처/마크 | 설명 | 적용 |
|------|-----------|------|------|
| Tap | 탭 | 일정영역을 사용자가 터치한다. | Yes |
| Touch & Hold | 터치 앤 홀드 | 화면을 터치한 후 계속 누르고 있는 상태. 해당영역 혹은 개체가 홀드된다. | No |
| Double Tap | 더블 탭 | 일정영역을 두 번 터치한다. 두 번 터치 시 액션이 실행된다. | No |
| Drag & Drop | 드래그 앤 드롭 | 터치 혹은 홀드 상태에서 오브젝트를 이동하여 원하는 위치에 배치시킨다. | Yes |
| Scroll Up | 스크롤 업 | 아래에서 위로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes |
| Scroll Down | 스크롤 다운 | 위에서 아래로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes |
| Swipe Left | 스와이프 레프트 | 오른쪽에서 왼쪽으로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes |
| Swipe Right | 스와이프 라이트 | 왼쪽에서 오른쪽으로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes |
| Pinch Zoom out | 핀치 줌 아웃 | 오브젝트 또는 화면을 축소한다. | Yes |
| Pinch Zoom in | 핀치 줌 인 | 오브젝트 혹은 화면을 확대한다. | Yes |
---
## 3. 반응형 웹 (Responsive Web)
> **페이지**: 5
### 3.1 브레이크 포인트
| 디바이스 | 브레이크 포인트 | Tailwind 접두사 |
|---------|---------------|----------------|
| 모바일 | < 640px | 기본 |
| 태블릿 | 768px ~ 1280px | `md` |
| 데스크탑 | 1280px+ | `lg` |
| 대형 모니터 | 1920px+ | `xl` |
### 3.2 레이아웃 구성
**PC Web 레이아웃**:
1. Contents 영역
2. Footer 영역
**Mobile Web 레이아웃**:
1. Contents 영역
2. Footer 영역
---
## 4. 화면 템플릿 (Screen Template)
> **페이지**: 6
모바일 화면 구조를 정의한다.
| 영역 | 코드 | 설명 |
|------|------|------|
| Status bar | A | 안테나, 통화, 배터리 시스템 OS 관리 영역. 모든 페이지 상단에 존재 |
| Browser 영역 | B | 브라우저 기능 영역 |
| Title 영역 | C | 텍스트 또는 기능 버튼으로 구현됨. 텍스트는 기본 가운데 정렬 |
| Content 영역 | D | 컨텐츠 내용 표시. 컨텐츠 길이가 길어질 경우 스크롤 제공 |
| Browser bar 영역 | E | 브라우저 유틸 영역 |
| Keypad 영역 | F | 키보드 입력할 활성화. 모든 페이지 위에 덮어쓰기 구현 |
---
## 5. 메시지 (Notifications)
> **페이지**: 7
| 유형 | 설명 |
|------|------|
| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업. `[확인]` 버튼 제공 |
| 확인 Alert | 사용자에게 확인이 필요할 경우 제공되는 팝업. `[취소]` `[확인]` 버튼 제공 |
| 토스트 메시지 | 단순 Notify. 2~3초 페이지 내에서 Fade out |
---
## 6. GNB, LNB, 푸터
> **페이지**: 8
PC 화면의 전체 레이아웃 구조를 정의한다.
### 6.1 구성 요소
| 번호 | 영역 | 설명 |
|------|------|------|
| 01 | 메뉴 버튼 | 클릭: 메뉴 영역(06) 축소/확장 토글. 디폴트: 메뉴 영역 확장 상태 |
| 02 | SAM 로고 버튼 | 클릭: 대시보드 화면으로 이동 |
| 03 | 알림 버튼 | 클릭: 알림 팝업 표시 |
| 04 | 개인 정보 버튼 | 항목: 디폴트 이미지, 이름, 직급. 클릭: 마이페이지 팝업 표시 |
| 05 | 회사 로고 | 회사정보 화면에서 등록한 로고 표시. 회사 변경 선택 해당 로고 변경 |
| 06 | 메뉴 영역 | 메뉴 클릭: 하위 메뉴 있을 경우 하단에 표시, 없을 경우 해당 메뉴 화면으로 이동. 목록 경우 해당 영역 스크롤 처리 |
| 07 | MES 메뉴 영역 | 영업관리, 판매관리, 구매관리 해당하는 MES 메뉴 영역 표시 |
| 08 | 푸터 영역 | 모든 화면 하단 공통 표시 |
| 09 | SAM AI 채팅 버튼 | 클릭: SAM AI 채팅 팝업 표시 |
### 6.2 메뉴 목록
- 대시보드
- MES 메뉴
- 인사관리
- 전자결재
- 게시판
- 회계관리
- 기준정보
- 보고서 분석
- 계정정보
- 회사정보
- 구독관리
- 결제내역
- 고객센터
### 6.3 푸터 내용
```
(C) 2025 SAM. All right reserved.
Codebridge X
상호: 코드 브릿지 엑스 대표: 이경호 사업자등록번호: 123-45-12345
주소: 서울특별시 강서구 양천로 583 우림블루나인 B동 1602호 (우: 07547)
팩스: 02-123-1234 통신판매업신고번호: 제 2019-서울강서-0001호
서비스이용문의: 02-1234-1234 이메일: cs@a.com
서비스 이용약관 | 개인정보 취급방침
```
---
## 7. 메뉴, 페이지, 섹션, 항목 영역
> **페이지**: 9
### 7.1 영역 구분
| 번호 | 영역 | 설명 |
|------|------|------|
| 01 | 메뉴 영역 | 축소 상태 |
| 02 | 페이지 영역 | - |
| 03 | 섹션 영역 | - |
| 04 | 항목 영역 | - |
### 7.2 텍스트 오버플로우 처리
텍스트가 영역보다 경우 "텍스트+..." 형태로 표시한다.
---
## 8. 메뉴 목록 (3Depth)
> **페이지**: 10
### 8.1 메뉴 계층 구조
| 번호 | 레벨 | 설명 |
|------|------|------|
| 01 | 대메뉴 | 1Depth 메뉴 |
| 02 | 중메뉴 | 2Depth 메뉴 (대메뉴 클릭 하단에 표시) |
| 03 | 소메뉴 | 3Depth 메뉴 (중메뉴 클릭 하단에 표시) |
**메뉴 확장 예시**:
```
대시보드
MES 메뉴
인사관리
전자결재
- 중메뉴명
- 중메뉴명
· 소메뉴명
· 소메뉴명
- 중메뉴명
- 중메뉴명
- 중메뉴명
게시판
...
```
---
## 9. 알림 팝업
> **페이지**: 11
> **경로**: 메인 > 알림 팝업
### 9.1 구성 요소
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 알림 목록 | 디폴트 썸네일, 종류(공지사항, 안내), 제목/내용, 전송일시 표시. 클릭: 해당 상세 화면으로 이동. 최신순 10개까지 표시 |
| 02 | New 아이콘 | 알림일 경우 New 아이콘 표시. 해당 알림 클릭 사라짐 |
| 02-1 | 붉은 아이콘 | 알림이 있을 경우 표시. 해당 알림 모두 클릭 사라짐 |
---
## 10. 마이페이지 팝업
> **페이지**: 12
> **경로**: 메인 > 마이페이지 팝업
### 10.1 구성 요소
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 계정 아이디 (이메일) | 이메일 주소 표시 (예: `name@company.com`) |
| 02 | 회사 셀렉트 박스 | 종류: 회사명 목록 (해당 계정이 생성한 회사(테넌트) 목록 표시). 정렬: 등록순. 회사만 소유중일 경우에는 해당 영역 숨김 |
| 03 | 로그아웃 버튼 | 클릭: "정말 로그아웃하시겠습니까?" 로그아웃 확인 Alert 표시. 확인 버튼 클릭시 로그아웃 처리 |
---
## 11. 셀렉트 박스
> **페이지**: 13
### 11.1 기본 셀렉트 박스
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 셀렉트 박스 | 클릭: 하단에 종류 목록 표시 |
| 02 | 종류 목록 | 목록 하나만 선택 가능 |
### 11.2 다중 선택 셀렉트 박스
| 번호 | 항목 | 설명 |
|------|------|------|
| 03 | 다중 선택 셀렉트 박스 | 선택된 첫번째 항목명 + 추가 표시. 텍스트 영역 부족할 경우 `항목..+3` 형태로 표시 |
| 04 | 다중 선택 종류 목록 | 목록 복수 선택 가능. 전체 선택 전체 선택/해제 토글 |
### 11.3 검색 셀렉트 박스
| 번호 | 항목 | 설명 |
|------|------|------|
| 05 | 검색 영역 | 검색어 입력 엔터 또는 검색 아이콘 클릭 검색 상태로 전환되며 검색 결과 표시 |
| 05-3 | 삭제 버튼 | 클릭: 검색어 삭제 처리, 전체 종류 목록 표시 |
### 11.4 셀렉트 박스 유형 정리
| 유형 | 단일 선택 | 다중 선택 | 검색 | 검색+다중 선택 |
|------|----------|----------|------|--------------|
| 선택 방식 | 하나만 | 복수 | 하나만 | 복수 |
| 검색 기능 | X | X | O | O |
| 전체 선택 | X | O | X | O |
---
## 12. 가이드 메시지
> **페이지**: 14
상황에 따라 입력 필드 하단 또는 Alert에 가이드 메시지를 표시한다.
### 12.1 표시 규칙
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 가이드 메시지 표시 위치 | 입력 필드 하단에 표시 |
| - | 긍정 메시지 | 녹색으로 표시 |
| 01-1 | 부정 메시지 | 붉은색으로 표시 |
---
## 13. 태블릿/모바일 헤더
> **페이지**: 15
### 13.1 동작 규칙
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 태블릿/모바일 헤더 | 하단으로 스크롤 숨김. 역스크롤 표시 |
### 13.2 적용 화면
- TABLET 가로 목록
- TABLET 세로 목록
- MOBILE 가로 목록
- MOBILE 세로 목록
---
## 14. 태블릿/모바일 바텀 버튼 영역
> **페이지**: 16
### 14.1 동작 규칙
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 태블릿/모바일 바텀 버튼 영역 | 최하단 바텀에 플로팅 표시. 하단으로 스크롤 숨김. 역스크롤 표시 |
### 14.2 버튼 예시
- `[수정]` `[삭제]`
---
## 15. 공지 팝업
> **페이지**: 17
### 15.1 구성
| 항목 | 설명 |
|------|------|
| 대상 | 전체, 설정 부서 |
| 내용 | 설정 기간동안 대상에게 팝업 표시 |
### 15.2 구성 요소
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 팝업 내용 영역 | 이미지, 텍스트 |
| 02 | 1일간 창을 열지 않음 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 체크 해제 상태. 체크 설정 1일 동안 팝업 미표시 (자정 기준) |
---
## 16. 목록 화면 - 4단계 반응형
> **페이지**: 18~25
PC, TABLET, MOBILE 환경에서 목록 화면의 4단계 반응형 표시를 정의한다.
### 16.1 반응형 단계 개요
| 단계 | 디바이스 | 화면명 |
|------|---------|--------|
| 1단계 | PC | PC_목록 |
| 2단계 | TABLET 가로 | TABLET_가로_목록 |
| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_목록, MOBILE_가로_목록 |
| 4단계 | MOBILE 세로 | MOBILE_세로_목록 |
### 16.2 PC_목록 (1단계)
> **페이지**: 19
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 헤더 영역 | 항목 클릭: 값이 국문/영문/숫자일 경우 오름/내림차순으로 토글 |
| 02 | 정렬 아이콘 | 현재 칼럼으로 정렬 상태일 경우에만 표시 |
**목록 테이블 예시 칼럼**:
| 칼럼 | 설명 |
|------|------|
| 시공번호 | 고유 식별 번호 |
| 거래처 | 회사명 |
| 현장명 | 현장 이름 |
| 공사PM | 담당 PM |
| 작업반장 | 작업반장 이름 |
| 작업자 | 작업자 |
| 시공투입일 | 시공 투입 날짜 |
| 시공완료일 | 시공 완료 날짜 |
| 상태 | 시공대기, 시공진행, 시공완료 |
### 16.3 TABLET_가로_목록 (2단계)
> **페이지**: 20
- PC와 동일한 테이블 구조
- 사이드 메뉴가 아이콘 축소 상태로 변경
### 16.4 TABLET_세로_목록 (3단계)
> **페이지**: 21~22
- 테이블 대신 카드형 목록으로 전환
- 카드에 시공번호와 상태 표시
- 카드 클릭 확장되어 상세 정보 표시
**확장 시 표시 항목**:
| 필드 | 예시 |
|------|---------|
| 거래처 | 회사명 |
| 현장명 | 현장명 |
| 공사PM | 홍길동 |
| 작업반장 | 홍길동 |
| 작업자 | 3 |
| 시공투입일 | 2026-01-01 |
| 시공완료일 | 2026-01-01 |
### 16.5 MOBILE_가로_목록 (3단계)
> **페이지**: 23~24
- 카드형 목록
- 시공번호와 상태 표시
- 클릭 확장하여 상세 항목 표시
### 16.6 MOBILE_세로_목록 (4단계)
> **페이지**: 25
- 카드형 목록 (세로 스크롤)
- 클릭 확장하여 상세 항목 표시
- 확장 거래처, 현장명, 공사PM, 작업반장, 작업자, 시공투입일, 시공완료일 표시
---
## 17. 상세 화면 - 4단계 반응형
> **페이지**: 26~31
PC, TABLET, MOBILE 환경에서 상세 화면의 4단계 반응형 표시를 정의한다.
### 17.1 반응형 단계 개요
| 단계 | 디바이스 | 화면명 |
|------|---------|--------|
| 1단계 | PC | PC_상세 |
| 2단계 | TABLET 가로 | TABLET_가로_상세 |
| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_상세, MOBILE_가로_상세 |
| 4단계 | MOBILE 세로 | MOBILE_세로_상세 |
### 17.2 PC_상세 (1단계)
> **페이지**: 27
- 페이지 제목: "메뉴 상세" + 설명: "메뉴 상세를 관리합니다"
- 섹션명: "시공 정보"
- 버튼: `[수정]` `[삭제]`
**표시 항목 예시**:
| 필드 | 예시 |
|------|---------|
| 시공번호 | 123123 |
| 상태 | 시공진행 |
| 현장 | 현장명 |
| 작업반장 | 홍길동 (셀렉트 박스) |
| 시공투입일 | 2025-12-15 |
| 시공완료일 | 2025-12-15 |
| 항목명 | 항목 (다수) |
### 17.3 TABLET_가로_상세 (2단계)
> **페이지**: 28
- PC와 동일한 상세 정보 표시
- 사이드 메뉴 아이콘 축소 상태
### 17.4 TABLET_세로_상세 (3단계)
> **페이지**: 29
- 항목 수가 줄어들며 스크롤로 나머지 확인
### 17.5 MOBILE_가로_상세 (3단계)
> **페이지**: 30
- 상세 항목을 세로 배치
- 바텀에 `[수정]` `[삭제]` 버튼 플로팅
### 17.6 MOBILE_세로_상세 (4단계)
> **페이지**: 31
- 모든 항목 세로 배치
- 바텀에 `[수정]` `[삭제]` 버튼 플로팅
---
## 18. PC 섹션 정리
> **페이지**: 32~33
PC 화면의 섹션 레이아웃 필터/정렬 구성을 정의한다.
### 18.1 필터 규칙
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 라디오 버튼형 필터 | 선택 값이 2개일 경우 사용 (예: 수취/발행) |
| 02 | 필터 셀렉트 박스 | 최소로만 활용 |
| 03 | 헤더 정렬 | 헤더 정렬로 정렬 셀렉트 박스는 삭제 |
### 18.2 PC 섹션 구성 요소
**상단 영역**:
- 페이지 제목 + 설명
- 집계 카드 (예: `수취어음 55건`, `발행어음 1건`, `만기임박 5건`, `결제완료 15건`)
- 기간 선택 (날짜 범위 + 단축 버튼: 전전월, 어제, 오늘, 전월, 당월, 당해년도)
- 버튼: `[버튼명]` `[버튼명]` `[버튼명]`
- 탭: 탭1, 탭2, 탭3
**필터 영역**:
- 셀렉트 박스 필터 (전체)
- 라디오 버튼형 필터 (수취/발행)
- 상태 셀렉트 박스 (보관중)
- `[저장]` 버튼
**목록 테이블 예시**:
| No. | 어음번호 | 구분 | 거래처 | 금액 | 발행일 | 만기일 | 차수 | 상태 |
|-----|---------|------|--------|------|--------|--------|------|------|
| 7 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 1 | 보관중 |
| 6 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 2 | 만기임박 |
**하단 정보**: `총 7건` / `1건 선택`
---
## 19. TBD (미정)
> **페이지**: 34
추후 결정 예정 영역이다.
---
## 20. 나의 메뉴
> **페이지**: 35~38
### 20.1 나의 메뉴 - 없음
> **페이지**: 35
- 나의 메뉴가 설정되지 않은 상태
- 콘텐츠 상단에 `[...]` 아이콘만 표시
### 20.2 나의 메뉴 - 있음
> **페이지**: 36
- 나의 메뉴가 1개 설정된 상태
- 콘텐츠 상단에 나의 메뉴명 표시 (예: `메뉴관리`)
### 20.3 나의 메뉴 - 여러 줄
> **페이지**: 37
- 나의 메뉴가 여러 설정된 상태
- 콘텐츠 상단에 여러 메뉴명이 나열됨
- 줄바꿈되어 여러 줄로 표시 가능 (예: `메뉴관리 메뉴명 메뉴명 메뉴명 ...`)
### 20.4 나의 메뉴 - 메뉴 영역에 통합
> **페이지**: 38
- 좌측 메뉴 영역에 "메뉴" / "나의 메뉴" 탭으로 통합
- 메뉴 탭: 일반 메뉴 목록 표시
- 나의 메뉴 탭: 사용자 즐겨찾기 메뉴 표시
---
## 21. 검색, 필터, 정렬 모음
> **페이지**: 39
### 21.1 구성 요소
| 영역 | 구성 |
|------|------|
| 기간 선택 | 날짜 범위 (`2025-09-01 ~ 2025-09-03`) + 단축 버튼 (전전월, 어제, 오늘, 전월, 당월, 당해년도) |
| 검색바 | 검색 입력 필드 |
| 필터 셀렉트 박스 | 복수의 전체 셀렉트 박스 |
| 정렬 | 최신순 셀렉트 박스 |
| 항목 필터 | 항목명 태그 형태로 나열 |
---
## 22. 페이지 설정 버튼
> **페이지**: 40
### 22.1 기능
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 섹션 표시 순서 변경 | 페이지 섹션 ON/OFF 토글 순서 변경 |
| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 |
### 22.2 설정 패널 구성
- 버전기록
- 가져오기
- 내보내기
- 섹션 목록: 각 섹션별 ON/OFF 토글
**예시**:
```
섹션명 [ON]
섹션명 [ON]
섹션명 [ON]
섹션명 [ON]
섹션명 [ON]
```
---
## 23. 섹션 설정 버튼
> **페이지**: 41
### 23.1 기능
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 항목 표시 및 순서 변경 | 섹션 내 항목 ON/OFF 토글 및 순서 변경 |
| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 |
### 23.2 설정 패널 구성
- 가져오기
- 내보내기
- 항목 목록: 각 항목별 ON/OFF 토글
**예시**:
```
기간 [ON]
기간단축버튼 [ON]
검색바 [ON]
필터명 [ON]
필터명 [ON]
필터명 [ON]
필터명 [ON]
정렬 [ON]
```
---
## 24. 태스크 알림 아이콘
> **페이지**: 42~43
### 24.1 동작 규칙
| 번호 | 항목 | 설명 |
|------|------|------|
| 01 | 태스크 알림 아이콘 | 태스크가 추가될 경우 카운트하여 표시 |
| - | 메뉴 확장 시 표시 | 대/중/소메뉴로 확장될 경우 해당 메뉴에 아이콘 표시 |
| - | 카운트 범위 | 최소 1 ~ 최대 99 |
### 24.2 표시 예시
**축소 상태**: 대메뉴 옆에 카운트 배지 표시 (예: `전자결재 [3]`)
**확장 상태 (2Depth)**:
```
전자결재 [3]
- 중메뉴명 [2]
- 중메뉴명 [1]
- 중메뉴명
- 중메뉴명
- 중메뉴명
```
**확장 상태 (3Depth)**:
```
전자결재 [3]
- 중메뉴명
- 중메뉴명 [1]
· 소메뉴명 [1]
· 소메뉴명
- 중메뉴명
- 중메뉴명
- 중메뉴명
```
### 24.3 페이지 내 표시
- 메뉴 축소 상태에서도 대메뉴 아이콘 옆에 카운트 배지 표시
---
## 부록: 페이지 맵
| 페이지 | 섹션 | 화면명 |
|--------|------|--------|
| 1 | 표지 | SAM_General Rule |
| 2 | 문서 이력 | Document History |
| 3 | 공통 | - |
| 4 | 인터랙션 | Interaction |
| 5 | 반응형 웹 | Responsive Web |
| 6 | 화면 템플릿 | Screen Template |
| 7 | 메시지 | Notifications |
| 8 | GNB, LNB, 푸터 | GNB, LNB, 푸터 |
| 9 | 영역 구분 | 메뉴, 페이지, 섹션, 항목 영역 |
| 10 | 메뉴 목록 | 메뉴 목록 3Depth |
| 11 | 알림 팝업 | 알림 팝업 |
| 12 | 마이페이지 | 마이페이지 팝업 |
| 13 | 셀렉트 박스 | 셀렉트 박스 (기본/다중/검색) |
| 14 | 가이드 메시지 | 가이드 메시지 |
| 15 | 태블릿/모바일 헤더 | 태블릿/모바일 헤더 |
| 16 | 태블릿/모바일 바텀 버튼 | 태블릿/모바일 바텀 버튼 영역 |
| 17 | 공지 팝업 | 공지 팝업 |
| 18 | (구분) | PC, TABLET, MOBILE - 목록 4단계 |
| 19 | 목록 1단계 | PC_목록 |
| 20 | 목록 2단계 | TABLET_가로_목록 |
| 21 | 목록 3단계 | TABLET_세로_목록 |
| 22 | 목록 3단계 확장 | TABLET_세로_목록_확장 |
| 23 | 목록 3단계 | MOBILE_가로_목록 |
| 24 | 목록 3단계 확장 | MOBILE_가로_목록_확장 |
| 25 | 목록 4단계 | MOBILE_세로_목록, MOBILE_세로_목록_확장 |
| 26 | (구분) | PC, TABLET, MOBILE - 상세 4단계 |
| 27 | 상세 1단계 | PC_상세 |
| 28 | 상세 2단계 | TABLET_가로_상세 |
| 29 | 상세 3단계 | TABLET_세로_상세 |
| 30 | 상세 3단계 | MOBILE_가로_상세 |
| 31 | 상세 4단계 | MOBILE_세로_상세 |
| 32 | (구분) | 섹션 정리 |
| 33 | 섹션 정리 | PC 섹션 정리 |
| 34 | TBD | 미정 |
| 35 | 나의 메뉴 | 나의 메뉴_없음 |
| 36 | 나의 메뉴 | 나의 메뉴_있음 |
| 37 | 나의 메뉴 | 나의 메뉴_여러 줄 |
| 38 | 나의 메뉴 | 나의 메뉴_메뉴 영역에 통합 |
| 39 | 검색/필터/정렬 | 검색, 필터, 정렬 모음 |
| 40 | 페이지 설정 | 페이지 설정 버튼 |
| 41 | 섹션 설정 | 섹션 설정 버튼 |
| 42~43 | 태스크 알림 | 태스크 알림 아이콘 |
---
## 관련 문서
- [SAM ERP 회계관리 스토리보드 D1.6](SAM_ERP_회계관리_Storyboard_D1.6.md)
- 원본 PDF: `SAM_General_Rule_Storyboard_D1.0_260116.pdf`
---
**최종 업데이트**: 2026-02-23

View File

@@ -0,0 +1,928 @@
# AI 견적서 자동생성 엔진 개발 계획
> **작성일**: 2026-03-02
> **상태**: 기획 초안
> **프로젝트**: SAM API + MNG
> **우선순위**: 🔴 필수
> **참조**: `docs/features/ai/README.md`, `docs/features/quotes/README.md`, `docs/rules/customer-pricing.md`
---
## 1. 개요
### 1.1 목적
SAM 계약 완료 후 매니저가 고객사 직원과 인터뷰를 진행할 때, **인터뷰 내용을 AI가 분석하여 SAM 표준 견적서 형태로 자동 변환**하는 엔진을 구축한다.
현재 매니저가 수동으로 수행하는 프로세스:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ SAM 계약 │ → │ 현장 인터뷰 │ → │ 업무 파악 │ → │ 견적서 작성 │
│ 완료 │ │ (매니저+직원)│ │ (수동 정리) │ │ (수동 작성) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
```
AI 엔진 도입 후:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ SAM 계약 │ → │ 현장 인터뷰 │ → │ AI 엔진 │ → │ 견적서 초안 │
│ 완료 │ │ (매니저+직원)│ │ (음성/텍스트 분석) │ │ (자동 생성) │
│ │ │ │ │ (업무 매핑) │ │ (매니저 확인)│
└──────────────┘ └──────────────┘ └──────────────────────┘ └──────────────┘
```
### 1.2 핵심 원칙
| 원칙 | 설명 |
|------|------|
| **자체 AI 엔진** | Claude API 기반으로 SAM 전용 AI 서비스 구축 |
| **기존 인프라 활용** | `ai_configs`, `ai_token_usages`, `ai_pricing_configs` 테이블 재사용 |
| **견적 시스템 연동** | 기존 `quotes`, `quote_items` 테이블과 직접 연동 |
| **Multi-tenant** | 모든 데이터에 `tenant_id` 격리 적용 |
| **점진적 확장** | Phase 1 텍스트 → Phase 2 음성 (STT 구현 완료, 통합만 필요) → Phase 3 학습 고도화 |
### 1.3 기존 인프라 현황
#### AI 인프라 (구축 완료)
| 구성요소 | 상태 | 비고 |
|---------|------|------|
| `ai_configs` 테이블 | ✅ 완료 | Claude provider 설정 가능 |
| `ai_token_usages` 테이블 | ✅ 완료 | 토큰/비용 자동 추적 |
| `ai_pricing_configs` 테이블 | ✅ 완료 | Claude 모델 단가 등록 |
| `AiTokenHelper` | ✅ 완료 | `saveClaudeUsage()` 메서드 존재 |
| MNG AI 설정 UI | ✅ 완료 | `/system/ai-config` |
#### 견적 시스템 (구축 완료)
| 구성요소 | 상태 | 비고 |
|---------|------|------|
| `quotes` 테이블 | ✅ 완료 | 견적 마스터 |
| `quote_items` 테이블 | ✅ 완료 | 견적 품목 상세 |
| `QuoteService` | ✅ 완료 | 견적 CRUD, 상태 관리 |
| `QuoteCalculationService` | ✅ 완료 | BOM 10단계 계산 |
| 견적 API 엔드포인트 | ✅ 완료 | REST API 전체 |
#### 음성 녹음/STT (구축 완료)
| 구성요소 | 상태 | 비고 |
|---------|------|------|
| `ai_voice_recordings` 테이블 | ✅ 완료 | DB 스키마 + CRUD |
| GCS 업로드 | ✅ 완료 | `GoogleCloudService` — GCS 저장/조회/삭제 |
| Google Cloud STT 변환 | ✅ 완료 | `GoogleCloudService::speechToText()` — LongRunningRecognize, ko-KR |
| Web Speech API (브라우저 STT) | ✅ 완료 | `voice-recorder.blade.php` — 실시간 음성→텍스트 (무료) |
| STT + Gemini AI 분석 | ✅ 완료 | `AiVoiceRecordingService` — 음성→STT→AI 분석 파이프라인 |
| 화자 분리 (Diarization) | ✅ 완료 | `MeetingMinuteService` — Speaker Diarization |
| 영업 상담 음성 녹음 | ✅ 완료 | `ConsultationController` — MediaRecorder + STT + GCS 백업 |
> **참조 구현 파일:**
> - `mng/app/Services/GoogleCloudService.php` — Google Cloud STT/GCS 통합 서비스
> - `mng/app/Services/AiVoiceRecordingService.php` — STT + Gemini 분석
> - `mng/app/Services/MeetingMinuteService.php` — 회의록 STT + 화자분리
> - `mng/app/Http/Controllers/Sales/ConsultationController.php` — 영업 상담 음성
> - `mng/resources/views/sales/modals/voice-recorder.blade.php` — 브라우저 음성 녹음 UI
> - `docs/features/voice-input-stt-guide.md` — STT 기술 가이드
---
## 📍 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | 기획 초안 작성 |
| **다음 작업** | Phase 1 상세 설계 → 구현 |
| **진행률** | 0/4 Phase (0%) |
| **마지막 업데이트** | 2026-03-02 |
---
## 2. 시스템 아키텍처
### 2.1 전체 구조
```
┌─────────────────────────────────────────────────────────────────┐
│ SAM AI 견적 엔진 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────────────┐ │
│ │ 입력 채널 │ │ AI 분석 파이프라인 │ │
│ │ │ │ │ │
│ │ 📝 텍스트 │───→│ 1. 인터뷰 전처리 │ │
│ │ 🎤 음성(P2) │ │ 2. 업무 도메인 분류 │ │
│ │ 📄 문서(P3) │ │ 3. SAM 모듈 매핑 │ │
│ └───────────────┘ │ 4. 견적 항목 추출 │ │
│ │ 5. 금액 산출 │ │
│ └──────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 견적서 생성기 │ │
│ │ │ │
│ │ SAM 표준 모듈 카탈로그 ←──→ Claude API │ │
│ │ 고객 요금 정책 ←──→ 프롬프트 엔진 │ │
│ │ 기존 견적 템플릿 ←──→ 결과 파서 │ │
│ └──────────────────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 출력 │ │
│ │ │ │
│ │ 📊 견적서 초안 (quotes 테이블) │ │
│ │ 📋 업무 분석 리포트 │ │
│ │ 💡 추천 모듈 목록 │ │
│ └───────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 Claude API 연동 구조
```
┌──────────────────┐ ┌──────────────────┐
│ SAM API Server │ │ Claude API │
│ (Laravel) │ │ (Anthropic) │
│ │ │ │
│ AiQuotation │ ──HTTP──→ Messages API │
│ Service │ ←─JSON── (claude-sonnet) │
│ │ │ │
│ ┌────────────┐ │ │ System Prompt: │
│ │ Prompt │ │ │ - SAM 모듈 목록 │
│ │ Engine │ │ │ - 요금 정책 │
│ │ │ │ │ - 견적 구조 │
│ │ - 모듈목록 │ │ │ - 출력 형식 │
│ │ - 요금표 │ │ │ │
│ │ - 템플릿 │ │ │ │
│ └────────────┘ │ │ │
└──────────────────┘ └──────────────────┘
```
### 2.3 데이터 흐름
```
매니저 인터뷰 입력
┌──────────────────────────────────────────────────┐
│ Step 1: 인터뷰 전처리 │
│ - 텍스트 정규화 (불필요한 표현 제거) │
│ - 핵심 키워드 추출 │
│ - 업무 도메인 태깅 │
└──────────────────────┬───────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Step 2: Claude API 1차 호출 — 업무 분석 │
│ - 고객사 업종/규모 파악 │
│ - 현재 업무 프로세스 분석 │
│ - 디지털화 필요 영역 식별 │
│ - Pain Point 도출 │
│ 출력: 구조화된 업무 분석 JSON │
└──────────────────────┬───────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Step 3: SAM 모듈 매핑 │
│ - 업무 분석 결과 ↔ SAM 모듈 카탈로그 대조 │
│ - 필수/선택 모듈 분류 │
│ - 사용자 수, 데이터량 추정 │
│ 출력: 추천 모듈 목록 + 근거 │
└──────────────────────┬───────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Step 4: Claude API 2차 호출 — 견적 생성 │
│ - SAM 요금 정책 적용 │
│ - 모듈별 개발비 + 월 구독료 계산 │
│ - 추가 옵션 (AI 토큰, 저장공간) 산출 │
│ - 할인 정책 적용 (통합 패키지 등) │
│ 출력: 견적서 JSON (SAM 표준 형식) │
└──────────────────────┬───────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Step 5: 견적서 초안 저장 │
│ - quotes 테이블에 저장 (status: draft) │
│ - quote_items에 모듈별 항목 저장 │
│ - 업무 분석 리포트 첨부 │
│ - 매니저에게 알림 → 검토/수정 → 확정 │
└──────────────────────────────────────────────────┘
```
---
## 3. SAM 모듈 카탈로그 (AI 프롬프트용)
> **참조**: `docs/rules/customer-pricing.md`
AI가 인터뷰 내용을 SAM 견적으로 변환하려면, SAM이 제공하는 모듈과 요금을 정확히 알아야 한다. 이 데이터는 **DB 테이블로 관리**하여 프롬프트에 동적 주입한다.
### 3.1 모듈 카탈로그 테이블 설계
```sql
CREATE TABLE ai_quotation_modules (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
module_code VARCHAR(50) NOT NULL, -- 'HR', 'SALES', 'FINANCE' 등
module_name VARCHAR(100) NOT NULL, -- '인사관리', '영업관리', '재무관리'
category ENUM('basic', 'individual', 'addon') NOT NULL,
description TEXT, -- 모듈 기능 설명
keywords JSON, -- AI 매핑용 키워드 목록
dev_cost DECIMAL(12,0) DEFAULT 0, -- 개발비 (원)
monthly_fee DECIMAL(10,0) DEFAULT 0, -- 월 구독료 (원)
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_tenant (tenant_id),
INDEX idx_category (category),
UNIQUE KEY uk_tenant_module (tenant_id, module_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 3.2 초기 데이터 (customer-pricing.md 기준)
| module_code | module_name | category | dev_cost | monthly_fee |
|-------------|-------------|----------|----------|-------------|
| `BASIC_PKG` | 기본 패키지 (인사+근태+급여+게시판) | basic | 5,000,000 | 200,000 |
| `HR` | 인사관리 | individual | 2,000,000 | 80,000 |
| `ATTENDANCE` | 근태관리 | individual | 1,500,000 | 60,000 |
| `PAYROLL` | 급여관리 | individual | 2,500,000 | 100,000 |
| `BOARD` | 게시판/공지사항 | individual | 500,000 | 20,000 |
| `SALES` | 영업관리 (CRM+견적+수주) | individual | 5,000,000 | 150,000 |
| `PURCHASE` | 구매/자재관리 | individual | 3,000,000 | 100,000 |
| `PRODUCTION` | 생산관리 (MES) | individual | 8,000,000 | 250,000 |
| `QUALITY` | 품질관리 | individual | 4,000,000 | 120,000 |
| `FINANCE` | 재무/회계관리 | individual | 5,000,000 | 150,000 |
| `LOGISTICS` | 물류/출하관리 | individual | 3,000,000 | 100,000 |
| `APPROVAL` | 전자결재 | individual | 3,000,000 | 80,000 |
| `DOCUMENT` | 문서관리 (전자서명) | individual | 2,000,000 | 60,000 |
| `EQUIPMENT` | 설비관리 | individual | 3,000,000 | 100,000 |
| `INTEGRATED` | 통합 패키지 | basic | 30,000,000 | 800,000 |
| `AI_TOKEN` | AI 토큰 추가 | addon | 0 | 별도 |
| `STORAGE` | 파일 저장공간 추가 | addon | 0 | 별도 |
| `CUSTOM_DEV` | 커스텀 개발 | addon | 별도 협의 | 0 |
### 3.3 keywords 필드 예시
```json
// HR 모듈
{
"keywords": ["직원", "사원", "인사", "조직도", "부서", "입퇴사", "인력"],
"pain_points": ["엑셀로 직원 관리", "입퇴사 관리가 번거로움", "조직도 없음"],
"business_needs": ["직원 정보 통합", "조직 구조 관리", "인력 현황 파악"]
}
// PRODUCTION 모듈
{
"keywords": ["생산", "제조", "작업지시", "공정", "LOT", "불량", "MES"],
"pain_points": ["생산 현황을 수기로 기록", "불량 추적 불가", "납기 관리 어려움"],
"business_needs": ["실시간 생산현황", "불량률 관리", "작업지시 자동화"]
}
```
---
## 4. AI 프롬프트 엔진
### 4.1 시스템 프롬프트 구조
```
┌─────────────────────────────────────────────────────┐
│ System Prompt │
├─────────────────────────────────────────────────────┤
│ │
│ [역할 정의] │
│ 너는 SAM ERP/MES 솔루션의 컨설팅 AI이다. │
│ 고객 인터뷰를 분석하여 맞춤형 견적서를 작성한다. │
│ │
│ [SAM 모듈 카탈로그] ← DB에서 동적 로드 │
│ 각 모듈의 기능, 키워드, 가격 정보 │
│ │
│ [요금 정책] ← customer-pricing 기반 │
│ 기본패키지, 개별모듈, 추가옵션 요금 체계 │
│ 할인 정책 (통합 패키지 할인 등) │
│ │
│ [출력 형식] │
│ JSON Schema 명시 (견적서 구조) │
│ │
│ [분석 지침] │
│ - 고객 업종/규모별 권장 모듈 기준 │
│ - 우선순위 결정 기준 (필수 vs 선택) │
│ - 비용 최적화 원칙 (패키지 vs 개별) │
│ │
└─────────────────────────────────────────────────────┘
```
### 4.2 프롬프트 템플릿 (1차: 업무 분석)
```
당신은 SAM(Smart Automation Management) ERP/MES 솔루션의 전문 컨설턴트입니다.
아래는 고객사 직원과의 인터뷰 내용입니다. 이를 분석하여 구조화된 업무 분석 보고서를 작성하세요.
## 고객 정보
- 회사명: {company_name}
- 업종: {industry}
- 직원 수: {employee_count}
- 인터뷰 대상: {interviewee_role}
## 인터뷰 내용
{interview_content}
## 분석 기준
다음 SAM 모듈 영역에 맞춰 분석하세요:
{module_catalog_json}
## 출력 형식 (JSON)
{
"company_analysis": {
"industry": "업종 분류",
"scale": "소규모/중소/중견",
"current_systems": ["현재 사용 중인 시스템"],
"digitalization_level": "상/중/하"
},
"business_domains": [
{
"domain": "인사/급여",
"current_process": "현재 처리 방식 설명",
"pain_points": ["문제점 1", "문제점 2"],
"improvement_needs": ["개선 필요사항"],
"priority": "필수/높음/보통/낮음",
"matched_modules": ["HR", "PAYROLL"]
}
],
"recommendations": {
"essential_modules": ["반드시 필요한 모듈 코드"],
"recommended_modules": ["권장 모듈 코드"],
"optional_modules": ["선택 모듈 코드"],
"package_suggestion": "BASIC_PKG 또는 INTEGRATED 또는 individual",
"reasoning": "패키지 추천 근거"
}
}
```
### 4.3 프롬프트 템플릿 (2차: 견적 생성)
```
아래 업무 분석 결과를 바탕으로 SAM 견적서를 생성하세요.
## 업무 분석 결과
{analysis_result_json}
## SAM 요금 정책
{pricing_policy_json}
## 견적 생성 규칙
1. 필수 모듈은 반드시 포함
2. 통합 패키지가 개별 합산보다 저렴하면 패키지 추천
3. 직원 수 기반 사용자 라이선스 산출
4. AI 토큰은 월 기본 100만 토큰 포함, 초과분 별도
5. 파일 저장공간은 기본 10GB, 초과분 별도
## 출력 형식 (JSON)
{
"quotation": {
"title": "견적서 제목",
"client_name": "고객사명",
"valid_until": "견적 유효기간",
"items": [
{
"category": "기본서비스/추가모듈/추가옵션",
"module_code": "모듈코드",
"module_name": "모듈명",
"description": "포함 기능 설명",
"dev_cost": 0,
"monthly_fee": 0,
"quantity": 1,
"note": "비고"
}
],
"summary": {
"total_dev_cost": 0,
"total_monthly_fee": 0,
"discount_type": "패키지할인/볼륨할인/없음",
"discount_rate": 0,
"final_dev_cost": 0,
"final_monthly_fee": 0
},
"implementation_plan": {
"estimated_months": 0,
"phases": [
{
"phase": 1,
"name": "단계명",
"modules": ["모듈코드"],
"duration_weeks": 0
}
]
},
"analysis_summary": "업무 분석 요약 (고객 설명용)"
}
}
```
---
## 5. API 설계
### 5.1 엔드포인트
| Method | Path | 설명 |
|--------|------|------|
| `POST` | `/api/v1/ai/quotation/analyze` | 인터뷰 분석 (1차) |
| `POST` | `/api/v1/ai/quotation/generate` | 견적서 생성 (2차) |
| `POST` | `/api/v1/ai/quotation/generate-full` | 분석+생성 통합 (원스텝) |
| `GET` | `/api/v1/ai/quotation/{id}` | AI 견적 상세 조회 |
| `GET` | `/api/v1/ai/quotation` | AI 견적 목록 |
| `PUT` | `/api/v1/ai/quotation/{id}` | AI 견적 수정 (매니저) |
| `POST` | `/api/v1/ai/quotation/{id}/confirm` | 정식 견적으로 전환 |
| `DELETE` | `/api/v1/ai/quotation/{id}` | AI 견적 삭제 |
### 5.2 요청/응답 예시
#### POST `/api/v1/ai/quotation/analyze`
**Request:**
```json
{
"client_id": 15,
"client_name": "(주)대한기계",
"industry": "기계제조업",
"employee_count": 45,
"interviewee_role": "관리부 팀장",
"interview_content": "현재 직원 관리는 엑셀로 하고 있어요. 출퇴근도 수기로 기록하고... 영업팀에서는 견적서를 한글 프로그램으로 만들어서 이메일로 보내는데, 이력 관리가 안 돼요. 생산 현장에서는 작업일보를 종이에 쓰고 있고, 불량이 나면 어디서 발생했는지 추적이 안 됩니다. 재고도 실사를 해봐야 알 수 있어요...",
"interview_type": "text"
}
```
**Response:**
```json
{
"success": true,
"data": {
"id": 1,
"analysis": {
"company_analysis": {
"industry": "기계제조업",
"scale": "중소기업 (45명)",
"current_systems": ["엑셀", "한글 프로그램", "종이 문서"],
"digitalization_level": "하"
},
"business_domains": [
{
"domain": "인사/급여",
"current_process": "엑셀로 직원 관리, 수기 출퇴근 기록",
"pain_points": ["인사정보 분산", "출퇴근 수기 기록"],
"priority": "필수",
"matched_modules": ["HR", "ATTENDANCE", "PAYROLL"]
},
{
"domain": "영업관리",
"current_process": "한글 프로그램 견적서, 이메일 발송",
"pain_points": ["견적 이력 관리 불가", "영업 현황 파악 어려움"],
"priority": "높음",
"matched_modules": ["SALES"]
},
{
"domain": "생산관리",
"current_process": "종이 작업일보, 수동 불량 관리",
"pain_points": ["불량 추적 불가", "생산현황 실시간 파악 불가"],
"priority": "필수",
"matched_modules": ["PRODUCTION", "QUALITY"]
},
{
"domain": "재고/물류",
"current_process": "실사로만 재고 파악",
"pain_points": ["실시간 재고 파악 불가"],
"priority": "높음",
"matched_modules": ["PURCHASE", "LOGISTICS"]
}
],
"recommendations": {
"essential_modules": ["HR", "ATTENDANCE", "PAYROLL", "PRODUCTION", "QUALITY"],
"recommended_modules": ["SALES", "PURCHASE", "LOGISTICS"],
"optional_modules": ["APPROVAL", "DOCUMENT"],
"package_suggestion": "INTEGRATED",
"reasoning": "8개 이상 모듈이 필요하므로 통합 패키지(30,000,000원)가 개별 합산(34,500,000원)보다 경제적"
}
},
"token_usage": {
"prompt_tokens": 1250,
"completion_tokens": 890,
"cost_krw": 45
}
}
}
```
### 5.3 컨트롤러 / 서비스 구조
```
app/Http/Controllers/Api/V1/
└── AiQuotationController.php
├── analyze() ← 인터뷰 분석
├── generate() ← 견적 생성
├── generateFull() ← 통합 (분석+생성)
├── index() ← 목록
├── show() ← 상세
├── update() ← 수정
├── confirm() ← 정식 견적 전환
└── destroy() ← 삭제
app/Services/
└── AiQuotationService.php
├── analyzeInterview() ← 1차: 인터뷰 분석
├── generateQuotation() ← 2차: 견적 생성
├── generateFull() ← 통합 처리
├── confirmToQuote() ← quotes 테이블로 전환
├── buildAnalysisPrompt() ← 분석 프롬프트 조립
├── buildQuotationPrompt() ← 견적 프롬프트 조립
├── loadModuleCatalog() ← DB에서 모듈 카탈로그 로드
├── loadPricingPolicy() ← DB에서 요금 정책 로드
├── callClaudeApi() ← Claude API 호출
├── parseResponse() ← 응답 JSON 파싱
└── saveTokenUsage() ← 토큰 사용량 기록
app/Http/Requests/V1/AiQuotation/
├── AiQuotationAnalyzeRequest.php
├── AiQuotationGenerateRequest.php
├── AiQuotationUpdateRequest.php
└── AiQuotationConfirmRequest.php
```
---
## 6. 데이터베이스 설계
### 6.1 신규 테이블
#### ai_quotations (AI 견적 마스터)
```sql
CREATE TABLE ai_quotations (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
-- 고객 정보
client_id BIGINT UNSIGNED NULL,
client_name VARCHAR(200) NOT NULL,
industry VARCHAR(100) NULL,
employee_count INT NULL,
interviewee_role VARCHAR(100) NULL,
-- 인터뷰 데이터
interview_content TEXT NOT NULL,
interview_type ENUM('text', 'voice', 'document') DEFAULT 'text',
voice_recording_id BIGINT UNSIGNED NULL, -- ai_voice_recordings 참조
-- AI 분석 결과
analysis_result JSON NULL, -- 1차 분석 결과
quotation_result JSON NULL, -- 2차 견적 결과
-- 상태 관리
status ENUM('analyzing', 'analyzed', 'generating', 'generated',
'confirmed', 'failed') DEFAULT 'analyzing',
error_message TEXT NULL,
-- 연결
quote_id BIGINT UNSIGNED NULL, -- 정식 견적 전환 시 quotes.id
-- 금액 요약
total_dev_cost DECIMAL(12,0) DEFAULT 0,
total_monthly_fee DECIMAL(10,0) DEFAULT 0,
discount_rate DECIMAL(5,2) DEFAULT 0,
final_dev_cost DECIMAL(12,0) DEFAULT 0,
final_monthly_fee DECIMAL(10,0) DEFAULT 0,
-- 토큰 사용
total_tokens INT DEFAULT 0,
total_cost_krw DECIMAL(12,2) DEFAULT 0,
-- 감사
created_by BIGINT UNSIGNED NULL,
updated_by BIGINT UNSIGNED NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_tenant_status (tenant_id, status),
INDEX idx_tenant_client (tenant_id, client_id),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
#### ai_quotation_items (AI 견적 항목)
```sql
CREATE TABLE ai_quotation_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
ai_quotation_id BIGINT UNSIGNED NOT NULL,
category ENUM('basic', 'module', 'addon') NOT NULL,
module_code VARCHAR(50) NOT NULL,
module_name VARCHAR(100) NOT NULL,
description TEXT NULL,
dev_cost DECIMAL(12,0) DEFAULT 0,
monthly_fee DECIMAL(10,0) DEFAULT 0,
quantity INT DEFAULT 1,
priority ENUM('essential', 'recommended', 'optional') DEFAULT 'recommended',
ai_reasoning TEXT NULL, -- AI가 이 모듈을 추천한 근거
matched_pain_points JSON NULL, -- 매칭된 고객 Pain Point
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_quotation (ai_quotation_id),
FOREIGN KEY (ai_quotation_id) REFERENCES ai_quotations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 6.2 기존 테이블 활용
| 테이블 | 용도 | 연동 방식 |
|--------|------|----------|
| `ai_configs` | Claude API 키/모델 설정 | provider='claude' 조회 |
| `ai_token_usages` | 토큰 비용 추적 | menu_name='AI견적' |
| `ai_pricing_configs` | Claude 모델 단가 | provider='claude' |
| `quotes` | 정식 견적 전환 대상 | `confirm` 시 생성 |
| `clients` | 고객사 정보 | client_id 참조 |
---
## 7. Phase별 개발 계획
### Phase 1: 텍스트 인터뷰 → AI 견적 (MVP)
> **목표**: 텍스트 인터뷰 입력 → Claude 분석 → 견적서 자동생성
> **기간**: 2~3주
> **우선순위**: 🔴 필수
| 단계 | 작업 | 프로젝트 | 상태 |
|------|------|---------|------|
| 1-1 | `ai_quotation_modules` 마이그레이션 + 시더 | API | ⏳ |
| 1-2 | `ai_quotations`, `ai_quotation_items` 마이그레이션 | API | ⏳ |
| 1-3 | 모델 생성 (`AiQuotation`, `AiQuotationItem`, `AiQuotationModule`) | API | ⏳ |
| 1-4 | `AiQuotationService` — Claude API 연동 | API | ⏳ |
| 1-5 | 프롬프트 엔진 구현 (분석 + 견적 템플릿) | API | ⏳ |
| 1-6 | `AiQuotationController` + FormRequest | API | ⏳ |
| 1-7 | API 라우트 등록 (`routes/api/v1/common.php`) | API | ⏳ |
| 1-8 | 정식 견적 전환 로직 (`confirmToQuote`) | API | ⏳ |
| 1-9 | MNG AI 견적 관리 화면 (목록/상세/수정) | MNG | ⏳ |
| 1-10 | 테스트 (인터뷰 샘플 3건 이상) | API | ⏳ |
### Phase 2: 음성 인터뷰 연동
> **목표**: 현장에서 녹음한 음성 → STT 변환 → AI 분석 → 견적 생성
> **기간**: 1주 (기존 STT 인프라 재사용으로 단축)
> **선행 조건**: Phase 1 완료
| 단계 | 작업 | 프로젝트 | 상태 | 비고 |
|------|------|---------|------|------|
| 2-1 | Google STT 서비스 구현 | MNG | ✅ 완료 | `GoogleCloudService::speechToText()` 재사용 |
| 2-2 | 음성 업로드 API (GCS 저장) | MNG | ✅ 완료 | `AiVoiceRecordingService`, `ConsultationController` 재사용 |
| 2-3 | STT → 텍스트 변환 파이프라인 | MNG | ✅ 완료 | LongRunningRecognize + 폴링 패턴 구현됨 |
| 2-4 | 음성 인터뷰 → AI 견적 통합 플로우 | API | ⏳ | 기존 STT 결과를 Claude 분석에 연결 |
| 2-5 | MNG 음성 녹음 업로드 UI | MNG | ✅ 완료 | `voice-recorder.blade.php` 재사용 가능 |
> **핵심**: Google Cloud STT, GCS, 브라우저 음성 녹음 UI가 모두 구현 완료 상태.
> Phase 2에서는 기존 STT 결과를 AI 견적 파이프라인(Claude API)에 연결하는 **통합 플로우(2-4)만 신규 개발**하면 된다.
### Phase 3: 학습 데이터 고도화
> **목표**: 과거 견적 데이터를 활용하여 AI 정확도 향상
> **기간**: 2주
> **선행 조건**: Phase 1 + 실제 사용 데이터 축적
| 단계 | 작업 | 프로젝트 | 상태 |
|------|------|---------|------|
| 3-1 | 과거 견적 데이터 → 프롬프트 Few-shot 예시 구성 | API | ⏳ |
| 3-2 | 확정된 AI 견적 → 학습 데이터 피드백 루프 | API | ⏳ |
| 3-3 | 업종별 견적 패턴 분석 → 추천 정확도 향상 | API | ⏳ |
| 3-4 | 프롬프트 A/B 테스트 프레임워크 | API | ⏳ |
### Phase 4: 고객 셀프서비스 (확장)
> **목표**: 고객이 직접 간단한 질문에 답변하면 견적 자동 생성
> **기간**: 3주
> **선행 조건**: Phase 1~3 안정화
| 단계 | 작업 | 프로젝트 | 상태 |
|------|------|---------|------|
| 4-1 | 고객용 인터뷰 설문 폼 설계 | React | ⏳ |
| 4-2 | 단계별 질문 → AI 분석 통합 | API + React | ⏳ |
| 4-3 | 견적서 미리보기 + PDF 다운로드 | React | ⏳ |
| 4-4 | 매니저 알림 → 후속 상담 연결 | API + MNG | ⏳ |
---
## 8. Claude API 연동 상세
### 8.1 SDK 설치
```bash
# API 프로젝트에 Anthropic SDK 설치
docker exec sam-api-1 composer require anthropic-ai/laravel
```
> **참고**: `anthropic-ai/laravel` 패키지는 Laravel용 공식 래퍼로, HTTP Client 기반으로 Claude API를 호출한다. 미출시/미지원 시 `GuzzleHttp`로 직접 HTTP 호출한다.
### 8.2 Claude API 호출 패턴
```php
// app/Services/AiQuotationService.php
class AiQuotationService
{
private function callClaudeApi(string $systemPrompt, string $userMessage): array
{
// 1. ai_configs에서 Claude 설정 로드
$config = AiConfig::where('provider', 'claude')
->where('is_active', true)
->first();
// 2. HTTP 호출
$response = Http::withHeaders([
'x-api-key' => $config->api_key,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
])->post($config->base_url . '/messages', [
'model' => $config->model, // claude-sonnet-4-20250514
'max_tokens' => 4096,
'temperature' => 0.3, // 견적은 일관성 중요
'system' => $systemPrompt,
'messages' => [
['role' => 'user', 'content' => $userMessage]
],
]);
// 3. 토큰 사용량 기록
$usage = $response->json('usage');
AiTokenHelper::saveClaudeUsage(
tenantId: auth()->user()->tenant_id,
menuName: 'AI견적',
promptTokens: $usage['input_tokens'],
completionTokens: $usage['output_tokens'],
model: $config->model,
);
// 4. 응답 파싱
$content = $response->json('content.0.text');
return json_decode($content, true);
}
}
```
### 8.3 비용 예측
| 항목 | 토큰 | 비용 (USD) | 비용 (KRW) |
|------|------|-----------|------------|
| 1차 분석 프롬프트 (시스템+사용자) | ~2,000 입력 | $0.0005 | ~1원 |
| 1차 분석 응답 | ~1,500 출력 | $0.0019 | ~3원 |
| 2차 견적 프롬프트 | ~3,000 입력 | $0.0008 | ~1원 |
| 2차 견적 응답 | ~2,000 출력 | $0.0025 | ~4원 |
| **견적 1건 합계** | **~8,500** | **~$0.006** | **~9원** |
> Claude Sonnet 기준. 1건당 약 **9원**으로 매우 경제적이다.
---
## 9. MNG 관리 화면
### 9.1 화면 목록
| 화면 | URL | 설명 |
|------|-----|------|
| AI 견적 목록 | `/ai-quotation` | 생성된 AI 견적 목록 |
| AI 견적 생성 | `/ai-quotation/create` | 인터뷰 입력 폼 |
| AI 견적 상세 | `/ai-quotation/{id}` | 분석 결과 + 견적서 조회 |
| AI 견적 수정 | `/ai-quotation/{id}/edit` | 매니저가 수정 |
| 모듈 카탈로그 관리 | `/ai-quotation/modules` | SAM 모듈 목록 관리 |
### 9.2 AI 견적 생성 화면 구성
```
┌─────────────────────────────────────────────────────┐
│ AI 견적서 생성 │
├─────────────────────────────────────────────────────┤
│ │
│ [고객 정보] │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 고객사 선택 ▼│ │ 업종 │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 직원 수 │ │ 인터뷰 대상 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ [인터뷰 내용] │
│ ┌─────────────────────────────────────────────┐ │
│ │ │ │
│ │ (텍스트 입력 또는 음성 녹음 업로드) │ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [🔄 분석 시작] [📊 분석+견적 한번에] │
│ │
├─────────────────────────────────────────────────────┤
│ [분석 결과] (Ajax 응답) │
│ │
│ 📊 업무 도메인 분석 │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ 도메인 │ 현재상태 │ 문제점 │ 추천모듈 │ │
│ ├──────────┼──────────┼──────────┼──────────┤ │
│ │ 인사/급여 │ 엑셀관리 │ 분산관리 │ HR,PAYROLL│ │
│ │ 생산관리 │ 종이기록 │ 추적불가 │PRODUCTION │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │
│ 💰 견적서 초안 │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ 모듈 │ 개발비 │ 월구독료 │ 우선순위 │ │
│ ├──────────┼──────────┼──────────┼──────────┤ │
│ │ 통합패키지 │30,000,000│ 800,000 │ 필수 │ │
│ │ AI토큰 │ 0│ 50,000 │ 선택 │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ 합계: 개발비 30,000,000원 / 월 850,000원 │
│ │
│ [✅ 정식 견적으로 전환] [✏️ 수정] [🗑️ 삭제] │
│ │
└─────────────────────────────────────────────────────┘
```
---
## 10. 보안 및 운영
### 10.1 보안 고려사항
| 항목 | 대책 |
|------|------|
| API 키 노출 | `ai_configs` 테이블에 암호화 저장, .env 폴백 |
| 인터뷰 데이터 보호 | tenant_id 격리, 접근 권한 제어 |
| Claude API 비용 제어 | 일일/월별 토큰 한도 설정 (ai_configs.options) |
| 프롬프트 인젝션 | 사용자 입력 sanitize, 시스템 프롬프트 분리 |
### 10.2 모니터링
| 항목 | 방법 |
|------|------|
| API 호출 성공/실패 | `ai_quotations.status` + `error_message` |
| 토큰 사용량 | `ai_token_usages` 테이블 (기존 인프라) |
| 비용 추적 | MNG `/system/ai-token-usage` (기존 UI) |
| 견적 전환율 | `ai_quotations``quotes` 전환 비율 통계 |
### 10.3 에러 처리
```php
try {
$result = $this->callClaudeApi($systemPrompt, $userMessage);
} catch (ConnectionException $e) {
// Claude API 연결 실패
$aiQuotation->update(['status' => 'failed', 'error_message' => 'Claude API 연결 실패']);
} catch (JsonException $e) {
// 응답 JSON 파싱 실패
$aiQuotation->update(['status' => 'failed', 'error_message' => 'AI 응답 파싱 실패']);
}
```
---
## 11. 기대 효과
| 항목 | Before (현재) | After (AI 엔진) |
|------|--------------|-----------------|
| 견적 작성 시간 | 2~4시간 (수동) | 5~10분 (AI 초안 + 검토) |
| 모듈 누락 위험 | 매니저 경험 의존 | AI가 체계적으로 분석 |
| 고객 맞춤화 | 표준 템플릿 복사 | 인터뷰 기반 맞춤 견적 |
| 비용 최적화 | 수동 비교 | AI가 패키지 vs 개별 자동 비교 |
| 견적 1건 AI 비용 | — | ~9원 (Claude Sonnet) |
---
## 변경 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-02 | 기획 초안 작성 |
---
## 관련 문서
| 문서 | 경로 |
|------|------|
| SAM 프로젝트 개요 | `docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` |
| 견적 기능 상세 | `docs/features/quotes/README.md` |
| 견적 시스템 분석 | `docs/data/견적/견적시스템_분석문서.md` |
| AI 기능 현황 | `docs/features/ai/README.md` |
| AI 설정 가이드 | `docs/guides/ai-config-settings.md` |
| 고객 요금 안내 | `docs/rules/customer-pricing.md` |
| 내부 과금 정책 | `docs/rules/billing-policy.md` |
| 단가 정책 | `docs/rules/pricing-policy.md` |
| Plans 가이드 | `docs/plans/GUIDE.md` |
---
**최종 업데이트**: 2026-03-02

View File

@@ -0,0 +1,284 @@
# MNG 근태현황 개발 계획서
> **작성일**: 2026-02-26
> **상태**: 계획 수립
---
## 1. 개요
### 1.1 목적
MNG 인사관리 > 근태현황 기능을 완성한다. 현재 기본 CRUD가 구현되어 있으나, 미완성 기능과 알려진 버그를 해결하고 실무에 필요한 추가 기능을 구현한다.
### 1.2 현재 상태 분석
#### 구현 완료
| 항목 | 상태 | 파일 |
|------|------|------|
| 근태 목록 조회 (HTMX 테이블) | ✅ | `index.blade.php`, `table.blade.php` |
| 월간 통계 카드 (5종) | ✅ | `index.blade.php` |
| 필터 (이름, 부서, 상태, 날짜) | ✅ | `index.blade.php` |
| 등록/수정 모달 | ✅ | `index.blade.php` |
| CRUD API (목록/등록/수정/삭제) | ✅ | `AttendanceController.php` (API) |
| AttendanceService | ✅ | `AttendanceService.php` |
| Attendance 모델 (8개 상태) | ✅ | `Attendance.php` |
| Soft Delete | ✅ | 모델 + 서비스 |
#### 알려진 문제 (E2E 테스트 결과)
| 문제 | 심각도 | 설명 |
|------|--------|------|
| 엑셀 다운로드 미구현 | 🟡 중요 | 버튼 없음, API 미연결 |
| 근태 등록 서버 에러 | 🔴 필수 | 모달 submit 시 500 에러 발생 가능 |
#### 미구현 기능 (API 대비)
| 기능 | API 지원 | MNG 상태 |
|------|---------|---------|
| 엑셀 내보내기 | ✅ `/v1/attendances/export` | ❌ 미구현 |
| 일괄 삭제 | ✅ `/v1/attendances/bulk-delete` | ❌ 미구현 |
| 개인별 근태 상세 | ✅ `/v1/attendances/{id}` | ❌ 미구현 |
| 월간 요약 통계 | ✅ `/v1/attendances/monthly-stats` | ⚠️ 기본만 구현 |
| 출퇴근 설정 관리 | ✅ `attendance_settings` 테이블 | ❌ 미구현 |
---
## 2. 구현 범위
### 2.1 Phase 1: 버그 수정 + 핵심 기능 (우선)
| # | 작업 | 난이도 | 설명 |
|---|------|--------|------|
| 1-1 | 근태 등록/수정 버그 수정 | 🟢 낮음 | store/update API 요청 오류 점검 및 수정 |
| 1-2 | 엑셀 다운로드 | 🟢 낮음 | API `/v1/attendances/export` 연동, 다운로드 버튼 추가 |
| 1-3 | 일괄 삭제 | 🟡 보통 | 체크박스 선택 → 일괄 삭제 버튼 |
| 1-4 | 월간 통계 기간 선택 | 🟢 낮음 | 현재 당월 고정 → 연/월 선택 가능하게 |
### 2.2 Phase 2: 확장 기능
| # | 작업 | 난이도 | 설명 |
|---|------|--------|------|
| 2-1 | 개인별 근태 상세 페이지 | 🟡 보통 | 사원 클릭 → 월간 달력 + 출퇴근 이력 |
| 2-2 | 출퇴근 설정 관리 | 🟡 보통 | 표준 출근시간, GPS 사용여부, 허용반경 설정 |
| 2-3 | 월간/주간 요약 뷰 | 🟡 보통 | 부서별/사원별 근태 요약 테이블 |
| 2-4 | 근태 일괄 등록 | 🔴 높음 | 날짜 범위 + 대상 사원 → 일괄 근태 등록 |
---
## 3. 상세 설계
### 3.1 Phase 1-1: 근태 등록/수정 버그 수정
**점검 항목**:
- MNG `AttendanceController::store()` validation 규칙과 실제 폼 데이터 일치 여부
- `check_in`, `check_out` 포맷 (HH:MM vs HH:MM:SS) 불일치 가능성
- `user_id` 전달 누락 여부
- HTMX `hx-headers` CSRF 토큰 전달 확인
**수정 대상 파일**:
- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php`
- `mng/resources/views/hr/attendances/index.blade.php` (JS `submitAttendance()`)
---
### 3.2 Phase 1-2: 엑셀 다운로드
**방식**: MNG에서 직접 엑셀 생성 (API 서버 미경유)
**구현**:
1. `AttendanceService::getExportData()` 메서드 추가
2. `AttendanceController::export()` 메서드 추가
3. 라우트: `GET /api/admin/hr/attendances/export`
4. 인덱스 페이지에 다운로드 버튼 추가
**엑셀 컬럼**:
| 컬럼 | 값 |
|------|-----|
| 날짜 | `base_date` |
| 사원명 | `user.name` |
| 부서 | `department.name` |
| 상태 | `status_label` |
| 출근 | `check_in` |
| 퇴근 | `check_out` |
| 비고 | `remarks` |
**수정 대상 파일**:
- `mng/app/Services/HR/AttendanceService.php`
- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php`
- `mng/routes/api.php`
- `mng/resources/views/hr/attendances/index.blade.php`
---
### 3.3 Phase 1-3: 일괄 삭제
**UI**: 테이블 각 행에 체크박스 추가, 헤더에 전체선택, 상단에 "선택 삭제" 버튼
**구현**:
1. `table.blade.php`에 체크박스 컬럼 추가
2. Alpine.js 컴포넌트로 선택 상태 관리
3. `AttendanceController::bulkDestroy()` 메서드 추가
4. 라우트: `POST /api/admin/hr/attendances/bulk-delete`
**수정 대상 파일**:
- `mng/resources/views/hr/attendances/partials/table.blade.php`
- `mng/resources/views/hr/attendances/index.blade.php`
- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php`
- `mng/app/Services/HR/AttendanceService.php`
- `mng/routes/api.php`
---
### 3.4 Phase 1-4: 월간 통계 기간 선택
**현재**: 당월 통계만 표시 (하드코딩)
**변경**: 연/월 드롭다운 추가 → 선택 시 통계 카드 HTMX 갱신
**구현**:
1. 통계 카드 영역을 별도 partial로 분리 (`partials/stats.blade.php`)
2. 연/월 선택 UI 추가
3. `hx-get` + `hx-vals`로 선택된 연/월 전달
4. `stats()` API가 `year`, `month` 파라미터 이미 지원
**수정 대상 파일**:
- `mng/resources/views/hr/attendances/index.blade.php`
- `mng/resources/views/hr/attendances/partials/stats.blade.php` (신규)
---
### 3.5 Phase 2-1: 개인별 근태 상세 페이지
**URL**: `/hr/attendances/{userId}`
**페이지 구성**:
1. **사원 프로필 카드**: 이름, 부서, 직급, 재직상태
2. **월간 달력**: 날짜별 근태 상태를 색상 도트로 표시
3. **월간 통계**: 정시출근 N일, 지각 N일, 결근 N일 등
4. **출퇴근 이력 테이블**: 해당 월의 상세 출퇴근 기록
**수정 대상 파일**:
- `mng/routes/web.php` (라우트 추가)
- `mng/app/Http/Controllers/HR/AttendanceController.php` (`show()` 추가)
- `mng/app/Services/HR/AttendanceService.php` (`getUserMonthlyAttendances()` 추가)
- `mng/resources/views/hr/attendances/show.blade.php` (신규)
---
### 3.6 Phase 2-2: 출퇴근 설정 관리
**URL**: `/hr/attendance-settings`
**설정 항목** (`attendance_settings` 테이블 기반):
| 항목 | 필드 | 설명 |
|------|------|------|
| GPS 출퇴근 사용 | `use_gps` | on/off 토글 |
| 자동 출퇴근 | `use_auto` | on/off 토글 |
| 허용 반경 | `allowed_radius` | 미터 단위 입력 |
| 본사 주소 | `hq_address` | 주소 입력 |
| 본사 위도/경도 | `hq_latitude`, `hq_longitude` | 좌표 입력 |
**수정 대상 파일**:
- `mng/routes/web.php`, `mng/routes/api.php`
- `mng/app/Http/Controllers/HR/AttendanceSettingController.php` (신규)
- `mng/app/Models/HR/AttendanceSetting.php` (신규 — API 모델 미러링)
- `mng/resources/views/hr/attendance-settings/index.blade.php` (신규)
---
## 4. 데이터 흐름
### 4.1 MNG 자체 CRUD 패턴 (현재)
```
브라우저 ──HTMX──→ MNG API Controller ──→ MNG Service ──→ DB (직접)
(api/admin/hr/attendances)
```
> MNG는 API 서버를 경유하지 않고 DB에 직접 접근한다.
### 4.2 엑셀 다운로드 흐름
```
브라우저 ──GET──→ MNG AttendanceController::export()
→ AttendanceService::getExportData()
→ ExportService::download() (라라벨 엑셀)
← BinaryFileResponse (.xlsx)
```
---
## 5. 구현 순서 및 의존 관계
```
Phase 1 (버그 수정 + 핵심)
1-1 버그 수정 ─────────────────────────────┐
1-2 엑셀 다운로드 ─────────────────────────┤ 독립적, 병렬 가능
1-3 일괄 삭제 ─────────────────────────────┤
1-4 통계 기간 선택 ────────────────────────┘
Phase 2 (확장)
2-1 개인별 상세 ───→ 2-3 월간/주간 요약 (데이터 재사용)
2-2 출퇴근 설정 ─── 독립적
2-4 일괄 등록 ───── 독립적
```
---
## 6. 관련 파일 목록
### MNG 프로젝트 (`/home/aweso/sam/mng`)
| 파일 | 역할 |
|------|------|
| `app/Models/HR/Attendance.php` | 모델 (8개 상태, json_details) |
| `app/Services/HR/AttendanceService.php` | 비즈니스 로직 |
| `app/Http/Controllers/HR/AttendanceController.php` | 뷰 컨트롤러 |
| `app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | API 컨트롤러 |
| `resources/views/hr/attendances/index.blade.php` | 메인 페이지 |
| `resources/views/hr/attendances/partials/table.blade.php` | 테이블 partial |
| `routes/web.php` | 웹 라우트 |
| `routes/api.php` | API 라우트 |
### API 프로젝트 (`/home/aweso/sam/api`)
| 파일 | 역할 |
|------|------|
| `database/migrations/2025_12_09_*_attendances*` | 마이그레이션 (2개) |
| `database/migrations/2025_12_17_*_attendance_settings*` | 설정 테이블 |
| `app/Models/Tenants/Attendance.php` | API 모델 (참조용) |
| `app/Models/Tenants/AttendanceSetting.php` | 설정 모델 (참조용) |
### 참조 문서
| 문서 | 경로 |
|------|------|
| 근태 API 규칙 | `docs/rules/attendance-api.md` |
| GPS 출퇴근 스펙 | `docs/specs/erp-analysis/03-gps-attendance.md` |
---
## 7. 검증 방법
### Phase 1 체크리스트
- [ ] 근태 등록 모달 → 사원 선택 + 날짜 + 상태 입력 → 저장 성공
- [ ] 근태 수정 모달 → 기존 데이터 로드 → 수정 → 저장 성공
- [ ] 동일 사원/날짜 중복 등록 시 기존 데이터 업데이트 (Upsert)
- [ ] 엑셀 다운로드 버튼 클릭 → .xlsx 파일 다운로드
- [ ] 체크박스 선택 → 일괄 삭제 → 테이블 갱신
- [ ] 연/월 선택 → 통계 카드 갱신
### Phase 2 체크리스트
- [ ] 사원 이름 클릭 → 개인별 상세 페이지 이동
- [ ] 달력에 근태 상태 색상 표시
- [ ] 출퇴근 설정 페이지 → GPS/자동 토글 → 저장
- [ ] 허용 반경 변경 → 저장 → DB 반영
---
**최종 업데이트**: 2026-02-26

View File

@@ -0,0 +1,706 @@
# 양식 디자이너(Block Builder) 고도화 계획
> **작성일**: 2026-03-06
> **상태**: 계획 수립
> **담당**: Claude Code + 개발팀
> **관련**: [문서양식관리](../features/documents/mng-document-template.md) | [문서관리](../features/documents/mng-document-system.md)
---
## 1. 현재 상태 진단
### 1.1 구현 완료 (Phase 1 — 2026-02-28)
- 13개 블록 타입 (기본 6 + 폼 7)
- 3패널 UI (팔레트 / 캔버스 / 속성)
- SortableJS 드래그-앤-드롭 정렬
- Undo/Redo (최대 50단계)
- JSON 스키마 저장 (`document_templates.schema`)
- 페이지 설정 (A4/A3/B5, 세로/가로, 여백)
### 1.2 핵심 미구현 사항
| 기능 | 상태 | 영향도 |
|------|------|--------|
| 문서 생성 시 블록 렌더링 | 미구현 | 블록 서식으로 문서 작성 불가 |
| 결재선 블록 | 미구현 | 결재 워크플로우 연동 불가 |
| 데이터 바인딩 (EAV 연동) | 미구현 | 입력값 저장/로드 불가 |
| 동적 행 추가 | 미구현 | 검사 데이터 행 추가 불가 |
| 변수/매크로 시스템 | 미구현 | 자동 값 주입 불가 |
| 인쇄/PDF 출력 | 미구현 | 블록 문서 인쇄 불가 |
| Columns 내부 블록 | 미구현 | 다단 레이아웃 활용 불가 |
> **결론**: 양식 디자이너는 **레이아웃 편집기**로만 동작. 실제 문서 생성/결재/인쇄에서는 Legacy Builder만 사용 가능.
---
## 2. 목표
Legacy Builder의 모든 기능을 양식 디자이너에서 지원하면서, 더 유연하고 확장 가능한 문서 시스템 구축.
**최종 목표:**
```
양식 디자이너로 서식 설계
블록 스키마 기반 문서 생성 (데이터 입력)
결재 워크플로우 (작성 → 검토 → 승인)
인쇄/PDF 출력
Legacy Builder 완전 대체
```
---
## 3. 개발 로드맵 (6단계)
### Phase 2: 블록 런타임 렌더러 (기반 인프라)
> **목표**: 저장된 블록 스키마를 문서 생성/조회/인쇄에서 렌더링
#### 2-1. 블록 렌더러 엔진
**위치**: `mng/resources/views/documents/partials/block-renderer.blade.php`
```
schema JSON 입력
블록 타입별 Blade 컴포넌트 렌더링
모드별 출력:
- view 모드: 읽기 전용 HTML
- edit 모드: 입력 폼 HTML
- print 모드: 인쇄 최적화 HTML
```
**핵심 구현:**
```php
// BlockRendererService
class BlockRendererService
{
public function render(array $schema, string $mode, array $data = []): string
{
$html = '';
foreach ($schema['blocks'] as $block) {
$html .= $this->renderBlock($block, $mode, $data);
}
return $html;
}
private function renderBlock(array $block, string $mode, array $data): string
{
return match($block['type']) {
'heading' => $this->renderHeading($block, $mode),
'paragraph' => $this->renderParagraph($block, $mode),
'table' => $this->renderTable($block, $mode, $data),
'text_field' => $this->renderTextField($block, $mode, $data),
'number_field' => $this->renderNumberField($block, $mode, $data),
'date_field' => $this->renderDateField($block, $mode, $data),
'select_field' => $this->renderSelectField($block, $mode, $data),
'checkbox_field' => $this->renderCheckboxField($block, $mode, $data),
'textarea_field' => $this->renderTextareaField($block, $mode, $data),
'signature_field'=> $this->renderSignatureField($block, $mode, $data),
'divider' => $this->renderDivider($block),
'spacer' => $this->renderSpacer($block),
'columns' => $this->renderColumns($block, $mode, $data),
'approval_line' => $this->renderApprovalLine($block, $mode, $data),
'dynamic_table' => $this->renderDynamicTable($block, $mode, $data),
default => '',
};
}
}
```
#### 2-2. 문서 편집 화면 통합
**수정 대상**: `mng/resources/views/documents/edit.blade.php`
```
Template 로드
isBlockBuilder() 체크
├── true → BlockRendererService::render(schema, 'edit', data)
└── false → 기존 Legacy 렌더링 (변경 없음)
```
#### 2-3. 데이터 바인딩 (EAV 매핑)
블록의 `binding` 속성으로 EAV 데이터와 연결:
```javascript
// 블록 스키마 예시
{
"type": "text_field",
"props": {
"label": "제품명",
"binding": "bf_product_name", // ← EAV field_key
"required": true
}
}
```
```
저장 시:
block.binding → document_data.field_key
input.value → document_data.field_value
block.id → document_data.section_id (블록 ID를 섹션으로 활용)
로드 시:
document_data 조회 → field_key로 블록 매칭 → 값 주입
```
**산출물:**
| 파일 | 작업 |
|------|------|
| `mng/app/Services/BlockRendererService.php` | 신규 생성 |
| `mng/resources/views/documents/partials/block-renderer.blade.php` | 신규 생성 |
| `mng/resources/views/documents/edit.blade.php` | 블록 빌더 분기 추가 |
| `mng/resources/views/documents/show.blade.php` | 블록 빌더 분기 추가 |
| `api/app/Services/DocumentService.php` | 블록 데이터 저장/로드 로직 |
---
### Phase 3: 결재선 블록
> **목표**: 블록 스키마 내에서 결재 워크플로우 정의
#### 3-1. approval_line 블록 타입 추가
**스키마:**
```json
{
"type": "approval_line",
"props": {
"steps": [
{ "role": "작성", "department": "", "name": "" },
{ "role": "검토", "department": "", "name": "" },
{ "role": "승인", "department": "", "name": "" }
],
"style": "horizontal",
"showStamp": true
}
}
```
#### 3-2. 팔레트에 결재선 블록 추가
```javascript
// 블록 팔레트 추가
{ type: 'approval_line', icon: '✓', label: '결재선', category: '워크플로우' }
```
#### 3-3. 속성 패널 결재선 편집기
```
┌─────────────────────────────┐
│ 결재선 설정 │
│ │
│ [+ 단계 추가] │
│ │
│ 1. 역할: [작성 ▼] │
│ 부서: [___________] │
│ 이름: [___________] │
│ │
│ 2. 역할: [검토 ▼] │
│ 부서: [___________] │
│ 이름: [___________] │
│ │
│ 3. 역할: [승인 ▼] │
│ 부서: [___________] │
│ 이름: [___________] │
│ │
│ ☐ 직인 표시 │
│ 스타일: [가로형 ▼] │
└─────────────────────────────┘
```
#### 3-4. 문서 생성 시 결재 연동
```
블록 스키마 → approval_line 블록 추출
DocumentApproval 레코드 자동 생성
기존 결재 워크플로우 (submit → approve → reject) 그대로 활용
```
**산출물:**
| 파일 | 작업 |
|------|------|
| `block-editor.blade.php` | approval_line 블록 추가 |
| `block-canvas.blade.php` | 결재선 렌더링 |
| `BlockRendererService.php` | 결재선 view/edit/print 렌더 |
| `DocumentService.php` | 블록 결재선 → DocumentApproval 변환 |
---
### Phase 4: 동적 테이블 블록 + 변수 시스템
> **목표**: 문서 작성 시 행 추가/삭제 가능한 테이블 + 자동 값 주입
#### 4-1. dynamic_table 블록 타입
기존 `table` 블록은 정적 (양식 설계 시 행 고정). `dynamic_table`은 문서 작성 시 행 동적 추가.
**스키마:**
```json
{
"type": "dynamic_table",
"props": {
"label": "검사 데이터",
"columns": [
{ "key": "col_item", "label": "항목", "type": "text", "width": 120 },
{ "key": "col_standard", "label": "기준값", "type": "text", "width": 100 },
{ "key": "col_measured", "label": "측정값", "type": "number", "width": 100 },
{ "key": "col_result", "label": "판정", "type": "select",
"options": ["합격", "불합격", "보류"], "width": 80 }
],
"minRows": 1,
"maxRows": 50,
"initialRows": 3,
"showRowNumber": true,
"binding": "inspection_data"
}
}
```
#### 4-2. EAV 데이터 매핑
```
dynamic_table 블록 데이터 저장:
document_data 레코드:
section_id = (dynamic_table 블록 ID → section 매핑)
column_id = (columns[].key → column 매핑)
row_index = 0, 1, 2, ...
field_key = "col_item", "col_standard", ...
field_value = 입력값
```
#### 4-3. 변수/매크로 시스템
**내장 변수:**
| 변수 | 값 | 설명 |
|------|-----|------|
| `{{today}}` | 현재 날짜 | YYYY-MM-DD |
| `{{now}}` | 현재 시각 | YYYY-MM-DD HH:mm |
| `{{user.name}}` | 로그인 사용자명 | |
| `{{user.department}}` | 로그인 사용자 부서 | |
| `{{doc.number}}` | 문서 번호 | 자동채번 |
| `{{doc.title}}` | 문서 제목 | |
| `{{template.company}}` | 서식 회사명 | |
**연결 데이터 변수 (linked data):**
| 변수 | 설명 |
|------|------|
| `{{item.name}}` | 연결 품목명 |
| `{{item.code}}` | 연결 품목 코드 |
| `{{order.number}}` | 연결 작업지시서 번호 |
| `{{order.quantity}}` | 연결 수량 |
**변수 사용 예시 (블록 속성):**
```json
{
"type": "text_field",
"props": {
"label": "검사일자",
"binding": "bf_inspection_date",
"default": "{{today}}"
}
}
```
```json
{
"type": "paragraph",
"props": {
"text": "작성자: {{user.name}} ({{user.department}})"
}
}
```
#### 4-4. 변수 해석 엔진
```php
// VariableResolver
class VariableResolver
{
public function resolve(string $text, array $context): string
{
return preg_replace_callback('/\{\{(\w+(?:\.\w+)*)\}\}/', function ($m) use ($context) {
return data_get($context, $m[1], $m[0]);
}, $text);
}
public function buildContext(Document $document, ?User $user = null): array
{
return [
'today' => now()->format('Y-m-d'),
'now' => now()->format('Y-m-d H:i'),
'user' => [
'name' => $user?->name,
'department' => $user?->department?->name,
],
'doc' => [
'number' => $document->document_number,
'title' => $document->title,
],
'item' => $this->resolveLinkedItem($document),
'order' => $this->resolveLinkedOrder($document),
];
}
}
```
**산출물:**
| 파일 | 작업 |
|------|------|
| `block-editor.blade.php` | dynamic_table 블록 추가 |
| `BlockRendererService.php` | 동적 테이블 렌더링 (edit: 행 추가/삭제 UI) |
| `mng/app/Services/VariableResolver.php` | 신규 생성 |
| `DocumentService.php` | 동적 테이블 EAV 저장/로드 |
---
### Phase 5: 고급 블록 + 조건부 로직
> **목표**: 수식 계산, 조건부 표시, 이미지 블록 등 고급 기능
#### 5-1. 수식 블록 (formula)
```json
{
"type": "formula_field",
"props": {
"label": "합계",
"expression": "SUM(inspection_data.col_measured)",
"format": "number",
"decimal": 2
}
}
```
**지원 함수:**
| 함수 | 설명 | 예시 |
|------|------|------|
| `SUM()` | 합계 | `SUM(table.col_amount)` |
| `AVG()` | 평균 | `AVG(table.col_measured)` |
| `COUNT()` | 개수 | `COUNT(table.col_item)` |
| `MIN()` / `MAX()` | 최솟값/최댓값 | `MIN(table.col_value)` |
| `IF()` | 조건 | `IF(AVG(table.col_measured) > 5, "합격", "불합격")` |
| `ROUND()` | 반올림 | `ROUND(AVG(table.col_measured), 2)` |
#### 5-2. 조건부 표시 (conditional visibility)
모든 블록에 `visibility` 속성 추가:
```json
{
"type": "paragraph",
"props": {
"text": "불합격 사유를 기재해 주세요.",
"visibility": {
"condition": "field",
"field": "bf_judgement",
"operator": "equals",
"value": "불합격"
}
}
}
```
**연산자:**
| 연산자 | 설명 |
|--------|------|
| `equals` | 같으면 표시 |
| `not_equals` | 다르면 표시 |
| `contains` | 포함하면 표시 |
| `greater_than` | 크면 표시 |
| `less_than` | 작으면 표시 |
| `is_empty` | 비어있으면 표시 |
| `is_not_empty` | 비어있지 않으면 표시 |
#### 5-3. 이미지 블록
```json
{
"type": "image",
"props": {
"label": "검사 사진",
"source": "upload",
"maxSize": 10,
"accept": ["jpeg", "png", "webp"],
"width": "100%",
"align": "center"
}
}
```
#### 5-4. Columns 내부 블록 (중첩 렌더링)
```json
{
"type": "columns",
"props": {
"count": 2,
"ratio": "1:1",
"children": [
[
{ "type": "text_field", "props": { "label": "품명" } },
{ "type": "date_field", "props": { "label": "검사일" } }
],
[
{ "type": "text_field", "props": { "label": "LOT NO" } },
{ "type": "select_field", "props": { "label": "판정", "options": ["합격","불합격"] } }
]
]
}
}
```
**산출물:**
| 파일 | 작업 |
|------|------|
| `block-editor.blade.php` | formula, image, conditional 블록 추가 |
| `mng/app/Services/FormulaEngine.php` | 수식 해석 엔진 |
| `BlockRendererService.php` | 조건부 표시, 수식 계산 렌더링 |
---
### Phase 6: 인쇄/PDF + Legacy 대체
> **목표**: 블록 문서 인쇄 완성, Legacy Builder 완전 대체
#### 6-1. 인쇄 레이아웃
```
print 모드 렌더링:
- 페이지 설정 (A4/A3) 적용
- 여백 적용
- 폼 필드 → 값 표시 (입력란 제거)
- 서명 → 서명 이미지 표시
- 결재선 → 직인 표시
- 페이지 넘김 (page-break) 자동 계산
```
#### 6-2. PDF 내보내기
```
블록 렌더러 (print 모드 HTML)
Puppeteer / wkhtmltopdf
PDF 파일 생성
다운로드 또는 첨부
```
#### 6-3. Legacy → Block 마이그레이션 도구
기존 Legacy 서식을 Block 스키마로 자동 변환:
```php
// LegacyToBlockMigrator
class LegacyToBlockMigrator
{
public function convert(DocumentTemplate $legacy): array
{
$blocks = [];
// 1. 결재선 → approval_line 블록
if ($legacy->approvalLines->isNotEmpty()) {
$blocks[] = $this->convertApprovalLines($legacy->approvalLines);
}
// 2. 기본필드 → text_field / date_field 블록
foreach ($legacy->basicFields as $field) {
$blocks[] = $this->convertBasicField($field);
}
// 3. 섹션 → heading + image 블록
foreach ($legacy->sections as $section) {
$blocks[] = ['type' => 'heading', 'props' => ['text' => $section->title]];
if ($section->image_path) {
$blocks[] = ['type' => 'image', 'props' => ['source' => $section->image_path]];
}
}
// 4. 컬럼 → dynamic_table 블록
if ($legacy->columns->isNotEmpty()) {
$blocks[] = $this->convertColumns($legacy->columns);
}
return [
'version' => '1.0',
'page' => ['size' => 'A4', 'orientation' => 'portrait'],
'blocks' => $blocks,
];
}
}
```
#### 6-4. Legacy Builder 비활성화
```
Phase 6 완료 후:
- 새 양식 생성: 양식 디자이너만 허용
- 기존 Legacy 서식: 조회/편집 가능 (변환 유도)
- Legacy Builder "새 양식" 버튼: "양식 디자이너" 사용 안내
```
**산출물:**
| 파일 | 작업 |
|------|------|
| `mng/resources/views/documents/print-block.blade.php` | 인쇄 전용 뷰 |
| `mng/app/Services/LegacyToBlockMigrator.php` | 변환 도구 |
| `mng/app/Services/PdfExportService.php` | PDF 생성 |
---
## 4. Phase별 우선순위 및 의존관계
```
Phase 2: 블록 런타임 렌더러 ──────────────────────┐
(렌더러 엔진, 데이터 바인딩, 문서 편집 통합) │
Phase 3: 결재선 블록 ─────────────────────┐ │
(approval_line 블록, 결재 워크플로우) │ │
↓ ↓
Phase 4: 동적 테이블 + 변수 ──────→ Phase 5: 고급 블록
(dynamic_table, 매크로) (수식, 조건부, 이미지)
Phase 6: 인쇄/PDF + Legacy 대체
(마이그레이션 도구)
```
| Phase | 의존 | 난이도 | 예상 범위 |
|-------|------|--------|----------|
| **Phase 2** | 없음 (기반) | 높음 | 렌더러 엔진 + EAV 매핑 |
| **Phase 3** | Phase 2 | 중간 | 결재선 블록 + 워크플로우 연동 |
| **Phase 4** | Phase 2 | 높음 | 동적 테이블 + 변수 해석 |
| **Phase 5** | Phase 4 | 높음 | 수식 엔진 + 조건부 로직 |
| **Phase 6** | Phase 3~5 | 중간 | 인쇄 + 마이그레이션 |
---
## 5. 스키마 버전 관리
### 5.1 버전 규칙
| 버전 | Phase | 변경 내용 |
|------|-------|----------|
| `1.0` | Phase 1 (현재) | 기본 13개 블록 |
| `2.0` | Phase 2~3 | 데이터 바인딩, approval_line 추가 |
| `3.0` | Phase 4 | dynamic_table, 변수 시스템 |
| `4.0` | Phase 5 | formula, conditional, image |
### 5.2 하위 호환
```json
{
"version": "3.0",
"page": { ... },
"blocks": [ ... ],
"variables": { ... },
"migrations": {
"from_1.0": "auto"
}
}
```
- 이전 버전 스키마 자동 인식 및 업그레이드
- 신규 블록 타입은 이전 버전에서 무시 (graceful degradation)
---
## 6. 신규 블록 타입 전체 목록
### Phase별 블록 추가 계획
| Phase | 블록 타입 | 카테고리 | 설명 |
|-------|----------|---------|------|
| 1 (완료) | `heading` | 기본 | 제목 |
| 1 (완료) | `paragraph` | 기본 | 문단 |
| 1 (완료) | `table` | 기본 | 정적 테이블 |
| 1 (완료) | `columns` | 기본 | 다단 레이아웃 |
| 1 (완료) | `divider` | 기본 | 구분선 |
| 1 (완료) | `spacer` | 기본 | 여백 |
| 1 (완료) | `text_field` | 폼 | 텍스트 입력 |
| 1 (완료) | `number_field` | 폼 | 숫자 입력 |
| 1 (완료) | `date_field` | 폼 | 날짜 입력 |
| 1 (완료) | `select_field` | 폼 | 드롭다운 |
| 1 (완료) | `checkbox_field` | 폼 | 체크박스 |
| 1 (완료) | `textarea_field` | 폼 | 장문 텍스트 |
| 1 (완료) | `signature_field` | 폼 | 서명 |
| **3** | `approval_line` | 워크플로우 | 결재선 |
| **4** | `dynamic_table` | 데이터 | 동적 행 테이블 |
| **5** | `formula_field` | 데이터 | 수식 계산 |
| **5** | `image` | 미디어 | 이미지 업로드/표시 |
---
## 7. 기술 스택 정리
| 구성 요소 | 기술 | 비고 |
|----------|------|------|
| 블록 에디터 UI | Alpine.js + Blade | 기존 유지 |
| 드래그-앤-드롭 | SortableJS | 기존 유지 |
| 블록 렌더러 | PHP (BlockRendererService) | 신규 |
| 변수 해석 | PHP (VariableResolver) | 신규 |
| 수식 엔진 | PHP (FormulaEngine) | 신규 |
| 데이터 저장 | EAV (document_data) | 기존 테이블 활용 |
| 결재 워크플로우 | DocumentApproval | 기존 로직 활용 |
| 인쇄 | CSS @media print | 신규 |
| PDF | Puppeteer 또는 wkhtmltopdf | 신규 |
---
## 8. 위험 요소 및 대응
| 위험 | 영향 | 대응 |
|------|------|------|
| EAV 매핑 복잡도 | 블록 ID ↔ section_id 매핑 불일치 | 블록 ID를 section 대용으로 사용, 매핑 테이블 추가 검토 |
| Legacy 데이터 호환 | 기존 문서 데이터 접근 불가 | Legacy 서식 문서는 기존 방식 유지, 신규 서식만 블록 적용 |
| 수식 엔진 보안 | 임의 코드 실행 위험 | 화이트리스트 함수만 허용, eval 사용 금지 |
| 인쇄 레이아웃 | 브라우저별 차이 | CSS @page 규격 준수, PDF 변환 권장 |
| 스키마 마이그레이션 | 버전 업그레이드 시 데이터 손실 | 하위 호환 보장, 자동 업그레이드 로직 |
---
## 9. 성공 기준
| 기준 | 측정 방법 |
|------|----------|
| 블록 서식으로 문서 생성 가능 | Phase 2 완료 후 테스트 |
| 결재 워크플로우 정상 동작 | Phase 3 완료 후 테스트 |
| 동적 행 추가/삭제 | Phase 4 완료 후 테스트 |
| 변수 자동 주입 | Phase 4 완료 후 테스트 |
| Legacy 서식 자동 변환 | Phase 6 완료 후 테스트 |
| 인쇄 품질 A4 기준 정상 | Phase 6 완료 후 테스트 |
---
## 관련 문서
- [문서양식관리](../features/documents/mng-document-template.md) — 현재 양식관리 기술문서
- [문서관리 시스템](../features/documents/mng-document-system.md) — 문서 생성/결재 기술문서
- [문서관리 API](../features/documents/README.md) — API 엔드포인트 목록
---
**최종 업데이트**: 2026-03-06

View File

@@ -0,0 +1,611 @@
# UI/UX 디자인 인사이트 연구 메뉴 기획서
> **작성일**: 2026-03-08
> **상태**: 기획 중
> **라우트**: `/rd/design-insight`
> **모티브**: 기획디자인 스토리보드 에디터 (`/rd/planning-design`)
---
## 1. 개요
### 1.1 배경
기획디자인 메뉴는 ERP 화면을 **설계(Output)**하는 도구다.
그런데 좋은 설계를 하려면 **연구(Input)**가 먼저 필요하다.
```
연구 (이 메뉴) 설계 (기획디자인)
┌─────────────────┐ ┌─────────────────┐
│ 레퍼런스 수집 │ │ 스토리보드 작성 │
│ 패턴 분석 │ ──→ │ 와이어프레임 설계 │
│ 인사이트 정리 │ │ HTML 내보내기 │
│ 디자인 원칙 학습 │ │ 인쇄 │
└─────────────────┘ └─────────────────┘
```
현재 SAM ERP 화면을 만들 때 참고할 디자인 패턴이나 인사이트를 체계적으로 관리하는 도구가 없다. 외부 서비스(Dribbble, Mobbin 등)를 참고하지만 **우리 ERP에 맞는 패턴**을 축적하는 곳이 없다.
### 1.2 목적
SAM ERP 화면 개발에 필요한 **UI/UX 디자인 인사이트를 수집·분석·축적**하는 연구 도구
### 1.3 핵심 가치
| 가치 | 설명 |
|------|------|
| **패턴 축적** | "이 화면은 왜 좋은가?" — 반복 사용할 패턴을 라이브러리화 |
| **Before/After** | 개선 전후를 비교하여 디자인 결정의 근거를 기록 |
| **팀 학습** | 디자인 인사이트를 팀원과 공유, 일관된 UI 품질 유지 |
| **빠른 참조** | 새 화면 설계 시 기존 패턴을 즉시 찾아 재사용 |
---
## 2. 기술 아키텍처
### 2.1 기획디자인과 동일한 패턴
기획디자인 메뉴의 성공 패턴을 그대로 적용한다.
| 항목 | 선택 | 이유 |
|------|------|------|
| 프레임워크 | Alpine.js 단일 파일 SPA | 서버 API 없이 즉시 사용, MNG 기존 스택 |
| 저장 | localStorage | 서버 의존성 제거, 즉시 사용 가능 |
| 뷰 파일 | `resources/views/rd/design-insight/index.blade.php` | 단일 파일 구조 |
| 컨트롤러 | `RdController@designInsight()` | 기존 R&D 컨트롤러 확장 |
| 이미지 | Base64 Data URL (localStorage) | 서버 업로드 불필요 |
### 2.2 라우트
```php
// routes/web.php — R&D 그룹 내 추가
Route::get('/rd/design-insight', [RdController::class, 'designInsight'])
->name('rd.design-insight');
```
### 2.3 localStorage 키
| 키 | 용도 |
|----|------|
| `di_projects` | 연구 프로젝트 목록 (메인 저장소) |
| `di_current` | 현재 프로젝트 ID |
| `di_patterns` | 디자인 패턴 라이브러리 (프로젝트 간 공유) |
---
## 3. 화면 구조
### 3.1 전체 레이아웃
```
┌──────────────────────────────────────────────────────────────┐
│ 툴바: [프로젝트명] [저장] [내보내기] [뷰: 보드│리스트│갤러리] │
├──────────────────────────────────────────────────────────────┤
│ 카테고리 탭: 전체 │ 레퍼런스 │ 분석 │ 패턴 │ Before/After │
├────────┬─────────────────────────────────────────────────────┤
│ │ │
│ 사이드 │ 메인 콘텐츠 영역 │
│ 바 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ ◆ 프로 │ │ 인사이트 │ │ 인사이트 │ │ 인사이트 │ │
│ 젝트 │ │ 카드 1 │ │ 카드 2 │ │ 카드 3 │ │
│ 목록 │ │ │ │ │ │ │ │
│ │ │ 🏷️태그 │ │ 🏷️태그 │ │ 🏷️태그 │ │
│ ◆ 태그 │ └─────────┘ └─────────┘ └─────────┘ │
│ 필터 │ │
│ │ ┌─────────┐ ┌─────────┐ │
│ ◆ 검색 │ │ 인사이트 │ │ + 새 카드 │ │
│ │ │ 카드 4 │ │ 추가 │ │
│ │ └─────────┘ └─────────┘ │
│ │ │
├────────┴─────────────────────────────────────────────────────┤
│ 상태바: 카드 12개 │ 패턴 5개 │ 태그 8개 │
└──────────────────────────────────────────────────────────────┘
```
### 3.2 뷰 모드 (3종)
| 뷰 | 설명 | 용도 |
|----|------|------|
| **보드 (Board)** | 칸반 스타일 카드 격자 배열 | 전체 현황 파악, 기본 뷰 |
| **리스트 (List)** | 테이블형 목록 (정렬/필터) | 대량 데이터 관리, 검색 |
| **갤러리 (Gallery)** | 이미지 중심 큰 썸네일 격자 | 시각적 비교, 레퍼런스 브라우징 |
---
## 4. 인사이트 카드 (핵심 데이터 단위)
### 4.1 카드 유형 (4종)
#### A. 레퍼런스 카드 (Reference)
외부/내부 화면 스크린샷을 수집하고 메모를 남긴다.
```
┌──────────────────────────────┐
│ 📷 [스크린샷 이미지] │
│ │
├──────────────────────────────┤
│ 📌 Notion 대시보드 │
│ "카드형 레이아웃이 정보 밀도를 │
│ 유지하면서도 시각적으로 깔끔" │
├──────────────────────────────┤
│ 출처: notion.so │
│ 🏷️ 대시보드 카드 레이아웃 │
│ ⭐⭐⭐⭐☆ │
└──────────────────────────────┘
```
| 필드 | 타입 | 설명 |
|------|------|------|
| `image` | string (Base64) | 스크린샷 이미지 |
| `title` | string | 제목 |
| `memo` | string | 인사이트 메모 (왜 좋은가/나쁜가) |
| `source` | string | 출처 (URL, 앱 이름 등) |
| `tags` | string[] | 태그 배열 |
| `rating` | number (1-5) | 평점 |
| `category` | string | 화면 카테고리 |
#### B. 분석 카드 (Analysis)
화면을 분석하고 디자인 원칙을 체크한다.
```
┌──────────────────────────────┐
│ 🔍 SAM 수주 목록 화면 분석 │
├──────────────────────────────┤
│ [스크린샷 + 어노테이션 오버레이]│
│ ①→ 검색 영역 너무 넓음 │
│ ②→ 버튼 정렬 불일치 │
│ ③→ 여백 불균형 │
├──────────────────────────────┤
│ ✅ 정렬 (Alignment) │
│ ❌ 대비 (Contrast) │
│ ✅ 반복 (Repetition) │
│ ⚠️ 근접성 (Proximity) │
├──────────────────────────────┤
│ 개선 제안: │
│ "검색 영역을 접을 수 있게 하고 │
│ 버튼 그룹을 우측 정렬" │
│ 🏷️ 목록화면 개선필요 │
└──────────────────────────────┘
```
| 필드 | 타입 | 설명 |
|------|------|------|
| `image` | string (Base64) | 분석 대상 스크린샷 |
| `annotations` | Annotation[] | 어노테이션 배열 (마커 번호, 좌표, 텍스트) |
| `principles` | object | CRAP 원칙 체크 (contrast, repetition, alignment, proximity) |
| `suggestion` | string | 개선 제안 |
| `severity` | string | 심각도 (info, warning, critical) |
#### C. 패턴 카드 (Pattern)
반복 사용할 UI 패턴을 템플릿으로 등록한다.
```
┌──────────────────────────────┐
│ 📐 검색 + 필터 + 목록 패턴 │
├──────────────────────────────┤
│ [패턴 와이어프레임 이미지] │
├──────────────────────────────┤
│ 사용처: │
│ • 수주 목록 │
│ • 거래처 목록 │
│ • 품목 목록 │
├──────────────────────────────┤
│ 구성 요소: │
│ ☑ 검색바 (상단 고정) │
│ ☑ 필터 칩 (접기/펼치기) │
│ ☑ 테이블 (정렬 가능) │
│ ☑ 페이지네이션 (하단) │
│ ☑ 액션 버튼 (우상단) │
├──────────────────────────────┤
│ 🏷️ 목록 CRUD 테이블 │
│ 📊 사용빈도: ★★★★★ (12회) │
└──────────────────────────────┘
```
| 필드 | 타입 | 설명 |
|------|------|------|
| `image` | string (Base64) | 패턴 와이어프레임 |
| `usedIn` | string[] | 사용처 목록 |
| `components` | Component[] | 구성 요소 체크리스트 |
| `guidelines` | string | 사용 가이드라인 |
| `frequency` | number | 사용 빈도 |
#### D. Before/After 카드 (Comparison)
디자인 개선 전후를 비교한다.
```
┌──────────────────────────────────────────┐
│ 🔄 거래처 상세 화면 리뉴얼 │
├───────────────────┬──────────────────────┤
│ ❌ Before │ ✅ After │
│ [이전 스크린샷] │ [개선 스크린샷] │
│ │ │
├───────────────────┴──────────────────────┤
│ 변경 포인트: │
│ 1. 탭 구조 → 섹션 접기/펼치기 변경 │
│ 2. 좌우 2컬럼 → 단일 컬럼 (모바일 대응) │
│ 3. 저장 버튼 하단 고정 → 상단 sticky │
├──────────────────────────────────────────┤
│ 효과: 스크롤 40% 감소, 작업 완료 시간 단축 │
│ 🏷️ 상세화면 폼 리뉴얼 │
└──────────────────────────────────────────┘
```
| 필드 | 타입 | 설명 |
|------|------|------|
| `beforeImage` | string (Base64) | 개선 전 스크린샷 |
| `afterImage` | string (Base64) | 개선 후 스크린샷 |
| `changes` | string[] | 변경 포인트 목록 |
| `effect` | string | 개선 효과 |
### 4.2 공통 필드
모든 카드 유형이 공유하는 기본 필드:
```json
{
"id": "di_1709856000000_abc",
"type": "reference",
"title": "카드 제목",
"createdAt": "2026-03-08T10:00:00",
"updatedAt": "2026-03-08T15:30:00",
"tags": ["대시보드", "카드", "레이아웃"],
"category": "dashboard",
"pinned": false,
"archived": false
}
```
### 4.3 카테고리 (화면 유형별)
| 카테고리 | 코드 | 설명 |
|---------|------|------|
| 대시보드 | `dashboard` | 통계, KPI, 차트 화면 |
| 목록 | `list` | 테이블, 검색, 필터 화면 |
| 상세/폼 | `form` | 입력, 편집, 상세 보기 |
| 모달/팝업 | `modal` | 모달 다이얼로그, 확인창 |
| 네비게이션 | `navigation` | 사이드바, 탭, 메뉴 |
| 로그인/온보딩 | `auth` | 인증, 초기 설정 |
| 보고서/인쇄 | `report` | 인쇄용, PDF 출력 화면 |
| 기타 | `etc` | 분류 불가 |
---
## 5. 기능 상세
### 5.1 이미지 수집
| 기능 | 설명 |
|------|------|
| 파일 업로드 | 이미지 파일 선택 (PNG, JPG, GIF) |
| 클립보드 붙여넣기 | `Ctrl+V`로 스크린샷 즉시 붙여넣기 |
| 드래그 앤 드롭 | 이미지 파일을 카드 영역에 드롭 |
> **Ctrl+V 붙여넣기가 핵심** — 스크린샷 캡처 후 즉시 카드 생성이 워크플로우의 핵심
### 5.2 어노테이션 (분석 카드)
분석 카드에서 이미지 위에 마커를 추가하여 문제점이나 인사이트를 표시한다.
| 어노테이션 유형 | 설명 |
|---------------|------|
| 번호 마커 (①②③) | 이미지 위 클릭 → 번호 자동 증가, 하단 설명과 연동 |
| 영역 하이라이트 | 드래그로 사각형 영역 표시 (반투명 컬러 오버레이) |
| 텍스트 메모 | 이미지 위 임의 위치에 짧은 메모 |
> 기획디자인의 번호 마커(marker 블록) + Description 패널 패턴을 재활용
### 5.3 태그 시스템
| 기능 | 설명 |
|------|------|
| 자유 태그 | 카드에 자유 태그 추가 (콤마 구분 입력) |
| 태그 자동 완성 | 기존 태그 목록에서 자동 완성 |
| 태그 필터 | 사이드바에서 태그 클릭 → 해당 태그 카드만 표시 |
| 태그 색상 | 카테고리별 자동 색상 배정 |
### 5.4 CRAP 디자인 원칙 체크리스트
분석 카드에서 사용하는 디자인 원칙 평가:
| 원칙 | 체크 항목 |
|------|----------|
| **C**ontrast (대비) | 중요 요소가 시각적으로 구분되는가? |
| **R**epetition (반복) | 일관된 스타일이 반복 적용되는가? |
| **A**lignment (정렬) | 요소들이 논리적으로 정렬되어 있는가? |
| **P**roximity (근접성) | 관련 요소가 가까이 그룹핑되어 있는가? |
추가 체크:
| 원칙 | 체크 항목 |
|------|----------|
| 여백 (Whitespace) | 적절한 여백이 확보되어 있는가? |
| 계층 (Hierarchy) | 정보의 우선순위가 시각적으로 드러나는가? |
| 일관성 (Consistency) | 다른 화면과 일관된 패턴을 따르는가? |
| 접근성 (Accessibility) | 색상 대비, 폰트 크기가 충분한가? |
### 5.5 검색 & 필터
| 기능 | 설명 |
|------|------|
| 텍스트 검색 | 제목, 메모, 태그에서 전문 검색 |
| 카테고리 필터 | 화면 유형별 필터 (탭) |
| 카드 유형 필터 | 레퍼런스 / 분석 / 패턴 / Before/After |
| 평점 필터 | ⭐ 3점 이상만 표시 등 |
| 정렬 | 최신순, 평점순, 이름순 |
### 5.6 내보내기
| 형식 | 설명 |
|------|------|
| JSON | 전체 프로젝트 데이터 백업/복원 |
| HTML | 인사이트 카드를 HTML 보고서로 출력 (인쇄 가능) |
| 패턴 → 기획디자인 | 패턴 카드의 와이어프레임을 기획디자인 템플릿으로 전송 |
### 5.7 키보드 단축키
| 단축키 | 기능 |
|--------|------|
| `Ctrl+V` | 클립보드 이미지로 새 카드 생성 |
| `Ctrl+S` | 프로젝트 저장 |
| `Ctrl+F` | 검색 포커스 |
| `Ctrl+N` | 새 카드 추가 |
| `Delete` | 선택 카드 삭제 |
| `Ctrl+Z` | 실행 취소 |
| `Ctrl+Y` | 다시 실행 |
---
## 6. 데이터 구조
### 6.1 프로젝트 (localStorage: `di_projects`)
```json
[
{
"id": "diproj_1709856000000",
"title": "SAM ERP v2 디자인 연구",
"description": "SAM ERP 화면 개선을 위한 UI/UX 인사이트 수집",
"cards": [],
"createdAt": "2026-03-08T10:00:00",
"updatedAt": "2026-03-08T15:30:00"
}
]
```
### 6.2 인사이트 카드 (cards 배열 내)
```json
{
"id": "di_1709856000000_abc",
"type": "reference",
"title": "Notion 대시보드 카드 레이아웃",
"image": "data:image/png;base64,...",
"memo": "카드형 레이아웃이 정보 밀도를 유지하면서도 시각적으로 깔끔",
"source": "notion.so",
"tags": ["대시보드", "카드", "레이아웃"],
"category": "dashboard",
"rating": 4,
"pinned": false,
"archived": false,
"createdAt": "2026-03-08T10:00:00",
"updatedAt": "2026-03-08T15:30:00"
}
```
### 6.3 분석 어노테이션
```json
{
"annotations": [
{
"id": "ann_001",
"type": "marker",
"num": 1,
"x": 150,
"y": 80,
"text": "검색 영역 너무 넓음 — 접기 기능 필요"
},
{
"id": "ann_002",
"type": "highlight",
"x": 200,
"y": 300,
"w": 150,
"h": 40,
"color": "rgba(239,68,68,0.3)",
"text": "버튼 정렬 불일치"
}
]
}
```
### 6.4 디자인 패턴 라이브러리 (localStorage: `di_patterns`)
프로젝트 간 공유되는 패턴 라이브러리:
```json
[
{
"id": "pat_001",
"name": "검색 + 필터 + 목록",
"image": "data:image/png;base64,...",
"components": [
{ "name": "검색바", "required": true },
{ "name": "필터 칩", "required": false },
{ "name": "데이터 테이블", "required": true },
{ "name": "페이지네이션", "required": true },
{ "name": "액션 버튼", "required": true }
],
"guidelines": "검색바는 상단 고정, 필터는 접기/펼치기 지원",
"usedIn": ["수주 목록", "거래처 목록", "품목 목록"],
"tags": ["목록", "CRUD", "테이블"],
"frequency": 12,
"createdAt": "2026-03-08T10:00:00"
}
]
```
---
## 7. 프리셋 데이터
### 7.1 기본 카테고리 (하드코딩)
```javascript
categories: [
{ code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' },
{ code: 'list', label: '목록', icon: '📋', color: '#3b82f6' },
{ code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' },
{ code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' },
{ code: 'navigation',label: '네비게이션',icon: '🧭', color: '#8b5cf6' },
{ code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' },
{ code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' },
{ code: 'etc', label: '기타', icon: '📎', color: '#64748b' },
]
```
### 7.2 CRAP 원칙 체크리스트 (하드코딩)
```javascript
designPrinciples: [
{ key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' },
{ key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' },
{ key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' },
{ key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' },
{ key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' },
{ key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' },
{ key: 'consistency', label: '일관성 (Consistency)',icon: '🔗', desc: '다른 화면과의 일관성' },
{ key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' },
]
```
### 7.3 샘플 패턴 템플릿 (프리셋)
| 패턴명 | 구성 요소 | SAM 내 사용처 |
|--------|----------|--------------|
| 검색 + 목록 | 검색바, 필터, 테이블, 페이지네이션, 액션버튼 | 수주/거래처/품목 목록 |
| 상세 폼 | 섹션 헤더, 라벨+입력, 저장/취소 버튼 | 수주 상세, 거래처 상세 |
| 대시보드 | 통계 카드 4개, 차트 2개, 요약 테이블 | 메인 대시보드 |
| 탭 레이아웃 | 탭 메뉴, 탭 콘텐츠, 액션 버튼 | 설정, 품목기준관리 |
| 트리 + 상세 | 좌측 트리, 우측 상세 패널 | 메뉴 관리, 조직도 |
| 모달 폼 | 모달 헤더, 입력 필드, 확인/취소 | 등록/수정 팝업 |
| 칸반 보드 | 컬럼 헤더, 드래그 카드, 필터 | 업무 관리 |
| 캘린더 | 월/주/일 뷰, 이벤트 카드, 필터 | 일정 관리, 근태 |
---
## 8. 워크플로우
### 8.1 일반 사용 흐름
```
1. 새 연구 프로젝트 생성 ("SAM ERP v2 디자인 연구")
2. 레퍼런스 수집
• 외부 서비스 스크린샷 → Ctrl+V 붙여넣기
• SAM 기존 화면 스크린샷 → 파일 업로드
• 태그 + 카테고리 분류
3. 화면 분석
• 분석 카드 생성 → 어노테이션 추가
• CRAP 원칙 체크
• 개선 제안 작성
4. 패턴 추출
• 반복되는 좋은 패턴 → 패턴 카드로 등록
• 구성 요소 정리, 사용 가이드라인 작성
5. Before/After 기록
• 개선 전후 비교 카드 생성
• 변경 포인트 + 효과 기록
6. 기획디자인 연계
• 패턴 라이브러리에서 참고하며 스토리보드 작성
```
### 8.2 기획디자인 연계
```
디자인 인사이트 기획디자인
┌──────────────┐ ┌──────────────┐
│ 패턴 카드: │ │ 스토리보드: │
│ "검색+목록" │──참조──→│ 새 페이지에 │
│ 구성요소 체크 │ │ 패턴 적용 │
│ 가이드라인 │ │ │
└──────────────┘ └──────────────┘
```
> 향후 패턴 카드의 구성 요소를 기획디자인 블록 템플릿으로 자동 변환하는 연계 기능을 검토한다.
---
## 9. 개발 로드맵
### Phase 1 — 기본 구조 (MVP)
| 항목 | 내용 |
|------|------|
| 라우트 + 컨트롤러 | `GET /rd/design-insight` → 뷰 반환 |
| 프로젝트 CRUD | 생성/저장/로드/삭제 (localStorage) |
| 레퍼런스 카드 | 이미지 업로드 + 메모 + 태그 + 카테고리 |
| 보드 뷰 | 카드 격자 배열 기본 화면 |
| 검색/필터 | 텍스트 검색, 카테고리 탭 필터 |
| Ctrl+V 붙여넣기 | 클립보드 이미지 → 새 카드 자동 생성 |
### Phase 2 — 분석 도구
| 항목 | 내용 |
|------|------|
| 분석 카드 | 어노테이션 시스템 (마커, 하이라이트) |
| CRAP 체크리스트 | 디자인 원칙 체크 UI |
| Before/After 카드 | 전후 비교 카드 유형 |
| 갤러리 뷰 | 이미지 중심 큰 썸네일 |
| 리스트 뷰 | 테이블형 정렬/필터 |
### Phase 3 — 패턴 라이브러리
| 항목 | 내용 |
|------|------|
| 패턴 카드 | 구성 요소 체크리스트, 가이드라인 |
| 패턴 프리셋 | SAM ERP 기본 패턴 8종 |
| 패턴 공유 | 프로젝트 간 패턴 공유 (di_patterns) |
| 내보내기 | JSON 백업, HTML 보고서 |
### Phase 4 — 연계 & 고도화
| 항목 | 내용 |
|------|------|
| 기획디자인 연계 | 패턴 → 블록 템플릿 변환 |
| DB 저장 전환 | localStorage → DB (협업 지원) |
| 팀 공유 | 다른 사용자와 인사이트 공유 |
---
## 10. 파일 구조 (예상)
```
mng/
├── app/Http/Controllers/
│ └── RdController.php ← designInsight() 메서드 추가
├── resources/views/rd/design-insight/
│ └── index.blade.php ← 전체 CSS + HTML + Alpine.js
└── routes/web.php ← Route 추가
```
---
## 11. 관련 문서
- [기획디자인 기술 스펙](../features/rd/planning-design.md) — 모티브가 된 스토리보드 에디터
- [기획디자인 프로젝트](../projects/planning-design/README.md) — 프로젝트 이력
- [R&D 메뉴 개요](../features/rd/README.md) — R&D 전체 메뉴 구조
---
**최종 업데이트**: 2026-03-08

View File

@@ -0,0 +1,753 @@
# 방화셔터 도면생성 기능 기획서
> **작성일**: 2026-03-08
> **상태**: 기획 초안
> **위치**: MNG > R&D > 방화셔터 도면생성
> **라우트**: `GET /rd/fire-shutter-drawing`
> **참고**: 기존 `자동도면 생성` (`/rd/auto-drawing`) 구조를 확장
---
## 1. 개요
### 1.1 목적
방화셔터의 **가이드레일 단면**과 **셔터박스(케이스) 형태**를 파라미터로 입력하면, **2D 단면도(SVG)**와 **3D 렌더링(Three.js)**을 실시간으로 생성하는 도구를 제공한다.
### 1.2 핵심 가치
| 기존 (수동) | 개선 (SAM 도면생성) |
|-------------|-------------------|
| CAD 프로그램에서 수동 작도 | 파라미터 입력 → 자동 도면 생성 |
| 도면 수정 시 전체 재작업 | 치수 변경 → 실시간 미리보기 |
| 제품별 도면 관리 어려움 | 프리셋 저장/불러오기로 재활용 |
| 영업/설치팀 도면 요청 대기 | 현장에서 즉시 단면도 확인 가능 |
### 1.3 대상 사용자
- 설계팀: 방화셔터 단면 설계 및 검토
- 영업팀: 고객 제안 시 단면도/3D 이미지 첨부
- 설치팀: 현장 설치 전 가이드레일/케이스 형태 확인
- 생산팀: 절곡/제작 사양 확인
---
## 2. 방화셔터 핵심 구조
### 2.1 전체 구성도
```
┌─────────────────── 천장 슬래브 ───────────────────┐
│ │
│ ┌──────────── 셔터박스 (HEAD BOX / CASE) ──────┐ │
│ │ ┌─────┐ ┌─────┐ │ │
│ │ │브래킷│ [샤프트+슬랫 감김] │브래킷│ │ │
│ │ └──┬──┘ [모터+감속기+브레이크] └──┬──┘ │ │
│ │ │ [밸런스 스프링] │ │ │
│ └─────┼─────────────────────────────────┼──────┘ │
│ │ │ │
│ ┌─────┴─────┐ ┌──────┴─────┐ │
│ │ 가이드레일 │ ← 슬랫 커튼 → │ 가이드레일 │ │
│ │ (좌) │ (강판/스크린) │ (우) │ │
│ │ │ │ │ │
│ │ 연기차단재│ │연기차단재 │ │
│ │ │ │ │ │
│ └─────┬─────┘ └──────┬─────┘ │
│ │ │ │
│ ══════╧═══ 하장바 (BOTTOM BAR) ═════════╧═══════ │
│ [고무 실링] │
└────────────────── 바닥 ──────────────────────────┘
```
### 2.2 주요 구성요소 상세
#### A. 가이드레일 (Guide Rail)
- **형태**: C-채널 단면 (ㄷ자 형태)
- **재질**: 강판 2.3mm 이상
- **기능**: 슬랫 커튼의 좌우 안내 + 연기 차단
- **표준 길이**: 2,438mm / 3,305mm / 4,430mm (조합 사용)
- **수량**: 항상 **2개** (좌우 1쌍)
- **부속**: 연기차단재(Smoke Seal Packing), 앵커볼트
```
가이드레일 단면 (상단에서 본 모습)
┌────────────┐
│ │ ← 가이드레일 본체 (C-채널)
│ ┌──────┐ │
│ │ 연기 │ │
│ │ 차단 │ │
│ │ 재 │ │
│ │ │ │
│ │슬랫 │ │
│ │엣지→ ● │
│ │ │ │
│ │ 연기 │ │
│ │ 차단 │ │
│ │ 재 │ │
│ └──────┘ │
│ │
└────────────┘
■■■■■■■■■■■■■ ← 방화벽
```
**파라미터**:
| 파라미터 | 설명 | 단위 | 기본값 |
|---------|------|------|--------|
| `rail_width` | 레일 전체 폭 | mm | 65 |
| `rail_depth` | 레일 깊이 (채널 깊이) | mm | 50 |
| `rail_thickness` | 강판 두께 | mm | 2.3 |
| `rail_lip` | 립(입구) 높이 | mm | 15 |
| `seal_thickness` | 연기차단재 두께 | mm | 5 |
| `seal_depth` | 연기차단재 깊이 | mm | 40 |
| `slat_thickness` | 슬랫 두께 (끼워지는 부분) | mm | 1.6 |
| `rail_height` | 레일 전체 높이 | mm | 3305 |
| `anchor_spacing` | 앵커볼트 간격 | mm | 500 |
#### B. 셔터박스 / 케이스 (Head Box / Case)
- **형태**: 직사각형 박스 (상부 천장 부착)
- **재질**: 강판 1.6mm 이상
- **기능**: 샤프트/모터/슬랫 감김 수납
- **표준 규격**: 1500×380mm / 500×380mm (개구부 크기에 따라)
```
셔터박스 단면 (정면에서 본 모습)
┌─────────────────────────────────────────┐ ← 상판
│ │
│ [브래킷] ┌──── 샤프트 ────┐ [브래킷] │
│ │ │ (슬랫 감김) │ │ │
│ ├──────┤ ├──────┤ │
│ │ │ ◎ 중심축 │ │ │
│ │ └───────────────┘ │ │
│ │ │ │
│ │ [모터+감속기] [브레이크] │ │
│ │ [밸런스 스프링] │ │
│ │
└───┬─────────────────────────────────┬───┘ ← 하판 (슬랫 출구)
│ ↓ 슬랫 하강 ↓ │
└─────────────────────────────────┘
```
**파라미터**:
| 파라미터 | 설명 | 단위 | 기본값 |
|---------|------|------|--------|
| `box_width` | 케이스 전체 폭 (= 개구부 폭 + 마진) | mm | 1500 |
| `box_height` | 케이스 높이 | mm | 380 |
| `box_depth` | 케이스 깊이 (전후) | mm | 380 |
| `box_thickness` | 케이스 강판 두께 | mm | 1.6 |
| `shaft_diameter` | 샤프트 직경 | mm | 120 |
| `shaft_offset_x` | 샤프트 중심 수평 오프셋 | mm | 0 |
| `shaft_offset_y` | 샤프트 중심 수직 오프셋 | mm | 0 |
| `motor_side` | 모터 위치 (좌/우) | - | 우 |
| `slat_exit_width` | 슬랫 출구 폭 | mm | 1400 |
| `bracket_width` | 브래킷 폭 | mm | 80 |
#### C. 슬랫 (Steel Slat / Screen)
- **강판형**: EGI 강판 1.6mm, C/S형 인터록킹 프로파일
- **스크린형**: 실리카/와이어 원단, 가이드레일 11mm 홈
- **피치**: 75~100mm
**파라미터**:
| 파라미터 | 설명 | 단위 | 기본값 |
|---------|------|------|--------|
| `slat_type` | 슬랫 유형 (강판/스크린) | - | 강판 |
| `slat_pitch` | 슬랫 피치 | mm | 80 |
| `slat_thickness` | 슬랫 두께 | mm | 1.6 |
| `slat_profile` | 단면 형태 (C형/S형) | - | C형 |
#### D. 하장바 (Bottom Bar)
- **기능**: 슬랫 커튼 하단 마감 + 바닥 밀착
- **부속**: 고무 실링
**파라미터**:
| 파라미터 | 설명 | 단위 | 기본값 |
|---------|------|------|--------|
| `bar_width` | 하장바 폭 | mm | 60 |
| `bar_height` | 하장바 높이 | mm | 40 |
| `bar_seal_height` | 고무 실링 높이 | mm | 15 |
---
## 3. 기능 설계
### 3.1 탭 구성
기존 자동도면 생성의 탭 구조를 참고하여 4개 탭으로 구성한다.
```
┌──────────┬──────────┬──────────┬──────────┐
│ 설정 │ 가이드 │ 셔터박스 │ 3D │
│ Settings │ 레일 │ (케이스) │ 렌더링 │
└──────────┴──────────┴──────────┴──────────┘
```
| 탭 | ID | 기능 |
|----|----|------|
| **설정** | `Settings` | 제품 유형 선택, 개구부 크기, 전역 설정 |
| **가이드레일** | `GuideRail` | 가이드레일 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 |
| **셔터박스** | `ShutterBox` | 셔터박스 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 |
| **3D 렌더링** | `3D` | 전체 방화셔터 조립체 3D 렌더링 (Three.js) |
### 3.2 설정 탭 (Settings)
#### 입력 항목
| 항목 | 타입 | 설명 |
|------|------|------|
| 제품 유형 | 드롭다운 | 강판형 (KFS) / 스크린형 (KSS) |
| 제품 모델 | 드롭다운 | KSS01, KSS02, KFS01 등 (유형 선택 시 필터링) |
| 개구부 폭 (W0) | 숫자 입력 | mm |
| 개구부 높이 (H0) | 숫자 입력 | mm |
| 수량 | 숫자 입력 | 기본값 1 |
#### 자동 계산 (표시 전용)
| 항목 | 수식 | 설명 |
|------|------|------|
| 제작 폭 (W1) | 스크린: W0+140 / 강판: W0+110 | 마진 포함 |
| 제작 높이 (H1) | H0+350 | 마진 포함 |
| 면적 (M) | W1 × H1 / 1,000,000 | m² |
| 중량 (K) | 스크린: M×2 / 강판: M×25 | kg |
| 권장 모터 | K 기준 자동 선택 | 150K~1500K |
#### 프리셋 관리
- **프리셋 저장**: 현재 파라미터를 이름 지정하여 localStorage에 저장
- **프리셋 불러오기**: 저장된 프리셋 목록에서 선택하여 파라미터 복원
- **기본 프리셋**: 강판형 기본, 스크린형 기본 (제품 유형 선택 시 자동 적용)
### 3.3 가이드레일 탭
#### UI 구성 (2컬럼 레이아웃)
```
┌──────────────────────┬──────────────────────────────────┐
│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │
│ │ │
│ ■ 레일 전체 폭: [65] │ │
│ ■ 레일 깊이: [50] │ ┌────────┐ │
│ ■ 강판 두께: [2.3] │ │ │ │
│ ■ 립 높이: [15] │ │ ┌────┐ │ ← SVG 실시간 │
│ ■ 연기차단재: [5/40] │ │ │ ● │ │ 렌더링 │
│ ■ 슬랫 두께: [1.6] │ │ └────┘ │ │
│ │ │ │ │
│ [치수 표시 ON/OFF] │ └────────┘ │
│ [연기차단재 ON/OFF] │ ← 치수 라벨 (mm) │
│ │ │
│ ■ 레일 높이: [3305] │ [줌 +] [줌 -] [리셋] [DXF 저장] │
│ ■ 앵커 간격: [500] │ │
└──────────────────────┴──────────────────────────────────┘
```
#### SVG 단면도 렌더링 상세
**뷰 모드 3가지**:
1. **횡단면도 (Cross-Section)**: 가이드레일을 위에서 본 단면 — 슬랫이 레일에 끼워진 형태
2. **종단면도 (Longitudinal)**: 가이드레일을 측면에서 본 단면 — 앵커볼트 배치
3. **정면도 (Front View)**: 가이드레일을 정면에서 본 모습 — 레일 전체 높이 + 앵커 위치
**렌더링 요소**:
| 요소 | 색상 | 설명 |
|------|------|------|
| 레일 본체 | `#94a3b8` (은회색) | 강판 단면 |
| 연기차단재 | `#f97316` (주황) | 실링 재질 |
| 슬랫 엣지 | `#60a5fa` (파랑) | 레일 안의 슬랫 |
| 방화벽 | `#a1887f` (갈색 해칭) | 콘크리트 벽 |
| 앵커볼트 | `#ef4444` (빨강) | 고정 부속 |
| 치수선 | `#3b82f6` (파랑) | mm 단위 치수 |
### 3.4 셔터박스 탭
#### UI 구성 (2컬럼 레이아웃)
```
┌──────────────────────┬──────────────────────────────────┐
│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │
│ │ │
│ ■ 케이스 폭: [1500] │ ┌────────────────────────────┐ │
│ ■ 케이스 높이: [380] │ │ [브래킷] ◎샤프트 [브래킷]│ │
│ ■ 케이스 깊이: [380] │ │ 감김 슬랫 │ │
│ ■ 강판 두께: [1.6] │ │ [모터+감속기] [브레이크] │ │
│ │ └────────────────────────────┘ │
│ ■ 샤프트 직경: [120] │ │
│ ■ 샤프트 오프셋 │ ← SVG 실시간 렌더링 │
│ X: [0] Y: [0] │ ← 치수 라벨 (mm) │
│ ■ 모터 위치: [좌/우] │ │
│ │ │
│ ■ 내부 부품 표시 │ [줌 +] [줌 -] [리셋] [DXF 저장] │
│ □ 샤프트 │ │
│ □ 모터/감속기 │ │
│ □ 브레이크 │ │
│ □ 밸런스 스프링 │ │
└──────────────────────┴──────────────────────────────────┘
```
#### SVG 단면도 렌더링 상세
**뷰 모드 3가지**:
1. **정면 단면도**: 케이스를 정면에서 본 내부 구조 (샤프트, 모터, 브래킷 위치)
2. **측면 단면도**: 케이스를 측면에서 본 단면 (깊이 방향, 슬랫 감김 단면)
3. **하부 상세도**: 슬랫 출구 부분 확대
**렌더링 요소**:
| 요소 | 색상 | 설명 |
|------|------|------|
| 케이스 외곽 | `#94a3b8` (은회색) | 강판 박스 |
| 샤프트 | `#64748b` (짙은 회색) | 중심축 + 감김 슬랫 |
| 모터 | `#3b82f6` (파랑) | 전동 개폐기 |
| 브레이크 | `#ef4444` (빨강) | 전자 브레이크 |
| 스프링 | `#22c55e` (녹색) | 밸런스 스프링 |
| 브래킷 | `#8b5cf6` (보라) | 벽 고정 브래킷 |
| 슬랫 | `#f59e0b` (주황) | 감긴 슬랫 단면 |
### 3.5 3D 렌더링 탭
#### 렌더링 대상
Three.js를 사용하여 방화셔터 전체 조립체를 3D로 시각화한다.
```
3D 렌더링 요소:
├── 셔터박스 (반투명 상자)
│ ├── 샤프트 (원통)
│ ├── 감긴 슬랫 (원통 표면)
│ ├── 모터+감속기 (박스)
│ ├── 브레이크 (디스크)
│ └── 브래킷 (L형 판)
├── 가이드레일 좌 (C-채널 압출)
├── 가이드레일 우 (C-채널 압출)
├── 슬랫 커튼 (평면 텍스처)
│ ├── 강판형: 줄무늬 텍스처 (인터록킹 표현)
│ └── 스크린형: 반투명 메쉬
├── 하장바 (직사각형 바)
└── 방화벽 (반투명 콘크리트 텍스처)
```
#### 3D 인터랙션
| 기능 | 조작 | 설명 |
|------|------|------|
| 회전 | 마우스 드래그 | OrbitControls |
| 줌 | 마우스 휠 | 확대/축소 |
| 팬 | 우클릭 드래그 | 시점 이동 |
| 부품 하이라이트 | 마우스 호버 | 해당 부품 강조 + 이름 표시 |
| 부품 ON/OFF | 체크박스 | 개별 부품 표시/숨김 |
| 투명도 | 슬라이더 | 케이스 투명도 조절 (내부 구조 확인) |
| 셔터 개폐 | 슬라이더 | 0%(전개)~100%(전폐) 애니메이션 |
| 조명 | 프리셋 | 기본/스튜디오/야외/드라마틱 |
#### 3D 모델링 방식
DB나 외부 3D 파일 없이, **파라미터 기반 절차적 모델링(Procedural Modeling)**으로 구현한다.
```javascript
// 가이드레일 C-채널 3D 생성 예시 (Three.js ExtrudeGeometry)
function createGuideRailMesh(params) {
const shape = new THREE.Shape();
// C-채널 프로파일 경로 정의
shape.moveTo(0, 0);
shape.lineTo(params.rail_width, 0);
shape.lineTo(params.rail_width, params.rail_lip);
shape.lineTo(params.rail_width - params.rail_thickness, params.rail_lip);
shape.lineTo(params.rail_width - params.rail_thickness, params.rail_thickness);
shape.lineTo(params.rail_thickness, params.rail_thickness);
shape.lineTo(params.rail_thickness, params.rail_lip);
shape.lineTo(0, params.rail_lip);
shape.lineTo(0, 0);
// 높이 방향으로 압출
const extrudeSettings = {
depth: params.rail_height,
bevelEnabled: false
};
return new THREE.Mesh(
new THREE.ExtrudeGeometry(shape, extrudeSettings),
new THREE.MeshStandardMaterial({ color: 0x94a3b8 })
);
}
```
### 3.6 출력 기능
| 기능 | 형식 | 설명 |
|------|------|------|
| **DXF 다운로드** | `.dxf` | 가이드레일/셔터박스 단면도를 CAD 호환 파일로 저장 |
| **PNG 다운로드** | `.png` | SVG 단면도를 이미지로 저장 |
| **3D 스크린샷** | `.png` | 3D 렌더링 현재 뷰를 이미지로 저장 |
| **파라미터 JSON** | `.json` | 현재 설정값을 파일로 내보내기/가져오기 |
---
## 4. 기술 설계
### 4.1 아키텍처
기존 자동도면 생성과 동일한 **순수 클라이언트 측** 아키텍처를 사용한다.
```
┌─────────────────────────────────────────────┐
│ Browser (Client-Side Only) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Blade Template │ │
│ │ (fire-shutter-drawing/index.blade) │ │
│ ├─────────────────────────────────────┤ │
│ │ JavaScript State Management │ │
│ │ (fireShutterState 객체) │ │
│ ├──────────┬──────────────────────────┤ │
│ │ SVG 엔진 │ Three.js 3D 엔진 │ │
│ │ (단면도) │ (조립체 렌더링) │ │
│ ├──────────┴──────────────────────────┤ │
│ │ DXF 생성기 │ PNG 내보내기 │ │
│ └─────────────────────────────────────┘ │
│ │
│ DB 연동: 없음 (localStorage 프리셋만 사용) │
│ API 호출: 없음 │
└─────────────────────────────────────────────┘
```
### 4.2 파일 구조
```
mng/
├── routes/web.php ← 라우트 추가
├── app/Http/Controllers/RdController.php ← 메서드 추가
└── resources/views/rd/fire-shutter-drawing/
├── index.blade.php ← 메인 레이아웃 + 탭 UI
├── partials/
│ ├── _settings.blade.php ← 설정 탭 HTML
│ ├── _guide-rail.blade.php ← 가이드레일 탭 HTML
│ ├── _shutter-box.blade.php ← 셔터박스 탭 HTML
│ └── _3d-viewer.blade.php ← 3D 렌더링 탭 HTML
└── js/
(인라인 또는 @push('scripts')에 포함)
```
> **참고**: 기존 `auto-drawing/index.blade.php`는 단일 파일 4,884줄이다. 유지보수성을 위해 **Blade partial로 분리**하되, JavaScript는 상태 공유가 필요하므로 메인 파일의 `@push('scripts')`에 통합한다.
### 4.3 상태 관리 객체
```javascript
const fireShutterState = {
// 활성 탭
activeTab: 'Settings',
// 설정 탭
settings: {
productType: 'steel', // 'steel' | 'screen'
productModel: 'KFS01', // 제품 모델 코드
openWidth: 2000, // 개구부 폭 W0 (mm)
openHeight: 3000, // 개구부 높이 H0 (mm)
quantity: 1,
// 자동 계산
mfgWidth: 0, // 제작 폭 W1
mfgHeight: 0, // 제작 높이 H1
area: 0, // 면적 M (m²)
weight: 0, // 중량 K (kg)
motorSpec: '', // 권장 모터
},
// 가이드레일 파라미터
guideRail: {
width: 65,
depth: 50,
thickness: 2.3,
lip: 15,
sealThickness: 5,
sealDepth: 40,
slatThickness: 1.6,
height: 3305,
anchorSpacing: 500,
// 뷰 옵션
showDimensions: true,
showSeal: true,
viewMode: 'cross', // 'cross' | 'longitudinal' | 'front'
},
// 셔터박스 파라미터
shutterBox: {
width: 1500,
height: 380,
depth: 380,
thickness: 1.6,
shaftDiameter: 120,
shaftOffsetX: 0,
shaftOffsetY: 0,
motorSide: 'right', // 'left' | 'right'
slatExitWidth: 1400,
bracketWidth: 80,
// 내부 부품 표시
showShaft: true,
showMotor: true,
showBrake: true,
showSpring: true,
viewMode: 'front', // 'front' | 'side' | 'bottom'
},
// 슬랫 파라미터
slat: {
type: 'steel', // 'steel' | 'screen'
pitch: 80,
thickness: 1.6,
profile: 'C', // 'C' | 'S'
},
// 하장바 파라미터
bottomBar: {
width: 60,
height: 40,
sealHeight: 15,
},
// 3D 뷰 설정
threeD: {
caseOpacity: 0.3, // 케이스 투명도
shutterPosition: 100, // 0=전개, 100=전폐
showComponents: {
case: true,
shaft: true,
motor: true,
brake: true,
spring: true,
guideRailL: true,
guideRailR: true,
slats: true,
bottomBar: true,
wall: true,
},
lightPreset: 'default',
},
// 프리셋 관리
presets: [], // localStorage에서 로드
// 뷰 컨트롤 (줌/팬)
view: {
scale: 1,
offset: { x: 0, y: 0 },
isDragging: false,
},
};
```
### 4.4 제품 유형별 기본값 매핑
```javascript
const PRODUCT_DEFAULTS = {
steel: {
label: '강판형',
marginW: 110, // W1 = W0 + 110
marginH: 350, // H1 = H0 + 350
weightFactor: 25, // K = M × 25
guideRail: { width: 65, depth: 50, thickness: 2.3, lip: 15 },
slat: { type: 'steel', pitch: 80, thickness: 1.6, profile: 'C' },
},
screen: {
label: '스크린형',
marginW: 140, // W1 = W0 + 140
marginH: 350, // H1 = H0 + 350
weightFactor: 2, // K = M × 2
guideRail: { width: 30, depth: 25, thickness: 1.5, lip: 11 },
slat: { type: 'screen', pitch: 100, thickness: 0.8, profile: 'flat' },
},
};
const MOTOR_TABLE = [
{ maxWeight: 150, spec: '150K', inch: 4 },
{ maxWeight: 300, spec: '300K', inch: 4 },
{ maxWeight: 500, spec: '500K', inch: 5 },
{ maxWeight: 750, spec: '750K', inch: 5 },
{ maxWeight: 1000, spec: '1000K', inch: 6 },
{ maxWeight: 1500, spec: '1500K', inch: 6 },
];
```
---
## 5. 개발 단계
### Phase 1: 기본 구조 + 가이드레일 단면도 (1단계)
> **목표**: 라우트/컨트롤러/뷰 생성, 설정 탭, 가이드레일 SVG 단면도
| 작업 | 상세 | 예상 |
|------|------|------|
| 라우트 등록 | `GET /rd/fire-shutter-drawing` | 10분 |
| 컨트롤러 메서드 | `RdController@fireShutterDrawing` | 10분 |
| 레이아웃 + 탭 UI | 4탭 구조, 다크 테마 | 1시간 |
| 설정 탭 | 제품 유형/개구부 크기 입력 + 자동 계산 | 1시간 |
| 가이드레일 SVG 엔진 | C-채널 단면도 + 치수선 + 연기차단재 | 3시간 |
| 줌/팬 컨트롤 | 기존 auto-drawing 코드 재사용 | 30분 |
**산출물**: 가이드레일 파라미터 입력 → SVG 횡단면도 실시간 렌더링
### Phase 2: 셔터박스 단면도 (2단계)
> **목표**: 셔터박스(케이스) SVG 단면도 + 내부 부품 표시
| 작업 | 상세 | 예상 |
|------|------|------|
| 셔터박스 SVG 엔진 | 케이스 외곽 + 내부 구조 | 3시간 |
| 내부 부품 렌더링 | 샤프트, 모터, 브레이크, 스프링 | 2시간 |
| 뷰 모드 전환 | 정면/측면/하부 | 1시간 |
| 부품 ON/OFF 토글 | 체크박스 → SVG 요소 표시/숨김 | 30분 |
**산출물**: 셔터박스 파라미터 입력 → SVG 단면도 (정면/측면/하부)
### Phase 3: 3D 렌더링 (3단계)
> **목표**: Three.js 기반 방화셔터 전체 조립체 3D 렌더링
| 작업 | 상세 | 예상 |
|------|------|------|
| Three.js 씬 구축 | 카메라, 조명, OrbitControls | 1시간 |
| 가이드레일 3D 모델 | ExtrudeGeometry (C-채널 압출) | 2시간 |
| 셔터박스 3D 모델 | BoxGeometry + 내부 부품 | 2시간 |
| 슬랫 커튼 3D 모델 | 평면 메쉬 + 텍스처 | 1시간 |
| 셔터 개폐 애니메이션 | 슬라이더 → 슬랫 위치 변경 | 1시간 |
| 조명/투명도 패널 | 기존 auto-drawing 패널 재사용 | 30분 |
**산출물**: 파라미터 연동 3D 방화셔터 조립체 + 인터랙션
### Phase 4: 출력 + 프리셋 (4단계)
> **목표**: DXF/PNG 저장, 프리셋 관리, 완성도 향상
| 작업 | 상세 | 예상 |
|------|------|------|
| DXF 내보내기 | 기존 DXF 생성기 확장 | 1시간 |
| PNG 내보내기 | SVG → Canvas → PNG | 30분 |
| 3D 스크린샷 | Three.js renderer.domElement.toDataURL | 15분 |
| 프리셋 저장/불러오기 | localStorage CRUD | 1시간 |
| JSON 가져오기/내보내기 | 파일 업로드/다운로드 | 30분 |
| UI 다듬기 | 반응형, 툴팁, 키보드 단축키 | 1시간 |
**산출물**: 완성된 방화셔터 도면생성 도구
---
## 6. UI/UX 설계
### 6.1 디자인 시스템
기존 자동도면 생성의 **다크 테마 (Space Theme)**를 그대로 이어간다.
| 요소 | 값 |
|------|------|
| 배경 | `#020617` (slate-950) |
| 패널 | `rgba(15, 23, 42, 0.7)` + backdrop-blur |
| 테두리 | `rgba(255, 255, 255, 0.1)` |
| 강조색 | `#3b82f6` (blue-500) |
| 텍스트 | `#f8fafc` (white) / `#94a3b8` (slate-400) |
| 입력 필드 | `bg-slate-950/80` + `border-slate-800` |
### 6.2 반응형 레이아웃
```
Desktop (1200px+): 2컬럼 (4:8 비율)
Tablet (768-1199px): 1컬럼 (상: 파라미터, 하: 미리보기)
Mobile: 지원 안 함 (최소 768px)
```
### 6.3 인터랙션 흐름
```
사용자 → 제품 유형 선택 (강판/스크린)
→ 기본값 자동 적용
→ 개구부 크기 입력
→ 제작 치수/중량/모터 자동 계산
→ 가이드레일 탭 이동
→ 파라미터 조정 (폭, 깊이, 두께 등)
→ SVG 실시간 업데이트 (입력 즉시)
→ 뷰 모드 전환 (횡단면/종단면/정면)
→ DXF 다운로드 가능
→ 셔터박스 탭 이동
→ 파라미터 조정
→ SVG 실시간 업데이트
→ 내부 부품 ON/OFF
→ 3D 탭 이동
→ 전체 조립체 3D 뷰
→ 셔터 개폐 애니메이션
→ 스크린샷 저장
```
---
## 7. 메뉴 등록
### 7.1 메뉴 위치
```
R&D
├── 대시보드
├── 조직도 관리
├── 중대재해처벌법 점검
├── AI 견적
├── 기획디자인
├── 디자인 인사이트
├── 사운드 로고 스튜디오
├── CM송 제작
├── 자동도면 생성 ← 기존
└── 방화셔터 도면생성 ← 신규 (자동도면 하위에 배치)
```
### 7.2 메뉴 등록 (tinker)
```php
// 개발 서버
App\Models\Commons\Menu::create([
'tenant_id' => 1,
'parent_id' => <R&D 메뉴 ID>,
'name' => '방화셔터 도면생성',
'url' => '/rd/fire-shutter-drawing',
'icon' => 'shield',
'sort_order' => <자동도면 다음 순서>,
'is_active' => true,
]);
```
> **주의**: 메뉴 시더 실행 금지 — tinker로 수동 등록
---
## 8. 향후 확장 가능성
| 확장 | 설명 | 우선순위 |
|------|------|---------|
| **STL/OBJ 내보내기** | 3D 프린팅/CAD 호환 | 🟡 중요 |
| **견적 연동** | 도면 파라미터 → 견적 자동 산출 | 🔴 필수 |
| **제품 카탈로그 연동** | DB에서 제품별 기본 파라미터 로드 | 🟡 중요 |
| **비교 모드** | 2개 설정을 나란히 비교 | 🟢 권장 |
| **PDF 도면 출력** | A3/A4 도면 양식 포함 출력 | 🟡 중요 |
| **설치 시뮬레이션** | 현장 사진 위에 3D 오버레이 | 🟢 권장 |
---
## 관련 문서
- `/home/aweso/sam/docs/features/academy/fire-shutter-image-prompts.md` — 방화셔터 이미지 프롬프트
- `/home/aweso/sam/docs/samples/방화셔터_견적구조_인터뷰.md` — 견적 구조 인터뷰
- `/home/aweso/sam/docs/features/quotes/README.md` — 견적 시스템 분석
- `/home/aweso/sam/docs/projects/quotation/phase-1-5130-analysis/js-formulas.md` — 견적 수식 상세
- `/home/aweso/sam/mng/resources/views/rd/auto-drawing/index.blade.php` — 기존 자동도면 생성
---
**최종 업데이트**: 2026-03-08

View File

@@ -0,0 +1,637 @@
# 사운드 로고 생성기 — 기획서
> **작성일**: 2026-03-08
> **상태**: 기획 확정
> **메뉴명**: 사운드 로고 생성기
> **라우트**: `GET /rd/sound-logo`
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
1~5초의 **짧고 강렬한 시그니처 사운드(Sound Logo)**를 생성하는 도구. 브라우저 내 Web Audio API 신디사이저와 Google Gemini AI를 결합하여, 누구나 전문적인 사운드 로고를 만들 수 있도록 한다.
### 1.2 벤치마킹
| 브랜드 | 사운드 | 길이 | 특징 |
|--------|--------|------|------|
| Intel | 봉-봉봉봉-봉 | 1.5초 | 5음, 밝고 미래적 |
| Netflix | 타-둠 | 3초 | 2음, 깊은 울림 + 리버브 |
| Samsung | 오버더호라이즌 | 2초 | 5음, 따뜻한 멜로디 |
| McDonald's | 바다바바~ | 2초 | 5음, 경쾌한 리듬 |
| Windows | 시작 사운드 | 3초 | 4음, 화음 진행 |
| 카카오톡 | 카톡~ | 0.5초 | 2음, 귀여운 효과음 |
| T-Mobile | 띠-띠띠-띠-띠 | 1초 | 5음, 단순 반복 |
### 1.3 핵심 차별점 — AI 어드바이저 엔진
단순 신디사이저 도구가 아니라, **Google Gemini AI가 음악 이론 기반으로 조언하고 생성을 도와주는** 지능형 도구.
```
┌─────────────────────────────────────────────────────┐
│ 사운드 로고 생성기 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 수동 모드 │ │ AI 어시스트│ │ AI 자동 │ │
│ │ (신디사이저)│ ←→ │ (Gemini) │ ←→ │ (Lyria) │ │
│ │ │ │ │ │ │ │
│ │ 음표 직접 │ │ 브랜드 분석│ │ 프롬프트→ │ │
│ │ 배치/편집 │ │ 음악 추천 │ │ AI 음악 │ │
│ │ │ │ 코드 제안 │ │ 직접 생성 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↕ ↕ ↕ │
│ ┌──────────────────────────────────────────┐ │
│ │ Web Audio API 재생 엔진 │ │
│ │ (실시간 미리듣기 + WAV 내보내기) │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
---
## 2. 3가지 모드 설계
### 2.1 모드 A — 수동 모드 (신디사이저)
> 음표를 직접 배치하고 파라미터를 조절하여 사운드를 만드는 전통적 방식
| 기능 | 설명 |
|------|------|
| 음표 시퀀서 | 음표(C4, E4 등) + 길이(0.05~2초) + 쉼표를 시각적 배열로 편집 |
| 신디사이저 4종 | Sine(부드러움), Square(8bit), Triangle(따뜻함), Sawtooth(날카로움) |
| ADSR 엔벨로프 | Attack(0~500ms), Decay(0~1s), Sustain(0~1), Release(0~3s) |
| 화음(Chord) | 동시에 여러 음 재생 (C+E+G = C Major 등) |
| 이펙트 | 리버브, 딜레이, 로우패스/하이패스 필터 |
| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 표시 |
**기술 기반**: Web Audio API (`OscillatorNode`, `GainNode`, `BiquadFilterNode`, `ConvolverNode`)
### 2.2 모드 B — AI 어시스트 (Gemini 텍스트)
> 브랜드 정보를 입력하면 Gemini가 **음악 이론 기반으로 사운드 로고를 설계**해 주고, 사용자가 미세 조정
**입력 → AI 분석 → 음표 데이터 출력 → Web Audio 재생**
```
사용자 입력 Gemini 분석·추천 출력
┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ 브랜드명 │ │ │ │ │
│ 업종/분위기 │──요청──→ │ 1. 브랜드 성격 분석 │──JSON──→ │ 음표 시퀀스 │
│ 키워드 │ │ 2. 조성/스케일 추천 │ │ BPM, 조성 │
│ 참고 브랜드 │ │ 3. 음표 시퀀스 생성 │ │ 신디 파라미터 │
│ 원하는 느낌 │ │ 4. ADSR 파라미터 제안 │ │ ADSR 값 │
└───────────────┘ │ 5. 이펙트 추천 │ │ 이펙트 설정 │
│ 6. 근거 설명 │ │ │
└──────────────────────┘ └──────────────┘
Web Audio 재생
사용자 미세 조정
```
**Gemini 프롬프트 설계**:
```
당신은 사운드 브랜딩 전문가이자 음악 이론가입니다.
다음 브랜드 정보를 분석하여 1~5초 사운드 로고를 설계해 주세요.
[브랜드 정보]
- 브랜드명: {name}
- 업종: {industry}
- 브랜드 성격: {personality} (예: 혁신적, 신뢰, 친근함)
- 참고 사운드: {reference} (예: 인텔처럼 밝은 느낌)
- 원하는 길이: {duration}초
[응답 형식 - 반드시 JSON으로]
{
"analysis": "브랜드 분석 설명 (한글)",
"reasoning": "이 사운드를 추천하는 음악 이론적 근거 (한글)",
"key": "C",
"scale": "major",
"bpm": 120,
"synth": "sine",
"adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 },
"effects": { "reverb": 0.3, "delay": 0 },
"notes": [
{ "note": "C5", "duration": 0.2, "velocity": 0.8 },
{ "note": "E5", "duration": 0.2, "velocity": 0.9 },
{ "note": "G5", "duration": 0.15, "velocity": 0.7 },
{ "rest": 0.05 },
{ "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 }
],
"variations": [
{ "name": "밝은 버전", "notes": [...] },
{ "name": "차분한 버전", "notes": [...] }
]
}
```
**AI 어시스트 기능 상세**:
| 기능 | 설명 |
|------|------|
| 브랜드 분석 | 업종·성격 기반 적합한 조성/템포/음색 추천 |
| 음표 시퀀스 생성 | 음악 이론(화성학, 리듬 패턴) 기반 멜로디 제안 |
| 변형 3종 제공 | 밝은/차분한/임팩트 버전 동시 생성 |
| 근거 설명 | "C Major → 신뢰감, 5도 상행 → 상승 에너지" 등 이론 설명 |
| 반복 개선 | "좀 더 밝게" "더 짧게" 등 자연어로 수정 요청 |
### 2.3 모드 C — AI 자동 생성 (Google Lyria)
> Google Lyria AI가 프롬프트 기반으로 **실제 음악을 직접 생성**
**2가지 Lyria 엔진 지원** (기존 API 키로 사용 가능, 별도 발급 불필요):
| 엔진 | 인증 | 방식 | 특징 |
|------|------|------|------|
| **Lyria RealTime** (권장) | 기존 Gemini API 키 | WebSocket (브라우저 직접) | 실시간 스트리밍, BPM/스케일 실시간 조절 |
| Lyria 2 (폴백) | Vertex AI 서비스 계정 | REST API (서버 경유) | 30초 단위 파일 생성, $0.06/30초 |
#### Lyria RealTime — 브라우저에서 직접 음악 생성
```
브라우저 (Alpine.js) Google API
┌───────────────────┐ ┌──────────────────┐
│ BPM: 130 │ │ │
│ Scale: C Major │──WebSocket 연결──→ │ Lyria RealTime │
│ 프롬프트 입력 │ │ (lyria-realtime- │
│ │←─2초 단위 오디오── │ exp) │
│ 🔊 실시간 재생 │ │ │
│ BPM 슬라이더 조절 │──실시간 파라미터──→ │ 즉시 반영 │
└───────────────────┘ └──────────────────┘
```
- **모델**: `lyria-realtime-exp` (experimental, Gemini API v1alpha)
- **인증**: 기존 `.env``GEMINI_API_KEY` 그대로 사용
- **출력**: 48kHz 스테레오, 2초 청크 단위 스트리밍
- **제어 파라미터**: BPM(60~200), Scale(Key + Mode), 텍스트 프롬프트
- **지연**: 파라미터 변경 후 최대 2초 이내 반영
**Lyria RealTime 프롬프트 예시**:
```
Short sonic logo. Bright, futuristic, memorable melody.
Clean synthesizer with light reverb.
Ascending progression, major chord resolution.
```
#### Lyria 2 — 서버 경유 파일 생성 (폴백)
기존 `BgmService::generateWithLyria()` 패턴 재활용. Vertex AI 서비스 계정(`google_service_account.json`) 이미 보유.
```
사용자 입력 서버 (Laravel) 결과
┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ 분위기 선택 │ │ SoundLogoService │ │ │
│ 길이 (1~5초) │──POST──→ │ → Lyria 2 API (REST) │──→ │ WAV/MP3 파일 │
│ 프롬프트 입력 │ │ (Vertex AI) │ │ (다운로드) │
└───────────────┘ └──────────────────────┘ └──────────────┘
```
> **우선순위**: Lyria RealTime(브라우저) 먼저 시도 → 실패 시 Lyria 2(서버) 폴백.
> 두 엔진 모두 기존 인증 정보로 사용 가능하며 별도 API 키 발급 불필요.
---
## 3. UI 레이아웃
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎵 사운드 로고 생성기 [내 프로젝트 ▾] [저장] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 모드: [🎹 수동] [🤖 AI 어시스트] [✨ AI 자동] │
│ │
│ ┌─ 모드 B: AI 어시스트 ──────────────────────────────────────┐ │
│ │ │ │
│ │ 브랜드명 [SAM ] │ │
│ │ 업종 [ERP/MES 통합 솔루션 ] │ │
│ │ 브랜드 성격 [○혁신적 ●신뢰 ○친근 ○고급 ○에너지] │ │
│ │ 참고 사운드 [인텔처럼 짧고 밝은 ▾ ] │ │
│ │ 길이 [━━━●━━━ 2초 ] │ │
│ │ │ │
│ │ [🤖 AI에게 사운드 설계 요청] │ │
│ │ │ │
│ │ ┌─ AI 분석 결과 ─────────────────────────────────────┐ │ │
│ │ │ 💡 "SAM은 ERP/MES 통합 솔루션으로, 신뢰와 기술력을 │ │ │
│ │ │ 전달해야 합니다. C Major 조성으로 안정감을, │ │ │
│ │ │ 5도 상행 진행으로 성장과 발전을 표현합니다." │ │ │
│ │ │ │ │ │
│ │ │ 추천: C Major | BPM 130 | Sine + 리버브 30% │ │ │
│ │ │ │ │ │
│ │ │ 변형 3종: │ │ │
│ │ │ [▶ 밝은 버전] [▶ 차분한 버전] [▶ 임팩트 버전] │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 💬 추가 요청: [좀 더 짧고 강렬하게 해줘 ] [전송] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 음표 에디터 (AI 결과 또는 수동 편집) ──────────────────────┐ │
│ │ C5(0.2s) E5(0.2s) G5(0.15s) .(0.05s) [CEG](0.8s) │ │
│ │ ████ ████ ███ · ████████████ │ │
│ │ [+음표] [+쉼표] [+화음] [삭제] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 파라미터 ────────────────────────────────────────────────┐ │
│ │ 음색: ●Sine ○Square ○Triangle ○Sawtooth │ │
│ │ BPM: ━━━━━━●━━━━━ 130 │ │
│ │ 리버브: ━━━●━━━━━━━ 30% │ │
│ │ Attack: ━●━━━━━━━━ 10ms Release: ━━━━━●━━━ 500ms │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 파형 시각화 ─────────────────────────────────────────────┐ │
│ │ ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [▶ 재생] [⏹ 정지] [💾 WAV 저장] [📋 JSON 내보내기] │
│ │
├─ 내 사운드 라이브러리 ──────────────────────────────────────────┤
│ 🎵 SAM 시그널 v1 | 🎵 알림음 v2 | 🎵 전환 효과 | [+ 새로] │
└─────────────────────────────────────────────────────────────────┘
```
---
## 4. 프리셋 템플릿 (10종)
| 프리셋 | 스타일 | 음표 수 | 길이 | 용도 |
|--------|--------|---------|------|------|
| 기업 시그널 (밝음) | Intel 스타일 | 5 | 1.5초 | 브랜드 인트로 |
| 기업 시그널 (무게감) | Netflix 스타일 | 2 | 3초 | 프리미엄 브랜드 |
| 알림음 (경쾌) | 카카오톡 스타일 | 2~3 | 0.5초 | 푸시 알림 |
| 알림음 (정보) | Slack 스타일 | 3 | 1초 | 시스템 알림 |
| 성공 사운드 | 게임 레벨업 | 4 | 1초 | 작업 완료 |
| 에러 사운드 | 경고음 | 2 | 0.5초 | 오류 알림 |
| 전환 효과 (업) | 상승 스윕 | 연속 | 0.5초 | 화면 전환 |
| 전환 효과 (다운) | 하강 스윕 | 연속 | 0.5초 | 메뉴 닫기 |
| 팡파레 | 축하 | 6 | 2초 | 이벤트/달성 |
| 로딩 루프 | 반복 패턴 | 4 | 2초 | 대기 상태 |
---
## 5. 기술 아키텍처
### 5.1 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 프론트엔드 | Blade + Alpine.js | 단일 파일 SPA (디자인 인사이트 패턴) |
| 오디오 엔진 | Web Audio API | `OscillatorNode`, `GainNode`, `ConvolverNode` |
| 시각화 | Canvas API | `AnalyserNode` → 파형/스펙트럼 렌더링 |
| AI 어시스트 | Gemini 2.5 Flash | 텍스트 기반 음악 이론 분석·추천 (모드 B) |
| AI 자동 생성 (1차) | Lyria RealTime | WebSocket 실시간 음악 스트리밍 (모드 C, 브라우저 직접) |
| AI 자동 생성 (폴백) | Lyria 2 (Vertex AI) | REST API 파일 생성 (모드 C, 서버 경유) |
| 저장 | localStorage + DB | 프로젝트 데이터(localStorage), 음원 파일(DB+Storage) |
### 5.2 기존 인프라 재활용
| 기존 코드 | 재활용 내용 |
|----------|-----------|
| `BgmService::generateWithLyria()` | Lyria API 호출 패턴, Vertex AI 인증 흐름 |
| `BgmService::getMoodChord()` | 분위기별 화음 주파수 매핑 |
| `BgmService::generateAmbient()` | FFmpeg 기반 오디오 합성 (서버사이드 폴백) |
| `CmSongController::generateLyrics()` | Gemini API 호출 패턴 (프롬프트 → JSON 응답) |
| `CmSongController::pcmToWav()` | PCM → WAV 변환 유틸리티 |
| `AiConfig::getActiveGemini()` | AI 설정 조회 (API 키, 모델, 리전) |
| `GoogleCloudService::getAccessToken()` | Vertex AI 인증 토큰 |
### 5.3 파일 구조
```
app/Http/Controllers/Rd/
└── SoundLogoController.php # 컨트롤러 (AI API 프록시)
app/Services/Rd/
└── SoundLogoService.php # Gemini 프롬프트 + Lyria 호출
resources/views/rd/sound-logo/
└── index.blade.php # 단일 파일 SPA
routes/web.php # 라우트 추가
```
### 5.4 라우트 설계
| Method | Path | 컨트롤러 | 설명 |
|--------|------|---------|------|
| `GET` | `/rd/sound-logo` | `soundLogo.index` | 메인 페이지 |
| `POST` | `/rd/sound-logo/ai-assist` | `soundLogo.aiAssist` | Gemini 음악 설계 요청 (모드 B) |
| `POST` | `/rd/sound-logo/ai-refine` | `soundLogo.aiRefine` | Gemini 추가 수정 요청 |
| `POST` | `/rd/sound-logo/ai-generate` | `soundLogo.aiGenerate` | Lyria 음악 생성 (모드 C) |
| `POST` | `/rd/sound-logo/save` | `soundLogo.save` | 사운드 저장 (DB + Storage) |
| `GET` | `/rd/sound-logo/{id}/download` | `soundLogo.download` | WAV 다운로드 |
### 5.5 Web Audio API 핵심 구조
```javascript
// 노드 그래프
const ctx = new AudioContext();
// Oscillator → Gain(ADSR) → Filter → Reverb → Analyser → Destination
function createSynthChain(type, freq, adsr, effects) {
const osc = ctx.createOscillator(); // 음원
const gain = ctx.createGain(); // ADSR 엔벨로프
const filter = ctx.createBiquadFilter(); // LP/HP 필터
const analyser = ctx.createAnalyser(); // 시각화
osc.type = type; // sine | square | triangle | sawtooth
osc.frequency.value = freq; // Hz
// ADSR
const now = ctx.currentTime;
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(1, now + adsr.attack);
gain.gain.linearRampToValueAtTime(adsr.sustain, now + adsr.attack + adsr.decay);
osc.connect(gain).connect(filter).connect(analyser).connect(ctx.destination);
return { osc, gain, filter, analyser };
}
// WAV 내보내기 (OfflineAudioContext)
async function exportWav(notes, params) {
const offline = new OfflineAudioContext(2, 44100 * duration, 44100);
// ... 노트 렌더링
const buffer = await offline.startRendering();
const wav = audioBufferToWav(buffer);
// Blob → 다운로드
}
```
### 5.6 음표 ↔ 주파수 매핑
```javascript
const NOTE_FREQ = {
'C3': 130.81, 'D3': 146.83, 'E3': 164.81, 'F3': 174.61,
'G3': 196.00, 'A3': 220.00, 'B3': 246.94,
'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23,
'G4': 392.00, 'A4': 440.00, 'B4': 493.88,
'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46,
'G5': 783.99, 'A5': 880.00, 'B5': 987.77,
'C6': 1046.50
// 반음(#/b)도 포함
};
```
---
## 6. Phase별 개발 계획
### Phase 1 — MVP (수동 모드 + 프리셋)
| 항목 | 내용 |
|------|------|
| 음표 시퀀서 UI | 음표 추가/삭제/편집, 드래그 순서 변경 |
| 신디사이저 4종 | Sine, Square, Triangle, Sawtooth |
| ADSR 슬라이더 | Attack, Decay, Sustain, Release |
| 실시간 재생 | Web Audio API 즉시 재생 |
| WAV 내보내기 | OfflineAudioContext → WAV 다운로드 |
| 프리셋 10종 | 즉시 로드 가능한 사운드 패턴 |
| 프로젝트 저장 | localStorage (디자인 인사이트 패턴) |
### Phase 2 — AI 어시스트 (Gemini)
| 항목 | 내용 |
|------|------|
| 브랜드 입력 폼 | 브랜드명, 업종, 성격, 참고 사운드, 길이 |
| Gemini 분석 API | 브랜드 → 음악 이론 기반 사운드 설계 JSON |
| 변형 3종 생성 | 밝은/차분한/임팩트 버전 동시 제공 |
| 대화형 개선 | "좀 더 밝게" 등 자연어 추가 수정 |
| 근거 표시 | AI가 이 사운드를 추천하는 이유 설명 |
| 라우트 | `POST /rd/sound-logo/ai-assist`, `ai-refine` |
### Phase 3 — AI 자동 생성 (Lyria RealTime + Lyria 2) + 고도화
| 항목 | 내용 |
|------|------|
| Lyria RealTime 연동 | WebSocket으로 브라우저에서 직접 실시간 음악 생성 (기존 Gemini API 키) |
| Lyria 2 폴백 | Vertex AI REST API로 서버 경유 파일 생성 (기존 서비스 계정) |
| 실시간 BPM/스케일 조절 | Lyria RealTime의 파라미터 실시간 변경 |
| 화음(Chord) 편집 | 동시에 여러 음 배치 |
| 이펙트 체인 | 리버브, 딜레이, 필터 |
| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 |
| DB 저장 | 사운드 로고를 DB + Storage에 영구 저장 |
| 공유/내보내기 | JSON 설정 공유, MP3 변환 |
### Phase 4 — 프로급 확장 (선택)
| 항목 | 내용 |
|------|------|
| 타임라인 UI | 드래그로 음표 배치하는 DAW 스타일 |
| 샘플 기반 음색 | 피아노, 벨, 마림바 등 실제 악기 |
| 드럼/퍼커션 | 노이즈 기반 킥/스네어/하이햇 |
| MIDI 내보내기 | 전문 DAW에서 추가 편집 가능 |
| A/B 비교 | 두 사운드를 나란히 비교 재생 |
---
## 7. Gemini AI 연동 상세
### 7.1 API 호출 흐름
```
Frontend (Alpine.js)
│ POST /rd/sound-logo/ai-assist
│ { brand_name, industry, personality, reference, duration }
SoundLogoController::aiAssist()
│ 프롬프트 구성
Gemini 2.5 Flash API
│ JSON 응답 (notes, adsr, effects, analysis)
SoundLogoController → JsonResponse
│ { success: true, data: { notes, params, analysis, variations } }
Frontend: 음표 에디터에 자동 로드 → 즉시 재생
```
### 7.2 대화형 개선 흐름
```
사용자: "좀 더 짧고 강렬하게"
POST /rd/sound-logo/ai-refine
{ previous_notes: [...], feedback: "좀 더 짧고 강렬하게" }
Gemini: 기존 노트를 분석하고 피드백 반영하여 수정된 JSON 반환
수정된 음표가 에디터에 반영
```
### 7.3 Lyria 음악 생성 흐름
#### 7.3.1 Lyria RealTime (브라우저 직접, 권장)
```
사용자: "AI 자동 생성" 탭 → Lyria RealTime 선택
브라우저 JavaScript (Alpine.js)
│ WebSocket 연결 (Gemini API v1alpha)
│ wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent
│ API Key: .env GEMINI_API_KEY (서버 경유 프록시)
│ Model: lyria-realtime-exp
Lyria RealTime 스트리밍
│ 2초 청크 단위 48kHz 스테레오 오디오
│ ← BPM/Scale 실시간 조절 가능
Web Audio API로 실시간 재생 + 녹음(MediaRecorder) → WAV 저장
```
> **API 키 보안**: 브라우저에서 직접 Gemini API 키를 노출하지 않기 위해,
> 서버를 WebSocket 프록시로 사용하거나 `/rd/sound-logo/ws-token` 엔드포인트에서
> 임시 토큰을 발급하는 방식을 검토한다.
#### 7.3.2 Lyria 2 (서버 경유, 폴백)
```
사용자: Lyria RealTime 실패 시 자동 전환
POST /rd/sound-logo/ai-generate
{ mood: "bright_futuristic", duration: 3, prompt: "..." }
SoundLogoService::generateWithLyria()
├── AiConfig::getActiveGemini() → Vertex AI 설정 확인
├── GoogleCloudService::getAccessToken() → OAuth 토큰
└── Lyria API 호출 → audioContent (base64)
WAV 파일 저장 → 다운로드 URL 반환
```
---
## 8. 데이터 모델
### 8.1 localStorage 구조 (Phase 1~2)
```json
{
"sl_projects": [
{
"id": "sl_1709000000_abc",
"title": "SAM 사운드 로고",
"sounds": [
{
"id": "snd_001",
"name": "SAM 시그널 v1",
"notes": [
{ "note": "C5", "duration": 0.2, "velocity": 0.8 },
{ "note": "E5", "duration": 0.2, "velocity": 0.9 },
{ "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 }
],
"params": {
"synth": "sine",
"bpm": 130,
"adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 },
"effects": { "reverb": 0.3, "delay": 0, "filterFreq": 2000 }
},
"aiAnalysis": "C Major 조성, 5도 상행으로 신뢰감과 성장 표현",
"createdAt": "2026-03-08T00:00:00.000Z"
}
],
"createdAt": "2026-03-08T00:00:00.000Z"
}
],
"sl_current": "sl_1709000000_abc"
}
```
### 8.2 DB 테이블 (Phase 3, API 프로젝트에서 마이그레이션)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | bigint | PK |
| `tenant_id` | bigint | FK → tenants |
| `user_id` | bigint | FK → users |
| `name` | varchar(200) | 사운드 이름 |
| `audio_path` | varchar(500) | WAV/MP3 파일 경로 |
| `options` | json | notes, params, aiAnalysis 등 |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |
---
## 9. API 인증 및 키 현황
> **별도 API 키 발급 불필요** — 기존 인증 정보로 모든 엔진 사용 가능
### 9.1 사용 가능한 인증 정보
| 엔진 | 인증 방식 | 설정 위치 | 상태 |
|------|----------|----------|------|
| Gemini 2.5 Flash (모드 B) | API 키 | `.env` `GEMINI_API_KEY` | ✅ 운영 중 |
| Lyria RealTime (모드 C) | 동일 API 키 | `.env` `GEMINI_API_KEY` | ✅ 사용 가능 (experimental) |
| Lyria 2 (모드 C 폴백) | 서비스 계정 | `GOOGLE_APPLICATION_CREDENTIALS` | ✅ 파일 존재 |
| Vertex AI | 프로젝트 ID | `.env` `VERTEX_AI_PROJECT_ID=codebridge-chatbot` | ✅ 설정됨 |
### 9.2 현재 .env 설정 (관련 항목)
```env
GEMINI_API_KEY=AIzaSy... # Gemini + Lyria RealTime 공용
GEMINI_MODEL=gemini-2.5-flash # 텍스트 AI (모드 B)
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
VERTEX_AI_PROJECT_ID=codebridge-chatbot # Lyria 2 (폴백)
VERTEX_AI_LOCATION=us-central1
GOOGLE_APPLICATION_CREDENTIALS=/var/www/sales/apikey/google_service_account.json
```
### 9.3 Lyria RealTime API 사양
| 항목 | 값 |
|------|------|
| 모델 | `lyria-realtime-exp` |
| API 버전 | `v1alpha` (experimental) |
| 프로토콜 | WebSocket (양방향 스트리밍) |
| 출력 포맷 | 48kHz 스테레오 PCM |
| 청크 크기 | 2초 단위 |
| 제어 파라미터 | BPM (60~200), Scale (Key + Mode) |
| 비용 | 무료 (experimental 기간) |
| 참고 | [공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) |
### 9.4 Lyria 2 API 사양 (폴백)
| 항목 | 값 |
|------|------|
| 모델 | `lyria` |
| API | Vertex AI REST (`/publishers/google/models/lyria:predict`) |
| 인증 | 서비스 계정 OAuth 토큰 |
| 출력 포맷 | WAV (base64) |
| 비용 | $0.06 / 30초 |
| 기존 코드 | `BgmService::generateWithLyria()` |
---
## 10. 관련 문서
- [AI 관리 종합 가이드](../guides/ai-management.md) — Gemini API 설정, 호출 흐름
- [R&D 메뉴 개요](../features/rd/README.md) — R&D 메뉴 구조
- [디자인 인사이트](../features/rd/design-insight.md) — 유사 SPA 패턴 참고
- [Lyria RealTime 공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) — Gemini API 음악 생성
- [Lyria 2 Vertex AI 문서](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/lyria-music-generation) — REST API 레퍼런스
- [Lyria RealTime 개발자 가이드](https://dev.to/googleai/lyria-realtime-the-developers-guide-to-infinite-music-streaming-4m1h) — 구현 튜토리얼
---
**최종 업데이트**: 2026-03-08

View File

@@ -0,0 +1,317 @@
# 조직도 관리 시스템
> **작성일**: 2026-03-06
> **상태**: 🟢 v1.0 구현 완료
> **프로젝트**: MNG 전용 (Blade + Alpine.js + SortableJS)
---
## 1. 개요
### 1.1 목적
테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 시스템.
부서 계층 구조와 직원 배치를 드래그 앤 드롭으로 관리한다.
### 1.2 주요 기능
| 기능 | 설명 |
|------|------|
| 트리형 조직도 | 회사 → 부서 → 하위부서 (무한 depth) 계층 표시 |
| 직원 배치 | 드래그 앤 드롭으로 직원을 부서에 배치/해제 |
| 부서 순서 변경 | 같은 레벨 내 부서 순서 드래그로 변경 |
| 부서 계층 이동 | 부서를 다른 부서 아래로 드래그하여 parent 변경 |
| 부서 숨기기 | 더블클릭 → 숨기기 버튼 → DB 저장 (영구) |
| 임원 필터링 | 대표이사/사장 등은 미배치 목록에서 제외 |
---
## 2. 기술 스택
| 구분 | 기술 |
|------|------|
| 백엔드 | Laravel (MNG 프로젝트) |
| 프론트엔드 | Alpine.js + 수동 DOM 렌더링 |
| 드래그 앤 드롭 | SortableJS |
| 스타일 | Tailwind CSS + inline style |
| 데이터 저장 | MySQL `departments`, `employees` 테이블 |
---
## 3. 아키텍처
### 3.1 렌더링 방식
> **핵심**: Alpine.js `x-for` 대신 수동 `innerHTML` 렌더링을 사용한다.
SortableJS와 Alpine.js `x-for` 템플릿이 동시에 DOM을 조작하면 **이중 업데이트 버그**가 발생한다.
이를 해결하기 위해 부서 트리는 JavaScript로 HTML 문자열을 생성하고 `innerHTML`로 삽입한다.
```
Alpine.js 데이터 변경
renderTree() 호출
기존 SortableJS 인스턴스 destroy
buildChildrenHtml(null, 0) → 재귀적 HTML 생성
$refs.deptTree.innerHTML = html
$nextTick → initDeptSortables() + initEmpSortables()
```
### 3.2 이벤트 처리
수동 렌더링된 HTML에는 Alpine 디렉티브가 없으므로 **이벤트 위임(Event Delegation)** 패턴을 사용한다.
```
루트 div @click="handleClick($event)"
@dblclick="handleDblClick($event)"
e.target.closest('[data-action]') 으로 액션 식별
data-action 값에 따라 분기:
- "unassign" → 직원 미배치
- "hide-dept" → 부서 숨기기
- "restore-dept" → 부서 복원
- "dept-dblclick" → 더블클릭 시 숨기기 버튼 토글
```
### 3.3 순환 참조 방지
부서를 자신의 하위로 드래그하면 무한 루프가 발생한다.
`isDescendant(ancestorId, targetId)` 재귀 함수로 이를 차단한다.
```javascript
// 드래그 대상(dragId)의 자손인 곳으로는 이동 불가
onMove: (evt) => {
const dragId = parseInt(evt.dragged.dataset.deptId);
const toPid = evt.to.dataset.parentId ? parseInt(evt.to.dataset.parentId) : null;
if (toPid === dragId || this.isDescendant(dragId, toPid)) return false;
}
```
---
## 4. 파일 구조
### 4.1 MNG 프로젝트
| 파일 | 역할 |
|------|------|
| `app/Http/Controllers/RdController.php` | 컨트롤러 (7개 메서드) |
| `app/Models/Tenants/Department.php` | 부서 모델 (`options` JSON cast) |
| `resources/views/rd/org-chart.blade.php` | 뷰 (Alpine.js + SortableJS) |
| `routes/web.php` | 라우트 (6개 엔드포인트) |
### 4.2 API 프로젝트
| 파일 | 역할 |
|------|------|
| `database/migrations/2026_03_06_201500_add_options_to_departments_table.php` | `options` JSON 컬럼 추가 |
---
## 5. API 엔드포인트
> 모든 엔드포인트는 `rd.` 네임 프리픽스 하위에 위치한다.
| Method | Route | 컨트롤러 메서드 | 설명 |
|--------|-------|---------------|------|
| GET | `/rd/org-chart` | `orgChart` | 조직도 페이지 |
| POST | `/rd/org-chart/assign` | `orgChartAssign` | 직원 부서 배치 |
| POST | `/rd/org-chart/unassign` | `orgChartUnassign` | 직원 부서 해제 |
| POST | `/rd/org-chart/reorder` | `orgChartReorder` | 직원 일괄 이동 |
| POST | `/rd/org-chart/reorder-depts` | `orgChartReorderDepts` | 부서 순서/계층 변경 |
| POST | `/rd/org-chart/toggle-hide` | `orgChartToggleHide` | 부서 숨기기/표시 토글 |
### 5.1 요청/응답 형식
**부서 배치** (`POST /rd/org-chart/assign`):
```json
{ "employee_id": 1, "department_id": 5 }
{ "success": true }
```
**부서 순서 변경** (`POST /rd/org-chart/reorder-depts`):
```json
{
"orders": [
{ "id": 1, "parent_id": null, "sort_order": 1 },
{ "id": 2, "parent_id": null, "sort_order": 2 },
{ "id": 3, "parent_id": 1, "sort_order": 1 }
]
}
{ "success": true }
```
**부서 숨기기** (`POST /rd/org-chart/toggle-hide`):
```json
{ "department_id": 5, "hidden": true }
{ "success": true }
```
---
## 6. DB 구조
### 6.1 departments 테이블 (관련 컬럼)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | int | PK |
| `tenant_id` | int | 테넌트 FK |
| `parent_id` | int (nullable) | 상위 부서 (null = 최상위) |
| `name` | varchar | 부서명 |
| `code` | varchar | 부서 코드 |
| `is_active` | bool | 활성 여부 |
| `sort_order` | int | 정렬 순서 |
| `options` | json (nullable) | 확장 속성 |
**`options` 키**:
| 키 | 타입 | 설명 |
|----|------|------|
| `orgchart_hidden` | boolean | 조직도에서 숨김 여부 |
### 6.2 employees 테이블 (관련 컬럼)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | int | PK |
| `tenant_id` | int | 테넌트 FK |
| `department_id` | int (nullable) | 소속 부서 (null = 미배치) |
| `display_name` | varchar | 표시 이름 |
| `position_label` | varchar | 직책/직급 |
| `employee_status` | enum | `active`, `leave`, `resigned` |
---
## 7. 프론트엔드 구현 상세
### 7.1 Alpine.js 컴포넌트 (`orgChart()`)
**데이터**:
| 속성 | 타입 | 설명 |
|------|------|------|
| `departments` | Array | 전체 부서 목록 (서버에서 전달) |
| `employees` | Array | 전체 직원 목록 (서버에서 전달) |
| `hiddenDepts` | Set | 숨긴 부서 ID (DB에서 초기화) |
| `dblClickDept` | int/null | 더블클릭된 부서 ID (숨기기 버튼 표시용) |
| `execTitles` | Array | 임원 직책 목록 (`['대표이사', '사장', '부사장', '회장', '부회장']`) |
**핵심 메서드**:
| 메서드 | 설명 |
|--------|------|
| `renderTree()` | SortableJS 파괴 → HTML 재생성 → SortableJS 재초기화 |
| `buildChildrenHtml(parentId, level)` | 재귀적 자식 부서 HTML 생성 |
| `buildNodeHtml(dept, level)` | 단일 부서 카드 HTML (level별 스타일 차등) |
| `buildEmpHtml(emp, isLarge)` | 직원 카드 HTML |
| `isDeptHidden(deptId)` | 부서 또는 상위 부서가 숨김인지 재귀 체크 |
| `isDescendant(ancestorId, targetId)` | 순환 참조 방지 |
| `isExecutive(emp)` | 임원 여부 판별 |
### 7.2 SortableJS 그룹
| 그룹 | 대상 | 핸들 | 기능 |
|------|------|------|------|
| `departments` | `.org-children`, `.org-drop-target` | `.dept-drag-handle` | 부서 순서/계층 변경 |
| `employees` | `.emp-zone`, `#unassigned-zone` | (전체) | 직원 배치/해제 |
### 7.3 CSS 연결선
부서 간 연결선은 CSS `::before`/`::after` 의사 요소로 구현한다.
```
부모 노드
│ (vertical: div 1px × 24px)
┌───────┼───────┐ (horizontal: ::before + ::after)
│ │ │ (vertical: div 1px × 24px)
자식1 자식2 자식3
```
| 선택자 | 역할 |
|--------|------|
| `.org-node-wrap` 내부 div (1px × 24px) | 세로 연결선 |
| `.org-node-wrap:not(:first-child)::before` | 왼쪽 가로선 (left:0 ~ right:50%) |
| `.org-node-wrap:not(:last-child)::after` | 오른쪽 가로선 (left:50% ~ right:0) |
| `:only-child` | 단일 자식이면 가로선 숨김 |
### 7.4 부서 숨기기 UX 흐름
```
① 부서 헤더 더블클릭
② dblClickDept = dept.id → renderTree()
③ 헤더에 빨간 "숨기기" 버튼 표시
④ "숨기기" 클릭
⑤ hiddenDepts.add(id) → renderTree() → POST /toggle-hide (DB 저장)
⑥ 해당 부서 + 하위 부서가 트리에서 제거
⑦ "숨겨진 부서" 패널에 표시
⑧ 패널에서 👁 아이콘 클릭 → hiddenDepts.delete(id) → POST /toggle-hide
```
### 7.5 부서 레벨별 스타일
| Level | 색상 테마 | 너비 | 아이콘 |
|-------|---------|------|--------|
| 0 (최상위) | 보라 (`#7C3AED`) | 200px | `ri-building-2-line` |
| 1 (중간) | 인디고 (`#6366F1`) | 180px | `ri-git-branch-line` |
| 2+ (하위) | 회색 (`#6B7280`) | 160px | `ri-subtract-line` |
---
## 8. 비즈니스 규칙
### 8.1 임원 필터링
미배치 직원 목록에서 다음 조건에 해당하면 제외:
- `position_label``['대표이사', '사장', '부사장', '회장', '부회장']` 중 하나
- `display_name`이 테넌트의 `ceo_name`과 일치
> 이유: 조직도 최상단에 "대표이사 OOO"이 이미 표시되므로 중복 방지
### 8.2 부서 숨기기
- `departments.options` JSON의 `orgchart_hidden` 키로 저장
- 숨긴 부서의 **하위 부서도 자동으로 숨겨짐** (`isDeptHidden` 재귀 체크)
- 숨겨진 부서 패널에는 **직접 숨긴 부서만** 표시 (자식은 부모 복원 시 같이 복원)
- 숨기기는 **조직도 표시 전용**`is_active`와 무관하며, 부서 데이터에 영향 없음
### 8.3 직원 표시 형식
- 직책이 있으면: `{직책} {이름}` (예: "과장 전진선")
- 직책이 없으면: `{이름}` (예: "김보곤")
---
## 9. 개발 이력
| 날짜 | 커밋 | 내용 |
|------|------|------|
| 2026-03-06 | `a12ee886` | CSS 연결선 수정 + 빈 드롭 타겟 숨김 |
| 2026-03-06 | `9fd72e49` | 부서 숨기기 기능 추가 (프론트 전용) |
| 2026-03-06 | `8c8fd5f6` | 대표이사 미배치 제외 + 숨긴 부서 연결선 제거 |
| 2026-03-06 | `81157a15` | 부서 숨기기 상태 DB 저장 (`options.orgchart_hidden`) |
---
## 관련 문서
- [rules/department-tree-api.md](../../rules/department-tree-api.md) — 부서 트리 API 규칙
- [rules/employee-api.md](../../rules/employee-api.md) — 직원 API 규칙
- [system/database/hr.md](../../system/database/hr.md) — HR 테이블 스키마
- [standards/options-column-policy.md](../../standards/options-column-policy.md) — options JSON 컬럼 정책
---
**최종 업데이트**: 2026-03-06

View File

@@ -0,0 +1,157 @@
# 기획디자인 스토리보드 에디터
> **시작일**: 2026-03-07
> **상태**: 🟢 v1.2 운영 중 (고도화 진행중)
> **경로**: MNG `/rd/planning-design`
> **담당**: Claude Code + 개발팀
---
## 1. 프로젝트 개요
### 1.1 배경
ERP 화면 기획서(스토리보드)를 PowerPoint나 Figma 없이 **SAM 관리자 웹 내에서 직접 설계**할 수 있는 도구가 필요했다. 기획자와 개발자가 같은 플랫폼에서 화면을 설계하고, 즉시 HTML/인쇄 출력까지 가능한 올인원 솔루션을 목표로 했다.
### 1.2 목표
- 브라우저 내 Notion/Figma 스타일 블록 에디터 구현
- ERP 스토리보드 표준 양식 (메뉴트리 + 와이어프레임 + Description) 지원
- 서버 API 없이 localStorage 기반 즉시 사용 가능
- HTML 내보내기 및 좌표 기반 WYSIWYG 인쇄
### 1.3 기술 스택
| 항목 | 선택 | 이유 |
|------|------|------|
| 프레임워크 | Alpine.js | 서버 없이 반응형 SPA, 기존 MNG 스택과 일치 |
| 캔버스 | DOM absolute positioning | Canvas API보다 접근성 좋고 텍스트 편집 용이 |
| 저장 | localStorage | 서버 API 불필요, 즉시 사용 가능 |
| 내보내기 | HTML 생성 + window.print() | 별도 라이브러리 없이 브라우저 내장 기능 활용 |
---
## 2. 구현 이력
### v1.0 — 기본 블록 에디터 (2026-03-07)
| 커밋 | 내용 |
|------|------|
| `063d8c61` | 스토리보드 블록 Undo/Redo 기능 (Ctrl+Z/Y, 50단계 히스토리) |
| `78c8f3f8` | 페이지 복사 기능 (블록 ID 재생성) |
| `a27d9921` | placeholder 색상 옅게 + italic 스타일 |
| `08cc866a` | 블록 툴바를 단위업무 상단으로 이동 (기획서 보기 방해 제거) |
| `20e5ab78` | 메뉴/캔버스 경계 드래그 리사이즈 (80~400px) |
| `7785dfed` | 올가미(마퀴) 다중 선택 + 그룹 이동/복사/삭제 |
| `ff373c71` | 올가미 선택 동작 수정 (캔버스 빈 영역 판별 개선) |
| `95cd217c` | Ctrl+X 잘라내기 기능 (단일/다중) |
| `f4131df0` | Ctrl+X 후 Ctrl+Z 복구 수정 (히스토리 인덱스 보정) |
| `8ff84e7f` | Description 패널 리사이즈 + 번호 마커 블록 (D&D/툴바) |
| `ac5ae6eb` | 좌표 기반 인쇄 + HTML 내보내기 블록 좌표 배치 |
### v1.1 — 서식 시스템 (2026-03-08)
| 커밋 | 내용 |
|------|------|
| `dfbbd3a1` | 플로팅 서식 툴바 + 우클릭 컨텍스트 메뉴 추가 |
| `280bfddb` | 블록 서식 CSS 상속 수정 (자식 요소 color inherit) |
### v1.2 — 작업 영역 극대화 (2026-03-08)
| 커밋 | 내용 |
|------|------|
| `5e0f1a63` | 좌측 사이드바 접기/펼치기 버튼 추가 |
| `f1202731` | 메뉴트리/Description 패널 접기/펼치기 + 캔버스 폭 자동 확장 (1100→1400px) |
| `a38c017c` | 이미지 블록 업로드를 더블클릭으로 변경 (드래그 중 파일 창 오픈 방지) |
---
## 3. 현재 기능 목록
### 3.1 블록 유형 (15종)
| 분류 | 유형 |
|------|------|
| 텍스트 | Heading (H1), Heading2 (H2), Text, Code |
| 레이아웃 | Divider, Callout |
| 데이터 | Table |
| UI 모형 | Button, Input, Select, Card, Badges |
| 미디어 | Image, Marker (번호 뱃지) |
| 체크 | Todo (체크리스트) |
### 3.2 편집 기능
- 자유 배치 캔버스 (드래그 이동, 핸들 리사이즈)
- 올가미 다중 선택 + 그룹 이동/복사/삭제
- Undo/Redo (50단계)
- 복사/붙여넣기/잘라내기 (Ctrl+C/V/X)
- 전체 선택 (Ctrl+A)
- 더블클릭 인라인 편집 (contenteditable)
- 이미지 블록 더블클릭 업로드 (드래그 충돌 방지)
### 3.3 서식 시스템
- 플로팅 서식 툴바: 글자색, 배경색, 크기, 굵기, 기울임, 정렬, z-index
- 우클릭 컨텍스트 메뉴: 복제/잘라내기/삭제/색상/정렬/레이어/서식 초기화
### 3.4 문서 관리
- 멀티 페이지 (추가/복사/삭제/이동)
- ERP 메뉴 트리 편집 (드래그 순서 변경)
- Description 패널 (기능 설명 + 번호 마커 D&D)
- 프리셋/커스텀 템플릿
### 3.6 작업 영역 극대화
- 좌측 사이드바(메뉴트리) 접기/펼치기 토글
- Description 패널 접기/펼치기 토글 바
- 패널 접힘 시 캔버스 폭 자동 확장 (1100px → 1400px)
- sb-editor 패딩 축소 (24px → 12px)
### 3.5 출력
- HTML 파일 내보내기 (좌표 기반 WYSIWYG)
- 인쇄 미리보기 (A4 Landscape, 페이지 분할)
---
## 4. 향후 로드맵
| 우선순위 | 기능 | 설명 | 상태 |
|---------|------|------|------|
| 🔴 필수 | DB 저장 | localStorage → DB 전환 (협업, 용량 해결) | ⚪ 대기 |
| 🔴 필수 | 스냅/그리드 정렬 | 블록 간 자석 가이드라인 | ⚪ 대기 |
| 🟡 중요 | 그룹핑 | 여러 블록을 하나의 그룹으로 묶기/풀기 | ⚪ 대기 |
| 🟡 중요 | 레이어 패널 | z-index 순서를 시각적으로 관리 | ⚪ 대기 |
| 🟡 중요 | 리치 텍스트 | 블록 내 부분 텍스트 서식 (인라인 B/I/색상) | ⚪ 대기 |
| 🟢 권장 | PDF 내보내기 | 서버사이드 PDF 생성 | ⚪ 대기 |
| 🟢 권장 | 버전 관리 | 명시적 스냅샷 저장 및 비교 | ⚪ 대기 |
| 🟢 권장 | 공유 링크 | 읽기 전용 공유 URL 생성 | ⚪ 대기 |
---
## 5. 기술적 특이사항
### 5.1 단일 파일 아키텍처
모든 CSS + HTML + JavaScript가 `index.blade.php` 하나에 포함 (~4,430줄). 서버 API가 없고 localStorage만 사용하므로, 컨트롤러는 뷰만 반환한다.
### 5.2 CSS 스타일 상속 문제
블록 자식 요소에 하드코딩된 `color`가 있어 부모의 인라인 스타일이 무시되는 문제를 CSS attribute selector(`[style*="color"]`)로 해결했다. 향후 블록 유형 추가 시 inherit 규칙도 함께 추가해야 한다.
### 5.3 localStorage 용량 한계
이미지를 base64 Data URL로 저장하므로 대량 사용 시 5~10MB 한계에 도달할 수 있다. DB 저장 전환이 중장기 과제.
---
## 6. 관련 문서
- [기술 스펙](../../features/rd/planning-design.md) — 데이터 구조, 블록 유형, CSS 상속 상세
- [R&D 메뉴 개요](../../features/rd/README.md) — R&D 전체 메뉴 구조
- [프로젝트 인덱스](../index_projects.md) — 전체 프로젝트 목록
---
**최종 업데이트**: 2026-03-08

Binary file not shown.

View File

@@ -0,0 +1,31 @@
const path = require('path');
module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules'));
const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js'));
const PptxGenJS = require('pptxgenjs');
async function main() {
const pres = new PptxGenJS();
pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 });
pres.layout = 'CUSTOM_16x9';
pres.author = '(주)코드브릿지엑스';
pres.subject = 'SAM 활용방안 - AI 자동화로 중소 제조업을 혁신하다';
const slideDir = __dirname;
const slideFiles = [
'slide-01.html', 'slide-02.html', 'slide-03.html',
'slide-04.html', 'slide-05.html', 'slide-06.html', 'slide-07.html'
];
for (const file of slideFiles) {
const htmlPath = path.join(slideDir, file);
console.log(`Converting: ${file}`);
await html2pptx(htmlPath, pres);
}
const outputPath = path.join(slideDir, 'SAM_활용방안.pptx');
await pres.writeFile({ fileName: outputPath });
console.log(`\nPPTX saved: ${outputPath}`);
}
main().catch(err => { console.error(err); process.exit(1); });

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #0f172a; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 50px 80px 60px 80px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 28px;">
<div style="width: 44px; height: 44px; background: #059669; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
</div>
<p style="white-space: nowrap; font-size: 18px; font-weight: 700; color: #6ee7b7; letter-spacing: 2px;">SAM PROJECT</p>
</div>
<p style="white-space: nowrap; font-size: 36px; font-weight: 800; color: #ffffff; text-align: center;">SAM 활용방안</p>
<p style="white-space: nowrap; font-size: 26px; font-weight: 700; color: #10b981; margin-top: 10px;">AI 자동화로 중소 제조업을 혁신하다</p>
<p style="white-space: nowrap; font-size: 14px; color: #94a3b8; text-align: center; margin-top: 22px;">방화셔터 제조업 실증 | 80% 공통화 전략 | Multi-tenant SaaS 플랫폼</p>
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 28px;">
<div style="border: 1px solid #1e4d3a; border-radius: 20px; padding: 6px 16px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 600; color: #6ee7b7;">코어 모델 실증</p>
</div>
<div style="border: 1px solid #312e81; border-radius: 20px; padding: 6px 16px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 600; color: #a5b4fc;">AI 자동화</p>
</div>
<div style="border: 1px solid #713f12; border-radius: 20px; padding: 6px 16px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 600; color: #fcd34d;">다산업군 확장</p>
</div>
</div>
</div>
<div style="position: absolute; bottom: 20px; left: 0; right: 0; text-align: center;">
<p style="white-space: nowrap; font-size: 11px; color: #475569;">SAM 활용방안 | (주)코드브릿지엑스 | 2026.03</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #f8fafc; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="background: #0f172a; padding: 16px 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">왜 SAM인가? &mdash; Before / After</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">중소 제조업의 현실과 SAM이 제시하는 변화</p>
</div>
<div style="display: flex; gap: 20px; padding: 20px 40px;">
<div style="flex: 1; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 18px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 14px;">
<div style="width: 26px; height: 26px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><path d="M6 18L18 6M6 6l12 12"/></svg>
</div>
<p style="white-space: nowrap; font-size: 14px; font-weight: 700; color: #991b1b;">Before &mdash; 기존 방식</p>
</div>
<p style="font-size: 12px; font-weight: 600; color: #7f1d1d; margin-bottom: 4px;">Excel 수기 관리</p>
<p style="font-size: 10px; color: #991b1b; margin-bottom: 12px;">데이터 유실, 버전 혼란, 실시간 공유 불가</p>
<p style="font-size: 12px; font-weight: 600; color: #7f1d1d; margin-bottom: 4px;">ERP 도입비 수천만원</p>
<p style="font-size: 10px; color: #991b1b; margin-bottom: 12px;">중소기업에 과도한 초기 투자 부담</p>
<p style="font-size: 12px; font-weight: 600; color: #7f1d1d; margin-bottom: 4px;">업체별 커스텀 6개월+</p>
<p style="font-size: 10px; color: #991b1b; margin-bottom: 12px;">도입까지 긴 시간, 업데이트 어려움</p>
<p style="font-size: 12px; font-weight: 600; color: #7f1d1d; margin-bottom: 4px;">부서간 정보 단절</p>
<p style="font-size: 10px; color: #991b1b;">영업/생산/경영 각자 관리, 의사결정 지연</p>
</div>
<div style="flex: 1; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 12px; padding: 18px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 14px;">
<div style="width: 26px; height: 26px; background: #059669; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><path d="M5 13l4 4L19 7"/></svg>
</div>
<p style="white-space: nowrap; font-size: 14px; font-weight: 700; color: #065f46;">After &mdash; SAM 도입 후</p>
</div>
<p style="font-size: 12px; font-weight: 600; color: #064e3b; margin-bottom: 4px;">시스템 기반 통합 관리</p>
<p style="font-size: 10px; color: #047857; margin-bottom: 12px;">실시간 데이터 공유, 단일 진실 공급원(SSOT)</p>
<p style="font-size: 12px; font-weight: 600; color: #064e3b; margin-bottom: 4px;">월 구독 SaaS</p>
<p style="font-size: 10px; color: #047857; margin-bottom: 12px;">초기 비용 최소화, 사용한 만큼 지불</p>
<p style="font-size: 12px; font-weight: 600; color: #064e3b; margin-bottom: 4px;">멀티테넌시 즉시 입주</p>
<p style="font-size: 10px; color: #047857; margin-bottom: 12px;">설정만으로 바로 사용, 지속적 업데이트</p>
<p style="font-size: 12px; font-weight: 600; color: #064e3b; margin-bottom: 4px;">영업~출고 원스톱 자동화</p>
<p style="font-size: 10px; color: #047857;">AI가 연결하는 End-to-End 프로세스</p>
</div>
</div>
<div style="position: absolute; bottom: 12px; left: 40px; right: 40px; display: flex; justify-content: space-between;">
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">SAM 활용방안 | (주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">2 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #f8fafc; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="background: #0f172a; padding: 16px 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">전체 프로세스 &mdash; 영업에서 출고까지</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">6단계 비즈니스 플로우와 AI 자동화 포인트</p>
</div>
<div style="display: flex; align-items: center; padding: 16px 24px 8px 24px; gap: 4px;">
<div style="flex: 1; text-align: center; background: #6366f1; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">01</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">영업</p>
<p style="white-space: nowrap; font-size: 8px; color: #e0e7ff; margin-top: 2px;">고객 DB 자동분류</p>
</div>
<p style="white-space: nowrap; font-size: 16px; color: #6366f1; font-weight: 700;">&rarr;</p>
<div style="flex: 1; text-align: center; background: #8b5cf6; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">02</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">상담</p>
<p style="white-space: nowrap; font-size: 8px; color: #ede9fe; margin-top: 2px;">STT 음성 기록</p>
</div>
<p style="white-space: nowrap; font-size: 16px; color: #8b5cf6; font-weight: 700;">&rarr;</p>
<div style="flex: 1; text-align: center; background: #a855f7; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">03</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">견적서</p>
<p style="white-space: nowrap; font-size: 8px; color: #f3e8ff; margin-top: 2px;">AI 자동 산출</p>
</div>
<p style="white-space: nowrap; font-size: 16px; color: #a855f7; font-weight: 700;">&rarr;</p>
<div style="flex: 1; text-align: center; background: #0ea5e9; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">04</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">수주서</p>
<p style="white-space: nowrap; font-size: 8px; color: #e0f2fe; margin-top: 2px;">자동 전환</p>
</div>
<p style="white-space: nowrap; font-size: 16px; color: #0ea5e9; font-weight: 700;">&rarr;</p>
<div style="flex: 1; text-align: center; background: #059669; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">05</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">작업공정</p>
<p style="white-space: nowrap; font-size: 8px; color: #d1fae5; margin-top: 2px;">AI 공정 최적화</p>
</div>
<p style="white-space: nowrap; font-size: 16px; color: #059669; font-weight: 700;">&rarr;</p>
<div style="flex: 1; text-align: center; background: #16a34a; border-radius: 10px; padding: 12px 4px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 800; color: #ffffff;">06</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #ffffff; margin-top: 2px;">출고</p>
<p style="white-space: nowrap; font-size: 8px; color: #dcfce7; margin-top: 2px;">배송 자동화</p>
</div>
</div>
<div style="padding: 10px 24px 0 24px;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 600; color: #334155; margin-bottom: 6px;">경동/주일 실증 현황</p>
<div style="border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;">
<div style="display: flex; background: #1e293b;">
<div style="width: 120px; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">단계</p></div>
<div style="flex: 1; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">구현 기능</p></div>
<div style="width: 80px; padding: 6px 10px; text-align: center;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">상태</p></div>
<div style="flex: 1; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">AI 적용</p></div>
</div>
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">영업관리</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">고객/거래처 CRM</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">운영중</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">고객 분류 자동화</p></div>
</div>
<div style="display: flex; background: #f8fafc; border-bottom: 1px solid #f1f5f9;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">상담/문의</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">상담 이력, 음성 입력</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">운영중</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">STT 음성→텍스트</p></div>
</div>
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">견적서</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">견적 작성/승인/발송</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">운영중</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">AI 견적 산출 (개발중)</p></div>
</div>
<div style="display: flex; background: #f8fafc; border-bottom: 1px solid #f1f5f9;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">수주서</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">견적→수주 연동</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">운영중</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">자동 전환 프로세스</p></div>
</div>
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">작업공정</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">BOM, 공정 관리</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dbeafe; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #2563eb;">개발중</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">AI 공정 최적화 (계획)</p></div>
</div>
<div style="display: flex; background: #f8fafc;">
<div style="width: 120px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">출고/배송</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">출고 지시, 배송 추적</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #f1f5f9; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #64748b;">계획</p></div></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">물류 자동화 (계획)</p></div>
</div>
</div>
</div>
<div style="position: absolute; bottom: 12px; left: 40px; right: 40px; display: flex; justify-content: space-between;">
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">SAM 활용방안 | (주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">3 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #f8fafc; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="background: #0f172a; padding: 16px 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">80% 공통화론 &mdash; 핵심 설득 논거</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">중소 제조업 업무의 80%는 업종과 무관하게 동일하다</p>
</div>
<div style="padding: 14px 40px 0 40px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<p style="white-space: nowrap; font-size: 10px; color: #64748b; width: 55px; text-align: right;">공통 업무</p>
<div style="flex: 1; display: flex;">
<div style="width: 80%; height: 24px; background: #10b981; border-radius: 6px; display: flex; align-items: center; padding: 0 10px;">
<p style="white-space: nowrap; font-size: 10px; font-weight: 700; color: #ffffff;">80% &mdash; 영업, 회계, 인사, 재고, 문서, 품질</p>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">
<p style="white-space: nowrap; font-size: 10px; color: #64748b; width: 55px; text-align: right;">커스텀</p>
<div style="flex: 1; display: flex;">
<div style="width: 20%; height: 24px; background: #f59e0b; border-radius: 6px; display: flex; align-items: center; padding: 0 10px;">
<p style="white-space: nowrap; font-size: 10px; font-weight: 700; color: #ffffff;">20%</p>
</div>
</div>
</div>
<p style="white-space: nowrap; font-size: 8px; color: #94a3b8; margin-left: 65px;">커스텀 20% = 상품 마스터, 견적 계산식, 공정 시퀀스</p>
</div>
<div style="padding: 10px 40px 0 40px;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 600; color: #334155; margin-bottom: 6px;">업종별 확장 시나리오</p>
<div style="border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;">
<div style="display: flex; background: #1e293b;">
<div style="width: 90px; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">업종</p></div>
<div style="flex: 1; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">공통 (80%)</p></div>
<div style="flex: 1; padding: 6px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">커스텀 (20%)</p></div>
<div style="width: 80px; padding: 6px 10px; text-align: center;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #ffffff;">난이도</p></div>
</div>
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<div style="width: 90px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">방화셔터</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">영업, 견적, 수주, 회계, 인사</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">셔터 규격 계산, 설치 공정</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">실증완료</p></div></div>
</div>
<div style="display: flex; background: #f8fafc; border-bottom: 1px solid #f1f5f9;">
<div style="width: 90px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">블라인드</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">영업, 견적, 수주, 회계, 인사</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">원단/슬랫 규격, 재단 공정</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dcfce7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #059669;">즉시가능</p></div></div>
</div>
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<div style="width: 90px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">금속가공</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">영업, 견적, 수주, 회계, 인사</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">소재/두께 단가표, CNC 공정</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #dbeafe; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #2563eb;">단기적용</p></div></div>
</div>
<div style="display: flex; background: #f8fafc; border-bottom: 1px solid #f1f5f9;">
<div style="width: 90px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">식품제조</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">영업, 견적, 수주, 회계, 인사</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">레시피 관리, HACCP, 유통기한</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #fef3c7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #d97706;">중기적용</p></div></div>
</div>
<div style="display: flex;">
<div style="width: 90px; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; font-weight: 600; color: #334155;">전자부품</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">영업, 견적, 수주, 회계, 인사</p></div>
<div style="flex: 1; padding: 5px 10px;"><p style="white-space: nowrap; font-size: 9px; color: #64748b;">PCB BOM, SMT 공정, 검사</p></div>
<div style="width: 80px; padding: 5px 10px; text-align: center;"><div style="background: #fef3c7; border-radius: 10px; padding: 1px 6px; display: inline-block;"><p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #d97706;">중기적용</p></div></div>
</div>
</div>
<div style="margin-top: 10px; border-left: 3px solid #6366f1; padding: 8px 12px; background: #f5f3ff; border-radius: 0 8px 8px 0;">
<p style="font-size: 11px; color: #4338ca; font-style: italic;">"상품만 바꾸면 새로운 제조업이 된다. 영업, 회계, 인사, 재고 &mdash; 이 80%는 이미 완성되어 있다."</p>
</div>
</div>
<div style="position: absolute; bottom: 12px; left: 40px; right: 40px; display: flex; justify-content: space-between;">
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">SAM 활용방안 | (주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">4 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #f8fafc; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="background: #0f172a; padding: 16px 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">멀티테넌시 &mdash; 하나의 플랫폼, 다수의 기업</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">tenant_id 기반 데이터 격리로 안전하게 다수 기업을 서비스</p>
</div>
<div style="padding: 16px 40px 0 40px;">
<div style="background: #0f172a; border-radius: 12px; padding: 16px 20px; margin-bottom: 16px;">
<div style="display: flex; gap: 10px; justify-content: center; margin-bottom: 10px;">
<div style="flex: 1; background: #1e3a5f; border: 1px solid #2563eb; border-radius: 8px; padding: 10px; text-align: center;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 700; color: #93c5fd;">A 기업 (경동)</p>
</div>
<div style="flex: 1; background: #1e3a5f; border: 1px solid #2563eb; border-radius: 8px; padding: 10px; text-align: center;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 700; color: #93c5fd;">B 기업 (주일)</p>
</div>
<div style="flex: 1; background: #1e3a5f; border: 1px solid #2563eb; border-radius: 8px; padding: 10px; text-align: center;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 700; color: #93c5fd;">C 기업 (금속)</p>
</div>
<div style="flex: 1; background: #1e3a5f; border: 1px solid #2563eb; border-radius: 8px; padding: 10px; text-align: center;">
<p style="white-space: nowrap; font-size: 11px; font-weight: 700; color: #93c5fd;">D 기업 (식품)</p>
</div>
</div>
<p style="white-space: nowrap; font-size: 14px; color: #6366f1; text-align: center; font-weight: 700; margin-bottom: 8px;">&#9660; &#9660; &#9660; &#9660;</p>
<div style="background: #312e81; border: 2px solid #6366f1; border-radius: 12px; padding: 14px 20px; text-align: center;">
<p style="white-space: nowrap; font-size: 16px; font-weight: 800; color: #ffffff;">SAM 플랫폼</p>
<div style="display: flex; gap: 20px; justify-content: center; margin-top: 6px;">
<p style="white-space: nowrap; font-size: 11px; color: #a5b4fc;">공유: 코드 100%</p>
<p style="white-space: nowrap; font-size: 11px; color: #fbbf24;">격리: 데이터 100%</p>
<p style="white-space: nowrap; font-size: 11px; color: #6ee7b7;">기반: tenant_id</p>
</div>
</div>
</div>
<div style="display: flex; gap: 12px;">
<div style="flex: 1; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 10px; padding: 14px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<div style="width: 22px; height: 22px; background: #059669; border-radius: 50%;"></div>
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #065f46;">비용 절감</p>
</div>
<p style="font-size: 10px; color: #047857; line-height: 1.5;">하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일.</p>
</div>
<div style="flex: 1; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 14px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<div style="width: 22px; height: 22px; background: #2563eb; border-radius: 50%;"></div>
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #1e40af;">즉시 입주</p>
</div>
<p style="font-size: 10px; color: #1d4ed8; line-height: 1.5;">tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용.</p>
</div>
<div style="flex: 1; background: #faf5ff; border: 1px solid #e9d5ff; border-radius: 10px; padding: 14px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<div style="width: 22px; height: 22px; background: #7c3aed; border-radius: 50%;"></div>
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #5b21b6;">데이터 격리</p>
</div>
<p style="font-size: 10px; color: #6d28d9; line-height: 1.5;">모든 쿼리에 tenant_id 자동 적용. A기업과 B기업 데이터 완전 분리.</p>
</div>
</div>
</div>
<div style="position: absolute; bottom: 12px; left: 40px; right: 40px; display: flex; justify-content: space-between;">
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">SAM 활용방안 | (주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">5 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #f8fafc; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="background: #0f172a; padding: 16px 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">AI 자동화 현황 &amp; 로드맵</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">구현 완료된 AI 기능과 향후 계획</p>
</div>
<div style="display: flex; gap: 18px; padding: 16px 40px 0 40px;">
<div style="flex: 1;">
<div style="margin-bottom: 10px;">
<div style="background: #dcfce7; border-radius: 10px; padding: 3px 10px; display: inline-block;">
<p style="white-space: nowrap; font-size: 10px; font-weight: 600; color: #059669;">구현 완료</p>
</div>
</div>
<div style="background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 10px; padding: 12px; margin-bottom: 8px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #065f46;">AI 재무 분석</p>
<p style="font-size: 9px; color: #047857; margin-top: 4px; line-height: 1.5;">CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공.</p>
</div>
<div style="background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 10px; padding: 12px; margin-bottom: 8px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #065f46;">STT 음성 입력</p>
<p style="font-size: 9px; color: #047857; margin-top: 4px; line-height: 1.5;">상담 메모, 현장 보고를 음성 입력. 자동 텍스트 변환 후 시스템 기록.</p>
</div>
<div style="background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 10px; padding: 12px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #065f46;">Claude Code 개발 자동화</p>
<p style="font-size: 9px; color: #047857; margin-top: 4px; line-height: 1.5;">SAM 시스템을 Claude Code로 개발. 코드 생성, 리뷰, 배포 자동화.</p>
</div>
</div>
<div style="flex: 1;">
<div style="margin-bottom: 10px;">
<div style="background: #dbeafe; border-radius: 10px; padding: 3px 10px; display: inline-block;">
<p style="white-space: nowrap; font-size: 10px; font-weight: 600; color: #2563eb;">향후 계획</p>
</div>
</div>
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px; margin-bottom: 8px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #1e40af;">AI 견적 자동 생성</p>
<p style="font-size: 9px; color: #1d4ed8; margin-top: 4px; line-height: 1.5;">고객 요구사항 입력 시 과거 데이터 기반 최적 견적 자동 산출.</p>
</div>
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px; margin-bottom: 8px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #1e40af;">AI 공정 최적화</p>
<p style="font-size: 9px; color: #1d4ed8; margin-top: 4px; line-height: 1.5;">생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안.</p>
</div>
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px;">
<p style="white-space: nowrap; font-size: 12px; font-weight: 700; color: #1e40af;">AI 고객 상담</p>
<p style="font-size: 9px; color: #1d4ed8; margin-top: 4px; line-height: 1.5;">FAQ 자동 응답, 견적 문의 자동 접수. 필요 시 담당자 연결.</p>
</div>
</div>
</div>
<div style="margin: 12px 40px 0 40px; border-left: 3px solid #6366f1; padding: 8px 12px; background: #f5f3ff; border-radius: 0 8px 8px 0;">
<p style="font-size: 11px; color: #4338ca; font-style: italic;">"공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다."</p>
</div>
<div style="position: absolute; bottom: 12px; left: 40px; right: 40px; display: flex; justify-content: space-between;">
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">SAM 활용방안 | (주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 9px; color: #94a3b8;">6 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Pretendard', sans-serif; }
body { width: 960px; height: 540px; background: #0f172a; overflow: hidden; position: relative; }
</style>
</head>
<body>
<div style="padding: 20px 40px 0 40px;">
<p style="white-space: nowrap; font-size: 20px; font-weight: 700; color: #ffffff;">로드맵 &amp; 비전</p>
<p style="white-space: nowrap; font-size: 11px; color: #94a3b8; margin-top: 4px;">방화셔터에서 시작하여 모든 중소 제조업으로</p>
</div>
<div style="padding: 20px 40px 0 56px; position: relative;">
<div style="position: absolute; left: 48px; top: 20px; width: 2px; height: 280px; background: #334155;"></div>
<div style="position: relative; padding-left: 24px; margin-bottom: 16px;">
<div style="position: absolute; left: -14px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: #059669;"></div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #10b981;">Phase 1</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 600; color: #e2e8f0;">코어 실증</p>
<p style="white-space: nowrap; font-size: 10px; color: #64748b;">2025~2026 상반기</p>
<div style="background: #1a3a2a; border-radius: 10px; padding: 1px 8px; display: inline-block;">
<p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #10b981;">진행중</p>
</div>
</div>
<p style="font-size: 10px; color: #94a3b8; line-height: 1.4;">경동/주일 방화셔터 제조업에서 전 프로세스 실증. 영업→출고 파이프라인 완성.</p>
</div>
<div style="position: relative; padding-left: 24px; margin-bottom: 16px;">
<div style="position: absolute; left: -14px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: #2563eb;"></div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #60a5fa;">Phase 2</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 600; color: #e2e8f0;">3~5사 확장</p>
<p style="white-space: nowrap; font-size: 10px; color: #64748b;">2026 하반기</p>
<div style="background: #1e293b; border-radius: 10px; padding: 1px 8px; display: inline-block;">
<p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #64748b;">계획</p>
</div>
</div>
<p style="font-size: 10px; color: #94a3b8; line-height: 1.4;">블라인드, 금속가공 등 유사 제조업 3~5사에 멀티테넌시 확장.</p>
</div>
<div style="position: relative; padding-left: 24px; margin-bottom: 16px;">
<div style="position: absolute; left: -14px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: #7c3aed;"></div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #a78bfa;">Phase 3</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 600; color: #e2e8f0;">AI 고도화</p>
<p style="white-space: nowrap; font-size: 10px; color: #64748b;">2027</p>
<div style="background: #1e293b; border-radius: 10px; padding: 1px 8px; display: inline-block;">
<p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #64748b;">계획</p>
</div>
</div>
<p style="font-size: 10px; color: #94a3b8; line-height: 1.4;">AI 견적 자동 산출, AI 공정 최적화, AI 고객 상담 순차 적용.</p>
</div>
<div style="position: relative; padding-left: 24px;">
<div style="position: absolute; left: -14px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: #dc2626;"></div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 3px;">
<p style="white-space: nowrap; font-size: 13px; font-weight: 700; color: #f87171;">Phase 4</p>
<p style="white-space: nowrap; font-size: 13px; font-weight: 600; color: #e2e8f0;">다산업군 플랫폼</p>
<p style="white-space: nowrap; font-size: 10px; color: #64748b;">2028~</p>
<div style="background: #1e293b; border-radius: 10px; padding: 1px 8px; display: inline-block;">
<p style="white-space: nowrap; font-size: 8px; font-weight: 600; color: #64748b;">비전</p>
</div>
</div>
<p style="font-size: 10px; color: #94a3b8; line-height: 1.4;">식품, 전자부품 등 다양한 제조업종. 중소 제조업 표준 SaaS.</p>
</div>
</div>
<div style="position: absolute; bottom: 36px; left: 40px; right: 40px; background: #312e81; border: 1px solid #6366f1; border-radius: 12px; padding: 12px 20px; text-align: center;">
<p style="white-space: nowrap; font-size: 15px; font-weight: 800; color: #ffffff;">"방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM"</p>
<p style="white-space: nowrap; font-size: 10px; color: #a5b4fc; margin-top: 4px;">AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼 | (주)코드브릿지엑스</p>
</div>
<div style="position: absolute; bottom: 12px; right: 40px;">
<p style="white-space: nowrap; font-size: 9px; color: #475569;">7 / 7</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,174 @@
# SAM 활용방안 — AI 자동화 비전
> **작성일**: 2026-03-02
> **상태**: 설계 확정
> **대상**: CEO, 경영진, 전 직원
> **관련 페이지**: MNG 관리자 → Claude Code → 활용방안
---
## 1. 개요
### 1.1 목적
SAM(Smart Automation Management)은 방화셔터 제조업(경동기업, 주일기업)을 코어 모델로 실증한 차세대 ERP/MES 통합 시스템이다. 이 문서는 SAM의 장기 비전과 AI 자동화 전략을 기술한다.
### 1.2 핵심 논지
> "중소 제조업 업무의 80%는 업종과 무관하게 동일하다. 상품만 바꾸면 새로운 제조업이 된다."
| 항목 | 내용 |
|------|------|
| **코어 모델** | 방화셔터 제조업 (경동/주일 실증 완료) |
| **확장 전략** | 80% 공통 프로세스 + 20% 상품 커스텀 |
| **최종 목표** | Multi-tenant SaaS 플랫폼 (다산업군) |
---
## 2. Before / After — 왜 SAM인가
### 2.1 기존 방식의 문제
| 문제 | 상세 |
|------|------|
| Excel 수기 관리 | 데이터 유실, 버전 혼란, 실시간 공유 불가 |
| ERP 도입비 수천만원 | 중소기업에 과도한 초기 투자 부담 |
| 업체별 커스텀 6개월+ | 도입까지 긴 시간, 업데이트 어려움 |
| 부서간 정보 단절 | 영업/생산/경영 각자 관리, 의사결정 지연 |
### 2.2 SAM 도입 후
| 개선 | 상세 |
|------|------|
| 시스템 기반 통합 관리 | 실시간 데이터 공유, 단일 진실 공급원(SSOT) |
| 월 구독 SaaS | 초기 비용 최소화, 사용한 만큼 지불 |
| 멀티테넌시 즉시 입주 | 설정만으로 바로 사용, 지속적 업데이트 |
| 영업~출고 원스톱 자동화 | AI가 연결하는 End-to-End 프로세스 |
---
## 3. 전체 프로세스 — 영업에서 출고까지
```
영업 → 상담 → 견적서 → 수주서 → 작업공정 → 출고
(01) (02) (03) (04) (05) (06)
```
### 3.1 각 단계별 AI 자동화 포인트
| 단계 | 구현 기능 | AI 적용 | 상태 |
|------|----------|---------|------|
| 영업관리 | 고객/거래처 CRM | 고객 분류 자동화 | 운영중 |
| 상담/문의 | 상담 이력, 음성 입력 | STT 음성→텍스트 변환 | 운영중 |
| 견적서 | 견적 작성/승인/발송 | AI 견적 자동 산출 | 운영중 (AI 개발중) |
| 수주서 | 견적→수주 연동 | 자동 전환 프로세스 | 운영중 |
| 작업공정 | BOM, 공정 관리 | AI 공정 최적화 | 개발중 |
| 출고/배송 | 출고 지시, 배송 추적 | 물류 자동화 | 계획 |
---
## 4. 80% 공통화론
### 4.1 업무 구성 비율
```
공통 업무 ██████████████████████████████████████████ 80%
커스텀 ██████████ 20%
```
- **공통 80%**: 영업/CRM, 회계/재무, 인사/근태, 재고관리, 문서/전자결재, 품질관리
- **커스텀 20%**: 상품 마스터, 견적 계산식, 공정 시퀀스 (업종마다 다른 부분)
### 4.2 업종별 확장 시나리오
| 업종 | 공통 (80%) | 커스텀 (20%) | 난이도 |
|------|-----------|-------------|--------|
| 방화셔터 | 영업, 견적, 수주, 회계, 인사 | 셔터 규격 계산, 설치 공정 | 실증완료 |
| 블라인드 | 영업, 견적, 수주, 회계, 인사 | 원단/슬랫 규격, 재단 공정 | 즉시가능 |
| 금속가공 | 영업, 견적, 수주, 회계, 인사 | 소재/두께 단가표, CNC 공정 | 단기적용 |
| 식품제조 | 영업, 견적, 수주, 회계, 인사 | 레시피 관리, HACCP, 유통기한 | 중기적용 |
| 전자부품 | 영업, 견적, 수주, 회계, 인사 | PCB BOM, SMT 공정, 검사 | 중기적용 |
---
## 5. 멀티테넌시 구조
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ A 기업 │ │ B 기업 │ │ C 기업 │ │ D 기업 │
│ (경동기업) │ │ (주일기업) │ │ (금속가공) │ │ (식품제조) │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
└─────────────┴──────┬──────┴─────────────┘
┌─────────▼─────────┐
│ SAM 플랫폼 │
│ │
│ 공유: 코드 100% │
│ 격리: 데이터 100% │
│ (tenant_id 기반) │
└───────────────────┘
```
### 5.1 핵심 이점
| 이점 | 상세 |
|------|------|
| **비용 절감** | 하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일 |
| **즉시 입주** | 새 기업 추가 = tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용 |
| **데이터 격리** | 모든 쿼리에 tenant_id 자동 적용. A기업이 B기업 데이터에 접근 불가 |
---
## 6. AI 자동화 현황 & 로드맵
### 6.1 구현 완료
| 기능 | 상세 |
|------|------|
| **AI 재무 분석** | CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공 |
| **STT 음성 입력** | 상담 메모, 현장 보고를 음성으로 입력. 자동 텍스트 변환 후 시스템 기록 |
| **Claude Code 개발 자동화** | SAM 시스템 자체를 Claude Code로 개발. 코드 생성, 리뷰, 테스트, 배포 자동화 |
### 6.2 향후 계획
| 기능 | 상세 |
|------|------|
| **AI 견적 자동 생성** | 고객 요구사항 입력 시 과거 데이터 기반으로 최적 견적 자동 산출 |
| **AI 공정 최적화** | 생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안. 불량률 예측 및 사전 경고 |
| **AI 고객 상담** | FAQ 자동 응답, 견적 문의 자동 접수. 사람의 개입이 필요한 경우만 담당자 연결 |
> "공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다."
---
## 7. 로드맵 — 4단계 비전
| Phase | 제목 | 기간 | 상태 | 핵심 목표 |
|-------|------|------|------|----------|
| **Phase 1** | 코어 실증 | 2025~2026 상반기 | 진행중 | 경동/주일 방화셔터에서 영업→출고 전 프로세스 실증 |
| **Phase 2** | 3~5사 확장 | 2026 하반기 | 계획 | 블라인드, 금속가공 등 유사 제조업 멀티테넌시 확장 |
| **Phase 3** | AI 고도화 | 2027 | 계획 | AI 견적 자동 산출, 공정 최적화, 고객 상담 순차 적용 |
| **Phase 4** | 다산업군 플랫폼 | 2028~ | 비전 | 식품, 전자부품 등 다양한 업종. 중소 제조업 표준 SaaS |
---
## 결론
> "방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM"
**AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼**
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [SAM 프로젝트 개요](../SAM_PROJECT_OVERVIEW_FOR_AI.md) | 기술적 개요 |
| [스케일링 로드맵](scaling-roadmap.md) | 10,000 테넌트 기술 스케일링 |
| [보안 정책](security-policy.md) | 보안 아키텍처 |
---
**최종 업데이트**: 2026-03-02

View File

@@ -0,0 +1,443 @@
# codebridge DB 분리
> **작성일**: 2026-03-07
> **상태**: 로컬/개발 서버 적용 완료, **운영 서버 코드 revert 상태 — DB 선행 작업 필요**
> **최종 수정**: 2026-03-09 — API 사용 테이블 점검, 로컬/개발 samdb 삭제 완료, 운영 코드 revert
---
## 1. 개요
### 1.1 목적
SAM 프로젝트의 DB를 **서비스용**과 **내부 관리용**으로 분리한다.
- **samdb**: React 서비스가 사용하는 테이블 (수주, 견적, 생산, 거래처 등)
- **codebridge**: MNG(관리자 패널)에서만 사용하는 코드브릿지엑스 내부 관리 테이블
### 1.2 핵심 원칙
- codebridge DB에 테이블을 복사한 후, samdb에서 해당 테이블을 **삭제**하여 실질적 분리
- MNG 모델에 `$connection = 'codebridge'`를 설정하여 읽기 대상 DB만 변경
- React/API 서비스에는 **영향 없음**
- 개발 서버: samdb에서 59개 테이블 삭제 완료 (백업: `/home/pro/backup/sam_backup_20260309.sql.gz`)
### 1.3 분리 기준
| 분류 | 대상 DB | 기준 |
|------|---------|------|
| React 서비스 테이블 | samdb (유지) | React 프론트엔드 또는 API에서 사용 |
| MNG 전용 테이블 | codebridge (이동) | MNG에서만 사용, React/API 미참조 |
| 공통 테이블 | samdb (유지) | 양쪽 모두 사용 (users, tenants 등) |
| **API 사용 테이블** | **samdb (유지 필수)** | **API에 모델/서비스/컨트롤러 존재 — 이동 시 데이터 불일치 발생** |
> **경고: API에서 모델/서비스/컨트롤러로 참조하는 테이블을 codebridge로 이동하면, API는 samdb에 쓰고 MNG는 codebridge에서 읽게 되어 데이터 불일치가 발생한다. 절대 이동 금지.**
---
## 2. codebridge 테이블 목록 (59개)
> 2026-03-09 점검: API 프로젝트 전체 코드 조사를 통해 API에서 사용하는 22개 테이블을 제외함. 제외된 테이블은 [3절](#3-api-사용-테이블--samdb-유지-필수-22개) 참조.
> Equipment 하위 테이블 4개 추가 (FK 의존성으로 equipments와 동일 DB 필수).
### Admin (9)
| 테이블 | 설명 |
|--------|------|
| `admin_api_flows` | API 플로우 정의 |
| `admin_api_flow_runs` | API 플로우 실행 이력 |
| `admin_pm_daily_logs` | PM 일일 로그 |
| `admin_pm_daily_log_entries` | PM 일일 로그 항목 |
| `admin_pm_issues` | PM 이슈 |
| `admin_pm_projects` | PM 프로젝트 |
| `admin_pm_tasks` | PM 태스크 |
| `admin_roadmap_milestones` | 로드맵 마일스톤 |
| `admin_roadmap_plans` | 로드맵 계획 |
### DevTools (5)
| 테이블 | 설명 | 비고 |
|--------|------|------|
| `admin_api_bookmarks` | API 북마크 | 기존명 `api_bookmarks` |
| `admin_api_deprecations` | API 지원종료 관리 | 기존명 `api_deprecations` |
| `admin_api_environments` | API 환경 설정 | 기존명 `api_environments` |
| `admin_api_histories` | API 호출 이력 | 기존명 `api_histories` |
| `admin_api_templates` | API 템플릿 | 기존명 `api_templates` |
### Sales (17)
| 테이블 | 설명 |
|--------|------|
| `sales_partners` | 영업 파트너 |
| `sales_managers` | 영업 담당자 |
| `sales_manager_documents` | 영업 담당자 문서 |
| `sales_commissions` | 영업 수당 |
| `sales_commission_details` | 영업 수당 상세 |
| `sales_consultations` | 영업 상담 |
| `sales_contract_products` | 계약 제품 |
| `sales_products` | 영업 제품 |
| `sales_product_categories` | 영업 제품 카테고리 |
| `sales_prospects` | 영업 전망 |
| `sales_prospect_consultations` | 전망 상담 |
| `sales_prospect_products` | 전망 제품 |
| `sales_prospect_scenarios` | 전망 시나리오 |
| `sales_records` | 영업 실적 |
| `sales_scenario_checklists` | 시나리오 체크리스트 |
| `sales_tenant_managements` | 테넌트 영업 관리 |
| `tenant_prospects` | 테넌트 전망 |
### Finance (9)
| 테이블 | 설명 |
|--------|------|
| `condolence_expenses` | 경조사비 |
| `consulting_fees` | 컨설팅비 |
| `corporate_cards` | 법인카드 |
| `corporate_card_prepayments` | 법인카드 선결제 |
| `customer_settlements` | 고객 정산 |
| `daily_fund_memos` | 일일 자금 메모 |
| `daily_fund_transactions` | 일일 자금 거래 |
| `incomes` | 수입 |
| `vat_records` | 부가세 기록 |
### ESign (2)
| 테이블 | 설명 |
|--------|------|
| `esign_field_templates` | 전자서명 필드 템플릿 |
| `esign_field_template_items` | 전자서명 필드 항목 |
> esign_contracts, esign_audit_logs, esign_sign_fields, esign_signers는 API에서 전자계약 기능으로 사용 중 → samdb 유지
### Equipment (6)
| 테이블 | 설명 |
|--------|------|
| `equipments` | 설비 |
| `equipment_processes` | 설비 공정 |
| `equipment_inspections` | 설비 점검 (FK → equipments) |
| `equipment_inspection_details` | 설비 점검 상세 (FK → equipment_inspections) |
| `equipment_inspection_templates` | 설비 점검 템플릿 (FK → equipments) |
| `equipment_repairs` | 설비 수리 (FK → equipments) |
> Equipment 하위 4개 테이블은 `equipments`에 FK 의존하므로 반드시 동일 DB에 있어야 한다.
### HR (1)
| 테이블 | 설명 |
|--------|------|
| `business_income_payments` | 사업소득 지급 |
> income_tax_brackets는 API IncomeTaxBracketSeeder에서 초기 데이터 관리 → samdb 유지
### System (1)
| 테이블 | 설명 |
|--------|------|
| `ai_configs` | AI 설정 |
> ai_pricing_configs, ai_token_usages는 API 모델에서 직접 사용 → samdb 유지
### 기타 (9)
| 테이블 | 설명 | 비고 |
|--------|------|------|
| `biz_cert` | 사업자등록증 | 문서 기존명 `biz_certs` → 실제 테이블명 (단수) |
| `cm_songs` | R&D 곡 관리 | |
| `construction_site_photos` | 시공 현장 사진 | |
| `construction_site_photo_rows` | 시공 사진 행 | |
| `admin_meeting_logs` | 회의 로그 | 문서 기존명 `meeting_logs` → 실제 테이블명 |
| `meeting_minutes` | 회의록 | |
| `meeting_minute_segments` | 회의록 세그먼트 | |
| `interview_knowledges` | 면접 지식 | |
| `sales_records` | 매출 기록 | |
---
## 3. API 사용 테이블 — samdb 유지 필수 (22개)
> **경고: 아래 테이블은 API 프로젝트에서 모델/서비스/컨트롤러/시더로 직접 참조한다. 절대 codebridge로 이동 금지.**
>
> **2026-03-09 점검**: sam/api 프로젝트 전체 코드 (모델, 컨트롤러, 서비스, 라우트, 시더) 조사 완료.
### Barobill (12) — 전체 samdb 유지
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `barobill_billing_records` | BarobillBillingService | 과금 기록 CRUD |
| `barobill_members` | BarobillUsageService | 회원사 사용량 집계 |
| `barobill_monthly_summaries` | BarobillBillingService | 월별 집계 갱신 |
| `barobill_pricing_policies` | BarobillUsageService | 과금 계산 |
| `bank_sync_statuses` | BankSyncStatus 모델 | 동기화 상태 추적 |
| `bank_transactions` | BankTransactionController | 은행 거래 조회/분개 |
| `bank_transaction_overrides` | BankTransactionOverride 모델 | 거래 재정의 |
| `bank_transaction_splits` | BankTransactionController | 은행 거래 분개 |
| `card_transaction_amount_logs` | CardTransactionAmountLog 모델 | 금액 수정 이력 + FK → card_transactions |
| `card_transaction_hides` | CardTransactionHide 모델 | 거래 숨김 + FK → card_transactions |
| `hometax_invoices` | BarobillUsageService | 세금계산서 사용량 집계 |
| `hometax_invoice_journals` | HometaxInvoiceJournal 모델 | 세금계산서 분개 + FK → hometax_invoices |
> **핵심**: API의 BarobillController, BarobillSettingController, BarobillService, EntertainmentService가 바로빌 테이블을 직접 참조. `barobill_card_transactions` (samdb 유지)와 FK로 연결된 자식 테이블도 분리 불가.
### ESign (4) — API 전자계약 기능
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `esign_contracts` | EsignContractController, EsignService | 전자계약 CRUD |
| `esign_audit_logs` | EsignService | 감사 추적 기록 |
| `esign_sign_fields` | EsignService | 서명 위치 데이터 |
| `esign_signers` | EsignService | 서명자 정보/인증 |
### Audit (2) — API 전사 감사 시스템
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `audit_logs` | AuditLog 모델, AuditLogService, AuditRollbackService | 전사 DML 감사 |
| `trigger_audit_logs` | TriggerAuditLog 모델, TriggerAuditLogController, RegenerateAuditTriggers 명령 | DB 트리거 감사 + 파티셔닝 관리 |
### DevTools (1)
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `api_request_logs` | ApiRequestLog 모델, SystemStatService | API 통계 집계 |
### System (2)
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `ai_pricing_configs` | AiPricingConfig 모델 | AI 서비스 비용 계산 (캐시 기반) |
| `ai_token_usages` | AiTokenUsage 모델 | 멀티테넌트 AI 토큰 사용량 추적 |
### HR (1)
| 테이블 | API 사용처 | 사유 |
|--------|-----------|------|
| `income_tax_brackets` | IncomeTaxBracketSeeder | 소득세 구간 초기 데이터 관리 |
---
## 4. 적용 현황
### 4.1 환경별 상태
| 환경 | codebridge DB | 테이블 복사 | samdb 삭제 | .env 설정 | MNG 코드 | 상태 |
|------|:---:|:---:|:---:|:---:|:---:|------|
| **로컬 Docker** | O | 100개 | **58개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 |
| **개발 서버** | O | 101개 | **63개 삭제** | O | O (develop) | ✅ 정상 작동, samdb 265개 |
| **운영 서버** | **X** | **X** | **X** | **X** | **revert됨** | ⚠️ DB 선행 작업 후 코드 재배포 필요 |
> **2026-03-09 작업 내역**:
> - API 사용 테이블 22개: codebridge 이동 대상에서 제외 → samdb 유지
> - `finance_*` 17개 + `barobill_companies` 1개: codebridge에 없는 유령 테이블 → samdb에서만 삭제
> - Equipment 하위 4개 테이블: FK 의존성으로 codebridge 이동 대상에 추가 (55→59개)
> - **개발 서버 samdb에서 63개 테이블 DROP 완료** (59개 + DevTools 실제 테이블명 4개 추가분)
> - **로컬 samdb에서 58개 테이블 DROP 완료** → 로컬/개발 265개로 동기화
> - 로컬에 `quality_documents` 등 4개 테이블 구조 동기화 (개발서버에서 복사)
> - 백업: `/home/pro/backup/sam_backup_20260309.sql.gz` (6.3MB)
>
> **테이블명 불일치 발견 (수정 완료)**:
> - `api_bookmarks` → 실제: `admin_api_bookmarks`
> - `meeting_logs` → 실제: `admin_meeting_logs`
> - `biz_certs` → 실제: `biz_cert` (단수형)
> - DevTools 4개: `api_deprecations` → `admin_api_deprecations`, `api_environments` → `admin_api_environments`, `api_histories` → `admin_api_histories`, `api_templates` → `admin_api_templates`
>
> **운영 서버 revert 사유 (2026-03-09)**:
> - MNG main에 codebridge 코드 2건 cherry-pick → Jenkins 배포됨 (빌드 #456, #457)
> - 운영 서버에 codebridge DB가 없는 상태에서 코드 배포 → **59개 모델 사용 페이지 오류 발생 위험**
> - kent가 main에서 revert 2건 push → 운영 서버 정상 복구
> - **교훈: 운영 서버는 반드시 DB 선행 작업(1~2단계) 완료 후 코드 배포(3단계)**
### 4.2 코드 변경 사항
**config/database.php**`codebridge` connection 추가:
```php
'codebridge' => [
'driver' => 'mysql',
'host' => env('CODEBRIDGE_DB_HOST', env('DB_HOST', '127.0.0.1')),
'port' => env('CODEBRIDGE_DB_PORT', env('DB_PORT', '3306')),
'database' => env('CODEBRIDGE_DB_DATABASE', 'codebridge'),
'username' => env('CODEBRIDGE_DB_USERNAME', env('DB_USERNAME')),
'password' => env('CODEBRIDGE_DB_PASSWORD', env('DB_PASSWORD')),
// ... (mysql 기본 설정과 동일)
],
```
**.env** — 추가 설정:
```
CODEBRIDGE_DB_DATABASE=codebridge
```
**MNG 모델**`$connection` 속성 추가 (codebridge 59개만):
```php
class SalesPartner extends Model
{
protected $connection = 'codebridge'; // 추가
protected $table = 'sales_partners';
// ...
}
```
> **주의**: API 사용 테이블 22개에 해당하는 MNG 모델은 `$connection = 'codebridge'`를 설정하지 않는다. 기본 samdb connection을 사용해야 API와 동일한 데이터를 참조한다.
### 4.3 samdb 테이블 삭제 절차 (개발 서버 완료)
> Sales 테이블 그룹은 FK 상호 참조가 있어 `FOREIGN_KEY_CHECKS = 0`으로 일괄 삭제.
```sql
-- FK 체크 비활성화 (Sales, Equipment 등 FK 체인 테이블)
SET FOREIGN_KEY_CHECKS = 0;
-- 59개 테이블 DROP (codebridge에 복제 완료 확인 후)
DROP TABLE IF EXISTS admin_api_flows, admin_api_flow_runs, ...;
SET FOREIGN_KEY_CHECKS = 1;
```
> **롤백**: 백업에서 특정 테이블만 복원 가능
> ```bash
> gunzip < /home/pro/backup/sam_backup_20260309.sql.gz | mysql -u codebridge -p sam
> ```
---
## 5. 운영 서버 적용 절차 (미완료)
> **전제**: 운영 서버 SSH 접근 + DB root 권한 필요
> **현재 상태**: 운영 서버 main 코드는 revert 상태 (codebridge 코드 없음). DB 작업 완료 후 코드 재배포 필요.
> **⚠️ 교훈**: 2026-03-09에 DB 없이 코드만 배포하여 장애 위험 발생 → **반드시 DB 선행 후 코드 배포**
### 순서 (반드시 1 → 2 → 3 → 4 → 5 순서로)
**1단계: 운영 sam DB 백업**
```bash
# 운영 서버 접속 후
mysqldump -u codebridge -p'[운영PW]' sam --single-transaction > ~/backup/sam_backup_$(date +%Y%m%d).sql
gzip ~/backup/sam_backup_$(date +%Y%m%d).sql
```
**2단계: codebridge DB 생성 + 59개 테이블 복사**
```bash
# DB 생성
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# DB 계정 권한 부여
mysql -u root -p -e "GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost'; FLUSH PRIVILEGES;"
# sam에서 59개 테이블 구조+데이터 복사
mysqldump -u codebridge -p sam \
admin_api_flows admin_api_flow_runs \
admin_pm_daily_logs admin_pm_daily_log_entries admin_pm_issues admin_pm_projects admin_pm_tasks \
admin_roadmap_milestones admin_roadmap_plans \
admin_api_bookmarks admin_api_deprecations admin_api_environments admin_api_histories admin_api_templates \
sales_partners sales_managers sales_manager_documents sales_commissions sales_commission_details \
sales_consultations sales_contract_products sales_products sales_product_categories \
sales_prospects sales_prospect_consultations sales_prospect_products sales_prospect_scenarios \
sales_records sales_scenario_checklists sales_tenant_managements tenant_prospects \
condolence_expenses consulting_fees corporate_cards corporate_card_prepayments \
customer_settlements daily_fund_memos daily_fund_transactions incomes vat_records \
esign_field_templates esign_field_template_items \
equipments equipment_process equipment_inspections equipment_inspection_details \
equipment_inspection_templates equipment_repairs \
business_income_payments ai_configs \
biz_cert cm_songs construction_site_photos construction_site_photo_rows \
admin_meeting_logs meeting_minutes meeting_minute_segments \
interview_knowledge \
| mysql -u codebridge -p codebridge
```
**3단계: .env 설정**
```bash
echo 'CODEBRIDGE_DB_DATABASE=codebridge' >> /home/webservice/mng/.env
cd /home/webservice/mng && php artisan config:clear
```
**4단계: MNG 코드 재배포 (main cherry-pick)**
> develop에 codebridge 코드가 있으므로, revert 커밋 이후 develop의 최신 커밋을 cherry-pick.
> 또는 develop의 해당 커밋을 다시 cherry-pick하여 main에 push.
```bash
# 로컬에서 실행
cd /home/aweso/sam/mng
git checkout main && git pull origin main
git cherry-pick <develop의 codebridge 커밋 해시>
git push origin main
git checkout develop
```
**5단계: 동작 확인 + samdb 테이블 삭제 (선택)**
MNG 관리자 페이지에서 영업관리, 설비, 재무 등 주요 메뉴 동작 확인 후, 문제없으면 sam DB에서 59개 테이블 삭제.
```sql
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS
admin_api_flows, admin_api_flow_runs,
admin_pm_daily_logs, admin_pm_daily_log_entries, admin_pm_issues, admin_pm_projects, admin_pm_tasks,
admin_roadmap_milestones, admin_roadmap_plans,
admin_api_bookmarks, admin_api_deprecations, admin_api_environments, admin_api_histories, admin_api_templates,
sales_partners, sales_managers, sales_manager_documents, sales_commissions, sales_commission_details,
sales_consultations, sales_contract_products, sales_products, sales_product_categories,
sales_prospects, sales_prospect_consultations, sales_prospect_products, sales_prospect_scenarios,
sales_records, sales_scenario_checklists, sales_tenant_managements, tenant_prospects,
condolence_expenses, consulting_fees, corporate_cards, corporate_card_prepayments,
customer_settlements, daily_fund_memos, daily_fund_transactions, incomes, vat_records,
esign_field_templates, esign_field_template_items,
equipments, equipment_process, equipment_inspections, equipment_inspection_details,
equipment_inspection_templates, equipment_repairs,
business_income_payments, ai_configs,
biz_cert, cm_songs, construction_site_photos, construction_site_photo_rows,
admin_meeting_logs, meeting_minutes, meeting_minute_segments,
interview_knowledge;
SET FOREIGN_KEY_CHECKS = 1;
```
> **⚠️ 핵심 주의사항**:
> - 반드시 **1→2→3→4** 순서 (DB 먼저, 코드 나중)
> - 4단계(코드 배포) 전에 3단계(.env)까지 완료되어야 함
> - 5단계(samdb 삭제)는 4단계 동작 확인 후 선택적 수행
---
## 6. 아키텍처 다이어그램
```
React (사용자)
|
API 서버 (Laravel)
|
┌─────┴─────┐
| |
samdb sam_stat
(서비스 DB) (통계 DB)
|
| (공통 + API 사용 테이블: users, tenants, barobill_*, esign_*, audit_* 등)
|
MNG (관리자)
|
┌─────┴─────┐
| |
samdb codebridge
(공통 참조) (MNG 전용 59개)
```
- **React → API → samdb**: 서비스 트래픽 (수주, 견적, 생산, 바로빌, 전자서명 등)
- **MNG → samdb**: 공통 테이블 (users, tenants, menus) + API 사용 테이블 22개 참조
- **MNG → codebridge**: MNG 전용 데이터 (영업관리, 재무, 설비, PM 도구 등)
---
## 관련 문서
- [database/README.md](README.md) — DB 스키마 전체 현황
- [codebridge-db-separation-plan.md](/home/aweso/sam/docs/plans/codebridge-db-separation-plan.md) — 분리 작업 계획서 (plans/)
---
**최종 업데이트**: 2026-03-09 (운영 revert 반영, 적용 절차 5단계로 개정)