refactor: [structure] sam/ 하위 문서를 docs 루트로 재배치
- .gitignore를 sam/ 기반에서 루트 기반으로 변경 - sam/docs/ 하위 문서를 루트로 이동 (contracts, features, guides, plans 등) - sam/ 폴더 삭제 (docker, coocon 포함)
This commit is contained in:
48
.gitignore
vendored
48
.gitignore
vendored
@@ -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/
|
||||
|
||||
119
changes/20260303_gemini_model_upgrade.md
Normal file
119
changes/20260303_gemini_model_upgrade.md
Normal 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)
|
||||
165
changes/20260304_eaccount_infinite_loop_fix.md
Normal file
165
changes/20260304_eaccount_infinite_loop_fix.md
Normal 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
42
contracts/CHANGELOG.md
Normal 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)
|
||||
BIN
contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx
Normal file
BIN
contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx
Normal file
Binary file not shown.
BIN
contracts/docx/비밀유지서약서.docx
Executable file
BIN
contracts/docx/비밀유지서약서.docx
Executable file
Binary file not shown.
BIN
contracts/docx/영업파트너 위촉계약서(단체용).docx
Executable file
BIN
contracts/docx/영업파트너 위촉계약서(단체용).docx
Executable file
Binary file not shown.
BIN
contracts/docx/영업파트너 위촉계약서.docx
Executable file
BIN
contracts/docx/영업파트너 위촉계약서.docx
Executable file
Binary file not shown.
458
contracts/markdown/01-service-agreement.md
Normal file
458
contracts/markdown/01-service-agreement.md
Normal 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호
|
||||
|
||||
199
contracts/markdown/02-nda.md
Normal file
199
contracts/markdown/02-nda.md
Normal 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부씩 보관합니다.**
|
||||
- **※ 서약 위반 시 민·형사상 책임을 질 수 있습니다.**
|
||||
276
contracts/markdown/03-partner-agreement.md
Normal file
276
contracts/markdown/03-partner-agreement.md
Normal 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호
|
||||
267
contracts/markdown/04-partner-agreement-group.md
Normal file
267
contracts/markdown/04-partner-agreement-group.md
Normal 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
58
contracts/revisions.json
Normal 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": "버전 관리 시스템 도입, 개정이력 추적 시작"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
334
contracts/scripts/extract_to_markdown.py
Normal file
334
contracts/scripts/extract_to_markdown.py
Normal 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())
|
||||
263
contracts/scripts/sync_check.py
Normal file
263
contracts/scripts/sync_check.py
Normal 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())
|
||||
279
data/interview-master-questions.sql
Normal file
279
data/interview-master-questions.sql
Normal 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;
|
||||
316
dev/dev_plans/qms-api-integration-plan.md
Normal file
316
dev/dev_plans/qms-api-integration-plan.md
Normal 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
|
||||
369
features/academy/fire-shutter-image-prompts.md
Normal file
369
features/academy/fire-shutter-image-prompts.md
Normal 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
|
||||
298
features/approvals/README.md
Normal file
298
features/approvals/README.md
Normal 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
|
||||
594
features/approvals/api-reference.md
Normal file
594
features/approvals/api-reference.md
Normal 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
|
||||
286
features/approvals/db-changes-and-model-sync.md
Normal file
286
features/approvals/db-changes-and-model-sync.md
Normal 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
|
||||
999
features/approvals/form-types.md
Normal file
999
features/approvals/form-types.md
Normal 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
|
||||
381
features/approvals/ui-screens.md
Normal file
381
features/approvals/ui-screens.md
Normal 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
|
||||
565
features/approvals/workflows.md
Normal file
565
features/approvals/workflows.md
Normal 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
|
||||
250
features/barobill-kakaotalk/esign-notification-guide.md
Normal file
250
features/barobill-kakaotalk/esign-notification-guide.md
Normal 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
|
||||
173
features/business-card-request.md
Normal file
173
features/business-card-request.md
Normal 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
|
||||
284
features/credit-evaluation/README.md
Normal file
284
features/credit-evaluation/README.md
Normal 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
|
||||
738
features/documents/mng-document-system.md
Normal file
738
features/documents/mng-document-system.md
Normal 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
|
||||
826
features/documents/mng-document-template.md
Normal file
826
features/documents/mng-document-template.md
Normal 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
129
features/planning/README.md
Normal 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
|
||||
275
features/planning/construction-photos.md
Normal file
275
features/planning/construction-photos.md
Normal 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
|
||||
456
features/planning/meeting-minutes.md
Normal file
456
features/planning/meeting-minutes.md
Normal 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
|
||||
222
features/planning/planning-views.md
Normal file
222
features/planning/planning-views.md
Normal 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
110
features/rd/README.md
Normal 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
|
||||
246
features/rd/design-insight.md
Normal file
246
features/rd/design-insight.md
Normal 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
|
||||
366
features/rd/planning-design.md
Normal file
366
features/rd/planning-design.md
Normal 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
|
||||
325
guides/ai-config-settings.md
Normal file
325
guides/ai-config-settings.md
Normal 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
291
guides/ai-management.md
Normal 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
|
||||
313
guides/ai-model-update-workflow.md
Normal file
313
guides/ai-model-update-workflow.md
Normal 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
|
||||
387
guides/pptx-generation-guide.md
Normal file
387
guides/pptx-generation-guide.md
Normal 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
|
||||
247
guides/server-how-it-works.md
Normal file
247
guides/server-how-it-works.md
Normal 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
|
||||
486
guides/table-design-guide.md
Normal file
486
guides/table-design-guide.md
Normal 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
|
||||
737
plans/SAM_General_Rule_Storyboard_D1.0.md
Normal file
737
plans/SAM_General_Rule_Storyboard_D1.0.md
Normal 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
|
||||
928
plans/ai-quotation-engine-plan.md
Normal file
928
plans/ai-quotation-engine-plan.md
Normal 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
|
||||
284
plans/attendance-management-plan.md
Normal file
284
plans/attendance-management-plan.md
Normal 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
|
||||
706
plans/block-builder-evolution-plan.md
Normal file
706
plans/block-builder-evolution-plan.md
Normal 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
|
||||
611
plans/design-insight-menu-plan.md
Normal file
611
plans/design-insight-menu-plan.md
Normal 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
|
||||
753
plans/fire-shutter-drawing-generator-plan.md
Normal file
753
plans/fire-shutter-drawing-generator-plan.md
Normal 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
|
||||
637
plans/sound-logo-generator-plan.md
Normal file
637
plans/sound-logo-generator-plan.md
Normal 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
|
||||
317
projects/org-chart/README.md
Normal file
317
projects/org-chart/README.md
Normal 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
|
||||
157
projects/planning-design/README.md
Normal file
157
projects/planning-design/README.md
Normal 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
|
||||
BIN
rules/slides/usage-plan/SAM_활용방안.pptx
Normal file
BIN
rules/slides/usage-plan/SAM_활용방안.pptx
Normal file
Binary file not shown.
31
rules/slides/usage-plan/convert.cjs
Normal file
31
rules/slides/usage-plan/convert.cjs
Normal 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); });
|
||||
42
rules/slides/usage-plan/slide-01.html
Normal file
42
rules/slides/usage-plan/slide-01.html
Normal 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>
|
||||
58
rules/slides/usage-plan/slide-02.html
Normal file
58
rules/slides/usage-plan/slide-02.html
Normal 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인가? — 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 — 기존 방식</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 — 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>
|
||||
108
rules/slides/usage-plan/slide-03.html
Normal file
108
rules/slides/usage-plan/slide-03.html
Normal 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;">전체 프로세스 — 영업에서 출고까지</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;">→</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;">→</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;">→</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;">→</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;">→</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>
|
||||
88
rules/slides/usage-plan/slide-04.html
Normal file
88
rules/slides/usage-plan/slide-04.html
Normal 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% 공통화론 — 핵심 설득 논거</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% — 영업, 회계, 인사, 재고, 문서, 품질</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;">"상품만 바꾸면 새로운 제조업이 된다. 영업, 회계, 인사, 재고 — 이 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>
|
||||
74
rules/slides/usage-plan/slide-05.html
Normal file
74
rules/slides/usage-plan/slide-05.html
Normal 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;">멀티테넌시 — 하나의 플랫폼, 다수의 기업</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;">▼ ▼ ▼ ▼</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>
|
||||
68
rules/slides/usage-plan/slide-06.html
Normal file
68
rules/slides/usage-plan/slide-06.html
Normal 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 자동화 현황 & 로드맵</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>
|
||||
82
rules/slides/usage-plan/slide-07.html
Normal file
82
rules/slides/usage-plan/slide-07.html
Normal 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;">로드맵 & 비전</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>
|
||||
174
system/ai-automation-vision.md
Normal file
174
system/ai-automation-vision.md
Normal 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
|
||||
443
system/database/codebridge-separation.md
Normal file
443
system/database/codebridge-separation.md
Normal 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단계로 개정)
|
||||
Reference in New Issue
Block a user