diff --git a/.gitignore b/.gitignore index 9109c08..bbbc6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # 추적할 파일만 허용 !.gitignore !CLAUDE.md +!INDEX.md +!README.md +!resources.md # .claude 폴더 - 스킬/에이전트는 추적 !.claude/ @@ -14,22 +17,37 @@ !.claude/agents/ !.claude/agents/** -# sam 문서 -!sam/ -sam/* -!sam/docs/ -!sam/docs/** -sam/docs/contracts/docx/backup/ - -# sam 배포/운영 문서 -!sam/deploys/ -!sam/deploys/** -!sam/front/ -!sam/front/** -!sam/projects/ -!sam/projects/** +# 문서 폴더 (루트 기준) +!assets/ +!assets/** +!brochure/ +!brochure/** +!changes/ +!changes/** +!contracts/ +!contracts/** +contracts/docx/backup/ +!data/ +!data/** +!dev/ +!dev/** +!features/ +!features/** +!frontend/ +!frontend/** +!guides/ +!guides/** +!plans/ +!plans/** +!projects/ +!projects/** +!requests/ +!requests/** +!rules/ +!rules/** +!system/ +!system/** # 기타 -sam/sales .DS_Store _to_notion/ diff --git a/changes/20260303_gemini_model_upgrade.md b/changes/20260303_gemini_model_upgrade.md new file mode 100644 index 0000000..e3806fc --- /dev/null +++ b/changes/20260303_gemini_model_upgrade.md @@ -0,0 +1,119 @@ +# Gemini 모델 업그레이드: 2.0-flash → 2.5-flash + +**날짜:** 2026-03-03 +**작업자:** Claude Code + +--- + +## 변경 개요 + +Google이 2026년 6월 1일부로 Gemini 2.0 Flash 모델 서비스를 종료한다는 통보를 받아, SAM 시스템 전체의 Gemini 모델을 `gemini-2.0-flash` → `gemini-2.5-flash`로 마이그레이션했다. + +--- + +## 변경 사유 + +- Google의 공식 메일 통보: Gemini 2.0 Flash / 2.0 Flash-Lite → 2026-06-01 강제 종료 +- 마이그레이션 경로: `gemini-2.0-flash` → `gemini-2.5-flash` +- API 키, Base URL 변경 없음 (모델명만 변경) + +--- + +## 수정된 파일 + +### API 프로젝트 (`/home/aweso/sam/api`) + +| 파일 | 변경 내용 | +|------|----------| +| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | +| `config/services.php` | fallback 기본값 `gemini-2.0-flash` → `gemini-2.5-flash` | +| `app/Services/AiReportService.php` | fallback 기본값 변경 | + +### MNG 프로젝트 (`/home/aweso/sam/mng`) + +| 파일 | 변경 내용 | +|------|----------| +| `.env` | `GEMINI_MODEL=gemini-2.0-flash` → `gemini-2.5-flash` | +| `config/services.php` | fallback 기본값 변경 | +| `app/Models/System/AiConfig.php` | `DEFAULT_MODELS['gemini']` 상수 + `getActiveGemini()` fallback 변경 | +| `app/Services/NotionService.php` | fallback 기본값 변경 | +| `resources/views/system/ai-config/index.blade.php` | UI placeholder, 기본값, JS defaultModels 변경 | +| `resources/views/google-cloud/ai-guide/index.blade.php` | 서비스 현황 테이블 모델명 7곳 변경 | +| `resources/views/academy/env-management.blade.php` | 환경변수 예시 테이블 변경 | + +### 문서 (`/home/aweso/sam/docs`) + +| 파일 | 변경 내용 | +|------|----------| +| `guides/ai-config-settings.md` | 기본 모델명 업데이트, 최종 업데이트 날짜 변경 | +| `guides/ai-management.md` | **신규** — AI 관리 종합 가이드 (아키텍처, 버전 이력, 온보딩) | +| `guides/ai-model-update-workflow.md` | **신규** — 모델 업데이트 표준 절차 (7단계 워크플로우) | +| `changes/20260303_gemini_model_upgrade.md` | **신규** — 이 변경 이력 문서 | + +### 수정하지 않은 파일 (의도적) + +| 파일 | 이유 | +|------|------| +| `api/database/migrations/2026_01_27_*.php` | 이미 실행된 마이그레이션 — 변경 시 DB 무결성 문제 | +| `api/database/migrations/2026_02_07_*.php` | 동일 | +| `api/database/migrations/2026_02_09_*.php` | 동일 | +| `mng/views/google-cloud/cloud-api-pricing/index.blade.php` | `2.0 → 2.5` 마이그레이션 안내 UI — 이전 모델명이 의도적 잔존 | + +--- + +## 서버 .env 수정 필요 (배포 후) + +| 환경 | 파일 | 변수 | 담당 | +|------|------|------|------| +| 개발서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | +| 개발서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | SSH 접속 수정 | +| 운영서버 | `/home/webservice/api/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | +| 운영서버 | `/home/webservice/mng/.env` | `GEMINI_MODEL=gemini-2.5-flash` | 개발팀장 직접 | + +수정 후 반드시 실행: +```bash +php artisan config:clear +``` + +--- + +## DB 단가 설정 필요 + +MNG `/system/ai-token-usage` → 단가 설정에서: +- 기존 `gemini-2.0-flash` 단가 → 비활성화 +- 신규 `gemini-2.5-flash` 단가 추가: + - `input_price_per_million`: 0.15 + - `output_price_per_million`: 0.60 + - `exchange_rate`: 현재 환율 + +--- + +## 테스트 체크리스트 + +- [x] 로컬 .env 수정 완료 +- [x] 코드 fallback 전체 변경 완료 +- [ ] 로컬 연결 테스트 (MNG `/system/ai-config`) +- [ ] 개발서버 .env 수정 + config:clear +- [ ] 개발서버 연결 테스트 +- [ ] 운영서버 .env 수정 + config:clear +- [ ] DB 단가 설정 (gemini-2.5-flash) +- [ ] 토큰 사용량 로그 확인 (새 모델명) + +--- + +## 롤백 절차 + +문제 발생 시 `.env`만 되돌리면 즉시 복구: +```bash +# 모든 환경의 .env에서 +GEMINI_MODEL=gemini-2.0-flash +php artisan config:clear +``` + +--- + +## 관련 문서 + +- [AI 관리 종합 가이드](../guides/ai-management.md) +- [모델 업데이트 워크플로우](../guides/ai-model-update-workflow.md) +- [AI 설정 기술문서](../guides/ai-config-settings.md) diff --git a/changes/20260304_eaccount_infinite_loop_fix.md b/changes/20260304_eaccount_infinite_loop_fix.md new file mode 100644 index 0000000..e754f9f --- /dev/null +++ b/changes/20260304_eaccount_infinite_loop_fix.md @@ -0,0 +1,165 @@ +# 계좌 입출금내역 부분 월 조회 시 무한루프 크래시 수정 + +**날짜:** 2026-03-04 +**작업자:** Claude Code + +--- + +## 변경 개요 + +계좌 입출금내역 페이지에서 **날짜를 수동 입력**하여 조회 시 500 에러가 발생하는 문제를 수정했다. +편의 버튼(이번달, 지난달 등)은 항상 전체 월(1일~말일)을 사용하여 문제가 없었으나, +수동으로 날짜를 입력하면 **부분 월**(예: 12/01~12/18)이 되어 무한루프가 발생했다. + +--- + +## 근본 원인 + +### `splitDateRangeMonthly()` 함수의 cursor 이동 버그 + +긴 기간 조회 시 바로빌 SOAP API의 한계로 인해 기간을 **월별 청크**로 분할하는 함수에서, +endDate가 **월 중간**일 때 cursor가 **같은 달 1일로 되돌아가** 무한루프가 발생했다. + +```php +// ❌ 버그 코드 — endDate가 월 중간이면 무한루프 +$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); + +// 예시: endDate = 20251218 +// chunkEnd = 20251218 +// → addDay() = 20251219 +// → startOfMonth() = 20251201 ← 같은 달 1일로 되돌아감! +// → while($cursor <= $end) 조건 여전히 true → 무한 반복 +``` + +```php +// ✅ 수정 코드 — chunkStart 기준으로 다음 월로 이동 +$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); + +// 예시: startDate = 20251201 +// chunkStart = 20251201 +// → addMonth() = 20260101 +// → startOfMonth() = 20260101 ← 다음 달로 정상 이동 +// → while($cursor <= $end) 조건 false → 루프 종료 +``` + +### 재현 조건 + +| 조건 | 결과 | +|------|------| +| 전체 월 (12/01~12/31) | 정상 — `addDay()` = 01/01 → `startOfMonth()` = 01/01 | +| 부분 월 (12/01~12/18) | **무한루프** — `addDay()` = 12/19 → `startOfMonth()` = 12/01 | +| 다중 월 (12/01~02/18) | **무한루프** — 마지막 월이 부분 월이면 동일 증상 | + +### 증상 + +- PHP 프로세스가 메모리 한도(256M/512M)에 도달하여 **Fatal Error로 크래시** +- Laravel 로그에 에러 기록 없음 (try-catch 밖에서 프로세스가 종료) +- 프론트엔드에 `서버 응답 오류 (500):` (빈 응답 본문) + +--- + +## 수정된 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `app/Http/Controllers/Barobill/EaccountController.php` | `splitDateRangeMonthly()` cursor 이동 로직 수정 | + +--- + +## 검증 결과 + +tinker에서 수정 전후 비교 테스트: + +``` +=== 수정 전 (버그): 20251201~20251218 === +→ 같은 청크 무한 반복 (10회 제한으로 강제 중단) + +=== 수정 후: 20251201~20251218 === +→ [{start: 20251201, end: 20251218}] ← 1개 청크, 정상 + +=== 수정 후: 20251201~20260218 (다중 월) === +→ [{20251201~20251231}, {20260101~20260131}, {20260201~20260218}] ← 3개 청크, 정상 + +=== 수정 후: 20251215~20251231 === +→ [{start: 20251215, end: 20251231}] ← 1개 청크, 정상 +``` + +--- + +## 동일 패턴 코드베이스 점검 결과 + +`sam/mng` 전체를 검색하여 유사 패턴을 점검했다: + +| 파일 | 함수 | 패턴 | 위험도 | +|------|------|------|--------| +| `EaccountController.php` | `splitDateRangeMonthly()` | 월별 청크 분할 | ✅ 수정 완료 | +| `DashboardStatService.php` | `generateDateRange()` | `addDay()` 단순 증가 | 안전 | +| `InspectionCycle.php` | `getHolidayDates()` | `addDay()` 단순 증가 | 안전 | +| `CorporateCardController.php` | `getNextBusinessDay()` | `addDay()` 단순 증가 | 안전 | +| `PartitionManagementService.php` | `addPartitions()` | `for` 루프 (고정 횟수) | 안전 | + +> **결론**: `EaccountController` 외에 동일 버그 패턴 없음. +> 다른 코드들은 모두 `addDay()` 단순 증가 패턴을 사용하여 무한루프 위험 없음. + +--- + +## 교훈 및 방지 규칙 + +### R1. 날짜 cursor 이동 시 `chunkEnd` 기반 이동 금지 + +```php +// ❌ 위험: chunkEnd가 월 중간이면 startOfMonth()가 같은 달로 되돌림 +$cursor = $chunkEnd->copy()->addDay()->startOfMonth(); + +// ✅ 안전: chunkStart 기준으로 항상 다음 월로 이동 +$cursor = $chunkStart->copy()->addMonth()->startOfMonth(); +``` + +### R2. 날짜 루프에 안전장치(max iterations) 추가 권장 + +```php +$maxIterations = 120; // 10년 = 120개월 +$iterations = 0; + +while ($cursor->lte($end) && $iterations < $maxIterations) { + // ... 청크 처리 ... + $iterations++; +} + +if ($iterations >= $maxIterations) { + Log::error('날짜 분할 루프 안전장치 작동', compact('startDate', 'endDate')); +} +``` + +### R3. 부분 월 테스트 필수 + +날짜 범위를 분할하는 코드 작성/수정 시 반드시 다음 케이스를 테스트: + +- [ ] 전체 월 (01일~말일) +- [ ] 부분 월 — 시작 (01일~중간) +- [ ] 부분 월 — 끝 (중간~말일) +- [ ] 다중 월 (마지막 월이 부분 월) +- [ ] 같은 날 (시작일 = 종료일) + +--- + +## 부수 개선 사항 + +이 문제 조사 과정에서 추가로 발견/수정된 항목: + +| 항목 | 내용 | +|------|------| +| WSDL 캐싱 | `WSDL_CACHE_NONE` → `WSDL_CACHE_BOTH` (4개 바로빌 컨트롤러 전체) | +| 소켓 타임아웃 | `default_socket_timeout` 60→120초 연장 | +| Shutdown handler | PHP Fatal Error 감지 시 Laravel 로그에 기록 | +| SOAP 호출 로깅 | 호출 시작/완료 시간 + 소요시간(ms) 기록 | + +--- + +## 관련 문서 + +- `app/Http/Controllers/Barobill/EaccountController.php` — 바로빌 계좌 입출금내역 + +--- + +**최종 업데이트**: 2026-03-04 diff --git a/sam/docs/dev/changes/20260306_purchase_request_payment_method.md b/changes/20260306_purchase_request_payment_method.md similarity index 100% rename from sam/docs/dev/changes/20260306_purchase_request_payment_method.md rename to changes/20260306_purchase_request_payment_method.md diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md new file mode 100644 index 0000000..cfd3346 --- /dev/null +++ b/contracts/CHANGELOG.md @@ -0,0 +1,42 @@ +# 계약서 개정이력 + +> **작성일**: 2026-02-22 +> **관리 대상**: 전자계약 DOCX 4종 + +--- + +## v4.1 (2026-02-22) + +**작성자**: 개발팀 +**대상**: 고객사 서비스 이용계약서 + +- 제4조에 사용량 기반 추가 과금 조항(4.5) 추가 + - 파일 저장 공간: 기본 100GB 초과 시 100GB당 100,000원/월 + - AI 토큰: 월 100만 토큰 기본, 초과 시 1,000토큰 단위 실비 과금 +- 제4조에 바로빌 부가 서비스 요금 조항(4.6) 추가 + - 계좌조회, 카드내역, 세금계산서 발행 요금 명시 + - 홈택스 매입/매출 조회는 회사 부담 명시 + +--- + +## v4.0 (2026-02-22) + +**작성자**: 개발팀 + +- 계약서 버전 관리 시스템 도입 +- DOCX → Markdown 미러링 체계 구축 +- 4개 전자계약 문서에 개정이력 테이블 삽입 +- 동기화 검증 스크립트 구축 + +### 대상 문서 + +| 파일 | 문서명 | +|------|--------| +| `01_고객_서비스이용계약서_v4_0_전자서명용.docx` | 고객사 서비스 이용계약서 | +| `비밀유지서약서.docx` | 비밀유지서약서 (NDA) | +| `영업파트너 위촉계약서.docx` | 영업파트너 위촉계약서 | +| `영업파트너 위촉계약서(단체용).docx` | 영업파트너 위촉계약서 (단체용) | + +--- + +**최종 업데이트**: 2026-02-22 (v4.1) diff --git a/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx b/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx new file mode 100644 index 0000000..de1e3ed Binary files /dev/null and b/contracts/docx/01_고객_서비스이용계약서_v4_0_전자서명용.docx differ diff --git a/contracts/docx/비밀유지서약서.docx b/contracts/docx/비밀유지서약서.docx new file mode 100755 index 0000000..635b42c Binary files /dev/null and b/contracts/docx/비밀유지서약서.docx differ diff --git a/contracts/docx/영업파트너 위촉계약서(단체용).docx b/contracts/docx/영업파트너 위촉계약서(단체용).docx new file mode 100755 index 0000000..1332c74 Binary files /dev/null and b/contracts/docx/영업파트너 위촉계약서(단체용).docx differ diff --git a/contracts/docx/영업파트너 위촉계약서.docx b/contracts/docx/영업파트너 위촉계약서.docx new file mode 100755 index 0000000..9e849b5 Binary files /dev/null and b/contracts/docx/영업파트너 위촉계약서.docx differ diff --git a/contracts/markdown/01-service-agreement.md b/contracts/markdown/01-service-agreement.md new file mode 100644 index 0000000..51c694c --- /dev/null +++ b/contracts/markdown/01-service-agreement.md @@ -0,0 +1,458 @@ +--- +title: "고객사 서비스 이용계약서" +version: "v4.2" +date: "2026-02-24" +docx_file: "01_고객_서비스이용계약서_v4_0_전자서명용.docx" +--- + +# 고객사 서비스 이용계약서 + +Customer Service Agreement + +계약번호: +계약일: + +본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 간에 SAM 서비스 제공과 관련하여 다음과 같이 계약을 체결합니다. + +## 제1조 (계약의 목적) + +본 계약은 회사가 고객에게 SAM(Smart MES/ERP Solution) 서비스를 제공함에 있어 필요한 사항을 규정하고, 양측의 권리와 의무를 명확히 함을 목적으로 합니다. + +## 제2조 (용어의 정의) + +- **서비스**: 회사가 제공하는 SAM 클라우드 기반 MES/ERP 솔루션 +- **SaaS**: Software as a Service (서비스형 소프트웨어) +- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 +- **액세스 제공**: 고객에게 서비스 사용 권한을 부여하는 것 +- **검수 기간**: 서비스 게시 전 고객이 완성도를 확인하는 기간 (최대 1개월) +- **하자**: 계약서에 명시된 기능의 오류, 미구현, 성능 미달 등 +- **하자담보 책임**: 서비스 게시 후 1년간 하자를 무상으로 수정하는 의무 + +## 제3조 (서비스 내용) + +### 3.1 서비스 범위 + +회사는 다음의 서비스를 제공합니다: +- **맞춤형 개발**: + - 고객 요구사항에 맞춘 SAM 시스템 개발 + - 개발 범위: [별첨 기획서 참조] + - 개발 기간: 계약일로부터 [ 3 ]개월 +- **클라우드 제공** (SaaS): + - 연중무휴 24시간 접근 가능 + - 자동 백업 및 보안 +- **기술 지원**: + - 고객센터 운영 (평일 09:00~18:00) + - 이메일 지원 (24시간) + - 긴급 장애 대응 +- **하자담보 책임** (1년): + - 서비스 게시일로부터 1년간 무상 수정 + - 버그, 미구현 기능, 성능 개선 등 + +### 3.2 제공 방식 + +- 회사는 서비스를 **SaaS 방식**으로 제공합니다. +- 고객은 서비스에 대한 **사용 권한**만을 부여받으며, 소유권은 회사에 귀속됩니다. + - 소스코드는 제공되지 않습니다. + +## 제4조 (비용 및 납부) + +### 4.1 개발비 + +| 구분 | 금액 (부가세 별도) | 지급 시기 | 비고 | +| --- | --- | --- | --- | +| 1차 개발비 | 총 개발비의 50% | 계약 체결 시 | 착수금 | +| 2차 개발비 | 총 개발비의 50% | 서비스 게시일로부터 3일 이내 | 잔금 | +| 총 개발비 | [ ]원 | | | + +### 4.2 월 구독료 + +| 구분 | 금액 (부가세 별도) | 지급 시기 | 비고 | +| --- | --- | --- | --- | +| 월 구독료 | 원 ~ | 매월 말일 | 후불제, 사용량 기준 청구 | + +> ⚠️ 중요: - 월 구독료는 원이며, 영업 협상 및 개발 범위에 따라 증액될 수 있습니다. + +- 계약 시 확정된 구독료: [ ]원/월 + +### 4.3 납부 방법 + +- **개발비**: + - 계좌이체 (세금계산서 발행) + - 입금 계좌: 기업은행 170-175519-04-011  (주)코드브릿지엑스 +- **구독료**: + - CMS 자동이체 (권장) + - 또는 세금계산서 발행 후 계좌이체 + +### 4.4 잔금 지급 기한 [법률 검토 반영] + +- **지급 기한**: 서비스 게시일로부터 **3일 이내** +- **사전 준비**: 회사는 영업 단계부터 납품 일정을 공유하여 고객이 미리 준비할 수 있도록 합니다. +- **미납 시 조치**: 제13조 참조 + +### 4.5 사용량 기반 추가 과금 + +기본 제공 한도 초과 시 다음과 같이 실비 과금됩니다. + +| 항목 | 기본 제공 | 추가 과금 기준 | +| --- | --- | --- | +| 파일 저장 공간 | 100GB | 100GB당 100,000원/월 (부가세 별도) | +| AI 토큰 | 월 100만 토큰 | 1,000토큰 단위 실비 과금 | + +- **파일 저장 공간: **기본 100GB를 초과하는 경우 100GB 단위로 월 100,000원(부가세 별도)이 추가 과금됩니다. +- **AI 토큰: **월 100만 토큰 기본 제공되며, 초과 사용 시 1,000토큰 단위로 실비 과금됩니다. + - 미사용 잔여 토큰은 이월되지 않습니다. (매월 1일 갱신) + - 기본 제공량 80%, 100% 소진 시 자동 알림이 발송됩니다. + +### 4.6 바로빌 부가 서비스 요금 + +고객이 선택적으로 이용하는 바로빌 연동 서비스의 요금은 다음과 같습니다. + +| 서비스 | 과금 방식 | 기본 제공 | 추가 과금 | +| --- | --- | --- | --- | +| 계좌조회 | 월정액 10,000원 | 1계좌 | 추가 1계좌당 10,000원 | +| 카드내역 | 월정액 10,000원 | 5장 | 추가 1장당 5,000원 | +| 세금계산서 발행 | 건별 | 100건 | 추가 50건당 5,000원 | + +- **바로빌 서비스 요금은 고객이 부담하며, 월 구독료와 별도로 청구됩니다.** + - 홈택스 매입/매출 조회 서비스(월 30,000원)는 회사가 부담합니다. + - 상기 금액은 부가세 별도입니다. + +## 제5조 (마일스톤 및 진행 일정) + +### 5.1 개발 단계 (5단계 통일) + +| 단계 | 주요 활동 | 진행률 | 기간 | 납부 | +| --- | --- | --- | --- | --- | +| M1 | 요구사항 분석 및 기획 | 20% | [ 2 ]주 | 1차 개발비 (착수금 50%) | +| M2 | 설계 및 개발 착수 | 50% | [ 2 ]주 | - | +| M3 | 개발 진행 (50% 완료) | 60% | [ 2 ]주 | - | +| M4 | 개발 완료 및 테스트 | 80% | [ 2 ]주 | - | +| M5 | 검수 및 서비스 게시 | 100% | 최대 2주 | 2차 개발비 (잔금 50%) | + +> ⚠️ 중요: - 5단계 마일스톤으로 통일 관리 - M5 검수 완료 후 서비스 게시 - 서비스 게시일로부터 3일 이내 잔금 납부 + +### 5.2 일정 조정 + + - 개발 일정은 고객의 협조에 따라 변동될 수 있습니다. + - 고객 귀책 사유로 인한 지연은 회사의 책임이 아닙니다. + - 불가항력으로 인한 지연 시 양측 협의하여 일정을 조정합니다. + +## 제6조 (서비스 게시 및 검수) + +### 6.1 서비스 게시 + +- 회사는 개발 완료 후 고객에게 **서비스 게시**를 통지합니다. +- **서비스 게시일**은 고객이 서비스에 접근 가능한 날짜를 의미합니다. + - 서비스 게시일부터 구독료가 발생합니다. + +### 6.2 검수 기간 + +- 고객은 개발 완료 후 **최대 2주간 검수 기간**을 가집니다. +- 검수 기간은 서비스 게시 **전**에 이루어집니다. + - 검수 기간 중 발견된 하자는 회사가 무상으로 수정합니다. + +### 6.3 검수 완료 + + - 고객이 서면으로 검수 완료를 통지하거나, + - 검수 기간 2주 종료 시점에 특별한 이의가 없으면 자동 승인으로 간주합니다. + - 검수 완료 후 서비스 게시일이 확정되고, 하자담보 책임 정책이 적용됩니다. + +## 제7조 (하자담보 책임) + +### 7.1 책임 기간 + +서비스 게시일로부터 1년 (소프트웨어산업진흥법 제16조, 민법 제667조) + +### 7.2 하자담보 범위 (무상 처리) + +| 항목 | 내용 | 예시 | +| --- | --- | --- | +| 버그 수정 | 소프트웨어 오류 | 계산 오류, 기능 미작동 | +| 미구현 기능 | 계약서에 명시된 기능 누락 | 약속된 기능 미구현 | +| 성능 개선 | 명시된 성능 기준 미달 | 속도 저하, 응답 지연 | +| UI/UX 수정 | 사용성 문제 | 버튼 미작동, 화면 깨짐 | +| 데이터 오류 | 데이터 손실 또는 오류 | 데이터 삭제, 중복 생성 | +| 보안 패치 | 보안 취약점 수정 | 해킹 방지, 암호화 | + +### 7.3 제외 사항 (별도 비용) + +| 항목 | 내용 | 예시 | +| --- | --- | --- | +| 신규 기능 개발 | 계약서에 없던 새 기능 | 새로운 모듈, 기능 확장 | +| 구조 변경 | 시스템 아키텍처 변경 | DB 구조, 프레임워크 교체 | +| 추가 모듈 | 새로운 모듈 개발 | 회계 모듈, 재고 모듈 | +| 기획 변경 | 초기 기획과 다른 요구사항 | 화면 구성, 프로세스 변경 | +| 교육/컨설팅 | 사용자 교육, 업무 컨설팅 | 직원 교육, 프로세스 개선 | + +### 7.4 하자 처리 절차 + +| 단계 | 내용 | 기간 | +| --- | --- | --- | +| 1. 하자 신고 | 고객이 이메일로 하자 신고 | - | +| 2. 하자 확인 | 회사가 하자 여부 판정 | 3영업일 | +| 3. 수정 작업 | 하자 인정 시 무상 수정 | 7영업일 | +| 4. 검수 완료 | 고객이 수정 사항 확인 | - | + +> ⚠️ 긴급 하자 (서비스 중단)는 24시간 이내 조치합니다. + +### 7.5 책임 면제 사유 + +다음의 경우 하자담보 책임이 면제됩니다: +- **고객 귀책 사유**: + - 고객의 임의 수정 또는 변경 + - 승인되지 않은 제3자 개입 + - 사용 환경 미준수 +- **불가항력**: + - 천재지변 (지진, 태풍 등) + - 전쟁, 테러, 전염병 + - 정부 규제 또는 법령 변경 +- **기간 만료**: + - 서비스 게시일로부터 1년 경과 + +## 제8조 (계약 해제 및 환불) + +### 8.1 환불 정책 개요 + +고객의 임의 해제 권리와 회사의 투입 비용 보전의 균형을 고려하여 수립되었습니다. + +### 8.2 단계별 환불 + +### Phase 1: 상담(인터뷰) 시작 전 + +- **환불율**: 100% (전액 환불) +- **조건**: 계약 후 상담(인터뷰) 배정 전 +- **위약금**: 없음 +- **임의 해제 가능** + +### Phase 2: 상담(인터뷰) 시작 후, 개발 착수 전 + +| 진행 상황 | 환불율 | 공제 내역 | +| --- | --- | --- | +| M1: 기획안 작성 중 (50% 미만) | 80% | 상담매니저 및 기획/개발자 투입 비용 20% 공제 | +| M2: 기획안 완료 (50% 이상) | 50% | 상담매니저 및 기획/개발자 투입 비용 50% 공제 | + +### Phase 3: 개발 진행 중 (5단계 마일스톤 기준) + +| 마일스톤 | 진행률 | 청구 금액(개발비 대비) | 비고 | +| --- | --- | --- | --- | +| M3: 개발 진행 중 (50%) | 70% | 70% | 30% 환불 | +| M4: 개발 완료 및 테스트 | 90% | 90% | 10% 환불 | +| M5: 서비스 개시 완료 | 100% | 100% | 환불 불가 | + +> ⚠️ 중요: 5단계 마일스톤으로 통일 관리 + +### Phase 4: 서비스 게시 후 + +- **환불율**: 0% (환불 불가) +- **개발비**: 전액 확정, 환불 불가 +- **구독료**: 매월 말일 후불제이므로 사용한 만큼만 청구 (환불 개념 없음) +- **대신 제공**: 하자담보 책임 (1년) + 유지보수 (구독 기간 전체) + +### 8.3 환불 불가 사유 + +- **고객 귀책 사유**: + - 협조 지연으로 인한 개발 지연 + - 요구사항 변경으로 인한 추가 개발 + - 승인 거부 또는 회피 +- **약관 위반**: + - 허위 정보 제공 + - 부정 사용 또는 재판매 + - 회사 명예 훼손 + +### 8.4 할인 계약 해지 시 추가 조건 + +본 계약이 정상가 대비 할인 조건으로 체결된 경우, 다음 조건이 추가 적용된다. + +- 발주자 귀책 해지 시 정상가(할인 전 금액) 기준으로 정산한다. + +## 제9조 (구독 및 해지) + +### 9.1 구독 시작 + +- **시작일**: 서비스 게시일 (검수 완료 후) +- **결제일**: 매월 말일 +- **청구 방식**: 후불제 (해당 월 사용량 기준) +- **일할 계산**: (사용 일수 / 해당 월 일수) × 구독료 + +> ⚠️ 중요: - 계약 시 확정된 구독료 금액은 [ ]원/월입니다. + +- 매월 말일에 해당 월 사용일수만큼만 후불 청구됩니다. + +### 9.2 구독 해지 + + - 고객은 언제든지 구독을 해지할 수 있습니다. (위약금 없음) + - 해지 신청 후 30일간 데이터 백업 기간 제공 + - 해지일로부터 30일 후 모든 데이터 완전 삭제 + +## 제10조 (유지보수 정책) + +### 10.1 유지보수 개요 + +- **적용 대상**: 구독료를 정상 납부하는 고객 +- **적용 기간**: 구독 기간 전체 (하자담보 책임 1년 이후에도 구독 중이면 계속 제공) +- **비용**: 월 구독료(500,000원)에 포함 + +### 10.2 하자담보 책임과의 차이 + +| 구분 | 하자담보 책임 (제7조) | 유지보수 (제9조의2) | +| --- | --- | --- | +| 기간 | 서비스 게시일로부터 1년 | 구독 기간 전체 | +| 근거 | 법적 의무 (소프트웨어산업진흥법) | 계약 조건 | +| 비용 | 무상 | 구독료에 포함 | +| 범위 | 하자(버그, 미구현 등) | 하자 + 일반 유지보수 | + +### 10.3 유지보수 범위 (구독료에 포함) + +> ✅ 무상 제공: - 모든 버그 수정 및 오류 처리 - 보안 패치 및 업데이트 - 성능 최적화 - 긴급 장애 대응 (24시간 이내) - 데이터 백업 및 복구 - 기술 지원 (고객센터, 이메일) - 플랫폼 업데이트 (프레임워크, 브라우저 호환성) + +> ❌ 별도 비용: - 신규 기능 개발 - 커스터마이징 및 추가 개발 - 기획 변경 (화면 구성, 프로세스 변경) - 외부 시스템 연동 - 추가 교육 및 컨설팅 + +### 10.4 서비스 레벨 (SLA) + +| 심각도 | 상황 | 응답 시간 | 해결 목표 | +| --- | --- | --- | --- | +| 긴급 (P0) | 서비스 완전 중단 | 1시간 | 24시간 | +| 높음 (P1) | 주요 기능 장애 | 4시간 | 3영업일 | +| 보통 (P2) | 일반 버그 | 1영업일 | 7영업일 | +| 낮음 (P3) | 문의/안내 | 1영업일 | 3영업일 | + +### 10.5 정기 유지보수 + +- **월간**: 보안 패치, 백업 점검 (매월 첫째 주 일요일 새벽) +- **분기**: 성능 최적화 (분기 말 일요일 새벽) +- **반기**: 시스템 점검 (6월/12월 일요일 새벽) + +> ⚠️ 모든 정기 점검은 최소 7일 전 사전 공지됩니다. + +### 10.6 유지보수 신청 + +- **고객센터**: 02-6347-0005 (평일 09:00~18:00 ) +- **이메일**: support@codebridge-x.com (24시간) +- **시스템 내**: SAM 시스템 내 고객지원 메뉴 + +### 10.7 유지보수 종료 + +다음의 경우 유지보수 서비스가 종료됩니다: 1. 구독 해지 시 2. 구독료 3개월 연속 미납 시 3. 중대한 약관 위반 시 + +## 제11조 (고객의 의무) + +고객은 다음 사항을 준수해야 합니다: +- **정확한 정보 제공**: 허위 정보 제공 금지 +- **협조 의무**: 개발에 필요한 자료 및 정보 제공 +- **부정 사용 금지**: 서비스의 재판매, 재배포 금지 +- **지적재산권 존중**: 무단 복제, 역설계 금지 + +## 제12조 (회사의 의무) + +회사는 다음 사항을 준수합니다: +- **서비스 제공**: 계약서에 명시된 서비스 제공 +- **하자담보 책임**: 1년간 하자 무상 수정 +- **개인정보 보호**: 개인정보보호법 준수 +- **기술 지원**: 고객센터 운영 및 기술 지원 + +## 제13조 (미입금 시 법적 조치) + +### 13.1 개발비 미입금 절차 + +| 단계 | 시점 | 조치 내용 | +| --- | --- | --- | +| 1차 독촉 | 기한 경과 후 3일 | 이메일 및 SMS 발송 | +| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | +| 추심등 | 기한 경과 후 14일 | 신용정보사 연체 등록, 법률대리인 위임 | +| 법적 조치 | 기한 경과 후 30일 | 지급명령 신청 또는 소송 제기 | + +### 13.2 구독료 미입금 절차 + +| 단계 | 시점 | 조치 내용 | +| --- | --- | --- | +| 1차 실패 | 익일 | 재출금 | +| 2차 실패 | 기한 경과 후 3일 | 재출금 | +| 3차 실패 | 미수금 처리 | 서비스 접근 제한, 1차 독촉 | +| 내용증명 | 기한 경과 후 7일 | 우편 발송, 7일 내 입금 요청 | +| 서비스 중단 | 기한 경과 후 14일 | 로그인 불가, 데이터 격리 | +| 강제 해지 | 기한 경과 후 30일 | 계약 해지, 법적 조치 검토 | + +## 제14조 (개인정보 보호) + + - 회사는 「개인정보 보호법」을 준수합니다. + - 고객의 개인정보는 서비스 제공 목적으로만 사용됩니다. + - 제3자에게 제공하지 않습니다. (법령 제외) + - 계약 종료 시 개인정보는 즉시 삭제됩니다. (법정 보관 의무 제외) + +## 제15조 (지적재산권) + +- **소유권**: 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. +- **사용 권한**: 고객은 서비스 사용 권한만을 부여받습니다. +- **금지 사항**: 복제, 배포, 역설계, 재판매 금지 + +## 제16조 (손해배상) + + - 회사 또는 고객이 본 계약을 위반하여 상대방에게 손해를 입힌 경우 배상 책임이 있습니다. + - 다만, 불가항력으로 인한 손해는 배상 책임에서 제외됩니다. + +## 제17조 (불가항력) + +다음의 사유로 서비스 제공이 불가능한 경우 회사는 책임을 지지 않습니다: + - 천재지변 (지진, 태풍, 홍수 등) + - 전쟁, 테러, 전염병 + - 정부 규제 또는 법령 변경 + - 제3자의 불법 행위 (해킹 등) + +## 제18조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제19조 (계약의 효력) + + - 본 계약은 계약일로부터 효력이 발생합니다. + - 본 계약은 구독 해지 시까지 유효합니다. + +## 제20조 (기타) + + - 본 계약서는 2부 작성하여 회사와 고객이 각 1부씩 보관합니다. + - 본 계약의 해석 및 적용은 대한민국 법률을 준거법으로 합니다. + +## 계약 당사자 + +### [회사] + +상호: 주식회사 코드브릿지엑스 +대표자: 이의찬 +사업자등록번호: 664-86-03713 +주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +이메일: contact@codebridge-x.com +전화: 02-6347-0005 +서명: +날짜: + +### [고객] + +상호: +대표자: +사업자등록번호: +주소: +이메일: +전화: +서명: +날짜: + +## 별첨 + +### 별첨 1: 기획서 + +[별도 첨부] + +### 별첨 2: 개발 일정표 + +[별도 첨부] + +### 별첨 3: 기능 명세서 + +[별도 첨부] + +주식회사 코드브릿지엑스 +이메일: contact@codebridge-x.com +전화: 02-6347-0005 +주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 + diff --git a/contracts/markdown/02-nda.md b/contracts/markdown/02-nda.md new file mode 100644 index 0000000..cb6ff9c --- /dev/null +++ b/contracts/markdown/02-nda.md @@ -0,0 +1,199 @@ +--- +title: "비밀유지서약서 (NDA)" +version: "v4.0" +date: "2026-02-22" +docx_file: "비밀유지서약서.docx" +--- + +# 비밀유지서약서 (NDA) + +- **작성일**: + +- **서약인 정보** +- **구분**: + +- **인적 사항:** +상호(성명): _______________ +대표자(본인): _______________ +사업자등록번호(주민등록번호): ____________________ +주소: ______________________________________________________________________ +연락처: _______________ +이메일: _______________ + +## 제1조 (목적) + + - 본 서약서는 주식회사 코드브릿지(이하 “회사”)와의 업무 관계에서 알게 된 기밀 정보를 + - 보호하기 위해 작성되었습니다. + +## 제2조 (기밀 정보의 정의) + + - 다음 각 호의 정보는 회사의 기밀 정보로 간주됩니다: + +### 2.1 고객 정보 + + - 고객사 명단 (법인명, 대표자명, 연락처) + - 고객사 담당자 정보 (성명, 부서, 연락처, 이메일) + - 계약 내역 (가입비, 할인율, 구독료, 특약 사항) + - 고객사의 사업 정보 (매출, 직원 수, 거래처 등) + - 고객사가 회사에 요구한 개발 내역 및 기획 문서 + +### 2.2 영업 정보 + + - 가격 정책 (정가, 할인 정책, 최소 가입비) + - 수수료 정책 (비율, 지급 기준, 상계 방식) + - 영업 전략 및 마케팅 계획 + - 잠재 고객 리스트 + - 계약 체결 노하우 및 제안서 템플릿 + +### 2.3 기술 정보 + + - SAM 시스템의 소스코드 + - 데이터베이스 구조 및 설계 문서 + - 개발 프로세스 및 방법론 + - 서버 인프라 구성 및 보안 정책 + - API 키, 접속 정보, 관리자 권한 + +### 2.4 경영 정보 + + - 회사의 재무 정보 (매출, 이익, 원가) + - 조직도 및 인사 정보 + - 사업 계획 및 전략 + - 투자 유치 및 M&A 관련 정보 + +### 2.5 기타 + +- 회사가 **“기밀(Confidential)”** 또는 **“대외비”**로 표시한 모든 문서 및 정보 + +## 제3조 (기밀 유지 의무) + +### 3.1 기본 의무 + + - 본인은 업무 수행 중 알게 된 모든 기밀 정보를: +- **외부에 누설하지 않습니다** +- **업무 목적 외에 사용하지 않습니다** +- **무단으로 복사, 복제, 전송하지 않습니다** +- **제3자에게 제공하거나 공개하지 않습니다** + +### 3.2 정보 관리 + + - 기밀 문서는 안전한 장소에 보관 + - 이메일, 메신저 등 전송 시 암호화 + - 업무 종료 시 모든 기밀 자료 반환 또는 파기 + - 개인 디바이스에 기밀 정보 저장 금지 + +### 3.3 제3자 접근 차단 + + - 가족, 친구 등 타인이 기밀 정보에 접근하지 못하도록 조치 + - 공공장소(카페, 도서관 등)에서 기밀 정보 취급 금지 + - 비밀번호 및 접속 정보 타인 공유 금지 + +## 제4조 (예외 사항) + + - 다음의 정보는 기밀 정보에서 제외됩니다: + - 본인이 알기 전에 이미 공개된 정보 + - 본인의 귀책사유 없이 공개된 정보 + - 제3자로부터 적법하게 취득한 정보 + - 본인이 독자적으로 개발한 정보 + - 법원, 정부기관의 법적 요구에 따라 공개해야 하는 정보 (단, 회사에 사전 통지 필수) + +## 제5조 (의무 기간) + +### 5.1 기간 + + - 본 서약서의 기밀 유지 의무는: +- **계약 체결일로부터 효력 발생** +- **계약 종료 후 2년간 유지** + +### 5.2 영구 보호 + +- 단, 다음 정보는 **영구적으로** 보호됩니다: + - 고객사 개인정보 + - 회사의 영업 비밀 (부정경쟁방지법상 영업 비밀) + - 기술 정보 (특허, 저작권 대상) + +## 제6조 (위반 시 책임) + +### 6.1 민사 책임 + + - 본인이 본 서약을 위반하여 회사 또는 고객에게 손해를 입힌 경우: +- **실손해**** 배상**: 실제 발생한 손해 전액 +- **징벌적 손해배상**: 실손해의 최대 3배 (악의적 유출 시) +- **법률 비용**: 소송 비용, 변호사 비용 등 + +### 6.2 형사 책임 + + - 다음의 경우 형사 고발 대상이 됩니다: +- **부정경쟁방지법** 위반 (영업 비밀 침해) +- **개인정보보호법** 위반 (고객 정보 유출) +- **정보통신망법** 위반 (기술 정보 침해) +- **형법** 위반 (업무상 배임) +- **※ 형사 처벌: 5년 이하 징역 또는 5천만원 이하 벌금** + +### 6.3 계약 해지 + + - 회사는 본 서약 위반 시 즉시 계약을 해지할 수 있으며, 이미 지급한 수수료 또는 + - 대금을 환수할 수 있습니다. + +## 제7조 (자료 반환) + +### 7.1 반환 대상 + + - 계약 종료 또는 요청 시 다음을 즉시 반환해야 합니다: + - 회사로부터 제공받은 모든 문서 (종이, 파일) + - 고객사 계약서 및 개인정보 + - 가격표, 제안서, 템플릿 등 영업 자료 + - USB, 하드디스크 등 저장 매체 + +### 7.2 파기 확인 + +- 반환 불가능한 파일(이메일, 클라우드 등)은 즉시 삭제하고, **삭제 확인서**를 회사에 + - 제출해야 합니다. + +## 제8조 (경업 금지) + +### 8.1 경업 금지 기간 + +- 계약 종료 후 **6개월간** 다음 행위를 금지합니다: + - 회사의 고객에게 경쟁 제품 판매 + - 회사의 기밀 정보를 이용한 유사 사업 + - 회사 직원 또는 영업파트너를 스카우트 + +### 8.2 예외 + + - 단순히 경쟁사 제품을 판매하는 것은 허용되나, 회사의 기밀 정보를 활용해서는 + - 안 됩니다. + +## 제9조 (분쟁 해결) + +### 9.1 관할 법원 + + - 본 서약과 관련된 분쟁은 회사 본사 소재지 관할 법원으로 합니다. + +### 9.2 준거법 + + - 본 서약은 대한민국 법률에 따라 해석됩니다. + +- **서약 확인** + - 본인은 위 내용을 충분히 이해하였으며, 이를 성실히 준수할 것을 서약합니다. +- **서약일**: ___________________ +- **서약인** +상호(성명): _______________ +대표자(본인): _______________ +주민등록번호(또는 사업자등록번호): _______________ +- **서명 또는 인**: _______________ + +- **수령인 (주식회사 ****코드브릿지엑스****)** + - 대표이사: 이의찬 +- **확인****일**: ___________________ +- **서명 또는 인**: _______________ + +- **참고: 관련 법률** +- **부정경쟁방지법 제2조 (영업비밀)** + - “영업비밀”이란 공공연히 알려져 있지 아니하고 독립된 경제적 가치를 가지는 것으로서, + - 비밀로 관리된 생산방법, 판매방법, 그 밖에 영업활동에 유용한 기술상 또는 경영상의 + - 정보를 말한다. +- **부정경쟁방지법 제18조 (벌칙)** +- 영업비밀을 외국에서 사용하거나 외국에서 사용되게 할 목적으로 취득·사용 또는 제3자에게 누설한 자는 **15년 이하의 징역** 또는 **15억원 이하의 벌금**에 처한다. + +- **※ 본 서약서는 2부를 작성하여 회사와 서약인이 각 1부씩 보관합니다.** +- **※ 서약 위반 시 민·형사상 책임을 질 수 있습니다.** \ No newline at end of file diff --git a/contracts/markdown/03-partner-agreement.md b/contracts/markdown/03-partner-agreement.md new file mode 100644 index 0000000..81e6af5 --- /dev/null +++ b/contracts/markdown/03-partner-agreement.md @@ -0,0 +1,276 @@ +--- +title: "영업파트너 위촉계약서" +version: "v4.0" +date: "2026-02-22" +docx_file: "영업파트너 위촉계약서.docx" +--- + +# < 영업파트너 위촉계약서 > + +# Sales Partner Engagement Agreement + + - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. + +## 제1조 (계약의 목적) + + - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 + - 명확히 함을 목적으로 합니다. + +## 제2조 (용어의 정의) + +- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 +- **관리자**: 판매자를 관리하고 지원하는 상급 영업파트너 +- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 +- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 +- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 + +## 제3조 (파트너의 역할 및 업무) + +### 3.1 판매자의 역할 + + - 잠재 고객 발굴 및 초기 접촉 + - SAM 서비스 소개 및 제안 + - 고객과의 계약 체결 지원 + - 계약 후 고객 관리 및 사후 지원 + +### 3.2 관리자의 역할 + + - 판매자 모집 및 관리 + - 판매자 교육 및 지원 + - 영업 전략 수립 및 실행 + - 회사와 판매자 간 소통 중재 + +### 3.3 공통 의무 + + - 회사의 브랜드 이미지 유지 + - 정확한 정보 제공 + - 윤리적 영업 활동 준수 + - 비밀 유지 의무 + +## 제4조 (수수료 정책) + +### 4.1 수수료 비율 + +| 역할 | 수수료 비율 | 산정 기준 | +| --- | --- | --- | +| 판매자 | 개발비의 20% | 1차,2차 입금액 기준 | +| 관리자 | 개발비의 5% | 1차,2차 입금액 기준 | + +### 4.2 수수료 산정 예시 + +- **총 개발비 80,000,000원 계약 시** + +| 단계 | 고객 입금 | 판매자 수수료 (20%) | 관리자 수수료 (5%) | +| --- | --- | --- | --- | +| 1차 착수금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | +| 2차 잔금 (50%) | 40,000,000원 | 8,000,000원 | 2,000,000원 | +| 총계 | 80,000,000원 | 16,000,000원 | 4,000,000원 | + +- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. + +### 4.3 지급 시기 + +- **지급일**: 고객 입금일 **익월 10일** +- **지급 방식**: 계좌 이체 +- **세금**: 3.3% 원천징수 (사업소득) + +### 4.4 수수료 지급 조건 + + - 고객이 개발비를 실제로 입금한 경우에만 지급 + - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 + - 파트너가 계약 위반 시 수수료 지급 보류 + +## 제5조 (수수료 정책 변경) + +### 5.1 사전 고지 의무 + +- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. + - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. + - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. + +### 5.2 변경 효력 + +- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. + - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. + - 진행 중인 계약은 최초 약정 조건을 유지합니다. + +### 5.3 변경 예시 + +#### 예시 1: 수수료율 변경 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 판매자 수수료 20% → 18% + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +#### 예시 2: 수수료 정책 폐지 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 수수료 정책 완전 폐지 + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +## 제6조 (계약 기간) + +- 본 계약은 계약일로부터 **1년간** 유효합니다. +- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. + - 자동 연장은 동일한 조건으로 반복됩니다. + +## 제7조 (계약 해지) + +### 7.1 일반 해지 (양방향) + +- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. +- **통지 방법**: 이메일 또는 등기우편 +- **효력 발생**: 통지일로부터 30일 후 +- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 +- **예시**: + - 통지일: 2026년 2월 1일 + - 해지일: 2026년 3월 1일 + - 2월 중 발생한 수수료는 3월 10일 정상 지급 + +### 7.2 즉시 해지 사유 + +- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: +- **(1) 품위 유지 결격사유 발생 [신설]** + - 음주운전으로 적발된 경우 + - 형사 범죄로 기소 또는 구속된 경우 + - 사회적 물의를 일으킨 경우 + - 기타 파트너로서의 품위를 훼손한 경우 +- **(2) 계약 위반** + - 허위 정보 제공 또는 사기 행위 + - 회사 명예 훼손 또는 영업 방해 + - 비밀 유지 의무 위반 + - 중대한 업무 태만 +- **(3) 부정 행위** + - 고객으로부터 금품 수수 + - 계약서 위조 또는 변조 + - 회사 자산 횡령 또는 유용 + +### 7.3 즉시 해지 시 조치 + + - 미지급 수수료는 지급하지 않습니다. + - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) + - 진행 중인 계약은 회사가 직접 관리합니다. + +## 제8조 (비밀 유지) + +### 8.1 비밀 정보 + + - 다음 정보는 비밀로 유지되어야 합니다: + - 회사의 영업 전략 및 계획 + - 고객 정보 (회사명, 담당자, 연락처 등) + - 수수료 정책 및 계약 조건 + - 기술 정보 및 노하우 + - 회사 내부 자료 + +### 8.2 비밀 유지 의무 + + - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. +- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. + - 위반 시 손해배상 책임이 있습니다. + +## 제9조 (지적재산권) + + - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. + - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. + - 영업 활동에 필요한 자료는 회사가 제공합니다. + +## 제10조 (세금 및 원천징수) + +### 10.1 사업소득 + +- 파트너 수수료는 **사업소득**으로 간주됩니다. + +### 10.2 원천징수 + +| 항목 | 비율 | 비고 | +| --- | --- | --- | +| 소득세 | 3.0% | | +| 지방소득세 | 0.3% | 소득세의 10% | +| 합계 | 3.3% | | + +### 10.3 지급명세서 + +- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. + +## 제11조 (손해배상) + +### 11.1 파트너의 귀책 사유 + + - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: + - 허위 정보 제공으로 계약 취소 + - 고객과의 분쟁으로 회사 명예 훼손 + - 비밀 유지 의무 위반 + - 부정 행위 + +### 11.2 회사의 귀책 사유 + + - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. + +## 제12조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제13조 (기타 사항) + +### 13.1 계약서 교부 + + - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. + +### 13.2 통지 + + - 모든 통지는 다음의 연락처로 발송됩니다: +- **회사**: +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- **파트너**: +- 이메일: +- 전화: + +### 13.3 준거법 + + - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. + +- **계약 당사자** +- **[회사]** +- **상호**: 주식회사 코드브릿지엑스 +- **대표자**: 이의찬 (인) +- **사업자등록번호**: 664-86-03713 +- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +- **이메일**: admin@codebridge-x.com +- **전화**: 02-6347-0005 +- **날짜**: + +- **[파트너]** +- **상호/성명**: +- **대표자/본인**: (서명) +- **사업자등록번호**: +- **주소**: +- **이메일**: +- **전화**: +- **날짜**: + +- **별첨** + +#### 별첨 1: 수수료 정산표 + +| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | +| --- | --- | --- | --- | --- | --- | --- | +| | | | | | | | + +#### 별첨 2: 영업 활동 보고서 + +| 날짜 | 활동내용 | 고객사 | 진행 상황 | +| --- | --- | --- | --- | +| | | | | + + - 첨부 서류 + - 사업자등록증 사본 (사업자인 경우) + - 주민등록등본 사본 (개인인 경우) + - 통장 사본 (수수료 입금용) + - 비밀유지서약서 + +- **주식회사 코드브릿지엑스** +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/contracts/markdown/04-partner-agreement-group.md b/contracts/markdown/04-partner-agreement-group.md new file mode 100644 index 0000000..b3251c4 --- /dev/null +++ b/contracts/markdown/04-partner-agreement-group.md @@ -0,0 +1,267 @@ +--- +title: "영업파트너 위촉계약서 (단체용)" +version: "v4.0" +date: "2026-02-22" +docx_file: "영업파트너 위촉계약서(단체용).docx" +--- + +# < 영업파트너 위촉계약서 > + +# Sales Partner Engagement Agreement + + - 본 계약은 주식회사 코드브릿지엑스(이하 “회사”)와 (이하 “파트너)간에 SAM 서비스 영업 활동과 관련하여 다음과 같이 위촉계약을 체결합니다. + +## 제1조 (계약의 목적) + + - 본 계약은 회사와 파트너 간의 영업파트너 위촉 관계를 규정하고, 상호 권리와 의무를 + - 명확히 함을 목적으로 합니다. + +## 제2조 (용어의 정의) + +- **판매자**: 고객을 발굴하고 계약 체결을 주도하는 영업파트너 +- **개발비**: 고객이 SAM 서비스 개발을 위해 지급하는 비용 +- **수수료**: 파트너가 영업 활동의 대가로 받는 보상 +- **서비스 게시**: 개발 완료 후 고객이 서비스에 접근 가능하도록 제공하는 것 + +## 제3조 (파트너의 역할 및 업무) + +### 3.1 판매자의 역할 + + - 잠재 고객 발굴 및 초기 접촉 + - SAM 서비스 소개 및 제안 + - 고객과의 계약 체결 지원 + - 계약 후 고객 관리 및 사후 지원 + +### 3.2 공통 의무 + + - 회사의 브랜드 이미지 유지 + - 정확한 정보 제공 + - 윤리적 영업 활동 준수 + - 비밀 유지 의무 + +## 제4조 (수수료 정책) + +### 4.1 수수료 비율 + +| 역할 | 수수료 비율 | 산정 기준 | +| --- | --- | --- | +| 판매자 | 개발비의 30% | 1차,2차 입금액 기준 | + +### 4.2 수수료 산정 예시 + +- **총 개발비 80,000,000원 계약 시** + +| 단계 | 고객 입금 | 판매자 수수료 (30%) | +| --- | --- | --- | +| 1차 착수금 (50%) | 40,000,000원 | 12,000,000원 | +| 2차 잔금 (50%) | 40,000,000원 | 12,000,000원 | +| 총계 | 80,000,000원 | 24,000,000원 | + +- **⚠️ 중요**: 개발비만 수수료 산정 대상이며, 구독료는 수수료 대상이 아닙니다. + +### 4.3 지급 시기 + +- **지급일**: 고객 입금일 **익월 10일** +- **지급 방식**: 계좌 이체 +- **세금**: 사업소득일 경우 3.3% 원천징수 + +### 4.4 수수료 지급 조건 + + - 고객이 개발비를 실제로 입금한 경우에만 지급 + - 계약 해지 또는 환불 시 수수료 미지급 또는 환수 + - 파트너가 계약 위반 시 수수료 지급 보류 + +## 제5조 (수수료 정책 변경) + +### 5.1 사전 고지 의무 + +- 회사는 수수료 정책을 변경할 경우 **최소 1개월 전** 서면 또는 이메일로 파트너에게 고지합니다. + - 수수료 정책을 완전히 폐지하는 경우에도 동일하게 1개월 전 고지합니다. + - 고지 기간 중 체결된 계약은 기존 수수료 정책을 적용합니다. + +### 5.2 변경 효력 + +- 변경된 수수료 정책은 고지일로부터 **1개월 후** 새로 체결되는 계약부터 적용됩니다. + - 고지 기간 만료 전에 체결된 계약은 기존 정책을 따릅니다. + - 진행 중인 계약은 최초 약정 조건을 유지합니다. + +### 5.3 변경 예시 + +#### 예시 1: 수수료율 변경 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 판매자 수수료 20% → 18% + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +#### 예시 2: 수수료 정책 폐지 + + - 고지일: 2026년 2월 1일 + - 변경 내용: 수수료 정책 완전 폐지 + - 적용일: 2026년 3월 1일 이후 체결 계약부터 + +## 제6조 (계약 기간) + +- 본 계약은 계약일로부터 **1년간** 유효합니다. +- 양측이 계약 만료 **30일 전**까지 서면으로 해지 의사를 통지하지 않으면 자동으로 **1년 연장**됩니다. + - 자동 연장은 동일한 조건으로 반복됩니다. + +## 제7조 (계약 해지) + +### 7.1 일반 해지 (양방향) + +- **통지 기간**: 양측은 **30일 전** 서면 통지로 계약을 해지할 수 있습니다. +- **통지 방법**: 이메일 또는 등기우편 +- **효력 발생**: 통지일로부터 30일 후 +- **미지급 수수료**: 해지일 이전에 발생한 수수료는 정산하여 지급 +- **예시**: + - 통지일: 2026년 2월 1일 + - 해지일: 2026년 3월 1일 + - 2월 중 발생한 수수료는 3월 10일 정상 지급 + +### 7.2 즉시 해지 사유 + +- 회사는 다음의 경우 **즉시 계약을 해지**할 수 있습니다: +- **(1) 품위 유지 결격사유 발생 [신설]** + - 음주운전으로 적발된 경우 + - 형사 범죄로 기소 또는 구속된 경우 + - 사회적 물의를 일으킨 경우 + - 기타 파트너로서의 품위를 훼손한 경우 +- **(2) 계약 위반** + - 허위 정보 제공 또는 사기 행위 + - 회사 명예 훼손 또는 영업 방해 + - 비밀 유지 의무 위반 + - 중대한 업무 태만 +- **(3) 부정 행위** + - 고객으로부터 금품 수수 + - 계약서 위조 또는 변조 + - 회사 자산 횡령 또는 유용 + +### 7.3 즉시 해지 시 조치 + + - 미지급 수수료는 지급하지 않습니다. + - 이미 지급한 수수료는 환수하지 않습니다. (단, 사기 행위는 예외) + - 진행 중인 계약은 회사가 직접 관리합니다. + +## 제8조 (비밀 유지) + +### 8.1 비밀 정보 + + - 다음 정보는 비밀로 유지되어야 합니다: + - 회사의 영업 전략 및 계획 + - 고객 정보 (회사명, 담당자, 연락처 등) + - 수수료 정책 및 계약 조건 + - 기술 정보 및 노하우 + - 회사 내부 자료 + +### 8.2 비밀 유지 의무 + + - 파트너는 업무 중 알게 된 비밀 정보를 외부에 누설하지 않습니다. +- 비밀 유지 의무는 계약 종료 후에도 **3년간** 유효합니다. + - 위반 시 손해배상 책임이 있습니다. + +## 제9조 (지적재산권) + + - SAM 서비스에 대한 모든 지적재산권은 회사에 귀속됩니다. + - 파트너는 회사의 사전 서면 동의 없이 회사의 상표, 로고, 브랜드를 무단으로 사용할 수 없습니다. + - 영업 활동에 필요한 자료는 회사가 제공합니다. + +## 제10조 (세금 및 원천징수) + +### 10.1 사업소득 + +- 파트너 수수료는 **사업소득**으로 간주됩니다. + +### 10.2 원천징수 + +| 항목 | 비율 | 비고 | +| --- | --- | --- | +| 소득세 | 3.0% | | +| 지방소득세 | 0.3% | 소득세의 10% | +| 합계 | 3.3% | | + +### 10.3 지급명세서 + +- 회사는 매월 수수료를 지급한 후에 파트너에게 **지급명세서**를 발급합니다. + +## 제11조 (손해배상) + +### 11.1 파트너의 귀책 사유 + + - 파트너가 다음의 행위로 회사에 손해를 입힌 경우 배상 책임이 있습니다: + - 허위 정보 제공으로 계약 취소 + - 고객과의 분쟁으로 회사 명예 훼손 + - 비밀 유지 의무 위반 + - 부정 행위 + +### 11.2 회사의 귀책 사유 + + - 회사가 정당한 사유 없이 수수료를 지급하지 않을 경우, 연체 이자를 더하여 지급합니다. + +## 제12조 (분쟁 해결) + + - 본 계약과 관련한 분쟁은 상호 협의하여 해결합니다. +- 협의가 이루어지지 않을 경우, **서울중앙지방법원**을 관할 법원으로 합니다. + +## 제13조 (기타 사항) + +### 13.1 계약서 교부 + + - 본 계약서는 2부 작성하여 회사와 파트너가 각 1부씩 보관합니다. + +### 13.2 통지 + + - 모든 통지는 다음의 연락처로 발송됩니다: +- **회사**: +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- **파트너**: +- 이메일: +- 전화: + +### 13.3 준거법 + + - 본 계약은 대한민국 법률에 따라 해석되고 적용됩니다. + +- **계약 당사자** +- **[회사]** +- **상호**: 주식회사 코드브릿지엑스 +- **대표자**: 이의찬 (인) +- **사업자등록번호**: 664-86-03713 +- **주소**: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 +- **이메일**: admin@codebridge-x.com +- **전화**: 02-6347-0005 +- **날짜**: + +- **[파트너]** +- **상호/성명**: +- **대표자/본인**: (서명) +- **사업자등록번호**: +- **주소**: +- **이메일**: +- **전화**: +- **날짜**: + +- **별첨** + +#### 별첨 1: 수수료 정산표 + +| 계약번호 | 고객사 | 입금일 | 입금액 | 수수료율 | 수수료 | 지급일 | +| --- | --- | --- | --- | --- | --- | --- | +| | | | | | | | + +#### 별첨 2: 영업 활동 보고서 + +| 날짜 | 활동내용 | 고객사 | 진행 상황 | +| --- | --- | --- | --- | +| | | | | + + - 첨부 서류 + - 사업자등록증 사본 (사업자인 경우) + - 주민등록등본 사본 (개인인 경우) + - 통장 사본 (수수료 입금용) + - 비밀유지서약서 + +- **주식회사 코드브릿지엑스** +- 이메일: admin@codebridge-x.com +- 전화: 02-6347-0005 +- 주소: 서울특별시 강서구 양천로 583, 우림블루나인 B동 1602호 diff --git a/contracts/revisions.json b/contracts/revisions.json new file mode 100644 index 0000000..1bcd843 --- /dev/null +++ b/contracts/revisions.json @@ -0,0 +1,58 @@ +{ + "documents": { + "01-service-agreement": { + "title": "고객사 서비스 이용계약서", + "docx_file": "01_고객_서비스이용계약서_v4_0_전자서명용.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + }, + { + "version": "v4.1", + "date": "2026-02-22", + "author": "개발팀", + "description": "제4조에 사용량 기반 추가 과금(4.5) 및 바로빌 부가 서비스 요금(4.6) 조항 추가" + } + ] + }, + "02-nda": { + "title": "비밀유지서약서 (NDA)", + "docx_file": "비밀유지서약서.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + }, + "03-partner-agreement": { + "title": "영업파트너 위촉계약서", + "docx_file": "영업파트너 위촉계약서.docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + }, + "04-partner-agreement-group": { + "title": "영업파트너 위촉계약서 (단체용)", + "docx_file": "영업파트너 위촉계약서(단체용).docx", + "revisions": [ + { + "version": "v4.0", + "date": "2026-02-22", + "author": "개발팀", + "description": "버전 관리 시스템 도입, 개정이력 추적 시작" + } + ] + } + } +} diff --git a/contracts/scripts/extract_to_markdown.py b/contracts/scripts/extract_to_markdown.py new file mode 100644 index 0000000..ea44889 --- /dev/null +++ b/contracts/scripts/extract_to_markdown.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +DOCX → Markdown 추출 스크립트 + +4개 전자계약 DOCX 파일을 Markdown으로 변환한다. +- 서비스이용계약서: Heading 스타일 기반 매핑 +- 나머지 3개: Bold 런 + 패턴 매칭으로 구조 유추 +""" + +import re +import sys +from datetime import date +from pathlib import Path + +from docx import Document + +# 경로 설정 +BASE_DIR = Path(__file__).resolve().parent.parent +DOCX_DIR = BASE_DIR / "docx" +MD_DIR = BASE_DIR / "markdown" + +# DOCX → Markdown 매핑 +FILE_MAP = { + "01_고객_서비스이용계약서_v4_0_전자서명용.docx": { + "output": "01-service-agreement.md", + "title": "고객사 서비스 이용계약서", + "type": "styled", + }, + "비밀유지서약서.docx": { + "output": "02-nda.md", + "title": "비밀유지서약서 (NDA)", + "type": "pattern", + }, + "영업파트너 위촉계약서.docx": { + "output": "03-partner-agreement.md", + "title": "영업파트너 위촉계약서", + "type": "pattern", + }, + "영업파트너 위촉계약서(단체용).docx": { + "output": "04-partner-agreement-group.md", + "title": "영업파트너 위촉계약서 (단체용)", + "type": "pattern", + }, +} + + +def table_to_markdown(table): + """DOCX 테이블을 Markdown 테이블로 변환""" + rows = [] + for row in table.rows: + cells = [cell.text.strip().replace("\n", " ") for cell in row.cells] + rows.append(cells) + + if not rows: + return "" + + lines = [] + # 헤더 + lines.append("| " + " | ".join(rows[0]) + " |") + lines.append("| " + " | ".join(["---"] * len(rows[0])) + " |") + # 본문 + for row in rows[1:]: + # 셀 개수 맞추기 + while len(row) < len(rows[0]): + row.append("") + lines.append("| " + " | ".join(row[: len(rows[0])]) + " |") + + return "\n".join(lines) + + +def get_paragraph_heading_level_styled(para): + """스타일 기반 문서의 헤딩 레벨 판별 (서비스이용계약서)""" + style = para.style.name if para.style else "" + + if style == "Heading 1": + return 1 + elif style == "Heading 2": + return 2 + elif style == "Heading 3": + return 3 + + return 0 + + +def get_paragraph_heading_level_pattern(para): + """패턴 매칭 기반 문서의 헤딩 레벨 판별 (비밀유지서약서, 영업파트너 위촉계약서)""" + text = para.text.strip() + has_bold = any(r.bold for r in para.runs if r.bold) + + if not text or not has_bold: + return 0 + + # "제X조" 패턴 → ## (h2) + if re.match(r"^ 0: + lines.append("") + lines.append(f"{'#' * level} {text}") + lines.append("") + elif style == "Compact": + # Bold 런이 있으면 강조 리스트 + has_bold = any(r.bold for r in para.runs if r.bold) + if has_bold: + # Bold 부분과 일반 부분 분리 + parts = [] + for run in para.runs: + if run.bold: + parts.append(f"**{run.text}**") + else: + parts.append(run.text) + combined = "".join(parts) + lines.append(f"- {combined}") + else: + # 들여쓰기된 하위 항목 + lines.append(f" - {text}") + elif style in ("Body Text", "First Paragraph"): + # 본문 텍스트 + if text.startswith("⚠️") or text.startswith("✅") or text.startswith("❌"): + lines.append("") + lines.append(f"> {text}") + lines.append("") + else: + lines.append(text) + else: + lines.append(text) + + elif tag == "tbl": + if table_idx <= len(doc.tables): + current_table_idx = sum( + 1 + for c in list(body)[: list(body).index(child)] + if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" + ) + if current_table_idx < len(doc.tables): + lines.append("") + lines.append(table_to_markdown(doc.tables[current_table_idx])) + lines.append("") + + return "\n".join(lines) + + +def extract_pattern_doc(doc, file_info): + """패턴 매칭 기반 문서 추출 (비밀유지서약서, 영업파트너 위촉계약서)""" + lines = [] + + body = doc.element.body + para_idx = 0 + + for child in body: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + + if tag == "p": + para = doc.paragraphs[para_idx] + para_idx += 1 + text = para.text.strip() + + if not text: + lines.append("") + continue + + level = get_paragraph_heading_level_pattern(para) + has_bold = any(r.bold for r in para.runs if r.bold) + + if level > 0: + lines.append("") + # 제목에서 < > 제거 + clean_text = re.sub(r"^<\s*|\s*>$", "", text).strip() + lines.append(f"{'#' * level} {clean_text}") + lines.append("") + elif has_bold: + # Bold 텍스트는 강조 처리 + parts = [] + for run in para.runs: + if run.bold: + parts.append(f"**{run.text}**") + else: + parts.append(run.text) + combined = "".join(parts) + + # (1), (2) 같은 번호 패턴 + if re.match(r"^\*\*\(\d+\)", combined): + lines.append(f"- {combined}") + # "예시 N:", "Phase N:" 같은 패턴 + elif re.match(r"^\*\*(예시|Phase|별첨)\s", combined): + lines.append("") + lines.append(f"#### {text}") + lines.append("") + else: + lines.append(f"- {combined}") + else: + # 일반 텍스트 + # 빈칸 양식 (___) 유지 + if "___" in text: + lines.append(text) + elif re.match(r"^(이메일|전화|주소|상호|대표|사업자|주민|연락처|날짜):", text): + lines.append(f"- {text}") + else: + lines.append(f" - {text}") + + elif tag == "tbl": + current_table_idx = sum( + 1 + for c in list(body)[: list(body).index(child)] + if (c.tag.split("}")[-1] if "}" in c.tag else c.tag) == "tbl" + ) + if current_table_idx < len(doc.tables): + lines.append("") + lines.append(table_to_markdown(doc.tables[current_table_idx])) + lines.append("") + + return "\n".join(lines) + + +def add_frontmatter(content, file_info, docx_name): + """YAML 프론트매터 추가""" + frontmatter = f"""--- +title: "{file_info['title']}" +version: "v4.0" +date: "{date.today().isoformat()}" +docx_file: "{docx_name}" +--- +""" + return frontmatter + "\n" + content + + +def extract_file(docx_name, file_info): + """단일 DOCX 파일 추출""" + docx_path = DOCX_DIR / docx_name + if not docx_path.exists(): + print(f" [SKIP] {docx_name} - 파일 없음") + return False + + doc = Document(str(docx_path)) + + if file_info["type"] == "styled": + content = extract_styled_doc(doc, file_info) + else: + content = extract_pattern_doc(doc, file_info) + + # 프론트매터 추가 + content = add_frontmatter(content, file_info, docx_name) + + # 연속 빈 줄 정리 (3줄 이상 → 2줄로) + content = re.sub(r"\n{3,}", "\n\n", content) + + # 파일 저장 + output_path = MD_DIR / file_info["output"] + output_path.write_text(content, encoding="utf-8") + print(f" [OK] {docx_name} → {file_info['output']}") + return True + + +def main(): + print("DOCX → Markdown 추출 시작") + print(f" DOCX 디렉토리: {DOCX_DIR}") + print(f" 출력 디렉토리: {MD_DIR}") + print() + + MD_DIR.mkdir(parents=True, exist_ok=True) + + success = 0 + for docx_name, file_info in FILE_MAP.items(): + if extract_file(docx_name, file_info): + success += 1 + + print(f"\n완료: {success}/{len(FILE_MAP)} 파일 변환됨") + return 0 if success == len(FILE_MAP) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contracts/scripts/sync_check.py b/contracts/scripts/sync_check.py new file mode 100644 index 0000000..09d55d9 --- /dev/null +++ b/contracts/scripts/sync_check.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +""" +DOCX ↔ Markdown 동기화 검증 스크립트 + +DOCX에서 텍스트를 추출하고 Markdown 파일의 텍스트와 비교하여 +불일치 항목을 리포트한다. +""" + +import difflib +import re +import sys +from pathlib import Path + +from docx import Document + +BASE_DIR = Path(__file__).resolve().parent.parent +DOCX_DIR = BASE_DIR / "docx" +MD_DIR = BASE_DIR / "markdown" + +# DOCX → Markdown 파일 매핑 +FILE_MAP = { + "01_고객_서비스이용계약서_v4_0_전자서명용.docx": "01-service-agreement.md", + "비밀유지서약서.docx": "02-nda.md", + "영업파트너 위촉계약서.docx": "03-partner-agreement.md", + "영업파트너 위촉계약서(단체용).docx": "04-partner-agreement-group.md", +} + + +def extract_text_from_docx(docx_path): + """DOCX에서 순수 텍스트만 추출 (개정이력 테이블 제외, 인터리빙 방식)""" + doc = Document(str(docx_path)) + lines = [] + + from docx.oxml.ns import qn as _qn + + body = doc.element.body + para_idx = 0 + table_idx = 0 + skip_revision = False + + for child in body: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + + if tag == "p": + if para_idx < len(doc.paragraphs): + text = doc.paragraphs[para_idx].text.strip() + para_idx += 1 + + if "개정이력" in text: + skip_revision = True + continue + if text: + skip_revision = False + lines.append(text) + + elif tag == "tbl": + if table_idx < len(doc.tables): + table = doc.tables[table_idx] + table_idx += 1 + + # 개정이력 테이블 건너뛰기 + if len(table.rows) > 0: + first_row_text = [cell.text.strip() for cell in table.rows[0].cells] + if "버전" in first_row_text and "날짜" in first_row_text: + skip_revision = False + continue + + if skip_revision: + skip_revision = False + continue + + for row in table.rows: + cells = [cell.text.strip() for cell in row.cells] + # 빈 셀만 있는 행 건너뛰기 + if not any(cells): + continue + row_text = " | ".join(cells) + if row_text.strip(): + lines.append(row_text) + + return lines + + +def extract_text_from_markdown(md_path): + """Markdown에서 순수 텍스트만 추출 (프론트매터, 마크업 제거)""" + content = md_path.read_text(encoding="utf-8") + lines = [] + + in_frontmatter = False + in_table = False + + for line in content.split("\n"): + stripped = line.strip() + + # YAML 프론트매터 건너뛰기 + if stripped == "---": + in_frontmatter = not in_frontmatter + continue + if in_frontmatter: + continue + + # 빈 줄 건너뛰기 + if not stripped: + in_table = False + continue + + # Markdown 마크업 제거 + text = stripped + + # 헤딩 마크업 제거 + text = re.sub(r"^#{1,6}\s+", "", text) + + # 리스트 마크업 제거 + text = re.sub(r"^\s*[-*+]\s+", "", text) + + # Bold/Italic 마크업 제거 + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + text = re.sub(r"\*(.+?)\*", r"\1", text) + + # 블록인용 제거 + text = re.sub(r"^>\s*", "", text) + + # 테이블 구분선 건너뛰기 + if re.match(r"^\|[\s\-|]+\|$", text): + continue + + # 테이블 행 + if text.startswith("|") and text.endswith("|"): + # 파이프 제거하고 셀 텍스트 추출 + cells = [c.strip() for c in text.strip("|").split("|")] + text = " | ".join(cells) + + text = text.strip() + if text: + lines.append(text) + + return lines + + +def normalize_text(text): + """비교를 위한 텍스트 정규화""" + # 공백 정규화 + text = re.sub(r"\s+", " ", text).strip() + # 특수문자 정규화 + text = text.replace("\u00a0", " ") # non-breaking space + text = text.replace("\u3000", " ") # ideographic space + # 언더스코어 빈칸 정규화 + text = re.sub(r"_{3,}", "___", text) + # Bold 마크업(**) 제거 (DOCX 텍스트에 리터럴 ** 포함되는 경우) + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + # 선행 리스트 마커 제거 (DOCX 텍스트가 "- "로 시작하는 경우) + text = re.sub(r"^-\s+", "", text) + return text + + +def compare_documents(docx_name, md_name): + """두 문서의 텍스트를 비교""" + docx_path = DOCX_DIR / docx_name + md_path = MD_DIR / md_name + + if not docx_path.exists(): + return {"status": "error", "message": f"DOCX 파일 없음: {docx_name}"} + if not md_path.exists(): + return {"status": "error", "message": f"Markdown 파일 없음: {md_name}"} + + docx_lines = [normalize_text(l) for l in extract_text_from_docx(docx_path) if l.strip()] + md_lines = [normalize_text(l) for l in extract_text_from_markdown(md_path) if l.strip()] + + # difflib로 비교 + matcher = difflib.SequenceMatcher(None, docx_lines, md_lines) + ratio = matcher.ratio() + + # 차이점 추출 + diffs = [] + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + continue + elif tag == "replace": + for idx in range(max(i2 - i1, j2 - j1)): + docx_text = docx_lines[i1 + idx] if i1 + idx < i2 else "(없음)" + md_text = md_lines[j1 + idx] if j1 + idx < j2 else "(없음)" + diffs.append({ + "type": "변경", + "docx": docx_text[:80], + "markdown": md_text[:80], + }) + elif tag == "delete": + for idx in range(i1, i2): + diffs.append({ + "type": "DOCX에만 존재", + "docx": docx_lines[idx][:80], + "markdown": "-", + }) + elif tag == "insert": + for idx in range(j1, j2): + diffs.append({ + "type": "Markdown에만 존재", + "docx": "-", + "markdown": md_lines[idx][:80], + }) + + return { + "status": "ok", + "similarity": round(ratio * 100, 1), + "docx_lines": len(docx_lines), + "md_lines": len(md_lines), + "diff_count": len(diffs), + "diffs": diffs[:20], # 상위 20개만 + } + + +def main(): + print("=" * 70) + print("DOCX ↔ Markdown 동기화 검증") + print("=" * 70) + + all_ok = True + + for docx_name, md_name in FILE_MAP.items(): + print(f"\n{'─' * 50}") + print(f"문서: {docx_name}") + print(f" ↔ {md_name}") + print(f"{'─' * 50}") + + result = compare_documents(docx_name, md_name) + + if result["status"] == "error": + print(f" [ERROR] {result['message']}") + all_ok = False + continue + + similarity = result["similarity"] + status_icon = "OK" if similarity >= 80 else "WARN" if similarity >= 60 else "FAIL" + + print(f" 유사도: {similarity}% [{status_icon}]") + print(f" DOCX 라인: {result['docx_lines']}") + print(f" Markdown 라인: {result['md_lines']}") + print(f" 차이점: {result['diff_count']}개") + + if result["diffs"]: + print(f"\n 주요 차이점 (상위 {min(len(result['diffs']), 10)}개):") + for i, diff in enumerate(result["diffs"][:10]): + print(f" [{diff['type']}]") + if diff["docx"] != "-": + print(f" DOCX: {diff['docx']}") + if diff["markdown"] != "-": + print(f" MD: {diff['markdown']}") + + if similarity < 80: + all_ok = False + + print(f"\n{'=' * 70}") + if all_ok: + print("결과: 모든 문서 동기화 상태 양호") + else: + print("결과: 일부 문서에서 불일치 발견 - 확인 필요") + print(f"{'=' * 70}") + + return 0 if all_ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/data/interview-master-questions.sql b/data/interview-master-questions.sql new file mode 100644 index 0000000..58b4899 --- /dev/null +++ b/data/interview-master-questions.sql @@ -0,0 +1,279 @@ +-- ============================================================ +-- 인터뷰 질문 마스터 데이터 SQL +-- 8개 도메인 × 16개 템플릿 × 80개 질문 +-- +-- 실행 방법: +-- 로컬: docker exec -i sam-mysql-1 mysql -u root -p samdb < docs/data/interview-master-questions.sql +-- 개발서버: mysql -u -p samdb < interview-master-questions.sql +-- phpMyAdmin: SQL 탭에서 전체 복사 후 실행 +-- +-- 주의: 한 번만 실행할 것. 중복 실행 시 데이터가 중복됨. +-- ============================================================ + +SET NAMES utf8mb4; +SET @tenant_id = 1; +SET @user_id = 1; +SET @now = NOW(); + +-- ============================================================ +-- 대분류: 제조업-방화셔터 (parent_id=null, 루트 카테고리) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, NULL, '제조업-방화셔터', '방화셔터 제조업 인터뷰', NULL, 1, 1, @user_id, @user_id, @now, @now); +SET @root_manufacturing = LAST_INSERT_ID(); + +-- ============================================================ +-- Domain 1: 제품 분류 체계 (product_classification) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '제품 분류 체계', '제품 카테고리, 모델 코드, 분류 기준 파악', 'product_classification', 3, 1, @user_id, @user_id, @now, @now); +SET @cat_1 = LAST_INSERT_ID(); + +-- 템플릿 1.1: 제품 카테고리 구조 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_1, '제품 카테고리 구조', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_1_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_1_1, '귀사의 주요 제품군을 모두 나열해주세요', 'text', NULL, '쉼표 구분으로 제품군 나열', NULL, NULL, 'product_classification', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '각 제품군의 하위 모델명과 코드 체계를 알려주세요', 'table_input', '{"columns":["모델코드","모델명","비고"]}', '코드-이름 매핑 테이블', NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '제품을 분류하는 기준은 무엇인가요? (소재, 용도, 크기 등)', 'multi_select', '{"choices":["소재별","용도별","크기별","설치방식별","인증여부별"]}', NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '인증(인정) 제품과 비인증 제품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'product_classification', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '인증 제품의 경우 구성이 고정되나요?', 'checkbox', NULL, NULL, NULL, '{"question_index":3,"value":"있음"}', 'product_classification', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '카테고리별 제품 수는 대략 몇 개인가요?', 'number', NULL, NULL, '개', NULL, 'product_classification', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '제품 코드 명명 규칙을 설명해주세요 (예: KSS01의 의미)', 'text', NULL, '코드 체계의 각 자릿수 의미', NULL, NULL, 'product_classification', 0, 7, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_1, '기존 시스템(ERP/엑셀)에서 사용하는 제품 분류 방식을 캡처하여 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'product_classification', 0, 8, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 1.2: 설치 유형별 분류 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_1, '설치 유형별 분류', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_1_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_1_2, '설치 유형(벽면형, 측면형, 혼합형 등)에 따라 견적이 달라지나요?', 'select', '{"choices":["예, 크게 달라짐","약간 달라짐","달라지지 않음"]}', NULL, NULL, NULL, 'product_classification', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_2, '각 설치 유형별로 어떤 부품이 달라지나요?', 'table_input', '{"columns":["설치유형","추가부품","제외부품","비고"]}', NULL, NULL, NULL, 'product_classification', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_1_2, '설치 유형에 따른 추가 비용 항목이 있나요?', 'text', NULL, NULL, NULL, NULL, 'product_classification', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 2: BOM 구조 (bom_structure) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, 'BOM 구조', '완제품-부품 관계, 부품 카테고리, BOM 레벨', 'bom_structure', 4, 1, @user_id, @user_id, @now, @now); +SET @cat_2 = LAST_INSERT_ID(); + +-- 템플릿 2.1: 완제품-부품 관계 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_2, '완제품-부품 관계', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_2_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_2_1, '대표 제품 1개의 완제품→부품 구성을 트리로 그려주세요', 'bom_tree', NULL, '최상위 제품부터 하위 부품까지 트리 구조', NULL, NULL, 'bom_structure', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '모든 제품에 공통으로 들어가는 부품은 무엇인가요?', 'multi_select', '{"choices":["가이드레일","케이스","모터","제어기","브라켓","볼트/너트"]}', '직접 입력 가능', NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '제품별로 선택적(옵션)인 부품은 무엇인가요?', 'table_input', '{"columns":["제품명","옵션부품","적용조건"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, 'BOM이 현재 엑셀로 관리되고 있나요? 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '하위 부품의 단계(레벨)는 최대 몇 단계인가요?', 'number', NULL, NULL, '단계', NULL, 'bom_structure', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_1, '부품 수량이 고정인 것과 계산이 필요한 것을 구분해주세요', 'table_input', '{"columns":["부품명","고정/계산","고정수량 또는 계산식"]}', NULL, NULL, NULL, 'bom_structure', 0, 6, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 2.2: 부품 카테고리 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_2, '부품 카테고리', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_2_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_2_2, '부품을 카테고리로 분류하면 어떻게 나눠지나요? (본체, 절곡품, 전동부, 부자재 등)', 'text', NULL, '부품 분류 체계', NULL, NULL, 'bom_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '각 카테고리에 속하는 부품 목록을 정리해주세요', 'table_input', '{"columns":["카테고리","부품명","규격"]}', NULL, NULL, NULL, 'bom_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '외주 구매 부품과 자체 제작 부품의 구분이 있나요?', 'select', '{"choices":["있음","없음"]}', NULL, NULL, NULL, 'bom_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_2_2, '부자재(볼트, 너트, 패킹 등)는 별도 관리하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'bom_structure', 0, 4, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 3: 치수/변수 계산 (dimension_formula) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '치수/변수 계산', '오픈 사이즈→제작 사이즈 변환, 파생 변수 계산', 'dimension_formula', 5, 1, @user_id, @user_id, @now, @now); +SET @cat_3 = LAST_INSERT_ID(); + +-- 템플릿 3.1: 오픈 사이즈 → 제작 사이즈 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_3, '오픈 사이즈 → 제작 사이즈', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_3_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_3_1, '고객이 입력하는 기본 치수 항목은 무엇인가요? (폭, 높이, 깊이 등)', 'multi_select', '{"choices":["폭(W)","높이(H)","깊이(D)","두께(T)","지름(Ø)"]}', NULL, NULL, NULL, 'dimension_formula', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '오픈 사이즈에서 제작 사이즈로 변환할 때 더하는 마진값은?', 'formula_input', NULL, '예: W1 = W0 + 120, H1 = H0 + 50', 'mm', NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '제품 카테고리별로 마진값이 다른가요?', 'table_input', '{"columns":["제품카테고리","W 마진(mm)","H 마진(mm)","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '면적(㎡) 계산 공식을 알려주세요', 'formula_input', NULL, '예: area = W1 * H1 / 1000000', '㎡', NULL, 'dimension_formula', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '중량(kg) 계산 공식을 알려주세요', 'formula_input', NULL, '예: weight = area * 단위중량(kg/㎡)', 'kg', NULL, 'dimension_formula', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '기타 파생 변수가 있나요? (예: 분할 개수, 절곡 길이 등)', 'table_input', '{"columns":["변수명","계산식","단위","비고"]}', NULL, NULL, NULL, 'dimension_formula', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_1, '치수 계산에 사용하는 엑셀 수식을 캡처해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 3.2: 변수 의존 관계 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_3, '변수 의존 관계', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_3_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_3_2, '변수 간 의존 관계를 설명해주세요 (A는 B와 C로 계산)', 'text', NULL, '계산 순서와 변수 의존성', NULL, NULL, 'dimension_formula', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_2, '계산 순서가 중요한 변수가 있나요?', 'text', NULL, NULL, NULL, NULL, 'dimension_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_3_2, '단위는 mm, m, kg 중 어떤 것을 기본으로 사용하나요?', 'select', '{"choices":["mm","m","cm","혼용"]}', NULL, NULL, NULL, 'dimension_formula', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 4: 부품 구성 상세 (component_config) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '부품 구성 상세', '주요 부품별 규격, 선택 기준, 특수 구성', 'component_config', 6, 1, @user_id, @user_id, @now, @now); +SET @cat_4 = LAST_INSERT_ID(); + +-- 템플릿 4.1: 주요 부품별 상세 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_4, '주요 부품별 상세', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_4_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_4_1, '가이드레일의 표준 길이 규격은? (예: 1219, 2438, 3305mm)', 'table_input', '{"columns":["규격코드","길이(mm)","비고"]}', NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '가이드레일 길이 조합 규칙은? (어떤 길이를 몇 개 사용?)', 'text', NULL, '높이에 따른 가이드레일 조합 로직', NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '케이스(하우징) 크기별 규격과 부속품 차이를 설명해주세요', 'table_input', '{"columns":["케이스규격","적용조건","부속품"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '모터 용량 종류와 선택 기준은? (무게별? 면적별?)', 'table_input', '{"columns":["모터용량","적용범위(최소)","적용범위(최대)","단위"]}', '무게/면적 범위별 모터 매핑', NULL, NULL, 'component_config', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '모터 전압 옵션은? (380V, 220V 등)', 'multi_select', '{"choices":["380V","220V","110V","DC 24V"]}', NULL, NULL, NULL, 'component_config', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '제어기 종류와 선택 기준은? (노출형/매립형 등)', 'table_input', '{"columns":["제어기유형","적용조건","비고"]}', NULL, NULL, NULL, 'component_config', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '절곡품(판재 가공) 목록과 각각의 치수 결정 방식은?', 'table_input', '{"columns":["절곡품명","치수결정방식","재질","두께(mm)"]}', NULL, NULL, NULL, 'component_config', 0, 7, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_1, '부자재(볼트, 너트, 패킹 등) 목록과 수량 결정 방식은?', 'table_input', '{"columns":["부자재명","규격","수량결정방식","기본수량"]}', NULL, NULL, NULL, 'component_config', 0, 8, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 4.2: 특수 구성 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_4, '특수 구성', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_4_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_4_2, '연기차단재 등 특수 부품이 있나요? 적용 조건은?', 'text', NULL, NULL, NULL, NULL, 'component_config', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_2, '보강재(샤프트, 파이프, 앵글 등) 사용 조건은?', 'table_input', '{"columns":["보강재명","규격","적용조건","수량"]}', NULL, NULL, NULL, 'component_config', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_4_2, '고객 요청에 따라 추가/제외되는 옵션 부품은?', 'table_input', '{"columns":["옵션부품","추가/제외","추가비용","비고"]}', NULL, NULL, NULL, 'component_config', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 5: 단가 체계 (pricing_structure) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '단가 체계', '단가 관리 방식, 계산 방식, 마진/LOSS율', 'pricing_structure', 7, 1, @user_id, @user_id, @now, @now); +SET @cat_5 = LAST_INSERT_ID(); + +-- 템플릿 5.1: 단가 관리 방식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_5, '단가 관리 방식', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_5_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_5_1, '부품별 단가를 어디서 관리하나요? (엑셀, ERP, 구두 등)', 'select', '{"choices":["엑셀","ERP 시스템","구두/경험","기타"]}', NULL, NULL, NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가표 파일을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가 변경 주기는? (월/분기/연 등)', 'select', '{"choices":["수시","월 단위","분기 단위","반기 단위","연 단위"]}', NULL, NULL, NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '단가에 포함되는 항목은? (재료비만? 가공비 포함?)', 'multi_select', '{"choices":["재료비","가공비","운송비","설치비","마진"]}', NULL, NULL, NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '고객별/거래처별 차등 단가가 있나요?', 'select', '{"choices":["있음 (등급별)","있음 (거래처별)","없음 (일괄 동일)"]}', NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, 'LOSS율(손실률)을 적용하나요? 적용 방식은?', 'formula_input', NULL, '예: 실제수량 = 계산수량 × (1 + LOSS율)', '%', NULL, 'pricing_structure', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_1, '마진율 설정 방식은? (일괄? 품목별?)', 'select', '{"choices":["일괄 마진율","품목별 마진율","카테고리별 마진율","고객별 마진율"]}', NULL, NULL, NULL, 'pricing_structure', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 5.2: 단가 계산 방식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_5, '단가 계산 방식', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_5_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_5_2, '면적 기반 단가 품목은? (원/㎡)', 'table_input', '{"columns":["품목명","단가(원/㎡)","비고"]}', NULL, '원/㎡', NULL, 'pricing_structure', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '중량 기반 단가 품목은? (원/kg)', 'table_input', '{"columns":["품목명","단가(원/kg)","비고"]}', NULL, '원/kg', NULL, 'pricing_structure', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '수량 기반 단가 품목은? (원/EA)', 'table_input', '{"columns":["품목명","단가(원/EA)","비고"]}', NULL, '원/EA', NULL, 'pricing_structure', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '길이 기반 단가 품목은? (원/m)', 'table_input', '{"columns":["품목명","단가(원/m)","비고"]}', NULL, '원/m', NULL, 'pricing_structure', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_5_2, '기타 특수 단가 계산 방식이 있나요?', 'text', NULL, NULL, NULL, NULL, 'pricing_structure', 0, 5, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 6: 수량 수식 (quantity_formula) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '수량 수식', '부품별 수량 결정 규칙, 계산식, 검증', 'quantity_formula', 8, 1, @user_id, @user_id, @now, @now); +SET @cat_6 = LAST_INSERT_ID(); + +-- 템플릿 6.1: 수량 결정 규칙 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_6, '수량 결정 규칙', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_6_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_6_1, '고정 수량 부품 목록 (항상 1개, 2개 등)', 'table_input', '{"columns":["부품명","고정수량","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '치수 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 슬랫수량 = CEIL(H1 / 슬랫피치)', NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '면적 기반 수량 계산 부품과 수식', 'formula_input', NULL, '예: 스크린수량 = area / 기준면적', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '중량 기반 수량 계산 부품과 수식', 'formula_input', NULL, NULL, NULL, NULL, 'quantity_formula', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '올림/내림/반올림 규칙이 있는 계산은?', 'table_input', '{"columns":["계산항목","올림/내림/반올림","소수점자릿수"]}', NULL, NULL, NULL, 'quantity_formula', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_1, '여유 수량(LOSS) 적용 품목과 비율은?', 'table_input', '{"columns":["품목명","LOSS율(%)","비고"]}', NULL, NULL, NULL, 'quantity_formula', 0, 6, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 6.2: 수식 검증 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_6, '수식 검증', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_6_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_6_2, '실제 견적서에서 수량 계산 예시를 보여주세요 (W=3000, H=2500일 때)', 'table_input', '{"columns":["부품명","수식","계산결과","단위"]}', NULL, NULL, NULL, 'quantity_formula', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_2, '수식에 사용하는 함수가 있나요? (SUM, CEIL, ROUND 등)', 'multi_select', '{"choices":["CEIL (올림)","FLOOR (내림)","ROUND (반올림)","MAX","MIN","IF 조건문","SUM"]}', NULL, NULL, NULL, 'quantity_formula', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_6_2, '조건에 따라 수식이 달라지는 경우가 있나요?', 'text', NULL, '예: 폭이 3000 초과이면 분할 계산', NULL, NULL, 'quantity_formula', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 7: 조건부 로직 (conditional_logic) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '조건부 로직', '범위/매핑 기반 부품 자동 선택 규칙', 'conditional_logic', 9, 1, @user_id, @user_id, @now, @now); +SET @cat_7 = LAST_INSERT_ID(); + +-- 템플릿 7.1: 범위 기반 선택 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_7, '범위 기반 선택', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_7_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_7_1, '무게 범위별 모터 용량 선택표를 작성해주세요', 'price_table', '{"columns":["범위 시작(kg)","범위 끝(kg)","모터용량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_1, '크기 범위별 부품 자동 선택 규칙이 있나요?', 'table_input', '{"columns":["조건(변수)","범위","선택부품","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_1, '브라켓 크기 결정 기준은?', 'table_input', '{"columns":["조건","범위","브라켓 규격"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 7.2: 매핑 기반 선택 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_7, '매핑 기반 선택', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_7_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_7_2, '제품 모델 → 기본 부품 세트 매핑표', 'table_input', '{"columns":["제품모델","기본부품1","기본부품2","기본부품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '설치 유형 → 추가 부품 매핑표', 'table_input', '{"columns":["설치유형","추가부품","수량","비고"]}', NULL, NULL, NULL, 'conditional_logic', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '제어기 유형 → 부속품 매핑표', 'table_input', '{"columns":["제어기유형","부속품1","부속품2","부속품3"]}', NULL, NULL, NULL, 'conditional_logic', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_7_2, '기타 조건부 자동 선택 규칙', 'text', NULL, '위 항목에 해당하지 않는 조건-결과 매핑', NULL, NULL, 'conditional_logic', 0, 4, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- Domain 8: 견적서 양식 (quote_format) +-- ============================================================ +INSERT INTO interview_categories (tenant_id, interview_project_id, parent_id, name, description, domain, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, NULL, @root_manufacturing, '견적서 양식', '출력 양식, 항목 그룹, 소계/합계 구조', 'quote_format', 10, 1, @user_id, @user_id, @now, @now); +SET @cat_8 = LAST_INSERT_ID(); + +-- 템플릿 8.1: 출력 양식 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_8, '출력 양식', 1, 1, @user_id, @user_id, @now, @now); +SET @tpl_8_1 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_8_1, '현재 사용 중인 견적서 양식을 업로드해주세요', 'file_upload', NULL, NULL, NULL, NULL, 'quote_format', 1, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적서에 표시되는 항목 그룹은? (재료비, 노무비, 설치비 등)', 'multi_select', '{"choices":["재료비","노무비","경비","설치비","운반비","이윤","부가세"]}', NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '소계/합계 계산 구조를 설명해주세요', 'text', NULL, '항목 그룹별 소계와 최종 합계의 관계', NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '할인 적용 방식은? (일괄? 항목별?)', 'select', '{"choices":["일괄 할인","항목별 할인","할인 없음","협의 할인"]}', NULL, NULL, NULL, 'quote_format', 0, 4, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '부가세 표시 방식은? (별도? 포함?)', 'select', '{"choices":["별도 표시","포함 표시","선택 가능"]}', NULL, NULL, NULL, 'quote_format', 0, 5, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적서에 표시하지 않는 내부 관리 항목은?', 'text', NULL, NULL, NULL, NULL, 'quote_format', 0, 6, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_1, '견적 번호 체계를 알려주세요', 'text', NULL, '예: Q-2026-001 형식', NULL, NULL, 'quote_format', 0, 7, 1, @user_id, @user_id, @now, @now); + +-- 템플릿 8.2: 특수 요구사항 +INSERT INTO interview_templates (tenant_id, interview_category_id, name, sort_order, is_active, created_by, updated_by, created_at, updated_at) +VALUES (@tenant_id, @cat_8, '특수 요구사항', 2, 1, @user_id, @user_id, @now, @now); +SET @tpl_8_2 = LAST_INSERT_ID(); + +INSERT INTO interview_questions (tenant_id, interview_template_id, question_text, question_type, options, ai_hint, expected_format, depends_on, domain, is_required, sort_order, is_active, created_by, updated_by, created_at, updated_at) VALUES +(@tenant_id, @tpl_8_2, '산출내역서(세부 내역)를 별도로 제공하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 1, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_2, '위치별(층/부호) 개별 산출이 필요한가요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 2, 1, @user_id, @user_id, @now, @now), +(@tenant_id, @tpl_8_2, '일괄 산출(여러 위치 합산)을 사용하나요?', 'checkbox', NULL, NULL, NULL, NULL, 'quote_format', 0, 3, 1, @user_id, @user_id, @now, @now); + +-- ============================================================ +-- 완료 확인 +-- ============================================================ +SELECT + (SELECT COUNT(*) FROM interview_categories WHERE interview_project_id IS NULL AND domain IS NOT NULL) AS master_categories, + (SELECT COUNT(*) FROM interview_templates t JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_templates, + (SELECT COUNT(*) FROM interview_questions q JOIN interview_templates t ON q.interview_template_id = t.id JOIN interview_categories c ON t.interview_category_id = c.id WHERE c.interview_project_id IS NULL AND c.domain IS NOT NULL) AS master_questions; diff --git a/dev/dev_plans/qms-api-integration-plan.md b/dev/dev_plans/qms-api-integration-plan.md new file mode 100644 index 0000000..c760f0d --- /dev/null +++ b/dev/dev_plans/qms-api-integration-plan.md @@ -0,0 +1,316 @@ +# 품질인정심사(QMS) API 연동 계획 + +> **작성일**: 2026-03-09 +> **상태**: 계획 수립 +> **URL**: `/quality/qms` +> **스토리보드**: 슬라이드 19~20 +> **관련 문서**: `docs/features/quality-management/quality-certification-audit.md` + +--- + +## 1. 현황 분석 + +### 1.1 프론트엔드 현황 + +| 항목 | 상태 | 비고 | +|------|------|------| +| `page.tsx` | ✅ 구현됨 | 14KB, 전체 페이지 레이아웃 | +| `types.ts` | ✅ 구현됨 | 95줄, 타입 정의 완료 | +| `mockData.ts` | ✅ 구현됨 | 543줄, 완전한 목업 데이터 | +| `components/` | ✅ 구현됨 | 12개 컴포넌트 + documents/ 7개 | +| `actions.ts` | ❌ 없음 | API 연동 0% | + +프론트엔드는 UI가 완성되어 있으나 **100% 목업 데이터**로 동작 중. + +### 1.2 백엔드 현황 + +| 영역 | 기존 API | 신규 필요 | +|------|----------|-----------| +| **1일차 (기준/매뉴얼 심사)** | ❌ 없음 | 모델, 마이그레이션, 서비스, 컨트롤러 전체 | +| **2일차 (로트 추적 심사)** | ⚠️ 부분 존재 | 기존 API 조합 + 서류 연결 API 신규 | + +**기존 활용 가능 API:** +- `GET /quality/documents` — 품질관리서 목록 (2일차 1단계) +- `GET /quality/documents/{id}` — 품질관리서 상세 + 수주/개소 (2일차 2단계) +- `GET /quality/performance-reports` — 실적신고 (분기 필터 활용) +- `GET /inspections` — 수입검사/중간검사 성적서 +- 출하/출고/납품 관련 기존 API + +--- + +## 2. 작업 범위 + +### Phase 1: 2일차 (로트 추적 심사) API 연동 + +> **우선순위 높음** — 기존 API 활용 가능하여 빠르게 연동 가능 + +#### 2.1 Frontend — `actions.ts` 생성 + +``` +react/src/app/[locale]/(protected)/quality/qms/actions.ts +``` + +| 액션 | 호출 API | 설명 | +|------|----------|------| +| `getQualityReports()` | `GET /quality/documents` | 품질관리서 목록 (분기 필터) | +| `getReportRoutes(reportId)` | `GET /quality/documents/{id}` | 수주코드 + 개소 목록 | +| `getRouteDocuments(routeId)` | 복합 조회 (아래 참조) | 개소별 관련 서류 8종 | +| `confirmUnitInspection(unitId)` | `PATCH /qms/lot-audit/confirm` | 개소 확인 완료 처리 | + +#### 2.2 관련 서류 조회 로직 + +2일차 3단계 "관련 서류"는 개소(Location)에 연결된 8종 서류를 조합 조회: + +| 서류 타입 | 데이터 소스 | 조회 방식 | +|-----------|-------------|-----------| +| 수입검사 성적서 | `inspections` (type=IQC) | 수주의 BOM 원자재 LOT 추적 | +| 수주서 | `orders` | 수주코드로 직접 조회 | +| 작업일지 | `work_orders` + 작업일지 | 수주 → 작업지시 → 작업일지 | +| 중간검사 성적서 | `inspections` (type=PQC) | 작업지시별 중간검사 | +| 납품확인서 | `shipments` | 출하 → 납품확인서 | +| 출고증 | `shipments` | 출하 → 출고증 | +| 제품검사 성적서 | `quality_document_locations` | 개소별 검사 문서 (EAV) | +| 품질관리서 | `quality_documents` | 품질관리서 원본 | + +#### 2.3 Backend — 신규 API (최소) + +``` +GET /api/v1/qms/lot-audit/reports — 분기별 품질관리서 목록 (전용 뷰) +GET /api/v1/qms/lot-audit/reports/{id} — 수주코드 + 개소 + 완료 상태 +GET /api/v1/qms/lot-audit/routes/{id}/documents — 개소별 8종 서류 조합 조회 +PATCH /api/v1/qms/lot-audit/units/{id}/confirm — 확인 완료 처리 +``` + +> 기존 `quality/documents` API를 래핑하여 QMS 전용 응답 형태로 가공하는 방식 권장. +> 8종 서류 조합 로직이 복잡하므로 **전용 서비스 메서드** 필요. + +#### 2.4 DB 변경 + +| 변경 | 테이블 | 설명 | +|------|--------|------| +| 컬럼 추가 | `quality_document_locations` | `options` JSON에 `lot_audit_confirmed`, `lot_audit_confirmed_at` 추가 | + +> 별도 테이블 없이 기존 개소(Location) 테이블의 `options` 활용 (컬럼 추가 정책 준수) + +--- + +### Phase 2: 1일차 (기준/매뉴얼 심사) 백엔드 구축 + +> **작업량 많음** — 완전 신규 백엔드 구축 필요 + +#### 2.1 DB 설계 (신규 테이블) + +``` +audit_checklists (심사 점검표 마스터) +├── id, tenant_id +├── year, quarter +├── type: 'standard_manual' (1일차) +├── status: draft/in_progress/completed +├── options: JSON +├── created_by, timestamps, soft_delete + +audit_checklist_categories (점검표 카테고리) +├── id, tenant_id +├── checklist_id (FK → audit_checklists) +├── title: '원재료 품질관리 기준' +├── sort_order +├── options: JSON + +audit_checklist_items (점검표 세부 항목) +├── id, tenant_id +├── category_id (FK → audit_checklist_categories) +├── name: '수입검사 기준 확인' +├── description +├── is_completed: boolean +├── completed_at, completed_by +├── sort_order +├── options: JSON + +audit_standard_documents (기준 문서) +├── id, tenant_id +├── checklist_item_id (FK → audit_checklist_items) +├── title, version, date +├── document_id (FK → documents, EAV) +├── options: JSON +``` + +#### 2.2 Backend 구현 + +| 파일 | 역할 | +|------|------| +| `api/app/Models/Qualitys/AuditChecklist.php` | 심사 점검표 모델 | +| `api/app/Models/Qualitys/AuditChecklistCategory.php` | 카테고리 모델 | +| `api/app/Models/Qualitys/AuditChecklistItem.php` | 세부 항목 모델 | +| `api/app/Models/Qualitys/AuditStandardDocument.php` | 기준 문서 모델 | +| `api/app/Services/AuditChecklistService.php` | 서비스 | +| `api/app/Http/Controllers/Api/V1/AuditChecklistController.php` | 컨트롤러 | +| `api/database/migrations/XXXX_create_audit_checklists_table.php` | 마이그레이션 (4테이블) | + +#### 2.3 API 엔드포인트 + +``` +GET /api/v1/qms/checklists — 점검표 목록 (연도/분기 필터) +POST /api/v1/qms/checklists — 점검표 생성 +GET /api/v1/qms/checklists/{id} — 점검표 상세 (카테고리+항목+문서) +PUT /api/v1/qms/checklists/{id} — 점검표 수정 +PATCH /api/v1/qms/checklists/{id}/complete — 점검표 완료 처리 + +PATCH /api/v1/qms/checklist-items/{id}/toggle — 항목 완료/미완료 토글 +GET /api/v1/qms/checklist-items/{id}/documents — 항목별 기준 문서 조회 +POST /api/v1/qms/checklist-items/{id}/documents — 기준 문서 연결 +DELETE /api/v1/qms/checklist-items/{id}/documents/{docId} — 기준 문서 연결 해제 +``` + +#### 2.4 Frontend — actions.ts 확장 + +| 액션 | 설명 | +|------|------| +| `getChecklists(year, quarter)` | 점검표 목록 | +| `getChecklistDetail(id)` | 점검표 상세 (카테고리+항목+문서) | +| `toggleChecklistItem(itemId)` | 항목 완료/미완료 토글 | +| `getCheckItemDocuments(itemId)` | 기준 문서 조회 | +| `confirmCheckItem(itemId)` | 기준/매뉴얼 확인 완료 | + +--- + +### Phase 3: 프론트엔드 목업 → API 전환 + +#### 3.1 page.tsx 수정 + +- `mockData.ts` import 제거 +- `actions.ts` import로 교체 +- `useEffect`에서 API 호출 +- 로딩/에러 상태 추가 + +#### 3.2 컴포넌트 수정 + +| 컴포넌트 | 변경 내용 | +|----------|-----------| +| `ReportList.tsx` | API 데이터 바인딩 | +| `RouteList.tsx` | API 데이터 바인딩 | +| `DocumentList.tsx` | 8종 서류 실제 조회 | +| `InspectionModal.tsx` | 실제 검사 문서 렌더링 | +| `Day1ChecklistPanel.tsx` | API 데이터 바인딩 | +| `Day1DocumentSection.tsx` | 기준 문서 API 조회 | +| `Day1DocumentViewer.tsx` | 실제 파일 미리보기 | +| `AuditProgressBar.tsx` | 실시간 진행률 계산 | +| `Filters.tsx` | 연도/분기 필터 API 연동 | + +#### 3.3 mockData.ts 처리 + +- Phase 3 완료 후 `mockData.ts` 삭제 +- 또는 `USE_MOCK` 플래그 패턴 적용 (개발 편의) + +--- + +## 3. 데이터 매핑 + +### 3.1 InspectionReport ↔ QualityDocument + +| 프론트 (InspectionReport) | 백엔드 (QualityDocument) | +|---------------------------|-------------------------| +| `id` | `quality_documents.id` | +| `code` | `quality_documents.code` (채번) | +| `siteName` | `quality_documents.site_name` | +| `item` | `quality_documents.options.product_type` 또는 인정특성 | +| `routeCount` | `quality_document_orders` COUNT | +| `totalRoutes` | `quality_document_locations` COUNT | +| `quarter` | `performance_reports.year` + `quarter` | +| `year` | `performance_reports.year` | +| `quarterNum` | `performance_reports.quarter` | + +### 3.2 RouteItem ↔ QualityDocumentOrder + +| 프론트 (RouteItem) | 백엔드 (QualityDocumentOrder) | +|--------------------|-------------------------------| +| `id` | `quality_document_orders.id` | +| `code` | `orders.order_code` | +| `date` | `orders.order_date` | +| `site` | `orders.site_name` | +| `locationCount` | `quality_document_locations` COUNT | +| `subItems` | `quality_document_locations` 변환 | + +### 3.3 ChecklistCategory ↔ AuditChecklistCategory + +| 프론트 (ChecklistCategory) | 백엔드 (AuditChecklistCategory) | +|---------------------------|--------------------------------| +| `id` | `audit_checklist_categories.id` | +| `title` | `audit_checklist_categories.title` | +| `subItems` | `audit_checklist_items` 관계 | + +--- + +## 4. 일정 산정 + +| Phase | 작업 내용 | 예상 소요 | +|-------|----------|-----------| +| **Phase 1** | 2일차 API 연동 (기존 API 활용) | | +| ├ 1-1 | Backend: 전용 서비스 + 컨트롤러 + 라우트 | 1일 | +| ├ 1-2 | Backend: 8종 서류 조합 조회 로직 | 1일 | +| ├ 1-3 | Frontend: actions.ts 생성 + 목업 교체 | 1일 | +| └ 1-4 | 테스트 및 디버깅 | 0.5일 | +| **Phase 2** | 1일차 백엔드 구축 (완전 신규) | | +| ├ 2-1 | DB 설계 + 마이그레이션 (4테이블) | 0.5일 | +| ├ 2-2 | 모델 4개 + 관계 설정 | 0.5일 | +| ├ 2-3 | 서비스 + 컨트롤러 + 라우트 | 1일 | +| └ 2-4 | 초기 데이터 시딩 (점검표 마스터) | 0.5일 | +| **Phase 3** | 프론트엔드 전환 | | +| ├ 3-1 | 2일차 컴포넌트 API 바인딩 | 1일 | +| ├ 3-2 | 1일차 컴포넌트 API 바인딩 | 1일 | +| └ 3-3 | 통합 테스트 + mockData 정리 | 0.5일 | + +**총 예상: ~8일** + +--- + +## 5. 의존성 및 리스크 + +### 5.1 의존성 + +| 항목 | 의존 대상 | 상태 | +|------|-----------|------| +| 품질관리서 데이터 | `quality_documents` 실 데이터 | ✅ 운영 중 | +| 실적신고 데이터 | `performance_reports` 실 데이터 | ✅ 운영 중 | +| 수입검사 성적서 | `inspections` (IQC) | ✅ 운영 중 | +| 중간검사 성적서 | `inspections` (PQC) | ⚠️ 구현 중 | +| 작업일지 | `work_orders` 연결 | ✅ 운영 중 | +| 출하/납품 | `shipments` | ✅ 운영 중 | +| 기준 문서 파일 | EAV Document 시스템 | ✅ 운영 중 | + +### 5.2 리스크 + +| 리스크 | 영향 | 완화 방안 | +|--------|------|-----------| +| 8종 서류 추적 로직 복잡 | Phase 1 지연 | 서류별 독립 조회 후 프론트에서 조합 | +| 1일차 점검표 초기 데이터 부재 | Phase 2 테스트 어려움 | 시더로 기본 점검표 생성 | +| 중간검사 미완성 | 2일차 일부 서류 누락 | 빈 상태로 표시, 추후 연동 | + +--- + +## 6. 권장 진행 순서 + +``` +Phase 1 (2일차 API 연동) — 3.5일 + ↓ +Phase 2 (1일차 백엔드 구축) — 2.5일 + ↓ +Phase 3 (프론트엔드 전환) — 2.5일 +``` + +**Phase 1을 먼저 하는 이유:** +- 기존 API 활용으로 빠르게 실 데이터 확인 가능 +- 로트 추적은 실적신고와 직접 연결되어 비즈니스 우선순위 높음 +- Phase 2(1일차)는 독립적인 신규 개발이므로 나중에 진행 가능 + +--- + +## 관련 문서 + +- [품질인정심사 기능 문서](../../features/quality-management/quality-certification-audit.md) +- [제품검사 관리](../../features/quality-management/inspection-management.md) +- [생산실적신고](../../features/quality-management/performance-reports.md) +- [통합 개선 마스터 플랜](./integrated-master-plan.md) + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/academy/fire-shutter-image-prompts.md b/features/academy/fire-shutter-image-prompts.md new file mode 100644 index 0000000..1614eda --- /dev/null +++ b/features/academy/fire-shutter-image-prompts.md @@ -0,0 +1,369 @@ +# 방화셔터 백과사전 이미지 생성 프롬프트 + +> **작성일**: 2026-02-22 +> **상태**: 확정 +> **용도**: Google Gemini (Nano Banana Pro) 이미지 생성용 + +--- + +## 1. 개요 + +### 1.1 목적 + +MNG 아카데미 > 방화셔터 백과사전 페이지에 삽입할 기술 일러스트레이션을 AI 이미지 생성 도구(Google Gemini)로 제작하기 위한 프롬프트 모음이다. + +### 1.2 사용 방법 + +1. Google Gemini (Nano Banana Pro 모델)에서 프롬프트를 입력한다 +2. 생성된 이미지를 `mng/public/images/academy/fire-shutter/` 경로에 저장한다 +3. Blade 뷰에서 `` 태그로 참조한다 + +### 1.3 주의사항 + +- **화면 내 모든 라벨은 영어**로 작성되어 있다 (한글 텍스트는 AI 이미지 생성 시 깨짐 현상 발생) +- 전체 구성도, 설치 장면 등 넓은 이미지는 **16:9** 비율 권장 +- 단면도, 부품 상세 등은 **1:1** 또는 **4:3** 비율 권장 +- 생성 실패 시 프롬프트 앞에 `Detailed technical engineering illustration, clean white background, ` 를 추가한다 + +--- + +## 2. 프롬프트 목록 + +### 2.1 방화셔터 전체 구성도 (Full Component Diagram) + +``` +Technical illustration of a fire shutter (automatic fire-rated rolling shutter) installed in a building opening, cutaway side view showing all components with English labels. + +Show these parts clearly labeled: +- Top: "CEILING SLAB" with "HEAD BOX / CASE" mounted below +- Inside head box: "SHAFT" with coiled steel slats, "BALANCE SPRING", "GEAR BOX", "MOTOR", "ELECTROMAGNETIC BRAKE", "BRACKET" on both sides +- Both sides: vertical "GUIDE RAIL" mounted on fireproof walls with "ANCHOR BOLTS" +- Center: multiple horizontal "STEEL SLATS" hanging down in interlocking pattern +- Bottom: "BOTTOM BAR" touching the floor with rubber seal +- Nearby wall: "MANUAL CONTROL BOX" with UP/STOP/DOWN buttons +- Ceiling: "SMOKE DETECTOR" and "HEAT DETECTOR" +- Wall-mounted: "FIRE SHUTTER CONTROLLER" + +Style: Clean technical cutaway diagram, white background, professional engineering illustration, labeled with arrows pointing to each component. Color-coded: structural parts in gray/silver, electrical parts in blue, safety parts in red. Isometric or 3/4 perspective view. +``` + +--- + +### 2.2 슬랫 인터록킹 구조 (Slat Interlocking) + +``` +Technical cross-section illustration showing how fire shutter steel slats interlock with each other. + +Show 3-4 slats connected in interlocking pattern: +- Each slat is a C-shaped or S-shaped profile made from 1.6mm EGI steel +- The curved edges of adjacent slats hook into each other, allowing flexibility while maintaining a continuous curtain surface +- One slat highlighted with dimension labels: "THICKNESS 1.6mm", "PITCH 75-100mm" +- Show the slight curved profile that allows the slat to wrap around the shaft when rolled up +- Arrow labeled "ROLLING DIRECTION" + +Label each part: "SLAT", "INTERLOCKING JOINT", "EGI STEEL 1.6mm" + +Style: Clean engineering cross-section diagram, white background, metallic silver color for steel. Include dimension lines. Zoomed-in detail view with magnified interlocking joint area in a callout circle. +``` + +--- + +### 2.3 가이드레일 단면도 (Guide Rail Cross-Section) + +``` +Technical cross-section illustration of a fire shutter guide rail mounted on a fireproof wall, viewed from top-down. + +Show the C-channel shaped guide rail: +- C-channel profile, steel thickness 2.3mm+ +- Inside the channel: slat edge sitting in the groove +- Smoke seal material strips on both sides of the channel, pressing against the slat +- Anchor bolts securing the guide rail to the concrete wall +- Wall shown as hatched concrete pattern + +Labels with arrows: +- "GUIDE RAIL BODY (C-CHANNEL)" +- "SLAT EDGE" +- "SMOKE SEAL PACKING" +- "ANCHOR BOLT" +- "FIREPROOF WALL" +- "STEEL 2.3mm+" + +Style: Clean technical cross-section, white background, steel parts in metallic gray, seal material in orange/red, wall in light brown hatched pattern. Include dimension annotations. +``` + +--- + +### 2.4 샤프트 어셈블리 (Shaft Assembly) + +``` +Technical illustration showing the inside of a fire shutter head box, exploded or cutaway view. + +Show these components assembled on or around the shaft: +- Central pipe labeled "SHAFT" with slats attached, partially wound +- Left side: "BRACKET" steel plate bolted to wall, with "BEARING" supporting shaft end +- Right side: "GEAR BOX" and "MOTOR" mounted on bracket +- "ELECTROMAGNETIC BRAKE" attached to motor assembly +- "BALANCE SPRING" torsion spring visible inside the shaft +- "AUTO CLOSER" device mounted near the brake +- "LIMIT SWITCH" small switches with actuator arms +- "HEAD BOX CASE" shown as transparent or partially removed to reveal internals +- Wiring connections going down labeled "TO CONTROLLER" + +Style: Exploded technical diagram or cutaway 3D illustration, white background, professional engineering style. Color-coded: mechanical parts in silver/gray, motor in dark blue, brake in red, spring in green. All labels in English with leader lines. +``` + +--- + +### 2.5 감속기+모터+브레이크 (Gear Box + Motor + Brake Assembly) + +``` +Technical illustration of a fire shutter drive unit assembly, showing three main components connected together. + +Show them assembled in sequence with labels: +1. "MOTOR (220V)" - cylindrical body with power cables +2. "ELECTROMAGNETIC BRAKE" - disc-type brake between motor and gearbox, showing brake disc, coil, and spring +3. "WORM GEAR BOX" - rectangular housing with cutaway revealing the worm gear and worm wheel inside + +Assembly order shown with arrows: MOTOR → BRAKE → GEAR BOX → "OUTPUT TO SHAFT" +Include rotation direction arrows + +Small inset callout showing worm gear mechanism detail labeled: "WORM", "WORM WHEEL", "SELF-LOCKING" + +Style: Technical exploded/assembly diagram, white background, metallic rendering, engineering illustration style. +``` + +--- + +### 2.6 연동제어기 시스템 (Controller System) + +``` +Technical schematic diagram showing the fire shutter interlock control system wiring and signal flow. + +Layout (block diagram style): +- Top center: "FIRE ALARM PANEL" - rectangular box +- Left: "SMOKE DETECTOR (PHOTOELECTRIC)" - circular device on ceiling +- Right: "HEAT DETECTOR (FIXED TEMP.)" - circular device on ceiling +- Center: "FIRE SHUTTER CONTROLLER" - panel with LED indicators labeled "POWER", "PARTIAL CLOSE", "FULL CLOSE" +- Below controller: "AUTO CLOSER" connected to shutter mechanism +- Bottom left: "MANUAL CONTROL BOX" with "UP / STOP / DOWN" buttons +- Bottom: "FIRE SHUTTER" shown schematically + +Signal flow arrows with labels: +- Smoke detector → Controller: "STAGE 1: PARTIAL CLOSE (1m gap)" +- Heat detector → Controller: "STAGE 2: FULL CLOSE (floor sealed)" +- Controller → Auto closer: "CLOSE COMMAND" +- Controller → Speaker icon: "ALARM OUTPUT" +- Controller ↔ Fire alarm panel: "STATUS SIGNAL" + +Style: Clean schematic/block diagram, white background, professional electrical diagram style. Color coding: red for fire signals, blue for power, green for status. All labels in English. +``` + +--- + +### 2.7 2단계 폐쇄 시퀀스 (2-Stage Closure Sequence) + +``` +Technical illustration showing the two-stage closing sequence of an automatic fire shutter, presented as 3 side-by-side panels: + +Panel 1 - Title: "NORMAL (OPEN)": +- Fire shutter fully open, rolled up inside head box +- People walking through the opening freely +- Smoke and heat detectors on ceiling shown in standby (green LED) +- Caption: "Shutter open, passage clear" + +Panel 2 - Title: "STAGE 1: PARTIAL CLOSE": +- Smoke detector activated (red LED, smoke wisps shown) +- Shutter descended leaving about 1 meter gap from floor +- A person crouching to pass under the gap +- Alarm buzzer icon showing sound waves +- Caption: "Smoke detected → Partial close, 1m gap for evacuation" + +Panel 3 - Title: "STAGE 2: FULL CLOSE": +- Heat detector activated (red LED, flames shown) +- Shutter fully closed to floor, bottom bar sealed against floor +- Fire and smoke on one side, clean air on other side +- Caption: "Heat detected → Full close, fire/smoke blocked" + +Arrow at bottom labeled "TIME SEQUENCE →" + +Style: Clean technical illustration with slight architectural rendering, sequential format left to right, white background. People as simple silhouettes. Fire/smoke rendered subtly. +``` + +--- + +### 2.8 롤포밍 공정 (Roll Forming Process) + +``` +Technical illustration showing the roll forming manufacturing process for fire shutter steel slats, production line viewed from the side. + +Show the line from left to right with labels: +1. "UNCOILER" - Steel coil (EGI 1.6mm) being unrolled +2. "LEVELER" - Flattening rollers correcting coil curvature +3. "ROLL FORMING STATION" - 6-8 pairs of forming rollers progressively shaping the flat strip into C/S-shaped slat profile +4. "CUTTING STATION" - Flying shear cutting the formed strip to length +5. "FINISHED SLATS" - Slats stacked neatly on output table + +Detail callout at top showing progressive cross-section shape changes: "FLAT → STAGE 1 → STAGE 2 → STAGE 3 → FINAL PROFILE" + +Arrow at bottom: "MATERIAL FLOW →" +Label on coil: "EGI STEEL COIL 1.6mm" + +Style: Technical factory/manufacturing illustration, clean white background, machinery in industrial gray/green, steel in silver. Side view. Directional arrows showing material flow. +``` + +--- + +### 2.9 현장 설치 (Field Installation) + +``` +Technical illustration showing fire shutter installation at a construction site, depicting key installation steps in a single scene. + +Scene showing a large building opening (about 5m wide, 4m tall) with: +- Two workers on scaffolding installing the head box assembly at the top +- Brackets already bolted to both side walls near the ceiling +- Guide rails mounted vertically on walls with anchor bolts +- Shaft with wound slat curtain being lifted up to place on brackets +- Manual control box being mounted on adjacent wall +- Wiring conduits visible running from controller to head box +- Construction tools: level tool, drill, anchor bolts, wrenches + +Labels with arrows pointing to activities: +- "BRACKET MOUNTING" +- "GUIDE RAIL ANCHORING" +- "SHAFT PLACEMENT" +- "ELECTRICAL WIRING" +- "LEVEL CHECK" +- "ANCHOR BOLT FIXING" + +Style: Technical construction illustration, slightly warm tone, realistic building interior with exposed concrete. Workers wearing safety helmets and vests. Clean architectural illustration style. All text in English. +``` + +--- + +### 2.10 유지보수 점검 (Maintenance Inspection) + +``` +Technical illustration showing fire shutter maintenance inspection scene. + +Show a maintenance technician inspecting a fire shutter: +- Technician with safety vest and hard hat, holding a tablet +- Fire shutter partially lowered (halfway) for testing +- Close-up callout bubbles showing key inspection points: + 1. "SLAT CONDITION" - checking for deformation, rust + 2. "SMOKE SEAL CHECK" - checking guide rail seal condition + 3. "BOTTOM BAR PACKING" - checking floor seal + 4. "MOTOR / BRAKE CHECK" - head box open, listening for sounds + 5. "MANUAL BOX TEST" - pressing UP/STOP/DOWN buttons + 6. "CONTROLLER STATUS" - checking LED indicators + +Checklist overlay in corner: +☑ MOTOR OPEN/CLOSE TEST +☑ DETECTOR INTERLOCK TEST +☑ ALARM SOUND CHECK +☑ MANUAL OPERATION CHECK +☑ BOTTOM BAR SEAL CHECK + +Style: Clean technical illustration, bright well-lit building interior, professional maintenance scene. Color callout bubbles with icons. All text in English. +``` + +--- + +### 2.11 강판형 vs 스크린형 (Steel Plate vs Screen Type) + +``` +Technical side-by-side comparison illustration of two types of fire shutters in similar building openings: + +Left side - Title "STEEL PLATE TYPE": +- Steel slat fire shutter in partially closed position +- Opaque metallic surface of interlocking steel slats visible +- Heavier, industrial appearance with thick guide rails +- Bottom bar with rubber seal +- Callout: "EGI STEEL 1.6mm / HEAVY / OPAQUE / HIGH SEALING" + +Right side - Title "SCREEN / FABRIC TYPE": +- Fabric fire shutter in partially closed position +- Semi-transparent woven silica fiber screen, you can faintly see light through it +- Lighter, sleeker with thin guide rails (11mm) +- Fabric gathered at top +- Callout: "SILICA FIBER / LIGHTWEIGHT / SEMI-TRANSPARENT / RAIL 11mm" + +Center dividing line with "VS" label +Bottom comparison bar: "WEIGHT: Heavy vs Light | VISIBILITY: Opaque vs See-through | RAIL WIDTH: Wide vs 11mm" + +Style: Clean technical comparison, white background, same scale, professional product comparison layout. All text in English. +``` + +--- + +### 2.12 주요 고장 유형 (Major Fault Types) + +``` +Technical illustration showing 6 common fire shutter failure types in a 2x3 grid layout, each in its own panel with a red problem highlight: + +Panel 1 - "SLAT DERAILMENT": +- A slat edge coming out of the guide rail groove, curtain jammed +- Red circle on problem area + +Panel 2 - "MOTOR BURNOUT": +- Motor with smoke marks, burnt wiring +- Overheat warning symbol + +Panel 3 - "BRAKE PAD WEAR": +- Electromagnetic brake with worn disc pad +- Side comparison: "NEW" thick pad vs "WORN" thin pad + +Panel 4 - "CONTROLLER MALFUNCTION": +- Controller panel with error LED, disconnected wires +- Broken signal path indicator + +Panel 5 - "CLOSER SPEED FAULT": +- Shutter dropping fast, speedometer showing "0.15 m/s LIMIT EXCEEDED" +- Governor mechanism detail + +Panel 6 - "SMOKE SEAL FAILURE": +- Smoke wisps leaking through guide rail gaps +- Comparison: "NEW SEAL" vs "DEGRADED SEAL" + +Style: Technical diagnostic illustration, white background, bordered panels. Problem areas in red/orange highlight. Clean maintenance manual style. All titles and labels in English. +``` + +--- + +## 3. 이미지 파일 관리 + +### 3.1 저장 경로 + +``` +mng/public/images/academy/fire-shutter/ +├── 01-full-component-diagram.png +├── 02-slat-interlocking.png +├── 03-guide-rail-cross-section.png +├── 04-shaft-assembly.png +├── 05-gearbox-motor-brake.png +├── 06-controller-system.png +├── 07-two-stage-closure.png +├── 08-roll-forming-process.png +├── 09-field-installation.png +├── 10-maintenance-inspection.png +├── 11-steel-vs-screen-type.png +└── 12-major-fault-types.png +``` + +### 3.2 Blade 참조 예시 + +```html +방화셔터 전체 구성도 +``` + +--- + +## 관련 문서 + +- `mng/resources/views/academy/fire-shutter.blade.php` - 방화셔터 백과사전 Blade 뷰 +- `mng/app/Http/Controllers/AcademyController.php` - 아카데미 컨트롤러 + +--- + +**최종 업데이트**: 2026-02-22 diff --git a/features/approvals/README.md b/features/approvals/README.md new file mode 100644 index 0000000..a43521c --- /dev/null +++ b/features/approvals/README.md @@ -0,0 +1,298 @@ +# 결재관리 시스템 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **프로젝트**: SAM MNG (관리자 웹) +> **우선순위**: 🔴 필수 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM MNG 전자결재 시스템. 기안부터 최종 승인, 반려, 회수, 보류, 전결, 참조까지 기업 결재 프로세스를 디지털화한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 시스템 전체 개요, 아키텍처, 상태 관리 | +| [form-types.md](form-types.md) | 양식별 필드/JSON 구조/인터랙션 기술 명세 | +| [workflows.md](workflows.md) | 상세 워크플로우 (승인/반려/회수/보류/전결/복사재기안) | +| [api-reference.md](api-reference.md) | API 엔드포인트 명세 | +| [ui-screens.md](ui-screens.md) | 화면별 UI 구성 및 동작 | +| [db-changes-and-model-sync.md](db-changes-and-model-sync.md) | DB 변경사항 및 API/MNG 모델 동기화 현황 | + +### 1.3 구현 현황 + +| Phase | 범위 | 상태 | +|-------|------|------| +| **Phase 1** | 순차결재, 기안/상신/승인/반려/회수 | ✅ 완료 | +| **Phase 2** | 보류/해제, 전결, 참조 열람 추적, 복사 재기안 | ✅ 완료 | +| **Phase 3** | 병렬결재, 위임(대결), 알림 | 미착수 | +| **Phase 4** | ERP 연동, 결재 통계, 관리자 설정 | 미착수 | + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + HTMX + Alpine.js | 동적 UI, 부분 렌더링 | +| API | Laravel Controller + Service | JSON API (내부용) | +| 모델 | Eloquent ORM | Multi-tenant 스코프 | +| DB | MySQL 8.0 | API 프로젝트에서 마이그레이션 관리 | + +### 2.2 프로젝트 분리 + +``` +API (/home/aweso/sam/api) +├── database/migrations/ ← 모든 결재 테이블 마이그레이션 + +MNG (/home/aweso/sam/mng) +├── app/Models/Approvals/ ← 모델 (Approval, ApprovalStep, ApprovalForm, ApprovalLine, ApprovalDelegation) +├── app/Services/ ← ApprovalService (비즈니스 로직) +├── app/Http/Controllers/ ← ApprovalController (웹), ApprovalApiController (API) +├── resources/views/approvals/ ← Blade 뷰 +└── routes/ ← 웹 라우트 + API 라우트 +``` + +### 2.3 핵심 클래스 + +``` +ApprovalService +├── 목록 조회: getMyDrafts(), getPendingForMe(), getCompletedByMe(), getReferencesForMe() +├── CRUD: createApproval(), updateApproval(), deleteApproval(), getApproval() +├── 워크플로우: submit(), approve(), reject(), cancel(), hold(), releaseHold(), preDecide(), copyForRedraft() +├── 참조: markAsRead() +└── 유틸: getBadgeCounts(), getApprovalLines(), getApprovalForms(), saveApprovalSteps() +``` + +--- + +## 3. 데이터베이스 + +### 3.1 테이블 관계 + +``` +approval_forms (결재 양식) + │ 1:N + ▼ +approvals (결재 문서) + │ 1:N │ N:1 (self) + ▼ ▼ +approval_steps (결재 단계) approvals (parent_doc_id → 원본 문서) + +approval_lines (결재선 템플릿) ← approvals.line_id 참조 + +approval_delegations (위임 설정) ← Phase 3 준비 +``` + +### 3.2 approvals (결재 문서) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT | 테넌트 격리 | +| `document_number` | VARCHAR | `APR-YYMMDD-001` 형식 | +| `form_id` | BIGINT FK | 양식 | +| `line_id` | BIGINT FK NULL | 결재선 템플릿 | +| `title` | VARCHAR(200) | 제목 | +| `content` | JSON | 양식 필드 데이터 | +| `body` | TEXT NULL | 본문 | +| `status` | VARCHAR(20) | 문서 상태 (6가지) | +| `is_urgent` | BOOLEAN | 긴급 여부 | +| `drafter_id` | BIGINT FK | 기안자 | +| `department_id` | BIGINT FK NULL | 기안 부서 | +| `current_step` | INT | 현재 결재 단계 번호 | +| `drafted_at` | TIMESTAMP NULL | 상신 일시 | +| `completed_at` | TIMESTAMP NULL | 완료 일시 | +| `recall_reason` | TEXT NULL | 회수 사유 | +| `parent_doc_id` | BIGINT FK NULL | 재기안 원본 문서 | +| `attachments` | JSON NULL | 첨부파일 | + +### 3.3 approval_steps (결재 단계) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `approval_id` | BIGINT FK | 결재 문서 | +| `step_order` | INT | 순서 (1, 2, 3...) | +| `step_type` | VARCHAR | `approval`, `agreement`, `reference` | +| `parallel_group` | INT NULL | 병렬 그룹 (Phase 3) | +| `approver_id` | BIGINT FK | 결재자 | +| `acted_by` | BIGINT FK NULL | 실제 처리자 (대결 시) | +| `approver_name` | VARCHAR | 결재자명 스냅샷 | +| `approver_department` | VARCHAR | 부서 스냅샷 | +| `approver_position` | VARCHAR | 직급 스냅샷 | +| `status` | VARCHAR(20) | 단계 상태 (5가지) | +| `approval_type` | VARCHAR(20) | `normal`, `pre_decided`, `delegated` | +| `comment` | TEXT NULL | 결재 의견 | +| `acted_at` | TIMESTAMP NULL | 처리 일시 | +| `is_read` | BOOLEAN | 참조 열람 여부 | +| `read_at` | TIMESTAMP NULL | 열람 일시 | + +### 3.4 approval_delegations (위임 설정, Phase 3) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | | +| `delegator_id` | BIGINT FK | 위임자 | +| `delegate_id` | BIGINT FK | 대리인 | +| `start_date` | DATE | 위임 시작일 | +| `end_date` | DATE | 위임 종료일 | +| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | +| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | +| `is_active` | BOOLEAN | 활성 여부 | +| `reason` | VARCHAR(200) | 위임 사유 | + +--- + +## 4. 상태 관리 + +### 4.1 문서 상태 (6가지) + +| 상태 | 코드 | 라벨 | 색상 | 설명 | +|------|------|------|------|------| +| 임시저장 | `draft` | 임시저장 | gray | 작성 중, 미상신 | +| 진행 | `pending` | 진행 | blue | 결재선 순환 중 | +| 완료 | `approved` | 완료 | green | 최종 승인 | +| 반려 | `rejected` | 반려 | red | 결재자가 반려 | +| 회수 | `cancelled` | 회수 | yellow | 기안자가 회수 | +| 보류 | `on_hold` | 보류 | amber | 결재자가 보류 | + +### 4.2 단계 상태 (5가지) + +| 상태 | 코드 | 라벨 | 아이콘 | 설명 | +|------|------|------|--------|------| +| 대기 | `pending` | 대기 | 숫자 | 차례 아직 아님 | +| 승인 | `approved` | 승인 | ✓ (녹색) | 승인 완료 | +| 반려 | `rejected` | 반려 | ✗ (적색) | 반려 | +| 건너뜀 | `skipped` | 건너뜀 | — (회색) | 전결/회수로 소멸 | +| 보류 | `on_hold` | 보류 | ⏸ (노란) | 보류 중 | + +### 4.3 결재 유형 (approval_type) + +| 유형 | 코드 | 아이콘 | 설명 | +|------|------|--------|------| +| 일반결재 | `normal` | ✓ | 기본 승인 | +| 전결 | `pre_decided` | ⚡ (남색) | 이후 단계 모두 건너뛰고 즉시 완료 | +| 대결 | `delegated` | — | 대리인이 처리 (Phase 3) | + +### 4.4 참여자 역할 (step_type) + +| 역할 | 코드 | 의사결정 | 설명 | +|------|------|---------|------| +| 결재 | `approval` | ✅ 있음 | 승인/반려/보류/전결 가능 | +| 합의 | `agreement` | ✅ 있음 | 타부서 동의 (승인/반려 가능) | +| 참조 | `reference` | ❌ 없음 | 열람만 가능, 열람 추적 | + +### 4.5 상태 전이 다이어그램 + +``` + ┌─────────────────────────────┐ + │ │ + ┌────────┐ submit() │ ┌─────────┐ │ + │ draft │────────────→│ │ pending │ │ + └────────┘ │ └────┬────┘ │ + ▲ │ │ │ + │ │ ┌────┼─────────┬───────┐ │ + │ (수정 후 재상신) │ │ │ │ │ │ + │ │ │ approve() reject() hold()│ + │ │ │ │ │ │ │ + │ │ │ ▼ ▼ ▼ │ + │ │ │ 다음 step rejected on_hold│ + │ │ │ 또는 │ │ │ + │ │ │ approved │ releaseHold() + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + │ │ └────┼────────┼───────┘ │ + │ │ │ │ │ + │ │ preDecide() │ │ + │ │ → approved │ │ + │ │ │ │ cancel() │ + │ │ │ │ │ │ + │ │ ▼ │ ▼ │ + │ │ ┌─────────┐ │ ┌──────────┐ + │ │ │approved │ │ │cancelled │ + │ │ └─────────┘ │ └──────────┘ + │ │ │ │ │ + │ │ │ │ │ + │ │ copyForRedraft() │ + │ │ │ │ │ + └───────────────────┼───────┴────────┘ │ + (새 draft 생성) │ │ + │ copyForRedraft() │ + │◀──────────────────────┘ + └─────────────────────────────┘ +``` + +--- + +## 5. 권한 매트릭스 + +### 5.1 누가 무엇을 할 수 있는가 + +| 액션 | 대상자 | 조건 | +|------|--------|------| +| **기안 작성** | 모든 사용자 | — | +| **수정** | 기안자 | `draft` 또는 `rejected` | +| **삭제** | 기안자 | `draft`만 | +| **상신** | 기안자 | `draft` 또는 `rejected`, 결재선 1명 이상 | +| **승인** | 현재 결재자 | `pending`, 자신이 현재 차례 | +| **반려** | 현재 결재자 | `pending`, 사유 필수 | +| **보류** | 현재 결재자 | `pending`, 사유 필수 | +| **보류 해제** | 보류한 결재자 | `on_hold`, 자신이 보류한 건 | +| **전결** | 현재 결재자 | `pending`, 이후 모든 단계 건너뜀 | +| **회수** | 기안자 | `pending` 또는 `on_hold`, 첫 결재자 미처리 | +| **복사 재기안** | 기안자 | `approved`, `rejected`, `cancelled` | +| **참조 열람** | 참조자 | `reference` step 보유 | + +### 5.2 회수 가능 조건 상세 + +``` +회수(cancel) 가능 여부 판단: + +1. 문서 상태가 pending 또는 on_hold인가? → 아니면 불가 +2. 요청자가 기안자(drafter_id)인가? → 아니면 불가 +3. 첫 번째 결재자(approval/agreement)의 상태가 pending 또는 on_hold인가? + → 이미 approved/rejected이면 불가 (첫 결재자가 이미 처리) +``` + +--- + +## 6. 메뉴 구조 + +``` +결재관리 +├── 기안함 /approval-mgmt/drafts ← 내가 기안한 문서 +├── 결재 대기함 /approval-mgmt/pending ← 내가 결재해야 할 문서 +├── 처리 완료함 /approval-mgmt/completed ← 내가 결재한 문서 +└── 참조함 /approval-mgmt/references ← 참조 문서 (열람 추적) +``` + +### 추가 페이지 + +| URL | 설명 | +|-----|------| +| `/approval-mgmt/create` | 기안 작성 | +| `/approval-mgmt/{id}` | 상세 조회 | +| `/approval-mgmt/{id}/edit` | 기안 수정 | + +--- + +## 7. 관련 문서 + +- [결재 양식 기술 명세](form-types.md) — 양식별 필드, JSON 구조, 인터랙션 +- [결재관리 워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [API 명세](api-reference.md) — 엔드포인트 목록 및 요청/응답 예시 +- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 +- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/approvals/api-reference.md b/features/approvals/api-reference.md new file mode 100644 index 0000000..b63e31b --- /dev/null +++ b/features/approvals/api-reference.md @@ -0,0 +1,594 @@ +# 결재관리 API 명세 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **Base URL**: `/api/admin/approvals` +> **미들웨어**: `web`, `auth`, `hq.member` +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +모든 API는 JSON 응답을 반환한다. 인증은 세션 기반이며, CSRF 토큰이 필요하다. + +### 1.1 공통 응답 형식 + +**성공:** + +```json +{ + "success": true, + "message": "처리 메시지", + "data": { ... } +} +``` + +**실패 (400):** + +```json +{ + "success": false, + "message": "에러 메시지" +} +``` + +### 1.2 공통 헤더 + +``` +Content-Type: application/json +Accept: application/json +X-CSRF-TOKEN: {csrf_token} +``` + +--- + +## 2. 목록 조회 API + +### 2.1 기안함 + +내가 기안한 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/drafts +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `status` | string | 상태 필터 (`draft`, `pending`, `approved`, `rejected`, `cancelled`, `on_hold`) | +| `is_urgent` | boolean | 긴급 문서만 | +| `date_from` | date | 시작일 (YYYY-MM-DD) | +| `date_to` | date | 종료일 (YYYY-MM-DD) | +| `per_page` | int | 페이지당 건수 (기본 15) | +| `page` | int | 페이지 번호 | + +**응답:** Laravel 페이지네이션 형식 + +```json +{ + "data": [ + { + "id": 1, + "document_number": "APR-260228-001", + "title": "휴가 신청", + "status": "pending", + "is_urgent": false, + "form": { "id": 1, "name": "휴가신청서" }, + "steps": [...], + "created_at": "2026-02-28T10:00:00", + "drafted_at": "2026-02-28T10:05:00" + } + ], + "current_page": 1, + "last_page": 3, + "per_page": 15, + "total": 42 +} +``` + +--- + +### 2.2 결재 대기함 + +내가 현재 결재해야 할 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/pending +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `is_urgent` | boolean | 긴급 문서만 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +> 현재 사용자가 결재 차례인 문서만 표시된다. 이미 승인/반려한 문서는 표시되지 않는다. + +--- + +### 2.3 처리 완료함 + +내가 승인 또는 반려한 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/completed +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `status` | string | 상태 필터 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +--- + +### 2.4 참조함 + +내가 참조자로 지정된 문서 목록을 조회한다. + +``` +GET /api/admin/approvals/references +``` + +**Query Parameters:** + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목/문서번호 검색 | +| `is_read` | string | 열람 상태 필터 (`true`=열람완료, `false`=미열람) | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +--- + +## 3. CRUD API + +### 3.1 상세 조회 + +``` +GET /api/admin/approvals/{id} +``` + +**응답:** + +```json +{ + "success": true, + "data": { + "id": 1, + "tenant_id": 1, + "document_number": "APR-260228-001", + "form_id": 1, + "line_id": null, + "title": "휴가 신청", + "content": {}, + "body": "2월 27일~28일 연차 사용 신청합니다.", + "status": "pending", + "is_urgent": false, + "drafter_id": 10, + "department_id": 3, + "current_step": 2, + "drafted_at": "2026-02-28T10:05:00", + "completed_at": null, + "recall_reason": null, + "parent_doc_id": null, + "form": { "id": 1, "name": "휴가신청서" }, + "drafter": { "id": 10, "name": "홍길동" }, + "line": null, + "steps": [ + { + "id": 1, + "step_order": 1, + "step_type": "approval", + "approver_id": 20, + "approver_name": "김과장", + "approver_department": "경영지원팀", + "approver_position": "과장", + "status": "approved", + "approval_type": "normal", + "comment": "승인합니다.", + "acted_at": "2026-02-28T11:00:00", + "is_read": false, + "read_at": null + }, + { + "id": 2, + "step_order": 2, + "step_type": "approval", + "approver_id": 30, + "approver_name": "박부장", + "approver_department": "경영지원팀", + "approver_position": "부장", + "status": "pending", + "approval_type": "normal", + "comment": null, + "acted_at": null, + "is_read": false, + "read_at": null + } + ] + } +} +``` + +--- + +### 3.2 생성 (임시저장) + +``` +POST /api/admin/approvals +``` + +**Request Body:** + +```json +{ + "form_id": 1, + "title": "휴가 신청", + "body": "2월 27일~28일 연차 사용", + "is_urgent": false, + "steps": [ + { "user_id": 20, "step_type": "approval" }, + { "user_id": 30, "step_type": "approval" }, + { "user_id": 40, "step_type": "reference" } + ] +} +``` + +**Validation:** + +| 필드 | 규칙 | +|------|------| +| `form_id` | required, exists:approval_forms,id | +| `title` | required, string, max:200 | +| `body` | nullable, string | +| `is_urgent` | boolean | +| `steps` | nullable, array | +| `steps.*.user_id` | required_with:steps, exists:users,id | +| `steps.*.step_type` | required_with:steps, in:approval,agreement,reference | + +**응답 (201):** + +```json +{ + "success": true, + "message": "결재 문서가 저장되었습니다.", + "data": { ... } +} +``` + +--- + +### 3.3 수정 + +``` +PUT /api/admin/approvals/{id} +``` + +> `draft` 또는 `rejected` 상태에서만 수정 가능 + +**Request Body:** (생성과 동일, 모든 필드 선택) + +**Validation:** + +| 필드 | 규칙 | +|------|------| +| `title` | sometimes, string, max:200 | +| `body` | nullable, string | +| `is_urgent` | boolean | +| `steps` | nullable, array | + +--- + +### 3.4 삭제 + +``` +DELETE /api/admin/approvals/{id} +``` + +> `draft` 상태에서만 삭제 가능 + +**응답:** + +```json +{ + "success": true, + "message": "결재 문서가 삭제되었습니다." +} +``` + +--- + +## 4. 워크플로우 API + +### 4.1 상신 + +``` +POST /api/admin/approvals/{id}/submit +``` + +> 기안자가 `draft`/`rejected` 문서를 결재 요청한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "결재가 상신되었습니다.", "data": {...} }` + +--- + +### 4.2 승인 + +``` +POST /api/admin/approvals/{id}/approve +``` + +> 현재 결재자가 승인한다. + +**Request Body:** + +```json +{ + "comment": "승인합니다." // 선택 +} +``` + +**응답:** `{ "success": true, "message": "승인되었습니다.", "data": {...} }` + +--- + +### 4.3 반려 + +``` +POST /api/admin/approvals/{id}/reject +``` + +> 현재 결재자가 반려한다. 사유 필수. + +**Request Body:** + +```json +{ + "comment": "예산 초과로 반려합니다." // 필수 +} +``` + +**Validation:** `comment` — required, string, max:1000 + +**응답:** `{ "success": true, "message": "반려되었습니다.", "data": {...} }` + +--- + +### 4.4 회수 + +``` +POST /api/admin/approvals/{id}/cancel +``` + +> 기안자가 `pending`/`on_hold` 문서를 회수한다. 첫 결재자 미처리 시에만 가능. + +**Request Body:** + +```json +{ + "recall_reason": "내용 수정 필요" // 선택 +} +``` + +**응답:** `{ "success": true, "message": "결재가 회수되었습니다.", "data": {...} }` + +--- + +### 4.5 보류 + +``` +POST /api/admin/approvals/{id}/hold +``` + +> 현재 결재자가 결재를 보류한다. 사유 필수. + +**Request Body:** + +```json +{ + "comment": "추가 자료 검토 필요" // 필수 +} +``` + +**Validation:** `comment` — required, string, max:1000 + +**응답:** `{ "success": true, "message": "보류되었습니다.", "data": {...} }` + +--- + +### 4.6 보류 해제 + +``` +POST /api/admin/approvals/{id}/release-hold +``` + +> 보류한 결재자가 보류를 해제한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "보류가 해제되었습니다.", "data": {...} }` + +--- + +### 4.7 전결 + +``` +POST /api/admin/approvals/{id}/pre-decide +``` + +> 현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인한다. + +**Request Body:** + +```json +{ + "comment": "전결 처리합니다." // 선택 +} +``` + +**응답:** `{ "success": true, "message": "전결 처리되었습니다.", "data": {...} }` + +--- + +### 4.8 복사 재기안 + +``` +POST /api/admin/approvals/{id}/copy +``` + +> 기안자가 `approved`/`rejected`/`cancelled` 문서를 복사하여 새 draft를 생성한다. + +**Request Body:** 없음 + +**응답:** + +```json +{ + "success": true, + "message": "문서가 복사되었습니다.", + "data": { + "id": 15, + "document_number": "APR-260228-003", + "parent_doc_id": 1, + "status": "draft", + ... + } +} +``` + +> 응답의 `data.id`를 사용하여 `/approval-mgmt/{id}/edit`로 이동한다. + +--- + +### 4.9 참조 열람 추적 + +``` +POST /api/admin/approvals/{id}/mark-read +``` + +> 참조자가 문서를 열람했음을 기록한다. + +**Request Body:** 없음 + +**응답:** `{ "success": true, "message": "열람 처리되었습니다." }` + +--- + +## 5. 유틸리티 API + +### 5.1 결재선 템플릿 목록 + +``` +GET /api/admin/approvals/lines +``` + +**응답:** + +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "일반 결재선", "steps": [...] } + ] +} +``` + +--- + +### 5.2 양식 목록 + +``` +GET /api/admin/approvals/forms +``` + +**응답:** + +```json +{ + "success": true, + "data": [ + { "id": 1, "name": "휴가신청서", "is_active": true } + ] +} +``` + +--- + +### 5.3 미처리 건수 (뱃지) + +``` +GET /api/admin/approvals/badge-counts +``` + +**응답:** + +```json +{ + "success": true, + "data": { + "pending": 3, + "draft": 1, + "reference_unread": 5 + } +} +``` + +| 필드 | 설명 | +|------|------| +| `pending` | 내가 결재해야 할 문서 수 | +| `draft` | 내 임시저장 문서 수 | +| `reference_unread` | 미열람 참조 문서 수 | + +--- + +## 6. 라우트 전체 목록 + +| Method | Path | 컨트롤러 메서드 | 이름 | 설명 | +|--------|------|---------------|------|------| +| GET | `/drafts` | `drafts` | `drafts` | 기안함 | +| GET | `/pending` | `pending` | `pending` | 결재 대기함 | +| GET | `/completed` | `completed` | `completed` | 처리 완료함 | +| GET | `/references` | `references` | `references` | 참조함 | +| GET | `/lines` | `lines` | `lines` | 결재선 템플릿 | +| GET | `/forms` | `forms` | `forms` | 양식 목록 | +| GET | `/badge-counts` | `badgeCounts` | `badge-counts` | 뱃지 건수 | +| POST | `/` | `store` | `store` | 생성 | +| GET | `/{id}` | `show` | `show` | 상세 | +| PUT | `/{id}` | `update` | `update` | 수정 | +| DELETE | `/{id}` | `destroy` | `destroy` | 삭제 | +| POST | `/{id}/submit` | `submit` | `submit` | 상신 | +| POST | `/{id}/approve` | `approve` | `approve` | 승인 | +| POST | `/{id}/reject` | `reject` | `reject` | 반려 | +| POST | `/{id}/cancel` | `cancel` | `cancel` | 회수 | +| POST | `/{id}/hold` | `hold` | `hold` | 보류 | +| POST | `/{id}/release-hold` | `releaseHold` | `release-hold` | 보류 해제 | +| POST | `/{id}/pre-decide` | `preDecide` | `pre-decide` | 전결 | +| POST | `/{id}/copy` | `copyForRedraft` | `copy` | 복사 재기안 | +| POST | `/{id}/mark-read` | `markAsRead` | `mark-read` | 열람 추적 | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [UI 화면 구성](ui-screens.md) — 화면별 동작 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/approvals/db-changes-and-model-sync.md b/features/approvals/db-changes-and-model-sync.md new file mode 100644 index 0000000..e59cd2b --- /dev/null +++ b/features/approvals/db-changes-and-model-sync.md @@ -0,0 +1,286 @@ +# 결재관리 DB 변경사항 및 API 모델 동기화 현황 + +> **작성일**: 2026-03-09 +> **상태**: 조사 완료 +> **관련**: [README.md](README.md) | [API 명세](api-reference.md) + +--- + +## 1. 개요 + +### 1.1 목적 + +2026-02-27 ~ 2026-03-05 기간에 결재관리 테이블에 대규모 컬럼 추가가 이루어졌다. 이 문서는 변경된 DB 스키마와 API/MNG 프로젝트 간 모델 동기화 상태를 기록한다. + +### 1.2 핵심 발견 + +- 마이그레이션 **15개** 실행 (API 프로젝트에서 관리) +- MNG 모델: ✅ 모든 신규 컬럼 반영 완료 +- API 모델: ❌ **`$fillable`/`$casts` 미반영** — 오류 원인 가능성 + +--- + +## 2. 마이그레이션 변경 타임라인 + +### 2.1 Phase 2 확장 (2026-02-27) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_columns_to_approvals_table` | `approvals` | `line_id`, `body`, `is_urgent`, `department_id` 추가 | +| `add_columns_to_approval_steps_table` | `approval_steps` | `approver_name`, `approver_department`, `approver_position` 추가 | +| `add_phase2_columns_to_approval_steps_table` | `approval_steps` | `parallel_group`, `acted_by`, `approval_type` 추가 | +| `add_phase2_columns_to_approvals_table` | `approvals` | `recall_reason`, `parent_doc_id` 추가 | +| `create_approval_delegations_table` | `approval_delegations` | 위임 테이블 신규 생성 | +| `add_linkable_to_approvals_table` | `approvals` | `linkable_type`, `linkable_id` 추가 (다형성) | + +### 2.2 도메인 연동 (2026-02-28) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_approval_id_to_leaves_table` | `leaves` | `approval_id` FK 추가 | +| `insert_leave_approval_form` | `approval_forms` | 휴가신청 양식 데이터 등록 | + +### 2.3 양식 확장 (2026-03-03 ~ 03-04) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `insert_attendance_approval_forms` | `approval_forms` | 근태신청, 사유서 양식 등록 | +| `add_body_template_to_approval_forms` | `approval_forms` | `body_template` 컬럼 추가 | +| `insert_expense_approval_form` | `approval_forms` | 지출결의서 양식 + body_template 등록 | +| `update_expense_approval_form_body_template` | `approval_forms` | 지출결의서 body_template 고도화 | + +### 2.4 추적 기능 (2026-03-05) + +| 마이그레이션 파일 | 대상 테이블 | 작업 | +|------------------|-----------|------| +| `add_drafter_read_at_to_approvals_table` | `approvals` | `drafter_read_at` 추가 | +| `add_resubmit_count_to_approvals_table` | `approvals` | `resubmit_count` 추가 | +| `add_rejection_history_to_approvals_table` | `approvals` | `rejection_history` 추가 | + +--- + +## 3. 추가된 컬럼 상세 + +### 3.1 `approvals` 테이블 (11개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `line_id` | BIGINT FK NULL | NULL | 02-27 | 결재선 템플릿 참조 | +| `body` | LONGTEXT NULL | NULL | 02-27 | 문서 본문 HTML | +| `is_urgent` | BOOLEAN | false | 02-27 | 긴급 여부 | +| `department_id` | BIGINT NULL | NULL | 02-27 | 기안 부서 | +| `recall_reason` | TEXT NULL | NULL | 02-27 | 회수 사유 | +| `parent_doc_id` | BIGINT FK NULL | NULL | 02-27 | 재기안 원본 문서 | +| `linkable_type` | VARCHAR NULL | NULL | 02-27 | 다형성 모델 타입 | +| `linkable_id` | BIGINT NULL | NULL | 02-27 | 다형성 모델 ID | +| `drafter_read_at` | TIMESTAMP NULL | NULL | 03-05 | 기안자 열람 시각 | +| `resubmit_count` | TINYINT UNSIGNED | 0 | 03-05 | 재상신 횟수 | +| `rejection_history` | JSON NULL | NULL | 03-05 | 반려 이력 배열 | + +### 3.2 `approval_steps` 테이블 (6개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `approver_name` | VARCHAR(50) NULL | NULL | 02-27 | 결재자명 스냅샷 | +| `approver_department` | VARCHAR(100) NULL | NULL | 02-27 | 결재자 부서 스냅샷 | +| `approver_position` | VARCHAR(50) NULL | NULL | 02-27 | 결재자 직급 스냅샷 | +| `parallel_group` | INT NULL | NULL | 02-27 | 병렬 결재 그룹 (Phase 3) | +| `acted_by` | BIGINT FK NULL | NULL | 02-27 | 실제 처리자 (대결) | +| `approval_type` | VARCHAR(20) | 'normal' | 02-27 | normal/pre_decided/delegated | + +### 3.3 `approval_forms` 테이블 (1개 컬럼 추가) + +| 컬럼 | 타입 | 기본값 | 추가일 | 용도 | +|------|------|--------|--------|------| +| `body_template` | TEXT NULL | NULL | 03-04 | HTML 양식 렌더링 템플릿 | + +### 3.4 `approval_delegations` 테이블 (신규 생성) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `delegator_id` | BIGINT FK | 위임자 | +| `delegate_id` | BIGINT FK | 대리인 | +| `start_date` | DATE | 위임 시작일 | +| `end_date` | DATE | 위임 종료일 | +| `form_ids` | JSON NULL | 대상 양식 (NULL=전체) | +| `notify_delegator` | BOOLEAN | 대결 시 보고 여부 | +| `is_active` | BOOLEAN | 활성 여부 | +| `reason` | VARCHAR(200) | 위임 사유 | + +--- + +## 4. API/MNG 모델 동기화 현황 + +### 4.1 Approval 모델 비교 + +| 항목 | MNG (`mng/app/Models/Approvals/Approval.php`) | API (`api/app/Models/Tenants/Approval.php`) | +|------|:---:|:---:| +| `line_id` in $fillable | ✅ | ❌ | +| `body` in $fillable | ✅ | ❌ | +| `is_urgent` in $fillable/$casts | ✅ boolean | ❌ | +| `department_id` in $fillable | ✅ | ❌ | +| `recall_reason` in $fillable | ✅ | ❌ | +| `parent_doc_id` in $fillable | ✅ | ❌ | +| `linkable_type/id` in $fillable | ✅ | ✅ | +| `drafter_read_at` in $fillable/$casts | ✅ datetime | ❌ | +| `resubmit_count` in $fillable/$casts | ✅ integer | ❌ | +| `rejection_history` in $fillable/$casts | ✅ array | ❌ | + +### 4.2 ApprovalStep 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `approver_name` in $fillable | ✅ | ❌ | +| `approver_department` in $fillable | ✅ | ❌ | +| `approver_position` in $fillable | ✅ | ❌ | +| `parallel_group` in $fillable | ✅ | ❌ | +| `acted_by` in $fillable | ✅ | ❌ | +| `approval_type` in $fillable | ✅ | ❌ | + +### 4.3 ApprovalForm 모델 비교 + +| 항목 | MNG | API | +|------|:---:|:---:| +| `body_template` in $fillable | ✅ | ❌ | + +### 4.4 ApprovalDelegation 모델 + +| 항목 | MNG | API | +|------|:---:|:---:| +| 모델 파일 존재 | ✅ | ❌ 미생성 | + +--- + +## 5. 오류 영향 분석 + +### 5.1 API 모델 미반영으로 인한 잠재적 오류 + +API 프로젝트의 모델 `$fillable`에 신규 컬럼이 누락되어, API 엔드포인트를 통한 결재 문서 처리 시 다음 오류가 발생할 수 있다: + +| 증상 | 원인 | 영향 범위 | +|------|------|----------| +| `create()`/`update()` 시 신규 필드 저장 안 됨 | `$fillable` 미포함 → mass assignment 차단 | API v1 결재 CRUD | +| JSON 필드(`rejection_history`) 문자열로 반환 | `$casts` 미정의 → 타입 변환 안 됨 | API 응답 파싱 오류 | +| `drafter_read_at` 날짜 비교 실패 | `$casts` datetime 미정의 → Carbon 미변환 | 열람 추적 기능 | +| `is_urgent` 비교 오류 | `$casts` boolean 미정의 → 문자열 비교 | 긴급 필터링 | +| 위임(delegation) 기능 완전 불가 | 모델 자체 미생성 | Phase 3 기능 전체 | + +### 5.2 MNG는 정상 + +MNG 프로젝트의 모델은 모든 신규 컬럼이 `$fillable`, `$casts`, `$attributes`에 반영되어 있으며, `ApprovalService`에서 정상 사용 중이다. + +``` +MNG 정상 동작 확인 기능: +✅ 반려 이력 저장 (rejection_history) +✅ 재상신 횟수 추적 (resubmit_count) +✅ 기안자 열람 추적 (drafter_read_at) +✅ 결재자 스냅샷 저장 (approver_name/department/position) +✅ 전결 처리 (approval_type = pre_decided) +✅ 회수 사유 기록 (recall_reason) +``` + +--- + +## 6. 수정 필요 파일 목록 + +### 6.1 API 모델 업데이트 필요 + +| 파일 | 수정 내용 | +|------|----------| +| `api/app/Models/Tenants/Approval.php` | `$fillable`에 9개 필드, `$casts`에 4개 필드 추가 | +| `api/app/Models/Tenants/ApprovalStep.php` | `$fillable`에 6개 필드 추가 | +| `api/app/Models/Tenants/ApprovalForm.php` | `$fillable`에 `body_template` 추가 | +| `api/app/Models/Tenants/ApprovalDelegation.php` | 모델 파일 신규 생성 | + +### 6.2 Approval.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'line_id', +'body', +'is_urgent', +'department_id', +'recall_reason', +'parent_doc_id', +'drafter_read_at', +'resubmit_count', +'rejection_history', +``` + +**`$casts` 추가 필요:** + +```php +'drafter_read_at' => 'datetime', +'resubmit_count' => 'integer', +'rejection_history' => 'array', +'is_urgent' => 'boolean', +``` + +### 6.3 ApprovalStep.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'approver_name', +'approver_department', +'approver_position', +'parallel_group', +'acted_by', +'approval_type', +``` + +### 6.4 ApprovalForm.php 수정 상세 + +**`$fillable` 추가 필요:** + +```php +'body_template', +``` + +--- + +## 7. 연관 테이블 참조 변경 + +결재 시스템과 연동된 다른 테이블의 변경사항: + +| 테이블 | 추가 컬럼 | 추가일 | 용도 | +|--------|----------|--------|------| +| `leaves` | `approval_id` (BIGINT FK) | 02-28 | 휴가 ↔ 결재 연동 | +| `purchases` | `approval_id` (BIGINT FK) | (기존) | 구매 ↔ 결재 연동 | + +--- + +## 8. 등록된 결재 양식 (13종) + +2026-02-28 ~ 03-07 기간에 마이그레이션으로 등록된 양식: + +| 코드 | 양식명 | 카테고리 | 등록일 | +|------|--------|---------|--------| +| `leave` | 휴가신청서 | request | 02-28 | +| `attendance_request` | 근태신청서 | request | 03-03 | +| `reason_report` | 사유서 | request | 03-03 | +| `expense` | 지출결의서 | expense | 03-04 | +| `employment_cert` | 재직증명서 | request | 03-05 | +| `career_cert` | 경력증명서 | request | 03-05 | +| `appointment_cert` | 위촉증명서 | request | 03-05 | +| `resignation` | 사직서 | request | 03-06 | +| `seal_usage` | 사용인감계 | request | 03-06 | +| `delegation` | 위임장 | request | 03-06 | +| `board_minutes` | 이사회의사록 | request | 03-06 | +| `quotation` | 견적서 | request | 03-06 | +| `official_letter` | 공문서 | request | 03-07 | + +--- + +## 관련 문서 + +- [결재관리 시스템 개요](README.md) — 아키텍처, 상태 관리, 권한 +- [API 명세](api-reference.md) — 20개 엔드포인트 상세 +- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 +- [기획서 원본](../../plans/approval-management-system-plan.md) — Phase 1~4 전체 기획 + +--- + +**최종 업데이트**: 2026-03-09 diff --git a/features/approvals/form-types.md b/features/approvals/form-types.md new file mode 100644 index 0000000..3242b78 --- /dev/null +++ b/features/approvals/form-types.md @@ -0,0 +1,999 @@ +# 결재 양식 기술 명세 + +> **작성일**: 2026-03-06 +> **상태**: Phase 2 구현 완료 +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM MNG 결재관리의 **기안함 양식** 기술 명세. 각 양식의 필드 구조, JSON Content 데이터 형식, UI 인터랙션, 특수 로직을 정의한다. + +### 1.2 양식 목록 + +| 코드 | 양식명 | 분류 | Blade 파일 | 설명 | +|------|--------|------|------------|------| +| `BUSINESS_DRAFT` | 업무기안서 | 일반 | (body 편집기) | 일반 업무 보고·요청 | +| `leave` | 휴가신청 | 인사/근태 | `_leave-form.blade.php` | 연차, 휴가, 근태 신청 | +| `attendance_request` | 근태신청 | 인사/근태 | `_leave-form.blade.php` | 외근, 출장, 조퇴 등 | +| `reason_report` | 사유서 | 인사/근태 | `_leave-form.blade.php` | 지각, 결근 등 사유 소명 | +| `resignation` | 사직서 | 인사/근태 | `_resignation-form.blade.php` | 퇴직 서류 | +| `employment_cert` | 재직증명서 | 증명서 | `_certificate-form.blade.php` | 재직 증명 발급 (PDF) | +| `career_cert` | 경력증명서 | 증명서 | `_career-cert-form.blade.php` | 경력 증명 발급 (PDF) | +| `appointment_cert` | 위촉증명서 | 증명서 | `_appointment-cert-form.blade.php` | 위촉/임명 증명 발급 (PDF) | +| `pr_expense` | 지출품의서 | 품의 | `_purchase-request-form.blade.php` | 지출 전 사전 승인 | +| `pr_contract` | 계약체결품의서 | 품의 | `_purchase-request-form.blade.php` | 계약 체결 전 승인 | +| `pr_purchase` | 구매품의서 | 품의 | `_purchase-request-form.blade.php` | 물품 구매 전 승인 | +| `pr_trip` | 출장품의서 | 품의 | `_purchase-request-form.blade.php` | 출장 계획 승인 | +| `pr_settlement` | 비용정산품의서 | 품의 | `_purchase-request-form.blade.php` | 비용 정산 승인 | +| `expense` | 지출결의서 | 재무 | `_expense-form.blade.php` | 법인카드/송금/자동이체 지출 | + +### 1.3 공통 구조 + +모든 양식은 동일한 패턴으로 동작한다: + +``` +양식 선택 (form_id) + ↓ +양식별 Blade 파셜 렌더링 (create.blade.php 내 조건부 display) + ↓ +사용자 입력 → Alpine.js / JavaScript 인터랙션 + ↓ +getFormData() → JSON content 생성 + ↓ +ApprovalService::createApproval() → Approval.content (JSON 컬럼) 저장 +``` + +### 1.4 양식 선택 UI (2단계 분류 + 설명 카드) + +양식 선택은 **2단계 드롭다운 + 설명 카드** 레이아웃으로 구성된다. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ 양식 * │ +│ ┌──── 30% ────────┐ ┌─────────────── 70% ───────────────────────────┐ │ +│ │ 📋 품의 ▼ │ │ ┌─────────────────────────────────────────┐ │ │ +│ │ │ │ │ 📋 지출품의서 │ │ │ +│ │ 지출품의서 ▼ │ │ │ 지출이 발생하기 전 사전 승인을 받는 │ │ │ +│ │ │ │ │ 문서입니다. 예산 범위 내에서 지출 항목과 │ │ │ +│ │ │ │ │ 금액을 기재하여 사전에 승락을 받습니다. │ │ │ +│ └──────────────────┘ │ └─────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +#### 1단계: 분류 선택 (`form_category`) + +| 분류 | 아이콘 | 포함 양식 | +|------|--------|----------| +| 일반 | 📄 | 업무기안서 | +| 인사/근태 | 👤 | 휴가신청, 근태신청, 사유서, 사직서 | +| 증명서 | 📜 | 재직증명서, 경력증명서, 위촉증명서 | +| 품의 | 📋 | 지출품의서, 계약체결품의서, 구매품의서, 출장품의서, 비용정산품의서 | +| 재무 | 💰 | 지출결의서 | + +#### 2단계: 양식 선택 (`form_id`) + +- 1단계 분류 선택 시 해당 분류에 속하지 않는 양식은 `display:none` + `disabled` +- 분류 내 첫 번째 양식 자동 선택 + +#### 설명 카드 (`formDescriptions`) + +- 양식 선택 시 우측에 해당 양식의 아이콘/제목/설명 텍스트 표시 +- 14종 전체 양식에 대한 설명 정의 (create/edit 공통) +- 색상: 양식별 Tailwind 테마 색상 (`border-*-200 bg-*-50`) + +#### 핵심 JavaScript 함수 + +| 함수 | 설명 | +|------|------| +| `buildCategoryOptions()` | 사용 가능한 카테고리만 `form_category` 옵션으로 생성 | +| `filterFormsByCategory(cat)` | 선택된 분류 외 양식 옵션 숨김/비활성화 | +| `selectCategoryByFormId(formId)` | formId로 카테고리 역산하여 자동 선택 | +| `updateFormDescription(formId)` | 설명 카드 DOM 업데이트 | + +### 1.5 파일 구조 + +``` +resources/views/approvals/ +├── create.blade.php ← 기안 작성 (2단계 양식 선택 + 설명 카드 + 동적 폼) +├── edit.blade.php ← 기안 수정 (create와 동일한 2단계 선택 구조) +├── show.blade.php ← 상세 조회 (양식별 조회 컴포넌트) +└── partials/ + ├── _leave-form.blade.php ← 휴가신청 폼 + ├── _expense-form.blade.php ← 지출결의서 폼 + ├── _expense-show.blade.php ← 지출결의서 조회 + ├── _purchase-request-form.blade.php ← 품의서 5종 통합 폼 (Alpine.js) + ├── _purchase-request-show.blade.php ← 품의서 5종 통합 조회 + ├── _certificate-form.blade.php ← 재직증명서 폼 + ├── _certificate-show.blade.php ← 재직증명서 조회 + ├── _career-cert-form.blade.php ← 경력증명서 폼 + ├── _career-cert-show.blade.php ← 경력증명서 조회 + ├── _appointment-cert-form.blade.php ← 위촉증명서 폼 + ├── _appointment-cert-show.blade.php ← 위촉증명서 조회 + ├── _resignation-form.blade.php ← 사직서 폼 + ├── _resignation-show.blade.php ← 사직서 조회 + ├── _approval-stamp-table.blade.php ← 결재 도장 테이블 + └── _approval-line-editor.blade.php ← 결재선 편집기 +``` + +--- + +## 2. 휴가신청 (leave) + +### 2.1 폼 필드 + +| 필드 ID | 라벨 | 타입 | 필수 | 기본값 | 설명 | +|---------|------|------|------|--------|------| +| `leave-user-id` | 신청자 | select | 필수 | `auth()->id()` | 활성 사원 목록 | +| `leave-type` | 유형 | select | 필수 | - | 휴가/근태신청/사유서 | +| `leave-start-date` | 시작일 | date | 필수 | - | `YYYY-MM-DD` | +| `leave-end-date` | 종료일 | date | 필수 | - | `YYYY-MM-DD` | +| `leave-reason` | 사유 | textarea | 선택 | - | 자유 텍스트 | + +### 2.2 Content JSON + +```json +{ + "user_id": "10", + "leave_type": "연차", + "start_date": "2026-03-06", + "end_date": "2026-03-07", + "reason": "개인 사유" +} +``` + +### 2.3 특수 로직 + +- **자동 선택**: 로그인 사용자가 기본 선택 (`auth()->id()`) +- **직원 목록**: `$employees` Props로 전달 (활성 사원만) +- **단순 구조**: Alpine.js 없이 Blade 폼으로 구현 + +--- + +## 3. 지출결의서 (expense) + +### 3.1 폼 구조 (Alpine.js 기반) + +```javascript +x-data="expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData)" +``` + +### 3.2 기본 정보 필드 + +| 필드 | 라벨 | 타입 | 필수 | 기본값 | +|------|------|------|------|--------| +| `expense_type` | 지출형식 | radio | 필수 | `corporate_card` | +| `tax_invoice` | 세금계산서 | radio | 필수 | `normal` | +| `write_date` | 작성일자 | date | 선택 | 오늘 | +| `approval_date` | 결재일자 | date | 선택 | 오늘 | +| `department` | 부서 | text | 선택 | `경리부` | +| `writer_name` | 이름 | text | 선택 | 인증 사용자명 | + +### 3.3 지출형식별 선택 + +| 지출형식 | 코드 | 연결 데이터 | +|---------|------|------------| +| 법인카드 | `corporate_card` | `$cards` → `selected_card` | +| 송금 | `transfer` | `$accounts` → `selected_account` | +| 자동이체 출금 | `auto_transfer` | `$accounts` → `selected_account` | +| 현금/가지급정산 | `cash_advance` | 없음 | + +**법인카드 선택 시 저장 구조:** + +```json +{ + "selected_card": { + "id": 1, + "card_name": "삼성카드", + "card_company": "삼성", + "card_number_last4": "1234", + "card_holder_name": "홍길동" + } +} +``` + +**계좌 선택 시 저장 구조:** + +```json +{ + "selected_account": { + "id": 1, + "bank_name": "국민은행", + "account_number": "123-456-789012", + "account_holder": "주일기업" + } +} +``` + +### 3.4 세금계산서 옵션 + +| 옵션 | 코드 | +|------|------| +| 일반 | `normal` | +| 이월발행 | `deferred` | +| 없음 | `none` | + +### 3.5 내역 테이블 + +**동적 rows** (`.items` 배열): + +| 필드 | 라벨 | 타입 | 설명 | +|------|------|------|------| +| `date` | 일자 | date | `YYYY-MM-DD` | +| `description` | 적요 | text | 지출 설명 | +| `amount` | 금액 | number | 콤마 제거 정수 | +| `vendor` | 거래처 | text | Autocomplete 검색 | +| `vendor_id` | 거래처 ID | hidden | API 연결 ID | +| `vendor_biz_no` | 사업자번호 | hidden | 자동 채움 | +| `bank` | 은행명 | text | 수동 입력 | +| `account_no` | 계좌번호 | text | 수동 입력 | +| `depositor` | 예금주 | text | 수동 입력 | +| `remark` | 비고 | text | 메모 | + +### 3.6 Content JSON (전체) + +```json +{ + "expense_type": "corporate_card", + "tax_invoice": "normal", + "write_date": "2026-03-06", + "approval_date": "2026-03-06", + "department": "경리부", + "writer_name": "홍길동", + "items": [ + { + "date": "2026-03-05", + "description": "사무용품 구매", + "amount": 150000, + "vendor": "오피스디포", + "vendor_id": 123, + "vendor_biz_no": "123-45-67890", + "bank": "", + "account_no": "", + "depositor": "", + "remark": "" + } + ], + "total_amount": 150000, + "attachment_memo": "영수증 첨부", + "selected_card": { ... }, + "selected_account": null +} +``` + +### 3.7 특수 기능 + +#### 거래처 검색 (Autocomplete) + +``` +입력 → 250ms 디바운싱 → API 호출 → 드롭다운 렌더링 + +API: /barobill/tax-invoice/search-partners?keyword=... +키보드: ↑↓(네비게이션), Enter(선택), Esc(닫기) +마우스: 항목 클릭(선택) +``` + +#### 금액 입력 포맷팅 + +``` +입력 시: 콤마 제거 → 정수 저장 (parseMoney) +표시 시: 콤마 포맷 (formatMoney) +합계: totalAmount getter → footer 실시간 업데이트 +``` + +#### 파일 업로드 + +``` +드래그 앤 드롭 + 파일 입력 +최대: 20MB +형식: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, jpeg, png, gif, zip, rar +API: POST /api/admin/approvals/upload-file +진행률: XHR 업로드 이벤트 +``` + +#### 카드/계좌 연동 + +``` +카드 선택 → 모든 내역 행에 "결제카드" 자동 표시 +계좌 선택 → 모든 내역 행에 "은행/계좌/예금주" 자동 채움 +``` + +### 3.8 조회 화면 (_expense-show.blade.php) + +| 섹션 | 내용 | +|------|------| +| 기본 정보 | 지출형식, 세금계산서, 작성일, 결재일, 부서, 이름 | +| 선택 카드/계좌 | 유색 박스로 표시 | +| 내역 테이블 | 읽기 전용, `number_format()` 금액 | +| 첨부서류 메모 | `whitespace-pre-wrap` | +| 첨부파일 목록 | 다운로드 링크 + 파일 크기 | + +--- + +## 4. 증명서 양식 공통 + +### 4.1 공통 패턴 + +모든 증명서 양식은 동일한 패턴을 따른다: + +``` +사원 선택 → loadXxxInfo(userId) → API 호출 → 읽기 전용 필드 자동 채움 + ↓ + 일부 필드만 수정 가능 + ↓ + 미리보기 모달 (인쇄 가능) +``` + +### 4.2 공통 함수 + +| 함수 | 설명 | +|------|------| +| `loadXxxInfo(userId)` | 사원 선택 시 인적/재직 정보 로드 | +| `openXxxPreview()` | 미리보기 모달 열기 | +| `printXxxPreview()` | 미리보기 인쇄 (`window.print()`) | +| `closeXxxPreview()` | 미리보기 닫기 | +| `onXxxPurposeChange()` | 용도 선택 시 직접입력 필드 표시 | + +### 4.3 조회 화면 공통 + +- 읽기 전용 필드 표시 +- PDF 다운로드: `route('api.admin.approvals.cert-pdf', $approval->id)` + +--- + +## 5. 재직증명서 (employment_cert) + +### 5.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `cert-name` | 성명 | text | readonly | DB 자동 채움 | +| | `cert-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | +| | `cert-address` | 주소 | text | editable | 직접 입력 | +| 재직사항 | `cert-company` | 회사명 | text | readonly | DB 자동 채움 | +| | `cert-business-num` | 사업자번호 | text | readonly | DB 자동 채움 | +| | `cert-department` | 근무부서 | text | readonly | DB 자동 채움 | +| | `cert-position` | 직급 | text | readonly | DB 자동 채움 | +| | `cert-hire-date` | 재직기간 | text | readonly | DB 자동 채움 | +| 발급정보 | `cert-purpose-select` | 사용용도 | select | editable | 드롭다운 선택 | +| | (custom) | 기타 용도 | text | editable | "기타" 선택 시 표시 | +| | `cert-issue-date` | 발급일 | text | readonly | `now()->format('Y-m-d')` | + +### 5.2 Content JSON + +```json +{ + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "address": "서울특별시 강남구", + "company_name": "(주)코드브릿지엑스", + "business_num": "123-45-67890", + "department": "개발팀", + "position": "과장", + "hire_date": "2020-03-01", + "purpose": "은행 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 6. 경력증명서 (career_cert) + +### 6.1 폼 필드 (재직증명서 대비 추가/변경) + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `cc-birth-date` | 생년월일 | text | readonly | DB 자동 채움 | +| 경력사항 | `cc-ceo-name` | 대표자 | text | readonly | DB 자동 채움 | +| | `cc-phone` | 대표전화 | text | readonly | DB 자동 채움 | +| | `cc-company-address` | 소재지 | text | readonly | DB 자동 채움 | +| | `cc-department` | 소속부서 | text | readonly | DB 자동 채움 | +| | `cc-position` | 직위/직급 | text | readonly | DB 자동 채움 | +| | `cc-hire-date` | 근무기간 시작 | text | readonly | DB 자동 채움 | +| | `cc-resign-date` | 근무기간 종료 | date | editable | 직접 입력 | +| | `cc-job-description` | 담당업무 | text | editable | 직접 입력 | +| 발급정보 | 용도 | select | editable | + "이직 제출용" 옵션 | + +### 6.2 Content JSON + +```json +{ + "name": "홍길동", + "birth_date": "1990-01-01", + "address": "서울특별시 강남구", + "company_name": "(주)코드브릿지엑스", + "business_num": "123-45-67890", + "ceo_name": "김대표", + "phone": "02-1234-5678", + "company_address": "서울특별시 강남구 테헤란로", + "department": "개발팀", + "position": "과장", + "hire_date": "2020-03-01", + "resign_date": "2026-02-28", + "job_description": "웹 애플리케이션 개발", + "purpose": "이직 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 7. 위촉증명서 (appointment_cert) + +### 7.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 | +|------|---------|------|------|------|------| +| 인적사항 | `ac-name` | 성명 | text | readonly | DB 자동 채움 | +| | `ac-resident` | 주민등록번호 | text | readonly | DB 자동 채움 | +| | `ac-department` | 소속 | text | readonly | DB 자동 채움 | +| | `ac-phone` | 연락처 | text | editable | 직접 입력 | +| 위촉정보 | `ac-hire-date` | 위촉기간 시작 | text | readonly | DB 자동 채움 | +| | `ac-resign-date` | 위촉기간 종료 | date | editable | 직접 입력 | +| | `ac-contract-type` | 계약자격 | text | editable | 직접 입력 | +| 발급정보 | `ac-purpose-select` | 용도 | select | editable | 드롭다운 선택 | +| | `ac-issue-date` | 발급일 | text | readonly | 자동 설정 | +| (숨김) | `ac-company-name` | 회사명 | hidden | - | 미리보기용 | +| | `ac-ceo-name` | 대표자명 | hidden | - | 미리보기용 | + +### 7.2 Content JSON + +```json +{ + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "department": "기술자문팀", + "phone": "010-1234-5678", + "hire_date": "2024-01-01", + "resign_date": "2026-12-31", + "contract_type": "기술자문위원", + "purpose": "관공서 제출용", + "issue_date": "2026-03-06" +} +``` + +--- + +## 8. 사직서 (resignation) + +### 8.1 폼 필드 + +| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 필수 | +|------|---------|------|------|------|------| +| 인적사항 | `rg-department` | 소속 | text | readonly | - | +| | `rg-position` | 직위 | text | readonly | - | +| | `rg-name` | 성명 | text | readonly | - | +| | `rg-resident` | 주민등록번호 | text | readonly | - | +| | `rg-hire-date` | 입사일 | text | readonly | - | +| | `rg-resign-date` | 퇴사(예정)일 | date | editable | 필수 | +| | `rg-address` | 주소 | text | editable | - | +| 사직사유 | `rg-reason-select` | 사유 | select | editable | 필수 | +| | (custom) | 기타 사유 | text | editable | - | +| 제출일 | `rg-issue-date` | 제출일 | text | readonly | - | + +### 8.2 사직사유 옵션 + +| 옵션 | +|------| +| 일신상의 사유 | +| 가사 사정 | +| 건강상의 이유 | +| 진학/학업 | +| 이직 | +| 기타 (직접입력) | + +### 8.3 Content JSON + +```json +{ + "department": "개발팀", + "position": "대리", + "name": "홍길동", + "resident_number": "900101-1XXXXXX", + "hire_date": "2020-03-01", + "resign_date": "2026-04-01", + "address": "서울특별시 강남구", + "reason": "이직", + "issue_date": "2026-03-06" +} +``` + +--- + +## 9. 품의서 5종 공통 (_purchase-request-form/show) + +### 9.1 통합 Alpine.js 컴포넌트 + +품의서 5종은 **단일 Blade 파일**(`_purchase-request-form.blade.php`)에서 `prType` 프로퍼티로 동적 전환된다. + +```javascript +x-data="purchaseRequestForm(initialData, authUserName, initialFiles)" +``` + +#### 타입 전환 메커니즘 + +``` +create.blade.php → switchFormMode() + ↓ +code.startsWith('pr_') 감지 + ↓ +#purchase-request-form-container display: block + ↓ +setTimeout(50ms) → _x_dataStack[0].setPrType(code) + ↓ +Alpine.js x-if 분기 → 해당 폼 렌더링 +``` + +#### prType 코드 및 라벨 + +| prType | 라벨 | 색상 | +|--------|------|------| +| `pr_expense` | 지출품의서 | `bg-orange-50 text-orange-700` | +| `pr_contract` | 계약체결품의서 | `bg-purple-50 text-purple-700` | +| `pr_purchase` | 구매품의서 | `bg-blue-50 text-blue-700` | +| `pr_trip` | 출장품의서 | `bg-green-50 text-green-700` | +| `pr_settlement` | 비용정산품의서 | `bg-teal-50 text-teal-700` | + +### 9.2 공통 필드 (모든 품의서) + +| 필드 | 라벨 | 타입 | 기본값 | +|------|------|------|--------| +| `write_date` | 작성일자 | date | 오늘 | +| `department` | 요청부서 | text | - | +| `writer_name` | 요청자 | text | 인증 사용자명 | +| `attachment_memo` | 첨부서류 메모 | textarea | - | +| `files` | 파일 업로드 | file[] | - | + +### 9.3 공통 함수 + +| 함수 | 설명 | +|------|------| +| `setPrType(type)` | 외부에서 prType 설정 (switchFormMode에서 호출) | +| `getFormData()` | prType별 다른 JSON 구조 반환 (base에 `pr_type` 포함) | +| `addItem()` | 내역 행 추가 | +| `removeItem(index)` | 내역 행 삭제 | +| `formatMoney(val)` | 숫자 → 콤마 포맷 | +| `parseMoney(str)` | 콤마 문자열 → 정수 | +| `prVendorSearch(target, fieldName)` | 범용 거래처 Autocomplete 검색 | + +### 9.4 조회 화면 분기 (show.blade.php) + +```php +// show.blade.php에서 pr_ prefix로 분기 +@if(str_starts_with($approval->form?->code ?? '', 'pr_')) + @include('approvals.partials._purchase-request-show', ['content' => $content]) +@endif +``` + +`_purchase-request-show.blade.php`에서 `$content['pr_type']`으로 5종 분기 렌더링. + +--- + +## 10. 지출품의서 (pr_expense) + +### 10.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `expense_category` | 지출항목 | text | 선택 | +| `usage_date` | 사용일자 | date | 선택 | +| `purpose` | 사용목적 | textarea | 필수 | + +### 10.2 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `description` | 항목 | text | +| `amount` | 금액 | number (콤마 포맷) | +| `remark` | 비고 | text | + +### 10.3 Content JSON + +```json +{ + "pr_type": "pr_expense", + "write_date": "2026-03-06", + "department": "개발팀", + "writer_name": "홍길동", + "expense_category": "사무용품", + "usage_date": "2026-03-05", + "purpose": "업무용 모니터 구매", + "items": [ + { "description": "27인치 모니터", "amount": 350000, "remark": "LG전자" } + ], + "total_amount": 350000, + "attachment_memo": "견적서 첨부" +} +``` + +--- + +## 11. 계약체결품의서 (pr_contract) + +### 11.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `contract_party` | 계약상대방 | text + Autocomplete | 필수 | +| `contract_party_biz_no` | 사업자번호 | text (자동) | - | +| `contract_content` | 계약내용 | textarea | 필수 | +| `contract_period_start` | 계약기간 시작 | date | 선택 | +| `contract_period_end` | 계약기간 종료 | date | 선택 | +| `contract_amount` | 계약금액 | number (콤마) | 필수 | +| `contract_conditions` | 주요조건 | textarea | 선택 | + +### 11.2 Content JSON + +```json +{ + "pr_type": "pr_contract", + "write_date": "2026-03-06", + "department": "경영지원팀", + "writer_name": "홍길동", + "contract_party": "(주)에이비씨", + "contract_party_biz_no": "123-45-67890", + "contract_content": "연간 IT 유지보수 계약", + "contract_period_start": "2026-04-01", + "contract_period_end": "2027-03-31", + "contract_amount": 12000000, + "contract_conditions": "월 1회 정기점검, 장애 발생 시 4시간 내 대응", + "attachment_memo": "계약서 초안 첨부" +} +``` + +### 11.3 특수 로직 + +- **거래처 검색**: `prVendorSearch(formData, 'contract_party')` — 계약상대방 필드에 Autocomplete 적용 +- 선택 시 `contract_party_biz_no` 자동 채움 + +--- + +## 12. 구매품의서 (pr_purchase) + +### 12.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `vendor` | 납품업체 | text + Autocomplete | 선택 | +| `vendor_biz_no` | 사업자번호 | text (자동) | - | +| `delivery_date` | 납품예정일 | date | 선택 | +| `delivery_location` | 납품장소 | text | 선택 | + +### 12.2 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `name` | 품목 | text | +| `spec` | 규격 | text | +| `quantity` | 수량 | number | +| `unit_price` | 단가 | number (콤마) | +| `amount` | 금액 | number (자동: 수량×단가) | +| `remark` | 비고 | text | + +### 12.3 Content JSON + +```json +{ + "pr_type": "pr_purchase", + "write_date": "2026-03-06", + "department": "생산팀", + "writer_name": "홍길동", + "vendor": "(주)공급사", + "vendor_biz_no": "987-65-43210", + "delivery_date": "2026-03-20", + "delivery_location": "본사 1층 창고", + "items": [ + { "name": "A4용지", "spec": "80g 500매", "quantity": 10, "unit_price": 25000, "amount": 250000, "remark": "" } + ], + "total_amount": 250000, + "attachment_memo": "" +} +``` + +### 12.4 특수 로직 + +- **금액 자동 계산**: `quantity × unit_price → amount` (x-effect 반응) +- **거래처 검색**: `prVendorSearch(formData, 'vendor')` — 납품업체 필드에 Autocomplete 적용 + +--- + +## 13. 출장품의서 (pr_trip) + +### 13.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `destination` | 출장지 | text | 필수 | +| `trip_period_start` | 출장기간 시작 | date | 필수 | +| `trip_period_end` | 출장기간 종료 | date | 필수 | +| `trip_purpose` | 출장목적 | textarea | 필수 | + +### 13.2 일정표 (items) + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `date` | 일자 | date | +| `schedule` | 일정 | text | +| `remark` | 비고 | text | + +### 13.3 경비 내역 (expenses) + +| 필드 | 라벨 | 타입 | +|------|------|------| +| `transport` | 교통비 | number (콤마) | +| `accommodation` | 숙박비 | number (콤마) | +| `meals` | 식비 | number (콤마) | +| `others` | 기타 | number (콤마) | +| (자동) | 합계 | number (합산) | + +### 13.4 Content JSON + +```json +{ + "pr_type": "pr_trip", + "write_date": "2026-03-06", + "department": "영업팀", + "writer_name": "홍길동", + "destination": "부산 해운대", + "trip_period_start": "2026-03-10", + "trip_period_end": "2026-03-11", + "trip_purpose": "거래처 방문 및 현장 점검", + "items": [ + { "date": "2026-03-10", "schedule": "거래처 미팅", "remark": "오전 10시" }, + { "date": "2026-03-11", "schedule": "현장 점검 및 복귀", "remark": "" } + ], + "expenses": { + "transport": 120000, + "accommodation": 80000, + "meals": 40000, + "others": 0 + }, + "total_amount": 240000, + "attachment_memo": "" +} +``` + +### 13.5 조회 화면 특수 구조 + +- **일정표**: 테이블 형태로 일자/일정/비고 렌더링 +- **경비 카드**: 교통비/숙박비/식비/기타 4개 항목 + 합계를 카드 그리드로 표시 + +--- + +## 14. 비용정산품의서 (pr_settlement) + +### 14.1 추가 필드 + +| 필드 | 라벨 | 타입 | 필수 | +|------|------|------|------| +| `settlement_period_start` | 정산기간 시작 | date | 선택 | +| `settlement_period_end` | 정산기간 종료 | date | 선택 | +| `payment_method` | 지급방법 | radio | 필수 | + +### 14.2 지급방법 옵션 + +| 값 | 라벨 | +|----|------| +| `corporate_card` | 법인카드 사용 | +| `personal_advance` | 개인 선지출 (환급 요청) | + +### 14.3 내역 테이블 + +| 컬럼 | 라벨 | 타입 | +|------|------|------| +| `date` | 사용일자 | date | +| `description` | 항목 | text | +| `amount` | 금액 | number (콤마) | +| `remark` | 비고 | text | + +### 14.4 Content JSON + +```json +{ + "pr_type": "pr_settlement", + "write_date": "2026-03-06", + "department": "개발팀", + "writer_name": "홍길동", + "settlement_period_start": "2026-02-01", + "settlement_period_end": "2026-02-28", + "payment_method": "personal_advance", + "items": [ + { "date": "2026-02-15", "description": "택시비", "amount": 25000, "remark": "야근 귀가" }, + { "date": "2026-02-20", "description": "회의 다과", "amount": 15000, "remark": "팀 미팅" } + ], + "total_amount": 40000, + "attachment_memo": "영수증 첨부" +} +``` + +### 14.5 조회 화면 특수 구조 + +- **지급방법 표시**: `corporate_card` → "법인카드 사용", `personal_advance` → "개인 선지출 (환급 요청)" +- 해당 라벨을 뱃지 형태로 표시 + +--- + +## 15. 결재 도장 테이블 (_approval-stamp-table.blade.php) + +### 15.1 구조 + +전통 한글 결재 양식의 도장 테이블을 구현한다. + +``` +┌──────┬────────┬────────┬────────┐ +│ │ 과장 │ 부장 │ 이사 │ ← 1행: 직급 헤더 +│ 결재 ├────────┼────────┼────────┤ +│ │ [승인] │ [대기] │ [대기] │ ← 2행: 서명/도장 영역 +│ ├────────┼────────┼────────┤ +│ │ 김과장 │ 박부장 │ 이이사 │ ← 3행: 이름 + 처리일 +│ │ 03/06 │ │ │ +└──────┴────────┴────────┴────────┘ +``` + +### 15.2 상태별 표시 + +| 상태 | approval_type | 표시 | 색상 | +|------|---------------|------|------| +| 승인 | `normal` | 빨간 원형 "승인" | `bg-red-500` | +| 전결 | `pre_decided` | 파란 원형 "전결" | `bg-blue-500` | +| 반려 | - | 빨간 원형 "반려" | `bg-red-500` | +| 보류 | - | 주황 원형 "보류" | `bg-amber-500` | +| 건너뜀 | - | 회색 "-" | `bg-gray-300` | + +--- + +## 16. 결재선 편집기 (_approval-line-editor.blade.php) + +### 16.1 2패널 구조 + +``` +┌─────────────────────┬─────────────────────┐ +│ 인원 목록 │ 결재선 │ +│ │ │ +│ [검색 input] │ [템플릿 선택 ▼] │ +│ │ │ +│ ▼ 개발팀 │ ① 김과장 (결재) [✗] │ +│ 홍길동 과장 [+] │ ② 박부장 (합의) [✗] │ +│ 김영희 대리 [+] │ ③ 이대리 (참조) [✗] │ +│ │ │ +│ ▼ 경영지원팀 │ (드래그로 순서 변경) │ +│ 박부장 부장 [+] │ │ +│ │ │ +├─────────────────────┴─────────────────────┤ +│ 결재: 1명 합의: 1명 참조: 1명 합계: 3명 │ +└───────────────────────────────────────────┘ +``` + +### 16.2 기능 + +| 기능 | 설명 | +|------|------| +| **인원 검색** | 이름/부서 실시간 검색 | +| **부서별 접기** | 부서 헤더 클릭으로 인원 접기/펼치기 | +| **드래그 정렬** | SortableJS로 결재선 순서 변경 | +| **유형 선택** | 각 단계별 approval/agreement/reference 선택 | +| **템플릿 로드** | 저장된 결재선 템플릿 드롭다운 | + +### 16.3 데이터 소스 + +``` +API: /api/admin/tenant-users/list + +응답: +[ + { + "department_id": 1, + "department_name": "개발팀", + "users": [ + { "id": 10, "name": "홍길동", "position": "과장", "job_title": "팀장" } + ] + } +] +``` + +### 16.4 Hidden Inputs (form 전송) + +```html + + + + +``` + +--- + +## 17. ApprovalForm 모델 + +### 17.1 테이블 스키마 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `name` | VARCHAR | 양식명 (예: "휴가신청서") | +| `code` | VARCHAR UNIQUE | 양식 코드 (예: `leave`) | +| `category` | ENUM | `request`, `expense`, `certificate`, `expense_estimate` | +| `template` | JSON | 필드 정의 메타데이터 | +| `body_template` | LONGTEXT NULL | HTML 본문 템플릿 | +| `is_active` | BOOLEAN | 활성 여부 | + +### 17.2 카테고리 + +#### DB 카테고리 (ApprovalForm.category) + +| 카테고리 | 설명 | 양식 코드 | +|---------|------|----------| +| `request` | 신청서 | `leave`, `attendance_request`, `reason_report` | +| `expense` | 지출결의서 | `expense` | +| `certificate` | 증명서/서류 | `employment_cert`, `career_cert`, `appointment_cert`, `resignation` | +| `expense_estimate` | 품의서 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | + +#### UI 분류 (formCategoryMap — 2단계 선택용) + +| UI 분류 | 양식 코드 | +|---------|----------| +| 일반 | `BUSINESS_DRAFT` | +| 인사/근태 | `leave`, `attendance_request`, `reason_report`, `resignation` | +| 증명서 | `employment_cert`, `career_cert`, `appointment_cert` | +| 품의 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` | +| 재무 | `expense` | + +> **참고**: DB 카테고리와 UI 분류는 별도 매핑이다. DB는 `approval_forms.category` ENUM이고, UI 분류는 JavaScript `formCategoryMap` 객체로 정의된다. + +--- + +## 18. 양식별 저장/조회 흐름 + +### 18.1 저장 (create/update) + +``` +사용자 입력 + ↓ +getFormData() (JavaScript) + ↓ +POST /api/admin/approvals + body: { form_id, title, content: {...}, body, steps: [...] } + ↓ +ApprovalService::createApproval() + ↓ +Approval.content = JSON encode → DB 저장 +``` + +### 18.2 조회 (show) + +``` +GET /approval-mgmt/{id} + ↓ +ApprovalController::show() + ↓ +Blade: show.blade.php + ↓ +양식 코드별 분기: + leave → (본문에 인라인 표시) + expense → @include('_expense-show') + pr_* → @include('_purchase-request-show') ← str_starts_with 매칭 + employment_cert → @include('_certificate-show') + career_cert → @include('_career-cert-show') + appointment_cert → @include('_appointment-cert-show') + resignation → @include('_resignation-show') +``` + +> **품의서 분기**: `str_starts_with($approval->form?->code ?? '', 'pr_')` 조건으로 5종 모두 단일 include로 처리. `_purchase-request-show.blade.php` 내부에서 `$content['pr_type']`으로 세부 분기. + +--- + +## 관련 문서 + +- [README.md](README.md) — 결재관리 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 +- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/approvals/ui-screens.md b/features/approvals/ui-screens.md new file mode 100644 index 0000000..f81b9ae --- /dev/null +++ b/features/approvals/ui-screens.md @@ -0,0 +1,381 @@ +# 결재관리 UI 화면 구성 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **기술**: Blade + HTMX + Alpine.js + Tailwind CSS +> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) + +--- + +## 1. 개요 + +결재관리 화면은 MNG(관리자 웹)에서 Blade 템플릿으로 구현되며, API 호출은 `fetch()`를 사용한다. + +### 1.1 파일 구조 + +``` +resources/views/approvals/ +├── drafts.blade.php ← 기안함 (목록) +├── pending.blade.php ← 결재 대기함 (목록) +├── completed.blade.php ← 처리 완료함 (목록) +├── references.blade.php ← 참조함 (목록) +├── create.blade.php ← 기안 작성 +├── edit.blade.php ← 기안 수정 +├── show.blade.php ← 상세 조회 + 결재 처리 +└── partials/ + ├── _status-badge.blade.php ← 상태 뱃지 컴포넌트 + └── _step-progress.blade.php ← 결재 단계 진행 표시 +``` + +--- + +## 2. 목록 화면 + +### 2.1 기안함 (`/approval-mgmt/drafts`) + +내가 기안한 모든 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 기안함 [+ 새 기안] │ +├──────────────────────────────────────────────────────────┤ +│ [검색] [상태 필터 ▼] [긴급만 □] [날짜 범위] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 양식 │ 상태 │ 기안일 │ +│ APR-260228-001│ 휴가 신청 │ 휴가서 │ 🟢완료 │ 02-28 │ +│ APR-260228-002│ 출장 보고 │ 출장서 │ 🔵진행 │ 02-28 │ +│ APR-260227-001│ 경비 청구 │ 경비서 │ ⬜임시 │ 02-27 │ +├──────────────────────────────────────────────────────────┤ +│ [◀ 이전] 1 / 3 [다음 ▶] │ +└──────────────────────────────────────────────────────────┘ +``` + +**상태 필터:** 전체, 임시저장, 진행, 완료, 반려, 회수, 보류 + +--- + +### 2.2 결재 대기함 (`/approval-mgmt/pending`) + +내가 현재 결재해야 할 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 결재 대기함 [뱃지: 3건] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 기안자 │ 양식 │ 상신일 │ +│ 🔴 APR-260..│ 긴급 승인 │ 홍길동 │ 구매서 │ 02-28 │ +│ APR-260..│ 휴가 신청 │ 김영희 │ 휴가서 │ 02-27 │ +└──────────────────────────────────────────────────────────┘ +``` + +> 긴급 문서는 🔴 아이콘과 함께 상단에 표시 + +--- + +### 2.3 참조함 (`/approval-mgmt/references`) + +내가 참조자로 지정된 문서를 표시한다. + +**UI 구성:** + +``` +┌──────────────────────────────────────────────────────────┐ +│ 참조함 │ +├──────────────────────────────────────────────────────────┤ +│ [전체] [미열람 (5)] [열람완료] │ +├──────────────────────────────────────────────────────────┤ +│ 문서번호 │ 제목 │ 기안자 │ 상태 │ 열람 │ +│ APR-260228-001│ 회의록 │ 박부장 │ 🟢완료 │ ❌미열람│ +│ APR-260227-003│ 인사발령 │ 이팀장 │ 🔵진행 │ ✅열람 │ +└──────────────────────────────────────────────────────────┘ +``` + +**열람 추적:** +- 문서 클릭 시 `mark-read` API가 자동 호출된다 +- 미열람/열람완료 탭으로 필터링 가능 +- 미열람 건수가 뱃지로 표시된다 + +--- + +## 3. 상세 화면 (`/approval-mgmt/{id}`) + +### 3.1 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 결재 상세 [수정] [목록으로] │ +│ APR-260228-001 │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 상태: [🔵 진행] [🔴 긴급] │ +│ 양식: 휴가신청서 기안자: 홍길동 │ +│ 기안일: 2026-02-28 10:05 완료일: - │ +│ 원본 문서: APR-260225-003 (재기안 시 표시) │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ 회수 사유 (cancelled 상태에서만) │ │ +│ │ 내용 수정이 필요하여 회수합니다. │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ 제목: 2월 연차 사용 신청 │ +│ 본문: 2월 27일~28일 연차 사용합니다... │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 결재 진행 │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ [결재 단계 프로그레스 바] │ │ +│ │ ✓김과장(승인) → ●박부장(대기) → ③이사(대기) │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ 결재 의견 │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ ✓ 김과장 2026-02-28 11:00 │ │ +│ │ 승인합니다. │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 결재 처리 (현재 결재자에게만 표시) │ +│ [결재 의견 textarea] │ +│ [승인] [반려] [보류] [전결] │ +│ │ +├──────────────────────────────────────────────────────────┤ +│ 보류 해제 (on_hold + 보류한 본인에게만) │ +│ [보류 해제] │ +├──────────────────────────────────────────────────────────┤ +│ 회수 (기안자 + pending/on_hold) │ +│ [회수 사유 textarea] │ +│ [결재 회수] │ +├──────────────────────────────────────────────────────────┤ +│ 복사 재기안 (기안자 + approved/rejected/cancelled) │ +│ [복사하여 재기안] │ +└──────────────────────────────────────────────────────────┘ +``` + +### 3.2 조건부 섹션 표시 + +| 섹션 | 표시 조건 | +|------|----------| +| **수정 버튼** | 기안자 + `draft`/`rejected` | +| **회수 사유** | `cancelled` + `recall_reason` 존재 | +| **원본 문서 링크** | `parent_doc_id` 존재 (재기안 문서) | +| **결재 처리** | `pending` + 현재 결재자 | +| **보류 해제** | `on_hold` + 보류한 본인 | +| **회수** | 기안자 + `pending`/`on_hold` | +| **복사 재기안** | 기안자 + `approved`/`rejected`/`cancelled` | + +--- + +## 4. 파셜 컴포넌트 + +### 4.1 상태 뱃지 (`_status-badge.blade.php`) + +문서 상태를 색상 뱃지로 표시한다. + +| 상태 | 라벨 | 스타일 | +|------|------|--------| +| `draft` | 임시저장 | `bg-gray-100 text-gray-700` | +| `pending` | 진행 | `bg-blue-100 text-blue-700` | +| `approved` | 완료 | `bg-green-100 text-green-700` | +| `rejected` | 반려 | `bg-red-100 text-red-700` | +| `cancelled` | 회수 | `bg-yellow-100 text-yellow-700` | +| `on_hold` | 보류 | `bg-amber-100 text-amber-700` | + +--- + +### 4.2 결재 단계 프로그레스 (`_step-progress.blade.php`) + +결재선의 각 단계를 가로 프로그레스 바로 표시한다. + +**단계 아이콘:** + +| 상태 | 아이콘 | 배경색 | 텍스트색 | +|------|--------|--------|---------| +| `approved` (normal) | ✓ | `bg-green-500` | white | +| `approved` (pre_decided) | ⚡ | `bg-indigo-500` | white | +| `rejected` | ✗ | `bg-red-500` | white | +| `on_hold` | ⏸ | `bg-amber-400` | white | +| `skipped` | — | `bg-gray-300` | gray | +| `pending` (현재 차례) | 번호 | `bg-blue-500` | white | +| `pending` (대기) | 번호 | `bg-gray-200` | gray | + +**레이아웃:** + +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ ✓ ──── ⚡ ──── — ──── — ──── ● ──── 3 │ +│ 김과장 박부장 이사장 팀장 최대리 참조자 │ +│ 경영팀 경영팀 대표실 개발팀 개발팀 인사팀 │ +│ (승인) (전결) (건너뜀)(건너뜀)(대기) (참조) │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +**특수 표시:** +- **전결** step: ⚡ 아이콘 + "전결" 라벨 (남색) +- **보류** step: ⏸ 아이콘 + "보류" 라벨 (노란색) +- **건너뜀** step: 이름에 취소선 (line-through) +- **참조** step: 별도 구분 없이 동일 프로그레스 바에 표시 +- **연결선**: 단계 사이 가로선 (`border-t-2`) + +--- + +## 5. 결재 처리 인터랙션 + +### 5.1 승인 + +``` +[승인 버튼 클릭] + → confirm("승인하시겠습니까?") + → POST /api/admin/approvals/{id}/approve + body: { comment: "의견 텍스트" } + → 성공 시: 토스트("승인되었습니다") + 페이지 리로드 +``` + +### 5.2 반려 + +``` +[반려 버튼 클릭] + → comment 빈 값 체크 → 경고 토스트("반려 시 사유를 입력해주세요") + → confirm("반려하시겠습니까?") + → POST /api/admin/approvals/{id}/reject + body: { comment: "사유" } + → 성공 시: 토스트("반려되었습니다") + 페이지 리로드 +``` + +### 5.3 보류 + +``` +[보류 버튼 클릭] + → comment 빈 값 체크 → 경고 토스트("보류 사유를 입력해주세요") + → confirm("이 결재를 보류하시겠습니까?") + → POST /api/admin/approvals/{id}/hold + body: { comment: "사유" } + → 성공 시: 토스트("보류되었습니다") + 페이지 리로드 +``` + +### 5.4 전결 + +``` +[전결 버튼 클릭] + → confirm("전결 처리하시겠습니까?\n이후 모든 결재를 건너뛰고 문서를 최종 승인합니다.") + → POST /api/admin/approvals/{id}/pre-decide + body: { comment: "의견(선택)" } + → 성공 시: 토스트("전결 처리되었습니다") + 페이지 리로드 +``` + +### 5.5 보류 해제 + +``` +[보류 해제 버튼 클릭] + → confirm("보류를 해제하시겠습니까?") + → POST /api/admin/approvals/{id}/release-hold + → 성공 시: 토스트("보류가 해제되었습니다") + 페이지 리로드 +``` + +### 5.6 회수 + +``` +[결재 회수 버튼 클릭] + → confirm("결재를 회수하시겠습니까? 이 작업은 되돌릴 수 없습니다.") + → POST /api/admin/approvals/{id}/cancel + body: { recall_reason: "사유(선택)" } + → 성공 시: 토스트("결재가 회수되었습니다") + 페이지 리로드 +``` + +### 5.7 복사 재기안 + +``` +[복사하여 재기안 버튼 클릭] + → confirm("이 문서를 복사하여 새 결재를 작성하시겠습니까?") + → POST /api/admin/approvals/{id}/copy + → 성공 시: 토스트("문서가 복사되었습니다") + → /approval-mgmt/{newId}/edit로 이동 +``` + +--- + +## 6. 결재 의견 표시 + +상세 페이지에서 결재 의견이 있는 step을 카드 형태로 표시한다. + +``` +┌──────────────────────────────────────┐ +│ ✓ 김과장 2026-02-28 11:00 │ +│ 승인합니다. │ +├──────────────────────────────────────┤ +│ ⚡ 박부장 (전결) 2026-02-28 14:00 │ +│ 전결 처리합니다. │ +├──────────────────────────────────────┤ +│ ⏸ 이사장 (보류) 2026-02-28 15:00 │ +│ 추가 자료 검토 필요 │ +├──────────────────────────────────────┤ +│ ✗ 팀장 2026-02-28 16:00 │ +│ 예산 초과로 반려합니다. │ +└──────────────────────────────────────┘ +``` + +**아이콘 색상:** +- ✓ 승인: 녹색 (`bg-green-100 text-green-600`) +- ⚡ 전결: 남색 (`bg-indigo-100 text-indigo-600`) +- ⏸ 보류: 노란색 (`bg-amber-100 text-amber-600`) +- ✗ 반려: 적색 (`bg-red-100 text-red-600`) + +--- + +## 7. 참조함 열람 추적 UI + +### 7.1 탭 필터 + +``` +[전체] [미열람 (5)] [열람완료] +``` + +- 탭 클릭 시 `is_read` 파라미터로 API 재호출 +- 미열람 탭에 건수 뱃지 표시 + +### 7.2 열람 상태 표시 + +| 상태 | 표시 | +|------|------| +| 미열람 | `bg-red-100 text-red-700` "미열람" | +| 열람완료 | `bg-green-100 text-green-700` "열람완료" | + +### 7.3 자동 열람 처리 + +문서 행 클릭 시: +1. `mark-read` API 호출 (비동기) +2. 상세 페이지로 이동 + +--- + +## 8. 버튼 스타일 가이드 + +| 버튼 | 색상 | Tailwind 클래스 | +|------|------|----------------| +| 승인 | 녹색 | `bg-green-600 hover:bg-green-700` | +| 반려 | 적색 | `bg-red-600 hover:bg-red-700` | +| 보류 | 노란색 | `bg-amber-500 hover:bg-amber-600` | +| 전결 | 남색 | `bg-indigo-600 hover:bg-indigo-700` | +| 보류 해제 | 노란색 | `bg-amber-500 hover:bg-amber-600` | +| 회수 | 노란색 | `bg-yellow-500 hover:bg-yellow-600` | +| 복사 재기안 | 회색 | `bg-gray-600 hover:bg-gray-700` | +| 수정 | 회색 | `bg-gray-600 hover:bg-gray-700` | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [워크플로우 상세](workflows.md) — 각 동작의 상세 흐름 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/approvals/workflows.md b/features/approvals/workflows.md new file mode 100644 index 0000000..202d525 --- /dev/null +++ b/features/approvals/workflows.md @@ -0,0 +1,565 @@ +# 결재관리 워크플로우 상세 + +> **작성일**: 2026-02-28 +> **상태**: Phase 2 구현 완료 +> **관련**: [README.md](README.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md) + +--- + +## 1. 개요 + +이 문서는 결재관리 시스템의 각 동작(Action)에 대한 상세 워크플로우를 정의한다. +모든 워크플로우는 `ApprovalService`에서 트랜잭션으로 처리된다. + +### 1.1 용어 정의 + +| 용어 | 설명 | +|------|------| +| **기안자** | 결재 문서를 작성한 사람 (`drafter_id`) | +| **현재 결재자** | 결재선에서 현재 차례인 사람 (가장 작은 `step_order`의 `pending` step) | +| **결재자** | `step_type`이 `approval` 또는 `agreement`인 참여자 | +| **참조자** | `step_type`이 `reference`인 참여자 (의사결정 권한 없음) | +| **전결** | 현재 결재자가 이후 모든 결재를 건너뛰고 즉시 최종 승인 | + +--- + +## 2. 기안 작성 (createApproval) + +### 2.1 흐름 + +``` +사용자 → [양식 선택] → [제목/본문 입력] → [결재선 설정] → [임시저장] + │ + ▼ + 새 Approval 생성 + status = 'draft' + current_step = 0 +``` + +### 2.2 조건 + +- 모든 로그인 사용자가 작성 가능 +- `form_id` 필수 (양식 선택) +- 결재선(steps)은 저장 시 선택사항 (상신 시 필수) + +### 2.3 처리 로직 + +1. 문서번호 자동 채번 (`APR-YYMMDD-001` 형식) +2. `numbering_sequences` 테이블로 일일 순번 관리 +3. 결재선 설정 시 `approval_steps` 저장 + 사용자 정보 스냅샷 (이름, 부서, 직급) +4. `status = 'draft'`, `current_step = 0` + +--- + +## 3. 상신 (submit) + +### 3.1 흐름 + +``` +기안자 → [상신 버튼] → 유효성 검사 → 결재선 검사 → 상신 완료 + │ + ▼ + status = 'pending' + current_step = 1 + drafted_at = now() +``` + +### 3.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `draft` 또는 `rejected` | +| 결재선 | 결재/합의 step 1명 이상 필수 | +| 요청자 | 기안자만 | + +### 3.3 처리 로직 + +1. `isSubmittable()` 검증 → `draft` 또는 `rejected`인지 확인 +2. 결재/합의 step 존재 확인 +3. **반려 후 재상신인 경우**: 모든 step을 `pending`으로 초기화 (comment, acted_at도 초기화) +4. `status → pending`, `drafted_at → now()`, `current_step → 1` + +### 3.4 반려 후 재상신 + +``` +rejected 문서 + │ + ├── 기안자가 내용 수정 (updateApproval) + │ + └── 상신 (submit) + ├── 모든 steps → pending (초기화) + ├── status → pending + └── current_step → 1 (처음부터 다시) +``` + +> 반려 후 재상신 시 결재선이 초기화되므로, 이전 결재 의견(comment)은 사라진다. + +--- + +## 4. 승인 (approve) + +### 4.1 흐름 + +``` +현재 결재자 → [의견 입력(선택)] → [승인 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'approved' │ + │ comment → (입력값) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + 다음 pending step 있음 마지막 결재자 + │ │ + current_step 갱신 status → 'approved' + (다음 순서 결재자 대기) completed_at → now() +``` + +### 4.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` | +| 요청자 | 현재 차례 결재자 (`approver_id === auth()->id()`) | + +### 4.3 처리 로직 + +1. `isActionable()` 검증 → `pending` 상태인지 확인 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 step → `approved` + comment + acted_at +4. 다음 pending 결재/합의 step 조회 + - **있으면**: `current_step` 갱신 + - **없으면**: 문서 `approved` + `completed_at` + +### 4.4 순차결재 순서 결정 + +``` +step_order = 1 (결재) → step_order = 2 (합의) → step_order = 3 (결재) + │ │ │ + 1번째 승인 → 2번째 승인 → 3번째 승인 → 문서 완료 +``` + +> 결재와 합의는 동일한 순차 흐름을 따른다. `step_order` 순서대로 처리된다. + +--- + +## 5. 반려 (reject) + +### 5.1 흐름 + +``` +현재 결재자 → [반려 사유 입력(필수)] → [반려 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'rejected' │ + │ comment → (사유) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'rejected' + completed_at → now() +``` + +### 5.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` | +| 요청자 | 현재 차례 결재자 | +| 반려 사유 | **필수** (빈 값 불가) | + +### 5.3 처리 로직 + +1. `isActionable()` 검증 +2. 현재 결재자 확인 +3. 반려 사유 빈 값 체크 +4. 현재 step → `rejected` + comment + acted_at +5. 문서 → `rejected` + completed_at + +### 5.4 반려 후 가능한 동작 + +``` +rejected 문서 + │ + ├── 기안자가 수정 → 재상신 (submit) + │ └── 결재선 초기화, 처음부터 다시 진행 + │ + └── 기안자가 복사 재기안 (copyForRedraft) + └── 새 문서 생성 (draft), 원본은 그대로 유지 +``` + +--- + +## 6. 회수 (cancel) + +### 6.1 흐름 + +``` +기안자 → [회수 사유 입력(선택)] → [회수 버튼] + │ + ┌──────────┴──────────┐ + │ 회수 가능 여부 판단 │ + │ (첫 결재자 미처리?) │ + └──────────┬──────────┘ + │ + ┌───────────┴───────────┐ + │ │ + 첫 결재자 첫 결재자 이미 + pending/on_hold 승인/반려 + │ │ + 회수 진행 회수 불가 + │ (에러 반환) + ▼ + 모든 pending/on_hold steps → 'skipped' + 문서 status → 'cancelled' + recall_reason → (입력값) + completed_at → now() +``` + +### 6.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` 또는 `on_hold` | +| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | +| 첫 결재자 상태 | `pending` 또는 `on_hold` (이미 처리했으면 불가) | + +### 6.3 회수 가능 판단 로직 + +```php +// 1단계: 문서 상태 확인 +$approval->isCancellable() // pending 또는 on_hold + +// 2단계: 기안자 확인 +$approval->drafter_id === auth()->id() + +// 3단계: 첫 결재자 상태 확인 +$firstStep = steps.approvalOnly().orderBy('step_order').first() +$firstStep->status === 'pending' || 'on_hold' // 미처리 상태여야 함 +``` + +### 6.4 처리 로직 + +1. `isCancellable()` 검증 → `pending` 또는 `on_hold` +2. 기안자 확인 +3. 첫 번째 결재/합의 step의 상태 확인 → `pending`/`on_hold`이 아니면 거부 +4. 모든 `pending`/`on_hold` steps → `skipped` +5. 문서 → `cancelled` + `recall_reason` + `completed_at` + +--- + +## 7. 보류 (hold) + +### 7.1 흐름 + +``` +현재 결재자 → [보류 사유 입력(필수)] → [보류 버튼] + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'on_hold' │ + │ comment → (사유) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'on_hold' +``` + +### 7.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` (`isHoldable()`) | +| 요청자 | 현재 차례 결재자 | +| 보류 사유 | **필수** (빈 값 불가) | + +### 7.3 처리 로직 + +1. `isHoldable()` 검증 → `pending` 상태인지 확인 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 결재자 확인 (`approver_id === auth()->id()`) +4. 보류 사유 빈 값 체크 +5. 현재 step → `on_hold` + comment + acted_at +6. 문서 → `on_hold` + +### 7.4 보류 상태의 영향 + +``` +on_hold 상태에서: +├── 다른 결재자는 아무 동작 불가 (결재 흐름 중단) +├── 기안자는 회수 가능 (첫 결재자가 미처리 상태이면) +└── 보류한 결재자만 보류 해제 가능 +``` + +--- + +## 8. 보류 해제 (releaseHold) + +### 8.1 흐름 + +``` +보류한 결재자 → [보류 해제 버튼] + │ + ┌──────────┴──────────┐ + │ on_hold step │ + │ status → 'pending' │ + │ comment → null │ + │ acted_at → null │ + └──────────┬──────────┘ + │ + ▼ + 문서 status → 'pending' + (결재 흐름 재개) +``` + +### 8.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `on_hold` (`isHoldReleasable()`) | +| 요청자 | 보류한 본인만 (`on_hold` step의 `approver_id === auth()->id()`) | + +### 8.3 처리 로직 + +1. `isHoldReleasable()` 검증 → `on_hold` 상태인지 확인 +2. `on_hold` 상태인 step 조회 +3. 해당 step의 `approver_id`가 현재 사용자인지 확인 +4. step → `pending` + comment/acted_at 초기화 +5. 문서 → `pending` + +--- + +## 9. 전결 (preDecide) + +### 9.1 흐름 + +``` +현재 결재자 → [의견 입력(선택)] → [전결 버튼] → 확인 팝업 + │ + ┌──────────┴──────────┐ + │ 현재 step │ + │ status → 'approved' │ + │ approval_type → │ + │ 'pre_decided' │ + │ comment → (입력값) │ + │ acted_at → now() │ + └──────────┬──────────┘ + │ + ▼ + 이후 모든 pending + approval/agreement steps + → status = 'skipped' + │ + ▼ + 문서 status → 'approved' + completed_at → now() +``` + +### 9.2 조건 + +| 조건 | 설명 | +|------|------| +| 문서 상태 | `pending` (`isActionable()`) | +| 요청자 | 현재 차례 결재자 | + +### 9.3 처리 로직 + +1. `isActionable()` 검증 +2. `getCurrentApproverStep()` → 현재 차례 step 조회 +3. 현재 결재자 확인 +4. 현재 step → `approved` + `approval_type = 'pre_decided'` + comment + acted_at +5. 이후 모든 pending 결재/합의 steps → `skipped` +6. 문서 → `approved` + `completed_at` + +### 9.4 전결 예시 + +``` +step_order=1 (이사장, 결재) → approved (normal) +step_order=2 (부장, 결재) → approved (pre_decided) ← 여기서 전결 +step_order=3 (과장, 합의) → skipped (전결로 건너뜀) +step_order=4 (팀장, 결재) → skipped (전결로 건너뜀) +step_order=5 (참조자, 참조) → (참조는 영향 없음, 그대로 유지) + +문서 → approved, completed_at = now() +``` + +> 전결은 결재/합의 step만 건너뛴다. 참조 step은 영향받지 않는다. + +--- + +## 10. 복사 재기안 (copyForRedraft) + +### 10.1 흐름 + +``` +기안자 → [복사하여 재기안 버튼] + │ + ▼ + ┌─────────────────────────────┐ + │ 원본 문서에서 복사 │ + │ ├── form_id │ + │ ├── title │ + │ ├── content (양식 데이터) │ + │ ├── body │ + │ ├── is_urgent │ + │ ├── department_id │ + │ └── 결재선 (모두 pending) │ + └─────────────┬───────────────┘ + │ + ▼ + 새 문서 생성 (status = 'draft') + parent_doc_id = 원본.id + 새 문서번호 채번 + │ + ▼ + 수정 페이지로 이동 + (/approval-mgmt/{newId}/edit) +``` + +### 10.2 조건 + +| 조건 | 설명 | +|------|------| +| 원본 문서 상태 | `approved`, `rejected`, `cancelled` (`isCopyable()`) | +| 요청자 | 기안자만 (`drafter_id === auth()->id()`) | + +### 10.3 처리 로직 + +1. `isCopyable()` 검증 → `approved`/`rejected`/`cancelled` 중 하나 +2. 기안자 확인 +3. 새 문서 생성: + - 새 문서번호 채번 + - 원본의 양식, 제목, 내용, 본문, 긴급 여부, 부서 복사 + - `parent_doc_id = 원본.id` + - `status = 'draft'`, `current_step = 0` +4. 결재선 복사: 원본의 모든 steps를 새 문서에 복사 (모두 `pending` 상태) +5. 새 문서의 edit 페이지로 리다이렉트 + +### 10.4 원본과의 관계 + +``` +원본 문서 (approved/rejected/cancelled) + │ + └── parent_doc_id로 연결 + │ + ▼ + 새 문서 (draft) + ├── 상세 페이지에서 "원본 문서" 링크 표시 + └── 기안자가 내용 수정 후 상신 가능 +``` + +--- + +## 11. 참조 열람 추적 (markAsRead) + +### 11.1 흐름 + +``` +참조자 → [참조함 목록에서 문서 클릭] + │ + ├── markAsRead API 호출 + │ ├── is_read → true + │ └── read_at → now() + │ + └── 상세 페이지로 이동 +``` + +### 11.2 조건 + +| 조건 | 설명 | +|------|------| +| 요청자 | 해당 문서의 참조자 (`step_type = 'reference'`) | + +### 11.3 처리 로직 + +1. 현재 사용자의 참조 step 조회 +2. `is_read = false`인 step → `is_read = true`, `read_at = now()` +3. 이미 열람한 경우 중복 업데이트 없음 (`where('is_read', false)`) + +--- + +## 12. 전체 상태 전이 요약 + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ │ +│ draft ──submit()──→ pending ──approve()──→ (다음 step 또는) │ +│ ▲ │ │ approved │ +│ │ │ │ │ +│ │ │ ├──reject()──→ rejected │ +│ │ │ │ │ │ +│ │ │ │ ├── 수정 → submit() │ +│ │ │ │ │ (재상신, draft X) │ +│ │ │ │ │ │ +│ │ │ │ └── copyForRedraft() │ +│ │ │ │ → 새 draft 생성 │ +│ │ │ │ │ +│ │ │ ├──hold()──→ on_hold │ +│ │ │ │ │ │ +│ │ │ │ ├── releaseHold() │ +│ │ │ │ │ → pending 복원 │ +│ │ │ │ │ │ +│ │ │ │ └── cancel() (기안자) │ +│ │ │ │ → cancelled │ +│ │ │ │ │ +│ │ │ ├──preDecide()──→ approved │ +│ │ │ │ (이후 steps → skipped) │ +│ │ │ │ │ +│ │ │ └──cancel()──→ cancelled │ +│ │ │ (기안자, 첫결재자 미처리 시) │ +│ │ │ │ │ +│ │ │ └── copyForRedraft() │ +│ │ │ → 새 draft 생성 │ +│ │ │ │ +│ │ └── approved ──copyForRedraft() │ +│ │ → 새 draft 생성 │ +│ │ │ +│ └── updateApproval() (draft/rejected 상태에서 수정) │ +│ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 13. 에러 케이스 정리 + +| 동작 | 에러 조건 | 에러 메시지 | +|------|----------|------------| +| submit | 상태가 draft/rejected 아님 | "상신할 수 없는 상태입니다." | +| submit | 결재선 없음 | "결재선을 설정해주세요." | +| approve | 상태가 pending 아님 | "승인할 수 없는 상태입니다." | +| approve | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| reject | 상태가 pending 아님 | "반려할 수 없는 상태입니다." | +| reject | 사유 미입력 | "반려 사유를 입력해주세요." | +| cancel | 상태가 pending/on_hold 아님 | "회수할 수 없는 상태입니다." | +| cancel | 기안자 아님 | "기안자만 회수할 수 있습니다." | +| cancel | 첫 결재자 이미 처리 | "첫 번째 결재자가 이미 처리하여 회수할 수 없습니다." | +| hold | 상태가 pending 아님 | "보류할 수 없는 상태입니다." | +| hold | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| hold | 사유 미입력 | "보류 사유를 입력해주세요." | +| releaseHold | 상태가 on_hold 아님 | "보류 해제할 수 없는 상태입니다." | +| releaseHold | 보류한 본인 아님 | "보류한 결재자만 해제할 수 있습니다." | +| preDecide | 상태가 pending 아님 | "전결할 수 없는 상태입니다." | +| preDecide | 현재 결재자 아님 | "현재 결재자가 아닙니다." | +| copyForRedraft | 상태가 approved/rejected/cancelled 아님 | "복사할 수 없는 상태입니다." | +| copyForRedraft | 기안자 아님 | "기안자만 복사할 수 있습니다." | +| update | 상태가 draft/rejected 아님 | "수정할 수 없는 상태입니다." | +| delete | 상태가 draft 아님 | "삭제할 수 없는 상태입니다." | + +--- + +## 관련 문서 + +- [README.md](README.md) — 시스템 전체 개요 +- [API 명세](api-reference.md) — 엔드포인트별 요청/응답 +- [UI 화면 구성](ui-screens.md) — 화면별 동작 + +--- + +**최종 업데이트**: 2026-02-28 diff --git a/features/barobill-kakaotalk/esign-notification-guide.md b/features/barobill-kakaotalk/esign-notification-guide.md new file mode 100644 index 0000000..dce2345 --- /dev/null +++ b/features/barobill-kakaotalk/esign-notification-guide.md @@ -0,0 +1,250 @@ +# 전자계약 알림톡/SMS 환경별 설정 가이드 + +> **작성일**: 2026-02-27 +> **상태**: 운영 중 +> **대상 프로젝트**: MNG + +--- + +## 1. 개요 + +### 1.1 목적 + +전자계약(E-Sign) 시스템의 카카오톡 알림톡, SMS, 이메일 발송을 **3개 환경(로컬/개발/운영)**에서 올바르게 설정하고 테스트하기 위한 가이드이다. + +### 1.2 핵심 원칙 + +- **역할 기반 알림**: 본사(creator)는 이메일, 상대방(counterpart)은 카카오톡/SMS +- **환경별 템플릿 분리**: 운영은 원본 템플릿, 개발은 `_DEV` 접미사 템플릿 사용 +- **URL 자동 분기**: `config('app.url')`로 환경별 도메인 자동 적용 + +--- + +## 2. 환경별 설정 + +### 2.1 도메인 및 APP_URL + +| 환경 | `APP_ENV` | `APP_URL` | 알림톡 버튼 URL 도메인 | +|------|-----------|-----------|----------------------| +| 로컬 (Docker) | `local` | `https://mng.sam.kr` | 로컬 — 알림톡 미사용 | +| 개발 서버 | `local` | `https://admin.codebridge-x.com` | `admin.codebridge-x.com` | +| 운영 서버 | `production` | `https://mng.codebridge-x.com` | `mng.codebridge-x.com` | + +### 2.2 바로빌 서버 모드 + +`barobill_members.server_mode` 컬럼으로 바로빌 API 엔드포인트를 결정한다: + +| server_mode | WSDL (카카오톡) | WSDL (SMS) | 용도 | +|-------------|----------------|------------|------| +| `test` | `testws.baroservice.com/KAKAOTALK.asmx` | `testws.baroservice.com/SMS.asmx` | 테스트 | +| `production` | `ws.baroservice.com/KAKAOTALK.asmx` | `ws.baroservice.com/SMS.asmx` | 실제 발송 | + +> `server_mode`는 환경(로컬/개발/운영)과 독립적이다. 개발서버에서도 `production` 모드로 실제 발송 가능. + +### 2.3 알림톡 템플릿 환경별 분기 + +코드에서 `resolveTemplateName()` 메서드가 `APP_ENV`에 따라 템플릿명을 자동 결정한다: + +```php +private function resolveTemplateName(string $baseName): string +{ + return $baseName . (app()->environment('production') ? '' : '_DEV'); +} +``` + +| 기본 템플릿명 | 운영 (`production`) | 개발/로컬 (기타) | +|-------------|--------------------|--------------------| +| `전자계약_서명요청` | `전자계약_서명요청` | `전자계약_서명요청_DEV` | +| `전자계약_완료` | `전자계약_완료` | `전자계약_완료_DEV` | +| `전자계약_리마인드` | `전자계약_리마인드` | `전자계약_리마인드_DEV` | + +--- + +## 3. 등록된 알림톡 템플릿 + +### 3.1 운영 템플릿 (mng.codebridge-x.com) + +| 템플릿명 | 용도 | 상태 | 버튼 URL | +|---------|------|------|---------| +| `전자계약_서명요청` | 서명 요청 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_완료` | 서명 완료 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_리마인드` | 서명 독촉 알림 | 승인 완료 | `https://mng.codebridge-x.com/esign/sign/#{토큰}` | + +### 3.2 개발 템플릿 (admin.codebridge-x.com) + +| 템플릿명 | 용도 | 상태 | 버튼 URL | +|---------|------|------|---------| +| `전자계약_서명요청_DEV` | 서명 요청 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_완료_DEV` | 서명 완료 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | +| `전자계약_리마인드_DEV` | 서명 독촉 알림 | 심사 중 | `https://admin.codebridge-x.com/esign/sign/#{토큰}` | + +> 개발 템플릿 본문은 운영 템플릿과 동일하며, 버튼 URL 도메인만 다르다. + +### 3.3 템플릿 변수 + +| 변수 | 용도 | 사용 템플릿 | +|------|------|-----------| +| `#{이름}` | 서명자 이름 | 서명요청, 완료, 리마인드 | +| `#{계약명}` | 계약 제목 | 서명요청, 완료, 리마인드 | +| `#{기한}` | 서명 기한 | 서명요청, 리마인드 | +| `#{완료일}` | 계약 완료일 | 완료 | +| `#{토큰}` | 서명자 액세스 토큰 | 버튼 URL | + +--- + +## 4. 역할 기반 알림 흐름 + +### 4.1 전체 흐름 + +``` +① 계약 발송 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 +② OTP 인증 ─→ 본사: 이메일 / 상대방: SMS +③ 다음 서명자 ─→ 본사: 이메일 / 상대방: 카카오톡 알림톡 +④ 서명 완료 ─→ 본사: 이메일(PDF) / 상대방: 카카오톡(PDF 다운로드) +``` + +### 4.2 역할 판별 + +```php +$isCounterpart = $signer->role === EsignSigner::ROLE_COUNTERPART; +``` + +| 역할 | 상수 | 알림톡 | SMS(OTP) | 이메일 | +|------|------|--------|----------|--------| +| 본사 (creator) | `ROLE_CREATOR` | ❌ | ❌ | ✅ 항상 | +| 상대방 (counterpart) | `ROLE_COUNTERPART` | ✅ 우선 | ✅ OTP만 | ✅ 폴백 | + +### 4.3 이메일 폴백 조건 + +상대방(counterpart)에게도 이메일을 보내는 경우: +- 전화번호가 없을 때 (`$signer->phone` 없음) +- 알림톡 발송 실패 시 (`$alimtalkFailed = true`) +- 발송 방식이 `email` 또는 `both`일 때 + +### 4.4 완료 알림 특수 처리 + +완료 알림톡 버튼은 **서명 페이지가 아닌 문서 다운로드 URL**로 강제 변경된다: + +```php +// sendCompletionAlimtalk() 내부 +$documentUrl = config('app.url') . '/esign/sign/' . $signer->access_token . '/api/document'; + +// 버튼 URL 강제 변경 (서명페이지 → 문서 다운로드) +if (str_contains($btn[$urlKey], '/esign/sign/') && !str_contains($btn[$urlKey], '/api/document')) { + $btn[$urlKey] = $documentUrl; +} +``` + +--- + +## 5. SMS (OTP 인증) + +### 5.1 발송 조건 + +상대방(counterpart)이 `alimtalk` 또는 `both` 발송 방식이고 전화번호가 있을 때 SMS로 OTP 발송: + +```php +if (in_array($sendMethod, ['alimtalk', 'both']) + && $signer->phone + && $signer->role === EsignSigner::ROLE_COUNTERPART) { + $this->sendOtpViaSms($contract, $signer, $otpCode); +} +``` + +### 5.2 SMS 발송 파라미터 + +| 항목 | 값 | +|------|-----| +| API | `BarobillService::sendSMSMessage()` | +| 발신번호 | `barobill_members.manager_hp` | +| 수신번호 | `esign_signers.phone` | +| 메시지 | `[SAM] 전자계약 인증코드: {코드} (5분 이내 입력)` | +| OTP 유효시간 | 5분 | +| 최대 시도 | 5회 | + +### 5.3 SMS 실패 시 이메일 폴백 + +SMS 발송 실패 → 이메일 OTP 폴백 → 이메일도 없으면 500 에러 반환. + +--- + +## 6. 바로빌 템플릿 등록 절차 + +### 6.1 관리자 페이지 + +``` +https://www.barobill.co.kr 로그인 → 카카오톡 → 템플릿관리 +``` + +### 6.2 DEV 템플릿 등록 시 주의사항 + +1. **본문**: 운영 템플릿과 **완전히 동일** (1글자도 다르면 안 됨) +2. **버튼 URL**: 도메인만 `admin.codebridge-x.com`으로 변경 +3. **템플릿명**: 운영 이름 + `_DEV` 접미사 (예: `전자계약_서명요청_DEV`) +4. **검수 기간**: 영업일 기준 2~3일 + +### 6.3 새 템플릿 추가 시 체크리스트 + +- [ ] 바로빌에서 운영용 + 개발용 2개 등록 +- [ ] 코드에서 `resolveTemplateName('기본명')`으로 호출 +- [ ] 본문의 변수 치환 로직 추가 (str_replace) +- [ ] 버튼 URL의 `#{토큰}` 치환 확인 +- [ ] 2단계 검증 (SendKey → GetSendKakaotalk) 포함 + +--- + +## 7. 관련 파일 + +| 파일 | 역할 | +|------|------| +| `app/Http/Controllers/ESign/EsignApiController.php` | 계약 발송, `sendAlimtalk()`, `resolveTemplateName()` | +| `app/Http/Controllers/ESign/EsignPublicController.php` | OTP SMS, 완료 알림톡, `sendCompletionAlimtalk()` | +| `app/Services/Barobill/BarobillService.php` | SOAP 클라이언트 (`sendATKakaotalkEx`, `sendSMSMessage`) | +| `app/Models/ESign/EsignSigner.php` | `ROLE_CREATOR`, `ROLE_COUNTERPART` 상수 | +| `app/Mail/EsignCompletedMail.php` | 완료 이메일 (PDF 다운로드 링크) | +| `app/Services/ESign/PdfSignatureService.php` | 서명 PDF 합성 (`mergeSignatures`) | + +--- + +## 8. 트러블슈팅 + +### 8.1 환경별 템플릿 미스매치 + +**증상**: `ResultCode=4` (템플릿 데이터 일치 오류) +**원인**: 개발서버에서 운영용 템플릿(`전자계약_서명요청`)으로 발송 시 버튼 URL 도메인 불일치 +**해결**: DEV 템플릿 등록 후 `APP_ENV`가 `production`이 아닌지 확인 + +### 8.2 서명 PDF 누락 (이메일) + +**증상**: 완료 이메일의 다운로드 링크가 서명 없는 초안 PDF 반환 +**원인**: `mergeSignatures()` 실패 → `signed_file_path` 미설정 → preview PDF 폴백 +**해결**: `downloadDocument()`가 완료 상태에서 자동 재생성 시도. 로그에서 trace 확인: + +```bash +# 개발서버 로그 확인 +ssh pro@114.203.209.83 "tail -100 /home/webservice/mng/storage/logs/laravel.log | grep 'PDF 서명'" +``` + +**주요 실패 원인**: +- `storage/fonts/Pretendard-Regular.ttf` 폰트 파일 누락 +- FPDI/TCPDF 패키지 미설치 → `composer install` 필요 +- `storage/app/esign/{tenant_id}/signed/` 디렉토리 권한 문제 + +### 8.3 MNG 모델 상수 누락 + +**증상**: `Undefined constant App\Models\ESign\EsignSigner::ROLE_COUNTERPART` +**원인**: API 프로젝트와 MNG 프로젝트의 모델이 독립적 — API에만 상수 정의됨 +**해결**: MNG `EsignSigner.php`에도 동일한 상수 추가 (2026-02-26 핫픽스 완료) + +--- + +## 관련 문서 + +- [바로빌 카카오톡 연동 README](./README.md) — SOAP API 전체 연동 가이드 +- [E-Sign 기술 설계](../../projects/e-sign/technical-design.md) — 전자계약 아키텍처 +- [E-Sign API 명세](../../projects/e-sign/api-specification.md) — API 엔드포인트 +- [알림톡 연동 계획](../../plans/esign-alimtalk-integration.md) — 초기 계획 (구현 완료) + +--- + +**최종 업데이트**: 2026-02-27 diff --git a/features/business-card-request.md b/features/business-card-request.md new file mode 100644 index 0000000..b574f00 --- /dev/null +++ b/features/business-card-request.md @@ -0,0 +1,173 @@ +# 명함신청 관리 + +> **작성일**: 2026-02-25 +> **상태**: 구현 완료 + +--- + +## 1. 개요 + +### 1.1 목적 + +영업파트너가 명함을 신청하면 본사에서 제작소에 의뢰하고, 완료 후 처리하는 3단계 워크플로우를 제공한다. + +### 1.2 워크플로우 + +``` +요청(pending) ──제작의뢰──→ 제작중(ordered) ──처리완료──→ 완료(processed) + 노랑 파랑 초록 +``` + +### 1.3 메뉴 구조 + +| 메뉴 | URL | 대상 | 설명 | +|------|-----|------|------| +| 파트너 명함신청 | `/sales/business-cards` | 모든 사용자 | 신청폼 + 내 이력 | +| 명함신청 처리 | `/sales/business-cards/manage` | 관리자 전용 | 3단계 처리 + 뱃지 | + +--- + +## 2. 테이블 구조 + +### 2.1 `business_card_requests` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `id` | bigint | PK | +| `tenant_id` | bigint | 테넌트 ID | +| `user_id` | bigint | 신청자 ID | +| `name` | varchar(50) | 성함 | +| `phone` | varchar(20) | 전화번호 | +| `title` | varchar(50) | 직함 (nullable) | +| `email` | varchar(100) | 이메일 (nullable) | +| `quantity` | int | 수량 (기본 100) | +| `memo` | text | 비고 (nullable) | +| `status` | varchar(20) | 상태: `pending`, `ordered`, `processed` | +| `ordered_by` | bigint | 제작의뢰 처리자 ID (nullable) | +| `ordered_at` | timestamp | 제작의뢰 일시 (nullable) | +| `processed_by` | bigint | 처리완료 처리자 ID (nullable) | +| `processed_at` | timestamp | 처리완료 일시 (nullable) | +| `process_memo` | text | 처리 메모 (nullable) | +| `created_at` | timestamp | 생성일 | +| `updated_at` | timestamp | 수정일 | + +**인덱스**: `(tenant_id, status)`, `user_id` + +--- + +## 3. 상태 전이 + +``` +pending ──→ ordered ──→ processed + │ ▲ + └── (역방향 전이 없음) ──┘ +``` + +| 상태 | 라벨 | 색상 | 설명 | +|------|------|------|------| +| `pending` | 요청 | 노랑 | 파트너가 신청, 관리자 확인 대기 | +| `ordered` | 제작의뢰 | 파랑 | 관리자가 제작소에 의뢰 | +| `processed` | 처리완료 | 초록 | 제작 완료, 전달 완료 | + +--- + +## 4. API 엔드포인트 + +| Method | Path | 이름 | 설명 | +|--------|------|------|------| +| GET | `/sales/business-cards` | `sales.business-cards.index` | 파트너 명함신청 (신청폼 + 이력) | +| POST | `/sales/business-cards` | `sales.business-cards.store` | 신청 등록 | +| GET | `/sales/business-cards/manage` | `sales.business-cards.manage` | 관리자 처리 화면 | +| POST | `/sales/business-cards/{id}/order` | `sales.business-cards.order` | 제작의뢰 (관리자) | +| POST | `/sales/business-cards/{id}/process` | `sales.business-cards.process` | 처리완료 (관리자) | + +--- + +## 5. 파일 구조 + +### 5.1 API 프로젝트 + +| 파일 | 설명 | +|------|------| +| `database/migrations/2026_02_24_100000_create_business_card_requests_table.php` | 테이블 생성 | +| `database/migrations/2026_02_25_100000_add_ordered_columns_to_business_card_requests_table.php` | ordered 컬럼 추가 | + +### 5.2 MNG 프로젝트 + +| 파일 | 설명 | +|------|------| +| `app/Models/Sales/BusinessCardRequest.php` | 모델 (상태 상수, 스코프, 헬퍼) | +| `app/Services/Sales/BusinessCardRequestService.php` | 서비스 (CRUD, 통계, 뱃지) | +| `app/Http/Controllers/Sales/BusinessCardRequestController.php` | 컨트롤러 | +| `app/Providers/ViewServiceProvider.php` | 사이드바 뱃지 연동 | +| `routes/web.php` | 라우트 5개 | +| `resources/views/sales/business-cards/admin-index.blade.php` | 관리자 뷰 | +| `resources/views/sales/business-cards/partner-index.blade.php` | 파트너 뷰 | + +--- + +## 6. 화면 구성 + +### 6.1 파트너 명함신청 (`partner-index`) + +``` +┌─ 회사 정보 안내 (코드브릿지엑스) ──────────────┐ +├─ 신청 폼 ─────────────────────────────────────┤ +│ 성함* │ 직함 │ 전화번호* │ 이메일 │ +│ 수량 │ 메모 │ [명함 신청하기] │ +├─ 내 신청 이력 ────────────────────────────────┤ +│ 신청일 │ 성함 │ 직함 │ 전화번호 │ 수량 │ 상태 │ +│ (요청=노랑, 제작중=파랑, 처리완료=초록) │ +└───────────────────────────────────────────────┘ +``` + +- 로그인 사용자 정보(name, phone, email)로 자동 채움 +- 관리자도 동일한 화면 접근 가능 + +### 6.2 명함신청 처리 (`admin-index`) + +``` +┌─ 통계 ──────────────────────────────────────┐ +│ 신규요청(노랑) │ 제작의뢰(파랑) │ 오늘처리(초록) │ 전체 │ +├─────────────────┬───────────────────────────┤ +│ 신규 요청 │ 제작 중 │ +│ [제작의뢰] 버튼 │ 의뢰일 + [처리완료] 버튼 │ +├─────────────────┴───────────────────────────┤ +│ 처리 완료 이력 (하단 스크롤 테이블) │ +└─────────────────────────────────────────────┘ +``` + +- 사이드바 뱃지: 요청 + 제작의뢰 합산 건수 표시 +- 처리 버튼 클릭 시 `showConfirm()` 확인 다이얼로그 + +--- + +## 7. 뱃지 연동 + +`ViewServiceProvider`에서 `BusinessCardRequestService::getPendingCount()`를 호출하여 사이드바 메뉴 뱃지에 대기 건수를 표시한다. + +- **카운트 기준**: `pending` + `ordered` 합산 +- **표시 위치**: "명함신청 처리" 메뉴 (`sales.business-cards.manage`) +- **0건일 때**: 뱃지 미표시 + +--- + +## 8. 메뉴 등록 정보 + +| ID | parent_id | 이름 | URL | sort_order | +|----|-----------|------|-----|------------| +| 15507 | 15456 | 파트너 명함신청 | `/sales/business-cards` | 5 | +| 15508 | 15456 | 명함신청 처리 | `/sales/business-cards/manage` | 6 | + +> 영업파트너에게는 "파트너 명함신청"만 보이도록 메뉴 권한 설정 필요 + +--- + +## 관련 문서 + +- 참고 패턴: `api/app/Models/CompanyRequest.php` (상태 관리 모델) +- 참고 뷰: `mng/resources/views/sales/managers/approvals.blade.php` (2분할 레이아웃) + +--- + +**최종 업데이트**: 2026-02-25 diff --git a/features/credit-evaluation/README.md b/features/credit-evaluation/README.md new file mode 100644 index 0000000..d4d38b3 --- /dev/null +++ b/features/credit-evaluation/README.md @@ -0,0 +1,284 @@ +# 신용평가 시스템 (쿠콘 연동) + +> **작성일**: 2026-03-02 +> **상태**: 운영중 + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM에서 거래처/협력업체의 **기업 신용정보를 조회**하여, 거래 안전성을 사전 판단하는 시스템이다. + +### 1.2 핵심 원칙 + +- **쿠콘(KooCon/나이스평가정보)** API로 기업 신용정보 7개 항목 조회 +- **국세청 공공데이터포털** API로 사업자등록 상태(영업/휴업/폐업) 확인 +- 모든 조회 결과는 DB에 원본 저장 (감사 추적용) +- 테넌트별 월 5건 무료, 초과 시 건당 2,000원 과금 + +--- + +## 2. 시스템 구조 + +### 2.1 전체 흐름 + +``` +사용자 (SAM MNG) + │ + ▼ +CreditController::search() + │ + ├──▶ CooconService::getAllCreditInfo() + │ ├── OA08: 기업 기본정보 + │ ├── OA12: 신용요약정보 + │ ├── OA13: 단기연체정보 + │ ├── OA14: 신용도판단정보 (KCI) + │ ├── OA15: 신용도판단정보 (CB) + │ ├── OA16: 당좌거래정지정보 + │ └── OA17: 법정관리/워크아웃 + │ + ├──▶ NtsBusinessService::getBusinessStatus() + │ └── 국세청 사업자등록 상태 조회 + │ + └──▶ CreditInquiry::createFromApiResponse() + └── DB에 조회 이력 저장 +``` + +### 2.2 파트너 구조 + +| 역할 | 대상 | 설명 | +|------|------|------| +| **API 제공사** | 쿠콘(KooCon) / 나이스평가정보 | 기업 신용정보 API 플랫폼 | +| **파트너사** | (주)코드브릿지엑스 | API 키 보유, 쿠콘과 직접 계약 | +| **이용사** | 각 테넌트 (주일, 경동 등) | SAM을 통해 신용조회 실행 | + +--- + +## 3. 쿠콘(KooCon) API + +### 3.1 API 엔드포인트 + +| 환경 | URL | +|------|-----| +| 테스트 | `https://dev2.coocon.co.kr:8443/sol/gateway/oapi_relay.jsp` | +| 운영 | `https://sgw.coocon.co.kr/sol/gateway/oapi_relay.jsp` | + +### 3.2 인증 방식 + +- **API_KEY**: 쿠콘에서 발급받은 인증키 (DB `coocon_configs` 테이블에서 관리) +- **API_ID**: 조회할 API 식별자 (OA08~OA17) +- **TR_SEQ**: 거래일련번호 (중복 방지용, `YmdHis` + 마이크로초 6자리) + +### 3.3 요청 형식 + +```json +{ + "API_KEY": "발급받은_API_키", + "API_ID": "OA12", + "TR_SEQ": "20260302173000123456", + "COMPANY_KEY": "1234567890" +} +``` + +- **Method**: POST +- **Content-Type**: application/json +- **Timeout**: 30초 + +### 3.4 API 목록 + +| API ID | 상수명 | 설명 | 데이터 출처 | +|--------|--------|------|------------| +| `OA08` | `API_COMPANY_INFO` | 기업 기본정보 | 나이스평가정보 | +| `OA12` | `API_CREDIT_SUMMARY` | 신용요약정보 (이슈 건수 요약) | 나이스평가정보 | +| `OA13` | `API_SHORT_TERM_OVERDUE` | 단기연체정보 | 한국신용정보원 | +| `OA14` | `API_NEGATIVE_INFO_KCI` | 신용도판단정보 (KCI) | 한국신용정보원 + 공공정보 | +| `OA15` | `API_NEGATIVE_INFO_CB` | 신용도판단정보 (CB) | 신용정보사 | +| `OA16` | `API_SUSPENSION_INFO` | 당좌거래정지정보 | 금융결제원 | +| `OA17` | `API_WORKOUT_INFO` | 법정관리/워크아웃정보 | 법원 | + +### 3.5 응답 형식 + +```json +{ + "RSLT_CD": "00000000", + "RSLT_MSG": "정상처리되었습니다.", + "RSLT_DATA": { ... } +} +``` + +- `RSLT_CD === '00000000'`: 성공 +- 기타 값: 에러 (에러 메시지는 `RSLT_MSG`에 포함) + +--- + +## 4. 국세청 사업자등록 조회 API + +### 4.1 API 정보 + +| 항목 | 값 | +|------|------| +| URL | `https://api.odcloud.kr/api/nts-businessman/v1/status` | +| 인증 | serviceKey (쿼리 파라미터) | +| 출처 | 공공데이터포털 | + +### 4.2 상태 코드 + +| 코드 | 상태 | 설명 | +|------|------|------| +| `01` | 계속사업자 | 정상 영업 중 | +| `02` | 휴업자 | 영업 중지 | +| `03` | 폐업자 | 사업 종료 | + +--- + +## 5. 데이터베이스 + +### 5.1 `coocon_configs` — API 설정 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `name` | VARCHAR(100) | 설정 이름 | +| `environment` | ENUM('test', 'production') | 환경 | +| `api_key` | VARCHAR(100) | 쿠콘 API 키 | +| `base_url` | VARCHAR(255) | API 기본 URL | +| `description` | TEXT | 설명 | +| `is_active` | BOOLEAN | 활성화 여부 | + +> **규칙**: 환경당 1개만 활성화 가능. 새 설정 활성화 시 기존 설정은 자동 비활성화. + +### 5.2 `credit_inquiries` — 조회 이력 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 | +| `inquiry_key` | VARCHAR(32) UNIQUE | 조회 고유키 | +| `company_key` | VARCHAR(20) | 사업자번호/법인번호 | +| `company_name` | VARCHAR | 업체명 | +| `user_id` | BIGINT FK | 조회자 | +| `inquired_at` | TIMESTAMP | 조회 일시 | +| `nts_status` | VARCHAR(20) | 국세청 상태 | +| `nts_status_code` | VARCHAR(2) | 국세청 상태코드 | +| `short_term_overdue_cnt` | UINT | 단기연체 건수 | +| `negative_info_kci_cnt` | UINT | KCI 건수 | +| `negative_info_pb_cnt` | UINT | 공공정보 건수 | +| `negative_info_cb_cnt` | UINT | CB 건수 | +| `suspension_info_cnt` | UINT | 당좌거래정지 건수 | +| `workout_cnt` | UINT | 법정관리/워크아웃 건수 | +| `raw_*` | JSON | 각 API 원본 응답 (7개 + NTS) | +| `status` | ENUM | success / partial / failed | + +--- + +## 6. 과금 정책 + +| 항목 | 값 | +|------|------| +| 월 무료 할당량 | **5건** | +| 초과 건당 요금 | **2,000원** | +| 계산식 | `max(0, (조회건수 - 5)) × 2,000` | + +### 요금 예시 + +| 월 조회 건수 | 무료 | 유료 | 요금 | +|-------------|------|------|------| +| 3건 | 3 | 0 | 0원 | +| 5건 | 5 | 0 | 0원 | +| 10건 | 5 | 5 | 10,000원 | +| 20건 | 5 | 15 | 30,000원 | + +--- + +## 7. 환경 설정 + +### 7.1 테스트/운영 분리 + +| 환경 | API URL | 설명 | +|------|---------|------| +| 테스트 | `dev2.coocon.co.kr:8443` | 개발/검증용 (과금 없음) | +| 운영 | `sgw.coocon.co.kr` | 실 서비스 (과금 발생) | + +- `coocon_configs` 테이블에서 환경별로 별도 설정 관리 +- 각 환경에서 `is_active=true`인 설정 1개만 사용 + +### 7.2 필요한 설정 + +| 항목 | 관리 위치 | 설명 | +|------|----------|------| +| 쿠콘 API 키 | DB (`coocon_configs`) | 쿠콘에서 발급 | +| 쿠콘 API URL | DB (`coocon_configs`) | 환경별 URL | +| 국세청 API 키 | 코드 내 하드코딩 | 공공데이터포털 발급 | + +--- + +## 8. MNG 라우트 + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/credit/inquiry` | 조회 이력 목록 | +| POST | `/credit/inquiry/search` | 신용정보 조회 실행 | +| POST | `/credit/inquiry/test` | API 연결 테스트 | +| GET | `/credit/inquiry/{key}/raw` | 원본 데이터 조회 | +| GET | `/credit/inquiry/{key}/report` | 리포트 조회 | +| DELETE | `/credit/inquiry/{id}` | 이력 삭제 | +| GET | `/credit/usage` | 조회회수 집계 | +| GET | `/credit/settings` | 설정 관리 | +| POST | `/credit/settings` | 설정 생성 | +| PUT | `/credit/settings/{id}` | 설정 수정 | +| DELETE | `/credit/settings/{id}` | 설정 삭제 | +| POST | `/credit/settings/{id}/toggle` | 활성화 토글 | + +--- + +## 9. 에러 코드 + +### 9.1 쿠콘 API + +| 코드 | 설명 | +|------|------| +| `NO_CONFIG` | API 설정 없음 | +| `HTTP_ERROR` | HTTP 통신 오류 | +| `EXCEPTION` | 예외 발생 | +| `RSLT_CD ≠ 00000000` | 쿠콘 API 에러 (RSLT_MSG 참조) | + +### 9.2 국세청 API + +| 코드 | 설명 | +|------|------| +| `INVALID_FORMAT` | 사업자번호 형식 오류 | +| `NOT_FOUND` | 조회 결과 없음 | +| `HTTP_ERROR` | HTTP 통신 오류 | + +--- + +## 10. 관련 파일 + +### MNG 프로젝트 + +| 구분 | 경로 | +|------|------| +| 컨트롤러 | `app/Http/Controllers/Credit/CreditController.php` | +| 컨트롤러 | `app/Http/Controllers/Credit/CreditUsageController.php` | +| 서비스 | `app/Services/Coocon/CooconService.php` | +| 서비스 | `app/Services/Nts/NtsBusinessService.php` | +| 모델 | `app/Models/Coocon/CooconConfig.php` | +| 모델 | `app/Models/Credit/CreditInquiry.php` | +| 뷰 | `resources/views/credit/inquiry/index.blade.php` | +| 뷰 | `resources/views/credit/usage/index.blade.php` | +| 뷰 | `resources/views/credit/settings/index.blade.php` | + +### API 프로젝트 (마이그레이션) + +| 경로 | +|------| +| `database/migrations/2026_01_22_192637_create_coocon_configs_table.php` | +| `database/migrations/2026_01_22_201143_create_credit_inquiries_table.php` | +| `database/migrations/2026_01_22_203001_add_company_info_to_credit_inquiries_table.php` | +| `database/migrations/2026_01_28_163000_add_tenant_id_to_credit_inquiries_table.php` | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/features/documents/mng-document-system.md b/features/documents/mng-document-system.md new file mode 100644 index 0000000..eae95a6 --- /dev/null +++ b/features/documents/mng-document-system.md @@ -0,0 +1,738 @@ +# MNG 문서관리 시스템 상세 기술 명세 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **관련**: [README.md](README.md) (API 명세) + +--- + +## 1. 개요 + +### 1.1 목적 + +블라인드/스크린 제조 현장의 **검사 성적서, 작업일지, 수입검사 기록** 등 품질/생산 문서를 전자화하여 관리하는 시스템. 문서 양식(Template)을 정의하면 EAV 패턴으로 데이터를 동적 저장하며, 다단계 결재 워크플로우를 지원한다. + +### 1.2 핵심 특징 + +| 특징 | 설명 | +|------|------| +| **EAV 패턴** | 양식별로 다른 필드를 하나의 `document_data` 테이블에 저장 | +| **2가지 양식 빌더** | 레거시 빌더 (DB 정규화) + 블록 빌더 (A4 JSON 스키마) | +| **결재 워크플로우** | 작성 → 검토 → 승인 (다단계 순차 결재) | +| **자동 데이터 매핑** | 작업지시서/수주 데이터에서 기본필드 자동 채움 | +| **다형성 연결** | work_order, sales_order 등 다양한 모델과 연결 | +| **자재 LOT 추적** | 검사 문서에서 투입 자재의 LOT 이력 조회 | + +### 1.3 문서 구조 + +| 문서 | 설명 | +|------|------| +| [README.md](README.md) | API 엔드포인트, 모델 요약, FormRequest | +| **이 문서** | MNG 화면별 상세, 동작원리, 데이터 흐름 | + +--- + +## 2. 메뉴/탭 구조 + +``` +생산 관리 +└── 문서관리 + ├── 문서 목록 /documents ← 문서 검색/필터/관리 + ├── 새 문서 작성 /documents/create ← 템플릿 선택 → 폼 입력 + ├── 문서 상세 /documents/{id} ← 읽기 전용 + 결재 현황 + ├── 문서 수정 /documents/{id}/edit ← DRAFT/REJECTED만 + ├── 인쇄 /documents/{id}/print ← 성적서 인쇄용 + │ + └── 문서양식 관리 + ├── 양식 목록 /document-templates ← 양식 검색/관리 + ├── 새 양식 (레거시) /document-templates/create ← 레거시 빌더 + ├── 양식 수정 /document-templates/{id}/edit ← 자동 빌더 판별 + ├── 양식 디자이너 /document-templates/block-create ← 블록 빌더 + └── 블록 수정 /document-templates/{id}/block-edit ← 블록 빌더 수정 +``` + +--- + +## 3. 파일 구조 + +``` +mng/ +├── app/Http/Controllers/ +│ ├── DocumentController.php ← 문서 CRUD 화면 +│ └── DocumentTemplateController.php ← 양식 관리 화면 +├── app/Models/Documents/ +│ ├── Document.php ← 문서 모델 +│ ├── DocumentApproval.php ← 결재 단계 +│ ├── DocumentData.php ← EAV 데이터 +│ ├── DocumentTemplate.php ← 양식 마스터 +│ └── ... (기타 템플릿 관련 모델) +└── resources/views/ + ├── documents/ + │ ├── index.blade.php ← 문서 목록 + │ ├── edit.blade.php ← 문서 작성/수정 + │ ├── show.blade.php ← 문서 상세 + │ └── print.blade.php ← 인쇄 전용 + └── document-templates/ + ├── index.blade.php ← 양식 목록 + ├── edit.blade.php ← 레거시 빌더 + ├── block-editor.blade.php ← 블록 빌더 + └── partials/ + ├── block-palette.blade.php ← 블록 타입 목록 + ├── block-canvas.blade.php ← 편집 캔버스 + └── block-properties.blade.php ← 속성 패널 +``` + +--- + +## 4. 데이터베이스 아키텍처 + +### 4.1 테이블 관계도 + +``` +document_templates (양식 마스터) +├── 1:N → document_template_approval_lines (결재선 정의) +├── 1:N → document_template_basic_fields (기본필드 정의) +├── 1:N → document_template_sections (섹션 정의) +│ └── 1:N → document_template_section_items (검사항목) +├── 1:N → document_template_columns (테이블 컬럼 정의) +├── 1:N → document_template_section_fields (섹션 필드) +├── 1:N → document_template_links (외부 연결 정의) +│ └── 1:N → document_template_link_values (템플릿 레벨 연결값) +│ +└── 1:N → documents (문서 인스턴스) + ├── 1:N → document_approvals (결재 진행) + ├── 1:N → document_data (EAV 필드값) + ├── 1:N → document_attachments (첨부파일) + └── 1:N → document_links (문서 레벨 연결) +``` + +### 4.2 documents (문서) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `template_id` | BIGINT FK | 사용 양식 | +| `document_no` | VARCHAR UNIQUE | 문서번호 (자동 채번) | +| `title` | VARCHAR | 문서 제목 | +| `status` | VARCHAR(20) | 상태 (5가지) | +| `linkable_type` | VARCHAR NULL | 다형성 모델 타입 | +| `linkable_id` | BIGINT NULL | 다형성 모델 ID | +| `submitted_at` | TIMESTAMP NULL | 결재 요청 일시 | +| `completed_at` | TIMESTAMP NULL | 결재 완료 일시 | +| `created_by` | BIGINT FK | 작성자 | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `(tenant_id, status)`, `document_no`, `(linkable_type, linkable_id)` + +### 4.3 document_data (EAV 필드값) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `document_id` | BIGINT FK | 소속 문서 | +| `section_id` | BIGINT FK NULL | 소속 섹션 (NULL=기본필드) | +| `column_id` | BIGINT FK NULL | 소속 컬럼 (테이블 데이터용) | +| `row_index` | INT | 테이블 행 번호 (기본: 0) | +| `field_key` | VARCHAR | 필드 식별자 (`bf_1`, `cf_2`, `col_3`) | +| `field_value` | TEXT NULL | 실제 값 | + +**인덱스**: `(document_id, section_id)`, `(document_id, field_key)` + +### 4.4 document_approvals (결재) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `document_id` | BIGINT FK | 소속 문서 | +| `user_id` | BIGINT FK | 결재자 | +| `step` | INT | 결재 순서 (1, 2, 3...) | +| `role` | VARCHAR | 역할 (작성, 검토, 승인) | +| `status` | VARCHAR(20) | PENDING / APPROVED / REJECTED | +| `comment` | TEXT NULL | 결재 의견 | +| `acted_at` | TIMESTAMP NULL | 처리 일시 | + +**인덱스**: `(document_id, step)`, `(user_id, status)` + +### 4.5 document_attachments (첨부파일) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `document_id` | BIGINT FK | 소속 문서 | +| `file_id` | BIGINT FK | File 모델 연결 | +| `attachment_type` | VARCHAR | `general`, `signature`, `image`, `reference` | +| `description` | VARCHAR NULL | 설명 | +| `created_by` | BIGINT FK | 업로드자 | + +--- + +## 5. 양식(Template) 시스템 + +### 5.1 두 가지 빌더 방식 + +| 방식 | 필드명 | 저장 구조 | UI | 상태 | +|------|--------|----------|-----|------| +| **레거시 빌더** | `builder_type = null` | 정규화 테이블들 | `edit.blade.php` | 기존 양식용 | +| **블록 빌더** | `builder_type = 'block'` | `schema` JSON | `block-editor.blade.php` | 신규 양식용 | + +**자동 판별 로직:** + +```php +// DocumentTemplateController::edit() +if ($template->isBlockBuilder()) { + return $this->blockEdit($id); // block-editor.blade.php +} else { + return view('document-templates.edit'); // 레거시 +} +``` + +### 5.2 양식 마스터 (document_templates) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `name` | VARCHAR | 양식명 (예: "제품검사 성적서") | +| `category` | VARCHAR | 분류 (common_codes 기반) | +| `title` | VARCHAR NULL | 문서 제목 템플릿 | +| `company_name` | VARCHAR NULL | 회사명 | +| `company_address` | VARCHAR NULL | 회사 주소 | +| `company_contact` | VARCHAR NULL | 연락처 | +| `footer_remark_label` | VARCHAR NULL | 비고란 라벨 | +| `footer_judgement_label` | VARCHAR NULL | 판정란 라벨 | +| `footer_judgement_options` | JSON NULL | 판정 선택지 (적합/부적합) | +| `builder_type` | VARCHAR NULL | `block` 또는 NULL | +| `schema` | JSON NULL | 블록 빌더 JSON 스키마 | +| `page_config` | JSON NULL | 페이지 설정 (A4, 여백 등) | +| `is_active` | BOOLEAN | 활성 여부 | + +### 5.3 레거시 빌더 구성 요소 + +#### 결재선 (document_template_approval_lines) + +``` +step 1: 작성 (작성자 본인) +step 2: 검토 (팀장) +step 3: 승인 (부장) +``` + +| 컬럼 | 설명 | +|------|------| +| `name` | 라벨 (작성, 검토, 승인) | +| `dept` | 부서 | +| `role` | 역할 | +| `sort_order` | 순서 | + +#### 기본필드 (document_template_basic_fields) + +문서 상단의 고정 필드 영역. + +| 컬럼 | 설명 | +|------|------| +| `label` | 필드 라벨 (품명, LOT NO, 납기일 등) | +| `field_key` | 식별자 (EAV 저장 시 사용) | +| `field_type` | 입력 타입 (text, date, number, item_search) | +| `default_value` | 기본값 | +| `sort_order` | 순서 | + +**EAV 저장 시 field_key 패턴:** + +``` +bf_1 → 기본필드 ID 1 (예: 품명) +bf_2 → 기본필드 ID 2 (예: LOT NO) +bf_3 → 기본필드 ID 3 (예: 납기일) +``` + +#### 섹션 (document_template_sections) + +검사 기준서의 섹션 단위. + +| 컬럼 | 설명 | +|------|------| +| `title` | 섹션 제목 (예: "겉모양 검사", "치수 검사") | +| `image_path` | 도해 이미지 경로 (검사 부위 도면) | +| `sort_order` | 순서 | + +#### 검사항목 (document_template_section_items) + +각 섹션 내의 개별 검사항목. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `category` | VARCHAR | 구분 (겉모양, 치수, 재질) | +| `item` | VARCHAR | 검사항목명 | +| `standard` | VARCHAR | 검사기준 (100mm ±5mm) | +| `tolerance` | JSON NULL | 허용오차 (min/max) | +| `standard_criteria` | VARCHAR NULL | 판정기준 | +| `method` | VARCHAR | 검사방법 (육안, 측정) | +| `measurement_type` | VARCHAR NULL | 측정 유형 | +| `frequency_n` | INT NULL | 검사건수 N | +| `frequency_c` | INT NULL | 합격건수 C | +| `frequency` | VARCHAR NULL | 검사빈도 텍스트 | +| `field_values` | JSON NULL | 확장 필드 (마이그레이션 없이 추가) | + +#### 테이블 컬럼 (document_template_columns) + +검사 데이터 테이블의 컬럼 정의. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `label` | VARCHAR | 컬럼 라벨 | +| `width` | INT NULL | 너비 (px) | +| `column_type` | VARCHAR | `text`, `check`, `complex`, `measurement`, `select` | +| `group_name` | VARCHAR NULL | 상단 병합 헤더명 | +| `sub_labels` | JSON NULL | complex 타입 하위 라벨 | +| `sort_order` | INT | 순서 | + +**컬럼 타입 상세:** + +| 타입 | 설명 | 예시 | +|------|------|------| +| `text` | 단순 텍스트 입력 | 비고, 메모 | +| `check` | 체크박스 (합격/부적합) | 외관 검사 합격 여부 | +| `complex` | 여러 서브필드 조합 | 측정값 + 단위 + 판정 | +| `measurement` | 수치 입력 | 길이: 100.5mm | +| `select` | 드롭다운 선택 | 판정: 합격/불합격/보류 | + +#### 외부 연결 (document_template_links) + +템플릿에서 외부 테이블 데이터를 참조하기 위한 정의. + +| 컬럼 | 설명 | +|------|------| +| `link_key` | 연결 식별자 | +| `label` | 화면 라벨 | +| `link_type` | `single` (1개 선택) / `multiple` (다중 선택) | +| `source_table` | 소스 테이블 (`items`, `processes`, `users`) | +| `search_params` | API 검색 추가 조건 (JSON) | +| `display_fields` | 표시 필드 (title, subtitle) | +| `is_required` | 필수 여부 | + +### 5.4 블록 빌더 구조 + +**페이지 설정 (page_config):** + +```json +{ + "size": "A4", + "orientation": "portrait", + "margin": { + "top": 20, + "right": 15, + "bottom": 20, + "left": 15 + } +} +``` + +**스키마 (schema):** + +블록 배열로 레이아웃 정의. 드래그앤드롭으로 편집. + +```json +{ + "blocks": [ + { "type": "text", "x": 0, "y": 0, "width": 100, "content": "검사 성적서" }, + { "type": "table", "x": 0, "y": 50, "columns": [...], "rows": [...] }, + { "type": "image", "x": 200, "y": 100, "src": "..." } + ] +} +``` + +**블록 빌더 UI (3패널):** + +``` +┌──────────┬────────────────────┬──────────┐ +│ 블록 │ │ 속성 │ +│ 팔레트 │ A4 캔버스 │ 패널 │ +│ │ │ │ +│ [텍스트] │ ┌──────────────┐ │ 너비: _ │ +│ [이미지] │ │ 드래그앤드롭 │ │ 높이: _ │ +│ [표] │ │ 블록 배치 │ │ 색상: _ │ +│ [선] │ │ │ │ 폰트: _ │ +│ [도형] │ └──────────────┘ │ │ +└──────────┴────────────────────┴──────────┘ +``` + +--- + +## 6. EAV 데이터 저장 패턴 + +### 6.1 핵심 개념 + +하나의 `document_data` 테이블에 **모든 양식의 모든 필드값**을 저장. 양식이 다르면 field_key가 다르고, 같은 양식이라도 섹션/행이 다르면 section_id/row_index로 구분. + +### 6.2 저장 구조 + +``` +document_data 레코드 예시: + +기본필드 (상단 고정 영역): +┌─────────────┬────────────┬───────────┬───────────┬───────────┬─────────────┐ +│ document_id │ section_id │ column_id │ row_index │ field_key │ field_value │ +├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ +│ 42 │ NULL │ NULL │ 0 │ bf_1 │ 블라인드A │ ← 품명 +│ 42 │ NULL │ NULL │ 0 │ bf_2 │ LOT-2026-001│ ← LOT NO +│ 42 │ NULL │ NULL │ 0 │ bf_3 │ 2026-03-15 │ ← 납기일 +├─────────────┼────────────┼───────────┼───────────┼───────────┼─────────────┤ + +테이블 데이터 (섹션별 검사 결과): +│ 42 │ 10 │ 20 │ 0 │ col_20 │ 합격 │ ← 섹션10, 컬럼20, 1행 +│ 42 │ 10 │ 20 │ 1 │ col_20 │ 부적합 │ ← 섹션10, 컬럼20, 2행 +│ 42 │ 10 │ 21 │ 0 │ col_21 │ 100.5 │ ← 섹션10, 컬럼21, 1행 +└─────────────┴────────────┴───────────┴───────────┴───────────┴─────────────┘ +``` + +### 6.3 field_key 네이밍 규칙 + +| 접두사 | 의미 | 예시 | +|--------|------|------| +| `bf_` | 기본필드 (BasicField) | `bf_1`, `bf_2` | +| `cf_` | 섹션필드 (SectionField) | `cf_5`, `cf_6` | +| `col_` | 컬럼 데이터 | `col_20`, `col_21` | + +### 6.4 데이터 조회 패턴 + +```php +// 기본필드 값 조회 +$data = DocumentData::where('document_id', $id) + ->whereNull('section_id') + ->get() + ->keyBy('field_key'); + +$productName = $data['bf_1']->field_value; + +// 섹션별 테이블 데이터 조회 +$rows = DocumentData::where('document_id', $id) + ->where('section_id', $sectionId) + ->get() + ->groupBy('row_index'); +``` + +--- + +## 7. 결재 워크플로우 + +### 7.1 상태 전이 + +``` +DRAFT (작성중) + │ + ├── submit() → PENDING (결재중) + │ │ + │ ├── approve() [step 1] → 다음 step 대기 + │ ├── approve() [step 2] → 다음 step 대기 + │ ├── approve() [마지막] → APPROVED (승인) + │ │ + │ └── reject() → REJECTED (반려) + │ │ + │ └── edit → submit() → PENDING (재요청) + │ + └── cancel() → CANCELLED (취소) +``` + +### 7.2 상태값 및 라벨 + +| 코드 | 라벨 | 색상 | 편집 가능 | +|------|------|------|----------| +| `DRAFT` | 작성중 | gray | 예 | +| `PENDING` | 결재중 | yellow | 아니오 | +| `APPROVED` | 승인 | green | 아니오 | +| `REJECTED` | 반려 | red | 예 (수정 후 재요청) | +| `CANCELLED` | 취소 | gray | 아니오 | + +### 7.3 결재 단계 (Approval) + +``` +DocumentTemplateApprovalLine (양식 정의) + ↓ (문서 생성 시 복사) +DocumentApproval (문서별 결재 레코드) + +step 1: 작성 → PENDING → 결재자 승인 → APPROVED +step 2: 검토 → PENDING → 결재자 승인 → APPROVED +step 3: 승인 → PENDING → 결재자 승인 → APPROVED → 문서 전체 APPROVED +``` + +### 7.4 결재 판단 메서드 + +```php +// Document 모델 +canEdit() // DRAFT 또는 REJECTED +canSubmit() // DRAFT 또는 REJECTED +canApprove() // PENDING (현재 결재자만) +canCancel() // DRAFT 또는 PENDING (작성자만) +``` + +--- + +## 8. 자동 데이터 매핑 + +### 8.1 개요 + +문서 작성/수정 시, 연결된 작업지시서(work_order)/수주(order) 데이터에서 기본필드를 **자동으로 채움**. 사용자 입력 부담을 줄이고 데이터 정확성을 보장. + +### 8.2 검사 성적서 매핑 (field_key 기반) + +| field_key | 라벨 | 소스 | +|-----------|------|------| +| `product_name` | 품명 | `workOrderItem.item_name` | +| `specification` | 규격 | `workOrderItem.specification` | +| `lot_no` | LOT NO | `order.order_no` | +| `lot_size` | LOT 크기 | `"N 개소"` (개소 수 기반) | +| `client` | 발주처 | `order.client_name` | +| `site_name` | 현장명 | `workOrder.project_name` | +| `inspection_date` | 검사일 | `workOrderItem.options.inspection_data.inspected_at` | +| `inspector` | 검사자 | 검사자 이름 | + +### 8.3 작업일지 매핑 (label 기반) + +| label 포함 문자열 | 소스 | +|------------------|------| +| `발주처` | `order.client_name` | +| `현장명` | `workOrder.project_name` | +| `작업일자` | `now()` | +| `LOT NO`, `LOT` | `order.order_no` | +| `납기일`, `납기` | `order.delivery_date` | +| `작업지시번호` | `workOrder.work_order_no` | +| `수주일` | `order.received_at` 또는 `order.created_at` | + +### 8.4 자동 매핑 흐름 + +``` +문서 작성/수정 페이지 로드 + ↓ +DocumentController::edit() + ↓ +resolveAndBackfillBasicFields($template, $document) + ↓ +linkable_type 확인 (work_order? order?) + ↓ +field_key 또는 label 매칭 + ↓ +DB에 값이 없으면 → 소스 데이터에서 resolve + ↓ +뷰에 자동 채움된 값 전달 +``` + +--- + +## 9. 자재 LOT 추적 + +### 9.1 개요 + +검사 성적서에서 해당 작업지시의 **투입 자재 LOT 이력**을 조회. `stock_transactions` 테이블의 OUT(투입)/IN(취소) 트랜잭션을 상쇄하여 순수 투입량을 계산. + +### 9.2 추적 구조 + +``` +work_orders (작업지시) + │ + ├── stock_transactions (재고 트랜잭션) + │ ├── OUT (투입): qty < 0 + │ └── IN (취소/반납): qty > 0 + │ → 순수 투입량 = ABS(SUM(qty)) where qty < 0 + │ + └── work_order_material_inputs (개소별 투입자재) + └── stock_lots (LOT 정보) JOIN +``` + +### 9.3 표시 내용 + +| 항목 | 설명 | +|------|------| +| 자재명 | 투입된 원자재/부자재 이름 | +| LOT 번호 | 자재의 LOT 식별 번호 | +| 투입 수량 | OUT 트랜잭션 합계 (절대값) | +| 투입일 | 트랜잭션 일시 | + +--- + +## 10. 화면별 상세 + +### 10.1 문서 목록 (/documents) + +**필터 항목:** + +| 필터 | 타입 | 설명 | +|------|------|------| +| 검색 | text | 문서번호 또는 제목 | +| 상태 | dropdown | DRAFT, PENDING, APPROVED, REJECTED, CANCELLED, 휴지통(admin) | +| 양식분류 | dropdown | category | +| 템플릿 | dropdown | template_id | +| 날짜 범위 | date | created_at (from ~ to) | + +**목록 테이블 컬럼:** + +``` +문서번호 | 제목 | 양식 | 상태 | 작성자 | 작성일 | 결재현황 +``` + +### 10.2 문서 작성/수정 (/documents/create, /documents/{id}/edit) + +**폼 구성:** + +``` +┌──────────────────────────────────────────────┐ +│ 템플릿 선택 (읽기전용) │ +│ 제목 (필수) │ +├──────────────────────────────────────────────┤ +│ 기본 필드 (template.basicFields) │ +│ ┌─────────────────┬─────────────────┐ │ +│ │ 품명: [자동채움] │ LOT NO: [자동] │ │ +│ │ 납기일: [날짜] │ 발주처: [자동] │ │ +│ └─────────────────┴─────────────────┘ │ +├──────────────────────────────────────────────┤ +│ 섹션 1: 겉모양 검사 │ +│ ┌──────────────────────────────────────┐ │ +│ │ 도해 이미지 (있으면) │ │ +│ ├──────┬──────┬──────┬──────┬──────┤ │ +│ │ 구분 │ 항목 │ 기준 │ 결과1│ 결과2│ │ +│ ├──────┼──────┼──────┼──────┼──────┤ │ +│ │ 치수 │ 길이 │±5mm │ [ ] │ [ ] │ │ +│ │ 외관 │ 흠집 │ 없음 │ [✓] │ [✓] │ │ +│ ├──────┴──────┴──────┴──────┴──────┤ │ +│ │ [+ 행 추가] [행 삭제] │ │ +│ └──────────────────────────────────────┘ │ +├──────────────────────────────────────────────┤ +│ 외부 연결 (template.links) │ +│ 품목 선택: [검색 드롭다운] │ +├──────────────────────────────────────────────┤ +│ 첨부파일 │ +│ [일반 문서] [서명 이미지] [검사 사진] [참고 자료] │ +├──────────────────────────────────────────────┤ +│ [임시저장] [결재 요청] │ +└──────────────────────────────────────────────┘ +``` + +### 10.3 문서 상세 (/documents/{id}) + +**읽기 전용 표시:** + +``` +┌──────────────────────────────────────────────┐ +│ 문서번호: DOC-260306-001 상태: [🟢 승인] │ +│ 제목: 블라인드A 검사 성적서 │ +├──────────────────────────────────────────────┤ +│ 기본 필드 (읽기 전용) │ +├──────────────────────────────────────────────┤ +│ 검사 데이터 테이블 (읽기 전용) │ +├──────────────────────────────────────────────┤ +│ 결재 현황 │ +│ ┌────────┬────────┬────────┐ │ +│ │ 작성 │ 검토 │ 승인 │ │ +│ │ 홍길동 │ 김과장 │ 박부장 │ │ +│ │ ✓승인 │ ✓승인 │ ●대기 │ │ +│ └────────┴────────┴────────┘ │ +├──────────────────────────────────────────────┤ +│ 자재 투입 LOT (작업지시 연결 시) │ +│ ┌────────┬──────────┬──────┬──────┐ │ +│ │ 자재명 │ LOT 번호 │ 수량 │ 투입일│ │ +│ └────────┴──────────┴──────┴──────┘ │ +├──────────────────────────────────────────────┤ +│ 첨부파일 목록 │ +├──────────────────────────────────────────────┤ +│ [수정] [인쇄] [결재 승인] [결재 반려] │ +└──────────────────────────────────────────────┘ +``` + +### 10.4 인쇄 (/documents/{id}/print) + +성적서 형식의 인쇄 전용 화면. `window.print()` 호출. 작업지시 관련 자재(work_order_items) 데이터 포함. + +### 10.5 양식 목록 (/document-templates) + +**필터:** +- 검색: 양식명, 제목, 분류 +- 카테고리: common_codes 기반 + 기존 데이터 폴백 +- 활성 상태: 활성 / 비활성 / 휴지통(admin) + +**HTMX**: 필터 변경 시 테이블 영역만 부분 로드 + +--- + +## 11. 첨부파일 유형 + +| 유형 | 코드 | 용도 | 예시 | +|------|------|------|------| +| 일반 문서 | `general` | PDF, 엑셀 등 | 규격서, 보고서 | +| 서명 이미지 | `signature` | 검사 완료 서명 | 검사자 서명 사진 | +| 검사 사진 | `image` | 검사 증빙 사진 | 불량 부위 촬영 | +| 참고 자료 | `reference` | 참고용 문서 | KS 규격, 작업 지침 | + +--- + +## 12. API 연동 (MNG → API) + +MNG 뷰에서 데이터 저장/삭제는 **API 서버를 호출**하여 처리. GET 요청(뷰 렌더링)은 MNG 컨트롤러가 직접 처리. + +| 작업 | MNG (GET 요청) | API (POST/PUT/DELETE) | +|------|---------------|----------------------| +| 목록 조회 | `DocumentController::index()` | `GET /v1/documents` | +| 상세 조회 | `DocumentController::show()` | `GET /v1/documents/{id}` | +| 생성 | 폼 표시만 | `POST /v1/documents` | +| 수정 | 폼 표시만 | `PATCH /v1/documents/{id}` | +| 삭제 | - | `DELETE /v1/documents/{id}` | +| 결재 요청 | - | `POST /v1/documents/{id}/submit` | +| 승인 | - | `POST /v1/documents/{id}/approve` | +| 반려 | - | `POST /v1/documents/{id}/reject` | + +--- + +## 13. 카테고리 해결 로직 + +양식 카테고리는 **common_codes 테이블**에서 조회하되, 없으면 **기존 데이터에서 추출**하여 폴백. + +```php +// DocumentTemplateController::getCategories() +$categories = CommonCode::where('group', 'document_category') + ->orderBy('sort_order') + ->get(); + +if ($categories->isEmpty()) { + // 폴백: 기존 템플릿의 category 값에서 중복 제거 + $categories = DocumentTemplate::distinct('category') + ->pluck('category') + ->filter(); +} +``` + +--- + +## 14. 검사항목 확장 (field_values JSON) + +`document_template_section_items.field_values` JSON 컬럼으로 마이그레이션 없이 새 필드를 추가할 수 있다. + +```json +{ + "custom_field_1": "추가 기준값", + "min_value": 95.0, + "max_value": 105.0, + "unit": "mm" +} +``` + +> options JSON 컬럼 정책(`docs/standards/options-column-policy.md`) 준용 + +--- + +## 15. HTMX 전체 페이지 로드 규칙 + +문서관리 페이지들은 JavaScript를 사용하므로 HTMX 부분 로드 시 스크립트 미실행 문제가 있다. 컨트롤러에서 HX-Request 감지 시 **HX-Redirect로 전체 페이지 리로드 강제**. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('documents.index')); +} +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — API 엔드포인트, 모델 요약, FormRequest +- [DB 스키마 — 문서/전자서명](../../system/database/documents.md) — 테이블 상세 +- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 참고 +- [결재관리 시스템](../approvals/README.md) — 별도 결재 시스템 (문서관리와 독립) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/documents/mng-document-template.md b/features/documents/mng-document-template.md new file mode 100644 index 0000000..5570865 --- /dev/null +++ b/features/documents/mng-document-template.md @@ -0,0 +1,826 @@ +# MNG 문서양식관리 (Document Template Management) + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/document-templates` +> **관련**: [README.md](README.md) | [MNG 문서관리](mng-document-system.md) + +--- + +## 1. 개요 + +문서관리 시스템에서 사용하는 **서식(Template)**을 생성, 편집, 복제, 관리하는 기능. 검사 성적서, 작업지시서 등 다양한 문서 양식을 정의하며, 2가지 빌더 타입을 지원한다. + +| 빌더 | builder_type | UI 명칭 | 설명 | +|------|-------------|---------|------| +| **Legacy Builder** | `legacy` 또는 null | 새 양식 | 탭 기반 폼 UI (순수 JavaScript) | +| **Block Builder** | `block` | 양식 디자이너 | WYSIWYG 캔버스 편집기 (Alpine.js + SortableJS) | + +> **명칭 변경 이력**: Block Builder의 UI 표시 명칭이 '블록 빌더' → '양식 디자이너'로 변경됨 (2026-02-28) + +**핵심 기능:** +- 결재선, 기본필드, 검사 기준서, 테이블 컬럼 정의 +- EAV 데이터 구조의 서식 스키마 관리 +- 양식 복제 (연결품목 제외) +- 프리셋 자동 제안 (카테고리별) +- 소프트 삭제 + 휴지통 관리 (슈퍼어드민) + +--- + +## 2. 라우트 + +### 2.1 웹 라우트 (페이지) + +``` +GET /document-templates → index (목록) +GET /document-templates/create → create (Legacy 신규 생성) +GET /document-templates/block-create → blockCreate (양식 디자이너 신규 생성) +GET /document-templates/{id}/edit → edit (Legacy 편집) +GET /document-templates/{id}/block-edit → blockEdit (양식 디자이너 편집) +``` + +### 2.2 API 라우트 (CRUD + 기능) + +``` +Prefix: /api/admin/document-templates (HQ 관리자 전용) + +GET / → index (HTMX 테이블) +POST / → store (생성) +GET /{id} → show (상세 조회) +PUT /{id} → update (수정) +DELETE /{id} → destroy (소프트 삭제) +DELETE /{id}/force → forceDestroy (영구삭제, 슈퍼어드민) +POST /{id}/restore → restore (복원, 슈퍼어드민) +POST /{id}/toggle-active → toggleActive (활성 토글) +POST /{id}/duplicate → duplicate (복제) +POST /upload-image → uploadImage (이미지 업로드) +GET /admin/common-codes/{group} → getCommonCodes (공통코드 조회) +``` + +--- + +## 3. 모델 구조 + +### 3.1 모델 관계도 + +``` +DocumentTemplate (서식 마스터) +├── 1:N DocumentTemplateApprovalLine (결재선) +├── 1:N DocumentTemplateBasicField (기본필드) +├── 1:N DocumentTemplateSection (섹션/기준서) +│ └── 1:N DocumentTemplateSectionItem (섹션 항목) +├── 1:N DocumentTemplateSectionField (섹션 필드) +├── 1:N DocumentTemplateColumn (테이블 컬럼) +└── 1:N DocumentTemplateLink (연결 설정) + └── 1:N DocumentTemplateLinkValue (연결 값) +``` + +### 3.2 DocumentTemplate 핵심 필드 + +```php +// 기본 정보 +builder_type // 'legacy' | 'block' +name // 양식명 +category // 분류명 +title // 문서 제목 + +// 회사 정보 +company_name // 회사명 +company_address // 회사 주소 +company_contact // 회사 연락처 + +// 하단 설정 +footer_remark_label // 비고 라벨 +footer_judgement_label // 판정 라벨 +footer_judgement_options // array - 판정 선택지 + +// Block Builder 전용 +schema // array - 블록 스키마 (JSON) +page_config // array - 페이지 설정 (A4/A3, 여백 등) + +// 연결 (레거시) +linked_item_ids // array - 연결 품목 ID 목록 +linked_process_id // int - 연결 공정 ID + +// 상태 +is_active // boolean - 활성 여부 +deleted_at // timestamp - 소프트 삭제 +deleted_by // int - 삭제자 +``` + +**Helper 메서드:** + +```php +isBlockBuilder(): bool // builder_type === 'block' +isLegacyBuilder(): bool // builder_type !== 'block' +``` + +### 3.3 DocumentTemplateApprovalLine (결재선) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `name` | string | 결재자 이름/직책 | +| `department` | string | 부서 | +| `role` | string | 역할 (작성/검토/승인) | +| `sort_order` | int | 순서 | + +### 3.4 DocumentTemplateBasicField (기본필드) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `field_key` | string | 필드 키 (bf_ 접두사) | +| `label` | string | 라벨 | +| `field_type` | string | text, date, select 등 | +| `default_value` | string | 기본값 | +| `is_required` | boolean | 필수 여부 | +| `sort_order` | int | 순서 | +| `options` | array | 선택지 (select 타입) | + +### 3.5 DocumentTemplateSection (섹션/검사 기준서) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `title` | string | 섹션 제목 | +| `image_path` | string | 섹션 이미지 경로 | +| `sort_order` | int | 순서 | + +**하위 관계:** + +``` +Section 1:N SectionItem + ├── category // 카테고리 (그룹핑) + ├── name // 항목명 + ├── standard // 기준 + ├── tolerance_type // 공차 유형 (symmetric/asymmetric/range/limit) + ├── tolerance_plus // +공차 + ├── tolerance_minus // -공차 + ├── reference_value // 기준값 + ├── method // 검사방법 + ├── measurement_type // 측정유형 + └── frequency // 검사주기 +``` + +### 3.6 DocumentTemplateColumn (테이블 컬럼) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `label` | string | 컬럼 라벨 | +| `group_name` | string | 그룹명 (다단계 "/" 구분) | +| `width` | int | 컬럼 너비 | +| `column_type` | string | text, check, complex, select, measurement | +| `sub_labels` | array | complex 타입 하위 라벨 | +| `sort_order` | int | 순서 | + +### 3.7 DocumentTemplateLink (연결 설정) + +| 필드 | 타입 | 설명 | +|------|------|------| +| `template_id` | FK | 서식 ID | +| `link_key` | string | 연결 키 | +| `label` | string | 라벨 | +| `link_type` | string | `single` / `multiple` | +| `source_table` | string | `items` / `processes` / `users` | +| `search_params` | array | 검색 파라미터 | +| `display_fields` | array | 표시 필드 | +| `is_required` | boolean | 필수 여부 | +| `sort_order` | int | 순서 | + +**하위 관계:** + +``` +Link 1:N LinkValue + ├── link_id // FK → Link + ├── linkable_id // 연결 엔티티 ID + └── (source_table에 따라 items/processes/users 참조) +``` + +**레거시 호환 처리:** + +```php +// 신규 links가 있으면 사용 +if ($template->links->isNotEmpty()) { + // template_links + link_values 사용 +} + +// 레거시만 있으면 가상 엔트리 생성 +if (!empty($template->linked_item_ids)) { + return [['link_key' => 'items', 'values' => [...]]] +} +``` + +--- + +## 4. 컨트롤러 상세 + +### 4.1 DocumentTemplateController (웹) + +| 메서드 | 동작 | +|--------|------| +| `index()` | HTMX 요청 → HX-Redirect 반환 (전체 페이지 로드 강제) | +| `create()` | Legacy 신규 생성 폼 렌더링 | +| `edit($id)` | Legacy 편집. 양식 디자이너 타입이면 `blockEdit`으로 자동 리다이렉트 | +| `blockCreate()` | 양식 디자이너 신규 생성 (빈 캔버스) | +| `blockEdit($id)` | 양식 디자이너 편집 (스키마 로드) | + +**공통 데이터 준비:** + +```php +// 현재 테넌트 조회 +$tenantId = getCurrentTenant(); // 세션의 selected_tenant_id + +// 카테고리 목록 = common_codes + 기존 템플릿 카테고리 +$categories = getCategories(); + +// 기본필드 키 옵션 +$basicFieldKeys = getBasicFieldKeys(); // common_codes 'doc_template_basic_field' +``` + +### 4.2 DocumentTemplateApiController (API) + +#### `index()` — HTMX 테이블 조회 + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 양식명/분류 검색 | +| `category` | string | 분류 필터 | +| `is_active` | string | `1` / `0` / `TRASHED` (휴지통) | + +```php +// 휴지통 모드 (슈퍼어드민 전용) +if ($isActive === 'TRASHED') { + $query->onlyTrashed(); +} +``` + +#### `store()` / `update()` — 생성/수정 + +``` +요청 데이터 + ↓ +검증 (직접 validate, FormRequest 미사용) + ↓ +연결품목 중복 검증 (checkLinkedItemDuplicates) + ↓ +DB::transaction 시작 + ↓ +Template 생성/수정 + ↓ +saveRelations() — 관계 데이터 upsert + ↓ +DB::transaction 완료 + ↓ +JSON 응답 +``` + +#### `duplicate()` — 양식 복제 + +```php +$source = DocumentTemplate::with([...all relationships...]); + +$newTemplate = DocumentTemplate::create([ + ...원본 데이터, + 'name' => request('name', '원본 (복사)'), + 'is_active' => false, // 비활성으로 생성 + 'linked_item_ids' => null, // 연결품목 제외 + 'linked_process_id' => null, // 연결공정 제외 +]); + +// 각 관계 데이터 복사 (approvalLines, basicFields, sections, columns...) +// linkValues는 복사 안 함 (동일 분류 내 중복 방지) +``` + +#### `forceDestroy()` — 영구삭제 + +```php +// 사전 검사: 참조하는 문서 존재 여부 +$documentCount = Document::withTrashed() + ->where('template_id', $id) + ->count(); + +if ($documentCount > 0) { + return 422; // "이 양식을 사용한 문서 {count}건이 있어 삭제 불가" +} +``` + +#### `uploadImage()` — 이미지 업로드 + +``` +요청 (multipart) + ↓ +ApiTokenService::exchangeToken($userId, $tenantId) + ↓ +API /files/upload 호출 (Bearer 토큰) + ↓ +응답: file_path (1/temp/2026/02/xxx.jpg) + ↓ +최종 URL: http://api.sam.kr/storage/tenants/{file_path} +``` + +--- + +## 5. 저장 메커니즘 (saveRelations) + +### 5.1 upsert 전략 + +| 관계 | 방식 | 이유 | +|------|------|------| +| approvalLines | 전체 삭제 → 재생성 | ID 참조 없음 | +| basicFields | 전체 삭제 → 재생성 | ID 참조 없음 | +| **sections** | **ID 보존 upsert** | document_data가 section_id 참조 | +| **sectionItems** | **ID 보존 upsert** | section 하위 항목 | +| **columns** | **ID 보존 upsert** | document_data가 column_id 참조 | +| sectionFields | 전체 삭제 → 재생성 | ID 참조 없음 | +| links + linkValues | 전체 삭제 → 재생성 | ID 참조 없음 | + +### 5.2 ID 보존 upsert 로직 + +```php +// 1. 요청 ID 수집 +$incomingIds = collect($data['sections'])->pluck('id')->filter(); + +// 2. 요청에 없는 항목 삭제 +$template->sections() + ->whereNotIn('id', $incomingIds) + ->each(function($s) { + $s->items()->delete(); + $s->delete(); + }); + +// 3. 각 항목 upsert +foreach ($data['sections'] as $section) { + if (!empty($section['id']) && $existing = $template->sections()->find($section['id'])) { + $existing->update($sectionData); // 기존: update + } else { + DocumentTemplateSection::create([...]); // 신규: create + } +} +``` + +> **ID 보존이 필수인 이유**: `document_data` 테이블이 `section_id`, `column_id`를 FK로 참조한다. 양식 수정 시 ID가 변경되면 기존 문서 데이터와의 매핑이 깨진다. + +--- + +## 6. 화면 구성 + +### 6.1 목록 화면 (`index.blade.php`) + +``` +┌─────────────────────────────────────────────────┐ +│ 문서양식관리 │ +│ [+ 새 양식] [+ 양식 디자이너] │ +├─────────────────────────────────────────────────┤ +│ 필터: [검색어] [분류 ▼] [활성/비활성/휴지통 ▼] │ +├─────────────────────────────────────────────────┤ +│ # │ 양식명 │ 분류 │ 활성 │ 수정일 │ 액션 │ +│ 1 │ FQC... │ 검사 │ ✅ │ 03-06 │ 편집 복제 삭제 │ +│ 2 │ 수입... │ 검사 │ ✅ │ 03-05 │ 편집 복제 삭제 │ +│ ...│ │ │ │ │ │ +└─────────────────────────────────────────────────┘ +``` + +**HTMX 테이블 로드:** + +```html +
+
+``` + +**액션 버튼:** +- **편집**: 새 양식 → `/document-templates/{id}/edit`, 양식 디자이너 → `/document-templates/{id}/block-edit` +- **복제**: `duplicateTemplate(id)` — 이름 입력 모달 후 POST +- **삭제**: `confirmDelete(id)` — 확인 후 DELETE +- **미리보기**: `previewTemplate(id)` — 모달 표시 +- **활성 토글**: `toggleActive(id)` — POST toggle-active +- **복원/영구삭제**: 휴지통 모드에서만 표시 (슈퍼어드민) + +### 6.2 Legacy Builder 편집 화면 (`edit.blade.php`) + +**4개 탭 구조:** + +``` +┌─────────────────────────────────────────────────────┐ +│ [기본정보] [기본필드] [검사 기준서] [테이블 컬럼] │ +├─────────────────────────────────────────────────────┤ +│ │ +│ (각 탭 콘텐츠) │ +│ │ +├─────────────────────────────────────────────────────┤ +│ [미리보기] [저장] [취소] │ +└─────────────────────────────────────────────────────┘ +``` + +#### 탭 1: 기본정보 + +| 필드 | 설명 | +|------|------| +| 양식명 | 서식 이름 (필수) | +| 제목 | 문서 제목 | +| 분류 | 카테고리 (common_codes + 기존값) | +| 회사명 | 문서 헤더 회사명 | +| 회사 주소/연락처 | 문서 헤더 | +| 활성 | 체크박스 | +| 결재선 | 동적 행 추가/삭제 (이름, 부서, 역할) | + +#### 탭 2: 기본필드 + +| 항목 | 설명 | +|------|------| +| 필드 키 | `bf_` 접두사 (common_codes에서 선택) | +| 라벨 | 표시 라벨 | +| 필드 타입 | text, date, select 등 | +| 기본값 | 문서 생성 시 자동 입력 | +| 필수 여부 | 체크박스 | + +#### 탭 3: 검사 기준서 + +``` +┌──────────────────────────────────────────────────┐ +│ 섹션 1: [제목 입력] [이미지 업로드] [+ 항목 추가] │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ 카테고리 │ 항목 │ 기준 │ 공차 │ 기준값 │ ... │ │ +│ │ 외관 │ 색상 │ 기준 │ ±0.5 │ 5.0 │ ... │ │ +│ │ 외관 │ 흠집 │ 무 │ │ │ ... │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ 섹션 2: [제목 입력] [이미지 업로드] [+ 항목 추가] │ +│ ... │ +│ [+ 섹션 추가] │ +└──────────────────────────────────────────────────┘ +``` + +**공차 유형:** + +| 유형 | 입력 | 표시 예 | +|------|------|--------| +| `symmetric` | ± 값 | ±0.5 | +| `asymmetric` | +값, -값 | +0.3 / -0.2 | +| `range` | 최소~최대 | 4.5 ~ 5.5 | +| `limit` | 상한 또는 하한 | ≤ 10 | + +#### 탭 4: 테이블 컬럼 + +| 항목 | 설명 | +|------|------| +| 라벨 | 컬럼 헤더 | +| 그룹명 | 다단계 그룹 ("/" 구분) | +| 너비 | 컬럼 너비 (px 또는 %) | +| 컬럼 타입 | text, check, complex, select, measurement | +| 하위 라벨 | complex 타입 시 sub_labels | + +**자동 컬럼 생성:** + +``` +[기준서에서 자동 생성] 버튼 클릭 + ↓ +검사 기준서 섹션의 항목들을 분석 + ↓ +카테고리 그룹별 컬럼 자동 생성 + ↓ +measurement_type에 따라 컬럼 타입 결정 +``` + +### 6.3 양식 디자이너 편집 화면 (`block-editor.blade.php`) + +**3패널 레이아웃:** + +``` +┌──────────┬──────────────────────────┬───────────┐ +│ 팔레트 │ 캔버스 │ 속성 패널 │ +│ (220px) │ (flex: 1) │ (300px) │ +│ │ │ │ +│ 기본: │ ┌──────────────────────┐ │ 선택 블록: │ +│ □ 제목 │ │ [제목 블록] │ │ │ +│ □ 문단 │ │ [문단 블록] │ │ 제목: ... │ +│ □ 테이블 │ │ [테이블 블록] │ │ 크기: ... │ +│ □ 컬럼 │ │ [입력 필드 블록] │ │ 정렬: ... │ +│ □ 구분선 │ │ │ │ │ +│ □ 여백 │ └──────────────────────┘ │ │ +│ │ │ │ +│ 폼: │ │ │ +│ □ 텍스트 │ │ │ +│ □ 숫자 │ │ │ +│ □ 날짜 │ │ │ +│ □ 선택 │ │ │ +│ □ 체크 │ │ │ +│ □ 텍스트영역│ │ │ +│ □ 서명 │ │ │ +└──────────┴──────────────────────────┴───────────┘ +``` + +**블록 타입 (15개):** + +| 분류 | 타입 | 설명 | +|------|------|------| +| 기본 | `heading` | 제목 (h1~h6) | +| 기본 | `paragraph` | 문단 텍스트 | +| 기본 | `table` | 테이블 (행/열 편집) | +| 기본 | `columns` | 다단 컬럼 레이아웃 | +| 기본 | `divider` | 구분선 | +| 기본 | `spacer` | 여백 | +| 폼 | `text_field` | 텍스트 입력 | +| 폼 | `number_field` | 숫자 입력 | +| 폼 | `date_field` | 날짜 입력 | +| 폼 | `select_field` | 선택 드롭다운 | +| 폼 | `checkbox_field` | 체크박스 | +| 폼 | `textarea_field` | 긴 텍스트 입력 | +| 폼 | `signature_field` | 서명 영역 | + +**Alpine.js 상태 관리:** + +```javascript +blockEditor(initialSchema, templateId) { + blocks: [], // 블록 배열 + selectedBlockId: null, // 현재 선택 블록 + history: [], // Undo/Redo 스택 (최대 50) + historyIndex: -1, + pageConfig: { // 페이지 설정 + size: 'A4', // A4 / A3 + orientation: 'portrait', // portrait / landscape + margins: { top, right, bottom, left } + }, + templateName: '', + category: '' +} +``` + +**키보드 단축키:** + +| 단축키 | 기능 | +|--------|------| +| `Ctrl+Z` / `Cmd+Z` | Undo | +| `Ctrl+Shift+Z` / `Cmd+Shift+Z` | Redo | +| `Ctrl+S` / `Cmd+S` | 저장 | + +**SortableJS:** +- 캔버스 내 블록 드래그-앤-드롭 정렬 +- 팔레트에서 캔버스로 블록 추가 + +--- + +## 7. 미리보기 시스템 + +### 7.1 Legacy Builder 미리보기 + +```javascript +buildDocumentPreviewHtml(data) +├── 결재란 테이블 (역할별 칸) +├── 기본필드 (2열 15:35:15:35 비율) +├── 섹션별 이미지 (title + image 또는 placeholder) +├── 검사 데이터 테이블 +│ ├── 다단계 그룹 헤더 (group_name "/" 구분) +│ ├── sub_labels (complex 컬럼) +│ ├── 항목 행 (카테고리 그룹핑) +│ └── 측정치 셀 (measurement_type별 렌더) +└── 비고/종합판정 섹션 +``` + +### 7.2 양식 디자이너 미리보기 + +```javascript +buildBlockPreviewHtml(data) +├── 블록 타입별 HTML 렌더링 +├── 폼 필드 placeholder 표시 +└── A4/A3 레이아웃 시뮬레이션 +``` + +### 7.3 이미지 URL 처리 + +```javascript +_previewImageUrl(imagePath) +├── http(s):// 시작 → 그대로 사용 +├── /^\d+\// 패턴 → API tenant storage URL 생성 +│ → http://api.sam.kr/storage/tenants/{imagePath} +└── 기타 → MNG local storage (/storage/{imagePath}) +``` + +--- + +## 8. 분류(Category) 관리 + +### 8.1 소스 (우선순위) + +1. **common_codes** (code_group = `document_category`, is_active = true) + - tenant_id가 있는 것 우선 (테넌트 전용) + - tenant_id가 null인 것도 포함 (공통) + - code 기준 중복 제거 (테넌트 우선) +2. **기존 템플릿의 category** (common_codes에 없는 값) + - 기존 이름 그대로 추가 + +### 8.2 연동 공통코드 그룹 + +| 그룹 | 용도 | +|------|------| +| `document_category` | 문서 분류 | +| `doc_template_basic_field` | 기본필드 키 옵션 | +| `doc_inspection_method` | 검사방법 | +| `doc_measurement_type` | 측정유형 | + +--- + +## 9. 프리셋 시스템 + +### 9.1 테이블 + +``` +document_template_field_presets +├── name // 프리셋 이름 +├── category // 대상 카테고리 +├── description // 설명 +└── field_definitions // array - 필드 정의 목록 + [{ field_key, label, field_type, options, ... }] +``` + +### 9.2 동작 + +``` +분류(Category) 변경 + ↓ +매칭 프리셋 검색 + ↓ +기존 section_fields가 비어있으면 + ↓ +"'{category}' 카테고리에 맞는 프리셋을 적용할까요?" 확인 + ↓ +승인 시 field_definitions 자동 적용 +``` + +> **주의**: 초기 로드 시에는 제안하지 않음. 분류 변경 시에만 제안. + +--- + +## 10. 연결품목 중복 검증 + +### 10.1 규칙 + +같은 category 내 서로 다른 템플릿이 동일한 items를 연결할 수 없다. + +### 10.2 검증 로직 + +```php +checkLinkedItemDuplicates($templateId, $category, $itemIds) + +// 1. 같은 category의 다른 템플릿 조회 +$otherTemplates = DocumentTemplate::where('category', $category) + ->where('id', '!=', $templateId) + ->get(); + +// 2. 각 템플릿의 연결품목 수집 +foreach ($otherTemplates as $other) { + // 레거시: linked_item_ids (JSON 배열) + // 신규: template_links → linkValues (source_table = 'items') + $existingItemIds = ...; +} + +// 3. 교집합 검사 +$duplicates = array_intersect($itemIds, $existingItemIds); +if (!empty($duplicates)) { + return 422; // 중복 항목 목록과 함께 오류 반환 +} +``` + +--- + +## 11. JavaScript 상태 관리 (Legacy Builder) + +### 11.1 templateState 객체 + +```javascript +const templateState = { + // 기본정보 + id, name, category, title, + company_name, company_address, company_contact, + footer_remark_label, footer_judgement_label, + footer_judgement_options, + is_active, + + // 관계 데이터 + approval_lines: [], // 결재선 + basic_fields: [], // 기본필드 + sections: [], // 섹션 + items + columns: [], // 테이블 컬럼 + section_fields: [], // 섹션 필드 + template_links: [], // 연결 설정 + values +}; +``` + +### 11.2 저장 흐름 + +``` +사용자 입력 (Blade 폼) + ↓ +templateState 객체 갱신 + ↓ +saveTemplate() 호출 + ↓ +fetch POST/PUT /api/admin/document-templates + ↓ +DocumentTemplateApiController::store/update() + ↓ +검증 → 중복 검사 → DB 트랜잭션 → saveRelations() + ↓ +JSON 응답 + ↓ +showToast() 메시지 + ↓ +htmx.trigger('#template-table', 'filterSubmit') → 테이블 새로고침 +``` + +--- + +## 12. 양식 디자이너(Block Builder) vs 새 양식(Legacy Builder) 비교 + +| 항목 | 양식 디자이너 | 새 양식 | +|------|:------------:|:-------------:| +| builder_type | `block` | `legacy` 또는 null | +| 편집 UI | WYSIWYG 캔버스 (Alpine.js) | 탭 폼 (순수 JavaScript) | +| 데이터 저장 | `schema` JSON 컬럼 | 관계 테이블 (7개) | +| Undo/Redo | 히스토리 스택 (최대 50) | 불가 | +| 블록 타입 | 15개 (기본 6 + 폼 7 + 기타 2) | N/A | +| 드래그-앤-드롭 | SortableJS | 불가 | +| 페이지 설정 | A4/A3, 여백, 방향 | 없음 | +| 복제 | 스키마 JSON 복사 | 각 관계 데이터 개별 복사 | +| 미리보기 함수 | `buildBlockPreviewHtml()` | `buildDocumentPreviewHtml()` | +| 적합 용도 | 자유 레이아웃 문서 | 정형화된 검사 성적서 | + +--- + +## 13. 권한 및 보안 + +### 13.1 미들웨어 + +- **웹 라우트**: 일반 인증 (auth) +- **API 라우트**: HQ 관리자 미들웨어 (`admin` prefix) + +### 13.2 슈퍼어드민 전용 기능 + +| 기능 | 엔드포인트 | +|------|-----------| +| 영구삭제 | `DELETE /{id}/force` | +| 복원 | `POST /{id}/restore` | +| 휴지통 조회 | `GET /?is_active=TRASHED` | + +### 13.3 삭제 보호 + +- 소프트 삭제: `deleted_at` + `deleted_by` 기록 +- 영구삭제 전 참조 문서 검사 (Document 테이블) +- 참조 문서가 있으면 영구삭제 불가 (422 응답) + +--- + +## 14. API 프로젝트 연동 + +### 14.1 API 서비스 + +```php +// DocumentTemplateService (API) +list(array $params): LengthAwarePaginator + // 필터: is_active, category, search + +show(int $id): DocumentTemplate + // 전체 관계 로드 (approvalLines, basicFields, sections, columns...) +``` + +### 14.2 API 엔드포인트 + +``` +GET /v1/document-templates → index (목록) +GET /v1/document-templates/{id} → show (상세) +``` + +> API는 **읽기 전용**. 서식 생성/수정은 MNG에서만 수행. + +--- + +## 15. 주요 파일 경로 + +| 기능 | 경로 | +|------|------| +| 웹 컨트롤러 | `mng/app/Http/Controllers/DocumentTemplateController.php` | +| API 컨트롤러 | `mng/app/Http/Controllers/Api/Admin/DocumentTemplateApiController.php` | +| 모델 (8개) | `mng/app/Models/DocumentTemplate*.php` | +| 뷰 - 목록 | `mng/resources/views/document-templates/index.blade.php` | +| 뷰 - Legacy 편집 | `mng/resources/views/document-templates/edit.blade.php` | +| 뷰 - 양식 디자이너 | `mng/resources/views/document-templates/block-editor.blade.php` | +| 뷰 - 테이블 | `mng/resources/views/document-templates/partials/table.blade.php` | +| 뷰 - 미리보기 | `mng/resources/views/document-templates/partials/preview-modal.blade.php` | +| API 서비스 | `api/app/Services/DocumentTemplateService.php` | +| API 모델 | `api/app/Models/Documents/DocumentTemplate*.php` | + +--- + +## 관련 문서 + +- [README.md](README.md) — 문서관리 시스템 개요 (API 중심) +- [MNG 문서관리](mng-document-system.md) — 문서 생성/편집/결재 (서식을 사용하는 측) +- [DB 스키마 — 문서](../../system/database/documents.md) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/README.md b/features/planning/README.md new file mode 100644 index 0000000..ef681d0 --- /dev/null +++ b/features/planning/README.md @@ -0,0 +1,129 @@ +# 주일기업 기획 메뉴 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **라우트 접두사**: `/juil` + +--- + +## 1. 개요 + +### 1.1 목적 + +블라인드/스크린 제조업체의 현장 관리를 위한 기획 도구 모음. 견적부터 공사, 준공까지의 업무 흐름과 현장 기록(사진대지), 회의 기록(STT/AI 요약)을 제공한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 아키텍처 | +| [construction-photos.md](construction-photos.md) | 공사현장 사진대지 기술 명세 | +| [meeting-minutes.md](meeting-minutes.md) | 회의록 작성 기술 명세 (STT/AI 통합) | +| [planning-views.md](planning-views.md) | 견적/프로젝트/워크플로우 화면 명세 | + +### 1.3 하위 메뉴 구조 + +``` +주일기업 기획 +├── 견적/입찰/공사관리 /juil/estimate +├── 프로젝트관리/기성청구 /juil/project +├── 업무 Workflow /juil/workflow +├── 공사현장 사진대지 /juil/construction-photos +└── 회의록 작성 /juil/meeting-minutes +``` + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + React (인라인) + Babel | 브라우저 트랜스파일 React 컴포넌트 | +| API | Laravel Controller + Service | JSON API (AJAX) | +| 모델 | Eloquent ORM | Multi-tenant (BelongsToTenant) | +| 파일 저장 | Google Cloud Storage | 사진, 오디오 파일 | +| AI | Gemini API (Vertex AI) | 요약, 화자 분리 | +| STT | Google Speech-to-Text V1/V2 + Web Speech API | 음성 인식 | + +### 2.2 프로젝트 파일 구조 + +``` +mng/ +├── app/Http/Controllers/ +│ ├── PlanningController.php ← 견적/프로젝트/워크플로우 +│ ├── ConstructionSitePhotoController.php ← 사진대지 CRUD + 파일 관리 +│ └── MeetingMinuteController.php ← 회의록 CRUD + AI 기능 +├── app/Services/ +│ ├── ConstructionSitePhotoService.php ← 사진대지 비즈니스 로직 +│ └── MeetingMinuteService.php ← 회의록 + AI 통합 로직 +├── app/Models/ +│ ├── ConstructionSitePhoto.php ← 사진대지 모델 +│ ├── ConstructionSitePhotoRow.php ← 사진 행 모델 +│ ├── MeetingMinute.php ← 회의록 모델 +│ └── MeetingMinuteSegment.php ← 회의 세그먼트 모델 +└── resources/views/juil/ + ├── estimate.blade.php ← 견적/입찰/공사관리 + ├── project.blade.php ← 프로젝트관리/기성청구 + ├── workflow.blade.php ← 업무 Workflow + ├── construction-photos.blade.php ← 사진대지 SPA + └── meeting-minutes.blade.php ← 회의록 SPA +``` + +### 2.3 기능별 구현 현황 + +| 기능 | 구현 방식 | 백엔드 | DB | +|------|----------|--------|-----| +| 견적/입찰/공사관리 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | +| 프로젝트관리/기성청구 | React 뷰 (목데이터) | PlanningController (뷰 반환만) | 없음 | +| 업무 Workflow | React 뷰 (정적 데이터) | PlanningController (뷰 반환만) | 없음 | +| 공사현장 사진대지 | React SPA + API | Controller + Service | 2 테이블 | +| 회의록 작성 | React SPA + API | Controller + Service + AI | 2 테이블 | + +--- + +## 3. 외부 서비스 의존성 + +| 서비스 | 용도 | 추적 | +|--------|------|------| +| **Google Cloud Storage** | 사진/오디오 파일 저장 | `AiTokenHelper::saveGcsStorageUsage()` | +| **Google Speech-to-Text V2 (Chirp2)** | 자동 화자 분리 (최우선) | `AiTokenHelper::saveSttUsage()` | +| **Google Speech-to-Text V1** | 화자 분리 (V2 실패 시 폴백) | `AiTokenHelper::saveSttUsage()` | +| **Gemini API (Vertex AI)** | 요약 생성 + 화자 재분배 | `AiTokenHelper::saveGeminiUsage()` | +| **Web Speech API** | 브라우저 음성 입력 (현장명/설명) | `logSttUsage()` | + +### 3.1 도메인 용어 힌트 (STT 정확도 향상) + +``` +블라인드, 스크린, 롤스크린, 허니콤, 버티컬, +원단, 바텀레일, 헤드레일, 브라켓, +주일, 경동, 주일블라인드, 경동블라인드, +수주, 발주, 납기, 출하, 재고, 원가, 단가, +SAM, ERP, MES +``` + +--- + +## 4. HTMX 전체 페이지 로드 규칙 + +모든 `/juil/*` 페이지는 React 인라인 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 컨트롤러 메서드에서 HTMX 요청 감지 시 **HX-Redirect로 전체 페이지 리로드를 강제**한다. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.estimate')); +} +``` + +--- + +## 5. 관련 문서 + +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 행 구조, 음성 입력 +- [회의록 작성](meeting-minutes.md) — STT/화자분리/AI 요약, 오디오 녹음 +- [견적/프로젝트/워크플로우](planning-views.md) — React 뷰 구성, 업무 프로세스 정의 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/construction-photos.md b/features/planning/construction-photos.md new file mode 100644 index 0000000..0a85ffb --- /dev/null +++ b/features/planning/construction-photos.md @@ -0,0 +1,275 @@ +# 공사현장 사진대지 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/juil/construction-photos` +> **관련**: [README.md](README.md) | [회의록](meeting-minutes.md) | [뷰 화면](planning-views.md) + +--- + +## 1. 개요 + +건설/시공 현장의 작업 과정을 **작업전/작업중/작업후** 3단계 사진으로 기록하고 관리하는 기능. Google Cloud Storage에 사진을 저장하며, 음성 입력(Web Speech API)으로 현장명과 설명을 입력할 수 있다. + +--- + +## 2. 라우트 + +``` +/juil/construction-photos +├── GET / → index (목록 페이지) +├── GET /list → list (JSON 목록) +├── POST / → store (새 사진대지 등록) +├── POST /log-stt-usage → logSttUsage (STT 시간 기록) +├── GET /{id} → show (상세 조회) +├── PUT /{id} → update (메타데이터 수정) +├── DELETE /{id} → destroy (삭제) +├── POST /{id}/rows → addRow (행 추가) +├── DELETE /{id}/rows/{rowId} → deleteRow (행 삭제) +├── POST /{id}/rows/{rowId}/upload → uploadPhoto (사진 업로드) +├── DELETE /{id}/rows/{rowId}/photo/{type} → deletePhoto (사진 삭제) +└── GET /{id}/rows/{rowId}/download/{type} → downloadPhoto (다운로드) +``` + +--- + +## 3. 데이터베이스 + +### 3.1 construction_site_photos (사진대지) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `user_id` | BIGINT FK | 등록자 | +| `site_name` | VARCHAR(200) | 현장명 (필수) | +| `work_date` | DATE | 작업일자 (필수) | +| `description` | TEXT NULL | 설명 | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `tenant_id`, `user_id`, `(tenant_id, work_date)` + +### 3.2 construction_site_photo_rows (사진 행) + +각 사진대지는 1개 이상의 행을 가지며, 각 행에 3개 타입(before/during/after) 사진 저장. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `construction_site_photo_id` | BIGINT FK | 부모 (cascade delete) | +| `sort_order` | INT | 정렬 순서 (0부터) | +| `before_photo_path` | VARCHAR(500) NULL | 작업전 GCS 경로 | +| `before_photo_gcs_uri` | VARCHAR(500) NULL | 작업전 GCS URI | +| `before_photo_size` | INT UNSIGNED NULL | 작업전 파일크기 (bytes) | +| `during_photo_path` | VARCHAR(500) NULL | 작업중 GCS 경로 | +| `during_photo_gcs_uri` | VARCHAR(500) NULL | 작업중 GCS URI | +| `during_photo_size` | INT UNSIGNED NULL | 작업중 파일크기 (bytes) | +| `after_photo_path` | VARCHAR(500) NULL | 작업후 GCS 경로 | +| `after_photo_gcs_uri` | VARCHAR(500) NULL | 작업후 GCS URI | +| `after_photo_size` | INT UNSIGNED NULL | 작업후 파일크기 (bytes) | + +### 3.3 테이블 관계 + +``` +construction_site_photos + │ 1:N + ▼ +construction_site_photo_rows (sort_order ASC) + ├── before_photo_* (작업전) + ├── during_photo_* (작업중) + └── after_photo_* (작업후) +``` + +--- + +## 4. API 명세 + +### 4.1 목록 조회 + +``` +GET /juil/construction-photos/list +``` + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 현장명 검색 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `per_page` | int | 페이지당 건수 | + +### 4.2 생성 + +``` +POST /juil/construction-photos +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `site_name` | required, max:200 | 현장명 | +| `work_date` | required, date | 작업일자 | +| `description` | nullable, max:2000 | 설명 | + +> 생성 시 빈 행 1개 자동 추가 + +### 4.3 사진 업로드 + +``` +POST /juil/construction-photos/{id}/rows/{rowId}/upload +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `type` | required, in:before,during,after | 사진 타입 | +| `photo` | required, image, mimes:jpeg,jpg,png,webp, max:10240 | 최대 10MB | + +### 4.4 사진 다운로드 + +``` +GET /juil/construction-photos/{id}/rows/{rowId}/download/{type}?inline=1 +``` + +| 파라미터 | 설명 | +|---------|------| +| `inline=1` | 브라우저 표시 (미지정 시 다운로드) | + +--- + +## 5. GCS 저장 구조 + +### 5.1 경로 패턴 + +``` +construction-site-photos/{tenant_id}/{photo_id}/{row_id}_{timestamp}_{type}.{ext} +``` + +**예시:** + +``` +construction-site-photos/1/42/15_1709723456_before.jpg +construction-site-photos/1/42/15_1709723456_during.jpg +construction-site-photos/1/42/15_1709723456_after.png +``` + +### 5.2 업로드 흐름 + +``` +클라이언트 (Canvas 이미지 압축: 1920px, quality 80%) + ↓ +FormData (multipart) 전송 + ↓ +컨트롤러: uploadPhoto() + ↓ +서비스: uploadPhoto() + ├── 기존 사진 있으면 GCS에서 삭제 + ├── GCS에 업로드 + ├── DB에 path + uri + size 저장 + └── AiTokenHelper::saveGcsStorageUsage() 호출 + ↓ +응답: { success, data: Photo with rows } +``` + +### 5.3 삭제 흐름 + +``` +사진 삭제: GCS 파일 삭제 → DB 필드 null +행 삭제: 행 내 모든 사진 GCS 삭제 → 행 삭제 → sort_order 재정렬 +사진대지 삭제: 모든 행의 모든 사진 GCS 삭제 → soft delete +``` + +--- + +## 6. 음성 입력 (Web Speech API) + +### 6.1 VoiceInputButton 컴포넌트 + +현장명, 설명 필드에 음성으로 텍스트 입력 가능. + +```javascript +// Web Speech Recognition 설정 +recognition.lang = 'ko-KR'; +recognition.continuous = true; +recognition.interimResults = true; +recognition.maxAlternatives = 1; +``` + +### 6.2 인식 상태 + +| 상태 | 표시 | 설명 | +|------|------|------| +| interim (미확정) | 이탤릭 + 회색 | 인식 중간 결과, 2초 후 소실 | +| final (확정) | 일반체 + 진한색 | 확정 텍스트, 영구 저장 | + +### 6.3 사용량 추적 + +``` +STT 사용 종료 시: +duration = Math.max(1, (Date.now() - startTime) / 1000) + ↓ +POST /juil/construction-photos/log-stt-usage + body: { duration_seconds } + ↓ +AiTokenHelper::saveSttUsage('공사현장사진대지-음성입력', seconds) +``` + +--- + +## 7. UI 구성 (React) + +### 7.1 사진 타입별 색상 + +| 타입 | 라벨 | 배경색 | 뱃지색 | +|------|------|--------|--------| +| `before` | 작업전 | `bg-blue-50` | `bg-blue-100 text-blue-800` | +| `during` | 작업중 | `bg-yellow-50` | `bg-yellow-100 text-yellow-800` | +| `after` | 작업후 | `bg-green-50` | `bg-green-100 text-green-800` | + +### 7.2 행 관리 + +- **행 추가**: sort_order 자동 계산 (마지막 + 1) +- **행 삭제**: 최소 1개 행 유지 필수 +- **행별 사진**: 각 행에 3개 타입 사진 독립 업로드/삭제 + +--- + +## 8. 모델 메서드 + +### 8.1 ConstructionSitePhoto + +```php +user() # BelongsTo User (등록자) +rows() # HasMany Row (sort_order ASC) +getPhotoCount(): int # 전체 사진 개수 (모든 행의 사진 합계) +``` + +### 8.2 ConstructionSitePhotoRow + +```php +constructionSitePhoto() # BelongsTo 부모 +hasPhoto(string $type): bool # 특정 타입 사진 존재 여부 +getPhotoCount(): int # 이 행의 사진 개수 (0~3) +``` + +### 8.3 ConstructionSitePhotoService + +```php +getList(array $filters) # 검색/필터 목록 (페이지네이션) +create(array $data) # 생성 + 빈 행 1개 자동 추가 +update(ConstructionSitePhoto, array $data) # 메타데이터만 수정 +delete(ConstructionSitePhoto) # GCS 전체 삭제 → soft delete +uploadPhoto(Row, UploadedFile, string $type) # GCS 업로드 + DB 기록 +deletePhotoByType(Row, string $type) # 특정 타입 GCS 삭제 +addRow(ConstructionSitePhoto) # 행 추가 (sort_order 자동) +deleteRow(Row) # 행 내 GCS 삭제 → 행 삭제 → 재정렬 +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 +- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/meeting-minutes.md b/features/planning/meeting-minutes.md new file mode 100644 index 0000000..09d089a --- /dev/null +++ b/features/planning/meeting-minutes.md @@ -0,0 +1,456 @@ +# 회의록 작성 + +> **작성일**: 2026-03-06 +> **상태**: 운영 중 +> **라우트**: `/juil/meeting-minutes` +> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [뷰 화면](planning-views.md) + +--- + +## 1. 개요 + +음성으로 회의 내용을 기록하고, **Google STT(화자 분리)** + **Gemini AI(요약/결정사항/액션아이템)** 로 자동 정리하는 회의록 시스템. 브라우저 MediaRecorder로 녹음하고, GCS에 오디오를 저장하며, 세그먼트(화자별 발화)를 관리한다. + +--- + +## 2. 라우트 + +``` +/juil/meeting-minutes +├── GET / → index (목록 페이지) +├── GET /list → list (JSON 목록) +├── POST / → store (새 회의록 생성) +├── POST /log-stt-usage → logSttUsage (STT 시간 기록) +├── GET /{id} → show (상세 조회 + segments) +├── PUT /{id} → update (메타데이터 수정) +├── DELETE /{id} → destroy (삭제) +├── POST /{id}/segments → saveSegments (세그먼트 저장) +├── POST /{id}/upload-audio → uploadAudio (오디오 업로드) +├── POST /{id}/summarize → summarize (AI 요약 생성) +├── POST /{id}/diarize → diarize (자동 화자 분리) +└── GET /{id}/download-audio → downloadAudio (오디오 다운로드) +``` + +--- + +## 3. 데이터베이스 + +### 3.1 meeting_minutes (회의록) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `tenant_id` | BIGINT FK | 테넌트 격리 | +| `user_id` | BIGINT FK | 작성자 | +| `title` | VARCHAR(300) | 제목 (기본: "무제 회의록") | +| `folder` | VARCHAR(100) NULL | 폴더 분류 | +| `participants` | JSON NULL | 참여자 목록 배열 | +| `meeting_date` | DATE | 회의 날짜 | +| `meeting_time` | TIME NULL | 회의 시작 시간 | +| `duration_seconds` | INT UNSIGNED | 녹음 총 시간(초) | +| `audio_file_path` | VARCHAR(500) NULL | 오디오 GCS 경로 | +| `audio_gcs_uri` | VARCHAR(500) NULL | 오디오 GCS URI | +| `audio_file_size` | BIGINT UNSIGNED NULL | 오디오 파일 크기 (bytes) | +| `full_transcript` | LONGTEXT NULL | 전체 트랜스크립트 | +| `summary` | LONGTEXT NULL | AI 요약 | +| `decisions` | JSON NULL | 결정사항 배열 | +| `action_items` | JSON NULL | 액션아이템 배열 | +| `status` | VARCHAR(20) | 상태 (5가지) | +| `stt_language` | VARCHAR(10) | STT 언어 (기본: ko-KR) | +| `deleted_at` | TIMESTAMP NULL | 소프트 삭제 | + +**인덱스**: `tenant_id`, `user_id`, `(tenant_id, meeting_date)`, `status` + +### 3.2 meeting_minute_segments (세그먼트) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | BIGINT PK | | +| `meeting_minute_id` | BIGINT FK | 회의록 (cascade delete) | +| `segment_order` | INT UNSIGNED | 순서 | +| `speaker_name` | VARCHAR(100) | 화자 이름 (기본: "화자 1") | +| `speaker_label` | VARCHAR(20) NULL | 화자 라벨/번호 | +| `text` | TEXT | 발화 텍스트 | +| `start_time_ms` | INT UNSIGNED | 시작 시간 (ms, 기본: 0) | +| `end_time_ms` | INT UNSIGNED NULL | 종료 시간 (ms) | +| `is_manual_speaker` | BOOLEAN | 수동 화자 전환 여부 (기본: true) | + +**인덱스**: `meeting_minute_id`, `(meeting_minute_id, segment_order)` + +### 3.3 테이블 관계 + +``` +meeting_minutes + │ 1:N + ▼ +meeting_minute_segments (segment_order ASC) + ├── speaker_name (화자명) + ├── text (발화 내용) + └── start_time_ms / end_time_ms (타임스탬프) +``` + +--- + +## 4. 상태 관리 + +### 4.1 상태값 + +| 상태 | 코드 | 색상 | 설명 | +|------|------|------|------| +| 초안 | `DRAFT` | 회색 | 생성 직후, 편집 가능 | +| 녹음중 | `RECORDING` | 빨강 | (클라이언트 상태) | +| 처리중 | `PROCESSING` | 노랑 | AI 요약/화자분리 처리 중 | +| 완료 | `COMPLETED` | 초록 | AI 처리 완료 | +| 실패 | `FAILED` | 빨강 | AI 처리 실패 | + +### 4.2 상태 전이 + +``` +DRAFT + ↓ [오디오 업로드, 세그먼트 추가] +DRAFT (계속 편집) + ↓ [summarize() 호출] +PROCESSING + ↓ +COMPLETED (성공) 또는 FAILED (실패) + +DRAFT + ↓ [diarize() 호출 → 화자 분리] +DRAFT (세그먼트 갱신, 상태 유지) +``` + +--- + +## 5. API 명세 + +### 5.1 목록 조회 + +``` +GET /juil/meeting-minutes/list +``` + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| `search` | string | 제목 검색 | +| `date_from` | date | 시작일 | +| `date_to` | date | 종료일 | +| `status` | string | 상태 필터 | +| `per_page` | int | 페이지당 건수 | + +### 5.2 생성 + +``` +POST /juil/meeting-minutes +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `title` | nullable, max:300 | 제목 (미입력 시 "무제 회의록") | +| `folder` | nullable, max:100 | 폴더 분류 | +| `participants` | nullable, array | 참여자 목록 | +| `meeting_date` | required, date | 회의 날짜 | +| `meeting_time` | nullable | 회의 시간 | +| `stt_language` | nullable, max:10 | STT 언어 (기본: ko-KR) | + +### 5.3 세그먼트 저장 + +``` +POST /juil/meeting-minutes/{id}/segments +``` + +```json +{ + "segments": [ + { + "speaker_name": "김과장", + "speaker_label": "1", + "text": "블라인드 납기일 확인 필요합니다.", + "start_time_ms": 0, + "end_time_ms": 5000, + "is_manual_speaker": true + } + ] +} +``` + +> **전처리**: 빈 텍스트 필터링, 언더스코어 노이즈 제거, 다중 공백 정규화 +> **자동 생성**: `full_transcript` = `[화자명] 발화텍스트\n...` 형식 + +### 5.4 오디오 업로드 + +``` +POST /juil/meeting-minutes/{id}/upload-audio +``` + +| 필드 | 규칙 | 설명 | +|------|------|------| +| `audio` | required, file | webm/mp3 등 | +| `duration_seconds` | required, integer, min:1 | 녹음 시간(초) | + +### 5.5 AI 요약 생성 + +``` +POST /juil/meeting-minutes/{id}/summarize +``` + +**요청**: 없음 (서버에서 `full_transcript` 사용) + +**응답 예시:** + +```json +{ + "success": true, + "message": "AI 요약이 완료되었습니다.", + "data": { + "summary": "블라인드 납품 일정과 현장 설치 계획을 논의했습니다...", + "decisions": [ + "납품일을 3월 15일로 확정", + "현장 실측은 3월 10일 진행" + ], + "action_items": [ + { + "assignee": "김과장", + "task": "거래처에 납기 확인 연락", + "deadline": "2026-03-08" + } + ], + "status": "COMPLETED" + } +} +``` + +### 5.6 자동 화자 분리 + +``` +POST /juil/meeting-minutes/{id}/diarize +``` + +| 필드 | 설명 | 기본값 | +|------|------|--------| +| `min_speakers` | 최소 화자 수 | 2 | +| `max_speakers` | 최대 화자 수 | 6 | + +**응답:** + +```json +{ + "success": true, + "message": "자동 화자 분리가 완료되었습니다. (3명 감지)", + "data": { /* Meeting with segments */ }, + "speaker_count": 3 +} +``` + +--- + +## 6. AI 통합 상세 + +### 6.1 화자 분리 (Diarization) 3단계 폴백 + +``` +[1단계] Google STT V2 (Chirp2) ← 최우선 + │ speechToTextWithDiarizationAuto() + │ 최신 모델, 높은 정확도 + │ 도메인 용어 힌트 포함 + │ + ↓ (실패 시) +[2단계] Google STT V1 (latest_long) ← 폴백 + │ 안정적이지만 약간 덜 정확 + │ + ↓ (1명만 인식 시) +[3단계] Gemini AI 화자 재분배 + splitSpeakersWithGemini() + 대화 맥락/호칭/질답 패턴/어투 변화 분석 + 2명 이상으로 재분배 +``` + +### 6.2 요약 생성 (Gemini API) + +``` +입력: full_transcript (전체 트랜스크립트) + ↓ +Gemini API 호출 + ├── 모드 1: Vertex AI (projectId, region, JWT) + └── 모드 2: Google AI Studio (API key) ← 폴백 + │ + │ Temperature: 0.3 (결정적) + │ Max tokens: 4096 + ↓ +출력 JSON: +{ + "summary": "3-5문장 요약", + "decisions": ["결정사항 1", "..."], + "action_items": [ + { "assignee": "담당자", "task": "할일", "deadline": "기한" } + ], + "keywords": ["키워드1", "..."] +} +``` + +### 6.3 Gemini 화자 재분배 + +Google STT가 1명만 인식할 때 Gemini로 대화 맥락 분석: + +``` +입력: 단일 화자 트랜스크립트 + 예상 화자 수 + ↓ +Gemini 프롬프트: + - 대화 맥락 분석 (호칭, 질답, 어투 변화) + - 지정된 수의 화자로 분리 + ↓ +출력: 화자별 세그먼트 배열 + → DB 세그먼트 교체 +``` + +--- + +## 7. 오디오 관리 (GCS) + +### 7.1 GCS 경로 패턴 + +``` +meeting-minutes/{tenant_id}/{meeting_id}/{timestamp}.webm +``` + +### 7.2 녹음 흐름 + +``` +브라우저 MediaRecorder API + ├── navigator.mediaDevices.getUserMedia({ audio: true }) + ├── new MediaRecorder(stream) + ├── recorder.ondataavailable → webm 블롭 수집 + └── 녹음 종료 → FormData로 업로드 + ↓ +POST /{id}/upload-audio + ├── GCS 업로드 + ├── DB: audio_file_path, audio_gcs_uri, audio_file_size, duration_seconds + └── AiTokenHelper::saveGcsStorageUsage() +``` + +### 7.3 다운로드 + +``` +GET /{id}/download-audio + → GCS에서 파일 콘텐츠 다운로드 + → Content-Disposition: attachment; filename="{title}.webm" +``` + +--- + +## 8. 세그먼트 처리 로직 + +### 8.1 저장 시 전처리 + +```php +// 1. 빈 텍스트 필터링 +trim($segment['text']) !== '' + +// 2. 언더스코어 노이즈 제거 +str_replace('_', '', $text) + +// 3. 다중 공백 정규화 +preg_replace('/\s{2,}/', ' ', $text) +``` + +### 8.2 전체 트랜스크립트 자동 생성 + +``` +[김과장] 블라인드 납기일 확인 필요합니다. +[박부장] 3월 15일로 확정합시다. +[김과장] 네, 거래처에 연락하겠습니다. +``` + +### 8.3 화자 분리 결과 세그먼트 변환 + +``` +Google STT 결과 → MeetingMinuteSegment 변환: +{ + segment_order: 순서, + speaker_name: "화자 N", + speaker_label: "N", + text: 발화 텍스트, + start_time_ms: 시작시간, + end_time_ms: 종료시간, + is_manual_speaker: false // 자동 분리 +} +``` + +--- + +## 9. UI 구성 (React) + +### 9.1 화자 색상 + +| 화자 | 배경색 | 뱃지색 | +|------|--------|--------| +| 화자 1 | `bg-blue-50` | `bg-blue-100 text-blue-800` | +| 화자 2 | `bg-green-50` | `bg-green-100 text-green-800` | +| 화자 3 | `bg-purple-50` | `bg-purple-100 text-purple-800` | +| 화자 4 | `bg-orange-50` | `bg-orange-100 text-orange-800` | + +### 9.2 지원 언어 + +| 코드 | 라벨 | +|------|------| +| `ko-KR` | 한국어 | +| `en-US` | English | +| `ja-JP` | 日本語 | +| `zh-CN` | 中文 | + +--- + +## 10. 사용량 추적 + +| 추적 항목 | 레이블 | Helper | +|----------|--------|--------| +| Web Speech API 사용 | `회의록-음성인식` | `AiTokenHelper::saveSttUsage()` | +| Google STT V1 화자 분리 | `회의록-화자분리` | `AiTokenHelper::saveSttUsage()` | +| Google STT V2 화자 분리 | `회의록-화자분리(Chirp2)` | `AiTokenHelper::saveSttUsage()` | +| GCS 오디오 저장 | `회의록-GCS저장` | `AiTokenHelper::saveGcsStorageUsage()` | +| Gemini 요약/분리 | `회의록-AI요약` | `AiTokenHelper::saveGeminiUsage()` | + +--- + +## 11. 모델 메서드 + +### 11.1 MeetingMinute + +```php +user() # BelongsTo User +segments() # HasMany Segment (segment_order ASC) +getFormattedDurationAttribute() # "H:MM:SS" 또는 "MM:SS" +``` + +**Cast**: `participants`, `decisions`, `action_items` → array, `meeting_date` → date + +### 11.2 MeetingMinuteService + +```php +# CRUD +getList(array $filters) # 검색/필터 목록 +create(array $data) # 생성 (DRAFT) +update(MeetingMinute, array $data) # 수정 +delete(MeetingMinute) # GCS 삭제 → soft delete + +# 세그먼트 +saveSegments(MeetingMinute, array $segments) # 전처리 + 저장 + 트랜스크립트 생성 +uploadAudio(MeetingMinute, UploadedFile, int $seconds) # GCS 업로드 +logSttUsage(int $seconds) # STT 사용량 기록 + +# AI +generateSummary(MeetingMinute) # Gemini 요약 생성 +processDiarization(MeetingMinute, int $min, int $max) # 3단계 화자 분리 +splitSpeakersWithGemini(string $text, int $expected) # Gemini 화자 재분배 +``` + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 +- [견적/프로젝트/워크플로우](planning-views.md) — 화면 명세 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/planning/planning-views.md b/features/planning/planning-views.md new file mode 100644 index 0000000..4b087ac --- /dev/null +++ b/features/planning/planning-views.md @@ -0,0 +1,222 @@ +# 견적/프로젝트/워크플로우 화면 명세 + +> **작성일**: 2026-03-06 +> **상태**: 뷰 구현 완료 (목데이터 기반, API 미연동) +> **라우트**: `/juil/estimate`, `/juil/project`, `/juil/workflow` +> **관련**: [README.md](README.md) | [사진대지](construction-photos.md) | [회의록](meeting-minutes.md) + +--- + +## 1. 개요 + +3개 화면 모두 **React 인라인 컴포넌트**(Babel 브라우저 트랜스파일)로 구현. 현재는 정적/목데이터 기반이며, 향후 API 연동 예정. PlanningController에서 뷰만 반환한다. + +--- + +## 2. 견적/입찰/공사관리 (/juil/estimate) + +### 2.1 개요 + +블라인드/스크린 설치 프로젝트의 견적서 작성, 입찰 관리, 공사 진행 현황을 한 화면에서 관리. + +### 2.2 데이터 구조 (initialEstimates) + +```javascript +{ + id: "string", + name: "프로젝트명", + client: "고객사명", + status: "견적중|입찰|계약|공사중|준공", + amount: number, // 금액 + startDate: "YYYY-MM-DD", + endDate: "YYYY-MM-DD", + manager: "담당자명", + items: [ // 품목 내역 + { name: "품목명", quantity: number, unitPrice: number } + ] +} +``` + +### 2.3 공사관리 정보 (initialConstructionData) + +```javascript +{ + id: "string", + estimateId: "string", // 연결 견적 + siteName: "현장명", + address: "현장 주소", + progress: number, // 진행률 (0~100) + workers: number, // 투입 인원 + safetyChecks: [ // 안전점검 + { date: "YYYY-MM-DD", result: "합격|불합격", inspector: "점검자" } + ] +} +``` + +### 2.4 상태별 배지 색상 + +| 상태 | 색상 | +|------|------| +| 견적중 | 파랑 | +| 입찰 | 보라 | +| 계약 | 초록 | +| 공사중 | 주황 | +| 준공 | 회색 | + +### 2.5 SAM 연계 + +- 견적서 작성 시 SAM 견적 시스템(`features/quotes/`) 데이터 활용 가능 +- 향후 `/juil/estimate` ↔ SAM 견적 API 연동 계획 + +--- + +## 3. 프로젝트관리/기성청구 (/juil/project) + +### 3.1 개요 + +계약된 프로젝트의 현장 관리, 발주/청구/인건비 상태 추적, 기성 청구 관리. + +### 3.2 데이터 구조 (initialProjects) + +```javascript +{ + id: "string", + name: "프로젝트명", + client: "발주처", + contractAmount: number, // 계약금액 + status: "진행중|완료|보류", + sites: [ // 현장 목록 + { + name: "현장명", + address: "주소", + progress: number // 진행률 + } + ], + orders: [ // 발주 내역 + { + vendor: "거래처", + amount: number, + status: "발주|납품|정산" + } + ], + claims: [ // 기성 청구 + { + round: number, // 차수 + amount: number, // 청구금액 + claimDate: "YYYY-MM-DD", + status: "청구|승인|입금" + } + ], + laborCosts: [ // 인건비 + { + month: "YYYY-MM", + amount: number, + workers: number + } + ] +} +``` + +### 3.3 금액 포맷 함수 + +```javascript +fmt(amount) // 1,234,567 (쉼표 포맷) +fmtBillion(amount) // 12.3억 (억 단위 축약) +``` + +--- + +## 4. 업무 Workflow (/juil/workflow) + +### 4.1 개요 + +블라인드/스크린 사업의 전체 업무 프로세스를 단계별로 시각화. 각 프로세스에 담당 부서, 산출물, 서브스텝을 정의. + +### 4.2 프로세스 데이터 구조 + +```javascript +{ + id: "S1-1", // 프로세스 ID + phase: "영업", // Phase 명 + name: "정보 수집", // 프로세스 이름 + icon: "icon-name", // 아이콘 + dept: "영업팀", // 담당 부서 + color: "#3B82F6", // 테마 색상 + description: "프로세스 설명", + documents: [ // 산출물 목록 + "현장조사서", "고객요구사항서" + ], + subSteps: [ // 상세 서브스텝 + { + name: "서브스텝명", + description: "상세 설명", + responsible: "담당자/팀", + output: "산출물" + } + ] +} +``` + +### 4.3 업무 Phase 목록 + +| Phase | ID 범위 | 설명 | +|-------|---------|------| +| **영업** | S1-1 ~ S1-4 | 정보 수집 → 현장 실측 → 고객 미팅 → 프로젝트 검토 | +| **견적서 작성** | S2-1 ~ S2-4 | 물량 산출 → 단가 산정 → 견적가 산출 → 견적서 작성/검토 | +| **입찰** | S3-* | 입찰 준비 → 제출 → 결과 확인 | +| **계약** | S4-* | 계약 협상 → 계약 체결 | +| **공사** | S5-* | 자재 발주 → 시공 → 현장 관리 | +| **준공** | S6-* | 검수 → 하자보수 → 준공 정산 | + +### 4.4 SAM 연계 포인트 + +```javascript +// 견적서 작성 Phase에서 SAM 견적 화면으로 연결 +{ samLink: '/juil/estimate', label: '견적서 작성 바로가기' } +``` + +--- + +## 5. 공통 특징 + +### 5.1 HTMX 전체 페이지 로드 + +3개 화면 모두 React 컴포넌트 사용하므로 HTMX 부분 로드 불가: + +```php +// PlanningController의 모든 메서드 +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.xxx')); +} +return view('juil.xxx'); +``` + +### 5.2 현재 구현 상태 + +| 항목 | 상태 | +|------|------| +| UI 화면 | 구현 완료 (React 인라인) | +| 목데이터 | 블레이드에 하드코딩 | +| API 연동 | 미연동 (향후 계획) | +| DB 테이블 | 미생성 (향후 계획) | +| CRUD 기능 | 뷰 조회만 (생성/수정/삭제 미구현) | + +### 5.3 향후 개발 방향 + +1. 견적/프로젝트 DB 테이블 설계 (API 프로젝트) +2. API 엔드포인트 구현 +3. React 컴포넌트 API 연동 +4. SAM 견적 시스템과 데이터 동기화 + +--- + +## 관련 문서 + +- [README.md](README.md) — 기획 메뉴 전체 개요 +- [공사현장 사진대지](construction-photos.md) — GCS 파일 관리, 음성 입력 +- [회의록 작성](meeting-minutes.md) — STT/AI 통합 회의 기록 +- [견적 시스템](../quotes/README.md) — SAM 견적 관리 (BOM, 10단계 로직) + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/features/rd/README.md b/features/rd/README.md new file mode 100644 index 0000000..1483160 --- /dev/null +++ b/features/rd/README.md @@ -0,0 +1,110 @@ +# R&D 메뉴 + +> **작성일**: 2026-03-08 +> **상태**: 운영 중 +> **프로젝트**: SAM MNG (관리자 웹) +> **라우트 접두사**: `/rd` + +--- + +## 1. 개요 + +### 1.1 목적 + +R&D 메뉴는 SAM 플랫폼의 **연구개발 및 내부 도구** 모음이다. AI 견적, 조직도 관리, 기획디자인(스토리보드 에디터), 안전점검 등 실험적이거나 내부 운영 목적의 기능을 제공한다. + +### 1.2 문서 구조 + +| 문서 | 설명 | +|------|------| +| **README.md** (이 문서) | 전체 개요, 메뉴 구조, 컨트롤러 매핑 | +| [planning-design.md](planning-design.md) | 기획디자인 스토리보드 에디터 기술 명세 | +| [design-insight.md](design-insight.md) | 디자인 인사이트 UI/UX 연구 도구 (100종 패턴, AI 프롬프트) | + +### 1.3 하위 메뉴 구조 + +``` +R&D +├── 대시보드 /rd +├── 조직도 관리 /rd/org-chart +├── 중대재해처벌법 점검 /rd/safety-audit +├── AI 견적 /rd/ai-quotation +│ ├── 목록 /rd/ai-quotation +│ ├── 생성 /rd/ai-quotation/create +│ ├── 상세 /rd/ai-quotation/{id} +│ ├── 편집 /rd/ai-quotation/{id}/edit +│ └── 문서 /rd/ai-quotation/{id}/document +├── 기획디자인 /rd/planning-design +└── 디자인 인사이트 /rd/design-insight +``` + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + Alpine.js | 반응형 SPA (서버 렌더링 없음) | +| 컨트롤러 | `RdController` | 모든 R&D 라우트 처리 | +| 서비스 | `AiQuotationService` | AI 견적 비즈니스 로직 | +| 모델 | `AiQuotation`, `Employee`, `Department`, `Tenant` | Multi-tenant | +| 저장 | localStorage (기획디자인), DB (견적/조직도) | 용도별 분리 | + +### 2.2 컨트롤러 구조 + +**파일**: `app/Http/Controllers/RdController.php` + +| 메서드 | 라우트 | 설명 | +|--------|--------|------| +| `index()` | `GET /rd` | R&D 대시보드 | +| `orgChart()` | `GET /rd/org-chart` | 조직도 관리 | +| `orgChartAssign()` | `POST /rd/org-chart/assign` | 직원 부서 배치 | +| `orgChartUnassign()` | `POST /rd/org-chart/unassign` | 직원 부서 해제 | +| `orgChartReorder()` | `POST /rd/org-chart/reorder` | 직원 순서/이동 | +| `orgChartReorderDepts()` | `POST /rd/org-chart/reorder-depts` | 부서 순서 변경 | +| `orgChartToggleHide()` | `POST /rd/org-chart/toggle-hide` | 부서 숨기기/표시 | +| `safetyAudit()` | `GET /rd/safety-audit` | 중대재해처벌법 점검 | +| `quotations()` | `GET /rd/ai-quotation` | AI 견적 목록 | +| `createQuotation()` | `GET /rd/ai-quotation/create` | AI 견적 생성 폼 | +| `showQuotation()` | `GET /rd/ai-quotation/{id}` | AI 견적 상세 | +| `editQuotation()` | `GET /rd/ai-quotation/{id}/edit` | AI 견적 편집 | +| `documentQuotation()` | `GET /rd/ai-quotation/{id}/document` | AI 견적 문서 | +| `planningDesign()` | `GET /rd/planning-design` | 기획디자인 | +| `designInsight()` | `GET /rd/design-insight` | 디자인 인사이트 | + +### 2.3 HTMX 전체 페이지 로드 규칙 + +모든 `/rd/*` 페이지는 Alpine.js 또는 React 컴포넌트를 사용하므로, HTMX 부분 로드 시 스크립트가 실행되지 않는다. 각 메서드에서 `HX-Request` 감지 시 `HX-Redirect`로 전체 페이지 로드를 강제한다. + +```php +if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.planning-design')); +} +``` + +--- + +## 3. 기능별 구현 현황 + +| 기능 | 구현 방식 | 백엔드 | DB | 상태 | +|------|----------|--------|-----|------| +| R&D 대시보드 | Blade | AiQuotationService | ai_quotations | 운영 중 | +| 조직도 관리 | Blade + Alpine.js | RdController (직접 쿼리) | employees, departments | 운영 중 | +| 중대재해처벌법 점검 | Blade (정적) | 없음 | 없음 | 운영 중 | +| AI 견적 | Blade + Alpine.js | AiQuotationService | ai_quotations | 운영 중 | +| **기획디자인** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | +| **디자인 인사이트** | **Blade + Alpine.js (SPA)** | **없음 (localStorage)** | **없음** | **운영 중** | + +--- + +## 4. 관련 문서 + +- [기획디자인 스토리보드 에디터](planning-design.md) — 블록 에디터, 서식, 인쇄, 내보내기 +- [디자인 인사이트](design-insight.md) — UI/UX 연구 도구 (100종 패턴, AI 프롬프트 복사) +- [조직도 관리 기술문서](../../projects/org-chart/) — 조직도 시스템 상세 (별도 프로젝트 문서) + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/features/rd/design-insight.md b/features/rd/design-insight.md new file mode 100644 index 0000000..683d02f --- /dev/null +++ b/features/rd/design-insight.md @@ -0,0 +1,246 @@ +# 디자인 인사이트 — UI/UX 연구 도구 + +> **작성일**: 2026-03-08 +> **상태**: 운영 중 +> **라우트**: `GET /rd/design-insight` +> **뷰**: `resources/views/rd/design-insight/index.blade.php` + +--- + +## 1. 개요 + +### 1.1 목적 + +디자인 인사이트는 UI/UX 패턴을 **수집·분석·비교**하는 연구 도구이다. 외부 서비스의 UI 레퍼런스를 카드 형태로 정리하고, CSS 와이어프레임 미리보기와 AI 프롬프트 생성 기능으로 디자인 의사결정을 지원한다. + +### 1.2 핵심 기능 + +| 기능 | 설명 | +|------|------| +| 카드 관리 | 레퍼런스/분석/패턴/Before-After 4종 카드 CRUD | +| 프로젝트 관리 | 다중 프로젝트, localStorage 저장 | +| CSS 와이어프레임 | 100종 UI 패턴의 순수 CSS 미니 와이어프레임 | +| 프리셋 템플릿 | 인기 UI 패턴 100종 원클릭 불러오기 | +| AI 프롬프트 복사 | 카드 정보를 AI용 구조화 프롬프트로 변환·복사 | +| 3종 뷰 | 보드(카테고리별)/갤러리(그리드)/리스트 뷰 | +| JSON 내보내기/가져오기 | 프로젝트 데이터 백업/복원 | + +--- + +## 2. 아키텍처 + +### 2.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 뷰 | Blade + Alpine.js | 단일 파일 SPA | +| 컨트롤러 | `RdController::designInsight()` | HX-Redirect 패턴 | +| 저장 | localStorage | `di_projects`, `di_current` 키 사용 | +| 백엔드 | 없음 | 서버 API 호출 없이 클라이언트 단독 동작 | +| 스타일 | 커스텀 CSS 변수 | `--di-*` 접두사 (Tailwind 미사용) | + +### 2.2 데이터 구조 + +```json +{ + "id": "di_1709000000_abc123", + "title": "프로젝트명", + "cards": [ + { + "id": "di_1709000001_def456", + "type": "pattern", + "title": "KPI 대시보드", + "category": "dashboard", + "rating": 5, + "tags": ["대시보드", "KPI", "통계"], + "memo": "레퍼런스 설명", + "guidelines": "디자인 가이드라인", + "usedIn": ["Stripe", "Shopify"], + "components": [ + { "name": "KPI 요약 카드", "required": true }, + { "name": "필터 영역", "required": false } + ], + "image": null, + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "createdAt": "2026-03-08T00:00:00.000Z", + "updatedAt": "2026-03-08T00:00:00.000Z" +} +``` + +--- + +## 3. 카드 유형 (4종) + +| 코드 | 라벨 | 용도 | +|------|------|------| +| `reference` | 레퍼런스 | 외부 서비스 UI 스크린샷 수집 | +| `analysis` | 분석 | CRAP 원칙 등 UX 분석 (8가지 디자인 원칙 평가) | +| `pattern` | 패턴 | 재사용 가능한 UI 패턴 정의 | +| `comparison` | Before/After | 개선 전후 비교 (이미지 2장) | + +--- + +## 4. 카테고리 (8종) + +| 코드 | 라벨 | 아이콘 | +|------|------|--------| +| `dashboard` | 대시보드 | 📊 | +| `list` | 목록 | 📋 | +| `form` | 상세/폼 | 📝 | +| `modal` | 모달/팝업 | 💬 | +| `navigation` | 네비게이션 | 🧭 | +| `auth` | 로그인 | 🔐 | +| `report` | 보고서 | 📄 | +| `etc` | 기타 | 📎 | + +--- + +## 5. CSS 와이어프레임 시스템 + +### 5.1 동작 원리 + +`getWireframe(card)` 함수가 카드의 `title`과 `tags`를 키워드 매칭하여 해당 패턴에 맞는 순수 CSS/HTML 미니 와이어프레임을 반환한다. + +```javascript +getWireframe(card) { + const t = (card.title || '').toLowerCase(); + const tags = (card.tags || []).join(' ').toLowerCase(); + const key = t + ' ' + tags; + + if (key.includes('kpi') || key.includes('대시보드') && key.includes('통계')) return `...`; + // ... 100종 패턴 매칭 + return `기본 와이어프레임 (매칭 안 됨)`; +} +``` + +### 5.2 프리셋 100종 분포 + +| 카테고리 | 패턴 수 | 대표 패턴 | +|---------|---------|----------| +| 대시보드 | 10 | KPI, 실시간 모니터링, 게이지/미터, 히트맵, 퍼널 | +| 목록 | 10 | 데이터 테이블, 칸반 보드, 마스터-디테일, 피벗 테이블 | +| 상세/폼 | 16 | 프로필, 설정, 위지윅 에디터, 멀티스텝 폼, 태그 입력 | +| 모달/팝업 | 10 | 확인 다이얼로그, 라이트박스, 바텀시트, 컨텍스트 메뉴 | +| 네비게이션 | 10 | 사이드바, 탭, 브레드크럼, FAB, 앵커 스크롤 | +| 로그인 | 8 | 로그인 폼, 회원가입, 비밀번호 재설정, RBAC, API 키 | +| 보고서 | 9 | 인쇄용 보고서, 간트 차트, 조직도, 워터폴, 리포트 빌더 | +| 기타 | 27 | 댓글, 에러 페이지, FAQ, 캐러셀, 파일 매니저 등 | + +--- + +## 6. AI 프롬프트 복사 기능 + +### 6.1 목적 + +카드에 정리된 UI 패턴 정보를 **AI가 이해할 수 있는 구조화된 마크다운 프롬프트**로 변환하여 클립보드에 복사한다. 복사한 프롬프트를 AI(Claude, ChatGPT 등)에 붙여넣으면 해당 스타일로 코드를 생성할 수 있다. + +### 6.2 UI 위치 + +카드 상세 모달 상단, **편집** 버튼 왼쪽에 보라색 `✨ AI 프롬프트` 버튼으로 배치. + +### 6.3 프롬프트 구조 + +`copyAiPrompt(card)` 함수가 카드 데이터를 다음 구조로 변환한다: + +```markdown +## UI 패턴 구현 요청 + +아래 UI/UX 패턴 레퍼런스를 참고하여, 동일한 스타일과 구조로 코드를 작성해 주세요. + +--- + +### 패턴 정보 +- **패턴명**: {title} +- **카테고리**: {category label} +- **완성도 평점**: ★★★☆☆ ({rating}/5) +- **키워드**: {tags} + +### 레퍼런스 설명 +{memo} + +### 실제 사용처 (벤치마킹 대상) +- {usedIn[0]} +- {usedIn[1]} + +### 필수 구성 요소 + +**필수 (반드시 포함)**: +- ✅ {required component} + +**선택 (권장)**: +- ○ {optional component} + +### 디자인 가이드라인 +{guidelines} + +### 개선 제안 +{suggestion} + +### 기대 효과 +{effect} + +--- + +### 구현 요구사항 + +1. **기술 스택**: HTML + Tailwind CSS (또는 프로젝트에 맞는 프레임워크) +2. **반응형**: 모바일/태블릿/데스크톱 대응 +3. **접근성**: 시맨틱 태그, ARIA 라벨, 키보드 네비게이션 +4. **인터랙션**: hover, focus, active 상태 포함 +5. **위 구성 요소를 모두 포함**하되, 실제 서비스처럼 자연스러운 더미 데이터 사용 +6. **위 가이드라인을 충실히 반영**하여 UX 완성도를 높일 것 +``` + +### 6.4 포함 필드 매핑 + +| 카드 필드 | 프롬프트 섹션 | 조건 | +|----------|-------------|------| +| `title` | 패턴 정보 > 패턴명 | 항상 | +| `category` | 패턴 정보 > 카테고리 | 항상 (라벨로 변환) | +| `rating` | 패턴 정보 > 평점 | 항상 (별점으로 변환) | +| `tags` | 패턴 정보 > 키워드 | 태그가 있을 때만 | +| `memo` | 레퍼런스 설명 | 값이 있을 때만 | +| `usedIn` | 실제 사용처 | 배열이 비어있지 않을 때만 | +| `components` | 필수 구성 요소 | 배열이 비어있지 않을 때만 | +| `guidelines` | 디자인 가이드라인 | 값이 있을 때만 | +| `suggestion` | 개선 제안 | 값이 있을 때만 | +| `effect` | 기대 효과 | 값이 있을 때만 | +| `principles` | UX 원칙 | `type === 'analysis'`일 때만 | + +--- + +## 7. 뷰 모드 (3종) + +| 모드 | 설명 | 정렬 | +|------|------|------| +| `board` | 카테고리별 컬럼 그룹핑 | 카테고리 → 생성순 | +| `gallery` | 그리드 갤러리 (와이어프레임 강조) | 필터 순 | +| `list` | 테이블형 리스트 | 필터 순 | + +--- + +## 8. 파일 구조 + +``` +resources/views/rd/design-insight/ +└── index.blade.php # 단일 파일 SPA (~6,200줄) + ├── + + + +

제목

+

본문 내용

+ + +``` + +### 3.2 슬라이드 크기 (body width/height) + +| 용도 | body 크기 | 변환 스크립트 layout | +|------|----------|---------------------| +| **16:9 가로형** (발표용) | `width: 720pt; height: 405pt;` | `width: 10, height: 5.625` | +| **9:16 세로형** (브로셔) | `width: 405pt; height: 720pt;` | `width: 5.625, height: 10` | +| **4:3 가로형** (구형) | `width: 720pt; height: 540pt;` | `width: 10, height: 7.5` | + +> **중요**: HTML의 body 크기와 변환 스크립트의 layout 크기가 일치해야 한다. 불일치 시 에러 발생. + +### 3.3 필수 규칙 + +#### 텍스트 줄바꿈 방지 (가장 중요) + +브라우저와 PowerPoint의 폰트 렌더링 차이로, HTML에서 한 줄인 텍스트가 PPTX에서 두 줄로 넘어가는 문제가 발생한다. + +```html + +

이 텍스트는 한 줄입니다

+ + +

이 텍스트는 한 줄입니다

+ + +

+ 여러 줄로 의도된 텍스트입니다.
+ 이 경우 nowrap을 넣지 않는다. +

+``` + +#### 적용 대상 + +- 뱃지 텍스트 (01, UC-01 등) +- 카드 제목, 태그, 짧은 라벨 +- 푸터 텍스트 +- 단일행 `

` 태그 전부 + +#### 이미지 경로 + +```html + + + + + +``` + +#### 스타일 작성 + +```html + +

+ + +``` + +### 3.4 사용 가능한 CSS 속성 + +| 속성 | 지원 | 비고 | +|------|:----:|------| +| background (색상) | ✅ | 단색, rgba 모두 지원 | +| background (그라데이션) | ✅ | linear-gradient 지원 | +| border | ✅ | 색상, 두께, radius | +| border-radius | ✅ | px, pt 단위 | +| font-size, font-weight | ✅ | pt 단위 권장 | +| color | ✅ | hex, rgba | +| padding, margin | ✅ | pt 단위 권장 | +| display: flex | ✅ | gap, align-items 등 | +| white-space: nowrap | ✅ | 줄바꿈 방지 (필수) | +| opacity | ✅ | | +| box-shadow | ⚠️ | 부분 지원 | +| transform | ❌ | 미지원 | +| animation | ❌ | 미지원 | + +--- + +## 4. 변환 스크립트 작성법 + +### 4.1 기본 구조 (복사해서 사용) + +```javascript +const path = require('path'); + +// ① 패키지 경로 설정 (이 두 줄은 항상 동일) +module.paths.unshift( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') +); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') +); + +async function main() { + const pres = new PptxGenJS(); + + // ② 레이아웃 설정 (HTML body 크기와 일치해야 함) + pres.defineLayout({ name: 'CUSTOM', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM'; + + // ③ HTML 파일 변환 + await html2pptx('/절대경로/slides/slide-01.html', pres); + + // ④ PPTX 출력 + await pres.writeFile({ fileName: '/절대경로/output.pptx' }); + console.log('완료!'); +} + +main().catch(console.error); +``` + +### 4.2 실전 예시: 여러 슬라이드 변환 + +```javascript +const path = require('path'); +module.paths.unshift( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules') +); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require( + path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js') +); + +async function main() { + const pres = new PptxGenJS(); + + // 16:9 가로형 + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + + const slidesDir = path.join(__dirname, 'slides'); + + // 변환할 HTML 파일 목록 + const slides = [ + 'slide-01.html', + 'slide-02.html', + 'slide-03.html', + ]; + + for (const file of slides) { + console.log(`Converting ${file} ...`); + try { + await html2pptx(path.join(slidesDir, file), pres); + } catch (err) { + console.error(`Error: ${err.message}`); + } + } + + const outputPath = path.join(__dirname, 'my-presentation.pptx'); + await pres.writeFile({ fileName: outputPath }); + console.log(`\nPPTX created: ${outputPath}`); +} + +main().catch(console.error); +``` + +### 4.3 실전 예시: 번호 기반 자동 변환 (18슬라이드) + +```javascript +// slide-01.html ~ slide-18.html 자동 변환 +const totalSlides = 18; +for (let i = 1; i <= totalSlides; i++) { + const num = String(i).padStart(2, '0'); + const htmlFile = path.join(slidesDir, `slide-${num}.html`); + await html2pptx(htmlFile, pres); +} +``` + +--- + +## 5. 실행 방법 + +### 5.1 터미널에서 직접 실행 + +```bash +# 해당 폴더로 이동 후 실행 +cd /home/aweso/sam/docs/brochure +node convert-2page.cjs + +# 또는 절대 경로로 실행 +node /home/aweso/sam/docs/brochure/convert-2page.cjs +``` + +### 5.2 실행 결과 + +``` +Converting brochure-2page-front.html ... +Converting brochure-2page-back.html ... + +PPTX created: /home/aweso/sam/docs/brochure/sam-brochure-2page.pptx +``` + +> 에러가 나면 HTML body 크기와 layout 설정 불일치가 가장 흔한 원인이다. + +--- + +## 6. 프로젝트 내 기존 사용 사례 + +| 경로 | 슬라이드 수 | 레이아웃 | 용도 | +|------|:-----------:|:--------:|------| +| `docs/usecase/` | 18장 | 16:9 가로 | 방화셔터 제안서 | +| `docs/usecase/brochure/` | 1장 / 2장 | 9:16 세로 | 방화셔터 브로셔 | +| `docs/brochure/` | 1장 / 2장 | 9:16 세로 | SAM 범용 브로셔 | +| `docs/plans/slides/` | N장 | 16:9 가로 | 배포 계획 발표 | +| `docs/rules/slides/` | N장 | 16:9 가로 | 정책 규칙 문서 | + +--- + +## 7. 폴더 구조 권장 패턴 + +새 PPTX를 만들 때 아래 구조를 따른다: + +``` +docs/my-document/ +├── slides/ ← HTML 슬라이드 파일들 +│ ├── slide-01.html +│ ├── slide-02.html +│ └── slide-03.html +├── convert.cjs ← 변환 스크립트 +└── my-document.pptx ← 생성된 PPTX (node convert.cjs 실행 후) +``` + +--- + +## 8. 문제 해결 + +| 증상 | 원인 | 해결 | +|------|------|------| +| `Error: dimensions don't match` | HTML body 크기 ≠ layout 설정 | body의 width/height와 `defineLayout` 값 맞추기 | +| 텍스트가 PPTX에서 줄바꿈됨 | `white-space: nowrap` 미적용 | 단일행 `

` 태그에 nowrap 추가 | +| 이미지 안 보임 | 상대 경로 사용 | 절대 경로(`/home/aweso/...`)로 변경 | +| `Cannot find module 'pptxgenjs'` | module.paths 설정 누락 | 스크립트 상단 2줄(module.paths.unshift) 확인 | +| `Cannot find module 'playwright'` | Playwright 미설치 | `cd ~/.claude/skills/pptx-skill/scripts && npx playwright install chromium` | +| PPTX는 생성되지만 빈 슬라이드 | HTML 내용 없음 | HTML 파일을 브라우저로 열어 확인 | + +--- + +## 9. 빠른 시작 (3분 가이드) + +### Step 1: 폴더 만들기 + +```bash +mkdir -p /home/aweso/sam/docs/my-pptx/slides +``` + +### Step 2: HTML 슬라이드 만들기 + +`slides/slide-01.html` 파일 생성: + +```html + + + + + + + +

MY COMPANY

+

발표 제목을 여기에

+

부제목 또는 설명

+ + +``` + +### Step 3: 변환 스크립트 만들기 + +`convert.cjs` 파일 생성: + +```javascript +const path = require('path'); +module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); +const PptxGenJS = require('pptxgenjs'); +const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); + +async function main() { + const pres = new PptxGenJS(); + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + + await html2pptx(path.join(__dirname, 'slides', 'slide-01.html'), pres); + + await pres.writeFile({ fileName: path.join(__dirname, 'my-presentation.pptx') }); + console.log('완료!'); +} +main().catch(console.error); +``` + +### Step 4: 실행 + +```bash +cd /home/aweso/sam/docs/my-pptx +node convert.cjs +# → my-presentation.pptx 생성됨 +``` + +--- + +**최종 업데이트**: 2026-03-01 diff --git a/guides/server-how-it-works.md b/guides/server-how-it-works.md new file mode 100644 index 0000000..974693b --- /dev/null +++ b/guides/server-how-it-works.md @@ -0,0 +1,247 @@ +# SAM 서버 동작 원리 초보자 가이드 + +> **작성일**: 2026-02-22 +> **대상**: SAM 프로젝트에 새로 합류한 개발자 + +--- + +## 1. 개요 + +### 1.1 이 문서의 목적 + +SAM 시스템에서 **웹 요청이 어떤 경로로 흐르는지**, **git push 후 서버에서 무슨 일이 일어나는지**를 설명한다. +설정값 나열이 아닌, **"왜 이런 구조인가"**에 초점을 맞춘다. + +### 1.2 SAM 전체 구조 + +``` +브라우저 → Nginx (SSL 종료, 도메인별 라우팅) + │ + ┌────┬───┴───┬─────┬─────┐ + ▼ ▼ ▼ ▼ ▼ + MNG API React Sales 5130 ← 5개 서비스 + (PHP)(PHP) (Node) (PHP) (PHP7.3) + └────┴───┬───┴─────┴─────┘ + ▼ + MySQL 8.0 ← 단일 DB 공유 +``` + +--- + +## 2. 웹 요청의 여정: URL에서 화면까지 + +### 2.1 전체 흐름 + +`https://mng.sam.kr/orders` 접속 시: + +``` +브라우저 →① Nginx →② PHP-FPM →③ Laravel →④ MySQL + │ +브라우저 ←────────────────────────────── ⑤ 응답 +``` + +### 2.2 Step 1: 브라우저 → Nginx + +Nginx는 **도메인 이름**을 보고 어떤 서비스로 보낼지 결정한다. + +- `mng.sam.kr` → MNG 컨테이너의 PHP-FPM (포트 9000) +- `api.sam.kr` → API 컨테이너의 PHP-FPM (포트 9000) +- `dev.sam.kr` → React 컨테이너의 Node.js (포트 3000) + +또한 HTTP(80) 요청을 HTTPS(443)로 리다이렉트하고, SSL 인증서를 처리한다. +이를 **SSL 종료**(SSL Termination)라 한다. 내부 통신은 암호화 없이 빠르게 진행된다. + +### 2.3 Step 2: Nginx → PHP-FPM + +Nginx는 PHP 코드를 직접 실행하지 못한다. 대신 **FastCGI 프로토콜**로 PHP-FPM에 요청을 전달한다. + +``` +Nginx: "이 PHP 파일을 실행해줘" → fastcgi_pass mng:9000 +PHP-FPM: "결과 HTML이야" → Nginx → 브라우저 +``` + +PHP-FPM은 여러 **워커 프로세스**를 미리 만들어 두고, 요청이 오면 빈 워커에 할당한다. +MNG의 경우 최대 20개 워커(`pm.max_children = 20`)가 동시에 요청을 처리할 수 있다. + +### 2.4 Step 3: PHP-FPM → Laravel + +PHP-FPM이 실행하는 진입점은 `public/index.php`다. 여기서 Laravel 프레임워크가 시작된다. + +``` +public/index.php + → Bootstrap (설정 로드, 서비스 등록) + → 미들웨어 (인증, 권한, 로깅) + → 라우터 (URL → 컨트롤러 매핑) + → 컨트롤러 (비즈니스 로직) + → 뷰 렌더링 (Blade 템플릿 → HTML) +``` + +### 2.5 Step 4: Laravel → MySQL + +컨트롤러에서 Eloquent ORM으로 DB를 조회한다. 예를 들어: + +```php +// 코드: Order::where('status', 'active')->get(); +// 실제 SQL: SELECT * FROM orders WHERE status = 'active' AND tenant_id = 1; +``` + +`tenant_id`는 글로벌 스코프로 자동 추가되어, 다른 테넌트의 데이터가 섞이지 않는다. + +### 2.6 Step 5: 응답이 돌아오는 길 + +MySQL → Laravel(HTML 생성) → PHP-FPM → Nginx → 브라우저 순으로 돌아온다. +MNG는 HTMX를 사용하므로, 이후 상호작용은 **HTML 조각**(partial)만 주고받아 페이지 전체를 새로고침하지 않는다. + +--- + +## 3. 각 구성 요소의 역할 + +| 구성 요소 | 역할 | 비유 | +|-----------|------|------| +| **Nginx** | 리버스 프록시, SSL, 정적 파일 | 안내 데스크 | +| **PHP-FPM** | PHP 워커 풀 관리 | 창구 직원 팀 | +| **Laravel** | MVC, 라우팅, 비즈니스 로직 | 업무 매뉴얼 | +| **MySQL** | 데이터 저장/조회 | 서류 보관실 | +| **Supervisor** | 프로세스 감시, 자동 재시작 | 관리 감독관 | + +### 3.1 Supervisor가 관리하는 프로세스 + +각 컨테이너 안에서 Supervisor가 여러 프로세스를 관리한다. + +**API 컨테이너** (`sam-api-1`): +- `php-fpm` — PHP 요청 처리 +- `nginx` — 컨테이너 내부 웹서버 +- `queue-worker` — 백그라운드 작업 (이메일, 알림 등) +- `scheduler` — 60초마다 예약 작업 실행 (`schedule:run`) + +**MNG 컨테이너** (`sam-mng-1`): +- `php-fpm`, `nginx` — 위와 동일 +- `queue-worker` x2 — 2개 워커가 병렬 처리 + +--- + +## 4. 로컬 환경 vs 서버 환경 + +### 4.1 비교 + +``` +[로컬 - Docker] [서버 - Bare-metal] +┌───────────────┐ ┌───────────────┐ +│ sam-nginx-1 │ │ Nginx │ +├───────────────┤ ├───────────────┤ +│ sam-mng-1 │ │ MNG (직접) │ +│ sam-api-1 │ │ API (직접) │ +├───────────────┤ ├───────────────┤ +│ sam-mysql-1 │ │ MySQL (직접) │ +└───────────────┘ └───────────────┘ + 네트워크: samnet 네트워크: localhost +``` + +### 4.2 핵심 차이 + +| 항목 | 로컬 (Docker) | 서버 (Bare-metal) | +|------|--------------|-------------------| +| **DB 접속** | `DB_HOST=sam-mysql-1` | `DB_HOST=127.0.0.1` | +| **코드 반영** | 볼륨 마운트 (실시간) | `git pull` 필요 | +| **명령 실행** | `docker exec sam-api-1 php artisan ...` | `php artisan ...` | + +--- + +## 5. "git push하면 무슨 일이 일어나는가?" + +### 5.1 배포 흐름 다이어그램 + +``` +개발자 PC (WSL) Gitea 서버 운영 서버 +┌──────────┐ push ┌──────────┐ pull ┌──────────┐ +│ 코드 수정 │ ──────────→ │ 원격 │ ←───────── │ 서버에서 │ +│ git add │ │ 저장소 │ │ 수동 pull │ +│ git commit│ └──────────┘ └──────────┘ +└──────────┘ +``` + +> **주의**: 자동 배포(CI/CD)가 없다. 서버에서 **수동으로 `git pull`** 해야 반영된다. + +### 5.2 PHP 앱 배포 (MNG, API) + +```bash +# 서버에서 실행하는 명령 (개발팀장이 수행) +cd /home/webservice/api +git pull # ① 최신 코드 받기 +composer install # ② 패키지 의존성 동기화 +php artisan migrate # ③ DB 구조 변경 적용 +php artisan config:clear # ④ 설정 캐시 초기화 +``` + +**각 명령이 필요한 이유**: + +| 명령 | 왜 필요한가 | +|------|------------| +| `git pull` | 코드를 최신 상태로 동기화 | +| `composer install` | 새로 추가된 PHP 패키지 설치 (`composer.json` 변경 시) | +| `php artisan migrate` | 새 테이블/컬럼 생성 등 DB 스키마 적용 (API만) | +| `php artisan config:clear` | `.env` 또는 `config/` 변경 시 캐시된 설정 갱신 | + +### 5.3 React 앱 배포 (Next.js) + +서버 스펙(2코어, 3.8GB RAM)으로는 Next.js 빌드가 메모리 부족으로 실패한다. +따라서 **로컬에서 빌드 → 결과물을 서버에 업로드**하는 방식을 사용한다. + +```bash +# deploy.sh가 수행하는 5단계 +① 로컬에서 npm run build # standalone 빌드 +② tar.gz로 압축 # .next/standalone + static + public +③ scp로 서버 업로드 # 압축 파일 전송 +④ 서버에서 압축 해제 + 시작 # node server.js (포트 3001) +⑤ 로컬 정리 # 임시 파일 삭제 +``` + +--- + +## 6. SAM 도메인별 요청 경로 + +### 6.1 도메인 → 서비스 매핑 + +| 도메인 | 서비스 | 기술 스택 | 응답 형태 | +|--------|--------|-----------|-----------| +| `mng.sam.kr` | MNG | Laravel + Blade + HTMX | HTML (서버 렌더링) | +| `api.sam.kr` | API | Laravel | JSON | +| `dev.sam.kr` | React | Next.js | HTML (SSR/CSR) | +| `sales.sam.kr` | Sales | Laravel | HTML | +| `5130.sam.kr` | 5130 | PHP 7.3 (레거시) | HTML | + +### 6.2 서비스별 요청 흐름 + +**MNG** (관리자 화면 — Blade + HTMX): +``` +브라우저 → Nginx(:443) → MNG PHP-FPM(:9000) → Laravel → Blade HTML +이후 HTMX가 HTML 조각을 Ajax로 교체 (전체 새로고침 없음) +``` + +**API** (REST API — JSON 응답): +``` +React/외부 → Nginx(:443) → API PHP-FPM(:9000) → Laravel → JSON +인증: Bearer 토큰 (Authorization 헤더) +``` + +**React** (Next.js — SSR + CSR): +``` +브라우저 → Nginx(:443) → Node.js(:3000) → SSR HTML +이후 React 하이드레이션 → CSR (클라이언트 렌더링) +API 호출 시 → Next.js API Route 프록시 → api.sam.kr +``` + +--- + +## 관련 문서 + +| 문서 | 설명 | +|------|------| +| [docker-setup.md](../specs/docker-setup.md) | Docker 환경 설정값 상세 | +| [system-overview.md](../architecture/system-overview.md) | 시스템 아키텍처 레퍼런스 | +| [production-deployment-plan.md](../plans/production-deployment-plan.md) | 운영 배포 계획 | +| [dev-commands.md](../quickstart/dev-commands.md) | 개발 명령어 모음 | + +--- + +**최종 업데이트**: 2026-02-22 diff --git a/guides/table-design-guide.md b/guides/table-design-guide.md new file mode 100644 index 0000000..a920f08 --- /dev/null +++ b/guides/table-design-guide.md @@ -0,0 +1,486 @@ +# SAM 테이블 설계 가이드 — 비전문가용 + +> **작성일**: 2026-03-02 +> **대상 독자**: 개발자, 기획자, 관리자 — 데이터베이스를 잘 모르는 분도 읽을 수 있습니다 +> **관련 정책**: `standards/options-column-policy.md` (개발자 전용 상세 규칙) + +--- + +## 1. 이 문서는 왜 필요한가? + +SAM은 **여러 회사가 하나의 시스템을 공유**하는 구조입니다. +A회사, B회사, C회사가 모두 같은 프로그램을 쓰지만, 각 회사가 필요한 정보는 다릅니다. + +이 문서는 SAM에서 **데이터를 어떻게 저장하는지**, 그 설계 철학을 누구나 이해할 수 있도록 설명합니다. + +--- + +## 2. 기본 개념: 데이터베이스 테이블이란? + +데이터베이스 테이블은 **엑셀 시트**와 같습니다. + +``` +"주문" 테이블 (= 엑셀 시트) + + 열(컬럼) → 주문번호 │ 고객명 │ 금액 │ 상태 + ───────────────────────────────────────────────────────── + 행(레코드) 1 → ORD-001 │ 김철수 │ 500,000 │ 완료 + 행(레코드) 2 → ORD-002 │ 이영희 │ 300,000 │ 진행중 + 행(레코드) 3 → ORD-003 │ 박민수 │ 800,000 │ 대기 +``` + +- **열(컬럼)** = 정보의 종류 (주문번호, 고객명, 금액...) +- **행(레코드)** = 실제 데이터 한 건 (주문 1건) + +--- + +## 3. 문제: 회사마다 필요한 정보가 다르다 + +SAM은 여러 회사가 같은 테이블을 공유합니다. + +``` +같은 "주문" 테이블을 쓰는데... + + 🏭 A회사 (블라인드 제조) + → "절곡 각도", "날개 수" 정보가 필요해요 + + 🏭 B회사 (스크린 제조) + → "메시 밀도", "소재 종류" 정보가 필요해요 + + 🏭 C회사 (셔터 제조) + → "날개 간격", "색상 코드" 정보가 필요해요 +``` + +--- + +### 3.1 전통적인 해결 방법 (SAM은 이렇게 안 합니다) + +필요할 때마다 엑셀에 열을 추가하는 것처럼, 테이블에 컬럼을 추가합니다. + +``` +"주문" 테이블 — 전통적 방식 + + 주문번호 │ 고객명 │ 금액 │ 절곡각도 │ 날개수 │ 메시밀도 │ 소재 │ 날개간격 │ 색상코드 + ───────────────────────────────────────────────────────────────────────────────── + ORD-001 │ 김철수 │ 50만 │ 45도 │ 12개 │ (빈칸) │ (빈칸) │ (빈칸) │ (빈칸) ← A회사 + ORD-002 │ 이영희 │ 30만 │ (빈칸) │ (빈칸)│ 18 │ 폴리 │ (빈칸) │ (빈칸) ← B회사 + ORD-003 │ 박민수 │ 80만 │ (빈칸) │ (빈칸)│ (빈칸) │ (빈칸) │ 25mm │ #FF0000 ← C회사 +``` + +**문제점:** + +- 회사가 100개면? 열이 수백 개로 늘어남 +- 각 회사는 자기 것 빼고 전부 빈칸 +- 새 회사가 들어올 때마다 시스템 전체를 수정해야 함 +- 열 추가 = 시스템 중단 위험이 있는 작업 + +--- + +### 3.2 SAM의 해결 방법: "메모칸(options)" 하나로 통합 + +**핵심 열만 남기고**, 나머지는 **메모칸 하나**에 자유롭게 적습니다. + +``` +"주문" 테이블 — SAM 방식 + + 주문번호 │ 고객명 │ 금액 │ 상태 │ options (메모칸) + ──────────────────────────────────────────────────────────────────────── + ORD-001 │ 김철수 │ 50만 │ 완료 │ {"절곡각도": 45, "날개수": 12} ← A회사 + ORD-002 │ 이영희 │ 30만 │ 진행 │ {"메시밀도": 18, "소재": "폴리에스터"} ← B회사 + ORD-003 │ 박민수 │ 80만 │ 대기 │ {"날개간격": 25, "색상코드": "#FF0000"} ← C회사 + ORD-004 │ 최지은 │ 40만 │ 대기 │ null ← 메모 없음 +``` + +**`options`** = JSON이라는 형식의 메모칸. `{ }` 안에 자유롭게 정보를 넣을 수 있습니다. + +--- + +## 4. 어떤 정보를 열(컬럼)로 만들고, 어떤 정보를 메모칸(options)에 넣나? + +이것이 SAM 테이블 설계의 **가장 중요한 판단 기준**입니다. + +### 4.1 판단 흐름 (5가지 질문) + +새로운 정보를 저장해야 할 때, 아래 질문에 답합니다. + +``` +질문 1. 이 정보로 다른 테이블의 데이터를 연결(참조)하나? + 예: 고객ID로 고객 테이블을 찾는다 + → YES: 일반 컬럼 + +질문 2. 이 정보로 자주 검색(필터)하나? + 예: "완료" 상태인 주문만 보여줘 + → YES: 일반 컬럼 + +질문 3. 이 정보로 정렬하나? + 예: 최신 주문부터 보여줘 + → YES: 일반 컬럼 + +질문 4. 이 정보가 절대 중복되면 안 되나? + 예: 주문번호는 세상에 하나뿐이어야 한다 + → YES: 일반 컬럼 + +질문 5. 이 정보로 합계/평균을 계산하나? + 예: 이번 달 매출 합계 + → YES: 일반 컬럼 + +질문 1~5 전부 NO → options 메모칸에 저장 +``` + +### 4.2 실생활 예시로 비교 + +#### 예시 1: "주문" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 주문번호 | **일반 컬럼** | 중복 불가 + 검색 필수 | +| 고객 ID | **일반 컬럼** | 고객 테이블과 연결 | +| 금액 | **일반 컬럼** | 합계 계산 필요 | +| 상태 (진행/완료) | **일반 컬럼** | 필터(검색) 필수 | +| 생성일 | **일반 컬럼** | 정렬 필요 | +| 배송지 주소 | **options** | 부가 정보, 검색 안 함 | +| 수신자 이름 | **options** | 부가 정보 | +| 수신자 연락처 | **options** | 부가 정보 | +| 특이사항 메모 | **options** | 있어도 되고 없어도 됨 | + +**실제 SAM 코드에서 주문(Order) 테이블:** + +``` +일반 컬럼: id, tenant_id, order_number, client_id, total_amount, status, created_at +options: {"shipping_cost_code":"착불", "receiver":"홍길동", + "receiver_contact":"010-1234-5678", + "shipping_address":"서울 강남구 역삼동 123"} +``` + +#### 예시 2: "입고검사" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 품목 ID | **일반 컬럼** | 품목 테이블과 연결 | +| 수량 | **일반 컬럼** | 합계 계산 | +| 입고일 | **일반 컬럼** | 정렬 + 검색 | +| 제조사 | **options** | 모든 입고에 있지는 않음 | +| 검사 결과 (합격/불합격) | **options** | 검사를 안 하는 회사도 있음 | +| 검사일 | **options** | 선택적 정보 | + +**실제 SAM 코드에서 입고(Receiving) 테이블:** + +``` +일반 컬럼: id, tenant_id, item_id, quantity, received_at, status +options: {"manufacturer":"삼성전자", + "inspection_status":"합격", + "inspection_date":"2026-03-01"} +``` + +> 검사 결과가 options에 있는 이유: **모든 회사가 입고검사를 하는 것은 아닙니다.** +> A회사는 검사를 하고, B회사는 안 합니다. 이걸 일반 컬럼으로 만들면 B회사에겐 항상 빈칸입니다. + +#### 예시 3: "공정" 테이블 + +| 정보 | 어디에 저장? | 이유 | +|------|:-----------:|------| +| 공정 코드 | **일반 컬럼** | 중복 불가 + 검색 | +| 공정명 | **일반 컬럼** | 검색 + 표시 | +| 담당 부서 | **일반 컬럼** | 필터 | +| 작업일지 필요 여부 | **options** | 회사별로 다름 | +| 검사 필요 여부 | **options** | 회사별로 다름 | + +``` +일반 컬럼: id, tenant_id, process_code, process_name, department +options: {"needs_work_log": true, "needs_inspection": false} +``` + +--- + +## 5. 메모칸(options)의 실제 모습: JSON이란? + +`options`에 저장되는 데이터 형식은 **JSON**입니다. +JSON은 프로그래밍 세계의 "구조화된 메모장"이라고 생각하면 됩니다. + +### 5.1 JSON 기본 문법 + +``` +{ ← 시작 + "키": "값", ← 문자(텍스트) + "이름": "홍길동", + "나이": 30, ← 숫자 (따옴표 없음) + "합격": true, ← 참/거짓 (따옴표 없음) + "메모": null ← 값 없음 +} ← 끝 +``` + +### 5.2 중첩(nested) — 메모 안의 메모 + +``` +{ + "배송": { ← 배송 관련 정보를 묶음 + "주소": "서울 강남구 역삼동", + "수신자": "홍길동", + "연락처": "010-1234-5678" + }, + "검사": { ← 검사 관련 정보를 묶음 + "결과": "합격", + "검사일": "2026-03-01", + "검사자ID": 5 + } +} +``` + +### 5.3 목록(배열) — 여러 개를 나열 + +``` +{ + "선택지": [ ← 대괄호 [ ] = 목록 + {"label": "블라인드", "value": "blind"}, + {"label": "스크린", "value": "screen"}, + {"label": "셔터", "value": "shutter"} + ] +} +``` + +> 이 형태는 드롭다운 메뉴의 선택지 목록을 저장할 때 사용합니다. + +--- + +## 6. 멀티테넌시란? — 여러 회사가 하나의 시스템을 쓰는 구조 + +### 6.1 개념 + +``` +┌──────────────────────────────────────────────┐ +│ SAM 시스템 (하나의 프로그램) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ A회사 │ │ B회사 │ │ C회사 │ │ +│ │ tenant=1 │ │ tenant=2 │ │ tenant=3 │ │ +│ │ │ │ │ │ │ │ +│ │ 블라인드 │ │ 스크린 │ │ 셔터 │ │ +│ │ 제조 │ │ 제조 │ │ 제조 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ 같은 테이블을 쓰지만, │ +│ tenant_id로 데이터가 완전히 분리됨 │ +└──────────────────────────────────────────────┘ +``` + +### 6.2 tenant_id = 회사 식별 번호 + +모든 테이블의 모든 행에 `tenant_id`(회사 번호)가 붙어 있습니다. + +``` +"주문" 테이블 + + id │ tenant_id │ 주문번호 │ 금액 │ options + ─────────────────────────────────────────────────────────── + 1 │ 1 │ ORD-001 │ 50만 │ {"절곡각도": 45} ← A회사 데이터 + 2 │ 1 │ ORD-002 │ 30만 │ {"절곡각도": 90} ← A회사 데이터 + 3 │ 2 │ ORD-001 │ 80만 │ {"메시밀도": 18} ← B회사 데이터 + 4 │ 3 │ ORD-001 │ 40만 │ {"날개간격": 25} ← C회사 데이터 +``` + +**A회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 1`인 데이터만 보여줌 +**B회사가 로그인하면** → 시스템이 자동으로 `tenant_id = 2`인 데이터만 보여줌 + +> A회사는 B회사의 데이터를 절대 볼 수 없습니다. 시스템이 자동으로 차단합니다. + +### 6.3 options + tenant_id = 강력한 조합 + +이 두 가지가 합쳐지면: + +``` +같은 테이블, 같은 컬럼 구조인데 + ✅ 회사마다 다른 데이터 (tenant_id로 분리) + ✅ 회사마다 다른 속성 (options로 유연하게) + ✅ 시스템 수정 없이 확장 가능 +``` + +--- + +## 7. SAM 테이블의 표준 구조 + +SAM에서 새 테이블을 만들면 항상 이 구조를 따릅니다. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SAM 표준 테이블 구조 │ +│ │ +│ ① 식별자 │ +│ id — 자동 생성 번호 (1, 2, 3...) │ +│ tenant_id — 어느 회사의 데이터인지 │ +│ │ +│ ② 핵심 정보 (검색/정렬/연결에 쓰는 것만) │ +│ code — 코드 (중복 불가) │ +│ status — 상태 (검색용) │ +│ is_active — 사용 여부 │ +│ sort_order — 표시 순서 │ +│ (+ FK 컬럼들) — 다른 테이블 연결 │ +│ │ +│ ③ 메모칸 │ +│ options — 나머지 전부 (JSON) │ +│ │ +│ ④ 감사 기록 (자동) │ +│ created_by — 누가 만들었나 │ +│ updated_by — 누가 수정했나 │ +│ deleted_by — 누가 삭제했나 │ +│ created_at — 언제 만들었나 │ +│ updated_at — 언제 수정했나 │ +│ deleted_at — 언제 삭제했나 (휴지통 개념) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 영역별 설명 + +| 영역 | 역할 | 비유 | +|------|------|------| +| ① 식별자 | "이 데이터가 누구 것인지" 구분 | 우편물의 받는 사람 + 주소 | +| ② 핵심 정보 | 검색, 정렬, 집계에 꼭 필요한 정보 | 엑셀의 고정 열 | +| ③ options | 회사마다 다른 부가 정보 | 엑셀의 "비고" 칸 (자유 서식) | +| ④ 감사 기록 | 언제 누가 뭘 했는지 자동 추적 | CCTV 기록 | + +--- + +## 8. 실제 SAM에서 options를 쓰는 테이블들 (22개) + +현재 SAM에서 options 메모칸을 사용하는 주요 테이블입니다. + +| 테이블 | 한글명 | options에 저장하는 정보 예시 | +|--------|--------|--------------------------| +| `orders` | 주문 | 배송지, 수신자, 연락처, 담당자 | +| `quotes` | 견적 | 견적 요약, 비용 항목, 가격 조정 | +| `receivings` | 입고 | 제조사, 검사 결과, 검사일 | +| `work_orders` | 작업지시 | 절곡 정보 (bending_info) | +| `work_order_items` | 작업지시 항목 | 작업 결과, 양품/불량 수량, LOT번호 | +| `processes` | 공정 | 작업일지 필요 여부, 검사 필요 여부 | +| `order_nodes` | 주문 노드 | 위치, 구역, 층, 실 (트리 구조) | +| `products` | 제품 | 동적 옵션 (라벨, 값, 단위) | +| `items` | 품목 | 품목별 동적 속성 | +| `materials` | 자재 | 자재 추가 속성 | +| `menus` | 메뉴 | 섹션, 메뉴 타입, 필요 권한 | +| `users` | 사용자 | 개인 설정/환경설정 | +| `tenants` | 회사(테넌트) | 회사 규모, 업종 | +| `document_template_section_fields` | 문서 양식 필드 | 선택지 목록, API 경로 | +| `item_fields` | 품목 필드 정의 | 필드별 세부 설정 | + +--- + +## 9. 자주 묻는 질문 (FAQ) + +### Q1. options에 넣으면 검색이 안 되나요? + +**아닙니다.** MySQL 8.0은 JSON 내부도 검색할 수 있습니다. + +``` +일반 컬럼 검색: "상태가 '완료'인 주문 찾아줘" → 매우 빠름 +options 검색: "제조사가 '삼성'인 입고 찾아줘" → 가능하지만 조금 느림 +``` + +다만, **매일 수천 번 검색하는 정보**라면 일반 컬럼으로 승격하는 것이 맞습니다. +가끔 검색하는 정보라면 options로 충분합니다. + +### Q2. options에 아무 정보나 마음대로 넣을 수 있나요? + +기술적으로는 가능하지만, 개발팀 내부에서 **어떤 키를 쓸지 미리 약속**합니다. + +``` +✅ 약속된 키: {"manufacturer": "삼성", "inspection_status": "합격"} +❌ 멋대로: {"asdf": 123, "temp_data": "???"} +``` + +코드에서 상수로 정의하여 일관성을 유지합니다. + +### Q3. 전통적 방식보다 뭐가 좋은 건가요? + +| 비교 항목 | 전통적 방식 (열 추가) | SAM 방식 (options JSON) | +|----------|:------------------:|:---------------------:| +| 새 정보 추가 시 | 시스템 수정 필요 | 코드만 변경 | +| 다른 회사에 영향 | 있음 (전체 구조 변경) | 없음 | +| 빈칸(null) 낭비 | 많음 | 없음 | +| 검색 속도 | 빠름 | 조금 느림 (충분히 실용적) | +| 유연성 | 낮음 | 높음 | +| 시스템 중단 위험 | 있음 (대형 테이블 수정 시) | 없음 | + +### Q4. 그럼 모든 정보를 options에 넣으면 되지 않나요? + +**아닙니다.** 핵심 정보는 반드시 일반 컬럼으로 만들어야 합니다. + +``` +❌ 나쁜 예: 모든 것을 options에 + + id │ tenant_id │ options + ────────────────────────────────────────────────────────────── + 1 │ 1 │ {"주문번호":"ORD-001", "금액":500000, "상태":"완료", ...} + + → 주문번호 검색 느림, 금액 합계 계산 불가, 중복 방지 불가 +``` + +``` +✅ 좋은 예: 핵심은 컬럼, 부가는 options + + id │ tenant_id │ order_number │ amount │ status │ options + ────────────────────────────────────────────────────────────── + 1 │ 1 │ ORD-001 │ 500000 │ 완료 │ {"배송지":"서울..."} + + → 검색 빠름, 합계 가능, 중복 방지 가능, 부가 정보도 유연 +``` + +### Q5. options 데이터는 화면에서 어떻게 보이나요? + +사용자 화면에서는 options 안에 있는지, 일반 컬럼인지 **구분할 수 없습니다**. +프로그램이 자동으로 꺼내서 보여줍니다. + +``` +화면에 보이는 모습: + + ┌─────────────────────────────────┐ + │ 입고 상세 정보 │ + │ │ + │ 품목: SUS304 스틸 │ ← 일반 컬럼 + │ 수량: 100개 │ ← 일반 컬럼 + │ 입고일: 2026-03-01 │ ← 일반 컬럼 + │ 제조사: 삼성전자 │ ← options에서 꺼냄 + │ 검사결과: 합격 │ ← options에서 꺼냄 + │ 검사일: 2026-03-01 │ ← options에서 꺼냄 + └─────────────────────────────────┘ + + 사용자는 어디에 저장되어 있는지 알 필요 없음! +``` + +--- + +## 10. 한 장 요약 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ │ +│ SAM 테이블 설계 = "핵심만 컬럼, 나머진 메모칸(options)" │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ tenant_id → 어느 회사 것인지 (자동 격리) │ │ +│ │ 핵심 컬럼들 → 검색/정렬/연결/집계에 쓰는 필수 정보 │ │ +│ │ options → 나머지 전부 (회사마다 다른 부가 정보) │ │ +│ │ 감사 컬럼들 → 누가/언제 만들고/수정하고/삭제했는지 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ 이렇게 하면: │ +│ ✅ 회사 추가해도 테이블 구조 안 바꿈 │ +│ ✅ 새 정보 추가해도 시스템 수정 최소화 │ +│ ✅ 회사마다 다른 정보를 유연하게 저장 │ +│ ✅ 데이터 보안 (회사 간 완전 분리) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 관련 문서 + +| 문서 | 설명 | 대상 | +|------|------|------| +| [options-column-policy.md](../standards/options-column-policy.md) | 개발자용 상세 정책 (코드 규칙, 마이그레이션 패턴) | 개발자 | +| [database/README.md](../system/database/README.md) | DB 스키마 전체 현황 (220개 모델) | 개발자 | +| [PROJECT_DEVELOPMENT_POLICY.md](PROJECT_DEVELOPMENT_POLICY.md) | 개발 공통 정책 (테이블 생성 절차) | 개발자 | +| [system/overview.md](../system/overview.md) | SAM 시스템 전체 아키텍처 | 전체 | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/plans/SAM_General_Rule_Storyboard_D1.0.md b/plans/SAM_General_Rule_Storyboard_D1.0.md new file mode 100644 index 0000000..ae23c7b --- /dev/null +++ b/plans/SAM_General_Rule_Storyboard_D1.0.md @@ -0,0 +1,737 @@ +# SAM General Rule Storyboard D1.0 + +> **작성일**: 2026-01-16 +> **버전**: D1.0 +> **원본**: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` (43페이지) +> **상태**: PC 섹션 정리 완료 + +--- + +## 1. 문서 이력 + +| 날짜 | 버전 | 주요 내용 | 세부 내용 | +|------|------|----------|----------| +| 2026-01-15 | D0.9 | 초안 | General Rule - PC, 태블릿, 모바일 UIUX 공통 작성 | +| 2026-01-16 | D1.0 | 작성 | PC 섹션 정리 33p | + +--- + +## 2. 인터랙션 (Interaction) + +> **페이지**: 4 + +사용자 입력 제스처 및 적용 여부를 정의한다. + +| Type | 제스처/마크 | 설명 | 적용 | +|------|-----------|------|------| +| Tap | 탭 | 일정영역을 사용자가 터치한다. | Yes | +| Touch & Hold | 터치 앤 홀드 | 화면을 터치한 후 계속 누르고 있는 상태. 해당영역 혹은 개체가 홀드된다. | No | +| Double Tap | 더블 탭 | 일정영역을 두 번 터치한다. 두 번 터치 시 액션이 실행된다. | No | +| Drag & Drop | 드래그 앤 드롭 | 터치 혹은 홀드 상태에서 오브젝트를 이동하여 원하는 위치에 배치시킨다. | Yes | +| Scroll Up | 스크롤 업 | 아래에서 위로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes | +| Scroll Down | 스크롤 다운 | 위에서 아래로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes | +| Swipe Left | 스와이프 레프트 | 오른쪽에서 왼쪽으로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes | +| Swipe Right | 스와이프 라이트 | 왼쪽에서 오른쪽으로 누르는 동작을 유지하면서 이동하였다가 뗀다. | Yes | +| Pinch Zoom out | 핀치 줌 아웃 | 오브젝트 또는 화면을 축소한다. | Yes | +| Pinch Zoom in | 핀치 줌 인 | 오브젝트 혹은 화면을 확대한다. | Yes | + +--- + +## 3. 반응형 웹 (Responsive Web) + +> **페이지**: 5 + +### 3.1 브레이크 포인트 + +| 디바이스 | 브레이크 포인트 | Tailwind 접두사 | +|---------|---------------|----------------| +| 모바일 | < 640px | 기본 | +| 태블릿 | 768px ~ 1280px | `md` | +| 데스크탑 | 1280px+ | `lg` | +| 대형 모니터 | 1920px+ | `xl` | + +### 3.2 레이아웃 구성 + +**PC Web 레이아웃**: +1. Contents 영역 +2. Footer 영역 + +**Mobile Web 레이아웃**: +1. Contents 영역 +2. Footer 영역 + +--- + +## 4. 화면 템플릿 (Screen Template) + +> **페이지**: 6 + +모바일 웹 화면 구조를 정의한다. + +| 영역 | 코드 | 설명 | +|------|------|------| +| Status bar | A | 안테나, 통화, 배터리 등 시스템 OS 관리 영역. 모든 페이지 상단에 존재 | +| Browser 영역 | B | 브라우저 기능 영역 | +| Title 영역 | C | 텍스트 또는 기능 버튼으로 구현됨. 텍스트는 기본 가운데 정렬 | +| Content 영역 | D | 컨텐츠 내용 표시. 컨텐츠 길이가 길어질 경우 스크롤 제공 | +| Browser bar 영역 | E | 브라우저 유틸 바 영역 | +| Keypad 영역 | F | 키보드 입력할 때 활성화. 모든 페이지 위에 덮어쓰기 구현 | + +--- + +## 5. 메시지 (Notifications) + +> **페이지**: 7 + +| 유형 | 설명 | +|------|------| +| 알림 Alert | 사용자에게 상황을 알려주기 위한 팝업. `[확인]` 버튼 제공 | +| 확인 Alert | 사용자에게 확인이 필요할 경우 제공되는 팝업. `[취소]` `[확인]` 버튼 제공 | +| 토스트 메시지 | 단순 Notify. 2~3초 후 페이지 내에서 Fade out | + +--- + +## 6. GNB, LNB, 푸터 + +> **페이지**: 8 + +PC 화면의 전체 레이아웃 구조를 정의한다. + +### 6.1 구성 요소 + +| 번호 | 영역 | 설명 | +|------|------|------| +| 01 | 메뉴 버튼 | 클릭: 메뉴 영역(06) 축소/확장 토글. 디폴트: 메뉴 영역 확장 상태 | +| 02 | SAM 로고 버튼 | 클릭: 대시보드 화면으로 이동 | +| 03 | 알림 버튼 | 클릭: 알림 팝업 표시 | +| 04 | 개인 정보 버튼 | 항목: 디폴트 이미지, 이름, 직급. 클릭: 마이페이지 팝업 표시 | +| 05 | 회사 로고 | 회사정보 화면에서 등록한 로고 표시. 회사 변경 선택 시 해당 로고 변경 | +| 06 | 메뉴 영역 | 메뉴 클릭: 하위 메뉴 있을 경우 하단에 표시, 없을 경우 해당 메뉴 화면으로 이동. 목록 길 경우 해당 영역 내 스크롤 처리 | +| 07 | MES 메뉴 영역 | 영업관리, 판매관리, 구매관리 등 해당하는 MES 메뉴 영역 표시 | +| 08 | 푸터 영역 | 모든 화면 하단 공통 표시 | +| 09 | SAM AI 채팅 버튼 | 클릭: SAM AI 채팅 팝업 표시 | + +### 6.2 메뉴 목록 + +- 대시보드 +- MES 메뉴 +- 인사관리 +- 전자결재 +- 게시판 +- 회계관리 +- 기준정보 +- 보고서 및 분석 +- 계정정보 +- 회사정보 +- 구독관리 +- 결제내역 +- 고객센터 + +### 6.3 푸터 내용 + +``` +(C) 2025 SAM. All right reserved. +Codebridge X +상호: 코드 브릿지 엑스 대표: 이경호 사업자등록번호: 123-45-12345 +주소: 서울특별시 강서구 양천로 583 우림블루나인 B동 1602호 (우: 07547) +팩스: 02-123-1234 통신판매업신고번호: 제 2019-서울강서-0001호 +서비스이용문의: 02-1234-1234 이메일: cs@a.com +서비스 이용약관 | 개인정보 취급방침 +``` + +--- + +## 7. 메뉴, 페이지, 섹션, 항목 영역 + +> **페이지**: 9 + +### 7.1 영역 구분 + +| 번호 | 영역 | 설명 | +|------|------|------| +| 01 | 메뉴 영역 | 축소 상태 | +| 02 | 페이지 영역 | - | +| 03 | 섹션 영역 | - | +| 04 | 항목 영역 | - | + +### 7.2 텍스트 오버플로우 처리 + +텍스트가 영역보다 길 경우 "텍스트+..." 형태로 표시한다. + +--- + +## 8. 메뉴 목록 (3Depth) + +> **페이지**: 10 + +### 8.1 메뉴 계층 구조 + +| 번호 | 레벨 | 설명 | +|------|------|------| +| 01 | 대메뉴 | 1Depth 메뉴 | +| 02 | 중메뉴 | 2Depth 메뉴 (대메뉴 클릭 시 하단에 표시) | +| 03 | 소메뉴 | 3Depth 메뉴 (중메뉴 클릭 시 하단에 표시) | + +**메뉴 확장 예시**: +``` +대시보드 +MES 메뉴 +인사관리 +전자결재 + - 중메뉴명 + - 중메뉴명 + · 소메뉴명 + · 소메뉴명 + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +게시판 +... +``` + +--- + +## 9. 알림 팝업 + +> **페이지**: 11 +> **경로**: 메인 > 알림 팝업 + +### 9.1 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 알림 목록 | 각 디폴트 썸네일, 종류(공지사항, 안내), 제목/내용, 전송일시 표시. 클릭: 해당 상세 화면으로 이동. 최신순 10개까지 표시 | +| 02 | New 아이콘 | 새 알림일 경우 New 아이콘 표시. 해당 알림 클릭 시 사라짐 | +| 02-1 | 붉은 점 아이콘 | 새 알림이 있을 경우 표시. 해당 알림 모두 클릭 시 사라짐 | + +--- + +## 10. 마이페이지 팝업 + +> **페이지**: 12 +> **경로**: 메인 > 마이페이지 팝업 + +### 10.1 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 계정 아이디 (이메일) | 이메일 주소 표시 (예: `name@company.com`) | +| 02 | 회사 셀렉트 박스 | 종류: 회사명 목록 (해당 계정이 생성한 회사(테넌트) 목록 표시). 정렬: 등록순. 한 회사만 소유중일 경우에는 해당 영역 숨김 | +| 03 | 로그아웃 버튼 | 클릭: "정말 로그아웃하시겠습니까?" 로그아웃 확인 Alert 표시. 확인 버튼 클릭시 로그아웃 처리 | + +--- + +## 11. 셀렉트 박스 + +> **페이지**: 13 + +### 11.1 기본 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 셀렉트 박스 | 클릭: 하단에 종류 목록 표시 | +| 02 | 종류 목록 | 목록 중 하나만 선택 가능 | + +### 11.2 다중 선택 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 03 | 다중 선택 셀렉트 박스 | 선택된 첫번째 항목명 + 추가 수 표시. 텍스트 영역 부족할 경우 `항목..+3` 형태로 표시 | +| 04 | 다중 선택 종류 목록 | 목록 중 복수 선택 가능. 전체 선택 시 전체 선택/해제 토글 | + +### 11.3 검색 셀렉트 박스 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 05 | 검색 영역 | 검색어 입력 후 엔터 또는 검색 아이콘 클릭 시 검색 상태로 전환되며 검색 결과 표시 | +| 05-3 | 삭제 버튼 | 클릭: 검색어 삭제 처리, 전체 종류 목록 표시 | + +### 11.4 셀렉트 박스 유형 정리 + +| 유형 | 단일 선택 | 다중 선택 | 검색 | 검색+다중 선택 | +|------|----------|----------|------|--------------| +| 선택 방식 | 하나만 | 복수 | 하나만 | 복수 | +| 검색 기능 | X | X | O | O | +| 전체 선택 | X | O | X | O | + +--- + +## 12. 가이드 메시지 + +> **페이지**: 14 + +상황에 따라 입력 필드 하단 또는 Alert에 가이드 메시지를 표시한다. + +### 12.1 표시 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 가이드 메시지 표시 위치 | 입력 필드 하단에 표시 | +| - | 긍정 메시지 | 녹색으로 표시 | +| 01-1 | 부정 메시지 | 붉은색으로 표시 | + +--- + +## 13. 태블릿/모바일 헤더 + +> **페이지**: 15 + +### 13.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태블릿/모바일 헤더 | 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | + +### 13.2 적용 화면 + +- TABLET 가로 목록 +- TABLET 세로 목록 +- MOBILE 가로 목록 +- MOBILE 세로 목록 + +--- + +## 14. 태블릿/모바일 바텀 버튼 영역 + +> **페이지**: 16 + +### 14.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태블릿/모바일 바텀 버튼 영역 | 최하단 바텀에 플로팅 표시. 하단으로 스크롤 시 숨김. 역스크롤 시 표시 | + +### 14.2 버튼 예시 + +- `[수정]` `[삭제]` + +--- + +## 15. 공지 팝업 + +> **페이지**: 17 + +### 15.1 구성 + +| 항목 | 설명 | +|------|------| +| 대상 | 전체, 설정 부서 | +| 내용 | 설정 기간동안 대상에게 팝업 표시 | + +### 15.2 구성 요소 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 팝업 내용 영역 | 이미지, 텍스트 | +| 02 | 1일간 이 창을 열지 않음 체크박스 | 클릭: 체크 설정/해제 토글. 디폴트: 체크 해제 상태. 체크 설정 시 1일 동안 팝업 미표시 (자정 기준) | + +--- + +## 16. 목록 화면 - 4단계 반응형 + +> **페이지**: 18~25 + +PC, TABLET, MOBILE 환경에서 목록 화면의 4단계 반응형 표시를 정의한다. + +### 16.1 반응형 단계 개요 + +| 단계 | 디바이스 | 화면명 | +|------|---------|--------| +| 1단계 | PC | PC_목록 | +| 2단계 | TABLET 가로 | TABLET_가로_목록 | +| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_목록, MOBILE_가로_목록 | +| 4단계 | MOBILE 세로 | MOBILE_세로_목록 | + +### 16.2 PC_목록 (1단계) + +> **페이지**: 19 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 헤더 영역 | 항목 클릭: 값이 국문/영문/숫자일 경우 오름/내림차순으로 토글 | +| 02 | 정렬 아이콘 | 현재 칼럼으로 정렬 상태일 경우에만 표시 | + +**목록 테이블 예시 칼럼**: + +| 칼럼 | 설명 | +|------|------| +| 시공번호 | 고유 식별 번호 | +| 거래처 | 회사명 | +| 현장명 | 현장 이름 | +| 공사PM | 담당 PM | +| 작업반장 | 작업반장 이름 | +| 작업자 | 작업자 수 | +| 시공투입일 | 시공 투입 날짜 | +| 시공완료일 | 시공 완료 날짜 | +| 상태 | 시공대기, 시공진행, 시공완료 | + +### 16.3 TABLET_가로_목록 (2단계) + +> **페이지**: 20 + +- PC와 동일한 테이블 구조 +- 사이드 메뉴가 아이콘 축소 상태로 변경 + +### 16.4 TABLET_세로_목록 (3단계) + +> **페이지**: 21~22 + +- 테이블 대신 카드형 목록으로 전환 +- 각 카드에 시공번호와 상태 표시 +- 카드 클릭 시 확장되어 상세 정보 표시 + +**확장 시 표시 항목**: + +| 필드 | 예시 값 | +|------|---------| +| 거래처 | 회사명 | +| 현장명 | 현장명 | +| 공사PM | 홍길동 | +| 작업반장 | 홍길동 | +| 작업자 | 3 | +| 시공투입일 | 2026-01-01 | +| 시공완료일 | 2026-01-01 | + +### 16.5 MOBILE_가로_목록 (3단계) + +> **페이지**: 23~24 + +- 카드형 목록 +- 시공번호와 상태 표시 +- 클릭 시 확장하여 상세 항목 표시 + +### 16.6 MOBILE_세로_목록 (4단계) + +> **페이지**: 25 + +- 카드형 목록 (세로 스크롤) +- 클릭 시 확장하여 상세 항목 표시 +- 확장 시 거래처, 현장명, 공사PM, 작업반장, 작업자, 시공투입일, 시공완료일 표시 + +--- + +## 17. 상세 화면 - 4단계 반응형 + +> **페이지**: 26~31 + +PC, TABLET, MOBILE 환경에서 상세 화면의 4단계 반응형 표시를 정의한다. + +### 17.1 반응형 단계 개요 + +| 단계 | 디바이스 | 화면명 | +|------|---------|--------| +| 1단계 | PC | PC_상세 | +| 2단계 | TABLET 가로 | TABLET_가로_상세 | +| 3단계 | TABLET 세로 / MOBILE 가로 | TABLET_세로_상세, MOBILE_가로_상세 | +| 4단계 | MOBILE 세로 | MOBILE_세로_상세 | + +### 17.2 PC_상세 (1단계) + +> **페이지**: 27 + +- 페이지 제목: "메뉴 상세" + 설명: "메뉴 상세를 관리합니다" +- 섹션명: "시공 정보" +- 버튼: `[수정]` `[삭제]` + +**표시 항목 예시**: + +| 필드 | 예시 값 | +|------|---------| +| 시공번호 | 123123 | +| 상태 | 시공진행 | +| 현장 | 현장명 | +| 작업반장 | 홍길동 (셀렉트 박스) | +| 시공투입일 | 2025-12-15 | +| 시공완료일 | 2025-12-15 | +| 항목명 | 항목 (다수) | + +### 17.3 TABLET_가로_상세 (2단계) + +> **페이지**: 28 + +- PC와 동일한 상세 정보 표시 +- 사이드 메뉴 아이콘 축소 상태 + +### 17.4 TABLET_세로_상세 (3단계) + +> **페이지**: 29 + +- 항목 수가 줄어들며 스크롤로 나머지 확인 + +### 17.5 MOBILE_가로_상세 (3단계) + +> **페이지**: 30 + +- 상세 항목을 세로 배치 +- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 + +### 17.6 MOBILE_세로_상세 (4단계) + +> **페이지**: 31 + +- 모든 항목 세로 배치 +- 바텀에 `[수정]` `[삭제]` 버튼 플로팅 + +--- + +## 18. PC 섹션 정리 + +> **페이지**: 32~33 + +PC 화면의 섹션 레이아웃 및 필터/정렬 구성을 정의한다. + +### 18.1 필터 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 라디오 버튼형 필터 | 선택 값이 2개일 경우 사용 (예: 수취/발행) | +| 02 | 필터 셀렉트 박스 | 최소로만 활용 | +| 03 | 표 헤더 정렬 | 표 헤더 정렬로 정렬 셀렉트 박스는 삭제 | + +### 18.2 PC 섹션 구성 요소 + +**상단 영역**: +- 페이지 제목 + 설명 +- 집계 카드 (예: `수취어음 55건`, `발행어음 1건`, `만기임박 5건`, `결제완료 15건`) +- 기간 선택 (날짜 범위 + 단축 버튼: 전전월, 어제, 오늘, 전월, 당월, 당해년도) +- 버튼: `[버튼명]` `[버튼명]` `[버튼명]` +- 탭: 탭1, 탭2, 탭3 + +**필터 영역**: +- 셀렉트 박스 필터 (전체) +- 라디오 버튼형 필터 (수취/발행) +- 상태 셀렉트 박스 (보관중) +- `[저장]` 버튼 + +**목록 테이블 예시**: + +| No. | 어음번호 | 구분 | 거래처 | 금액 | 발행일 | 만기일 | 차수 | 상태 | +|-----|---------|------|--------|------|--------|--------|------|------| +| 7 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 1 | 보관중 | +| 6 | 123123 | 수취 | 회사명 | 1,000,000 | 2025-12-12 | 2025-12-12 | 2 | 만기임박 | + +**하단 정보**: `총 7건` / `1건 선택` + +--- + +## 19. TBD (미정) + +> **페이지**: 34 + +추후 결정 예정 영역이다. + +--- + +## 20. 나의 메뉴 + +> **페이지**: 35~38 + +### 20.1 나의 메뉴 - 없음 + +> **페이지**: 35 + +- 나의 메뉴가 설정되지 않은 상태 +- 콘텐츠 상단에 `[...]` 아이콘만 표시 + +### 20.2 나의 메뉴 - 있음 + +> **페이지**: 36 + +- 나의 메뉴가 1개 설정된 상태 +- 콘텐츠 상단에 나의 메뉴명 탭 표시 (예: `메뉴관리`) + +### 20.3 나의 메뉴 - 여러 줄 + +> **페이지**: 37 + +- 나의 메뉴가 여러 개 설정된 상태 +- 콘텐츠 상단에 여러 메뉴명이 나열됨 +- 줄바꿈되어 여러 줄로 표시 가능 (예: `메뉴관리 메뉴명 메뉴명 메뉴명 ...`) + +### 20.4 나의 메뉴 - 메뉴 영역에 통합 + +> **페이지**: 38 + +- 좌측 메뉴 영역에 "메뉴" / "나의 메뉴" 탭으로 통합 +- 메뉴 탭: 일반 메뉴 목록 표시 +- 나의 메뉴 탭: 사용자 즐겨찾기 메뉴 표시 + +--- + +## 21. 검색, 필터, 정렬 모음 + +> **페이지**: 39 + +### 21.1 구성 요소 + +| 영역 | 구성 | +|------|------| +| 기간 선택 | 날짜 범위 (`2025-09-01 ~ 2025-09-03`) + 단축 버튼 (전전월, 어제, 오늘, 전월, 당월, 당해년도) | +| 검색바 | 검색 입력 필드 | +| 필터 셀렉트 박스 | 복수의 전체 셀렉트 박스 | +| 정렬 | 최신순 셀렉트 박스 | +| 항목 필터 | 항목명 태그 형태로 나열 | + +--- + +## 22. 페이지 설정 버튼 + +> **페이지**: 40 + +### 22.1 기능 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 섹션 표시 및 순서 변경 | 페이지 내 섹션 ON/OFF 토글 및 순서 변경 | +| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | + +### 22.2 설정 패널 구성 + +- 버전기록 +- 가져오기 +- 내보내기 +- 섹션 목록: 각 섹션별 ON/OFF 토글 + +**예시**: +``` +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +섹션명 [ON] +``` + +--- + +## 23. 섹션 설정 버튼 + +> **페이지**: 41 + +### 23.1 기능 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 항목 표시 및 순서 변경 | 섹션 내 항목 ON/OFF 토글 및 순서 변경 | +| 02 | 일반 설정 | 일반 설정 > 페이지/섹션 설정 > 공통 요소 모두 제어 | + +### 23.2 설정 패널 구성 + +- 가져오기 +- 내보내기 +- 항목 목록: 각 항목별 ON/OFF 토글 + +**예시**: +``` +기간 [ON] +기간단축버튼 [ON] +검색바 [ON] +필터명 [ON] +필터명 [ON] +필터명 [ON] +필터명 [ON] +정렬 [ON] +``` + +--- + +## 24. 태스크 알림 아이콘 + +> **페이지**: 42~43 + +### 24.1 동작 규칙 + +| 번호 | 항목 | 설명 | +|------|------|------| +| 01 | 태스크 알림 아이콘 | 태스크가 추가될 경우 카운트하여 표시 | +| - | 메뉴 확장 시 표시 | 대/중/소메뉴로 확장될 경우 해당 메뉴에 아이콘 표시 | +| - | 카운트 범위 | 최소 1 ~ 최대 99 | + +### 24.2 표시 예시 + +**축소 상태**: 대메뉴 옆에 카운트 배지 표시 (예: `전자결재 [3]`) + +**확장 상태 (2Depth)**: +``` +전자결재 [3] + - 중메뉴명 [2] + - 중메뉴명 [1] + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +``` + +**확장 상태 (3Depth)**: +``` +전자결재 [3] + - 중메뉴명 + - 중메뉴명 [1] + · 소메뉴명 [1] + · 소메뉴명 + - 중메뉴명 + - 중메뉴명 + - 중메뉴명 +``` + +### 24.3 페이지 내 표시 + +- 메뉴 축소 상태에서도 대메뉴 아이콘 옆에 카운트 배지 표시 + +--- + +## 부록: 페이지 맵 + +| 페이지 | 섹션 | 화면명 | +|--------|------|--------| +| 1 | 표지 | SAM_General Rule | +| 2 | 문서 이력 | Document History | +| 3 | 공통 | - | +| 4 | 인터랙션 | Interaction | +| 5 | 반응형 웹 | Responsive Web | +| 6 | 화면 템플릿 | Screen Template | +| 7 | 메시지 | Notifications | +| 8 | GNB, LNB, 푸터 | GNB, LNB, 푸터 | +| 9 | 영역 구분 | 메뉴, 페이지, 섹션, 항목 영역 | +| 10 | 메뉴 목록 | 메뉴 목록 3Depth | +| 11 | 알림 팝업 | 알림 팝업 | +| 12 | 마이페이지 | 마이페이지 팝업 | +| 13 | 셀렉트 박스 | 셀렉트 박스 (기본/다중/검색) | +| 14 | 가이드 메시지 | 가이드 메시지 | +| 15 | 태블릿/모바일 헤더 | 태블릿/모바일 헤더 | +| 16 | 태블릿/모바일 바텀 버튼 | 태블릿/모바일 바텀 버튼 영역 | +| 17 | 공지 팝업 | 공지 팝업 | +| 18 | (구분) | PC, TABLET, MOBILE - 목록 4단계 | +| 19 | 목록 1단계 | PC_목록 | +| 20 | 목록 2단계 | TABLET_가로_목록 | +| 21 | 목록 3단계 | TABLET_세로_목록 | +| 22 | 목록 3단계 확장 | TABLET_세로_목록_확장 | +| 23 | 목록 3단계 | MOBILE_가로_목록 | +| 24 | 목록 3단계 확장 | MOBILE_가로_목록_확장 | +| 25 | 목록 4단계 | MOBILE_세로_목록, MOBILE_세로_목록_확장 | +| 26 | (구분) | PC, TABLET, MOBILE - 상세 4단계 | +| 27 | 상세 1단계 | PC_상세 | +| 28 | 상세 2단계 | TABLET_가로_상세 | +| 29 | 상세 3단계 | TABLET_세로_상세 | +| 30 | 상세 3단계 | MOBILE_가로_상세 | +| 31 | 상세 4단계 | MOBILE_세로_상세 | +| 32 | (구분) | 섹션 정리 | +| 33 | 섹션 정리 | PC 섹션 정리 | +| 34 | TBD | 미정 | +| 35 | 나의 메뉴 | 나의 메뉴_없음 | +| 36 | 나의 메뉴 | 나의 메뉴_있음 | +| 37 | 나의 메뉴 | 나의 메뉴_여러 줄 | +| 38 | 나의 메뉴 | 나의 메뉴_메뉴 영역에 통합 | +| 39 | 검색/필터/정렬 | 검색, 필터, 정렬 모음 | +| 40 | 페이지 설정 | 페이지 설정 버튼 | +| 41 | 섹션 설정 | 섹션 설정 버튼 | +| 42~43 | 태스크 알림 | 태스크 알림 아이콘 | + +--- + +## 관련 문서 + +- [SAM ERP 회계관리 스토리보드 D1.6](SAM_ERP_회계관리_Storyboard_D1.6.md) +- 원본 PDF: `SAM_General_Rule_Storyboard_D1.0_260116.pdf` + +--- + +**최종 업데이트**: 2026-02-23 diff --git a/plans/ai-quotation-engine-plan.md b/plans/ai-quotation-engine-plan.md new file mode 100644 index 0000000..c1b5098 --- /dev/null +++ b/plans/ai-quotation-engine-plan.md @@ -0,0 +1,928 @@ +# AI 견적서 자동생성 엔진 개발 계획 + +> **작성일**: 2026-03-02 +> **상태**: 기획 초안 +> **프로젝트**: SAM API + MNG +> **우선순위**: 🔴 필수 +> **참조**: `docs/features/ai/README.md`, `docs/features/quotes/README.md`, `docs/rules/customer-pricing.md` + +--- + +## 1. 개요 + +### 1.1 목적 + +SAM 계약 완료 후 매니저가 고객사 직원과 인터뷰를 진행할 때, **인터뷰 내용을 AI가 분석하여 SAM 표준 견적서 형태로 자동 변환**하는 엔진을 구축한다. + +현재 매니저가 수동으로 수행하는 프로세스: + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ SAM 계약 │ → │ 현장 인터뷰 │ → │ 업무 파악 │ → │ 견적서 작성 │ +│ 완료 │ │ (매니저+직원)│ │ (수동 정리) │ │ (수동 작성) │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +AI 엔진 도입 후: + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ SAM 계약 │ → │ 현장 인터뷰 │ → │ AI 엔진 │ → │ 견적서 초안 │ +│ 완료 │ │ (매니저+직원)│ │ (음성/텍스트 분석) │ │ (자동 생성) │ +│ │ │ │ │ (업무 매핑) │ │ (매니저 확인)│ +└──────────────┘ └──────────────┘ └──────────────────────┘ └──────────────┘ +``` + +### 1.2 핵심 원칙 + +| 원칙 | 설명 | +|------|------| +| **자체 AI 엔진** | Claude API 기반으로 SAM 전용 AI 서비스 구축 | +| **기존 인프라 활용** | `ai_configs`, `ai_token_usages`, `ai_pricing_configs` 테이블 재사용 | +| **견적 시스템 연동** | 기존 `quotes`, `quote_items` 테이블과 직접 연동 | +| **Multi-tenant** | 모든 데이터에 `tenant_id` 격리 적용 | +| **점진적 확장** | Phase 1 텍스트 → Phase 2 음성 (STT 구현 완료, 통합만 필요) → Phase 3 학습 고도화 | + +### 1.3 기존 인프라 현황 + +#### AI 인프라 (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `ai_configs` 테이블 | ✅ 완료 | Claude provider 설정 가능 | +| `ai_token_usages` 테이블 | ✅ 완료 | 토큰/비용 자동 추적 | +| `ai_pricing_configs` 테이블 | ✅ 완료 | Claude 모델 단가 등록 | +| `AiTokenHelper` | ✅ 완료 | `saveClaudeUsage()` 메서드 존재 | +| MNG AI 설정 UI | ✅ 완료 | `/system/ai-config` | + +#### 견적 시스템 (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `quotes` 테이블 | ✅ 완료 | 견적 마스터 | +| `quote_items` 테이블 | ✅ 완료 | 견적 품목 상세 | +| `QuoteService` | ✅ 완료 | 견적 CRUD, 상태 관리 | +| `QuoteCalculationService` | ✅ 완료 | BOM 10단계 계산 | +| 견적 API 엔드포인트 | ✅ 완료 | REST API 전체 | + +#### 음성 녹음/STT (구축 완료) + +| 구성요소 | 상태 | 비고 | +|---------|------|------| +| `ai_voice_recordings` 테이블 | ✅ 완료 | DB 스키마 + CRUD | +| GCS 업로드 | ✅ 완료 | `GoogleCloudService` — GCS 저장/조회/삭제 | +| Google Cloud STT 변환 | ✅ 완료 | `GoogleCloudService::speechToText()` — LongRunningRecognize, ko-KR | +| Web Speech API (브라우저 STT) | ✅ 완료 | `voice-recorder.blade.php` — 실시간 음성→텍스트 (무료) | +| STT + Gemini AI 분석 | ✅ 완료 | `AiVoiceRecordingService` — 음성→STT→AI 분석 파이프라인 | +| 화자 분리 (Diarization) | ✅ 완료 | `MeetingMinuteService` — Speaker Diarization | +| 영업 상담 음성 녹음 | ✅ 완료 | `ConsultationController` — MediaRecorder + STT + GCS 백업 | + +> **참조 구현 파일:** +> - `mng/app/Services/GoogleCloudService.php` — Google Cloud STT/GCS 통합 서비스 +> - `mng/app/Services/AiVoiceRecordingService.php` — STT + Gemini 분석 +> - `mng/app/Services/MeetingMinuteService.php` — 회의록 STT + 화자분리 +> - `mng/app/Http/Controllers/Sales/ConsultationController.php` — 영업 상담 음성 +> - `mng/resources/views/sales/modals/voice-recorder.blade.php` — 브라우저 음성 녹음 UI +> - `docs/features/voice-input-stt-guide.md` — STT 기술 가이드 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 기획 초안 작성 | +| **다음 작업** | Phase 1 상세 설계 → 구현 | +| **진행률** | 0/4 Phase (0%) | +| **마지막 업데이트** | 2026-03-02 | + +--- + +## 2. 시스템 아키텍처 + +### 2.1 전체 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SAM AI 견적 엔진 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────┐ ┌───────────────────────┐ │ +│ │ 입력 채널 │ │ AI 분석 파이프라인 │ │ +│ │ │ │ │ │ +│ │ 📝 텍스트 │───→│ 1. 인터뷰 전처리 │ │ +│ │ 🎤 음성(P2) │ │ 2. 업무 도메인 분류 │ │ +│ │ 📄 문서(P3) │ │ 3. SAM 모듈 매핑 │ │ +│ └───────────────┘ │ 4. 견적 항목 추출 │ │ +│ │ 5. 금액 산출 │ │ +│ └──────────┬────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ 견적서 생성기 │ │ +│ │ │ │ +│ │ SAM 표준 모듈 카탈로그 ←──→ Claude API │ │ +│ │ 고객 요금 정책 ←──→ 프롬프트 엔진 │ │ +│ │ 기존 견적 템플릿 ←──→ 결과 파서 │ │ +│ └──────────────────────┬────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ 출력 │ │ +│ │ │ │ +│ │ 📊 견적서 초안 (quotes 테이블) │ │ +│ │ 📋 업무 분석 리포트 │ │ +│ │ 💡 추천 모듈 목록 │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Claude API 연동 구조 + +``` +┌──────────────────┐ ┌──────────────────┐ +│ SAM API Server │ │ Claude API │ +│ (Laravel) │ │ (Anthropic) │ +│ │ │ │ +│ AiQuotation │ ──HTTP──→ Messages API │ +│ Service │ ←─JSON── (claude-sonnet) │ +│ │ │ │ +│ ┌────────────┐ │ │ System Prompt: │ +│ │ Prompt │ │ │ - SAM 모듈 목록 │ +│ │ Engine │ │ │ - 요금 정책 │ +│ │ │ │ │ - 견적 구조 │ +│ │ - 모듈목록 │ │ │ - 출력 형식 │ +│ │ - 요금표 │ │ │ │ +│ │ - 템플릿 │ │ │ │ +│ └────────────┘ │ │ │ +└──────────────────┘ └──────────────────┘ +``` + +### 2.3 데이터 흐름 + +``` +매니저 인터뷰 입력 + │ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 1: 인터뷰 전처리 │ +│ - 텍스트 정규화 (불필요한 표현 제거) │ +│ - 핵심 키워드 추출 │ +│ - 업무 도메인 태깅 │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 2: Claude API 1차 호출 — 업무 분석 │ +│ - 고객사 업종/규모 파악 │ +│ - 현재 업무 프로세스 분석 │ +│ - 디지털화 필요 영역 식별 │ +│ - Pain Point 도출 │ +│ 출력: 구조화된 업무 분석 JSON │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 3: SAM 모듈 매핑 │ +│ - 업무 분석 결과 ↔ SAM 모듈 카탈로그 대조 │ +│ - 필수/선택 모듈 분류 │ +│ - 사용자 수, 데이터량 추정 │ +│ 출력: 추천 모듈 목록 + 근거 │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 4: Claude API 2차 호출 — 견적 생성 │ +│ - SAM 요금 정책 적용 │ +│ - 모듈별 개발비 + 월 구독료 계산 │ +│ - 추가 옵션 (AI 토큰, 저장공간) 산출 │ +│ - 할인 정책 적용 (통합 패키지 등) │ +│ 출력: 견적서 JSON (SAM 표준 형식) │ +└──────────────────────┬───────────────────────────┘ + ▼ +┌──────────────────────────────────────────────────┐ +│ Step 5: 견적서 초안 저장 │ +│ - quotes 테이블에 저장 (status: draft) │ +│ - quote_items에 모듈별 항목 저장 │ +│ - 업무 분석 리포트 첨부 │ +│ - 매니저에게 알림 → 검토/수정 → 확정 │ +└──────────────────────────────────────────────────┘ +``` + +--- + +## 3. SAM 모듈 카탈로그 (AI 프롬프트용) + +> **참조**: `docs/rules/customer-pricing.md` + +AI가 인터뷰 내용을 SAM 견적으로 변환하려면, SAM이 제공하는 모듈과 요금을 정확히 알아야 한다. 이 데이터는 **DB 테이블로 관리**하여 프롬프트에 동적 주입한다. + +### 3.1 모듈 카탈로그 테이블 설계 + +```sql +CREATE TABLE ai_quotation_modules ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + module_code VARCHAR(50) NOT NULL, -- 'HR', 'SALES', 'FINANCE' 등 + module_name VARCHAR(100) NOT NULL, -- '인사관리', '영업관리', '재무관리' + category ENUM('basic', 'individual', 'addon') NOT NULL, + description TEXT, -- 모듈 기능 설명 + keywords JSON, -- AI 매핑용 키워드 목록 + dev_cost DECIMAL(12,0) DEFAULT 0, -- 개발비 (원) + monthly_fee DECIMAL(10,0) DEFAULT 0, -- 월 구독료 (원) + is_active BOOLEAN DEFAULT TRUE, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_tenant (tenant_id), + INDEX idx_category (category), + UNIQUE KEY uk_tenant_module (tenant_id, module_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 3.2 초기 데이터 (customer-pricing.md 기준) + +| module_code | module_name | category | dev_cost | monthly_fee | +|-------------|-------------|----------|----------|-------------| +| `BASIC_PKG` | 기본 패키지 (인사+근태+급여+게시판) | basic | 5,000,000 | 200,000 | +| `HR` | 인사관리 | individual | 2,000,000 | 80,000 | +| `ATTENDANCE` | 근태관리 | individual | 1,500,000 | 60,000 | +| `PAYROLL` | 급여관리 | individual | 2,500,000 | 100,000 | +| `BOARD` | 게시판/공지사항 | individual | 500,000 | 20,000 | +| `SALES` | 영업관리 (CRM+견적+수주) | individual | 5,000,000 | 150,000 | +| `PURCHASE` | 구매/자재관리 | individual | 3,000,000 | 100,000 | +| `PRODUCTION` | 생산관리 (MES) | individual | 8,000,000 | 250,000 | +| `QUALITY` | 품질관리 | individual | 4,000,000 | 120,000 | +| `FINANCE` | 재무/회계관리 | individual | 5,000,000 | 150,000 | +| `LOGISTICS` | 물류/출하관리 | individual | 3,000,000 | 100,000 | +| `APPROVAL` | 전자결재 | individual | 3,000,000 | 80,000 | +| `DOCUMENT` | 문서관리 (전자서명) | individual | 2,000,000 | 60,000 | +| `EQUIPMENT` | 설비관리 | individual | 3,000,000 | 100,000 | +| `INTEGRATED` | 통합 패키지 | basic | 30,000,000 | 800,000 | +| `AI_TOKEN` | AI 토큰 추가 | addon | 0 | 별도 | +| `STORAGE` | 파일 저장공간 추가 | addon | 0 | 별도 | +| `CUSTOM_DEV` | 커스텀 개발 | addon | 별도 협의 | 0 | + +### 3.3 keywords 필드 예시 + +```json +// HR 모듈 +{ + "keywords": ["직원", "사원", "인사", "조직도", "부서", "입퇴사", "인력"], + "pain_points": ["엑셀로 직원 관리", "입퇴사 관리가 번거로움", "조직도 없음"], + "business_needs": ["직원 정보 통합", "조직 구조 관리", "인력 현황 파악"] +} + +// PRODUCTION 모듈 +{ + "keywords": ["생산", "제조", "작업지시", "공정", "LOT", "불량", "MES"], + "pain_points": ["생산 현황을 수기로 기록", "불량 추적 불가", "납기 관리 어려움"], + "business_needs": ["실시간 생산현황", "불량률 관리", "작업지시 자동화"] +} +``` + +--- + +## 4. AI 프롬프트 엔진 + +### 4.1 시스템 프롬프트 구조 + +``` +┌─────────────────────────────────────────────────────┐ +│ System Prompt │ +├─────────────────────────────────────────────────────┤ +│ │ +│ [역할 정의] │ +│ 너는 SAM ERP/MES 솔루션의 컨설팅 AI이다. │ +│ 고객 인터뷰를 분석하여 맞춤형 견적서를 작성한다. │ +│ │ +│ [SAM 모듈 카탈로그] ← DB에서 동적 로드 │ +│ 각 모듈의 기능, 키워드, 가격 정보 │ +│ │ +│ [요금 정책] ← customer-pricing 기반 │ +│ 기본패키지, 개별모듈, 추가옵션 요금 체계 │ +│ 할인 정책 (통합 패키지 할인 등) │ +│ │ +│ [출력 형식] │ +│ JSON Schema 명시 (견적서 구조) │ +│ │ +│ [분석 지침] │ +│ - 고객 업종/규모별 권장 모듈 기준 │ +│ - 우선순위 결정 기준 (필수 vs 선택) │ +│ - 비용 최적화 원칙 (패키지 vs 개별) │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +### 4.2 프롬프트 템플릿 (1차: 업무 분석) + +``` +당신은 SAM(Smart Automation Management) ERP/MES 솔루션의 전문 컨설턴트입니다. + +아래는 고객사 직원과의 인터뷰 내용입니다. 이를 분석하여 구조화된 업무 분석 보고서를 작성하세요. + +## 고객 정보 +- 회사명: {company_name} +- 업종: {industry} +- 직원 수: {employee_count} +- 인터뷰 대상: {interviewee_role} + +## 인터뷰 내용 +{interview_content} + +## 분석 기준 +다음 SAM 모듈 영역에 맞춰 분석하세요: +{module_catalog_json} + +## 출력 형식 (JSON) +{ + "company_analysis": { + "industry": "업종 분류", + "scale": "소규모/중소/중견", + "current_systems": ["현재 사용 중인 시스템"], + "digitalization_level": "상/중/하" + }, + "business_domains": [ + { + "domain": "인사/급여", + "current_process": "현재 처리 방식 설명", + "pain_points": ["문제점 1", "문제점 2"], + "improvement_needs": ["개선 필요사항"], + "priority": "필수/높음/보통/낮음", + "matched_modules": ["HR", "PAYROLL"] + } + ], + "recommendations": { + "essential_modules": ["반드시 필요한 모듈 코드"], + "recommended_modules": ["권장 모듈 코드"], + "optional_modules": ["선택 모듈 코드"], + "package_suggestion": "BASIC_PKG 또는 INTEGRATED 또는 individual", + "reasoning": "패키지 추천 근거" + } +} +``` + +### 4.3 프롬프트 템플릿 (2차: 견적 생성) + +``` +아래 업무 분석 결과를 바탕으로 SAM 견적서를 생성하세요. + +## 업무 분석 결과 +{analysis_result_json} + +## SAM 요금 정책 +{pricing_policy_json} + +## 견적 생성 규칙 +1. 필수 모듈은 반드시 포함 +2. 통합 패키지가 개별 합산보다 저렴하면 패키지 추천 +3. 직원 수 기반 사용자 라이선스 산출 +4. AI 토큰은 월 기본 100만 토큰 포함, 초과분 별도 +5. 파일 저장공간은 기본 10GB, 초과분 별도 + +## 출력 형식 (JSON) +{ + "quotation": { + "title": "견적서 제목", + "client_name": "고객사명", + "valid_until": "견적 유효기간", + "items": [ + { + "category": "기본서비스/추가모듈/추가옵션", + "module_code": "모듈코드", + "module_name": "모듈명", + "description": "포함 기능 설명", + "dev_cost": 0, + "monthly_fee": 0, + "quantity": 1, + "note": "비고" + } + ], + "summary": { + "total_dev_cost": 0, + "total_monthly_fee": 0, + "discount_type": "패키지할인/볼륨할인/없음", + "discount_rate": 0, + "final_dev_cost": 0, + "final_monthly_fee": 0 + }, + "implementation_plan": { + "estimated_months": 0, + "phases": [ + { + "phase": 1, + "name": "단계명", + "modules": ["모듈코드"], + "duration_weeks": 0 + } + ] + }, + "analysis_summary": "업무 분석 요약 (고객 설명용)" + } +} +``` + +--- + +## 5. API 설계 + +### 5.1 엔드포인트 + +| Method | Path | 설명 | +|--------|------|------| +| `POST` | `/api/v1/ai/quotation/analyze` | 인터뷰 분석 (1차) | +| `POST` | `/api/v1/ai/quotation/generate` | 견적서 생성 (2차) | +| `POST` | `/api/v1/ai/quotation/generate-full` | 분석+생성 통합 (원스텝) | +| `GET` | `/api/v1/ai/quotation/{id}` | AI 견적 상세 조회 | +| `GET` | `/api/v1/ai/quotation` | AI 견적 목록 | +| `PUT` | `/api/v1/ai/quotation/{id}` | AI 견적 수정 (매니저) | +| `POST` | `/api/v1/ai/quotation/{id}/confirm` | 정식 견적으로 전환 | +| `DELETE` | `/api/v1/ai/quotation/{id}` | AI 견적 삭제 | + +### 5.2 요청/응답 예시 + +#### POST `/api/v1/ai/quotation/analyze` + +**Request:** +```json +{ + "client_id": 15, + "client_name": "(주)대한기계", + "industry": "기계제조업", + "employee_count": 45, + "interviewee_role": "관리부 팀장", + "interview_content": "현재 직원 관리는 엑셀로 하고 있어요. 출퇴근도 수기로 기록하고... 영업팀에서는 견적서를 한글 프로그램으로 만들어서 이메일로 보내는데, 이력 관리가 안 돼요. 생산 현장에서는 작업일보를 종이에 쓰고 있고, 불량이 나면 어디서 발생했는지 추적이 안 됩니다. 재고도 실사를 해봐야 알 수 있어요...", + "interview_type": "text" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": 1, + "analysis": { + "company_analysis": { + "industry": "기계제조업", + "scale": "중소기업 (45명)", + "current_systems": ["엑셀", "한글 프로그램", "종이 문서"], + "digitalization_level": "하" + }, + "business_domains": [ + { + "domain": "인사/급여", + "current_process": "엑셀로 직원 관리, 수기 출퇴근 기록", + "pain_points": ["인사정보 분산", "출퇴근 수기 기록"], + "priority": "필수", + "matched_modules": ["HR", "ATTENDANCE", "PAYROLL"] + }, + { + "domain": "영업관리", + "current_process": "한글 프로그램 견적서, 이메일 발송", + "pain_points": ["견적 이력 관리 불가", "영업 현황 파악 어려움"], + "priority": "높음", + "matched_modules": ["SALES"] + }, + { + "domain": "생산관리", + "current_process": "종이 작업일보, 수동 불량 관리", + "pain_points": ["불량 추적 불가", "생산현황 실시간 파악 불가"], + "priority": "필수", + "matched_modules": ["PRODUCTION", "QUALITY"] + }, + { + "domain": "재고/물류", + "current_process": "실사로만 재고 파악", + "pain_points": ["실시간 재고 파악 불가"], + "priority": "높음", + "matched_modules": ["PURCHASE", "LOGISTICS"] + } + ], + "recommendations": { + "essential_modules": ["HR", "ATTENDANCE", "PAYROLL", "PRODUCTION", "QUALITY"], + "recommended_modules": ["SALES", "PURCHASE", "LOGISTICS"], + "optional_modules": ["APPROVAL", "DOCUMENT"], + "package_suggestion": "INTEGRATED", + "reasoning": "8개 이상 모듈이 필요하므로 통합 패키지(30,000,000원)가 개별 합산(34,500,000원)보다 경제적" + } + }, + "token_usage": { + "prompt_tokens": 1250, + "completion_tokens": 890, + "cost_krw": 45 + } + } +} +``` + +### 5.3 컨트롤러 / 서비스 구조 + +``` +app/Http/Controllers/Api/V1/ +└── AiQuotationController.php + ├── analyze() ← 인터뷰 분석 + ├── generate() ← 견적 생성 + ├── generateFull() ← 통합 (분석+생성) + ├── index() ← 목록 + ├── show() ← 상세 + ├── update() ← 수정 + ├── confirm() ← 정식 견적 전환 + └── destroy() ← 삭제 + +app/Services/ +└── AiQuotationService.php + ├── analyzeInterview() ← 1차: 인터뷰 분석 + ├── generateQuotation() ← 2차: 견적 생성 + ├── generateFull() ← 통합 처리 + ├── confirmToQuote() ← quotes 테이블로 전환 + │ + ├── buildAnalysisPrompt() ← 분석 프롬프트 조립 + ├── buildQuotationPrompt() ← 견적 프롬프트 조립 + ├── loadModuleCatalog() ← DB에서 모듈 카탈로그 로드 + ├── loadPricingPolicy() ← DB에서 요금 정책 로드 + ├── callClaudeApi() ← Claude API 호출 + ├── parseResponse() ← 응답 JSON 파싱 + └── saveTokenUsage() ← 토큰 사용량 기록 + +app/Http/Requests/V1/AiQuotation/ +├── AiQuotationAnalyzeRequest.php +├── AiQuotationGenerateRequest.php +├── AiQuotationUpdateRequest.php +└── AiQuotationConfirmRequest.php +``` + +--- + +## 6. 데이터베이스 설계 + +### 6.1 신규 테이블 + +#### ai_quotations (AI 견적 마스터) + +```sql +CREATE TABLE ai_quotations ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + + -- 고객 정보 + client_id BIGINT UNSIGNED NULL, + client_name VARCHAR(200) NOT NULL, + industry VARCHAR(100) NULL, + employee_count INT NULL, + interviewee_role VARCHAR(100) NULL, + + -- 인터뷰 데이터 + interview_content TEXT NOT NULL, + interview_type ENUM('text', 'voice', 'document') DEFAULT 'text', + voice_recording_id BIGINT UNSIGNED NULL, -- ai_voice_recordings 참조 + + -- AI 분석 결과 + analysis_result JSON NULL, -- 1차 분석 결과 + quotation_result JSON NULL, -- 2차 견적 결과 + + -- 상태 관리 + status ENUM('analyzing', 'analyzed', 'generating', 'generated', + 'confirmed', 'failed') DEFAULT 'analyzing', + error_message TEXT NULL, + + -- 연결 + quote_id BIGINT UNSIGNED NULL, -- 정식 견적 전환 시 quotes.id + + -- 금액 요약 + total_dev_cost DECIMAL(12,0) DEFAULT 0, + total_monthly_fee DECIMAL(10,0) DEFAULT 0, + discount_rate DECIMAL(5,2) DEFAULT 0, + final_dev_cost DECIMAL(12,0) DEFAULT 0, + final_monthly_fee DECIMAL(10,0) DEFAULT 0, + + -- 토큰 사용 + total_tokens INT DEFAULT 0, + total_cost_krw DECIMAL(12,2) DEFAULT 0, + + -- 감사 + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + + INDEX idx_tenant_status (tenant_id, status), + INDEX idx_tenant_client (tenant_id, client_id), + INDEX idx_created (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +#### ai_quotation_items (AI 견적 항목) + +```sql +CREATE TABLE ai_quotation_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ai_quotation_id BIGINT UNSIGNED NOT NULL, + + category ENUM('basic', 'module', 'addon') NOT NULL, + module_code VARCHAR(50) NOT NULL, + module_name VARCHAR(100) NOT NULL, + description TEXT NULL, + + dev_cost DECIMAL(12,0) DEFAULT 0, + monthly_fee DECIMAL(10,0) DEFAULT 0, + quantity INT DEFAULT 1, + priority ENUM('essential', 'recommended', 'optional') DEFAULT 'recommended', + + ai_reasoning TEXT NULL, -- AI가 이 모듈을 추천한 근거 + matched_pain_points JSON NULL, -- 매칭된 고객 Pain Point + + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_quotation (ai_quotation_id), + FOREIGN KEY (ai_quotation_id) REFERENCES ai_quotations(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 6.2 기존 테이블 활용 + +| 테이블 | 용도 | 연동 방식 | +|--------|------|----------| +| `ai_configs` | Claude API 키/모델 설정 | provider='claude' 조회 | +| `ai_token_usages` | 토큰 비용 추적 | menu_name='AI견적' | +| `ai_pricing_configs` | Claude 모델 단가 | provider='claude' | +| `quotes` | 정식 견적 전환 대상 | `confirm` 시 생성 | +| `clients` | 고객사 정보 | client_id 참조 | + +--- + +## 7. Phase별 개발 계획 + +### Phase 1: 텍스트 인터뷰 → AI 견적 (MVP) + +> **목표**: 텍스트 인터뷰 입력 → Claude 분석 → 견적서 자동생성 +> **기간**: 2~3주 +> **우선순위**: 🔴 필수 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 1-1 | `ai_quotation_modules` 마이그레이션 + 시더 | API | ⏳ | +| 1-2 | `ai_quotations`, `ai_quotation_items` 마이그레이션 | API | ⏳ | +| 1-3 | 모델 생성 (`AiQuotation`, `AiQuotationItem`, `AiQuotationModule`) | API | ⏳ | +| 1-4 | `AiQuotationService` — Claude API 연동 | API | ⏳ | +| 1-5 | 프롬프트 엔진 구현 (분석 + 견적 템플릿) | API | ⏳ | +| 1-6 | `AiQuotationController` + FormRequest | API | ⏳ | +| 1-7 | API 라우트 등록 (`routes/api/v1/common.php`) | API | ⏳ | +| 1-8 | 정식 견적 전환 로직 (`confirmToQuote`) | API | ⏳ | +| 1-9 | MNG AI 견적 관리 화면 (목록/상세/수정) | MNG | ⏳ | +| 1-10 | 테스트 (인터뷰 샘플 3건 이상) | API | ⏳ | + +### Phase 2: 음성 인터뷰 연동 + +> **목표**: 현장에서 녹음한 음성 → STT 변환 → AI 분석 → 견적 생성 +> **기간**: 1주 (기존 STT 인프라 재사용으로 단축) +> **선행 조건**: Phase 1 완료 + +| 단계 | 작업 | 프로젝트 | 상태 | 비고 | +|------|------|---------|------|------| +| 2-1 | Google STT 서비스 구현 | MNG | ✅ 완료 | `GoogleCloudService::speechToText()` 재사용 | +| 2-2 | 음성 업로드 API (GCS 저장) | MNG | ✅ 완료 | `AiVoiceRecordingService`, `ConsultationController` 재사용 | +| 2-3 | STT → 텍스트 변환 파이프라인 | MNG | ✅ 완료 | LongRunningRecognize + 폴링 패턴 구현됨 | +| 2-4 | 음성 인터뷰 → AI 견적 통합 플로우 | API | ⏳ | 기존 STT 결과를 Claude 분석에 연결 | +| 2-5 | MNG 음성 녹음 업로드 UI | MNG | ✅ 완료 | `voice-recorder.blade.php` 재사용 가능 | + +> **핵심**: Google Cloud STT, GCS, 브라우저 음성 녹음 UI가 모두 구현 완료 상태. +> Phase 2에서는 기존 STT 결과를 AI 견적 파이프라인(Claude API)에 연결하는 **통합 플로우(2-4)만 신규 개발**하면 된다. + +### Phase 3: 학습 데이터 고도화 + +> **목표**: 과거 견적 데이터를 활용하여 AI 정확도 향상 +> **기간**: 2주 +> **선행 조건**: Phase 1 + 실제 사용 데이터 축적 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 3-1 | 과거 견적 데이터 → 프롬프트 Few-shot 예시 구성 | API | ⏳ | +| 3-2 | 확정된 AI 견적 → 학습 데이터 피드백 루프 | API | ⏳ | +| 3-3 | 업종별 견적 패턴 분석 → 추천 정확도 향상 | API | ⏳ | +| 3-4 | 프롬프트 A/B 테스트 프레임워크 | API | ⏳ | + +### Phase 4: 고객 셀프서비스 (확장) + +> **목표**: 고객이 직접 간단한 질문에 답변하면 견적 자동 생성 +> **기간**: 3주 +> **선행 조건**: Phase 1~3 안정화 + +| 단계 | 작업 | 프로젝트 | 상태 | +|------|------|---------|------| +| 4-1 | 고객용 인터뷰 설문 폼 설계 | React | ⏳ | +| 4-2 | 단계별 질문 → AI 분석 통합 | API + React | ⏳ | +| 4-3 | 견적서 미리보기 + PDF 다운로드 | React | ⏳ | +| 4-4 | 매니저 알림 → 후속 상담 연결 | API + MNG | ⏳ | + +--- + +## 8. Claude API 연동 상세 + +### 8.1 SDK 설치 + +```bash +# API 프로젝트에 Anthropic SDK 설치 +docker exec sam-api-1 composer require anthropic-ai/laravel +``` + +> **참고**: `anthropic-ai/laravel` 패키지는 Laravel용 공식 래퍼로, HTTP Client 기반으로 Claude API를 호출한다. 미출시/미지원 시 `GuzzleHttp`로 직접 HTTP 호출한다. + +### 8.2 Claude API 호출 패턴 + +```php +// app/Services/AiQuotationService.php + +class AiQuotationService +{ + private function callClaudeApi(string $systemPrompt, string $userMessage): array + { + // 1. ai_configs에서 Claude 설정 로드 + $config = AiConfig::where('provider', 'claude') + ->where('is_active', true) + ->first(); + + // 2. HTTP 호출 + $response = Http::withHeaders([ + 'x-api-key' => $config->api_key, + 'anthropic-version' => '2023-06-01', + 'content-type' => 'application/json', + ])->post($config->base_url . '/messages', [ + 'model' => $config->model, // claude-sonnet-4-20250514 + 'max_tokens' => 4096, + 'temperature' => 0.3, // 견적은 일관성 중요 + 'system' => $systemPrompt, + 'messages' => [ + ['role' => 'user', 'content' => $userMessage] + ], + ]); + + // 3. 토큰 사용량 기록 + $usage = $response->json('usage'); + AiTokenHelper::saveClaudeUsage( + tenantId: auth()->user()->tenant_id, + menuName: 'AI견적', + promptTokens: $usage['input_tokens'], + completionTokens: $usage['output_tokens'], + model: $config->model, + ); + + // 4. 응답 파싱 + $content = $response->json('content.0.text'); + return json_decode($content, true); + } +} +``` + +### 8.3 비용 예측 + +| 항목 | 토큰 | 비용 (USD) | 비용 (KRW) | +|------|------|-----------|------------| +| 1차 분석 프롬프트 (시스템+사용자) | ~2,000 입력 | $0.0005 | ~1원 | +| 1차 분석 응답 | ~1,500 출력 | $0.0019 | ~3원 | +| 2차 견적 프롬프트 | ~3,000 입력 | $0.0008 | ~1원 | +| 2차 견적 응답 | ~2,000 출력 | $0.0025 | ~4원 | +| **견적 1건 합계** | **~8,500** | **~$0.006** | **~9원** | + +> Claude Sonnet 기준. 1건당 약 **9원**으로 매우 경제적이다. + +--- + +## 9. MNG 관리 화면 + +### 9.1 화면 목록 + +| 화면 | URL | 설명 | +|------|-----|------| +| AI 견적 목록 | `/ai-quotation` | 생성된 AI 견적 목록 | +| AI 견적 생성 | `/ai-quotation/create` | 인터뷰 입력 폼 | +| AI 견적 상세 | `/ai-quotation/{id}` | 분석 결과 + 견적서 조회 | +| AI 견적 수정 | `/ai-quotation/{id}/edit` | 매니저가 수정 | +| 모듈 카탈로그 관리 | `/ai-quotation/modules` | SAM 모듈 목록 관리 | + +### 9.2 AI 견적 생성 화면 구성 + +``` +┌─────────────────────────────────────────────────────┐ +│ AI 견적서 생성 │ +├─────────────────────────────────────────────────────┤ +│ │ +│ [고객 정보] │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ 고객사 선택 ▼│ │ 업종 │ │ +│ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ 직원 수 │ │ 인터뷰 대상 │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +│ [인터뷰 내용] │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ (텍스트 입력 또는 음성 녹음 업로드) │ │ +│ │ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ [🔄 분석 시작] [📊 분석+견적 한번에] │ +│ │ +├─────────────────────────────────────────────────────┤ +│ [분석 결과] (Ajax 응답) │ +│ │ +│ 📊 업무 도메인 분석 │ +│ ┌──────────┬──────────┬──────────┬──────────┐ │ +│ │ 도메인 │ 현재상태 │ 문제점 │ 추천모듈 │ │ +│ ├──────────┼──────────┼──────────┼──────────┤ │ +│ │ 인사/급여 │ 엑셀관리 │ 분산관리 │ HR,PAYROLL│ │ +│ │ 생산관리 │ 종이기록 │ 추적불가 │PRODUCTION │ │ +│ └──────────┴──────────┴──────────┴──────────┘ │ +│ │ +│ 💰 견적서 초안 │ +│ ┌──────────┬──────────┬──────────┬──────────┐ │ +│ │ 모듈 │ 개발비 │ 월구독료 │ 우선순위 │ │ +│ ├──────────┼──────────┼──────────┼──────────┤ │ +│ │ 통합패키지 │30,000,000│ 800,000 │ 필수 │ │ +│ │ AI토큰 │ 0│ 50,000 │ 선택 │ │ +│ └──────────┴──────────┴──────────┴──────────┘ │ +│ 합계: 개발비 30,000,000원 / 월 850,000원 │ +│ │ +│ [✅ 정식 견적으로 전환] [✏️ 수정] [🗑️ 삭제] │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 10. 보안 및 운영 + +### 10.1 보안 고려사항 + +| 항목 | 대책 | +|------|------| +| API 키 노출 | `ai_configs` 테이블에 암호화 저장, .env 폴백 | +| 인터뷰 데이터 보호 | tenant_id 격리, 접근 권한 제어 | +| Claude API 비용 제어 | 일일/월별 토큰 한도 설정 (ai_configs.options) | +| 프롬프트 인젝션 | 사용자 입력 sanitize, 시스템 프롬프트 분리 | + +### 10.2 모니터링 + +| 항목 | 방법 | +|------|------| +| API 호출 성공/실패 | `ai_quotations.status` + `error_message` | +| 토큰 사용량 | `ai_token_usages` 테이블 (기존 인프라) | +| 비용 추적 | MNG `/system/ai-token-usage` (기존 UI) | +| 견적 전환율 | `ai_quotations` → `quotes` 전환 비율 통계 | + +### 10.3 에러 처리 + +```php +try { + $result = $this->callClaudeApi($systemPrompt, $userMessage); +} catch (ConnectionException $e) { + // Claude API 연결 실패 + $aiQuotation->update(['status' => 'failed', 'error_message' => 'Claude API 연결 실패']); +} catch (JsonException $e) { + // 응답 JSON 파싱 실패 + $aiQuotation->update(['status' => 'failed', 'error_message' => 'AI 응답 파싱 실패']); +} +``` + +--- + +## 11. 기대 효과 + +| 항목 | Before (현재) | After (AI 엔진) | +|------|--------------|-----------------| +| 견적 작성 시간 | 2~4시간 (수동) | 5~10분 (AI 초안 + 검토) | +| 모듈 누락 위험 | 매니저 경험 의존 | AI가 체계적으로 분석 | +| 고객 맞춤화 | 표준 템플릿 복사 | 인터뷰 기반 맞춤 견적 | +| 비용 최적화 | 수동 비교 | AI가 패키지 vs 개별 자동 비교 | +| 견적 1건 AI 비용 | — | ~9원 (Claude Sonnet) | + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-02 | 기획 초안 작성 | + +--- + +## 관련 문서 + +| 문서 | 경로 | +|------|------| +| SAM 프로젝트 개요 | `docs/SAM_PROJECT_OVERVIEW_FOR_AI.md` | +| 견적 기능 상세 | `docs/features/quotes/README.md` | +| 견적 시스템 분석 | `docs/data/견적/견적시스템_분석문서.md` | +| AI 기능 현황 | `docs/features/ai/README.md` | +| AI 설정 가이드 | `docs/guides/ai-config-settings.md` | +| 고객 요금 안내 | `docs/rules/customer-pricing.md` | +| 내부 과금 정책 | `docs/rules/billing-policy.md` | +| 단가 정책 | `docs/rules/pricing-policy.md` | +| Plans 가이드 | `docs/plans/GUIDE.md` | + +--- + +**최종 업데이트**: 2026-03-02 diff --git a/plans/attendance-management-plan.md b/plans/attendance-management-plan.md new file mode 100644 index 0000000..c5f8889 --- /dev/null +++ b/plans/attendance-management-plan.md @@ -0,0 +1,284 @@ +# MNG 근태현황 개발 계획서 + +> **작성일**: 2026-02-26 +> **상태**: 계획 수립 + +--- + +## 1. 개요 + +### 1.1 목적 + +MNG 인사관리 > 근태현황 기능을 완성한다. 현재 기본 CRUD가 구현되어 있으나, 미완성 기능과 알려진 버그를 해결하고 실무에 필요한 추가 기능을 구현한다. + +### 1.2 현재 상태 분석 + +#### 구현 완료 + +| 항목 | 상태 | 파일 | +|------|------|------| +| 근태 목록 조회 (HTMX 테이블) | ✅ | `index.blade.php`, `table.blade.php` | +| 월간 통계 카드 (5종) | ✅ | `index.blade.php` | +| 필터 (이름, 부서, 상태, 날짜) | ✅ | `index.blade.php` | +| 등록/수정 모달 | ✅ | `index.blade.php` | +| CRUD API (목록/등록/수정/삭제) | ✅ | `AttendanceController.php` (API) | +| AttendanceService | ✅ | `AttendanceService.php` | +| Attendance 모델 (8개 상태) | ✅ | `Attendance.php` | +| Soft Delete | ✅ | 모델 + 서비스 | + +#### 알려진 문제 (E2E 테스트 결과) + +| 문제 | 심각도 | 설명 | +|------|--------|------| +| 엑셀 다운로드 미구현 | 🟡 중요 | 버튼 없음, API 미연결 | +| 근태 등록 서버 에러 | 🔴 필수 | 모달 submit 시 500 에러 발생 가능 | + +#### 미구현 기능 (API 대비) + +| 기능 | API 지원 | MNG 상태 | +|------|---------|---------| +| 엑셀 내보내기 | ✅ `/v1/attendances/export` | ❌ 미구현 | +| 일괄 삭제 | ✅ `/v1/attendances/bulk-delete` | ❌ 미구현 | +| 개인별 근태 상세 | ✅ `/v1/attendances/{id}` | ❌ 미구현 | +| 월간 요약 통계 | ✅ `/v1/attendances/monthly-stats` | ⚠️ 기본만 구현 | +| 출퇴근 설정 관리 | ✅ `attendance_settings` 테이블 | ❌ 미구현 | + +--- + +## 2. 구현 범위 + +### 2.1 Phase 1: 버그 수정 + 핵심 기능 (우선) + +| # | 작업 | 난이도 | 설명 | +|---|------|--------|------| +| 1-1 | 근태 등록/수정 버그 수정 | 🟢 낮음 | store/update API 요청 오류 점검 및 수정 | +| 1-2 | 엑셀 다운로드 | 🟢 낮음 | API `/v1/attendances/export` 연동, 다운로드 버튼 추가 | +| 1-3 | 일괄 삭제 | 🟡 보통 | 체크박스 선택 → 일괄 삭제 버튼 | +| 1-4 | 월간 통계 기간 선택 | 🟢 낮음 | 현재 당월 고정 → 연/월 선택 가능하게 | + +### 2.2 Phase 2: 확장 기능 + +| # | 작업 | 난이도 | 설명 | +|---|------|--------|------| +| 2-1 | 개인별 근태 상세 페이지 | 🟡 보통 | 사원 클릭 → 월간 달력 + 출퇴근 이력 | +| 2-2 | 출퇴근 설정 관리 | 🟡 보통 | 표준 출근시간, GPS 사용여부, 허용반경 설정 | +| 2-3 | 월간/주간 요약 뷰 | 🟡 보통 | 부서별/사원별 근태 요약 테이블 | +| 2-4 | 근태 일괄 등록 | 🔴 높음 | 날짜 범위 + 대상 사원 → 일괄 근태 등록 | + +--- + +## 3. 상세 설계 + +### 3.1 Phase 1-1: 근태 등록/수정 버그 수정 + +**점검 항목**: +- MNG `AttendanceController::store()` validation 규칙과 실제 폼 데이터 일치 여부 +- `check_in`, `check_out` 포맷 (HH:MM vs HH:MM:SS) 불일치 가능성 +- `user_id` 전달 누락 여부 +- HTMX `hx-headers` CSRF 토큰 전달 확인 + +**수정 대상 파일**: +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/resources/views/hr/attendances/index.blade.php` (JS `submitAttendance()`) + +--- + +### 3.2 Phase 1-2: 엑셀 다운로드 + +**방식**: MNG에서 직접 엑셀 생성 (API 서버 미경유) + +**구현**: +1. `AttendanceService::getExportData()` 메서드 추가 +2. `AttendanceController::export()` 메서드 추가 +3. 라우트: `GET /api/admin/hr/attendances/export` +4. 인덱스 페이지에 다운로드 버튼 추가 + +**엑셀 컬럼**: + +| 컬럼 | 값 | +|------|-----| +| 날짜 | `base_date` | +| 사원명 | `user.name` | +| 부서 | `department.name` | +| 상태 | `status_label` | +| 출근 | `check_in` | +| 퇴근 | `check_out` | +| 비고 | `remarks` | + +**수정 대상 파일**: +- `mng/app/Services/HR/AttendanceService.php` +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/routes/api.php` +- `mng/resources/views/hr/attendances/index.blade.php` + +--- + +### 3.3 Phase 1-3: 일괄 삭제 + +**UI**: 테이블 각 행에 체크박스 추가, 헤더에 전체선택, 상단에 "선택 삭제" 버튼 + +**구현**: +1. `table.blade.php`에 체크박스 컬럼 추가 +2. Alpine.js 컴포넌트로 선택 상태 관리 +3. `AttendanceController::bulkDestroy()` 메서드 추가 +4. 라우트: `POST /api/admin/hr/attendances/bulk-delete` + +**수정 대상 파일**: +- `mng/resources/views/hr/attendances/partials/table.blade.php` +- `mng/resources/views/hr/attendances/index.blade.php` +- `mng/app/Http/Controllers/Api/Admin/HR/AttendanceController.php` +- `mng/app/Services/HR/AttendanceService.php` +- `mng/routes/api.php` + +--- + +### 3.4 Phase 1-4: 월간 통계 기간 선택 + +**현재**: 당월 통계만 표시 (하드코딩) +**변경**: 연/월 드롭다운 추가 → 선택 시 통계 카드 HTMX 갱신 + +**구현**: +1. 통계 카드 영역을 별도 partial로 분리 (`partials/stats.blade.php`) +2. 연/월 선택 UI 추가 +3. `hx-get` + `hx-vals`로 선택된 연/월 전달 +4. `stats()` API가 `year`, `month` 파라미터 이미 지원 + +**수정 대상 파일**: +- `mng/resources/views/hr/attendances/index.blade.php` +- `mng/resources/views/hr/attendances/partials/stats.blade.php` (신규) + +--- + +### 3.5 Phase 2-1: 개인별 근태 상세 페이지 + +**URL**: `/hr/attendances/{userId}` + +**페이지 구성**: +1. **사원 프로필 카드**: 이름, 부서, 직급, 재직상태 +2. **월간 달력**: 날짜별 근태 상태를 색상 도트로 표시 +3. **월간 통계**: 정시출근 N일, 지각 N일, 결근 N일 등 +4. **출퇴근 이력 테이블**: 해당 월의 상세 출퇴근 기록 + +**수정 대상 파일**: +- `mng/routes/web.php` (라우트 추가) +- `mng/app/Http/Controllers/HR/AttendanceController.php` (`show()` 추가) +- `mng/app/Services/HR/AttendanceService.php` (`getUserMonthlyAttendances()` 추가) +- `mng/resources/views/hr/attendances/show.blade.php` (신규) + +--- + +### 3.6 Phase 2-2: 출퇴근 설정 관리 + +**URL**: `/hr/attendance-settings` + +**설정 항목** (`attendance_settings` 테이블 기반): + +| 항목 | 필드 | 설명 | +|------|------|------| +| GPS 출퇴근 사용 | `use_gps` | on/off 토글 | +| 자동 출퇴근 | `use_auto` | on/off 토글 | +| 허용 반경 | `allowed_radius` | 미터 단위 입력 | +| 본사 주소 | `hq_address` | 주소 입력 | +| 본사 위도/경도 | `hq_latitude`, `hq_longitude` | 좌표 입력 | + +**수정 대상 파일**: +- `mng/routes/web.php`, `mng/routes/api.php` +- `mng/app/Http/Controllers/HR/AttendanceSettingController.php` (신규) +- `mng/app/Models/HR/AttendanceSetting.php` (신규 — API 모델 미러링) +- `mng/resources/views/hr/attendance-settings/index.blade.php` (신규) + +--- + +## 4. 데이터 흐름 + +### 4.1 MNG 자체 CRUD 패턴 (현재) + +``` +브라우저 ──HTMX──→ MNG API Controller ──→ MNG Service ──→ DB (직접) + (api/admin/hr/attendances) +``` + +> MNG는 API 서버를 경유하지 않고 DB에 직접 접근한다. + +### 4.2 엑셀 다운로드 흐름 + +``` +브라우저 ──GET──→ MNG AttendanceController::export() + → AttendanceService::getExportData() + → ExportService::download() (라라벨 엑셀) + ← BinaryFileResponse (.xlsx) +``` + +--- + +## 5. 구현 순서 및 의존 관계 + +``` +Phase 1 (버그 수정 + 핵심) + 1-1 버그 수정 ─────────────────────────────┐ + 1-2 엑셀 다운로드 ─────────────────────────┤ 독립적, 병렬 가능 + 1-3 일괄 삭제 ─────────────────────────────┤ + 1-4 통계 기간 선택 ────────────────────────┘ + +Phase 2 (확장) + 2-1 개인별 상세 ───→ 2-3 월간/주간 요약 (데이터 재사용) + 2-2 출퇴근 설정 ─── 독립적 + 2-4 일괄 등록 ───── 독립적 +``` + +--- + +## 6. 관련 파일 목록 + +### MNG 프로젝트 (`/home/aweso/sam/mng`) + +| 파일 | 역할 | +|------|------| +| `app/Models/HR/Attendance.php` | 모델 (8개 상태, json_details) | +| `app/Services/HR/AttendanceService.php` | 비즈니스 로직 | +| `app/Http/Controllers/HR/AttendanceController.php` | 뷰 컨트롤러 | +| `app/Http/Controllers/Api/Admin/HR/AttendanceController.php` | API 컨트롤러 | +| `resources/views/hr/attendances/index.blade.php` | 메인 페이지 | +| `resources/views/hr/attendances/partials/table.blade.php` | 테이블 partial | +| `routes/web.php` | 웹 라우트 | +| `routes/api.php` | API 라우트 | + +### API 프로젝트 (`/home/aweso/sam/api`) + +| 파일 | 역할 | +|------|------| +| `database/migrations/2025_12_09_*_attendances*` | 마이그레이션 (2개) | +| `database/migrations/2025_12_17_*_attendance_settings*` | 설정 테이블 | +| `app/Models/Tenants/Attendance.php` | API 모델 (참조용) | +| `app/Models/Tenants/AttendanceSetting.php` | 설정 모델 (참조용) | + +### 참조 문서 + +| 문서 | 경로 | +|------|------| +| 근태 API 규칙 | `docs/rules/attendance-api.md` | +| GPS 출퇴근 스펙 | `docs/specs/erp-analysis/03-gps-attendance.md` | + +--- + +## 7. 검증 방법 + +### Phase 1 체크리스트 + +- [ ] 근태 등록 모달 → 사원 선택 + 날짜 + 상태 입력 → 저장 성공 +- [ ] 근태 수정 모달 → 기존 데이터 로드 → 수정 → 저장 성공 +- [ ] 동일 사원/날짜 중복 등록 시 기존 데이터 업데이트 (Upsert) +- [ ] 엑셀 다운로드 버튼 클릭 → .xlsx 파일 다운로드 +- [ ] 체크박스 선택 → 일괄 삭제 → 테이블 갱신 +- [ ] 연/월 선택 → 통계 카드 갱신 + +### Phase 2 체크리스트 + +- [ ] 사원 이름 클릭 → 개인별 상세 페이지 이동 +- [ ] 달력에 근태 상태 색상 표시 +- [ ] 출퇴근 설정 페이지 → GPS/자동 토글 → 저장 +- [ ] 허용 반경 변경 → 저장 → DB 반영 + +--- + +**최종 업데이트**: 2026-02-26 diff --git a/plans/block-builder-evolution-plan.md b/plans/block-builder-evolution-plan.md new file mode 100644 index 0000000..57385f3 --- /dev/null +++ b/plans/block-builder-evolution-plan.md @@ -0,0 +1,706 @@ +# 양식 디자이너(Block Builder) 고도화 계획 + +> **작성일**: 2026-03-06 +> **상태**: 계획 수립 +> **담당**: Claude Code + 개발팀 +> **관련**: [문서양식관리](../features/documents/mng-document-template.md) | [문서관리](../features/documents/mng-document-system.md) + +--- + +## 1. 현재 상태 진단 + +### 1.1 구현 완료 (Phase 1 — 2026-02-28) + +- 13개 블록 타입 (기본 6 + 폼 7) +- 3패널 UI (팔레트 / 캔버스 / 속성) +- SortableJS 드래그-앤-드롭 정렬 +- Undo/Redo (최대 50단계) +- JSON 스키마 저장 (`document_templates.schema`) +- 페이지 설정 (A4/A3/B5, 세로/가로, 여백) + +### 1.2 핵심 미구현 사항 + +| 기능 | 상태 | 영향도 | +|------|------|--------| +| 문서 생성 시 블록 렌더링 | 미구현 | 블록 서식으로 문서 작성 불가 | +| 결재선 블록 | 미구현 | 결재 워크플로우 연동 불가 | +| 데이터 바인딩 (EAV 연동) | 미구현 | 입력값 저장/로드 불가 | +| 동적 행 추가 | 미구현 | 검사 데이터 행 추가 불가 | +| 변수/매크로 시스템 | 미구현 | 자동 값 주입 불가 | +| 인쇄/PDF 출력 | 미구현 | 블록 문서 인쇄 불가 | +| Columns 내부 블록 | 미구현 | 다단 레이아웃 활용 불가 | + +> **결론**: 양식 디자이너는 **레이아웃 편집기**로만 동작. 실제 문서 생성/결재/인쇄에서는 Legacy Builder만 사용 가능. + +--- + +## 2. 목표 + +Legacy Builder의 모든 기능을 양식 디자이너에서 지원하면서, 더 유연하고 확장 가능한 문서 시스템 구축. + +**최종 목표:** +``` +양식 디자이너로 서식 설계 + ↓ +블록 스키마 기반 문서 생성 (데이터 입력) + ↓ +결재 워크플로우 (작성 → 검토 → 승인) + ↓ +인쇄/PDF 출력 + ↓ +Legacy Builder 완전 대체 +``` + +--- + +## 3. 개발 로드맵 (6단계) + +### Phase 2: 블록 런타임 렌더러 (기반 인프라) + +> **목표**: 저장된 블록 스키마를 문서 생성/조회/인쇄에서 렌더링 + +#### 2-1. 블록 렌더러 엔진 + +**위치**: `mng/resources/views/documents/partials/block-renderer.blade.php` + +``` +schema JSON 입력 + ↓ +블록 타입별 Blade 컴포넌트 렌더링 + ↓ +모드별 출력: + - view 모드: 읽기 전용 HTML + - edit 모드: 입력 폼 HTML + - print 모드: 인쇄 최적화 HTML +``` + +**핵심 구현:** + +```php +// BlockRendererService +class BlockRendererService +{ + public function render(array $schema, string $mode, array $data = []): string + { + $html = ''; + foreach ($schema['blocks'] as $block) { + $html .= $this->renderBlock($block, $mode, $data); + } + return $html; + } + + private function renderBlock(array $block, string $mode, array $data): string + { + return match($block['type']) { + 'heading' => $this->renderHeading($block, $mode), + 'paragraph' => $this->renderParagraph($block, $mode), + 'table' => $this->renderTable($block, $mode, $data), + 'text_field' => $this->renderTextField($block, $mode, $data), + 'number_field' => $this->renderNumberField($block, $mode, $data), + 'date_field' => $this->renderDateField($block, $mode, $data), + 'select_field' => $this->renderSelectField($block, $mode, $data), + 'checkbox_field' => $this->renderCheckboxField($block, $mode, $data), + 'textarea_field' => $this->renderTextareaField($block, $mode, $data), + 'signature_field'=> $this->renderSignatureField($block, $mode, $data), + 'divider' => $this->renderDivider($block), + 'spacer' => $this->renderSpacer($block), + 'columns' => $this->renderColumns($block, $mode, $data), + 'approval_line' => $this->renderApprovalLine($block, $mode, $data), + 'dynamic_table' => $this->renderDynamicTable($block, $mode, $data), + default => '', + }; + } +} +``` + +#### 2-2. 문서 편집 화면 통합 + +**수정 대상**: `mng/resources/views/documents/edit.blade.php` + +``` +Template 로드 + ↓ +isBlockBuilder() 체크 + ├── true → BlockRendererService::render(schema, 'edit', data) + └── false → 기존 Legacy 렌더링 (변경 없음) +``` + +#### 2-3. 데이터 바인딩 (EAV 매핑) + +블록의 `binding` 속성으로 EAV 데이터와 연결: + +```javascript +// 블록 스키마 예시 +{ + "type": "text_field", + "props": { + "label": "제품명", + "binding": "bf_product_name", // ← EAV field_key + "required": true + } +} +``` + +``` +저장 시: + block.binding → document_data.field_key + input.value → document_data.field_value + block.id → document_data.section_id (블록 ID를 섹션으로 활용) + +로드 시: + document_data 조회 → field_key로 블록 매칭 → 값 주입 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `mng/app/Services/BlockRendererService.php` | 신규 생성 | +| `mng/resources/views/documents/partials/block-renderer.blade.php` | 신규 생성 | +| `mng/resources/views/documents/edit.blade.php` | 블록 빌더 분기 추가 | +| `mng/resources/views/documents/show.blade.php` | 블록 빌더 분기 추가 | +| `api/app/Services/DocumentService.php` | 블록 데이터 저장/로드 로직 | + +--- + +### Phase 3: 결재선 블록 + +> **목표**: 블록 스키마 내에서 결재 워크플로우 정의 + +#### 3-1. approval_line 블록 타입 추가 + +**스키마:** + +```json +{ + "type": "approval_line", + "props": { + "steps": [ + { "role": "작성", "department": "", "name": "" }, + { "role": "검토", "department": "", "name": "" }, + { "role": "승인", "department": "", "name": "" } + ], + "style": "horizontal", + "showStamp": true + } +} +``` + +#### 3-2. 팔레트에 결재선 블록 추가 + +```javascript +// 블록 팔레트 추가 +{ type: 'approval_line', icon: '✓', label: '결재선', category: '워크플로우' } +``` + +#### 3-3. 속성 패널 결재선 편집기 + +``` +┌─────────────────────────────┐ +│ 결재선 설정 │ +│ │ +│ [+ 단계 추가] │ +│ │ +│ 1. 역할: [작성 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ 2. 역할: [검토 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ 3. 역할: [승인 ▼] │ +│ 부서: [___________] │ +│ 이름: [___________] │ +│ │ +│ ☐ 직인 표시 │ +│ 스타일: [가로형 ▼] │ +└─────────────────────────────┘ +``` + +#### 3-4. 문서 생성 시 결재 연동 + +``` +블록 스키마 → approval_line 블록 추출 + ↓ +DocumentApproval 레코드 자동 생성 + ↓ +기존 결재 워크플로우 (submit → approve → reject) 그대로 활용 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | approval_line 블록 추가 | +| `block-canvas.blade.php` | 결재선 렌더링 | +| `BlockRendererService.php` | 결재선 view/edit/print 렌더 | +| `DocumentService.php` | 블록 결재선 → DocumentApproval 변환 | + +--- + +### Phase 4: 동적 테이블 블록 + 변수 시스템 + +> **목표**: 문서 작성 시 행 추가/삭제 가능한 테이블 + 자동 값 주입 + +#### 4-1. dynamic_table 블록 타입 + +기존 `table` 블록은 정적 (양식 설계 시 행 고정). `dynamic_table`은 문서 작성 시 행 동적 추가. + +**스키마:** + +```json +{ + "type": "dynamic_table", + "props": { + "label": "검사 데이터", + "columns": [ + { "key": "col_item", "label": "항목", "type": "text", "width": 120 }, + { "key": "col_standard", "label": "기준값", "type": "text", "width": 100 }, + { "key": "col_measured", "label": "측정값", "type": "number", "width": 100 }, + { "key": "col_result", "label": "판정", "type": "select", + "options": ["합격", "불합격", "보류"], "width": 80 } + ], + "minRows": 1, + "maxRows": 50, + "initialRows": 3, + "showRowNumber": true, + "binding": "inspection_data" + } +} +``` + +#### 4-2. EAV 데이터 매핑 + +``` +dynamic_table 블록 데이터 저장: + +document_data 레코드: + section_id = (dynamic_table 블록 ID → section 매핑) + column_id = (columns[].key → column 매핑) + row_index = 0, 1, 2, ... + field_key = "col_item", "col_standard", ... + field_value = 입력값 +``` + +#### 4-3. 변수/매크로 시스템 + +**내장 변수:** + +| 변수 | 값 | 설명 | +|------|-----|------| +| `{{today}}` | 현재 날짜 | YYYY-MM-DD | +| `{{now}}` | 현재 시각 | YYYY-MM-DD HH:mm | +| `{{user.name}}` | 로그인 사용자명 | | +| `{{user.department}}` | 로그인 사용자 부서 | | +| `{{doc.number}}` | 문서 번호 | 자동채번 | +| `{{doc.title}}` | 문서 제목 | | +| `{{template.company}}` | 서식 회사명 | | + +**연결 데이터 변수 (linked data):** + +| 변수 | 설명 | +|------|------| +| `{{item.name}}` | 연결 품목명 | +| `{{item.code}}` | 연결 품목 코드 | +| `{{order.number}}` | 연결 작업지시서 번호 | +| `{{order.quantity}}` | 연결 수량 | + +**변수 사용 예시 (블록 속성):** + +```json +{ + "type": "text_field", + "props": { + "label": "검사일자", + "binding": "bf_inspection_date", + "default": "{{today}}" + } +} +``` + +```json +{ + "type": "paragraph", + "props": { + "text": "작성자: {{user.name}} ({{user.department}})" + } +} +``` + +#### 4-4. 변수 해석 엔진 + +```php +// VariableResolver +class VariableResolver +{ + public function resolve(string $text, array $context): string + { + return preg_replace_callback('/\{\{(\w+(?:\.\w+)*)\}\}/', function ($m) use ($context) { + return data_get($context, $m[1], $m[0]); + }, $text); + } + + public function buildContext(Document $document, ?User $user = null): array + { + return [ + 'today' => now()->format('Y-m-d'), + 'now' => now()->format('Y-m-d H:i'), + 'user' => [ + 'name' => $user?->name, + 'department' => $user?->department?->name, + ], + 'doc' => [ + 'number' => $document->document_number, + 'title' => $document->title, + ], + 'item' => $this->resolveLinkedItem($document), + 'order' => $this->resolveLinkedOrder($document), + ]; + } +} +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | dynamic_table 블록 추가 | +| `BlockRendererService.php` | 동적 테이블 렌더링 (edit: 행 추가/삭제 UI) | +| `mng/app/Services/VariableResolver.php` | 신규 생성 | +| `DocumentService.php` | 동적 테이블 EAV 저장/로드 | + +--- + +### Phase 5: 고급 블록 + 조건부 로직 + +> **목표**: 수식 계산, 조건부 표시, 이미지 블록 등 고급 기능 + +#### 5-1. 수식 블록 (formula) + +```json +{ + "type": "formula_field", + "props": { + "label": "합계", + "expression": "SUM(inspection_data.col_measured)", + "format": "number", + "decimal": 2 + } +} +``` + +**지원 함수:** + +| 함수 | 설명 | 예시 | +|------|------|------| +| `SUM()` | 합계 | `SUM(table.col_amount)` | +| `AVG()` | 평균 | `AVG(table.col_measured)` | +| `COUNT()` | 개수 | `COUNT(table.col_item)` | +| `MIN()` / `MAX()` | 최솟값/최댓값 | `MIN(table.col_value)` | +| `IF()` | 조건 | `IF(AVG(table.col_measured) > 5, "합격", "불합격")` | +| `ROUND()` | 반올림 | `ROUND(AVG(table.col_measured), 2)` | + +#### 5-2. 조건부 표시 (conditional visibility) + +모든 블록에 `visibility` 속성 추가: + +```json +{ + "type": "paragraph", + "props": { + "text": "불합격 사유를 기재해 주세요.", + "visibility": { + "condition": "field", + "field": "bf_judgement", + "operator": "equals", + "value": "불합격" + } + } +} +``` + +**연산자:** + +| 연산자 | 설명 | +|--------|------| +| `equals` | 같으면 표시 | +| `not_equals` | 다르면 표시 | +| `contains` | 포함하면 표시 | +| `greater_than` | 크면 표시 | +| `less_than` | 작으면 표시 | +| `is_empty` | 비어있으면 표시 | +| `is_not_empty` | 비어있지 않으면 표시 | + +#### 5-3. 이미지 블록 + +```json +{ + "type": "image", + "props": { + "label": "검사 사진", + "source": "upload", + "maxSize": 10, + "accept": ["jpeg", "png", "webp"], + "width": "100%", + "align": "center" + } +} +``` + +#### 5-4. Columns 내부 블록 (중첩 렌더링) + +```json +{ + "type": "columns", + "props": { + "count": 2, + "ratio": "1:1", + "children": [ + [ + { "type": "text_field", "props": { "label": "품명" } }, + { "type": "date_field", "props": { "label": "검사일" } } + ], + [ + { "type": "text_field", "props": { "label": "LOT NO" } }, + { "type": "select_field", "props": { "label": "판정", "options": ["합격","불합격"] } } + ] + ] + } +} +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `block-editor.blade.php` | formula, image, conditional 블록 추가 | +| `mng/app/Services/FormulaEngine.php` | 수식 해석 엔진 | +| `BlockRendererService.php` | 조건부 표시, 수식 계산 렌더링 | + +--- + +### Phase 6: 인쇄/PDF + Legacy 대체 + +> **목표**: 블록 문서 인쇄 완성, Legacy Builder 완전 대체 + +#### 6-1. 인쇄 레이아웃 + +``` +print 모드 렌더링: + - 페이지 설정 (A4/A3) 적용 + - 여백 적용 + - 폼 필드 → 값 표시 (입력란 제거) + - 서명 → 서명 이미지 표시 + - 결재선 → 직인 표시 + - 페이지 넘김 (page-break) 자동 계산 +``` + +#### 6-2. PDF 내보내기 + +``` +블록 렌더러 (print 모드 HTML) + ↓ +Puppeteer / wkhtmltopdf + ↓ +PDF 파일 생성 + ↓ +다운로드 또는 첨부 +``` + +#### 6-3. Legacy → Block 마이그레이션 도구 + +기존 Legacy 서식을 Block 스키마로 자동 변환: + +```php +// LegacyToBlockMigrator +class LegacyToBlockMigrator +{ + public function convert(DocumentTemplate $legacy): array + { + $blocks = []; + + // 1. 결재선 → approval_line 블록 + if ($legacy->approvalLines->isNotEmpty()) { + $blocks[] = $this->convertApprovalLines($legacy->approvalLines); + } + + // 2. 기본필드 → text_field / date_field 블록 + foreach ($legacy->basicFields as $field) { + $blocks[] = $this->convertBasicField($field); + } + + // 3. 섹션 → heading + image 블록 + foreach ($legacy->sections as $section) { + $blocks[] = ['type' => 'heading', 'props' => ['text' => $section->title]]; + if ($section->image_path) { + $blocks[] = ['type' => 'image', 'props' => ['source' => $section->image_path]]; + } + } + + // 4. 컬럼 → dynamic_table 블록 + if ($legacy->columns->isNotEmpty()) { + $blocks[] = $this->convertColumns($legacy->columns); + } + + return [ + 'version' => '1.0', + 'page' => ['size' => 'A4', 'orientation' => 'portrait'], + 'blocks' => $blocks, + ]; + } +} +``` + +#### 6-4. Legacy Builder 비활성화 + +``` +Phase 6 완료 후: + - 새 양식 생성: 양식 디자이너만 허용 + - 기존 Legacy 서식: 조회/편집 가능 (변환 유도) + - Legacy Builder "새 양식" 버튼: "양식 디자이너" 사용 안내 +``` + +**산출물:** + +| 파일 | 작업 | +|------|------| +| `mng/resources/views/documents/print-block.blade.php` | 인쇄 전용 뷰 | +| `mng/app/Services/LegacyToBlockMigrator.php` | 변환 도구 | +| `mng/app/Services/PdfExportService.php` | PDF 생성 | + +--- + +## 4. Phase별 우선순위 및 의존관계 + +``` +Phase 2: 블록 런타임 렌더러 ──────────────────────┐ + (렌더러 엔진, 데이터 바인딩, 문서 편집 통합) │ + │ +Phase 3: 결재선 블록 ─────────────────────┐ │ + (approval_line 블록, 결재 워크플로우) │ │ + ↓ ↓ +Phase 4: 동적 테이블 + 변수 ──────→ Phase 5: 고급 블록 + (dynamic_table, 매크로) (수식, 조건부, 이미지) + │ + ↓ + Phase 6: 인쇄/PDF + Legacy 대체 + (마이그레이션 도구) +``` + +| Phase | 의존 | 난이도 | 예상 범위 | +|-------|------|--------|----------| +| **Phase 2** | 없음 (기반) | 높음 | 렌더러 엔진 + EAV 매핑 | +| **Phase 3** | Phase 2 | 중간 | 결재선 블록 + 워크플로우 연동 | +| **Phase 4** | Phase 2 | 높음 | 동적 테이블 + 변수 해석 | +| **Phase 5** | Phase 4 | 높음 | 수식 엔진 + 조건부 로직 | +| **Phase 6** | Phase 3~5 | 중간 | 인쇄 + 마이그레이션 | + +--- + +## 5. 스키마 버전 관리 + +### 5.1 버전 규칙 + +| 버전 | Phase | 변경 내용 | +|------|-------|----------| +| `1.0` | Phase 1 (현재) | 기본 13개 블록 | +| `2.0` | Phase 2~3 | 데이터 바인딩, approval_line 추가 | +| `3.0` | Phase 4 | dynamic_table, 변수 시스템 | +| `4.0` | Phase 5 | formula, conditional, image | + +### 5.2 하위 호환 + +```json +{ + "version": "3.0", + "page": { ... }, + "blocks": [ ... ], + "variables": { ... }, + "migrations": { + "from_1.0": "auto" + } +} +``` + +- 이전 버전 스키마 자동 인식 및 업그레이드 +- 신규 블록 타입은 이전 버전에서 무시 (graceful degradation) + +--- + +## 6. 신규 블록 타입 전체 목록 + +### Phase별 블록 추가 계획 + +| Phase | 블록 타입 | 카테고리 | 설명 | +|-------|----------|---------|------| +| 1 (완료) | `heading` | 기본 | 제목 | +| 1 (완료) | `paragraph` | 기본 | 문단 | +| 1 (완료) | `table` | 기본 | 정적 테이블 | +| 1 (완료) | `columns` | 기본 | 다단 레이아웃 | +| 1 (완료) | `divider` | 기본 | 구분선 | +| 1 (완료) | `spacer` | 기본 | 여백 | +| 1 (완료) | `text_field` | 폼 | 텍스트 입력 | +| 1 (완료) | `number_field` | 폼 | 숫자 입력 | +| 1 (완료) | `date_field` | 폼 | 날짜 입력 | +| 1 (완료) | `select_field` | 폼 | 드롭다운 | +| 1 (완료) | `checkbox_field` | 폼 | 체크박스 | +| 1 (완료) | `textarea_field` | 폼 | 장문 텍스트 | +| 1 (완료) | `signature_field` | 폼 | 서명 | +| **3** | `approval_line` | 워크플로우 | 결재선 | +| **4** | `dynamic_table` | 데이터 | 동적 행 테이블 | +| **5** | `formula_field` | 데이터 | 수식 계산 | +| **5** | `image` | 미디어 | 이미지 업로드/표시 | + +--- + +## 7. 기술 스택 정리 + +| 구성 요소 | 기술 | 비고 | +|----------|------|------| +| 블록 에디터 UI | Alpine.js + Blade | 기존 유지 | +| 드래그-앤-드롭 | SortableJS | 기존 유지 | +| 블록 렌더러 | PHP (BlockRendererService) | 신규 | +| 변수 해석 | PHP (VariableResolver) | 신규 | +| 수식 엔진 | PHP (FormulaEngine) | 신규 | +| 데이터 저장 | EAV (document_data) | 기존 테이블 활용 | +| 결재 워크플로우 | DocumentApproval | 기존 로직 활용 | +| 인쇄 | CSS @media print | 신규 | +| PDF | Puppeteer 또는 wkhtmltopdf | 신규 | + +--- + +## 8. 위험 요소 및 대응 + +| 위험 | 영향 | 대응 | +|------|------|------| +| EAV 매핑 복잡도 | 블록 ID ↔ section_id 매핑 불일치 | 블록 ID를 section 대용으로 사용, 매핑 테이블 추가 검토 | +| Legacy 데이터 호환 | 기존 문서 데이터 접근 불가 | Legacy 서식 문서는 기존 방식 유지, 신규 서식만 블록 적용 | +| 수식 엔진 보안 | 임의 코드 실행 위험 | 화이트리스트 함수만 허용, eval 사용 금지 | +| 인쇄 레이아웃 | 브라우저별 차이 | CSS @page 규격 준수, PDF 변환 권장 | +| 스키마 마이그레이션 | 버전 업그레이드 시 데이터 손실 | 하위 호환 보장, 자동 업그레이드 로직 | + +--- + +## 9. 성공 기준 + +| 기준 | 측정 방법 | +|------|----------| +| 블록 서식으로 문서 생성 가능 | Phase 2 완료 후 테스트 | +| 결재 워크플로우 정상 동작 | Phase 3 완료 후 테스트 | +| 동적 행 추가/삭제 | Phase 4 완료 후 테스트 | +| 변수 자동 주입 | Phase 4 완료 후 테스트 | +| Legacy 서식 자동 변환 | Phase 6 완료 후 테스트 | +| 인쇄 품질 A4 기준 정상 | Phase 6 완료 후 테스트 | + +--- + +## 관련 문서 + +- [문서양식관리](../features/documents/mng-document-template.md) — 현재 양식관리 기술문서 +- [문서관리 시스템](../features/documents/mng-document-system.md) — 문서 생성/결재 기술문서 +- [문서관리 API](../features/documents/README.md) — API 엔드포인트 목록 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/plans/design-insight-menu-plan.md b/plans/design-insight-menu-plan.md new file mode 100644 index 0000000..28122f5 --- /dev/null +++ b/plans/design-insight-menu-plan.md @@ -0,0 +1,611 @@ +# UI/UX 디자인 인사이트 연구 메뉴 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 중 +> **라우트**: `/rd/design-insight` +> **모티브**: 기획디자인 스토리보드 에디터 (`/rd/planning-design`) + +--- + +## 1. 개요 + +### 1.1 배경 + +기획디자인 메뉴는 ERP 화면을 **설계(Output)**하는 도구다. +그런데 좋은 설계를 하려면 **연구(Input)**가 먼저 필요하다. + +``` +연구 (이 메뉴) 설계 (기획디자인) +┌─────────────────┐ ┌─────────────────┐ +│ 레퍼런스 수집 │ │ 스토리보드 작성 │ +│ 패턴 분석 │ ──→ │ 와이어프레임 설계 │ +│ 인사이트 정리 │ │ HTML 내보내기 │ +│ 디자인 원칙 학습 │ │ 인쇄 │ +└─────────────────┘ └─────────────────┘ +``` + +현재 SAM ERP 화면을 만들 때 참고할 디자인 패턴이나 인사이트를 체계적으로 관리하는 도구가 없다. 외부 서비스(Dribbble, Mobbin 등)를 참고하지만 **우리 ERP에 맞는 패턴**을 축적하는 곳이 없다. + +### 1.2 목적 + +SAM ERP 화면 개발에 필요한 **UI/UX 디자인 인사이트를 수집·분석·축적**하는 연구 도구 + +### 1.3 핵심 가치 + +| 가치 | 설명 | +|------|------| +| **패턴 축적** | "이 화면은 왜 좋은가?" — 반복 사용할 패턴을 라이브러리화 | +| **Before/After** | 개선 전후를 비교하여 디자인 결정의 근거를 기록 | +| **팀 학습** | 디자인 인사이트를 팀원과 공유, 일관된 UI 품질 유지 | +| **빠른 참조** | 새 화면 설계 시 기존 패턴을 즉시 찾아 재사용 | + +--- + +## 2. 기술 아키텍처 + +### 2.1 기획디자인과 동일한 패턴 + +기획디자인 메뉴의 성공 패턴을 그대로 적용한다. + +| 항목 | 선택 | 이유 | +|------|------|------| +| 프레임워크 | Alpine.js 단일 파일 SPA | 서버 API 없이 즉시 사용, MNG 기존 스택 | +| 저장 | localStorage | 서버 의존성 제거, 즉시 사용 가능 | +| 뷰 파일 | `resources/views/rd/design-insight/index.blade.php` | 단일 파일 구조 | +| 컨트롤러 | `RdController@designInsight()` | 기존 R&D 컨트롤러 확장 | +| 이미지 | Base64 Data URL (localStorage) | 서버 업로드 불필요 | + +### 2.2 라우트 + +```php +// routes/web.php — R&D 그룹 내 추가 +Route::get('/rd/design-insight', [RdController::class, 'designInsight']) + ->name('rd.design-insight'); +``` + +### 2.3 localStorage 키 + +| 키 | 용도 | +|----|------| +| `di_projects` | 연구 프로젝트 목록 (메인 저장소) | +| `di_current` | 현재 프로젝트 ID | +| `di_patterns` | 디자인 패턴 라이브러리 (프로젝트 간 공유) | + +--- + +## 3. 화면 구조 + +### 3.1 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 툴바: [프로젝트명] [저장] [내보내기] [뷰: 보드│리스트│갤러리] │ +├──────────────────────────────────────────────────────────────┤ +│ 카테고리 탭: 전체 │ 레퍼런스 │ 분석 │ 패턴 │ Before/After │ +├────────┬─────────────────────────────────────────────────────┤ +│ │ │ +│ 사이드 │ 메인 콘텐츠 영역 │ +│ 바 │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ ◆ 프로 │ │ 인사이트 │ │ 인사이트 │ │ 인사이트 │ │ +│ 젝트 │ │ 카드 1 │ │ 카드 2 │ │ 카드 3 │ │ +│ 목록 │ │ │ │ │ │ │ │ +│ │ │ 🏷️태그 │ │ 🏷️태그 │ │ 🏷️태그 │ │ +│ ◆ 태그 │ └─────────┘ └─────────┘ └─────────┘ │ +│ 필터 │ │ +│ │ ┌─────────┐ ┌─────────┐ │ +│ ◆ 검색 │ │ 인사이트 │ │ + 새 카드 │ │ +│ │ │ 카드 4 │ │ 추가 │ │ +│ │ └─────────┘ └─────────┘ │ +│ │ │ +├────────┴─────────────────────────────────────────────────────┤ +│ 상태바: 카드 12개 │ 패턴 5개 │ 태그 8개 │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 3.2 뷰 모드 (3종) + +| 뷰 | 설명 | 용도 | +|----|------|------| +| **보드 (Board)** | 칸반 스타일 카드 격자 배열 | 전체 현황 파악, 기본 뷰 | +| **리스트 (List)** | 테이블형 목록 (정렬/필터) | 대량 데이터 관리, 검색 | +| **갤러리 (Gallery)** | 이미지 중심 큰 썸네일 격자 | 시각적 비교, 레퍼런스 브라우징 | + +--- + +## 4. 인사이트 카드 (핵심 데이터 단위) + +### 4.1 카드 유형 (4종) + +#### A. 레퍼런스 카드 (Reference) + +외부/내부 화면 스크린샷을 수집하고 메모를 남긴다. + +``` +┌──────────────────────────────┐ +│ 📷 [스크린샷 이미지] │ +│ │ +├──────────────────────────────┤ +│ 📌 Notion 대시보드 │ +│ "카드형 레이아웃이 정보 밀도를 │ +│ 유지하면서도 시각적으로 깔끔" │ +├──────────────────────────────┤ +│ 출처: notion.so │ +│ 🏷️ 대시보드 카드 레이아웃 │ +│ ⭐⭐⭐⭐☆ │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 스크린샷 이미지 | +| `title` | string | 제목 | +| `memo` | string | 인사이트 메모 (왜 좋은가/나쁜가) | +| `source` | string | 출처 (URL, 앱 이름 등) | +| `tags` | string[] | 태그 배열 | +| `rating` | number (1-5) | 평점 | +| `category` | string | 화면 카테고리 | + +#### B. 분석 카드 (Analysis) + +화면을 분석하고 디자인 원칙을 체크한다. + +``` +┌──────────────────────────────┐ +│ 🔍 SAM 수주 목록 화면 분석 │ +├──────────────────────────────┤ +│ [스크린샷 + 어노테이션 오버레이]│ +│ ①→ 검색 영역 너무 넓음 │ +│ ②→ 버튼 정렬 불일치 │ +│ ③→ 여백 불균형 │ +├──────────────────────────────┤ +│ ✅ 정렬 (Alignment) │ +│ ❌ 대비 (Contrast) │ +│ ✅ 반복 (Repetition) │ +│ ⚠️ 근접성 (Proximity) │ +├──────────────────────────────┤ +│ 개선 제안: │ +│ "검색 영역을 접을 수 있게 하고 │ +│ 버튼 그룹을 우측 정렬" │ +│ 🏷️ 목록화면 개선필요 │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 분석 대상 스크린샷 | +| `annotations` | Annotation[] | 어노테이션 배열 (마커 번호, 좌표, 텍스트) | +| `principles` | object | CRAP 원칙 체크 (contrast, repetition, alignment, proximity) | +| `suggestion` | string | 개선 제안 | +| `severity` | string | 심각도 (info, warning, critical) | + +#### C. 패턴 카드 (Pattern) + +반복 사용할 UI 패턴을 템플릿으로 등록한다. + +``` +┌──────────────────────────────┐ +│ 📐 검색 + 필터 + 목록 패턴 │ +├──────────────────────────────┤ +│ [패턴 와이어프레임 이미지] │ +├──────────────────────────────┤ +│ 사용처: │ +│ • 수주 목록 │ +│ • 거래처 목록 │ +│ • 품목 목록 │ +├──────────────────────────────┤ +│ 구성 요소: │ +│ ☑ 검색바 (상단 고정) │ +│ ☑ 필터 칩 (접기/펼치기) │ +│ ☑ 테이블 (정렬 가능) │ +│ ☑ 페이지네이션 (하단) │ +│ ☑ 액션 버튼 (우상단) │ +├──────────────────────────────┤ +│ 🏷️ 목록 CRUD 테이블 │ +│ 📊 사용빈도: ★★★★★ (12회) │ +└──────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `image` | string (Base64) | 패턴 와이어프레임 | +| `usedIn` | string[] | 사용처 목록 | +| `components` | Component[] | 구성 요소 체크리스트 | +| `guidelines` | string | 사용 가이드라인 | +| `frequency` | number | 사용 빈도 | + +#### D. Before/After 카드 (Comparison) + +디자인 개선 전후를 비교한다. + +``` +┌──────────────────────────────────────────┐ +│ 🔄 거래처 상세 화면 리뉴얼 │ +├───────────────────┬──────────────────────┤ +│ ❌ Before │ ✅ After │ +│ [이전 스크린샷] │ [개선 스크린샷] │ +│ │ │ +├───────────────────┴──────────────────────┤ +│ 변경 포인트: │ +│ 1. 탭 구조 → 섹션 접기/펼치기 변경 │ +│ 2. 좌우 2컬럼 → 단일 컬럼 (모바일 대응) │ +│ 3. 저장 버튼 하단 고정 → 상단 sticky │ +├──────────────────────────────────────────┤ +│ 효과: 스크롤 40% 감소, 작업 완료 시간 단축 │ +│ 🏷️ 상세화면 폼 리뉴얼 │ +└──────────────────────────────────────────┘ +``` + +| 필드 | 타입 | 설명 | +|------|------|------| +| `beforeImage` | string (Base64) | 개선 전 스크린샷 | +| `afterImage` | string (Base64) | 개선 후 스크린샷 | +| `changes` | string[] | 변경 포인트 목록 | +| `effect` | string | 개선 효과 | + +### 4.2 공통 필드 + +모든 카드 유형이 공유하는 기본 필드: + +```json +{ + "id": "di_1709856000000_abc", + "type": "reference", + "title": "카드 제목", + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00", + "tags": ["대시보드", "카드", "레이아웃"], + "category": "dashboard", + "pinned": false, + "archived": false +} +``` + +### 4.3 카테고리 (화면 유형별) + +| 카테고리 | 코드 | 설명 | +|---------|------|------| +| 대시보드 | `dashboard` | 통계, KPI, 차트 화면 | +| 목록 | `list` | 테이블, 검색, 필터 화면 | +| 상세/폼 | `form` | 입력, 편집, 상세 보기 | +| 모달/팝업 | `modal` | 모달 다이얼로그, 확인창 | +| 네비게이션 | `navigation` | 사이드바, 탭, 메뉴 | +| 로그인/온보딩 | `auth` | 인증, 초기 설정 | +| 보고서/인쇄 | `report` | 인쇄용, PDF 출력 화면 | +| 기타 | `etc` | 분류 불가 | + +--- + +## 5. 기능 상세 + +### 5.1 이미지 수집 + +| 기능 | 설명 | +|------|------| +| 파일 업로드 | 이미지 파일 선택 (PNG, JPG, GIF) | +| 클립보드 붙여넣기 | `Ctrl+V`로 스크린샷 즉시 붙여넣기 | +| 드래그 앤 드롭 | 이미지 파일을 카드 영역에 드롭 | + +> **Ctrl+V 붙여넣기가 핵심** — 스크린샷 캡처 후 즉시 카드 생성이 워크플로우의 핵심 + +### 5.2 어노테이션 (분석 카드) + +분석 카드에서 이미지 위에 마커를 추가하여 문제점이나 인사이트를 표시한다. + +| 어노테이션 유형 | 설명 | +|---------------|------| +| 번호 마커 (①②③) | 이미지 위 클릭 → 번호 자동 증가, 하단 설명과 연동 | +| 영역 하이라이트 | 드래그로 사각형 영역 표시 (반투명 컬러 오버레이) | +| 텍스트 메모 | 이미지 위 임의 위치에 짧은 메모 | + +> 기획디자인의 번호 마커(marker 블록) + Description 패널 패턴을 재활용 + +### 5.3 태그 시스템 + +| 기능 | 설명 | +|------|------| +| 자유 태그 | 카드에 자유 태그 추가 (콤마 구분 입력) | +| 태그 자동 완성 | 기존 태그 목록에서 자동 완성 | +| 태그 필터 | 사이드바에서 태그 클릭 → 해당 태그 카드만 표시 | +| 태그 색상 | 카테고리별 자동 색상 배정 | + +### 5.4 CRAP 디자인 원칙 체크리스트 + +분석 카드에서 사용하는 디자인 원칙 평가: + +| 원칙 | 체크 항목 | +|------|----------| +| **C**ontrast (대비) | 중요 요소가 시각적으로 구분되는가? | +| **R**epetition (반복) | 일관된 스타일이 반복 적용되는가? | +| **A**lignment (정렬) | 요소들이 논리적으로 정렬되어 있는가? | +| **P**roximity (근접성) | 관련 요소가 가까이 그룹핑되어 있는가? | + +추가 체크: + +| 원칙 | 체크 항목 | +|------|----------| +| 여백 (Whitespace) | 적절한 여백이 확보되어 있는가? | +| 계층 (Hierarchy) | 정보의 우선순위가 시각적으로 드러나는가? | +| 일관성 (Consistency) | 다른 화면과 일관된 패턴을 따르는가? | +| 접근성 (Accessibility) | 색상 대비, 폰트 크기가 충분한가? | + +### 5.5 검색 & 필터 + +| 기능 | 설명 | +|------|------| +| 텍스트 검색 | 제목, 메모, 태그에서 전문 검색 | +| 카테고리 필터 | 화면 유형별 필터 (탭) | +| 카드 유형 필터 | 레퍼런스 / 분석 / 패턴 / Before/After | +| 평점 필터 | ⭐ 3점 이상만 표시 등 | +| 정렬 | 최신순, 평점순, 이름순 | + +### 5.6 내보내기 + +| 형식 | 설명 | +|------|------| +| JSON | 전체 프로젝트 데이터 백업/복원 | +| HTML | 인사이트 카드를 HTML 보고서로 출력 (인쇄 가능) | +| 패턴 → 기획디자인 | 패턴 카드의 와이어프레임을 기획디자인 템플릿으로 전송 | + +### 5.7 키보드 단축키 + +| 단축키 | 기능 | +|--------|------| +| `Ctrl+V` | 클립보드 이미지로 새 카드 생성 | +| `Ctrl+S` | 프로젝트 저장 | +| `Ctrl+F` | 검색 포커스 | +| `Ctrl+N` | 새 카드 추가 | +| `Delete` | 선택 카드 삭제 | +| `Ctrl+Z` | 실행 취소 | +| `Ctrl+Y` | 다시 실행 | + +--- + +## 6. 데이터 구조 + +### 6.1 프로젝트 (localStorage: `di_projects`) + +```json +[ + { + "id": "diproj_1709856000000", + "title": "SAM ERP v2 디자인 연구", + "description": "SAM ERP 화면 개선을 위한 UI/UX 인사이트 수집", + "cards": [], + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00" + } +] +``` + +### 6.2 인사이트 카드 (cards 배열 내) + +```json +{ + "id": "di_1709856000000_abc", + "type": "reference", + "title": "Notion 대시보드 카드 레이아웃", + "image": "data:image/png;base64,...", + "memo": "카드형 레이아웃이 정보 밀도를 유지하면서도 시각적으로 깔끔", + "source": "notion.so", + "tags": ["대시보드", "카드", "레이아웃"], + "category": "dashboard", + "rating": 4, + "pinned": false, + "archived": false, + "createdAt": "2026-03-08T10:00:00", + "updatedAt": "2026-03-08T15:30:00" +} +``` + +### 6.3 분석 어노테이션 + +```json +{ + "annotations": [ + { + "id": "ann_001", + "type": "marker", + "num": 1, + "x": 150, + "y": 80, + "text": "검색 영역 너무 넓음 — 접기 기능 필요" + }, + { + "id": "ann_002", + "type": "highlight", + "x": 200, + "y": 300, + "w": 150, + "h": 40, + "color": "rgba(239,68,68,0.3)", + "text": "버튼 정렬 불일치" + } + ] +} +``` + +### 6.4 디자인 패턴 라이브러리 (localStorage: `di_patterns`) + +프로젝트 간 공유되는 패턴 라이브러리: + +```json +[ + { + "id": "pat_001", + "name": "검색 + 필터 + 목록", + "image": "data:image/png;base64,...", + "components": [ + { "name": "검색바", "required": true }, + { "name": "필터 칩", "required": false }, + { "name": "데이터 테이블", "required": true }, + { "name": "페이지네이션", "required": true }, + { "name": "액션 버튼", "required": true } + ], + "guidelines": "검색바는 상단 고정, 필터는 접기/펼치기 지원", + "usedIn": ["수주 목록", "거래처 목록", "품목 목록"], + "tags": ["목록", "CRUD", "테이블"], + "frequency": 12, + "createdAt": "2026-03-08T10:00:00" + } +] +``` + +--- + +## 7. 프리셋 데이터 + +### 7.1 기본 카테고리 (하드코딩) + +```javascript +categories: [ + { code: 'dashboard', label: '대시보드', icon: '📊', color: '#6366f1' }, + { code: 'list', label: '목록', icon: '📋', color: '#3b82f6' }, + { code: 'form', label: '상세/폼', icon: '📝', color: '#10b981' }, + { code: 'modal', label: '모달/팝업', icon: '💬', color: '#f59e0b' }, + { code: 'navigation',label: '네비게이션',icon: '🧭', color: '#8b5cf6' }, + { code: 'auth', label: '로그인', icon: '🔐', color: '#ec4899' }, + { code: 'report', label: '보고서', icon: '📄', color: '#0ea5e9' }, + { code: 'etc', label: '기타', icon: '📎', color: '#64748b' }, +] +``` + +### 7.2 CRAP 원칙 체크리스트 (하드코딩) + +```javascript +designPrinciples: [ + { key: 'contrast', label: '대비 (Contrast)', icon: '🔲', desc: '중요 요소가 시각적으로 구분' }, + { key: 'repetition', label: '반복 (Repetition)', icon: '🔁', desc: '일관된 스타일 반복 적용' }, + { key: 'alignment', label: '정렬 (Alignment)', icon: '📏', desc: '논리적 정렬' }, + { key: 'proximity', label: '근접성 (Proximity)', icon: '🧲', desc: '관련 요소 그룹핑' }, + { key: 'whitespace', label: '여백 (Whitespace)', icon: '⬜', desc: '적절한 여백 확보' }, + { key: 'hierarchy', label: '계층 (Hierarchy)', icon: '🔺', desc: '정보 우선순위 시각화' }, + { key: 'consistency', label: '일관성 (Consistency)',icon: '🔗', desc: '다른 화면과의 일관성' }, + { key: 'a11y', label: '접근성 (A11y)', icon: '♿', desc: '색상 대비, 폰트 크기' }, +] +``` + +### 7.3 샘플 패턴 템플릿 (프리셋) + +| 패턴명 | 구성 요소 | SAM 내 사용처 | +|--------|----------|--------------| +| 검색 + 목록 | 검색바, 필터, 테이블, 페이지네이션, 액션버튼 | 수주/거래처/품목 목록 | +| 상세 폼 | 섹션 헤더, 라벨+입력, 저장/취소 버튼 | 수주 상세, 거래처 상세 | +| 대시보드 | 통계 카드 4개, 차트 2개, 요약 테이블 | 메인 대시보드 | +| 탭 레이아웃 | 탭 메뉴, 탭 콘텐츠, 액션 버튼 | 설정, 품목기준관리 | +| 트리 + 상세 | 좌측 트리, 우측 상세 패널 | 메뉴 관리, 조직도 | +| 모달 폼 | 모달 헤더, 입력 필드, 확인/취소 | 등록/수정 팝업 | +| 칸반 보드 | 컬럼 헤더, 드래그 카드, 필터 | 업무 관리 | +| 캘린더 | 월/주/일 뷰, 이벤트 카드, 필터 | 일정 관리, 근태 | + +--- + +## 8. 워크플로우 + +### 8.1 일반 사용 흐름 + +``` +1. 새 연구 프로젝트 생성 ("SAM ERP v2 디자인 연구") + ↓ +2. 레퍼런스 수집 + • 외부 서비스 스크린샷 → Ctrl+V 붙여넣기 + • SAM 기존 화면 스크린샷 → 파일 업로드 + • 태그 + 카테고리 분류 + ↓ +3. 화면 분석 + • 분석 카드 생성 → 어노테이션 추가 + • CRAP 원칙 체크 + • 개선 제안 작성 + ↓ +4. 패턴 추출 + • 반복되는 좋은 패턴 → 패턴 카드로 등록 + • 구성 요소 정리, 사용 가이드라인 작성 + ↓ +5. Before/After 기록 + • 개선 전후 비교 카드 생성 + • 변경 포인트 + 효과 기록 + ↓ +6. 기획디자인 연계 + • 패턴 라이브러리에서 참고하며 스토리보드 작성 +``` + +### 8.2 기획디자인 연계 + +``` +디자인 인사이트 기획디자인 +┌──────────────┐ ┌──────────────┐ +│ 패턴 카드: │ │ 스토리보드: │ +│ "검색+목록" │──참조──→│ 새 페이지에 │ +│ 구성요소 체크 │ │ 패턴 적용 │ +│ 가이드라인 │ │ │ +└──────────────┘ └──────────────┘ +``` + +> 향후 패턴 카드의 구성 요소를 기획디자인 블록 템플릿으로 자동 변환하는 연계 기능을 검토한다. + +--- + +## 9. 개발 로드맵 + +### Phase 1 — 기본 구조 (MVP) + +| 항목 | 내용 | +|------|------| +| 라우트 + 컨트롤러 | `GET /rd/design-insight` → 뷰 반환 | +| 프로젝트 CRUD | 생성/저장/로드/삭제 (localStorage) | +| 레퍼런스 카드 | 이미지 업로드 + 메모 + 태그 + 카테고리 | +| 보드 뷰 | 카드 격자 배열 기본 화면 | +| 검색/필터 | 텍스트 검색, 카테고리 탭 필터 | +| Ctrl+V 붙여넣기 | 클립보드 이미지 → 새 카드 자동 생성 | + +### Phase 2 — 분석 도구 + +| 항목 | 내용 | +|------|------| +| 분석 카드 | 어노테이션 시스템 (마커, 하이라이트) | +| CRAP 체크리스트 | 디자인 원칙 체크 UI | +| Before/After 카드 | 전후 비교 카드 유형 | +| 갤러리 뷰 | 이미지 중심 큰 썸네일 | +| 리스트 뷰 | 테이블형 정렬/필터 | + +### Phase 3 — 패턴 라이브러리 + +| 항목 | 내용 | +|------|------| +| 패턴 카드 | 구성 요소 체크리스트, 가이드라인 | +| 패턴 프리셋 | SAM ERP 기본 패턴 8종 | +| 패턴 공유 | 프로젝트 간 패턴 공유 (di_patterns) | +| 내보내기 | JSON 백업, HTML 보고서 | + +### Phase 4 — 연계 & 고도화 + +| 항목 | 내용 | +|------|------| +| 기획디자인 연계 | 패턴 → 블록 템플릿 변환 | +| DB 저장 전환 | localStorage → DB (협업 지원) | +| 팀 공유 | 다른 사용자와 인사이트 공유 | + +--- + +## 10. 파일 구조 (예상) + +``` +mng/ +├── app/Http/Controllers/ +│ └── RdController.php ← designInsight() 메서드 추가 +├── resources/views/rd/design-insight/ +│ └── index.blade.php ← 전체 CSS + HTML + Alpine.js +└── routes/web.php ← Route 추가 +``` + +--- + +## 11. 관련 문서 + +- [기획디자인 기술 스펙](../features/rd/planning-design.md) — 모티브가 된 스토리보드 에디터 +- [기획디자인 프로젝트](../projects/planning-design/README.md) — 프로젝트 이력 +- [R&D 메뉴 개요](../features/rd/README.md) — R&D 전체 메뉴 구조 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/plans/fire-shutter-drawing-generator-plan.md b/plans/fire-shutter-drawing-generator-plan.md new file mode 100644 index 0000000..1b9a680 --- /dev/null +++ b/plans/fire-shutter-drawing-generator-plan.md @@ -0,0 +1,753 @@ +# 방화셔터 도면생성 기능 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 초안 +> **위치**: MNG > R&D > 방화셔터 도면생성 +> **라우트**: `GET /rd/fire-shutter-drawing` +> **참고**: 기존 `자동도면 생성` (`/rd/auto-drawing`) 구조를 확장 + +--- + +## 1. 개요 + +### 1.1 목적 + +방화셔터의 **가이드레일 단면**과 **셔터박스(케이스) 형태**를 파라미터로 입력하면, **2D 단면도(SVG)**와 **3D 렌더링(Three.js)**을 실시간으로 생성하는 도구를 제공한다. + +### 1.2 핵심 가치 + +| 기존 (수동) | 개선 (SAM 도면생성) | +|-------------|-------------------| +| CAD 프로그램에서 수동 작도 | 파라미터 입력 → 자동 도면 생성 | +| 도면 수정 시 전체 재작업 | 치수 변경 → 실시간 미리보기 | +| 제품별 도면 관리 어려움 | 프리셋 저장/불러오기로 재활용 | +| 영업/설치팀 도면 요청 대기 | 현장에서 즉시 단면도 확인 가능 | + +### 1.3 대상 사용자 + +- 설계팀: 방화셔터 단면 설계 및 검토 +- 영업팀: 고객 제안 시 단면도/3D 이미지 첨부 +- 설치팀: 현장 설치 전 가이드레일/케이스 형태 확인 +- 생산팀: 절곡/제작 사양 확인 + +--- + +## 2. 방화셔터 핵심 구조 + +### 2.1 전체 구성도 + +``` +┌─────────────────── 천장 슬래브 ───────────────────┐ +│ │ +│ ┌──────────── 셔터박스 (HEAD BOX / CASE) ──────┐ │ +│ │ ┌─────┐ ┌─────┐ │ │ +│ │ │브래킷│ [샤프트+슬랫 감김] │브래킷│ │ │ +│ │ └──┬──┘ [모터+감속기+브레이크] └──┬──┘ │ │ +│ │ │ [밸런스 스프링] │ │ │ +│ └─────┼─────────────────────────────────┼──────┘ │ +│ │ │ │ +│ ┌─────┴─────┐ ┌──────┴─────┐ │ +│ │ 가이드레일 │ ← 슬랫 커튼 → │ 가이드레일 │ │ +│ │ (좌) │ (강판/스크린) │ (우) │ │ +│ │ │ │ │ │ +│ │ 연기차단재│ │연기차단재 │ │ +│ │ │ │ │ │ +│ └─────┬─────┘ └──────┬─────┘ │ +│ │ │ │ +│ ══════╧═══ 하장바 (BOTTOM BAR) ═════════╧═══════ │ +│ [고무 실링] │ +└────────────────── 바닥 ──────────────────────────┘ +``` + +### 2.2 주요 구성요소 상세 + +#### A. 가이드레일 (Guide Rail) + +- **형태**: C-채널 단면 (ㄷ자 형태) +- **재질**: 강판 2.3mm 이상 +- **기능**: 슬랫 커튼의 좌우 안내 + 연기 차단 +- **표준 길이**: 2,438mm / 3,305mm / 4,430mm (조합 사용) +- **수량**: 항상 **2개** (좌우 1쌍) +- **부속**: 연기차단재(Smoke Seal Packing), 앵커볼트 + +``` +가이드레일 단면 (상단에서 본 모습) + + ┌────────────┐ + │ │ ← 가이드레일 본체 (C-채널) + │ ┌──────┐ │ + │ │ 연기 │ │ + │ │ 차단 │ │ + │ │ 재 │ │ + │ │ │ │ + │ │슬랫 │ │ + │ │엣지→ ● │ + │ │ │ │ + │ │ 연기 │ │ + │ │ 차단 │ │ + │ │ 재 │ │ + │ └──────┘ │ + │ │ + └────────────┘ + ■■■■■■■■■■■■■ ← 방화벽 +``` + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `rail_width` | 레일 전체 폭 | mm | 65 | +| `rail_depth` | 레일 깊이 (채널 깊이) | mm | 50 | +| `rail_thickness` | 강판 두께 | mm | 2.3 | +| `rail_lip` | 립(입구) 높이 | mm | 15 | +| `seal_thickness` | 연기차단재 두께 | mm | 5 | +| `seal_depth` | 연기차단재 깊이 | mm | 40 | +| `slat_thickness` | 슬랫 두께 (끼워지는 부분) | mm | 1.6 | +| `rail_height` | 레일 전체 높이 | mm | 3305 | +| `anchor_spacing` | 앵커볼트 간격 | mm | 500 | + +#### B. 셔터박스 / 케이스 (Head Box / Case) + +- **형태**: 직사각형 박스 (상부 천장 부착) +- **재질**: 강판 1.6mm 이상 +- **기능**: 샤프트/모터/슬랫 감김 수납 +- **표준 규격**: 1500×380mm / 500×380mm (개구부 크기에 따라) + +``` +셔터박스 단면 (정면에서 본 모습) + + ┌─────────────────────────────────────────┐ ← 상판 + │ │ + │ [브래킷] ┌──── 샤프트 ────┐ [브래킷] │ + │ │ │ (슬랫 감김) │ │ │ + │ ├──────┤ ├──────┤ │ + │ │ │ ◎ 중심축 │ │ │ + │ │ └───────────────┘ │ │ + │ │ │ │ + │ │ [모터+감속기] [브레이크] │ │ + │ │ [밸런스 스프링] │ │ + │ │ + └───┬─────────────────────────────────┬───┘ ← 하판 (슬랫 출구) + │ ↓ 슬랫 하강 ↓ │ + └─────────────────────────────────┘ +``` + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `box_width` | 케이스 전체 폭 (= 개구부 폭 + 마진) | mm | 1500 | +| `box_height` | 케이스 높이 | mm | 380 | +| `box_depth` | 케이스 깊이 (전후) | mm | 380 | +| `box_thickness` | 케이스 강판 두께 | mm | 1.6 | +| `shaft_diameter` | 샤프트 직경 | mm | 120 | +| `shaft_offset_x` | 샤프트 중심 수평 오프셋 | mm | 0 | +| `shaft_offset_y` | 샤프트 중심 수직 오프셋 | mm | 0 | +| `motor_side` | 모터 위치 (좌/우) | - | 우 | +| `slat_exit_width` | 슬랫 출구 폭 | mm | 1400 | +| `bracket_width` | 브래킷 폭 | mm | 80 | + +#### C. 슬랫 (Steel Slat / Screen) + +- **강판형**: EGI 강판 1.6mm, C/S형 인터록킹 프로파일 +- **스크린형**: 실리카/와이어 원단, 가이드레일 11mm 홈 +- **피치**: 75~100mm + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `slat_type` | 슬랫 유형 (강판/스크린) | - | 강판 | +| `slat_pitch` | 슬랫 피치 | mm | 80 | +| `slat_thickness` | 슬랫 두께 | mm | 1.6 | +| `slat_profile` | 단면 형태 (C형/S형) | - | C형 | + +#### D. 하장바 (Bottom Bar) + +- **기능**: 슬랫 커튼 하단 마감 + 바닥 밀착 +- **부속**: 고무 실링 + +**파라미터**: + +| 파라미터 | 설명 | 단위 | 기본값 | +|---------|------|------|--------| +| `bar_width` | 하장바 폭 | mm | 60 | +| `bar_height` | 하장바 높이 | mm | 40 | +| `bar_seal_height` | 고무 실링 높이 | mm | 15 | + +--- + +## 3. 기능 설계 + +### 3.1 탭 구성 + +기존 자동도면 생성의 탭 구조를 참고하여 4개 탭으로 구성한다. + +``` +┌──────────┬──────────┬──────────┬──────────┐ +│ 설정 │ 가이드 │ 셔터박스 │ 3D │ +│ Settings │ 레일 │ (케이스) │ 렌더링 │ +└──────────┴──────────┴──────────┴──────────┘ +``` + +| 탭 | ID | 기능 | +|----|----|------| +| **설정** | `Settings` | 제품 유형 선택, 개구부 크기, 전역 설정 | +| **가이드레일** | `GuideRail` | 가이드레일 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | +| **셔터박스** | `ShutterBox` | 셔터박스 단면 파라미터 입력 + SVG 단면도 실시간 미리보기 | +| **3D 렌더링** | `3D` | 전체 방화셔터 조립체 3D 렌더링 (Three.js) | + +### 3.2 설정 탭 (Settings) + +#### 입력 항목 + +| 항목 | 타입 | 설명 | +|------|------|------| +| 제품 유형 | 드롭다운 | 강판형 (KFS) / 스크린형 (KSS) | +| 제품 모델 | 드롭다운 | KSS01, KSS02, KFS01 등 (유형 선택 시 필터링) | +| 개구부 폭 (W0) | 숫자 입력 | mm | +| 개구부 높이 (H0) | 숫자 입력 | mm | +| 수량 | 숫자 입력 | 기본값 1 | + +#### 자동 계산 (표시 전용) + +| 항목 | 수식 | 설명 | +|------|------|------| +| 제작 폭 (W1) | 스크린: W0+140 / 강판: W0+110 | 마진 포함 | +| 제작 높이 (H1) | H0+350 | 마진 포함 | +| 면적 (M) | W1 × H1 / 1,000,000 | m² | +| 중량 (K) | 스크린: M×2 / 강판: M×25 | kg | +| 권장 모터 | K 기준 자동 선택 | 150K~1500K | + +#### 프리셋 관리 + +- **프리셋 저장**: 현재 파라미터를 이름 지정하여 localStorage에 저장 +- **프리셋 불러오기**: 저장된 프리셋 목록에서 선택하여 파라미터 복원 +- **기본 프리셋**: 강판형 기본, 스크린형 기본 (제품 유형 선택 시 자동 적용) + +### 3.3 가이드레일 탭 + +#### UI 구성 (2컬럼 레이아웃) + +``` +┌──────────────────────┬──────────────────────────────────┐ +│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ +│ │ │ +│ ■ 레일 전체 폭: [65] │ │ +│ ■ 레일 깊이: [50] │ ┌────────┐ │ +│ ■ 강판 두께: [2.3] │ │ │ │ +│ ■ 립 높이: [15] │ │ ┌────┐ │ ← SVG 실시간 │ +│ ■ 연기차단재: [5/40] │ │ │ ● │ │ 렌더링 │ +│ ■ 슬랫 두께: [1.6] │ │ └────┘ │ │ +│ │ │ │ │ +│ [치수 표시 ON/OFF] │ └────────┘ │ +│ [연기차단재 ON/OFF] │ ← 치수 라벨 (mm) │ +│ │ │ +│ ■ 레일 높이: [3305] │ [줌 +] [줌 -] [리셋] [DXF 저장] │ +│ ■ 앵커 간격: [500] │ │ +└──────────────────────┴──────────────────────────────────┘ +``` + +#### SVG 단면도 렌더링 상세 + +**뷰 모드 3가지**: + +1. **횡단면도 (Cross-Section)**: 가이드레일을 위에서 본 단면 — 슬랫이 레일에 끼워진 형태 +2. **종단면도 (Longitudinal)**: 가이드레일을 측면에서 본 단면 — 앵커볼트 배치 +3. **정면도 (Front View)**: 가이드레일을 정면에서 본 모습 — 레일 전체 높이 + 앵커 위치 + +**렌더링 요소**: + +| 요소 | 색상 | 설명 | +|------|------|------| +| 레일 본체 | `#94a3b8` (은회색) | 강판 단면 | +| 연기차단재 | `#f97316` (주황) | 실링 재질 | +| 슬랫 엣지 | `#60a5fa` (파랑) | 레일 안의 슬랫 | +| 방화벽 | `#a1887f` (갈색 해칭) | 콘크리트 벽 | +| 앵커볼트 | `#ef4444` (빨강) | 고정 부속 | +| 치수선 | `#3b82f6` (파랑) | mm 단위 치수 | + +### 3.4 셔터박스 탭 + +#### UI 구성 (2컬럼 레이아웃) + +``` +┌──────────────────────┬──────────────────────────────────┐ +│ 왼쪽: 파라미터 입력 │ 오른쪽: SVG 단면도 미리보기 │ +│ │ │ +│ ■ 케이스 폭: [1500] │ ┌────────────────────────────┐ │ +│ ■ 케이스 높이: [380] │ │ [브래킷] ◎샤프트 [브래킷]│ │ +│ ■ 케이스 깊이: [380] │ │ 감김 슬랫 │ │ +│ ■ 강판 두께: [1.6] │ │ [모터+감속기] [브레이크] │ │ +│ │ └────────────────────────────┘ │ +│ ■ 샤프트 직경: [120] │ │ +│ ■ 샤프트 오프셋 │ ← SVG 실시간 렌더링 │ +│ X: [0] Y: [0] │ ← 치수 라벨 (mm) │ +│ ■ 모터 위치: [좌/우] │ │ +│ │ │ +│ ■ 내부 부품 표시 │ [줌 +] [줌 -] [리셋] [DXF 저장] │ +│ □ 샤프트 │ │ +│ □ 모터/감속기 │ │ +│ □ 브레이크 │ │ +│ □ 밸런스 스프링 │ │ +└──────────────────────┴──────────────────────────────────┘ +``` + +#### SVG 단면도 렌더링 상세 + +**뷰 모드 3가지**: + +1. **정면 단면도**: 케이스를 정면에서 본 내부 구조 (샤프트, 모터, 브래킷 위치) +2. **측면 단면도**: 케이스를 측면에서 본 단면 (깊이 방향, 슬랫 감김 단면) +3. **하부 상세도**: 슬랫 출구 부분 확대 + +**렌더링 요소**: + +| 요소 | 색상 | 설명 | +|------|------|------| +| 케이스 외곽 | `#94a3b8` (은회색) | 강판 박스 | +| 샤프트 | `#64748b` (짙은 회색) | 중심축 + 감김 슬랫 | +| 모터 | `#3b82f6` (파랑) | 전동 개폐기 | +| 브레이크 | `#ef4444` (빨강) | 전자 브레이크 | +| 스프링 | `#22c55e` (녹색) | 밸런스 스프링 | +| 브래킷 | `#8b5cf6` (보라) | 벽 고정 브래킷 | +| 슬랫 | `#f59e0b` (주황) | 감긴 슬랫 단면 | + +### 3.5 3D 렌더링 탭 + +#### 렌더링 대상 + +Three.js를 사용하여 방화셔터 전체 조립체를 3D로 시각화한다. + +``` +3D 렌더링 요소: +├── 셔터박스 (반투명 상자) +│ ├── 샤프트 (원통) +│ ├── 감긴 슬랫 (원통 표면) +│ ├── 모터+감속기 (박스) +│ ├── 브레이크 (디스크) +│ └── 브래킷 (L형 판) +├── 가이드레일 좌 (C-채널 압출) +├── 가이드레일 우 (C-채널 압출) +├── 슬랫 커튼 (평면 텍스처) +│ ├── 강판형: 줄무늬 텍스처 (인터록킹 표현) +│ └── 스크린형: 반투명 메쉬 +├── 하장바 (직사각형 바) +└── 방화벽 (반투명 콘크리트 텍스처) +``` + +#### 3D 인터랙션 + +| 기능 | 조작 | 설명 | +|------|------|------| +| 회전 | 마우스 드래그 | OrbitControls | +| 줌 | 마우스 휠 | 확대/축소 | +| 팬 | 우클릭 드래그 | 시점 이동 | +| 부품 하이라이트 | 마우스 호버 | 해당 부품 강조 + 이름 표시 | +| 부품 ON/OFF | 체크박스 | 개별 부품 표시/숨김 | +| 투명도 | 슬라이더 | 케이스 투명도 조절 (내부 구조 확인) | +| 셔터 개폐 | 슬라이더 | 0%(전개)~100%(전폐) 애니메이션 | +| 조명 | 프리셋 | 기본/스튜디오/야외/드라마틱 | + +#### 3D 모델링 방식 + +DB나 외부 3D 파일 없이, **파라미터 기반 절차적 모델링(Procedural Modeling)**으로 구현한다. + +```javascript +// 가이드레일 C-채널 3D 생성 예시 (Three.js ExtrudeGeometry) +function createGuideRailMesh(params) { + const shape = new THREE.Shape(); + // C-채널 프로파일 경로 정의 + shape.moveTo(0, 0); + shape.lineTo(params.rail_width, 0); + shape.lineTo(params.rail_width, params.rail_lip); + shape.lineTo(params.rail_width - params.rail_thickness, params.rail_lip); + shape.lineTo(params.rail_width - params.rail_thickness, params.rail_thickness); + shape.lineTo(params.rail_thickness, params.rail_thickness); + shape.lineTo(params.rail_thickness, params.rail_lip); + shape.lineTo(0, params.rail_lip); + shape.lineTo(0, 0); + + // 높이 방향으로 압출 + const extrudeSettings = { + depth: params.rail_height, + bevelEnabled: false + }; + + return new THREE.Mesh( + new THREE.ExtrudeGeometry(shape, extrudeSettings), + new THREE.MeshStandardMaterial({ color: 0x94a3b8 }) + ); +} +``` + +### 3.6 출력 기능 + +| 기능 | 형식 | 설명 | +|------|------|------| +| **DXF 다운로드** | `.dxf` | 가이드레일/셔터박스 단면도를 CAD 호환 파일로 저장 | +| **PNG 다운로드** | `.png` | SVG 단면도를 이미지로 저장 | +| **3D 스크린샷** | `.png` | 3D 렌더링 현재 뷰를 이미지로 저장 | +| **파라미터 JSON** | `.json` | 현재 설정값을 파일로 내보내기/가져오기 | + +--- + +## 4. 기술 설계 + +### 4.1 아키텍처 + +기존 자동도면 생성과 동일한 **순수 클라이언트 측** 아키텍처를 사용한다. + +``` +┌─────────────────────────────────────────────┐ +│ Browser (Client-Side Only) │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Blade Template │ │ +│ │ (fire-shutter-drawing/index.blade) │ │ +│ ├─────────────────────────────────────┤ │ +│ │ JavaScript State Management │ │ +│ │ (fireShutterState 객체) │ │ +│ ├──────────┬──────────────────────────┤ │ +│ │ SVG 엔진 │ Three.js 3D 엔진 │ │ +│ │ (단면도) │ (조립체 렌더링) │ │ +│ ├──────────┴──────────────────────────┤ │ +│ │ DXF 생성기 │ PNG 내보내기 │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ DB 연동: 없음 (localStorage 프리셋만 사용) │ +│ API 호출: 없음 │ +└─────────────────────────────────────────────┘ +``` + +### 4.2 파일 구조 + +``` +mng/ +├── routes/web.php ← 라우트 추가 +├── app/Http/Controllers/RdController.php ← 메서드 추가 +└── resources/views/rd/fire-shutter-drawing/ + ├── index.blade.php ← 메인 레이아웃 + 탭 UI + ├── partials/ + │ ├── _settings.blade.php ← 설정 탭 HTML + │ ├── _guide-rail.blade.php ← 가이드레일 탭 HTML + │ ├── _shutter-box.blade.php ← 셔터박스 탭 HTML + │ └── _3d-viewer.blade.php ← 3D 렌더링 탭 HTML + └── js/ + (인라인 또는 @push('scripts')에 포함) +``` + +> **참고**: 기존 `auto-drawing/index.blade.php`는 단일 파일 4,884줄이다. 유지보수성을 위해 **Blade partial로 분리**하되, JavaScript는 상태 공유가 필요하므로 메인 파일의 `@push('scripts')`에 통합한다. + +### 4.3 상태 관리 객체 + +```javascript +const fireShutterState = { + // 활성 탭 + activeTab: 'Settings', + + // 설정 탭 + settings: { + productType: 'steel', // 'steel' | 'screen' + productModel: 'KFS01', // 제품 모델 코드 + openWidth: 2000, // 개구부 폭 W0 (mm) + openHeight: 3000, // 개구부 높이 H0 (mm) + quantity: 1, + // 자동 계산 + mfgWidth: 0, // 제작 폭 W1 + mfgHeight: 0, // 제작 높이 H1 + area: 0, // 면적 M (m²) + weight: 0, // 중량 K (kg) + motorSpec: '', // 권장 모터 + }, + + // 가이드레일 파라미터 + guideRail: { + width: 65, + depth: 50, + thickness: 2.3, + lip: 15, + sealThickness: 5, + sealDepth: 40, + slatThickness: 1.6, + height: 3305, + anchorSpacing: 500, + // 뷰 옵션 + showDimensions: true, + showSeal: true, + viewMode: 'cross', // 'cross' | 'longitudinal' | 'front' + }, + + // 셔터박스 파라미터 + shutterBox: { + width: 1500, + height: 380, + depth: 380, + thickness: 1.6, + shaftDiameter: 120, + shaftOffsetX: 0, + shaftOffsetY: 0, + motorSide: 'right', // 'left' | 'right' + slatExitWidth: 1400, + bracketWidth: 80, + // 내부 부품 표시 + showShaft: true, + showMotor: true, + showBrake: true, + showSpring: true, + viewMode: 'front', // 'front' | 'side' | 'bottom' + }, + + // 슬랫 파라미터 + slat: { + type: 'steel', // 'steel' | 'screen' + pitch: 80, + thickness: 1.6, + profile: 'C', // 'C' | 'S' + }, + + // 하장바 파라미터 + bottomBar: { + width: 60, + height: 40, + sealHeight: 15, + }, + + // 3D 뷰 설정 + threeD: { + caseOpacity: 0.3, // 케이스 투명도 + shutterPosition: 100, // 0=전개, 100=전폐 + showComponents: { + case: true, + shaft: true, + motor: true, + brake: true, + spring: true, + guideRailL: true, + guideRailR: true, + slats: true, + bottomBar: true, + wall: true, + }, + lightPreset: 'default', + }, + + // 프리셋 관리 + presets: [], // localStorage에서 로드 + + // 뷰 컨트롤 (줌/팬) + view: { + scale: 1, + offset: { x: 0, y: 0 }, + isDragging: false, + }, +}; +``` + +### 4.4 제품 유형별 기본값 매핑 + +```javascript +const PRODUCT_DEFAULTS = { + steel: { + label: '강판형', + marginW: 110, // W1 = W0 + 110 + marginH: 350, // H1 = H0 + 350 + weightFactor: 25, // K = M × 25 + guideRail: { width: 65, depth: 50, thickness: 2.3, lip: 15 }, + slat: { type: 'steel', pitch: 80, thickness: 1.6, profile: 'C' }, + }, + screen: { + label: '스크린형', + marginW: 140, // W1 = W0 + 140 + marginH: 350, // H1 = H0 + 350 + weightFactor: 2, // K = M × 2 + guideRail: { width: 30, depth: 25, thickness: 1.5, lip: 11 }, + slat: { type: 'screen', pitch: 100, thickness: 0.8, profile: 'flat' }, + }, +}; + +const MOTOR_TABLE = [ + { maxWeight: 150, spec: '150K', inch: 4 }, + { maxWeight: 300, spec: '300K', inch: 4 }, + { maxWeight: 500, spec: '500K', inch: 5 }, + { maxWeight: 750, spec: '750K', inch: 5 }, + { maxWeight: 1000, spec: '1000K', inch: 6 }, + { maxWeight: 1500, spec: '1500K', inch: 6 }, +]; +``` + +--- + +## 5. 개발 단계 + +### Phase 1: 기본 구조 + 가이드레일 단면도 (1단계) + +> **목표**: 라우트/컨트롤러/뷰 생성, 설정 탭, 가이드레일 SVG 단면도 + +| 작업 | 상세 | 예상 | +|------|------|------| +| 라우트 등록 | `GET /rd/fire-shutter-drawing` | 10분 | +| 컨트롤러 메서드 | `RdController@fireShutterDrawing` | 10분 | +| 레이아웃 + 탭 UI | 4탭 구조, 다크 테마 | 1시간 | +| 설정 탭 | 제품 유형/개구부 크기 입력 + 자동 계산 | 1시간 | +| 가이드레일 SVG 엔진 | C-채널 단면도 + 치수선 + 연기차단재 | 3시간 | +| 줌/팬 컨트롤 | 기존 auto-drawing 코드 재사용 | 30분 | + +**산출물**: 가이드레일 파라미터 입력 → SVG 횡단면도 실시간 렌더링 + +### Phase 2: 셔터박스 단면도 (2단계) + +> **목표**: 셔터박스(케이스) SVG 단면도 + 내부 부품 표시 + +| 작업 | 상세 | 예상 | +|------|------|------| +| 셔터박스 SVG 엔진 | 케이스 외곽 + 내부 구조 | 3시간 | +| 내부 부품 렌더링 | 샤프트, 모터, 브레이크, 스프링 | 2시간 | +| 뷰 모드 전환 | 정면/측면/하부 | 1시간 | +| 부품 ON/OFF 토글 | 체크박스 → SVG 요소 표시/숨김 | 30분 | + +**산출물**: 셔터박스 파라미터 입력 → SVG 단면도 (정면/측면/하부) + +### Phase 3: 3D 렌더링 (3단계) + +> **목표**: Three.js 기반 방화셔터 전체 조립체 3D 렌더링 + +| 작업 | 상세 | 예상 | +|------|------|------| +| Three.js 씬 구축 | 카메라, 조명, OrbitControls | 1시간 | +| 가이드레일 3D 모델 | ExtrudeGeometry (C-채널 압출) | 2시간 | +| 셔터박스 3D 모델 | BoxGeometry + 내부 부품 | 2시간 | +| 슬랫 커튼 3D 모델 | 평면 메쉬 + 텍스처 | 1시간 | +| 셔터 개폐 애니메이션 | 슬라이더 → 슬랫 위치 변경 | 1시간 | +| 조명/투명도 패널 | 기존 auto-drawing 패널 재사용 | 30분 | + +**산출물**: 파라미터 연동 3D 방화셔터 조립체 + 인터랙션 + +### Phase 4: 출력 + 프리셋 (4단계) + +> **목표**: DXF/PNG 저장, 프리셋 관리, 완성도 향상 + +| 작업 | 상세 | 예상 | +|------|------|------| +| DXF 내보내기 | 기존 DXF 생성기 확장 | 1시간 | +| PNG 내보내기 | SVG → Canvas → PNG | 30분 | +| 3D 스크린샷 | Three.js renderer.domElement.toDataURL | 15분 | +| 프리셋 저장/불러오기 | localStorage CRUD | 1시간 | +| JSON 가져오기/내보내기 | 파일 업로드/다운로드 | 30분 | +| UI 다듬기 | 반응형, 툴팁, 키보드 단축키 | 1시간 | + +**산출물**: 완성된 방화셔터 도면생성 도구 + +--- + +## 6. UI/UX 설계 + +### 6.1 디자인 시스템 + +기존 자동도면 생성의 **다크 테마 (Space Theme)**를 그대로 이어간다. + +| 요소 | 값 | +|------|------| +| 배경 | `#020617` (slate-950) | +| 패널 | `rgba(15, 23, 42, 0.7)` + backdrop-blur | +| 테두리 | `rgba(255, 255, 255, 0.1)` | +| 강조색 | `#3b82f6` (blue-500) | +| 텍스트 | `#f8fafc` (white) / `#94a3b8` (slate-400) | +| 입력 필드 | `bg-slate-950/80` + `border-slate-800` | + +### 6.2 반응형 레이아웃 + +``` +Desktop (1200px+): 2컬럼 (4:8 비율) +Tablet (768-1199px): 1컬럼 (상: 파라미터, 하: 미리보기) +Mobile: 지원 안 함 (최소 768px) +``` + +### 6.3 인터랙션 흐름 + +``` +사용자 → 제품 유형 선택 (강판/스크린) + → 기본값 자동 적용 + → 개구부 크기 입력 + → 제작 치수/중량/모터 자동 계산 + + → 가이드레일 탭 이동 + → 파라미터 조정 (폭, 깊이, 두께 등) + → SVG 실시간 업데이트 (입력 즉시) + → 뷰 모드 전환 (횡단면/종단면/정면) + → DXF 다운로드 가능 + + → 셔터박스 탭 이동 + → 파라미터 조정 + → SVG 실시간 업데이트 + → 내부 부품 ON/OFF + + → 3D 탭 이동 + → 전체 조립체 3D 뷰 + → 셔터 개폐 애니메이션 + → 스크린샷 저장 +``` + +--- + +## 7. 메뉴 등록 + +### 7.1 메뉴 위치 + +``` +R&D +├── 대시보드 +├── 조직도 관리 +├── 중대재해처벌법 점검 +├── AI 견적 +├── 기획디자인 +├── 디자인 인사이트 +├── 사운드 로고 스튜디오 +├── CM송 제작 +├── 자동도면 생성 ← 기존 +└── 방화셔터 도면생성 ← 신규 (자동도면 하위에 배치) +``` + +### 7.2 메뉴 등록 (tinker) + +```php +// 개발 서버 +App\Models\Commons\Menu::create([ + 'tenant_id' => 1, + 'parent_id' => , + 'name' => '방화셔터 도면생성', + 'url' => '/rd/fire-shutter-drawing', + 'icon' => 'shield', + 'sort_order' => <자동도면 다음 순서>, + 'is_active' => true, +]); +``` + +> **주의**: 메뉴 시더 실행 금지 — tinker로 수동 등록 + +--- + +## 8. 향후 확장 가능성 + +| 확장 | 설명 | 우선순위 | +|------|------|---------| +| **STL/OBJ 내보내기** | 3D 프린팅/CAD 호환 | 🟡 중요 | +| **견적 연동** | 도면 파라미터 → 견적 자동 산출 | 🔴 필수 | +| **제품 카탈로그 연동** | DB에서 제품별 기본 파라미터 로드 | 🟡 중요 | +| **비교 모드** | 2개 설정을 나란히 비교 | 🟢 권장 | +| **PDF 도면 출력** | A3/A4 도면 양식 포함 출력 | 🟡 중요 | +| **설치 시뮬레이션** | 현장 사진 위에 3D 오버레이 | 🟢 권장 | + +--- + +## 관련 문서 + +- `/home/aweso/sam/docs/features/academy/fire-shutter-image-prompts.md` — 방화셔터 이미지 프롬프트 +- `/home/aweso/sam/docs/samples/방화셔터_견적구조_인터뷰.md` — 견적 구조 인터뷰 +- `/home/aweso/sam/docs/features/quotes/README.md` — 견적 시스템 분석 +- `/home/aweso/sam/docs/projects/quotation/phase-1-5130-analysis/js-formulas.md` — 견적 수식 상세 +- `/home/aweso/sam/mng/resources/views/rd/auto-drawing/index.blade.php` — 기존 자동도면 생성 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/plans/sound-logo-generator-plan.md b/plans/sound-logo-generator-plan.md new file mode 100644 index 0000000..a3ecf48 --- /dev/null +++ b/plans/sound-logo-generator-plan.md @@ -0,0 +1,637 @@ +# 사운드 로고 생성기 — 기획서 + +> **작성일**: 2026-03-08 +> **상태**: 기획 확정 +> **메뉴명**: 사운드 로고 생성기 +> **라우트**: `GET /rd/sound-logo` +> **담당**: R&D실 + +--- + +## 1. 개요 + +### 1.1 목적 + +1~5초의 **짧고 강렬한 시그니처 사운드(Sound Logo)**를 생성하는 도구. 브라우저 내 Web Audio API 신디사이저와 Google Gemini AI를 결합하여, 누구나 전문적인 사운드 로고를 만들 수 있도록 한다. + +### 1.2 벤치마킹 + +| 브랜드 | 사운드 | 길이 | 특징 | +|--------|--------|------|------| +| Intel | 봉-봉봉봉-봉 | 1.5초 | 5음, 밝고 미래적 | +| Netflix | 타-둠 | 3초 | 2음, 깊은 울림 + 리버브 | +| Samsung | 오버더호라이즌 | 2초 | 5음, 따뜻한 멜로디 | +| McDonald's | 바다바바~ | 2초 | 5음, 경쾌한 리듬 | +| Windows | 시작 사운드 | 3초 | 4음, 화음 진행 | +| 카카오톡 | 카톡~ | 0.5초 | 2음, 귀여운 효과음 | +| T-Mobile | 띠-띠띠-띠-띠 | 1초 | 5음, 단순 반복 | + +### 1.3 핵심 차별점 — AI 어드바이저 엔진 + +단순 신디사이저 도구가 아니라, **Google Gemini AI가 음악 이론 기반으로 조언하고 생성을 도와주는** 지능형 도구. + +``` +┌─────────────────────────────────────────────────────┐ +│ 사운드 로고 생성기 │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 수동 모드 │ │ AI 어시스트│ │ AI 자동 │ │ +│ │ (신디사이저)│ ←→ │ (Gemini) │ ←→ │ (Lyria) │ │ +│ │ │ │ │ │ │ │ +│ │ 음표 직접 │ │ 브랜드 분석│ │ 프롬프트→ │ │ +│ │ 배치/편집 │ │ 음악 추천 │ │ AI 음악 │ │ +│ │ │ │ 코드 제안 │ │ 직접 생성 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ ↕ ↕ ↕ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Web Audio API 재생 엔진 │ │ +│ │ (실시간 미리듣기 + WAV 내보내기) │ │ +│ └──────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 3가지 모드 설계 + +### 2.1 모드 A — 수동 모드 (신디사이저) + +> 음표를 직접 배치하고 파라미터를 조절하여 사운드를 만드는 전통적 방식 + +| 기능 | 설명 | +|------|------| +| 음표 시퀀서 | 음표(C4, E4 등) + 길이(0.05~2초) + 쉼표를 시각적 배열로 편집 | +| 신디사이저 4종 | Sine(부드러움), Square(8bit), Triangle(따뜻함), Sawtooth(날카로움) | +| ADSR 엔벨로프 | Attack(0~500ms), Decay(0~1s), Sustain(0~1), Release(0~3s) | +| 화음(Chord) | 동시에 여러 음 재생 (C+E+G = C Major 등) | +| 이펙트 | 리버브, 딜레이, 로우패스/하이패스 필터 | +| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 표시 | + +**기술 기반**: Web Audio API (`OscillatorNode`, `GainNode`, `BiquadFilterNode`, `ConvolverNode`) + +### 2.2 모드 B — AI 어시스트 (Gemini 텍스트) + +> 브랜드 정보를 입력하면 Gemini가 **음악 이론 기반으로 사운드 로고를 설계**해 주고, 사용자가 미세 조정 + +**입력 → AI 분석 → 음표 데이터 출력 → Web Audio 재생** + +``` +사용자 입력 Gemini 분석·추천 출력 +┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ 브랜드명 │ │ │ │ │ +│ 업종/분위기 │──요청──→ │ 1. 브랜드 성격 분석 │──JSON──→ │ 음표 시퀀스 │ +│ 키워드 │ │ 2. 조성/스케일 추천 │ │ BPM, 조성 │ +│ 참고 브랜드 │ │ 3. 음표 시퀀스 생성 │ │ 신디 파라미터 │ +│ 원하는 느낌 │ │ 4. ADSR 파라미터 제안 │ │ ADSR 값 │ +└───────────────┘ │ 5. 이펙트 추천 │ │ 이펙트 설정 │ + │ 6. 근거 설명 │ │ │ + └──────────────────────┘ └──────────────┘ + ↓ + Web Audio 재생 + ↓ + 사용자 미세 조정 +``` + +**Gemini 프롬프트 설계**: + +``` +당신은 사운드 브랜딩 전문가이자 음악 이론가입니다. +다음 브랜드 정보를 분석하여 1~5초 사운드 로고를 설계해 주세요. + +[브랜드 정보] +- 브랜드명: {name} +- 업종: {industry} +- 브랜드 성격: {personality} (예: 혁신적, 신뢰, 친근함) +- 참고 사운드: {reference} (예: 인텔처럼 밝은 느낌) +- 원하는 길이: {duration}초 + +[응답 형식 - 반드시 JSON으로] +{ + "analysis": "브랜드 분석 설명 (한글)", + "reasoning": "이 사운드를 추천하는 음악 이론적 근거 (한글)", + "key": "C", + "scale": "major", + "bpm": 120, + "synth": "sine", + "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, + "effects": { "reverb": 0.3, "delay": 0 }, + "notes": [ + { "note": "C5", "duration": 0.2, "velocity": 0.8 }, + { "note": "E5", "duration": 0.2, "velocity": 0.9 }, + { "note": "G5", "duration": 0.15, "velocity": 0.7 }, + { "rest": 0.05 }, + { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } + ], + "variations": [ + { "name": "밝은 버전", "notes": [...] }, + { "name": "차분한 버전", "notes": [...] } + ] +} +``` + +**AI 어시스트 기능 상세**: + +| 기능 | 설명 | +|------|------| +| 브랜드 분석 | 업종·성격 기반 적합한 조성/템포/음색 추천 | +| 음표 시퀀스 생성 | 음악 이론(화성학, 리듬 패턴) 기반 멜로디 제안 | +| 변형 3종 제공 | 밝은/차분한/임팩트 버전 동시 생성 | +| 근거 설명 | "C Major → 신뢰감, 5도 상행 → 상승 에너지" 등 이론 설명 | +| 반복 개선 | "좀 더 밝게" "더 짧게" 등 자연어로 수정 요청 | + +### 2.3 모드 C — AI 자동 생성 (Google Lyria) + +> Google Lyria AI가 프롬프트 기반으로 **실제 음악을 직접 생성** + +**2가지 Lyria 엔진 지원** (기존 API 키로 사용 가능, 별도 발급 불필요): + +| 엔진 | 인증 | 방식 | 특징 | +|------|------|------|------| +| **Lyria RealTime** (권장) | 기존 Gemini API 키 | WebSocket (브라우저 직접) | 실시간 스트리밍, BPM/스케일 실시간 조절 | +| Lyria 2 (폴백) | Vertex AI 서비스 계정 | REST API (서버 경유) | 30초 단위 파일 생성, $0.06/30초 | + +#### Lyria RealTime — 브라우저에서 직접 음악 생성 + +``` +브라우저 (Alpine.js) Google API +┌───────────────────┐ ┌──────────────────┐ +│ BPM: 130 │ │ │ +│ Scale: C Major │──WebSocket 연결──→ │ Lyria RealTime │ +│ 프롬프트 입력 │ │ (lyria-realtime- │ +│ │←─2초 단위 오디오── │ exp) │ +│ 🔊 실시간 재생 │ │ │ +│ BPM 슬라이더 조절 │──실시간 파라미터──→ │ 즉시 반영 │ +└───────────────────┘ └──────────────────┘ +``` + +- **모델**: `lyria-realtime-exp` (experimental, Gemini API v1alpha) +- **인증**: 기존 `.env`의 `GEMINI_API_KEY` 그대로 사용 +- **출력**: 48kHz 스테레오, 2초 청크 단위 스트리밍 +- **제어 파라미터**: BPM(60~200), Scale(Key + Mode), 텍스트 프롬프트 +- **지연**: 파라미터 변경 후 최대 2초 이내 반영 + +**Lyria RealTime 프롬프트 예시**: + +``` +Short sonic logo. Bright, futuristic, memorable melody. +Clean synthesizer with light reverb. +Ascending progression, major chord resolution. +``` + +#### Lyria 2 — 서버 경유 파일 생성 (폴백) + +기존 `BgmService::generateWithLyria()` 패턴 재활용. Vertex AI 서비스 계정(`google_service_account.json`) 이미 보유. + +``` +사용자 입력 서버 (Laravel) 결과 +┌───────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ 분위기 선택 │ │ SoundLogoService │ │ │ +│ 길이 (1~5초) │──POST──→ │ → Lyria 2 API (REST) │──→ │ WAV/MP3 파일 │ +│ 프롬프트 입력 │ │ (Vertex AI) │ │ (다운로드) │ +└───────────────┘ └──────────────────────┘ └──────────────┘ +``` + +> **우선순위**: Lyria RealTime(브라우저) 먼저 시도 → 실패 시 Lyria 2(서버) 폴백. +> 두 엔진 모두 기존 인증 정보로 사용 가능하며 별도 API 키 발급 불필요. + +--- + +## 3. UI 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎵 사운드 로고 생성기 [내 프로젝트 ▾] [저장] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 모드: [🎹 수동] [🤖 AI 어시스트] [✨ AI 자동] │ +│ │ +│ ┌─ 모드 B: AI 어시스트 ──────────────────────────────────────┐ │ +│ │ │ │ +│ │ 브랜드명 [SAM ] │ │ +│ │ 업종 [ERP/MES 통합 솔루션 ] │ │ +│ │ 브랜드 성격 [○혁신적 ●신뢰 ○친근 ○고급 ○에너지] │ │ +│ │ 참고 사운드 [인텔처럼 짧고 밝은 ▾ ] │ │ +│ │ 길이 [━━━●━━━ 2초 ] │ │ +│ │ │ │ +│ │ [🤖 AI에게 사운드 설계 요청] │ │ +│ │ │ │ +│ │ ┌─ AI 분석 결과 ─────────────────────────────────────┐ │ │ +│ │ │ 💡 "SAM은 ERP/MES 통합 솔루션으로, 신뢰와 기술력을 │ │ │ +│ │ │ 전달해야 합니다. C Major 조성으로 안정감을, │ │ │ +│ │ │ 5도 상행 진행으로 성장과 발전을 표현합니다." │ │ │ +│ │ │ │ │ │ +│ │ │ 추천: C Major | BPM 130 | Sine + 리버브 30% │ │ │ +│ │ │ │ │ │ +│ │ │ 변형 3종: │ │ │ +│ │ │ [▶ 밝은 버전] [▶ 차분한 버전] [▶ 임팩트 버전] │ │ │ +│ │ └─────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ 💬 추가 요청: [좀 더 짧고 강렬하게 해줘 ] [전송] │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 음표 에디터 (AI 결과 또는 수동 편집) ──────────────────────┐ │ +│ │ C5(0.2s) E5(0.2s) G5(0.15s) .(0.05s) [CEG](0.8s) │ │ +│ │ ████ ████ ███ · ████████████ │ │ +│ │ [+음표] [+쉼표] [+화음] [삭제] │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 파라미터 ────────────────────────────────────────────────┐ │ +│ │ 음색: ●Sine ○Square ○Triangle ○Sawtooth │ │ +│ │ BPM: ━━━━━━●━━━━━ 130 │ │ +│ │ 리버브: ━━━●━━━━━━━ 30% │ │ +│ │ Attack: ━●━━━━━━━━ 10ms Release: ━━━━━●━━━ 500ms │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 파형 시각화 ─────────────────────────────────────────────┐ │ +│ │ ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿ │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ [▶ 재생] [⏹ 정지] [💾 WAV 저장] [📋 JSON 내보내기] │ +│ │ +├─ 내 사운드 라이브러리 ──────────────────────────────────────────┤ +│ 🎵 SAM 시그널 v1 | 🎵 알림음 v2 | 🎵 전환 효과 | [+ 새로] │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 프리셋 템플릿 (10종) + +| 프리셋 | 스타일 | 음표 수 | 길이 | 용도 | +|--------|--------|---------|------|------| +| 기업 시그널 (밝음) | Intel 스타일 | 5 | 1.5초 | 브랜드 인트로 | +| 기업 시그널 (무게감) | Netflix 스타일 | 2 | 3초 | 프리미엄 브랜드 | +| 알림음 (경쾌) | 카카오톡 스타일 | 2~3 | 0.5초 | 푸시 알림 | +| 알림음 (정보) | Slack 스타일 | 3 | 1초 | 시스템 알림 | +| 성공 사운드 | 게임 레벨업 | 4 | 1초 | 작업 완료 | +| 에러 사운드 | 경고음 | 2 | 0.5초 | 오류 알림 | +| 전환 효과 (업) | 상승 스윕 | 연속 | 0.5초 | 화면 전환 | +| 전환 효과 (다운) | 하강 스윕 | 연속 | 0.5초 | 메뉴 닫기 | +| 팡파레 | 축하 | 6 | 2초 | 이벤트/달성 | +| 로딩 루프 | 반복 패턴 | 4 | 2초 | 대기 상태 | + +--- + +## 5. 기술 아키텍처 + +### 5.1 기술 스택 + +| 계층 | 기술 | 설명 | +|------|------|------| +| 프론트엔드 | Blade + Alpine.js | 단일 파일 SPA (디자인 인사이트 패턴) | +| 오디오 엔진 | Web Audio API | `OscillatorNode`, `GainNode`, `ConvolverNode` | +| 시각화 | Canvas API | `AnalyserNode` → 파형/스펙트럼 렌더링 | +| AI 어시스트 | Gemini 2.5 Flash | 텍스트 기반 음악 이론 분석·추천 (모드 B) | +| AI 자동 생성 (1차) | Lyria RealTime | WebSocket 실시간 음악 스트리밍 (모드 C, 브라우저 직접) | +| AI 자동 생성 (폴백) | Lyria 2 (Vertex AI) | REST API 파일 생성 (모드 C, 서버 경유) | +| 저장 | localStorage + DB | 프로젝트 데이터(localStorage), 음원 파일(DB+Storage) | + +### 5.2 기존 인프라 재활용 + +| 기존 코드 | 재활용 내용 | +|----------|-----------| +| `BgmService::generateWithLyria()` | Lyria API 호출 패턴, Vertex AI 인증 흐름 | +| `BgmService::getMoodChord()` | 분위기별 화음 주파수 매핑 | +| `BgmService::generateAmbient()` | FFmpeg 기반 오디오 합성 (서버사이드 폴백) | +| `CmSongController::generateLyrics()` | Gemini API 호출 패턴 (프롬프트 → JSON 응답) | +| `CmSongController::pcmToWav()` | PCM → WAV 변환 유틸리티 | +| `AiConfig::getActiveGemini()` | AI 설정 조회 (API 키, 모델, 리전) | +| `GoogleCloudService::getAccessToken()` | Vertex AI 인증 토큰 | + +### 5.3 파일 구조 + +``` +app/Http/Controllers/Rd/ +└── SoundLogoController.php # 컨트롤러 (AI API 프록시) + +app/Services/Rd/ +└── SoundLogoService.php # Gemini 프롬프트 + Lyria 호출 + +resources/views/rd/sound-logo/ +└── index.blade.php # 단일 파일 SPA + +routes/web.php # 라우트 추가 +``` + +### 5.4 라우트 설계 + +| Method | Path | 컨트롤러 | 설명 | +|--------|------|---------|------| +| `GET` | `/rd/sound-logo` | `soundLogo.index` | 메인 페이지 | +| `POST` | `/rd/sound-logo/ai-assist` | `soundLogo.aiAssist` | Gemini 음악 설계 요청 (모드 B) | +| `POST` | `/rd/sound-logo/ai-refine` | `soundLogo.aiRefine` | Gemini 추가 수정 요청 | +| `POST` | `/rd/sound-logo/ai-generate` | `soundLogo.aiGenerate` | Lyria 음악 생성 (모드 C) | +| `POST` | `/rd/sound-logo/save` | `soundLogo.save` | 사운드 저장 (DB + Storage) | +| `GET` | `/rd/sound-logo/{id}/download` | `soundLogo.download` | WAV 다운로드 | + +### 5.5 Web Audio API 핵심 구조 + +```javascript +// 노드 그래프 +const ctx = new AudioContext(); + +// Oscillator → Gain(ADSR) → Filter → Reverb → Analyser → Destination +function createSynthChain(type, freq, adsr, effects) { + const osc = ctx.createOscillator(); // 음원 + const gain = ctx.createGain(); // ADSR 엔벨로프 + const filter = ctx.createBiquadFilter(); // LP/HP 필터 + const analyser = ctx.createAnalyser(); // 시각화 + + osc.type = type; // sine | square | triangle | sawtooth + osc.frequency.value = freq; // Hz + + // ADSR + const now = ctx.currentTime; + gain.gain.setValueAtTime(0, now); + gain.gain.linearRampToValueAtTime(1, now + adsr.attack); + gain.gain.linearRampToValueAtTime(adsr.sustain, now + adsr.attack + adsr.decay); + + osc.connect(gain).connect(filter).connect(analyser).connect(ctx.destination); + return { osc, gain, filter, analyser }; +} + +// WAV 내보내기 (OfflineAudioContext) +async function exportWav(notes, params) { + const offline = new OfflineAudioContext(2, 44100 * duration, 44100); + // ... 노트 렌더링 + const buffer = await offline.startRendering(); + const wav = audioBufferToWav(buffer); + // Blob → 다운로드 +} +``` + +### 5.6 음표 ↔ 주파수 매핑 + +```javascript +const NOTE_FREQ = { + 'C3': 130.81, 'D3': 146.83, 'E3': 164.81, 'F3': 174.61, + 'G3': 196.00, 'A3': 220.00, 'B3': 246.94, + 'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23, + 'G4': 392.00, 'A4': 440.00, 'B4': 493.88, + 'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46, + 'G5': 783.99, 'A5': 880.00, 'B5': 987.77, + 'C6': 1046.50 + // 반음(#/b)도 포함 +}; +``` + +--- + +## 6. Phase별 개발 계획 + +### Phase 1 — MVP (수동 모드 + 프리셋) + +| 항목 | 내용 | +|------|------| +| 음표 시퀀서 UI | 음표 추가/삭제/편집, 드래그 순서 변경 | +| 신디사이저 4종 | Sine, Square, Triangle, Sawtooth | +| ADSR 슬라이더 | Attack, Decay, Sustain, Release | +| 실시간 재생 | Web Audio API 즉시 재생 | +| WAV 내보내기 | OfflineAudioContext → WAV 다운로드 | +| 프리셋 10종 | 즉시 로드 가능한 사운드 패턴 | +| 프로젝트 저장 | localStorage (디자인 인사이트 패턴) | + +### Phase 2 — AI 어시스트 (Gemini) + +| 항목 | 내용 | +|------|------| +| 브랜드 입력 폼 | 브랜드명, 업종, 성격, 참고 사운드, 길이 | +| Gemini 분석 API | 브랜드 → 음악 이론 기반 사운드 설계 JSON | +| 변형 3종 생성 | 밝은/차분한/임팩트 버전 동시 제공 | +| 대화형 개선 | "좀 더 밝게" 등 자연어 추가 수정 | +| 근거 표시 | AI가 이 사운드를 추천하는 이유 설명 | +| 라우트 | `POST /rd/sound-logo/ai-assist`, `ai-refine` | + +### Phase 3 — AI 자동 생성 (Lyria RealTime + Lyria 2) + 고도화 + +| 항목 | 내용 | +|------|------| +| Lyria RealTime 연동 | WebSocket으로 브라우저에서 직접 실시간 음악 생성 (기존 Gemini API 키) | +| Lyria 2 폴백 | Vertex AI REST API로 서버 경유 파일 생성 (기존 서비스 계정) | +| 실시간 BPM/스케일 조절 | Lyria RealTime의 파라미터 실시간 변경 | +| 화음(Chord) 편집 | 동시에 여러 음 배치 | +| 이펙트 체인 | 리버브, 딜레이, 필터 | +| 파형 시각화 | Canvas 실시간 파형 + 스펙트럼 | +| DB 저장 | 사운드 로고를 DB + Storage에 영구 저장 | +| 공유/내보내기 | JSON 설정 공유, MP3 변환 | + +### Phase 4 — 프로급 확장 (선택) + +| 항목 | 내용 | +|------|------| +| 타임라인 UI | 드래그로 음표 배치하는 DAW 스타일 | +| 샘플 기반 음색 | 피아노, 벨, 마림바 등 실제 악기 | +| 드럼/퍼커션 | 노이즈 기반 킥/스네어/하이햇 | +| MIDI 내보내기 | 전문 DAW에서 추가 편집 가능 | +| A/B 비교 | 두 사운드를 나란히 비교 재생 | + +--- + +## 7. Gemini AI 연동 상세 + +### 7.1 API 호출 흐름 + +``` +Frontend (Alpine.js) + │ + │ POST /rd/sound-logo/ai-assist + │ { brand_name, industry, personality, reference, duration } + │ + ▼ +SoundLogoController::aiAssist() + │ + │ 프롬프트 구성 + │ + ▼ +Gemini 2.5 Flash API + │ + │ JSON 응답 (notes, adsr, effects, analysis) + │ + ▼ +SoundLogoController → JsonResponse + │ + │ { success: true, data: { notes, params, analysis, variations } } + │ + ▼ +Frontend: 음표 에디터에 자동 로드 → 즉시 재생 +``` + +### 7.2 대화형 개선 흐름 + +``` +사용자: "좀 더 짧고 강렬하게" + │ + ▼ +POST /rd/sound-logo/ai-refine +{ previous_notes: [...], feedback: "좀 더 짧고 강렬하게" } + │ + ▼ +Gemini: 기존 노트를 분석하고 피드백 반영하여 수정된 JSON 반환 + │ + ▼ +수정된 음표가 에디터에 반영 +``` + +### 7.3 Lyria 음악 생성 흐름 + +#### 7.3.1 Lyria RealTime (브라우저 직접, 권장) + +``` +사용자: "AI 자동 생성" 탭 → Lyria RealTime 선택 + │ + ▼ +브라우저 JavaScript (Alpine.js) + │ + │ WebSocket 연결 (Gemini API v1alpha) + │ wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent + │ API Key: .env GEMINI_API_KEY (서버 경유 프록시) + │ Model: lyria-realtime-exp + │ + ▼ +Lyria RealTime 스트리밍 + │ + │ 2초 청크 단위 48kHz 스테레오 오디오 + │ ← BPM/Scale 실시간 조절 가능 + │ + ▼ +Web Audio API로 실시간 재생 + 녹음(MediaRecorder) → WAV 저장 +``` + +> **API 키 보안**: 브라우저에서 직접 Gemini API 키를 노출하지 않기 위해, +> 서버를 WebSocket 프록시로 사용하거나 `/rd/sound-logo/ws-token` 엔드포인트에서 +> 임시 토큰을 발급하는 방식을 검토한다. + +#### 7.3.2 Lyria 2 (서버 경유, 폴백) + +``` +사용자: Lyria RealTime 실패 시 자동 전환 + │ + ▼ +POST /rd/sound-logo/ai-generate +{ mood: "bright_futuristic", duration: 3, prompt: "..." } + │ + ▼ +SoundLogoService::generateWithLyria() + ├── AiConfig::getActiveGemini() → Vertex AI 설정 확인 + ├── GoogleCloudService::getAccessToken() → OAuth 토큰 + └── Lyria API 호출 → audioContent (base64) + │ + ▼ +WAV 파일 저장 → 다운로드 URL 반환 +``` + +--- + +## 8. 데이터 모델 + +### 8.1 localStorage 구조 (Phase 1~2) + +```json +{ + "sl_projects": [ + { + "id": "sl_1709000000_abc", + "title": "SAM 사운드 로고", + "sounds": [ + { + "id": "snd_001", + "name": "SAM 시그널 v1", + "notes": [ + { "note": "C5", "duration": 0.2, "velocity": 0.8 }, + { "note": "E5", "duration": 0.2, "velocity": 0.9 }, + { "chord": ["C5", "E5", "G5"], "duration": 0.8, "velocity": 1.0 } + ], + "params": { + "synth": "sine", + "bpm": 130, + "adsr": { "attack": 0.01, "decay": 0.1, "sustain": 0.7, "release": 0.5 }, + "effects": { "reverb": 0.3, "delay": 0, "filterFreq": 2000 } + }, + "aiAnalysis": "C Major 조성, 5도 상행으로 신뢰감과 성장 표현", + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "createdAt": "2026-03-08T00:00:00.000Z" + } + ], + "sl_current": "sl_1709000000_abc" +} +``` + +### 8.2 DB 테이블 (Phase 3, API 프로젝트에서 마이그레이션) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint | PK | +| `tenant_id` | bigint | FK → tenants | +| `user_id` | bigint | FK → users | +| `name` | varchar(200) | 사운드 이름 | +| `audio_path` | varchar(500) | WAV/MP3 파일 경로 | +| `options` | json | notes, params, aiAnalysis 등 | +| `created_at` | timestamp | | +| `updated_at` | timestamp | | + +--- + +## 9. API 인증 및 키 현황 + +> **별도 API 키 발급 불필요** — 기존 인증 정보로 모든 엔진 사용 가능 + +### 9.1 사용 가능한 인증 정보 + +| 엔진 | 인증 방식 | 설정 위치 | 상태 | +|------|----------|----------|------| +| Gemini 2.5 Flash (모드 B) | API 키 | `.env` `GEMINI_API_KEY` | ✅ 운영 중 | +| Lyria RealTime (모드 C) | 동일 API 키 | `.env` `GEMINI_API_KEY` | ✅ 사용 가능 (experimental) | +| Lyria 2 (모드 C 폴백) | 서비스 계정 | `GOOGLE_APPLICATION_CREDENTIALS` | ✅ 파일 존재 | +| Vertex AI | 프로젝트 ID | `.env` `VERTEX_AI_PROJECT_ID=codebridge-chatbot` | ✅ 설정됨 | + +### 9.2 현재 .env 설정 (관련 항목) + +```env +GEMINI_API_KEY=AIzaSy... # Gemini + Lyria RealTime 공용 +GEMINI_MODEL=gemini-2.5-flash # 텍스트 AI (모드 B) +GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta +VERTEX_AI_PROJECT_ID=codebridge-chatbot # Lyria 2 (폴백) +VERTEX_AI_LOCATION=us-central1 +GOOGLE_APPLICATION_CREDENTIALS=/var/www/sales/apikey/google_service_account.json +``` + +### 9.3 Lyria RealTime API 사양 + +| 항목 | 값 | +|------|------| +| 모델 | `lyria-realtime-exp` | +| API 버전 | `v1alpha` (experimental) | +| 프로토콜 | WebSocket (양방향 스트리밍) | +| 출력 포맷 | 48kHz 스테레오 PCM | +| 청크 크기 | 2초 단위 | +| 제어 파라미터 | BPM (60~200), Scale (Key + Mode) | +| 비용 | 무료 (experimental 기간) | +| 참고 | [공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) | + +### 9.4 Lyria 2 API 사양 (폴백) + +| 항목 | 값 | +|------|------| +| 모델 | `lyria` | +| API | Vertex AI REST (`/publishers/google/models/lyria:predict`) | +| 인증 | 서비스 계정 OAuth 토큰 | +| 출력 포맷 | WAV (base64) | +| 비용 | $0.06 / 30초 | +| 기존 코드 | `BgmService::generateWithLyria()` | + +--- + +## 10. 관련 문서 + +- [AI 관리 종합 가이드](../guides/ai-management.md) — Gemini API 설정, 호출 흐름 +- [R&D 메뉴 개요](../features/rd/README.md) — R&D 메뉴 구조 +- [디자인 인사이트](../features/rd/design-insight.md) — 유사 SPA 패턴 참고 +- [Lyria RealTime 공식 문서](https://ai.google.dev/gemini-api/docs/music-generation) — Gemini API 음악 생성 +- [Lyria 2 Vertex AI 문서](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/lyria-music-generation) — REST API 레퍼런스 +- [Lyria RealTime 개발자 가이드](https://dev.to/googleai/lyria-realtime-the-developers-guide-to-infinite-music-streaming-4m1h) — 구현 튜토리얼 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/projects/org-chart/README.md b/projects/org-chart/README.md new file mode 100644 index 0000000..238ce52 --- /dev/null +++ b/projects/org-chart/README.md @@ -0,0 +1,317 @@ +# 조직도 관리 시스템 + +> **작성일**: 2026-03-06 +> **상태**: 🟢 v1.0 구현 완료 +> **프로젝트**: MNG 전용 (Blade + Alpine.js + SortableJS) + +--- + +## 1. 개요 + +### 1.1 목적 + +테넌트별 조직 구조를 시각적으로 관리하는 트리형 조직도 시스템. +부서 계층 구조와 직원 배치를 드래그 앤 드롭으로 관리한다. + +### 1.2 주요 기능 + +| 기능 | 설명 | +|------|------| +| 트리형 조직도 | 회사 → 부서 → 하위부서 (무한 depth) 계층 표시 | +| 직원 배치 | 드래그 앤 드롭으로 직원을 부서에 배치/해제 | +| 부서 순서 변경 | 같은 레벨 내 부서 순서 드래그로 변경 | +| 부서 계층 이동 | 부서를 다른 부서 아래로 드래그하여 parent 변경 | +| 부서 숨기기 | 더블클릭 → 숨기기 버튼 → DB 저장 (영구) | +| 임원 필터링 | 대표이사/사장 등은 미배치 목록에서 제외 | + +--- + +## 2. 기술 스택 + +| 구분 | 기술 | +|------|------| +| 백엔드 | Laravel (MNG 프로젝트) | +| 프론트엔드 | Alpine.js + 수동 DOM 렌더링 | +| 드래그 앤 드롭 | SortableJS | +| 스타일 | Tailwind CSS + inline style | +| 데이터 저장 | MySQL `departments`, `employees` 테이블 | + +--- + +## 3. 아키텍처 + +### 3.1 렌더링 방식 + +> **핵심**: Alpine.js `x-for` 대신 수동 `innerHTML` 렌더링을 사용한다. + +SortableJS와 Alpine.js `x-for` 템플릿이 동시에 DOM을 조작하면 **이중 업데이트 버그**가 발생한다. +이를 해결하기 위해 부서 트리는 JavaScript로 HTML 문자열을 생성하고 `innerHTML`로 삽입한다. + +``` +Alpine.js 데이터 변경 + ↓ +renderTree() 호출 + ↓ +기존 SortableJS 인스턴스 destroy + ↓ +buildChildrenHtml(null, 0) → 재귀적 HTML 생성 + ↓ +$refs.deptTree.innerHTML = html + ↓ +$nextTick → initDeptSortables() + initEmpSortables() +``` + +### 3.2 이벤트 처리 + +수동 렌더링된 HTML에는 Alpine 디렉티브가 없으므로 **이벤트 위임(Event Delegation)** 패턴을 사용한다. + +``` +루트 div @click="handleClick($event)" + @dblclick="handleDblClick($event)" + ↓ +e.target.closest('[data-action]') 으로 액션 식별 + ↓ +data-action 값에 따라 분기: + - "unassign" → 직원 미배치 + - "hide-dept" → 부서 숨기기 + - "restore-dept" → 부서 복원 + - "dept-dblclick" → 더블클릭 시 숨기기 버튼 토글 +``` + +### 3.3 순환 참조 방지 + +부서를 자신의 하위로 드래그하면 무한 루프가 발생한다. +`isDescendant(ancestorId, targetId)` 재귀 함수로 이를 차단한다. + +```javascript +// 드래그 대상(dragId)의 자손인 곳으로는 이동 불가 +onMove: (evt) => { + const dragId = parseInt(evt.dragged.dataset.deptId); + const toPid = evt.to.dataset.parentId ? parseInt(evt.to.dataset.parentId) : null; + if (toPid === dragId || this.isDescendant(dragId, toPid)) return false; +} +``` + +--- + +## 4. 파일 구조 + +### 4.1 MNG 프로젝트 + +| 파일 | 역할 | +|------|------| +| `app/Http/Controllers/RdController.php` | 컨트롤러 (7개 메서드) | +| `app/Models/Tenants/Department.php` | 부서 모델 (`options` JSON cast) | +| `resources/views/rd/org-chart.blade.php` | 뷰 (Alpine.js + SortableJS) | +| `routes/web.php` | 라우트 (6개 엔드포인트) | + +### 4.2 API 프로젝트 + +| 파일 | 역할 | +|------|------| +| `database/migrations/2026_03_06_201500_add_options_to_departments_table.php` | `options` JSON 컬럼 추가 | + +--- + +## 5. API 엔드포인트 + +> 모든 엔드포인트는 `rd.` 네임 프리픽스 하위에 위치한다. + +| Method | Route | 컨트롤러 메서드 | 설명 | +|--------|-------|---------------|------| +| GET | `/rd/org-chart` | `orgChart` | 조직도 페이지 | +| POST | `/rd/org-chart/assign` | `orgChartAssign` | 직원 부서 배치 | +| POST | `/rd/org-chart/unassign` | `orgChartUnassign` | 직원 부서 해제 | +| POST | `/rd/org-chart/reorder` | `orgChartReorder` | 직원 일괄 이동 | +| POST | `/rd/org-chart/reorder-depts` | `orgChartReorderDepts` | 부서 순서/계층 변경 | +| POST | `/rd/org-chart/toggle-hide` | `orgChartToggleHide` | 부서 숨기기/표시 토글 | + +### 5.1 요청/응답 형식 + +**부서 배치** (`POST /rd/org-chart/assign`): +```json +{ "employee_id": 1, "department_id": 5 } +→ { "success": true } +``` + +**부서 순서 변경** (`POST /rd/org-chart/reorder-depts`): +```json +{ + "orders": [ + { "id": 1, "parent_id": null, "sort_order": 1 }, + { "id": 2, "parent_id": null, "sort_order": 2 }, + { "id": 3, "parent_id": 1, "sort_order": 1 } + ] +} +→ { "success": true } +``` + +**부서 숨기기** (`POST /rd/org-chart/toggle-hide`): +```json +{ "department_id": 5, "hidden": true } +→ { "success": true } +``` + +--- + +## 6. DB 구조 + +### 6.1 departments 테이블 (관련 컬럼) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | int | PK | +| `tenant_id` | int | 테넌트 FK | +| `parent_id` | int (nullable) | 상위 부서 (null = 최상위) | +| `name` | varchar | 부서명 | +| `code` | varchar | 부서 코드 | +| `is_active` | bool | 활성 여부 | +| `sort_order` | int | 정렬 순서 | +| `options` | json (nullable) | 확장 속성 | + +**`options` 키**: + +| 키 | 타입 | 설명 | +|----|------|------| +| `orgchart_hidden` | boolean | 조직도에서 숨김 여부 | + +### 6.2 employees 테이블 (관련 컬럼) + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | int | PK | +| `tenant_id` | int | 테넌트 FK | +| `department_id` | int (nullable) | 소속 부서 (null = 미배치) | +| `display_name` | varchar | 표시 이름 | +| `position_label` | varchar | 직책/직급 | +| `employee_status` | enum | `active`, `leave`, `resigned` | + +--- + +## 7. 프론트엔드 구현 상세 + +### 7.1 Alpine.js 컴포넌트 (`orgChart()`) + +**데이터**: + +| 속성 | 타입 | 설명 | +|------|------|------| +| `departments` | Array | 전체 부서 목록 (서버에서 전달) | +| `employees` | Array | 전체 직원 목록 (서버에서 전달) | +| `hiddenDepts` | Set | 숨긴 부서 ID (DB에서 초기화) | +| `dblClickDept` | int/null | 더블클릭된 부서 ID (숨기기 버튼 표시용) | +| `execTitles` | Array | 임원 직책 목록 (`['대표이사', '사장', '부사장', '회장', '부회장']`) | + +**핵심 메서드**: + +| 메서드 | 설명 | +|--------|------| +| `renderTree()` | SortableJS 파괴 → HTML 재생성 → SortableJS 재초기화 | +| `buildChildrenHtml(parentId, level)` | 재귀적 자식 부서 HTML 생성 | +| `buildNodeHtml(dept, level)` | 단일 부서 카드 HTML (level별 스타일 차등) | +| `buildEmpHtml(emp, isLarge)` | 직원 카드 HTML | +| `isDeptHidden(deptId)` | 부서 또는 상위 부서가 숨김인지 재귀 체크 | +| `isDescendant(ancestorId, targetId)` | 순환 참조 방지 | +| `isExecutive(emp)` | 임원 여부 판별 | + +### 7.2 SortableJS 그룹 + +| 그룹 | 대상 | 핸들 | 기능 | +|------|------|------|------| +| `departments` | `.org-children`, `.org-drop-target` | `.dept-drag-handle` | 부서 순서/계층 변경 | +| `employees` | `.emp-zone`, `#unassigned-zone` | (전체) | 직원 배치/해제 | + +### 7.3 CSS 연결선 + +부서 간 연결선은 CSS `::before`/`::after` 의사 요소로 구현한다. + +``` + 부모 노드 + │ (vertical: div 1px × 24px) + ┌───────┼───────┐ (horizontal: ::before + ::after) + │ │ │ (vertical: div 1px × 24px) + 자식1 자식2 자식3 +``` + +| 선택자 | 역할 | +|--------|------| +| `.org-node-wrap` 내부 div (1px × 24px) | 세로 연결선 | +| `.org-node-wrap:not(:first-child)::before` | 왼쪽 가로선 (left:0 ~ right:50%) | +| `.org-node-wrap:not(:last-child)::after` | 오른쪽 가로선 (left:50% ~ right:0) | +| `:only-child` | 단일 자식이면 가로선 숨김 | + +### 7.4 부서 숨기기 UX 흐름 + +``` +① 부서 헤더 더블클릭 + ↓ +② dblClickDept = dept.id → renderTree() + ↓ +③ 헤더에 빨간 "숨기기" 버튼 표시 + ↓ +④ "숨기기" 클릭 + ↓ +⑤ hiddenDepts.add(id) → renderTree() → POST /toggle-hide (DB 저장) + ↓ +⑥ 해당 부서 + 하위 부서가 트리에서 제거 +⑦ "숨겨진 부서" 패널에 표시 + ↓ +⑧ 패널에서 👁 아이콘 클릭 → hiddenDepts.delete(id) → POST /toggle-hide +``` + +### 7.5 부서 레벨별 스타일 + +| Level | 색상 테마 | 너비 | 아이콘 | +|-------|---------|------|--------| +| 0 (최상위) | 보라 (`#7C3AED`) | 200px | `ri-building-2-line` | +| 1 (중간) | 인디고 (`#6366F1`) | 180px | `ri-git-branch-line` | +| 2+ (하위) | 회색 (`#6B7280`) | 160px | `ri-subtract-line` | + +--- + +## 8. 비즈니스 규칙 + +### 8.1 임원 필터링 + +미배치 직원 목록에서 다음 조건에 해당하면 제외: + +- `position_label`이 `['대표이사', '사장', '부사장', '회장', '부회장']` 중 하나 +- `display_name`이 테넌트의 `ceo_name`과 일치 + +> 이유: 조직도 최상단에 "대표이사 OOO"이 이미 표시되므로 중복 방지 + +### 8.2 부서 숨기기 + +- `departments.options` JSON의 `orgchart_hidden` 키로 저장 +- 숨긴 부서의 **하위 부서도 자동으로 숨겨짐** (`isDeptHidden` 재귀 체크) +- 숨겨진 부서 패널에는 **직접 숨긴 부서만** 표시 (자식은 부모 복원 시 같이 복원) +- 숨기기는 **조직도 표시 전용** — `is_active`와 무관하며, 부서 데이터에 영향 없음 + +### 8.3 직원 표시 형식 + +- 직책이 있으면: `{직책} {이름}` (예: "과장 전진선") +- 직책이 없으면: `{이름}` (예: "김보곤") + +--- + +## 9. 개발 이력 + +| 날짜 | 커밋 | 내용 | +|------|------|------| +| 2026-03-06 | `a12ee886` | CSS 연결선 수정 + 빈 드롭 타겟 숨김 | +| 2026-03-06 | `9fd72e49` | 부서 숨기기 기능 추가 (프론트 전용) | +| 2026-03-06 | `8c8fd5f6` | 대표이사 미배치 제외 + 숨긴 부서 연결선 제거 | +| 2026-03-06 | `81157a15` | 부서 숨기기 상태 DB 저장 (`options.orgchart_hidden`) | + +--- + +## 관련 문서 + +- [rules/department-tree-api.md](../../rules/department-tree-api.md) — 부서 트리 API 규칙 +- [rules/employee-api.md](../../rules/employee-api.md) — 직원 API 규칙 +- [system/database/hr.md](../../system/database/hr.md) — HR 테이블 스키마 +- [standards/options-column-policy.md](../../standards/options-column-policy.md) — options JSON 컬럼 정책 + +--- + +**최종 업데이트**: 2026-03-06 diff --git a/projects/planning-design/README.md b/projects/planning-design/README.md new file mode 100644 index 0000000..12dba75 --- /dev/null +++ b/projects/planning-design/README.md @@ -0,0 +1,157 @@ +# 기획디자인 스토리보드 에디터 + +> **시작일**: 2026-03-07 +> **상태**: 🟢 v1.2 운영 중 (고도화 진행중) +> **경로**: MNG `/rd/planning-design` +> **담당**: Claude Code + 개발팀 + +--- + +## 1. 프로젝트 개요 + +### 1.1 배경 + +ERP 화면 기획서(스토리보드)를 PowerPoint나 Figma 없이 **SAM 관리자 웹 내에서 직접 설계**할 수 있는 도구가 필요했다. 기획자와 개발자가 같은 플랫폼에서 화면을 설계하고, 즉시 HTML/인쇄 출력까지 가능한 올인원 솔루션을 목표로 했다. + +### 1.2 목표 + +- 브라우저 내 Notion/Figma 스타일 블록 에디터 구현 +- ERP 스토리보드 표준 양식 (메뉴트리 + 와이어프레임 + Description) 지원 +- 서버 API 없이 localStorage 기반 즉시 사용 가능 +- HTML 내보내기 및 좌표 기반 WYSIWYG 인쇄 + +### 1.3 기술 스택 + +| 항목 | 선택 | 이유 | +|------|------|------| +| 프레임워크 | Alpine.js | 서버 없이 반응형 SPA, 기존 MNG 스택과 일치 | +| 캔버스 | DOM absolute positioning | Canvas API보다 접근성 좋고 텍스트 편집 용이 | +| 저장 | localStorage | 서버 API 불필요, 즉시 사용 가능 | +| 내보내기 | HTML 생성 + window.print() | 별도 라이브러리 없이 브라우저 내장 기능 활용 | + +--- + +## 2. 구현 이력 + +### v1.0 — 기본 블록 에디터 (2026-03-07) + +| 커밋 | 내용 | +|------|------| +| `063d8c61` | 스토리보드 블록 Undo/Redo 기능 (Ctrl+Z/Y, 50단계 히스토리) | +| `78c8f3f8` | 페이지 복사 기능 (블록 ID 재생성) | +| `a27d9921` | placeholder 색상 옅게 + italic 스타일 | +| `08cc866a` | 블록 툴바를 단위업무 상단으로 이동 (기획서 보기 방해 제거) | +| `20e5ab78` | 메뉴/캔버스 경계 드래그 리사이즈 (80~400px) | +| `7785dfed` | 올가미(마퀴) 다중 선택 + 그룹 이동/복사/삭제 | +| `ff373c71` | 올가미 선택 동작 수정 (캔버스 빈 영역 판별 개선) | +| `95cd217c` | Ctrl+X 잘라내기 기능 (단일/다중) | +| `f4131df0` | Ctrl+X 후 Ctrl+Z 복구 수정 (히스토리 인덱스 보정) | +| `8ff84e7f` | Description 패널 리사이즈 + 번호 마커 블록 (D&D/툴바) | +| `ac5ae6eb` | 좌표 기반 인쇄 + HTML 내보내기 블록 좌표 배치 | + +### v1.1 — 서식 시스템 (2026-03-08) + +| 커밋 | 내용 | +|------|------| +| `dfbbd3a1` | 플로팅 서식 툴바 + 우클릭 컨텍스트 메뉴 추가 | +| `280bfddb` | 블록 서식 CSS 상속 수정 (자식 요소 color inherit) | + +### v1.2 — 작업 영역 극대화 (2026-03-08) + +| 커밋 | 내용 | +|------|------| +| `5e0f1a63` | 좌측 사이드바 접기/펼치기 버튼 추가 | +| `f1202731` | 메뉴트리/Description 패널 접기/펼치기 + 캔버스 폭 자동 확장 (1100→1400px) | +| `a38c017c` | 이미지 블록 업로드를 더블클릭으로 변경 (드래그 중 파일 창 오픈 방지) | + +--- + +## 3. 현재 기능 목록 + +### 3.1 블록 유형 (15종) + +| 분류 | 유형 | +|------|------| +| 텍스트 | Heading (H1), Heading2 (H2), Text, Code | +| 레이아웃 | Divider, Callout | +| 데이터 | Table | +| UI 모형 | Button, Input, Select, Card, Badges | +| 미디어 | Image, Marker (번호 뱃지) | +| 체크 | Todo (체크리스트) | + +### 3.2 편집 기능 + +- 자유 배치 캔버스 (드래그 이동, 핸들 리사이즈) +- 올가미 다중 선택 + 그룹 이동/복사/삭제 +- Undo/Redo (50단계) +- 복사/붙여넣기/잘라내기 (Ctrl+C/V/X) +- 전체 선택 (Ctrl+A) +- 더블클릭 인라인 편집 (contenteditable) +- 이미지 블록 더블클릭 업로드 (드래그 충돌 방지) + +### 3.3 서식 시스템 + +- 플로팅 서식 툴바: 글자색, 배경색, 크기, 굵기, 기울임, 정렬, z-index +- 우클릭 컨텍스트 메뉴: 복제/잘라내기/삭제/색상/정렬/레이어/서식 초기화 + +### 3.4 문서 관리 + +- 멀티 페이지 (추가/복사/삭제/이동) +- ERP 메뉴 트리 편집 (드래그 순서 변경) +- Description 패널 (기능 설명 + 번호 마커 D&D) +- 프리셋/커스텀 템플릿 + +### 3.6 작업 영역 극대화 + +- 좌측 사이드바(메뉴트리) 접기/펼치기 토글 +- Description 패널 접기/펼치기 토글 바 +- 패널 접힘 시 캔버스 폭 자동 확장 (1100px → 1400px) +- sb-editor 패딩 축소 (24px → 12px) + +### 3.5 출력 + +- HTML 파일 내보내기 (좌표 기반 WYSIWYG) +- 인쇄 미리보기 (A4 Landscape, 페이지 분할) + +--- + +## 4. 향후 로드맵 + +| 우선순위 | 기능 | 설명 | 상태 | +|---------|------|------|------| +| 🔴 필수 | DB 저장 | localStorage → DB 전환 (협업, 용량 해결) | ⚪ 대기 | +| 🔴 필수 | 스냅/그리드 정렬 | 블록 간 자석 가이드라인 | ⚪ 대기 | +| 🟡 중요 | 그룹핑 | 여러 블록을 하나의 그룹으로 묶기/풀기 | ⚪ 대기 | +| 🟡 중요 | 레이어 패널 | z-index 순서를 시각적으로 관리 | ⚪ 대기 | +| 🟡 중요 | 리치 텍스트 | 블록 내 부분 텍스트 서식 (인라인 B/I/색상) | ⚪ 대기 | +| 🟢 권장 | PDF 내보내기 | 서버사이드 PDF 생성 | ⚪ 대기 | +| 🟢 권장 | 버전 관리 | 명시적 스냅샷 저장 및 비교 | ⚪ 대기 | +| 🟢 권장 | 공유 링크 | 읽기 전용 공유 URL 생성 | ⚪ 대기 | + +--- + +## 5. 기술적 특이사항 + +### 5.1 단일 파일 아키텍처 + +모든 CSS + HTML + JavaScript가 `index.blade.php` 하나에 포함 (~4,430줄). 서버 API가 없고 localStorage만 사용하므로, 컨트롤러는 뷰만 반환한다. + +### 5.2 CSS 스타일 상속 문제 + +블록 자식 요소에 하드코딩된 `color`가 있어 부모의 인라인 스타일이 무시되는 문제를 CSS attribute selector(`[style*="color"]`)로 해결했다. 향후 블록 유형 추가 시 inherit 규칙도 함께 추가해야 한다. + +### 5.3 localStorage 용량 한계 + +이미지를 base64 Data URL로 저장하므로 대량 사용 시 5~10MB 한계에 도달할 수 있다. DB 저장 전환이 중장기 과제. + +--- + +## 6. 관련 문서 + +- [기술 스펙](../../features/rd/planning-design.md) — 데이터 구조, 블록 유형, CSS 상속 상세 +- [R&D 메뉴 개요](../../features/rd/README.md) — R&D 전체 메뉴 구조 +- [프로젝트 인덱스](../index_projects.md) — 전체 프로젝트 목록 + +--- + +**최종 업데이트**: 2026-03-08 diff --git a/rules/slides/usage-plan/SAM_활용방안.pptx b/rules/slides/usage-plan/SAM_활용방안.pptx new file mode 100644 index 0000000..ed996ef Binary files /dev/null and b/rules/slides/usage-plan/SAM_활용방안.pptx differ diff --git a/rules/slides/usage-plan/convert.cjs b/rules/slides/usage-plan/convert.cjs new file mode 100644 index 0000000..b35464a --- /dev/null +++ b/rules/slides/usage-plan/convert.cjs @@ -0,0 +1,31 @@ +const path = require('path'); +module.paths.unshift(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/node_modules')); + +const html2pptx = require(path.join(require('os').homedir(), '.claude/skills/pptx-skill/scripts/html2pptx.js')); +const PptxGenJS = require('pptxgenjs'); + +async function main() { + const pres = new PptxGenJS(); + pres.defineLayout({ name: 'CUSTOM_16x9', width: 10, height: 5.625 }); + pres.layout = 'CUSTOM_16x9'; + pres.author = '(주)코드브릿지엑스'; + pres.subject = 'SAM 활용방안 - AI 자동화로 중소 제조업을 혁신하다'; + + const slideDir = __dirname; + const slideFiles = [ + 'slide-01.html', 'slide-02.html', 'slide-03.html', + 'slide-04.html', 'slide-05.html', 'slide-06.html', 'slide-07.html' + ]; + + for (const file of slideFiles) { + const htmlPath = path.join(slideDir, file); + console.log(`Converting: ${file}`); + await html2pptx(htmlPath, pres); + } + + const outputPath = path.join(slideDir, 'SAM_활용방안.pptx'); + await pres.writeFile({ fileName: outputPath }); + console.log(`\nPPTX saved: ${outputPath}`); +} + +main().catch(err => { console.error(err); process.exit(1); }); diff --git a/rules/slides/usage-plan/slide-01.html b/rules/slides/usage-plan/slide-01.html new file mode 100644 index 0000000..bfa9d9f --- /dev/null +++ b/rules/slides/usage-plan/slide-01.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
+ +
+

SAM PROJECT

+
+ +

SAM 활용방안

+

AI 자동화로 중소 제조업을 혁신하다

+ +

방화셔터 제조업 실증 | 80% 공통화 전략 | Multi-tenant SaaS 플랫폼

+ +
+
+

코어 모델 실증

+
+
+

AI 자동화

+
+
+

다산업군 확장

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스 | 2026.03

+
+ + diff --git a/rules/slides/usage-plan/slide-02.html b/rules/slides/usage-plan/slide-02.html new file mode 100644 index 0000000..c16747f --- /dev/null +++ b/rules/slides/usage-plan/slide-02.html @@ -0,0 +1,58 @@ + + + + + + + +
+

왜 SAM인가? — Before / After

+

중소 제조업의 현실과 SAM이 제시하는 변화

+
+ +
+
+
+
+ +
+

Before — 기존 방식

+
+

Excel 수기 관리

+

데이터 유실, 버전 혼란, 실시간 공유 불가

+

ERP 도입비 수천만원

+

중소기업에 과도한 초기 투자 부담

+

업체별 커스텀 6개월+

+

도입까지 긴 시간, 업데이트 어려움

+

부서간 정보 단절

+

영업/생산/경영 각자 관리, 의사결정 지연

+
+ +
+
+
+ +
+

After — SAM 도입 후

+
+

시스템 기반 통합 관리

+

실시간 데이터 공유, 단일 진실 공급원(SSOT)

+

월 구독 SaaS

+

초기 비용 최소화, 사용한 만큼 지불

+

멀티테넌시 즉시 입주

+

설정만으로 바로 사용, 지속적 업데이트

+

영업~출고 원스톱 자동화

+

AI가 연결하는 End-to-End 프로세스

+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

2 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-03.html b/rules/slides/usage-plan/slide-03.html new file mode 100644 index 0000000..07e9995 --- /dev/null +++ b/rules/slides/usage-plan/slide-03.html @@ -0,0 +1,108 @@ + + + + + + + +
+

전체 프로세스 — 영업에서 출고까지

+

6단계 비즈니스 플로우와 AI 자동화 포인트

+
+ +
+
+

01

+

영업

+

고객 DB 자동분류

+
+

+
+

02

+

상담

+

STT 음성 기록

+
+

+
+

03

+

견적서

+

AI 자동 산출

+
+

+
+

04

+

수주서

+

자동 전환

+
+

+
+

05

+

작업공정

+

AI 공정 최적화

+
+

+
+

06

+

출고

+

배송 자동화

+
+
+ +
+

경동/주일 실증 현황

+
+
+

단계

+

구현 기능

+

상태

+

AI 적용

+
+
+

영업관리

+

고객/거래처 CRM

+

운영중

+

고객 분류 자동화

+
+
+

상담/문의

+

상담 이력, 음성 입력

+

운영중

+

STT 음성→텍스트

+
+
+

견적서

+

견적 작성/승인/발송

+

운영중

+

AI 견적 산출 (개발중)

+
+
+

수주서

+

견적→수주 연동

+

운영중

+

자동 전환 프로세스

+
+
+

작업공정

+

BOM, 공정 관리

+

개발중

+

AI 공정 최적화 (계획)

+
+
+

출고/배송

+

출고 지시, 배송 추적

+

계획

+

물류 자동화 (계획)

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

3 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-04.html b/rules/slides/usage-plan/slide-04.html new file mode 100644 index 0000000..d6fb551 --- /dev/null +++ b/rules/slides/usage-plan/slide-04.html @@ -0,0 +1,88 @@ + + + + + + + +
+

80% 공통화론 — 핵심 설득 논거

+

중소 제조업 업무의 80%는 업종과 무관하게 동일하다

+
+ +
+
+

공통 업무

+
+
+

80% — 영업, 회계, 인사, 재고, 문서, 품질

+
+
+
+
+

커스텀

+
+
+

20%

+
+
+
+

커스텀 20% = 상품 마스터, 견적 계산식, 공정 시퀀스

+
+ +
+

업종별 확장 시나리오

+
+
+

업종

+

공통 (80%)

+

커스텀 (20%)

+

난이도

+
+
+

방화셔터

+

영업, 견적, 수주, 회계, 인사

+

셔터 규격 계산, 설치 공정

+

실증완료

+
+
+

블라인드

+

영업, 견적, 수주, 회계, 인사

+

원단/슬랫 규격, 재단 공정

+

즉시가능

+
+
+

금속가공

+

영업, 견적, 수주, 회계, 인사

+

소재/두께 단가표, CNC 공정

+

단기적용

+
+
+

식품제조

+

영업, 견적, 수주, 회계, 인사

+

레시피 관리, HACCP, 유통기한

+

중기적용

+
+
+

전자부품

+

영업, 견적, 수주, 회계, 인사

+

PCB BOM, SMT 공정, 검사

+

중기적용

+
+
+ +
+

"상품만 바꾸면 새로운 제조업이 된다. 영업, 회계, 인사, 재고 — 이 80%는 이미 완성되어 있다."

+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

4 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-05.html b/rules/slides/usage-plan/slide-05.html new file mode 100644 index 0000000..87cf510 --- /dev/null +++ b/rules/slides/usage-plan/slide-05.html @@ -0,0 +1,74 @@ + + + + + + + +
+

멀티테넌시 — 하나의 플랫폼, 다수의 기업

+

tenant_id 기반 데이터 격리로 안전하게 다수 기업을 서비스

+
+ +
+
+
+
+

A 기업 (경동)

+
+
+

B 기업 (주일)

+
+
+

C 기업 (금속)

+
+
+

D 기업 (식품)

+
+
+

▼ ▼ ▼ ▼

+
+

SAM 플랫폼

+
+

공유: 코드 100%

+

격리: 데이터 100%

+

기반: tenant_id

+
+
+
+ +
+
+
+
+

비용 절감

+
+

하나의 코드베이스로 N개 기업 서비스. 기업이 늘어도 개발비 동일.

+
+
+
+
+

즉시 입주

+
+

tenant_id 발급 + 기본 설정. 별도 개발 없이 수일 내 사용.

+
+
+
+
+

데이터 격리

+
+

모든 쿼리에 tenant_id 자동 적용. A기업과 B기업 데이터 완전 분리.

+
+
+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

5 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-06.html b/rules/slides/usage-plan/slide-06.html new file mode 100644 index 0000000..ea16e8a --- /dev/null +++ b/rules/slides/usage-plan/slide-06.html @@ -0,0 +1,68 @@ + + + + + + + +
+

AI 자동화 현황 & 로드맵

+

구현 완료된 AI 기능과 향후 계획

+
+ +
+
+
+
+

구현 완료

+
+
+
+

AI 재무 분석

+

CEO 대시보드에서 매출/비용/손익 AI 분석. Claude API로 자연어 인사이트 제공.

+
+
+

STT 음성 입력

+

상담 메모, 현장 보고를 음성 입력. 자동 텍스트 변환 후 시스템 기록.

+
+
+

Claude Code 개발 자동화

+

SAM 시스템을 Claude Code로 개발. 코드 생성, 리뷰, 배포 자동화.

+
+
+ +
+
+
+

향후 계획

+
+
+
+

AI 견적 자동 생성

+

고객 요구사항 입력 시 과거 데이터 기반 최적 견적 자동 산출.

+
+
+

AI 공정 최적화

+

생산 데이터 분석으로 최적 공정 순서, 자재 배치 제안.

+
+
+

AI 고객 상담

+

FAQ 자동 응답, 견적 문의 자동 접수. 필요 시 담당자 연결.

+
+
+
+ +
+

"공정의 다양성은 천차만별. 이를 AI와 데이터로 정복하는 것이 SAM의 연구 과제다."

+
+ +
+

SAM 활용방안 | (주)코드브릿지엑스

+

6 / 7

+
+ + diff --git a/rules/slides/usage-plan/slide-07.html b/rules/slides/usage-plan/slide-07.html new file mode 100644 index 0000000..ed9da70 --- /dev/null +++ b/rules/slides/usage-plan/slide-07.html @@ -0,0 +1,82 @@ + + + + + + + +
+

로드맵 & 비전

+

방화셔터에서 시작하여 모든 중소 제조업으로

+
+ +
+
+ +
+
+
+

Phase 1

+

코어 실증

+

2025~2026 상반기

+
+

진행중

+
+
+

경동/주일 방화셔터 제조업에서 전 프로세스 실증. 영업→출고 파이프라인 완성.

+
+ +
+
+
+

Phase 2

+

3~5사 확장

+

2026 하반기

+
+

계획

+
+
+

블라인드, 금속가공 등 유사 제조업 3~5사에 멀티테넌시 확장.

+
+ +
+
+
+

Phase 3

+

AI 고도화

+

2027

+
+

계획

+
+
+

AI 견적 자동 산출, AI 공정 최적화, AI 고객 상담 순차 적용.

+
+ +
+
+
+

Phase 4

+

다산업군 플랫폼

+

2028~

+
+

비전

+
+
+

식품, 전자부품 등 다양한 제조업종. 중소 제조업 표준 SaaS.

+
+
+ +
+

"방화셔터에서 시작하여, 모든 중소 제조업의 디지털 전환을 이끄는 SAM"

+

AI 자동화 + 멀티테넌시 + 80% 공통화 = 중소 제조업 혁신 플랫폼 | (주)코드브릿지엑스

+
+ +
+

7 / 7

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