docs: [bending] 절곡품 전용 테이블 분리 완료 문서

- README: bending_items 266건 + bending_models 62건 DB 검증 완료
- README: 하장바 검색 문제 해결 (10건 정상)
- README: bending_data JSON 통합, bending_item_mappings DROP
- README: LOT 코드 체계, 테이블 관계도, 레거시 대응표 갱신
- step1: 데이터분석 업데이트
- step5: canvas 그리기 추가
- .gitattributes CRLF→LF 정규화
This commit is contained in:
강영보
2026-03-19 20:03:46 +09:00
parent 6484e73976
commit 220ab78041
41 changed files with 16081 additions and 14435 deletions

18
.gitattributes vendored Normal file
View File

@@ -0,0 +1,18 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.pptx binary
*.xlsx binary
*.docx binary
*.zip binary
*.tar.gz binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.otf binary

352
WSL_MIGRATION_PLAN.md Normal file
View File

@@ -0,0 +1,352 @@
# WSL 내부 Docker 환경 마이그레이션 계획
## 1. 현재 구조 (문제점)
```
┌─ Windows 11 (NTFS) ─────────────────────────────────────┐
│ C:/work/@KD_SAM/SAM/ │
│ ├── api/ (406MB) 소스코드 │
│ ├── react/ (649MB) 소스 20MB + node_modules 629MB │
│ ├── mng/ (89MB) 소스코드 │
│ ├── 5130/ (799MB) 레거시 │
│ ├── docker/ (237MB) Docker 설정 + SSL │
│ └── docs/ (57MB) 문서 │
│ │
│ ──── 9P 프로토콜 (느림) ──── │
│ │
│ ┌─ Docker Desktop (WSL2 backend) ──────────────────┐ │
│ │ nginx, api, react, mng, legacy, mysql │ │
│ │ 바인드 마운트가 Windows NTFS 경유 → 느림 │ │
│ │ │ │
│ │ extra_hosts: api.sam.kr → host-gateway │ │
│ │ → React SSR API 호출이 Docker 밖으로 나감 │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
```
### 병목 3가지
| # | 병목 | 원인 | 영향 |
|---|------|------|------|
| 1 | **파일 I/O 느림** | Windows NTFS → WSL2 ext4 간 9P 프로토콜 | HMR 3~5초, npm install 수분 |
| 2 | **API 호출 우회** | extra_hosts로 Docker→WSL호스트→Docker 왕복 | API 호출마다 +10~20ms |
| 3 | **파일 감시 폴링** | inotify 불가 → 3초 폴링 | 코드 변경 반영 3초 지연 |
### 현재 시스템 사양
| 항목 | 값 |
|------|-----|
| RAM | 32GB |
| CPU | 20 논리 코어 |
| .wslconfig | 없음 (기본값) |
| WSL 배포판 | docker-desktop만 있음 |
| Docker | Docker Desktop (WSL2 backend) |
### 현재 DB 크기
| DB | 크기 |
|----|------|
| samdb | 177 MB |
| sam | 25 MB |
| chandj | 21 MB |
| 기타 (backup 등) | ~20 MB |
| **합계** | **~250 MB** |
### 현재 실행 중 컨테이너
| 컨테이너 | 상태 |
|----------|------|
| nginx | Running (26h) |
| mysql | Running (25h) |
| api | Running (19h) |
| mng | Running (26h) |
| legacy | Running (26h) |
| react | **Not Running** |
| phpmyadmin | **Not Running** |
---
## 2. 목표 구조
```
┌─ WSL2 Ubuntu 24.04 (ext4 네이티브) ─────────────────────┐
│ │
│ ~/sam/ │
│ ├── api/ ← git clone (네이티브 ext4) │
│ ├── react/ ← git clone (네이티브 ext4) │
│ ├── mng/ ← git clone (네이티브 ext4) │
│ ├── 5130/ ← 파일 복사 (git 없으면) │
│ ├── docker/ ← 복사 + 수정 │
│ └── docs/ ← 복사 │
│ │
│ ┌─ Docker Engine (WSL 네이티브) ───────────────────┐ │
│ │ 바인드 마운트 = 같은 ext4 → 네이티브 속도 │ │
│ │ │ │
│ │ 서비스 간 통신: Docker 내부 DNS 직접 연결 │ │
│ │ → extra_hosts 불필요 │ │
│ │ → 파일 폴링 불필요 (inotify 정상 작동) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Windows에서 접근: \\wsl$\Ubuntu-24.04\home\user\sam │
│ VSCode: Remote WSL 확장으로 편집 │
│ │
└───────────────────────────────────────────────────────────┘
```
### 개선 효과 예상
| 항목 | 현재 | 목표 | 개선폭 |
|------|------|------|--------|
| 파일 I/O | 9P (느림) | ext4 네이티브 | **5~10배** |
| Next.js HMR | 3~5초 | 0.3~1초 | **3~5배** |
| npm install | 3~5분 | 30초~1분 | **3~5배** |
| API 내부 호출 | Docker→WSL→Docker | Docker 내부 직접 | **2~3배** |
| 파일 감시 | 3초 폴링 | inotify 즉시 | **즉시 반영** |
---
## 3. 마이그레이션 단계
### Phase 1: WSL Ubuntu 설치 + 기본 설정 (10분)
```bash
# 1-1. Ubuntu 24.04 설치
wsl --install -d Ubuntu-24.04
# 1-2. 초기 설정 (Ubuntu 내부)
sudo apt update && sudo apt upgrade -y
# 1-3. 기본 도구 설치
sudo apt install -y git curl wget unzip
```
### Phase 2: Docker Engine 설치 (10분)
> Docker Desktop 대신 WSL 내부에 Docker Engine 직접 설치
```bash
# 2-1. Docker 공식 GPG 키 + 저장소 추가
sudo apt install -y ca-certificates gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 2-2. Docker Engine 설치
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 2-3. 현재 사용자를 docker 그룹에 추가
sudo usermod -aG docker $USER
# 2-4. Docker 시작
sudo service docker start
```
### Phase 3: .wslconfig 설정 (2분)
> Windows 측에서 WSL 리소스 할당
**파일**: `C:\Users\codeb\.wslconfig`
```ini
[wsl2]
memory=12GB
processors=8
swap=4GB
localhostForwarding=true
```
> 32GB RAM 중 12GB 할당 (Docker + 6개 컨테이너 충분)
### Phase 4: 소스코드 가져오기 (10~15분)
```bash
# 4-1. 디렉토리 구조 생성
mkdir -p ~/sam
# 4-2. Git clone (소스코드만, vendor/node_modules 제외)
cd ~/sam
git clone http://114.203.209.83:3000/SamProject/sam-api.git api
git clone http://114.203.209.83:3000/SamProject/sam-react.git react
git clone http://114.203.209.83:3000/SamProject/sam-manage.git mng
# 4-3. 각 저장소 브랜치 맞추기
cd ~/sam/api && git checkout develop
cd ~/sam/mng && git checkout sam-kkk
# 4-4. Git 사용자 설정
git config --global user.name "강영보"
git config --global user.email "sam-kkk@codebridge-x.com"
# 4-5. 레거시(5130) - Git 저장소가 없으면 Windows에서 복사
cp -r /mnt/c/work/@KD_SAM/SAM/5130 ~/sam/5130
# 4-6. Docker 설정 복사
cp -r /mnt/c/work/@KD_SAM/SAM/docker ~/sam/docker
# 4-7. 문서 복사
cp -r /mnt/c/work/@KD_SAM/SAM/docs ~/sam/docs
# 4-8. .env 파일 복사 (git에 포함 안 된 경우)
cp /mnt/c/work/@KD_SAM/SAM/api/.env ~/sam/api/.env
cp /mnt/c/work/@KD_SAM/SAM/react/.env ~/sam/react/.env
cp /mnt/c/work/@KD_SAM/SAM/mng/.env ~/sam/mng/.env
cp /mnt/c/work/@KD_SAM/SAM/5130/.env ~/sam/5130/.env 2>/dev/null
```
### Phase 5: Docker 설정 수정 (5분)
#### 5-1. docker-compose.yml 수정
**변경사항**:
| 항목 | 변경 전 | 변경 후 | 이유 |
|------|---------|---------|------|
| extra_hosts | `api.sam.kr:host-gateway` | **제거** | Docker 내부 DNS로 충분 |
| WATCHPACK_POLLING | `3000` | **제거** | ext4에서 inotify 정상 작동 |
| CHOKIDAR_USEPOLLING | `true` | **제거** | 폴링 불필요 |
| CHOKIDAR_INTERVAL | `3000` | **제거** | 폴링 불필요 |
#### 5-2. React 환경변수 수정 (SSR 내부 통신 최적화)
```yaml
react:
environment:
- NEXT_PUBLIC_API_URL=https://api.sam.kr # 브라우저용 (유지)
- API_URL_INTERNAL=http://api:9000 # SSR용 (추가)
# 아래 3줄 제거
# - WATCHPACK_POLLING=3000
# - CHOKIDAR_USEPOLLING=true
# - CHOKIDAR_INTERVAL=3000
# extra_hosts 제거
```
#### 5-3. Windows hosts 파일 유지
**`C:\Windows\System32\drivers\etc\hosts`** (기존과 동일):
```
127.0.0.1 dev.sam.kr api.sam.kr mng.sam.kr admin.sam.kr 5130.sam.kr
```
> WSL2의 localhostForwarding=true 덕분에 Windows에서 localhost로 WSL 포트 접근 가능
### Phase 6: DB 마이그레이션 (5~10분)
```bash
# 6-1. Windows Docker에서 DB 덤프 (Windows 측에서 실행)
# PowerShell 또는 Git Bash에서:
docker exec docker-mysql-1 mysqldump -uroot -proot --all-databases --routines --triggers > /mnt/c/temp/sam_db_dump.sql
# 6-2. WSL Ubuntu에서 Docker 컨테이너 시작 (MySQL만 먼저)
cd ~/sam/docker
docker compose up -d mysql
# MySQL 초기화 대기 (약 30초)
sleep 30
# 6-3. DB 덤프 복원
docker exec -i $(docker compose ps -q mysql) mysql -uroot -proot < /mnt/c/temp/sam_db_dump.sql
```
### Phase 7: 전체 서비스 빌드 + 실행 (10~15분)
```bash
cd ~/sam/docker
# 7-1. 이미지 빌드
docker compose build
# 7-2. 전체 서비스 시작
docker compose up -d
# 7-3. 상태 확인
docker compose ps
# 7-4. 로그 확인
docker compose logs -f --tail=50
```
### Phase 8: 검증 (5분)
| # | 확인 항목 | 방법 |
|---|----------|------|
| 1 | 서비스 실행 상태 | `docker compose ps` - 모든 컨테이너 Up |
| 2 | Nginx 접근 | 브라우저에서 `https://dev.sam.kr` 열기 |
| 3 | API 응답 | `curl -k https://api.sam.kr/api/health` |
| 4 | 관리자 접근 | `https://admin.sam.kr` 열기 |
| 5 | DB 연결 | phpMyAdmin (`http://localhost:8080`) |
| 6 | HMR 작동 | React 소스 수정 → 즉시 반영 확인 |
| 7 | 파일 감시 | `WATCHPACK_POLLING` 없이 변경 감지 확인 |
### Phase 9: Docker Desktop 정리 (선택)
```bash
# 기존 Docker Desktop 컨테이너 중지 (Windows에서)
docker compose -f C:/work/@KD_SAM/SAM/docker/docker-compose.yml down
# Docker Desktop 비활성화 또는 제거 (선택)
# → WSL Ubuntu Docker와 충돌 방지
```
---
## 4. 주의사항
### 포트 충돌
| 포트 | 용도 | 주의 |
|------|------|------|
| 80/443 | Nginx | Docker Desktop 먼저 중지해야 함 |
| 3306 | MySQL | 동시에 2개 MySQL 불가 |
| 8080 | phpMyAdmin | 충돌 가능 |
> **반드시 기존 Docker Desktop 컨테이너를 먼저 중지** 후 WSL Docker 시작
### VSCode 편집 환경
```
기존: VSCode에서 C:/work/@KD_SAM/SAM/ 직접 편집
변경: VSCode Remote WSL 확장 사용
- Ctrl+Shift+P → "WSL: Connect to WSL"
- ~/sam/ 폴더 열기
- 터미널도 WSL Ubuntu 내부에서 실행
```
### Git 작업 흐름
```
기존: Windows Git Bash에서 커밋/푸시
변경: WSL Ubuntu 터미널에서 커밋/푸시 (동일한 원격 저장소)
- 또는 VSCode Remote WSL의 Git 기능 사용
```
### 백업/롤백 계획
```
Windows 원본 소스: C:/work/@KD_SAM/SAM/ (그대로 보존)
Windows Docker: 컨테이너 중지만 (삭제 안 함)
DB 덤프: C:/temp/sam_db_dump.sql (보존)
문제 시 롤백:
1. WSL Docker 중지: docker compose down
2. Windows Docker 재시작: docker compose up -d
→ 5분 이내 원복 가능
```
---
## 5. 일정 요약
| Phase | 작업 | 소요 시간 |
|-------|------|----------|
| 1 | Ubuntu 설치 + 기본 설정 | 10분 |
| 2 | Docker Engine 설치 | 10분 |
| 3 | .wslconfig 설정 | 2분 |
| 4 | 소스코드 가져오기 | 10~15분 |
| 5 | Docker 설정 수정 | 5분 |
| 6 | DB 마이그레이션 | 5~10분 |
| 7 | 빌드 + 실행 | 10~15분 |
| 8 | 검증 | 5분 |
| 9 | Docker Desktop 정리 | 3분 |
| **합계** | | **약 60~75분** |
> 롤백 가능하므로 리스크 낮음. Windows 원본은 그대로 보존.

File diff suppressed because one or more lines are too long

View File

@@ -1,133 +1,133 @@
# 자금일보 바로빌 자동동기화 및 계정과목 데이터 정리
**날짜:** 2026-03-11
**작업자:** Claude Code
---
## 변경 개요
두 가지 문제를 수정한다:
1. **자금일보 출금 내역 누락**`periodReport()`가 DB 캐시만 조회하고 바로빌 API 동기화를 트리거하지 않아, 최신 거래내역이 반영되지 않는 문제
2. **홈택스 분개 계정과목 오류** — 드롭다운에 2,549개 코드 표시(정상: 163개), 분개 기본값에 존재하지 않는 코드 사용
---
## 수정된 파일
| 파일 | 프로젝트 | 변경 내용 |
|------|---------|----------|
| `app/Services/Barobill/BarobillBankSyncService.php` | MNG (신규) | 바로빌 계좌 거래내역 동기화 서비스 |
| `app/Http/Controllers/Finance/DailyFundController.php` | MNG | `periodReport()`에 자동 동기화 호출 추가 |
| `resources/views/barobill/hometax/index.blade.php` | MNG | 분개 기본 계정과목 코드 수정 |
| `database/migrations/2026_03_11_101502_fix_account_codes_duplicate_data.php` | API (신규) | 중복 계정과목 비활성화 + 분개 코드 일괄 수정 |
---
## 상세 변경 사항
### 1. 바로빌 자동 동기화 서비스 (MNG)
**문제**: `DailyFundController::periodReport()``barobill_bank_transactions` 테이블만 조회한다. 바로빌 API에서 데이터를 가져오는 동기화는 `EaccountController`에서만 수행되어, 자금일보 페이지에서는 캐시가 갱신되지 않으면 최신 거래가 누락된다.
**해결**: `EaccountController`의 동기화 로직을 `BarobillBankSyncService`로 분리하여 재사용 가능하게 한다.
```
DailyFundController::periodReport()
├── BarobillBankSyncService::syncIfNeeded() ← 신규
│ ├── BarobillMember 조회 (바로빌 인증)
│ ├── SOAP 클라이언트 초기화
│ ├── 등록 계좌 목록 조회
│ └── 월별 청크 순회
│ ├── BankSyncStatus 캐시 판단
│ │ ├── 과거 월: 항상 캐시 (API 호출 안 함)
│ │ └── 현재 월: 10분 이내면 캐시
│ └── 필요 시 API 호출 → DB 캐시 저장
└── DB에서 거래내역 조회 (기존 로직)
```
**캐시 정책**:
| 조건 | 동작 |
|------|------|
| 과거 월 + 동기화 이력 있음 | 캐시 사용 (API 호출 안 함) |
| 현재 월 + 10분 이내 동기화 | 캐시 사용 |
| 현재 월 + 10분 초과 | API에서 갱신 |
| 동기화 이력 없음 | API에서 갱신 |
**실패 처리**: 동기화 실패 시 예외를 catch하고 로그만 남기며, 기존 DB 캐시로 응답을 계속한다.
---
### 2. 계정과목 중복 데이터 정리 (API 마이그레이션)
**문제**: `account_codes` 테이블에 비표준 코드가 대량 등록되어 드롭다운이 오염되었다.
| 코드 유형 | 건수 | 예시 | 상태 |
|----------|------|------|------|
| 3자리 더존 표준 코드 | 163개 | `101` 현금, `108` 외상매출금 | ✅ 정상 |
| 5자리 KIS 코드 (중복) | ~2,290개 | `10100` Cash, `10800` Accounts Receivable | ❌ 비활성화 |
| 1~2자리 카테고리 헤더 | ~96개 | `1` Assets, `10` Current Assets | ❌ 비활성화 |
**해결**: `LENGTH(code) != 3`인 코드를 `is_active = false`로 비활성화한다. 데이터는 삭제하지 않으며 필요 시 복원 가능하다.
---
### 3. 홈택스 분개 기본 코드 수정
**문제**: `getDefaultLines()` 함수에서 하드코딩된 계정과목 코드가 실제 DB 코드와 불일치한다.
| 거래 유형 | 항목 | 기존 코드 | 수정 코드 | 비고 |
|----------|------|----------|----------|------|
| 매출 | 부가세예수금 | `255` (장기미지급금) | `208` | 코드 불일치 |
| 매입 | 부가세대급금 | `135` (미존재) | `117` | DB에 없는 코드 |
| 매입 | 외상매입금 | `251` (장기차입금) | `201` | 코드 불일치 |
| 매입 | 적요명 | 상품매입 | 상품매출원가 | `501` 코드에 맞는 명칭 |
**API 마이그레이션으로 기존 분개 데이터도 일괄 수정**:
```sql
-- 135 → 117 (부가세대급금)
UPDATE hometax_invoice_journals SET account_code='117', account_name='부가세대급금' WHERE account_code='135';
-- 251 → 201 (외상매입금)
UPDATE hometax_invoice_journals SET account_code='201' WHERE account_code='251' AND account_name='외상매입금';
-- 255 → 208 (부가세예수금)
UPDATE hometax_invoice_journals SET account_code='208' WHERE account_code='255' AND account_name='부가세예수금';
```
---
## 배포
| 프로젝트 | 커밋 | develop | main |
|---------|------|---------|------|
| MNG | `ca36e8e5` (동기화 서비스), `afa64280` (계정과목 수정) | ✅ 푸시 완료 | ✅ 체리픽 완료 |
| API | `6f48b86` (데이터 마이그레이션) | ✅ 푸시 완료 | ✅ 체리픽 완료 |
Jenkins가 양쪽 서버에서 자동 배포 및 마이그레이션 실행을 완료했다.
---
## 테스트 체크리스트
- [x] 로컬 DB에서 `account_codes` 비표준 코드 비활성화 확인
- [x] 바로빌 동기화 후 2026-03-10 거래내역 10건 정상 조회
- [x] 홈택스 분개 기본값에 올바른 코드(`117`, `201`, `208`) 반영
- [x] 개발 서버 마이그레이션 실행 확인
- [x] 운영 서버 마이그레이션 자동 실행 확인
---
## 관련 문서
- [재무 관리](../../features/finance/README.md)
- [DB 스키마 - 재무](../../system/database/finance.md)
---
**최종 업데이트**: 2026-03-11
# 자금일보 바로빌 자동동기화 및 계정과목 데이터 정리
**날짜:** 2026-03-11
**작업자:** Claude Code
---
## 변경 개요
두 가지 문제를 수정한다:
1. **자금일보 출금 내역 누락**`periodReport()`가 DB 캐시만 조회하고 바로빌 API 동기화를 트리거하지 않아, 최신 거래내역이 반영되지 않는 문제
2. **홈택스 분개 계정과목 오류** — 드롭다운에 2,549개 코드 표시(정상: 163개), 분개 기본값에 존재하지 않는 코드 사용
---
## 수정된 파일
| 파일 | 프로젝트 | 변경 내용 |
|------|---------|----------|
| `app/Services/Barobill/BarobillBankSyncService.php` | MNG (신규) | 바로빌 계좌 거래내역 동기화 서비스 |
| `app/Http/Controllers/Finance/DailyFundController.php` | MNG | `periodReport()`에 자동 동기화 호출 추가 |
| `resources/views/barobill/hometax/index.blade.php` | MNG | 분개 기본 계정과목 코드 수정 |
| `database/migrations/2026_03_11_101502_fix_account_codes_duplicate_data.php` | API (신규) | 중복 계정과목 비활성화 + 분개 코드 일괄 수정 |
---
## 상세 변경 사항
### 1. 바로빌 자동 동기화 서비스 (MNG)
**문제**: `DailyFundController::periodReport()``barobill_bank_transactions` 테이블만 조회한다. 바로빌 API에서 데이터를 가져오는 동기화는 `EaccountController`에서만 수행되어, 자금일보 페이지에서는 캐시가 갱신되지 않으면 최신 거래가 누락된다.
**해결**: `EaccountController`의 동기화 로직을 `BarobillBankSyncService`로 분리하여 재사용 가능하게 한다.
```
DailyFundController::periodReport()
├── BarobillBankSyncService::syncIfNeeded() ← 신규
│ ├── BarobillMember 조회 (바로빌 인증)
│ ├── SOAP 클라이언트 초기화
│ ├── 등록 계좌 목록 조회
│ └── 월별 청크 순회
│ ├── BankSyncStatus 캐시 판단
│ │ ├── 과거 월: 항상 캐시 (API 호출 안 함)
│ │ └── 현재 월: 10분 이내면 캐시
│ └── 필요 시 API 호출 → DB 캐시 저장
└── DB에서 거래내역 조회 (기존 로직)
```
**캐시 정책**:
| 조건 | 동작 |
|------|------|
| 과거 월 + 동기화 이력 있음 | 캐시 사용 (API 호출 안 함) |
| 현재 월 + 10분 이내 동기화 | 캐시 사용 |
| 현재 월 + 10분 초과 | API에서 갱신 |
| 동기화 이력 없음 | API에서 갱신 |
**실패 처리**: 동기화 실패 시 예외를 catch하고 로그만 남기며, 기존 DB 캐시로 응답을 계속한다.
---
### 2. 계정과목 중복 데이터 정리 (API 마이그레이션)
**문제**: `account_codes` 테이블에 비표준 코드가 대량 등록되어 드롭다운이 오염되었다.
| 코드 유형 | 건수 | 예시 | 상태 |
|----------|------|------|------|
| 3자리 더존 표준 코드 | 163개 | `101` 현금, `108` 외상매출금 | ✅ 정상 |
| 5자리 KIS 코드 (중복) | ~2,290개 | `10100` Cash, `10800` Accounts Receivable | ❌ 비활성화 |
| 1~2자리 카테고리 헤더 | ~96개 | `1` Assets, `10` Current Assets | ❌ 비활성화 |
**해결**: `LENGTH(code) != 3`인 코드를 `is_active = false`로 비활성화한다. 데이터는 삭제하지 않으며 필요 시 복원 가능하다.
---
### 3. 홈택스 분개 기본 코드 수정
**문제**: `getDefaultLines()` 함수에서 하드코딩된 계정과목 코드가 실제 DB 코드와 불일치한다.
| 거래 유형 | 항목 | 기존 코드 | 수정 코드 | 비고 |
|----------|------|----------|----------|------|
| 매출 | 부가세예수금 | `255` (장기미지급금) | `208` | 코드 불일치 |
| 매입 | 부가세대급금 | `135` (미존재) | `117` | DB에 없는 코드 |
| 매입 | 외상매입금 | `251` (장기차입금) | `201` | 코드 불일치 |
| 매입 | 적요명 | 상품매입 | 상품매출원가 | `501` 코드에 맞는 명칭 |
**API 마이그레이션으로 기존 분개 데이터도 일괄 수정**:
```sql
-- 135 → 117 (부가세대급금)
UPDATE hometax_invoice_journals SET account_code='117', account_name='부가세대급금' WHERE account_code='135';
-- 251 → 201 (외상매입금)
UPDATE hometax_invoice_journals SET account_code='201' WHERE account_code='251' AND account_name='외상매입금';
-- 255 → 208 (부가세예수금)
UPDATE hometax_invoice_journals SET account_code='208' WHERE account_code='255' AND account_name='부가세예수금';
```
---
## 배포
| 프로젝트 | 커밋 | develop | main |
|---------|------|---------|------|
| MNG | `ca36e8e5` (동기화 서비스), `afa64280` (계정과목 수정) | ✅ 푸시 완료 | ✅ 체리픽 완료 |
| API | `6f48b86` (데이터 마이그레이션) | ✅ 푸시 완료 | ✅ 체리픽 완료 |
Jenkins가 양쪽 서버에서 자동 배포 및 마이그레이션 실행을 완료했다.
---
## 테스트 체크리스트
- [x] 로컬 DB에서 `account_codes` 비표준 코드 비활성화 확인
- [x] 바로빌 동기화 후 2026-03-10 거래내역 10건 정상 조회
- [x] 홈택스 분개 기본값에 올바른 코드(`117`, `201`, `208`) 반영
- [x] 개발 서버 마이그레이션 실행 확인
- [x] 운영 서버 마이그레이션 자동 실행 확인
---
## 관련 문서
- [재무 관리](../../features/finance/README.md)
- [DB 스키마 - 재무](../../system/database/finance.md)
---
**최종 업데이트**: 2026-03-11

View File

@@ -1,136 +1,136 @@
# 전자서명 체크박스, 전표 적요 동기화, 거래처 드롭다운, 바로빌 중복 키 수정
**날짜:** 2026-03-11
**작업자:** Claude Code
---
## 변경 개요
네 가지 개선/수정 사항:
1. **전자서명 템플릿 체크박스** — 체크박스 필드에 변수 연결 UI를 추가했다가, "배치 위치에 무조건 체크 표시" 방식으로 단순화
2. **전표 적요 → 자금일보 동기화** — 일반전표 적요 수정 시 일일자금일보에 반영되지 않던 문제 해결
3. **거래처 드롭다운 클릭 버그** — 다른 요소에서 포커스 이동 후 클릭 시 드롭다운이 즉시 닫히는 문제 해결
4. **바로빌 은행거래 중복 키 에러**`EaccountController` 동기화 시 `insert``insertOrIgnore` 변경
---
## 수정된 파일
| 파일 | 프로젝트 | 변경 내용 |
|------|---------|----------|
| `resources/views/esign/template-fields.blade.php` | MNG | 체크박스 필드 속성 패널에 안내 문구 + PDF 오버레이에 ☑ 표시 |
| `app/Http/Controllers/Finance/JournalEntryController.php` | MNG | `update()``BankTransactionOverride` 동기화 추가 |
| `resources/views/finance/journal-entries.blade.php` | MNG | `TradingPartnerSelect``justFocusedRef` 플래그 추가 |
| `app/Http/Controllers/Barobill/EaccountController.php` | MNG | `insert``insertOrIgnore` 변경 |
---
## 상세 변경 사항
### 1. 전자서명 템플릿 체크박스 단순화
**문제**: 체크박스 필드를 템플릿에 배치할 때 변수 연결 드롭다운이 표시되었으나, 선택 가능한 체크박스 변수가 없어 사용 불가.
**해결**: 체크박스는 "이 위치에 체크 표시를 넣겠다"는 의미이므로 변수 연결 자체가 불필요. 다음과 같이 단순화:
- 변수 연결 UI 제거 → "☑ 이 위치에 체크 표시가 렌더링됩니다" 안내 문구 표시
- PDF 오버레이에서 체크박스 필드는 ☑ 아이콘으로 시각적 표시
- 커스텀 변수의 체크박스 타입 옵션 제거
```
체크박스 필드 배치 → 해당 위치에 무조건 ☑ 렌더링
(변수 연결 불필요, 위치 정보만 저장)
```
---
### 2. 전표 적요 수정 → 자금일보 반영
**문제**: 일반전표의 적요를 수정하면 `journal_entries.description`만 업데이트되고, 일일자금일보가 참조하는 `barobill_bank_transactions.summary`는 변경되지 않음.
```
JournalEntry.description 수정
↓ (기존: 연결 없음)
일일자금일보 → barobill_bank_transactions.summary (이전 값 그대로)
```
**해결**: `JournalEntryController::update()` 트랜잭션 안에서, `source_type = 'bank_transaction'`인 전표의 적요 수정 시 `BankTransactionOverride``modified_summary`를 저장.
```
JournalEntry.description 수정
↓ (신규: 자동 동기화)
BankTransactionOverride.modified_summary 저장
일일자금일보 periodReport() → override 적용 → 수정된 적요 표시
```
**기존 `modified_cast` 보존**: override 저장 시 기존 `modified_cast` 값을 조회하여 유지.
---
### 3. 거래처 드롭다운 클릭 버그 수정
**문제**: `TradingPartnerSelect` 컴포넌트에서 다른 요소에 포커스가 있을 때 클릭하면 드롭다운이 열렸다가 즉시 닫힘.
**원인**: 이벤트 순서 — `onFocus` → 드롭다운 열림 → `onClick``setIsOpen(!isOpen)` 토글로 다시 닫힘. React 렌더 타이밍에 따라 `onClick``isOpen = true` 상태에서 실행되어 `false`로 전환.
**해결**: `justFocusedRef` 플래그 추가.
```javascript
onFocus justFocusedRef = true, setIsOpen(true)
onClick justFocusedRef가 true면 토글 건너뜀 (이미 열림)
justFocusedRef가 false면 정상 토글 (이미 포커스된 상태에서 클릭)
```
---
### 4. 바로빌 은행거래 동기화 중복 키 에러
**문제**: `EaccountController`의 거래내역 저장 시 `Duplicate entry` 에러 발생.
**원인**: 기존 레코드 조회 WHERE에 `summary`를 포함하지만, DB unique index(`barobill_bank_trans_unique`)에는 `summary`가 없음.
| 구분 | 포함 컬럼 |
|------|----------|
| WHERE 조회 | `tenant_id`, `bank_account_num`, `trans_dt`, `deposit`, `withdraw`, `balance`, **`summary`** |
| DB unique index | `tenant_id`, `bank_account_num`, `trans_dt`, `deposit`, `withdraw`, `balance` |
같은 거래인데 `summary`만 다른 경우(전각/반각 문자 차이 등) → WHERE에서 기존 레코드 못 찾음 → INSERT 시도 → unique index 위반.
**해결**: `DB::table()->insert()``DB::table()->insertOrIgnore()` 변경.
---
## 배포
| 커밋 | 내용 | develop | main |
|------|------|---------|------|
| `f11b1238` | 체크박스 변수 연결 추가 | ✅ | ✅ |
| `4f033172` | 체크박스 단순화 | ✅ | ✅ |
| `a97396df` | 전표 적요 → 자금일보 동기화 | ✅ | ✅ |
| `0be1fe7a` | 거래처 드롭다운 버그 수정 | ✅ | ✅ |
| `2d3f915a` | 바로빌 중복 키 수정 | ✅ | ✅ |
---
## 테스트 체크리스트
- [x] 전자서명 템플릿에서 체크박스 필드 배치 시 ☑ 안내 표시
- [x] 일반전표 적요 수정 후 저장 → 자금일보에서 수정된 적요 반영
- [x] 거래처 드롭다운을 마우스 클릭으로 열기 정상 동작
- [x] Tab 키로 거래처 이동 시 자동 열림 정상 동작
- [x] 바로빌 동기화 시 중복 거래에서 에러 없이 처리
---
## 관련 문서
- [전자서명](../../features/esign/README.md)
- [재무 관리](../../features/finance/README.md)
- [자금일보 동기화 변경](20260311_daily_fund_sync_and_account_codes_fix.md)
---
**최종 업데이트**: 2026-03-11
# 전자서명 체크박스, 전표 적요 동기화, 거래처 드롭다운, 바로빌 중복 키 수정
**날짜:** 2026-03-11
**작업자:** Claude Code
---
## 변경 개요
네 가지 개선/수정 사항:
1. **전자서명 템플릿 체크박스** — 체크박스 필드에 변수 연결 UI를 추가했다가, "배치 위치에 무조건 체크 표시" 방식으로 단순화
2. **전표 적요 → 자금일보 동기화** — 일반전표 적요 수정 시 일일자금일보에 반영되지 않던 문제 해결
3. **거래처 드롭다운 클릭 버그** — 다른 요소에서 포커스 이동 후 클릭 시 드롭다운이 즉시 닫히는 문제 해결
4. **바로빌 은행거래 중복 키 에러**`EaccountController` 동기화 시 `insert``insertOrIgnore` 변경
---
## 수정된 파일
| 파일 | 프로젝트 | 변경 내용 |
|------|---------|----------|
| `resources/views/esign/template-fields.blade.php` | MNG | 체크박스 필드 속성 패널에 안내 문구 + PDF 오버레이에 ☑ 표시 |
| `app/Http/Controllers/Finance/JournalEntryController.php` | MNG | `update()``BankTransactionOverride` 동기화 추가 |
| `resources/views/finance/journal-entries.blade.php` | MNG | `TradingPartnerSelect``justFocusedRef` 플래그 추가 |
| `app/Http/Controllers/Barobill/EaccountController.php` | MNG | `insert``insertOrIgnore` 변경 |
---
## 상세 변경 사항
### 1. 전자서명 템플릿 체크박스 단순화
**문제**: 체크박스 필드를 템플릿에 배치할 때 변수 연결 드롭다운이 표시되었으나, 선택 가능한 체크박스 변수가 없어 사용 불가.
**해결**: 체크박스는 "이 위치에 체크 표시를 넣겠다"는 의미이므로 변수 연결 자체가 불필요. 다음과 같이 단순화:
- 변수 연결 UI 제거 → "☑ 이 위치에 체크 표시가 렌더링됩니다" 안내 문구 표시
- PDF 오버레이에서 체크박스 필드는 ☑ 아이콘으로 시각적 표시
- 커스텀 변수의 체크박스 타입 옵션 제거
```
체크박스 필드 배치 → 해당 위치에 무조건 ☑ 렌더링
(변수 연결 불필요, 위치 정보만 저장)
```
---
### 2. 전표 적요 수정 → 자금일보 반영
**문제**: 일반전표의 적요를 수정하면 `journal_entries.description`만 업데이트되고, 일일자금일보가 참조하는 `barobill_bank_transactions.summary`는 변경되지 않음.
```
JournalEntry.description 수정
↓ (기존: 연결 없음)
일일자금일보 → barobill_bank_transactions.summary (이전 값 그대로)
```
**해결**: `JournalEntryController::update()` 트랜잭션 안에서, `source_type = 'bank_transaction'`인 전표의 적요 수정 시 `BankTransactionOverride``modified_summary`를 저장.
```
JournalEntry.description 수정
↓ (신규: 자동 동기화)
BankTransactionOverride.modified_summary 저장
일일자금일보 periodReport() → override 적용 → 수정된 적요 표시
```
**기존 `modified_cast` 보존**: override 저장 시 기존 `modified_cast` 값을 조회하여 유지.
---
### 3. 거래처 드롭다운 클릭 버그 수정
**문제**: `TradingPartnerSelect` 컴포넌트에서 다른 요소에 포커스가 있을 때 클릭하면 드롭다운이 열렸다가 즉시 닫힘.
**원인**: 이벤트 순서 — `onFocus` → 드롭다운 열림 → `onClick``setIsOpen(!isOpen)` 토글로 다시 닫힘. React 렌더 타이밍에 따라 `onClick``isOpen = true` 상태에서 실행되어 `false`로 전환.
**해결**: `justFocusedRef` 플래그 추가.
```javascript
onFocus justFocusedRef = true, setIsOpen(true)
onClick justFocusedRef가 true면 토글 건너뜀 (이미 열림)
justFocusedRef가 false면 정상 토글 (이미 포커스된 상태에서 클릭)
```
---
### 4. 바로빌 은행거래 동기화 중복 키 에러
**문제**: `EaccountController`의 거래내역 저장 시 `Duplicate entry` 에러 발생.
**원인**: 기존 레코드 조회 WHERE에 `summary`를 포함하지만, DB unique index(`barobill_bank_trans_unique`)에는 `summary`가 없음.
| 구분 | 포함 컬럼 |
|------|----------|
| WHERE 조회 | `tenant_id`, `bank_account_num`, `trans_dt`, `deposit`, `withdraw`, `balance`, **`summary`** |
| DB unique index | `tenant_id`, `bank_account_num`, `trans_dt`, `deposit`, `withdraw`, `balance` |
같은 거래인데 `summary`만 다른 경우(전각/반각 문자 차이 등) → WHERE에서 기존 레코드 못 찾음 → INSERT 시도 → unique index 위반.
**해결**: `DB::table()->insert()``DB::table()->insertOrIgnore()` 변경.
---
## 배포
| 커밋 | 내용 | develop | main |
|------|------|---------|------|
| `f11b1238` | 체크박스 변수 연결 추가 | ✅ | ✅ |
| `4f033172` | 체크박스 단순화 | ✅ | ✅ |
| `a97396df` | 전표 적요 → 자금일보 동기화 | ✅ | ✅ |
| `0be1fe7a` | 거래처 드롭다운 버그 수정 | ✅ | ✅ |
| `2d3f915a` | 바로빌 중복 키 수정 | ✅ | ✅ |
---
## 테스트 체크리스트
- [x] 전자서명 템플릿에서 체크박스 필드 배치 시 ☑ 안내 표시
- [x] 일반전표 적요 수정 후 저장 → 자금일보에서 수정된 적요 반영
- [x] 거래처 드롭다운을 마우스 클릭으로 열기 정상 동작
- [x] Tab 키로 거래처 이동 시 자동 열림 정상 동작
- [x] 바로빌 동기화 시 중복 거래에서 에러 없이 처리
---
## 관련 문서
- [전자서명](../../features/esign/README.md)
- [재무 관리](../../features/finance/README.md)
- [자금일보 동기화 변경](20260311_daily_fund_sync_and_account_codes_fix.md)
---
**최종 업데이트**: 2026-03-11

View File

@@ -1,327 +1,327 @@
# API 품질 개선 — 테스트 인프라 + 56개 테스트 + N+1 최적화
**날짜:** 2026-03-14
**작업자:** R&D 개발실장 + Claude Code
**배포 대상:** 개발 서버 (API develop 브랜치)
---
## 변경 개요
API 프로젝트의 기술 부채 분석 결과(D1~D2)에 따라 **테스트 커버리지 확충**과 **N+1 쿼리 최적화**를 수행했다. 비즈니스 핵심 흐름(수주→재고→결재→작업지시)에 대한 안전망을 확보하고, 대량 처리 시 쿼리 95%를 절감했다.
---
## 1. 왜 이 작업을 했는가 (근거)
### 1.1 기술 부채 분석 (근거 문서)
`system/api-analysis-report.md`에서 식별한 8건의 기술 부채 중 최우선 2건을 착수했다.
| ID | 영역 | 현황 (수정 전) | 영향도 |
|:--:|------|-------------|:------:|
| **D1** | 테스트 부재 | 165개 (1,400 EP 대비 부족), 핵심 도메인 미커버 | 높음 |
| **D2** | N+1 쿼리 | 루프 내 개별 DB 조회 3건 발견 | 높음 |
### 1.2 D1이 먼저인 이유
테스트가 없으면 코드 수정 후 "고쳐도 안전한가?"를 검증할 수 없다. D2(N+1 최적화) 같은 성능 개선을 안전하게 수행하려면 테스트 안전망이 선행되어야 한다.
### 1.3 D2 수정 대상 선정 근거
`app/Services/` 전체를 정적 분석하여 **foreach 루프 안에서 DB 쿼리를 실행하는 패턴**을 검색했다. 발견된 3건 모두 데이터 양에 비례하여 쿼리가 선형 증가하는 구조였다.
---
## 2. D1: 테스트 커버리지 확충
### 2.1 테스트 인프라 정비
기존 11개 테스트 파일이 동일한 setUp 코드(약 40줄)를 매번 복붙하고 있었다.
**수정 내용:**
| 파일 | 변경 | 이유 |
|------|------|------|
| `tests/TestCase.php` | 공통 메서드 4개 추가 | 중복 setUp 코드 제거, 신규 테스트 작성 속도 향상 |
| 기존 테스트 11개 | `private` 프로퍼티 → TestCase 상속 | TestCase 공통화에 따른 호환성 |
**추가된 공통 메서드:**
| 메서드 | 역할 |
|--------|------|
| `setUpAuthenticatedUser()` | API Key + Tenant + User + 로그인 토큰 일괄 생성 |
| `api($method, $uri, $data)` | 인증된 API 요청 헬퍼 |
| `assertApiSuccess($response)` | 표준 응답 구조 검증 |
| `assertApiPaginated($response)` | 페이지네이션 응답 검증 |
### 2.2 Factory 생성
테스트 데이터를 간편하게 생성하기 위해 Factory 5개를 추가했다.
| Factory | 모델 | 이유 |
|---------|------|------|
| `TenantFactory` | Tenant | 모든 테스트의 기본 |
| `ClientFactory` | Client | 수주 테스트에 거래처 필요 |
| `OrderFactory` | Order | 수주 CRUD + 상태전이 테스트 |
| `StockFactory` | Stock | 재고 FIFO 테스트 |
| `StockLotFactory` | StockLot | LOT 단위 입출고 테스트 |
### 2.3 신규 테스트 56개
| 도메인 | 파일 | 테스트 수 | 검증 내용 |
|--------|------|:--------:|---------|
| **수주 (Order)** | `tests/Feature/Orders/OrderApiTest.php` | 12 | CRUD, 상태변경(DRAFT→CONFIRMED→CANCELLED), 일괄삭제, 인증 |
| **재고 (Stock)** | `tests/Feature/Inventory/StockApiTest.php` | 13 | API 목록/통계, FIFO 차감, LOT 걸침 처리, 예약/해제, 거래이력, 상태 자동계산 |
| **결재 (Approval)** | `tests/Feature/Approval/ApprovalApiTest.php` | 15 | CRUD, 상신→승인/반려/회수 워크플로우, 결재자 별도 로그인, 결재함/참조함/완료함 |
| **작업지시 (WorkOrder)** | `tests/Feature/Production/WorkOrderApiTest.php` | 16 | CRUD, 상태전이 4단계(미배정→대기→준비→진행→완료), 담당자배정, 공정단계, 자재조회 |
**커버된 핵심 비즈니스 흐름:**
```
견적 → 수주(12) → 재고예약(13) → 작업지시(16) → 결재(15)
FIFO 검증 상태전이 검증 워크플로우 검증
```
### 2.4 테스트 실행 결과
```
수정 전: 165개 테스트
수정 후: 221개 테스트 (+56개, +34%)
최종 실행: 164개 통과 / 3개 Skip (기존 라우트 충돌)
실행 시간: ~12초
```
### 2.5 테스트 중 발견된 문제
| 발견 | 내용 | 후속 조치 |
|------|------|----------|
| 빈 데이터 수주 생성 허용 | `POST /api/v1/orders` 에 빈 body 전송 시 200 반환 | `StoreOrderRequest` 검증 강화 필요 (D4) |
| 기존 테스트 실패 3건 | `PrefixResolverTest`, `BendingLotPipelineTest` — 이번 변경과 무관 | 별도 수정 필요 |
| `ItemMasterApiTest` 에러 | `section_id` 컬럼 미존재 — 마이그레이션 불일치 | 별도 수정 필요 |
---
## 3. D2: N+1 쿼리 최적화
### 3.1 수정 대상 3건
| # | 파일 | 메서드 | 문제 | 쿼리 수 (수정 전) |
|:-:|------|--------|------|:-----------------:|
| 1 | `WorkOrderService.php` | `getMaterials()` | 루프 내 `Item::find()` + 중첩 루프 내 `Item::find()` | 1 + N + M |
| 2 | `OrderService.php` | `createWorkOrderFromOrder()` | 루프 내 `DB::table('items')->value()` + `DB::table('process_items')->value()` | 1 + 2N |
| 3 | `OrderService.php` | `checkBendingStockForOrder()` | 루프 내 `StockService::getAvailableStock()` 개별 호출 | 1 + N |
### 3.2 수정 방법 — 배치 사전 조회 패턴
모든 수정에 동일한 패턴을 적용했다:
```
수정 전: foreach (items) { DB::find(id); } ← N+1
수정 후: map = DB::whereIn(ids)->keyBy('id'); ← 1회 배치
foreach (items) { map[id]; } ← 메모리 참조
```
### 3.3 수정 상세
**수정 1: `WorkOrderService::getMaterials()` (라인 1470~1500)**
```php
// 수정 전: 루프 안에서 개별 조회
foreach ($workOrder->items as $woItem) {
$item = Item::find($woItem->item_id); // N+1
foreach ($item->bom as $bomItem) {
$childItem = Item::find($childItemId); // N+1 (중첩)
}
}
// 수정 후: 루프 전 배치 조회
$bomItemsMap = Item::whereIn('id', $parentIds)->get()->keyBy('id');
$bomChildItemsMap = Item::whereIn('id', $childIds)->get()->keyBy('id');
foreach ($workOrder->items as $woItem) {
$item = $bomItemsMap[$woItem->item_id]; // 메모리 참조
foreach ($item->bom as $bomItem) {
$childItem = $bomChildItemsMap[$childItemId]; // 메모리 참조
}
}
```
**수정 2: `OrderService::createWorkOrderFromOrder()` (라인 1239~1297)**
```php
// 수정 전: fallback에서 루프마다 DB 쿼리 x2
foreach ($order->items as $orderItem) {
$resolvedId = DB::table('items')->where('code', $code)->value('id'); // N+1
$pi = DB::table('process_items')->where('item_id', $id)->value('pid'); // N+1
}
// 수정 후: 루프 전 모든 item_code, process_items 일괄 조회
$codeToIdMap = DB::table('items')->whereIn('code', $allCodes)->get()->keyBy('code');
$itemProcessMap = DB::table('process_items')->whereIn('item_id', $allIds)->get()->keyBy('item_id');
foreach ($order->items as $orderItem) {
$resolvedId = $codeToIdMap[$code] ?? null; // 메모리 참조
$processId = $itemProcessMap[$resolvedId] ?? null; // 메모리 참조
}
```
**수정 3: `OrderService::checkBendingStockForOrder()` (라인 1880~1885)**
```php
// 수정 전: 루프마다 StockService 호출 (내부에서 DB 쿼리)
foreach ($bendingItems as $item) {
$stockInfo = $stockService->getAvailableStock($item->id); // N+1
}
// 수정 후: 배치 조회 후 맵 참조
$stocksMap = Stock::whereIn('item_id', $ids)->get()->keyBy('item_id');
foreach ($bendingItems as $item) {
$stock = $stocksMap->get($item->id); // 메모리 참조
}
```
### 3.4 성능 개선 효과
| 시나리오 | 수정 전 쿼리 | 수정 후 쿼리 | 절감률 |
|---------|:----------:|:----------:|:-----:|
| 수주 50개 품목 → 작업지시 생성 | ~150 | ~8 | **95%** |
| 작업지시 자재 조회 (BOM 20개) | ~45 | ~3 | **93%** |
| 벤딩 재고 확인 (30개 품목) | ~31 | ~2 | **94%** |
### 3.5 회귀 테스트 결과
수정 후 전체 테스트 164개 통과, 기존 기능에 영향 없음 확인.
---
## 수정된 파일 전체 목록
### 신규 생성 (10개)
| 파일 | 설명 |
|------|------|
| `tests/Feature/Orders/OrderApiTest.php` | 수주 API 테스트 12개 |
| `tests/Feature/Inventory/StockApiTest.php` | 재고 API + FIFO 테스트 13개 |
| `tests/Feature/Approval/ApprovalApiTest.php` | 결재 워크플로우 테스트 15개 |
| `tests/Feature/Production/WorkOrderApiTest.php` | 작업지시 테스트 16개 |
| `database/factories/TenantFactory.php` | Tenant 모델 Factory |
| `database/factories/ClientFactory.php` | Client 모델 Factory |
| `database/factories/OrderFactory.php` | Order 모델 Factory (상태 빌더 포함) |
| `database/factories/StockFactory.php` | Stock 모델 Factory |
| `database/factories/StockLotFactory.php` | StockLot 모델 Factory |
### 수정 (14개)
| 파일 | 변경 내용 |
|------|----------|
| `tests/TestCase.php` | 공통 헬퍼 4개 추가 (인증, API 호출, 응답 검증) |
| `tests/Feature/Account/AccountApiTest.php` | `private` → TestCase 상속, 중복 제거 |
| `tests/Feature/BadDebt/BadDebtApiTest.php` | 동일 |
| `tests/Feature/Category/CategoryApiTest.php` | 동일 |
| `tests/Feature/Company/CompanyApiTest.php` | 동일 |
| `tests/Feature/ItemMaster/ItemMasterApiTest.php` | 동일 |
| `tests/Feature/Payment/PaymentApiTest.php` | 동일 |
| `tests/Feature/Popup/PopupApiTest.php` | 동일 |
| `tests/Feature/Production/BendingLotPipelineTest.php` | `use DatabaseTransactions` 중복 제거 |
| `tests/Feature/Subscription/SubscriptionApiTest.php` | 동일 |
| `tests/Feature/User/NotificationSettingApiTest.php` | 동일 |
| `tests/Feature/User/UserInvitationApiTest.php` | 동일 |
| `app/Services/WorkOrderService.php` | N+1 수정 — BOM 배치 사전 로드 |
| `app/Services/OrderService.php` | N+1 수정 — item_code/process_items 배치 조회, Stock 배치 조회 |
---
## 4. 운영 코드 안전성 검토
배포 후 수정된 운영 코드(테스트 파일 제외)가 기존 API 동작에 영향을 미치는지 코드 리뷰 + 전체 테스트로 검증했다.
### 4.1 검토 대상
실제 운영 코드를 수정한 파일은 **2개뿐**이다. 나머지 22개는 모두 테스트/Factory 파일이다.
| 파일 | 수정 메서드 | 수정 내용 |
|------|-----------|----------|
| `WorkOrderService.php` | `getMaterials()` | BOM 루프 내 `find()` → 배치 사전 로드 |
| `OrderService.php` | `createWorkOrderFromOrder()` | fallback 루프 내 DB 쿼리 → 배치 사전 조회 |
| `OrderService.php` | `checkBendingStockForOrder()` | StockService 루프 호출 → 배치 조회 |
### 4.2 동작 동등성 검증 (수정 전 = 수정 후)
| 수정 | 판정 | 근거 |
|------|:----:|------|
| `getMaterials()` BOM 배치 | **동등** | null 처리, 빈 배열, BOM 없는 경우 모두 동일. `$bomItemsMap[$id] ?? null``find($id)`와 동일한 null 반환 |
| `createWorkOrderFromOrder()` fallback | **동등** | 사전 배치 조회 결과가 즉석 조회와 동일. `DB::transaction` 내부이므로 중간 데이터 변경 없음. 캐시(`codeToIdMap`) 동작도 동일 |
| `checkBendingStockForOrder()` Stock | **동등** | `Stock::whereIn()` 결과가 `StockService::getAvailableStock()` 결과와 동일. `BelongsToTenant` 스코프 + 명시적 `tenant_id` 조건으로 격리 보장 |
### 4.3 엣지 케이스 검증
| 케이스 | 수정 전 | 수정 후 | 동일? |
|--------|--------|--------|:-----:|
| `item_id`가 null인 품목 | `if ($woItem->item_id)` skip | 맵에 포함되지 않아 동일하게 skip | ✅ |
| BOM JSON이 비어있는 품목 | `empty($item->bom)` skip | 동일 | ✅ |
| DB에 없는 `item_code` | `find()` → null | `$map[$code] ?? null` → null | ✅ |
| 재고가 0인 품목 | Stock 없음 → available_qty=0 | `$stocksMap->get($id)` → null → 0 | ✅ |
| 빈 주문 (items 0건) | 루프 미실행 | 배치 조회도 빈 배열, 루프 미실행 | ✅ |
### 4.4 전체 테스트 실행 결과
```
PHPUnit 11.5.27 / PHP 8.4.18
전체: 256개 테스트 실행
통과: 243개
실패: 7개 (모두 수정 전부터 존재하던 기존 문제)
Skip: 6개
이번 수정으로 인한 실패: 0건
```
**실패 7건 상세 (모두 기존 문제):**
| 테스트 | 원인 | 이번 수정과 관계 |
|--------|------|:--------------:|
| `PrefixResolverTest` (1건) | Unit 로직 불일치 (XX vs CF) | 무관 |
| `BendingLotPipelineTest` (3건) | TENANT_ID=287 고정, 로컬 DB 데이터 없음 | 무관 |
| `ItemMasterApiTest` (3건) | `section_id` 컬럼 미존재 (마이그레이션 불일치) | 무관 |
### 4.5 발견된 기존 문제 (수정과 무관, 별도 대응 필요)
`process_items` 테이블 조회에 `tenant_id` 필터가 없다. 수정 전부터 존재하던 문제이며 이번 수정으로 악화되지 않았다. 멀티테넌트 격리가 필요하면 별도 수정이 필요하다.
```php
// OrderService.php — tenant_id 조건 누락 (수정 전/후 동일)
DB::table('process_items')
->whereIn('item_id', $ids)
->where('is_active', true) // tenant_id 없음
->get();
```
### 4.6 결론
**이번 수정으로 기존 API 동작이 깨지는 경우는 없다.** 수정 전과 후의 결과가 정확히 동일하며, 쿼리 수만 줄어든 순수 성능 개선이다.
---
## 테스트 체크리스트
- [x] TestCase 공통 헬퍼 작성 및 기존 11개 테스트 호환 확인
- [x] Factory 5개 생성 (Tenant, Client, Order, Stock, StockLot)
- [x] Order API 테스트 12개 통과
- [x] Stock API + FIFO 테스트 13개 통과
- [x] Approval 워크플로우 테스트 15개 통과
- [x] WorkOrder API 테스트 16개 통과
- [x] N+1 쿼리 3건 배치 조회로 최적화
- [x] 전체 테스트 164개 회귀 없음 확인
- [x] 개발 서버 배포 완료 (2026-03-14)
---
## 관련 문서
- [API 구조 분석 및 개선 로드맵](../../system/api-analysis-report.md) — D1~D8 기술 부채 정의
- [API 개발 규칙](../standards/api-rules.md) — Service-First, FormRequest 컨벤션
- [품질 체크리스트](../standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14
# API 품질 개선 — 테스트 인프라 + 56개 테스트 + N+1 최적화
**날짜:** 2026-03-14
**작업자:** R&D 개발실장 + Claude Code
**배포 대상:** 개발 서버 (API develop 브랜치)
---
## 변경 개요
API 프로젝트의 기술 부채 분석 결과(D1~D2)에 따라 **테스트 커버리지 확충**과 **N+1 쿼리 최적화**를 수행했다. 비즈니스 핵심 흐름(수주→재고→결재→작업지시)에 대한 안전망을 확보하고, 대량 처리 시 쿼리 95%를 절감했다.
---
## 1. 왜 이 작업을 했는가 (근거)
### 1.1 기술 부채 분석 (근거 문서)
`system/api-analysis-report.md`에서 식별한 8건의 기술 부채 중 최우선 2건을 착수했다.
| ID | 영역 | 현황 (수정 전) | 영향도 |
|:--:|------|-------------|:------:|
| **D1** | 테스트 부재 | 165개 (1,400 EP 대비 부족), 핵심 도메인 미커버 | 높음 |
| **D2** | N+1 쿼리 | 루프 내 개별 DB 조회 3건 발견 | 높음 |
### 1.2 D1이 먼저인 이유
테스트가 없으면 코드 수정 후 "고쳐도 안전한가?"를 검증할 수 없다. D2(N+1 최적화) 같은 성능 개선을 안전하게 수행하려면 테스트 안전망이 선행되어야 한다.
### 1.3 D2 수정 대상 선정 근거
`app/Services/` 전체를 정적 분석하여 **foreach 루프 안에서 DB 쿼리를 실행하는 패턴**을 검색했다. 발견된 3건 모두 데이터 양에 비례하여 쿼리가 선형 증가하는 구조였다.
---
## 2. D1: 테스트 커버리지 확충
### 2.1 테스트 인프라 정비
기존 11개 테스트 파일이 동일한 setUp 코드(약 40줄)를 매번 복붙하고 있었다.
**수정 내용:**
| 파일 | 변경 | 이유 |
|------|------|------|
| `tests/TestCase.php` | 공통 메서드 4개 추가 | 중복 setUp 코드 제거, 신규 테스트 작성 속도 향상 |
| 기존 테스트 11개 | `private` 프로퍼티 → TestCase 상속 | TestCase 공통화에 따른 호환성 |
**추가된 공통 메서드:**
| 메서드 | 역할 |
|--------|------|
| `setUpAuthenticatedUser()` | API Key + Tenant + User + 로그인 토큰 일괄 생성 |
| `api($method, $uri, $data)` | 인증된 API 요청 헬퍼 |
| `assertApiSuccess($response)` | 표준 응답 구조 검증 |
| `assertApiPaginated($response)` | 페이지네이션 응답 검증 |
### 2.2 Factory 생성
테스트 데이터를 간편하게 생성하기 위해 Factory 5개를 추가했다.
| Factory | 모델 | 이유 |
|---------|------|------|
| `TenantFactory` | Tenant | 모든 테스트의 기본 |
| `ClientFactory` | Client | 수주 테스트에 거래처 필요 |
| `OrderFactory` | Order | 수주 CRUD + 상태전이 테스트 |
| `StockFactory` | Stock | 재고 FIFO 테스트 |
| `StockLotFactory` | StockLot | LOT 단위 입출고 테스트 |
### 2.3 신규 테스트 56개
| 도메인 | 파일 | 테스트 수 | 검증 내용 |
|--------|------|:--------:|---------|
| **수주 (Order)** | `tests/Feature/Orders/OrderApiTest.php` | 12 | CRUD, 상태변경(DRAFT→CONFIRMED→CANCELLED), 일괄삭제, 인증 |
| **재고 (Stock)** | `tests/Feature/Inventory/StockApiTest.php` | 13 | API 목록/통계, FIFO 차감, LOT 걸침 처리, 예약/해제, 거래이력, 상태 자동계산 |
| **결재 (Approval)** | `tests/Feature/Approval/ApprovalApiTest.php` | 15 | CRUD, 상신→승인/반려/회수 워크플로우, 결재자 별도 로그인, 결재함/참조함/완료함 |
| **작업지시 (WorkOrder)** | `tests/Feature/Production/WorkOrderApiTest.php` | 16 | CRUD, 상태전이 4단계(미배정→대기→준비→진행→완료), 담당자배정, 공정단계, 자재조회 |
**커버된 핵심 비즈니스 흐름:**
```
견적 → 수주(12) → 재고예약(13) → 작업지시(16) → 결재(15)
FIFO 검증 상태전이 검증 워크플로우 검증
```
### 2.4 테스트 실행 결과
```
수정 전: 165개 테스트
수정 후: 221개 테스트 (+56개, +34%)
최종 실행: 164개 통과 / 3개 Skip (기존 라우트 충돌)
실행 시간: ~12초
```
### 2.5 테스트 중 발견된 문제
| 발견 | 내용 | 후속 조치 |
|------|------|----------|
| 빈 데이터 수주 생성 허용 | `POST /api/v1/orders` 에 빈 body 전송 시 200 반환 | `StoreOrderRequest` 검증 강화 필요 (D4) |
| 기존 테스트 실패 3건 | `PrefixResolverTest`, `BendingLotPipelineTest` — 이번 변경과 무관 | 별도 수정 필요 |
| `ItemMasterApiTest` 에러 | `section_id` 컬럼 미존재 — 마이그레이션 불일치 | 별도 수정 필요 |
---
## 3. D2: N+1 쿼리 최적화
### 3.1 수정 대상 3건
| # | 파일 | 메서드 | 문제 | 쿼리 수 (수정 전) |
|:-:|------|--------|------|:-----------------:|
| 1 | `WorkOrderService.php` | `getMaterials()` | 루프 내 `Item::find()` + 중첩 루프 내 `Item::find()` | 1 + N + M |
| 2 | `OrderService.php` | `createWorkOrderFromOrder()` | 루프 내 `DB::table('items')->value()` + `DB::table('process_items')->value()` | 1 + 2N |
| 3 | `OrderService.php` | `checkBendingStockForOrder()` | 루프 내 `StockService::getAvailableStock()` 개별 호출 | 1 + N |
### 3.2 수정 방법 — 배치 사전 조회 패턴
모든 수정에 동일한 패턴을 적용했다:
```
수정 전: foreach (items) { DB::find(id); } ← N+1
수정 후: map = DB::whereIn(ids)->keyBy('id'); ← 1회 배치
foreach (items) { map[id]; } ← 메모리 참조
```
### 3.3 수정 상세
**수정 1: `WorkOrderService::getMaterials()` (라인 1470~1500)**
```php
// 수정 전: 루프 안에서 개별 조회
foreach ($workOrder->items as $woItem) {
$item = Item::find($woItem->item_id); // N+1
foreach ($item->bom as $bomItem) {
$childItem = Item::find($childItemId); // N+1 (중첩)
}
}
// 수정 후: 루프 전 배치 조회
$bomItemsMap = Item::whereIn('id', $parentIds)->get()->keyBy('id');
$bomChildItemsMap = Item::whereIn('id', $childIds)->get()->keyBy('id');
foreach ($workOrder->items as $woItem) {
$item = $bomItemsMap[$woItem->item_id]; // 메모리 참조
foreach ($item->bom as $bomItem) {
$childItem = $bomChildItemsMap[$childItemId]; // 메모리 참조
}
}
```
**수정 2: `OrderService::createWorkOrderFromOrder()` (라인 1239~1297)**
```php
// 수정 전: fallback에서 루프마다 DB 쿼리 x2
foreach ($order->items as $orderItem) {
$resolvedId = DB::table('items')->where('code', $code)->value('id'); // N+1
$pi = DB::table('process_items')->where('item_id', $id)->value('pid'); // N+1
}
// 수정 후: 루프 전 모든 item_code, process_items 일괄 조회
$codeToIdMap = DB::table('items')->whereIn('code', $allCodes)->get()->keyBy('code');
$itemProcessMap = DB::table('process_items')->whereIn('item_id', $allIds)->get()->keyBy('item_id');
foreach ($order->items as $orderItem) {
$resolvedId = $codeToIdMap[$code] ?? null; // 메모리 참조
$processId = $itemProcessMap[$resolvedId] ?? null; // 메모리 참조
}
```
**수정 3: `OrderService::checkBendingStockForOrder()` (라인 1880~1885)**
```php
// 수정 전: 루프마다 StockService 호출 (내부에서 DB 쿼리)
foreach ($bendingItems as $item) {
$stockInfo = $stockService->getAvailableStock($item->id); // N+1
}
// 수정 후: 배치 조회 후 맵 참조
$stocksMap = Stock::whereIn('item_id', $ids)->get()->keyBy('item_id');
foreach ($bendingItems as $item) {
$stock = $stocksMap->get($item->id); // 메모리 참조
}
```
### 3.4 성능 개선 효과
| 시나리오 | 수정 전 쿼리 | 수정 후 쿼리 | 절감률 |
|---------|:----------:|:----------:|:-----:|
| 수주 50개 품목 → 작업지시 생성 | ~150 | ~8 | **95%** |
| 작업지시 자재 조회 (BOM 20개) | ~45 | ~3 | **93%** |
| 벤딩 재고 확인 (30개 품목) | ~31 | ~2 | **94%** |
### 3.5 회귀 테스트 결과
수정 후 전체 테스트 164개 통과, 기존 기능에 영향 없음 확인.
---
## 수정된 파일 전체 목록
### 신규 생성 (10개)
| 파일 | 설명 |
|------|------|
| `tests/Feature/Orders/OrderApiTest.php` | 수주 API 테스트 12개 |
| `tests/Feature/Inventory/StockApiTest.php` | 재고 API + FIFO 테스트 13개 |
| `tests/Feature/Approval/ApprovalApiTest.php` | 결재 워크플로우 테스트 15개 |
| `tests/Feature/Production/WorkOrderApiTest.php` | 작업지시 테스트 16개 |
| `database/factories/TenantFactory.php` | Tenant 모델 Factory |
| `database/factories/ClientFactory.php` | Client 모델 Factory |
| `database/factories/OrderFactory.php` | Order 모델 Factory (상태 빌더 포함) |
| `database/factories/StockFactory.php` | Stock 모델 Factory |
| `database/factories/StockLotFactory.php` | StockLot 모델 Factory |
### 수정 (14개)
| 파일 | 변경 내용 |
|------|----------|
| `tests/TestCase.php` | 공통 헬퍼 4개 추가 (인증, API 호출, 응답 검증) |
| `tests/Feature/Account/AccountApiTest.php` | `private` → TestCase 상속, 중복 제거 |
| `tests/Feature/BadDebt/BadDebtApiTest.php` | 동일 |
| `tests/Feature/Category/CategoryApiTest.php` | 동일 |
| `tests/Feature/Company/CompanyApiTest.php` | 동일 |
| `tests/Feature/ItemMaster/ItemMasterApiTest.php` | 동일 |
| `tests/Feature/Payment/PaymentApiTest.php` | 동일 |
| `tests/Feature/Popup/PopupApiTest.php` | 동일 |
| `tests/Feature/Production/BendingLotPipelineTest.php` | `use DatabaseTransactions` 중복 제거 |
| `tests/Feature/Subscription/SubscriptionApiTest.php` | 동일 |
| `tests/Feature/User/NotificationSettingApiTest.php` | 동일 |
| `tests/Feature/User/UserInvitationApiTest.php` | 동일 |
| `app/Services/WorkOrderService.php` | N+1 수정 — BOM 배치 사전 로드 |
| `app/Services/OrderService.php` | N+1 수정 — item_code/process_items 배치 조회, Stock 배치 조회 |
---
## 4. 운영 코드 안전성 검토
배포 후 수정된 운영 코드(테스트 파일 제외)가 기존 API 동작에 영향을 미치는지 코드 리뷰 + 전체 테스트로 검증했다.
### 4.1 검토 대상
실제 운영 코드를 수정한 파일은 **2개뿐**이다. 나머지 22개는 모두 테스트/Factory 파일이다.
| 파일 | 수정 메서드 | 수정 내용 |
|------|-----------|----------|
| `WorkOrderService.php` | `getMaterials()` | BOM 루프 내 `find()` → 배치 사전 로드 |
| `OrderService.php` | `createWorkOrderFromOrder()` | fallback 루프 내 DB 쿼리 → 배치 사전 조회 |
| `OrderService.php` | `checkBendingStockForOrder()` | StockService 루프 호출 → 배치 조회 |
### 4.2 동작 동등성 검증 (수정 전 = 수정 후)
| 수정 | 판정 | 근거 |
|------|:----:|------|
| `getMaterials()` BOM 배치 | **동등** | null 처리, 빈 배열, BOM 없는 경우 모두 동일. `$bomItemsMap[$id] ?? null``find($id)`와 동일한 null 반환 |
| `createWorkOrderFromOrder()` fallback | **동등** | 사전 배치 조회 결과가 즉석 조회와 동일. `DB::transaction` 내부이므로 중간 데이터 변경 없음. 캐시(`codeToIdMap`) 동작도 동일 |
| `checkBendingStockForOrder()` Stock | **동등** | `Stock::whereIn()` 결과가 `StockService::getAvailableStock()` 결과와 동일. `BelongsToTenant` 스코프 + 명시적 `tenant_id` 조건으로 격리 보장 |
### 4.3 엣지 케이스 검증
| 케이스 | 수정 전 | 수정 후 | 동일? |
|--------|--------|--------|:-----:|
| `item_id`가 null인 품목 | `if ($woItem->item_id)` skip | 맵에 포함되지 않아 동일하게 skip | ✅ |
| BOM JSON이 비어있는 품목 | `empty($item->bom)` skip | 동일 | ✅ |
| DB에 없는 `item_code` | `find()` → null | `$map[$code] ?? null` → null | ✅ |
| 재고가 0인 품목 | Stock 없음 → available_qty=0 | `$stocksMap->get($id)` → null → 0 | ✅ |
| 빈 주문 (items 0건) | 루프 미실행 | 배치 조회도 빈 배열, 루프 미실행 | ✅ |
### 4.4 전체 테스트 실행 결과
```
PHPUnit 11.5.27 / PHP 8.4.18
전체: 256개 테스트 실행
통과: 243개
실패: 7개 (모두 수정 전부터 존재하던 기존 문제)
Skip: 6개
이번 수정으로 인한 실패: 0건
```
**실패 7건 상세 (모두 기존 문제):**
| 테스트 | 원인 | 이번 수정과 관계 |
|--------|------|:--------------:|
| `PrefixResolverTest` (1건) | Unit 로직 불일치 (XX vs CF) | 무관 |
| `BendingLotPipelineTest` (3건) | TENANT_ID=287 고정, 로컬 DB 데이터 없음 | 무관 |
| `ItemMasterApiTest` (3건) | `section_id` 컬럼 미존재 (마이그레이션 불일치) | 무관 |
### 4.5 발견된 기존 문제 (수정과 무관, 별도 대응 필요)
`process_items` 테이블 조회에 `tenant_id` 필터가 없다. 수정 전부터 존재하던 문제이며 이번 수정으로 악화되지 않았다. 멀티테넌트 격리가 필요하면 별도 수정이 필요하다.
```php
// OrderService.php — tenant_id 조건 누락 (수정 전/후 동일)
DB::table('process_items')
->whereIn('item_id', $ids)
->where('is_active', true) // tenant_id 없음
->get();
```
### 4.6 결론
**이번 수정으로 기존 API 동작이 깨지는 경우는 없다.** 수정 전과 후의 결과가 정확히 동일하며, 쿼리 수만 줄어든 순수 성능 개선이다.
---
## 테스트 체크리스트
- [x] TestCase 공통 헬퍼 작성 및 기존 11개 테스트 호환 확인
- [x] Factory 5개 생성 (Tenant, Client, Order, Stock, StockLot)
- [x] Order API 테스트 12개 통과
- [x] Stock API + FIFO 테스트 13개 통과
- [x] Approval 워크플로우 테스트 15개 통과
- [x] WorkOrder API 테스트 16개 통과
- [x] N+1 쿼리 3건 배치 조회로 최적화
- [x] 전체 테스트 164개 회귀 없음 확인
- [x] 개발 서버 배포 완료 (2026-03-14)
---
## 관련 문서
- [API 구조 분석 및 개선 로드맵](../../system/api-analysis-report.md) — D1~D8 기술 부채 정의
- [API 개발 규칙](../standards/api-rules.md) — Service-First, FormRequest 컨벤션
- [품질 체크리스트](../standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14

View File

@@ -1,186 +1,186 @@
# API 테스트 인프라 정비 및 수주 API 테스트 추가
**날짜:** 2026-03-14
**작업자:** R&D 개발실장 + Claude Code
## 변경 개요
API 프로젝트의 테스트 기반을 체계적으로 정비하고, 미커버 핵심 도메인인 수주(Order) API에 대한 Feature 테스트를 신규 작성했다. 기술 부채 분석(D1: 테스트 커버리지 확충)의 첫 번째 실행 단계이다.
---
## 1. 테스트 인프라 정비
### 1.1 TestCase 공통화
기존 11개 테스트 파일이 동일한 setUp 코드(약 40줄)를 매번 복붙하고 있었다. `tests/TestCase.php`에 공통 메서드를 추가하여 중복을 제거했다.
**추가된 공통 메서드:**
| 메서드 | 용도 |
|--------|------|
| `setUpAuthenticatedUser()` | API Key + Tenant + User + 로그인 토큰 일괄 생성 |
| `api($method, $uri, $data)` | 인증된 API 요청 (X-API-KEY + Bearer 자동 포함) |
| `assertApiSuccess($response)` | 표준 응답 구조 검증 (`success`, `message`, `data`) |
| `assertApiPaginated($response)` | 페이지네이션 응답 구조 검증 |
**Before (각 테스트 파일마다 반복):**
```php
private Tenant $tenant;
private User $user;
private string $apiKey;
private string $token;
protected function setUp(): void {
// 40줄의 동일한 초기화 코드...
}
protected function loginAndGetToken(): void { ... }
protected function authenticatedRequest(...) { ... }
```
**After (한 줄 호출):**
```php
protected function setUp(): void {
parent::setUp();
$this->setUpAuthenticatedUser();
}
// api(), assertApiSuccess() 등 TestCase에서 상속
```
### 1.2 기존 테스트 파일 정리
11개 기존 테스트 파일에서 `private` 프로퍼티 선언, `use DatabaseTransactions`, 중복 헬퍼 메서드를 제거하고 TestCase 상속으로 전환했다.
### 1.3 Factory 신규 생성
기존에 `UserFactory` 1개만 존재했다. 핵심 도메인 테스트에 필요한 Factory 3개를 추가했다.
| Factory | 모델 | 주요 필드 |
|---------|------|----------|
| `TenantFactory` | `Tenant` | company_name, code, email, phone, business_num |
| `ClientFactory` | `Client` | name, client_code, contact_person, phone, business_no |
| `OrderFactory` | `Order` | order_no, order_type_code, status_code, quantity, supply_amount |
`OrderFactory`에는 상태별 빌더 메서드도 포함:
```php
OrderFactory::new()->confirmed() // 확정 상태
OrderFactory::new()->inProduction() // 생산중 상태
OrderFactory::new()->completed() // 완료 상태
OrderFactory::new()->cancelled() // 취소 상태
```
---
## 2. 수주(Order) API 테스트
### 2.1 테스트 목록 (12개)
| 테스트 | 검증 내용 | 결과 |
|--------|----------|:----:|
| `test_수주_목록_조회` | GET `/api/v1/orders` 페이지네이션 응답 | ✅ |
| `test_수주_통계_조회` | GET `/api/v1/orders/stats` 집계 데이터 | ✅ |
| `test_수주_생성_성공` | POST `/api/v1/orders` + items 배열 | ✅ |
| `test_수주_생성_빈_데이터_허용_확인` | 빈 데이터 생성 허용 여부 확인 | ✅ |
| `test_수주_상세_조회` | GET `/api/v1/orders/{id}` 단건 | ✅ |
| `test_존재하지_않는_수주_조회시_404` | 없는 ID 조회 → 404 | ✅ |
| `test_수주_수정_성공` | PUT `/api/v1/orders/{id}` 필드 변경 | ✅ |
| `test_수주_삭제_성공` | DELETE → SoftDelete 확인 | ✅ |
| `test_수주_일괄_삭제` | DELETE `/api/v1/orders/bulk` | ✅ |
| `test_수주_상태_등록에서_확정으로_변경` | PATCH `/{id}/status` DRAFT→CONFIRMED | ✅ |
| `test_수주_상태_취소` | PATCH `/{id}/status` DRAFT→CANCELLED | ✅ |
| `test_미인증_요청시_401` | Bearer 토큰 없이 요청 → 401 | ✅ |
### 2.2 테스트 실행 결과
```
PHPUnit 11.5.27
PHP 8.4.18
전체: 120개 통과, 3개 Skip (기존 라우트 충돌 이슈)
신규: 12개 전부 통과 (46 assertions)
실행 시간: ~8초
```
---
## 3. 발견된 문제
### 3.1 빈 데이터로 수주 생성 허용
```
POST /api/v1/orders (body: {})
→ 200 OK (수주가 생성됨)
```
`StoreOrderRequest`의 검증 규칙이 느슨하여 필수 필드 없이도 수주가 생성된다. FormRequest 검증 강화가 필요하다 (D4 개선 대상).
### 3.2 기존 테스트 실패 (변경 전부터 존재)
| 테스트 | 원인 | 영향 |
|--------|------|------|
| `PrefixResolverTest` | Unit 테스트 로직 불일치 (XX vs CF) | Production 도메인 |
| `BendingLotPipelineTest` (3개) | TENANT_ID=287 고정, 로컬 DB에 해당 데이터 없음 | Production 도메인 |
| `ItemMasterApiTest` (3개) | `section_id` 컬럼 미존재 (마이그레이션 불일치) | ItemMaster 도메인 |
> 이 실패들은 이번 변경과 무관한 기존 문제이다.
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `tests/TestCase.php` | 공통 헬퍼 메서드 4개 추가 (`setUpAuthenticatedUser`, `api`, `assertApiSuccess`, `assertApiPaginated`) |
| `tests/Feature/Account/AccountApiTest.php` | `private` → TestCase 상속, 중복 제거 |
| `tests/Feature/BadDebt/BadDebtApiTest.php` | 동일 |
| `tests/Feature/Category/CategoryApiTest.php` | 동일 |
| `tests/Feature/Company/CompanyApiTest.php` | 동일 |
| `tests/Feature/ItemMaster/ItemMasterApiTest.php` | 동일 |
| `tests/Feature/Payment/PaymentApiTest.php` | 동일 |
| `tests/Feature/Popup/PopupApiTest.php` | 동일 |
| `tests/Feature/Production/BendingLotPipelineTest.php` | `use DatabaseTransactions` 중복 제거 |
| `tests/Feature/Subscription/SubscriptionApiTest.php` | 동일 |
| `tests/Feature/User/NotificationSettingApiTest.php` | 동일 |
| `tests/Feature/User/UserInvitationApiTest.php` | 동일 |
| `database/factories/TenantFactory.php` | **신규** — Tenant 모델 Factory |
| `database/factories/ClientFactory.php` | **신규** — Client 모델 Factory |
| `database/factories/OrderFactory.php` | **신규** — Order 모델 Factory (상태 빌더 포함) |
| `tests/Feature/Orders/OrderApiTest.php` | **신규** — 수주 API 테스트 12개 |
---
## 테스트 체크리스트
- [x] TestCase 공통 헬퍼 작성
- [x] 기존 11개 테스트 파일 중복 제거
- [x] Factory 3개 생성 (Tenant, Client, Order)
- [x] Order API 테스트 12개 작성 및 통과
- [x] 기존 테스트 회귀 없음 확인 (기존 실패는 변경 전부터 존재)
- [ ] StockService 테스트 (다음 단계)
- [ ] ApprovalService 테스트 (다음 단계)
- [ ] WorkOrderService 테스트 (다음 단계)
---
## 다음 단계
기술 부채 D1(테스트 커버리지 확충) 로드맵에 따라 다음 서비스 테스트를 순차 진행한다:
1. **StockService** — 재고 관리 (FIFO, LOT 추적)
2. **ApprovalService** — 전자결재 워크플로우
3. **WorkOrderService** — 작업지시 (가장 큰 서비스, 4,097줄)
---
## 관련 문서
- [API 구조 분석 및 개선 로드맵](../../system/api-analysis-report.md)
- [API 개발 규칙](../standards/api-rules.md)
- [품질 체크리스트](../standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14
# API 테스트 인프라 정비 및 수주 API 테스트 추가
**날짜:** 2026-03-14
**작업자:** R&D 개발실장 + Claude Code
## 변경 개요
API 프로젝트의 테스트 기반을 체계적으로 정비하고, 미커버 핵심 도메인인 수주(Order) API에 대한 Feature 테스트를 신규 작성했다. 기술 부채 분석(D1: 테스트 커버리지 확충)의 첫 번째 실행 단계이다.
---
## 1. 테스트 인프라 정비
### 1.1 TestCase 공통화
기존 11개 테스트 파일이 동일한 setUp 코드(약 40줄)를 매번 복붙하고 있었다. `tests/TestCase.php`에 공통 메서드를 추가하여 중복을 제거했다.
**추가된 공통 메서드:**
| 메서드 | 용도 |
|--------|------|
| `setUpAuthenticatedUser()` | API Key + Tenant + User + 로그인 토큰 일괄 생성 |
| `api($method, $uri, $data)` | 인증된 API 요청 (X-API-KEY + Bearer 자동 포함) |
| `assertApiSuccess($response)` | 표준 응답 구조 검증 (`success`, `message`, `data`) |
| `assertApiPaginated($response)` | 페이지네이션 응답 구조 검증 |
**Before (각 테스트 파일마다 반복):**
```php
private Tenant $tenant;
private User $user;
private string $apiKey;
private string $token;
protected function setUp(): void {
// 40줄의 동일한 초기화 코드...
}
protected function loginAndGetToken(): void { ... }
protected function authenticatedRequest(...) { ... }
```
**After (한 줄 호출):**
```php
protected function setUp(): void {
parent::setUp();
$this->setUpAuthenticatedUser();
}
// api(), assertApiSuccess() 등 TestCase에서 상속
```
### 1.2 기존 테스트 파일 정리
11개 기존 테스트 파일에서 `private` 프로퍼티 선언, `use DatabaseTransactions`, 중복 헬퍼 메서드를 제거하고 TestCase 상속으로 전환했다.
### 1.3 Factory 신규 생성
기존에 `UserFactory` 1개만 존재했다. 핵심 도메인 테스트에 필요한 Factory 3개를 추가했다.
| Factory | 모델 | 주요 필드 |
|---------|------|----------|
| `TenantFactory` | `Tenant` | company_name, code, email, phone, business_num |
| `ClientFactory` | `Client` | name, client_code, contact_person, phone, business_no |
| `OrderFactory` | `Order` | order_no, order_type_code, status_code, quantity, supply_amount |
`OrderFactory`에는 상태별 빌더 메서드도 포함:
```php
OrderFactory::new()->confirmed() // 확정 상태
OrderFactory::new()->inProduction() // 생산중 상태
OrderFactory::new()->completed() // 완료 상태
OrderFactory::new()->cancelled() // 취소 상태
```
---
## 2. 수주(Order) API 테스트
### 2.1 테스트 목록 (12개)
| 테스트 | 검증 내용 | 결과 |
|--------|----------|:----:|
| `test_수주_목록_조회` | GET `/api/v1/orders` 페이지네이션 응답 | ✅ |
| `test_수주_통계_조회` | GET `/api/v1/orders/stats` 집계 데이터 | ✅ |
| `test_수주_생성_성공` | POST `/api/v1/orders` + items 배열 | ✅ |
| `test_수주_생성_빈_데이터_허용_확인` | 빈 데이터 생성 허용 여부 확인 | ✅ |
| `test_수주_상세_조회` | GET `/api/v1/orders/{id}` 단건 | ✅ |
| `test_존재하지_않는_수주_조회시_404` | 없는 ID 조회 → 404 | ✅ |
| `test_수주_수정_성공` | PUT `/api/v1/orders/{id}` 필드 변경 | ✅ |
| `test_수주_삭제_성공` | DELETE → SoftDelete 확인 | ✅ |
| `test_수주_일괄_삭제` | DELETE `/api/v1/orders/bulk` | ✅ |
| `test_수주_상태_등록에서_확정으로_변경` | PATCH `/{id}/status` DRAFT→CONFIRMED | ✅ |
| `test_수주_상태_취소` | PATCH `/{id}/status` DRAFT→CANCELLED | ✅ |
| `test_미인증_요청시_401` | Bearer 토큰 없이 요청 → 401 | ✅ |
### 2.2 테스트 실행 결과
```
PHPUnit 11.5.27
PHP 8.4.18
전체: 120개 통과, 3개 Skip (기존 라우트 충돌 이슈)
신규: 12개 전부 통과 (46 assertions)
실행 시간: ~8초
```
---
## 3. 발견된 문제
### 3.1 빈 데이터로 수주 생성 허용
```
POST /api/v1/orders (body: {})
→ 200 OK (수주가 생성됨)
```
`StoreOrderRequest`의 검증 규칙이 느슨하여 필수 필드 없이도 수주가 생성된다. FormRequest 검증 강화가 필요하다 (D4 개선 대상).
### 3.2 기존 테스트 실패 (변경 전부터 존재)
| 테스트 | 원인 | 영향 |
|--------|------|------|
| `PrefixResolverTest` | Unit 테스트 로직 불일치 (XX vs CF) | Production 도메인 |
| `BendingLotPipelineTest` (3개) | TENANT_ID=287 고정, 로컬 DB에 해당 데이터 없음 | Production 도메인 |
| `ItemMasterApiTest` (3개) | `section_id` 컬럼 미존재 (마이그레이션 불일치) | ItemMaster 도메인 |
> 이 실패들은 이번 변경과 무관한 기존 문제이다.
---
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `tests/TestCase.php` | 공통 헬퍼 메서드 4개 추가 (`setUpAuthenticatedUser`, `api`, `assertApiSuccess`, `assertApiPaginated`) |
| `tests/Feature/Account/AccountApiTest.php` | `private` → TestCase 상속, 중복 제거 |
| `tests/Feature/BadDebt/BadDebtApiTest.php` | 동일 |
| `tests/Feature/Category/CategoryApiTest.php` | 동일 |
| `tests/Feature/Company/CompanyApiTest.php` | 동일 |
| `tests/Feature/ItemMaster/ItemMasterApiTest.php` | 동일 |
| `tests/Feature/Payment/PaymentApiTest.php` | 동일 |
| `tests/Feature/Popup/PopupApiTest.php` | 동일 |
| `tests/Feature/Production/BendingLotPipelineTest.php` | `use DatabaseTransactions` 중복 제거 |
| `tests/Feature/Subscription/SubscriptionApiTest.php` | 동일 |
| `tests/Feature/User/NotificationSettingApiTest.php` | 동일 |
| `tests/Feature/User/UserInvitationApiTest.php` | 동일 |
| `database/factories/TenantFactory.php` | **신규** — Tenant 모델 Factory |
| `database/factories/ClientFactory.php` | **신규** — Client 모델 Factory |
| `database/factories/OrderFactory.php` | **신규** — Order 모델 Factory (상태 빌더 포함) |
| `tests/Feature/Orders/OrderApiTest.php` | **신규** — 수주 API 테스트 12개 |
---
## 테스트 체크리스트
- [x] TestCase 공통 헬퍼 작성
- [x] 기존 11개 테스트 파일 중복 제거
- [x] Factory 3개 생성 (Tenant, Client, Order)
- [x] Order API 테스트 12개 작성 및 통과
- [x] 기존 테스트 회귀 없음 확인 (기존 실패는 변경 전부터 존재)
- [ ] StockService 테스트 (다음 단계)
- [ ] ApprovalService 테스트 (다음 단계)
- [ ] WorkOrderService 테스트 (다음 단계)
---
## 다음 단계
기술 부채 D1(테스트 커버리지 확충) 로드맵에 따라 다음 서비스 테스트를 순차 진행한다:
1. **StockService** — 재고 관리 (FIFO, LOT 추적)
2. **ApprovalService** — 전자결재 워크플로우
3. **WorkOrderService** — 작업지시 (가장 큰 서비스, 4,097줄)
---
## 관련 문서
- [API 구조 분석 및 개선 로드맵](../../system/api-analysis-report.md)
- [API 개발 규칙](../standards/api-rules.md)
- [품질 체크리스트](../standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14

File diff suppressed because it is too large Load Diff

View File

@@ -1,242 +1,242 @@
# 바로빌 서비스 출시 단계별 준비 계획
> **작성일**: 2026-03-17
> **상태**: 계획 수립
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
MNG에서 운영 중인 바로빌 연동 시스템을 서비스(API+React)로 이관하여, 멀티테넌트 고객이 직접 사용할 수 있는 SaaS 형태로 출시한다.
### 1.2 현재 상태
- **MNG (백오피스)**: 바로빌 SOAP 연동 완료, tenant_id=1 (코드브릿지엑스)에서 실무 운영 중
- **API**: DB 모델 15개 + REST API 42개 엔드포인트 구현 완료 (데이터 조회/분개용)
- **React**: 바로빌 설정 페이지 기본 구현
### 1.3 목표
고객(테넌트)이 SAM 서비스에서 바로빌 기능을 직접 설정하고 사용할 수 있도록 한다:
- 계좌조회, 카드내역, 홈택스 세금계산서 자동 수집
- 전자세금계산서 발행
- 카카오톡/SMS 알림
---
## 2. 단계별 로드맵
```
Phase 1 Phase 2 Phase 3 Phase 4
SOAP 이관 UI 구현 베타테스트 정식 출시
(API 개발) (React 개발) (내부→외부) (온보딩 가동)
───────────── → ───────────── → ───────────── → ─────────────
```
---
## 3. Phase 1: SOAP 연동 이관 (API 개발)
> **핵심**: MNG의 BarobillService를 API로 이관하여 멀티테넌트 지원
### 3.1 작업 목록
| # | 작업 | 상세 | 난이도 |
|---|------|------|--------|
| 1-1 | BarobillService 이관 | MNG 1,761줄 → API로 이동, 멀티테넌트 리팩토링 | 상 |
| 1-2 | 회원사 관리 API | 등록/수정/조회/상태확인 엔드포인트 | 중 |
| 1-3 | 인증서 관리 API | 등록URL/유효성/만료일 조회 엔드포인트 | 중 |
| 1-4 | 계좌 관리 API | 등록/목록/입출금 조회 엔드포인트 | 중 |
| 1-5 | 카드 관리 API | 등록/수정/해지/사용내역 조회 엔드포인트 | 중 |
| 1-6 | 세금계산서 발행 API | 작성/발행/조회 엔드포인트 | 상 |
| 1-7 | 동기화 스케줄러 | 은행/카드/홈택스 자동 수집 (Queue Job) | 중 |
| 1-8 | 테스트/운영 모드 전환 API | 회원사별 server_mode 전환 | 하 |
### 3.2 기술 과제
| 과제 | 설명 | 대응 방안 |
|------|------|----------|
| CERTKEY 관리 | 현재 전역 1개 → 멀티테넌트 대응 필요 | 바로빌 파트너 계약 구조 확인 후 결정 |
| PHP SOAP 확장 | API 서버에 `php-soap` 설치 필요 | Docker/서버 환경 확인 |
| 암호화 키 공유 | MNG/API 간 `APP_KEY` 동일해야 복호화 가능 | 현재 동일 키 사용 중 (확인 필요) |
| 동기화 부하 | 테넌트 수 증가 시 SOAP 호출량 증가 | Queue 분산, 호출 간격 조절 |
### 3.3 환경 준비
```bash
# API 서버에 PHP SOAP 확장 확인
php -m | grep soap
# 없으면 설치 (개발 서버 Level 2)
sudo apt install php8.4-soap
sudo systemctl restart php8.4-fpm
# .env 설정 추가
BAROBILL_CERT_KEY_TEST=<테스트 인증키>
BAROBILL_CERT_KEY_PROD=<운영 인증키>
BAROBILL_CORP_NUM=<파트너 사업자번호>
BAROBILL_TEST_MODE=true
```
---
## 4. Phase 2: UI 구현 (React 개발)
> **핵심**: 고객이 직접 바로빌을 설정하고 데이터를 조회할 수 있는 화면
### 4.1 작업 목록
| # | 작업 | 상세 | 난이도 |
|---|------|------|--------|
| 2-1 | 바로빌 설정 페이지 | 회원사 등록/수정, 서버 모드 표시 | 중 |
| 2-2 | 인증서 관리 화면 | 등록 URL 안내, 유효기간 표시, 갱신 알림 | 중 |
| 2-3 | 계좌 관리 화면 | 등록 계좌 목록, 등록 URL 안내 | 중 |
| 2-4 | 카드 관리 화면 | 등록 카드 목록, 추가/해지 | 중 |
| 2-5 | 카드 거래내역 조회 | 기간별 조회, 분개 연동, 숨김/분할 | 상 |
| 2-6 | 은행 거래내역 조회 | 기간별 조회, 분개 연동, 오버라이드/분할 | 상 |
| 2-7 | 홈택스 세금계산서 | 매출/매입 조회, 분개 연동 | 중 |
| 2-8 | 세금계산서 발행 화면 | 작성/발행 폼, 미리보기 | 상 |
### 4.2 화면 구성 (메뉴 구조)
```
재무관리
├─ 계좌관리
│ ├─ 보유계좌 관리 (바로빌 계좌 등록 포함)
│ └─ 계좌 입출금 내역
├─ 카드관리
│ ├─ 법인카드 관리 (바로빌 카드 등록 포함)
│ └─ 카드 사용내역
├─ 세금계산서
│ ├─ 매출 세금계산서
│ ├─ 매입 세금계산서
│ └─ 세금계산서 발행
└─ 설정
└─ 바로빌 연동 설정 (인증서, 모드, 충전잔액)
```
---
## 5. Phase 3: 베타테스트
> **핵심**: 내부 → 외부 순서로 검증, 테스트 모드 사용
### 5.1 내부 베타테스트
| 항목 | 내용 |
|------|------|
| **대상** | tenant_id=1 (코드브릿지엑스 본사) |
| **기간** | 2주 |
| **모드** | 테스트 모드 |
| **검증 항목** | 전체 기능 동작, UI/UX, 데이터 정합성 |
| **비교 기준** | MNG 운영 데이터와 서비스 데이터 일치 확인 |
**내부 베타 체크리스트**:
- [ ] 회원사 등록/수정 정상 동작
- [ ] 인증서 등록 URL 정상 접근
- [ ] 계좌 등록 및 입출금 내역 조회
- [ ] 카드 등록 및 사용내역 조회
- [ ] 홈택스 매출/매입 세금계산서 수집
- [ ] 세금계산서 발행 (테스트 서버)
- [ ] 분개 연동 정상 동작
- [ ] 동기화 스케줄러 자동 수집 확인
- [ ] MNG 데이터와 서비스 데이터 일치
### 5.2 외부 베타테스트
| 항목 | 내용 |
|------|------|
| **대상** | 선별 고객사 2~3곳 |
| **기간** | 2~4주 |
| **모드** | 테스트 모드 |
| **검증 항목** | 실사용 시나리오, 다양한 사업자 유형, 피드백 수집 |
**외부 베타 체크리스트**:
- [ ] 다양한 사업자번호로 회원 등록
- [ ] 다양한 은행/카드사 연동 확인
- [ ] 고객 직접 인증서/계좌/카드 등록 가능 확인
- [ ] 고객 피드백 수집 및 반영
- [ ] 성능 (다수 테넌트 동시 동기화)
---
## 6. Phase 4: 정식 출시
> **핵심**: 운영 모드 전환, 과금 시작, 온보딩 프로세스 가동
### 6.1 출시 준비 체크리스트
**인프라**:
- [ ] API 서버 `php-soap` 확장 설치 확인
- [ ] 운영 `.env``BAROBILL_CERT_KEY_PROD`, `BAROBILL_CORP_NUM` 설정
- [ ] `BAROBILL_TEST_MODE=false` 설정
- [ ] 동기화 스케줄러 Supervisor 등록
- [ ] 바로빌 운영 CERTKEY 충전잔액 확보
**과금**:
- [ ] `barobill_pricing_policies` 요금 정책 데이터 입력
- [ ] 월정액 구독 자동 과금 배치 등록 (매월 1일)
- [ ] 과금 내역 고객 조회 화면 (선택)
**운영**:
- [ ] 인증서 만료 알림 (이메일/카카오톡)
- [ ] 충전잔액 부족 알림
- [ ] 동기화 실패 알림 및 재시도 로직
- [ ] 바로빌 장애 시 대응 매뉴얼
### 6.2 온보딩 프로세스 정립
정식 출시 후 신규 고객 가입 시:
```
계약 → 테넌트 생성 → 회원등록(테스트) → 인증서/계좌/카드 → 검증 → 운영전환 → 실무사용
```
> 상세 프로세스: `features/barobill/tenant-onboarding.md` 참조
---
## 7. 바로빌 파트너 정책 확인 필요 사항
> **경고: 개발 착수 전 바로빌 측에 확인해야 할 사항**
| # | 확인 사항 | 이유 | 현재 상태 |
|---|----------|------|----------|
| 1 | 멀티테넌트 CERTKEY 구조 | 파트너 1개 키로 다수 회원사 관리 가능한지 | 미확인 |
| 2 | 테스트 서버 제한 | 테스트 API 호출 횟수/기간 제한 | 미확인 |
| 3 | 과금 구조 | 파트너 단가표 (건당/월정액) | 미확인 |
| 4 | SLA | 바로빌 API 가용성 보장 수준 | 미확인 |
| 5 | 회원사 대량 등록 | 일괄 등록 API 또는 제한 | 미확인 |
| 6 | 인증서 대리 등록 | 고객 대신 등록 가능 여부 | 미확인 |
---
## 8. 리스크 및 대응
| 리스크 | 영향 | 대응 |
|--------|------|------|
| 바로빌 API 장애 | 거래 데이터 수집 중단 | 재시도 로직 + 장애 알림 |
| 인증서 만료 | 계좌/세금계산서 조회 불가 | 만료 30일 전 알림 |
| SOAP 호출 지연 | 페이지 응답 지연 | 비동기 Queue 처리 |
| 테넌트 급증 | 동기화 부하 | 호출 간격 분산, 우선순위 큐 |
| 충전잔액 부족 | API 호출 실패 | 잔액 모니터링 + 자동 알림 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [바로빌 연동 시스템](../../features/barobill/README.md) | 전체 구조, 모드, 과금 |
| [테넌트 온보딩](../../features/barobill/tenant-onboarding.md) | 온보딩 6단계 프로세스 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | REST API 42개 엔드포인트 |
| [이관 현황](../../system/migration-status.md) | MNG→API+React 전체 이관 현황 |
---
**최종 업데이트**: 2026-03-17
# 바로빌 서비스 출시 단계별 준비 계획
> **작성일**: 2026-03-17
> **상태**: 계획 수립
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
MNG에서 운영 중인 바로빌 연동 시스템을 서비스(API+React)로 이관하여, 멀티테넌트 고객이 직접 사용할 수 있는 SaaS 형태로 출시한다.
### 1.2 현재 상태
- **MNG (백오피스)**: 바로빌 SOAP 연동 완료, tenant_id=1 (코드브릿지엑스)에서 실무 운영 중
- **API**: DB 모델 15개 + REST API 42개 엔드포인트 구현 완료 (데이터 조회/분개용)
- **React**: 바로빌 설정 페이지 기본 구현
### 1.3 목표
고객(테넌트)이 SAM 서비스에서 바로빌 기능을 직접 설정하고 사용할 수 있도록 한다:
- 계좌조회, 카드내역, 홈택스 세금계산서 자동 수집
- 전자세금계산서 발행
- 카카오톡/SMS 알림
---
## 2. 단계별 로드맵
```
Phase 1 Phase 2 Phase 3 Phase 4
SOAP 이관 UI 구현 베타테스트 정식 출시
(API 개발) (React 개발) (내부→외부) (온보딩 가동)
───────────── → ───────────── → ───────────── → ─────────────
```
---
## 3. Phase 1: SOAP 연동 이관 (API 개발)
> **핵심**: MNG의 BarobillService를 API로 이관하여 멀티테넌트 지원
### 3.1 작업 목록
| # | 작업 | 상세 | 난이도 |
|---|------|------|--------|
| 1-1 | BarobillService 이관 | MNG 1,761줄 → API로 이동, 멀티테넌트 리팩토링 | 상 |
| 1-2 | 회원사 관리 API | 등록/수정/조회/상태확인 엔드포인트 | 중 |
| 1-3 | 인증서 관리 API | 등록URL/유효성/만료일 조회 엔드포인트 | 중 |
| 1-4 | 계좌 관리 API | 등록/목록/입출금 조회 엔드포인트 | 중 |
| 1-5 | 카드 관리 API | 등록/수정/해지/사용내역 조회 엔드포인트 | 중 |
| 1-6 | 세금계산서 발행 API | 작성/발행/조회 엔드포인트 | 상 |
| 1-7 | 동기화 스케줄러 | 은행/카드/홈택스 자동 수집 (Queue Job) | 중 |
| 1-8 | 테스트/운영 모드 전환 API | 회원사별 server_mode 전환 | 하 |
### 3.2 기술 과제
| 과제 | 설명 | 대응 방안 |
|------|------|----------|
| CERTKEY 관리 | 현재 전역 1개 → 멀티테넌트 대응 필요 | 바로빌 파트너 계약 구조 확인 후 결정 |
| PHP SOAP 확장 | API 서버에 `php-soap` 설치 필요 | Docker/서버 환경 확인 |
| 암호화 키 공유 | MNG/API 간 `APP_KEY` 동일해야 복호화 가능 | 현재 동일 키 사용 중 (확인 필요) |
| 동기화 부하 | 테넌트 수 증가 시 SOAP 호출량 증가 | Queue 분산, 호출 간격 조절 |
### 3.3 환경 준비
```bash
# API 서버에 PHP SOAP 확장 확인
php -m | grep soap
# 없으면 설치 (개발 서버 Level 2)
sudo apt install php8.4-soap
sudo systemctl restart php8.4-fpm
# .env 설정 추가
BAROBILL_CERT_KEY_TEST=<테스트 인증키>
BAROBILL_CERT_KEY_PROD=<운영 인증키>
BAROBILL_CORP_NUM=<파트너 사업자번호>
BAROBILL_TEST_MODE=true
```
---
## 4. Phase 2: UI 구현 (React 개발)
> **핵심**: 고객이 직접 바로빌을 설정하고 데이터를 조회할 수 있는 화면
### 4.1 작업 목록
| # | 작업 | 상세 | 난이도 |
|---|------|------|--------|
| 2-1 | 바로빌 설정 페이지 | 회원사 등록/수정, 서버 모드 표시 | 중 |
| 2-2 | 인증서 관리 화면 | 등록 URL 안내, 유효기간 표시, 갱신 알림 | 중 |
| 2-3 | 계좌 관리 화면 | 등록 계좌 목록, 등록 URL 안내 | 중 |
| 2-4 | 카드 관리 화면 | 등록 카드 목록, 추가/해지 | 중 |
| 2-5 | 카드 거래내역 조회 | 기간별 조회, 분개 연동, 숨김/분할 | 상 |
| 2-6 | 은행 거래내역 조회 | 기간별 조회, 분개 연동, 오버라이드/분할 | 상 |
| 2-7 | 홈택스 세금계산서 | 매출/매입 조회, 분개 연동 | 중 |
| 2-8 | 세금계산서 발행 화면 | 작성/발행 폼, 미리보기 | 상 |
### 4.2 화면 구성 (메뉴 구조)
```
재무관리
├─ 계좌관리
│ ├─ 보유계좌 관리 (바로빌 계좌 등록 포함)
│ └─ 계좌 입출금 내역
├─ 카드관리
│ ├─ 법인카드 관리 (바로빌 카드 등록 포함)
│ └─ 카드 사용내역
├─ 세금계산서
│ ├─ 매출 세금계산서
│ ├─ 매입 세금계산서
│ └─ 세금계산서 발행
└─ 설정
└─ 바로빌 연동 설정 (인증서, 모드, 충전잔액)
```
---
## 5. Phase 3: 베타테스트
> **핵심**: 내부 → 외부 순서로 검증, 테스트 모드 사용
### 5.1 내부 베타테스트
| 항목 | 내용 |
|------|------|
| **대상** | tenant_id=1 (코드브릿지엑스 본사) |
| **기간** | 2주 |
| **모드** | 테스트 모드 |
| **검증 항목** | 전체 기능 동작, UI/UX, 데이터 정합성 |
| **비교 기준** | MNG 운영 데이터와 서비스 데이터 일치 확인 |
**내부 베타 체크리스트**:
- [ ] 회원사 등록/수정 정상 동작
- [ ] 인증서 등록 URL 정상 접근
- [ ] 계좌 등록 및 입출금 내역 조회
- [ ] 카드 등록 및 사용내역 조회
- [ ] 홈택스 매출/매입 세금계산서 수집
- [ ] 세금계산서 발행 (테스트 서버)
- [ ] 분개 연동 정상 동작
- [ ] 동기화 스케줄러 자동 수집 확인
- [ ] MNG 데이터와 서비스 데이터 일치
### 5.2 외부 베타테스트
| 항목 | 내용 |
|------|------|
| **대상** | 선별 고객사 2~3곳 |
| **기간** | 2~4주 |
| **모드** | 테스트 모드 |
| **검증 항목** | 실사용 시나리오, 다양한 사업자 유형, 피드백 수집 |
**외부 베타 체크리스트**:
- [ ] 다양한 사업자번호로 회원 등록
- [ ] 다양한 은행/카드사 연동 확인
- [ ] 고객 직접 인증서/계좌/카드 등록 가능 확인
- [ ] 고객 피드백 수집 및 반영
- [ ] 성능 (다수 테넌트 동시 동기화)
---
## 6. Phase 4: 정식 출시
> **핵심**: 운영 모드 전환, 과금 시작, 온보딩 프로세스 가동
### 6.1 출시 준비 체크리스트
**인프라**:
- [ ] API 서버 `php-soap` 확장 설치 확인
- [ ] 운영 `.env``BAROBILL_CERT_KEY_PROD`, `BAROBILL_CORP_NUM` 설정
- [ ] `BAROBILL_TEST_MODE=false` 설정
- [ ] 동기화 스케줄러 Supervisor 등록
- [ ] 바로빌 운영 CERTKEY 충전잔액 확보
**과금**:
- [ ] `barobill_pricing_policies` 요금 정책 데이터 입력
- [ ] 월정액 구독 자동 과금 배치 등록 (매월 1일)
- [ ] 과금 내역 고객 조회 화면 (선택)
**운영**:
- [ ] 인증서 만료 알림 (이메일/카카오톡)
- [ ] 충전잔액 부족 알림
- [ ] 동기화 실패 알림 및 재시도 로직
- [ ] 바로빌 장애 시 대응 매뉴얼
### 6.2 온보딩 프로세스 정립
정식 출시 후 신규 고객 가입 시:
```
계약 → 테넌트 생성 → 회원등록(테스트) → 인증서/계좌/카드 → 검증 → 운영전환 → 실무사용
```
> 상세 프로세스: `features/barobill/tenant-onboarding.md` 참조
---
## 7. 바로빌 파트너 정책 확인 필요 사항
> **경고: 개발 착수 전 바로빌 측에 확인해야 할 사항**
| # | 확인 사항 | 이유 | 현재 상태 |
|---|----------|------|----------|
| 1 | 멀티테넌트 CERTKEY 구조 | 파트너 1개 키로 다수 회원사 관리 가능한지 | 미확인 |
| 2 | 테스트 서버 제한 | 테스트 API 호출 횟수/기간 제한 | 미확인 |
| 3 | 과금 구조 | 파트너 단가표 (건당/월정액) | 미확인 |
| 4 | SLA | 바로빌 API 가용성 보장 수준 | 미확인 |
| 5 | 회원사 대량 등록 | 일괄 등록 API 또는 제한 | 미확인 |
| 6 | 인증서 대리 등록 | 고객 대신 등록 가능 여부 | 미확인 |
---
## 8. 리스크 및 대응
| 리스크 | 영향 | 대응 |
|--------|------|------|
| 바로빌 API 장애 | 거래 데이터 수집 중단 | 재시도 로직 + 장애 알림 |
| 인증서 만료 | 계좌/세금계산서 조회 불가 | 만료 30일 전 알림 |
| SOAP 호출 지연 | 페이지 응답 지연 | 비동기 Queue 처리 |
| 테넌트 급증 | 동기화 부하 | 호출 간격 분산, 우선순위 큐 |
| 충전잔액 부족 | API 호출 실패 | 잔액 모니터링 + 자동 알림 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [바로빌 연동 시스템](../../features/barobill/README.md) | 전체 구조, 모드, 과금 |
| [테넌트 온보딩](../../features/barobill/tenant-onboarding.md) | 온보딩 6단계 프로세스 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | REST API 42개 엔드포인트 |
| [이관 현황](../../system/migration-status.md) | MNG→API+React 전체 이관 현황 |
---
**최종 업데이트**: 2026-03-17

View File

@@ -3,7 +3,8 @@
> **시작일**: 2026-03-16
> **위치**: MNG 생산관리 > 절곡품 관리 (신규 메뉴)
> **목표**: 경동기업(5130) 수준의 절곡품 마스터 관리 + 전개도 데이터 + 이미지 관리
> **원칙**: 기존 BendingInfoBuilder/PrefixResolver 보존, items.options 확장 방식
> **원칙**: 기존 BendingInfoBuilder/PrefixResolver 보존, **전용 테이블 분리 방식**
> **최종수정**: 2026-03-19 (테이블 분리 완료, 데이터 이관 완료)
---
@@ -16,6 +17,340 @@ SAM은 절곡품의 "계산과 조합"(BendingInfoBuilder/PrefixResolver)은 잘
---
## ⚠️ 아키텍처 변경: items → 전용 테이블 분리 (2026-03-19)
### 변경 결정 배경
기존에는 `items` 테이블(`item_category='BENDING'`)에 `options` JSON으로 절곡 속성을 저장했으나,
다음 문제로 **전용 테이블 분리**로 방향 전환:
| 문제 | 설명 |
|------|------|
| **검색 불가** | 레거시 5130에서 "하장바" 검색 시 5건+ 나오지만 MNG2 기초관리에서 0건 |
| **options 누락** | BD-LEGACY-* 210건 중 상당수가 `options.item_name` 미채워짐 → 검색 불가 |
| **JSON 비정규화** | 20+개 절곡 속성이 options JSON 안에 있어 인덱싱/검색/정렬 불가 |
| **코드 체계 불일치** | BD-LEGACY-*, BD-{품명}-* 혼재, LOT 코드 체계 적용 불가 |
| **스키마 불명확** | options 키가 코드에만 정의(OPTION_KEYS), DB 레벨 제약 없음 |
### 하장바 검색 문제 — ✅ 해결됨
```
[레거시 5130] chandj.bending WHERE item_name LIKE '%하장바%'
→ 13건 (삭제 3건 제외 = 유효 10건)
[이전 MNG2 — items 방식] items WHERE item_category='BENDING' AND name LIKE '%하장바%'
→ 2건만 (options.item_name 누락 → 검색 불가)
[현재 MNG2 — bending_items 전용 테이블] bending_items WHERE item_name LIKE '%하장바%'
→ 10건 ✅ (정규 컬럼 item_name에 인덱스, chandj 유효건과 일치)
[해결 방법]
테이블 분리(bending_items) + bending:clean-reimport로 chandj.bending 직접 이관
→ item_name이 정규 컬럼으로 승격되어 검색 정상 동작
```
### 새 테이블 구조: `bending_items`
```sql
CREATE TABLE bending_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
-- 코드 체계 (LOT 코드 = 제품Code + 종류Code + YYMMDD)
code VARCHAR(50) NOT NULL, -- LOT: {제품}{종류}{YYMMDD} (예: CP260319 = 케이스 점검구)
legacy_code VARCHAR(50) NULL, -- 이전 BD-LEGACY-* / BD-{품명}-* 코드
legacy_bending_id INT UNSIGNED NULL, -- chandj.bending.id 참조
-- 기본 정보 (기존 options에서 정규 컬럼으로 승격)
item_name VARCHAR(100) NOT NULL, -- 품명 (검색 가능!)
item_sep VARCHAR(20) NULL, -- 대분류: 스크린/철재
item_bending VARCHAR(50) NULL, -- 중분류: 가이드레일/케이스/하단마감재/...
material VARCHAR(50) NULL, -- 재질: SUS 1.2T / EGI 1.55T
item_spec VARCHAR(100) NULL, -- 규격: 120*70
model_name VARCHAR(50) NULL, -- 소속 모델: KSS01
model_UA VARCHAR(20) NULL, -- 인정여부: 인정/비인정
-- 절곡 전용 속성
rail_width DECIMAL(10,2) NULL, -- 레일폭
exit_direction VARCHAR(20) NULL, -- 출구방향 (케이스 전용)
box_width DECIMAL(10,2) NULL, -- 박스폭 (케이스 전용)
box_height DECIMAL(10,2) NULL, -- 박스높이 (케이스 전용)
front_bottom DECIMAL(10,2) NULL, -- 전면밑 (케이스 전용)
inspection_door VARCHAR(20) NULL, -- 점검구 (케이스 전용)
-- 메타 (비정형 속성만 — 검색/필터 대상 아닌 것)
options JSON NULL, -- memo, author, search_keyword, modified_by 등
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by BIGINT UNSIGNED NULL,
updated_by BIGINT UNSIGNED NULL,
deleted_by BIGINT UNSIGNED NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
-- 인덱스
INDEX idx_tenant (tenant_id),
INDEX idx_item_name (item_name),
INDEX idx_item_sep (item_sep),
INDEX idx_item_bending (item_bending),
INDEX idx_material (material),
INDEX idx_model_name (model_name),
INDEX idx_code (code),
INDEX idx_legacy_code (legacy_code),
UNIQUE KEY uk_tenant_code (tenant_id, code, deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 전개도 데이터: `bending_items.bending_data` (JSON 컬럼)
> **변경 이력**: 초기 설계는 별도 `bending_data` 테이블이었으나, JSON 통합으로 최종 결정.
> 마이그레이션 `100007_move_bending_data_back_to_json`으로 `bending_data` 테이블 DROP 완료.
```json
// bending_items.bending_data JSON 구조
[
{ "no": 1, "input": 10, "rate": "", "sum": 10, "color": true, "aAngle": false },
{ "no": 2, "input": 11, "rate": "", "sum": 21, "color": false, "aAngle": false },
{ "no": 3, "input": 110, "rate": "-1", "sum": 130, "color": false, "aAngle": false }
]
```
### 테이블 관계도 (최종)
```
┌──────────────────────────┐ ┌──────────────────────┐
│ bending_items (266건) │ │ bending_models (62건) │
│ ──────────────────────── │ │ ──────────────────── │
│ 기초관리 마스터 │ │ 가이드레일/케이스/ │
│ 품명/재질/규격 (정규컬럼) │◄···│ 하단마감재 모델 │
│ bending_data: JSON (내장) │ │ components JSON │
│ code: RM260319 등 │ │ (sam_item_id 참조) │
└──────────┬───────────────┘ └──────────────────────┘
│ 코드 매핑 (FK 없음)
│ code 앞 2자리로 items.code 패턴 매칭
┌──────────────────────┐
│ items (기존 무변경) │
│ ──────────────────── │
│ BD-{PREFIX}-{LENGTH} │
│ 재고/BOM/작업지시서 │
│ BendingInfoBuilder │
└──────────────────────┘
```
### items 테이블과의 관계 — FK 없음, 코드 매핑
```
┌──────────────────────────┐ ┌──────────────────────────┐
│ bending_items (신규) │ │ items (기존 유지) │
│ ──────────────────────── │ │ ──────────────────────── │
│ 기초관리 마스터 전용 │ 코드 │ BD-{prod}{spec}-{length} │
│ 품명/재질/규격 정규 컬럼 │ ·····→ │ 재고/BOM/작업지시서 연결 │
│ 전개도 데이터 │ 매핑 │ item_category='BENDING' │
│ code: CP260319 등 │ │ 재고관리용 (무변경) │
│ bending_item_mappings 흡수│ │ │
└──────────────────────────┘ └──────────────────────────┘
연결 방식: FK 없음
- 재고 조회 필요 시: code 앞 2자리(제품+종류)로 items.code 패턴 매칭
- items 테이블의 재고/BOM 기능은 완전히 독립 유지
- BendingInfoBuilder는 items 테이블 계속 참조 (무변경)
- bending_item_mappings 테이블 → 제거 (code에 흡수)
```
### 영향도 분석 — 변경 / 무변경 구분
| 컴포넌트 | 변경 여부 | 설명 |
|----------|----------|------|
| `bending_items` 테이블 | **신규 생성** | 전용 테이블 + mappings 흡수 |
| `bending_item_mappings` 테이블 | **제거** | bending_items에 컬럼 흡수 |
| `BendingItemService` | **수정** | Item::where(BENDING) → BendingItem 모델 |
| `BendingItemResource` | **수정** | getOption() → 정규 컬럼 직접 참조 |
| `BendingItemController` (API) | 최소 수정 | 서비스만 바뀜, 라우트 동일 |
| `BendingItemMapping` 모델 | **제거** | |
| `BendingBaseController` (MNG) | 무변경 | API 클라이언트, URL 동일 |
| `BendingProductController` (MNG) | 무변경 | API 클라이언트 |
| `BendingInfoBuilder` | **무변경** | items 테이블 기반 (재고/BOM용) |
| `Migrate5130BendingStock` | **무변경** | items 테이블 재고 생성용 |
| `ValidateBendingItems` | **무변경** | items의 BD-* 재고 검증용 |
| `files` (이미지) | **수정** | fileable_type='BendingItem' |
| `work_order_items` | 무변경 | items.id 참조 유지 |
### 수정 대상 파일 및 완료 상태 (2026-03-19)
```
[API 프로젝트] /home/kkk/sam/api/
✅ 기초관리 (bending_items):
app/Models/BendingItem.php ← Eloquent (bending_data JSON, files)
app/Services/BendingItemService.php ← BendingItem 모델, JSON 직접 저장
app/Services/BendingCodeService.php ← BendingItem 조회 (LOT 코드)
app/Http/Resources/Api/V1/BendingItemResource.php ← 정규 컬럼 + bending_data JSON + 수치 int 캐스팅
app/Http/Requests/Api/V1/BendingItemStoreRequest.php ← unique → bending_items
app/Swagger/v1/BendingItemApi.php ← Swagger 스키마
✅ 절곡품 모델 (bending_models):
app/Models/BendingModel.php ← Eloquent (components JSON, files)
app/Services/GuiderailModelService.php ← BendingModel + component 이미지 자동 복사
app/Http/Resources/Api/V1/GuiderailModelResource.php ← 정규 컬럼 + 수치 int 캐스팅
✅ 파일 처리:
app/Http/Controllers/Api/V1/ItemsFileController.php ← items → bending_items → bending_models 폴백
✅ 이관 커맨드:
app/Console/Commands/BendingCleanReimport.php ← 기초관리 클린 재이관 + 이미지 (1커맨드)
app/Console/Commands/BendingModelImport.php ← 모델 이관 + 조립도 JSON 업로드 + component 이미지 복사 (1커맨드)
✅ 마이그레이션:
2026_03_19_100000_create_bending_items_table.php
2026_03_19_100001~100003 (bending_data 테이블 → JSON 통합 과정)
2026_03_19_100004_drop_bending_item_mappings_table.php
2026_03_19_100005_add_length_columns_to_bending_items.php
2026_03_19_100006_create_bending_models_table.php
2026_03_19_100007_move_bending_data_back_to_json.php
✅ 제거:
app/Models/Production/BendingItemMapping.php ← 삭제됨
app/Models/BendingDataRow.php ← 삭제됨 (JSON 통합)
bending_item_mappings 테이블 ← DROP
bending_data 테이블 ← DROP (JSON 통합)
무변경:
app/Http/Controllers/Api/V1/BendingItemController.php ← 서비스 주입 (변경 불필요)
app/Http/Controllers/Api/V1/GuiderailModelController.php ← 서비스 주입
app/Services/Production/BendingInfoBuilder.php ← items 직접 사용 (재고/BOM)
app/Console/Commands/Migrate5130BendingStock.php ← items 재고용
app/Console/Commands/ValidateBendingItems.php ← items 재고 검증용
```
### 전체 복원 커맨드
```bash
# Step 1: 기초관리 (265건 + bending_data JSON + 이미지 265건)
php artisan bending:clean-reimport --legacy-img-path=/tmp/bending_img
# Step 2: 절곡품 모델 (61건 + 조립도 61건 + 부품이미지 276건 + sam_item_id)
php artisan bending:model-import --legacy-path=/tmp/legacy_5130
# 사전 준비 (docker 컨테이너에 레거시 파일 복사):
docker cp /home/kkk/sam/5130/bending/img docker-api-1:/tmp/bending_img
docker compose exec -T api mkdir -p /tmp/legacy_5130
docker cp /home/kkk/sam/5130/guiderail docker-api-1:/tmp/legacy_5130/guiderail
docker cp /home/kkk/sam/5130/shutterbox docker-api-1:/tmp/legacy_5130/shutterbox
docker cp /home/kkk/sam/5130/bottombar docker-api-1:/tmp/legacy_5130/bottombar
```
### 데이터 현황 (2026-03-19 DB 검증 완료)
| 테이블 | 건수 | 소스 | 상태 |
|--------|------|------|------|
| bending_items | **266건** | chandj.bending 직접 (bending_data JSON 포함) | ✅ 이관 완료 |
| bending_models | **62건** | chandj guiderail 21 + shutterbox 30 + bottombar 11 | ✅ 이관 완료 |
| bending_item_mappings | 0건 | **DROP 완료** | ✅ 제거됨 |
| items (BENDING) | 215건 | 기존 재고/BOM용 — **무변경 유지** | ✅ 독립 |
| 파일 (R2 업로드) | 예정 건수 | 현재 | 비고 |
|------------------|----------|------|------|
| bending_item / bending_diagram | 266건 | ⬜ 미업로드 | `bending:clean-reimport --legacy-img-path` 필요 |
| bending_model / assembly_image | 62건 | ⬜ 미업로드 | `bending:model-import --legacy-path` 필요 |
| bending_model / component_image | ~280건 | ⬜ 미업로드 | 부품별 독립 복사본 (스냅샷) |
| 레거시 대비 | chandj | bending_models | 상태 |
|-------------|--------|----------------|------|
| 가이드레일 | 20건 | 21건 | ✅ |
| 케이스 | 30건 | 30건 | ✅ |
| 하단마감재(스크린) | 8건 | 8건 | ✅ |
| 하단마감재(철재) | 3건 | 3건 | ✅ |
| 검색 검증 | 이전(items) | 현재(bending_items) | 상태 |
|----------|------------|-------------------|------|
| 하장바 | 2건 | **10건** (chandj 유효건 일치) | ✅ 해결 |
> **이미지 업로드 안내**: 레거시 이미지 파일을 docker 컨테이너에 복사 후 artisan 커맨드 실행 필요 (위 "전체 복원 커맨드" 참조)
### 이미지 스냅샷 정책
```
기초관리 이미지 수정 → 모델 component에 영향 없음 (독립 복사본)
구조:
bending_items → files (bending_diagram) ← 원본 (수정 가능)
bending_models → components[].image_file_id ← 복사본 (독립)
→ files (assembly_image) ← 조립도 (별도)
신규 부품 추가 시:
API(GuiderailModelService)에서 image_file_id 자동 복사 예정
MNG2 editPartOriginal() → sam_item_id로 기초관리 편집 페이지 연결
```
### LOT 코드 체계
```
형식: {제품Code}{종류Code}{YYMMDD}
유니크: (tenant_id, code, length_code, deleted_at)
예시:
RS260319 + length_code=30 → 가이드레일(벽면) SUS마감 3000mm
CF260319 → 케이스 전면부
BS260319 + length_code=40 → 하단마감재(스크린) SUS 4000mm
변환 완료: BD-PREFIX-LEN 112건 → LOT 코드
미변환: BD-한글 58건, BD-LEGACY 40건 (legacy_code 유지, 향후 변환)
```
### LOT 코드 체계 (레거시 형태 유지)
```
형식: {제품Code}{종류Code}{YYMMDD}
예시:
RM260319 → 가이드레일(벽면형) 본체, 2026-03-19
RS260319 → 가이드레일(벽면형) SUS마감재, 2026-03-19
CF260319 → 케이스 전면부, 2026-03-19
BS260319 → 하단마감재(스크린) SUS, 2026-03-19
```
**LOT 코드 테이블 (정본)**:
| 제품 | 제품Code | 종류명 | 종류Code |
|------|----------|--------|----------|
| 가이드레일(벽면형) | R | 본체 | M |
| | | 본체(철재) | T |
| | | C형 | C |
| | | D형 | D |
| | | SUS 마감재 | S |
| 가이드레일(측면형) | S | 본체디딤 | M |
| | | 본체(철재) | T |
| | | C형 | C |
| | | D형 | D |
| | | SUS 마감재1 | S |
| | | SUS 마감재2 | U |
| 케이스 | C | 전면부 | F |
| | | 점검구 | P |
| | | 린텔부 | L |
| | | 후면코너부 | B |
| 하단마감재(스크린) | B | SUS | S |
| | | EGI | E |
| 하단마감재(철재) | T | SUS | S |
| | | EGI | E |
| L-Bar | L | 스크린용 | A |
| 연기차단재 | G | 화이바원단(W50) | I |
| | | 화이바원단(W80) | I |
### 데이터 현황 (2026-03-19 DB 검증 완료)
| 항목 | 건수 | 비고 |
|------|------|------|
| **bending_items** (전용 테이블) | **266건** | ✅ 전건 bending_data JSON 포함 |
| **bending_models** (전용 테이블) | **62건** | ✅ guiderail 21 + shutterbox 30 + bottombar 11 |
| items BENDING (기존, 무변경) | 215건 | 재고/BOM용 독립 유지 |
| bending_item_mappings | **DROP 완료** | bending_items.code에 흡수 |
| 하장바 (bending_items) | **10건** | ✅ chandj 유효건과 일치 |
| 이미지 (R2) | **미업로드** | 레거시 파일 docker 복사 후 커맨드 실행 필요 |
---
## MNG 현재 구조
### 생산관리 메뉴 (sidebar-static.blade.php)
@@ -47,6 +382,10 @@ SAM은 절곡품의 "계산과 조합"(BendingInfoBuilder/PrefixResolver)은 잘
```
Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React 연동)
✅ 완료 ✅ 완료 ✅ 완료 (샘플용) ⬜ 미착수
테이블 분리: ✅ 완료 (bending_items + bending_models 전용 테이블)
데이터 이관: ✅ 완료 (266건 기초관리 + 62건 모델)
이미지 업로드: ⬜ 미완료 (레거시 파일 docker 복사 후 커맨드 실행 필요)
```
| 문서 | 내용 | 상태 |
@@ -55,19 +394,23 @@ Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React
| `step2-API.md` | API 엔드포인트 + 컨트롤러 설계 | ✅ 완료 |
| `step3-MNG화면.md` | Blade 뷰 + HTMX + 메뉴 등록 | ✅ 완료 |
| `step4-React연동.md` | React 운영 화면 구현 | ⬜ 미착수 |
| `legacy-guiderail-analysis.md` | 레거시 guiderail 모듈 상세 분석 | ✅ 완료 |
### 완료된 작업 (2026-03-16~17)
### 완료된 작업 (2026-03-16~19)
**Step 1 완료:**
- `bending:fill-options` — BD-* prefix/분류 속성 자동 보강 (170건)
- `bending:import-legacy` — chandj 전개도(bendingData) 임포트 (139/170건)
- `guiderail:import-legacy` — chandj guiderail 20건 임포트
- `bending-product:import-legacy` — chandj shutterbox 30건 + bottombar 10건 임포트
**Step 1 완료 (DB 분석 + 테이블 분리):**
- `bending_items` 전용 테이블 생성 — 정규 컬럼 승격 (item_name, item_sep, material 등 인덱스)
- `bending_models` 전용 테이블 생성 — 가이드레일/케이스/하단마감재 3개 타입 통합
- `bending_data` 테이블 → JSON 통합 → `bending_items.bending_data` 컬럼
- `bending_item_mappings` 테이블 DROP — `bending_items.code`에 흡수
- `bending:clean-reimport` — chandj.bending 266건 직접 이관 (bending_data JSON 포함)
- `bending:model-import` — chandj guiderail 21 + shutterbox 30 + bottombar 11 = 62건 이관
- ~~`bending:fill-options`~~ / ~~`bending:import-legacy`~~ — 구 items 방식 커맨드 (대체됨)
**Step 2 완료:**
**Step 2 완료 (API):**
- `BendingItemController` — CRUD + filters + pagination (6 엔드포인트)
- `GuiderailModelController` — CRUD + filters (6 엔드포인트, 3개 카테고리 통합)
- `BendingItemResource` / `GuiderailModelResource` — API 응답 포맷
- `BendingItemResource` / `GuiderailModelResource` — API 응답 포맷 (정규 컬럼 직접 참조)
- `FormRequest` — Index/Store/Update 유효성 검증
- `ApiKeyMiddleware` — bending/guiderail/files 화이트리스트
@@ -79,6 +422,10 @@ Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React
- 파일: FileViewController (API R2 프록시) + 이미지 업로드/표시
- DB 메뉴: 기초관리 + 절곡품 + 케이스 + 하단마감재 (4개)
**미완료:**
- ⬜ 이미지 R2 업로드 — 레거시 파일 docker 복사 후 커맨드 재실행 필요
- ⬜ Step 4 React 연동 — 미착수
---
## 참조 문서
@@ -121,11 +468,13 @@ Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React
│ │ Blade │ │ Laravel │ │ Next.js │ │
│ └──────────┘ └─────┬────┘ └──────────┘ │
│ │ │
│ ┌────────┐
│ │ samdb
│ │ items │ ← item_category = 'BENDING'
│ │ files │ ← field_key = 'bending_diagram'
└─────────┘
│ ┌─────────────┐ │
│ │ samdb
│ │bending_items ← 기초관리 마스터 (전용, bending_data JSON 포함)
│ │bending_models│ ← 절곡품 모델 (가이드레일/케이스/하단마감재)
│ items ← 재고/BOM용 (기존 무변경)
│ │ files │ ← bending_diagram 이미지 │
│ └─────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ R2 │ ← Cloudflare (이미지 저장) │
@@ -136,37 +485,34 @@ Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React
---
## 2. 데이터 구조 (2계층)
## 2. 데이터 구조 (2계층 — 전용 테이블 + JSON)
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ [1계층] 기초관리 — 개별 부품 (items 테이블)
│ ════════════════════════════════════════
│ [1계층] 기초관리 — 개별 부품 (bending_items 전용 테이블) │
│ ══════════════════════════════════════════════
│ │
items (item_category = 'BENDING')
bending_items (266건)
│ ┌──────────────────────────────────────────────────┐ │
│ │ id: 100 │ │
│ │ code: BD-가이드레일-KSS01-SUS-120*70 │
│ │ name: 가이드레일 KSS01 SUS 120*70
│ │ options: { │ │
│ │ item_name: "마감재" ← 부품 품명 │ │
│ │ item_sep: "스크린" ← 대분류 │ │
│ │ item_bending: "가이드레일" ← 중분류 │ │
│ │ material: "SUS 1.2T" ← 재질 │ │
│ │ model_name: "KSS01"소속 모델 │ │
│ │ model_UA: "인정" ← 인정여부 │ │
│ │ item_spec: "120*70" ← 규격 │ │
│ │ rail_width: 70 ← 레일폭 │ │
│ │ bendingData: [ ← 전개도 데이터 │ │
│ │ {no:1, input:10, rate:"", sum:10, ...}, │ │
│ │ {no:2, input:11, rate:"", sum:21, ...},
│ │ ... │ │
│ │ ] │ │
│ │ + 케이스전용: exit_direction, box_width, ... │ │
│ │ } │ │
│ │ code: RM260319 ← LOT 코드 (제품+종류+날짜)│
│ │ legacy_code: BD-LEGACY-042 ← 이전 코드 보존 │ │
│ │ item_name: "마감재" ← 정규 컬럼 (인덱스) │ │
│ │ item_sep: "스크린" ← 정규 컬럼 (인덱스) │ │
│ │ item_bending: "가이드레일" ← 정규 컬럼 (인덱스) │ │
│ │ material: "SUS 1.2T" ← 정규 컬럼 (인덱스) │ │
│ │ model_name: "KSS01" ← 정규 컬럼 │ │
│ │ model_UA: "인정" 정규 컬럼 │ │
│ │ item_spec: "120*70" ← 정규 컬럼 │ │
│ │ rail_width: 70 ← 정규 컬럼 │ │
│ │ + 케이스전용: exit_direction, box_width... 정규 │ │
│ │ bending_data: JSON 배열 ← 전개도 데이터 (내장) │ │
│ │ [{no:1, input:10, rate:"", sum:10, color:true}, │ │
│ │ {no:2, input:11, rate:"", sum:21, color:false}, │ │
│ │ {no:3, input:110, rate:"-1", sum:130}, ...] │ │
│ └──────────────────────────────────────────────────┘ │
│ ↑ 265건 (레거시) + α
│ ↑ 266건 (전건 bending_data JSON 포함)
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
@@ -188,7 +534,7 @@ Step 1 (DB분석) → Step 2 (API) → Step 3 (MNG 화면) → Step 4 (React
│ │ │ │
│ │ 재질별 폭합: SUS 1.2T → 406 | EGI 1.55T → 398 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↑ 가이드레일 20건 + 케이스 + 하단마감재
│ ↑ 가이드레일 21건 + 케이스 30건 + 하단마감재 11건 = 62건
│ │
└─────────────────────────────────────────────────────────────────┘
```
@@ -391,20 +737,28 @@ Step 1 Step 2 Step 3 Step 4
## 9. 레거시 → SAM 대응표
```
레거시 (5130) SAM
━━━━━━━━━━━━━ ━━━━━
chandj.bending (265건) → items (item_category='BENDING') + options
chandj.guiderail (20건) → guiderail-models API (신규 저장 구조)
레거시 (5130) SAM (테이블 분리 완료)
━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━
chandj.bending (265건) → bending_items (266건, 전용 테이블, 정규 컬럼)
chandj.bending.전개도 배열 → bending_items.bending_data (JSON 컬럼, 내장)
chandj.guiderail (20건) → bending_models (62건, guiderail+shutterbox+bottombar)
guiderail/list.php → MNG /bending/products (절곡품 목록)
bending CRUD → MNG /bending/base (기초관리)
put_guiderail_image.php → 기존 ItemsFileController (R2)
put_guiderail_image.php → 기존 FileController (R2) — ⬜ 이미지 업로드 미완료
fetch_guiderail_detail.php → React GuiderailPreview
drawingTool.js (Canvas) → 2차 구현 (1차는 이미지 업로드만)
inputList[] (별도 배열) → bendingData[] (객체 배열)
bendingrateList[] → bendingData[].rate
sumList[] → bendingData[].sum
colorList[] → bendingData[].color
AList[] → bendingData[].aAngle
inputList[] (별도 배열 5개) → bending_data JSON [{no, input, rate, sum, color, aAngle}]
items (BENDING) + options → items 유지 (재고/BOM용, BendingInfoBuilder 무변경)
bending_item_mappings DROP 완료 (bending_items.code에 흡수)
```
---
## 10. 레거시 guiderail 모듈 상세 분석
> 별도 문서로 분리: [`legacy-guiderail-analysis.md`](./legacy-guiderail-analysis.md)
>
> 포함 내용: 파일 구성(21개), DB 스키마(guiderail/bending), CRUD 흐름,
> 전개도 생성, 구성요소(벽면형/측면형), 검색/필터, guidebook

View File

@@ -388,3 +388,79 @@ php artisan bending-model:import-assembly-images # ✅ 결합형태 이미지 6
- [x] artisan command 7개 (위 목록 참조)
- [x] CRUD 검증 완료
- [x] 이미지 마이그레이션 완료 (총 473건 R2 업로드)
{
"lot_no_code_table": [
{
"제품": "가이드레일(벽면형)",
"제품Code": "R",
"종류": [
{ "종류명": "본체", "Code": "M" },
{ "종류명": "본체(철재)", "Code": "T" },
{ "종류명": "C형", "Code": "C" },
{ "종류명": "D형", "Code": "D" },
{ "종류명": "SUS 마감재", "Code": "S" }
]
},
{
"제품": "가이드레일(측면형)",
"제품Code": "S",
"종류": [
{ "종류명": "본체디딤", "Code": "M" },
{ "종류명": "본체(철재)", "Code": "T" },
{ "종류명": "C형", "Code": "C" },
{ "종류명": "D형", "Code": "D" },
{ "종류명": "SUS 마감재①", "Code": "S" },
{ "종류명": "SUS 마감재②", "Code": "U" }
]
},
{
"제품": "케이스",
"제품Code": "C",
"종류": [
{ "종류명": "전면부", "Code": "F" },
{ "종류명": "점검구", "Code": "P" },
{ "종류명": "린텔부", "Code": "L" },
{ "종류명": "후면코너부", "Code": "B" }
]
},
{
"제품": "하단마감재(스크린)",
"제품Code": "B",
"종류": [
{ "종류명": "SUS", "Code": "S" },
{ "종류명": "EGI", "Code": "E" }
]
},
{
"제품": "하단마감재(철재)",
"제품Code": "T",
"종류": [
{ "종류명": "SUS", "Code": "S" },
{ "종류명": "EGI", "Code": "E" }
]
},
{
"제품": "L-Bar",
"제품Code": "L",
"종류": [
{ "종류명": "스크린용", "Code": "A" }
]
},
{
"제품": "연기차단재",
"제품Code": "G",
"종류": [
{ "종류명": "화이바원단(W50)", "Code": "I" },
{ "종류명": "화이바원단(W80)", "Code": "I" }
]
}
],
"비고": {
"년월일": "Code 참조"
}
}

View File

@@ -0,0 +1,370 @@
# Step 5: Canvas 그리기 기능 (5130 → MNG 적용)
> **프로젝트**: MNG (`sam/mng`)
> **선행 조건**: Step 3 (MNG 화면) 완료
> **참조**: 레거시 `5130/js/imageEditor.js`, `5130/js/drawingModule.js`
> **상태**: ⬜ 분석 완료, 구현 미착수
---
## 1. 레거시 Canvas 분석 결과
### 1-1. 5130 Canvas 구현체 3개
| 파일 | 크기 | 라이브러리 | 용도 | 채택 여부 |
|------|------|-----------|------|:---:|
| `5130/js/imageEditor.js` | ~511줄 | **Fabric.js 5.3.0** | 프로덕션 이미지 에디터 (모달 dialog) | ✅ **채택** |
| `5130/js/drawingModule.js` | ~966줄 | Pure Canvas 2D | 독립 모달 + 전체 UI 포함 | ❌ 중복 |
| `5130/js/drawLib.js` | ~272줄 | Pure Canvas 2D | 경량 버전 | ❌ 기능 부족 |
**채택 이유**: `imageEditor.js`가 Fabric.js 기반으로 가장 안정적이며, 오브젝트 선택/편집/삭제 등 고급 기능 지원.
### 1-2. imageEditor.js 핵심 기능
```
┌─────────────────────────────────────────────────────────┐
│ Canvas Editor (1300×800 모달 dialog) │
├─────────────────────────────────────────────────────────┤
│ │
│ [Polyline] [Free] [Line] [Text] [Eraser] [Select] │ ← 도구 모음
│ [Clear] [Apply] │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ Canvas (800×600) │ │
│ │ │ │
│ │ · 직각 고정 모드 (0°/90°/180°/270°) │ │
│ │ · 프리뷰 라인 (대시) │ │
│ │ · SVG 커서 (지우개) │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 색상: [●검정] [●빨강] [●파랑] [●초록] [●주황] [●보라] │
│ 선굵기: [━━━] 지우개크기: [━━━] │
│ │
└─────────────────────────────────────────────────────────┘
```
**그리기 모드**:
| 모드 | 키보드 | 동작 |
|------|--------|------|
| Polyline | (기본) | 클릭으로 점 찍기 → 선 연결, ESC로 종료 |
| Free | — | 드래그로 자유 그리기 (PencilBrush) |
| Line | L | 클릭+드래그 → 직선, 직각 고정 지원 |
| Text | — | 클릭 → IText 생성 → 인라인 편집 |
| Eraser | — | 드래그로 지우기 (SVG 원형 커서, 5~100px) |
| Select | — | 오브젝트 선택/이동/삭제 (Delete키) |
**직각 고정 알고리즘**:
```javascript
// 각도 계산 후 0°/90°/180°/270° 중 가장 가까운 방향으로 스냅
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
// → horizontal or vertical 결정
```
### 1-3. 5130에서의 사용처
| 화면 | Canvas 크기 | 배경 이미지 | 용도 |
|------|------------|-----------|------|
| `bending/write_form.php` | 370×300 | ❌ 없음 | 전개도 그리기 |
| `guiderail/list.php` | 800×600 | ✅ 제품 사진 위 | 치수 표기 |
| `shutterbox/list.php` | 800×600 | ✅ 제품 사진 위 | 치수 표기 |
| `bottombar/list.php` | 800×600 | ✅ 제품 사진 위 | 치수 표기 |
---
## 2. MNG 현재 Canvas 현황
### 2-1. 기존 Canvas 사용처 (참고용)
| 화면 | 라이브러리 | 용도 | 재활용 |
|------|-----------|------|:---:|
| `esign/sign/sign.blade.php` | signature_pad 4.1.7 | 전자서명 | ❌ 다른 용도 |
| `document-templates/block-editor.blade.php` | Alpine.js DOM | 블록 드래그 | ❌ 다른 용도 |
| `rd/fire-shutter-drawing/index.blade.php` | Pure Canvas | 방화셔터 도면 | ❌ 특화 로직 |
### 2-2. MNG 기술 스택
| 항목 | 현재 | Canvas 추가 시 |
|------|------|---------------|
| JS 프레임워크 | Alpine.js 3.x | Alpine.js 유지 |
| HTMX | 1.9.10 | HTMX 유지 |
| CSS | Tailwind + DaisyUI | 유지 |
| Canvas 라이브러리 | ❌ 없음 | **Fabric.js 5.3.0 CDN 추가** |
| 아이콘 | RemixIcon | RemixIcon (또는 Bootstrap Icons 병행) |
---
## 3. 적용 대상 화면
### 3-1. 기초관리 폼 (`/bending/base/{id}/edit`)
```
현재 (1차 구현): 추가 (2차):
┌──────────────────┐ ┌──────────────────┐
│ [형상 이미지] │ │ [형상 이미지] │
│ │ │ │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │ 미리보기 │ │ │ │ 미리보기 │ │
│ └────────────┘ │ → │ └────────────┘ │
│ │ │ │
│ [파일 선택] │ │ [파일 선택] │
│ [Ctrl+V 붙여넣기]│ │ [Ctrl+V 붙여넣기]│
│ │ │ [✏️ 그리기] │ ← Canvas 모달
│ │ │ │
└──────────────────┘ └──────────────────┘
```
### 3-2. 절곡품 폼 (`/bending/products/{id}/edit`)
```
현재: 추가:
┌──────────────────┐ ┌──────────────────┐
│ [결합형태 이미지] │ │ [결합형태 이미지] │
│ [파일 선택] │ → │ [파일 선택] │
│ │ │ [✏️ 그리기] │ ← Canvas 모달
└──────────────────┘ └──────────────────┘
```
---
## 4. 구현 설계
### 4-1. 파일 구조
```
mng/
├── public/js/
│ └── canvas-editor.js ← imageEditor.js 이식 (MNG 맞춤)
├── resources/views/
│ └── components/
│ └── canvas-editor.blade.php ← 모달 Blade 컴포넌트
```
### 4-2. Fabric.js 로딩
```html
<!-- layouts/app.blade.php 또는 필요한 페이지에서만 -->
@stack('scripts')
<!-- canvas-editor 사용 페이지에서 -->
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
<script src="{{ asset('js/canvas-editor.js') }}"></script>
@endpush
```
### 4-3. Blade 컴포넌트 설계
```html
<!-- 사용법 -->
<x-canvas-editor
target-input="image_data" {{-- hidden input name --}}
:existing-image="$imageUrl" {{-- 기존 이미지 URL --}}
:width="800"
:height="600"
/>
<!-- 렌더링 -->
<dialog id="canvas-editor-dialog" class="modal">
<div class="modal-box max-w-6xl p-0">
<!-- 도구 모음 -->
<div class="flex gap-1 p-2 bg-base-200 border-b">
<button class="btn btn-sm" data-mode="polyline">Polyline</button>
<button class="btn btn-sm" data-mode="free">Free</button>
<button class="btn btn-sm" data-mode="line">Line</button>
<button class="btn btn-sm" data-mode="text">Text</button>
<button class="btn btn-sm btn-warning" data-mode="eraser">Eraser</button>
<button class="btn btn-sm" data-mode="select">Select</button>
<div class="divider divider-horizontal mx-0"></div>
<input type="checkbox" class="toggle toggle-sm" id="right-angle" checked>
<label for="right-angle" class="text-sm">직각고정</label>
<div class="flex-1"></div>
<button class="btn btn-sm btn-error" id="canvas-clear">초기화</button>
<button class="btn btn-sm btn-success" id="canvas-apply">적용</button>
</div>
<!-- Canvas -->
<canvas id="fabric-canvas" width="800" height="600"></canvas>
<!-- 색상/옵션 -->
<div class="flex gap-2 p-2 bg-base-200 border-t items-center">
<!-- 색상 팔레트 -->
<div class="flex gap-1">
<button class="w-6 h-6 rounded-full bg-black" data-color="#000000"></button>
<button class="w-6 h-6 rounded-full bg-red-600" data-color="#ff0000"></button>
<button class="w-6 h-6 rounded-full bg-blue-600" data-color="#0000ff"></button>
<button class="w-6 h-6 rounded-full bg-green-600" data-color="#00aa00"></button>
<button class="w-6 h-6 rounded-full bg-orange-500" data-color="#ff8800"></button>
<button class="w-6 h-6 rounded-full bg-purple-600" data-color="#800080"></button>
</div>
<div class="divider divider-horizontal mx-0"></div>
<!-- 선 굵기/지우개 크기 -->
<label class="text-sm">선:</label>
<input type="range" min="1" max="10" value="2" class="range range-xs w-20" id="line-width">
<label class="text-sm">지우개:</label>
<input type="range" min="5" max="100" value="20" class="range range-xs w-20" id="eraser-size">
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
```
### 4-4. canvas-editor.js 핵심 구조
```javascript
class CanvasEditor {
constructor(options) {
this.canvas = null; // Fabric.js Canvas
this.mode = 'polyline'; // 현재 모드
this.drawColor = '#000000';
this.lineWidth = 2;
this.eraserSize = 20;
this.rightAngle = true; // 직각 고정
this.polyPoints = []; // polyline 점 목록
this.previewLine = null; // 프리뷰 라인
this.targetInput = options.targetInput; // 결과 저장할 input
this.existingImage = options.existingImage;
}
// 주요 메서드
init() // Fabric.js 캔버스 초기화
setMode(mode) // 모드 변경
loadBackgroundImage(url) // 기존 이미지를 배경으로 로드
handlePolyline(e) // polyline 클릭 처리
handleLine(e) // 직선 드래그
handleText(e) // 텍스트 배치
setupEraser() // PencilBrush 지우개 모드
snapToRightAngle(p1,p2) // 직각 고정 계산
clear() // 캔버스 초기화
apply() // Base64 → hidden input, 모달 닫기
toDataURL() // PNG/JPEG 내보내기
}
```
### 4-5. 데이터 흐름
```
[그리기 버튼 클릭]
→ Canvas 모달 열기
→ (기존 이미지 있으면) 배경 이미지로 로드
→ 사용자 그리기 작업
→ [적용] 클릭
→ Canvas → Base64 (DataURL)
→ hidden input에 저장
→ 폼 submit 시 API 전송
→ API: Base64 디코딩 → R2 저장 → file_id 반환
```
---
## 5. 이식 시 변경 사항
### 5-1. 5130 → MNG 차이점
| 항목 | 5130 (레거시) | MNG (SAM) |
|------|-------------|-----------|
| 모달 | `<dialog>` 직접 생성 | DaisyUI `modal` 컴포넌트 |
| 아이콘 | Bootstrap Icons CDN | RemixIcon (이미 로드됨) |
| CSS | 인라인 + Bootstrap | Tailwind + DaisyUI |
| JS 구조 | 전역 함수 | Class 기반 모듈 |
| 이벤트 | jQuery | Vanilla JS + Alpine.js |
| 이미지 저장 | FormData → PHP 파일 저장 | Base64 → API → R2 |
| 배경 이미지 | 별도 로드 로직 | `/files/{id}/view` 프록시 |
### 5-2. 제거할 것 (5130 전용)
```
❌ jQuery 의존성 (MNG는 Alpine.js)
❌ Bootstrap Icons CDN (RemixIcon으로 교체)
❌ 전역 변수/함수 (Class로 캡슐화)
❌ 인라인 CSS 스타일 (Tailwind 클래스로 교체)
```
### 5-3. 추가할 것 (MNG 맞춤)
```
✅ DaisyUI 모달 통합
✅ Alpine.js x-data 바인딩 (모달 상태 관리)
✅ RemixIcon 아이콘 매핑
✅ HTMX 호환 (폼 submit 시 hx-post 지원)
✅ Base64 → API 업로드 로직
✅ 반응형 캔버스 크기 (모바일 대응 불필요 — MNG는 PC 전용)
```
---
## 6. 아이콘 매핑 (Bootstrap Icons → RemixIcon)
| 기능 | Bootstrap Icons | RemixIcon |
|------|----------------|-----------|
| Polyline | `bi-vector-pen` | `ri-pen-nib-line` |
| Free Draw | `bi-brush` | `ri-brush-line` |
| Line | `bi-slash-lg` | `ri-subtract-line` |
| Text | `bi-type` | `ri-text` |
| Eraser | `bi-eraser-fill` | `ri-eraser-line` |
| Select | `bi-cursor-text` | `ri-cursor-line` |
| Clear | (텍스트) | `ri-delete-bin-line` |
| Apply | (텍스트) | `ri-check-line` |
---
## 7. 작업 순서
```
7-1. Fabric.js CDN 추가 (@push('scripts'))
7-2. canvas-editor.js 작성 (imageEditor.js 기반 이식)
7-3. canvas-editor.blade.php 컴포넌트 생성
7-4. 기초관리 폼에 [그리기] 버튼 + 컴포넌트 통합
7-5. 절곡품 폼에 동일 적용
7-6. API 연동 (Base64 → 이미지 업로드)
7-7. 테스트 (그리기 → 저장 → 재로드 → 편집)
```
### 예상 파일 변경
| 파일 | 변경 내용 |
|------|----------|
| `public/js/canvas-editor.js` | 🆕 신규 (imageEditor.js 이식) |
| `resources/views/components/canvas-editor.blade.php` | 🆕 신규 (모달 컴포넌트) |
| `resources/views/bending/base/form.blade.php` | 수정 — [그리기] 버튼 추가 |
| `resources/views/bending/products/form.blade.php` | 수정 — [그리기] 버튼 추가 |
| `resources/views/layouts/app.blade.php` | 확인 — `@stack('scripts')` 존재 여부 |
---
## 8. 주의사항
### 아키텍처
- ✅ MNG는 **샘플 확인용** — 동일 로직을 React에도 적용 예정
-`canvas-editor.js`**프레임워크 무관** Class로 작성 → React 포팅 용이
- ✅ Fabric.js는 CDN으로 로드 (npm 설치 불필요 — MNG는 Vite 빌드 최소화)
- ❌ drawingModule.js (966줄) 이식 불필요 — imageEditor.js로 충분
### 기존 코드 보호
- ⚠️ 기존 이미지 업로드 로직 유지 (그리기는 **추가** 옵션)
- ⚠️ 기존 Ctrl+V 붙여넣기 유지
- ⚠️ `canvas-editor.js`는 새 파일 — 기존 JS 무변경
### 성능
- ⚠️ Fabric.js 5.3.0 = ~800KB (CDN gzip ~200KB) — 필요 페이지에서만 로드
- ⚠️ Base64 이미지 크기 → JPEG 변환 (PNG 대비 1/3~1/5 크기)
---
## 관련 문서
| 문서 | 경로 |
|------|------|
| Step 3 MNG 화면 (이미지 전략) | `step3-MNG화면.md` §7 |
| 레거시 분석 | `legacy-guiderail-analysis.md` |
| 5130 imageEditor.js | `5130/js/imageEditor.js` |
| 5130 drawingModule.js | `5130/js/drawingModule.js` |
---
**최종 업데이트**: 2026-03-18

View File

@@ -294,9 +294,294 @@ BD-XX-XXX 품목이 `items` 테이블에 등록만 되어 있고, **절곡품
4. **전개도 데이터 구조** — 치수/연신율/합계 JSON 저장 방안 설계
5. **이미지 업로드 기능** — 절곡품별 전개도 이미지 관리
---
## 6. API 구현 완료 현황 (2026-03-16)
### 6.1 절곡품 마스터 API (`/api/v1/bending-items`)
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| GET | `/api/v1/bending-items` | 목록 (필터+페이지네이션) | ✅ 완료 |
| GET | `/api/v1/bending-items/filters` | 필터 옵션 (드롭다운용) | ✅ 완료 |
| GET | `/api/v1/bending-items/{id}` | 상세 | ✅ 완료 |
| POST | `/api/v1/bending-items` | 등록 | ✅ 완료 |
| PUT | `/api/v1/bending-items/{id}` | 수정 | ✅ 완료 |
| DELETE | `/api/v1/bending-items/{id}` | 삭제 | ✅ 완료 |
**API 파일 목록**:
| 파일 | 설명 |
|------|------|
| `app/Http/Controllers/Api/V1/BendingItemController.php` | 컨트롤러 |
| `app/Services/BendingItemService.php` | 서비스 (OPTION_KEYS 정의) |
| `app/Http/Resources/Api/V1/BendingItemResource.php` | 응답 리소스 |
| `app/Http/Requests/Api/V1/BendingItemIndexRequest.php` | 목록 검증 |
| `app/Http/Requests/Api/V1/BendingItemStoreRequest.php` | 등록 검증 |
| `app/Http/Requests/Api/V1/BendingItemUpdateRequest.php` | 수정 검증 |
| `routes/api/v1/production.php` | 라우트 정의 (127~135행) |
### 6.2 이미지 업로드 API (`/api/v1/items/{id}/files`)
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| POST | `/api/v1/items/{id}/files` | 업로드 (`field_key=bending_diagram`) | ✅ 완료 |
| GET | `/api/v1/items/{id}/files` | 파일 목록 (`?field_key=bending_diagram`) | ✅ 완료 |
| DELETE | `/api/v1/items/{id}/files/{fileId}` | 파일 삭제 | ✅ 완료 |
| GET | `/api/v1/files/{id}/view` | 인라인 보기 (이미지 표시) | ✅ 완료 |
| GET | `/api/v1/files/{id}/download` | 다운로드 | ✅ 완료 |
**R2 저장 경로**: `{tenant_id}/items/{year}/{month}/{hex}.{ext}`
**예시**: `287/items/2026/03/1b4eba14ff5a832b.jpg`
### 6.3 API 응답 구조
#### 절곡품 상세 (`GET /api/v1/bending-items/{id}`)
```json
{
"success": true,
"message": "조회 성공",
"data": {
"id": 15862,
"code": "BD-BE-30",
"name": "하단마감재(스크린) EGI 3000mm",
"item_type": "PT",
"item_category": "BENDING",
"unit": "EA",
"is_active": true,
"item_name": "하단마감재",
"item_sep": "스크린",
"item_bending": "하단마감재",
"item_spec": "60*40",
"material": "EGI 1.55T",
"model_name": null,
"model_UA": "인정",
"search_keyword": null,
"rail_width": null,
"registration_date": "2025-07-21",
"author": "개발자",
"memo": "메모",
"exit_direction": null,
"front_bottom_width": null,
"box_width": null,
"box_height": null,
"bendingData": [
{ "no": 1, "input": 15, "rate": null, "sum": 15, "color": false, "aAngle": false },
{ "no": 2, "input": 14, "rate": "-1", "sum": 28, "color": false, "aAngle": false },
{ "no": 3, "input": 40, "rate": "-1", "sum": 67, "color": false, "aAngle": false }
],
"prefix": "BE",
"length_code": "30",
"length_mm": 3000,
"width_sum": 193,
"bend_count": 6,
"created_at": "2026-02-21 19:47:01",
"updated_at": "2026-03-16 21:11:12"
}
}
```
#### 필터 옵션 (`GET /api/v1/bending-items/filters`)
```json
{
"success": true,
"data": {
"item_sep": ["스크린", "철재"],
"item_bending": ["가이드레일", "케이스", "하단마감재", "마구리", "L-BAR"],
"material": ["EGI 1.15T", "EGI 1.55T", "SUS 1.2T", "SUS 1.5T"],
"model_UA": ["비인정", "인정"],
"model_name": ["KSS01", "KSS02", "KSE01"]
}
}
```
### 6.4 MNG 관리 화면 (완료)
| 화면 | URL | 설명 |
|------|-----|------|
| 목록 | `/bending/base` | 필터 + 페이지네이션 |
| 등록 | `/bending/base/create` | 폼 + 전개도 테이블 + 이미지 업로드 |
| 상세 | `/bending/base/{id}` | 읽기 전용 |
| 수정 | `/bending/base/{id}/edit` | 수정 모드 + 이미지 교체 |
### 6.5 레거시 분류 조건 분석
`item_bending` 컬럼으로 타입 구분 (같은 bending 테이블 사용):
| `item_bending` 값 | 전용 필드 | 레거시 디렉토리 |
|-------------------|----------|----------------|
| `가이드레일` | `rail_width` | `/bending/` |
| `케이스` | `exit_direction`, `box_width`, `box_height`, `front_bottom_width` | `/shutterbox/` |
| `하단마감재` | 없음 (공통 필드만) | `/bottombar/` |
| `마구리` | 없음 | - |
| `L-BAR` | 없음 | - |
| `보강평철` | 없음 | - |
| `연기차단재` | 없음 | - |
---
## 7. React 연동 시 참고사항
### 7.1 서버 액션 추가 필요
React에 `/api/v1/bending-items` 전용 서버 액션이 없음. 추가 필요:
```
예상 파일: src/components/bending/actions.ts
필요 함수:
- fetchBendingItems(params) → GET /api/v1/bending-items
- fetchBendingFilters() → GET /api/v1/bending-items/filters
- fetchBendingItem(id) → GET /api/v1/bending-items/{id}
- createBendingItem(data) → POST /api/v1/bending-items
- updateBendingItem(id, data) → PUT /api/v1/bending-items/{id}
- deleteBendingItem(id) → DELETE /api/v1/bending-items/{id}
- uploadBendingImage(itemId, file) → POST /api/v1/items/{id}/files
```
### 7.2 bendingData 필드 매핑 (API → React)
API 응답의 `bendingData`와 기존 React `BendingDetail` 타입 불일치:
| API 필드 | API 타입 | React 기존 타입 (`BendingDetail`) | 조치 |
|----------|---------|----------------------------------|------|
| `no` | integer | `no: number` | ✅ 일치 |
| `input` | numeric | `input: number` | ✅ 일치 |
| `rate` | string \| null | `elongation: number` | ⚠️ 이름+타입 다름 |
| `sum` | numeric | `sum: number` | ✅ 일치 |
| `color` | boolean | `shaded: boolean` | ⚠️ 이름 다름 |
| `aAngle` | boolean | `aAngle?: number` | ⚠️ 타입 다름 |
| - | - | `id: string` | React에만 존재 (클라이언트 전용) |
| - | - | `calculated: number` | React에만 존재 (클라이언트 계산값) |
**권장**: React 타입을 API에 맞추거나, 변환 레이어 추가
```typescript
// API → React 변환 예시
function transformBendingData(apiData: ApiBendingData[]): BendingDetail[] {
return apiData.map((d, i) => ({
id: `detail-${i}`,
no: d.no,
input: d.input,
elongation: d.rate ? parseFloat(d.rate) : -1, // rate → elongation
calculated: d.input + (d.rate ? parseFloat(d.rate) : 0),
sum: d.sum,
shaded: d.color, // color → shaded
aAngle: d.aAngle ? 1 : 0, // boolean → number
}));
}
```
### 7.3 인증 방식
API는 현재 **Bearer 토큰 없이 X-TENANT-ID 헤더**로 동작 (화이트리스트):
- `api/v1/bending-items`, `api/v1/bending-items/*`
- `api/v1/items/*/files`
- `api/v1/files/*/view`, `api/v1/files/*/download`
React는 **HttpOnly Cookie + Next.js 프록시**로 인증하므로, Bearer 토큰이 자동 전달됨.
React 연동 시 화이트리스트 의존 없이 정상 인증 경로로 동작할 것.
### 7.4 이미지 표시 경로
```
React에서 이미지 표시:
프록시 경로: /api/proxy/files/{fileId}/view
→ Next.js API Route가 Bearer 토큰 붙여서
→ API: GET /api/v1/files/{fileId}/view
→ R2에서 stream
```
### 7.5 Update 시 code unique 검증
`BendingItemUpdateRequest`에 자기 자신 제외 unique 체크 누락:
```php
// 현재 (미흡)
'code' => 'sometimes|string|max:100',
// 수정 필요
'code' => 'sometimes|string|max:100|unique:items,code,' . $this->route('id'),
```
### 7.6 React API 호환성 검증 결과 (2026-03-17)
#### 호환 항목 (API 수정 불필요)
| 항목 | 상태 | 비고 |
|------|------|------|
| 응답 래핑 `{success, message, data}` | ✅ 호환 | ApiResponse 헬퍼 공용 |
| 페이지네이션 구조 | ✅ 호환 | `toPaginationMeta()` 재사용 가능 |
| 에러 응답 (422/404/500) | ✅ 호환 | 동일 에러 핸들링 구조 |
| Null 처리 | ✅ 호환 | 선택적 필드 패턴 일치 |
| 날짜 형식 | ✅ 호환 | `Y-m-d` 동일 |
| snake_case → camelCase | ✅ 호환 | 기존 `transformItemFromApi()` 재사용 |
#### React 측 작업 필요 항목
**1. bendingData 필드 매핑** (Server Action 레벨)
| API 응답 (`bendingData`) | React 타입 (`BendingDetail`) | 조치 |
|--------------------------|------------------------------|------|
| `rate` (string\|null) | `elongation` (number) | 이름+타입 변환 |
| `color` (boolean) | `shaded` (boolean) | 이름 변환 |
| `aAngle` (boolean) | `aAngle` (number) | 타입 변환 |
| `input`, `sum`, `no` | 동일 | ✅ 일치 |
변환 예시:
```typescript
function transformBendingData(apiData: ApiBendingData[]): BendingDetail[] {
return apiData.map((d, i) => ({
id: `detail-${i}`,
no: d.no,
input: d.input,
elongation: d.rate ? parseFloat(d.rate) : -1,
calculated: d.input + (d.rate ? parseFloat(d.rate) : 0),
sum: d.sum,
shaded: d.color,
aAngle: d.aAngle ? 1 : 0,
}));
}
```
**2. GuiderailModel 타입 + API 클라이언트 신규 작성**
- React에 `GuiderailModel` 관련 타입 없음
- `components[]`, `material_summary` 필드 구조 정의 필요
- Server Action: `fetchGuiderailModels()`, `fetchGuiderailModel(id)`
**3. 파일 URL 프록시 처리**
- API: `/api/v1/files/{id}/view`
- React: `/api/proxy/files/{id}/view` (Next.js 프록시 경로)
#### 인증 방식 차이 (자동 호환)
| 호출자 | X-API-KEY | Bearer | tenant_id 소스 | 비고 |
|--------|-----------|--------|---------------|------|
| **MNG** | ✅ | ❌ | X-TENANT-ID 헤더 (`ensureContext`) | 현재 |
| **React** | ✅ (프록시) | ✅ (쿠키) | Bearer → User → userTenants | 향후 |
React는 Next.js 프록시가 HttpOnly 쿠키에서 Bearer 토큰을 읽어 자동 첨부하므로,
API의 `allowWithoutAuth` 화이트리스트에 의존하지 않고 정상 인증 경로로 동작함.
`ensureContext()`는 Bearer 없을 때만 동작하는 fallback이라 충돌 없음.
### 7.7 MNG 부품 추가 시 리다이렉트 수정 (2026-03-17)
**문제**: `/bending/cases/{id}/edit`에서 부품 추가 시 `form.submit()` → 컨트롤러 `update()`가 show 페이지로 redirect, edit 모드 종료됨
**수정 내용**:
- `form.blade.php`: `submitAndStayEdit()` 함수 추가 — hidden input `_redirect=edit` 세팅 후 submit
- `BendingProductController::update()`: `_redirect=edit`이면 edit 페이지로 리다이렉트
- `movePart()` 순서 변경도 동일 패턴 적용 (`location.reload()``submitAndStayEdit()`)
---
### 관련 문서
- 통합 마스터 플랜: `docs/dev/dev_plans/integrated-master-plan.md`
- Phase 2 (절곡 분석/설계): `docs/dev/dev_plans/integrated-phase-2.md`
- Phase 3 (절곡 검사, 완료): `docs/dev/dev_plans/integrated-phase-3.md`
- 품목 정책: `docs/rules/item-policy.md`
- 품목 정책: `docs/rules/item-policy.md`
---
**최종 업데이트**: 2026-03-17

File diff suppressed because it is too large Load Diff

View File

@@ -1,150 +1,150 @@
# Claude Code /btw (Side Question) 기능 가이드
> **작성일**: 2026-03-14
> **상태**: 확정
> **도입 버전**: Claude Code v2.1.72 (2026-03-10)
---
## 1. 개요
### 1.1 목적
`/btw` (By The Way)는 Claude Code에서 **작업 중단 없이 빠른 질문**을 할 수 있는 사이드 질문 기능이다. 대화 이력에 추가되지 않으며, 현재 세션의 컨텍스트를 기반으로 즉답을 제공한다.
### 1.2 핵심 원칙
- 대화 이력을 오염시키지 않는 임시 질문
- Claude가 작업 중일 때도 사용 가능
- 도구(파일 읽기, 명령 실행 등)에 접근하지 않고 **이미 알고 있는 정보만** 활용
- 프롬프트 캐시를 재사용하여 토큰 비용 최소화
---
## 2. 사용법
### 2.1 기본 문법
```
/btw 질문 내용
```
### 2.2 사용 예시
```
/btw 아까 수정한 설정 파일 이름이 뭐였지?
/btw 우리 DB 커넥션 이름이 뭐야?
/btw 방금 만든 API 엔드포인트 경로가 뭐지?
/btw tenant_id 컬럼 타입이 뭐였지?
/btw 아까 논의한 마이그레이션 순서 알려줘
```
### 2.3 답변 닫기
답변은 오버레이 형태로 표시되며, 아래 키로 닫을 수 있다:
| 키 | 동작 |
|----|------|
| `Space` | 닫기 |
| `Enter` | 닫기 |
| `Escape` | 닫기 |
---
## 3. 특징
### 3.1 작업 중에도 사용 가능
Claude가 코드를 작성하거나 파일을 읽는 중에도 `/btw`를 실행할 수 있다. 메인 작업을 중단하지 않는다.
### 3.2 전체 컨텍스트 접근
사이드 질문은 현재 대화의 전체 컨텍스트를 볼 수 있다:
- Claude가 이미 읽은 코드
- 이전 대화에서 논의한 아키텍처 결정
- 세션 중 수행한 모든 작업 내역
### 3.3 도구 접근 불가
```
❌ 파일 읽기/쓰기
❌ 명령어 실행 (bash, git 등)
❌ 웹 검색
❌ 새로운 정보 탐색
```
오직 **현재 컨텍스트에 있는 정보만** 사용하여 답변한다.
### 3.4 단발성 응답
후속 대화(follow-up)가 불가능하다. 추가 질문이 필요하면 일반 프롬프트를 사용한다.
### 3.5 비용 효율성
- 부모 대화의 프롬프트 캐시를 재사용
- 대화 이력에 추가되지 않아 이후 토큰 소비 없음
- 동일 정보를 일반 프롬프트로 물어보는 것 대비 비용 절감
---
## 4. /btw vs 서브에이전트 비교
| 항목 | `/btw` | 서브에이전트 (Agent) |
|------|--------|---------------------|
| **컨텍스트** | 전체 대화 내용 접근 가능 | 빈 컨텍스트에서 시작 |
| **도구 접근** | 불가 | 전체 도구 사용 가능 |
| **용도** | 이미 아는 정보 조회 | 새로운 정보 탐색 |
| **작업 중단** | 없음 | 병렬 실행 가능 |
| **대화 이력** | 추가되지 않음 | 결과가 이력에 포함 |
| **비용** | 최소 (캐시 재사용) | 별도 토큰 소비 |
### 4.1 판단 기준
```
"Claude가 이미 알고 있는 정보인가?"
→ Yes → /btw 사용
→ No → 일반 프롬프트 또는 서브에이전트 사용
```
---
## 5. 사용 요건
| 항목 | 요건 |
|------|------|
| 환경 | Claude Code CLI (터미널) 전용 |
| 버전 | v2.1.72 이상 |
| 계정 | Pro, Max, Teams, Enterprise, Console |
---
## 6. 베스트 프랙티스
### 6.1 적합한 사용 사례
```
✅ 파일명, 경로, 변수명 등 참조 정보 확인
✅ 이전 논의에서 결정한 사항 재확인
✅ 현재 작업 컨텍스트에 대한 빠른 질문
✅ 코드 구조나 아키텍처 결정 사항 확인
```
### 6.2 부적합한 사용 사례
```
❌ 새 파일을 읽어야 하는 질문
❌ 명령어 실행이 필요한 작업
❌ 웹 검색이 필요한 조사
❌ 후속 대화가 필요한 복잡한 논의
```
---
## 관련 문서
- [Claude Code → 슬랙 붙여넣기 가이드](claude-code-to-slack.md)
- [개발 명령어 모음](../quickstart/dev-commands.md)
---
**최종 업데이트**: 2026-03-14
# Claude Code /btw (Side Question) 기능 가이드
> **작성일**: 2026-03-14
> **상태**: 확정
> **도입 버전**: Claude Code v2.1.72 (2026-03-10)
---
## 1. 개요
### 1.1 목적
`/btw` (By The Way)는 Claude Code에서 **작업 중단 없이 빠른 질문**을 할 수 있는 사이드 질문 기능이다. 대화 이력에 추가되지 않으며, 현재 세션의 컨텍스트를 기반으로 즉답을 제공한다.
### 1.2 핵심 원칙
- 대화 이력을 오염시키지 않는 임시 질문
- Claude가 작업 중일 때도 사용 가능
- 도구(파일 읽기, 명령 실행 등)에 접근하지 않고 **이미 알고 있는 정보만** 활용
- 프롬프트 캐시를 재사용하여 토큰 비용 최소화
---
## 2. 사용법
### 2.1 기본 문법
```
/btw 질문 내용
```
### 2.2 사용 예시
```
/btw 아까 수정한 설정 파일 이름이 뭐였지?
/btw 우리 DB 커넥션 이름이 뭐야?
/btw 방금 만든 API 엔드포인트 경로가 뭐지?
/btw tenant_id 컬럼 타입이 뭐였지?
/btw 아까 논의한 마이그레이션 순서 알려줘
```
### 2.3 답변 닫기
답변은 오버레이 형태로 표시되며, 아래 키로 닫을 수 있다:
| 키 | 동작 |
|----|------|
| `Space` | 닫기 |
| `Enter` | 닫기 |
| `Escape` | 닫기 |
---
## 3. 특징
### 3.1 작업 중에도 사용 가능
Claude가 코드를 작성하거나 파일을 읽는 중에도 `/btw`를 실행할 수 있다. 메인 작업을 중단하지 않는다.
### 3.2 전체 컨텍스트 접근
사이드 질문은 현재 대화의 전체 컨텍스트를 볼 수 있다:
- Claude가 이미 읽은 코드
- 이전 대화에서 논의한 아키텍처 결정
- 세션 중 수행한 모든 작업 내역
### 3.3 도구 접근 불가
```
❌ 파일 읽기/쓰기
❌ 명령어 실행 (bash, git 등)
❌ 웹 검색
❌ 새로운 정보 탐색
```
오직 **현재 컨텍스트에 있는 정보만** 사용하여 답변한다.
### 3.4 단발성 응답
후속 대화(follow-up)가 불가능하다. 추가 질문이 필요하면 일반 프롬프트를 사용한다.
### 3.5 비용 효율성
- 부모 대화의 프롬프트 캐시를 재사용
- 대화 이력에 추가되지 않아 이후 토큰 소비 없음
- 동일 정보를 일반 프롬프트로 물어보는 것 대비 비용 절감
---
## 4. /btw vs 서브에이전트 비교
| 항목 | `/btw` | 서브에이전트 (Agent) |
|------|--------|---------------------|
| **컨텍스트** | 전체 대화 내용 접근 가능 | 빈 컨텍스트에서 시작 |
| **도구 접근** | 불가 | 전체 도구 사용 가능 |
| **용도** | 이미 아는 정보 조회 | 새로운 정보 탐색 |
| **작업 중단** | 없음 | 병렬 실행 가능 |
| **대화 이력** | 추가되지 않음 | 결과가 이력에 포함 |
| **비용** | 최소 (캐시 재사용) | 별도 토큰 소비 |
### 4.1 판단 기준
```
"Claude가 이미 알고 있는 정보인가?"
→ Yes → /btw 사용
→ No → 일반 프롬프트 또는 서브에이전트 사용
```
---
## 5. 사용 요건
| 항목 | 요건 |
|------|------|
| 환경 | Claude Code CLI (터미널) 전용 |
| 버전 | v2.1.72 이상 |
| 계정 | Pro, Max, Teams, Enterprise, Console |
---
## 6. 베스트 프랙티스
### 6.1 적합한 사용 사례
```
✅ 파일명, 경로, 변수명 등 참조 정보 확인
✅ 이전 논의에서 결정한 사항 재확인
✅ 현재 작업 컨텍스트에 대한 빠른 질문
✅ 코드 구조나 아키텍처 결정 사항 확인
```
### 6.2 부적합한 사용 사례
```
❌ 새 파일을 읽어야 하는 질문
❌ 명령어 실행이 필요한 작업
❌ 웹 검색이 필요한 조사
❌ 후속 대화가 필요한 복잡한 논의
```
---
## 관련 문서
- [Claude Code → 슬랙 붙여넣기 가이드](claude-code-to-slack.md)
- [개발 명령어 모음](../quickstart/dev-commands.md)
---
**최종 업데이트**: 2026-03-14

View File

@@ -1,116 +1,116 @@
# Claude Code CLI 출력을 슬랙에 전달하는 방법
> **작성일**: 2026-03-11
> **대상**: 슬랙으로 협업하는 모든 팀원
---
## 1. 문제
Claude Code CLI에서 복사한 텍스트를 슬랙 채팅창에 붙여넣으면 줄바꿈이 깨지고, 마크다운 문법이 원본 그대로 노출된다.
---
## 2. 원인 분석
텍스트가 깨지는 원인은 3가지가 복합적으로 작용한다.
### 2.1 터미널 줄바꿈 혼재
터미널에는 2종류의 줄바꿈이 존재한다.
| 종류 | 설명 | 복사 시 동작 |
|------|------|-------------|
| Hard wrap | 원본 텍스트의 실제 `\n` | 보통 유지됨 |
| Soft wrap | 터미널 창 너비에 의한 시각적 줄바꿈 | 유실되거나 불필요하게 추가됨 |
Claude Code CLI는 터미널에서 실행되므로, 드래그 복사 시 실제 줄바꿈과 시각적 줄바꿈이 뒤섞여 클립보드에 들어간다. 그 결과 어떤 줄은 합쳐지고, 어떤 줄은 엉뚱한 곳에서 끊어진다.
### 2.2 Markdown vs Slack mrkdwn 문법 차이
Claude Code는 표준 Markdown을 출력하지만, 슬랙은 자체 마크업 문법(mrkdwn)을 사용한다.
| 요소 | Markdown (Claude Code) | Slack mrkdwn | 직접 붙여넣기 시 |
|------|----------------------|-------------|----------------|
| 볼드 | `**텍스트**` | `*텍스트*` | `**텍스트**` 그대로 표시 |
| 이탤릭 | `*텍스트*` | `_텍스트_` | 슬랙에서 볼드로 오인식 |
| 제목 | `## 제목` | 지원 안 함 | `## 제목` 그대로 표시 |
| 취소선 | `~~텍스트~~` | `~텍스트~` | `~~텍스트~~` 그대로 표시 |
| 링크 | `[텍스트](URL)` | `<URL\|텍스트>` | 원본 문법 그대로 표시 |
| 구분선 | `---` | 지원 안 함 | `---` 대시 3개로 표시 |
| 코드블록 | ` ```lang ``` ` | ` ``` ``` ` | 언어 지정자가 텍스트로 노출될 수 있음 |
### 2.3 슬랙 입력창의 공백 처리
슬랙의 메시지 입력창은 붙여넣기 시 다음 처리를 수행한다:
- 연속 빈 줄을 1개로 축소
- 앞뒤 공백 제거
- 일부 특수문자 이스케이프
이 3가지가 합쳐지면 **줄 합침, 문법 노출, 공백 손실**이 동시에 발생한다.
---
## 3. 해결 방법: 클코 → 슬랙 변환기
### 3.1 접속 경로
MNG 관리자 패널에 변환 도구가 있다.
| 환경 | URL |
|------|-----|
| 로컬 | `http://mng.sam.kr/rd/cc-to-slack` |
| 개발 서버 | `https://admin.codebridge-x.com/rd/cc-to-slack` |
| 운영 서버 | `https://mng.codebridge-x.com/rd/cc-to-slack` |
**메뉴 위치**: 연구개발 > 클코 to 슬랙형태
### 3.2 사용법
1. Claude Code CLI에서 메시지를 드래그하여 복사 (`Ctrl+C`)
2. 변환기의 왼쪽 입력란에 붙여넣기 (`Ctrl+V`)
3. 오른쪽 슬랙 미리보기에 변환 결과가 자동 표시됨
4. **복사** 버튼 클릭 (또는 `Ctrl+Enter`)
5. 슬랙 채팅창에 붙여넣기 (`Ctrl+V`)
### 3.3 변환 규칙
| 변환 전 (Markdown) | 변환 후 (Slack) | 설명 |
|-------------------|----------------|------|
| `**볼드**` | `*볼드*` | 슬랙 볼드 문법 |
| `## 제목` | `*제목*` | 볼드 처리로 대체 |
| `~~취소~~` | `~취소~` | 슬랙 취소선 문법 |
| `[텍스트](URL)` | `<URL\|텍스트>` | 슬랙 링크 문법 |
| `---` | `———` | em dash로 구분선 대체 |
| 테이블 구분선 `\|---\|` | 제거 | 불필요한 구분선 삭제 |
| 연속 빈 줄 | 1개로 정리 | 공백 정리 |
| 코드블록 | 유지 | 슬랙도 ` ``` ` 지원 |
---
## 4. 작동 원리
변환기가 정상 작동하는 핵심 이유는 **리치 텍스트(HTML) 복사** 방식을 사용하기 때문이다.
```
일반 텍스트 복사 (plain text)
→ 슬랙이 자체적으로 재해석 → 줄바꿈 깨짐, 문법 노출
리치 텍스트 복사 (HTML)
→ 슬랙이 HTML 서식을 그대로 수용 → 볼드, 코드블록, 줄바꿈 보존
```
브라우저의 Selection API로 DOM 요소를 선택하면 클립보드에 `text/html``text/plain` 두 가지 형식이 동시에 저장된다. 슬랙은 `text/html` 버전을 읽어서 `<b>`, `<code>`, `<br>` 등의 서식을 그대로 적용한다.
---
## 관련 문서
- MNG 소스: `mng/resources/views/rd/cc-to-slack/index.blade.php`
- 컨트롤러: `mng/app/Http/Controllers/RdController.php``ccToSlack()`
- 라우트: `GET /rd/cc-to-slack`
---
**최종 업데이트**: 2026-03-11
# Claude Code CLI 출력을 슬랙에 전달하는 방법
> **작성일**: 2026-03-11
> **대상**: 슬랙으로 협업하는 모든 팀원
---
## 1. 문제
Claude Code CLI에서 복사한 텍스트를 슬랙 채팅창에 붙여넣으면 줄바꿈이 깨지고, 마크다운 문법이 원본 그대로 노출된다.
---
## 2. 원인 분석
텍스트가 깨지는 원인은 3가지가 복합적으로 작용한다.
### 2.1 터미널 줄바꿈 혼재
터미널에는 2종류의 줄바꿈이 존재한다.
| 종류 | 설명 | 복사 시 동작 |
|------|------|-------------|
| Hard wrap | 원본 텍스트의 실제 `\n` | 보통 유지됨 |
| Soft wrap | 터미널 창 너비에 의한 시각적 줄바꿈 | 유실되거나 불필요하게 추가됨 |
Claude Code CLI는 터미널에서 실행되므로, 드래그 복사 시 실제 줄바꿈과 시각적 줄바꿈이 뒤섞여 클립보드에 들어간다. 그 결과 어떤 줄은 합쳐지고, 어떤 줄은 엉뚱한 곳에서 끊어진다.
### 2.2 Markdown vs Slack mrkdwn 문법 차이
Claude Code는 표준 Markdown을 출력하지만, 슬랙은 자체 마크업 문법(mrkdwn)을 사용한다.
| 요소 | Markdown (Claude Code) | Slack mrkdwn | 직접 붙여넣기 시 |
|------|----------------------|-------------|----------------|
| 볼드 | `**텍스트**` | `*텍스트*` | `**텍스트**` 그대로 표시 |
| 이탤릭 | `*텍스트*` | `_텍스트_` | 슬랙에서 볼드로 오인식 |
| 제목 | `## 제목` | 지원 안 함 | `## 제목` 그대로 표시 |
| 취소선 | `~~텍스트~~` | `~텍스트~` | `~~텍스트~~` 그대로 표시 |
| 링크 | `[텍스트](URL)` | `<URL\|텍스트>` | 원본 문법 그대로 표시 |
| 구분선 | `---` | 지원 안 함 | `---` 대시 3개로 표시 |
| 코드블록 | ` ```lang ``` ` | ` ``` ``` ` | 언어 지정자가 텍스트로 노출될 수 있음 |
### 2.3 슬랙 입력창의 공백 처리
슬랙의 메시지 입력창은 붙여넣기 시 다음 처리를 수행한다:
- 연속 빈 줄을 1개로 축소
- 앞뒤 공백 제거
- 일부 특수문자 이스케이프
이 3가지가 합쳐지면 **줄 합침, 문법 노출, 공백 손실**이 동시에 발생한다.
---
## 3. 해결 방법: 클코 → 슬랙 변환기
### 3.1 접속 경로
MNG 관리자 패널에 변환 도구가 있다.
| 환경 | URL |
|------|-----|
| 로컬 | `http://mng.sam.kr/rd/cc-to-slack` |
| 개발 서버 | `https://admin.codebridge-x.com/rd/cc-to-slack` |
| 운영 서버 | `https://mng.codebridge-x.com/rd/cc-to-slack` |
**메뉴 위치**: 연구개발 > 클코 to 슬랙형태
### 3.2 사용법
1. Claude Code CLI에서 메시지를 드래그하여 복사 (`Ctrl+C`)
2. 변환기의 왼쪽 입력란에 붙여넣기 (`Ctrl+V`)
3. 오른쪽 슬랙 미리보기에 변환 결과가 자동 표시됨
4. **복사** 버튼 클릭 (또는 `Ctrl+Enter`)
5. 슬랙 채팅창에 붙여넣기 (`Ctrl+V`)
### 3.3 변환 규칙
| 변환 전 (Markdown) | 변환 후 (Slack) | 설명 |
|-------------------|----------------|------|
| `**볼드**` | `*볼드*` | 슬랙 볼드 문법 |
| `## 제목` | `*제목*` | 볼드 처리로 대체 |
| `~~취소~~` | `~취소~` | 슬랙 취소선 문법 |
| `[텍스트](URL)` | `<URL\|텍스트>` | 슬랙 링크 문법 |
| `---` | `———` | em dash로 구분선 대체 |
| 테이블 구분선 `\|---\|` | 제거 | 불필요한 구분선 삭제 |
| 연속 빈 줄 | 1개로 정리 | 공백 정리 |
| 코드블록 | 유지 | 슬랙도 ` ``` ` 지원 |
---
## 4. 작동 원리
변환기가 정상 작동하는 핵심 이유는 **리치 텍스트(HTML) 복사** 방식을 사용하기 때문이다.
```
일반 텍스트 복사 (plain text)
→ 슬랙이 자체적으로 재해석 → 줄바꿈 깨짐, 문법 노출
리치 텍스트 복사 (HTML)
→ 슬랙이 HTML 서식을 그대로 수용 → 볼드, 코드블록, 줄바꿈 보존
```
브라우저의 Selection API로 DOM 요소를 선택하면 클립보드에 `text/html``text/plain` 두 가지 형식이 동시에 저장된다. 슬랙은 `text/html` 버전을 읽어서 `<b>`, `<code>`, `<br>` 등의 서식을 그대로 적용한다.
---
## 관련 문서
- MNG 소스: `mng/resources/views/rd/cc-to-slack/index.blade.php`
- 컨트롤러: `mng/app/Http/Controllers/RdController.php``ccToSlack()`
- 라우트: `GET /rd/cc-to-slack`
---
**최종 업데이트**: 2026-03-11

File diff suppressed because it is too large Load Diff

View File

@@ -1,412 +1,412 @@
# 이메일 발송 정책 (멀티테넌시)
> **작성일**: 2026-03-11
> **상태**: 설계 확정
---
## 1. 개요
### 1.1 목적
SAM 멀티테넌시 환경에서 이메일 발송의 테넌트 격리, 브랜딩, 발송 추적, 쿼터 관리를 위한 표준 정책을 정의한다.
### 1.2 핵심 원칙
| 원칙 | 설명 |
|------|------|
| 🔴 테넌트 격리 | 테넌트 A의 메일 설정/발송 기록이 테넌트 B에 노출되지 않는다 |
| 🔴 중앙 서비스 경유 | 모든 메일 발송은 `TenantMailService`를 경유한다 |
| 🟡 테넌트 브랜딩 | 발신자명, 로고, 서명을 테넌트별로 커스터마이징한다 |
| 🟡 발송 기록 | 모든 발송은 `mail_logs` 테이블에 기록한다 |
| 🟢 쿼터 관리 | 테넌트별 일일 발송 한도를 관리한다 |
### 1.3 현재 상태 (AS-IS)
```
문제점:
├── 단일 SMTP (.env 고정) → Gmail 일일 500건 제한에 전체 영향
├── 단일 발신 주소 (develop@codebridge-x.com) → 테넌트 구분 불가
├── EsignRequestMail 중복 (API + MNG 양쪽에 존재)
├── 발송 기록 없음 → 추적/감사 불가
├── Mail::to() 직접 호출 → 테넌트 설정 적용 불가
└── 템플릿 하드코딩 → 테넌트별 브랜딩 불가
```
---
## 2. 아키텍처
### 2.1 3-Layer 구조
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 테넌트 메일 설정 (tenant_mail_configs) │
│ SMTP 설정, 발신자 주소, 브랜딩 정보 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 메일 발송 서비스 (TenantMailService) │
│ 테넌트 설정 자동 적용, 큐 발송, Fallback │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 발송 기록 (mail_logs) │
│ 발송 이력, 상태 추적, 일일 쿼터 관리 │
└─────────────────────────────────────────────────────────┘
```
### 2.2 발송 흐름
```
Controller / Service
TenantMailService::send($mailable, $to, $tenantId?)
├── 1. 테넌트 설정 조회 (tenant_mail_configs)
│ └── 미설정 시 플랫폼 기본 SMTP 사용
├── 2. 쿼터 확인 (일일 발송 한도)
│ └── 초과 시 예외 발생 + 관리자 알림
├── 3. Mailer 동적 구성
│ ├── SMTP host/port/user/pass 설정
│ ├── from address/name 설정
│ └── 브랜딩 데이터 주입
├── 4. 발송 모드 결정
│ ├── sync: OTP, 비밀번호 (시간 민감)
│ └── queue: 나머지 전부
├── 5. mail_logs 기록 (status: queued/sent)
└── 6. Fallback (자체 SMTP 실패 시)
└── 플랫폼 기본 SMTP로 재시도
```
---
## 3. 테이블 설계
### 3.1 `tenant_mail_configs` (테넌트 메일 설정)
> **마이그레이션 위치**: `/home/aweso/sam/api/database/migrations/`
```sql
CREATE TABLE tenant_mail_configs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
provider ENUM('platform', 'smtp', 'ses', 'mailgun') DEFAULT 'platform',
from_address VARCHAR(255) NOT NULL COMMENT '발신 이메일',
from_name VARCHAR(255) NOT NULL COMMENT '발신자명',
reply_to VARCHAR(255) NULL COMMENT '회신 주소',
is_verified BOOLEAN DEFAULT FALSE COMMENT '도메인 검증 여부',
daily_limit INT UNSIGNED DEFAULT 500 COMMENT '일일 발송 한도',
is_active BOOLEAN DEFAULT TRUE,
options JSON NULL COMMENT 'SMTP 설정, 브랜딩 정보',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
UNIQUE KEY uq_tenant_mail_configs (tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) COMMENT '테넌트 메일 설정';
```
**`options` JSON 구조**:
```json
{
"smtp": {
"host": "smtp.example.com",
"port": 587,
"username": "user@example.com",
"password": "<encrypted>",
"encryption": "tls"
},
"branding": {
"logo_url": "/storage/tenants/1/logo.png",
"primary_color": "#1a56db",
"company_name": "테넌트 회사명",
"company_address": "서울시 강남구...",
"company_phone": "02-1234-5678",
"footer_text": "본 메일은 SAM 시스템에서 발송되었습니다."
},
"ses": {
"key": "<encrypted>",
"secret": "<encrypted>",
"region": "ap-northeast-2"
}
}
```
> **보안**: `options.smtp.password`, `options.ses.key`, `options.ses.secret`은 `encrypt()` / `decrypt()`로 저장/조회한다.
### 3.2 `mail_logs` (발송 기록)
```sql
CREATE TABLE mail_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
mailable_type VARCHAR(100) NOT NULL COMMENT 'Mailable 클래스명',
to_address VARCHAR(255) NOT NULL COMMENT '수신자',
from_address VARCHAR(255) NOT NULL COMMENT '발신자',
subject VARCHAR(500) NOT NULL COMMENT '제목',
status ENUM('queued', 'sent', 'failed', 'bounced') DEFAULT 'queued',
sent_at TIMESTAMP NULL COMMENT '발송 시각',
options JSON NULL COMMENT '에러 메시지, 재시도 횟수, 관련 모델',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
INDEX idx_mail_logs_tenant_status (tenant_id, status),
INDEX idx_mail_logs_tenant_date (tenant_id, created_at),
INDEX idx_mail_logs_mailable (tenant_id, mailable_type),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) COMMENT '메일 발송 기록';
```
**`options` JSON 구조**:
```json
{
"error_message": "Connection refused",
"retry_count": 2,
"related_model": "App\\Models\\ESign\\EsignContract",
"related_id": 123,
"provider_used": "smtp",
"fallback_used": true
}
```
> **개인정보 보호**: `mail_logs`에 메일 본문(body)은 저장하지 않는다. 메타데이터만 기록한다.
---
## 4. 서비스 설계
### 4.1 `TenantMailService`
> **위치**: `/home/aweso/sam/api/app/Services/Mail/TenantMailService.php`
> MNG에서도 동일 패턴의 서비스를 생성한다.
```php
class TenantMailService
{
/**
* 테넌트 설정을 적용하여 메일 발송
*
* @param Mailable $mailable 발송할 Mailable 인스턴스
* @param string|array $to 수신자
* @param int|null $tenantId 테넌트 ID (null이면 현재 테넌트)
* @param bool $sync 즉시 발송 여부 (기본: false = queue)
*/
public function send(
Mailable $mailable,
string|array $to,
?int $tenantId = null,
bool $sync = false
): MailLog;
}
```
### 4.2 발송 모드 기준
| 모드 | Mailable | 사유 |
|------|----------|------|
| **sync** (즉시) | `EsignOtpMail` | OTP 시간 제한 |
| **sync** (즉시) | `UserPasswordMail` | 즉시 로그인 필요 |
| **queue** (큐) | `EsignRequestMail` | 비동기 가능 |
| **queue** (큐) | `EsignCompletedMail` | 비동기 가능 |
| **queue** (큐) | `PayslipMail` | 대량 발송 가능 |
### 4.3 Fallback 전략
```
테넌트 자체 SMTP 발송 시도
├── 성공 → mail_logs (status: sent)
└── 실패 → 플랫폼 기본 SMTP로 재시도
├── 성공 → mail_logs (status: sent, fallback_used: true)
└── 실패 → mail_logs (status: failed)
└── 3회까지 자동 재시도 (queue retry)
```
---
## 5. 메일 타입 정리
### 5.1 현재 Mailable 목록
| Mailable | 위치 | 트리거 | 비고 |
|----------|------|--------|------|
| `EsignRequestMail` | API + MNG (중복) | 서명 요청 | 🔴 중복 제거 필요 |
| `EsignOtpMail` | MNG | OTP 인증 | |
| `EsignCompletedMail` | MNG | 서명 완료 | |
| `UserPasswordMail` | MNG | 계정 생성/비번 초기화 | |
| `PayslipMail` | MNG | 급여명세서 | |
### 5.2 Mailable 위치 정리 방향
```
❌ 현재: API와 MNG에 중복 존재
✅ 목표: 비즈니스 로직은 API, MNG는 관리자 트리거만
API (app/Mail/)
├── EsignRequestMail.php ← API에서 통합 관리
├── EsignOtpMail.php
├── EsignCompletedMail.php
├── UserPasswordMail.php
└── PayslipMail.php
MNG → API의 Mailable을 HTTP API로 호출
또는 공유 패키지로 분리
```
> **현실적 방향**: 현재 MNG에서 직접 DB 접근하므로, MNG의 Mailable을 유지하되 `TenantMailService` 경유로 통일한다. API의 중복 `EsignRequestMail`은 제거한다.
---
## 6. 템플릿 브랜딩
### 6.1 공통 레이아웃
```
┌─ emails.layouts.tenant ──────────────────────────┐
│ │
│ ┌─ 헤더 ──────────────────────────────────────┐ │
│ │ [테넌트 로고] 테넌트명 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ 본문 ──────────────────────────────────────┐ │
│ │ @yield('content') │ │
│ │ (각 Mailable에서 제공) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ 푸터 ──────────────────────────────────────┐ │
│ │ {{ 회사명 }} | {{ 주소 }} | {{ 연락처 }} │ │
│ │ "SAM 시스템에서 발송된 메일입니다" │ │
│ └─────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────┘
```
### 6.2 브랜딩 요소
| 요소 | 저장 위치 | 기본값 |
|------|----------|--------|
| 로고 이미지 | `options.branding.logo_url` | SAM BI 로고 |
| 회사명 | `options.branding.company_name` | (주)코드브릿지엑스 |
| 주소 | `options.branding.company_address` | — |
| 연락처 | `options.branding.company_phone` | — |
| 테마 컬러 | `options.branding.primary_color` | `#1a56db` |
| 푸터 문구 | `options.branding.footer_text` | "SAM 시스템에서 발송된 메일입니다" |
---
## 7. 쿼터 관리
### 7.1 쿼터 정책
| 항목 | 기본값 | 설명 |
|------|--------|------|
| 일일 발송 한도 | 500건 | `tenant_mail_configs.daily_limit` |
| 경고 임계치 | 80% (400건) | 도달 시 관리자 알림 |
| 초과 시 동작 | 발송 차단 + 예외 | `MailQuotaExceededException` |
### 7.2 쿼터 확인
```php
// mail_logs에서 오늘 발송 건수 조회
$todayCount = MailLog::where('tenant_id', $tenantId)
->whereDate('created_at', today())
->whereIn('status', ['queued', 'sent'])
->count();
if ($todayCount >= $config->daily_limit) {
throw new MailQuotaExceededException($tenantId);
}
```
---
## 8. 보안 규칙
### 8.1 필수 준수 사항
```
✅ SMTP 비밀번호는 encrypt()로 암호화 저장
✅ mail_logs에 메일 본문 저장 금지 (메타데이터만)
✅ 급여명세서 등 민감 메일은 related_model/related_id만 기록
✅ tenant_mail_configs 조회 시 TenantScope 자동 적용
✅ API 응답에 SMTP 비밀번호 노출 금지 (hidden 처리)
```
### 8.2 금지 사항
```
❌ Mail::to() 직접 호출 금지 → TenantMailService 사용
❌ .env SMTP 설정에 운영 크리덴셜 하드코딩 금지
❌ 타 테넌트 mail_logs 조회 금지 (TenantScope로 방지)
❌ 이메일 본문에 비밀번호 평문 포함 금지 (임시 비밀번호 제외)
```
---
## 9. 구현 단계
| Phase | 범위 | 주요 작업 | 우선순위 |
|-------|------|----------|---------|
| **Phase 1** | 기반 구축 | `tenant_mail_configs` + `mail_logs` 마이그레이션, `TenantMailService` 생성, 모델 생성 | 🔴 필수 |
| **Phase 2** | 기존 전환 | 현재 5개 Mailable을 `TenantMailService` 경유로 변경, API `EsignRequestMail` 중복 제거 | 🔴 필수 |
| **Phase 3** | 브랜딩 | 공통 레이아웃 생성, 테넌트별 로고/컬러/서명 적용, MNG 관리 화면 | 🟡 중요 |
| **Phase 4** | 고급 기능 | 실패 재시도, 바운스 처리, 발송 통계 대시보드, SES/Mailgun 연동 | 🟢 권장 |
---
## 10. SMTP 제공자 비교
| 제공자 | 일일 한도 | 비용 | 적합 시점 |
|--------|---------|------|----------|
| Gmail SMTP | 500건 | 무료 | 현재 (소규모) |
| Amazon SES | 무제한 | $0.10/1,000건 | 테넌트 10개+ |
| Mailgun | 5,000건/월 무료 | $0.80/1,000건 | 중규모 |
| 자체 SMTP | 무제한 | 서버 비용 | 테넌트 자체 운영 |
> **권장**: Phase 1~2는 Gmail SMTP 유지, Phase 4에서 Amazon SES 전환 검토
---
## 11. 기존 코드 전환 가이드
### 11.1 Before (현재)
```php
// MNG 컨트롤러에서 직접 발송
Mail::to($signer->email)->send(new EsignRequestMail($contract, $signer));
```
### 11.2 After (전환 후)
```php
// TenantMailService 경유
app(TenantMailService::class)->send(
mailable: new EsignRequestMail($contract, $signer),
to: $signer->email,
// tenantId는 자동 감지 (현재 세션 기반)
);
```
---
## 관련 문서
- [테넌트 이메일 연동 가이드](../guides/tenant-email-integration-guide.md) — MNG에서 테넌트 메일 설정, SMTP 프리셋, 연결 테스트
- [테넌트 DB 구조](../../system/database/tenants.md)
- [전자서명 기능](../../features/esign/README.md)
- [급여관리 기능](../../features/finance/payroll.md)
- [API 개발 규칙](api-rules.md)
- [options JSON 컬럼 정책](options-column-policy.md)
---
**최종 업데이트**: 2026-03-11
# 이메일 발송 정책 (멀티테넌시)
> **작성일**: 2026-03-11
> **상태**: 설계 확정
---
## 1. 개요
### 1.1 목적
SAM 멀티테넌시 환경에서 이메일 발송의 테넌트 격리, 브랜딩, 발송 추적, 쿼터 관리를 위한 표준 정책을 정의한다.
### 1.2 핵심 원칙
| 원칙 | 설명 |
|------|------|
| 🔴 테넌트 격리 | 테넌트 A의 메일 설정/발송 기록이 테넌트 B에 노출되지 않는다 |
| 🔴 중앙 서비스 경유 | 모든 메일 발송은 `TenantMailService`를 경유한다 |
| 🟡 테넌트 브랜딩 | 발신자명, 로고, 서명을 테넌트별로 커스터마이징한다 |
| 🟡 발송 기록 | 모든 발송은 `mail_logs` 테이블에 기록한다 |
| 🟢 쿼터 관리 | 테넌트별 일일 발송 한도를 관리한다 |
### 1.3 현재 상태 (AS-IS)
```
문제점:
├── 단일 SMTP (.env 고정) → Gmail 일일 500건 제한에 전체 영향
├── 단일 발신 주소 (develop@codebridge-x.com) → 테넌트 구분 불가
├── EsignRequestMail 중복 (API + MNG 양쪽에 존재)
├── 발송 기록 없음 → 추적/감사 불가
├── Mail::to() 직접 호출 → 테넌트 설정 적용 불가
└── 템플릿 하드코딩 → 테넌트별 브랜딩 불가
```
---
## 2. 아키텍처
### 2.1 3-Layer 구조
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 테넌트 메일 설정 (tenant_mail_configs) │
│ SMTP 설정, 발신자 주소, 브랜딩 정보 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 메일 발송 서비스 (TenantMailService) │
│ 테넌트 설정 자동 적용, 큐 발송, Fallback │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 발송 기록 (mail_logs) │
│ 발송 이력, 상태 추적, 일일 쿼터 관리 │
└─────────────────────────────────────────────────────────┘
```
### 2.2 발송 흐름
```
Controller / Service
TenantMailService::send($mailable, $to, $tenantId?)
├── 1. 테넌트 설정 조회 (tenant_mail_configs)
│ └── 미설정 시 플랫폼 기본 SMTP 사용
├── 2. 쿼터 확인 (일일 발송 한도)
│ └── 초과 시 예외 발생 + 관리자 알림
├── 3. Mailer 동적 구성
│ ├── SMTP host/port/user/pass 설정
│ ├── from address/name 설정
│ └── 브랜딩 데이터 주입
├── 4. 발송 모드 결정
│ ├── sync: OTP, 비밀번호 (시간 민감)
│ └── queue: 나머지 전부
├── 5. mail_logs 기록 (status: queued/sent)
└── 6. Fallback (자체 SMTP 실패 시)
└── 플랫폼 기본 SMTP로 재시도
```
---
## 3. 테이블 설계
### 3.1 `tenant_mail_configs` (테넌트 메일 설정)
> **마이그레이션 위치**: `/home/aweso/sam/api/database/migrations/`
```sql
CREATE TABLE tenant_mail_configs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
provider ENUM('platform', 'smtp', 'ses', 'mailgun') DEFAULT 'platform',
from_address VARCHAR(255) NOT NULL COMMENT '발신 이메일',
from_name VARCHAR(255) NOT NULL COMMENT '발신자명',
reply_to VARCHAR(255) NULL COMMENT '회신 주소',
is_verified BOOLEAN DEFAULT FALSE COMMENT '도메인 검증 여부',
daily_limit INT UNSIGNED DEFAULT 500 COMMENT '일일 발송 한도',
is_active BOOLEAN DEFAULT TRUE,
options JSON NULL COMMENT 'SMTP 설정, 브랜딩 정보',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
deleted_at TIMESTAMP NULL,
UNIQUE KEY uq_tenant_mail_configs (tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) COMMENT '테넌트 메일 설정';
```
**`options` JSON 구조**:
```json
{
"smtp": {
"host": "smtp.example.com",
"port": 587,
"username": "user@example.com",
"password": "<encrypted>",
"encryption": "tls"
},
"branding": {
"logo_url": "/storage/tenants/1/logo.png",
"primary_color": "#1a56db",
"company_name": "테넌트 회사명",
"company_address": "서울시 강남구...",
"company_phone": "02-1234-5678",
"footer_text": "본 메일은 SAM 시스템에서 발송되었습니다."
},
"ses": {
"key": "<encrypted>",
"secret": "<encrypted>",
"region": "ap-northeast-2"
}
}
```
> **보안**: `options.smtp.password`, `options.ses.key`, `options.ses.secret`은 `encrypt()` / `decrypt()`로 저장/조회한다.
### 3.2 `mail_logs` (발송 기록)
```sql
CREATE TABLE mail_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
mailable_type VARCHAR(100) NOT NULL COMMENT 'Mailable 클래스명',
to_address VARCHAR(255) NOT NULL COMMENT '수신자',
from_address VARCHAR(255) NOT NULL COMMENT '발신자',
subject VARCHAR(500) NOT NULL COMMENT '제목',
status ENUM('queued', 'sent', 'failed', 'bounced') DEFAULT 'queued',
sent_at TIMESTAMP NULL COMMENT '발송 시각',
options JSON NULL COMMENT '에러 메시지, 재시도 횟수, 관련 모델',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
INDEX idx_mail_logs_tenant_status (tenant_id, status),
INDEX idx_mail_logs_tenant_date (tenant_id, created_at),
INDEX idx_mail_logs_mailable (tenant_id, mailable_type),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) COMMENT '메일 발송 기록';
```
**`options` JSON 구조**:
```json
{
"error_message": "Connection refused",
"retry_count": 2,
"related_model": "App\\Models\\ESign\\EsignContract",
"related_id": 123,
"provider_used": "smtp",
"fallback_used": true
}
```
> **개인정보 보호**: `mail_logs`에 메일 본문(body)은 저장하지 않는다. 메타데이터만 기록한다.
---
## 4. 서비스 설계
### 4.1 `TenantMailService`
> **위치**: `/home/aweso/sam/api/app/Services/Mail/TenantMailService.php`
> MNG에서도 동일 패턴의 서비스를 생성한다.
```php
class TenantMailService
{
/**
* 테넌트 설정을 적용하여 메일 발송
*
* @param Mailable $mailable 발송할 Mailable 인스턴스
* @param string|array $to 수신자
* @param int|null $tenantId 테넌트 ID (null이면 현재 테넌트)
* @param bool $sync 즉시 발송 여부 (기본: false = queue)
*/
public function send(
Mailable $mailable,
string|array $to,
?int $tenantId = null,
bool $sync = false
): MailLog;
}
```
### 4.2 발송 모드 기준
| 모드 | Mailable | 사유 |
|------|----------|------|
| **sync** (즉시) | `EsignOtpMail` | OTP 시간 제한 |
| **sync** (즉시) | `UserPasswordMail` | 즉시 로그인 필요 |
| **queue** (큐) | `EsignRequestMail` | 비동기 가능 |
| **queue** (큐) | `EsignCompletedMail` | 비동기 가능 |
| **queue** (큐) | `PayslipMail` | 대량 발송 가능 |
### 4.3 Fallback 전략
```
테넌트 자체 SMTP 발송 시도
├── 성공 → mail_logs (status: sent)
└── 실패 → 플랫폼 기본 SMTP로 재시도
├── 성공 → mail_logs (status: sent, fallback_used: true)
└── 실패 → mail_logs (status: failed)
└── 3회까지 자동 재시도 (queue retry)
```
---
## 5. 메일 타입 정리
### 5.1 현재 Mailable 목록
| Mailable | 위치 | 트리거 | 비고 |
|----------|------|--------|------|
| `EsignRequestMail` | API + MNG (중복) | 서명 요청 | 🔴 중복 제거 필요 |
| `EsignOtpMail` | MNG | OTP 인증 | |
| `EsignCompletedMail` | MNG | 서명 완료 | |
| `UserPasswordMail` | MNG | 계정 생성/비번 초기화 | |
| `PayslipMail` | MNG | 급여명세서 | |
### 5.2 Mailable 위치 정리 방향
```
❌ 현재: API와 MNG에 중복 존재
✅ 목표: 비즈니스 로직은 API, MNG는 관리자 트리거만
API (app/Mail/)
├── EsignRequestMail.php ← API에서 통합 관리
├── EsignOtpMail.php
├── EsignCompletedMail.php
├── UserPasswordMail.php
└── PayslipMail.php
MNG → API의 Mailable을 HTTP API로 호출
또는 공유 패키지로 분리
```
> **현실적 방향**: 현재 MNG에서 직접 DB 접근하므로, MNG의 Mailable을 유지하되 `TenantMailService` 경유로 통일한다. API의 중복 `EsignRequestMail`은 제거한다.
---
## 6. 템플릿 브랜딩
### 6.1 공통 레이아웃
```
┌─ emails.layouts.tenant ──────────────────────────┐
│ │
│ ┌─ 헤더 ──────────────────────────────────────┐ │
│ │ [테넌트 로고] 테넌트명 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ 본문 ──────────────────────────────────────┐ │
│ │ @yield('content') │ │
│ │ (각 Mailable에서 제공) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ 푸터 ──────────────────────────────────────┐ │
│ │ {{ 회사명 }} | {{ 주소 }} | {{ 연락처 }} │ │
│ │ "SAM 시스템에서 발송된 메일입니다" │ │
│ └─────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────┘
```
### 6.2 브랜딩 요소
| 요소 | 저장 위치 | 기본값 |
|------|----------|--------|
| 로고 이미지 | `options.branding.logo_url` | SAM BI 로고 |
| 회사명 | `options.branding.company_name` | (주)코드브릿지엑스 |
| 주소 | `options.branding.company_address` | — |
| 연락처 | `options.branding.company_phone` | — |
| 테마 컬러 | `options.branding.primary_color` | `#1a56db` |
| 푸터 문구 | `options.branding.footer_text` | "SAM 시스템에서 발송된 메일입니다" |
---
## 7. 쿼터 관리
### 7.1 쿼터 정책
| 항목 | 기본값 | 설명 |
|------|--------|------|
| 일일 발송 한도 | 500건 | `tenant_mail_configs.daily_limit` |
| 경고 임계치 | 80% (400건) | 도달 시 관리자 알림 |
| 초과 시 동작 | 발송 차단 + 예외 | `MailQuotaExceededException` |
### 7.2 쿼터 확인
```php
// mail_logs에서 오늘 발송 건수 조회
$todayCount = MailLog::where('tenant_id', $tenantId)
->whereDate('created_at', today())
->whereIn('status', ['queued', 'sent'])
->count();
if ($todayCount >= $config->daily_limit) {
throw new MailQuotaExceededException($tenantId);
}
```
---
## 8. 보안 규칙
### 8.1 필수 준수 사항
```
✅ SMTP 비밀번호는 encrypt()로 암호화 저장
✅ mail_logs에 메일 본문 저장 금지 (메타데이터만)
✅ 급여명세서 등 민감 메일은 related_model/related_id만 기록
✅ tenant_mail_configs 조회 시 TenantScope 자동 적용
✅ API 응답에 SMTP 비밀번호 노출 금지 (hidden 처리)
```
### 8.2 금지 사항
```
❌ Mail::to() 직접 호출 금지 → TenantMailService 사용
❌ .env SMTP 설정에 운영 크리덴셜 하드코딩 금지
❌ 타 테넌트 mail_logs 조회 금지 (TenantScope로 방지)
❌ 이메일 본문에 비밀번호 평문 포함 금지 (임시 비밀번호 제외)
```
---
## 9. 구현 단계
| Phase | 범위 | 주요 작업 | 우선순위 |
|-------|------|----------|---------|
| **Phase 1** | 기반 구축 | `tenant_mail_configs` + `mail_logs` 마이그레이션, `TenantMailService` 생성, 모델 생성 | 🔴 필수 |
| **Phase 2** | 기존 전환 | 현재 5개 Mailable을 `TenantMailService` 경유로 변경, API `EsignRequestMail` 중복 제거 | 🔴 필수 |
| **Phase 3** | 브랜딩 | 공통 레이아웃 생성, 테넌트별 로고/컬러/서명 적용, MNG 관리 화면 | 🟡 중요 |
| **Phase 4** | 고급 기능 | 실패 재시도, 바운스 처리, 발송 통계 대시보드, SES/Mailgun 연동 | 🟢 권장 |
---
## 10. SMTP 제공자 비교
| 제공자 | 일일 한도 | 비용 | 적합 시점 |
|--------|---------|------|----------|
| Gmail SMTP | 500건 | 무료 | 현재 (소규모) |
| Amazon SES | 무제한 | $0.10/1,000건 | 테넌트 10개+ |
| Mailgun | 5,000건/월 무료 | $0.80/1,000건 | 중규모 |
| 자체 SMTP | 무제한 | 서버 비용 | 테넌트 자체 운영 |
> **권장**: Phase 1~2는 Gmail SMTP 유지, Phase 4에서 Amazon SES 전환 검토
---
## 11. 기존 코드 전환 가이드
### 11.1 Before (현재)
```php
// MNG 컨트롤러에서 직접 발송
Mail::to($signer->email)->send(new EsignRequestMail($contract, $signer));
```
### 11.2 After (전환 후)
```php
// TenantMailService 경유
app(TenantMailService::class)->send(
mailable: new EsignRequestMail($contract, $signer),
to: $signer->email,
// tenantId는 자동 감지 (현재 세션 기반)
);
```
---
## 관련 문서
- [테넌트 이메일 연동 가이드](../guides/tenant-email-integration-guide.md) — MNG에서 테넌트 메일 설정, SMTP 프리셋, 연결 테스트
- [테넌트 DB 구조](../../system/database/tenants.md)
- [전자서명 기능](../../features/esign/README.md)
- [급여관리 기능](../../features/finance/payroll.md)
- [API 개발 규칙](api-rules.md)
- [options JSON 컬럼 정책](options-column-policy.md)
---
**최종 업데이트**: 2026-03-11

View File

@@ -1,329 +1,329 @@
# DomPDF 사용 가이드
> **작성일**: 2026-03-11
> **패키지**: `barryvdh/laravel-dompdf` v3.1 (DomPDF v3.1.5)
> **구현 참조**: `mng/app/Services/HR/PayrollService.php`
---
## 1. DomPDF 인스턴스 규칙
### 1.1 폰트 등록은 반드시 렌더링할 인스턴스에
`Pdf::loadView()`는 **매번 새 DomPDF 인스턴스**를 생성한다. 다른 인스턴스에 폰트를 등록해도 렌더링 인스턴스에는 적용되지 않는다.
```php
// ❌ 인스턴스 불일치 — 폰트가 적용되지 않음
$dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF(); // 인스턴스 A
$dompdf->getFontMetrics()->registerFont(...);
$pdf = Pdf::loadView('view', $data); // 인스턴스 B (폰트 없음)
$pdf->output();
// ✅ 동일 인스턴스에 등록
$pdf = Pdf::loadView('view', $data);
$dompdf = $pdf->getDomPDF(); // loadView가 만든 바로 그 인스턴스
$dompdf->getFontMetrics()->registerFont(...);
$pdf->output();
```
### 1.2 등록 → 렌더링 순서
```
Pdf::loadView() → registerFont() → saveFontFamilies() → $pdf->output()
```
`output()` 호출 시 내부에서 `render()`가 실행되므로, 그 전에 폰트 등록이 완료되어야 한다.
---
## 2. setOptions() 사용 금지
### 2.1 문제
`->setOptions([...])` 호출 시 DomPDF 내부에서 `new Options($options)`를 실행한다. 이때 **전달한 옵션만 설정되고 나머지는 DomPDF 기본값으로 초기화**된다.
```php
// ❌ chroot, font_dir 등 config/dompdf.php 설정이 모두 초기화됨
$pdf = Pdf::loadView('view', $data)
->setOptions([
'font_dir' => storage_path('fonts'),
'enable_font_subsetting' => true,
]);
// 이 시점에서 chroot = vendor/dompdf/dompdf (DomPDF 기본값)
```
### 2.2 해결
`config/dompdf.php`에 모든 설정을 선언하고, 코드에서 `setOptions()`를 호출하지 않는다.
```php
// config/dompdf.php — 여기에 모든 설정
'options' => [
'font_dir' => storage_path('fonts'),
'font_cache' => storage_path('fonts'),
'enable_font_subsetting' => true,
'chroot' => array_filter([
realpath(base_path()),
realpath(storage_path('fonts')),
]),
// ...
],
// ✅ 코드에서는 setOptions 없이 사용
$pdf = Pdf::loadView('view', $data)->setPaper('a4');
```
---
## 3. chroot와 파일 경로
### 3.1 chroot 검증 원리
DomPDF의 `validateLocalUri()`는 폰트 파일 접근 시 다음을 검사한다:
```
realpath(파일 경로)가 realpath(chroot) 하위인가?
```
**symlink는 realpath()로 해소**되므로, symlink 경로가 chroot 밖을 가리키면 차단된다.
### 3.2 릴리스 기반 배포 환경
```
mng/current → releases/20260311_134148/ (배포마다 변경)
releases/XXXXX/storage/fonts → ../../shared/storage/fonts/ (symlink)
```
- `storage_path('fonts')``/home/.../releases/XXXXX/storage/fonts` (symlink 경로)
- `realpath()``/home/.../shared/storage/fonts` (실제 경로)
- `base_path()``/home/.../releases/XXXXX/` (릴리스 경로)
**shared 경로는 릴리스 경로 하위가 아니므로** chroot에 별도 등록이 필요하다.
### 3.3 chroot 설정
```php
// config/dompdf.php
'chroot' => array_filter([
realpath(base_path()), // 릴리스 내부 파일 허용
realpath(storage_path('fonts')), // shared 폰트 디렉토리 허용
]),
```
`array_filter()``realpath()``false`를 반환할 경우(경로 미존재) 제거하기 위함이다.
---
## 4. 폰트 파일 경로 선택
### 4.1 resource_path() vs storage_path()
| 항목 | `resource_path()` | `storage_path()` |
|------|-------------------|-------------------|
| 릴리스 변경 시 | 경로 변경됨 | symlink → shared (불변) |
| .ufm 캐시 | 매 배포마다 재생성 | 유지됨 |
| installed-fonts.json | 경로 불일치로 무효화 | 안정적 |
| Git 포함 | O (원본 보관용) | X (.gitignore) |
**결론**: 폰트 등록 시 `storage_path()` 사용. 원본 TTF는 `resources/fonts/`에 Git으로 관리하고, 최초 실행 시 `storage/fonts/`로 복사한다.
### 4.2 폰트 복사 패턴
```php
$fontDir = storage_path('fonts');
$dst = $fontDir.'/Pretendard-Regular.ttf';
// 최초 1회만 복사 (이후 shared에 유지)
if (! file_exists($dst)) {
$src = resource_path('fonts/Pretendard-Regular.ttf');
if (! file_exists($src)) {
return;
}
copy($src, $dst);
}
```
---
## 5. 외부 폰트 및 금지 사항
### 5.1 구글 폰트 금지
```
❌ @import url('https://fonts.googleapis.com/...');
❌ <link href="https://fonts.googleapis.com/..." rel="stylesheet">
❌ @font-face src: url('https://...');
```
DomPDF는 웹 브라우저가 아니다. 외부 폰트 다운로드는 네트워크 의존성, 방화벽 차단, 성능 저하를 유발한다.
### 5.2 isRemoteEnabled 금지
```php
// ❌ 보안 위험 + 외부 의존성
->setOptions(['isRemoteEnabled' => true])
```
### 5.3 font-weight 800 이상 사용 금지
Pretendard는 `normal`(400)과 `bold`(700)만 DomPDF에 등록되어 있다. `font-weight: 800` 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 `?`로 깨진다.
```css
/* ❌ DomPDF에서 한글 깨짐 — 800 weight에 매칭되는 폰트 없음 */
h1 { font-weight: 800; }
/* ✅ bold(700)까지만 사용 */
h1 { font-weight: bold; }
```
### 5.4 시스템 전용 폰트 단독 사용 금지
DomPDF는 OS 시스템 폰트를 자동 인식하지 않는다. `registerFont()`로 등록된 폰트만 사용 가능하다.
```css
/* ❌ DomPDF가 인식 못함 → 한글 ??? */
body { font-family: 'Malgun Gothic', sans-serif; }
/* ✅ DomPDF에 등록된 폰트 사용 */
body { font-family: 'Pretendard', 'Malgun Gothic', sans-serif; }
```
> `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback 용도로만 기재한다.
---
## 6. PDF 경량화 설정
### 6.1 폰트 서브셋팅 (필수)
`config/dompdf.php`에서 `enable_font_subsetting``true`로 설정한다. PDF에 실제 사용된 문자의 글리프만 포함하여 용량을 대폭 줄인다.
```php
// config/dompdf.php
'options' => [
'enable_font_subsetting' => true, // ✅ 필수 — 사용 글자만 임베딩
'enable_javascript' => false, // ✅ 권장 — PDF 내 JS 불필요
// ...
],
```
| 설정 | 변경 전 | 변경 후 | 효과 |
|------|---------|---------|------|
| `enable_font_subsetting` | `false` | `true` | 폰트 전체(~2-5MB) → 사용 글자만(수십KB) |
| `enable_javascript` | `true` | `false` | PDF 내 JS 코드 제거 |
> 한글 폰트는 11,172개의 완성형 글자를 포함하지만, 급여명세서에 사용되는 글자는 100~200자 수준이다. 서브셋팅으로 99% 이상의 불필요한 글리프를 제거한다.
---
## 7. 표준 PDF 생성 패턴
### 7.1 전체 코드
```php
$pdf = Pdf::loadView('emails.payslip', ['payslipData' => $data])
->setPaper('a4');
$this->registerKoreanFont($pdf); // 동일 인스턴스에 등록
$pdfContent = $pdf->output();
```
### 7.2 registerKoreanFont 구현
```php
private function registerKoreanFont(\Barryvdh\DomPDF\PDF $pdf): void
{
$fontDir = storage_path('fonts');
$normalDst = $fontDir.'/Pretendard-Regular.ttf';
$boldDst = $fontDir.'/Pretendard-Bold.ttf';
// resources → storage 복사 (최초 1회, 이후 shared에 유지)
if (! file_exists($normalDst)) {
$src = resource_path('fonts/Pretendard-Regular.ttf');
if (! file_exists($src)) {
return;
}
if (! is_dir($fontDir)) {
mkdir($fontDir, 0755, true);
}
copy($src, $normalDst);
}
if (! file_exists($boldDst)) {
$src = resource_path('fonts/Pretendard-Bold.ttf');
if (file_exists($src)) {
copy($src, $boldDst);
}
}
$dompdf = $pdf->getDomPDF();
$fm = $dompdf->getFontMetrics();
$fm->registerFont(
['family' => 'pretendard', 'style' => 'normal', 'weight' => 'normal'],
$normalDst
);
if (file_exists($boldDst)) {
$fm->registerFont(
['family' => 'pretendard', 'style' => 'normal', 'weight' => 'bold'],
$boldDst
);
}
$fm->saveFontFamilies();
}
```
---
## 8. 폰트 캐시 구조
```
storage/fonts/ ← shared 디렉토리 (배포 간 유지)
├── installed-fonts.json ← DomPDF 폰트 레지스트리
├── Pretendard-Regular.ttf ← resources/에서 복사된 원본
├── Pretendard-Bold.ttf
├── pretendard_normal_*.ufm ← DomPDF 메트릭 캐시 (자동 생성)
├── pretendard_normal_*.ttf ← DomPDF 서브셋 (자동 생성)
└── pretendard_bold_*.*
```
> `storage/fonts/`는 `.gitignore`에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다.
---
## 9. 체크리스트
### PDF 뷰 작성 시
- [ ] 외부 폰트 URL 미포함 (`@import`, `<link>`)
- [ ] `font-family``Pretendard` 포함 (DomPDF 한글 지원)
- [ ] `font-family` fallback에 `Malgun Gothic`, `sans-serif` 포함 (브라우저용)
- [ ] `font-weight``normal`/`bold`만 사용 (800 이상 금지)
### PDF 생성 코드 작성 시
- [ ] `setOptions()` 미사용 (config/dompdf.php에 선언)
- [ ] `Pdf::loadView()` 후 **동일 인스턴스**에 폰트 등록
- [ ] 폰트 경로는 `storage_path()` 사용 (`resource_path()` 아님)
- [ ] `isRemoteEnabled` 미사용
### 배포 환경 확인
- [ ] `config/dompdf.php` chroot에 `realpath(storage_path('fonts'))` 포함
- [ ] `storage/fonts/` 디렉토리 쓰기 권한 확인
- [ ] 폰트 TTF 파일이 `resources/fonts/`에 Git 관리됨
- [ ] `enable_font_subsetting``true`
---
## 관련 문서
- DomPDF 설정: `mng/config/dompdf.php`
- 구현 참조: `mng/app/Services/HR/PayrollService.php``registerKoreanFont()`
- 폰트 원본: `mng/resources/fonts/` — Pretendard TTF (Git 관리)
- 서버 운영: `dev/deploys/ops-manual/README.md`
---
# DomPDF 사용 가이드
> **작성일**: 2026-03-11
> **패키지**: `barryvdh/laravel-dompdf` v3.1 (DomPDF v3.1.5)
> **구현 참조**: `mng/app/Services/HR/PayrollService.php`
---
## 1. DomPDF 인스턴스 규칙
### 1.1 폰트 등록은 반드시 렌더링할 인스턴스에
`Pdf::loadView()`는 **매번 새 DomPDF 인스턴스**를 생성한다. 다른 인스턴스에 폰트를 등록해도 렌더링 인스턴스에는 적용되지 않는다.
```php
// ❌ 인스턴스 불일치 — 폰트가 적용되지 않음
$dompdf = app(\Barryvdh\DomPDF\PDF::class)->getDomPDF(); // 인스턴스 A
$dompdf->getFontMetrics()->registerFont(...);
$pdf = Pdf::loadView('view', $data); // 인스턴스 B (폰트 없음)
$pdf->output();
// ✅ 동일 인스턴스에 등록
$pdf = Pdf::loadView('view', $data);
$dompdf = $pdf->getDomPDF(); // loadView가 만든 바로 그 인스턴스
$dompdf->getFontMetrics()->registerFont(...);
$pdf->output();
```
### 1.2 등록 → 렌더링 순서
```
Pdf::loadView() → registerFont() → saveFontFamilies() → $pdf->output()
```
`output()` 호출 시 내부에서 `render()`가 실행되므로, 그 전에 폰트 등록이 완료되어야 한다.
---
## 2. setOptions() 사용 금지
### 2.1 문제
`->setOptions([...])` 호출 시 DomPDF 내부에서 `new Options($options)`를 실행한다. 이때 **전달한 옵션만 설정되고 나머지는 DomPDF 기본값으로 초기화**된다.
```php
// ❌ chroot, font_dir 등 config/dompdf.php 설정이 모두 초기화됨
$pdf = Pdf::loadView('view', $data)
->setOptions([
'font_dir' => storage_path('fonts'),
'enable_font_subsetting' => true,
]);
// 이 시점에서 chroot = vendor/dompdf/dompdf (DomPDF 기본값)
```
### 2.2 해결
`config/dompdf.php`에 모든 설정을 선언하고, 코드에서 `setOptions()`를 호출하지 않는다.
```php
// config/dompdf.php — 여기에 모든 설정
'options' => [
'font_dir' => storage_path('fonts'),
'font_cache' => storage_path('fonts'),
'enable_font_subsetting' => true,
'chroot' => array_filter([
realpath(base_path()),
realpath(storage_path('fonts')),
]),
// ...
],
// ✅ 코드에서는 setOptions 없이 사용
$pdf = Pdf::loadView('view', $data)->setPaper('a4');
```
---
## 3. chroot와 파일 경로
### 3.1 chroot 검증 원리
DomPDF의 `validateLocalUri()`는 폰트 파일 접근 시 다음을 검사한다:
```
realpath(파일 경로)가 realpath(chroot) 하위인가?
```
**symlink는 realpath()로 해소**되므로, symlink 경로가 chroot 밖을 가리키면 차단된다.
### 3.2 릴리스 기반 배포 환경
```
mng/current → releases/20260311_134148/ (배포마다 변경)
releases/XXXXX/storage/fonts → ../../shared/storage/fonts/ (symlink)
```
- `storage_path('fonts')``/home/.../releases/XXXXX/storage/fonts` (symlink 경로)
- `realpath()``/home/.../shared/storage/fonts` (실제 경로)
- `base_path()``/home/.../releases/XXXXX/` (릴리스 경로)
**shared 경로는 릴리스 경로 하위가 아니므로** chroot에 별도 등록이 필요하다.
### 3.3 chroot 설정
```php
// config/dompdf.php
'chroot' => array_filter([
realpath(base_path()), // 릴리스 내부 파일 허용
realpath(storage_path('fonts')), // shared 폰트 디렉토리 허용
]),
```
`array_filter()``realpath()``false`를 반환할 경우(경로 미존재) 제거하기 위함이다.
---
## 4. 폰트 파일 경로 선택
### 4.1 resource_path() vs storage_path()
| 항목 | `resource_path()` | `storage_path()` |
|------|-------------------|-------------------|
| 릴리스 변경 시 | 경로 변경됨 | symlink → shared (불변) |
| .ufm 캐시 | 매 배포마다 재생성 | 유지됨 |
| installed-fonts.json | 경로 불일치로 무효화 | 안정적 |
| Git 포함 | O (원본 보관용) | X (.gitignore) |
**결론**: 폰트 등록 시 `storage_path()` 사용. 원본 TTF는 `resources/fonts/`에 Git으로 관리하고, 최초 실행 시 `storage/fonts/`로 복사한다.
### 4.2 폰트 복사 패턴
```php
$fontDir = storage_path('fonts');
$dst = $fontDir.'/Pretendard-Regular.ttf';
// 최초 1회만 복사 (이후 shared에 유지)
if (! file_exists($dst)) {
$src = resource_path('fonts/Pretendard-Regular.ttf');
if (! file_exists($src)) {
return;
}
copy($src, $dst);
}
```
---
## 5. 외부 폰트 및 금지 사항
### 5.1 구글 폰트 금지
```
❌ @import url('https://fonts.googleapis.com/...');
❌ <link href="https://fonts.googleapis.com/..." rel="stylesheet">
❌ @font-face src: url('https://...');
```
DomPDF는 웹 브라우저가 아니다. 외부 폰트 다운로드는 네트워크 의존성, 방화벽 차단, 성능 저하를 유발한다.
### 5.2 isRemoteEnabled 금지
```php
// ❌ 보안 위험 + 외부 의존성
->setOptions(['isRemoteEnabled' => true])
```
### 5.3 font-weight 800 이상 사용 금지
Pretendard는 `normal`(400)과 `bold`(700)만 DomPDF에 등록되어 있다. `font-weight: 800` 이상을 지정하면 DomPDF가 매칭되는 폰트를 찾지 못해 해당 텍스트의 한글이 `?`로 깨진다.
```css
/* ❌ DomPDF에서 한글 깨짐 — 800 weight에 매칭되는 폰트 없음 */
h1 { font-weight: 800; }
/* ✅ bold(700)까지만 사용 */
h1 { font-weight: bold; }
```
### 5.4 시스템 전용 폰트 단독 사용 금지
DomPDF는 OS 시스템 폰트를 자동 인식하지 않는다. `registerFont()`로 등록된 폰트만 사용 가능하다.
```css
/* ❌ DomPDF가 인식 못함 → 한글 ??? */
body { font-family: 'Malgun Gothic', sans-serif; }
/* ✅ DomPDF에 등록된 폰트 사용 */
body { font-family: 'Pretendard', 'Malgun Gothic', sans-serif; }
```
> `Malgun Gothic`은 브라우저에서 HTML을 직접 볼 때의 fallback 용도로만 기재한다.
---
## 6. PDF 경량화 설정
### 6.1 폰트 서브셋팅 (필수)
`config/dompdf.php`에서 `enable_font_subsetting``true`로 설정한다. PDF에 실제 사용된 문자의 글리프만 포함하여 용량을 대폭 줄인다.
```php
// config/dompdf.php
'options' => [
'enable_font_subsetting' => true, // ✅ 필수 — 사용 글자만 임베딩
'enable_javascript' => false, // ✅ 권장 — PDF 내 JS 불필요
// ...
],
```
| 설정 | 변경 전 | 변경 후 | 효과 |
|------|---------|---------|------|
| `enable_font_subsetting` | `false` | `true` | 폰트 전체(~2-5MB) → 사용 글자만(수십KB) |
| `enable_javascript` | `true` | `false` | PDF 내 JS 코드 제거 |
> 한글 폰트는 11,172개의 완성형 글자를 포함하지만, 급여명세서에 사용되는 글자는 100~200자 수준이다. 서브셋팅으로 99% 이상의 불필요한 글리프를 제거한다.
---
## 7. 표준 PDF 생성 패턴
### 7.1 전체 코드
```php
$pdf = Pdf::loadView('emails.payslip', ['payslipData' => $data])
->setPaper('a4');
$this->registerKoreanFont($pdf); // 동일 인스턴스에 등록
$pdfContent = $pdf->output();
```
### 7.2 registerKoreanFont 구현
```php
private function registerKoreanFont(\Barryvdh\DomPDF\PDF $pdf): void
{
$fontDir = storage_path('fonts');
$normalDst = $fontDir.'/Pretendard-Regular.ttf';
$boldDst = $fontDir.'/Pretendard-Bold.ttf';
// resources → storage 복사 (최초 1회, 이후 shared에 유지)
if (! file_exists($normalDst)) {
$src = resource_path('fonts/Pretendard-Regular.ttf');
if (! file_exists($src)) {
return;
}
if (! is_dir($fontDir)) {
mkdir($fontDir, 0755, true);
}
copy($src, $normalDst);
}
if (! file_exists($boldDst)) {
$src = resource_path('fonts/Pretendard-Bold.ttf');
if (file_exists($src)) {
copy($src, $boldDst);
}
}
$dompdf = $pdf->getDomPDF();
$fm = $dompdf->getFontMetrics();
$fm->registerFont(
['family' => 'pretendard', 'style' => 'normal', 'weight' => 'normal'],
$normalDst
);
if (file_exists($boldDst)) {
$fm->registerFont(
['family' => 'pretendard', 'style' => 'normal', 'weight' => 'bold'],
$boldDst
);
}
$fm->saveFontFamilies();
}
```
---
## 8. 폰트 캐시 구조
```
storage/fonts/ ← shared 디렉토리 (배포 간 유지)
├── installed-fonts.json ← DomPDF 폰트 레지스트리
├── Pretendard-Regular.ttf ← resources/에서 복사된 원본
├── Pretendard-Bold.ttf
├── pretendard_normal_*.ufm ← DomPDF 메트릭 캐시 (자동 생성)
├── pretendard_normal_*.ttf ← DomPDF 서브셋 (자동 생성)
└── pretendard_bold_*.*
```
> `storage/fonts/`는 `.gitignore`에 포함되어 있다. 각 환경에서 첫 PDF 생성 시 자동으로 생성된다.
---
## 9. 체크리스트
### PDF 뷰 작성 시
- [ ] 외부 폰트 URL 미포함 (`@import`, `<link>`)
- [ ] `font-family``Pretendard` 포함 (DomPDF 한글 지원)
- [ ] `font-family` fallback에 `Malgun Gothic`, `sans-serif` 포함 (브라우저용)
- [ ] `font-weight``normal`/`bold`만 사용 (800 이상 금지)
### PDF 생성 코드 작성 시
- [ ] `setOptions()` 미사용 (config/dompdf.php에 선언)
- [ ] `Pdf::loadView()` 후 **동일 인스턴스**에 폰트 등록
- [ ] 폰트 경로는 `storage_path()` 사용 (`resource_path()` 아님)
- [ ] `isRemoteEnabled` 미사용
### 배포 환경 확인
- [ ] `config/dompdf.php` chroot에 `realpath(storage_path('fonts'))` 포함
- [ ] `storage/fonts/` 디렉토리 쓰기 권한 확인
- [ ] 폰트 TTF 파일이 `resources/fonts/`에 Git 관리됨
- [ ] `enable_font_subsetting``true`
---
## 관련 문서
- DomPDF 설정: `mng/config/dompdf.php`
- 구현 참조: `mng/app/Services/HR/PayrollService.php``registerKoreanFont()`
- 폰트 원본: `mng/resources/fonts/` — Pretendard TTF (Git 관리)
- 서버 운영: `dev/deploys/ops-manual/README.md`
---
**최종 업데이트**: 2026-03-11

View File

@@ -1,344 +1,344 @@
# 결재관리 MNG↔API 비교 분석 및 반영 가이드
> **작성일**: 2026-03-11
> **상태**: 분석 완료
> **목적**: React 결재관리 화면 구현 시 참고 자료
---
## 1. 개요
### 1.1 배경
결재관리는 API(2025-12-17)에서 최초 구현되었고, MNG(2026-02-27)가 이를 참조하여 구축했다. MNG가 실제 관리자 화면을 만들면서 ~90개 커밋에 걸쳐 대폭 진화했으며, API는 2026-03-11에 "MNG 스타일로 전면 개선" 커밋으로 역반영을 시작했다.
### 1.2 핵심 사실
- MNG와 API는 **동일한 5개 테이블**을 공유한다 (`approvals`, `approval_steps`, `approval_lines`, `approval_forms`, `approval_delegations`)
- 마이그레이션은 **API에서만** 관리한다 (24개 마이그레이션)
- MNG 모델은 API 모델의 **수동 동기 사본**이다
---
## 2. 개발 타임라인
```
2025-12-17 API 최초 구현 (기본 CRUD + 워크플로우 4종)
▼ ─── 약 2개월간 API만 단독 운영 ───
2026-02-27 MNG Phase 1 MVP (API 참조하여 구축, 19파일 2806줄)
2026-02-27 MNG Phase 2 고급기능 (보류/전결/복사재기안, 10파일 757줄)
▼ ─── MNG 독자 진화 시작 ───
2026-02-28 결재선 UI/UX 대폭 개선 (~12 커밋)
- Alpine.js v3 호환, 2패널 에디터, Toss 스타일
- 결재선 템플릿 CRUD, 드래그앤드롭, 모달 전환
- 기본 결재선 자동 선택, Quill.js 편집기
2026-02-28 휴가신청 → 전자결재 자동연동
2026-03-03 삭제 권한 기능 (isDeletableBy, 관리자 삭제)
2026-03-04 지출결의서 전용 폼 (첨부파일 GCS, 법인카드/송금 선택)
2026-03-05 반려이력/재상신, 결재서명란, 거래처 검색, 불러오기
2026-03-05 증명서류 양식 (재직/경력/위촉증명서, 사직서) + PDF
2026-03-06 업무양식 (품의서5종, 인감/위임장/의사록/견적서/공문서)
2026-03-07 연차촉진통지서, 결재/참조선 2영역 분리
▼ ─── MNG 실무 검증 완료 ───
2026-03-11 API "MNG 스타일로 전면 개선" (역방향 반영)
```
---
## 3. 공유 테이블 (5개)
| 테이블 | MNG 모델 경로 | API 모델 경로 |
|--------|-------------|-------------|
| `approvals` | `mng/app/Models/Approvals/Approval.php` | `api/app/Models/Tenants/Approval.php` |
| `approval_steps` | `mng/app/Models/Approvals/ApprovalStep.php` | `api/app/Models/Tenants/ApprovalStep.php` |
| `approval_lines` | `mng/app/Models/Approvals/ApprovalLine.php` | `api/app/Models/Tenants/ApprovalLine.php` |
| `approval_forms` | `mng/app/Models/Approvals/ApprovalForm.php` | `api/app/Models/Tenants/ApprovalForm.php` |
| `approval_delegations` | `mng/app/Models/Approvals/ApprovalDelegation.php` | `api/app/Models/Tenants/ApprovalDelegation.php` |
> MNG에만 있는 별도 테이블: `document_approvals`, `document_template_approval_lines` (문서관리 전용, 메인 결재관리와 무관)
---
## 4. MNG vs API 기능 비교
### 4.1 MNG에서 추가/개선된 기능 (API 원본에 없던 것)
| 분류 | MNG 기능 | 설명 | API 반영 상태 |
|------|----------|------|-------------|
| **워크플로우** | 보류/해제 | `hold()`, `releaseHold()` | ✅ 반영됨 |
| **워크플로우** | 전결 | `preDecide()` — 이후 단계 건너뜀 | ✅ 반영됨 |
| **워크플로우** | 복사재기안 | `copyForRedraft()` — 완료/반려/회수 문서 복사 | ✅ 반영됨 |
| **재상신** | `resubmit_count`, `rejection_history` | 반려 이력 JSON, 재상신 횟수 추적 | ✅ 반영됨 |
| **결재선 UI** | 2패널 에디터 | 좌: 조직도, 우: 선택된 결재선 | ❌ React 구현 필요 |
| **결재선 UI** | 드래그앤드롭 | 결재 순서 변경 | ❌ React 구현 필요 |
| **결재선 UI** | 결재/참조 2영역 분리 | 결재선과 참조선 별도 관리 | ❌ React 구현 필요 |
| **결재선 UI** | Toss 스타일 모달 | 결재선 관리 모달 | ❌ React 구현 필요 |
| **양식 시스템** | 2단계 선택 | 분류 → 양식 드릴다운 | ❌ React 구현 필요 |
| **양식 시스템** | 본문 자동채움 | `body_template` 기반 자동 렌더링 | ❌ React 구현 필요 |
| **양식 시스템** | Quill.js 편집기 | 리치텍스트 본문 편집 토글 | ❌ React 구현 필요 |
| **전용 양식 15종** | 지출결의서 외 다수 | 아래 4.2 참조 | ❌ React 구현 필요 |
| **지출결의서** | 법인카드/송금 선택 | 지출형식별 동적 UI | ❌ React 구현 필요 |
| **지출결의서** | 첨부파일 업로드 | GCS 연동 | ❌ API 엔드포인트 필요 |
| **지출결의서** | 거래처 검색 | 업체명 자동완성 | ❌ React 구현 필요 |
| **지출결의서** | 불러오기 | 이전 결의서 복사 | ❌ React 구현 필요 |
| **결재서명란** | 전통 도장식 테이블 | `_approval-stamp-table.blade.php` | ❌ React 구현 필요 |
| **증명서 PDF** | 4종 PDF 다운로드 | 재직/경력/위촉증명서, 사직서 | ❌ API PDF 엔드포인트 필요 |
| **삭제 권한** | `isDeletableBy()` | 관리자/슈퍼관리자 강제삭제, 일괄삭제 | ❌ API 엔드포인트 필요 |
| **뱃지** | 진행중 건수 | 기안함 뱃지 = 진행중 문서 수 | ✅ badge-counts 반영 |
| **완료함** | 미읽음 알림 | `drafter_read_at` 기반 | ✅ 반영됨 |
| **휴가 연동** | 결재→Leave 자동 변경 | 승인/반려/회수 시 Leave 상태 동기화 | ✅ 반영됨 |
### 4.2 MNG 전용 양식 목록 (15종+)
| 카테고리 | 양식명 | MNG Blade 파일 | PDF |
|---------|--------|---------------|-----|
| **근태** | 연차/반차/조퇴 신청 | `_leave-form.blade.php` | — |
| **지출** | 지출결의서 | `_expense-form.blade.php` | — |
| **품의** | 지출품의서 | `_purchase-request-form.blade.php` | — |
| **품의** | 계약체결품의서 | 상동 (5종 분기) | — |
| **품의** | 구매품의서 | 상동 | — |
| **품의** | 출장품의서 | 상동 | — |
| **품의** | 비용정산서 | 상동 | — |
| **증명** | 재직증명서 | `_certificate-form.blade.php` | ✅ |
| **증명** | 경력증명서 | `_career-cert-form.blade.php` | ✅ |
| **증명** | 위촉증명서 | `_appointment-cert-form.blade.php` | ✅ |
| **인사** | 사직서 | `_resignation-form.blade.php` | ✅ |
| **업무** | 사용인감계 | `_seal-usage-form.blade.php` | — |
| **업무** | 위임장 | `_delegation-form.blade.php` | — |
| **업무** | 이사회의사록 | `_board-minutes-form.blade.php` | — |
| **업무** | 견적서 | `_quotation-form.blade.php` | — |
| **업무** | 공문서 | `_official-letter-form.blade.php` | — |
| **근태** | 연차촉진통지서 1차 | `_leave-promotion-1st-form.blade.php` | — |
| **근태** | 연차촉진통지서 2차 | `_leave-promotion-2nd-form.blade.php` | — |
> 각 양식은 `_<name>-form.blade.php` (기안 작성)와 `_<name>-show.blade.php` (상세 조회) 쌍으로 구성
---
## 5. API 반영 현황
### 5.1 이미 반영 완료
| 항목 | 설명 |
|------|------|
| DB 스키마 | 마이그레이션 24개 — MNG와 완전 동기화 |
| 모델 상수/관계/헬퍼 | 6가지 문서 상태, 5가지 단계 상태, 3가지 step_type |
| 워크플로우 전체 | submit/approve/reject/cancel/hold/releaseHold/preDecide/copyForRedraft |
| 위임 CRUD | delegations 엔드포인트 4개 |
| 결재함 4종 + badge-counts | drafts/inbox/reference/completed + summary |
| FormRequest 검증 21개 | MNG보다 정교하게 분리 |
| Swagger 문서 | 3개 파일 (ApprovalApi, ApprovalFormApi, ApprovalLineApi) |
| Leave/Document 연동 | 결재 상태 변경 시 연관 엔티티 자동 동기화 |
| Observer 알림 | TodayIssue 발행 (ApprovalIssueObserver, ApprovalStepIssueObserver) |
| 테넌트 부트스트랩 | 새 테넌트 생성 시 기본 양식 5종 자동 시딩 |
### 5.2 API에 미반영 (React 화면 개발 시 필요)
| 항목 | 설명 | 난이도 | 비고 |
|------|------|--------|------|
| 🔴 전용 양식 렌더링 | 15종 양식별 폼 UI + 조회 UI | 대형 | MNG Blade를 React로 재구현 |
| 🔴 결재선 에디터 UI | 2패널 + 드래그앤드롭 + 결재/참조 분리 | 대형 | MNG의 `_approval-line-editor.blade.php` 참조 |
| 🟡 결재서명란 | 전통 도장식 결재 테이블 | 중형 | MNG의 `_approval-stamp-table.blade.php` 참조 |
| 🟡 양식 2단계 선택 | 분류 → 양식 드릴다운 | 중형 | |
| 🟡 증명서 PDF 출력 | API에서 DomPDF 렌더링 엔드포인트 | 중형 | MNG는 자체 처리 중 |
| 🟡 위임 결재 실제 동작 | `acted_by` 대결 처리 로직 | 중형 | Phase 3 |
| 🟡 병렬 결재 | `parallel_group` 활용 로직 | 중형 | Phase 3 |
| 🟢 첨부파일 업로드 | 결재 전용 업로드/다운로드 엔드포인트 | 소형 | 공통 File API 활용 가능 |
| 🟢 관리자 강제삭제 | 슈퍼관리자 권한 관리/일괄삭제 | 소형 | |
| 🟢 결재 통계/리포트 | 기간별/부서별/양식별 통계 | 소형 | Phase 4 |
---
## 6. 프로젝트별 역할 분담
```
MNG (관리자 화면 - Blade) API (REST 백엔드) React (사용자 화면)
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ ✅ 결재 전체 기능 │ │ ✅ 워크플로우 로직 │ │ ❌ 화면 미구현 │
│ ✅ 15종+ 양식 UI │ │ ✅ REST API 전체 │ │ │
│ ✅ 결재선 에디터 │ │ ✅ FormRequest 검증 │ │ React에서 구현 필요: │
│ ✅ 결재서명란 │ │ ✅ Swagger 문서 │ │ - 양식별 폼 UI │
│ ✅ PDF 출력 4종 │ │ ✅ Observer 알림 │ │ - 결재선 에디터 │
│ ✅ 첨부파일 GCS │ │ ✅ Leave/Doc 연동 │ │ - 결재서명란 │
│ ✅ 관리자 강제삭제 │ │ ✅ 테넌트 부트스트랩 │ │ - PDF 다운로드 │
│ ✅ 일괄삭제 │ │ │ │ - 파일 업로드 │
│ │ │ ❌ PDF 생성 엔드포인트│ │ │
│ │ │ ❌ 위임 대결 로직 │ │ │
│ │ │ ❌ 병렬 결재 로직 │ │ │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
가장 완성도 높음 백엔드 로직 완성 UI 구현 대기 중
```
---
## 7. React 구현 시 MNG 참조 파일 가이드
### 7.1 뷰 파일 구조 (`mng/resources/views/approvals/`)
```
approvals/
├── drafts.blade.php ← 기안함 목록
├── pending.blade.php ← 결재 대기함 목록
├── completed.blade.php ← 처리 완료함 목록
├── references.blade.php ← 참조함 목록
├── create.blade.php ← 기안 작성
├── edit.blade.php ← 기안 수정
├── show.blade.php ← 결재 상세 조회
├── partials/
│ ├── _approval-line-editor.blade.php ← 🔴 결재선 에디터 (핵심 UI)
│ ├── _approval-stamp-table.blade.php ← 🟡 결재서명란
│ ├── _status-badge.blade.php ← 상태 뱃지
│ ├── _step-progress.blade.php ← 결재 진행 현황
│ │
│ ├── _expense-form.blade.php ← 지출결의서 기안
│ ├── _expense-show.blade.php ← 지출결의서 조회
│ ├── _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 ← 사직서 조회
│ ├── _purchase-request-form.blade.php ← 품의서 기안 (5종)
│ ├── _purchase-request-show.blade.php ← 품의서 조회
│ ├── _seal-usage-form.blade.php ← 인감 기안
│ ├── _seal-usage-show.blade.php ← 인감 조회
│ ├── _delegation-form.blade.php ← 위임장 기안
│ ├── _delegation-show.blade.php ← 위임장 조회
│ ├── _board-minutes-form.blade.php ← 의사록 기안
│ ├── _board-minutes-show.blade.php ← 의사록 조회
│ ├── _quotation-form.blade.php ← 견적서 기안
│ ├── _quotation-show.blade.php ← 견적서 조회
│ ├── _official-letter-form.blade.php ← 공문서 기안
│ ├── _official-letter-show.blade.php ← 공문서 조회
│ ├── _leave-form.blade.php ← 근태신청 기안
│ ├── _leave-show.blade.php ← 근태신청 조회
│ ├── _leave-promotion-1st-form.blade.php ← 연차촉진 1차
│ ├── _leave-promotion-1st-show.blade.php
│ ├── _leave-promotion-2nd-form.blade.php ← 연차촉진 2차
│ └── _leave-promotion-2nd-show.blade.php
```
### 7.2 API 엔드포인트 참조
| MNG 엔드포인트 (내부 API) | 대응하는 API 엔드포인트 |
|--------------------------|----------------------|
| `POST /api/v1/approval-mgmt` | `POST /v1/approvals` |
| `GET /api/v1/approval-mgmt/{id}` | `GET /v1/approvals/{id}` |
| `PUT /api/v1/approval-mgmt/{id}` | `PATCH /v1/approvals/{id}` |
| `DELETE /api/v1/approval-mgmt/{id}` | `DELETE /v1/approvals/{id}` |
| `POST /api/v1/approval-mgmt/{id}/submit` | `POST /v1/approvals/{id}/submit` |
| `POST /api/v1/approval-mgmt/{id}/approve` | `POST /v1/approvals/{id}/approve` |
| `POST /api/v1/approval-mgmt/{id}/reject` | `POST /v1/approvals/{id}/reject` |
| `POST /api/v1/approval-mgmt/{id}/cancel` | `POST /v1/approvals/{id}/cancel` |
| `POST /api/v1/approval-mgmt/{id}/hold` | `POST /v1/approvals/{id}/hold` |
| `POST /api/v1/approval-mgmt/{id}/release-hold` | `POST /v1/approvals/{id}/release-hold` |
| `POST /api/v1/approval-mgmt/{id}/pre-decide` | `POST /v1/approvals/{id}/pre-decide` |
| `POST /api/v1/approval-mgmt/{id}/copy` | `POST /v1/approvals/{id}/copy` |
| `GET /api/v1/approval-mgmt/forms` | `GET /v1/approval-forms/active` |
| `GET /api/v1/approval-mgmt/lines` | `GET /v1/approval-lines` |
| `POST /api/v1/approval-mgmt/upload-file` | (공통 File API 사용) |
| `GET /api/v1/approval-mgmt/badge-counts` | `GET /v1/approvals/badge-counts` |
### 7.3 서비스 클래스 비교
| 기능 | MNG `ApprovalService` | API `ApprovalService` |
|------|----------------------|----------------------|
| 기안함 | `getMyDrafts()` | `drafts()` |
| 결재함 | `getPendingForMe()` | `inbox()` |
| 완료함 | `getCompletedByMe()` | `completed()` |
| 참조함 | `getReferencesForMe()` | `reference()` |
| 현황 카드 | — | `draftsSummary()`, `inboxSummary()`, `completedSummary()` |
| 생성 | `createApproval()` | `store()` |
| 수정 | `updateApproval()` | `update()` |
| 삭제 | `deleteApproval()`, `forceDeleteApproval()` | `destroy()` |
| 상신 | `submitApproval()` | `submit()` |
| 승인 | `approveApprovalStep()` | `approve()` |
| 반려 | `rejectApprovalStep()` | `reject()` |
| 회수 | `cancelApproval()` | `cancel()` |
| 보류 | `holdApproval()` | `hold()` |
| 보류해제 | `releaseApprovalHold()` | `releaseHold()` |
| 전결 | `preDecide()` (ApprovalApiController 내) | `preDecide()` |
| 복사재기안 | `copyForRedraft()` | `copyForRedraft()` |
| 결재선 CRUD | `createLine()`, `updateLine()`, `deleteLine()` | `lineStore()`, `lineUpdate()`, `lineDestroy()` |
| 양식 CRUD | — | `formStore()`, `formUpdate()`, `formDestroy()` |
| 위임 CRUD | — | `delegationStore()`, `delegationUpdate()`, `delegationDestroy()` |
| 휴가 연동 | `handleApprovalCompleted/Rejected/Deleted/Cancelled()` | 내부 처리 |
| PDF 생성 | 컨트롤러에서 DomPDF 직접 호출 | ❌ 미구현 |
---
## 8. 모델 동기화 주의사항
### 8.1 동기화가 필요한 시점
| 이벤트 | 조치 |
|--------|------|
| API에서 마이그레이션 추가 | MNG 모델의 `$fillable`, `$casts` 업데이트 |
| API 모델에 새 관계 추가 | MNG 모델에도 동일 관계 추가 |
| API 모델에 새 상수 추가 | MNG 모델에도 동일 상수 추가 |
| MNG에서 새 헬퍼 메서드 추가 | API 모델에 반영 여부 판단 (UI 전용이면 불필요) |
### 8.2 현재 차이점
| 항목 | MNG 모델 | API 모델 |
|------|---------|---------|
| Trait | `BelongsToTenant`, `SoftDeletes` | `BelongsToTenant`, `SoftDeletes`, `Auditable`, `ModelTrait` |
| 네임스페이스 | `App\Models\Approvals` | `App\Models\Tenants` |
| 감사 로깅 | — | `Auditable` trait으로 자동 기록 |
---
## 9. React 구현 우선순위 제안
### Phase A: 기본 결재 (필수)
1. 기안함/결재함/완료함/참조함 목록 화면
2. 기안 작성 (양식 선택 → 결재선 지정 → 본문 작성 → 상신)
3. 결재 상세 조회 + 승인/반려/회수 액션
4. 결재선 에디터 (2패널 + 결재/참조 분리)
5. badge-counts (사이드바 뱃지)
### Phase B: 양식 확장
1. 지출결의서 전용 폼 (법인카드/송금/첨부파일)
2. 품의서 5종
3. 근태신청 (휴가 연동)
4. 결재서명란
### Phase C: 증명서/업무 양식
1. 재직/경력/위촉증명서 + PDF 다운로드
2. 사직서, 인감, 위임장, 의사록, 견적서, 공문서
3. 연차촉진통지서
### Phase D: 고급 기능
1. 위임 대결 (Phase 3)
2. 병렬 결재 (Phase 3)
3. 결재 통계/리포트 (Phase 4)
---
## 관련 문서
- [결재관리 개요](README.md) — 시스템 전체 개요
- [양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조
- [워크플로우 상세](workflows.md) — 각 동작의 흐름
- [API 명세](api-reference.md) — 엔드포인트 목록
- [UI 화면 구성](ui-screens.md) — MNG 화면별 UI
- [DB 변경 및 모델 동기화](db-changes-and-model-sync.md) — 마이그레이션 이력
- [API 결재 연동 명세](../../frontend/api-specs/approval-api.md) — React용 API 명세
- [결재 통합 계획](../../dev/dev_plans/approval-system-unification-plan.md) — MNG→API 통합 계획
---
**최종 업데이트**: 2026-03-11
# 결재관리 MNG↔API 비교 분석 및 반영 가이드
> **작성일**: 2026-03-11
> **상태**: 분석 완료
> **목적**: React 결재관리 화면 구현 시 참고 자료
---
## 1. 개요
### 1.1 배경
결재관리는 API(2025-12-17)에서 최초 구현되었고, MNG(2026-02-27)가 이를 참조하여 구축했다. MNG가 실제 관리자 화면을 만들면서 ~90개 커밋에 걸쳐 대폭 진화했으며, API는 2026-03-11에 "MNG 스타일로 전면 개선" 커밋으로 역반영을 시작했다.
### 1.2 핵심 사실
- MNG와 API는 **동일한 5개 테이블**을 공유한다 (`approvals`, `approval_steps`, `approval_lines`, `approval_forms`, `approval_delegations`)
- 마이그레이션은 **API에서만** 관리한다 (24개 마이그레이션)
- MNG 모델은 API 모델의 **수동 동기 사본**이다
---
## 2. 개발 타임라인
```
2025-12-17 API 최초 구현 (기본 CRUD + 워크플로우 4종)
▼ ─── 약 2개월간 API만 단독 운영 ───
2026-02-27 MNG Phase 1 MVP (API 참조하여 구축, 19파일 2806줄)
2026-02-27 MNG Phase 2 고급기능 (보류/전결/복사재기안, 10파일 757줄)
▼ ─── MNG 독자 진화 시작 ───
2026-02-28 결재선 UI/UX 대폭 개선 (~12 커밋)
- Alpine.js v3 호환, 2패널 에디터, Toss 스타일
- 결재선 템플릿 CRUD, 드래그앤드롭, 모달 전환
- 기본 결재선 자동 선택, Quill.js 편집기
2026-02-28 휴가신청 → 전자결재 자동연동
2026-03-03 삭제 권한 기능 (isDeletableBy, 관리자 삭제)
2026-03-04 지출결의서 전용 폼 (첨부파일 GCS, 법인카드/송금 선택)
2026-03-05 반려이력/재상신, 결재서명란, 거래처 검색, 불러오기
2026-03-05 증명서류 양식 (재직/경력/위촉증명서, 사직서) + PDF
2026-03-06 업무양식 (품의서5종, 인감/위임장/의사록/견적서/공문서)
2026-03-07 연차촉진통지서, 결재/참조선 2영역 분리
▼ ─── MNG 실무 검증 완료 ───
2026-03-11 API "MNG 스타일로 전면 개선" (역방향 반영)
```
---
## 3. 공유 테이블 (5개)
| 테이블 | MNG 모델 경로 | API 모델 경로 |
|--------|-------------|-------------|
| `approvals` | `mng/app/Models/Approvals/Approval.php` | `api/app/Models/Tenants/Approval.php` |
| `approval_steps` | `mng/app/Models/Approvals/ApprovalStep.php` | `api/app/Models/Tenants/ApprovalStep.php` |
| `approval_lines` | `mng/app/Models/Approvals/ApprovalLine.php` | `api/app/Models/Tenants/ApprovalLine.php` |
| `approval_forms` | `mng/app/Models/Approvals/ApprovalForm.php` | `api/app/Models/Tenants/ApprovalForm.php` |
| `approval_delegations` | `mng/app/Models/Approvals/ApprovalDelegation.php` | `api/app/Models/Tenants/ApprovalDelegation.php` |
> MNG에만 있는 별도 테이블: `document_approvals`, `document_template_approval_lines` (문서관리 전용, 메인 결재관리와 무관)
---
## 4. MNG vs API 기능 비교
### 4.1 MNG에서 추가/개선된 기능 (API 원본에 없던 것)
| 분류 | MNG 기능 | 설명 | API 반영 상태 |
|------|----------|------|-------------|
| **워크플로우** | 보류/해제 | `hold()`, `releaseHold()` | ✅ 반영됨 |
| **워크플로우** | 전결 | `preDecide()` — 이후 단계 건너뜀 | ✅ 반영됨 |
| **워크플로우** | 복사재기안 | `copyForRedraft()` — 완료/반려/회수 문서 복사 | ✅ 반영됨 |
| **재상신** | `resubmit_count`, `rejection_history` | 반려 이력 JSON, 재상신 횟수 추적 | ✅ 반영됨 |
| **결재선 UI** | 2패널 에디터 | 좌: 조직도, 우: 선택된 결재선 | ❌ React 구현 필요 |
| **결재선 UI** | 드래그앤드롭 | 결재 순서 변경 | ❌ React 구현 필요 |
| **결재선 UI** | 결재/참조 2영역 분리 | 결재선과 참조선 별도 관리 | ❌ React 구현 필요 |
| **결재선 UI** | Toss 스타일 모달 | 결재선 관리 모달 | ❌ React 구현 필요 |
| **양식 시스템** | 2단계 선택 | 분류 → 양식 드릴다운 | ❌ React 구현 필요 |
| **양식 시스템** | 본문 자동채움 | `body_template` 기반 자동 렌더링 | ❌ React 구현 필요 |
| **양식 시스템** | Quill.js 편집기 | 리치텍스트 본문 편집 토글 | ❌ React 구현 필요 |
| **전용 양식 15종** | 지출결의서 외 다수 | 아래 4.2 참조 | ❌ React 구현 필요 |
| **지출결의서** | 법인카드/송금 선택 | 지출형식별 동적 UI | ❌ React 구현 필요 |
| **지출결의서** | 첨부파일 업로드 | GCS 연동 | ❌ API 엔드포인트 필요 |
| **지출결의서** | 거래처 검색 | 업체명 자동완성 | ❌ React 구현 필요 |
| **지출결의서** | 불러오기 | 이전 결의서 복사 | ❌ React 구현 필요 |
| **결재서명란** | 전통 도장식 테이블 | `_approval-stamp-table.blade.php` | ❌ React 구현 필요 |
| **증명서 PDF** | 4종 PDF 다운로드 | 재직/경력/위촉증명서, 사직서 | ❌ API PDF 엔드포인트 필요 |
| **삭제 권한** | `isDeletableBy()` | 관리자/슈퍼관리자 강제삭제, 일괄삭제 | ❌ API 엔드포인트 필요 |
| **뱃지** | 진행중 건수 | 기안함 뱃지 = 진행중 문서 수 | ✅ badge-counts 반영 |
| **완료함** | 미읽음 알림 | `drafter_read_at` 기반 | ✅ 반영됨 |
| **휴가 연동** | 결재→Leave 자동 변경 | 승인/반려/회수 시 Leave 상태 동기화 | ✅ 반영됨 |
### 4.2 MNG 전용 양식 목록 (15종+)
| 카테고리 | 양식명 | MNG Blade 파일 | PDF |
|---------|--------|---------------|-----|
| **근태** | 연차/반차/조퇴 신청 | `_leave-form.blade.php` | — |
| **지출** | 지출결의서 | `_expense-form.blade.php` | — |
| **품의** | 지출품의서 | `_purchase-request-form.blade.php` | — |
| **품의** | 계약체결품의서 | 상동 (5종 분기) | — |
| **품의** | 구매품의서 | 상동 | — |
| **품의** | 출장품의서 | 상동 | — |
| **품의** | 비용정산서 | 상동 | — |
| **증명** | 재직증명서 | `_certificate-form.blade.php` | ✅ |
| **증명** | 경력증명서 | `_career-cert-form.blade.php` | ✅ |
| **증명** | 위촉증명서 | `_appointment-cert-form.blade.php` | ✅ |
| **인사** | 사직서 | `_resignation-form.blade.php` | ✅ |
| **업무** | 사용인감계 | `_seal-usage-form.blade.php` | — |
| **업무** | 위임장 | `_delegation-form.blade.php` | — |
| **업무** | 이사회의사록 | `_board-minutes-form.blade.php` | — |
| **업무** | 견적서 | `_quotation-form.blade.php` | — |
| **업무** | 공문서 | `_official-letter-form.blade.php` | — |
| **근태** | 연차촉진통지서 1차 | `_leave-promotion-1st-form.blade.php` | — |
| **근태** | 연차촉진통지서 2차 | `_leave-promotion-2nd-form.blade.php` | — |
> 각 양식은 `_<name>-form.blade.php` (기안 작성)와 `_<name>-show.blade.php` (상세 조회) 쌍으로 구성
---
## 5. API 반영 현황
### 5.1 이미 반영 완료
| 항목 | 설명 |
|------|------|
| DB 스키마 | 마이그레이션 24개 — MNG와 완전 동기화 |
| 모델 상수/관계/헬퍼 | 6가지 문서 상태, 5가지 단계 상태, 3가지 step_type |
| 워크플로우 전체 | submit/approve/reject/cancel/hold/releaseHold/preDecide/copyForRedraft |
| 위임 CRUD | delegations 엔드포인트 4개 |
| 결재함 4종 + badge-counts | drafts/inbox/reference/completed + summary |
| FormRequest 검증 21개 | MNG보다 정교하게 분리 |
| Swagger 문서 | 3개 파일 (ApprovalApi, ApprovalFormApi, ApprovalLineApi) |
| Leave/Document 연동 | 결재 상태 변경 시 연관 엔티티 자동 동기화 |
| Observer 알림 | TodayIssue 발행 (ApprovalIssueObserver, ApprovalStepIssueObserver) |
| 테넌트 부트스트랩 | 새 테넌트 생성 시 기본 양식 5종 자동 시딩 |
### 5.2 API에 미반영 (React 화면 개발 시 필요)
| 항목 | 설명 | 난이도 | 비고 |
|------|------|--------|------|
| 🔴 전용 양식 렌더링 | 15종 양식별 폼 UI + 조회 UI | 대형 | MNG Blade를 React로 재구현 |
| 🔴 결재선 에디터 UI | 2패널 + 드래그앤드롭 + 결재/참조 분리 | 대형 | MNG의 `_approval-line-editor.blade.php` 참조 |
| 🟡 결재서명란 | 전통 도장식 결재 테이블 | 중형 | MNG의 `_approval-stamp-table.blade.php` 참조 |
| 🟡 양식 2단계 선택 | 분류 → 양식 드릴다운 | 중형 | |
| 🟡 증명서 PDF 출력 | API에서 DomPDF 렌더링 엔드포인트 | 중형 | MNG는 자체 처리 중 |
| 🟡 위임 결재 실제 동작 | `acted_by` 대결 처리 로직 | 중형 | Phase 3 |
| 🟡 병렬 결재 | `parallel_group` 활용 로직 | 중형 | Phase 3 |
| 🟢 첨부파일 업로드 | 결재 전용 업로드/다운로드 엔드포인트 | 소형 | 공통 File API 활용 가능 |
| 🟢 관리자 강제삭제 | 슈퍼관리자 권한 관리/일괄삭제 | 소형 | |
| 🟢 결재 통계/리포트 | 기간별/부서별/양식별 통계 | 소형 | Phase 4 |
---
## 6. 프로젝트별 역할 분담
```
MNG (관리자 화면 - Blade) API (REST 백엔드) React (사용자 화면)
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ ✅ 결재 전체 기능 │ │ ✅ 워크플로우 로직 │ │ ❌ 화면 미구현 │
│ ✅ 15종+ 양식 UI │ │ ✅ REST API 전체 │ │ │
│ ✅ 결재선 에디터 │ │ ✅ FormRequest 검증 │ │ React에서 구현 필요: │
│ ✅ 결재서명란 │ │ ✅ Swagger 문서 │ │ - 양식별 폼 UI │
│ ✅ PDF 출력 4종 │ │ ✅ Observer 알림 │ │ - 결재선 에디터 │
│ ✅ 첨부파일 GCS │ │ ✅ Leave/Doc 연동 │ │ - 결재서명란 │
│ ✅ 관리자 강제삭제 │ │ ✅ 테넌트 부트스트랩 │ │ - PDF 다운로드 │
│ ✅ 일괄삭제 │ │ │ │ - 파일 업로드 │
│ │ │ ❌ PDF 생성 엔드포인트│ │ │
│ │ │ ❌ 위임 대결 로직 │ │ │
│ │ │ ❌ 병렬 결재 로직 │ │ │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
가장 완성도 높음 백엔드 로직 완성 UI 구현 대기 중
```
---
## 7. React 구현 시 MNG 참조 파일 가이드
### 7.1 뷰 파일 구조 (`mng/resources/views/approvals/`)
```
approvals/
├── drafts.blade.php ← 기안함 목록
├── pending.blade.php ← 결재 대기함 목록
├── completed.blade.php ← 처리 완료함 목록
├── references.blade.php ← 참조함 목록
├── create.blade.php ← 기안 작성
├── edit.blade.php ← 기안 수정
├── show.blade.php ← 결재 상세 조회
├── partials/
│ ├── _approval-line-editor.blade.php ← 🔴 결재선 에디터 (핵심 UI)
│ ├── _approval-stamp-table.blade.php ← 🟡 결재서명란
│ ├── _status-badge.blade.php ← 상태 뱃지
│ ├── _step-progress.blade.php ← 결재 진행 현황
│ │
│ ├── _expense-form.blade.php ← 지출결의서 기안
│ ├── _expense-show.blade.php ← 지출결의서 조회
│ ├── _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 ← 사직서 조회
│ ├── _purchase-request-form.blade.php ← 품의서 기안 (5종)
│ ├── _purchase-request-show.blade.php ← 품의서 조회
│ ├── _seal-usage-form.blade.php ← 인감 기안
│ ├── _seal-usage-show.blade.php ← 인감 조회
│ ├── _delegation-form.blade.php ← 위임장 기안
│ ├── _delegation-show.blade.php ← 위임장 조회
│ ├── _board-minutes-form.blade.php ← 의사록 기안
│ ├── _board-minutes-show.blade.php ← 의사록 조회
│ ├── _quotation-form.blade.php ← 견적서 기안
│ ├── _quotation-show.blade.php ← 견적서 조회
│ ├── _official-letter-form.blade.php ← 공문서 기안
│ ├── _official-letter-show.blade.php ← 공문서 조회
│ ├── _leave-form.blade.php ← 근태신청 기안
│ ├── _leave-show.blade.php ← 근태신청 조회
│ ├── _leave-promotion-1st-form.blade.php ← 연차촉진 1차
│ ├── _leave-promotion-1st-show.blade.php
│ ├── _leave-promotion-2nd-form.blade.php ← 연차촉진 2차
│ └── _leave-promotion-2nd-show.blade.php
```
### 7.2 API 엔드포인트 참조
| MNG 엔드포인트 (내부 API) | 대응하는 API 엔드포인트 |
|--------------------------|----------------------|
| `POST /api/v1/approval-mgmt` | `POST /v1/approvals` |
| `GET /api/v1/approval-mgmt/{id}` | `GET /v1/approvals/{id}` |
| `PUT /api/v1/approval-mgmt/{id}` | `PATCH /v1/approvals/{id}` |
| `DELETE /api/v1/approval-mgmt/{id}` | `DELETE /v1/approvals/{id}` |
| `POST /api/v1/approval-mgmt/{id}/submit` | `POST /v1/approvals/{id}/submit` |
| `POST /api/v1/approval-mgmt/{id}/approve` | `POST /v1/approvals/{id}/approve` |
| `POST /api/v1/approval-mgmt/{id}/reject` | `POST /v1/approvals/{id}/reject` |
| `POST /api/v1/approval-mgmt/{id}/cancel` | `POST /v1/approvals/{id}/cancel` |
| `POST /api/v1/approval-mgmt/{id}/hold` | `POST /v1/approvals/{id}/hold` |
| `POST /api/v1/approval-mgmt/{id}/release-hold` | `POST /v1/approvals/{id}/release-hold` |
| `POST /api/v1/approval-mgmt/{id}/pre-decide` | `POST /v1/approvals/{id}/pre-decide` |
| `POST /api/v1/approval-mgmt/{id}/copy` | `POST /v1/approvals/{id}/copy` |
| `GET /api/v1/approval-mgmt/forms` | `GET /v1/approval-forms/active` |
| `GET /api/v1/approval-mgmt/lines` | `GET /v1/approval-lines` |
| `POST /api/v1/approval-mgmt/upload-file` | (공통 File API 사용) |
| `GET /api/v1/approval-mgmt/badge-counts` | `GET /v1/approvals/badge-counts` |
### 7.3 서비스 클래스 비교
| 기능 | MNG `ApprovalService` | API `ApprovalService` |
|------|----------------------|----------------------|
| 기안함 | `getMyDrafts()` | `drafts()` |
| 결재함 | `getPendingForMe()` | `inbox()` |
| 완료함 | `getCompletedByMe()` | `completed()` |
| 참조함 | `getReferencesForMe()` | `reference()` |
| 현황 카드 | — | `draftsSummary()`, `inboxSummary()`, `completedSummary()` |
| 생성 | `createApproval()` | `store()` |
| 수정 | `updateApproval()` | `update()` |
| 삭제 | `deleteApproval()`, `forceDeleteApproval()` | `destroy()` |
| 상신 | `submitApproval()` | `submit()` |
| 승인 | `approveApprovalStep()` | `approve()` |
| 반려 | `rejectApprovalStep()` | `reject()` |
| 회수 | `cancelApproval()` | `cancel()` |
| 보류 | `holdApproval()` | `hold()` |
| 보류해제 | `releaseApprovalHold()` | `releaseHold()` |
| 전결 | `preDecide()` (ApprovalApiController 내) | `preDecide()` |
| 복사재기안 | `copyForRedraft()` | `copyForRedraft()` |
| 결재선 CRUD | `createLine()`, `updateLine()`, `deleteLine()` | `lineStore()`, `lineUpdate()`, `lineDestroy()` |
| 양식 CRUD | — | `formStore()`, `formUpdate()`, `formDestroy()` |
| 위임 CRUD | — | `delegationStore()`, `delegationUpdate()`, `delegationDestroy()` |
| 휴가 연동 | `handleApprovalCompleted/Rejected/Deleted/Cancelled()` | 내부 처리 |
| PDF 생성 | 컨트롤러에서 DomPDF 직접 호출 | ❌ 미구현 |
---
## 8. 모델 동기화 주의사항
### 8.1 동기화가 필요한 시점
| 이벤트 | 조치 |
|--------|------|
| API에서 마이그레이션 추가 | MNG 모델의 `$fillable`, `$casts` 업데이트 |
| API 모델에 새 관계 추가 | MNG 모델에도 동일 관계 추가 |
| API 모델에 새 상수 추가 | MNG 모델에도 동일 상수 추가 |
| MNG에서 새 헬퍼 메서드 추가 | API 모델에 반영 여부 판단 (UI 전용이면 불필요) |
### 8.2 현재 차이점
| 항목 | MNG 모델 | API 모델 |
|------|---------|---------|
| Trait | `BelongsToTenant`, `SoftDeletes` | `BelongsToTenant`, `SoftDeletes`, `Auditable`, `ModelTrait` |
| 네임스페이스 | `App\Models\Approvals` | `App\Models\Tenants` |
| 감사 로깅 | — | `Auditable` trait으로 자동 기록 |
---
## 9. React 구현 우선순위 제안
### Phase A: 기본 결재 (필수)
1. 기안함/결재함/완료함/참조함 목록 화면
2. 기안 작성 (양식 선택 → 결재선 지정 → 본문 작성 → 상신)
3. 결재 상세 조회 + 승인/반려/회수 액션
4. 결재선 에디터 (2패널 + 결재/참조 분리)
5. badge-counts (사이드바 뱃지)
### Phase B: 양식 확장
1. 지출결의서 전용 폼 (법인카드/송금/첨부파일)
2. 품의서 5종
3. 근태신청 (휴가 연동)
4. 결재서명란
### Phase C: 증명서/업무 양식
1. 재직/경력/위촉증명서 + PDF 다운로드
2. 사직서, 인감, 위임장, 의사록, 견적서, 공문서
3. 연차촉진통지서
### Phase D: 고급 기능
1. 위임 대결 (Phase 3)
2. 병렬 결재 (Phase 3)
3. 결재 통계/리포트 (Phase 4)
---
## 관련 문서
- [결재관리 개요](README.md) — 시스템 전체 개요
- [양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조
- [워크플로우 상세](workflows.md) — 각 동작의 흐름
- [API 명세](api-reference.md) — 엔드포인트 목록
- [UI 화면 구성](ui-screens.md) — MNG 화면별 UI
- [DB 변경 및 모델 동기화](db-changes-and-model-sync.md) — 마이그레이션 이력
- [API 결재 연동 명세](../../frontend/api-specs/approval-api.md) — React용 API 명세
- [결재 통합 계획](../../dev/dev_plans/approval-system-unification-plan.md) — MNG→API 통합 계획
---
**최종 업데이트**: 2026-03-11

View File

@@ -1,419 +1,398 @@
# 바로빌(Barobill) 연동 시스템
> **작성일**: 2026-03-17
> **상태**: MNG 운영 중 / API SOAP 서비스 구축 완료 (2026-03-17)
---
## 1. 개요
### 1.1 목적
바로빌은 전자세금계산서, 계좌조회, 카드내역, 홈택스 연동, 카카오톡/SMS 발송 등을 제공하는 SOAP 기반 B2B 서비스다. SAM에서 재무/회계 데이터를 자동 수집하고 세금계산서를 발행하기 위해 연동한다.
### 1.2 현재 상태
| 항목 | MNG (백오피스) | API (서비스) | React (프론트) |
|------|:------------:|:----------:|:------------:|
| SOAP 연동 서비스 | ✅ 완료 (1,761줄) | **구축 완료** (49+7 메서드) | — |
| 회원사 관리 | ✅ 운영 중 | ✅ SOAP 등록/수정/상태 | 설정 페이지 |
| 카드 거래 조회 | ✅ 운영 중 | ✅ REST 16개 + SOAP 동기화 | — |
| 은행 거래 조회 | ✅ 운영 중 | ✅ REST 13개 + SOAP 동기화 | — |
| 홈택스 세금계산서 | ✅ 운영 중 | ✅ REST 13개 + upsert 동기화 | — |
| 카카오톡/SMS | ✅ 운영 중 | **SOAP 메서드 구현** (19개) | — |
| 과금 시스템 | ✅ 구현 완료 | — (모델만 존재) | — |
| 자동 동기화 | MNG 수동 | ✅ Queue Job + 스케줄러 | — |
> **현재 상태 (2026-03-17)**: API SOAP 서비스가 MNG와 100% 동등하게 구축되었다. MNG(본사 tenant_id=1)와 API(서비스 tenant_id=2~N)가 각각 독립적으로 SOAP 호출하며, 같은 DB(samdb)를 공유한다.
```
MNG (본사 전용, 수정 없음) API (서비스 고객용, 신규 구축 완료)
┌──────────────────────┐ ┌──────────────────────┐
│ BarobillService │ │ BarobillSoapService │
│ (1,761줄 그대로) │ │ (49+7 메서드) │
│ BankSyncService │ │ BankSyncService │
│ HometaxSyncService │ │ CardSyncService │
│ │ │ HometaxSyncService │
│ tenant_id=1 본사 │ │ tenant_id=2~N 고객 │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
└────────── 같은 DB (samdb) ────────────┘
```
### 1.3 바로빌 공식 자료
- 개발자 센터: `https://dev.barobill.co.kr/`
- 운영 WSDL: `https://ws.baroservice.com/`
- 테스트 WSDL: `https://testws.baroservice.com/`
---
## 2. 테스트 모드 vs 운영 모드
> **경고: 개발 시 반드시 테스트 모드를 사용한다. 운영 모드는 실제 과금이 발생한다.**
### 2.1 모드 비교
| 항목 | 테스트 모드 | 운영 모드 |
|------|-----------|----------|
| WSDL 엔드포인트 | `https://testws.baroservice.com/` | `https://ws.baroservice.com/` |
| CERTKEY | 테스트용 별도 발급 | 운영용 별도 발급 |
| 과금 | ❌ 무과금 | ✅ 실제 과금 |
| 데이터 | 테스트 데이터 (초기화 가능) | 실제 세금계산서/거래 데이터 |
| 국세청 전송 | ❌ 미전송 | ✅ 실제 전송 |
| 회원사 등록 | 테스트 서버에 등록 | 운영 서버에 등록 |
| 인증서 | 테스트 인증서 사용 가능 | 실제 공동인증서 필수 |
### 2.2 모드 전환 구조
```
┌───────────────────────────────────────────────────────┐
모드 결정 흐름
├───────────────────────────────────────────────────────┤
1. BarobillMember.server_mode
│ └─ 'test' 또는 'production' (회원사별 설정)
│ │
│ 2. BarobillService.switchServerMode(isTestMode) │
│ └─ SOAP 클라이언트 재초기화 │
│ └─ initializeConfig() 호출 │
│ │
│ 3. initializeConfig() │
│ ├─ DB 우선: BarobillConfig.getActive(isTestMode) │
│ │ └─ environment = 'test' | 'production' │
│ └─ .env 폴백: │
│ ├─ test → BAROBILL_CERT_KEY_TEST │
│ └─ prod → BAROBILL_CERT_KEY_PROD │
│ │
│ 4. SOAP URL 구성 │
│ ├─ test → testws.baroservice.com/* │
└─ prod → ws.baroservice.com/* │
│ │
└───────────────────────────────────────────────────────┘
```
### 2.3 설정 우선순위
1. **DB 설정** (`barobill_configs` 테이블) — 최우선
2. **.env 환경변수** — DB 설정 없을 때 폴백
```php
// BarobillService::initializeConfig()
$dbConfig = BarobillConfig::getActive($this->isTestMode);
if ($dbConfig) {
// DB에서 cert_key, corp_num, base_url 사용
} else {
// .env에서 BAROBILL_CERT_KEY_TEST/PROD, BAROBILL_CORP_NUM 사용
}
```
### 2.4 환경변수
```bash
# .env (MNG, API 동일)
BAROBILL_CERT_KEY_TEST=<테스트 인증키>
BAROBILL_CERT_KEY_PROD=<운영 인증키>
BAROBILL_CORP_NUM=<파트너 사업자번호>
BAROBILL_TEST_MODE=true # 기본값: 테스트 모드
```
### 2.5 개발 시 주의사항
```
✅ 로컬/개발 서버: BAROBILL_TEST_MODE=true (기본값)
✅ 운영 서버: BAROBILL_TEST_MODE=false + 운영 CERTKEY
✅ 회원사별 server_mode로 개별 전환 가능
❌ 테스트 CERTKEY로 운영 서버 호출 불가 (에러 -11102)
❌ 운영 모드에서 테스트 데이터 생성 금지 (실제 과금)
```
---
## 3. 아키텍처
### 3.1 전체 데이터 흐름
```
바로빌 SOAP API (ws.baroservice.com / testws.baroservice.com)
SOAP (6개 서비스) SOAP (6개 서비스)
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────────┐
MNG (BarobillService) │ │ API (BarobillSoapService)
본사 전용 (tenant_id=1) 서비스 고객 (tenant_id=2~N)
│ ├─ CORPSTATE 회원관리 │ │ ├─ CORPSTATE 회원관리 │
│ ├─ TI 세금계산서 │ │ ├─ TI 세금계산서
├─ BANKACCOUNT 계좌 │ │ ├─ BANKACCOUNT 계좌 │
│ ├─ CARD 카드 │ │ ├─ CARD 카드 │
├─ KAKAOTALK 알림톡 │ │ ├─ KAKAOTALK 알림톡
SMS 문자 │ │ └─ SMS 문자
└──────────┬───────────────────┘ │ + BankSyncService (동기화)
│ │ + CardSyncService (동기화)
│ │ + HometaxSyncService (동기화) │
│ + SyncBarobillDataJob (스케줄러)│
│ └──────────┬───────────────────────┘
│ │
└─────────── MySQL (samdb) ─────────┘
REST API (53개 엔드포인트)
┌────────────────────┐
│ React (사용자 UI) │
└────────────────────┘
```
### 3.2 SOAP 서비스 목록
| 서비스 | WSDL 경로 | 기능 |
|--------|----------|------|
| CORPSTATE | `/CORPSTATE.asmx` | 회원사 등록/조회/수정 |
| TI | `/TI.asmx` | 전자세금계산서 발행/조회 |
| BANKACCOUNT | `/BANKACCOUNT.asmx` | 계좌 등록/입출금 내역 조회 |
| CARD | `/CARD.asmx` | 카드 등록/사용내역 조회 |
| KAKAOTALK | `/KAKAOTALK.asmx` | 카카오톡 알림톡 발송 |
| SMS | `/SMS.asmx` | 문자 메시지 발송 |
### 3.3 인증 구조
```
모든 API 호출
└─ CERTKEY (파트너 인증키) — 필수 파라미터
├─ 바로빌 파트너 계약 시 발급
├─ 테스트/운영 별도 키
└─ BarobillService.call()에서 자동 주입
```
---
## 4. 과금 정책
### 4.1 바로빌 과금 구조 (SAM 내부 정책)
| 서비스 | 월정액 | 비고 |
|--------|-------|------|
| 계좌조회 (`bank_account`) | 10,000원/월 | 테넌트별 |
| 카드내역 (`card`) | 10,000원/월 | 테넌트별 |
| 홈택스 매입/매출 (`hometax`) | 0원 | 본사 부담 (무료 제공) |
### 4.2 추가 과금 (건별)
`BarobillPricingPolicy` 모델 기반:
| 서비스 | 무료 기본량 | 추가 과금 단위 | 추가 금액 |
|--------|-----------|-------------|----------|
| 법인카드 등록 (`card`) | 정책 설정값 | 정책 설정값 | 정책 설정값 |
| 계산서 발행 (`tax_invoice`) | 정책 설정값 | 건당 | 정책 설정값 |
| 계좌조회 수집 (`bank_account`) | 정책 설정값 | 정책 설정값 | 정책 설정값 |
> 과금 계산: `BarobillPricingPolicy::calculateBilling(usageCount)` — 무료 제공량 초과분만 과금
### 4.3 과금 처리 흐름
```
매월 1일 (배치)
└─ BarobillBillingService::processMonthlyBilling()
├─ 활성 구독 조회 (BarobillSubscription::active())
├─ 이미 과금된 기록 중복 방지
├─ BarobillBillingRecord 생성 (subscription 타입)
└─ BarobillMonthlySummary 갱신
건별 발생 시
└─ BarobillBillingService::recordUsage()
├─ BarobillBillingRecord 생성 (usage 타입)
└─ BarobillMonthlySummary 갱신
```
### 4.4 테스트 모드에서의 과금
```
✅ 테스트 모드: 바로빌 API 호출에 대한 바로빌 측 과금 없음
✅ SAM 내부 과금 시스템은 모드와 무관하게 기록 가능 (테스트용)
❌ 운영 모드: 바로빌 측 실제 과금 발생 (충전잔액 차감)
```
---
## 5. 멀티테넌트 처리
### 5.1 데이터 격리
모든 바로빌 테이블은 `tenant_id` 컬럼으로 데이터를 격리한다.
```
tenant_id=1 (코드브릿지엑스) → 본사 실무 데이터
tenant_id=N (고객사) → 해당 고객사 데이터만 접근
```
### 5.2 회원사별 설정
각 테넌트는 `barobill_members` 테이블에 독립된 바로빌 회원사 정보를 가진다:
| 컬럼 | 설명 |
|------|------|
| `tenant_id` | 테넌트 FK |
| `biz_no` | 사업자번호 (UNIQUE with tenant_id) |
| `barobill_id` | 바로빌 로그인 ID |
| `barobill_pwd` | 바로빌 비밀번호 (Laravel Encryption) |
| `server_mode` | `test` 또는 `production` (회원사별 전환) |
| `status` | `active` / `inactive` / `pending` |
### 5.3 서비스 이관 시 고려사항
```
🔴 필수: 테넌트별 CERTKEY 관리 방안 (현재는 전역 1개)
🔴 필수: 테넌트 온보딩 시 바로빌 회원 자동 등록 플로우
🟡 중요: 테스트→운영 모드 전환 프로세스 정의
🟡 중요: 과금 정책을 테넌트별로 다르게 적용 가능하도록 확장
🟢 권장: 바로빌 API 호출 로그/모니터링
```
---
## 6. 프로젝트별 코드 위치
### 6.1 MNG (`/home/aweso/sam/mng`)
| 유형 | 경로 |
|------|------|
| 서비스 | `app/Services/Barobill/BarobillService.php` (1,761줄, 핵심) |
| 서비스 | `app/Services/Barobill/HometaxSyncService.php` |
| 서비스 | `app/Services/Barobill/BarobillBillingService.php` |
| 서비스 | `app/Services/Barobill/BarobillUsageService.php` |
| 서비스 | `app/Services/Barobill/BarobillBankSyncService.php` |
| 모델 | `app/Models/Barobill/` (18개 모델) |
| 컨트롤러 | `app/Http/Controllers/Barobill/` (7개) |
| Admin API | `app/Http/Controllers/Api/Admin/Barobill/` (7개) |
| 뷰 | `resources/views/barobill/` (10개 페이지) |
### 6.2 API (`/home/aweso/sam/api`)
| 유형 | 경로 | 설명 |
|------|------|------|
| **SOAP 서비스** | `app/Services/Barobill/BarobillSoapService.php` | 6개 SOAP 서비스, 49+7 메서드 |
| **동기화** | `app/Services/Barobill/BarobillBankSyncService.php` | 은행 거래 SOAP→DB 동기화 |
| **동기화** | `app/Services/Barobill/BarobillCardSyncService.php` | 카드 거래 SOAP→DB 동기화 |
| **동기화** | `app/Services/Barobill/HometaxSyncService.php` | 홈택스 세금계산서 upsert |
| **기존 서비스** | `app/Services/BarobillService.php` | REST 설정/세금계산서 발행 (기존) |
| **Queue Job** | `app/Jobs/Barobill/SyncBarobillDataJob.php` | 자동 동기화 스케줄러 Job |
| **컨트롤러** | `app/Http/Controllers/Api/V1/BarobillSyncController.php` | SOAP 동기화/회원/인증서 API |
| **컨트롤러** | `app/Http/Controllers/Api/V1/BarobillController.php` | 기존 설정/URL API (보강됨) |
| 모델 | `app/Models/Barobill/` (15개) | 기존 모델 |
| 라우트 | `routes/api/v1/finance.php` | 기존 42개 + 신규 11개 |
| 스케줄러 | `routes/console.php` | 06:00 은행, 06:30 카드 동기화 |
### 6.3 React (`/home/aweso/sam/react`)
| 유형 | 경로 |
|------|------|
| 컴포넌트 | `src/components/settings/BarobillIntegration/` |
| 페이지 | `/settings/barobill-integration` |
---
## 7. DB 테이블 구조
### 7.1 테이블 목록
| 테이블 | 용도 | 마이그레이션 위치 |
|--------|------|-----------------|
| `barobill_members` | 회원사 정보 | API |
| `barobill_configs` | API 설정 (test/prod 분리) | API |
| `barobill_settings` | 테넌트별 서비스 설정 | API |
| `barobill_subscriptions` | 월정액 구독 | API |
| `barobill_billing_records` | 과금 기록 | API |
| `barobill_monthly_summaries` | 월별 과금 요약 | API |
| `barobill_pricing_policies` | 요금 정책 | API |
| `hometax_invoices` | 홈택스 세금계산서 | API |
| `hometax_invoice_journals` | 세금계산서 분개 | API |
| `barobill_bank_transactions` | 은행 거래 내역 | API |
| `barobill_bank_transaction_overrides` | 은행 적요 수정 | API |
| `barobill_bank_transaction_splits` | 은행 거래 분할 | API |
| `barobill_bank_sync_status` | 은행 동기화 상태 | API |
| `barobill_card_transactions` | 카드 거래 내역 | API |
| `barobill_card_transaction_splits` | 카드 거래 분할 | API |
| `barobill_card_transaction_amount_logs` | 카드 금액 수정 로그 | API |
| `barobill_card_transaction_hides` | 카드 거래 숨김 | API |
| `account_codes` | 계정과목 마스터 | API |
### 7.2 핵심 테이블 스키마
**barobill_members**:
- `tenant_id` + `biz_no` UNIQUE
- `server_mode`: `test` | `production`
- `barobill_pwd`: Laravel Encryption (복호화 가능, API 호출 시 필요)
**hometax_invoices**:
- `tenant_id` + `nts_confirm_num` + `invoice_type` UNIQUE
- `invoice_type`: `sales` | `purchase`
- `tax_type`: 1=과세, 2=영세, 3=면세
- `issue_type`: 1=정발행, 2=역발행
---
## 8. 에러 코드 매핑
| 코드 | 의미 | 대응 |
|------|------|------|
| -11101 | 사업자번호 미설정/유효하지 않음 | 회원사 정보 확인 |
| -11102 | CERTKEY 유효하지 않음 | 테스트/운영 키 확인 |
| -11103 | 인증서 만료/유효하지 않음 | 공동인증서 갱신 |
| -11104 | 미등록 사업자 | 회원사 등록 먼저 |
| -11105 | 이미 등록된 사업자 | 중복 등록 방지 |
| -26001 | 공동인증서 미등록 | 인증서 등록 안내 |
| -32001 | 사업자번호 형식 오류 | 10자리 숫자 확인 |
| -32010 | 이미 등록된 사업자번호 | 기존 회원 확인 |
| -32011 | 이미 등록된 아이디 | 다른 아이디 사용 |
---
## 9. 서비스 이관 계획
### 9.1 이관 현황 (2026-03-17 기준)
| 기능 | MNG | API 이관 | React | 상태 |
|------|:---:|:-------:|:-----:|:----:|
| SOAP 연동 서비스 (6개) | ✅ | ✅ **완료** | — | 49 메서드 100% |
| 회원사 등록/관리 | ✅ | ✅ **완료** | 설정 페이지 | API 3개 엔드포인트 |
| 테스트/운영 모드 전환 | ✅ | ✅ **완료** | — | initForMember() |
| 은행 거래 동기화 | ✅ | ✅ **완료** | — | BankSyncService |
| 카드 거래 동기화 | ✅ | ✅ **완료** | — | CardSyncService |
| 홈택스 동기화 | ✅ | ✅ **완료** | — | HometaxSyncService |
| 자동 동기화 스케줄러 | — | ✅ **완료** | — | 06:00/06:30 Job |
| 카카오톡 (15개) | ✅ | ✅ **완료** | — | SOAP 메서드 |
| SMS (4개) | ✅ | ✅ **완료** | — | SOAP 메서드 |
| 세금계산서 발행 | ✅ | ✅ 기존 REST | — | BarobillService |
| 과금 시스템 | ✅ | 모델만 | — | **미이관** |
| 카카오톡/SMS API 엔드포인트 | ✅ | — | — | **미구현** (SOAP만) |
| 온보딩 대시보드 (MNG) | — | — | — | **기획 중** |
### 9.2 남은 과제
| 우선순위 | 과제 | 설명 |
|---------|------|------|
| 🔴 P1 | 온보딩 실제 테스트 | 테스트 테넌트로 풀 시나리오 검증 |
| 🔴 P1 | 카카오톡/SMS REST API 엔드포인트 | SOAP 메서드는 구현됨, Controller+Route 추가 필요 |
| 🟡 P2 | React 바로빌 관리 화면 | 동기화 트리거, 인증서/계좌/카드 관리 |
| 🟡 P2 | MNG 온보딩 대시보드 | 테넌트별 연동 진행 상태 시각화 |
| 🟢 P3 | 과금 시스템 API 이관 | BillingService, UsageService |
| 🟢 P3 | 인증서 만료/잔액 부족 알림 | 스케줄러 기반 자동 알림 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [테넌트 온보딩](./tenant-onboarding.md) | 온보딩 개념 정의, 테스트→운영 전환 프로세스 |
| [API SOAP 기술 참조](./api-soap-reference.md) | API BarobillSoapService 전체 메서드, 동기화, 스케줄러 |
| [온보딩 실행 가이드](../../guides/barobill-onboarding-guide.md) | 7단계 실행 절차, API 예시, 트러블슈팅, 고객 안내 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | 카드/은행/홈택스 REST API 42개 엔드포인트 |
| [바로빌 회원 마이그레이션](../../dev/guides/barobill-members-migration.md) | 회원 데이터 이관 가이드 |
| [바로빌 카카오톡](../barobill-kakaotalk/README.md) | 카카오톡 알림톡 연동 |
| [바로빌 출시 계획](../../dev/dev_plans/barobill-service-launch-plan.md) | 4단계 출시 로드맵 |
| [재무관리](../finance/README.md) | 재무/자금관리 전체 개요 |
---
**최종 업데이트**: 2026-03-17
# 바로빌(Barobill) 연동 시스템
> **작성일**: 2026-03-17
> **상태**: MNG 운영 중 / 서비스 이관 준비
---
## 1. 개요
### 1.1 목적
바로빌은 전자세금계산서, 계좌조회, 카드내역, 홈택스 연동, 카카오톡/SMS 발송 등을 제공하는 SOAP 기반 B2B 서비스다. SAM에서 재무/회계 데이터를 자동 수집하고 세금계산서를 발행하기 위해 연동한다.
### 1.2 현재 상태
| 항목 | MNG (백오피스) | API (서비스) | React (프론트) |
|------|:------------:|:----------:|:------------:|
| SOAP 연동 서비스 | ✅ 완료 (1,761줄) | 기본 설정만 | — |
| 회원사 관리 | ✅ 운영 중 | 모델만 존재 | 설정 페이지 |
| 카드 거래 조회 | ✅ 운영 중 | ✅ REST API 16개 | — |
| 은행 거래 조회 | ✅ 운영 중 | ✅ REST API 13개 | — |
| 홈택스 세금계산서 | ✅ 운영 중 | ✅ REST API 13개 | — |
| 카카오톡/SMS | ✅ 운영 중 | | — |
| 과금 시스템 | ✅ 구현 완료 | — | — |
> **핵심**: tenant_id=1 (코드브릿지엑스 본사)에서 실무 운영 중. 서비스 이관 시 멀티테넌트 SOAP 연동이 핵심 과제.
### 1.3 바로빌 공식 자료
- 개발자 센터: `https://dev.barobill.co.kr/`
- 운영 WSDL: `https://ws.baroservice.com/`
- 테스트 WSDL: `https://testws.baroservice.com/`
---
## 2. 테스트 모드 vs 운영 모드
> **경고: 개발 시 반드시 테스트 모드를 사용한다. 운영 모드는 실제 과금이 발생한다.**
### 2.1 모드 비교
| 항목 | 테스트 모드 | 운영 모드 |
|------|-----------|----------|
| WSDL 엔드포인트 | `https://testws.baroservice.com/` | `https://ws.baroservice.com/` |
| CERTKEY | 테스트용 별도 발급 | 운영용 별도 발급 |
| 과금 | ❌ 무과금 | ✅ 실제 과금 |
| 데이터 | 테스트 데이터 (초기화 가능) | 실제 세금계산서/거래 데이터 |
| 국세청 전송 | ❌ 미전송 | ✅ 실제 전송 |
| 회원사 등록 | 테스트 서버에 등록 | 운영 서버에 등록 |
| 인증서 | 테스트 인증서 사용 가능 | 실제 공동인증서 필수 |
### 2.2 모드 전환 구조
```
┌───────────────────────────────────────────────────────┐
│ 모드 결정 흐름 │
├───────────────────────────────────────────────────────┤
│ │
│ 1. BarobillMember.server_mode │
│ └─ 'test' 또는 'production' (회원사별 설정) │
│ │
│ 2. BarobillService.switchServerMode(isTestMode) │
│ └─ SOAP 클라이언트 재초기화 │
│ └─ initializeConfig() 호출 │
│ │
│ 3. initializeConfig() │
│ ├─ DB 우선: BarobillConfig.getActive(isTestMode) │
│ │ └─ environment = 'test' | 'production' │
│ └─ .env 폴백: │
│ ├─ test → BAROBILL_CERT_KEY_TEST │
└─ prod → BAROBILL_CERT_KEY_PROD
│ │
4. SOAP URL 구성
├─ test → testws.baroservice.com/*
│ └─ prod → ws.baroservice.com/*
│ │
└───────────────────────────────────────────────────────┘
```
### 2.3 설정 우선순위
1. **DB 설정** (`barobill_configs` 테이블) — 최우선
2. **.env 환경변수** — DB 설정 없을 때 폴백
```php
// BarobillService::initializeConfig()
$dbConfig = BarobillConfig::getActive($this->isTestMode);
if ($dbConfig) {
// DB에서 cert_key, corp_num, base_url 사용
} else {
// .env에서 BAROBILL_CERT_KEY_TEST/PROD, BAROBILL_CORP_NUM 사용
}
```
### 2.4 환경변수
```bash
# .env (MNG, API 동일)
BAROBILL_CERT_KEY_TEST=<테스트 인증키>
BAROBILL_CERT_KEY_PROD=<운영 인증키>
BAROBILL_CORP_NUM=<파트너 사업자번호>
BAROBILL_TEST_MODE=true # 기본값: 테스트 모드
```
### 2.5 개발 시 주의사항
```
✅ 로컬/개발 서버: BAROBILL_TEST_MODE=true (기본값)
✅ 운영 서버: BAROBILL_TEST_MODE=false + 운영 CERTKEY
✅ 회원사별 server_mode로 개별 전환 가능
❌ 테스트 CERTKEY로 운영 서버 호출 불가 (에러 -11102)
❌ 운영 모드에서 테스트 데이터 생성 금지 (실제 과금)
```
---
## 3. 아키텍처
### 3.1 전체 데이터 흐름
```
바로빌 SOAP API (ws.baroservice.com)
│ SOAP (6개 서비스)
┌─────────────────────────────────┐
│ MNG (BarobillService) │
│ ├─ CORPSTATE — 회원사 관리 │
│ ├─ TI — 전자세금계산서 │
│ ├─ BANKACCOUNT — 계좌조회 │
│ ├─ CARD — 카드조회 │
│ ├─ KAKAOTALK — 알림톡 │
│ └─ SMS — 문자 발송 │
└──────────┬──────────────────────┘
│ MySQL 저장
┌─────────────────────────────────┐
│ MySQL (samdb)
│ ├─ barobill_members │
├─ barobill_card_transactions │
│ ├─ barobill_bank_transactions │
├─ hometax_invoices
└─ (18개 테이블)
└──────────┬──────────────────────┘
REST API
┌─────────────────────────────────┐
API (42개 엔드포인트)
/api/v1/barobill-card-*
│ ├─ /api/v1/barobill-bank-*
│ └─ /api/v1/hometax-invoices/*
└──────────┬──────────────────────┘
┌─────────────────────────────────┐
React (사용자 UI) │
│ └─ BarobillIntegration 컴포넌트
└─────────────────────────────────┘
```
### 3.2 SOAP 서비스 목록
| 서비스 | WSDL 경로 | 기능 |
|--------|----------|------|
| CORPSTATE | `/CORPSTATE.asmx` | 회원사 등록/조회/수정 |
| TI | `/TI.asmx` | 전자세금계산서 발행/조회 |
| BANKACCOUNT | `/BANKACCOUNT.asmx` | 계좌 등록/입출금 내역 조회 |
| CARD | `/CARD.asmx` | 카드 등록/사용내역 조회 |
| KAKAOTALK | `/KAKAOTALK.asmx` | 카카오톡 알림톡 발송 |
| SMS | `/SMS.asmx` | 문자 메시지 발송 |
### 3.3 인증 구조
```
모든 API 호출
└─ CERTKEY (파트너 인증키) — 필수 파라미터
├─ 바로빌 파트너 계약 시 발급
├─ 테스트/운영 별도 키
└─ BarobillService.call()에서 자동 주입
```
---
## 4. 과금 정책
### 4.1 바로빌 과금 구조 (SAM 내부 정책)
| 서비스 | 월정액 | 비고 |
|--------|-------|------|
| 계좌조회 (`bank_account`) | 10,000원/월 | 테넌트별 |
| 카드내역 (`card`) | 10,000원/월 | 테넌트별 |
| 홈택스 매입/매출 (`hometax`) | 0원 | 본사 부담 (무료 제공) |
### 4.2 추가 과금 (건별)
`BarobillPricingPolicy` 모델 기반:
| 서비스 | 무료 기본량 | 추가 과금 단위 | 추가 금액 |
|--------|-----------|-------------|----------|
| 법인카드 등록 (`card`) | 정책 설정값 | 정책 설정값 | 정책 설정값 |
| 계산서 발행 (`tax_invoice`) | 정책 설정값 | 건당 | 정책 설정값 |
| 계좌조회 수집 (`bank_account`) | 정책 설정값 | 정책 설정값 | 정책 설정값 |
> 과금 계산: `BarobillPricingPolicy::calculateBilling(usageCount)` — 무료 제공량 초과분만 과금
### 4.3 과금 처리 흐름
```
매월 1일 (배치)
└─ BarobillBillingService::processMonthlyBilling()
├─ 활성 구독 조회 (BarobillSubscription::active())
├─ 이미 과금된 기록 중복 방지
├─ BarobillBillingRecord 생성 (subscription 타입)
└─ BarobillMonthlySummary 갱신
건별 발생 시
└─ BarobillBillingService::recordUsage()
├─ BarobillBillingRecord 생성 (usage 타입)
└─ BarobillMonthlySummary 갱신
```
### 4.4 테스트 모드에서의 과금
```
✅ 테스트 모드: 바로빌 API 호출에 대한 바로빌 측 과금 없음
✅ SAM 내부 과금 시스템은 모드와 무관하게 기록 가능 (테스트용)
❌ 운영 모드: 바로빌 측 실제 과금 발생 (충전잔액 차감)
```
---
## 5. 멀티테넌트 처리
### 5.1 데이터 격리
모든 바로빌 테이블은 `tenant_id` 컬럼으로 데이터를 격리한다.
```
tenant_id=1 (코드브릿지엑스) → 본사 실무 데이터
tenant_id=N (고객사) → 해당 고객사 데이터만 접근
```
### 5.2 회원사별 설정
각 테넌트는 `barobill_members` 테이블에 독립된 바로빌 회원사 정보를 가진다:
| 컬럼 | 설명 |
|------|------|
| `tenant_id` | 테넌트 FK |
| `biz_no` | 사업자번호 (UNIQUE with tenant_id) |
| `barobill_id` | 바로빌 로그인 ID |
| `barobill_pwd` | 바로빌 비밀번호 (Laravel Encryption) |
| `server_mode` | `test` 또는 `production` (회원사별 전환) |
| `status` | `active` / `inactive` / `pending` |
### 5.3 서비스 이관 시 고려사항
```
🔴 필수: 테넌트별 CERTKEY 관리 방안 (현재는 전역 1개)
🔴 필수: 테넌트 온보딩 시 바로빌 회원 자동 등록 플로우
🟡 중요: 테스트→운영 모드 전환 프로세스 정의
🟡 중요: 과금 정책을 테넌트별로 다르게 적용 가능하도록 확장
🟢 권장: 바로빌 API 호출 로그/모니터링
```
---
## 6. 프로젝트별 코드 위치
### 6.1 MNG (`/home/aweso/sam/mng`)
| 유형 | 경로 |
|------|------|
| 서비스 | `app/Services/Barobill/BarobillService.php` (1,761줄, 핵심) |
| 서비스 | `app/Services/Barobill/HometaxSyncService.php` |
| 서비스 | `app/Services/Barobill/BarobillBillingService.php` |
| 서비스 | `app/Services/Barobill/BarobillUsageService.php` |
| 서비스 | `app/Services/Barobill/BarobillBankSyncService.php` |
| 모델 | `app/Models/Barobill/` (18개 모델) |
| 컨트롤러 | `app/Http/Controllers/Barobill/` (7개) |
| Admin API | `app/Http/Controllers/Api/Admin/Barobill/` (7개) |
| | `resources/views/barobill/` (10개 페이지) |
### 6.2 API (`/home/aweso/sam/api`)
| 유형 | 경로 |
|------|------|
| 서비스 | `app/Services/BarobillService.php` (기본 설정만) |
| 모델 | `app/Models/Barobill/` (15개) |
| 모델 | `app/Models/Tenants/BarobillSetting.php` |
| 컨트롤러 | `app/Http/Controllers/Api/V1/Barobill*Controller.php` |
| 마이그레이션 | `database/migrations/` (19개 바로빌 관련) |
### 6.3 React (`/home/aweso/sam/react`)
| 유형 | 경로 |
|------|------|
| 컴포넌트 | `src/components/settings/BarobillIntegration/` |
| 페이지 | `/settings/barobill-integration` |
---
## 7. DB 테이블 구조
### 7.1 테이블 목록
| 테이블 | 용도 | 마이그레이션 위치 |
|--------|------|-----------------|
| `barobill_members` | 회원사 정보 | API |
| `barobill_configs` | API 설정 (test/prod 분리) | API |
| `barobill_settings` | 테넌트별 서비스 설정 | API |
| `barobill_subscriptions` | 월정액 구독 | API |
| `barobill_billing_records` | 과금 기록 | API |
| `barobill_monthly_summaries` | 월별 과금 요약 | API |
| `barobill_pricing_policies` | 요금 정책 | API |
| `hometax_invoices` | 홈택스 세금계산서 | API |
| `hometax_invoice_journals` | 세금계산서 분개 | API |
| `barobill_bank_transactions` | 은행 거래 내역 | API |
| `barobill_bank_transaction_overrides` | 은행 적요 수정 | API |
| `barobill_bank_transaction_splits` | 은행 거래 분할 | API |
| `barobill_bank_sync_status` | 은행 동기화 상태 | API |
| `barobill_card_transactions` | 카드 거래 내역 | API |
| `barobill_card_transaction_splits` | 카드 거래 분할 | API |
| `barobill_card_transaction_amount_logs` | 카드 금액 수정 로그 | API |
| `barobill_card_transaction_hides` | 카드 거래 숨김 | API |
| `account_codes` | 계정과목 마스터 | API |
### 7.2 핵심 테이블 스키마
**barobill_members**:
- `tenant_id` + `biz_no` UNIQUE
- `server_mode`: `test` | `production`
- `barobill_pwd`: Laravel Encryption (복호화 가능, API 호출 시 필요)
**hometax_invoices**:
- `tenant_id` + `nts_confirm_num` + `invoice_type` UNIQUE
- `invoice_type`: `sales` | `purchase`
- `tax_type`: 1=과세, 2=영세, 3=면세
- `issue_type`: 1=정발행, 2=역발행
---
## 8. 에러 코드 매핑
| 코드 | 의미 | 대응 |
|------|------|------|
| -11101 | 사업자번호 미설정/유효하지 않음 | 회원사 정보 확인 |
| -11102 | CERTKEY 유효하지 않음 | 테스트/운영 키 확인 |
| -11103 | 인증서 만료/유효하지 않음 | 공동인증서 갱신 |
| -11104 | 미등록 사업자 | 회원사 등록 먼저 |
| -11105 | 이미 등록된 사업자 | 중복 등록 방지 |
| -26001 | 공동인증서 미등록 | 인증서 등록 안내 |
| -32001 | 사업자번호 형식 오류 | 10자리 숫자 확인 |
| -32010 | 이미 등록된 사업자번호 | 기존 회원 확인 |
| -32011 | 이미 등록된 아이디 | 다른 아이디 사용 |
---
## 9. 서비스 이관 계획
### 9.1 이관 범위
| 기능 | MNG (현재) | API 이관 | React 이관 | 우선순위 |
|------|:---------:|:-------:|:---------:|:-------:|
| SOAP 연동 서비스 | ✅ | 🔴 필수 | — | P1 |
| 회원사 등록/관리 | ✅ | 🔴 필수 | 🔴 필수 | P1 |
| 테스트/운영 모드 전환 | ✅ | 🔴 필수 | 🟡 관리자 | P1 |
| 카드 거래 동기화 | ✅ | 🟡 중요 | ✅ 완료 | P2 |
| 은행 거래 동기화 | ✅ | 🟡 중요 | — | P2 |
| 홈택스 동기화 | ✅ | 🟡 중요 | — | P2 |
| 세금계산서 발행 | ✅ | 🟡 중요 | 🟡 중요 | P2 |
| 과금 시스템 | ✅ | 🟢 권장 | 🟢 권장 | P3 |
| 카카오톡/SMS | ✅ | 🟢 권장 | — | P3 |
### 9.2 이관 시 핵심 과제
1. **SOAP 서비스 이관**: MNG의 `BarobillService` (1,761줄)를 API로 이동
2. **멀티테넌트 CERTKEY**: 테넌트별로 바로빌 파트너 계약이 필요한지, 공용 CERTKEY로 처리 가능한지 확인
3. **테스트 모드 관리**: 신규 테넌트는 테스트 모드로 시작 → 관리자가 운영 모드로 전환
4. **동기화 스케줄러**: MNG에서 실행 중인 은행/카드/홈택스 동기화를 API Queue로 이관
5. **인증서 관리**: 공동인증서 등록 URL을 테넌트 사용자에게 제공하는 플로우
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [테넌트 온보딩](./tenant-onboarding.md) | 온보딩 개념 정의, 테스트→운영 전환 프로세스 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | 카드/은행/홈택스 REST API 42개 엔드포인트 |
| [바로빌 회원 마이그레이션](../../dev/guides/barobill-members-migration.md) | 회원 데이터 이관 가이드 |
| [바로빌 카카오톡](../barobill-kakaotalk/README.md) | 카카오톡 알림톡 연동 |
| [재무관리](../finance/README.md) | 재무/자금관리 전체 개요 |
---
**최종 업데이트**: 2026-03-17

View File

@@ -1,236 +1,190 @@
# 바로빌 테넌트 온보딩 프로세스
> **작성일**: 2026-03-17
> **상태**: 설계 중 (서비스 이관 준비)
---
## 1. 온보딩(Onboarding)이란
### 1.1 정의
**온보딩**: 새로운 고객(테넌트)이 서비스에 가입하여 실제 사용을 시작하기까지의 초기 설정 과정.
SAM 바로빌 맥락에서는 **정식 계약 고객이 바로빌 연동 기능(계좌조회, 카드내역, 세금계산서 등)을 실무에서 사용할 수 있도록 초기 세팅하는 일련의 절차**를 의미한다.
### 1.2 온보딩 vs 베타테스트
| 구분 | 온보딩 | 베타테스트 |
|------|--------|----------|
| **대상** | 정식 계약 고객 | 서비스 출시 전 검증 참여자 |
| **목적** | 고객이 기능을 쓸 수 있게 초기 세팅 | 서비스 안정성/기능 검증 |
| **시점** | 고객 가입할 때마다 반복 발생 | 서비스 출시 전 1회성 |
| **테스트 모드** | 초기 세팅 확인용으로 잠깐 사용 가능 | 전체 기간 테스트 모드로 운영 |
| **데이터** | 실제 업무 데이터 | 검증용 테스트 데이터 |
| **과금** | 정식 과금 (운영 모드 전환 후) | 무과금 |
### 1.3 단계별 관계
서비스 이관 완료 후 다음 순서로 진행한다:
```
서비스 이관 (개발)
└─ 베타테스트 (출시 전 검증)
└─ 정식 출시
└─ 테넌트 온보딩 (고객 가입 시마다 반복)
```
---
## 2. 바로빌 온보딩 전체 흐름
### 2.1 단계별 프로세스
```
Phase 1: 테넌트 계약
┌──────────────────────────────────────────┐
│ 고객사 SAM 서비스 계약 체결 │
│ └─ SAM 테넌트 생성 (tenant_id 발급) │
│ └─ 바로빌 서비스 이용 여부 확인 │
└──────────────────────┬───────────────────┘
Phase 2: 바로빌 회원 등록 (테스트 모드)
┌──────────────────────────────────────────┐
│ 바로빌 회원사 등록 (BarobillService) │
│ ├─ 사업자번호, 상호, 대표자 등록 │
│ ├─ 바로빌 ID/PW 생성 │
│ ├─ server_mode = 'test' (기본값) │
│ └─ 테스트 서버에서 연동 확인 │
└──────────────────────┬───────────────────┘
Phase 3: 인증서 및 계좌/카드 연결
┌──────────────────────────────────────────┐
│ 공동인증서 등록 │
│ ├─ getCertificateRegistUrl() → 고객 직접│
│ ├─ 인증서 유효성 확인 │
│ └─ 인증서 만료일 모니터링 설정 │
│ │
│ 계좌 연결 │
│ ├─ getBankAccountScrapRequestUrl() │
│ └─ 고객이 직접 계좌 등록 │
│ │
│ 카드 연결 │
│ ├─ registCard() │
│ └─ 카드사별 등록 │
└──────────────────────┬───────────────────┘
Phase 4: 연동 검증
┌──────────────────────────────────────────┐
│ 테스트 모드에서 기능 확인 │
│ ├─ 계좌 입출금 내역 조회 확인 │
│ ├─ 카드 사용내역 조회 확인 │
│ ├─ 홈택스 세금계산서 수집 확인 │
│ └─ 문제 없으면 다음 단계 │
└──────────────────────┬───────────────────┘
Phase 5: 운영 모드 전환
┌──────────────────────────────────────────┐
│ 관리자가 server_mode → 'production' 전환 │
│ ├─ 운영 CERTKEY로 SOAP 재연결 │
│ ├─ 실제 데이터 수집 시작 │
│ └─ 과금 시작 (구독 등록) │
└──────────────────────┬───────────────────┘
Phase 6: 실무 사용 시작
┌──────────────────────────────────────────┐
│ 정기 동기화 스케줄러 활성화 │
│ ├─ 은행 거래 자동 수집 │
│ ├─ 카드 내역 자동 수집 │
│ ├─ 홈택스 세금계산서 자동 수집 │
│ └─ 월정액 과금 자동 처리 │
└──────────────────────────────────────────┘
```
### 2.2 역할 분담
| 단계 | 수행 주체 | 설명 |
|------|----------|------|
| 테넌트 생성 | SAM 관리자 | MNG에서 테넌트 생성 |
| 회원사 등록 | SAM 관리자 또는 고객 | 사업자 정보 입력 |
| 인증서 등록 | **고객 직접** | 바로빌 제공 URL에서 직접 등록 |
| 계좌/카드 등록 | **고객 직접** | 바로빌 제공 URL에서 직접 등록 |
| 연동 검증 | SAM 관리자 | 테스트 모드에서 데이터 수집 확인 |
| 운영 전환 | SAM 관리자 | `server_mode` 변경 |
---
## 3. 테스트 모드 활용
### 3.1 온보딩에서의 테스트 모드 역할
테스트 모드는 **온보딩 Phase 2~4에서 연동을 검증**하기 위해 사용한다.
```
✅ 바로빌 회원 등록이 정상적으로 되는지 확인
✅ 인증서/계좌/카드 연결이 작동하는지 확인
✅ API 호출이 정상 응답하는지 확인
❌ 실제 세금계산서 발행 (국세청 미전송)
❌ 실제 거래 데이터 수집 (테스트 데이터만)
```
### 3.2 테스트 모드 체크리스트
Phase 4 (연동 검증) 완료 기준:
- [ ] 바로빌 회원 상태: `active`
- [ ] 공동인증서: 등록됨 + 유효기간 확인
- [ ] 계좌: 1개 이상 등록, 입출금 조회 응답 정상
- [ ] 카드: 1개 이상 등록, 사용내역 조회 응답 정상
- [ ] 홈택스: 매출/매입 세금계산서 수집 응답 정상
- [ ] 에러 없이 모든 API 호출 성공
### 3.3 운영 전환 체크리스트
Phase 5 (운영 모드 전환) 전 확인:
- [ ] 테스트 모드 검증 완료
- [ ] 고객 동의 (실제 과금 시작 안내)
- [ ] 운영 CERTKEY 설정 확인
- [ ] 구독 등록 (월정액 과금 설정)
- [ ] `server_mode``production` 전환
- [ ] 운영 모드에서 첫 데이터 수집 성공 확인
---
## 4. 개발 시 주의사항
### 4.1 개발 단계에서의 테스트 모드
온보딩 프로세스와 별개로, **서비스 이관 개발 중에는 항상 테스트 모드를 사용**한다.
| 단계 | 모드 | 이유 |
|------|------|------|
| 서비스 이관 개발 | 테스트 | 코드 검증, 무과금 |
| 베타테스트 | 테스트 | 실사용 시나리오 검증, 무과금 |
| 고객 온보딩 Phase 2~4 | 테스트 | 연동 설정 확인, 무과금 |
| 고객 온보딩 Phase 5~ | **운영** | 실무 사용, 과금 시작 |
### 4.2 서비스 이관 완료 후 출시 순서
```
1. 서비스 이관 개발 완료 (테스트 모드)
2. 내부 베타테스트 (tenant_id=1, 코드브릿지엑스, 테스트 모드)
3. 외부 베타테스트 (선별 고객 2~3곳, 테스트 모드)
4. 정식 출시
5. 신규 고객 온보딩 프로세스 가동 (반복)
```
---
## 5. 구현 현황 (2026-03-17)
### 5.1 API SOAP 서비스 구축 완료
API에 MNG와 100% 동등한 SOAP 서비스가 독립 구축되었다.
| 항목 | 파일 | 상태 |
|------|------|:----:|
| SOAP 래퍼 (6개 서비스, 49 메서드) | `BarobillSoapService.php` | ✅ |
| 은행 동기화 | `BarobillBankSyncService.php` | ✅ |
| 카드 동기화 | `BarobillCardSyncService.php` | ✅ |
| 홈택스 동기화 | `HometaxSyncService.php` | ✅ |
| 동기화/회원 API (11개) | `BarobillSyncController.php` | ✅ |
| 자동 동기화 Job | `SyncBarobillDataJob.php` | ✅ |
> 상세: [API SOAP 기술 참조](./api-soap-reference.md)
### 5.2 테스트 환경 데이터
| tenant_id | 회사 | 바로빌 ID | 모드 | 테스트 적합성 |
|-----------|------|----------|------|:----------:|
| 290 | (주)주일기업 | `juil5130` | test | 즉시 가능 (Step 3~7) |
| 289 | (주)경동기업 | `kd5130` | test | 즉시 가능 |
| 291 | (미생성) | — | — | 풀 온보딩 테스트용 |
### 5.3 온보딩 테스트 시나리오
**시나리오 A: 빠른 SOAP 검증** (tenant_id=290)
```
이미 바로빌 회원이 등록된 상태.
→ Step 3(인증서 URL 확인)부터 Step 7(동기화)까지 테스트.
→ 소요: 10분
```
**시나리오 B: 풀 온보딩** (tenant_id=291 신규 생성)
```
테넌트 생성 → 바로빌 회원 등록 → 인증서/계좌/카드 → 검증 → 운영 전환
→ Step 1부터 Step 7까지 전 과정 테스트.
→ 소요: 1시간 (인증서/계좌 등록 포함)
```
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [바로빌 연동 시스템](./README.md) | 전체 구조, 테스트/운영 모드, 과금 정책 |
| [API SOAP 기술 참조](./api-soap-reference.md) | API SOAP 서비스 전체 메서드, 동기화, 스케줄러 |
| [온보딩 실행 가이드](../../guides/barobill-onboarding-guide.md) | 7단계 실행 절차, API 예시, 트러블슈팅 |
| [온보딩 실행 가이드 PPT](../../guides/barobill-onboarding-guide.pptx) | 12슬라이드 프레젠테이션 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | REST API 42개 + SOAP 11개 엔드포인트 |
| [데모 테넌트 정책](../sales/demo-tenant-policy.md) | 영업파트너 데모 테넌트 3-Tier 전략 |
---
**최종 업데이트**: 2026-03-17
# 바로빌 테넌트 온보딩 프로세스
> **작성일**: 2026-03-17
> **상태**: 설계 중 (서비스 이관 준비)
---
## 1. 온보딩(Onboarding)이란
### 1.1 정의
**온보딩**: 새로운 고객(테넌트)이 서비스에 가입하여 실제 사용을 시작하기까지의 초기 설정 과정.
SAM 바로빌 맥락에서는 **정식 계약 고객이 바로빌 연동 기능(계좌조회, 카드내역, 세금계산서 등)을 실무에서 사용할 수 있도록 초기 세팅하는 일련의 절차**를 의미한다.
### 1.2 온보딩 vs 베타테스트
| 구분 | 온보딩 | 베타테스트 |
|------|--------|----------|
| **대상** | 정식 계약 고객 | 서비스 출시 전 검증 참여자 |
| **목적** | 고객이 기능을 쓸 수 있게 초기 세팅 | 서비스 안정성/기능 검증 |
| **시점** | 고객 가입할 때마다 반복 발생 | 서비스 출시 전 1회성 |
| **테스트 모드** | 초기 세팅 확인용으로 잠깐 사용 가능 | 전체 기간 테스트 모드로 운영 |
| **데이터** | 실제 업무 데이터 | 검증용 테스트 데이터 |
| **과금** | 정식 과금 (운영 모드 전환 후) | 무과금 |
### 1.3 단계별 관계
서비스 이관 완료 후 다음 순서로 진행한다:
```
서비스 이관 (개발)
└─ 베타테스트 (출시 전 검증)
└─ 정식 출시
└─ 테넌트 온보딩 (고객 가입 시마다 반복)
```
---
## 2. 바로빌 온보딩 전체 흐름
### 2.1 단계별 프로세스
```
Phase 1: 테넌트 계약
┌──────────────────────────────────────────┐
│ 고객사 SAM 서비스 계약 체결 │
│ └─ SAM 테넌트 생성 (tenant_id 발급) │
│ └─ 바로빌 서비스 이용 여부 확인 │
└──────────────────────┬───────────────────┘
Phase 2: 바로빌 회원 등록 (테스트 모드)
┌──────────────────────────────────────────┐
│ 바로빌 회원사 등록 (BarobillService) │
│ ├─ 사업자번호, 상호, 대표자 등록 │
│ ├─ 바로빌 ID/PW 생성 │
│ ├─ server_mode = 'test' (기본값) │
│ └─ 테스트 서버에서 연동 확인 │
└──────────────────────┬───────────────────┘
Phase 3: 인증서 및 계좌/카드 연결
┌──────────────────────────────────────────┐
│ 공동인증서 등록 │
│ ├─ getCertificateRegistUrl() → 고객 직접│
│ ├─ 인증서 유효성 확인 │
│ └─ 인증서 만료일 모니터링 설정 │
│ │
│ 계좌 연결 │
│ ├─ getBankAccountScrapRequestUrl() │
│ └─ 고객이 직접 계좌 등록 │
│ │
│ 카드 연결 │
│ ├─ registCard() │
│ └─ 카드사별 등록 │
└──────────────────────┬───────────────────┘
Phase 4: 연동 검증
┌──────────────────────────────────────────┐
│ 테스트 모드에서 기능 확인 │
│ ├─ 계좌 입출금 내역 조회 확인 │
│ ├─ 카드 사용내역 조회 확인 │
│ ├─ 홈택스 세금계산서 수집 확인 │
│ └─ 문제 없으면 다음 단계 │
└──────────────────────┬───────────────────┘
Phase 5: 운영 모드 전환
┌──────────────────────────────────────────┐
│ 관리자가 server_mode → 'production' 전환 │
│ ├─ 운영 CERTKEY로 SOAP 재연결 │
│ ├─ 실제 데이터 수집 시작 │
│ └─ 과금 시작 (구독 등록) │
└──────────────────────┬───────────────────┘
Phase 6: 실무 사용 시작
┌──────────────────────────────────────────┐
│ 정기 동기화 스케줄러 활성화 │
│ ├─ 은행 거래 자동 수집 │
│ ├─ 카드 내역 자동 수집 │
│ ├─ 홈택스 세금계산서 자동 수집 │
│ └─ 월정액 과금 자동 처리 │
└──────────────────────────────────────────┘
```
### 2.2 역할 분담
| 단계 | 수행 주체 | 설명 |
|------|----------|------|
| 테넌트 생성 | SAM 관리자 | MNG에서 테넌트 생성 |
| 회원사 등록 | SAM 관리자 또는 고객 | 사업자 정보 입력 |
| 인증서 등록 | **고객 직접** | 바로빌 제공 URL에서 직접 등록 |
| 계좌/카드 등록 | **고객 직접** | 바로빌 제공 URL에서 직접 등록 |
| 연동 검증 | SAM 관리자 | 테스트 모드에서 데이터 수집 확인 |
| 운영 전환 | SAM 관리자 | `server_mode` 변경 |
---
## 3. 테스트 모드 활용
### 3.1 온보딩에서의 테스트 모드 역할
테스트 모드는 **온보딩 Phase 2~4에서 연동을 검증**하기 위해 사용한다.
```
✅ 바로빌 회원 등록이 정상적으로 되는지 확인
✅ 인증서/계좌/카드 연결이 작동하는지 확인
✅ API 호출이 정상 응답하는지 확인
❌ 실제 세금계산서 발행 (국세청 미전송)
❌ 실제 거래 데이터 수집 (테스트 데이터만)
```
### 3.2 테스트 모드 체크리스트
Phase 4 (연동 검증) 완료 기준:
- [ ] 바로빌 회원 상태: `active`
- [ ] 공동인증서: 등록됨 + 유효기간 확인
- [ ] 계좌: 1개 이상 등록, 입출금 조회 응답 정상
- [ ] 카드: 1개 이상 등록, 사용내역 조회 응답 정상
- [ ] 홈택스: 매출/매입 세금계산서 수집 응답 정상
- [ ] 에러 없이 모든 API 호출 성공
### 3.3 운영 전환 체크리스트
Phase 5 (운영 모드 전환) 전 확인:
- [ ] 테스트 모드 검증 완료
- [ ] 고객 동의 (실제 과금 시작 안내)
- [ ] 운영 CERTKEY 설정 확인
- [ ] 구독 등록 (월정액 과금 설정)
- [ ] `server_mode``production` 전환
- [ ] 운영 모드에서 첫 데이터 수집 성공 확인
---
## 4. 개발 시 주의사항
### 4.1 개발 단계에서의 테스트 모드
온보딩 프로세스와 별개로, **서비스 이관 개발 중에는 항상 테스트 모드를 사용**한다.
| 단계 | 모드 | 이유 |
|------|------|------|
| 서비스 이관 개발 | 테스트 | 코드 검증, 무과금 |
| 베타테스트 | 테스트 | 실사용 시나리오 검증, 무과금 |
| 고객 온보딩 Phase 2~4 | 테스트 | 연동 설정 확인, 무과금 |
| 고객 온보딩 Phase 5~ | **운영** | 실무 사용, 과금 시작 |
### 4.2 서비스 이관 완료 후 출시 순서
```
1. 서비스 이관 개발 완료 (테스트 모드)
2. 내부 베타테스트 (tenant_id=1, 코드브릿지엑스, 테스트 모드)
3. 외부 베타테스트 (선별 고객 2~3곳, 테스트 모드)
4. 정식 출시
5. 신규 고객 온보딩 프로세스 가동 (반복)
```
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [바로빌 연동 시스템](./README.md) | 전체 구조, 테스트/운영 모드, 과금 정책 |
| [바로빌 API 명세](../../frontend/api-specs/barobill-api.md) | REST API 42개 엔드포인트 |
| [데모 테넌트 정책](../sales/demo-tenant-policy.md) | 영업파트너 데모 테넌트 3-Tier 전략 |
---
**최종 업데이트**: 2026-03-17

File diff suppressed because it is too large Load Diff

View File

@@ -1,148 +1,148 @@
# 이메일 시스템 (Tenant Email)
> **상태**: Phase 1~2 구현 완료
> **최종 갱신**: 2026-03-12
---
## 1. 개요
SAM 멀티테넌시 환경에서 테넌트별 이메일 발송을 관리하는 시스템.
본사(코드브릿지엑스)가 MNG 관리 화면에서 각 테넌트의 메일 설정을 대행 관리한다.
**핵심 기능:**
- 테넌트별 SMTP 설정 및 발송 격리
- 한국 메일 제공자 프리셋 (Gmail, 네이버, 네이버웍스, 다음, MS365, 카페24, 가비아)
- SMTP 연결 테스트 (SmtpConnectionTester)
- 발송 기록 및 일일 쿼터 관리
- Fallback: 자체 SMTP 실패 시 플랫폼 기본 SMTP로 재시도
---
## 2. 아키텍처 (3-Layer)
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 테넌트 메일 설정 (tenant_mail_configs) │
│ SMTP 설정, 발신자 주소, 브랜딩 정보 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 메일 발송 서비스 (TenantMailService) │
│ 테넌트 설정 자동 적용, 큐 발송, Fallback │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 발송 기록 (mail_logs) │
│ 발송 이력, 상태 추적, 일일 쿼터 관리 │
└─────────────────────────────────────────────────────────┘
```
---
## 3. 테이블
| 테이블 | 설명 | 주요 컬럼 |
|--------|------|----------|
| `tenant_mail_configs` | 테넌트별 메일 설정 (1:1) | `tenant_id`, `provider`, `from_address`, `from_name`, `is_verified`, `daily_limit`, `options` |
| `mail_logs` | 발송 기록 | `tenant_id`, `mailable_type`, `to_address`, `from_address`, `subject`, `status`, `sent_at`, `options` |
**마이그레이션 위치**: `api/database/migrations/2026_03_12_*`
---
## 4. 모델
| 모델 | 위치 | Traits |
|------|------|--------|
| `TenantMailConfig` | API + MNG | BelongsToTenant, SoftDeletes, Auditable |
| `MailLog` | API + MNG | BelongsToTenant |
**`tenant_mail_configs.options` JSON 구조:**
| 키 | 설명 |
|----|------|
| `smtp.*` | SMTP 접속 정보 (host, port, username, password, encryption) |
| `preset` | 프리셋 식별자 (gmail, naver, custom 등) |
| `branding.*` | 메일 템플릿 브랜딩 (로고, 컬러, 회사명, 푸터) |
| `connection_test.*` | 마지막 SMTP 연결 테스트 결과 |
---
## 5. 서비스 (MNG)
| 서비스 | 설명 |
|--------|------|
| `TenantMailService` | 메일 발송 (설정 조회 → 쿼터 확인 → SMTP 구성 → 발송 → 로그) |
| `SmtpConnectionTester` | SMTP 연결 테스트 (TCP → TLS → AUTH → 테스트 메일) |
**에러 코드:**
| 코드 | 설명 |
|------|------|
| `CONN_REFUSED` | SMTP 서버 접속 거부 |
| `TLS_FAILED` | TLS 핸드셰이크 실패 |
| `AUTH_FAILED` | 인증 실패 (앱 비밀번호 확인) |
| `TIMEOUT` | 연결 시간 초과 |
---
## 6. MNG 파일 구조
```
mng/
├── app/Http/Controllers/System/
│ └── TenantMailConfigController.php
├── app/Services/Mail/
│ ├── TenantMailService.php
│ └── SmtpConnectionTester.php
├── config/
│ └── mail-presets.php
└── resources/views/system/tenant-mail/
├── index.blade.php (테넌트 목록)
└── edit.blade.php (설정 폼)
```
---
## 7. MNG 라우트
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/system/tenant-mail` | 테넌트 메일 설정 목록 |
| `GET` | `/system/tenant-mail/presets` | SMTP 프리셋 JSON |
| `GET` | `/system/tenant-mail/{tenant}/edit` | 설정 폼 |
| `PUT` | `/system/tenant-mail/{tenant}` | 설정 저장 |
| `POST` | `/system/tenant-mail/{tenant}/test` | SMTP 연결 테스트 |
---
## 8. 연동 방식 (3단계 전략)
| Phase | 방식 | 설명 | 상태 |
|-------|------|------|------|
| Phase 1 | 플랫폼 발송 + Reply-To | SAM 공용 SMTP로 발송, 테넌트 Reply-To 적용 | ✅ 구현 완료 |
| Phase 2 | SMTP 릴레이 + 프리셋 | 테넌트 자체 SMTP로 발송, 프리셋 자동 채움 | ✅ 구현 완료 |
| Phase 3 | OAuth2 연동 | Google/MS OAuth2 토큰 기반 발송 | 🟢 향후 |
---
## 9. 보안 규칙
```
✅ SMTP 비밀번호: encrypt()/decrypt()로 암호화 저장
✅ mail_logs에 메일 본문 저장 금지 (메타데이터만)
✅ API 응답에 SMTP 비밀번호 노출 금지 (hidden 처리)
✅ TenantScope 자동 적용 (테넌트 격리)
❌ Mail::to() 직접 호출 금지 → TenantMailService 사용
```
---
## 관련 문서
- [이메일 발송 정책](../../dev/standards/email-policy.md) — 내부 발송 아키텍처, 테이블 설계, 쿼터 관리, 서비스 설계
- [테넌트 이메일 연동 가이드](../../dev/guides/tenant-email-integration-guide.md) — SMTP 프리셋, MNG 관리 화면, 연결 테스트, 테넌트 사전 준비 안내
- [테넌트 DB 구조](../../system/database/tenants.md)
- [options JSON 컬럼 정책](../../dev/standards/options-column-policy.md)
- [전자서명 기능](../esign/README.md) — 메일 발송이 많은 주요 기능
---
**최종 업데이트**: 2026-03-12
# 이메일 시스템 (Tenant Email)
> **상태**: Phase 1~2 구현 완료
> **최종 갱신**: 2026-03-12
---
## 1. 개요
SAM 멀티테넌시 환경에서 테넌트별 이메일 발송을 관리하는 시스템.
본사(코드브릿지엑스)가 MNG 관리 화면에서 각 테넌트의 메일 설정을 대행 관리한다.
**핵심 기능:**
- 테넌트별 SMTP 설정 및 발송 격리
- 한국 메일 제공자 프리셋 (Gmail, 네이버, 네이버웍스, 다음, MS365, 카페24, 가비아)
- SMTP 연결 테스트 (SmtpConnectionTester)
- 발송 기록 및 일일 쿼터 관리
- Fallback: 자체 SMTP 실패 시 플랫폼 기본 SMTP로 재시도
---
## 2. 아키텍처 (3-Layer)
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 테넌트 메일 설정 (tenant_mail_configs) │
│ SMTP 설정, 발신자 주소, 브랜딩 정보 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 메일 발송 서비스 (TenantMailService) │
│ 테넌트 설정 자동 적용, 큐 발송, Fallback │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 발송 기록 (mail_logs) │
│ 발송 이력, 상태 추적, 일일 쿼터 관리 │
└─────────────────────────────────────────────────────────┘
```
---
## 3. 테이블
| 테이블 | 설명 | 주요 컬럼 |
|--------|------|----------|
| `tenant_mail_configs` | 테넌트별 메일 설정 (1:1) | `tenant_id`, `provider`, `from_address`, `from_name`, `is_verified`, `daily_limit`, `options` |
| `mail_logs` | 발송 기록 | `tenant_id`, `mailable_type`, `to_address`, `from_address`, `subject`, `status`, `sent_at`, `options` |
**마이그레이션 위치**: `api/database/migrations/2026_03_12_*`
---
## 4. 모델
| 모델 | 위치 | Traits |
|------|------|--------|
| `TenantMailConfig` | API + MNG | BelongsToTenant, SoftDeletes, Auditable |
| `MailLog` | API + MNG | BelongsToTenant |
**`tenant_mail_configs.options` JSON 구조:**
| 키 | 설명 |
|----|------|
| `smtp.*` | SMTP 접속 정보 (host, port, username, password, encryption) |
| `preset` | 프리셋 식별자 (gmail, naver, custom 등) |
| `branding.*` | 메일 템플릿 브랜딩 (로고, 컬러, 회사명, 푸터) |
| `connection_test.*` | 마지막 SMTP 연결 테스트 결과 |
---
## 5. 서비스 (MNG)
| 서비스 | 설명 |
|--------|------|
| `TenantMailService` | 메일 발송 (설정 조회 → 쿼터 확인 → SMTP 구성 → 발송 → 로그) |
| `SmtpConnectionTester` | SMTP 연결 테스트 (TCP → TLS → AUTH → 테스트 메일) |
**에러 코드:**
| 코드 | 설명 |
|------|------|
| `CONN_REFUSED` | SMTP 서버 접속 거부 |
| `TLS_FAILED` | TLS 핸드셰이크 실패 |
| `AUTH_FAILED` | 인증 실패 (앱 비밀번호 확인) |
| `TIMEOUT` | 연결 시간 초과 |
---
## 6. MNG 파일 구조
```
mng/
├── app/Http/Controllers/System/
│ └── TenantMailConfigController.php
├── app/Services/Mail/
│ ├── TenantMailService.php
│ └── SmtpConnectionTester.php
├── config/
│ └── mail-presets.php
└── resources/views/system/tenant-mail/
├── index.blade.php (테넌트 목록)
└── edit.blade.php (설정 폼)
```
---
## 7. MNG 라우트
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/system/tenant-mail` | 테넌트 메일 설정 목록 |
| `GET` | `/system/tenant-mail/presets` | SMTP 프리셋 JSON |
| `GET` | `/system/tenant-mail/{tenant}/edit` | 설정 폼 |
| `PUT` | `/system/tenant-mail/{tenant}` | 설정 저장 |
| `POST` | `/system/tenant-mail/{tenant}/test` | SMTP 연결 테스트 |
---
## 8. 연동 방식 (3단계 전략)
| Phase | 방식 | 설명 | 상태 |
|-------|------|------|------|
| Phase 1 | 플랫폼 발송 + Reply-To | SAM 공용 SMTP로 발송, 테넌트 Reply-To 적용 | ✅ 구현 완료 |
| Phase 2 | SMTP 릴레이 + 프리셋 | 테넌트 자체 SMTP로 발송, 프리셋 자동 채움 | ✅ 구현 완료 |
| Phase 3 | OAuth2 연동 | Google/MS OAuth2 토큰 기반 발송 | 🟢 향후 |
---
## 9. 보안 규칙
```
✅ SMTP 비밀번호: encrypt()/decrypt()로 암호화 저장
✅ mail_logs에 메일 본문 저장 금지 (메타데이터만)
✅ API 응답에 SMTP 비밀번호 노출 금지 (hidden 처리)
✅ TenantScope 자동 적용 (테넌트 격리)
❌ Mail::to() 직접 호출 금지 → TenantMailService 사용
```
---
## 관련 문서
- [이메일 발송 정책](../../dev/standards/email-policy.md) — 내부 발송 아키텍처, 테이블 설계, 쿼터 관리, 서비스 설계
- [테넌트 이메일 연동 가이드](../../dev/guides/tenant-email-integration-guide.md) — SMTP 프리셋, MNG 관리 화면, 연결 테스트, 테넌트 사전 준비 안내
- [테넌트 DB 구조](../../system/database/tenants.md)
- [options JSON 컬럼 정책](../../dev/standards/options-column-policy.md)
- [전자서명 기능](../esign/README.md) — 메일 발송이 많은 주요 기능
---
**최종 업데이트**: 2026-03-12

View File

@@ -1,489 +1,489 @@
{
"projectName": "SAM 설비관리 (Equipment Management)",
"company": "(주)코드브릿지엑스",
"author": "R&D실",
"date": "2026.03.12",
"version": "1.0",
"purpose": "생산 설비의 등록, 점검, 수리이력을 체계적으로 관리하는 설비관리 시스템의 화면 설계 기획서",
"features": [
"설비 대시보드 (현황 통계, 유형별 분포, 최근 수리이력)",
"설비 대장 CRUD (등록/조회/수정/삭제/복원)",
"6주기 점검 그리드 (일일/주간/월간/2개월/분기/반년)",
"점검 템플릿 관리 (설비별 점검항목 정의, 주기간 복사)",
"수리이력 관리 (사내/외주, 비용, 시간, 업체)",
"설비 사진 관리 (GCS 업로드, 자동 압축, 최대 10장)",
"엑셀 Import (미리보기 + 일괄등록, 이미지 포함)"
],
"effects": [
{ "icon": "📊", "title": "설비 현황 가시화", "desc": "대시보드를 통한 전체 설비 상태 실시간 파악" },
{ "icon": "🔧", "title": "정기 점검 체계화", "desc": "6주기 점검 그리드로 누락 없는 예방보전 관리" },
{ "icon": "📋", "title": "이력 추적성", "desc": "수리이력과 점검기록의 체계적 관리로 감사 대응" },
{ "icon": "📱", "title": "모바일 점검", "desc": "QR 스캔 기반 현장 모바일 점검 지원" }
],
"tocItems": [
{ "num": "01", "title": "프로젝트 개요", "desc": "설비관리 시스템 목적 및 주요 기능" },
{ "num": "02", "title": "메뉴 구조 (IA)", "desc": "Information Architecture" },
{ "num": "03", "title": "설비 대시보드", "desc": "현황 통계 및 유형별 분포" },
{ "num": "04", "title": "설비 대장 목록", "desc": "설비 목록 조회 및 필터링" },
{ "num": "05", "title": "설비 등록", "desc": "신규 설비 등록 폼" },
{ "num": "06", "title": "설비 상세", "desc": "설비 정보, 점검항목, 수리이력, 사진 탭" },
{ "num": "07", "title": "점검 그리드", "desc": "6주기 점검 현황 그리드" },
{ "num": "08", "title": "수리이력", "desc": "수리이력 목록 및 등록" },
{ "num": "09", "title": "엑셀 Import", "desc": "엑셀 파일 업로드 및 일괄 등록" }
],
"mainMenus": [
{
"title": "설비 대시보드",
"children": ["현황 통계", "유형별 분포", "최근 수리"]
},
{
"title": "설비 대장",
"children": ["목록", "등록", "상세/수정"]
},
{
"title": "점검 관리",
"children": ["점검 그리드", "점검항목 설정"]
},
{
"title": "수리이력",
"children": ["목록", "등록/수정"]
},
{
"title": "Import",
"children": ["엑셀 업로드"]
}
],
"screens": [
{
"taskName": "설비 대시보드",
"route": "/equipment",
"screenName": "설비 현황 대시보드",
"screenId": "EQP_001",
"descriptions": [
{
"title": "현황 통계 카드",
"content": "전체/가동/유휴/폐기 설비 수를 카드 형태로 표시. 각 카드 클릭 시 해당 상태 필터 적용된 목록으로 이동",
"markerX": 1.8,
"markerY": 1.5
},
{
"title": "유형별 분포 차트",
"content": "포밍기/미싱기/샤링기/V컷팅기/절곡기/프레스/드릴/기타별 설비 수를 PieChart로 시각화",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "최근 수리이력",
"content": "최근 5건의 수리이력을 테이블로 표시. 설비명, 수리일, 보전구분(사내/외주), 수리내용 포함",
"markerX": 4.2,
"markerY": 2.8
},
{
"title": "이번 달 점검 현황",
"content": "당월 점검 완료율, 미점검 설비 수 등 점검 진행 상황 요약",
"markerX": 4.2,
"markerY": 4.2
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 현황 대시보드", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "0d9488", "text": "전체\n128대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.9, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "22c55e", "text": "가동\n95대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 4.2, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "f59e0b", "text": "유휴\n25대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 5.5, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "dc2626", "text": "폐기\n8대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 2.2, "fill": "FFFFFF", "text": "[유형별 분포 PieChart]\n포밍기 32%\n미싱기 25%\n절곡기 18%\n기타 25%", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.2, "y": 2.6, "w": 2.5, "h": 1.3, "fill": "FFFFFF", "text": "최근 수리이력\n───────────────\n포밍기-01 | 03.10 | 사내\n미싱기-03 | 03.08 | 외주\n절곡기-02 | 03.05 | 사내", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.2, "y": 4.0, "w": 2.5, "h": 0.8, "fill": "FFFFFF", "text": "이번 달 점검 현황\n완료율: 78% (94/120)\n미점검: 26건", "fontSize": 7, "color": "1e293b"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry",
"screenName": "설비 대장 목록",
"screenId": "EQP_002",
"descriptions": [
{
"title": "상태 필터 탭",
"content": "전체/가동(active)/유휴(idle)/폐기(disposed) 탭으로 상태별 필터링. 각 탭에 해당 건수 표시",
"markerX": 1.8,
"markerY": 1.5
},
{
"title": "검색 및 필터",
"content": "설비코드, 설비명으로 텍스트 검색. 생산라인(스라트/스크린/절곡/기타), 설비유형 드롭다운 필터",
"markerX": 1.8,
"markerY": 2.0
},
{
"title": "설비 목록 테이블",
"content": "설비코드, 설비명, 유형, 생산라인, 상태, 담당자, 구입일 컬럼. 행 클릭 시 상세 페이지로 이동",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "액션 버튼",
"content": "설비 등록 버튼, 엑셀 다운로드 버튼. UniversalListPage 템플릿 적용",
"markerX": 5.8,
"markerY": 1.5
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 대장", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "0d9488", "text": "전체 128", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.55, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "가동 95", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 3.5, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "유휴 25", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.45, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "폐기 8", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.8, "y": 1.65, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "+ 설비 등록", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.05, "w": 2.0, "h": 0.3, "fill": "FFFFFF", "text": "🔍 설비코드/설비명 검색", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 3.7, "y": 2.05, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "생산라인 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 2.05, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "설비유형 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "설비코드 | 설비명 | 유형 | 생산라인 | 상태 | 담당자 | 구입일", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.85, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "FM-001 | 포밍기 1호 | 포밍기 | 스라트 | 가동 | 김철수 | 2023-05-10", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.15, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "MS-003 | 미싱기 3호 | 미싱기 | 스크린 | 가동 | 박영희 | 2024-01-15", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.45, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "SH-002 | 샤링기 2호 | 샤링기 | 절곡 | 유휴 | 이민수 | 2022-11-20", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.75, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "VC-001 | V컷팅기 1호 | V컷팅기 | 스라트 | 가동 | 김철수 | 2024-06-01", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.05, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "PR-004 | 프레스 4호 | 프레스 | 기타 | 폐기 | - | 2019-03-22", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 3.0, "y": 4.5, "w": 2.0, "h": 0.3, "text": "< 1 2 3 4 5 >", "fontSize": 8, "color": "64748b"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/create",
"screenName": "설비 등록",
"screenId": "EQP_003",
"descriptions": [
{
"title": "기본 정보 입력",
"content": "설비코드(tenant 내 unique), 설비명, 설비유형(드롭다운 8종), 생산라인(드롭다운 4종), 상태(active/idle/disposed)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "담당자 지정",
"content": "정 담당자(manager_id), 부 담당자(sub_manager_id) 선택. 사용자 목록에서 검색 선택",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "구입/설치 정보",
"content": "구입일, 설치일(DatePicker), 구입가격(decimal), 내용연수(년), 메모(textarea)",
"markerX": 1.8,
"markerY": 3.6
},
{
"title": "공정 매핑",
"content": "설비가 소속된 공정을 N:N으로 선택. 주 설비 여부(is_primary) 체크박스",
"markerX": 4.5,
"markerY": 3.6
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 등록", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비코드 * FM-005", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 1.7, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비명 * 포밍기 5호", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비유형 포밍기 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "생산라인 스라트 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "상태 가동 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "정 담당자 김철수 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "부 담당자 박영희 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "구입일 2026-03-01", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 3.3, "y": 3.5, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "설치일 2026-03-10", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.95, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "구입가격 35,000,000", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 3.3, "y": 3.95, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "내용연수 10년", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 5.0, "y": 3.5, "w": 1.8, "h": 0.8, "fill": "f1f5f9", "text": "공정 매핑\n☑ 절곡공정 (주)\n☐ 포장공정\n☑ 조립공정", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.4, "w": 5.3, "h": 0.5, "fill": "FFFFFF", "text": "메모\n특이사항 입력...", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 5.4, "y": 5.0, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "저장", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.2, "y": 5.0, "w": 0.7, "h": 0.3, "fill": "64748b", "text": "취소", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/{id}",
"screenName": "설비 상세 (기본정보 탭)",
"screenId": "EQP_004",
"descriptions": [
{
"title": "탭 구조",
"content": "기본정보 | 점검항목 | 수리이력 | 사진 4개 탭. 현재 활성 탭 하이라이트 표시",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "기본정보 그리드",
"content": "설비 기본 정보를 라벨:값 그리드로 표시. 수정 버튼 클릭 시 수정 페이지로 이동",
"markerX": 1.8,
"markerY": 2.3
},
{
"title": "상태 배지",
"content": "가동(녹색)/유휴(주황)/폐기(적색) 상태를 컬러 배지로 표시",
"markerX": 5.5,
"markerY": 1.5
},
{
"title": "액션 버튼",
"content": "수정, 삭제(SoftDelete), 복원 버튼. 삭제된 설비는 복원 버튼 표시",
"markerX": 5.5,
"markerY": 5.0
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 4.0, "h": 0.4, "text": "포밍기 1호 (FM-001)", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 5.8, "y": 1.25, "w": 0.6, "h": 0.25, "fill": "22c55e", "text": "가동", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "0d9488", "text": "기본정보", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.8, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "점검항목", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.0, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "수리이력", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "e2e8f0", "text": "사진", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.1, "w": 5.3, "h": 2.8, "fill": "FFFFFF"},
{"type": "rect", "x": 1.8, "y": 2.2, "w": 1.0, "h": 0.25, "text": "설비코드", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.2, "w": 1.5, "h": 0.25, "text": "FM-001", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.2, "w": 1.0, "h": 0.25, "text": "설비명", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.2, "w": 1.3, "h": 0.25, "text": "포밍기 1호", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 2.55, "w": 1.0, "h": 0.25, "text": "설비유형", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.55, "w": 1.5, "h": 0.25, "text": "포밍기", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.55, "w": 1.0, "h": 0.25, "text": "생산라인", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.55, "w": 1.3, "h": 0.25, "text": "스라트", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 2.9, "w": 1.0, "h": 0.25, "text": "정 담당자", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.9, "w": 1.5, "h": 0.25, "text": "김철수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.9, "w": 1.0, "h": 0.25, "text": "부 담당자", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.9, "w": 1.3, "h": 0.25, "text": "박영희", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 3.25, "w": 1.0, "h": 0.25, "text": "구입일", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 3.25, "w": 1.5, "h": 0.25, "text": "2023-05-10", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 3.25, "w": 1.0, "h": 0.25, "text": "설치일", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 3.25, "w": 1.3, "h": 0.25, "text": "2023-06-01", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 3.6, "w": 1.0, "h": 0.25, "text": "구입가격", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 3.6, "w": 1.5, "h": 0.25, "text": "35,000,000원", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 3.6, "w": 1.0, "h": 0.25, "text": "내용연수", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 3.6, "w": 1.3, "h": 0.25, "text": "10년", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 5.5, "y": 4.95, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "수정", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.3, "y": 4.95, "w": 0.7, "h": 0.3, "fill": "dc2626", "text": "삭제", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/{id}#inspection-items",
"screenName": "설비 상세 (점검항목 탭)",
"screenId": "EQP_005",
"descriptions": [
{
"title": "주기 선택 탭",
"content": "일일/주간/월간/2개월/분기/반년 6개 주기 탭. 선택한 주기의 점검항목 표시",
"markerX": 1.8,
"markerY": 2.2
},
{
"title": "점검항목 목록",
"content": "항목번호, 점검개소, 점검항목, 시기(가동중/정지중), 점검방법 컬럼. 인라인 수정 지원",
"markerX": 1.8,
"markerY": 2.9
},
{
"title": "항목 추가/복사",
"content": "점검항목 추가 버튼으로 새 항목 생성. 주기간 복사 기능으로 다른 주기의 항목을 일괄 복사",
"markerX": 5.0,
"markerY": 2.2
},
{
"title": "항목 삭제",
"content": "각 행의 삭제 버튼으로 개별 항목 삭제. 삭제 시 확인 다이얼로그 표시",
"markerX": 6.5,
"markerY": 2.9
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 4.0, "h": 0.4, "text": "포밍기 1호 (FM-001)", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "기본정보", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 2.8, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "0d9488", "text": "점검항목", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 4.0, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "수리이력", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "e2e8f0", "text": "사진", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "0d9488", "text": "일일", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 2.3, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "주간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.0, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "월간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.7, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "2개월", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 2.1, "w": 1.0, "h": 0.25, "fill": "0d9488", "text": "+ 항목 추가", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 6.1, "y": 2.1, "w": 0.8, "h": 0.25, "fill": "64748b", "text": "주기 복사", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.3, "fill": "1e293b", "text": "No | 점검개소 | 점검항목 | 시기 | 점검방법 | 삭제", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.8, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "1 | 유압부 | 유압 호스 상태 | 가동중 | 육안 확인 후 이상 시 교체 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.1, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2 | 전기부 | 전원 케이블 | 정지중 | 절연 저항 측정 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.4, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "3 | 구동부 | 벨트 장력 | 가동중 | 텐션게이지 측정 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.7, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "4 | 안전장치 | 비상정지 버튼 | 정지중 | 작동 테스트 | X", "fontSize": 7, "color": "1e293b"}
]
},
{
"taskName": "점검 관리",
"route": "/equipment/inspections",
"screenName": "점검 그리드",
"screenId": "EQP_006",
"descriptions": [
{
"title": "주기/기간 선택",
"content": "6개 주기 탭(일일~반년) 선택. 일일: 년-월 선택(1~31일 열), 그 외: 년 선택(주간:1~52주, 월간:1~12월 등)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "점검 그리드",
"content": "행: 설비x점검항목, 열: 날짜/기간. 셀 클릭 시 결과 순환(빈칸->O->X->Triangle->빈칸). 색상으로 구분",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "종합판정/노트",
"content": "설비별 종합판정(OK/NG), 수리내역, 이상내용 편집 가능. 인라인 수정 지원",
"markerX": 5.5,
"markerY": 2.8
},
{
"title": "생산라인 필터",
"content": "생산라인별 필터로 해당 라인 설비만 표시. 전체/스라트/스크린/절곡/기타",
"markerX": 5.0,
"markerY": 1.7
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 점검", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "0d9488", "text": "일일", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.15, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "주간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 2.7, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "월간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.25, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "2개월", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.8, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "분기", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 4.35, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "반년", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 1.65, "w": 1.0, "h": 0.25, "fill": "FFFFFF", "text": "2026-03 ▼", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 6.1, "y": 1.65, "w": 0.8, "h": 0.25, "fill": "FFFFFF", "text": "전체 ▼", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.0, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "설비 | 항목 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | 판정", "fontSize": 6, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.35, "w": 5.3, "h": 0.55, "fill": "FFFFFF", "text": "포밍기 유압호스 O O O - - O X △ ... OK\n1호 벨트장력 O O O - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.9, "w": 5.3, "h": 0.55, "fill": "f8fafc", "text": "미싱기 전원케이블 O O X - - △ O O ... NG\n3호 안전장치 O O O - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.45, "w": 5.3, "h": 0.55, "fill": "FFFFFF", "text": "샤링기 유압압력 O O O - - O O O ... OK\n2호 칼날마모 O X △ - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.1, "w": 5.3, "h": 0.5, "fill": "f1f5f9", "text": "범례: O = 양호(good) X = 불량(bad) △ = 수리(repaired) - = 휴일/주말\n셀 클릭 시: 빈칸 -> O -> X -> △ -> 빈칸 순환", "fontSize": 7, "color": "64748b"}
]
},
{
"taskName": "수리이력",
"route": "/equipment/repairs",
"screenName": "수리이력 목록",
"screenId": "EQP_007",
"descriptions": [
{
"title": "필터 영역",
"content": "설비 선택(드롭다운), 보전구분(사내/외주), 날짜 범위(시작일~종료일), 텍스트 검색(수리내용)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "수리이력 테이블",
"content": "수리일, 설비명, 보전구분, 수리내용, 수리시간, 비용, 수리자/업체 컬럼. 행 클릭 시 수정 모달",
"markerX": 1.8,
"markerY": 2.6
},
{
"title": "수리이력 등록",
"content": "수리이력 등록 버튼 클릭 시 등록 폼(모달 또는 별도 페이지)으로 이동",
"markerX": 5.8,
"markerY": 1.3
},
{
"title": "비용 집계",
"content": "조회된 수리이력의 총 비용, 사내/외주 비용 비율 등 하단 요약 표시",
"markerX": 1.8,
"markerY": 4.5
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "수리이력", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 5.8, "y": 1.25, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "+ 수리 등록", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.5, "h": 0.3, "fill": "FFFFFF", "text": "설비 선택 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 3.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "FFFFFF", "text": "보전구분 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.3, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "2026-01 ~ 2026-03", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "🔍 검색", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "수리일 | 설비명 | 보전구분 | 수리내용 | 시간 | 비용 | 수리자/업체", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "2026-03-10 | 포밍기 1호 | 사내 | 유압호스 교체 | 2.0h | 150,000원 | 김철수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.8, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2026-03-08 | 미싱기 3호 | 외주 | 모터 오버홀 | 8.0h | 2,500,000원 | ABC기계", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.1, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "2026-03-05 | 샤링기 2호 | 사내 | 칼날 교체 | 1.5h | 85,000원 | 이민수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.4, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2026-02-28 | 절곡기 1호 | 외주 | 유압실린더 교체 | 4.0h | 1,200,000원 | XYZ설비", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 3.0, "y": 3.9, "w": 2.0, "h": 0.3, "text": "< 1 2 3 >", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 4.3, "w": 5.3, "h": 0.4, "fill": "f1f5f9", "text": "총 수리비용: 3,935,000원 | 사내: 235,000원 (6%) | 외주: 3,700,000원 (94%)", "fontSize": 8, "color": "1e293b", "bold": true}
]
},
{
"taskName": "수리이력",
"route": "/equipment/repairs/create",
"screenName": "수리이력 등록",
"screenId": "EQP_008",
"descriptions": [
{
"title": "설비 선택",
"content": "수리 대상 설비를 드롭다운에서 선택. 설비코드+설비명 표시. 필수 입력",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "수리 정보",
"content": "수리일(DatePicker), 보전구분(사내/외주 라디오), 수리시간(시간 단위), 비용(원)",
"markerX": 1.8,
"markerY": 2.3
},
{
"title": "수리 상세",
"content": "수리내용(textarea), 수리자(사내 선택) 또는 외주업체명(텍스트 입력). 보전구분에 따라 전환",
"markerX": 1.8,
"markerY": 3.3
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "수리이력 등록", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 5.3, "h": 0.35, "fill": "FFFFFF", "text": "설비 * FM-001 포밍기 1호 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리일 * 2026-03-12", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "보전구분 * ● 사내 ○ 외주", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리시간 2.0 시간", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리비용 150,000 원", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리자 김철수 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 5.3, "h": 1.0, "fill": "FFFFFF", "text": "수리내용\n유압호스 노후로 인한 교체 작업.\n기존 호스 균열 발견, 동일 규격 신품으로 교체 완료.\n압력 테스트 정상 확인.", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 5.4, "y": 4.7, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "저장", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.2, "y": 4.7, "w": 0.7, "h": 0.3, "fill": "64748b", "text": "취소", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "Import",
"route": "/equipment/import",
"screenName": "엑셀 Import",
"screenId": "EQP_009",
"descriptions": [
{
"title": "파일 업로드",
"content": "엑셀 파일(.xlsx) 드래그&드롭 또는 파일 선택. 업로드 후 자동으로 미리보기 단계로 전환",
"markerX": 1.8,
"markerY": 1.8
},
{
"title": "미리보기 테이블",
"content": "파싱된 데이터를 테이블로 표시. 한글/영문 헤더 자동 매핑. 오류 행 빨간색 표시. 이미지(Drawing) 자동 추출 표시",
"markerX": 1.8,
"markerY": 2.7
},
{
"title": "중복 처리 옵션",
"content": "동일 설비코드 존재 시: 건너뜀(skip) 또는 덮어쓰기(overwrite) 선택. 라디오 버튼",
"markerX": 1.8,
"markerY": 4.2
},
{
"title": "Import 실행",
"content": "Import 실행 버튼 클릭 시 일괄 등록. 결과 요약(성공/실패/건너뜀 건수) 표시",
"markerX": 5.8,
"markerY": 4.2
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "엑셀 Import", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 5.3, "h": 0.8, "fill": "f1f5f9", "text": "📁 엑셀 파일을 드래그하거나 클릭하여 선택하세요\n(.xlsx 형식, 최대 10MB)", "fontSize": 9, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 5.3, "h": 0.3, "fill": "1e293b", "text": "설비코드 | 설비명 | 유형 | 라인 | 상태 | 구입일 | 이미지", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.9, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "FM-006 | 포밍기 6호 | 포밍기 | 스라트 | 가동 | 2026-01 | [img]", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.2, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "MS-005 | 미싱기 5호 | 미싱기 | 스크린 | 가동 | 2025-11 | [img]", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 5.3, "h": 0.3, "fill": "fef2f2", "text": "FM-001 | 포밍기 1호 | 포밍기 | 스라트 | 가동 | 2023-05 | - (중복!)", "fontSize": 7, "color": "dc2626"},
{"type": "rect", "x": 1.6, "y": 3.9, "w": 3.0, "h": 0.35, "fill": "f1f5f9", "text": "중복 처리: ● 건너뜀(skip) ○ 덮어쓰기(overwrite)", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.35, "w": 3.0, "h": 0.3, "text": "총 3건 | 신규: 2건 | 중복: 1건 | 오류: 0건", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.8, "y": 4.35, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "Import 실행", "fontSize": 8, "color": "FFFFFF", "bold": true}
]
}
]
}
{
"projectName": "SAM 설비관리 (Equipment Management)",
"company": "(주)코드브릿지엑스",
"author": "R&D실",
"date": "2026.03.12",
"version": "1.0",
"purpose": "생산 설비의 등록, 점검, 수리이력을 체계적으로 관리하는 설비관리 시스템의 화면 설계 기획서",
"features": [
"설비 대시보드 (현황 통계, 유형별 분포, 최근 수리이력)",
"설비 대장 CRUD (등록/조회/수정/삭제/복원)",
"6주기 점검 그리드 (일일/주간/월간/2개월/분기/반년)",
"점검 템플릿 관리 (설비별 점검항목 정의, 주기간 복사)",
"수리이력 관리 (사내/외주, 비용, 시간, 업체)",
"설비 사진 관리 (GCS 업로드, 자동 압축, 최대 10장)",
"엑셀 Import (미리보기 + 일괄등록, 이미지 포함)"
],
"effects": [
{ "icon": "📊", "title": "설비 현황 가시화", "desc": "대시보드를 통한 전체 설비 상태 실시간 파악" },
{ "icon": "🔧", "title": "정기 점검 체계화", "desc": "6주기 점검 그리드로 누락 없는 예방보전 관리" },
{ "icon": "📋", "title": "이력 추적성", "desc": "수리이력과 점검기록의 체계적 관리로 감사 대응" },
{ "icon": "📱", "title": "모바일 점검", "desc": "QR 스캔 기반 현장 모바일 점검 지원" }
],
"tocItems": [
{ "num": "01", "title": "프로젝트 개요", "desc": "설비관리 시스템 목적 및 주요 기능" },
{ "num": "02", "title": "메뉴 구조 (IA)", "desc": "Information Architecture" },
{ "num": "03", "title": "설비 대시보드", "desc": "현황 통계 및 유형별 분포" },
{ "num": "04", "title": "설비 대장 목록", "desc": "설비 목록 조회 및 필터링" },
{ "num": "05", "title": "설비 등록", "desc": "신규 설비 등록 폼" },
{ "num": "06", "title": "설비 상세", "desc": "설비 정보, 점검항목, 수리이력, 사진 탭" },
{ "num": "07", "title": "점검 그리드", "desc": "6주기 점검 현황 그리드" },
{ "num": "08", "title": "수리이력", "desc": "수리이력 목록 및 등록" },
{ "num": "09", "title": "엑셀 Import", "desc": "엑셀 파일 업로드 및 일괄 등록" }
],
"mainMenus": [
{
"title": "설비 대시보드",
"children": ["현황 통계", "유형별 분포", "최근 수리"]
},
{
"title": "설비 대장",
"children": ["목록", "등록", "상세/수정"]
},
{
"title": "점검 관리",
"children": ["점검 그리드", "점검항목 설정"]
},
{
"title": "수리이력",
"children": ["목록", "등록/수정"]
},
{
"title": "Import",
"children": ["엑셀 업로드"]
}
],
"screens": [
{
"taskName": "설비 대시보드",
"route": "/equipment",
"screenName": "설비 현황 대시보드",
"screenId": "EQP_001",
"descriptions": [
{
"title": "현황 통계 카드",
"content": "전체/가동/유휴/폐기 설비 수를 카드 형태로 표시. 각 카드 클릭 시 해당 상태 필터 적용된 목록으로 이동",
"markerX": 1.8,
"markerY": 1.5
},
{
"title": "유형별 분포 차트",
"content": "포밍기/미싱기/샤링기/V컷팅기/절곡기/프레스/드릴/기타별 설비 수를 PieChart로 시각화",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "최근 수리이력",
"content": "최근 5건의 수리이력을 테이블로 표시. 설비명, 수리일, 보전구분(사내/외주), 수리내용 포함",
"markerX": 4.2,
"markerY": 2.8
},
{
"title": "이번 달 점검 현황",
"content": "당월 점검 완료율, 미점검 설비 수 등 점검 진행 상황 요약",
"markerX": 4.2,
"markerY": 4.2
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 현황 대시보드", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "0d9488", "text": "전체\n128대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.9, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "22c55e", "text": "가동\n95대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 4.2, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "f59e0b", "text": "유휴\n25대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 5.5, "y": 1.7, "w": 1.2, "h": 0.7, "fill": "dc2626", "text": "폐기\n8대", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 2.2, "fill": "FFFFFF", "text": "[유형별 분포 PieChart]\n포밍기 32%\n미싱기 25%\n절곡기 18%\n기타 25%", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.2, "y": 2.6, "w": 2.5, "h": 1.3, "fill": "FFFFFF", "text": "최근 수리이력\n───────────────\n포밍기-01 | 03.10 | 사내\n미싱기-03 | 03.08 | 외주\n절곡기-02 | 03.05 | 사내", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.2, "y": 4.0, "w": 2.5, "h": 0.8, "fill": "FFFFFF", "text": "이번 달 점검 현황\n완료율: 78% (94/120)\n미점검: 26건", "fontSize": 7, "color": "1e293b"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry",
"screenName": "설비 대장 목록",
"screenId": "EQP_002",
"descriptions": [
{
"title": "상태 필터 탭",
"content": "전체/가동(active)/유휴(idle)/폐기(disposed) 탭으로 상태별 필터링. 각 탭에 해당 건수 표시",
"markerX": 1.8,
"markerY": 1.5
},
{
"title": "검색 및 필터",
"content": "설비코드, 설비명으로 텍스트 검색. 생산라인(스라트/스크린/절곡/기타), 설비유형 드롭다운 필터",
"markerX": 1.8,
"markerY": 2.0
},
{
"title": "설비 목록 테이블",
"content": "설비코드, 설비명, 유형, 생산라인, 상태, 담당자, 구입일 컬럼. 행 클릭 시 상세 페이지로 이동",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "액션 버튼",
"content": "설비 등록 버튼, 엑셀 다운로드 버튼. UniversalListPage 템플릿 적용",
"markerX": 5.8,
"markerY": 1.5
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 대장", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "0d9488", "text": "전체 128", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.55, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "가동 95", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 3.5, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "유휴 25", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.45, "y": 1.65, "w": 0.9, "h": 0.3, "fill": "e2e8f0", "text": "폐기 8", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.8, "y": 1.65, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "+ 설비 등록", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.05, "w": 2.0, "h": 0.3, "fill": "FFFFFF", "text": "🔍 설비코드/설비명 검색", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 3.7, "y": 2.05, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "생산라인 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 2.05, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "설비유형 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "설비코드 | 설비명 | 유형 | 생산라인 | 상태 | 담당자 | 구입일", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.85, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "FM-001 | 포밍기 1호 | 포밍기 | 스라트 | 가동 | 김철수 | 2023-05-10", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.15, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "MS-003 | 미싱기 3호 | 미싱기 | 스크린 | 가동 | 박영희 | 2024-01-15", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.45, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "SH-002 | 샤링기 2호 | 샤링기 | 절곡 | 유휴 | 이민수 | 2022-11-20", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.75, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "VC-001 | V컷팅기 1호 | V컷팅기 | 스라트 | 가동 | 김철수 | 2024-06-01", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.05, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "PR-004 | 프레스 4호 | 프레스 | 기타 | 폐기 | - | 2019-03-22", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 3.0, "y": 4.5, "w": 2.0, "h": 0.3, "text": "< 1 2 3 4 5 >", "fontSize": 8, "color": "64748b"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/create",
"screenName": "설비 등록",
"screenId": "EQP_003",
"descriptions": [
{
"title": "기본 정보 입력",
"content": "설비코드(tenant 내 unique), 설비명, 설비유형(드롭다운 8종), 생산라인(드롭다운 4종), 상태(active/idle/disposed)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "담당자 지정",
"content": "정 담당자(manager_id), 부 담당자(sub_manager_id) 선택. 사용자 목록에서 검색 선택",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "구입/설치 정보",
"content": "구입일, 설치일(DatePicker), 구입가격(decimal), 내용연수(년), 메모(textarea)",
"markerX": 1.8,
"markerY": 3.6
},
{
"title": "공정 매핑",
"content": "설비가 소속된 공정을 N:N으로 선택. 주 설비 여부(is_primary) 체크박스",
"markerX": 4.5,
"markerY": 3.6
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 등록", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비코드 * FM-005", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 1.7, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비명 * 포밍기 5호", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "설비유형 포밍기 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "생산라인 스라트 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "상태 가동 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "정 담당자 김철수 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "부 담당자 박영희 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "구입일 2026-03-01", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 3.3, "y": 3.5, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "설치일 2026-03-10", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.95, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "구입가격 35,000,000", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 3.3, "y": 3.95, "w": 1.6, "h": 0.35, "fill": "FFFFFF", "text": "내용연수 10년", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 5.0, "y": 3.5, "w": 1.8, "h": 0.8, "fill": "f1f5f9", "text": "공정 매핑\n☑ 절곡공정 (주)\n☐ 포장공정\n☑ 조립공정", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.4, "w": 5.3, "h": 0.5, "fill": "FFFFFF", "text": "메모\n특이사항 입력...", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 5.4, "y": 5.0, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "저장", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.2, "y": 5.0, "w": 0.7, "h": 0.3, "fill": "64748b", "text": "취소", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/{id}",
"screenName": "설비 상세 (기본정보 탭)",
"screenId": "EQP_004",
"descriptions": [
{
"title": "탭 구조",
"content": "기본정보 | 점검항목 | 수리이력 | 사진 4개 탭. 현재 활성 탭 하이라이트 표시",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "기본정보 그리드",
"content": "설비 기본 정보를 라벨:값 그리드로 표시. 수정 버튼 클릭 시 수정 페이지로 이동",
"markerX": 1.8,
"markerY": 2.3
},
{
"title": "상태 배지",
"content": "가동(녹색)/유휴(주황)/폐기(적색) 상태를 컬러 배지로 표시",
"markerX": 5.5,
"markerY": 1.5
},
{
"title": "액션 버튼",
"content": "수정, 삭제(SoftDelete), 복원 버튼. 삭제된 설비는 복원 버튼 표시",
"markerX": 5.5,
"markerY": 5.0
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 4.0, "h": 0.4, "text": "포밍기 1호 (FM-001)", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 5.8, "y": 1.25, "w": 0.6, "h": 0.25, "fill": "22c55e", "text": "가동", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "0d9488", "text": "기본정보", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.8, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "점검항목", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.0, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "수리이력", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "e2e8f0", "text": "사진", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.1, "w": 5.3, "h": 2.8, "fill": "FFFFFF"},
{"type": "rect", "x": 1.8, "y": 2.2, "w": 1.0, "h": 0.25, "text": "설비코드", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.2, "w": 1.5, "h": 0.25, "text": "FM-001", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.2, "w": 1.0, "h": 0.25, "text": "설비명", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.2, "w": 1.3, "h": 0.25, "text": "포밍기 1호", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 2.55, "w": 1.0, "h": 0.25, "text": "설비유형", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.55, "w": 1.5, "h": 0.25, "text": "포밍기", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.55, "w": 1.0, "h": 0.25, "text": "생산라인", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.55, "w": 1.3, "h": 0.25, "text": "스라트", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 2.9, "w": 1.0, "h": 0.25, "text": "정 담당자", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 2.9, "w": 1.5, "h": 0.25, "text": "김철수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 2.9, "w": 1.0, "h": 0.25, "text": "부 담당자", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 2.9, "w": 1.3, "h": 0.25, "text": "박영희", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 3.25, "w": 1.0, "h": 0.25, "text": "구입일", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 3.25, "w": 1.5, "h": 0.25, "text": "2023-05-10", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 3.25, "w": 1.0, "h": 0.25, "text": "설치일", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 3.25, "w": 1.3, "h": 0.25, "text": "2023-06-01", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.8, "y": 3.6, "w": 1.0, "h": 0.25, "text": "구입가격", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 2.8, "y": 3.6, "w": 1.5, "h": 0.25, "text": "35,000,000원", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 4.5, "y": 3.6, "w": 1.0, "h": 0.25, "text": "내용연수", "fontSize": 7, "color": "64748b", "bold": true},
{"type": "rect", "x": 5.5, "y": 3.6, "w": 1.3, "h": 0.25, "text": "10년", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 5.5, "y": 4.95, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "수정", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.3, "y": 4.95, "w": 0.7, "h": 0.3, "fill": "dc2626", "text": "삭제", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "설비 대장",
"route": "/equipment/registry/{id}#inspection-items",
"screenName": "설비 상세 (점검항목 탭)",
"screenId": "EQP_005",
"descriptions": [
{
"title": "주기 선택 탭",
"content": "일일/주간/월간/2개월/분기/반년 6개 주기 탭. 선택한 주기의 점검항목 표시",
"markerX": 1.8,
"markerY": 2.2
},
{
"title": "점검항목 목록",
"content": "항목번호, 점검개소, 점검항목, 시기(가동중/정지중), 점검방법 컬럼. 인라인 수정 지원",
"markerX": 1.8,
"markerY": 2.9
},
{
"title": "항목 추가/복사",
"content": "점검항목 추가 버튼으로 새 항목 생성. 주기간 복사 기능으로 다른 주기의 항목을 일괄 복사",
"markerX": 5.0,
"markerY": 2.2
},
{
"title": "항목 삭제",
"content": "각 행의 삭제 버튼으로 개별 항목 삭제. 삭제 시 확인 다이얼로그 표시",
"markerX": 6.5,
"markerY": 2.9
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 4.0, "h": 0.4, "text": "포밍기 1호 (FM-001)", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "기본정보", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 2.8, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "0d9488", "text": "점검항목", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 4.0, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "e2e8f0", "text": "수리이력", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "e2e8f0", "text": "사진", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "0d9488", "text": "일일", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 2.3, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "주간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.0, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "월간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.7, "y": 2.1, "w": 0.7, "h": 0.25, "fill": "f1f5f9", "text": "2개월", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 2.1, "w": 1.0, "h": 0.25, "fill": "0d9488", "text": "+ 항목 추가", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 6.1, "y": 2.1, "w": 0.8, "h": 0.25, "fill": "64748b", "text": "주기 복사", "fontSize": 7, "color": "FFFFFF"},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.3, "fill": "1e293b", "text": "No | 점검개소 | 점검항목 | 시기 | 점검방법 | 삭제", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.8, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "1 | 유압부 | 유압 호스 상태 | 가동중 | 육안 확인 후 이상 시 교체 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.1, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2 | 전기부 | 전원 케이블 | 정지중 | 절연 저항 측정 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.4, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "3 | 구동부 | 벨트 장력 | 가동중 | 텐션게이지 측정 | X", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.7, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "4 | 안전장치 | 비상정지 버튼 | 정지중 | 작동 테스트 | X", "fontSize": 7, "color": "1e293b"}
]
},
{
"taskName": "점검 관리",
"route": "/equipment/inspections",
"screenName": "점검 그리드",
"screenId": "EQP_006",
"descriptions": [
{
"title": "주기/기간 선택",
"content": "6개 주기 탭(일일~반년) 선택. 일일: 년-월 선택(1~31일 열), 그 외: 년 선택(주간:1~52주, 월간:1~12월 등)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "점검 그리드",
"content": "행: 설비x점검항목, 열: 날짜/기간. 셀 클릭 시 결과 순환(빈칸->O->X->Triangle->빈칸). 색상으로 구분",
"markerX": 1.8,
"markerY": 2.8
},
{
"title": "종합판정/노트",
"content": "설비별 종합판정(OK/NG), 수리내역, 이상내용 편집 가능. 인라인 수정 지원",
"markerX": 5.5,
"markerY": 2.8
},
{
"title": "생산라인 필터",
"content": "생산라인별 필터로 해당 라인 설비만 표시. 전체/스라트/스크린/절곡/기타",
"markerX": 5.0,
"markerY": 1.7
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "설비 점검", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "0d9488", "text": "일일", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 2.15, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "주간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 2.7, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "월간", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.25, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "2개월", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 3.8, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "분기", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 4.35, "y": 1.65, "w": 0.55, "h": 0.25, "fill": "f1f5f9", "text": "반년", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.0, "y": 1.65, "w": 1.0, "h": 0.25, "fill": "FFFFFF", "text": "2026-03 ▼", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 6.1, "y": 1.65, "w": 0.8, "h": 0.25, "fill": "FFFFFF", "text": "전체 ▼", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.0, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "설비 | 항목 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | 판정", "fontSize": 6, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.35, "w": 5.3, "h": 0.55, "fill": "FFFFFF", "text": "포밍기 유압호스 O O O - - O X △ ... OK\n1호 벨트장력 O O O - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.9, "w": 5.3, "h": 0.55, "fill": "f8fafc", "text": "미싱기 전원케이블 O O X - - △ O O ... NG\n3호 안전장치 O O O - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.45, "w": 5.3, "h": 0.55, "fill": "FFFFFF", "text": "샤링기 유압압력 O O O - - O O O ... OK\n2호 칼날마모 O X △ - - O O O ... OK", "fontSize": 6, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.1, "w": 5.3, "h": 0.5, "fill": "f1f5f9", "text": "범례: O = 양호(good) X = 불량(bad) △ = 수리(repaired) - = 휴일/주말\n셀 클릭 시: 빈칸 -> O -> X -> △ -> 빈칸 순환", "fontSize": 7, "color": "64748b"}
]
},
{
"taskName": "수리이력",
"route": "/equipment/repairs",
"screenName": "수리이력 목록",
"screenId": "EQP_007",
"descriptions": [
{
"title": "필터 영역",
"content": "설비 선택(드롭다운), 보전구분(사내/외주), 날짜 범위(시작일~종료일), 텍스트 검색(수리내용)",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "수리이력 테이블",
"content": "수리일, 설비명, 보전구분, 수리내용, 수리시간, 비용, 수리자/업체 컬럼. 행 클릭 시 수정 모달",
"markerX": 1.8,
"markerY": 2.6
},
{
"title": "수리이력 등록",
"content": "수리이력 등록 버튼 클릭 시 등록 폼(모달 또는 별도 페이지)으로 이동",
"markerX": 5.8,
"markerY": 1.3
},
{
"title": "비용 집계",
"content": "조회된 수리이력의 총 비용, 사내/외주 비용 비율 등 하단 요약 표시",
"markerX": 1.8,
"markerY": 4.5
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "수리이력", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 5.8, "y": 1.25, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "+ 수리 등록", "fontSize": 8, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 1.5, "h": 0.3, "fill": "FFFFFF", "text": "설비 선택 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 3.2, "y": 1.7, "w": 1.0, "h": 0.3, "fill": "FFFFFF", "text": "보전구분 ▼", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 4.3, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "2026-01 ~ 2026-03", "fontSize": 7, "color": "64748b"},
{"type": "rect", "x": 5.6, "y": 1.7, "w": 1.2, "h": 0.3, "fill": "FFFFFF", "text": "🔍 검색", "fontSize": 8, "color": "94a3b8"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 5.3, "h": 0.35, "fill": "1e293b", "text": "수리일 | 설비명 | 보전구분 | 수리내용 | 시간 | 비용 | 수리자/업체", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.5, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "2026-03-10 | 포밍기 1호 | 사내 | 유압호스 교체 | 2.0h | 150,000원 | 김철수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.8, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2026-03-08 | 미싱기 3호 | 외주 | 모터 오버홀 | 8.0h | 2,500,000원 | ABC기계", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.1, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "2026-03-05 | 샤링기 2호 | 사내 | 칼날 교체 | 1.5h | 85,000원 | 이민수", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.4, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "2026-02-28 | 절곡기 1호 | 외주 | 유압실린더 교체 | 4.0h | 1,200,000원 | XYZ설비", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 3.0, "y": 3.9, "w": 2.0, "h": 0.3, "text": "< 1 2 3 >", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 4.3, "w": 5.3, "h": 0.4, "fill": "f1f5f9", "text": "총 수리비용: 3,935,000원 | 사내: 235,000원 (6%) | 외주: 3,700,000원 (94%)", "fontSize": 8, "color": "1e293b", "bold": true}
]
},
{
"taskName": "수리이력",
"route": "/equipment/repairs/create",
"screenName": "수리이력 등록",
"screenId": "EQP_008",
"descriptions": [
{
"title": "설비 선택",
"content": "수리 대상 설비를 드롭다운에서 선택. 설비코드+설비명 표시. 필수 입력",
"markerX": 1.8,
"markerY": 1.7
},
{
"title": "수리 정보",
"content": "수리일(DatePicker), 보전구분(사내/외주 라디오), 수리시간(시간 단위), 비용(원)",
"markerX": 1.8,
"markerY": 2.3
},
{
"title": "수리 상세",
"content": "수리내용(textarea), 수리자(사내 선택) 또는 외주업체명(텍스트 입력). 보전구분에 따라 전환",
"markerX": 1.8,
"markerY": 3.3
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "수리이력 등록", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 5.3, "h": 0.35, "fill": "FFFFFF", "text": "설비 * FM-001 포밍기 1호 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리일 * 2026-03-12", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.15, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "보전구분 * ● 사내 ○ 외주", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리시간 2.0 시간", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 4.3, "y": 2.6, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리비용 150,000 원", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.05, "w": 2.5, "h": 0.35, "fill": "FFFFFF", "text": "수리자 김철수 ▼", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 5.3, "h": 1.0, "fill": "FFFFFF", "text": "수리내용\n유압호스 노후로 인한 교체 작업.\n기존 호스 균열 발견, 동일 규격 신품으로 교체 완료.\n압력 테스트 정상 확인.", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 5.4, "y": 4.7, "w": 0.7, "h": 0.3, "fill": "0d9488", "text": "저장", "fontSize": 9, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 6.2, "y": 4.7, "w": 0.7, "h": 0.3, "fill": "64748b", "text": "취소", "fontSize": 9, "color": "FFFFFF"}
]
},
{
"taskName": "Import",
"route": "/equipment/import",
"screenName": "엑셀 Import",
"screenId": "EQP_009",
"descriptions": [
{
"title": "파일 업로드",
"content": "엑셀 파일(.xlsx) 드래그&드롭 또는 파일 선택. 업로드 후 자동으로 미리보기 단계로 전환",
"markerX": 1.8,
"markerY": 1.8
},
{
"title": "미리보기 테이블",
"content": "파싱된 데이터를 테이블로 표시. 한글/영문 헤더 자동 매핑. 오류 행 빨간색 표시. 이미지(Drawing) 자동 추출 표시",
"markerX": 1.8,
"markerY": 2.7
},
{
"title": "중복 처리 옵션",
"content": "동일 설비코드 존재 시: 건너뜀(skip) 또는 덮어쓰기(overwrite) 선택. 라디오 버튼",
"markerX": 1.8,
"markerY": 4.2
},
{
"title": "Import 실행",
"content": "Import 실행 버튼 클릭 시 일괄 등록. 결과 요약(성공/실패/건너뜀 건수) 표시",
"markerX": 5.8,
"markerY": 4.2
}
],
"wireframeElements": [
{"type": "rect", "x": 1.6, "y": 1.2, "w": 5.3, "h": 0.4, "text": "엑셀 Import", "fontSize": 14, "bold": true, "color": "1e293b", "fill": "f8fafc"},
{"type": "rect", "x": 1.6, "y": 1.7, "w": 5.3, "h": 0.8, "fill": "f1f5f9", "text": "📁 엑셀 파일을 드래그하거나 클릭하여 선택하세요\n(.xlsx 형식, 최대 10MB)", "fontSize": 9, "color": "64748b"},
{"type": "rect", "x": 1.6, "y": 2.6, "w": 5.3, "h": 0.3, "fill": "1e293b", "text": "설비코드 | 설비명 | 유형 | 라인 | 상태 | 구입일 | 이미지", "fontSize": 7, "color": "FFFFFF", "bold": true},
{"type": "rect", "x": 1.6, "y": 2.9, "w": 5.3, "h": 0.3, "fill": "FFFFFF", "text": "FM-006 | 포밍기 6호 | 포밍기 | 스라트 | 가동 | 2026-01 | [img]", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.2, "w": 5.3, "h": 0.3, "fill": "f8fafc", "text": "MS-005 | 미싱기 5호 | 미싱기 | 스크린 | 가동 | 2025-11 | [img]", "fontSize": 7, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 3.5, "w": 5.3, "h": 0.3, "fill": "fef2f2", "text": "FM-001 | 포밍기 1호 | 포밍기 | 스라트 | 가동 | 2023-05 | - (중복!)", "fontSize": 7, "color": "dc2626"},
{"type": "rect", "x": 1.6, "y": 3.9, "w": 3.0, "h": 0.35, "fill": "f1f5f9", "text": "중복 처리: ● 건너뜀(skip) ○ 덮어쓰기(overwrite)", "fontSize": 8, "color": "1e293b"},
{"type": "rect", "x": 1.6, "y": 4.35, "w": 3.0, "h": 0.3, "text": "총 3건 | 신규: 2건 | 중복: 1건 | 오류: 0건", "fontSize": 8, "color": "64748b"},
{"type": "rect", "x": 5.8, "y": 4.35, "w": 1.0, "h": 0.3, "fill": "0d9488", "text": "Import 실행", "fontSize": 8, "color": "FFFFFF", "bold": true}
]
}
]
}

View File

@@ -1,369 +1,369 @@
# 영업파트너 데모 테넌트 정책
> **작성일**: 2026-03-13
> **상태**: 설계 확정
---
## 1. 개요
### 1.1 목적
영업파트너가 SAM 시스템을 직접 체험하고, 잠재 고객에게 실제 화면을 시연하여 계약 전환율을 높인다.
### 1.2 핵심 원칙
- 영업파트너에게 "보여줄 수 있는 무기"를 제공한다
- 고객이 직접 체험하여 의사결정을 촉진한다
- 체험 데이터를 정식 테넌트로 전환하여 Lock-in 효과를 만든다
### 1.3 현황 및 문제점
| 항목 | 현재 상태 | 문제 |
|------|----------|------|
| 파트너 영업 도구 | 수당 시뮬레이터(`/price/`)만 존재 | 고객에게 실제 화면을 보여줄 수 없음 |
| Trial 상태 | `subscriptions` 모델에 정의만 됨 | 활성화 로직 미구현 |
| 데모 환경 | 없음 | 파트너가 SAM을 직접 체험하지 못하고 영업함 |
| 고객 설득 | 제안서 + 가격표만 제공 | "보여줄 수 없는 2천만원짜리 솔루션" → 전환율 저하 |
---
## 2. 데모 테넌트 전략 (3-Tier)
### 2.1 티어 구조
```
┌─────────────────────────────────────────────────────────────────────┐
│ SAM 데모 프로그램 3-Tier │
├──────────────┬──────────────────┬───────────────────────────────────┤
│ Tier 1 │ Tier 2 │ Tier 3 │
│ 쇼케이스 │ 파트너 데모 │ 고객 체험 │
│ (공용) │ (파트너별 1개) │ (영업건별) │
├──────────────┼──────────────────┼───────────────────────────────────┤
│ 읽기 전용 │ 풀 기능 체험 │ 고객 맞춤 데모 │
│ 샘플 데이터 │ 샘플+직접 입력 │ 고객 업종 데이터 │
│ 기간 제한 X │ 파트너 활동 중 │ 30일 제한 │
│ URL 1개 공유 │ 파트너 로그인 │ 고객 직접 로그인 │
└──────────────┴──────────────────┴───────────────────────────────────┘
```
### 2.2 Tier 1: 쇼케이스 (공용 데모)
**목적**: 누구나 SAM이 어떤 시스템인지 3분 안에 파악
| 항목 | 내용 |
|------|------|
| **대상** | 모든 방문자, 파트너 후보, 잠재 고객 |
| **접근** | 공개 URL (예: `demo.codebridge-x.com`) |
| **계정** | 게스트 계정 자동 로그인 (ID/PW 없음) |
| **데이터** | 제조업 샘플 데이터 (가상 회사 "데모제조") |
| **기능** | 읽기 전용 — 조회, 검색, 리포트 확인만 가능 |
| **리셋** | 매일 자정 자동 리셋 (Scheduler) |
| **기간** | 무제한 |
| **비용** | 없음 (마케팅 비용) |
**포함 샘플 데이터**:
- 품목 50개, 거래처 20개, 견적 10건, 수주 15건
- 생산계획 5건, 출하 10건
- 직원 10명, 부서 3개
- 최근 3개월 매출/생산 데이터 (차트용)
**활용 시나리오**:
```
파트너: "SAM이 어떤 시스템인지 보여드리겠습니다"
→ 스마트폰/태블릿으로 demo.codebridge-x.com 접속
→ 실시간 화면 시연 (3~5분)
→ "이런 시스템을 귀사 맞춤으로 구축해드립니다"
```
### 2.3 Tier 2: 파트너 데모 테넌트 (파트너별 전용)
**목적**: 파트너가 SAM을 직접 사용해보고, 고객 영업 시 맞춤 시연 가능
| 항목 | 내용 |
|------|------|
| **대상** | 승인된 영업파트너 (status=`active`) |
| **생성 시점** | 파트너 승인 시 자동 생성 |
| **계정** | 파트너 본인 계정으로 로그인 |
| **데이터** | 업종별 샘플 데이터 프리셋 + 직접 입력 가능 |
| **기능** | 전체 기능 사용 가능 (일부 제한) |
| **리셋** | 월 1회 자동 리셋 또는 파트너 요청 시 수동 리셋 |
| **기간** | 파트너 활동 기간 동안 유지 |
| **비용** | 없음 (파트너 지원) |
**기능 제한**:
| 기능 | 사용 가능 | 제한 사항 |
|------|:---------:|----------|
| 품목/거래처 관리 | ✅ | 최대 100개 |
| 견적/수주/출하 | ✅ | 최대 50건 |
| 생산관리 | ✅ | 최대 30건 |
| 사용자 초대 | ✅ | 최대 5명 |
| 리포트/통계 | ✅ | 제한 없음 |
| 바로빌 연동 | ❌ | 실제 금융 연동 차단 |
| 파일 저장 | ✅ | 최대 1GB |
| AI 토큰 | ✅ | 월 10만 토큰 |
**업종별 샘플 데이터 프리셋**:
| 프리셋 | 포함 데이터 | 대상 고객 |
|--------|-----------|----------|
| 제조업 기본 | 원자재→생산→출하 흐름 | 일반 제조업체 |
| 블라인드/스크린 | 커튼/블라인드 품목 + 시공 | 인테리어 업체 |
| 시공/건설 | 공사관리 + 자재 | 건설/시공 업체 |
| 유통/도소매 | 입출고 + 재고 | 유통 업체 |
### 2.4 Tier 3: 고객 체험 테넌트 (영업건별)
**목적**: 계약 직전 고객이 자기 데이터로 직접 체험하여 의사결정 촉진
| 항목 | 내용 |
|------|------|
| **대상** | 계약 검토 단계의 잠재 고객 |
| **생성** | 파트너가 Sales 시스템에서 요청 → 본사 승인 |
| **계정** | 고객 담당자 이메일로 초대 |
| **데이터** | 고객이 직접 입력하거나 CSV 임포트 |
| **기능** | Tier 2와 동일한 제한 |
| **기간** | 30일 (1회 연장 가능, 최대 60일) |
| **비용** | 없음 (영업 비용) |
| **전환** | 계약 시 → 데모 데이터를 정식 테넌트로 마이그레이션 가능 |
**핵심 가치 — "체험 데이터 → 정식 전환" 마이그레이션**:
```
고객 체험 (30일)
├── 품목 등록, 거래처 등록, 견적 작성 체험
├── "이미 입력한 데이터가 있으니 계약하면 바로 쓸 수 있다"
└── 계약 전환 시 → 데모 데이터를 정식 테넌트로 이관
→ Lock-in 효과 + 온보딩 시간 단축
```
---
## 3. 기술 구현 방안
### 3.1 데이터 모델 변경
```sql
-- tenants 테이블 확장
ALTER TABLE tenants ADD COLUMN tenant_type ENUM('production', 'demo_showcase', 'demo_partner', 'demo_trial') DEFAULT 'production';
ALTER TABLE tenants ADD COLUMN demo_expires_at DATETIME NULL;
ALTER TABLE tenants ADD COLUMN demo_source_partner_id BIGINT UNSIGNED NULL;
ALTER TABLE tenants ADD COLUMN demo_preset VARCHAR(50) NULL;
ALTER TABLE tenants ADD COLUMN demo_limits JSON NULL;
```
### 3.2 구독 모델 활용
기존 `subscriptions.status``trial` 상태를 활성화:
```
demo_showcase → subscription 없음 (무료 공용)
demo_partner → subscription.status = 'trial', plan = 'demo_partner'
demo_trial → subscription.status = 'trial', plan = 'demo_trial'
expires_at = created_at + 30 days
```
### 3.3 제한 적용 미들웨어
```php
// DemoLimitMiddleware
class DemoLimitMiddleware
{
public function handle($request, Closure $next)
{
$tenant = currentTenant();
if ($tenant->isDemoTenant()) {
// 1. 기간 만료 체크
if ($tenant->demo_expires_at && now()->gt($tenant->demo_expires_at)) {
return response()->json(['error' => '체험 기간이 만료되었습니다.'], 403);
}
// 2. 수량 제한 체크
$limits = $tenant->demo_limits;
// 3. 금지 기능 체크 (바로빌 등 외부 연동)
if ($this->isBlockedFeature($request)) {
return response()->json(['error' => '데모에서 사용할 수 없는 기능입니다.'], 403);
}
}
return $next($request);
}
}
```
### 3.4 샘플 데이터 시더 (프리셋별)
```php
// DemoDataSeeder — 테넌트 생성 시 호출
class DemoDataSeeder
{
public function seed(Tenant $tenant, string $preset = 'manufacturing')
{
$presets = [
'manufacturing' => ManufacturingPreset::class,
'blinds' => BlindsPreset::class,
'construction' => ConstructionPreset::class,
'distribution' => DistributionPreset::class,
];
$presets[$preset]::seed($tenant);
}
}
```
### 3.5 데모 → 정식 전환 프로세스
```
[Tier 3 체험 테넌트]
↓ 계약 체결
[전환 프로세스]
1. tenant_type = 'production' 변경
2. demo_limits = NULL (제한 해제)
3. demo_expires_at = NULL
4. subscription → status='active', plan='starter'/'business'
5. 기존 데이터 유지 (마이그레이션 불필요)
6. 추가 기능 활성화 (바로빌 등 외부 연동)
```
---
## 4. 영업 지원 도구 연계
### 4.1 Sales 시스템 연동
| 기능 | 설명 | API |
|------|------|-----|
| 데모 테넌트 요청 | 파트너가 고객 체험 테넌트 생성 요청 | `POST /api/demo-tenants` |
| 데모 현황 조회 | 내가 생성한 데모 테넌트 목록 | `GET /api/demo-tenants` |
| 데모 리셋 | 파트너 데모 데이터 초기화 | `POST /api/demo-tenants/{id}/reset` |
| 체험 연장 | 고객 체험 기간 연장 요청 | `POST /api/demo-tenants/{id}/extend` |
| 전환 요청 | 체험 → 정식 전환 요청 | `POST /api/demo-tenants/{id}/convert` |
### 4.2 파트너 대시보드 지표
```
┌──────────────────────────────────────────────────────┐
│ 내 데모 현황 │
├──────────────┬──────────────┬────────────────────────┤
│ 파트너 데모 │ 고객 체험 │ 전환율 │
│ 1개 (활성) │ 3개 (진행중) │ 40% (2/5) │
│ │ 1개 (만료) │ │
├──────────────┴──────────────┴────────────────────────┤
│ 최근 체험 활동 │
│ - [데모제조A] 3시간 전 견적서 3건 작성 │
│ - [시공업체B] 1일 전 품목 45개 등록 │
│ - [유통사C] 5일 전 마지막 접속 (비활성 경고) │
└──────────────────────────────────────────────────────┘
```
---
## 5. 영업 프로세스 통합
### 5.1 데모 활용 영업 플로우
```
1단계: 초기 접촉
└── Tier 1 쇼케이스 URL 공유 (3분 시연)
└── "이런 시스템입니다" — 관심 유발
2단계: 심층 상담
└── 파트너 Tier 2 데모로 고객 업종 맞춤 시연 (30분)
└── 프리셋 데이터로 실제 업무 흐름 보여주기
└── 가격 시뮬레이터로 견적 제시
3단계: 고객 체험 (의사결정 촉진)
└── Tier 3 고객 체험 테넌트 생성 (30일)
└── 고객이 직접 데이터 입력하며 체험
└── 파트너가 체험 활동 모니터링
└── "이미 입력한 데이터 살려드립니다" — 전환 유도
4단계: 계약 전환
└── 체험 데이터 → 정식 테넌트로 전환
└── 온보딩 기간 단축 (이미 익숙함)
└── 수당 정산 시작
```
### 5.2 KPI 및 전환 추적
| 지표 | 측정 대상 | 목표 |
|------|----------|------|
| 데모 요청 수 | 파트너별 월간 Tier 3 생성 수 | 파트너당 3건/월 |
| 체험 활성도 | 고객 로그인 횟수, 데이터 입력량 | 주 3회 이상 접속 |
| 체험→계약 전환율 | Tier 3 → 정식 계약 비율 | 30% 이상 |
| 평균 전환 기간 | 체험 시작 → 계약 체결 일수 | 21일 이내 |
| 데모 비활성 알림 | 7일 미접속 고객 자동 알림 | 파트너에게 팔로업 유도 |
---
## 6. 비용 및 리소스
### 6.1 인프라 비용
| 항목 | Tier 1 (1개) | Tier 2 (파트너 N개) | Tier 3 (건별) |
|------|:-----------:|:------------------:|:------------:|
| DB 용량 | ~50MB | ~100MB x N | ~100MB x 건 |
| 파일 저장 | 고정 | 1GB x N | 1GB x 건 |
| AI 토큰 | 없음 | 10만/월 x N | 10만/30일 x 건 |
| 월 예상 비용 | ~0원 | ~5천원 x N | ~5천원 x 건 |
### 6.2 개발 공수
| 작업 | 우선순위 | 규모 |
|------|:-------:|------|
| `tenant_type` 컬럼 + 마이그레이션 | 🔴 필수 | 소 |
| `DemoLimitMiddleware` | 🔴 필수 | 중 |
| 샘플 데이터 프리셋 (제조업 1종) | 🔴 필수 | 중 |
| Tier 1 쇼케이스 자동 리셋 스케줄러 | 🔴 필수 | 소 |
| 파트너 승인 시 Tier 2 자동 생성 | 🟡 중요 | 소 |
| Tier 3 생성/만료/전환 API | 🟡 중요 | 중 |
| Sales 대시보드 데모 현황 UI | 🟡 중요 | 중 |
| 추가 프리셋 (블라인드/시공/유통) | 🟢 권장 | 중 |
| 체험 활동 모니터링 + 알림 | 🟢 권장 | 중 |
| 데모 → 정식 데이터 전환 로직 | 🟢 권장 | 소 |
---
## 7. 단계별 실행 로드맵
```
Phase 1 (즉시) ─────────────────────────────────────
✅ Tier 1 쇼케이스: 공용 데모 테넌트 수동 생성
✅ 샘플 데이터 수동 입력 (제조업 기본)
✅ demo.codebridge-x.com 도메인 매핑
→ 파트너에게 즉시 공유 가능한 URL 확보
Phase 2 (자동화) ───────────────────────────────────
✅ tenant_type 컬럼 + DemoLimitMiddleware
✅ Tier 2 파트너 데모 자동 생성
✅ 샘플 데이터 시더 (프리셋 1종)
✅ 일일 리셋 스케줄러
Phase 3 (영업 연계) ────────────────────────────────
✅ Tier 3 고객 체험 생성/관리 API
✅ Sales 대시보드 연동
✅ 체험 → 정식 전환 프로세스
✅ 활동 모니터링 + 비활성 알림
Phase 4 (고도화) ───────────────────────────────────
✅ 추가 업종 프리셋
✅ 전환율 분석 대시보드
✅ A/B 테스트 (데모 제공 vs 미제공 전환율 비교)
```
---
## 관련 문서
- [features/sales/README.md](README.md) — 영업 관리 모듈 전체
- [features/sales/partners.md](partners.md) — 파트너 관리 기능
- [rules/customer-pricing.md](../../rules/customer-pricing.md) — 고객 요금표
- [rules/partner-commission.md](../../rules/partner-commission.md) — 영업파트너 수당 체계
- [features/settlement/subscriptions.md](../settlement/subscriptions.md) — 구독 관리
---
**최종 업데이트**: 2026-03-13
# 영업파트너 데모 테넌트 정책
> **작성일**: 2026-03-13
> **상태**: 설계 확정
---
## 1. 개요
### 1.1 목적
영업파트너가 SAM 시스템을 직접 체험하고, 잠재 고객에게 실제 화면을 시연하여 계약 전환율을 높인다.
### 1.2 핵심 원칙
- 영업파트너에게 "보여줄 수 있는 무기"를 제공한다
- 고객이 직접 체험하여 의사결정을 촉진한다
- 체험 데이터를 정식 테넌트로 전환하여 Lock-in 효과를 만든다
### 1.3 현황 및 문제점
| 항목 | 현재 상태 | 문제 |
|------|----------|------|
| 파트너 영업 도구 | 수당 시뮬레이터(`/price/`)만 존재 | 고객에게 실제 화면을 보여줄 수 없음 |
| Trial 상태 | `subscriptions` 모델에 정의만 됨 | 활성화 로직 미구현 |
| 데모 환경 | 없음 | 파트너가 SAM을 직접 체험하지 못하고 영업함 |
| 고객 설득 | 제안서 + 가격표만 제공 | "보여줄 수 없는 2천만원짜리 솔루션" → 전환율 저하 |
---
## 2. 데모 테넌트 전략 (3-Tier)
### 2.1 티어 구조
```
┌─────────────────────────────────────────────────────────────────────┐
│ SAM 데모 프로그램 3-Tier │
├──────────────┬──────────────────┬───────────────────────────────────┤
│ Tier 1 │ Tier 2 │ Tier 3 │
│ 쇼케이스 │ 파트너 데모 │ 고객 체험 │
│ (공용) │ (파트너별 1개) │ (영업건별) │
├──────────────┼──────────────────┼───────────────────────────────────┤
│ 읽기 전용 │ 풀 기능 체험 │ 고객 맞춤 데모 │
│ 샘플 데이터 │ 샘플+직접 입력 │ 고객 업종 데이터 │
│ 기간 제한 X │ 파트너 활동 중 │ 30일 제한 │
│ URL 1개 공유 │ 파트너 로그인 │ 고객 직접 로그인 │
└──────────────┴──────────────────┴───────────────────────────────────┘
```
### 2.2 Tier 1: 쇼케이스 (공용 데모)
**목적**: 누구나 SAM이 어떤 시스템인지 3분 안에 파악
| 항목 | 내용 |
|------|------|
| **대상** | 모든 방문자, 파트너 후보, 잠재 고객 |
| **접근** | 공개 URL (예: `demo.codebridge-x.com`) |
| **계정** | 게스트 계정 자동 로그인 (ID/PW 없음) |
| **데이터** | 제조업 샘플 데이터 (가상 회사 "데모제조") |
| **기능** | 읽기 전용 — 조회, 검색, 리포트 확인만 가능 |
| **리셋** | 매일 자정 자동 리셋 (Scheduler) |
| **기간** | 무제한 |
| **비용** | 없음 (마케팅 비용) |
**포함 샘플 데이터**:
- 품목 50개, 거래처 20개, 견적 10건, 수주 15건
- 생산계획 5건, 출하 10건
- 직원 10명, 부서 3개
- 최근 3개월 매출/생산 데이터 (차트용)
**활용 시나리오**:
```
파트너: "SAM이 어떤 시스템인지 보여드리겠습니다"
→ 스마트폰/태블릿으로 demo.codebridge-x.com 접속
→ 실시간 화면 시연 (3~5분)
→ "이런 시스템을 귀사 맞춤으로 구축해드립니다"
```
### 2.3 Tier 2: 파트너 데모 테넌트 (파트너별 전용)
**목적**: 파트너가 SAM을 직접 사용해보고, 고객 영업 시 맞춤 시연 가능
| 항목 | 내용 |
|------|------|
| **대상** | 승인된 영업파트너 (status=`active`) |
| **생성 시점** | 파트너 승인 시 자동 생성 |
| **계정** | 파트너 본인 계정으로 로그인 |
| **데이터** | 업종별 샘플 데이터 프리셋 + 직접 입력 가능 |
| **기능** | 전체 기능 사용 가능 (일부 제한) |
| **리셋** | 월 1회 자동 리셋 또는 파트너 요청 시 수동 리셋 |
| **기간** | 파트너 활동 기간 동안 유지 |
| **비용** | 없음 (파트너 지원) |
**기능 제한**:
| 기능 | 사용 가능 | 제한 사항 |
|------|:---------:|----------|
| 품목/거래처 관리 | ✅ | 최대 100개 |
| 견적/수주/출하 | ✅ | 최대 50건 |
| 생산관리 | ✅ | 최대 30건 |
| 사용자 초대 | ✅ | 최대 5명 |
| 리포트/통계 | ✅ | 제한 없음 |
| 바로빌 연동 | ❌ | 실제 금융 연동 차단 |
| 파일 저장 | ✅ | 최대 1GB |
| AI 토큰 | ✅ | 월 10만 토큰 |
**업종별 샘플 데이터 프리셋**:
| 프리셋 | 포함 데이터 | 대상 고객 |
|--------|-----------|----------|
| 제조업 기본 | 원자재→생산→출하 흐름 | 일반 제조업체 |
| 블라인드/스크린 | 커튼/블라인드 품목 + 시공 | 인테리어 업체 |
| 시공/건설 | 공사관리 + 자재 | 건설/시공 업체 |
| 유통/도소매 | 입출고 + 재고 | 유통 업체 |
### 2.4 Tier 3: 고객 체험 테넌트 (영업건별)
**목적**: 계약 직전 고객이 자기 데이터로 직접 체험하여 의사결정 촉진
| 항목 | 내용 |
|------|------|
| **대상** | 계약 검토 단계의 잠재 고객 |
| **생성** | 파트너가 Sales 시스템에서 요청 → 본사 승인 |
| **계정** | 고객 담당자 이메일로 초대 |
| **데이터** | 고객이 직접 입력하거나 CSV 임포트 |
| **기능** | Tier 2와 동일한 제한 |
| **기간** | 30일 (1회 연장 가능, 최대 60일) |
| **비용** | 없음 (영업 비용) |
| **전환** | 계약 시 → 데모 데이터를 정식 테넌트로 마이그레이션 가능 |
**핵심 가치 — "체험 데이터 → 정식 전환" 마이그레이션**:
```
고객 체험 (30일)
├── 품목 등록, 거래처 등록, 견적 작성 체험
├── "이미 입력한 데이터가 있으니 계약하면 바로 쓸 수 있다"
└── 계약 전환 시 → 데모 데이터를 정식 테넌트로 이관
→ Lock-in 효과 + 온보딩 시간 단축
```
---
## 3. 기술 구현 방안
### 3.1 데이터 모델 변경
```sql
-- tenants 테이블 확장
ALTER TABLE tenants ADD COLUMN tenant_type ENUM('production', 'demo_showcase', 'demo_partner', 'demo_trial') DEFAULT 'production';
ALTER TABLE tenants ADD COLUMN demo_expires_at DATETIME NULL;
ALTER TABLE tenants ADD COLUMN demo_source_partner_id BIGINT UNSIGNED NULL;
ALTER TABLE tenants ADD COLUMN demo_preset VARCHAR(50) NULL;
ALTER TABLE tenants ADD COLUMN demo_limits JSON NULL;
```
### 3.2 구독 모델 활용
기존 `subscriptions.status``trial` 상태를 활성화:
```
demo_showcase → subscription 없음 (무료 공용)
demo_partner → subscription.status = 'trial', plan = 'demo_partner'
demo_trial → subscription.status = 'trial', plan = 'demo_trial'
expires_at = created_at + 30 days
```
### 3.3 제한 적용 미들웨어
```php
// DemoLimitMiddleware
class DemoLimitMiddleware
{
public function handle($request, Closure $next)
{
$tenant = currentTenant();
if ($tenant->isDemoTenant()) {
// 1. 기간 만료 체크
if ($tenant->demo_expires_at && now()->gt($tenant->demo_expires_at)) {
return response()->json(['error' => '체험 기간이 만료되었습니다.'], 403);
}
// 2. 수량 제한 체크
$limits = $tenant->demo_limits;
// 3. 금지 기능 체크 (바로빌 등 외부 연동)
if ($this->isBlockedFeature($request)) {
return response()->json(['error' => '데모에서 사용할 수 없는 기능입니다.'], 403);
}
}
return $next($request);
}
}
```
### 3.4 샘플 데이터 시더 (프리셋별)
```php
// DemoDataSeeder — 테넌트 생성 시 호출
class DemoDataSeeder
{
public function seed(Tenant $tenant, string $preset = 'manufacturing')
{
$presets = [
'manufacturing' => ManufacturingPreset::class,
'blinds' => BlindsPreset::class,
'construction' => ConstructionPreset::class,
'distribution' => DistributionPreset::class,
];
$presets[$preset]::seed($tenant);
}
}
```
### 3.5 데모 → 정식 전환 프로세스
```
[Tier 3 체험 테넌트]
↓ 계약 체결
[전환 프로세스]
1. tenant_type = 'production' 변경
2. demo_limits = NULL (제한 해제)
3. demo_expires_at = NULL
4. subscription → status='active', plan='starter'/'business'
5. 기존 데이터 유지 (마이그레이션 불필요)
6. 추가 기능 활성화 (바로빌 등 외부 연동)
```
---
## 4. 영업 지원 도구 연계
### 4.1 Sales 시스템 연동
| 기능 | 설명 | API |
|------|------|-----|
| 데모 테넌트 요청 | 파트너가 고객 체험 테넌트 생성 요청 | `POST /api/demo-tenants` |
| 데모 현황 조회 | 내가 생성한 데모 테넌트 목록 | `GET /api/demo-tenants` |
| 데모 리셋 | 파트너 데모 데이터 초기화 | `POST /api/demo-tenants/{id}/reset` |
| 체험 연장 | 고객 체험 기간 연장 요청 | `POST /api/demo-tenants/{id}/extend` |
| 전환 요청 | 체험 → 정식 전환 요청 | `POST /api/demo-tenants/{id}/convert` |
### 4.2 파트너 대시보드 지표
```
┌──────────────────────────────────────────────────────┐
│ 내 데모 현황 │
├──────────────┬──────────────┬────────────────────────┤
│ 파트너 데모 │ 고객 체험 │ 전환율 │
│ 1개 (활성) │ 3개 (진행중) │ 40% (2/5) │
│ │ 1개 (만료) │ │
├──────────────┴──────────────┴────────────────────────┤
│ 최근 체험 활동 │
│ - [데모제조A] 3시간 전 견적서 3건 작성 │
│ - [시공업체B] 1일 전 품목 45개 등록 │
│ - [유통사C] 5일 전 마지막 접속 (비활성 경고) │
└──────────────────────────────────────────────────────┘
```
---
## 5. 영업 프로세스 통합
### 5.1 데모 활용 영업 플로우
```
1단계: 초기 접촉
└── Tier 1 쇼케이스 URL 공유 (3분 시연)
└── "이런 시스템입니다" — 관심 유발
2단계: 심층 상담
└── 파트너 Tier 2 데모로 고객 업종 맞춤 시연 (30분)
└── 프리셋 데이터로 실제 업무 흐름 보여주기
└── 가격 시뮬레이터로 견적 제시
3단계: 고객 체험 (의사결정 촉진)
└── Tier 3 고객 체험 테넌트 생성 (30일)
└── 고객이 직접 데이터 입력하며 체험
└── 파트너가 체험 활동 모니터링
└── "이미 입력한 데이터 살려드립니다" — 전환 유도
4단계: 계약 전환
└── 체험 데이터 → 정식 테넌트로 전환
└── 온보딩 기간 단축 (이미 익숙함)
└── 수당 정산 시작
```
### 5.2 KPI 및 전환 추적
| 지표 | 측정 대상 | 목표 |
|------|----------|------|
| 데모 요청 수 | 파트너별 월간 Tier 3 생성 수 | 파트너당 3건/월 |
| 체험 활성도 | 고객 로그인 횟수, 데이터 입력량 | 주 3회 이상 접속 |
| 체험→계약 전환율 | Tier 3 → 정식 계약 비율 | 30% 이상 |
| 평균 전환 기간 | 체험 시작 → 계약 체결 일수 | 21일 이내 |
| 데모 비활성 알림 | 7일 미접속 고객 자동 알림 | 파트너에게 팔로업 유도 |
---
## 6. 비용 및 리소스
### 6.1 인프라 비용
| 항목 | Tier 1 (1개) | Tier 2 (파트너 N개) | Tier 3 (건별) |
|------|:-----------:|:------------------:|:------------:|
| DB 용량 | ~50MB | ~100MB x N | ~100MB x 건 |
| 파일 저장 | 고정 | 1GB x N | 1GB x 건 |
| AI 토큰 | 없음 | 10만/월 x N | 10만/30일 x 건 |
| 월 예상 비용 | ~0원 | ~5천원 x N | ~5천원 x 건 |
### 6.2 개발 공수
| 작업 | 우선순위 | 규모 |
|------|:-------:|------|
| `tenant_type` 컬럼 + 마이그레이션 | 🔴 필수 | 소 |
| `DemoLimitMiddleware` | 🔴 필수 | 중 |
| 샘플 데이터 프리셋 (제조업 1종) | 🔴 필수 | 중 |
| Tier 1 쇼케이스 자동 리셋 스케줄러 | 🔴 필수 | 소 |
| 파트너 승인 시 Tier 2 자동 생성 | 🟡 중요 | 소 |
| Tier 3 생성/만료/전환 API | 🟡 중요 | 중 |
| Sales 대시보드 데모 현황 UI | 🟡 중요 | 중 |
| 추가 프리셋 (블라인드/시공/유통) | 🟢 권장 | 중 |
| 체험 활동 모니터링 + 알림 | 🟢 권장 | 중 |
| 데모 → 정식 데이터 전환 로직 | 🟢 권장 | 소 |
---
## 7. 단계별 실행 로드맵
```
Phase 1 (즉시) ─────────────────────────────────────
✅ Tier 1 쇼케이스: 공용 데모 테넌트 수동 생성
✅ 샘플 데이터 수동 입력 (제조업 기본)
✅ demo.codebridge-x.com 도메인 매핑
→ 파트너에게 즉시 공유 가능한 URL 확보
Phase 2 (자동화) ───────────────────────────────────
✅ tenant_type 컬럼 + DemoLimitMiddleware
✅ Tier 2 파트너 데모 자동 생성
✅ 샘플 데이터 시더 (프리셋 1종)
✅ 일일 리셋 스케줄러
Phase 3 (영업 연계) ────────────────────────────────
✅ Tier 3 고객 체험 생성/관리 API
✅ Sales 대시보드 연동
✅ 체험 → 정식 전환 프로세스
✅ 활동 모니터링 + 비활성 알림
Phase 4 (고도화) ───────────────────────────────────
✅ 추가 업종 프리셋
✅ 전환율 분석 대시보드
✅ A/B 테스트 (데모 제공 vs 미제공 전환율 비교)
```
---
## 관련 문서
- [features/sales/README.md](README.md) — 영업 관리 모듈 전체
- [features/sales/partners.md](partners.md) — 파트너 관리 기능
- [rules/customer-pricing.md](../../rules/customer-pricing.md) — 고객 요금표
- [rules/partner-commission.md](../../rules/partner-commission.md) — 영업파트너 수당 체계
- [features/settlement/subscriptions.md](../settlement/subscriptions.md) — 구독 관리
---
**최종 업데이트**: 2026-03-13

View File

@@ -1,340 +1,340 @@
# 데모 테넌트 사용 가이드
> **작성일**: 2026-03-13
> **상태**: 운영 중
> **대상**: 영업파트너, 관리자
---
## 1. 개요
### 1.1 목적
영업파트너가 고객에게 SAM 시스템을 직접 체험할 수 있는 데모 환경을 제공하고 관리하는 방법을 안내한다.
### 1.2 데모 유형 (3-Tier)
| 티어 | 유형 | 대상 | 기간 | 특징 |
|------|------|------|------|------|
| Tier 1 | 쇼케이스 | 전체 공유 | 무제한 | 읽기 전용, 매일 자동 리셋 |
| Tier 2 | 파트너 데모 | 파트너별 1개 | 파트너 활동 중 | 전체 기능 체험 가능 |
| Tier 3 | 고객 체험 | 영업건별 생성 | 30일 (연장 가능) | 고객 직접 로그인, 정식 전환 가능 |
> **Tier 3 (고객 체험)**이 가장 자주 사용하는 기능이다. 이 문서는 Tier 3 중심으로 설명한다.
---
## 2. 고객 체험 테넌트 생성
### 2.1 Sales 앱에서 생성
Sales 앱의 데모 관리 화면에서 다음 정보를 입력하여 생성한다:
| 항목 | 필수 | 설명 |
|------|:----:|------|
| 회사명 | O | 고객사 이름 (예: "테스트블라인드") |
| 이메일 | O | 고객 담당자 이메일 |
| 체험 기간 | - | 7~60일, 기본 30일 |
| 프리셋 | - | `manufacturing` 선택 시 샘플 데이터 자동 생성 |
### 2.2 프리셋 데이터 (manufacturing)
프리셋을 선택하면 고객이 빈 화면이 아닌 실제 데이터가 입력된 상태에서 체험을 시작한다:
| 데이터 | 건수 | 내용 |
|--------|------|------|
| 부서 | 6개 | 경영지원, 영업, 생산, 품질, 구매, 물류 |
| 거래처 | 10개 | 다양한 업종의 샘플 거래처 |
| 품목 | 20개 | 제품 10 + 자재 10 |
| 견적 | 5건 | 다양한 상태의 견적서 |
| 수주 | 8건 | 확정/진행 중/완료 수주 |
| 대시보드 통계 | 90일 | 매출/생산 차트용 일간 통계 |
### 2.3 API 직접 호출 (개발자용)
```
POST /api/v1/demo-tenants
```
```json
{
"company_name": "테스트블라인드",
"email": "customer@example.com",
"duration_days": 30,
"preset": "manufacturing"
}
```
**응답 예시:**
```json
{
"success": true,
"message": "데모 테넌트가 생성되었습니다.",
"data": {
"tenant_id": 292,
"company_name": "테스트블라인드",
"tenant_type": "DEMO_TRIAL",
"demo_expires_at": "2026-04-12",
"admin_email": "customer@example.com"
}
}
```
---
## 3. 데모 테넌트 관리
### 3.1 목록 조회
내가 생성한 데모 테넌트 목록을 확인한다.
```
GET /api/v1/demo-tenants
```
### 3.2 상세 조회
특정 데모 테넌트의 상세 정보를 확인한다.
```
GET /api/v1/demo-tenants/{id}
```
### 3.3 데이터 리셋
고객이 데이터를 지저분하게 만들었을 때 초기 상태로 복원한다. 프리셋 데이터가 다시 생성된다.
```
POST /api/v1/demo-tenants/{id}/reset
```
> 리셋은 기존 데이터를 모두 삭제하고 프리셋을 다시 시딩한다. 고객이 직접 입력한 데이터도 삭제된다.
### 3.4 체험 기간 연장
고객이 추가 체험을 원할 때 기간을 연장한다.
```
POST /api/v1/demo-tenants/{id}/extend
```
```json
{
"days": 14
}
```
> 기본 14일 연장. 연장은 1회만 가능하다.
### 3.5 정식 전환
고객이 계약을 결정하면 데모 테넌트를 정식 테넌트로 전환한다.
```
POST /api/v1/demo-tenants/{id}/convert
```
**전환 시 변경 사항:**
| 항목 | 전환 전 | 전환 후 |
|------|---------|---------|
| 테넌트 유형 | `DEMO_TRIAL` | `STD` (정식) |
| 만료일 | 30일 제한 | 제한 없음 |
| 기능 제한 | 바로빌 등 외부 연동 차단 | 전체 기능 사용 |
| 데이터 | 그대로 유지 | 그대로 유지 |
> 고객이 체험 중 입력한 데이터가 정식 환경에 그대로 이어진다. 데이터 재입력이 불필요하다.
---
## 4. 현황 분석 (관리자용)
### 4.1 대시보드 요약
전체 데모 현황을 한눈에 확인한다.
```
GET /api/v1/demo-analytics/summary
```
**제공 정보:**
- 전환율 퍼널 (체험 → 활성 → 만료 → 전환)
- 전체 전환율 (%)
- 평균 전환 기간 (일)
- 비활성 테넌트 수 (7일 이상 활동 없음)
- 유형별 데모 수 (쇼케이스/파트너/체험)
### 4.2 전환율 퍼널
전체 또는 파트너별 전환율을 분석한다.
```
GET /api/v1/demo-analytics/conversion-funnel
GET /api/v1/demo-analytics/conversion-funnel?partner_id=5
```
**응답 예시:**
```json
{
"funnel": {
"total_trials": 15,
"active_trials": 8,
"expired_trials": 3,
"converted": 4
},
"conversion_rate": 26.7,
"avg_conversion_days": 18
}
```
### 4.3 파트너별 성과
각 파트너의 데모 생성 수, 전환 수, 전환율을 비교한다.
```
GET /api/v1/demo-analytics/partner-performance
```
> 전환율 내림차순으로 정렬되어 우수 파트너를 빠르게 파악할 수 있다.
### 4.4 활동 현황 리포트
각 데모 테넌트의 실제 사용 현황을 확인한다.
```
GET /api/v1/demo-analytics/activity-report
GET /api/v1/demo-analytics/activity-report?partner_id=5
```
**활동 상태 분류:**
| 상태 | 기준 | 의미 |
|------|------|------|
| `active` | 1일 이내 활동 | 적극 사용 중 |
| `normal` | 3일 이내 활동 | 정상 사용 중 |
| `low` | 7일 이내 활동 | 관심 저하, 후속 조치 필요 |
| `inactive` | 7일 초과 | 비활성, 즉시 연락 필요 |
| `no_data` | 데이터 없음 | 아직 사용하지 않음 |
---
## 5. 자동 스케줄
시스템이 자동으로 처리하는 작업이다. 별도 조치 불필요.
| 시간 | 작업 | 설명 |
|------|------|------|
| 매일 00:00 | 쇼케이스 리셋 | Tier 1 데모 데이터를 매일 초기화 |
| 매일 04:20 | 만료 체크 | 만료 테넌트 비활성 처리 + D-7 경고 로그 |
| 매일 09:30 | 비활성 알림 | 7일 이상 활동 없는 테넌트 경고 로그 |
---
## 6. Artisan 커맨드 (서버 관리자용)
### 6.1 쇼케이스 리셋
```bash
# 쇼케이스 데이터 삭제 + 샘플 재시딩
php artisan demo:reset-showcase --seed
# 데이터 삭제만 (재시딩 없이)
php artisan demo:reset-showcase
```
### 6.2 만료 테넌트 체크
```bash
# 만료 처리 실행
php artisan demo:check-expired
# 대상만 확인 (변경 없음)
php artisan demo:check-expired --dry-run
```
### 6.3 비활성 테넌트 탐지
```bash
# 기본 7일 기준
php artisan demo:check-inactive
# 기준일 변경 (예: 3일)
php artisan demo:check-inactive --days=3
```
---
## 7. 영업 시나리오별 가이드
### 7.1 고객 방문 영업
```
1. 방문 전: 데모 테넌트 생성 (preset: manufacturing)
2. 방문 시: 고객에게 로그인 정보 전달, 함께 화면 시연
3. 방문 후: 고객이 직접 사용해보도록 안내 (30일 체험)
```
### 7.2 관심 고객 후속 조치
```
1. 활동 리포트 확인 → active/normal이면 전환 제안
2. low/inactive이면 전화 또는 방문하여 추가 시연
3. 데이터가 지저분하면 리셋 후 재시연
```
### 7.3 계약 결정 시
```
1. POST /{id}/convert 로 정식 전환
2. 고객 데이터는 그대로 유지됨 (재입력 불필요)
3. 바로빌 등 외부 연동 기능 활성화
```
### 7.4 체험 기간 부족 시
```
1. POST /{id}/extend 로 14일 연장 (1회 제한)
2. 추가 연장 필요 시 관리자에게 요청
```
---
## 8. API 전체 엔드포인트
### 8.1 데모 관리
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/api/v1/demo-tenants` | 내 데모 목록 |
| `POST` | `/api/v1/demo-tenants` | 체험 테넌트 생성 |
| `GET` | `/api/v1/demo-tenants/stats` | 간단 통계 |
| `GET` | `/api/v1/demo-tenants/{id}` | 상세 조회 |
| `POST` | `/api/v1/demo-tenants/{id}/reset` | 데이터 리셋 |
| `POST` | `/api/v1/demo-tenants/{id}/extend` | 기간 연장 |
| `POST` | `/api/v1/demo-tenants/{id}/convert` | 정식 전환 |
### 8.2 분석
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/api/v1/demo-analytics/summary` | 대시보드 요약 |
| `GET` | `/api/v1/demo-analytics/conversion-funnel` | 전환율 퍼널 |
| `GET` | `/api/v1/demo-analytics/partner-performance` | 파트너별 성과 |
| `GET` | `/api/v1/demo-analytics/activity-report` | 활동 현황 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [demo-tenant-policy.md](demo-tenant-policy.md) | 3-Tier 데모 전략 설계 문서 |
| [partners.md](partners.md) | 영업파트너 관리 |
| [README.md](README.md) | 영업 관리 기능 개요 |
---
**최종 업데이트**: 2026-03-13
# 데모 테넌트 사용 가이드
> **작성일**: 2026-03-13
> **상태**: 운영 중
> **대상**: 영업파트너, 관리자
---
## 1. 개요
### 1.1 목적
영업파트너가 고객에게 SAM 시스템을 직접 체험할 수 있는 데모 환경을 제공하고 관리하는 방법을 안내한다.
### 1.2 데모 유형 (3-Tier)
| 티어 | 유형 | 대상 | 기간 | 특징 |
|------|------|------|------|------|
| Tier 1 | 쇼케이스 | 전체 공유 | 무제한 | 읽기 전용, 매일 자동 리셋 |
| Tier 2 | 파트너 데모 | 파트너별 1개 | 파트너 활동 중 | 전체 기능 체험 가능 |
| Tier 3 | 고객 체험 | 영업건별 생성 | 30일 (연장 가능) | 고객 직접 로그인, 정식 전환 가능 |
> **Tier 3 (고객 체험)**이 가장 자주 사용하는 기능이다. 이 문서는 Tier 3 중심으로 설명한다.
---
## 2. 고객 체험 테넌트 생성
### 2.1 Sales 앱에서 생성
Sales 앱의 데모 관리 화면에서 다음 정보를 입력하여 생성한다:
| 항목 | 필수 | 설명 |
|------|:----:|------|
| 회사명 | O | 고객사 이름 (예: "테스트블라인드") |
| 이메일 | O | 고객 담당자 이메일 |
| 체험 기간 | - | 7~60일, 기본 30일 |
| 프리셋 | - | `manufacturing` 선택 시 샘플 데이터 자동 생성 |
### 2.2 프리셋 데이터 (manufacturing)
프리셋을 선택하면 고객이 빈 화면이 아닌 실제 데이터가 입력된 상태에서 체험을 시작한다:
| 데이터 | 건수 | 내용 |
|--------|------|------|
| 부서 | 6개 | 경영지원, 영업, 생산, 품질, 구매, 물류 |
| 거래처 | 10개 | 다양한 업종의 샘플 거래처 |
| 품목 | 20개 | 제품 10 + 자재 10 |
| 견적 | 5건 | 다양한 상태의 견적서 |
| 수주 | 8건 | 확정/진행 중/완료 수주 |
| 대시보드 통계 | 90일 | 매출/생산 차트용 일간 통계 |
### 2.3 API 직접 호출 (개발자용)
```
POST /api/v1/demo-tenants
```
```json
{
"company_name": "테스트블라인드",
"email": "customer@example.com",
"duration_days": 30,
"preset": "manufacturing"
}
```
**응답 예시:**
```json
{
"success": true,
"message": "데모 테넌트가 생성되었습니다.",
"data": {
"tenant_id": 292,
"company_name": "테스트블라인드",
"tenant_type": "DEMO_TRIAL",
"demo_expires_at": "2026-04-12",
"admin_email": "customer@example.com"
}
}
```
---
## 3. 데모 테넌트 관리
### 3.1 목록 조회
내가 생성한 데모 테넌트 목록을 확인한다.
```
GET /api/v1/demo-tenants
```
### 3.2 상세 조회
특정 데모 테넌트의 상세 정보를 확인한다.
```
GET /api/v1/demo-tenants/{id}
```
### 3.3 데이터 리셋
고객이 데이터를 지저분하게 만들었을 때 초기 상태로 복원한다. 프리셋 데이터가 다시 생성된다.
```
POST /api/v1/demo-tenants/{id}/reset
```
> 리셋은 기존 데이터를 모두 삭제하고 프리셋을 다시 시딩한다. 고객이 직접 입력한 데이터도 삭제된다.
### 3.4 체험 기간 연장
고객이 추가 체험을 원할 때 기간을 연장한다.
```
POST /api/v1/demo-tenants/{id}/extend
```
```json
{
"days": 14
}
```
> 기본 14일 연장. 연장은 1회만 가능하다.
### 3.5 정식 전환
고객이 계약을 결정하면 데모 테넌트를 정식 테넌트로 전환한다.
```
POST /api/v1/demo-tenants/{id}/convert
```
**전환 시 변경 사항:**
| 항목 | 전환 전 | 전환 후 |
|------|---------|---------|
| 테넌트 유형 | `DEMO_TRIAL` | `STD` (정식) |
| 만료일 | 30일 제한 | 제한 없음 |
| 기능 제한 | 바로빌 등 외부 연동 차단 | 전체 기능 사용 |
| 데이터 | 그대로 유지 | 그대로 유지 |
> 고객이 체험 중 입력한 데이터가 정식 환경에 그대로 이어진다. 데이터 재입력이 불필요하다.
---
## 4. 현황 분석 (관리자용)
### 4.1 대시보드 요약
전체 데모 현황을 한눈에 확인한다.
```
GET /api/v1/demo-analytics/summary
```
**제공 정보:**
- 전환율 퍼널 (체험 → 활성 → 만료 → 전환)
- 전체 전환율 (%)
- 평균 전환 기간 (일)
- 비활성 테넌트 수 (7일 이상 활동 없음)
- 유형별 데모 수 (쇼케이스/파트너/체험)
### 4.2 전환율 퍼널
전체 또는 파트너별 전환율을 분석한다.
```
GET /api/v1/demo-analytics/conversion-funnel
GET /api/v1/demo-analytics/conversion-funnel?partner_id=5
```
**응답 예시:**
```json
{
"funnel": {
"total_trials": 15,
"active_trials": 8,
"expired_trials": 3,
"converted": 4
},
"conversion_rate": 26.7,
"avg_conversion_days": 18
}
```
### 4.3 파트너별 성과
각 파트너의 데모 생성 수, 전환 수, 전환율을 비교한다.
```
GET /api/v1/demo-analytics/partner-performance
```
> 전환율 내림차순으로 정렬되어 우수 파트너를 빠르게 파악할 수 있다.
### 4.4 활동 현황 리포트
각 데모 테넌트의 실제 사용 현황을 확인한다.
```
GET /api/v1/demo-analytics/activity-report
GET /api/v1/demo-analytics/activity-report?partner_id=5
```
**활동 상태 분류:**
| 상태 | 기준 | 의미 |
|------|------|------|
| `active` | 1일 이내 활동 | 적극 사용 중 |
| `normal` | 3일 이내 활동 | 정상 사용 중 |
| `low` | 7일 이내 활동 | 관심 저하, 후속 조치 필요 |
| `inactive` | 7일 초과 | 비활성, 즉시 연락 필요 |
| `no_data` | 데이터 없음 | 아직 사용하지 않음 |
---
## 5. 자동 스케줄
시스템이 자동으로 처리하는 작업이다. 별도 조치 불필요.
| 시간 | 작업 | 설명 |
|------|------|------|
| 매일 00:00 | 쇼케이스 리셋 | Tier 1 데모 데이터를 매일 초기화 |
| 매일 04:20 | 만료 체크 | 만료 테넌트 비활성 처리 + D-7 경고 로그 |
| 매일 09:30 | 비활성 알림 | 7일 이상 활동 없는 테넌트 경고 로그 |
---
## 6. Artisan 커맨드 (서버 관리자용)
### 6.1 쇼케이스 리셋
```bash
# 쇼케이스 데이터 삭제 + 샘플 재시딩
php artisan demo:reset-showcase --seed
# 데이터 삭제만 (재시딩 없이)
php artisan demo:reset-showcase
```
### 6.2 만료 테넌트 체크
```bash
# 만료 처리 실행
php artisan demo:check-expired
# 대상만 확인 (변경 없음)
php artisan demo:check-expired --dry-run
```
### 6.3 비활성 테넌트 탐지
```bash
# 기본 7일 기준
php artisan demo:check-inactive
# 기준일 변경 (예: 3일)
php artisan demo:check-inactive --days=3
```
---
## 7. 영업 시나리오별 가이드
### 7.1 고객 방문 영업
```
1. 방문 전: 데모 테넌트 생성 (preset: manufacturing)
2. 방문 시: 고객에게 로그인 정보 전달, 함께 화면 시연
3. 방문 후: 고객이 직접 사용해보도록 안내 (30일 체험)
```
### 7.2 관심 고객 후속 조치
```
1. 활동 리포트 확인 → active/normal이면 전환 제안
2. low/inactive이면 전화 또는 방문하여 추가 시연
3. 데이터가 지저분하면 리셋 후 재시연
```
### 7.3 계약 결정 시
```
1. POST /{id}/convert 로 정식 전환
2. 고객 데이터는 그대로 유지됨 (재입력 불필요)
3. 바로빌 등 외부 연동 기능 활성화
```
### 7.4 체험 기간 부족 시
```
1. POST /{id}/extend 로 14일 연장 (1회 제한)
2. 추가 연장 필요 시 관리자에게 요청
```
---
## 8. API 전체 엔드포인트
### 8.1 데모 관리
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/api/v1/demo-tenants` | 내 데모 목록 |
| `POST` | `/api/v1/demo-tenants` | 체험 테넌트 생성 |
| `GET` | `/api/v1/demo-tenants/stats` | 간단 통계 |
| `GET` | `/api/v1/demo-tenants/{id}` | 상세 조회 |
| `POST` | `/api/v1/demo-tenants/{id}/reset` | 데이터 리셋 |
| `POST` | `/api/v1/demo-tenants/{id}/extend` | 기간 연장 |
| `POST` | `/api/v1/demo-tenants/{id}/convert` | 정식 전환 |
### 8.2 분석
| Method | Path | 설명 |
|--------|------|------|
| `GET` | `/api/v1/demo-analytics/summary` | 대시보드 요약 |
| `GET` | `/api/v1/demo-analytics/conversion-funnel` | 전환율 퍼널 |
| `GET` | `/api/v1/demo-analytics/partner-performance` | 파트너별 성과 |
| `GET` | `/api/v1/demo-analytics/activity-report` | 활동 현황 |
---
## 관련 문서
| 문서 | 설명 |
|------|------|
| [demo-tenant-policy.md](demo-tenant-policy.md) | 3-Tier 데모 전략 설계 문서 |
| [partners.md](partners.md) | 영업파트너 관리 |
| [README.md](README.md) | 영업 관리 기능 개요 |
---
**최종 업데이트**: 2026-03-13

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,277 +1,277 @@
# 급여관리 프론트엔드 개발 가이드
> **작성일**: 2026-03-11
> **상태**: API 개발 완료 (개발서버 배포됨)
> **대상**: 프론트엔드 개발자
> **API 명세**: [payroll-api.md](../api-specs/payroll-api.md)
---
## 1. 개요
급여관리 API가 개발 완료되어 개발 서버(`api.dev.codebridge-x.com`)에 배포되었다.
이 문서는 프론트엔드 개발 시 필요한 핵심 정보를 요약한다.
상세 Request/Response 예시는 [API 명세 문서](../api-specs/payroll-api.md)를 참고한다.
---
## 2. 주요 기능
| # | 기능 | 설명 |
|---|------|------|
| 1 | 급여 CRUD | 사원별 월급여 등록/조회/수정/삭제 |
| 2 | 자동 계산 엔진 | 4대보험 + 근로소득세 + 지방소득세 자동 계산 |
| 3 | 상태 관리 | draft → confirmed → paid 3단계 흐름 |
| 4 | 일괄 처리 | 재직사원 일괄 생성, 전월 복사, 일괄 계산, 일괄 확정 |
| 5 | 계산 미리보기 | 저장 없이 실시간 계산 결과 확인 (폼 입력 시 활용) |
| 6 | 급여명세서 | 인쇄용 명세서 데이터 조회 |
| 7 | 설정 관리 | 보험 요율, 수당/공제 유형 테넌트별 설정 |
---
## 3. 엔드포인트 요약 (18개)
| # | Method | Path | 설명 |
|---|--------|------|------|
| 1 | GET | `/api/v1/payrolls` | 급여 목록 (페이지네이션) |
| 2 | POST | `/api/v1/payrolls` | 급여 등록 |
| 3 | GET | `/api/v1/payrolls/summary` | 월간 요약 통계 |
| 4 | POST | `/api/v1/payrolls/calculate` | 급여 일괄 계산 (draft 재계산) |
| 5 | POST | `/api/v1/payrolls/calculate-preview` | 계산 미리보기 (저장 안 함) |
| 6 | POST | `/api/v1/payrolls/bulk-confirm` | 일괄 확정 |
| 7 | POST | `/api/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 |
| 8 | POST | `/api/v1/payrolls/copy-from-previous` | 전월 급여 복사 |
| 9 | GET | `/api/v1/payrolls/{id}` | 급여 상세 |
| 10 | PUT | `/api/v1/payrolls/{id}` | 급여 수정 |
| 11 | DELETE | `/api/v1/payrolls/{id}` | 급여 삭제 |
| 12 | POST | `/api/v1/payrolls/{id}/confirm` | 확정 |
| 13 | POST | `/api/v1/payrolls/{id}/unconfirm` | 확정 취소 |
| 14 | POST | `/api/v1/payrolls/{id}/pay` | 지급 처리 |
| 15 | POST | `/api/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) |
| 16 | GET | `/api/v1/payrolls/{id}/payslip` | 급여명세서 조회 |
| 17 | GET | `/api/v1/payrolls/settings` | 급여 설정 조회 |
| 18 | PUT | `/api/v1/payrolls/settings` | 급여 설정 수정 |
---
## 4. 상태 흐름
```
draft(작성중) ──[확정]──→ confirmed(확정) ──[지급]──→ paid(지급완료)
▲ │ │
└───[확정취소]───────────┘ │
└───[지급취소*]────────────────────────────────────┘
* 지급취소는 슈퍼관리자 전용 (paid → draft로 초기화)
```
### 4.1 상태별 배지 색상
| 상태 | 값 | 배지 색상 | 의미 |
|------|---|----------|------|
| 작성중 | `draft` | gray | 수정/삭제/확정 가능 |
| 확정 | `confirmed` | blue | 확정취소/지급 가능 |
| 지급완료 | `paid` | green | 상세보기만 가능 |
### 4.2 상태별 가능한 작업
| 상태 | 일반 사용자 | 슈퍼관리자 |
|------|-----------|-----------|
| `draft` | 수정, 삭제, 확정, 일괄계산 | 동일 |
| `confirmed` | 확정취소, 지급처리 | + **수정** |
| `paid` | 상세보기만 | + **수정**, **지급취소** |
> 슈퍼관리자 수정 시 `_is_super_admin: true`를 Request Body에 전달한다.
---
## 5. 화면 구성 안내
### 5.1 급여 목록 화면
```
┌─────────────────────────────────────────────────────┐
│ 급여관리 [2026년 03월 ▼] │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ 총 인원 │ │ 총 지급액 │ │ 총 공제액 │ │ 실수령액 ││
│ │ 15명 │ │ 62,500천원│ │ 15,800천원│ │46,700천원││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
│ │
│ [일괄생성] [전월복사] [일괄계산] [일괄확정] [+ 등록] │
│ │
│ ┌───┬──────┬──────┬──────┬──────┬──────┬────┬────┐│
│ │ # │ 사원 │ 기본급│총지급액│총공제액│실수령액│상태 │ 작업││
│ ├───┼──────┼──────┼──────┼──────┼──────┼────┼────┤│
│ │ 1 │홍길동│ 350만│ 410만│ 98만 │ 312만│작성│ ⋮ ││
│ │ 2 │김철수│ 300만│ 350만│ 85만 │ 265만│확정│ ⋮ ││
│ └───┴──────┴──────┴──────┴──────┴──────┴────┴────┘│
└─────────────────────────────────────────────────────┘
```
- 연월 선택 → `year` + `month` 필터로 목록 조회
- 요약 카드 → `GET /payrolls/summary` API 활용
- 상태별 필터링 → `status` 파라미터
- 부서별 필터링 → `department_id` 파라미터
- 사원 검색 → `search` 파라미터
### 5.2 급여 등록/수정 폼
```
┌─────────────────────────────────────────────────────┐
│ 급여 등록 │
├─────────────────────────────────────────────────────┤
│ 사원: [홍길동 ▼] 연도: [2026] 월: [3 ▼] │
│ │
│ ── 지급 항목 ──────────────────────────────────── │
│ 기본급: [ 3,500,000 ] │
│ 연장근로수당: [ 0 ] │
│ 식대(비과세): [ 200,000 ] │
│ 수당: │
│ 직책수당 [ 300,000 ] [삭제] │
│ 교통비 [ 100,000 ] [삭제] │
│ [+ 수당 추가] │
│ ────────────────────────── 총 지급액: 4,100,000 │
│ │
│ ── 공제 항목 (자동 계산) ──────────────────────── │
│ 근로소득세: [ 117,750 ] ← 자동 (수정 가능) │
│ 지방소득세: [ 11,770 ] ← 자동 (수정 가능) │
│ 건강보험: [ 138,220 ] ← 자동 (수정 가능) │
│ 장기요양보험: [ 1,250 ] ← 자동 (수정 가능) │
│ 국민연금: [ 175,500 ] ← 자동 (수정 가능) │
│ 고용보험: [ 35,100 ] ← 자동 (수정 가능) │
│ 기타 공제: │
│ 대출상환 [ 500,000 ] [삭제] │
│ [+ 기타 공제 추가] │
│ ────────────────────────── 총 공제액: 979,590 │
│ │
│ ═════════════════════════ 실수령액: 3,120,410 │
│ │
│ 메모: [ ] │
│ │
│ [취소] [미리보기] [저장]│
└─────────────────────────────────────────────────────┘
```
### 5.3 급여명세서 (인쇄용)
```
┌─────────────────────────────────────────────┐
│ 급 여 명 세 서 │
│ │
│ 사원명: 홍길동 귀속기간: 2026년 03월 │
│ │
│ ┌──── 지급 내역 ────┬── 공제 내역 ────┐ │
│ │ 기본급 3,500,000│ 소득세 117,750│ │
│ │ 식대 200,000│ 지방소득세 11,770│ │
│ │ 직책수당 300,000│ 건강보험 138,220│ │
│ │ 교통비 100,000│ 장기요양 1,250│ │
│ │ │ 국민연금 175,500│ │
│ │ │ 고용보험 35,100│ │
│ │ │ 대출상환 500,000│ │
│ ├───────────────────┼─────────────────┤ │
│ │ 지급합계 4,100,000│ 공제합계 979,590│ │
│ └───────────────────┴─────────────────┘ │
│ │
│ 실수령액: 3,120,410원 │
└─────────────────────────────────────────────┘
```
- `GET /payrolls/{id}/payslip` 응답의 `earnings`/`deductions` 구조 활용
- 인쇄 레이아웃은 A4 세로 기준 권장
---
## 6. 구현 시 유의사항
### 6.1 계산 미리보기 (핵심)
급여 등록/수정 폼에서 지급 항목 변경 시 `POST /calculate-preview` API를 호출하여 공제 항목을 실시간 갱신한다.
```
사용자가 기본급 변경 → debounce 300ms → calculate-preview 호출 → 공제 항목 자동 갱신
```
- `user_id`를 전달하면 해당 사원의 가족수를 자동 반영
- `user_id` 미전달 시 가족수 1로 계산
### 6.2 법정 공제 수동 입력
- 법정 공제 필드(소득세, 4대보험)는 기본 readonly
- "수정" 토글로 수동 입력 허용
- 수동 변경한 항목만 `deduction_overrides` 객체에 담아 전달
- 지정하지 않은 항목은 자동 계산값 유지
```json
{
"deduction_overrides": {
"pension": 175000
}
}
```
### 6.3 수당/공제 드롭다운
`GET /payrolls/settings` 응답의 `allowance_types``deduction_types`를 사용하여 드롭다운 목록을 구성한다.
```json
{
"allowance_types": [
{"code": "meal", "name": "식대", "is_taxable": false},
{"code": "position", "name": "직책수당", "is_taxable": true}
],
"deduction_types": [
{"code": "loan", "name": "대출상환"},
{"code": "union", "name": "조합비"}
]
}
```
### 6.4 금액 표시 규칙
- 모든 금액은 **원(KRW)** 단위 정수
- 천 단위 콤마 필수: `3,500,000`
- 음수 금액(환급): 빨간색 + `-` 부호
---
## 7. 월간 워크플로우
프론트엔드 UI에서 안내할 급여 처리 순서:
```
1. 월초 → [일괄생성] 또는 [전월복사] 실행
2. 개별 급여 데이터 확인/수정
3. [일괄계산] 실행 (공제 항목 최신 요율로 재계산)
4. 데이터 확인 완료 → [일괄확정]
5. 급여 지급일 → 개별 [지급처리] (출금과 연결)
6. 급여명세서 조회/인쇄
```
---
## 8. 에러 처리
주요 에러 메시지와 프론트엔드 대응:
| 에러 상황 | HTTP 코드 | 메시지 | 프론트엔드 대응 |
|----------|----------|--------|----------------|
| 동일 연월+사원 중복 | 400 | 해당 연월에 이미 급여가 등록되어 있습니다. | 토스트 경고 |
| draft 외 수정 시도 | 400 | 작성중 상태의 급여만 수정할 수 있습니다. | 버튼 비활성화로 사전 방지 |
| draft 외 삭제 시도 | 400 | 작성중 상태의 급여만 삭제할 수 있습니다. | 버튼 숨김으로 사전 방지 |
| 전월 데이터 없음 | 400 | 전월 급여 데이터가 없습니다. | 토스트 안내 + 일괄생성 유도 |
| 검증 실패 | 422 | 필드별 에러 메시지 | 폼 필드 하단에 에러 표시 |
---
## 관련 문서
- [급여관리 API 전체 명세](../api-specs/payroll-api.md) — 18개 엔드포인트 상세 Request/Response
- [급여관리 기능 상세](../../features/finance/payroll.md) — 전표 변환, 권한, 멀티테넌트
- [결재관리 API 명세](../api-specs/approval-api.md) — 참고용 (유사 구조)
---
**최종 업데이트**: 2026-03-11
# 급여관리 프론트엔드 개발 가이드
> **작성일**: 2026-03-11
> **상태**: API 개발 완료 (개발서버 배포됨)
> **대상**: 프론트엔드 개발자
> **API 명세**: [payroll-api.md](../api-specs/payroll-api.md)
---
## 1. 개요
급여관리 API가 개발 완료되어 개발 서버(`api.dev.codebridge-x.com`)에 배포되었다.
이 문서는 프론트엔드 개발 시 필요한 핵심 정보를 요약한다.
상세 Request/Response 예시는 [API 명세 문서](../api-specs/payroll-api.md)를 참고한다.
---
## 2. 주요 기능
| # | 기능 | 설명 |
|---|------|------|
| 1 | 급여 CRUD | 사원별 월급여 등록/조회/수정/삭제 |
| 2 | 자동 계산 엔진 | 4대보험 + 근로소득세 + 지방소득세 자동 계산 |
| 3 | 상태 관리 | draft → confirmed → paid 3단계 흐름 |
| 4 | 일괄 처리 | 재직사원 일괄 생성, 전월 복사, 일괄 계산, 일괄 확정 |
| 5 | 계산 미리보기 | 저장 없이 실시간 계산 결과 확인 (폼 입력 시 활용) |
| 6 | 급여명세서 | 인쇄용 명세서 데이터 조회 |
| 7 | 설정 관리 | 보험 요율, 수당/공제 유형 테넌트별 설정 |
---
## 3. 엔드포인트 요약 (18개)
| # | Method | Path | 설명 |
|---|--------|------|------|
| 1 | GET | `/api/v1/payrolls` | 급여 목록 (페이지네이션) |
| 2 | POST | `/api/v1/payrolls` | 급여 등록 |
| 3 | GET | `/api/v1/payrolls/summary` | 월간 요약 통계 |
| 4 | POST | `/api/v1/payrolls/calculate` | 급여 일괄 계산 (draft 재계산) |
| 5 | POST | `/api/v1/payrolls/calculate-preview` | 계산 미리보기 (저장 안 함) |
| 6 | POST | `/api/v1/payrolls/bulk-confirm` | 일괄 확정 |
| 7 | POST | `/api/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 |
| 8 | POST | `/api/v1/payrolls/copy-from-previous` | 전월 급여 복사 |
| 9 | GET | `/api/v1/payrolls/{id}` | 급여 상세 |
| 10 | PUT | `/api/v1/payrolls/{id}` | 급여 수정 |
| 11 | DELETE | `/api/v1/payrolls/{id}` | 급여 삭제 |
| 12 | POST | `/api/v1/payrolls/{id}/confirm` | 확정 |
| 13 | POST | `/api/v1/payrolls/{id}/unconfirm` | 확정 취소 |
| 14 | POST | `/api/v1/payrolls/{id}/pay` | 지급 처리 |
| 15 | POST | `/api/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) |
| 16 | GET | `/api/v1/payrolls/{id}/payslip` | 급여명세서 조회 |
| 17 | GET | `/api/v1/payrolls/settings` | 급여 설정 조회 |
| 18 | PUT | `/api/v1/payrolls/settings` | 급여 설정 수정 |
---
## 4. 상태 흐름
```
draft(작성중) ──[확정]──→ confirmed(확정) ──[지급]──→ paid(지급완료)
▲ │ │
└───[확정취소]───────────┘ │
└───[지급취소*]────────────────────────────────────┘
* 지급취소는 슈퍼관리자 전용 (paid → draft로 초기화)
```
### 4.1 상태별 배지 색상
| 상태 | 값 | 배지 색상 | 의미 |
|------|---|----------|------|
| 작성중 | `draft` | gray | 수정/삭제/확정 가능 |
| 확정 | `confirmed` | blue | 확정취소/지급 가능 |
| 지급완료 | `paid` | green | 상세보기만 가능 |
### 4.2 상태별 가능한 작업
| 상태 | 일반 사용자 | 슈퍼관리자 |
|------|-----------|-----------|
| `draft` | 수정, 삭제, 확정, 일괄계산 | 동일 |
| `confirmed` | 확정취소, 지급처리 | + **수정** |
| `paid` | 상세보기만 | + **수정**, **지급취소** |
> 슈퍼관리자 수정 시 `_is_super_admin: true`를 Request Body에 전달한다.
---
## 5. 화면 구성 안내
### 5.1 급여 목록 화면
```
┌─────────────────────────────────────────────────────┐
│ 급여관리 [2026년 03월 ▼] │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ 총 인원 │ │ 총 지급액 │ │ 총 공제액 │ │ 실수령액 ││
│ │ 15명 │ │ 62,500천원│ │ 15,800천원│ │46,700천원││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
│ │
│ [일괄생성] [전월복사] [일괄계산] [일괄확정] [+ 등록] │
│ │
│ ┌───┬──────┬──────┬──────┬──────┬──────┬────┬────┐│
│ │ # │ 사원 │ 기본급│총지급액│총공제액│실수령액│상태 │ 작업││
│ ├───┼──────┼──────┼──────┼──────┼──────┼────┼────┤│
│ │ 1 │홍길동│ 350만│ 410만│ 98만 │ 312만│작성│ ⋮ ││
│ │ 2 │김철수│ 300만│ 350만│ 85만 │ 265만│확정│ ⋮ ││
│ └───┴──────┴──────┴──────┴──────┴──────┴────┴────┘│
└─────────────────────────────────────────────────────┘
```
- 연월 선택 → `year` + `month` 필터로 목록 조회
- 요약 카드 → `GET /payrolls/summary` API 활용
- 상태별 필터링 → `status` 파라미터
- 부서별 필터링 → `department_id` 파라미터
- 사원 검색 → `search` 파라미터
### 5.2 급여 등록/수정 폼
```
┌─────────────────────────────────────────────────────┐
│ 급여 등록 │
├─────────────────────────────────────────────────────┤
│ 사원: [홍길동 ▼] 연도: [2026] 월: [3 ▼] │
│ │
│ ── 지급 항목 ──────────────────────────────────── │
│ 기본급: [ 3,500,000 ] │
│ 연장근로수당: [ 0 ] │
│ 식대(비과세): [ 200,000 ] │
│ 수당: │
│ 직책수당 [ 300,000 ] [삭제] │
│ 교통비 [ 100,000 ] [삭제] │
│ [+ 수당 추가] │
│ ────────────────────────── 총 지급액: 4,100,000 │
│ │
│ ── 공제 항목 (자동 계산) ──────────────────────── │
│ 근로소득세: [ 117,750 ] ← 자동 (수정 가능) │
│ 지방소득세: [ 11,770 ] ← 자동 (수정 가능) │
│ 건강보험: [ 138,220 ] ← 자동 (수정 가능) │
│ 장기요양보험: [ 1,250 ] ← 자동 (수정 가능) │
│ 국민연금: [ 175,500 ] ← 자동 (수정 가능) │
│ 고용보험: [ 35,100 ] ← 자동 (수정 가능) │
│ 기타 공제: │
│ 대출상환 [ 500,000 ] [삭제] │
│ [+ 기타 공제 추가] │
│ ────────────────────────── 총 공제액: 979,590 │
│ │
│ ═════════════════════════ 실수령액: 3,120,410 │
│ │
│ 메모: [ ] │
│ │
│ [취소] [미리보기] [저장]│
└─────────────────────────────────────────────────────┘
```
### 5.3 급여명세서 (인쇄용)
```
┌─────────────────────────────────────────────┐
│ 급 여 명 세 서 │
│ │
│ 사원명: 홍길동 귀속기간: 2026년 03월 │
│ │
│ ┌──── 지급 내역 ────┬── 공제 내역 ────┐ │
│ │ 기본급 3,500,000│ 소득세 117,750│ │
│ │ 식대 200,000│ 지방소득세 11,770│ │
│ │ 직책수당 300,000│ 건강보험 138,220│ │
│ │ 교통비 100,000│ 장기요양 1,250│ │
│ │ │ 국민연금 175,500│ │
│ │ │ 고용보험 35,100│ │
│ │ │ 대출상환 500,000│ │
│ ├───────────────────┼─────────────────┤ │
│ │ 지급합계 4,100,000│ 공제합계 979,590│ │
│ └───────────────────┴─────────────────┘ │
│ │
│ 실수령액: 3,120,410원 │
└─────────────────────────────────────────────┘
```
- `GET /payrolls/{id}/payslip` 응답의 `earnings`/`deductions` 구조 활용
- 인쇄 레이아웃은 A4 세로 기준 권장
---
## 6. 구현 시 유의사항
### 6.1 계산 미리보기 (핵심)
급여 등록/수정 폼에서 지급 항목 변경 시 `POST /calculate-preview` API를 호출하여 공제 항목을 실시간 갱신한다.
```
사용자가 기본급 변경 → debounce 300ms → calculate-preview 호출 → 공제 항목 자동 갱신
```
- `user_id`를 전달하면 해당 사원의 가족수를 자동 반영
- `user_id` 미전달 시 가족수 1로 계산
### 6.2 법정 공제 수동 입력
- 법정 공제 필드(소득세, 4대보험)는 기본 readonly
- "수정" 토글로 수동 입력 허용
- 수동 변경한 항목만 `deduction_overrides` 객체에 담아 전달
- 지정하지 않은 항목은 자동 계산값 유지
```json
{
"deduction_overrides": {
"pension": 175000
}
}
```
### 6.3 수당/공제 드롭다운
`GET /payrolls/settings` 응답의 `allowance_types``deduction_types`를 사용하여 드롭다운 목록을 구성한다.
```json
{
"allowance_types": [
{"code": "meal", "name": "식대", "is_taxable": false},
{"code": "position", "name": "직책수당", "is_taxable": true}
],
"deduction_types": [
{"code": "loan", "name": "대출상환"},
{"code": "union", "name": "조합비"}
]
}
```
### 6.4 금액 표시 규칙
- 모든 금액은 **원(KRW)** 단위 정수
- 천 단위 콤마 필수: `3,500,000`
- 음수 금액(환급): 빨간색 + `-` 부호
---
## 7. 월간 워크플로우
프론트엔드 UI에서 안내할 급여 처리 순서:
```
1. 월초 → [일괄생성] 또는 [전월복사] 실행
2. 개별 급여 데이터 확인/수정
3. [일괄계산] 실행 (공제 항목 최신 요율로 재계산)
4. 데이터 확인 완료 → [일괄확정]
5. 급여 지급일 → 개별 [지급처리] (출금과 연결)
6. 급여명세서 조회/인쇄
```
---
## 8. 에러 처리
주요 에러 메시지와 프론트엔드 대응:
| 에러 상황 | HTTP 코드 | 메시지 | 프론트엔드 대응 |
|----------|----------|--------|----------------|
| 동일 연월+사원 중복 | 400 | 해당 연월에 이미 급여가 등록되어 있습니다. | 토스트 경고 |
| draft 외 수정 시도 | 400 | 작성중 상태의 급여만 수정할 수 있습니다. | 버튼 비활성화로 사전 방지 |
| draft 외 삭제 시도 | 400 | 작성중 상태의 급여만 삭제할 수 있습니다. | 버튼 숨김으로 사전 방지 |
| 전월 데이터 없음 | 400 | 전월 급여 데이터가 없습니다. | 토스트 안내 + 일괄생성 유도 |
| 검증 실패 | 422 | 필드별 에러 메시지 | 폼 필드 하단에 에러 표시 |
---
## 관련 문서
- [급여관리 API 전체 명세](../api-specs/payroll-api.md) — 18개 엔드포인트 상세 Request/Response
- [급여관리 기능 상세](../../features/finance/payroll.md) — 전표 변환, 권한, 멀티테넌트
- [결재관리 API 명세](../api-specs/approval-api.md) — 참고용 (유사 구조)
---
**최종 업데이트**: 2026-03-11

File diff suppressed because it is too large Load Diff

View File

@@ -1,375 +1,375 @@
# SAM 가격정책 & 가격 시뮬레이터 안내
> **작성일**: 2026-03-15
> **대상**: 영업파트너
> **상태**: 설계 확정
---
## 1. SAM 서비스란?
SAM(Smart Automation Management)은 제조업·시공업을 위한 **클라우드 업무 관리 시스템**이다.
품목관리, 견적, 수주, 생산, 출하, 인사, 회계까지 **하나의 시스템**으로 관리할 수 있다.
---
## 2. 요금 구조 한눈에 보기
SAM의 요금은 **딱 두 가지**로 구성된다.
```
┌────────────────────────────────────────────┐
│ │
│ 1) 개발비 = 처음 한 번만 내는 돈 │
│ 2) 구독료 = 매달 내는 돈 │
│ │
│ ※ 모든 금액은 VAT(부가세) 별도 │
│ │
└────────────────────────────────────────────┘
```
| 구분 | 의미 | 비유 |
|------|------|------|
| **개발비** | 고객 맞춤 시스템을 구축하는 비용 | 집을 짓는 비용 |
| **구독료** | 시스템 유지·관리·클라우드 비용 | 매달 내는 관리비 |
---
## 3. 기본 상품 가격표
### 3.1 제조업 기본 패키지 (가장 많이 팔리는 상품)
```
┌─────────────────────────────────────────────────────┐
│ 제조업 기본 패키지 │
│ │
│ 포함 기능: │
│ 품목관리 → 견적 → 수주 → 생산 → 출하 │
│ + ERP (인사/회계) 무료 포함 │
│ │
│ 개발비: 2,000만원 │
│ 구독료: 50만원/월 │
└─────────────────────────────────────────────────────┘
```
### 3.2 개별 모듈 (필요한 것만 추가)
| 모듈명 | 개발비 | 구독료/월 | 어떤 기능? |
|--------|-------:|--------:|-----------|
| QR코드 관리 | 1,020만원 | 5만원 | 제품에 QR코드 부착, 이력 추적 |
| 사진/출하 관리 | 1,920만원 | 10만원 | 출하 사진 촬영·등록, 배송 관리 |
| 검사/토큰 적용 | 1,020만원 | 5만원 | 품질 검사, AI 토큰 활용 |
### 3.3 통합 패키지 (대형 프로젝트용)
| 패키지명 | 개발비 | 구독료/월 | 어떤 기능? |
|---------|-------:|--------:|-----------|
| 공사관리 패키지 | 4,000만원 | 20만원 | 시공 현장 관리, 공정 추적 |
| 공정/정부지원사업 | 8,000만원 | 40만원 | 정부지원사업 관리 + 전체 공정 관리 |
### 3.4 추가 옵션
| 옵션명 | 개발비 추가 | 구독료 추가/월 |
|--------|----------:|------------:|
| 생산공정 1개 추가 | 500만원 | 10만원 |
| 품질관리 (인정검사) | 2,000만원 | 50만원 |
| 사진 등록 | - | 10만원 |
| 챗봇/녹음/업무일지 | - | 각 20만원 |
| 연구소 연구노트 | - | 5만원 |
| 장비점검, 사무소 정비 | - | 5만원 |
---
## 4. 할인/프로모션 (고객에게 제안할 수 있는 것들)
가격 시뮬레이터에서 **3가지 할인**을 조합할 수 있다.
### 4.1 개발비 할인
| 방식 | 설명 | 예시 |
|------|------|------|
| **% 할인** | 개발비에서 N% 깎아주기 | 2,000만원 → 10% 할인 → **1,800만원** |
| **정액 할인** | 개발비에서 N원 깎아주기 | 2,000만원 → 300만원 할인 → **1,700만원** |
| **전액 면제** | 개발비 0원 (특별 프로모션) | 최저가가 0원인 상품만 가능 |
### 4.2 구독료 할인
구독료에서 **최대 50%**까지 할인 가능하다.
```
예) 구독료 50만원/월 → 20% 할인 → 40만원/월
```
### 4.3 개발비-구독료 연동 (선택)
개발비를 깎아주면 구독료도 같은 비율로 자동 조정하는 기능이다.
```
기본: 개발비 2,000만원, 구독료 50만원/월 (비율 2.5%)
개발비 1,500만원으로 조정하면?
→ 구독료도 자동으로 37.5만원/월로 조정
개발비 1,000만원으로 조정하면?
→ 구독료도 자동으로 25만원/월로 조정
```
> 이 기능은 **켜고 끌 수 있다**. 끄면 개발비만 조정하고 구독료는 정가 유지.
---
## 5. 1년차 총 비용 계산법
```
┌─────────────────────────────────────────────────────┐
│ │
│ 1년차 총 비용 = 개발비 + (월 구독료 × 유료 개월수) │
│ │
└─────────────────────────────────────────────────────┘
```
### 예시: 제조업 기본 패키지, 10% 할인
| 항목 | 계산 | 금액 |
|------|------|-----:|
| 개발비 (정가) | - | 2,000만원 |
| 개발비 할인 | 2,000 × 10% | -200만원 |
| **최종 개발비** | - | **1,800만원** |
| 월 구독료 | - | 50만원 |
| 유료 개월수 | 12개월 | 12개월 |
| 연 구독료 | 50 × 12 | 600만원 |
| **1년차 총 비용** | 1,800 + 600 | **2,400만원** |
> 2년차부터는 구독료만 내면 된다: 50만원 × 12 = **600만원/년**
---
## 6. 영업파트너 수당 (내가 얼마 받나?)
### 6.1 수당 핵심 원칙
```
┌─────────────────────────────────────────────────────┐
│ │
│ 수당 = 최종 개발비 × 수당률 │
│ │
│ ※ 구독료는 수당 계산에 포함되지 않는다 │
│ ※ 할인 적용 후 개발비가 수당 기준이다 │
│ │
└─────────────────────────────────────────────────────┘
```
### 6.2 가입 유형별 수당률
| | 개인 가입 | 단체 가입 |
|---|:---:|:---:|
| **파트너 수당** | **20%** | **30%** |
| **유치 파트너 수당** | 5% | - |
| **매니저 수당** | 첫달 구독료 | - |
- **개인 가입**: 개별 고객이 직접 계약 (파트너 20% + 유치 파트너 5% + 매니저 수당)
- **단체 가입**: 여러 고객을 묶어서 한꺼번에 계약 (단체 수당 30%)
### 6.3 수당 계산 예시 (제조업 기본 패키지)
#### 정가 그대로 판매한 경우 (개발비 2,000만원)
| 가입유형 | 파트너 수당 | 유치 파트너 수당 | 합계 | 매니저 수당 |
|---------|----------:|----------:|-----:|----------:|
| 개인 가입 | **400만원** | 100만원 | 500만원 | 첫달 구독료 (50만원) |
| 단체 가입 | **600만원** | 60만원 | 660만원 | 첫달 구독료 (50만원) |
#### 10% 할인 판매한 경우 (개발비 1,800만원)
| 가입유형 | 파트너 수당 | 유치 파트너 수당 | 합계 | 매니저 수당 |
|---------|----------:|----------:|-----:|----------:|
| 개인 가입 | **360만원** | 90만원 | 450만원 | 첫달 구독료 (50만원) |
| 단체 가입 | **540만원** | 54만원 | 594만원 | 첫달 구독료 (50만원) |
> 할인하면 내 수당도 줄어든다! **가능하면 정가에 가깝게 팔수록 수당이 많다.**
### 6.4 수당은 언제 받나?
개발비를 고객이 **2번에 나눠** 내면, 수당도 **2번에 나눠** 받는다.
```
고객 1차 납입 (계약금)
익월 10일 → 수당 50% 지급
고객 2차 납입 (잔금)
익월 10일 → 수당 50% 지급
```
**예시**: 개발비 2,000만원, 파트너 수당 400만원
| 시점 | 받는 금액 |
|------|--------:|
| 1차 납입 후 익월 10일 | 200만원 |
| 2차 납입 후 익월 10일 | 200만원 |
| **합계** | **400만원** |
---
## 7. 사용량 기반 추가 과금 (고객 안내용)
기본 제공 한도를 초과하면 실비가 부과된다. 고객에게 미리 안내하면 좋다.
| 항목 | 기본 제공 | 초과 시 |
|------|----------|--------|
| 파일 저장 공간 | 100GB | 100GB당 5만원/월 |
| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 |
**AI 토큰 100만 개로 할 수 있는 일:**
| 용도 | 처리량 |
|------|--------|
| 음성 회의 요약 | 약 520분 (8.6시간) |
| 문서 정리 (A4) | 약 300~400매 |
| 이메일/노트 분류 | 약 1,500~2,000건 |
> 대부분의 중소기업은 기본 제공량으로 충분하다.
---
## 8. 바로빌 부가 서비스 (선택 사항)
세금계산서, 카드내역, 계좌조회 등을 SAM에서 바로 처리할 수 있는 연동 서비스이다.
| 서비스 | 월 요금 | 기본 제공 | 초과 과금 |
|--------|-------:|----------|----------|
| 계좌조회 | 1만원 | 1계좌 | 추가 1계좌당 1만원 |
| 카드내역 | 1만원 | 5장 | 추가 1장당 5천원 |
| 홈택스 매입/매출 | **무료** | - | - |
| 세금계산서 발행 | - | 100건 | 추가 50건당 5천원 |
---
## 9. 가격 시뮬레이터 사용법
가격 시뮬레이터는 MNG(관리자 시스템)에서 사용할 수 있는 **영업 지원 도구**이다.
### 9.1 시뮬레이터로 할 수 있는 일
```
┌─────────────────────────────────────────────────────┐
│ 가격 시뮬레이터 │
│ │
│ 1. 상품을 골라서 체크한다 │
│ 2. 할인을 조합한다 │
│ 3. 가입 유형(개인/단체)을 선택한다 │
│ 4. 자동으로 계산된다: │
│ - 최종 개발비 │
│ - 최종 구독료 │
│ - 1년차 총 비용 │
│ - 내 수당 (파트너/유치 파트너) │
│ │
│ → 고객에게 제안하기 전에 미리 확인! │
└─────────────────────────────────────────────────────┘
```
### 9.2 사용 순서
**Step 1: 상품 선택**
- 카테고리별로 상품 목록이 표시된다
- 필수 상품은 이미 체크되어 있다
- 고객에게 필요한 상품을 추가로 체크한다
**Step 2: 개발비 조정 (선택)**
- 재량권이 있는 상품은 슬라이더로 개발비를 조정할 수 있다
- 최저 개발비 이하로는 내릴 수 없다
**Step 3: 할인 설정**
- 개발비 할인: % 또는 정액
- 구독료 할인: 0~50%
- 개발비-구독료 연동: ON/OFF
**Step 4: 가입 유형 선택**
- 개인 가입 / 단체 가입 전환
- 수당률이 자동으로 바뀐다
**Step 5: 결과 확인**
- 최종 개발비, 구독료, 1년차 총 비용
- 파트너 수당, 유치 파트너 수당이 실시간으로 표시된다
### 9.3 주의 사항
```
※ 시뮬레이터 결과는 "참고용"이다
※ 실제 계약 금액은 고객과 별도 협의에 따른다
※ 최저가 이하로는 어떤 할인도 적용되지 않는다
```
---
## 10. 자주 묻는 질문 (FAQ)
### Q1. 개발비를 깎아주면 내 수당도 줄어드나?
**그렇다.** 수당은 최종 개발비 기준으로 계산한다. 개발비를 10% 깎으면 수당도 10% 줄어든다.
### Q2. 구독료를 깎아줘도 수당이 줄어드나?
**아니다.** 수당은 개발비에서만 계산한다. 구독료 할인은 수당에 영향 없다.
### Q3. 단체 가입이 수당률이 더 높은가?
**그렇다.** 단체 가입은 수당률 30%, 개인 가입은 파트너 20% + 유치 파트너 5% = 25%이므로 단체가 더 높다.
### Q4. 수당은 한 번에 받나?
**2번에 나눠서** 받는다. 고객이 계약금(1차)을 내면 수당 50%, 잔금(2차)을 내면 수당 50%를 익월 10일에 지급한다.
### Q5. 개발비를 전액 면제하면 수당은?
**0원이다.** 개발비가 0원이면 수당도 0원이다. 전액 면제는 최저 개발비가 0원인 상품에서만 가능하다.
### Q6. 2년차부터는 고객이 얼마를 내나?
2년차부터는 **구독료만** 낸다. 예를 들어 월 구독료 50만원이면 연 600만원이다.
---
## 11. 빠른 참고 요약표
### 수당 조견표 (개인 가입, 파트너 수당 20% + 유치 파트너 5%)
| 최종 개발비 | 파트너 수당 | 유치 파트너 수당 | 합계 |
|----------:|----------:|----------:|-----:|
| 2,000만원 | 400만원 | 100만원 | 500만원 |
| 1,800만원 | 360만원 | 90만원 | 450만원 |
| 1,500만원 | 300만원 | 75만원 | 375만원 |
| 1,000만원 | 200만원 | 50만원 | 250만원 |
| 500만원 | 100만원 | 25만원 | 125만원 |
> 매니저 수당은 별도로 첫달 구독료를 지급한다.
### 수당 조견표 (단체 가입, 단체 수당 30%)
| 최종 개발비 | 단체 수당 |
|----------:|----------:|
| 2,000만원 | 600만원 |
| 1,800만원 | 540만원 |
| 1,500만원 | 450만원 |
| 1,000만원 | 300만원 |
| 500만원 | 150만원 |
> 매니저 수당은 별도로 첫달 구독료를 지급한다.
---
## 관련 문서
- [고객 요금 안내](../rules/customer-pricing.md) - 상세 요금 체계
- [영업파트너 수당 체계](../rules/partner-commission.md) - 수당 정산 상세
---
> **(주)코드브릿지엑스** | SAM (Smart Automation Management)
---
**최종 업데이트**: 2026-03-16
# SAM 가격정책 & 가격 시뮬레이터 안내
> **작성일**: 2026-03-15
> **대상**: 영업파트너
> **상태**: 설계 확정
---
## 1. SAM 서비스란?
SAM(Smart Automation Management)은 제조업·시공업을 위한 **클라우드 업무 관리 시스템**이다.
품목관리, 견적, 수주, 생산, 출하, 인사, 회계까지 **하나의 시스템**으로 관리할 수 있다.
---
## 2. 요금 구조 한눈에 보기
SAM의 요금은 **딱 두 가지**로 구성된다.
```
┌────────────────────────────────────────────┐
│ │
│ 1) 개발비 = 처음 한 번만 내는 돈 │
│ 2) 구독료 = 매달 내는 돈 │
│ │
│ ※ 모든 금액은 VAT(부가세) 별도 │
│ │
└────────────────────────────────────────────┘
```
| 구분 | 의미 | 비유 |
|------|------|------|
| **개발비** | 고객 맞춤 시스템을 구축하는 비용 | 집을 짓는 비용 |
| **구독료** | 시스템 유지·관리·클라우드 비용 | 매달 내는 관리비 |
---
## 3. 기본 상품 가격표
### 3.1 제조업 기본 패키지 (가장 많이 팔리는 상품)
```
┌─────────────────────────────────────────────────────┐
│ 제조업 기본 패키지 │
│ │
│ 포함 기능: │
│ 품목관리 → 견적 → 수주 → 생산 → 출하 │
│ + ERP (인사/회계) 무료 포함 │
│ │
│ 개발비: 2,000만원 │
│ 구독료: 50만원/월 │
└─────────────────────────────────────────────────────┘
```
### 3.2 개별 모듈 (필요한 것만 추가)
| 모듈명 | 개발비 | 구독료/월 | 어떤 기능? |
|--------|-------:|--------:|-----------|
| QR코드 관리 | 1,020만원 | 5만원 | 제품에 QR코드 부착, 이력 추적 |
| 사진/출하 관리 | 1,920만원 | 10만원 | 출하 사진 촬영·등록, 배송 관리 |
| 검사/토큰 적용 | 1,020만원 | 5만원 | 품질 검사, AI 토큰 활용 |
### 3.3 통합 패키지 (대형 프로젝트용)
| 패키지명 | 개발비 | 구독료/월 | 어떤 기능? |
|---------|-------:|--------:|-----------|
| 공사관리 패키지 | 4,000만원 | 20만원 | 시공 현장 관리, 공정 추적 |
| 공정/정부지원사업 | 8,000만원 | 40만원 | 정부지원사업 관리 + 전체 공정 관리 |
### 3.4 추가 옵션
| 옵션명 | 개발비 추가 | 구독료 추가/월 |
|--------|----------:|------------:|
| 생산공정 1개 추가 | 500만원 | 10만원 |
| 품질관리 (인정검사) | 2,000만원 | 50만원 |
| 사진 등록 | - | 10만원 |
| 챗봇/녹음/업무일지 | - | 각 20만원 |
| 연구소 연구노트 | - | 5만원 |
| 장비점검, 사무소 정비 | - | 5만원 |
---
## 4. 할인/프로모션 (고객에게 제안할 수 있는 것들)
가격 시뮬레이터에서 **3가지 할인**을 조합할 수 있다.
### 4.1 개발비 할인
| 방식 | 설명 | 예시 |
|------|------|------|
| **% 할인** | 개발비에서 N% 깎아주기 | 2,000만원 → 10% 할인 → **1,800만원** |
| **정액 할인** | 개발비에서 N원 깎아주기 | 2,000만원 → 300만원 할인 → **1,700만원** |
| **전액 면제** | 개발비 0원 (특별 프로모션) | 최저가가 0원인 상품만 가능 |
### 4.2 구독료 할인
구독료에서 **최대 50%**까지 할인 가능하다.
```
예) 구독료 50만원/월 → 20% 할인 → 40만원/월
```
### 4.3 개발비-구독료 연동 (선택)
개발비를 깎아주면 구독료도 같은 비율로 자동 조정하는 기능이다.
```
기본: 개발비 2,000만원, 구독료 50만원/월 (비율 2.5%)
개발비 1,500만원으로 조정하면?
→ 구독료도 자동으로 37.5만원/월로 조정
개발비 1,000만원으로 조정하면?
→ 구독료도 자동으로 25만원/월로 조정
```
> 이 기능은 **켜고 끌 수 있다**. 끄면 개발비만 조정하고 구독료는 정가 유지.
---
## 5. 1년차 총 비용 계산법
```
┌─────────────────────────────────────────────────────┐
│ │
│ 1년차 총 비용 = 개발비 + (월 구독료 × 유료 개월수) │
│ │
└─────────────────────────────────────────────────────┘
```
### 예시: 제조업 기본 패키지, 10% 할인
| 항목 | 계산 | 금액 |
|------|------|-----:|
| 개발비 (정가) | - | 2,000만원 |
| 개발비 할인 | 2,000 × 10% | -200만원 |
| **최종 개발비** | - | **1,800만원** |
| 월 구독료 | - | 50만원 |
| 유료 개월수 | 12개월 | 12개월 |
| 연 구독료 | 50 × 12 | 600만원 |
| **1년차 총 비용** | 1,800 + 600 | **2,400만원** |
> 2년차부터는 구독료만 내면 된다: 50만원 × 12 = **600만원/년**
---
## 6. 영업파트너 수당 (내가 얼마 받나?)
### 6.1 수당 핵심 원칙
```
┌─────────────────────────────────────────────────────┐
│ │
│ 수당 = 최종 개발비 × 수당률 │
│ │
│ ※ 구독료는 수당 계산에 포함되지 않는다 │
│ ※ 할인 적용 후 개발비가 수당 기준이다 │
│ │
└─────────────────────────────────────────────────────┘
```
### 6.2 가입 유형별 수당률
| | 개인 가입 | 단체 가입 |
|---|:---:|:---:|
| **파트너 수당** | **20%** | **30%** |
| **유치 파트너 수당** | 5% | - |
| **매니저 수당** | 첫달 구독료 | - |
- **개인 가입**: 개별 고객이 직접 계약 (파트너 20% + 유치 파트너 5% + 매니저 수당)
- **단체 가입**: 여러 고객을 묶어서 한꺼번에 계약 (단체 수당 30%)
### 6.3 수당 계산 예시 (제조업 기본 패키지)
#### 정가 그대로 판매한 경우 (개발비 2,000만원)
| 가입유형 | 파트너 수당 | 유치 파트너 수당 | 합계 | 매니저 수당 |
|---------|----------:|----------:|-----:|----------:|
| 개인 가입 | **400만원** | 100만원 | 500만원 | 첫달 구독료 (50만원) |
| 단체 가입 | **600만원** | 60만원 | 660만원 | 첫달 구독료 (50만원) |
#### 10% 할인 판매한 경우 (개발비 1,800만원)
| 가입유형 | 파트너 수당 | 유치 파트너 수당 | 합계 | 매니저 수당 |
|---------|----------:|----------:|-----:|----------:|
| 개인 가입 | **360만원** | 90만원 | 450만원 | 첫달 구독료 (50만원) |
| 단체 가입 | **540만원** | 54만원 | 594만원 | 첫달 구독료 (50만원) |
> 할인하면 내 수당도 줄어든다! **가능하면 정가에 가깝게 팔수록 수당이 많다.**
### 6.4 수당은 언제 받나?
개발비를 고객이 **2번에 나눠** 내면, 수당도 **2번에 나눠** 받는다.
```
고객 1차 납입 (계약금)
익월 10일 → 수당 50% 지급
고객 2차 납입 (잔금)
익월 10일 → 수당 50% 지급
```
**예시**: 개발비 2,000만원, 파트너 수당 400만원
| 시점 | 받는 금액 |
|------|--------:|
| 1차 납입 후 익월 10일 | 200만원 |
| 2차 납입 후 익월 10일 | 200만원 |
| **합계** | **400만원** |
---
## 7. 사용량 기반 추가 과금 (고객 안내용)
기본 제공 한도를 초과하면 실비가 부과된다. 고객에게 미리 안내하면 좋다.
| 항목 | 기본 제공 | 초과 시 |
|------|----------|--------|
| 파일 저장 공간 | 100GB | 100GB당 5만원/월 |
| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 |
**AI 토큰 100만 개로 할 수 있는 일:**
| 용도 | 처리량 |
|------|--------|
| 음성 회의 요약 | 약 520분 (8.6시간) |
| 문서 정리 (A4) | 약 300~400매 |
| 이메일/노트 분류 | 약 1,500~2,000건 |
> 대부분의 중소기업은 기본 제공량으로 충분하다.
---
## 8. 바로빌 부가 서비스 (선택 사항)
세금계산서, 카드내역, 계좌조회 등을 SAM에서 바로 처리할 수 있는 연동 서비스이다.
| 서비스 | 월 요금 | 기본 제공 | 초과 과금 |
|--------|-------:|----------|----------|
| 계좌조회 | 1만원 | 1계좌 | 추가 1계좌당 1만원 |
| 카드내역 | 1만원 | 5장 | 추가 1장당 5천원 |
| 홈택스 매입/매출 | **무료** | - | - |
| 세금계산서 발행 | - | 100건 | 추가 50건당 5천원 |
---
## 9. 가격 시뮬레이터 사용법
가격 시뮬레이터는 MNG(관리자 시스템)에서 사용할 수 있는 **영업 지원 도구**이다.
### 9.1 시뮬레이터로 할 수 있는 일
```
┌─────────────────────────────────────────────────────┐
│ 가격 시뮬레이터 │
│ │
│ 1. 상품을 골라서 체크한다 │
│ 2. 할인을 조합한다 │
│ 3. 가입 유형(개인/단체)을 선택한다 │
│ 4. 자동으로 계산된다: │
│ - 최종 개발비 │
│ - 최종 구독료 │
│ - 1년차 총 비용 │
│ - 내 수당 (파트너/유치 파트너) │
│ │
│ → 고객에게 제안하기 전에 미리 확인! │
└─────────────────────────────────────────────────────┘
```
### 9.2 사용 순서
**Step 1: 상품 선택**
- 카테고리별로 상품 목록이 표시된다
- 필수 상품은 이미 체크되어 있다
- 고객에게 필요한 상품을 추가로 체크한다
**Step 2: 개발비 조정 (선택)**
- 재량권이 있는 상품은 슬라이더로 개발비를 조정할 수 있다
- 최저 개발비 이하로는 내릴 수 없다
**Step 3: 할인 설정**
- 개발비 할인: % 또는 정액
- 구독료 할인: 0~50%
- 개발비-구독료 연동: ON/OFF
**Step 4: 가입 유형 선택**
- 개인 가입 / 단체 가입 전환
- 수당률이 자동으로 바뀐다
**Step 5: 결과 확인**
- 최종 개발비, 구독료, 1년차 총 비용
- 파트너 수당, 유치 파트너 수당이 실시간으로 표시된다
### 9.3 주의 사항
```
※ 시뮬레이터 결과는 "참고용"이다
※ 실제 계약 금액은 고객과 별도 협의에 따른다
※ 최저가 이하로는 어떤 할인도 적용되지 않는다
```
---
## 10. 자주 묻는 질문 (FAQ)
### Q1. 개발비를 깎아주면 내 수당도 줄어드나?
**그렇다.** 수당은 최종 개발비 기준으로 계산한다. 개발비를 10% 깎으면 수당도 10% 줄어든다.
### Q2. 구독료를 깎아줘도 수당이 줄어드나?
**아니다.** 수당은 개발비에서만 계산한다. 구독료 할인은 수당에 영향 없다.
### Q3. 단체 가입이 수당률이 더 높은가?
**그렇다.** 단체 가입은 수당률 30%, 개인 가입은 파트너 20% + 유치 파트너 5% = 25%이므로 단체가 더 높다.
### Q4. 수당은 한 번에 받나?
**2번에 나눠서** 받는다. 고객이 계약금(1차)을 내면 수당 50%, 잔금(2차)을 내면 수당 50%를 익월 10일에 지급한다.
### Q5. 개발비를 전액 면제하면 수당은?
**0원이다.** 개발비가 0원이면 수당도 0원이다. 전액 면제는 최저 개발비가 0원인 상품에서만 가능하다.
### Q6. 2년차부터는 고객이 얼마를 내나?
2년차부터는 **구독료만** 낸다. 예를 들어 월 구독료 50만원이면 연 600만원이다.
---
## 11. 빠른 참고 요약표
### 수당 조견표 (개인 가입, 파트너 수당 20% + 유치 파트너 5%)
| 최종 개발비 | 파트너 수당 | 유치 파트너 수당 | 합계 |
|----------:|----------:|----------:|-----:|
| 2,000만원 | 400만원 | 100만원 | 500만원 |
| 1,800만원 | 360만원 | 90만원 | 450만원 |
| 1,500만원 | 300만원 | 75만원 | 375만원 |
| 1,000만원 | 200만원 | 50만원 | 250만원 |
| 500만원 | 100만원 | 25만원 | 125만원 |
> 매니저 수당은 별도로 첫달 구독료를 지급한다.
### 수당 조견표 (단체 가입, 단체 수당 30%)
| 최종 개발비 | 단체 수당 |
|----------:|----------:|
| 2,000만원 | 600만원 |
| 1,800만원 | 540만원 |
| 1,500만원 | 450만원 |
| 1,000만원 | 300만원 |
| 500만원 | 150만원 |
> 매니저 수당은 별도로 첫달 구독료를 지급한다.
---
## 관련 문서
- [고객 요금 안내](../rules/customer-pricing.md) - 상세 요금 체계
- [영업파트너 수당 체계](../rules/partner-commission.md) - 수당 정산 상세
---
> **(주)코드브릿지엑스** | SAM (Smart Automation Management)
---
**최종 업데이트**: 2026-03-16

View File

@@ -1,433 +1,433 @@
# SAM 가격정책 쉬운 안내서
> **작성일**: 2026-03-15
> **대상**: 영업파트너
> **상태**: 설계 확정
---
## 한 장으로 보는 SAM 요금 구조
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 고객이 내는 돈은 딱 2가지뿐이다 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 개 발 비 │ │ 구 독 료 │ │
│ │ │ │ │ │
│ │ 처음 1번만 │ │ 매달 내는 돈 │ │
│ │ 내는 돈 │ │ │ │
│ │ │ │ │ │
│ │ 집 짓는 비용 │ │ 관리비 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ※ 모든 금액은 부가세(VAT) 별도 │
│ │
└─────────────────────────────────────────────────────────────┘
```
**쉽게 비유하면:**
- **개발비** = 고객 맞춤으로 **집을 지어주는 비용** (한 번만 냄)
- **구독료** = 지은 집의 **관리비** (매달 냄)
---
## 1. 상품별 가격표
### 가장 많이 팔리는 상품
```
┌─────────────────────────────────────────────────────┐
│ │
│ ★ 제조업 기본 패키지 ★ │
│ │
│ 품목관리 → 견적 → 수주 → 생산 → 출하 │
│ + ERP(인사/회계) 무료 포함! │
│ │
│ 개발비: 2,000만원 │
│ 구독료: 50만원/월 │
│ │
└─────────────────────────────────────────────────────┘
```
### 개별 모듈 (필요한 것만 골라서 추가)
| 모듈명 | 개발비 | 구독료/월 | 이런 고객에게 추천 |
|--------|------:|--------:|-----------------|
| QR코드 관리 | 1,020만원 | 5만원 | 제품 이력 추적이 필요한 곳 |
| 사진/출하 관리 | 1,920만원 | 10만원 | 출하·배송 기록이 중요한 곳 |
| 검사/토큰 적용 | 1,020만원 | 5만원 | 품질검사 + AI 활용이 필요한 곳 |
### 대형 패키지 (큰 프로젝트용)
| 패키지명 | 개발비 | 구독료/월 | 이런 고객에게 추천 |
|---------|------:|--------:|-----------------|
| 공사관리 패키지 | 4,000만원 | 20만원 | 시공 현장 관리가 필요한 곳 |
| 공정/정부지원사업 | 8,000만원 | 40만원 | 정부지원사업 + 전체 공정 관리 |
### 추가 옵션 (선택 사항)
| 옵션명 | 개발비 추가 | 구독료 추가/월 |
|--------|----------:|------------:|
| 생산공정 1개 추가 | 500만원 | 10만원 |
| 품질관리 (인정검사) | 2,000만원 | 50만원 |
| 사진 등록 | - | 10만원 |
| 챗봇 | - | 20만원 |
| 녹음 | - | 20만원 |
| 업무일지 | - | 20만원 |
| 연구소 연구노트 | - | 5만원 |
| 장비점검, 사무소 정비 | - | 5만원 |
---
## 2. 내 수당은 얼마인가?
### 핵심 공식 (이것만 기억하면 된다)
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 내 수당 = 최종 개발비 × 수당률 │
│ │
│ ※ 구독료는 수당 계산에 포함 안 됨! │
│ ※ 할인 적용 후 개발비가 기준! │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 개인 가입 vs 단체 가입
```
┌─ 개인 가입 ─────────────────┐ ┌─ 단체 가입 ─────────────────┐
│ │ │ │
│ 파트너 수당: 20% │ │ 단체 수당: 30% │
│ 유치 파트너 수당: 5% │ │ │
│ ───────────── │ │ (여러 고객 묶어서 계약) │
│ 합계: 25% │ │ │
│ │ │ │
│ (개별 고객이 직접 계약) │ │ │
│ │ │ │
│ + 매니저 수당: 첫달 구독료 │ │ │
│ (별도 지급) │ │ │
└──────────────────────────────┘ └──────────────────────────────┘
```
### 수당 계산 예시 (제조업 기본 패키지 기준)
**정가(2,000만원) 그대로 판매한 경우:**
| | 개인 가입 | 단체 가입 |
|---|---:|---:|
| 파트너/단체 수당 | **400만원** | **600만원** |
| 유치 파트너 수당 | 100만원 | - |
| **합계** | **500만원** | **600만원** |
| + 매니저 수당 (별도) | 첫달 구독료 50만원 | - |
**10% 할인(1,800만원)으로 판매한 경우:**
| | 개인 가입 | 단체 가입 |
|---|---:|---:|
| 파트너/단체 수당 | **360만원** | **540만원** |
| 유치 파트너 수당 | 90만원 | - |
| **합계** | **450만원** | **540만원** |
| + 매니저 수당 (별도) | 첫달 구독료 50만원 | - |
> 할인하면 내 수당도 줄어든다. **정가에 가깝게 팔수록 수당이 크다!**
> 매니저 수당은 개발비 요율이 아니라, 첫달 구독료를 별도 지급하는 형태이다.
### 수당 빠른 조견표 (개인 가입, 20%)
| 최종 개발비 | 내 수당 |
|----------:|-------:|
| 2,000만원 | **400만원** |
| 1,800만원 | **360만원** |
| 1,500만원 | **300만원** |
| 1,000만원 | **200만원** |
| 500만원 | **100만원** |
### 수당 빠른 조견표 (단체 가입, 30%)
| 최종 개발비 | 내 수당 |
|----------:|-------:|
| 2,000만원 | **600만원** |
| 1,800만원 | **540만원** |
| 1,500만원 | **450만원** |
| 1,000만원 | **300만원** |
| 500만원 | **150만원** |
---
## 3. 수당은 언제 받나?
```
고객이 돈을 2번에 나눠 내면, 수당도 2번에 나눠 받는다.
┌──────────────┐ ┌──────────────┐
│ 고객 1차 납입 │ │ 고객 2차 납입 │
│ (계약금) │ │ (잔금) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
익월 10일에 익월 10일에
수당 50% 지급 수당 50% 지급
```
**예시: 파트너 수당 400만원인 경우**
| 시점 | 받는 금액 |
|------|--------:|
| 고객 계약금 납입 후 다음달 10일 | 200만원 |
| 고객 잔금 납입 후 다음달 10일 | 200만원 |
| **합계** | **400만원** |
---
## 4. 고객에게 제안할 수 있는 할인 3가지
가격 시뮬레이터에서 이 3가지를 조합해서 고객 맞춤 제안을 만들 수 있다.
### 할인 1: 개발비 할인
```
방법 A: "몇 % 깎아드립니다"
예) 2,000만원의 10% 할인 → 1,800만원
방법 B: "얼마 깎아드립니다"
예) 2,000만원에서 300만원 할인 → 1,700만원
방법 C: 개발비 전액 면제 (특별 프로모션)
→ 최저가가 0원인 상품만 가능
```
> 주의: 개발비를 깎으면 내 수당도 같이 줄어든다!
### 할인 2: 구독료 할인
```
구독료에서 최대 50%까지 할인 가능
예) 월 50만원 → 20% 할인 → 월 40만원
```
> 구독료 할인은 내 수당에 영향 없다 (수당은 개발비 기준)
### 할인 3: 개발비-구독료 연동
```
개발비와 구독료가 반비례로 연동
기본: 개발비 2,000만원, 구독료 50만원/월
↓ 개발비를 4,000만원으로 조정
연동: 구독료 → 25만원/월로 자동 조정 (개발비 UP → 구독료 DOWN)
```
> 이 기능은 켜고 끌 수 있다. 끄면 개발비만 조정.
### 할인 조합별 내 수당 영향 정리
| 할인 종류 | 내 수당에 영향? | 이유 |
|----------|:-----------:|------|
| 개발비 할인 | **있음** (줄어듬) | 수당 = 개발비 기준 |
| 구독료 할인 | **없음** | 수당은 개발비에서만 계산 |
| 개발비-구독료 연동 | **있음** (개발비 변동 시) | 개발비가 바뀌면 수당도 변동 |
> 팁: **구독료 할인**은 고객에게 혜택을 주면서 내 수당은 안 줄어드는 좋은 전략!
---
## 5. 1년차 총 비용 계산법
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 1년차 = 개발비 + (월 구독료 × 유료 개월수) │
│ 2년차~ = 월 구독료 × 12개월 (개발비 없음!) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 실전 예시
**상황: 제조업 기본 패키지, 10% 할인**
| 항목 | 계산 | 금액 |
|------|------|----:|
| 개발비 (정가) | | 2,000만원 |
| 개발비 할인 10% | 2,000 × 10% | -200만원 |
| **최종 개발비** | | **1,800만원** |
| 월 구독료 | | 50만원 |
| 연간 구독료 | 50 × 12 | 600만원 |
| **1년차 총 비용** | 1,800 + 600 | **2,400만원** |
> 2년차부터: 50만원 × 12 = **연 600만원만** 내면 된다
**고객에게 이렇게 설명하면 좋다:**
```
"첫해에는 시스템 구축비용이 포함되어 약 2,400만원이지만,
2년차부터는 월 50만원(연 600만원)만 내시면 됩니다."
```
---
## 6. 고객이 추가로 내는 것 (사용량 과금)
기본 제공량을 넘으면 추가 비용이 발생한다. **대부분의 중소기업은 기본량으로 충분하다.**
| 항목 | 기본 제공 | 초과 시 |
|------|----------|--------|
| 파일 저장 공간 | 100GB | 100GB당 5만원/월 |
| AI 토큰 | 월 100만 개 | 실비 과금 |
**AI 토큰 100만 개로 할 수 있는 일:**
- 회의 녹음 요약: 약 8시간 30분
- 문서 정리: A4 약 300~400장
- 이메일/노트 분류: 약 1,500~2,000건
---
## 7. 바로빌 부가 서비스 (고객 선택)
세금계산서, 계좌조회 등을 SAM에서 바로 처리하는 서비스이다.
| 서비스 | 월 요금 | 비고 |
|--------|------:|------|
| 계좌조회 | 1만원 | 기본 1계좌, 추가 1계좌당 1만원 |
| 카드내역 | 1만원 | 기본 5장, 추가 1장당 5천원 |
| 홈택스 매입/매출 | **무료** | (주)코드브릿지엑스가 지원! |
| 세금계산서 발행 | 기본 100건 무료 | 추가 50건당 5천원 |
---
## 8. 가격 시뮬레이터란?
가격 시뮬레이터는 **고객에게 제안하기 전에 미리 비용과 수당을 확인하는 도구**이다.
### 시뮬레이터 사용 흐름
```
Step 1 Step 2 Step 3 Step 4
상품 고르기 → 할인 설정 → 가입유형 선택 → 결과 확인!
☑ 기본 패키지 개발비 10% 할인 ○ 개인 가입 개발비: 1,800만원
☑ QR코드 구독료 20% 할인 ● 단체 가입 구독료: 44만원/월
☐ 공사관리 1년차: 약 2,328만원
내 수당: 540만원!
```
### 상품 선택 화면에서 알아야 할 것
| 표시 | 의미 | 내가 할 수 있는 것 |
|------|------|-----------------|
| **필수** | 기본 포함 상품 | 해제 불가 (자동 체크됨) |
| **재량권** | 개발비 조정 가능 상품 | 슬라이더로 개발비를 최저가~정가 범위에서 조정 |
| **선택** | 추가 상품 | 자유롭게 체크/해제 |
### 시뮬레이터에서 확인할 수 있는 것
```
┌─ 최종 금액 ────────────────────────────────────────────────┐
│ │
│ 최종 개발비: 1,800만원 (정가 2,000만원 → 10% 할인) │
│ 최종 구독료: 44만원/월 (정가 55만원 → 20% 할인) │
│ 1년차 총 비용: 2,196만원 │
│ │
├─ 내 수당 ──────────────────────────────────────────────────┤
│ │
│ 파트너 수당: 360만원 (개인) / 540만원 (단체) │
│ 유치 파트너 수당: 90만원 (개인) / - (단체) │
│ 매니저 수당: 첫달 구독료 (별도 지급) │
│ │
└────────────────────────────────────────────────────────────┘
```
> 시뮬레이터 결과는 **참고용**이다. 실제 계약 금액은 고객과 별도 협의한다.
---
## 9. 영업 실전 팁
### 고객 유형별 추천 전략
| 고객 상황 | 추천 접근법 |
|----------|-----------|
| 초기 비용 부담이 클 때 | 개발비 할인 + 구독료 할인 |
| 매달 비용이 부담될 때 | 구독료 할인 (최대 50%) |
| 세금계산서·카드 관리가 필요할 때 | 바로빌 부가 서비스 안내 (기본 포함) |
| 대형 제조업체 | 기본 패키지 + 공정관리 + 품질관리 |
| 여러 업체를 묶을 수 있을 때 | 단체 가입 (수당률 30%로 UP!) |
### 고객이 자주 하는 질문과 답변
**Q. "왜 이렇게 비싸요?"**
```
"기존에 ERP 도입하면 보통 5천만~1억이 듭니다.
SAM은 2천만원에 ERP + MES + AI까지 포함이에요.
2년차부터는 월 50만원만 내시면 됩니다."
```
**Q. "매달 내는 구독료가 아까운데요?"**
```
"구독료에는 서버 유지, 보안 업데이트, 기능 개선이 포함됩니다.
자체 서버를 운영하시면 서버비 + 인건비로 월 200만원 이상이에요.
월 50만원은 정말 합리적입니다."
```
**Q. "먼저 써볼 수 있나요?"**
```
"네, 데모 환경을 통해 SAM의 주요 기능을 체험해 보실 수 있습니다.
실제 데이터를 넣어보시고 만족하신 후 결정하실 수 있어요."
```
---
## 10. 자주 묻는 질문 (파트너용)
### Q1. 개발비를 깎으면 내 수당도 줄어드나?
**그렇다.** 수당 = 최종 개발비 × 수당률이므로, 개발비를 10% 깎으면 수당도 10% 줄어든다.
### Q2. 구독료를 깎아줘도 수당이 줄어드나?
**아니다.** 수당은 개발비에서만 계산한다. 구독료 할인은 수당에 영향 없다.
### Q3. 단체 가입이 뭔가?
여러 고객을 묶어서 한꺼번에 계약하는 것이다. **단체 수당 30%**를 받는다.
### Q4. 유치 파트너 수당이 뭔가?
나를 유치한 상위 파트너에게 지급되는 수당이다. 개인 가입 시 개발비의 **5%**가 지급된다.
### Q5. 매니저 수당이 뭔가?
매니저 수당은 개발비 요율이 아니라, **첫달 구독료를 별도 지급**하는 형태이다. 예를 들어 구독료가 50만원/월이면 매니저 수당도 50만원이다.
### Q6. 개발비를 전액 면제하면?
수당도 **0원**이 된다. 개발비가 0이면 수당 계산 기준이 0이기 때문이다.
### Q7. 2년차부터 고객이 내는 돈은?
**구독료만** 낸다. 개발비는 처음 한 번만 내는 비용이다.
### Q8. 시뮬레이터 결과대로 계약해야 하나?
**아니다.** 시뮬레이터는 참고용이다. 실제 계약은 고객과 별도 협의로 결정한다.
---
## 관련 문서
- [가격 시뮬레이터 상세 가이드](price-simulator-partner-guide.md) - 시뮬레이터 기능 상세 설명
- [고객 요금 안내](../rules/customer-pricing.md) - 고객에게 보여줄 수 있는 공식 요금표
- [영업파트너 수당 체계](../rules/partner-commission.md) - 수당 정산 상세
---
> **(주)코드브릿지엑스** | SAM (Smart Automation Management)
---
**최종 업데이트**: 2026-03-16
# SAM 가격정책 쉬운 안내서
> **작성일**: 2026-03-15
> **대상**: 영업파트너
> **상태**: 설계 확정
---
## 한 장으로 보는 SAM 요금 구조
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 고객이 내는 돈은 딱 2가지뿐이다 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 개 발 비 │ │ 구 독 료 │ │
│ │ │ │ │ │
│ │ 처음 1번만 │ │ 매달 내는 돈 │ │
│ │ 내는 돈 │ │ │ │
│ │ │ │ │ │
│ │ 집 짓는 비용 │ │ 관리비 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ※ 모든 금액은 부가세(VAT) 별도 │
│ │
└─────────────────────────────────────────────────────────────┘
```
**쉽게 비유하면:**
- **개발비** = 고객 맞춤으로 **집을 지어주는 비용** (한 번만 냄)
- **구독료** = 지은 집의 **관리비** (매달 냄)
---
## 1. 상품별 가격표
### 가장 많이 팔리는 상품
```
┌─────────────────────────────────────────────────────┐
│ │
│ ★ 제조업 기본 패키지 ★ │
│ │
│ 품목관리 → 견적 → 수주 → 생산 → 출하 │
│ + ERP(인사/회계) 무료 포함! │
│ │
│ 개발비: 2,000만원 │
│ 구독료: 50만원/월 │
│ │
└─────────────────────────────────────────────────────┘
```
### 개별 모듈 (필요한 것만 골라서 추가)
| 모듈명 | 개발비 | 구독료/월 | 이런 고객에게 추천 |
|--------|------:|--------:|-----------------|
| QR코드 관리 | 1,020만원 | 5만원 | 제품 이력 추적이 필요한 곳 |
| 사진/출하 관리 | 1,920만원 | 10만원 | 출하·배송 기록이 중요한 곳 |
| 검사/토큰 적용 | 1,020만원 | 5만원 | 품질검사 + AI 활용이 필요한 곳 |
### 대형 패키지 (큰 프로젝트용)
| 패키지명 | 개발비 | 구독료/월 | 이런 고객에게 추천 |
|---------|------:|--------:|-----------------|
| 공사관리 패키지 | 4,000만원 | 20만원 | 시공 현장 관리가 필요한 곳 |
| 공정/정부지원사업 | 8,000만원 | 40만원 | 정부지원사업 + 전체 공정 관리 |
### 추가 옵션 (선택 사항)
| 옵션명 | 개발비 추가 | 구독료 추가/월 |
|--------|----------:|------------:|
| 생산공정 1개 추가 | 500만원 | 10만원 |
| 품질관리 (인정검사) | 2,000만원 | 50만원 |
| 사진 등록 | - | 10만원 |
| 챗봇 | - | 20만원 |
| 녹음 | - | 20만원 |
| 업무일지 | - | 20만원 |
| 연구소 연구노트 | - | 5만원 |
| 장비점검, 사무소 정비 | - | 5만원 |
---
## 2. 내 수당은 얼마인가?
### 핵심 공식 (이것만 기억하면 된다)
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 내 수당 = 최종 개발비 × 수당률 │
│ │
│ ※ 구독료는 수당 계산에 포함 안 됨! │
│ ※ 할인 적용 후 개발비가 기준! │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 개인 가입 vs 단체 가입
```
┌─ 개인 가입 ─────────────────┐ ┌─ 단체 가입 ─────────────────┐
│ │ │ │
│ 파트너 수당: 20% │ │ 단체 수당: 30% │
│ 유치 파트너 수당: 5% │ │ │
│ ───────────── │ │ (여러 고객 묶어서 계약) │
│ 합계: 25% │ │ │
│ │ │ │
│ (개별 고객이 직접 계약) │ │ │
│ │ │ │
│ + 매니저 수당: 첫달 구독료 │ │ │
│ (별도 지급) │ │ │
└──────────────────────────────┘ └──────────────────────────────┘
```
### 수당 계산 예시 (제조업 기본 패키지 기준)
**정가(2,000만원) 그대로 판매한 경우:**
| | 개인 가입 | 단체 가입 |
|---|---:|---:|
| 파트너/단체 수당 | **400만원** | **600만원** |
| 유치 파트너 수당 | 100만원 | - |
| **합계** | **500만원** | **600만원** |
| + 매니저 수당 (별도) | 첫달 구독료 50만원 | - |
**10% 할인(1,800만원)으로 판매한 경우:**
| | 개인 가입 | 단체 가입 |
|---|---:|---:|
| 파트너/단체 수당 | **360만원** | **540만원** |
| 유치 파트너 수당 | 90만원 | - |
| **합계** | **450만원** | **540만원** |
| + 매니저 수당 (별도) | 첫달 구독료 50만원 | - |
> 할인하면 내 수당도 줄어든다. **정가에 가깝게 팔수록 수당이 크다!**
> 매니저 수당은 개발비 요율이 아니라, 첫달 구독료를 별도 지급하는 형태이다.
### 수당 빠른 조견표 (개인 가입, 20%)
| 최종 개발비 | 내 수당 |
|----------:|-------:|
| 2,000만원 | **400만원** |
| 1,800만원 | **360만원** |
| 1,500만원 | **300만원** |
| 1,000만원 | **200만원** |
| 500만원 | **100만원** |
### 수당 빠른 조견표 (단체 가입, 30%)
| 최종 개발비 | 내 수당 |
|----------:|-------:|
| 2,000만원 | **600만원** |
| 1,800만원 | **540만원** |
| 1,500만원 | **450만원** |
| 1,000만원 | **300만원** |
| 500만원 | **150만원** |
---
## 3. 수당은 언제 받나?
```
고객이 돈을 2번에 나눠 내면, 수당도 2번에 나눠 받는다.
┌──────────────┐ ┌──────────────┐
│ 고객 1차 납입 │ │ 고객 2차 납입 │
│ (계약금) │ │ (잔금) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
익월 10일에 익월 10일에
수당 50% 지급 수당 50% 지급
```
**예시: 파트너 수당 400만원인 경우**
| 시점 | 받는 금액 |
|------|--------:|
| 고객 계약금 납입 후 다음달 10일 | 200만원 |
| 고객 잔금 납입 후 다음달 10일 | 200만원 |
| **합계** | **400만원** |
---
## 4. 고객에게 제안할 수 있는 할인 3가지
가격 시뮬레이터에서 이 3가지를 조합해서 고객 맞춤 제안을 만들 수 있다.
### 할인 1: 개발비 할인
```
방법 A: "몇 % 깎아드립니다"
예) 2,000만원의 10% 할인 → 1,800만원
방법 B: "얼마 깎아드립니다"
예) 2,000만원에서 300만원 할인 → 1,700만원
방법 C: 개발비 전액 면제 (특별 프로모션)
→ 최저가가 0원인 상품만 가능
```
> 주의: 개발비를 깎으면 내 수당도 같이 줄어든다!
### 할인 2: 구독료 할인
```
구독료에서 최대 50%까지 할인 가능
예) 월 50만원 → 20% 할인 → 월 40만원
```
> 구독료 할인은 내 수당에 영향 없다 (수당은 개발비 기준)
### 할인 3: 개발비-구독료 연동
```
개발비와 구독료가 반비례로 연동
기본: 개발비 2,000만원, 구독료 50만원/월
↓ 개발비를 4,000만원으로 조정
연동: 구독료 → 25만원/월로 자동 조정 (개발비 UP → 구독료 DOWN)
```
> 이 기능은 켜고 끌 수 있다. 끄면 개발비만 조정.
### 할인 조합별 내 수당 영향 정리
| 할인 종류 | 내 수당에 영향? | 이유 |
|----------|:-----------:|------|
| 개발비 할인 | **있음** (줄어듬) | 수당 = 개발비 기준 |
| 구독료 할인 | **없음** | 수당은 개발비에서만 계산 |
| 개발비-구독료 연동 | **있음** (개발비 변동 시) | 개발비가 바뀌면 수당도 변동 |
> 팁: **구독료 할인**은 고객에게 혜택을 주면서 내 수당은 안 줄어드는 좋은 전략!
---
## 5. 1년차 총 비용 계산법
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ 1년차 = 개발비 + (월 구독료 × 유료 개월수) │
│ 2년차~ = 월 구독료 × 12개월 (개발비 없음!) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 실전 예시
**상황: 제조업 기본 패키지, 10% 할인**
| 항목 | 계산 | 금액 |
|------|------|----:|
| 개발비 (정가) | | 2,000만원 |
| 개발비 할인 10% | 2,000 × 10% | -200만원 |
| **최종 개발비** | | **1,800만원** |
| 월 구독료 | | 50만원 |
| 연간 구독료 | 50 × 12 | 600만원 |
| **1년차 총 비용** | 1,800 + 600 | **2,400만원** |
> 2년차부터: 50만원 × 12 = **연 600만원만** 내면 된다
**고객에게 이렇게 설명하면 좋다:**
```
"첫해에는 시스템 구축비용이 포함되어 약 2,400만원이지만,
2년차부터는 월 50만원(연 600만원)만 내시면 됩니다."
```
---
## 6. 고객이 추가로 내는 것 (사용량 과금)
기본 제공량을 넘으면 추가 비용이 발생한다. **대부분의 중소기업은 기본량으로 충분하다.**
| 항목 | 기본 제공 | 초과 시 |
|------|----------|--------|
| 파일 저장 공간 | 100GB | 100GB당 5만원/월 |
| AI 토큰 | 월 100만 개 | 실비 과금 |
**AI 토큰 100만 개로 할 수 있는 일:**
- 회의 녹음 요약: 약 8시간 30분
- 문서 정리: A4 약 300~400장
- 이메일/노트 분류: 약 1,500~2,000건
---
## 7. 바로빌 부가 서비스 (고객 선택)
세금계산서, 계좌조회 등을 SAM에서 바로 처리하는 서비스이다.
| 서비스 | 월 요금 | 비고 |
|--------|------:|------|
| 계좌조회 | 1만원 | 기본 1계좌, 추가 1계좌당 1만원 |
| 카드내역 | 1만원 | 기본 5장, 추가 1장당 5천원 |
| 홈택스 매입/매출 | **무료** | (주)코드브릿지엑스가 지원! |
| 세금계산서 발행 | 기본 100건 무료 | 추가 50건당 5천원 |
---
## 8. 가격 시뮬레이터란?
가격 시뮬레이터는 **고객에게 제안하기 전에 미리 비용과 수당을 확인하는 도구**이다.
### 시뮬레이터 사용 흐름
```
Step 1 Step 2 Step 3 Step 4
상품 고르기 → 할인 설정 → 가입유형 선택 → 결과 확인!
☑ 기본 패키지 개발비 10% 할인 ○ 개인 가입 개발비: 1,800만원
☑ QR코드 구독료 20% 할인 ● 단체 가입 구독료: 44만원/월
☐ 공사관리 1년차: 약 2,328만원
내 수당: 540만원!
```
### 상품 선택 화면에서 알아야 할 것
| 표시 | 의미 | 내가 할 수 있는 것 |
|------|------|-----------------|
| **필수** | 기본 포함 상품 | 해제 불가 (자동 체크됨) |
| **재량권** | 개발비 조정 가능 상품 | 슬라이더로 개발비를 최저가~정가 범위에서 조정 |
| **선택** | 추가 상품 | 자유롭게 체크/해제 |
### 시뮬레이터에서 확인할 수 있는 것
```
┌─ 최종 금액 ────────────────────────────────────────────────┐
│ │
│ 최종 개발비: 1,800만원 (정가 2,000만원 → 10% 할인) │
│ 최종 구독료: 44만원/월 (정가 55만원 → 20% 할인) │
│ 1년차 총 비용: 2,196만원 │
│ │
├─ 내 수당 ──────────────────────────────────────────────────┤
│ │
│ 파트너 수당: 360만원 (개인) / 540만원 (단체) │
│ 유치 파트너 수당: 90만원 (개인) / - (단체) │
│ 매니저 수당: 첫달 구독료 (별도 지급) │
│ │
└────────────────────────────────────────────────────────────┘
```
> 시뮬레이터 결과는 **참고용**이다. 실제 계약 금액은 고객과 별도 협의한다.
---
## 9. 영업 실전 팁
### 고객 유형별 추천 전략
| 고객 상황 | 추천 접근법 |
|----------|-----------|
| 초기 비용 부담이 클 때 | 개발비 할인 + 구독료 할인 |
| 매달 비용이 부담될 때 | 구독료 할인 (최대 50%) |
| 세금계산서·카드 관리가 필요할 때 | 바로빌 부가 서비스 안내 (기본 포함) |
| 대형 제조업체 | 기본 패키지 + 공정관리 + 품질관리 |
| 여러 업체를 묶을 수 있을 때 | 단체 가입 (수당률 30%로 UP!) |
### 고객이 자주 하는 질문과 답변
**Q. "왜 이렇게 비싸요?"**
```
"기존에 ERP 도입하면 보통 5천만~1억이 듭니다.
SAM은 2천만원에 ERP + MES + AI까지 포함이에요.
2년차부터는 월 50만원만 내시면 됩니다."
```
**Q. "매달 내는 구독료가 아까운데요?"**
```
"구독료에는 서버 유지, 보안 업데이트, 기능 개선이 포함됩니다.
자체 서버를 운영하시면 서버비 + 인건비로 월 200만원 이상이에요.
월 50만원은 정말 합리적입니다."
```
**Q. "먼저 써볼 수 있나요?"**
```
"네, 데모 환경을 통해 SAM의 주요 기능을 체험해 보실 수 있습니다.
실제 데이터를 넣어보시고 만족하신 후 결정하실 수 있어요."
```
---
## 10. 자주 묻는 질문 (파트너용)
### Q1. 개발비를 깎으면 내 수당도 줄어드나?
**그렇다.** 수당 = 최종 개발비 × 수당률이므로, 개발비를 10% 깎으면 수당도 10% 줄어든다.
### Q2. 구독료를 깎아줘도 수당이 줄어드나?
**아니다.** 수당은 개발비에서만 계산한다. 구독료 할인은 수당에 영향 없다.
### Q3. 단체 가입이 뭔가?
여러 고객을 묶어서 한꺼번에 계약하는 것이다. **단체 수당 30%**를 받는다.
### Q4. 유치 파트너 수당이 뭔가?
나를 유치한 상위 파트너에게 지급되는 수당이다. 개인 가입 시 개발비의 **5%**가 지급된다.
### Q5. 매니저 수당이 뭔가?
매니저 수당은 개발비 요율이 아니라, **첫달 구독료를 별도 지급**하는 형태이다. 예를 들어 구독료가 50만원/월이면 매니저 수당도 50만원이다.
### Q6. 개발비를 전액 면제하면?
수당도 **0원**이 된다. 개발비가 0이면 수당 계산 기준이 0이기 때문이다.
### Q7. 2년차부터 고객이 내는 돈은?
**구독료만** 낸다. 개발비는 처음 한 번만 내는 비용이다.
### Q8. 시뮬레이터 결과대로 계약해야 하나?
**아니다.** 시뮬레이터는 참고용이다. 실제 계약은 고객과 별도 협의로 결정한다.
---
## 관련 문서
- [가격 시뮬레이터 상세 가이드](price-simulator-partner-guide.md) - 시뮬레이터 기능 상세 설명
- [고객 요금 안내](../rules/customer-pricing.md) - 고객에게 보여줄 수 있는 공식 요금표
- [영업파트너 수당 체계](../rules/partner-commission.md) - 수당 정산 상세
---
> **(주)코드브릿지엑스** | SAM (Smart Automation Management)
---
**최종 업데이트**: 2026-03-16

View File

@@ -1,339 +1,339 @@
# 급여관리 API 구현 계획
> **작성일**: 2026-03-11
> **상태**: 계획 수립
> **참조**: MNG 급여관리 시스템 (`mng/app/Services/HR/PayrollService.php`)
---
## 1. 개요
### 1.1 목적
MNG에서 운영 중인 급여관리 시스템의 핵심 비즈니스 로직을 API 서버에 구현한다. React 프론트엔드에서 급여 관리 기능을 사용할 수 있도록 완전한 REST API를 제공한다.
### 1.2 배경
- MNG 급여관리: 완성도 100% (CRUD, 자동계산, 일괄생성, PDF 명세서, 전표변환)
- API 급여관리: 완성도 ~50% (기본 CRUD만 구현, 핵심 계산 로직 누락)
- React에서 급여관리 화면을 구현하려면 API에 동일한 비즈니스 로직이 필요하다
### 1.3 원칙
- MNG의 검증된 로직을 API 컨벤션에 맞게 이식한다
- API 프로젝트의 Service-First 아키텍처, i18n, FormRequest 패턴을 준수한다
- 기존 `payrolls` 테이블 스키마를 그대로 사용한다 (추가 마이그레이션 최소화)
---
## 2. 현황 분석 (GAP)
### 2.1 기능 비교
| 기능 | MNG | API | GAP |
|------|:---:|:---:|-----|
| 급여 CRUD | ✅ | ✅ | - |
| 급여 설정 CRUD | ✅ | ✅ | - |
| 목록 조회 (필터/페이지네이션) | ✅ | ✅ | - |
| 월별 통계 | ✅ | ✅ | - |
| 확정 (`confirm`) | ✅ | ✅ | - |
| 지급 처리 (`pay`) | ✅ | ✅ | - |
| 일괄 확정 (`bulkConfirm`) | ✅ | ✅ | - |
| **소득세 자동 계산** | ✅ | ❌ | 간이세액표 기반 계산 로직 전체 누락 |
| **4대보험 자동 계산** | ✅ | ⚠️ | 설정값만 존재, `calculateAmounts()` 미구현 |
| **공제 오버라이드** | ✅ | ❌ | 수동 공제 수정 후 재계산 미지원 |
| **확정 취소 (`unconfirm`)** | ✅ | ❌ | 상태 복구 불가 |
| **지급 취소 (`unpay`)** | ✅ | ❌ | 슈퍼관리자 기능 누락 |
| **일괄 생성 (`bulkGenerate`)** | ✅ | ❌ | 재직사원 기반 신규 생성 미구현 |
| **전월 복사 (`copyFromPrevious`)** | ✅ | ❌ | 이전 월 데이터 복사 미구현 |
| **급여명세서 PDF 생성** | ✅ | ❌ | 데이터 조회만 가능, PDF 미생성 |
| **급여명세서 이메일 발송** | ✅ | ❌ | 이메일 발송 미구현 |
| **전표 자동 생성** | ✅ | ❌ | `generateJournalEntry()` 미구현 |
| **엑셀 내보내기** | ✅ | ❌ | export 미구현 |
| **공제대상가족수 자동 산출** | ✅ | ❌ | 피부양자 기반 가족수 미산출 |
### 2.2 API 기존 코드 현황
| 파일 | 상태 | 비고 |
|------|------|------|
| `Controllers/Api/V1/PayrollController.php` | 기본 CRUD 구현 | 누락 엔드포인트 추가 필요 |
| `Services/PayrollService.php` | 기본 CRUD + 제한적 계산 | 핵심 로직 이식 필요 |
| `Models/Tenants/Payroll.php` | 모델 정의 완료 | 상태 헬퍼 메서드 보강 필요 |
| `Models/Tenants/PayrollSetting.php` | 설정 모델 완료 | - |
| `Requests/V1/Payroll/` | FormRequest 5개 존재 | 추가 Request 필요 |
| `routes/api/v1/finance.php` | 기본 라우트 정의 | 누락 엔드포인트 추가 |
---
## 3. 구현 범위
### Phase 1: 핵심 계산 엔진 (필수)
> **목표**: 급여 자동 계산이 동작하도록 핵심 비즈니스 로직을 이식한다.
| # | 작업 | 참조 (MNG) | 대상 파일 (API) |
|---|------|-----------|----------------|
| 1-1 | `calculateAmounts()` 메서드 구현 | `PayrollService:529-590` | `Services/PayrollService.php` |
| 1-2 | `calculateIncomeTax()` 소득세 계산 | `PayrollService:592-670` | `Services/PayrollService.php` |
| 1-3 | 4대보험 개별 계산 메서드 | `PayrollService:672-720` | `Services/PayrollService.php` |
| 1-4 | `applyDeductionOverrides()` 공제 수동 수정 | `PayrollService:722-760` | `Services/PayrollService.php` |
| 1-5 | `resolveFamilyCount()` 가족수 산출 | `PayrollService:762-800` | `Services/PayrollService.php` |
| 1-6 | `IncomeTaxBracket` 모델 생성 | `Models/HR/IncomeTaxBracket.php` | `Models/Tenants/IncomeTaxBracket.php` |
| 1-7 | `income_tax_brackets` 마이그레이션 실행 확인 | 이미 존재 확인 필요 | `database/migrations/` |
| 1-8 | `store()`/`update()` 에서 자동 계산 적용 | `PayrollService:150-250` | `Services/PayrollService.php` |
**계산 흐름**:
```
입력: base_salary, overtime_pay, bonus, allowances, deductions
├─ Step 1: 총 지급액 = base_salary + overtime_pay + bonus + Σ(allowances)
├─ Step 2: 과세표준 = 총 지급액 - bonus (비과세)
├─ Step 3: 4대보험 = 과세표준 × 요율 (PayrollSetting 참조)
│ ├─ 건강보험 = 과세표준 × 3.545%
│ ├─ 장기요양 = 건강보험 × 0.9082%
│ ├─ 국민연금 = clamp(min, max, 과세표준) × 4.5%
│ └─ 고용보험 = 과세표준 × 0.9%
├─ Step 4: 근로소득세 = 간이세액표 조회 (가족수 반영)
│ ├─ < 770천원: 0원
│ ├─ 770~10,000천원: DB 간이세액표
│ └─ > 10,000천원: 소득세법 시행령 별표2 공식
├─ Step 5: 지방소득세 = 근로소득세 × 10%
├─ Step 6: 총 공제액 = 4대보험 + 세금 + Σ(deductions)
└─ Step 7: 실수령액 = 총 지급액 - 총 공제액
※ 모든 금액: 10원 단위 절삭 (floor)
```
---
### Phase 2: 상태 관리 + 일괄 처리
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 2-1 | `unconfirm()` 확정 취소 | `PayrollService:340-360` | confirmed → draft |
| 2-2 | `unpay()` 지급 취소 | `PayrollService:380-400` | paid → draft (슈퍼관리자) |
| 2-3 | `bulkGenerate()` 재직사원 일괄 생성 | `PayrollService:442-521` | Employee 연봉 기반 |
| 2-4 | `copyFromPreviousMonth()` 전월 복사 | `PayrollService:402-440` | soft-delete 처리 포함 |
| 2-5 | Payroll 모델에 상태 헬퍼 메서드 추가 | `Models/HR/Payroll.php` | `isEditable()`, `isConfirmable()` 등 |
**일괄 생성 로직**:
```
bulkGenerate(year, month)
├─ 1. PayrollSetting 조회
├─ 2. 활성 재직사원 전체 조회
├─ 3. 각 사원별:
│ ├─ 이미 존재 → skip
│ ├─ soft-deleted 존재 → forceDelete 후 재생성
│ ├─ 기본급 = 연봉 / 12
│ ├─ calculateAmounts() 호출
│ └─ Payroll 생성 (status: draft)
└─ 4. 결과: {created: N, skipped: M}
```
---
### Phase 3: 문서 생성 + 내보내기
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 3-1 | `sendPayslip()` 급여명세서 PDF + 이메일 | `PayrollService:820-920` | DomPDF + Pretendard |
| 3-2 | `generateJournalEntry()` 전표 자동 생성 | `PayrollController:900-1088` | 분개 구조 동일 |
| 3-3 | `export()` 엑셀 내보내기 | `PayrollService:100-140` | 동적 열 포함 |
| 3-4 | 급여명세서 Blade 뷰 생성 | `emails/payslip.blade.php` | PDF 폰트 정책 준수 |
| 3-5 | PayslipMail Mailable 생성 | `Mail/PayslipMail.php` | |
---
## 4. API 엔드포인트 설계
### 4.1 추가 엔드포인트
기존 라우트(`routes/api/v1/finance.php`)에 추가할 엔드포인트:
| Method | URI | 설명 | Phase |
|--------|-----|------|:-----:|
| POST | `/v1/payrolls/{id}/unconfirm` | 확정 취소 | 2 |
| POST | `/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) | 2 |
| POST | `/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 | 2 |
| POST | `/v1/payrolls/copy-from-previous` | 전월 복사 | 2 |
| POST | `/v1/payrolls/{id}/send-payslip` | 급여명세서 이메일 발송 | 3 |
| POST | `/v1/payrolls/generate-journal-entry` | 전표 자동 생성 | 3 |
| GET | `/v1/payrolls/export` | 엑셀 내보내기 | 3 |
### 4.2 기존 엔드포인트 수정
| URI | 변경 내용 | Phase |
|-----|----------|:-----:|
| `POST /v1/payrolls` | `calculateAmounts()` 자동 적용 | 1 |
| `PUT /v1/payrolls/{id}` | 공제 오버라이드 지원 | 1 |
| `POST /v1/payrolls/calculate` | 소득세 포함 전체 계산으로 개선 | 1 |
### 4.3 요청/응답 예시
**급여 등록 요청** (`POST /v1/payrolls`):
```json
{
"user_id": 15,
"pay_year": 2026,
"pay_month": 3,
"base_salary": 3500000,
"overtime_pay": 500000,
"bonus": 200000,
"allowances": [
{"name": "교통비", "amount": 100000}
],
"deductions": [
{"name": "대출상환", "amount": 300000}
],
"deduction_overrides": {
"pension": 180000,
"health_insurance": null
}
}
```
**자동 계산 응답** (`POST /v1/payrolls/calculate`):
```json
{
"success": true,
"data": {
"gross_salary": 4300000,
"taxable_base": 4100000,
"pension": 184500,
"health_insurance": 145345,
"long_term_care": 13200,
"employment_insurance": 36900,
"income_tax": 78340,
"resident_tax": 7830,
"total_deductions": 766115,
"net_salary": 3533885,
"family_count": 2
}
}
```
---
## 5. 데이터베이스
### 5.1 기존 테이블 (변경 불필요)
- `payrolls` — 이미 모든 필드 존재 (options JSON 컬럼 포함)
- `payroll_settings` — 설정 테이블 완비
### 5.2 확인 필요
| 테이블 | 상태 | 조치 |
|--------|------|------|
| `income_tax_brackets` | 마이그레이션 존재 확인 필요 | 없으면 생성 + 2024 간이세액표 시딩 |
| `payrolls.long_term_care` | 2026-02-27 추가 완료 | - |
| `payrolls.options` | 2026-03-10 추가 완료 | - |
### 5.3 간이세액표 시딩
`income_tax_brackets` 테이블에 2024년 국세청 간이세액표 데이터가 필요하다.
- 770천원 ~ 10,000천원 구간
- 가족수 1~11명별 세액
- MNG에 이미 시더 존재 → API로 이관
---
## 6. 추가 생성 파일
### 6.1 Phase 1
| 파일 | 설명 |
|------|------|
| `app/Models/Tenants/IncomeTaxBracket.php` | 간이세액표 모델 |
| `app/Http/Requests/V1/Payroll/BulkGenerateRequest.php` | 일괄 생성 요청 |
| `app/Http/Requests/V1/Payroll/CopyFromPreviousRequest.php` | 전월 복사 요청 |
### 6.2 Phase 3
| 파일 | 설명 |
|------|------|
| `app/Mail/PayslipMail.php` | 급여명세서 Mailable |
| `resources/views/emails/payslip.blade.php` | 급여명세서 PDF 뷰 |
| `resources/views/emails/payslip-notification.blade.php` | 이메일 본문 |
| `app/Exports/PayrollExport.php` | 엑셀 내보내기 |
---
## 7. 주의사항
### 7.1 필수 준수
- ✅ 마이그레이션은 API 프로젝트에서만 생성 (CLAUDE.md 규칙)
- ✅ PDF 생성 시 Pretendard 폰트 + `ensureKoreanFont()` 적용 (폰트 정책)
- ✅ 모든 응답 메시지는 i18n 키 사용 (`__('message.xxx')`)
-`ApiResponse::handle()` 패턴 사용
- ✅ FormRequest로 입력 검증
### 7.2 MNG 코드 이식 시 변환 규칙
| MNG 패턴 | API 패턴 |
|----------|---------|
| `auth()->id()` | `$this->apiUserId()` |
| `session('tenant_id')` | `$this->tenantId()` |
| 직접 JSON 응답 | `ApiResponse::success()` / `ApiResponse::handle()` |
| 하드코딩 한글 메시지 | `__('message.payroll.xxx')` |
| HTMX 부분 렌더링 | JSON 응답 전용 |
| `Payroll::query()` | `Payroll::query()->forTenant($this->tenantId())` |
### 7.3 Salary 모델과의 관계
- `Payroll` = 상세 급여 관리 (세금/보험 자동 계산, MNG 연동)
- `Salary` = React용 간소화 급여 현황 (별도 유지)
- 두 모델은 독립적으로 운영하며, 추후 통합 여부 검토
---
## 8. 작업 순서 (권장)
```
Phase 1 (핵심 계산) ─────────────────────────────────────
1-6. IncomeTaxBracket 모델 생성
1-7. 간이세액표 마이그레이션/시딩 확인
1-1. calculateAmounts() 구현
1-2. calculateIncomeTax() 구현
1-3. 4대보험 계산 메서드 구현
1-4. applyDeductionOverrides() 구현
1-5. resolveFamilyCount() 구현
1-8. store()/update()에 자동 계산 적용
─── 테스트: 급여 등록 → 자동 계산 검증 ───
Phase 2 (상태 + 일괄) ──────────────────────────────────
2-5. Payroll 모델 상태 헬퍼 추가
2-1. unconfirm() 구현 + 라우트
2-2. unpay() 구현 + 라우트
2-3. bulkGenerate() 구현 + 라우트
2-4. copyFromPreviousMonth() 구현 + 라우트
─── 테스트: 상태 전이, 일괄 생성 검증 ───
Phase 3 (문서 + 내보내기) ──────────────────────────────
3-4. 급여명세서 Blade 뷰 생성
3-1. sendPayslip() PDF + 이메일 구현
3-2. generateJournalEntry() 전표 생성 구현
3-3. export() 엑셀 내보내기 구현
─── 테스트: PDF 생성, 이메일 발송, 전표 검증 ───
```
---
## 관련 문서
- [급여관리 기능 문서](../features/finance/payroll.md) — MNG 급여관리 상세
- [API 개발 규칙](../dev/standards/api-rules.md) — Service-First, FormRequest 패턴
- [DB 스키마 — 인사](../system/database/hr.md) — payrolls 테이블 구조
- [PDF 폰트 정책](../dev/standards/pdf-font-policy.md) — DomPDF 한글 폰트
- [options 컬럼 정책](../dev/standards/options-column-policy.md) — JSON 확장 필드
---
**최종 업데이트**: 2026-03-11
# 급여관리 API 구현 계획
> **작성일**: 2026-03-11
> **상태**: 계획 수립
> **참조**: MNG 급여관리 시스템 (`mng/app/Services/HR/PayrollService.php`)
---
## 1. 개요
### 1.1 목적
MNG에서 운영 중인 급여관리 시스템의 핵심 비즈니스 로직을 API 서버에 구현한다. React 프론트엔드에서 급여 관리 기능을 사용할 수 있도록 완전한 REST API를 제공한다.
### 1.2 배경
- MNG 급여관리: 완성도 100% (CRUD, 자동계산, 일괄생성, PDF 명세서, 전표변환)
- API 급여관리: 완성도 ~50% (기본 CRUD만 구현, 핵심 계산 로직 누락)
- React에서 급여관리 화면을 구현하려면 API에 동일한 비즈니스 로직이 필요하다
### 1.3 원칙
- MNG의 검증된 로직을 API 컨벤션에 맞게 이식한다
- API 프로젝트의 Service-First 아키텍처, i18n, FormRequest 패턴을 준수한다
- 기존 `payrolls` 테이블 스키마를 그대로 사용한다 (추가 마이그레이션 최소화)
---
## 2. 현황 분석 (GAP)
### 2.1 기능 비교
| 기능 | MNG | API | GAP |
|------|:---:|:---:|-----|
| 급여 CRUD | ✅ | ✅ | - |
| 급여 설정 CRUD | ✅ | ✅ | - |
| 목록 조회 (필터/페이지네이션) | ✅ | ✅ | - |
| 월별 통계 | ✅ | ✅ | - |
| 확정 (`confirm`) | ✅ | ✅ | - |
| 지급 처리 (`pay`) | ✅ | ✅ | - |
| 일괄 확정 (`bulkConfirm`) | ✅ | ✅ | - |
| **소득세 자동 계산** | ✅ | ❌ | 간이세액표 기반 계산 로직 전체 누락 |
| **4대보험 자동 계산** | ✅ | ⚠️ | 설정값만 존재, `calculateAmounts()` 미구현 |
| **공제 오버라이드** | ✅ | ❌ | 수동 공제 수정 후 재계산 미지원 |
| **확정 취소 (`unconfirm`)** | ✅ | ❌ | 상태 복구 불가 |
| **지급 취소 (`unpay`)** | ✅ | ❌ | 슈퍼관리자 기능 누락 |
| **일괄 생성 (`bulkGenerate`)** | ✅ | ❌ | 재직사원 기반 신규 생성 미구현 |
| **전월 복사 (`copyFromPrevious`)** | ✅ | ❌ | 이전 월 데이터 복사 미구현 |
| **급여명세서 PDF 생성** | ✅ | ❌ | 데이터 조회만 가능, PDF 미생성 |
| **급여명세서 이메일 발송** | ✅ | ❌ | 이메일 발송 미구현 |
| **전표 자동 생성** | ✅ | ❌ | `generateJournalEntry()` 미구현 |
| **엑셀 내보내기** | ✅ | ❌ | export 미구현 |
| **공제대상가족수 자동 산출** | ✅ | ❌ | 피부양자 기반 가족수 미산출 |
### 2.2 API 기존 코드 현황
| 파일 | 상태 | 비고 |
|------|------|------|
| `Controllers/Api/V1/PayrollController.php` | 기본 CRUD 구현 | 누락 엔드포인트 추가 필요 |
| `Services/PayrollService.php` | 기본 CRUD + 제한적 계산 | 핵심 로직 이식 필요 |
| `Models/Tenants/Payroll.php` | 모델 정의 완료 | 상태 헬퍼 메서드 보강 필요 |
| `Models/Tenants/PayrollSetting.php` | 설정 모델 완료 | - |
| `Requests/V1/Payroll/` | FormRequest 5개 존재 | 추가 Request 필요 |
| `routes/api/v1/finance.php` | 기본 라우트 정의 | 누락 엔드포인트 추가 |
---
## 3. 구현 범위
### Phase 1: 핵심 계산 엔진 (필수)
> **목표**: 급여 자동 계산이 동작하도록 핵심 비즈니스 로직을 이식한다.
| # | 작업 | 참조 (MNG) | 대상 파일 (API) |
|---|------|-----------|----------------|
| 1-1 | `calculateAmounts()` 메서드 구현 | `PayrollService:529-590` | `Services/PayrollService.php` |
| 1-2 | `calculateIncomeTax()` 소득세 계산 | `PayrollService:592-670` | `Services/PayrollService.php` |
| 1-3 | 4대보험 개별 계산 메서드 | `PayrollService:672-720` | `Services/PayrollService.php` |
| 1-4 | `applyDeductionOverrides()` 공제 수동 수정 | `PayrollService:722-760` | `Services/PayrollService.php` |
| 1-5 | `resolveFamilyCount()` 가족수 산출 | `PayrollService:762-800` | `Services/PayrollService.php` |
| 1-6 | `IncomeTaxBracket` 모델 생성 | `Models/HR/IncomeTaxBracket.php` | `Models/Tenants/IncomeTaxBracket.php` |
| 1-7 | `income_tax_brackets` 마이그레이션 실행 확인 | 이미 존재 확인 필요 | `database/migrations/` |
| 1-8 | `store()`/`update()` 에서 자동 계산 적용 | `PayrollService:150-250` | `Services/PayrollService.php` |
**계산 흐름**:
```
입력: base_salary, overtime_pay, bonus, allowances, deductions
├─ Step 1: 총 지급액 = base_salary + overtime_pay + bonus + Σ(allowances)
├─ Step 2: 과세표준 = 총 지급액 - bonus (비과세)
├─ Step 3: 4대보험 = 과세표준 × 요율 (PayrollSetting 참조)
│ ├─ 건강보험 = 과세표준 × 3.545%
│ ├─ 장기요양 = 건강보험 × 0.9082%
│ ├─ 국민연금 = clamp(min, max, 과세표준) × 4.5%
│ └─ 고용보험 = 과세표준 × 0.9%
├─ Step 4: 근로소득세 = 간이세액표 조회 (가족수 반영)
│ ├─ < 770천원: 0원
│ ├─ 770~10,000천원: DB 간이세액표
│ └─ > 10,000천원: 소득세법 시행령 별표2 공식
├─ Step 5: 지방소득세 = 근로소득세 × 10%
├─ Step 6: 총 공제액 = 4대보험 + 세금 + Σ(deductions)
└─ Step 7: 실수령액 = 총 지급액 - 총 공제액
※ 모든 금액: 10원 단위 절삭 (floor)
```
---
### Phase 2: 상태 관리 + 일괄 처리
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 2-1 | `unconfirm()` 확정 취소 | `PayrollService:340-360` | confirmed → draft |
| 2-2 | `unpay()` 지급 취소 | `PayrollService:380-400` | paid → draft (슈퍼관리자) |
| 2-3 | `bulkGenerate()` 재직사원 일괄 생성 | `PayrollService:442-521` | Employee 연봉 기반 |
| 2-4 | `copyFromPreviousMonth()` 전월 복사 | `PayrollService:402-440` | soft-delete 처리 포함 |
| 2-5 | Payroll 모델에 상태 헬퍼 메서드 추가 | `Models/HR/Payroll.php` | `isEditable()`, `isConfirmable()` 등 |
**일괄 생성 로직**:
```
bulkGenerate(year, month)
├─ 1. PayrollSetting 조회
├─ 2. 활성 재직사원 전체 조회
├─ 3. 각 사원별:
│ ├─ 이미 존재 → skip
│ ├─ soft-deleted 존재 → forceDelete 후 재생성
│ ├─ 기본급 = 연봉 / 12
│ ├─ calculateAmounts() 호출
│ └─ Payroll 생성 (status: draft)
└─ 4. 결과: {created: N, skipped: M}
```
---
### Phase 3: 문서 생성 + 내보내기
| # | 작업 | 참조 (MNG) | 비고 |
|---|------|-----------|------|
| 3-1 | `sendPayslip()` 급여명세서 PDF + 이메일 | `PayrollService:820-920` | DomPDF + Pretendard |
| 3-2 | `generateJournalEntry()` 전표 자동 생성 | `PayrollController:900-1088` | 분개 구조 동일 |
| 3-3 | `export()` 엑셀 내보내기 | `PayrollService:100-140` | 동적 열 포함 |
| 3-4 | 급여명세서 Blade 뷰 생성 | `emails/payslip.blade.php` | PDF 폰트 정책 준수 |
| 3-5 | PayslipMail Mailable 생성 | `Mail/PayslipMail.php` | |
---
## 4. API 엔드포인트 설계
### 4.1 추가 엔드포인트
기존 라우트(`routes/api/v1/finance.php`)에 추가할 엔드포인트:
| Method | URI | 설명 | Phase |
|--------|-----|------|:-----:|
| POST | `/v1/payrolls/{id}/unconfirm` | 확정 취소 | 2 |
| POST | `/v1/payrolls/{id}/unpay` | 지급 취소 (슈퍼관리자) | 2 |
| POST | `/v1/payrolls/bulk-generate` | 재직사원 일괄 생성 | 2 |
| POST | `/v1/payrolls/copy-from-previous` | 전월 복사 | 2 |
| POST | `/v1/payrolls/{id}/send-payslip` | 급여명세서 이메일 발송 | 3 |
| POST | `/v1/payrolls/generate-journal-entry` | 전표 자동 생성 | 3 |
| GET | `/v1/payrolls/export` | 엑셀 내보내기 | 3 |
### 4.2 기존 엔드포인트 수정
| URI | 변경 내용 | Phase |
|-----|----------|:-----:|
| `POST /v1/payrolls` | `calculateAmounts()` 자동 적용 | 1 |
| `PUT /v1/payrolls/{id}` | 공제 오버라이드 지원 | 1 |
| `POST /v1/payrolls/calculate` | 소득세 포함 전체 계산으로 개선 | 1 |
### 4.3 요청/응답 예시
**급여 등록 요청** (`POST /v1/payrolls`):
```json
{
"user_id": 15,
"pay_year": 2026,
"pay_month": 3,
"base_salary": 3500000,
"overtime_pay": 500000,
"bonus": 200000,
"allowances": [
{"name": "교통비", "amount": 100000}
],
"deductions": [
{"name": "대출상환", "amount": 300000}
],
"deduction_overrides": {
"pension": 180000,
"health_insurance": null
}
}
```
**자동 계산 응답** (`POST /v1/payrolls/calculate`):
```json
{
"success": true,
"data": {
"gross_salary": 4300000,
"taxable_base": 4100000,
"pension": 184500,
"health_insurance": 145345,
"long_term_care": 13200,
"employment_insurance": 36900,
"income_tax": 78340,
"resident_tax": 7830,
"total_deductions": 766115,
"net_salary": 3533885,
"family_count": 2
}
}
```
---
## 5. 데이터베이스
### 5.1 기존 테이블 (변경 불필요)
- `payrolls` — 이미 모든 필드 존재 (options JSON 컬럼 포함)
- `payroll_settings` — 설정 테이블 완비
### 5.2 확인 필요
| 테이블 | 상태 | 조치 |
|--------|------|------|
| `income_tax_brackets` | 마이그레이션 존재 확인 필요 | 없으면 생성 + 2024 간이세액표 시딩 |
| `payrolls.long_term_care` | 2026-02-27 추가 완료 | - |
| `payrolls.options` | 2026-03-10 추가 완료 | - |
### 5.3 간이세액표 시딩
`income_tax_brackets` 테이블에 2024년 국세청 간이세액표 데이터가 필요하다.
- 770천원 ~ 10,000천원 구간
- 가족수 1~11명별 세액
- MNG에 이미 시더 존재 → API로 이관
---
## 6. 추가 생성 파일
### 6.1 Phase 1
| 파일 | 설명 |
|------|------|
| `app/Models/Tenants/IncomeTaxBracket.php` | 간이세액표 모델 |
| `app/Http/Requests/V1/Payroll/BulkGenerateRequest.php` | 일괄 생성 요청 |
| `app/Http/Requests/V1/Payroll/CopyFromPreviousRequest.php` | 전월 복사 요청 |
### 6.2 Phase 3
| 파일 | 설명 |
|------|------|
| `app/Mail/PayslipMail.php` | 급여명세서 Mailable |
| `resources/views/emails/payslip.blade.php` | 급여명세서 PDF 뷰 |
| `resources/views/emails/payslip-notification.blade.php` | 이메일 본문 |
| `app/Exports/PayrollExport.php` | 엑셀 내보내기 |
---
## 7. 주의사항
### 7.1 필수 준수
- ✅ 마이그레이션은 API 프로젝트에서만 생성 (CLAUDE.md 규칙)
- ✅ PDF 생성 시 Pretendard 폰트 + `ensureKoreanFont()` 적용 (폰트 정책)
- ✅ 모든 응답 메시지는 i18n 키 사용 (`__('message.xxx')`)
-`ApiResponse::handle()` 패턴 사용
- ✅ FormRequest로 입력 검증
### 7.2 MNG 코드 이식 시 변환 규칙
| MNG 패턴 | API 패턴 |
|----------|---------|
| `auth()->id()` | `$this->apiUserId()` |
| `session('tenant_id')` | `$this->tenantId()` |
| 직접 JSON 응답 | `ApiResponse::success()` / `ApiResponse::handle()` |
| 하드코딩 한글 메시지 | `__('message.payroll.xxx')` |
| HTMX 부분 렌더링 | JSON 응답 전용 |
| `Payroll::query()` | `Payroll::query()->forTenant($this->tenantId())` |
### 7.3 Salary 모델과의 관계
- `Payroll` = 상세 급여 관리 (세금/보험 자동 계산, MNG 연동)
- `Salary` = React용 간소화 급여 현황 (별도 유지)
- 두 모델은 독립적으로 운영하며, 추후 통합 여부 검토
---
## 8. 작업 순서 (권장)
```
Phase 1 (핵심 계산) ─────────────────────────────────────
1-6. IncomeTaxBracket 모델 생성
1-7. 간이세액표 마이그레이션/시딩 확인
1-1. calculateAmounts() 구현
1-2. calculateIncomeTax() 구현
1-3. 4대보험 계산 메서드 구현
1-4. applyDeductionOverrides() 구현
1-5. resolveFamilyCount() 구현
1-8. store()/update()에 자동 계산 적용
─── 테스트: 급여 등록 → 자동 계산 검증 ───
Phase 2 (상태 + 일괄) ──────────────────────────────────
2-5. Payroll 모델 상태 헬퍼 추가
2-1. unconfirm() 구현 + 라우트
2-2. unpay() 구현 + 라우트
2-3. bulkGenerate() 구현 + 라우트
2-4. copyFromPreviousMonth() 구현 + 라우트
─── 테스트: 상태 전이, 일괄 생성 검증 ───
Phase 3 (문서 + 내보내기) ──────────────────────────────
3-4. 급여명세서 Blade 뷰 생성
3-1. sendPayslip() PDF + 이메일 구현
3-2. generateJournalEntry() 전표 생성 구현
3-3. export() 엑셀 내보내기 구현
─── 테스트: PDF 생성, 이메일 발송, 전표 검증 ───
```
---
## 관련 문서
- [급여관리 기능 문서](../features/finance/payroll.md) — MNG 급여관리 상세
- [API 개발 규칙](../dev/standards/api-rules.md) — Service-First, FormRequest 패턴
- [DB 스키마 — 인사](../system/database/hr.md) — payrolls 테이블 구조
- [PDF 폰트 정책](../dev/standards/pdf-font-policy.md) — DomPDF 한글 폰트
- [options 컬럼 정책](../dev/standards/options-column-policy.md) — JSON 확장 필드
---
**최종 업데이트**: 2026-03-11

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,289 +1,289 @@
# SAM API 구조 분석 및 개선 로드맵
> **작성일**: 2026-03-14
> **상태**: 분석 완료, 개선 진행중
> **작성자**: R&D 개발실장
> **대상**: `/home/aweso/sam/api` (Laravel 12 REST API)
---
## 1. 개요
### 1.1 목적
SAM API의 현재 구조를 정량적으로 분석하고, 기술 부채를 식별하여 우선순위별 개선 로드맵을 수립한다. R&D 개발실장이 백엔드 전반을 파악하고 체계적으로 품질을 끌어올리기 위한 기준 문서이다.
### 1.2 분석 기준일
- 코드베이스: 2026-03-14 기준 develop 브랜치
- Claude Code v2.1.75 (Opus 4.6)로 자동 분석
---
## 2. 프로젝트 규모
### 2.1 전체 수치
| 항목 | 수량 | 비고 |
|------|------|------|
| **엔드포인트** | ~1,400개 | 19개 도메인 라우트 파일 |
| **컨트롤러** | 152개 | V1 144개 + Admin 1개 + Equipment 7개 |
| **서비스** | 202개 | 22개 도메인 폴더 + 루트 98개 |
| **모델** | 261개 | 32개 도메인 폴더 |
| **FormRequest** | 305개 | 42개 도메인 폴더 |
| **마이그레이션** | 545개 | 메인 523 + 통계 22 |
| **미들웨어** | 10개 | |
| **아티산 커맨드** | 39개 | |
| **옵저버** | 15개 | 감사/이벤트 처리 |
| **Swagger 문서** | 110개 | OpenAPI 스펙 |
| **테스트** | 14개 | Feature 11 + Unit 3 |
| **PHP 코드** | 179,010줄 | |
### 2.2 도메인별 엔드포인트 분포
| 도메인 | 엔드포인트 | 비중 | 핵심 기능 |
|--------|:---------:|:----:|----------|
| Finance | 271 | 19% | 카드, 계좌, 입출금, 급여, 세금계산서, 바로빌 |
| HR | 153 | 11% | 부서, 직원, 근태, 휴가, 전자결재, 시공관리 |
| Sales | 135 | 10% | 거래처, 견적, 입찰, 수주, 단가, 데모테넌트 |
| Common | 134 | 10% | 메뉴, 권한, 카테고리, 대시보드, 알림 |
| Inventory | 85 | 6% | 품목, BOM, 자재, 입고, 배차 |
| Boards | 84 | 6% | 게시판, 품목기준관리(ItemMaster) |
| Production | 73 | 5% | 공정, 작업지시, 작업실적, 검사 |
| Design | 61 | 4% | 모델, 버전, BOM 템플릿, 계산 엔진 |
| Quality | 36 | 3% | 품질관리, 검사, 인증 |
| Users | 34 | 2% | 프로필, 역할, 테넌트 전환 |
| Admin | 29 | 2% | 테넌트, 메뉴, FCM, API Key |
| Equipment | 27 | 2% | 설비, 정비 |
| Tenants | 23 | 2% | 테넌트 설정, 구독 |
| Files | 21 | 2% | 업로드, 저장 |
| Documents | 18 | 1% | 템플릿, 생성 |
| ESign | 18 | 1% | 전자서명 워크플로우 |
| Auth | 9 | 1% | 로그인, 토큰 |
| Stats | 9 | 1% | 통계/리포트 |
| Audit | 7 | 0.5% | 감사 로그 |
---
## 3. 아키텍처 분석
### 3.1 3대 설계 원칙
```
┌─────────────────────────────────────────────────────┐
│ 1. Service-First 패턴 │
│ Controller (라우팅만) → Service (로직) → Model │
│ Controller에 비즈니스 로직 작성 금지 │
├─────────────────────────────────────────────────────┤
│ 2. Multi-Tenant (tenant_id 기반) │
│ BelongsToTenant 트레이트 → 자동 WHERE 필터링 │
│ 1명 사용자가 여러 테넌트 접근 가능 │
├─────────────────────────────────────────────────────┤
│ 3. 5계층 보안 │
│ Nginx → Rate Limit → API Key → Sanctum → 권한 │
│ 메뉴 기반 RBAC (7가지 권한 타입) │
└─────────────────────────────────────────────────────┘
```
### 3.2 요청 처리 흐름
```
HTTP Request
Nginx (악성 경로 차단, Rate Limit)
Middleware Stack
├─ ApiKeyMiddleware (X-API-KEY 검증)
├─ ApiVersionMiddleware (v1/v2 폴백)
├─ auth:sanctum (Bearer 토큰)
├─ PermMapper (메뉴→권한 매핑)
├─ CheckPermission (RBAC 체크)
└─ SetAuditSessionVariables (감사 추적)
Controller (FormRequest 타입힌팅 → 자동 검증)
Service (비즈니스 로직)
├─ tenantId() / apiUserId() 자동 제공
└─ Model (BelongsToTenant 자동 필터링)
ApiResponse::handle() → JSON { success, message, data }
```
### 3.3 표준 테이블 구조
```sql
id BIGINT PK
tenant_id BIGINT FK -- 멀티테넌트 격리
-- 비즈니스 컬럼 (FK, WHERE/ORDER BY 대상만)
options JSON NULL -- 유연한 확장 속성
created_by BIGINT FK -- 감사 추적
updated_by BIGINT FK
deleted_by BIGINT FK
created_at TIMESTAMP
updated_at TIMESTAMP
deleted_at TIMESTAMP -- SoftDeletes
```
### 3.4 핵심 트레이트
| 트레이트 | 역할 | 적용 범위 |
|---------|------|----------|
| `BelongsToTenant` | tenant_id 자동 필터링 (Global Scope) | 모든 테넌트 모델 |
| `Auditable` | created_by, updated_by, deleted_by 자동 기록 | 모든 비즈니스 모델 |
| `ModelTrait` | is_active 스코프, 날짜 처리 | 모든 모델 |
| `SoftDeletes` | 논리 삭제 (물리 삭제 금지) | 모든 모델 |
### 3.5 주요 파일 위치
```
/home/aweso/sam/api/
├── app/
│ ├── Http/Controllers/Api/V1/ ← 컨트롤러 (144개)
│ ├── Http/Middleware/ ← 미들웨어 (10개)
│ ├── Http/Requests/ ← FormRequest (305개)
│ ├── Models/ ← 모델 (261개, 32개 폴더)
│ ├── Services/ ← 서비스 (202개, 22개 폴더)
│ ├── Traits/ ← 공용 트레이트 (6개)
│ ├── Observers/ ← 옵저버 (15개)
│ ├── Console/Commands/ ← 아티산 커맨드 (39개)
│ └── Swagger/v1/ ← API 문서 (110개)
├── routes/api/v1/ ← 도메인별 라우트 (19개)
├── database/migrations/ ← 마이그레이션 (545개)
├── config/ ← 설정 (24개)
└── tests/ ← 테스트 (14개)
```
---
## 4. 이관 현황
### 4.1 전체 현황
| 상태 | 수량 | 설명 |
|------|:----:|------|
| 이관 완료 | 24개 | API + React 모두 구현됨 |
| 이관 대상 (P1) | 15개 | 자금일정, 정산, VAT, 채번, 전자서명 등 |
| React UI만 필요 | 8개 | API 완료, UI 미구현 (QMS, 설비 등) |
| MNG 유지 | 11개 | PMIS, 신용평가, Dev Tools 등 |
### 4.2 이관 대상 우선순위
| Phase | 대상 | 우선순위 |
|-------|------|:-------:|
| Phase 1 | 자금일정, 일일자금, 정산, VAT, 채번, 전자서명 | 🔴 P1 |
| Phase 2 | 견적수식, 판매수수료, 전자서명 고도화 | 🟡 P2 |
| Phase 3 | QMS UI, 설비 UI, 요금제 UI | 🟡 P2 |
| Phase 4 | 휴가정책 자동부여, 통합근태 | 🟢 P3 |
---
## 5. 기술 부채 및 개선 로드맵
### 5.1 식별된 기술 부채
| ID | 영역 | 현황 | 영향도 | 우선순위 |
|:--:|------|------|:------:|:-------:|
| D1 | **테스트 부재** | 14개 (1,400 EP 대비 1%) | 🔴 높음 | P1 |
| D2 | **N+1 쿼리** | 일부 Service에서 반복 쿼리 | 🔴 높음 | P1 |
| D3 | **마이그레이션 정리** | 545개, 순서/네이밍 혼재 | 🟡 중간 | P2 |
| D4 | **i18n 미완** | 일부 에러에 직접 문자열 잔존 | 🟡 중간 | P2 |
| D5 | **FK 제약 과다** | Production 도메인 중심 | 🟡 중간 | P2 |
| D6 | **통계 DB 동기화** | sam_stat 수동/불완전 | 🟢 낮음 | P3 |
| D7 | **API 문서 최신화** | 110개 중 일부 outdated | 🟢 낮음 | P3 |
| D8 | **Rate Limiting** | 10회/분 고정, 도메인별 미세분화 | 🟢 낮음 | P3 |
### 5.2 개선 로드맵
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 P1 즉시 착수 (품질 기반 확보)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D1] 테스트 커버리지 확충
→ 핵심 도메인(Finance, Sales, HR) Feature 테스트 우선
→ 목표: 주요 CRUD + 비즈니스 로직 커버
[D2] N+1 쿼리 최적화
→ 대형 컨트롤러 5개 우선 점검
(ItemsBomController, WorkOrderController,
BarobillCardTransactionController,
ApprovalController, QuoteController)
→ Service에서 with() eager load 적용
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟡 P2 단기 개선 (안정성 강화)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D3] 마이그레이션 정리
→ squash 또는 네이밍 표준화
→ MNG 전용 마이그레이션 분리 확인
[D4] i18n 완성
→ 직접 문자열 → message key 전환
→ grep으로 잔존 문자열 스캔
[D5] FK 제약 최적화
→ Production 도메인 FK 관계 정리
→ 삭제 순서 복잡도 감소
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 P3 중장기 (확장성)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D6] 통계 DB 실시간화
→ Event Sourcing 또는 CDC 검토
[D7] Swagger 문서 최신화
→ 자동 생성 파이프라인 개선
[D8] Rate Limiting 동적화
→ 테넌트/도메인별 차등 적용
```
### 5.3 강점 (유지해야 할 것)
```
✅ Service-First 패턴 — 일관된 비즈니스 로직 분리
✅ 5계층 보안 — Nginx ~ Permission까지 다층 방어
✅ BelongsToTenant — 멀티테넌트 자동 격리, 데이터 누출 방지
✅ Auditable — 모든 변경사항 자동 추적
✅ options JSON — 스키마 변경 없이 유연한 확장
✅ FormRequest 분리 — 305개 검증 클래스, 입력 검증 표준화
✅ ApiResponse::handle() — 통일된 응답 포맷
```
---
## 6. 진행 추적
> 아래 체크리스트로 개선 작업 진행 상황을 추적한다.
### P1 (즉시)
- [ ] [D1] 테스트 커버리지 확충 — 핵심 도메인 Feature 테스트
- [ ] [D2] N+1 쿼리 최적화 — 대형 컨트롤러 5개 점검
### P2 (단기)
- [ ] [D3] 마이그레이션 정리
- [ ] [D4] i18n 직접 문자열 제거
- [ ] [D5] FK 제약 최적화
### P3 (중장기)
- [ ] [D6] 통계 DB 실시간화
- [ ] [D7] Swagger 문서 최신화
- [ ] [D8] Rate Limiting 동적화
---
## 관련 문서
- [시스템 아키텍처](overview.md)
- [API 서버 구조](api-structure.md)
- [API 개발 규칙](../dev/standards/api-rules.md)
- [이관 현황](migration-status.md)
- [DB 스키마](database/README.md)
- [보안 정책](security-policy.md)
- [품질 체크리스트](../dev/standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14
# SAM API 구조 분석 및 개선 로드맵
> **작성일**: 2026-03-14
> **상태**: 분석 완료, 개선 진행중
> **작성자**: R&D 개발실장
> **대상**: `/home/aweso/sam/api` (Laravel 12 REST API)
---
## 1. 개요
### 1.1 목적
SAM API의 현재 구조를 정량적으로 분석하고, 기술 부채를 식별하여 우선순위별 개선 로드맵을 수립한다. R&D 개발실장이 백엔드 전반을 파악하고 체계적으로 품질을 끌어올리기 위한 기준 문서이다.
### 1.2 분석 기준일
- 코드베이스: 2026-03-14 기준 develop 브랜치
- Claude Code v2.1.75 (Opus 4.6)로 자동 분석
---
## 2. 프로젝트 규모
### 2.1 전체 수치
| 항목 | 수량 | 비고 |
|------|------|------|
| **엔드포인트** | ~1,400개 | 19개 도메인 라우트 파일 |
| **컨트롤러** | 152개 | V1 144개 + Admin 1개 + Equipment 7개 |
| **서비스** | 202개 | 22개 도메인 폴더 + 루트 98개 |
| **모델** | 261개 | 32개 도메인 폴더 |
| **FormRequest** | 305개 | 42개 도메인 폴더 |
| **마이그레이션** | 545개 | 메인 523 + 통계 22 |
| **미들웨어** | 10개 | |
| **아티산 커맨드** | 39개 | |
| **옵저버** | 15개 | 감사/이벤트 처리 |
| **Swagger 문서** | 110개 | OpenAPI 스펙 |
| **테스트** | 14개 | Feature 11 + Unit 3 |
| **PHP 코드** | 179,010줄 | |
### 2.2 도메인별 엔드포인트 분포
| 도메인 | 엔드포인트 | 비중 | 핵심 기능 |
|--------|:---------:|:----:|----------|
| Finance | 271 | 19% | 카드, 계좌, 입출금, 급여, 세금계산서, 바로빌 |
| HR | 153 | 11% | 부서, 직원, 근태, 휴가, 전자결재, 시공관리 |
| Sales | 135 | 10% | 거래처, 견적, 입찰, 수주, 단가, 데모테넌트 |
| Common | 134 | 10% | 메뉴, 권한, 카테고리, 대시보드, 알림 |
| Inventory | 85 | 6% | 품목, BOM, 자재, 입고, 배차 |
| Boards | 84 | 6% | 게시판, 품목기준관리(ItemMaster) |
| Production | 73 | 5% | 공정, 작업지시, 작업실적, 검사 |
| Design | 61 | 4% | 모델, 버전, BOM 템플릿, 계산 엔진 |
| Quality | 36 | 3% | 품질관리, 검사, 인증 |
| Users | 34 | 2% | 프로필, 역할, 테넌트 전환 |
| Admin | 29 | 2% | 테넌트, 메뉴, FCM, API Key |
| Equipment | 27 | 2% | 설비, 정비 |
| Tenants | 23 | 2% | 테넌트 설정, 구독 |
| Files | 21 | 2% | 업로드, 저장 |
| Documents | 18 | 1% | 템플릿, 생성 |
| ESign | 18 | 1% | 전자서명 워크플로우 |
| Auth | 9 | 1% | 로그인, 토큰 |
| Stats | 9 | 1% | 통계/리포트 |
| Audit | 7 | 0.5% | 감사 로그 |
---
## 3. 아키텍처 분석
### 3.1 3대 설계 원칙
```
┌─────────────────────────────────────────────────────┐
│ 1. Service-First 패턴 │
│ Controller (라우팅만) → Service (로직) → Model │
│ Controller에 비즈니스 로직 작성 금지 │
├─────────────────────────────────────────────────────┤
│ 2. Multi-Tenant (tenant_id 기반) │
│ BelongsToTenant 트레이트 → 자동 WHERE 필터링 │
│ 1명 사용자가 여러 테넌트 접근 가능 │
├─────────────────────────────────────────────────────┤
│ 3. 5계층 보안 │
│ Nginx → Rate Limit → API Key → Sanctum → 권한 │
│ 메뉴 기반 RBAC (7가지 권한 타입) │
└─────────────────────────────────────────────────────┘
```
### 3.2 요청 처리 흐름
```
HTTP Request
Nginx (악성 경로 차단, Rate Limit)
Middleware Stack
├─ ApiKeyMiddleware (X-API-KEY 검증)
├─ ApiVersionMiddleware (v1/v2 폴백)
├─ auth:sanctum (Bearer 토큰)
├─ PermMapper (메뉴→권한 매핑)
├─ CheckPermission (RBAC 체크)
└─ SetAuditSessionVariables (감사 추적)
Controller (FormRequest 타입힌팅 → 자동 검증)
Service (비즈니스 로직)
├─ tenantId() / apiUserId() 자동 제공
└─ Model (BelongsToTenant 자동 필터링)
ApiResponse::handle() → JSON { success, message, data }
```
### 3.3 표준 테이블 구조
```sql
id BIGINT PK
tenant_id BIGINT FK -- 멀티테넌트 격리
-- 비즈니스 컬럼 (FK, WHERE/ORDER BY 대상만)
options JSON NULL -- 유연한 확장 속성
created_by BIGINT FK -- 감사 추적
updated_by BIGINT FK
deleted_by BIGINT FK
created_at TIMESTAMP
updated_at TIMESTAMP
deleted_at TIMESTAMP -- SoftDeletes
```
### 3.4 핵심 트레이트
| 트레이트 | 역할 | 적용 범위 |
|---------|------|----------|
| `BelongsToTenant` | tenant_id 자동 필터링 (Global Scope) | 모든 테넌트 모델 |
| `Auditable` | created_by, updated_by, deleted_by 자동 기록 | 모든 비즈니스 모델 |
| `ModelTrait` | is_active 스코프, 날짜 처리 | 모든 모델 |
| `SoftDeletes` | 논리 삭제 (물리 삭제 금지) | 모든 모델 |
### 3.5 주요 파일 위치
```
/home/aweso/sam/api/
├── app/
│ ├── Http/Controllers/Api/V1/ ← 컨트롤러 (144개)
│ ├── Http/Middleware/ ← 미들웨어 (10개)
│ ├── Http/Requests/ ← FormRequest (305개)
│ ├── Models/ ← 모델 (261개, 32개 폴더)
│ ├── Services/ ← 서비스 (202개, 22개 폴더)
│ ├── Traits/ ← 공용 트레이트 (6개)
│ ├── Observers/ ← 옵저버 (15개)
│ ├── Console/Commands/ ← 아티산 커맨드 (39개)
│ └── Swagger/v1/ ← API 문서 (110개)
├── routes/api/v1/ ← 도메인별 라우트 (19개)
├── database/migrations/ ← 마이그레이션 (545개)
├── config/ ← 설정 (24개)
└── tests/ ← 테스트 (14개)
```
---
## 4. 이관 현황
### 4.1 전체 현황
| 상태 | 수량 | 설명 |
|------|:----:|------|
| 이관 완료 | 24개 | API + React 모두 구현됨 |
| 이관 대상 (P1) | 15개 | 자금일정, 정산, VAT, 채번, 전자서명 등 |
| React UI만 필요 | 8개 | API 완료, UI 미구현 (QMS, 설비 등) |
| MNG 유지 | 11개 | PMIS, 신용평가, Dev Tools 등 |
### 4.2 이관 대상 우선순위
| Phase | 대상 | 우선순위 |
|-------|------|:-------:|
| Phase 1 | 자금일정, 일일자금, 정산, VAT, 채번, 전자서명 | 🔴 P1 |
| Phase 2 | 견적수식, 판매수수료, 전자서명 고도화 | 🟡 P2 |
| Phase 3 | QMS UI, 설비 UI, 요금제 UI | 🟡 P2 |
| Phase 4 | 휴가정책 자동부여, 통합근태 | 🟢 P3 |
---
## 5. 기술 부채 및 개선 로드맵
### 5.1 식별된 기술 부채
| ID | 영역 | 현황 | 영향도 | 우선순위 |
|:--:|------|------|:------:|:-------:|
| D1 | **테스트 부재** | 14개 (1,400 EP 대비 1%) | 🔴 높음 | P1 |
| D2 | **N+1 쿼리** | 일부 Service에서 반복 쿼리 | 🔴 높음 | P1 |
| D3 | **마이그레이션 정리** | 545개, 순서/네이밍 혼재 | 🟡 중간 | P2 |
| D4 | **i18n 미완** | 일부 에러에 직접 문자열 잔존 | 🟡 중간 | P2 |
| D5 | **FK 제약 과다** | Production 도메인 중심 | 🟡 중간 | P2 |
| D6 | **통계 DB 동기화** | sam_stat 수동/불완전 | 🟢 낮음 | P3 |
| D7 | **API 문서 최신화** | 110개 중 일부 outdated | 🟢 낮음 | P3 |
| D8 | **Rate Limiting** | 10회/분 고정, 도메인별 미세분화 | 🟢 낮음 | P3 |
### 5.2 개선 로드맵
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔴 P1 즉시 착수 (품질 기반 확보)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D1] 테스트 커버리지 확충
→ 핵심 도메인(Finance, Sales, HR) Feature 테스트 우선
→ 목표: 주요 CRUD + 비즈니스 로직 커버
[D2] N+1 쿼리 최적화
→ 대형 컨트롤러 5개 우선 점검
(ItemsBomController, WorkOrderController,
BarobillCardTransactionController,
ApprovalController, QuoteController)
→ Service에서 with() eager load 적용
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟡 P2 단기 개선 (안정성 강화)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D3] 마이그레이션 정리
→ squash 또는 네이밍 표준화
→ MNG 전용 마이그레이션 분리 확인
[D4] i18n 완성
→ 직접 문자열 → message key 전환
→ grep으로 잔존 문자열 스캔
[D5] FK 제약 최적화
→ Production 도메인 FK 관계 정리
→ 삭제 순서 복잡도 감소
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 P3 중장기 (확장성)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[D6] 통계 DB 실시간화
→ Event Sourcing 또는 CDC 검토
[D7] Swagger 문서 최신화
→ 자동 생성 파이프라인 개선
[D8] Rate Limiting 동적화
→ 테넌트/도메인별 차등 적용
```
### 5.3 강점 (유지해야 할 것)
```
✅ Service-First 패턴 — 일관된 비즈니스 로직 분리
✅ 5계층 보안 — Nginx ~ Permission까지 다층 방어
✅ BelongsToTenant — 멀티테넌트 자동 격리, 데이터 누출 방지
✅ Auditable — 모든 변경사항 자동 추적
✅ options JSON — 스키마 변경 없이 유연한 확장
✅ FormRequest 분리 — 305개 검증 클래스, 입력 검증 표준화
✅ ApiResponse::handle() — 통일된 응답 포맷
```
---
## 6. 진행 추적
> 아래 체크리스트로 개선 작업 진행 상황을 추적한다.
### P1 (즉시)
- [ ] [D1] 테스트 커버리지 확충 — 핵심 도메인 Feature 테스트
- [ ] [D2] N+1 쿼리 최적화 — 대형 컨트롤러 5개 점검
### P2 (단기)
- [ ] [D3] 마이그레이션 정리
- [ ] [D4] i18n 직접 문자열 제거
- [ ] [D5] FK 제약 최적화
### P3 (중장기)
- [ ] [D6] 통계 DB 실시간화
- [ ] [D7] Swagger 문서 최신화
- [ ] [D8] Rate Limiting 동적화
---
## 관련 문서
- [시스템 아키텍처](overview.md)
- [API 서버 구조](api-structure.md)
- [API 개발 규칙](../dev/standards/api-rules.md)
- [이관 현황](migration-status.md)
- [DB 스키마](database/README.md)
- [보안 정책](security-policy.md)
- [품질 체크리스트](../dev/standards/quality-checklist.md)
---
**최종 업데이트**: 2026-03-14

View File

@@ -1,226 +1,226 @@
# MNG → API+React 이관 현황 및 로드맵
> **작성일**: 2026-03-12
> **상태**: 분석 완료, 로드맵 수립
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
MNG(관리자 웹)에서 API+React로 이관할 남은 항목을 식별하고, 멀티테넌시 적합도를 기준으로 실전 서비스 개발 우선순위를 수립한다.
### 1.2 현재 규모
| 프로젝트 | 규모 |
|----------|------|
| **MNG** | 컨트롤러 212개, 모델 227개, 서비스 113개 |
| **API** | 컨트롤러 100+, 모델 120+, 마이그레이션 130+ |
| **React** | 페이지 253개, 컴포넌트 36개 도메인 |
### 1.3 멀티테넌시 판단 기준
| 기준 | 이관 대상 | 이관 불필요 |
|------|----------|-----------|
| 2개+ 테넌트가 사용하는가? | 재무, 영업, 인사, 견적 | PMIS(특정 테넌트 전용) |
| `tenant_id` 격리가 의미 있는가? | 자금일정, 채번규칙, 수수료 | 신용평가, 개발도구 |
| SaaS 고객에게 제공 가치가 있는가? | 전자서명, 정산, VAT | 로드맵, R&D, 스크럼 |
| 관리자만 사용하는가? | 사용자도 씀 → 이관 | Dev Tools, 감사 추적 |
---
## 2. 이관 완료 도메인
API + React 모두 구현 완료된 기능이다.
| 도메인 | 완성도 | 비고 |
|--------|:------:|------|
| 인증/계정 | 100% | 로그인, 토큰, 회원가입 |
| 사용자/테넌트/부서 | 100% | 조직 관리 전체 |
| 역할/권한 | 100% | 3단계 권한 체계 |
| 메뉴 관리 | 100% | 글로벌/테넌트 메뉴 |
| 품목 관리 (Item) | 95% | BOM, 동적 필드 |
| 카테고리/공통코드 | 100% | 트리 구조, 동기화 |
| 거래처 (Client) | 95% | 그룹, OCR |
| 견적/수주/매출 | 90% | 견적서 PDF, 발송 |
| 입찰 (Bidding) | 90% | 상태 관리 |
| 단가 관리 (Pricing) | 90% | 일괄 등록 |
| 카드/계좌/입출금 | 90% | 바로빌 부분 연동 |
| 급여 (Payroll) | 85% | 계산, 확정, 지급 |
| 채권/거래처원장 | 85% | 조회, 메모 |
| 세금계산서 | 85% | 발행, 홈택스 |
| 근태/휴가/직원 | 85% | 출퇴근, 연차 |
| 전자결재 | 80% | 양식, 결재선, 위임 |
| 게시판 | 80% | 커스텀 필드, 댓글 |
| 파일/문서 관리 | 80% | 업로드, 공유, 버전 |
| 생산 (공정/작업지시) | 80% | 작업일지, 자재 투입 |
| 재고 (구매/입고/출하) | 80% | 배차, Lot 추적 |
| 설계/BOM | 75% | 모델 버전, 계산 엔진 |
| 대시보드 | 90% | CEO 포함 5가지 타입 |
| 현장관리 (Site) | 75% | 현장설명회, 계약 |
| 바로빌 (기본) | 70% | 카드/계좌 연동 |
---
## 3. 미이관 항목 분석
### 3.1 멀티테넌트 이관 대상 (P1~P2)
실전 서비스에서 복수 테넌트가 사용할 핵심 기능이다.
| # | 기능 | MNG 컨트롤러 | 적합도 | 우선순위 |
|---|------|-------------|:------:|:--------:|
| 1 | 자금일정 (Fund Schedule) | `FundScheduleController` | 높음 | P1 |
| 2 | 일일자금 (Daily Fund) | `DailyFundController` | 높음 | P1 |
| 3 | 미지급금 (Payable) | `PayableController` | 높음 | P1 |
| 4 | 부가세 (VAT) | `VatRecordController` | 높음 | P1 |
| 5 | 정산 (Settlement) | `SettlementController` | 높음 | P1 |
| 6 | 환불 (Refund) | `RefundController` | 높음 | P1 |
| 7 | 채번규칙 (Numbering Rule) | `NumberingRuleController` | 높음 | P1 |
| 8 | 견적수식 (Quote Formula) | `QuoteFormulaController` | 높음 | P1 |
| 9 | 전자서명 (eSign) | `EsignController` 외 2개 | 높음 | P1 |
| 10 | 분개기록 (Journal Entry) | `JournalEntryController` | 중간 | P2 |
| 11 | 구독관리 (Subscription) | `SubscriptionController` | 중간 | P2 |
| 12 | 판매수수료 | `SalesCommissionController` | 중간 | P2 |
| 13 | 휴가정책 (Leave Promotion) | `LeavePromotionController` | 중간 | P2 |
| 14 | 통합근태 | `AttendanceIntegratedController` | 중간 | P2 |
| 15 | 사업소득자 | `BusinessIncomeEarnerController` | 중간 | P2 |
> **참고**: API에 `ExpectedExpense`(미지급비용), `GeneralJournalEntry`(일반전표)가 이미 존재한다. MNG 버전과 기능 범위가 다르므로 비교 후 보강한다.
### 3.2 MNG 전용 (이관 불필요)
관리자 전용이거나 특정 테넌트에 국한되어 멀티테넌트 이관 가치가 낮은 기능이다.
| # | 기능 | MNG 컨트롤러 수 | 사유 |
|---|------|:--------------:|------|
| 1 | PMIS 시공관리 | 12개 | 특정 테넌트 건설 전용, `Juil/` 네임스페이스 |
| 2 | 신용평가 (Credit) | 1개 | 외부 API 연동, 관리자 전용 |
| 3 | Dev Tools | 2개 | API Explorer, Flow Tester (개발자 전용) |
| 4 | 추가 기능 | 5개 | Kiosk, Notion, RAG, Docx, Pptx (유틸리티) |
| 5 | 튜토리얼 비디오 | 1개 | 관리자 콘텐츠 관리 |
| 6 | 로드맵/R&D | 2개 | 내부 기획 도구 |
| 7 | 일일 스크럼 | 1개 | 내부 개발팀 도구 |
| 8 | 감사 로그/트리거 추적 | 3개 | 관리자 감사 도구 (API 읽기전용 API만 검토) |
**후순위** (규모별 차이가 커서 범용성 낮음):
| # | 기능 | 비고 |
|---|------|------|
| 9 | 경조사비/회사차량/차량일지 | 회사 규모별 차이 큼 |
| 10 | 명함 요청 | 마이너 기능 |
| 11 | 면접 시나리오 | 영업 내부 도구 |
### 3.3 React UI 보강 대상
API는 완료되었으나 React 화면이 미구현 또는 부분 구현된 기능이다.
| # | 기능 | API | React | 우선순위 |
|---|------|:---:|:-----:|:--------:|
| 1 | 품질관리 (QMS) | 완료 | 개발중 | P1 |
| 2 | 설비/장비 (Equipment) | 완료 | 개발중 | P1 |
| 3 | 어음 관리 (Bill) | 완료 | 미구현 | P2 |
| 4 | 가지급금 (Loan) | 완료 | 미구현 | P2 |
| 5 | 요금제/결제 | 완료 | 부분 | P1 |
| 6 | 캘린더 일정 | 완료 | 부분 | P2 |
| 7 | AI 보고서 | 완료 | 부분 | P2 |
| 8 | 팝업 관리 | 완료 | 부분 | P3 |
---
## 4. 실전 서비스 이관 로드맵
### Phase 1: 핵심 재무 (즉시 착수)
테넌트가 실제 업무에 필수적으로 사용하는 재무 기능이다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 1-1 | 자금일정 + 일일자금 | API 신규 + React 신규 | 높음 |
| 1-2 | 정산 (Settlement) | API 신규 + React 신규 | 높음 |
| 1-3 | 환불 (Refund) | API 신규 + React 신규 | 중간 |
| 1-4 | 부가세 (VAT) | API 신규 + React 신규 | 높음 |
| 1-5 | 채번규칙 | API 신규 + React 설정화면 | 중간 |
### Phase 2: 영업/견적 고도화
견적 자동화와 수수료 체계를 구축한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 2-1 | 견적수식 (Quote Formula) | API 신규 + React 신규 | 매우 높음 |
| 2-2 | 판매수수료 | API 신규 + React 신규 | 높음 |
| 2-3 | 전자서명 (eSign) | API 신규 + React 신규 | 매우 높음 |
### Phase 3: React UI 보강
API는 완료되어 있으므로 React 화면만 구현한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 3-1 | 품질관리 (QMS) | React 신규 | 높음 |
| 3-2 | 설비/장비 | React 신규 | 중간 |
| 3-3 | 어음/가지급금 | React 신규 | 중간 |
| 3-4 | 요금제/결제 화면 | React 보강 | 중간 |
### Phase 4: 인사 고도화
기본 HR은 완료되어 있으며, 정책/통합 기능을 추가한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 4-1 | 휴가정책 자동부여 | API 보강 + React | 중간 |
| 4-2 | 통합근태 | API 신규 + React | 높음 |
| 4-3 | 사업소득자/지급 | API 신규 + React | 중간 |
---
## 5. 이관 작업 시 공통 체크리스트
### API 이관 시
- [ ] `tenant_id` 기반 멀티테넌트 격리 적용
- [ ] MNG HTMX 코드 → REST API로 분리
- [ ] 마이그레이션 소유 기준 확인 (API 전용/공용 → API, MNG 전용 → MNG)
- [ ] `options` JSON 컬럼 정책 준수
- [ ] Service-First 아키텍처 (Controller → Service → Model)
- [ ] FormRequest 검증 적용
- [ ] Swagger 문서 작성
### React 이관 시
- [ ] Server Actions 패턴 사용 (`executePaginatedAction`, `executeServerAction`)
- [ ] `createCrudService` 팩토리 활용 (반복 코드 제거)
- [ ] Zod 스키마 검증 적용
- [ ] `FormField` molecule 사용
- [ ] 기존 `UniversalListPage` 템플릿 활용
- [ ] 모바일 반응형 고려 (`MobileCard`)
---
## 6. 수량 요약
| 분류 | 항목 수 | 비고 |
|------|:-------:|------|
| 이관 완료 | 24개 도메인 | API + React 모두 존재 |
| 이관 대상 (P1) | 9개 기능 | Phase 1~2 |
| 이관 대상 (P2) | 6개 기능 | Phase 2~4 |
| MNG 유지 | 11개 기능 | 관리자/특정 테넌트 전용 |
| React UI 보강 | 8개 기능 | API 완료, UI 미구현 |
---
## 관련 문서
- [system/overview.md](overview.md) — 전체 시스템 아키텍처
- [system/api-structure.md](api-structure.md) — API 서버 구조
- [system/react-structure.md](react-structure.md) — React 프론트엔드 구조
- [system/mng-structure.md](mng-structure.md) — MNG 관리자 패널 구조
- [dev/standards/api-rules.md](../dev/standards/api-rules.md) — API 개발 규칙
- [dev/standards/options-column-policy.md](../dev/standards/options-column-policy.md) — options JSON 정책
---
**최종 업데이트**: 2026-03-12
# MNG → API+React 이관 현황 및 로드맵
> **작성일**: 2026-03-12
> **상태**: 분석 완료, 로드맵 수립
> **담당**: R&D실
---
## 1. 개요
### 1.1 목적
MNG(관리자 웹)에서 API+React로 이관할 남은 항목을 식별하고, 멀티테넌시 적합도를 기준으로 실전 서비스 개발 우선순위를 수립한다.
### 1.2 현재 규모
| 프로젝트 | 규모 |
|----------|------|
| **MNG** | 컨트롤러 212개, 모델 227개, 서비스 113개 |
| **API** | 컨트롤러 100+, 모델 120+, 마이그레이션 130+ |
| **React** | 페이지 253개, 컴포넌트 36개 도메인 |
### 1.3 멀티테넌시 판단 기준
| 기준 | 이관 대상 | 이관 불필요 |
|------|----------|-----------|
| 2개+ 테넌트가 사용하는가? | 재무, 영업, 인사, 견적 | PMIS(특정 테넌트 전용) |
| `tenant_id` 격리가 의미 있는가? | 자금일정, 채번규칙, 수수료 | 신용평가, 개발도구 |
| SaaS 고객에게 제공 가치가 있는가? | 전자서명, 정산, VAT | 로드맵, R&D, 스크럼 |
| 관리자만 사용하는가? | 사용자도 씀 → 이관 | Dev Tools, 감사 추적 |
---
## 2. 이관 완료 도메인
API + React 모두 구현 완료된 기능이다.
| 도메인 | 완성도 | 비고 |
|--------|:------:|------|
| 인증/계정 | 100% | 로그인, 토큰, 회원가입 |
| 사용자/테넌트/부서 | 100% | 조직 관리 전체 |
| 역할/권한 | 100% | 3단계 권한 체계 |
| 메뉴 관리 | 100% | 글로벌/테넌트 메뉴 |
| 품목 관리 (Item) | 95% | BOM, 동적 필드 |
| 카테고리/공통코드 | 100% | 트리 구조, 동기화 |
| 거래처 (Client) | 95% | 그룹, OCR |
| 견적/수주/매출 | 90% | 견적서 PDF, 발송 |
| 입찰 (Bidding) | 90% | 상태 관리 |
| 단가 관리 (Pricing) | 90% | 일괄 등록 |
| 카드/계좌/입출금 | 90% | 바로빌 부분 연동 |
| 급여 (Payroll) | 85% | 계산, 확정, 지급 |
| 채권/거래처원장 | 85% | 조회, 메모 |
| 세금계산서 | 85% | 발행, 홈택스 |
| 근태/휴가/직원 | 85% | 출퇴근, 연차 |
| 전자결재 | 80% | 양식, 결재선, 위임 |
| 게시판 | 80% | 커스텀 필드, 댓글 |
| 파일/문서 관리 | 80% | 업로드, 공유, 버전 |
| 생산 (공정/작업지시) | 80% | 작업일지, 자재 투입 |
| 재고 (구매/입고/출하) | 80% | 배차, Lot 추적 |
| 설계/BOM | 75% | 모델 버전, 계산 엔진 |
| 대시보드 | 90% | CEO 포함 5가지 타입 |
| 현장관리 (Site) | 75% | 현장설명회, 계약 |
| 바로빌 (기본) | 70% | 카드/계좌 연동 |
---
## 3. 미이관 항목 분석
### 3.1 멀티테넌트 이관 대상 (P1~P2)
실전 서비스에서 복수 테넌트가 사용할 핵심 기능이다.
| # | 기능 | MNG 컨트롤러 | 적합도 | 우선순위 |
|---|------|-------------|:------:|:--------:|
| 1 | 자금일정 (Fund Schedule) | `FundScheduleController` | 높음 | P1 |
| 2 | 일일자금 (Daily Fund) | `DailyFundController` | 높음 | P1 |
| 3 | 미지급금 (Payable) | `PayableController` | 높음 | P1 |
| 4 | 부가세 (VAT) | `VatRecordController` | 높음 | P1 |
| 5 | 정산 (Settlement) | `SettlementController` | 높음 | P1 |
| 6 | 환불 (Refund) | `RefundController` | 높음 | P1 |
| 7 | 채번규칙 (Numbering Rule) | `NumberingRuleController` | 높음 | P1 |
| 8 | 견적수식 (Quote Formula) | `QuoteFormulaController` | 높음 | P1 |
| 9 | 전자서명 (eSign) | `EsignController` 외 2개 | 높음 | P1 |
| 10 | 분개기록 (Journal Entry) | `JournalEntryController` | 중간 | P2 |
| 11 | 구독관리 (Subscription) | `SubscriptionController` | 중간 | P2 |
| 12 | 판매수수료 | `SalesCommissionController` | 중간 | P2 |
| 13 | 휴가정책 (Leave Promotion) | `LeavePromotionController` | 중간 | P2 |
| 14 | 통합근태 | `AttendanceIntegratedController` | 중간 | P2 |
| 15 | 사업소득자 | `BusinessIncomeEarnerController` | 중간 | P2 |
> **참고**: API에 `ExpectedExpense`(미지급비용), `GeneralJournalEntry`(일반전표)가 이미 존재한다. MNG 버전과 기능 범위가 다르므로 비교 후 보강한다.
### 3.2 MNG 전용 (이관 불필요)
관리자 전용이거나 특정 테넌트에 국한되어 멀티테넌트 이관 가치가 낮은 기능이다.
| # | 기능 | MNG 컨트롤러 수 | 사유 |
|---|------|:--------------:|------|
| 1 | PMIS 시공관리 | 12개 | 특정 테넌트 건설 전용, `Juil/` 네임스페이스 |
| 2 | 신용평가 (Credit) | 1개 | 외부 API 연동, 관리자 전용 |
| 3 | Dev Tools | 2개 | API Explorer, Flow Tester (개발자 전용) |
| 4 | 추가 기능 | 5개 | Kiosk, Notion, RAG, Docx, Pptx (유틸리티) |
| 5 | 튜토리얼 비디오 | 1개 | 관리자 콘텐츠 관리 |
| 6 | 로드맵/R&D | 2개 | 내부 기획 도구 |
| 7 | 일일 스크럼 | 1개 | 내부 개발팀 도구 |
| 8 | 감사 로그/트리거 추적 | 3개 | 관리자 감사 도구 (API 읽기전용 API만 검토) |
**후순위** (규모별 차이가 커서 범용성 낮음):
| # | 기능 | 비고 |
|---|------|------|
| 9 | 경조사비/회사차량/차량일지 | 회사 규모별 차이 큼 |
| 10 | 명함 요청 | 마이너 기능 |
| 11 | 면접 시나리오 | 영업 내부 도구 |
### 3.3 React UI 보강 대상
API는 완료되었으나 React 화면이 미구현 또는 부분 구현된 기능이다.
| # | 기능 | API | React | 우선순위 |
|---|------|:---:|:-----:|:--------:|
| 1 | 품질관리 (QMS) | 완료 | 개발중 | P1 |
| 2 | 설비/장비 (Equipment) | 완료 | 개발중 | P1 |
| 3 | 어음 관리 (Bill) | 완료 | 미구현 | P2 |
| 4 | 가지급금 (Loan) | 완료 | 미구현 | P2 |
| 5 | 요금제/결제 | 완료 | 부분 | P1 |
| 6 | 캘린더 일정 | 완료 | 부분 | P2 |
| 7 | AI 보고서 | 완료 | 부분 | P2 |
| 8 | 팝업 관리 | 완료 | 부분 | P3 |
---
## 4. 실전 서비스 이관 로드맵
### Phase 1: 핵심 재무 (즉시 착수)
테넌트가 실제 업무에 필수적으로 사용하는 재무 기능이다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 1-1 | 자금일정 + 일일자금 | API 신규 + React 신규 | 높음 |
| 1-2 | 정산 (Settlement) | API 신규 + React 신규 | 높음 |
| 1-3 | 환불 (Refund) | API 신규 + React 신규 | 중간 |
| 1-4 | 부가세 (VAT) | API 신규 + React 신규 | 높음 |
| 1-5 | 채번규칙 | API 신규 + React 설정화면 | 중간 |
### Phase 2: 영업/견적 고도화
견적 자동화와 수수료 체계를 구축한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 2-1 | 견적수식 (Quote Formula) | API 신규 + React 신규 | 매우 높음 |
| 2-2 | 판매수수료 | API 신규 + React 신규 | 높음 |
| 2-3 | 전자서명 (eSign) | API 신규 + React 신규 | 매우 높음 |
### Phase 3: React UI 보강
API는 완료되어 있으므로 React 화면만 구현한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 3-1 | 품질관리 (QMS) | React 신규 | 높음 |
| 3-2 | 설비/장비 | React 신규 | 중간 |
| 3-3 | 어음/가지급금 | React 신규 | 중간 |
| 3-4 | 요금제/결제 화면 | React 보강 | 중간 |
### Phase 4: 인사 고도화
기본 HR은 완료되어 있으며, 정책/통합 기능을 추가한다.
| 순서 | 기능 | 작업 범위 | 복잡도 |
|:----:|------|----------|:------:|
| 4-1 | 휴가정책 자동부여 | API 보강 + React | 중간 |
| 4-2 | 통합근태 | API 신규 + React | 높음 |
| 4-3 | 사업소득자/지급 | API 신규 + React | 중간 |
---
## 5. 이관 작업 시 공통 체크리스트
### API 이관 시
- [ ] `tenant_id` 기반 멀티테넌트 격리 적용
- [ ] MNG HTMX 코드 → REST API로 분리
- [ ] 마이그레이션 소유 기준 확인 (API 전용/공용 → API, MNG 전용 → MNG)
- [ ] `options` JSON 컬럼 정책 준수
- [ ] Service-First 아키텍처 (Controller → Service → Model)
- [ ] FormRequest 검증 적용
- [ ] Swagger 문서 작성
### React 이관 시
- [ ] Server Actions 패턴 사용 (`executePaginatedAction`, `executeServerAction`)
- [ ] `createCrudService` 팩토리 활용 (반복 코드 제거)
- [ ] Zod 스키마 검증 적용
- [ ] `FormField` molecule 사용
- [ ] 기존 `UniversalListPage` 템플릿 활용
- [ ] 모바일 반응형 고려 (`MobileCard`)
---
## 6. 수량 요약
| 분류 | 항목 수 | 비고 |
|------|:-------:|------|
| 이관 완료 | 24개 도메인 | API + React 모두 존재 |
| 이관 대상 (P1) | 9개 기능 | Phase 1~2 |
| 이관 대상 (P2) | 6개 기능 | Phase 2~4 |
| MNG 유지 | 11개 기능 | 관리자/특정 테넌트 전용 |
| React UI 보강 | 8개 기능 | API 완료, UI 미구현 |
---
## 관련 문서
- [system/overview.md](overview.md) — 전체 시스템 아키텍처
- [system/api-structure.md](api-structure.md) — API 서버 구조
- [system/react-structure.md](react-structure.md) — React 프론트엔드 구조
- [system/mng-structure.md](mng-structure.md) — MNG 관리자 패널 구조
- [dev/standards/api-rules.md](../dev/standards/api-rules.md) — API 개발 규칙
- [dev/standards/options-column-policy.md](../dev/standards/options-column-policy.md) — options JSON 정책
---
**최종 업데이트**: 2026-03-12

View File

@@ -1,481 +1,481 @@
# React 컴포넌트 아키텍처 현황
> **작성일**: 2026-03-12
> **상태**: 현황 분석 완료
> **관련 문서**: [react-structure.md](react-structure.md) — 전체 프로젝트 구조
---
## 1. 개요
### 1.1 목적
SAM React 프로젝트의 컴포넌트 아키텍처 현황을 기록한다. Atomic Design 폴더 구조를 채택했으나 **실제 계층적 의존성은 부분적으로만 작동**하고 있으며, 이 문서는 현실과 이상의 차이를 정리하여 신규 화면 개발 시 올바른 패턴을 참고하도록 한다.
### 1.2 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| UI 프리미티브 | Radix UI (13개 패키지) | 접근성 기반 헤드리스 컴포넌트 |
| 스타일 | shadcn/ui + Tailwind CSS v4 + CVA | 프로젝트 맞춤 커스터마이징 |
| 폼 | React Hook Form + Zod | 타입 안전한 검증 |
| 상태 | Zustand 5 + Immer | 전역 상태 (persist) |
| 아이콘 | Lucide React (550+) | |
| 차트 | Recharts v3 | |
| 에디터 | Tiptap | 리치텍스트 |
| 테마 | CSS 변수 + Zustand | light / dark / senior 3종 |
---
## 2. 폴더 구조
```
src/components/
├── ui/ ← shadcn/ui 프리미티브 (60개)
│ Button, Input, Select, Dialog, DatePicker,
│ CurrencyInput, PhoneInput, FileDropzone 등
├── atoms/ ← Atomic Design 원자 (3개)
│ BadgeSm, TabChip, ScrollableButtonGroup
├── molecules/ ← Atomic Design 분자 (13개)
│ FormField, StatusBadge, MobileCard,
│ DateRangeSelector, StandardDialog 등
├── organisms/ ← Atomic Design 유기체 (14개)
│ PageLayout, PageHeader, DataTable,
│ StatCards, EmptyState, SearchFilter 등
├── templates/ ← 페이지 템플릿 (5개, 2개 미사용)
│ UniversalListPage, IntegratedDetailTemplate,
│ IntegratedListTemplateV2
├── layout/ ← 전역 레이아웃
│ Sidebar, CommandMenuSearch, HeaderFavoritesBar
├── business/ ← 대시보드
├── accounting/ ← 회계 도메인
├── hr/ ← 인사 도메인
├── approval/ ← 전자결재 도메인
├── items/ ← 품목 도메인
├── production/ ← 생산 도메인
├── ... (도메인별 ~600개)
└── common/ ← 공통 (DataTable 독립 구현)
```
---
## 3. 실제 의존성 분석
### 3.1 이상 vs 현실
```
[이상적 Atomic Design]
Page → Templates → Organisms → Molecules → Atoms → ui/
[실제 구조]
Page ──→ Templates ──────────────────────→ ui/ (직접)
├─→ Organisms ──────────────────────→ ui/ (직접)
├─→ Molecules ──────────────────────→ ui/ (직접)
└─→ ui/ ────────────────────────────→ ui/ (직접)
계층 간 연결:
atoms → molecules: 2/13만 사용 (17%)
molecules → organisms: 0회 (완전 단절)
organisms → templates: 2회만 (PageLayout, PageHeader)
```
### 3.2 Import 비율 (전체 2,186회)
| 대상 | 횟수 | 비율 | 평가 |
|------|------|------|------|
| **ui/ 직접** | 1,831회 | **83.7%** | 압도적 |
| templates | 192회 | 8.8% | 핵심 2개 템플릿 |
| organisms | 148회 | 6.8% | PageLayout/MobileCard 중심 |
| molecules | 45회 | 2.1% | FormField/StatusBadge 중심 |
| atoms | 15회 | 0.7% | 거의 미사용 |
### 3.3 계층별 ui/ 의존
| 계층 | ui/ import 횟수 | atoms/molecules/organisms import | 비고 |
|------|----------------|--------------------------------|------|
| templates | 25회+ | 4회 | ui/ 직접 의존 |
| organisms | 25회+ | 0회 | molecules 완전 미사용 |
| molecules | 28회+ | 2회 (atoms) | 대부분 ui/ 직접 |
| 도메인 코드 | 1,700회+ | ~300회 | 83% ui/ 직접 |
---
## 4. 핵심 컴포넌트 사용 현황
### 4.1 고빈도 컴포넌트 (TOP 10)
| 순위 | 컴포넌트 | 계층 | 사용 횟수 | 역할 |
|------|---------|------|---------|------|
| 1 | `FormField` | molecules | 216회 | Label + Input 통합 |
| 2 | `UniversalListPage` | templates | 214회 | 목록 페이지 전체 |
| 3 | `IntegratedDetailTemplate` | templates | 182회 | 상세/폼 페이지 전체 |
| 4 | `MobileCard` | molecules+organisms | 172+129회 | 모바일 카드 |
| 5 | `StatusBadge` | molecules | 125회 | 상태 뱃지 |
| 6 | `PageLayout` | organisms | 67회 | 페이지 래퍼 |
| 7 | `BadgeSm` | atoms | 63회 | 소형 뱃지 |
| 8 | `PageHeader` | organisms | 56회 | 페이지 헤더 |
| 9 | `ListMobileCard` | organisms | 50회 | 모바일 목록 카드 |
| 10 | `DateRangeSelector` | molecules | 45회 | 날짜 범위 필터 |
### 4.2 미사용 컴포넌트
| 컴포넌트 | 계층 | 비고 |
|---------|------|------|
| `ListPageTemplate` | templates | 0회 — `UniversalListPage`로 대체됨 |
| `ResponsiveFormTemplate` | templates | 0회 — `IntegratedDetailTemplate`로 대체됨 |
### 4.3 저사용 컴포넌트 (5회 미만)
| 컴포넌트 | 계층 | 사용 횟수 |
|---------|------|---------|
| `FormActions` | organisms | 4회 |
| `ScreenVersionHistory` | organisms | 4회 |
---
## 5. 실제 페이지 개발 패턴
### 5.1 목록 페이지 (90%+ 사용 패턴)
```tsx
// 실제 도메인 코드 패턴
import { UniversalListPage } from '@/components/templates/UniversalListPage';
import { StatusBadge } from '@/components/molecules';
import { MobileCard } from '@/components/organisms/MobileCard';
import { Button } from '@/components/ui/button';
import { Dialog } from '@/components/ui/dialog';
```
`UniversalListPage`가 내부적으로 테이블, 페이지네이션, 검색 필터, 모바일 대응을 모두 포함한다.
### 5.2 상세/폼 페이지 (90%+ 사용 패턴)
```tsx
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { FormField } from '@/components/molecules/FormField';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
```
`IntegratedDetailTemplate`이 내부적으로 DetailField, DetailSection, DetailGrid, DetailActions 등 9개 하위 컴포넌트를 자체 포함한다.
### 5.3 도메인 폴더 내부 구조
```
src/components/accounting/BillManagement/
├── BillManagementClient.tsx ← 메인 컴포넌트
├── actions.ts ← Server Action (API 호출)
├── types.ts ← TypeScript 타입
├── billConfig.ts ← 설정, 필터 옵션
└── modals/ ← 하위 모달 컴포넌트
```
---
## 6. ui/ 컴포넌트 목록 (shadcn/ui 기반, 60개)
### 6.1 기본 UI
| 컴포넌트 | 설명 |
|---------|------|
| `button` | 6개 variant (default/destructive/outline/secondary/ghost/link), 4개 size |
| `input` | HTML input 래퍼, aria-invalid 지원 |
| `label` | HTML label 래퍼 |
| `card` | Card/CardHeader/CardTitle/CardContent/CardFooter |
| `badge` | 5개 variant |
| `alert` | Alert/AlertTitle/AlertDescription |
| `skeleton` | 로딩 스켈레톤 |
### 6.2 폼 입력
| 컴포넌트 | 설명 |
|---------|------|
| `checkbox` | Radix UI Checkbox |
| `radio-group` | Radix UI RadioGroup |
| `select` | Radix UI Select (검색 없음) |
| `switch` | Radix UI Switch |
| `slider` | Radix UI Slider |
| `date-picker` | 날짜 선택 |
| `date-range-picker` | 날짜 범위 |
| `date-time-picker` | 날짜+시간 |
| `file-input` | 파일 선택 |
| `file-dropzone` | 드래그앤드롭 파일 |
| `image-upload` | 이미지 업로드 |
| `multi-select-combobox` | 다중 선택 콤보박스 |
| `searchable-select` | 검색 가능 셀렉트 |
### 6.3 한국형 입력 (자동 포맷팅)
| 컴포넌트 | 포맷 | 용도 |
|---------|------|------|
| `phone-input` | `010-1234-5678` | 전화번호 |
| `business-number-input` | `123-45-67890` | 사업자등록번호 |
| `personal-number-input` | `123456-7890123` | 주민번호 |
| `account-number-input` | 은행별 포맷 | 계좌번호 |
| `card-number-input` | `1234 5678 9012 3456` | 카드번호 |
| `currency-input` | `1,234,567` | 금액 (천단위) |
| `quantity-input` | 정수 | 수량 |
| `number-input` | 소수점 | 숫자 |
### 6.4 오버레이/피드백
| 컴포넌트 | 설명 |
|---------|------|
| `dialog` | Radix UI Dialog |
| `alert-dialog` | 확인/취소 다이얼로그 |
| `drawer` | Vaul 드로어 |
| `sheet` | 사이드 패널 |
| `popover` | Radix UI Popover |
| `tooltip` | Radix UI Tooltip |
| `dropdown-menu` | Radix UI DropdownMenu |
| `confirm-dialog` | 커스텀 확인 다이얼로그 |
| `command` | cmdk 커맨드 팔레트 |
| `loading-spinner` | 스피너 |
| `progress` | Radix UI Progress |
| `sonner` | 토스트 알림 |
### 6.5 레이아웃/데이터
| 컴포넌트 | 설명 |
|---------|------|
| `table` | HTML table 래퍼 |
| `tabs` | Radix UI Tabs |
| `accordion` | Radix UI Accordion |
| `collapsible` | Radix UI Collapsible |
| `scroll-area` | Radix UI ScrollArea |
---
## 7. 테마 시스템
### 7.1 3가지 테마
| 테마 | 클래스 | 특징 |
|------|--------|------|
| Light | `:root` (기본) | 밝은 배경, 표준 글자 크기 |
| Dark | `.dark` | 어두운 배경, 밝은 글자 |
| Senior | `.senior` | 큰 글자(18px), 높은 대비, 굵은 폰트 |
### 7.2 CSS 변수 체계
```css
:root {
--primary: #3B82F6; /* 주 색상 */
--destructive: #EF4444; /* 위험/삭제 */
--background: #FAFAFA; /* 배경 */
--card: #FFFFFF; /* 카드 배경 */
--border: #E2E8F0; /* 테두리 */
/* 60+ 색상 변수 */
}
.dark { --background: #0F172A; --primary: #60A5FA; ... }
.senior { --font-size: 18px; --font-weight-medium: 600; ... }
```
### 7.3 Tailwind variant
```css
@variant dark (&:is(.dark *));
@variant senior (&:is(.senior *));
```
코드에서 `dark:bg-slate-800 senior:text-lg` 형태로 사용한다.
### 7.4 상태 관리
```
Zustand (themeStore) → document.documentElement.className 변경
→ localStorage persist (새로고침 유지)
```
---
## 8. 모바일 반응형 전략
### 8.1 자동 전환
| 뷰포트 | 표시 방식 |
|--------|----------|
| 768px 이상 (md:) | `DataTable` (테이블) |
| 768px 미만 | `MobileCard` / `ListMobileCard` (카드 목록) |
### 8.2 핵심 모바일 컴포넌트
| 컴포넌트 | 용도 | 사용 횟수 |
|---------|------|---------|
| `MobileCard` (molecules) | 필터/정보 카드 | 172회 |
| `MobileCard` (organisms) | 상세 조회 카드 | 129회 |
| `ListMobileCard` | 목록 카드 | 50회 |
| `MobileFilter` | 모바일 필터 | 14회 |
### 8.3 iOS Safe Area
```css
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
}
```
Capacitor 기반 모바일 앱 대응이 포함되어 있다.
---
## 9. 폼 패턴
### 9.1 신규 폼 (권장 패턴)
```tsx
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
name: z.string().min(1, '필수'),
amount: z.number().min(0),
status: z.enum(['active', 'inactive']),
});
type FormData = z.infer<typeof schema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { name: '', amount: 0, status: 'active' },
});
return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;
}
```
### 9.2 CVA 기반 variant 시스템
```tsx
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-md',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
destructive: 'bg-destructive text-white',
outline: 'border bg-background',
ghost: 'hover:bg-accent',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3',
lg: 'h-10 px-6',
icon: 'size-9',
},
},
}
);
```
---
## 10. 신규 화면 개발 가이드
### 10.1 목록 페이지 작성 시
```
1. UniversalListPage 사용 (목록 90%+ 커버)
2. 도메인 폴더에 actions.ts, types.ts, config.ts 분리
3. StatusBadge로 상태 표시
4. MobileCard 렌더 함수 정의 (모바일 대응)
5. 추가 UI는 ui/ 에서 직접 import
```
### 10.2 상세/폼 페이지 작성 시
```
1. IntegratedDetailTemplate 사용 (상세/폼 90%+ 커버)
2. FormField로 Label + Input 조합 (일관성)
3. Zod 스키마 정의 (검증)
4. 추가 UI는 ui/ 에서 직접 import
```
### 10.3 Import 우선순위
```
1순위: templates (UniversalListPage, IntegratedDetailTemplate)
2순위: molecules (FormField, StatusBadge, DateRangeSelector)
3순위: organisms (PageLayout, PageHeader, EmptyState)
4순위: ui/ (Button, Dialog, Input 등 프리미티브)
```
> atoms는 현재 3개뿐이므로 필요 시 ui/에서 직접 사용한다.
### 10.4 하지 말 것
```
❌ ListPageTemplate 사용 (dead code — UniversalListPage 사용)
❌ ResponsiveFormTemplate 사용 (dead code — IntegratedDetailTemplate 사용)
❌ 새 atoms 만들기 (ui/ 컴포넌트로 충분)
❌ 도메인 코드에서 다른 도메인 컴포넌트 import
```
---
## 11. 현황 평가 요약
### 11.1 잘 작동하는 부분
| 항목 | 설명 |
|------|------|
| **2개 핵심 템플릿** | `UniversalListPage`(214회) + `IntegratedDetailTemplate`(182회)가 전체 페이지의 90%+ 커버 |
| **shadcn/ui 기반 ui/** | 60개 프리미티브가 일관된 디자인 시스템 제공 |
| **고빈도 molecules** | `FormField`(216회), `StatusBadge`(125회)가 폼/상태 표시 표준화 |
| **모바일 대응** | MobileCard 기반 자동 전환 |
| **테마 시스템** | light/dark/senior 3종 CSS 변수 기반 |
| **한국형 입력** | 전화번호, 사업자번호, 계좌번호 등 자동 포맷팅 |
### 11.2 개선이 필요한 부분
| 항목 | 현상 | 영향 |
|------|------|------|
| **계층 간 의존성 붕괴** | organisms가 molecules를 0회 사용 | Atomic Design 의미 퇴색 |
| **atoms 유명무실** | 3개만 존재, ui/ 60개에 비해 극소 | 계층 존재 이유 불분명 |
| **ui/ 과의존** | 전체 import의 83.7% | 추상화 효과 없음 |
| **Dead code** | ListPageTemplate, ResponsiveFormTemplate 미사용 | 혼란 유발 |
| **index.ts 비일관** | atoms/templates는 index.ts 미사용 | import 패턴 불통일 |
### 11.3 결론
**실제 아키텍처는 "Templates + UI Components" 2계층 구조**이다.
```
실제 작동 구조:
Layer 1 — 페이지 템플릿 (2개가 전체 지배)
├── UniversalListPage ← 목록 페이지
└── IntegratedDetailTemplate ← 상세/폼 페이지
Layer 2 — UI 프리미티브 (shadcn/ui)
└── ui/ 60개 컴포넌트 ← 모든 곳에서 직접 사용
보조 — molecules (FormField, StatusBadge 등 고빈도 유틸)
보조 — organisms (PageLayout, PageHeader, MobileCard)
```
Atomic Design 폴더명(atoms/molecules/organisms/templates)은 유지되어 있으나, 실제 개발 시에는 **templates → ui/ 직접 사용** 패턴을 따르는 것이 현실적이다.
---
## 관련 문서
- [React 프론트엔드 구조](react-structure.md) — 프로젝트 규모, 도메인, 아키텍처 패턴
- [API 서버 구조](api-structure.md) — API 서버 구조
- [MNG 관리자 패널 구조](mng-structure.md) — MNG 구조
---
**최종 업데이트**: 2026-03-12
# React 컴포넌트 아키텍처 현황
> **작성일**: 2026-03-12
> **상태**: 현황 분석 완료
> **관련 문서**: [react-structure.md](react-structure.md) — 전체 프로젝트 구조
---
## 1. 개요
### 1.1 목적
SAM React 프로젝트의 컴포넌트 아키텍처 현황을 기록한다. Atomic Design 폴더 구조를 채택했으나 **실제 계층적 의존성은 부분적으로만 작동**하고 있으며, 이 문서는 현실과 이상의 차이를 정리하여 신규 화면 개발 시 올바른 패턴을 참고하도록 한다.
### 1.2 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| UI 프리미티브 | Radix UI (13개 패키지) | 접근성 기반 헤드리스 컴포넌트 |
| 스타일 | shadcn/ui + Tailwind CSS v4 + CVA | 프로젝트 맞춤 커스터마이징 |
| 폼 | React Hook Form + Zod | 타입 안전한 검증 |
| 상태 | Zustand 5 + Immer | 전역 상태 (persist) |
| 아이콘 | Lucide React (550+) | |
| 차트 | Recharts v3 | |
| 에디터 | Tiptap | 리치텍스트 |
| 테마 | CSS 변수 + Zustand | light / dark / senior 3종 |
---
## 2. 폴더 구조
```
src/components/
├── ui/ ← shadcn/ui 프리미티브 (60개)
│ Button, Input, Select, Dialog, DatePicker,
│ CurrencyInput, PhoneInput, FileDropzone 등
├── atoms/ ← Atomic Design 원자 (3개)
│ BadgeSm, TabChip, ScrollableButtonGroup
├── molecules/ ← Atomic Design 분자 (13개)
│ FormField, StatusBadge, MobileCard,
│ DateRangeSelector, StandardDialog 등
├── organisms/ ← Atomic Design 유기체 (14개)
│ PageLayout, PageHeader, DataTable,
│ StatCards, EmptyState, SearchFilter 등
├── templates/ ← 페이지 템플릿 (5개, 2개 미사용)
│ UniversalListPage, IntegratedDetailTemplate,
│ IntegratedListTemplateV2
├── layout/ ← 전역 레이아웃
│ Sidebar, CommandMenuSearch, HeaderFavoritesBar
├── business/ ← 대시보드
├── accounting/ ← 회계 도메인
├── hr/ ← 인사 도메인
├── approval/ ← 전자결재 도메인
├── items/ ← 품목 도메인
├── production/ ← 생산 도메인
├── ... (도메인별 ~600개)
└── common/ ← 공통 (DataTable 독립 구현)
```
---
## 3. 실제 의존성 분석
### 3.1 이상 vs 현실
```
[이상적 Atomic Design]
Page → Templates → Organisms → Molecules → Atoms → ui/
[실제 구조]
Page ──→ Templates ──────────────────────→ ui/ (직접)
├─→ Organisms ──────────────────────→ ui/ (직접)
├─→ Molecules ──────────────────────→ ui/ (직접)
└─→ ui/ ────────────────────────────→ ui/ (직접)
계층 간 연결:
atoms → molecules: 2/13만 사용 (17%)
molecules → organisms: 0회 (완전 단절)
organisms → templates: 2회만 (PageLayout, PageHeader)
```
### 3.2 Import 비율 (전체 2,186회)
| 대상 | 횟수 | 비율 | 평가 |
|------|------|------|------|
| **ui/ 직접** | 1,831회 | **83.7%** | 압도적 |
| templates | 192회 | 8.8% | 핵심 2개 템플릿 |
| organisms | 148회 | 6.8% | PageLayout/MobileCard 중심 |
| molecules | 45회 | 2.1% | FormField/StatusBadge 중심 |
| atoms | 15회 | 0.7% | 거의 미사용 |
### 3.3 계층별 ui/ 의존
| 계층 | ui/ import 횟수 | atoms/molecules/organisms import | 비고 |
|------|----------------|--------------------------------|------|
| templates | 25회+ | 4회 | ui/ 직접 의존 |
| organisms | 25회+ | 0회 | molecules 완전 미사용 |
| molecules | 28회+ | 2회 (atoms) | 대부분 ui/ 직접 |
| 도메인 코드 | 1,700회+ | ~300회 | 83% ui/ 직접 |
---
## 4. 핵심 컴포넌트 사용 현황
### 4.1 고빈도 컴포넌트 (TOP 10)
| 순위 | 컴포넌트 | 계층 | 사용 횟수 | 역할 |
|------|---------|------|---------|------|
| 1 | `FormField` | molecules | 216회 | Label + Input 통합 |
| 2 | `UniversalListPage` | templates | 214회 | 목록 페이지 전체 |
| 3 | `IntegratedDetailTemplate` | templates | 182회 | 상세/폼 페이지 전체 |
| 4 | `MobileCard` | molecules+organisms | 172+129회 | 모바일 카드 |
| 5 | `StatusBadge` | molecules | 125회 | 상태 뱃지 |
| 6 | `PageLayout` | organisms | 67회 | 페이지 래퍼 |
| 7 | `BadgeSm` | atoms | 63회 | 소형 뱃지 |
| 8 | `PageHeader` | organisms | 56회 | 페이지 헤더 |
| 9 | `ListMobileCard` | organisms | 50회 | 모바일 목록 카드 |
| 10 | `DateRangeSelector` | molecules | 45회 | 날짜 범위 필터 |
### 4.2 미사용 컴포넌트
| 컴포넌트 | 계층 | 비고 |
|---------|------|------|
| `ListPageTemplate` | templates | 0회 — `UniversalListPage`로 대체됨 |
| `ResponsiveFormTemplate` | templates | 0회 — `IntegratedDetailTemplate`로 대체됨 |
### 4.3 저사용 컴포넌트 (5회 미만)
| 컴포넌트 | 계층 | 사용 횟수 |
|---------|------|---------|
| `FormActions` | organisms | 4회 |
| `ScreenVersionHistory` | organisms | 4회 |
---
## 5. 실제 페이지 개발 패턴
### 5.1 목록 페이지 (90%+ 사용 패턴)
```tsx
// 실제 도메인 코드 패턴
import { UniversalListPage } from '@/components/templates/UniversalListPage';
import { StatusBadge } from '@/components/molecules';
import { MobileCard } from '@/components/organisms/MobileCard';
import { Button } from '@/components/ui/button';
import { Dialog } from '@/components/ui/dialog';
```
`UniversalListPage`가 내부적으로 테이블, 페이지네이션, 검색 필터, 모바일 대응을 모두 포함한다.
### 5.2 상세/폼 페이지 (90%+ 사용 패턴)
```tsx
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { FormField } from '@/components/molecules/FormField';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
```
`IntegratedDetailTemplate`이 내부적으로 DetailField, DetailSection, DetailGrid, DetailActions 등 9개 하위 컴포넌트를 자체 포함한다.
### 5.3 도메인 폴더 내부 구조
```
src/components/accounting/BillManagement/
├── BillManagementClient.tsx ← 메인 컴포넌트
├── actions.ts ← Server Action (API 호출)
├── types.ts ← TypeScript 타입
├── billConfig.ts ← 설정, 필터 옵션
└── modals/ ← 하위 모달 컴포넌트
```
---
## 6. ui/ 컴포넌트 목록 (shadcn/ui 기반, 60개)
### 6.1 기본 UI
| 컴포넌트 | 설명 |
|---------|------|
| `button` | 6개 variant (default/destructive/outline/secondary/ghost/link), 4개 size |
| `input` | HTML input 래퍼, aria-invalid 지원 |
| `label` | HTML label 래퍼 |
| `card` | Card/CardHeader/CardTitle/CardContent/CardFooter |
| `badge` | 5개 variant |
| `alert` | Alert/AlertTitle/AlertDescription |
| `skeleton` | 로딩 스켈레톤 |
### 6.2 폼 입력
| 컴포넌트 | 설명 |
|---------|------|
| `checkbox` | Radix UI Checkbox |
| `radio-group` | Radix UI RadioGroup |
| `select` | Radix UI Select (검색 없음) |
| `switch` | Radix UI Switch |
| `slider` | Radix UI Slider |
| `date-picker` | 날짜 선택 |
| `date-range-picker` | 날짜 범위 |
| `date-time-picker` | 날짜+시간 |
| `file-input` | 파일 선택 |
| `file-dropzone` | 드래그앤드롭 파일 |
| `image-upload` | 이미지 업로드 |
| `multi-select-combobox` | 다중 선택 콤보박스 |
| `searchable-select` | 검색 가능 셀렉트 |
### 6.3 한국형 입력 (자동 포맷팅)
| 컴포넌트 | 포맷 | 용도 |
|---------|------|------|
| `phone-input` | `010-1234-5678` | 전화번호 |
| `business-number-input` | `123-45-67890` | 사업자등록번호 |
| `personal-number-input` | `123456-7890123` | 주민번호 |
| `account-number-input` | 은행별 포맷 | 계좌번호 |
| `card-number-input` | `1234 5678 9012 3456` | 카드번호 |
| `currency-input` | `1,234,567` | 금액 (천단위) |
| `quantity-input` | 정수 | 수량 |
| `number-input` | 소수점 | 숫자 |
### 6.4 오버레이/피드백
| 컴포넌트 | 설명 |
|---------|------|
| `dialog` | Radix UI Dialog |
| `alert-dialog` | 확인/취소 다이얼로그 |
| `drawer` | Vaul 드로어 |
| `sheet` | 사이드 패널 |
| `popover` | Radix UI Popover |
| `tooltip` | Radix UI Tooltip |
| `dropdown-menu` | Radix UI DropdownMenu |
| `confirm-dialog` | 커스텀 확인 다이얼로그 |
| `command` | cmdk 커맨드 팔레트 |
| `loading-spinner` | 스피너 |
| `progress` | Radix UI Progress |
| `sonner` | 토스트 알림 |
### 6.5 레이아웃/데이터
| 컴포넌트 | 설명 |
|---------|------|
| `table` | HTML table 래퍼 |
| `tabs` | Radix UI Tabs |
| `accordion` | Radix UI Accordion |
| `collapsible` | Radix UI Collapsible |
| `scroll-area` | Radix UI ScrollArea |
---
## 7. 테마 시스템
### 7.1 3가지 테마
| 테마 | 클래스 | 특징 |
|------|--------|------|
| Light | `:root` (기본) | 밝은 배경, 표준 글자 크기 |
| Dark | `.dark` | 어두운 배경, 밝은 글자 |
| Senior | `.senior` | 큰 글자(18px), 높은 대비, 굵은 폰트 |
### 7.2 CSS 변수 체계
```css
:root {
--primary: #3B82F6; /* 주 색상 */
--destructive: #EF4444; /* 위험/삭제 */
--background: #FAFAFA; /* 배경 */
--card: #FFFFFF; /* 카드 배경 */
--border: #E2E8F0; /* 테두리 */
/* 60+ 색상 변수 */
}
.dark { --background: #0F172A; --primary: #60A5FA; ... }
.senior { --font-size: 18px; --font-weight-medium: 600; ... }
```
### 7.3 Tailwind variant
```css
@variant dark (&:is(.dark *));
@variant senior (&:is(.senior *));
```
코드에서 `dark:bg-slate-800 senior:text-lg` 형태로 사용한다.
### 7.4 상태 관리
```
Zustand (themeStore) → document.documentElement.className 변경
→ localStorage persist (새로고침 유지)
```
---
## 8. 모바일 반응형 전략
### 8.1 자동 전환
| 뷰포트 | 표시 방식 |
|--------|----------|
| 768px 이상 (md:) | `DataTable` (테이블) |
| 768px 미만 | `MobileCard` / `ListMobileCard` (카드 목록) |
### 8.2 핵심 모바일 컴포넌트
| 컴포넌트 | 용도 | 사용 횟수 |
|---------|------|---------|
| `MobileCard` (molecules) | 필터/정보 카드 | 172회 |
| `MobileCard` (organisms) | 상세 조회 카드 | 129회 |
| `ListMobileCard` | 목록 카드 | 50회 |
| `MobileFilter` | 모바일 필터 | 14회 |
### 8.3 iOS Safe Area
```css
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
}
```
Capacitor 기반 모바일 앱 대응이 포함되어 있다.
---
## 9. 폼 패턴
### 9.1 신규 폼 (권장 패턴)
```tsx
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
name: z.string().min(1, '필수'),
amount: z.number().min(0),
status: z.enum(['active', 'inactive']),
});
type FormData = z.infer<typeof schema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { name: '', amount: 0, status: 'active' },
});
return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;
}
```
### 9.2 CVA 기반 variant 시스템
```tsx
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 rounded-md',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
destructive: 'bg-destructive text-white',
outline: 'border bg-background',
ghost: 'hover:bg-accent',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3',
lg: 'h-10 px-6',
icon: 'size-9',
},
},
}
);
```
---
## 10. 신규 화면 개발 가이드
### 10.1 목록 페이지 작성 시
```
1. UniversalListPage 사용 (목록 90%+ 커버)
2. 도메인 폴더에 actions.ts, types.ts, config.ts 분리
3. StatusBadge로 상태 표시
4. MobileCard 렌더 함수 정의 (모바일 대응)
5. 추가 UI는 ui/ 에서 직접 import
```
### 10.2 상세/폼 페이지 작성 시
```
1. IntegratedDetailTemplate 사용 (상세/폼 90%+ 커버)
2. FormField로 Label + Input 조합 (일관성)
3. Zod 스키마 정의 (검증)
4. 추가 UI는 ui/ 에서 직접 import
```
### 10.3 Import 우선순위
```
1순위: templates (UniversalListPage, IntegratedDetailTemplate)
2순위: molecules (FormField, StatusBadge, DateRangeSelector)
3순위: organisms (PageLayout, PageHeader, EmptyState)
4순위: ui/ (Button, Dialog, Input 등 프리미티브)
```
> atoms는 현재 3개뿐이므로 필요 시 ui/에서 직접 사용한다.
### 10.4 하지 말 것
```
❌ ListPageTemplate 사용 (dead code — UniversalListPage 사용)
❌ ResponsiveFormTemplate 사용 (dead code — IntegratedDetailTemplate 사용)
❌ 새 atoms 만들기 (ui/ 컴포넌트로 충분)
❌ 도메인 코드에서 다른 도메인 컴포넌트 import
```
---
## 11. 현황 평가 요약
### 11.1 잘 작동하는 부분
| 항목 | 설명 |
|------|------|
| **2개 핵심 템플릿** | `UniversalListPage`(214회) + `IntegratedDetailTemplate`(182회)가 전체 페이지의 90%+ 커버 |
| **shadcn/ui 기반 ui/** | 60개 프리미티브가 일관된 디자인 시스템 제공 |
| **고빈도 molecules** | `FormField`(216회), `StatusBadge`(125회)가 폼/상태 표시 표준화 |
| **모바일 대응** | MobileCard 기반 자동 전환 |
| **테마 시스템** | light/dark/senior 3종 CSS 변수 기반 |
| **한국형 입력** | 전화번호, 사업자번호, 계좌번호 등 자동 포맷팅 |
### 11.2 개선이 필요한 부분
| 항목 | 현상 | 영향 |
|------|------|------|
| **계층 간 의존성 붕괴** | organisms가 molecules를 0회 사용 | Atomic Design 의미 퇴색 |
| **atoms 유명무실** | 3개만 존재, ui/ 60개에 비해 극소 | 계층 존재 이유 불분명 |
| **ui/ 과의존** | 전체 import의 83.7% | 추상화 효과 없음 |
| **Dead code** | ListPageTemplate, ResponsiveFormTemplate 미사용 | 혼란 유발 |
| **index.ts 비일관** | atoms/templates는 index.ts 미사용 | import 패턴 불통일 |
### 11.3 결론
**실제 아키텍처는 "Templates + UI Components" 2계층 구조**이다.
```
실제 작동 구조:
Layer 1 — 페이지 템플릿 (2개가 전체 지배)
├── UniversalListPage ← 목록 페이지
└── IntegratedDetailTemplate ← 상세/폼 페이지
Layer 2 — UI 프리미티브 (shadcn/ui)
└── ui/ 60개 컴포넌트 ← 모든 곳에서 직접 사용
보조 — molecules (FormField, StatusBadge 등 고빈도 유틸)
보조 — organisms (PageLayout, PageHeader, MobileCard)
```
Atomic Design 폴더명(atoms/molecules/organisms/templates)은 유지되어 있으나, 실제 개발 시에는 **templates → ui/ 직접 사용** 패턴을 따르는 것이 현실적이다.
---
## 관련 문서
- [React 프론트엔드 구조](react-structure.md) — 프로젝트 규모, 도메인, 아키텍처 패턴
- [API 서버 구조](api-structure.md) — API 서버 구조
- [MNG 관리자 패널 구조](mng-structure.md) — MNG 구조
---
**최종 업데이트**: 2026-03-12

View File

@@ -1,166 +1,166 @@
# Untitled UI 도입 검토
> **작성일**: 2026-03-12
> **상태**: 검토 중
> **작성자**: 개발팀장
---
## 1. 개요
### 1.1 목적
외부 UI 컴포넌트 라이브러리 **Untitled UI** 도입 여부를 검토한다. SAM 프로젝트의 현재 디자인 시스템(shadcn/ui + Tailwind CSS)과의 호환성, 비용 대비 효과, 도입 방안을 분석한다.
### 1.2 배경
SAM React 프론트엔드는 현재 shadcn/ui 60개 컴포넌트 + Atomic Design 구조로 운영 중이다. 디자인 품질 향상과 개발 속도 개선을 위해 상용 UI Kit 도입을 검토한다.
---
## 2. Untitled UI 소개
### 2.1 제품 개요
Untitled UI는 세계 최대 규모의 Figma UI Kit + React 컴포넌트 라이브러리이다. Atomic Design 시스템으로 유명하며, 디자이너와 개발자 모두를 위한 도구를 제공한다.
- **공식 사이트**: https://www.untitledui.com
- **핵심 특징**: Atomic Design 기반, 접근성(Accessibility) 준수, 반응형 지원
### 2.2 제품 구성 및 가격
| 제품 | 설명 | Free | Solo (1인) | Team (5인) |
|------|------|------|-----------|-----------|
| **Figma** | UI Kit / 디자인 시스템 | $0 | $129 | $359 |
| **React** | React + Tailwind CSS + React Aria 컴포넌트 | $0 | $349 | $699 |
| **Icons** | 4,600+ 아이콘 (다중 스타일) | $0 | $59 | $109 |
> **결제 방식**: 1회 결제 (one-time payment), 평생 업데이트 포함, 무제한 프로젝트 사용 가능
### 2.3 라이선스 구조
- **Solo**: 1명 사용, 무제한 프로젝트
- **Team**: 5명 사용, 무제한 프로젝트
- 디자이너와 개발자가 **다른 제품**을 각각 Solo로 구매하면 Team 불필요
---
## 3. SAM 현재 디자인 시스템과 비교
### 3.1 SAM 현재 스택
| 항목 | 현재 (SAM) | Untitled UI React |
|------|-----------|-------------------|
| UI 프리미티브 | shadcn/ui (60개) | Untitled UI 컴포넌트 |
| 스타일링 | Tailwind CSS v4 | Tailwind CSS |
| 접근성 | Radix UI | React Aria |
| 폼 | React Hook Form + Zod | (별도) |
| 아이콘 | Lucide React (550+) | 4,600+ (유료) |
| 테마 | CSS 변수 (light/dark/senior) | CSS 변수 기반 |
### 3.2 SAM 프론트엔드 현황 (참고)
- **페이지**: 249개
- **컴포넌트**: 612개 (shadcn/ui 기반)
- **핵심 템플릿**: `UniversalListPage` (214회), `IntegratedDetailTemplate` (182회)
- **고빈도 molecules**: `FormField` (216회), `StatusBadge` (125회)
- **프레임워크**: Next.js 15 + React 19
> 상세 현황: `docs/system/react-component-architecture.md` 참조
### 3.3 기술 호환성
| 항목 | 호환 여부 | 비고 |
|------|----------|------|
| Tailwind CSS | ✅ 호환 | 동일 스타일링 프레임워크 |
| React 19 | ✅ 호환 | React 기반 |
| Next.js 15 | ✅ 호환 | React 프레임워크이므로 문제 없음 |
| Radix UI ↔ React Aria | ⚠️ 공존 가능 | 접근성 라이브러리가 다름, 혼용 시 번들 증가 |
| shadcn/ui 기존 코드 | ⚠️ 점진적 교체 필요 | 612개 컴포넌트가 이미 구축됨 |
---
## 4. 도입 시 장단점 분석
### 4.1 장점
| 항목 | 설명 |
|------|------|
| 디자인 품질 | 전문 디자이너가 설계한 고품질 UI 패턴 |
| 디자인-개발 일관성 | Figma 시안 → React 코드 변환이 1:1 대응 |
| 시간 절약 | 새 화면 개발 시 프리셋 활용으로 속도 향상 |
| 접근성 | React Aria 기반의 체계적 접근성 지원 |
| 아이콘 | 4,600+ 다양한 스타일의 아이콘 |
| 업데이트 | 1회 결제로 평생 업데이트 |
### 4.2 단점
| 항목 | 설명 |
|------|------|
| 기존 코드 충돌 | shadcn/ui 612개 컴포넌트와 역할 중복 |
| 학습 비용 | 새 컴포넌트 API 학습 필요 |
| 번들 크기 | Radix UI + React Aria 공존 시 증가 |
| 마이그레이션 부담 | 전면 교체 시 비용 큼 |
| 커스텀 한계 | SAM 특화 컴포넌트(한국형 입력 등)는 자체 유지 필요 |
---
## 5. 추천 방안
### 5.1 방안 A: Figma만 구매 ($129) — 추천
```
디자이너 → Figma Solo ($129) 구매 → 시안 제작
개발자 → 기존 shadcn/ui 컴포넌트로 구현 (변경 없음)
```
**적합한 경우**:
- 디자이너가 합류하여 체계적 시안이 필요할 때
- 현재 React 코드를 유지하면서 디자인 품질만 높이고 싶을 때
- 가장 저렴하고 리스크 없는 방법
### 5.2 방안 B: Figma + React 모두 구매 ($478)
```
디자이너 → Figma Solo ($129) → 시안 제작
개발자 → React Solo ($349) → 새 화면에 Untitled UI 컴포넌트 사용
```
**적합한 경우**:
- 신규 프로젝트(MES 등)를 처음부터 Untitled UI로 시작할 때
- 기존 SAM 코드는 shadcn/ui 유지, 신규 코드만 Untitled UI 적용
### 5.3 방안 C: React만 구매 ($349)
```
개발자 → React Solo ($349) → 참고용 + 부분 적용
디자이너 → Figma Free 버전으로 참고
```
**적합한 경우**:
- 디자이너 없이 개발자가 직접 UI를 구현할 때
- 고급 컴포넌트 패턴을 참고 자료로 활용할 때
### 5.4 권장 결론
> **현재 상황**: 디자이너 부재, 612개 컴포넌트 이미 구축, SAM 기능 개발 진행 중
**단기 (현재)**: 도입 보류 — 기존 shadcn/ui 시스템이 잘 작동 중이며, 249 페이지가 이미 구축됨
**중기 (디자이너 합류 시)**: 방안 A (Figma $129) — 디자인 시안 품질 향상
**장기 (MES 등 신규 프로젝트)**: 방안 B ($478) — 신규 코드에 Untitled UI 적용 검토
---
## 6. SAM 내 디자인 관련 기존 자료
| 문서 | 경로 | 내용 |
|------|------|------|
| React 컴포넌트 아키텍처 | `system/react-component-architecture.md` | Atomic Design, shadcn/ui 60개, 의존성 분석 |
| React 프로젝트 구조 | `system/react-structure.md` | 249페이지, 612 컴포넌트 현황 |
| MES Atomic Design | `projects/mes/00_baseline/docs_breakdown/react_atomic_design_summary.md` | 100종 UI 패턴, 4단계 마이그레이션 |
| 디자인 인사이트 | `features/rd/design-insight.md` | UI/UX 패턴 수집, 100종 CSS 와이어프레임 |
| 기획디자인 에디터 | `projects/planning-design/README.md` | Figma 대체 목적 자체 스토리보드 에디터 |
---
**최종 업데이트**: 2026-03-12
# Untitled UI 도입 검토
> **작성일**: 2026-03-12
> **상태**: 검토 중
> **작성자**: 개발팀장
---
## 1. 개요
### 1.1 목적
외부 UI 컴포넌트 라이브러리 **Untitled UI** 도입 여부를 검토한다. SAM 프로젝트의 현재 디자인 시스템(shadcn/ui + Tailwind CSS)과의 호환성, 비용 대비 효과, 도입 방안을 분석한다.
### 1.2 배경
SAM React 프론트엔드는 현재 shadcn/ui 60개 컴포넌트 + Atomic Design 구조로 운영 중이다. 디자인 품질 향상과 개발 속도 개선을 위해 상용 UI Kit 도입을 검토한다.
---
## 2. Untitled UI 소개
### 2.1 제품 개요
Untitled UI는 세계 최대 규모의 Figma UI Kit + React 컴포넌트 라이브러리이다. Atomic Design 시스템으로 유명하며, 디자이너와 개발자 모두를 위한 도구를 제공한다.
- **공식 사이트**: https://www.untitledui.com
- **핵심 특징**: Atomic Design 기반, 접근성(Accessibility) 준수, 반응형 지원
### 2.2 제품 구성 및 가격
| 제품 | 설명 | Free | Solo (1인) | Team (5인) |
|------|------|------|-----------|-----------|
| **Figma** | UI Kit / 디자인 시스템 | $0 | $129 | $359 |
| **React** | React + Tailwind CSS + React Aria 컴포넌트 | $0 | $349 | $699 |
| **Icons** | 4,600+ 아이콘 (다중 스타일) | $0 | $59 | $109 |
> **결제 방식**: 1회 결제 (one-time payment), 평생 업데이트 포함, 무제한 프로젝트 사용 가능
### 2.3 라이선스 구조
- **Solo**: 1명 사용, 무제한 프로젝트
- **Team**: 5명 사용, 무제한 프로젝트
- 디자이너와 개발자가 **다른 제품**을 각각 Solo로 구매하면 Team 불필요
---
## 3. SAM 현재 디자인 시스템과 비교
### 3.1 SAM 현재 스택
| 항목 | 현재 (SAM) | Untitled UI React |
|------|-----------|-------------------|
| UI 프리미티브 | shadcn/ui (60개) | Untitled UI 컴포넌트 |
| 스타일링 | Tailwind CSS v4 | Tailwind CSS |
| 접근성 | Radix UI | React Aria |
| 폼 | React Hook Form + Zod | (별도) |
| 아이콘 | Lucide React (550+) | 4,600+ (유료) |
| 테마 | CSS 변수 (light/dark/senior) | CSS 변수 기반 |
### 3.2 SAM 프론트엔드 현황 (참고)
- **페이지**: 249개
- **컴포넌트**: 612개 (shadcn/ui 기반)
- **핵심 템플릿**: `UniversalListPage` (214회), `IntegratedDetailTemplate` (182회)
- **고빈도 molecules**: `FormField` (216회), `StatusBadge` (125회)
- **프레임워크**: Next.js 15 + React 19
> 상세 현황: `docs/system/react-component-architecture.md` 참조
### 3.3 기술 호환성
| 항목 | 호환 여부 | 비고 |
|------|----------|------|
| Tailwind CSS | ✅ 호환 | 동일 스타일링 프레임워크 |
| React 19 | ✅ 호환 | React 기반 |
| Next.js 15 | ✅ 호환 | React 프레임워크이므로 문제 없음 |
| Radix UI ↔ React Aria | ⚠️ 공존 가능 | 접근성 라이브러리가 다름, 혼용 시 번들 증가 |
| shadcn/ui 기존 코드 | ⚠️ 점진적 교체 필요 | 612개 컴포넌트가 이미 구축됨 |
---
## 4. 도입 시 장단점 분석
### 4.1 장점
| 항목 | 설명 |
|------|------|
| 디자인 품질 | 전문 디자이너가 설계한 고품질 UI 패턴 |
| 디자인-개발 일관성 | Figma 시안 → React 코드 변환이 1:1 대응 |
| 시간 절약 | 새 화면 개발 시 프리셋 활용으로 속도 향상 |
| 접근성 | React Aria 기반의 체계적 접근성 지원 |
| 아이콘 | 4,600+ 다양한 스타일의 아이콘 |
| 업데이트 | 1회 결제로 평생 업데이트 |
### 4.2 단점
| 항목 | 설명 |
|------|------|
| 기존 코드 충돌 | shadcn/ui 612개 컴포넌트와 역할 중복 |
| 학습 비용 | 새 컴포넌트 API 학습 필요 |
| 번들 크기 | Radix UI + React Aria 공존 시 증가 |
| 마이그레이션 부담 | 전면 교체 시 비용 큼 |
| 커스텀 한계 | SAM 특화 컴포넌트(한국형 입력 등)는 자체 유지 필요 |
---
## 5. 추천 방안
### 5.1 방안 A: Figma만 구매 ($129) — 추천
```
디자이너 → Figma Solo ($129) 구매 → 시안 제작
개발자 → 기존 shadcn/ui 컴포넌트로 구현 (변경 없음)
```
**적합한 경우**:
- 디자이너가 합류하여 체계적 시안이 필요할 때
- 현재 React 코드를 유지하면서 디자인 품질만 높이고 싶을 때
- 가장 저렴하고 리스크 없는 방법
### 5.2 방안 B: Figma + React 모두 구매 ($478)
```
디자이너 → Figma Solo ($129) → 시안 제작
개발자 → React Solo ($349) → 새 화면에 Untitled UI 컴포넌트 사용
```
**적합한 경우**:
- 신규 프로젝트(MES 등)를 처음부터 Untitled UI로 시작할 때
- 기존 SAM 코드는 shadcn/ui 유지, 신규 코드만 Untitled UI 적용
### 5.3 방안 C: React만 구매 ($349)
```
개발자 → React Solo ($349) → 참고용 + 부분 적용
디자이너 → Figma Free 버전으로 참고
```
**적합한 경우**:
- 디자이너 없이 개발자가 직접 UI를 구현할 때
- 고급 컴포넌트 패턴을 참고 자료로 활용할 때
### 5.4 권장 결론
> **현재 상황**: 디자이너 부재, 612개 컴포넌트 이미 구축, SAM 기능 개발 진행 중
**단기 (현재)**: 도입 보류 — 기존 shadcn/ui 시스템이 잘 작동 중이며, 249 페이지가 이미 구축됨
**중기 (디자이너 합류 시)**: 방안 A (Figma $129) — 디자인 시안 품질 향상
**장기 (MES 등 신규 프로젝트)**: 방안 B ($478) — 신규 코드에 Untitled UI 적용 검토
---
## 6. SAM 내 디자인 관련 기존 자료
| 문서 | 경로 | 내용 |
|------|------|------|
| React 컴포넌트 아키텍처 | `system/react-component-architecture.md` | Atomic Design, shadcn/ui 60개, 의존성 분석 |
| React 프로젝트 구조 | `system/react-structure.md` | 249페이지, 612 컴포넌트 현황 |
| MES Atomic Design | `projects/mes/00_baseline/docs_breakdown/react_atomic_design_summary.md` | 100종 UI 패턴, 4단계 마이그레이션 |
| 디자인 인사이트 | `features/rd/design-insight.md` | UI/UX 패턴 수집, 100종 CSS 와이어프레임 |
| 기획디자인 에디터 | `projects/planning-design/README.md` | Figma 대체 목적 자체 스토리보드 에디터 |
---
**최종 업데이트**: 2026-03-12