Compare commits

125 Commits

Author SHA1 Message Date
김보곤
ee1aaf183d chore: .claude 폴더를 git 추적에서 제외 (로컬 전용) 2026-03-09 23:06:34 +09:00
김보곤
c143c7e9f8 chore: CLAUDE.md를 git 추적에서 제외 (로컬 전용) 2026-03-09 23:02:30 +09:00
7a969b9d57 refactor: [structure] sam/ 하위 문서를 docs 루트로 재배치
- .gitignore를 sam/ 기반에서 루트 기반으로 변경
- sam/docs/ 하위 문서를 루트로 이동 (contracts, features, guides, plans 등)
- sam/ 폴더 삭제 (docker, coocon 포함)
2026-03-09 22:53:07 +09:00
cc38b00c11 refactor: [structure] sam/ 하위 문서를 docs 루트로 재배치
- sam/docs/ 하위 62개 신규 파일을 루트로 이동 (contracts, features, guides, plans 등)
- sam/docs/ 하위 52개 변경 파일을 루트에 덮어쓰기 (brochure, rules 등)
- sam/ 폴더 전체 삭제 (docker, coocon 포함)
2026-03-09 22:36:16 +09:00
bfcd6178ea docs: [quality] 품질관리 시스템 기능 문서 작성
- README.md: 전체 개요, 역할별 프로세스 플로우, 메뉴 구조, 데이터 구조, API, 스토리보드 참조
- inspection-management.md: 제품검사 관리 (15개 검사항목, 상태판정, 캘린더뷰, 요청서/성적서 양식)
- performance-reports.md: 생산실적신고 (자동생성, 확정, 누락체크, 건기원 프로세스)
- quality-certification-audit.md: 품질인정심사 (기준/매뉴얼 심사 + 로트 추적 심사)
- INDEX.md에 품질관리 문서 등록
2026-03-09 22:36:15 +09:00
04e877dea3 docs: [ops-manual] sam-dev 서버 유지보수 정책 문서화
- 01-server-overview: sam-dev 서비스 현황 갱신 (Swap, PHP 5.6/Apache 비활성화, cron 정리)
- 02-daily-operations: sam-dev 리소스 관리 섹션 추가 (Swap, Gitea 캐시, 비활성 서비스)
- 06-database: sam-dev binlog 7일 보관 정책 추가
2026-03-09 22:36:15 +09:00
85dc30bfcd docs: [infra] 서버 정보 오류 수정 (ops-manual 기준 정렬)
- server-access-management.md: sam-cicd IP 정정 (114.203.209.83 → 110.10.147.46), sam-dev 추가, DB 계정/백업 경로 갱신, 리플리케이션 섹션 제거
- CLAUDE.md: dev 서버에서 Jenkins 제거 (Jenkins는 cicd 서버), MySQL 8.0 → 8.4, Next.js 포트 수정
2026-03-09 22:36:15 +09:00
김보곤
e94123ad49 docs: [rd, approvals] 누락 문서 2건 복원
- features/rd/sound-logo-studio.md (사운드 로고 스튜디오)
- dev/changes/20260306_purchase_request_payment_method.md (품의서 지급방법)
2026-03-09 22:29:07 +09:00
김보곤
1f7bd13816 docs: [database] codebridge 분리 문서 최종 상태 업데이트
- 운영서버 revert 사유 및 교훈 기록
- 로컬 samdb 58개 삭제, 로컬/개발 265개 동기화 반영
- DevTools 테이블 실제 이름(admin_ prefix) 수정
- 운영서버 적용 절차 5단계로 개정 (DB 선행 필수)
2026-03-09 21:32:47 +09:00
김보곤
03ccd7ba93 docs: [database] codebridge 분리 문서 최종 업데이트
- Equipment 하위 4개 테이블 추가 (55→59개)
- 개발 서버 samdb에서 59개 테이블 삭제 완료 반영
- 테이블명 불일치 수정 (api_bookmarks→admin_api_bookmarks 등)
- 운영 서버 적용 절차 4단계로 구체화
2026-03-09 21:02:50 +09:00
김보곤
ec3abc1a85 docs: [approvals] 결재관리 DB 변경사항 및 API 모델 동기화 현황 문서 작성
- 2026-02-27 ~ 03-05 마이그레이션 15개 변경 타임라인 정리
- API/MNG 모델 $fillable/$casts 동기화 비교표 작성
- API 모델 미반영으로 인한 잠재적 오류 영향 분석
2026-03-09 20:34:11 +09:00
김보곤
5000c67ec1 docs: [database] codebridge 분리 대상에서 API 사용 테이블 22개 제외
- Barobill 12개: API 모델/서비스/컨트롤러에서 직접 사용
- ESign 4개: API 전자계약 기능 (EsignService, EsignContractController)
- Audit 2개: API 전사 감사 시스템 (AuditLogService, TriggerAuditLogController)
- DevTools 1개: api_request_logs (SystemStatService)
- System 2개: ai_pricing_configs, ai_token_usages (API 모델)
- HR 1개: income_tax_brackets (API Seeder)
- codebridge 이동 대상 100개 → 55개로 축소
2026-03-09 19:58:07 +09:00
925ed82ae1 docs: 신규 개발자 로컬 환경 셋팅 가이드 추가
- Docker 기반 로컬 개발 환경 전체 셋팅 절차
- api, mng, react, docs, hotfix 5개 저장소 설명
- SSL 인증서, hosts, 환경변수, 트러블슈팅 포함

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:21:39 +09:00
유병철
8f939d3609 docs: [frontend] 프론트엔드 아키텍처/가이드 문서 v1 작성
- _index.md: 문서 목록 및 버전 관리
- 01~09: 아키텍처, API패턴, 컴포넌트, 폼, 스타일, 인증, 대시보드, 컨벤션
- 10: 문서 API 연동 스펙 (api-specs에서 이관)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:24:25 +09:00
김보곤
2efe56df70 docs: [plans] 방화셔터 도면생성 기능 기획서 작성
- 가이드레일 단면도 + 셔터박스 단면도 + 3D 렌더링 4탭 구성
- 파라미터 기반 SVG 실시간 렌더링 + Three.js 3D 조립체
- 기존 자동도면 생성 아키텍처 확장 (순수 클라이언트 측)
- 4단계 개발 계획: 가이드레일 → 셔터박스 → 3D → 출력/프리셋
2026-03-08 19:20:23 +09:00
김보곤
129332d4b1 Merge remote-tracking branch 'origin/main' 2026-03-08 15:11:09 +09:00
김보곤
4d13301ce0 docs: [plans] 사운드 로고 생성기 기획서 고도화
- 모드 C를 Lyria RealTime(WebSocket, 브라우저 직접) + Lyria 2(REST, 폴백) 듀얼 구조로 개편
- 별도 API 키 발급 불필요 확인 (기존 Gemini API 키 + Vertex AI 서비스 계정 재활용)
- API 인증 현황, Lyria RealTime/Lyria 2 사양, 공식 문서 참조 추가
2026-03-08 12:09:51 +09:00
김보곤
7c9f7afb52 docs: [plans] 사운드 로고 생성기 기획서 작성
- 3가지 모드 설계: 수동(Web Audio), AI 어시스트(Gemini), AI 자동(Lyria)
- 기존 BgmService/CmSongController 인프라 재활용 계획
- 4 Phase 개발 로드맵, UI 레이아웃, API 설계 포함
2026-03-08 12:05:03 +09:00
김보곤
8e700fcd64 docs: [rd] 디자인 인사이트 기능 문서 추가
- features/rd/design-insight.md 신규 작성 (아키텍처, 카드/카테고리 체계, CSS 와이어프레임, AI 프롬프트 복사)
- features/rd/README.md에 디자인 인사이트 메뉴·컨트롤러·관련 문서 등록
- INDEX.md에 디자인 인사이트 문서 등록
2026-03-08 11:22:09 +09:00
김보곤
ba68e138e6 docs: [plans] UI/UX 디자인 인사이트 연구 메뉴 기획서 작성
- 기획디자인 모티브의 UI/UX 연구 도구 기획
- 4종 인사이트 카드 (레퍼런스/분석/패턴/Before-After)
- CRAP 디자인 원칙 체크리스트
- 4 Phase 개발 로드맵
2026-03-08 09:43:17 +09:00
김보곤
95b9efbcc5 docs: [planning-design] v1.2 작업 영역 극대화 기능 문서 업데이트
- 사이드바/Description 패널 접기/펼치기 기능 추가
- 캔버스 폭 자동 확장 (1100→1400px) 반영
- 이미지 블록 더블클릭 업로드 변경 반영
- 파일 줄 수 4,300→4,430줄 갱신
- 버전 v1.1 → v1.2 갱신
2026-03-08 09:30:14 +09:00
김보곤
2dc20952b2 docs: [projects] 기획디자인 스토리보드 에디터 프로젝트 문서 추가
- projects/planning-design/README.md: 프로젝트 개요, 구현 이력(v1.0~v1.1), 로드맵
- index_projects.md에 planning-design 프로젝트 등록
2026-03-08 08:46:37 +09:00
김보곤
428e77aa9b docs: [rd] R&D 기획디자인 스토리보드 에디터 기술문서 추가
- features/rd/README.md: R&D 메뉴 전체 개요 (라우트, 컨트롤러, 기능현황)
- features/rd/planning-design.md: 기획디자인 에디터 상세 기술문서
  - 블록 유형 15종, 데이터 구조, 서식 시스템
  - 올가미 다중 선택, Undo/Redo, 키보드 단축키
  - 플로팅 서식 툴바, 우클릭 컨텍스트 메뉴
  - HTML 내보내기, 좌표 기반 인쇄
- INDEX.md에 R&D 문서 등록
2026-03-08 01:35:33 +09:00
03850fefdd docs: 서버 접근 권한 관리 문서 업데이트
- 운영 관리자 정보 추가 (hskwon/권혁성, pro/김보곤)
- 서버 시간대 설정 정리 (OS, PHP CLI/FPM, Laravel)
- develop 그룹 + setgid 공동 관리 구조 추가
- DB 리플리케이션 현황 (sam, sam_stat, codebridge)
- DB 백업 설정 (/data/ 경로, codebridge 포함)
- sam-dev 서버 정보 추가
- INDEX.md에 서버 접근/백업 문서 등록

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:28:08 +09:00
cf189fd453 docs: FQC 문서 시스템 계획 Phase 3 완료 (100%)
- Phase 3 통합 테스트 전체 통과
- 검증 결과 및 테스트 시나리오 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:02:51 +09:00
b1472f3c35 docs: FQC 문서 시스템 계획 Phase 2 완료 (67%)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:43:07 +09:00
5bfc89afa7 docs: [제품검사] FQC 문서 시스템 계획 + 스냅샷 Lazy Snapshot 반영
- fqc-document-system-plan.md: FormRequest 상태 수정, Phase 2.4 Lazy Snapshot 확정, 참고 파일 추가
- document-snapshot-architecture-plan.md: Lazy Snapshot 캡처 원칙 추가
- server-access-management.md 신규
- README.md 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:09:57 +09:00
f1683f753e docs: [문서스냅샷] 계획 문서 보정 - API 수정, 오프스크린 렌더링, 변경이력 반영
- Phase 2 보정 내용 변경이력 3건 추가
- 참고 파일에 UpsertRequest.php, capture-rendered-html.tsx 추가
- 자기완결성 점검 작업 수 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:35:39 +09:00
김보곤
5798058125 docs: [projects] 조직도 관리 시스템 기술문서 추가
- projects/org-chart/README.md: 아키텍처, API, DB, 프론트엔드 상세
- index_projects.md: 조직도 프로젝트 등록
- INDEX.md: 조직도 문서 링크 추가
2026-03-06 20:34:06 +09:00
614e90066f docs: [제품검사] FQC 문서 시스템 구축 계획 작성 + 방법론 수정 6건
- 제품검사 성적서(template 65 보완) + 요청서(신규) 개발 계획
- 방안 C 확정(template items 완전 이관), 시더 방식, 자동 생성 확정
- 방법론 수정: 컬럼 아키텍처(template 2개+시각 8컬럼), rowSpan 복합키,
  프론트 타입 갭, measurement_type=none, Template ID 안정성, 시더 위치 명확화

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:10:50 +09:00
8efe0ac477 docs: [문서스냅샷] Phase 3.3 완료 - 코드 작업 100% 완료
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:54:07 +09:00
303de36e1c docs: [문서스냅샷] Phase 2 완료 - 진행률 90% 업데이트
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:47:13 +09:00
334c8f3918 docs: 문서 스냅샷 아키텍처 계획 + 절곡 문서 매칭 계획
- document-snapshot-architecture-plan.md: B안(HTML 스냅샷) + 구조화 데이터 병행 계획
- mng-bending-document-matching-plan.md: 절곡 중간검사/작업일지 MNG 매칭 계획

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:52:27 +09:00
김보곤
6d042f5bfd docs: [approvals] 품의서 5종 상세 스펙 및 2단계 양식 선택 UI 문서화
- 품의서 5종 공통 Alpine.js 컴포넌트 구조 문서화 (섹션 9)
- 지출/계약체결/구매/출장/비용정산 품의서 Content JSON 스펙 (섹션 10~14)
- 2단계 양식 선택 UI 구조 문서화 (분류→양식 드롭다운)
- 14종 양식 설명 카드 기능 문서화
- 파일 구조에 _purchase-request-form/show 추가
- ApprovalForm 카테고리 DB/UI 분류 구분 설명
- 조회 흐름에 pr_ prefix 분기 로직 추가
2026-03-06 13:33:10 +09:00
김보곤
51446080db docs: [plans] 양식 디자이너 고도화 계획 수립 (6 Phase)
- Phase 2: 블록 런타임 렌더러 + EAV 데이터 바인딩
- Phase 3: 결재선 블록 + 워크플로우 연동
- Phase 4: 동적 테이블 + 변수/매크로 시스템
- Phase 5: 수식 엔진 + 조건부 표시 + 이미지 블록
- Phase 6: 인쇄/PDF + Legacy Builder 대체
- INDEX.md에 계획 문서 등록
2026-03-06 09:29:06 +09:00
김보곤
37bbab7cd4 docs: [documents] 문서양식관리 UI 명칭 반영 (블록 빌더 → 양식 디자이너) 2026-03-06 08:56:52 +09:00
김보곤
29117d65d4 docs: [documents] MNG 문서양식관리 기술문서 추가
- Legacy Builder / Block Builder 비교 및 상세 동작
- saveRelations ID 보존 upsert 메커니즘
- 프리셋 시스템, 연결품목 중복 검증
- 화면 구성 (목록, 편집, 블록 에디터, 미리보기)
- README에 관련 문서 링크 추가
2026-03-06 08:43:14 +09:00
김보곤
78cfc292a9 docs: [documents] MNG 문서관리 시스템 상세 기술문서 추가
- 탭별 기능 (문서목록, 서식관리, FQC현황)
- EAV 데이터 저장 패턴 상세 설명
- 서식 빌더 (Legacy/Block) 아키텍처
- 결재 워크플로우 및 자재 LOT 추적
- README에 관련 문서 링크 추가
2026-03-06 08:36:46 +09:00
김보곤
4f90c0e869 docs: [planning] 주일기업 기획 메뉴 기술문서 추가
- README.md: 전체 개요, 5개 하위 메뉴 구조, 아키텍처
- construction-photos.md: 공사현장 사진대지 (GCS, 행 구조, 음성입력)
- meeting-minutes.md: 회의록 (STT 화자분리, Gemini AI 요약, 오디오 녹음)
- planning-views.md: 견적/프로젝트/워크플로우 화면 명세
- INDEX.md: 문서 인덱스에 planning 등록
2026-03-06 08:25:20 +09:00
김보곤
09793e629b docs: [approvals] 결재 양식 기술 명세 문서 추가
- form-types.md: 6개 양식(휴가/지출결의/재직증명/경력증명/위촉증명/사직서) 필드 구조, JSON Content, UI 인터랙션 명세
- README.md: 문서 구조에 form-types.md 링크 추가
2026-03-06 08:09:56 +09:00
0223c33fd9 docs: [품질관리] 개발 계획 Phase 4 프론트엔드 API 연동 완료 업데이트
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:45:27 +09:00
김보곤
ee42b12c2b docs: [changes] 계좌 입출금내역 무한루프 버그 분석 문서 추가
- 근본 원인: splitDateRangeMonthly() cursor 이동 버그
- 재현 조건, 검증 결과, 교훈/방지 규칙 포함
- 코드베이스 전체 유사 패턴 점검 결과 포함
2026-03-04 13:32:06 +09:00
김보곤
4c581ad7f5 docs: [ai] Gemini 2.5-flash 마이그레이션 문서 추가
- AI 관리 종합 가이드 신규 (ai-management.md)
- 모델 업데이트 워크플로우 신규 (ai-model-update-workflow.md)
- 변경 이력 기록 (20260303_gemini_model_upgrade.md)
- AI 설정 기술문서 모델명 업데이트
- INDEX.md에 AI 문서 3건 등록
2026-03-03 08:09:12 +09:00
김보곤
52417acad6 docs: [credit] 신용평가 시스템 내부 문서 추가
- 쿠콘 API 연동, 국세청 API, DB 구조, 과금 정책 등 정리
2026-03-02 18:40:30 +09:00
김보곤
8cb15cf3c4 docs: [guides] 테이블 설계 가이드 비전문가용 문서 추가
- options JSON 컬럼 패턴을 엑셀 비유로 쉽게 설명
- 멀티테넌시(tenant_id) 개념 해설
- 실제 SAM 테이블 예시 (주문, 입고, 공정)
- FAQ 5개, 판단 흐름도, 한 장 요약 포함
- INDEX.md에 문서 등록
2026-03-02 17:15:25 +09:00
김보곤
a3c910d91b docs: [CLAUDE.md] options JSON 컬럼 정책 필수 참조 규칙 추가
- 테이블 생성/수정 시 options-column-policy.md 참조 의무화
- 전용 컬럼 vs options JSON 분류 기준 요약 포함
- 필수 준수 사항 및 작업 전 체크리스트 추가
2026-03-02 17:06:50 +09:00
김보곤
f8c4536331 docs: [ai-quotation] STT/음성 녹음 현황 반영 — 미구현→구현완료 업데이트
- 음성 녹음 섹션: 기초만→구축 완료로 변경 (GoogleCloudService, AiVoiceRecordingService 등)
- Phase 2: 기존 STT 인프라 재사용 반영, 기간 2주→1주 단축
- 참조 구현 파일 목록 추가
2026-03-02 15:27:41 +09:00
김보곤
23570d3ee9 docs: [vision] SAM AI 자동화 비전 문서 및 PPTX 슬라이드 추가
- docs/system/ai-automation-vision.md 장기 비전 기술문서 생성
- docs/rules/slides/usage-plan/ 7장 HTML 슬라이드 + PPTX 변환
- INDEX.md에 ai-automation-vision.md 등록
2026-03-02 13:25:26 +09:00
김보곤
b8fa244271 docs: [brochure] v7 1page 개선 - 히어로 SVG, Before/After, 기술태그 추가
- 히어로 섹션에 대시보드 모니터 SVG 아이콘 추가
- Before/After 인포그래픽 추가
- 6대 핵심기능 2열 그리드 + SVG 아이콘
- 기술 태그 5개 (실시간, PC+모바일, 역할별 권한, 데이터 암호화, 클라우드)
- PPTX 재생성
2026-03-01 19:37:52 +09:00
김보곤
4c9fd233cc docs: [brochure] v6~v9 CEO Dashboard 브로셔 4종 추가
- v6: Corporate Blue & White (대기업/공공기관 스타일)
- v7: Warm Gray + Teal (IT/SaaS 스타일)
- v8: Two-Tone Navy/White Split (금융/컨설팅 스타일)
- v9: Minimal White + Indigo (Apple/디자인 에이전시 스타일)
- README.md에 v6~v9 디자인 컨셉 문서화
2026-03-01 19:35:32 +09:00
김보곤
8769b68ef0 docs: [brochure] v1~v5 버전별 디자인 컨셉 README 문서화
- 버전별 컬러 팔레트, 디자인 컨셉, 콘텐츠 구성 정리
- 폴더 구조 및 PPTX 변환 주의사항 포함
2026-03-01 19:22:02 +09:00
김보곤
132c573ab9 fix: [brochure] v5 PPTX 빈 파일 수정 - body 그래디언트 → 단색 변경
- html2pptx가 CSS gradient 검출 시 슬라이드 생성 전에 throw하여 빈 PPTX 생성됨
- body background를 단색(#1A1640)으로 변경, 그래디언트는 convert 스크립트에서 PNG로 덮어쓰기
- 구분선 gradient도 solid rgba로 교체
2026-03-01 19:06:55 +09:00
김보곤
415c55b7c0 docs: [brochure] v5 Premium Executive Gradient 브로셔 생성
- 네이비→인디고 그래디언트 배경 + 골드 액센트 디자인
- 글래스모피즘 카드, SVG 아이콘 적용
- Sharp로 그래디언트 배경 PNG 사전 렌더링 (PPTX 호환)
- 앞면/뒷면/1페이지 통합본 + PPTX 변환 스크립트
2026-03-01 18:28:21 +09:00
김보곤
66db1832da refactor: [brochure] docs/brochure-vN → docs/brochure/vN 구조로 통합
- docs/brochure/ → docs/brochure/v1/
- docs/brochure-v2/ → docs/brochure/v2/
- docs/brochure-v3/ → docs/brochure/v3/
- docs/brochure-v4/ → docs/brochure/v4/
- docs 하위 폴더를 큰 단위로 유지
2026-03-01 18:12:38 +09:00
김보곤
d5e6172c22 fix: [brochure] v3/v4 Before/After 텍스트 줄바꿈 방지
- <br> 멀티라인 <p>를 개별 <p> + white-space: nowrap으로 분리
- PPTX 렌더링 시 <span> 텍스트가 다음 행으로 넘어가는 문제 해결
- html2pptx 엔진이 nowrap 감지 → wrap: false 적용
2026-03-01 17:48:41 +09:00
김보곤
08577b5af9 fix: [brochure] v3/v4 Before/After 카드 하단 패딩 12pt로 확대
- PPTX 렌더링 시 AFTER 카드 마지막 줄 잘림 현상 수정
- Before/After 카드 하단 padding: 5pt → 12pt
2026-03-01 17:45:32 +09:00
김보곤
7b45b4c635 fix: [brochure] v4 1page Before/After 카드 하단 텍스트 넘침 수정
- Before/After 카드 padding 5pt → 5pt 6pt 8pt 6pt로 하단 여백 확대
- PPTX 폰트 렌더링 차이로 마지막 줄이 카드 경계를 벗어나는 문제 해결
2026-03-01 17:43:09 +09:00
김보곤
1f06c1a607 docs: [brochure] v4 밝은 배경 CEO Dashboard 브로셔 생성
- v3 다크 테마(#0B1929) → v4 라이트 테마(#F8FAFC)로 변환
- 텍스트 색상: 흰색 → 슬레이트 계열(#0F172A, #475569, #94A3B8)
- 카드 배경: 반투명 다크 → 화이트 + 그림자/테두리
- BI 로고: sam_bi_white → sam_bi_black
- 앞면/뒷면/1page 통합본 HTML + PPTX 변환 스크립트 포함
2026-03-01 16:44:43 +09:00
김보곤
00d7a583cb docs: [guides] HTML → PPTX 변환 도구 사용법 가이드 추가
- 슬라이드 작성법, 변환 스크립트 구조, 실행 방법 포함
- 기존 사용 사례, 문제 해결, 빠른 시작 가이드 포함
2026-03-01 13:02:43 +09:00
김보곤
8be729c698 docs: [brochure] SAM 전체 프로젝트 범용 영업 브로셔 생성
- 1장/2장 브로셔 HTML 슬라이드 및 PPTX 생성
- usecase 방화셔터 브로셔 패턴 활용, 범용 제조업 타깃으로 변환
- 핵심 모듈 8종, 가격 체계, 도입 프로세스, 기대 효과 포함
2026-03-01 12:52:45 +09:00
김보곤
9c5443aec1 docs: [interview] 마스터 질문 SQL에 parent_id 계층 구조 반영
- 대분류 '제조업-방화셔터' INSERT 추가
- 8개 도메인 카테고리에 parent_id=@root_manufacturing 설정
2026-02-28 21:23:36 +09:00
김보곤
5d76705f4f fix: [interview] SQL 파일 한글 인코딩 깨짐 수정
- SET NAMES utf8mb4 추가하여 double-encoding 방지
2026-02-28 21:02:22 +09:00
김보곤
b52c31a700 docs: [interview] 인터뷰 마스터 질문 SQL 파일 추가
- 8개 도메인 × 16개 템플릿 × 80개 질문
- 시더 대신 직접 SQL INSERT 방식
2026-02-28 20:24:54 +09:00
김보곤
490477421d docs: [approvals] 결재관리 시스템 문서 4종 작성
- README.md: 시스템 개요, 아키텍처, DB 스키마, 상태 관리, 권한 매트릭스
- workflows.md: 워크플로우 상세 (승인/반려/회수/보류/전결/복사재기안)
- api-reference.md: API 엔드포인트 20개 명세
- ui-screens.md: UI 화면 구성 및 인터랙션
- INDEX.md에 결재관리 문서 등록
2026-02-28 00:09:08 +09:00
김보곤
359dc5d029 docs: CLAUDE.md MNG 커밋 즉시 자동 푸시 정책 추가
- MNG: 커밋 후 develop push + main cherry-pick 자동 실행
- API/React: 기존 트리거 워드 방식 유지
2026-02-27 12:59:47 +09:00
김보곤
0123c3d780 docs: CLAUDE.md 운영서버 푸시 대상에 API 추가 2026-02-27 09:34:20 +09:00
김보곤
fc97dfe454 revert: CLAUDE.md DB 마이그레이션 정책 원래대로 복원 2026-02-27 09:30:10 +09:00
김보곤
0ae6eec973 docs: CLAUDE.md DB 마이그레이션 정책 변경 (MNG 허용) 2026-02-27 09:24:17 +09:00
김보곤
d24a19a3f1 docs: [esign] 전자계약 알림톡/SMS 환경별 설정 가이드 작성
- esign-notification-guide.md 신규 작성 (환경별 설정, 역할 기반 알림, 템플릿 분기)
- README.md 현황 업데이트 (완료 템플릿, OTP SMS, 환경별 분기 반영)
- INDEX.md에 새 문서 등록
2026-02-27 08:26:14 +09:00
김보곤
ac35c5c0f9 docs: [attendance] MNG 근태현황 개발 계획서 작성
- Phase 1: 버그 수정, 엑셀 다운로드, 일괄 삭제, 통계 기간 선택
- Phase 2: 개인별 상세, 출퇴근 설정, 월간/주간 요약, 일괄 등록
2026-02-26 20:32:43 +09:00
김보곤
fd60e51ac9 docs: CLAUDE.md 운영서버 SSH 접근 불가 정책 추가 2026-02-25 17:10:18 +09:00
김보곤
dd3b045c46 docs: CLAUDE.md 운영서버 푸시를 cherry-pick 방식으로 변경 2026-02-25 17:01:36 +09:00
김보곤
62fdc6869b docs: CLAUDE.md MNG 운영 브랜치 master→main 통일 2026-02-25 15:46:03 +09:00
김보곤
d44b99d5e4 docs: CLAUDE.md 푸시 정책 트리거 워드 기반으로 개편
- "개발서버 푸시" / "운영서버 푸시" 트리거 워드 추가
- 운영서버 푸시 시 main 최신화 → merge → push → develop 동기화 절차 명시
- 브랜치 동기화 규칙 추가 (충돌 방지)
- 푸시 대상 자동 판별 규칙 추가
2026-02-25 15:36:58 +09:00
김보곤
007277d401 fix: [docs] MNG 개발서버 도메인 수정
- mng.dev.codebridge-x.com → admin.codebridge-x.com
- 도메인 스왑(48ef98e) 반영
2026-02-25 09:27:03 +09:00
김보곤
9c00447e18 docs: docs/CLAUDE.md 인프라 변경 동기화
- ~/CLAUDE.md와 동일한 인프라 정보 반영
- 기술 스택: Laravel 12 + PHP 8.4 + MySQL 8.0
- 서버 접속 정보: 개발/운영 2서버, Jenkins CI/CD
- React 빌드: Jenkins 자동화 + fallback 정책
- DB 환경 분리: samdb/sam_prod/sam_stat
- 실행 환경: 3-Tier 비교, 서버 구조도, 도메인 매핑
- 공동 개발: 브랜치 전략, 비상 수동 배포 절차
2026-02-25 06:20:58 +09:00
김보곤
bbe410150d docs: CLAUDE.md 인프라 변경 반영
- 기술 스택: Laravel 12 + PHP 8.4 + MySQL 8.0 업데이트
- 서버 접속 정보: 개발/운영 2서버 분리, Jenkins CI/CD 반영
- 배포 흐름: Jenkins 자동화 파이프라인 다이어그램 추가
- React 빌드: Jenkins 빌드 + fallback 정책으로 변경
- DB 아키텍처: 환경별 분리(samdb/sam_prod/sam_stat), --force 플래그
- 실행 환경: 3-Tier 환경 비교, 서버 구조도, 도메인 매핑 추가
- 공동 개발: 브랜치 전략(develop→개발, main→운영), 비상 수동 배포 절차
2026-02-25 06:16:21 +09:00
김보곤
b8a8ca5442 merge: origin/main sam-docs 저장소 통합
- Gitea sam-docs 원격 저장소 연결
- ops-manual, deploys, 운영 매뉴얼 문서 반영
- admin/mng 도메인 스왑 문서 포함
2026-02-25 05:56:58 +09:00
김보곤
8d6fd5aee6 docs: [business-card] 명함신청 기능 문서 추가
- features/business-card-request.md 생성 (테이블, 워크플로우, 화면 구성, API)
- INDEX.md에 문서 등록
2026-02-25 05:49:33 +09:00
김보곤
41b1e01ce4 refactor: [contracts] 영업파트너 위촉계약서(단체용)에서 관리자(3%) 역할 제거
- 용어 정의에서 관리자 항목 삭제
- 3.2 관리자의 역할 섹션 삭제, 3.3→3.2 번호 조정
- 수수료 비율 테이블에서 관리자 행 삭제
- 수수료 산정 예시 테이블에서 관리자 칼럼 삭제
- docx, md 동시 반영
2026-02-24 17:04:38 +09:00
김보곤
b8e249c6b3 fix: [contracts] 8.4 할인 계약 해지 조건 간소화
- 구독료 관련 조항 제거, 개발비 정산 조건만 유지
2026-02-24 16:56:18 +09:00
김보곤
5acac8f558 refactor: [contracts] 개발사 보호 특약을 제8조 8.4항으로 통합
- 별도 특약 6개 조항 → 제8조 내 4개 항목으로 간소화
- 중복 내용 제거, 핵심 조건만 유지
- docx, md 파일 동시 반영
2026-02-24 16:46:28 +09:00
김보곤
f98d287958 feat: [contracts] 서비스이용계약서 마크다운에 개발사 보호 특약 추가
- docx와 동일한 특약 6개 조항 md 파일에 반영
- 버전 v4.1 → v4.2 업데이트
2026-02-24 16:39:10 +09:00
김보곤
e9c7cd21cc feat: [contracts] 전자계약서에 개발사 보호 특약 추가
- 제1조 특별 할인 및 가격 구조
- 제2조 할인 환수 (1년/2년/3년 감면)
- 제3조 중도 해지 시 정산
- 제4조 서비스 게시 후 해지
- 제5조 손해배상의 예정 (30% 조항)
- 제6조 특약의 효력
2026-02-24 16:37:22 +09:00
김보곤
0c923401bf refactor: [docker] tenant 저장소를 shared-storage에서 mng/storage/app/tenants로 변경
- docker-compose: shared-storage 볼륨 마운트 제거
- entrypoint: storage/app/tenants 디렉토리 생성으로 변경
- nginx: tenant-storage alias 경로 변경
2026-02-23 21:32:51 +09:00
김보곤
73d25b99ec docs: [CLAUDE.md] 로컬(Docker) vs 서버(네이티브) 환경 구분 명확화
- Docker 환경 섹션을 실행 환경 섹션으로 변경
- 서버는 Docker 없이 네이티브로 운영됨을 명시
- 로컬/서버 명령어 비교표 추가
- 서버 접근 정책에서 docker ps/logs 제거 (서버에 Docker 없음)
- 마이그레이션 실행, 공동 개발 워크플로우 등 관련 섹션 일괄 수정
2026-02-23 14:04:15 +09:00
김보곤
b7d1fb97b4 chore: [docker] MNG 컨테이너에 Pretendard 폰트 설치 추가
- LibreOffice Word→PDF 변환 시 Pretendard 폰트 인식을 위해 설치
- wget으로 GitHub 릴리즈에서 OTF 폰트 9개 weight 다운로드
- fc-cache 갱신으로 시스템 폰트로 등록
2026-02-23 13:34:06 +09:00
김보곤
5957261ffa chore: [claude] svg-precision 스킬 CLAUDE.md에 등록 2026-02-23 13:31:13 +09:00
김보곤
9660c58bf4 fix: [contracts] 홈택스 조회 서비스 금액 30,000원으로 수정
- docx 계약서 변경사항 반영 (33,000원 → 30,000원)
2026-02-23 10:43:16 +09:00
김보곤
8601d1738e chore: [docker] MNG 컨테이너에 sales 폴더 read-only 마운트 추가 2026-02-23 08:08:42 +09:00
김보곤
f0f4a8627d docs: [plans] General Rule 스토리보드 D1.0 마크다운 변환
- SAM_General_Rule_Storyboard_D1.0_260116.pdf (43p) → 마크다운 변환
- UIUX 공통 규칙: 인터랙션, 반응형 브레이크포인트, 화면 템플릿, 메시지, GNB/LNB/푸터
- 목록/상세 화면 4단계 반응형 (PC/태블릿/모바일) 정의
- 셀렉트박스, 나의메뉴, 검색필터정렬, 페이지/섹션 설정, 태스크 알림 아이콘
- INDEX.md에 새 문서 등록
- .gitignore에 sam/docs/plans/*.md 허용 추가
2026-02-23 00:55:26 +09:00
김보곤
a0de29d5ec docs: [guides] 서버 동작 원리 초보자 가이드 추가
- 웹 요청 흐름 (브라우저→Nginx→PHP-FPM→Laravel→MySQL)
- 각 구성 요소 역할 및 Supervisor 프로세스 구성
- 로컬 Docker vs 서버 Bare-metal 환경 비교
- git push 후 PHP/React 배포 절차 설명
- 도메인별 서비스 매핑 및 요청 경로
- INDEX.md에 새 문서 등록
2026-02-22 20:50:40 +09:00
김보곤
27e17bad48 docs: [academy] 방화셔터 백과사전 이미지 생성 프롬프트 문서 추가
- Gemini용 이미지 생성 프롬프트 12종 기술문서 저장
- docs/INDEX.md에 문서 등록
- .gitignore에 features/academy 경로 허용
2026-02-22 20:18:54 +09:00
김보곤
98b01bf633 fix: [과금정책] 홈택스 매입/매출 조회 단일 서비스 33,000원으로 수정
- 홈택스 매입/매출은 하나의 서비스 (월 33,000원 × 2 → 월 33,000원)
- billing-policy.md: 매입/매출 2행 → 1행 통합
- customer-pricing.md: × 2 제거
- 서비스이용계약서 DOCX/Markdown: × 2 제거
- 슬라이드 HTML 2종: × 2 제거, 2행 → 1행 통합
2026-02-22 18:55:58 +09:00
김보곤
cd3b155cdc fix: [contracts] 4.5/4.6 테이블 헤더 음영 적용
- 기존 테이블과 동일한 D9D9D9 회색 음영을 헤더 행에 적용
2026-02-22 18:27:56 +09:00
김보곤
e4b875a69f fix: [contracts] 4.5/4.6 테이블 테두리 스타일 적용
- 새로 추가된 사용량 기반 과금 및 바로빌 테이블에 기존 테이블과 동일한 테두리 적용
- tblStyle, tblBorders, tblLayout, tblW 속성 추가
2026-02-22 18:24:58 +09:00
김보곤
92c5b3575d feat: [contracts] 서비스이용계약서 v4.1 사용량 기반 추가 과금 조항 추가
- 제4조 4.5 사용량 기반 추가 과금 조항 추가 (저장 공간, AI 토큰)
- 제4조 4.6 바로빌 부가 서비스 요금 조항 추가
- Markdown 미러 재추출 및 동기화 100% 달성
- revisions.json, CHANGELOG.md 업데이트
2026-02-22 18:05:20 +09:00
김보곤
52e3f8e375 fix: [contracts] Markdown ↔ DOCX 동기화 100% 달성
- 분할 문단 원복 (비밀유지서약서, 영업파트너 위촉계약서 2종)
- 제목 꺾쇠(< >) 복원 (영업파트너 위촉계약서 2종)
- 회사 이메일 누락 복원 (영업파트너 위촉계약서 2종)
- sync_check 정규화 개선 (Bold 마커, 리스트 접두사, 빈 테이블 행)
2026-02-22 17:52:57 +09:00
김보곤
3013406100 fix: [contracts] DOCX 원본 복원 및 개정이력 삽입 스크립트 제거
- DOCX 파일에서 개정이력 테이블 제거 (원본 복원)
- insert_revision_table.py 삭제 (버전 관리는 Markdown에서만)
- docx/ 폴더는 바로 사용 가능한 배포본 유지
2026-02-22 17:46:21 +09:00
김보곤
96f25fc0eb feat: [contracts] 계약서 버전 관리 시스템 구축
- DOCX 4종 → Markdown 미러링 체계 구축 (Git diff 추적)
- DOCX에 개정이력 테이블 삽입 (Pretendard 9pt, 파란 헤더)
- 자동화 스크립트 3종 (추출/삽입/동기화 검증)
- revisions.json, CHANGELOG.md, INDEX.md 업데이트
- .gitignore에 contracts 경로 allowlist 추가
2026-02-22 17:42:31 +09:00
김보곤
d57b84c8f2 docs: [과금정책] 3분할 문서 + 프레젠테이션 PPTX 3종 + 브랜딩 통합
- billing-policy.md → customer-pricing / partner-commission / billing-policy 3분할
- HTML 슬라이드 20장 디자인 (div+flexbox, html2pptx 호환)
- PPTX 3종 생성: customer-pricing(7장), partner-commission(6장), billing-policy(7장)
- 회사명 통일: 주일/경동 → (주)코드브릿지엑스 (전체 문서 + PPTX)
- SAM BI 로고 이미지 7종 추가 (docs/assets/bi/)
- CLAUDE.md PPT 제작 전역 규칙 추가
- e-sign PPTX 내부 회사명 교체
2026-02-21 21:09:56 +09:00
김보곤
01eee88e40 docs: [과금정책] 단체가입 수수료율 추가 및 부가세 별도 명시
- 개인가입(20%/5%/3%) vs 단체가입(30%/0%/3%) 수당 체계 추가
- 유치자 협업지원금(3%) 정책 추가
- 모든 개발비/구독료에 VAT 별도 명시
2026-02-21 19:24:19 +09:00
김보곤
bf6bbf92f7 docs: [과금정책] 가입비 → 개발비 명칭 변경 2026-02-21 19:07:08 +09:00
김보곤
24a542cb95 docs: [과금정책] 과금정책 통합 문서 작성
- 본사 지출 과금정책 (바로빌, AI/클라우드)
- 고객 안내용 과금정책 (가입비, 구독료, 추가 옵션)
- 내부 정산 정책 (영업 수당, 마진 구조)
- INDEX.md에 과금정책 문서 등록
2026-02-21 19:03:12 +09:00
김보곤
6fb6e4fdbe docs: [서버정책] 서버 직접 접근 금지 규칙 추가
- SSH 접속, 파일 수정, 명령 실행 전면 금지
- Claude는 코드 작성/커밋까지만, 배포는 사용자/팀장 역할
- 2026-02-21 502 사고 재발 방지
2026-02-21 16:52:13 +09:00
김보곤
833a957d9e docs: [react] React 빌드/배포 정책 추가
- 서버 빌드 금지, 로컬 빌드 후 배포 정책 명시
- 프로젝트 경로에 react 추가
2026-02-21 15:50:02 +09:00
김보곤
1a1ef04798 docs: [claude] Git 커밋 규칙을 sam/docs 협약 기준으로 통일
- 커밋 형식: type:메시지 → type: [scope] 작업내용
- Co-Authored-By 서명 제외 정책 반영
- style, test 타입 추가
- 푸시 정책 (자동 푸시 금지) 추가
- 커밋 전 체크리스트 보강
2026-02-20 21:34:01 +09:00
김보곤
ccb93e3aca docs:CLAUDE.md에 sam/docs 문서 작성 규칙 협약 추가
- 개발팀 협약으로 sam/docs 문서 작성 기법 준용 기록
- 폴더 선택 기준, 파일명 규칙, 문서 구조 템플릿 정리
- 작성 스타일 규칙 (한글 기본, 섹션 번호, 코드 블록 등)
- plans/ 워크플로우 및 문서 작성 체크리스트 포함

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:26:02 +09:00
김보곤
06e8d5f328 chore:API Docker 환경 설정 (Queue Worker, Scheduler, OPcache 추가)
- supervisord.conf에 queue-worker 1개 + scheduler 추가
- opcache.ini 생성 (MNG 설정과 동일, 256MB)
- docker-compose.yml에 opcache.ini 볼륨 마운트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:56:08 +09:00
김보곤
71654f5f63 fix:supervisord.conf 경로 수정 (/var/www/sales → /var/www/mng)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 10:09:31 +09:00
김보곤
03e12d8fe2 chore:Supervisor에 Queue Worker 2개 자동 실행 추가
- numprocs=2 (영상 2개 동시 생성 가능)
- timeout=1800, max-jobs=10, max-time=3600
- 자동 재시작 (autorestart=true)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 09:11:58 +09:00
김보곤
56fdf76f49 chore:Docker MNG에 FFmpeg 설치 추가 (영상 합성용)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 08:47:09 +09:00
김보곤
8278284e97 feat:Claude Code 스킬/에이전트 파일 Git 추적 추가
- .gitignore에 .claude/skills/, .claude/agents/ 허용 규칙 추가
- pptx-skill SKILL.md에 Direct PptxGenJS 방식 추가 (권장 방법)
- 전체 12개 에이전트, 40+ 스킬 파일 초기 커밋

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 20:54:21 +09:00
김보곤
665e6b52a4 chore:Docker MNG에 나눔 한글 폰트 추가 (DOCX→PDF 한글 깨짐 해결)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:56:58 +09:00
김보곤
61f226be7f chore:Docker MNG에 LibreOffice 설치 추가 (DOCX→PDF 변환용)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 09:13:10 +09:00
김보곤
b1b6a83aef chore:Docker MNG에 GD 확장 추가 (PDF 서명 합성용)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:42:17 +09:00
김보곤
768ab68f13 docs:CLAUDE.md 메뉴 관리 규칙 추가 (시더 실행 금지)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:22:39 +09:00
김보곤
017f492d70 docs:CLAUDE.md 에이전트 목록 업데이트 (12개 전체 반영)
- 신규 에이전트 9개 추가 (code-reviewer, debugger, test-runner, security-auditor, performance-optimizer, refactoring-agent, laravel-expert, git-manager, doc-writer)
- 카테고리별 분류 (코드품질/개발, 워크플로우/문서)
- 모델 및 출처 정보 표기

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 08:02:45 +09:00
김보곤
24271cfef3 docs:CLAUDE.md 스킬 목록 업데이트 (44개 전체 반영)
- 카테고리별 분류 (문서, 코드품질, 테스트, 보안, 디버깅, 프론트엔드, 유틸리티)
- 신규 설치 스킬 21개 추가 (levnikolaevich, Trail of Bits, anthropics 공식)
- 각 스킬별 출처 표기

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 07:41:34 +09:00
pro
6ba8738b71 docs:공동 개발 워크플로우 가이드 추가
- 로컬(Docker) 환경 업데이트 절차
- 서버 환경 업데이트 절차
- 환경별 명령어 요약 표
- pull 후 체크리스트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:11:12 +09:00
pro
a8e5e2fba0 docs:데이터베이스 아키텍처 규칙 추가
- 모든 마이그레이션은 API 프로젝트에서만 관리
- MNG database/migrations 폴더에 파일 생성 금지
- 마이그레이션 실행은 sam-api-1 컨테이너에서만

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:53:55 +09:00
pro
9a2948da6c docs:쿠콘 나이스평가정보 API 문서 마크다운 변환
- PDF 문서(477KB)를 마크다운(29KB)으로 변환
- 용량 약 94% 절감

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:56:37 +09:00
pro
89226629eb docs: Docker 환경 필수 인지 규칙 추가
- 로컬 개발 환경이 Docker 기반임을 명시
- Docker 컨테이너 구조 설명
- php artisan, composer 등 명령어는 docker exec로 실행
- 체크리스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 08:18:13 +09:00
pro
a28a4ef2f2 fix:.gitignore .claude 폴더 제외 (민감정보 보호) 2026-01-21 19:49:40 +09:00
pro
57b9a189a4 chore:전역 CLAUDE.md 및 .gitignore 초기 설정 2026-01-21 19:49:21 +09:00
142 changed files with 27007 additions and 121 deletions

41
.gitignore vendored
View File

@@ -1,2 +1,43 @@
# 모든 파일 무시
*
# 추적할 파일만 허용
!.gitignore
!INDEX.md
!README.md
!resources.md
# 문서 폴더 (루트 기준)
!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/**
# 기타
.DS_Store
_to_notion/

View File

@@ -1,7 +1,7 @@
# SAM 문서 인덱스 (Claude Code용)
> 작업 유형에 맞는 문서를 먼저 읽고 시작하세요.
> 최종 갱신: 2026-03-05
> 최종 갱신: 2026-03-07
---
@@ -21,6 +21,7 @@
| 견적관리 | `features/quotes/README.md` | 견적 시스템, BOM 계산 |
| 운영 배포 | `dev/dev_plans/production-deployment-plan.md` | 배포 계획 |
| 서버 운영 | `dev/deploys/ops-manual/README.md` | 서버 운영 매뉴얼 |
| 서버 접근/백업 | `system/server-access-management.md` | 계정, 권한, 백업, 리플리케이션 |
| MES | `projects/mes/README.md` | MES 프로젝트 |
---
@@ -72,6 +73,7 @@ docs/
| [docker-setup.md](system/docker-setup.md) | Docker 환경 + CI/CD |
| [database/README.md](system/database/README.md) | DB 스키마 인덱스 |
| [security-policy.md](system/security-policy.md) | 보안 정책 |
| [server-access-management.md](system/server-access-management.md) | 서버 접근 권한, 백업, 리플리케이션 |
| [scaling-roadmap.md](system/scaling-roadmap.md) | 스케일링 로드맵 |
| [board-system-spec.md](system/board-system-spec.md) | 게시판 시스템 설계 |
| [item-master-integration.md](system/item-master-integration.md) | 품목 마스터 통합 설계 |
@@ -139,6 +141,7 @@ DB 도메인별:
| [card-vehicle/README.md](features/card-vehicle/README.md) | 법인카드·차량 |
| [settlement/README.md](features/settlement/README.md) | 정산 |
| [barobill-kakaotalk/README.md](features/barobill-kakaotalk/README.md) | 바로빌 카카오톡 |
| [quality-management/README.md](features/quality-management/README.md) | 품질관리 (제품검사, 실적신고) |
---

View File

@@ -1,4 +1,4 @@
# SAM 프로젝트 문서
# SAM 프로젝트 문서
SAM ERP 시스템의 기술 문서, 비즈니스 규칙, 기능 명세를 관리하는 저장소입니다.

Binary file not shown.

Binary file not shown.

View File

@@ -197,10 +197,12 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.08); padding-top: 10pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.7);">SAM</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.7);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3);">무료 데모 및 상담</p>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 600; color: #10B981;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -209,17 +209,19 @@
<div style="margin-top: auto; background: rgba(16,185,129,0.08); border: 1pt solid rgba(16,185,129,0.2); border-radius: 8pt; padding: 12pt 14pt;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<p style="white-space: nowrap; font-size: 10pt; font-weight: 800; color: #ffffff;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 7.5pt; color: rgba(255,255,255,0.45); margin-top: 3pt;">귀사에 최적화된 맞춤 데모를 제공합니다</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.4); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #10B981;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.4); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 6pt; text-align: center;">
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2);">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2);">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -110,8 +110,8 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.08); padding-top: 12pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.7);">SAM</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.7);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.35);">뒷면에서 상세 기능과 가격을 확인하세요</p>

View File

@@ -248,10 +248,12 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.06); padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.65);">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.25); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.65);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.25); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.3);">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 600; color: #0EA5E9;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -224,17 +224,19 @@
<div style="margin-top: auto; background: rgba(14,165,233,0.08); border: 1.5pt solid rgba(14,165,233,0.2); border-radius: 8pt; padding: 10pt 14pt;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<p style="white-space: nowrap; font-size: 10pt; font-weight: 800; color: #ffffff;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.4); margin-top: 2pt;">대표님 전용 대시보드를 직접 체험해 보세요</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.35); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #0EA5E9;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.35); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 5pt; text-align: center;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.18);">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.18);">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -160,8 +160,8 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.06); padding-top: 10pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.65);">SAM</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.25); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 700; color: rgba(255,255,255,0.65);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.25); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 7pt; color: rgba(255,255,255,0.3);">뒷면에서 상세 기능을 확인하세요</p>

View File

@@ -392,10 +392,12 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.06); padding-top: 7pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: rgba(255,255,255,0.6);">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: rgba(255,255,255,0.6);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.25);">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #0EA5E9;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -354,18 +354,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#0EA5E9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #ffffff;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.35); margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #0EA5E9;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.15);">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.15);">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -250,8 +250,8 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(255,255,255,0.06); padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.6);">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.6);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.25);">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -392,10 +392,12 @@
<div style="margin-top: auto; border-top: 1pt solid #E2E8F0; padding-top: 7pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #475569;">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: #CBD5E1; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #475569;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: #CBD5E1; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8;">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #0EA5E9;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -354,18 +354,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#0EA5E9" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #0F172A;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8; margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8; margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #0EA5E9;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8; margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: #CBD5E1;">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #CBD5E1;">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -248,8 +248,8 @@
<div style="margin-top: auto; border-top: 1pt solid #E2E8F0; padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #475569;">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #CBD5E1; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #475569;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #CBD5E1; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8;">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -306,10 +306,12 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(251,191,36,0.1); padding-top: 7pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: rgba(255,255,255,0.6);">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: rgba(255,255,255,0.6);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.25);">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #FBBF24;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -292,18 +292,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#FBBF24" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #ffffff;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.25); margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #FBBF24;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.25); margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.15);">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.15);">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -204,8 +204,8 @@
<div style="margin-top: auto; border-top: 1pt solid rgba(251,191,36,0.1); padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.6);">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: rgba(255,255,255,0.6);">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.2); margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.25);">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -359,10 +359,12 @@
<div style="margin-top: auto; background: #EFF6FF; border-radius: 5pt; padding: 7pt 10pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #1E293B;">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #1E293B;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: #94A3B8;">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #2563EB;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -316,18 +316,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#FFFFFF" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #FFFFFF;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.6); margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.5); margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #FFFFFF;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.5); margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: #94A3B8;">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #94A3B8;">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</div>

View File

@@ -217,8 +217,8 @@
<div style="margin-top: auto; background: #EFF6FF; border-radius: 5pt; padding: 8pt 12pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #1E293B;">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #1E293B;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8;">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -363,10 +363,12 @@
<div style="margin-top: auto; border-top: 1pt solid #D6D3D1; padding-top: 7pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #57534E;">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: #A8A29E; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 700; color: #57534E;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: #A8A29E; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: #A8A29E;">무료 데모 신청</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #0D9488;">contact@codebridge-x.com</p>
</div>
</div>
</div>

View File

@@ -354,18 +354,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#0D9488" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #292524;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #A8A29E; margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: #A8A29E; margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #0D9488;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: #A8A29E; margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: #A8A29E;">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #A8A29E;">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -266,8 +266,8 @@
<div style="margin-top: auto; border-top: 1pt solid #D6D3D1; padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #57534E;">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #A8A29E; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #57534E;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #A8A29E; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: #A8A29E;">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -218,18 +218,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#F97316" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 800; color: #FFFFFF;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.5); margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6pt; font-weight: 700; color: #F97316;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="margin-top: 5pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5pt; color: #94A3B8;">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5pt; color: #94A3B8;">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</div>
</body>

View File

@@ -301,18 +301,20 @@
<path d="M11 6 L11 11 L15 13" fill="none" stroke="#F97316" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div>
<p style="white-space: nowrap; font-size: 9pt; font-weight: 800; color: #FFFFFF;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: rgba(255,255,255,0.5); margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #F97316;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 6pt; color: rgba(255,255,255,0.3); margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- 회사명 -->
<div style="padding: 5pt 0; text-align: center;">
<p style="white-space: nowrap; font-size: 5.5pt; color: #94A3B8;">SAM Smart Automation Management</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #94A3B8;">(주)코드브릿지엑스 | SAM - Smart Automation Management</p>
</div>
</body>
</html>

View File

@@ -268,8 +268,8 @@
<div style="margin-top: auto; border-top: 1pt solid #E2E8F0; padding-top: 8pt;">
<div style="display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #475569;">SAM</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7.5pt; font-weight: 700; color: #475569;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<div style="text-align: right;">
<p style="white-space: nowrap; font-size: 6.5pt; color: #94A3B8;">뒷면에서 상세 기능을 확인하세요 &#9654;</p>

View File

@@ -250,15 +250,17 @@
<div style="margin-top: auto; background: #F8FAFC; border: 1pt solid #F1F5F9; border-radius: 5pt; padding: 8pt 10pt;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<p style="white-space: nowrap; font-size: 8pt; font-weight: 800; color: #0F172A;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #64748B; margin-top: 1pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
<div style="text-align: right; border-left: 2pt solid #6366F1; padding-left: 8pt;">
<p style="white-space: nowrap; font-size: 5pt; color: #CBD5E1; margin-top: 1pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6pt; font-weight: 700; color: #6366F1;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 5pt; color: #CBD5E1; margin-top: 1pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<div style="margin-top: 4pt; text-align: center;">
<p style="white-space: nowrap; font-size: 4.5pt; color: #CBD5E1;">SAM</p>
<p style="white-space: nowrap; font-size: 4.5pt; color: #CBD5E1;">(주)코드브릿지엑스</p>
</div>
</body>
</html>

View File

@@ -211,17 +211,19 @@
<div style="margin-top: auto; background: #F8FAFC; border: 1pt solid #F1F5F9; border-radius: 6pt; padding: 12pt 14pt;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<p style="white-space: nowrap; font-size: 10pt; font-weight: 800; color: #0F172A;">무료 데모를 신청하세요</p>
<p style="white-space: nowrap; font-size: 6pt; color: #64748B; margin-top: 2pt;">대표님 전용 대시보드를 직접 체험</p>
</div>
<div style="text-align: right; border-left: 2pt solid #6366F1; padding-left: 10pt;">
<p style="white-space: nowrap; font-size: 5.5pt; color: #CBD5E1; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 6.5pt; font-weight: 700; color: #6366F1;">contact@codebridge-x.com</p>
<p style="white-space: nowrap; font-size: 5.5pt; color: #CBD5E1; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
</div>
</div>
<!-- Footer -->
<div style="margin-top: 6pt; text-align: center;">
<p style="white-space: nowrap; font-size: 5pt; color: #CBD5E1;">SAM</p>
<p style="white-space: nowrap; font-size: 5pt; color: #CBD5E1;">(주)코드브릿지엑스</p>
</div>
</body>
</html>

View File

@@ -172,8 +172,8 @@
<!-- Footer -->
<div style="margin-top: auto; display: flex; justify-content: space-between; align-items: flex-end;">
<div>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #0F172A;">SAM</p>
<p style="white-space: nowrap; font-size: 6pt; color: #CBD5E1; margin-top: 2pt;">www.sam.it.kr</p>
<p style="white-space: nowrap; font-size: 7pt; font-weight: 600; color: #0F172A;">(주)코드브릿지엑스</p>
<p style="white-space: nowrap; font-size: 6pt; color: #CBD5E1; margin-top: 2pt;">www.codebridge-x.com</p>
</div>
<p style="white-space: nowrap; font-size: 5.5pt; color: #CBD5E1;">뒷면에서 상세 기능을 확인하세요</p>
</div>

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
# 품의서 지급방법 UI 개선
**날짜:** 2026-03-06
**작업자:** Claude Code
## 변경 개요
품의서 2종(구매품의서, 비용정산품의서)에 지급방법 선택 기능을 추가/개선하였다.
## 변경 내용
### 1. 구매품의서 (pr_purchase) - 지급방법 추가
- 납품 정보(납품업체/납품예정일/납품장소) 아래에 **지급방법 radio** 추가
- 옵션: `법인카드` / `계좌이체`
- **일괄 선택** 방식 (전체 구매건에 하나의 지급방법)
### 2. 비용정산품의서 (pr_settlement) - 지급방법 행별 변경
- 기존: 테이블 아래에 일괄 radio (법인카드/개인선지출)
- 변경: 각 내역행에 **지급방법 select** 컬럼 추가
- 테이블 하단에 **지급방법별 합계표** 추가 (법인카드 합계 / 개인선지출 합계)
- 이유: 하나의 정산서에 법인카드/개인선지출 내역이 혼재할 수 있음
### 3. 지출품의서 (pr_expense) - 라벨 변경
- `사용일자` -> `지출일자` 라벨 변경 (폼 + 조회 화면)
## 수정된 파일
| 파일 | 변경 내용 |
|------|----------|
| `mng/resources/views/approvals/partials/_purchase-request-form.blade.php` | 구매품의서 지급방법 radio 추가, 비용정산품의서 행별 select + 합계표, 지출일자 라벨 변경 |
| `mng/resources/views/approvals/partials/_purchase-request-show.blade.php` | 구매품의서/비용정산품의서 조회 화면 동기화 |
## Alpine.js 데이터 변경
### 구매품의서
```javascript
// formData에 추가
payment_method: initialData?.payment_method || '',
// getFormData()에 포함
{ ...base, ..., payment_method: this.formData.payment_method }
```
### 비용정산품의서
```javascript
// makeItem()에 추가
payment_method: data?.payment_method || '',
// computed 속성 추가
get corporateCardTotal() { /* corporate_card 행만 합산 */ },
get personalAdvanceTotal() { /* personal_advance 행만 합산 */ },
// getFormData() 변경
// 기존: payment_method: this.formData.payment_method (일괄)
// 변경: 각 item.payment_method (행별) + corporate_card_total, personal_advance_total
```
## 관련 문서
- [결재 양식 기술 명세](../features/approvals/form-types.md) - 섹션 12, 14 업데이트
---
**최종 업데이트**: 2026-03-06

42
contracts/CHANGELOG.md Normal file
View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

58
contracts/revisions.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -218,14 +218,24 @@
### 서비스 현황
| 서비스 | 포트 | 상태 |
|--------|------|------|
| Nginx | 80/443 | active |
| Apache | 8080 | active (레거시) |
| MySQL 8.4 | 3306 (localhost) | active |
| Gitea | 3000 | active |
| Next.js (PM2) | 3001 | active |
| fail2ban | - | active |
| 서비스 | 포트 | 상태 | 비고 |
|--------|------|------|------|
| Nginx | 80/443 | active | |
| PHP 8.4 FPM | unix socket | active | SAM 앱 (api, mng, sales) |
| PHP 7.3 FPM | unix socket | active | 5130.codebridge-x.com 레거시 전용 |
| MySQL 8.4 | 3306 (localhost) | active | binlog 7일 보관 |
| Gitea | 3000 | active | |
| Next.js (PM2) | 3001 | active | |
| fail2ban | - | active | |
| Swap | - | active | 4G (/swapfile, fstab 등록) |
| ~~Apache~~ | ~~8080~~ | **disabled** | 2026-03-09 비활성화 (front.5130.co.kr 미사용) |
| ~~PHP 5.6 FPM~~ | - | **disabled** | 2026-03-09 비활성화 (미사용) |
### 자동 정리 (cron, hskwon)
| 작업 | 주기 | 설명 |
|------|------|------|
| Gitea repo-archive 캐시 정리 | 매주 일요일 04:00 | 7일 이상 된 캐시 파일 삭제 + 빈 디렉토리 정리 |
### 방화벽 (UFW) 규칙

View File

@@ -48,6 +48,59 @@ chmod 640 /home/webservice/mng/shared/.env
---
## [개발] sam-dev 리소스 관리
sam-dev는 2 vCPU / 3.8GB RAM으로 여러 서비스가 공존하므로 리소스 관리가 중요하다.
### 주요 리소스 소비자
| 프로세스 | 메모리 | CPU (상시) | 비고 |
|----------|--------|-----------|------|
| MySQL | ~1.2G (30%) | 2~5% | |
| Gitea | ~400M (10%) | 1.8% | |
| Next.js (PM2) | ~380M (9.5%) | 0.5% | |
| PHP 8.4 FPM | ~250M (변동) | 요청 시 높음 | max_children=5 |
| PHP 7.3 FPM | ~30M | idle | 5130 레거시 전용 |
### Swap (4G, 2026-03-09 추가)
Swap 없이 운영하면 메모리 부족 시 OOM Killer가 프로세스를 즉시 종료한다.
```bash
# Swap 상태 확인
swapon --show
free -m
# /etc/fstab에 등록되어 있으므로 재부팅 후에도 유지됨
# /swapfile none swap sw 0 0
```
### Gitea repo-archive 캐시
Gitea는 Web UI에서 ZIP/TAR.GZ 다운로드 시 `/var/lib/gitea/data/repo-archive/`에 캐시를 생성한다.
기본 설정에서는 만료가 없어 무한 증가하므로, cron으로 7일 이상 된 캐시를 정리한다.
```bash
# 캐시 크기 확인
sudo du -sh /var/lib/gitea/data/repo-archive/
# 수동 정리 (긴급 시)
sudo rm -rf /var/lib/gitea/data/repo-archive/*
# 자동 정리: hskwon crontab (매주 일요일 04:00)
# 0 4 * * 0 find /var/lib/gitea/data/repo-archive -type f -mtime +7 -delete
# 0 4 * * 0 find /var/lib/gitea/data/repo-archive -type d -empty -delete
```
### 비활성화된 서비스 (2026-03-09)
| 서비스 | 사유 | 복원 명령 |
|--------|------|----------|
| PHP 5.6 FPM | 미사용 (아무 사이트도 참조 안 함) | `sudo systemctl enable --now php5.6-fpm` |
| Apache | front.5130.co.kr 미사용 | `sudo systemctl enable --now apache2` |
---
## 시스템 리소스 모니터링
양쪽 서버 공통 명령어:

View File

@@ -92,6 +92,33 @@ gunzip -c /home/hskwon/backups/mysql/gitea_YYYYMMDD_HHMMSS.sql.gz | mysql gitea
---
## [개발] MySQL Binlog 관리
sam-dev에서는 리플리케이션/PITR을 사용하지 않으므로 binlog 보관을 최소화한다.
**설정 파일:** `/etc/mysql/mysql.conf.d/mysqld.cnf`
```ini
binlog_expire_logs_seconds = 604800 # 7일 보관 (2026-03-09 설정)
max_binlog_size = 100M
```
```bash
# binlog 상태 확인
sudo ls -lh /var/lib/mysql/binlog.0* | wc -l # 파일 수
sudo du -shc /var/lib/mysql/binlog.0* | tail -1 # 총 크기
# 수동 퍼지 (필요 시)
mysql -u pro -p -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY);"
# binlog 완전 비활성화가 필요하면 (권장하지 않음)
# mysqld.cnf에 skip-log-bin 추가 후 MySQL 재시작
```
> **참고:** 운영서버(sam-prod)의 binlog는 CI/CD 백업에 활용되므로 별도 정책 적용.
---
## Slow Query 분석 (운영)
```bash

View File

@@ -0,0 +1,385 @@
# 문서 스냅샷 아키텍처 계획
> **작성일**: 2026-03-06
> **목적**: 문서 보기/인쇄 시 HTML 스냅샷 기반 출력으로 전환 (B안 + 구조화 데이터 병행)
> **상태**: ✅ 코드 완료 (검증 대기)
> **영향 범위**: API(저장), React(캡처/전송), MNG(출력)
---
## 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 2 전면 보정: API 누락 수정, 오프스크린 렌더링 적용, readOnly 자동 캡처 제거 |
| **다음 작업** | Phase 4: 브라우저 검증 + 기존 partial 정리 |
| **진행률** | 13/13 (100% 코드 완료, 검증 대기) |
| **마지막 업데이트** | 2026-03-06 |
---
## 1. 개요
### 1.1 배경
현재 MNG 문서 보기(`show.blade.php`)는 문서 양식별로 전용 blade partial을 만들어 렌더링한다:
- `bending-inspection-data.blade.php` (절곡 중간검사)
- `bending-worklog.blade.php` (절곡 작업일지)
이 방식의 문제:
1. **확장 불가**: 회사마다 다양한 양식이 존재 → 양식마다 blade 파일 생성 불가
2. **스냅샷 미보장**: 하드코딩된 제품 목록/도면치수가 정책 변경 시 과거 문서를 깨뜨림
3. **이중 렌더링**: React와 MNG에서 동일 문서를 각각 렌더링 → 불일치 발생
### 1.2 목표 아키텍처
```
[React] 문서 저장 시
├── 구조화 데이터 저장 (기존 유지)
│ ├── document_data (EAV 플랫)
│ └── work_order_items.options.inspection_data (JSON 스냅샷)
└── rendered_html 저장 (신규)
└── React가 렌더링한 HTML을 캡처 → documents.rendered_html에 저장
[MNG] 문서 보기 시
├── rendered_html 있으면 → 그대로 출력 (렌더링 로직 0)
└── rendered_html 없으면 → 기존 동적 렌더링 fallback
```
### 1.3 핵심 원칙
```
1. 하나의 view 파일로 모든 문서를 보기 (문서 양식별 blade 파일 금지)
2. rendered_html이 있으면 무조건 그것을 사용 (완전한 스냅샷)
3. 구조화 데이터는 편집/검색/통계용으로 병행 유지
4. React에서만 문서 렌더링 책임 → MNG는 출력만 담당
5. Lazy Snapshot: 조회 시 rendered_html 없으면 자동 캡처 → 저장 (점진적 스냅샷 전환)
```
### 1.4 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| 즉시 가능 | blade 템플릿 수정, 기존 partial 정리 | 불필요 |
| 컨펌 필요 | API 저장 로직 변경, React 저장 흐름 변경 | **필수** |
| 금지 | documents 테이블 구조 변경 (이미 rendered_html 존재) | 불필요 |
---
## 2. 현황 분석
### 2.1 DB 현황
`documents` 테이블에 이미 `rendered_html` (LONGTEXT, nullable) 컬럼이 존재:
- 마이그레이션: `api/database/migrations/2026_02_28_100001_add_block_data_to_documents.php`
- 현재 값: 모든 문서에서 NULL (사용 안 됨)
- **DB 변경 불필요**
### 2.2 React 현황 (구현 완료)
#### 캡처 원칙 A: 입력 시 저장 (Active Capture)
입력 화면에서 저장할 때 해당 데이터의 "문서 뷰"를 캡처. 보기(readOnly)에서는 캡처하지 않음.
| Save Path | 파일 | 방식 | 캡처 대상 |
|-----------|------|------|----------|
| 작업일지 저장 | `WorkLogModal.tsx` | contentWrapperRef.innerHTML | 작업일지 문서 뷰 |
| 검사성적서 저장 (edit) | `InspectionReportModal.tsx` | contentWrapperRef.innerHTML | 검사 성적서 문서 뷰 |
| 수입검사 저장 | `ImportInspectionInputModal.tsx` | 오프스크린 렌더링 (`captureRenderedHtml`) | 수입검사 성적서 문서 (`ImportInspectionDocument`) |
| WorkerScreen 인라인 검사 저장 | `index.tsx` | 미캡처 (데이터만 저장) | 성적서 모달에서 저장 시 캡처 |
> **WorkerScreen 인라인 저장**: 검사 입력 시점에 성적서 문서가 렌더링되지 않으므로 rendered_html 미포함.
> 이후 InspectionReportModal을 edit 모드로 열어 저장하면 캡처됨.
> 향후 오프스크린 렌더링으로 확장 가능 (템플릿 로딩 등 async 의존성 해결 필요).
#### 캡처 원칙 B: 조회 시 자동 캡처 (Lazy Snapshot)
문서 조회(view/readOnly) 시 `rendered_html`이 없으면 자동 캡처하여 백그라운드 저장.
```
문서 View 시
├── rendered_html 있음 → 그대로 표시 (기존)
└── rendered_html 없음 → 동적 렌더링 완료 후 캡처 → API로 rendered_html 저장
(다음 조회부터는 스냅샷 사용)
```
**적용 대상**:
- readonly 문서 (제품검사 요청서 등 — 입력 없이 자동 생성되는 문서)
- 마이그레이션 이전 기존 데이터 (rendered_html이 NULL인 과거 문서)
- WorkerScreen 인라인 저장 후 아직 모달에서 저장하지 않은 문서
**구현 방식**:
```typescript
// 문서 표시 컴포넌트에서 (DocumentViewer, Modal 등)
useEffect(() => {
if (document && !document.rendered_html && isContentRendered) {
const html = contentWrapperRef.current?.innerHTML
|| await captureRenderedHtml(DocumentComponent, props);
patchDocumentRenderedHtml(document.id, html); // 백그라운드 저장
}
}, [document, isContentRendered]);
```
**고려사항**:
- 사용자 UX 영향 없음 (백그라운드 비동기 저장)
- 조회 권한만 있는 사용자도 트리거 가능해야 함
- 동시 접속 시 중복 저장 가능 → 같은 HTML이므로 실질적 문제 없음
- 캡처 타이밍: template 로드 + 데이터 바인딩 완료 후 (isContentRendered 판단 필요)
### 2.3 API 현황 (구현 완료)
- Document 모델 `$fillable``rendered_html` 포함 ✅
- `DocumentService` store/update에서 `rendered_html` 저장 ✅
- `DocumentService` upsert에서 `rendered_html` 전달 ✅ (수입검사 경로)
- `StoreRequest`/`UpdateRequest``rendered_html` nullable string 검증 ✅
- `UpsertRequest``rendered_html` nullable string 검증 ✅
### 2.4 MNG 현황 (구현 완료)
- `show.blade.php`: rendered_html 우선 출력, 없으면 기존 동적 렌더링 fallback ✅
- `print.blade.php`: 동일 패턴 적용 ✅
- 전용 partial 파일 (삭제 대기):
- `partials/bending-inspection-data.blade.php`
- `partials/bending-worklog.blade.php`
---
## 3. 작업 범위
### Phase 0: 사전 정리
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 0.1 | API Document 모델 $fillable 확인 및 rendered_html 추가 | ✅ | |
| 0.2 | 기존 절곡 전용 partial 파일 정리 방침 결정 | ✅ | rendered_html 전환 후 삭제 |
### Phase 1: API - rendered_html 저장 지원
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | Document 모델 $fillable에 rendered_html 추가 | ✅ | |
| 1.2 | DocumentService store/update에서 rendered_html 저장 | ✅ | |
| 1.3 | StoreRequest/UpdateRequest에 rendered_html 검증 추가 | ✅ | nullable, string |
| 1.4 | WorkOrderService inspection/worklog에 rendered_html 전달 | ✅ | create + update 모두 |
### Phase 2: React - HTML 캡처 및 전송
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | 오프스크린 렌더링 유틸리티 생성 | ✅ | `captureRenderedHtml()``flushSync` + `createRoot` |
| 2.2 | InspectionReportModal 저장 시 rendered_html 포함 전송 | ✅ | contentWrapperRef.innerHTML 캡처 |
| 2.3 | 작업일지 저장 시 rendered_html 포함 전송 | ✅ | contentWrapperRef.innerHTML 캡처 |
| 2.4 | ImportInspectionInputModal 수입검사 저장 시 rendered_html | ✅ | 오프스크린 성적서 문서 렌더링 |
| 2.5 | ReceivingManagement/actions saveInspectionData 파라미터 추가 | ✅ | rendered_html → /documents/upsert 전달 |
| 2.6 | API UpsertRequest에 rendered_html 검증 추가 | ✅ | nullable string |
### Phase 3: MNG - 스냅샷 출력
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | show.blade.php에 rendered_html 우선 출력 로직 추가 | ✅ | |
| 3.2 | 기존 전용 partial 파일 fallback으로 유지 (과도기) | ✅ | |
| 3.3 | print.blade.php에도 rendered_html 출력 적용 | ✅ | 스냅샷 우선, 레거시 fallback |
### Phase 4: 검증 및 정리
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 4.1 | 브라우저 검증 (MNG 보기/인쇄) | ⏳ | |
| 4.2 | 기존 전용 partial 파일 삭제 | ⏳ | rendered_html 전환 완료 후 |
---
## 4. 상세 작업 내용
### 4.1 HTML 캡처 방식 (Phase 2.1)
React에서 문서 컨텐츠 영역의 DOM을 캡처할 때 고려사항:
**방법 A: innerHTML 직접 추출 + CSS 인라인화**
```typescript
// 문서 컨텐츠 영역에 ref 부여
const contentRef = useRef<HTMLDivElement>(null);
// 저장 시 HTML 추출
const captureHtml = () => {
const el = contentRef.current;
if (!el) return '';
// Tailwind 클래스 → 인라인 스타일 변환 (또는 스타일시트 포함)
// 방법 1: 계산된 스타일을 인라인으로
// 방법 2: 필요한 Tailwind CSS를 <style> 태그로 포함
return el.innerHTML;
};
```
**방법 B: 자체 CSS 포함 완전한 HTML 조각**
```html
<div class="document-snapshot">
<style>/* 필요한 스타일만 추출 */</style>
<!-- 문서 컨텐츠 -->
</div>
```
**권장: 방법 B** — MNG에서 Tailwind가 로드되어 있으므로, Tailwind 클래스를 그대로 사용하되 MNG에 없는 커스텀 스타일만 `<style>` 태그로 포함. MNG도 Tailwind를 사용하므로 대부분의 클래스가 호환됨.
### 4.2 MNG 출력 구조 (Phase 3.1)
```blade
{{-- show.blade.php --}}
@if($document->rendered_html)
{{-- 스냅샷 모드: React가 생성한 HTML 그대로 출력 --}}
<div class="document-snapshot-container">
{!! $document->rendered_html !!}
</div>
@else
{{-- 동적 모드: 기존 템플릿 기반 렌더링 (fallback) --}}
@include('documents.partials.dynamic-render')
@endif
```
### 4.3 스타일 호환성 전략
| 항목 | React (Tailwind) | MNG (Tailwind) | 호환성 |
|------|------------------|----------------|--------|
| 기본 클래스 | px-2, py-1, border 등 | 동일 | 완전 호환 |
| 반응형 | sm:, md:, lg: | inline style 정책 | 주의 필요 |
| 커스텀 컴포넌트 | shadcn/ui | 없음 | `<style>` 포함 필요 |
**결론**: React에서 문서 영역은 순수 HTML+Tailwind로 렌더링 (shadcn 컴포넌트 미사용) → MNG 호환성 높음.
단, `@media` 쿼리나 MNG에서 빌드되지 않은 Tailwind 클래스가 있을 수 있으므로 필요 시 인라인 스타일 변환.
### 4.4 XSS 보안 고려
`{!! !!}` (unescaped output) 사용 시 XSS 위험:
- `rendered_html`**자체 시스템(React)에서만 생성** → 외부 입력 아님
- API 저장 시 **sanitize** 처리 권장 (script 태그, on* 이벤트 제거)
- 또는 CSP(Content Security Policy)로 인라인 스크립트 차단
---
## 5. 참고 파일
### React (수정 대상)
- `react/src/components/production/WorkOrders/documents/InspectionReportModal.tsx` — 저장 흐름 (contentWrapperRef)
- `react/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx` — 검사 문서 렌더링
- `react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx` — 작업일지 렌더링
- `react/src/components/production/WorkOrders/documents/bending/` — 절곡 섹션 컴포넌트들
- `react/src/components/production/WorkerScreen/WorkLogModal.tsx` — 작업일지 저장 시 캡처
- `react/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx` — 수입검사 저장 시 오프스크린 캡처
- `react/src/components/material/ReceivingManagement/actions.ts` — saveInspectionData rendered_html 전달
- `react/src/lib/utils/capture-rendered-html.tsx` — 오프스크린 렌더링 유틸리티 (신규)
### API (수정 대상)
- `api/app/Models/Documents/Document.php` — $fillable
- `api/app/Services/DocumentService.php` — store/update/upsert
- `api/app/Http/Requests/Documents/StoreRequest.php` — 검증
- `api/app/Http/Requests/Documents/UpdateRequest.php` — 검증
- `api/app/Http/Requests/Document/UpsertRequest.php` — 검증 (수입검사 경로)
### MNG (수정 대상)
- `mng/resources/views/documents/show.blade.php` — 메인 보기
- `mng/resources/views/documents/print.blade.php` — 인쇄
- `mng/resources/views/documents/partials/bending-inspection-data.blade.php` — 삭제 예정
- `mng/resources/views/documents/partials/bending-worklog.blade.php` — 삭제 예정
- `mng/app/Http/Controllers/DocumentController.php` — show()
### DB
- `api/database/migrations/2026_02_28_100001_add_block_data_to_documents.php` — rendered_html 컬럼 (이미 존재)
### 문서
- `docs/features/documents/README.md` — 문서관리 시스템
- `docs/system/database/documents.md` — DB 스키마
---
## 6. 의존성 및 순서
```
Phase 0 (사전 정리)
Phase 1 (API: rendered_html 저장)
Phase 2 (React: HTML 캡처 + 전송) ← Phase 1 완료 필요
Phase 3 (MNG: 스냅샷 출력) ← Phase 2 완료 후 데이터 존재
Phase 4 (검증 + 정리) ← 모든 Phase 완료 후
```
Phase 1과 Phase 3의 MNG 코드 수정은 병렬 가능 (fallback 유지).
단, 실제 데이터가 있어야 검증 가능하므로 Phase 2 완료 후 통합 검증.
---
## 7. 리스크 및 대응
| 리스크 | 영향 | 대응 |
|--------|------|------|
| HTML 용량 | 문서당 50-200KB, 수만 건 시 GB | LONGTEXT 이미 사용, 필요 시 gzip 압축 |
| Tailwind 클래스 불일치 | MNG에서 일부 스타일 깨짐 | 인라인 스타일 변환 또는 MNG Tailwind 빌드에 포함 |
| XSS | rendered_html에 악성 코드 | API에서 sanitize, CSP 정책 |
| 과도기 fallback | rendered_html 없는 기존 문서 | 기존 동적 렌더링 유지 |
| React 미경유 문서 | MNG에서만 생성된 문서 | MNG 저장 시에도 rendered_html 생성 검토 |
---
## 8. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-03-06 | - | 계획 문서 작성 | - | - |
| 2026-03-06 | Phase 0~3 | Phase 0~3 전체 구현 완료 | API/React/MNG 다수 | ✅ |
| 2026-03-06 | Phase 2.4~2.6 | 모든 저장 경로에 rendered_html 추가 | InspectionReportModal, ImportInspectionInputModal, actions.ts | ✅ |
| 2026-03-06 | Phase 2 보정 | API UpsertRequest rendered_html 누락 수정, DocumentService upsert() rendered_html 전달 추가 | UpsertRequest.php, DocumentService.php | ✅ |
| 2026-03-06 | Phase 2 보정 | ImportInspection: 입력폼 캡처 → 오프스크린 성적서 렌더링으로 변경 | ImportInspectionInputModal.tsx, capture-rendered-html.tsx | ✅ |
| 2026-03-06 | Phase 2 보정 | InspectionReportModal readOnly 자동 캡처 useEffect 제거 | InspectionReportModal.tsx | ✅ |
| 2026-03-06 | 원칙 확장 | Lazy Snapshot 패턴 추가 — 조회 시 rendered_html 없으면 자동 캡처/저장 | 아키텍처 원칙 | - |
---
## 9. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 9.1 성공 기준
| 기준 | 달성 | 비고 |
|------|------|------|
| React 저장 시 rendered_html이 documents 테이블에 저장 | ⏳ | |
| mng.sam.kr/documents/36 에서 rendered_html로 문서 출력 | ⏳ | |
| mng.sam.kr/documents/39 에서 rendered_html로 문서 출력 | ⏳ | |
| rendered_html 없는 기존 문서가 기존대로 렌더링 | ⏳ | |
| 인쇄 시에도 스냅샷 기반 출력 | ⏳ | |
| 양식별 전용 blade 파일 없이 동작 | ⏳ | |
---
## 10. 자기완결성 점검 결과
### 10.1 체크리스트 검증
| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 스냅샷 기반 문서 출력 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.1 참조 |
| 3 | 작업 범위가 구체적인가? | ✅ | 5 Phase, 15개 작업 |
| 4 | 의존성이 명시되어 있는가? | ✅ | 6. 의존성 참조 |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 5. 참고 파일 |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3. 작업 범위 |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 성공 기준 |
| 8 | 모호한 표현이 없는가? | ✅ | |
### 10.2 새 세션 시뮬레이션 테스트
| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | Phase 0 → 1 → 2 → 3 |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 참고 파일 |
| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 성공 기준 |
| Q5. 막혔을 때 참고 문서는? | ✅ | 5. 참고 파일, docs/ |
---
*이 문서는 /plan 스킬로 생성되었습니다.*

View File

@@ -0,0 +1,873 @@
# 제품검사 문서 시스템 구축 계획
> **작성일**: 2026-03-06
> **목적**: 제품검사 성적서(template ID 65 보완) + 제품검사 요청서(신규 template)를 mng 양식 기반 동적 렌더링 + rendered_html 스냅샷 구조로 구현
> **기준 문서**: `docs/dev/dev_plans/document-system-master.md`, `docs/dev/dev_plans/document-snapshot-architecture-plan.md`
> **상태**: 확정 (2026-03-06)
---
## 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Phase 3 전체 완료 (통합 테스트 + fallback 검증 + 호환성 확인) |
| **다음 작업** | 완료 — 추후 syncRequestDocument 기존 데이터 수동 실행 고려 |
| **진행률** | 12/12 (100%) |
| **마지막 업데이트** | 2026-03-06 |
---
## 1. 개요
### 1.1 배경
현재 제품검사 관련 문서가 **두 가지 방식**으로 혼재:
- **하드코딩 React 컴포넌트** (`InspectionReportDocument.tsx`, `InspectionRequestDocument.tsx`) — mockData 기반 변환, 문서 저장 없음
- **양식 기반 동적 렌더링** (`FqcDocumentContent.tsx`) — template ID 65 기반, 일부 구현됨 (4컬럼 단순 버전)
기존 중간검사/수입검사/작업일지는 이미 **mng 양식 등록 → React 동적 렌더링 → rendered_html 스냅샷** 패턴으로 동작 중. 제품검사도 동일 패턴으로 통일해야 함.
### 1.2 핵심 원칙
```
1. mng에서 양식(template) 등록/관리 → React에서 동적 렌더링
2. 저장 시 rendered_html 스냅샷 함께 저장
3. mng/히스토리에서는 스냅샷 출력 (렌더링 로직 0)
4. 기존 하드코딩 컴포넌트는 fallback으로 유지 → 점진적 전환
```
### 1.3 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| 즉시 가능 | mng 양식 항목 추가/수정, React 컴포넌트 수정 | 불필요 |
| 컨펌 필요 | API 저장 로직 변경, 새 API 엔드포인트, DB 마이그레이션 | **필수** |
| 금지 | documents 테이블 구조 변경, 기존 기능 삭제 | 별도 협의 |
### 1.4 준수 규칙
- `docs/dev/standards/quality-checklist.md` - 품질 체크리스트
- `docs/dev/standards/git-conventions.md` - Git 커밋 컨벤션
- `docs/features/documents/README.md` - 문서관리 시스템 스펙
---
## 2. 현재 시스템 분석
### 2.1 기존 패턴 (참조 모델: 중간검사)
```
[mng] DocumentTemplate (양식 등록)
├── approval_lines: 결재선 (작성/검토/승인)
├── basic_fields: 기본필드 (납품명, 제품명, 발주처 등)
├── sections: 섹션 (검사기준서 이미지, 검사 DATA)
│ └── items: 검사항목 (항목명, 기준, 측정유형)
└── columns: 컬럼 정의 (NO, 검사항목, 검사기준, 판정)
[React] 동적 렌더링
├── API로 template 조회 (GET /v1/document-templates/{id})
├── TemplateInspectionContent / FqcDocumentContent 등으로 렌더링
├── 편집 모드: 판정 토글, 측정값 입력
└── 저장 시 rendered_html 캡처 → API 전송
[API] 저장
├── document_data (EAV): 구조화 데이터 (검색/통계용)
└── documents.rendered_html: HTML 스냅샷 (출력용)
[MNG] 출력
├── rendered_html 있으면 → 그대로 출력 ({!! $document->rendered_html !!})
└── 없으면 → 기존 동적 렌더링 fallback
```
### 2.2 제품검사 성적서 현재 상태 (template ID 65)
**mng 양식 (ID 65)**: Phase 5.2에서 시더로 생성됨
- 시더 위치: `mng/database/seeders/ProductInspectionTemplateSeeder.php` (tenant_id=287 하드코딩)
- 결재 3인 (작성/검토/승인)
- 기본필드 7개 (납품명, 제품명, 발주처, LOT NO, 로트크기, 검사일자, 검사자)
- 섹션 2개 (검사기준서 이미지, 검사 DATA)
- 검사항목 11개 (설치 후 최종검사, 모두 visual/checkbox)
- **컬럼 4개** (NO, 검사항목, 검사기준, 판정) ← **보완 대상**
**React 구현 현황**:
- `FqcDocumentContent.tsx` — template 기반 동적 렌더링 (읽기/편집 모드), **현재 4컬럼**
- `InspectionReportDocument.tsx` — 하드코딩 fallback (**8컬럼 상세 버전**)
- `InspectionReportModal.tsx` — 듀얼 모드 (useFqcMode: fqcDocumentMap 존재 여부로 판단)
**문제점**:
1. 하드코딩 버전이 훨씬 상세함 (8컬럼, rowSpan 병합, 측정값, 검사방법/주기)
2. template ID 65는 4컬럼으로 단순 → 하드코딩 수준으로 보완 필요
3. rendered_html 스냅샷 저장이 FQC 문서에 아직 미적용
### 2.3 제품검사 요청서 현재 상태
**mng 양식**: 없음 (신규 생성 필요)
**React 구현**:
- `InspectionRequestDocument.tsx` — 하드코딩 컴포넌트 (완성도 높음)
- `InspectionRequestModal.tsx` — DocumentViewer + 하드코딩 컴포넌트
- 데이터 변환: `buildRequestDocumentData()` (mockData.ts:398-423)
**구조**:
- 결재라인 (작성/승인)
- 기본정보 (수주처, 업체명, 담당자, 수주번호, 연락처, 현장명, 납품일, 현장주소, 총개소, 접수일, 검사방문요청일)
- 입력사항 4개 섹션 (건축공사장, 자재유통업자, 공사시공자, 공사감리자)
- 검사 요청 시 필독 (고정 텍스트)
- 검사대상 사전 고지 정보 테이블 (No, 층수, 부호, 발주규격 가로/세로, 시공후규격 가로/세로, 변경사유)
---
## 3. 대상 범위
### Phase 1: 제품검사 성적서 (template ID 65) 보완
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | mng template ID 65 양식 보완 — section_items 교체 + template columns 2개로 재정의 | ✅ | items 11→22개, columns 4→2개(측정값+판정) |
| 1.2 | FqcDocumentContent.tsx 동적 렌더링 보완 + 프론트 타입 수정 | ✅ | 8컬럼 시각 레이아웃 + rowSpan 복합키 병합 + measurement_type=none 처리 |
| 1.3 | rendered_html 스냅샷 저장 적용 | ✅ | saveFqcDocument에 renderedHtml 파라미터 추가 + contentWrapperRef 준비 |
| 1.4 | InspectionReportModal에서 양식 모드 기본값 전환 | ✅ | FQC 모드 우선, template 로드 실패 시 legacy fallback |
### Phase 2: 제품검사 요청서 (신규 template)
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | mng에 제품검사 요청서 template 신규 등록 (시더) | ✅ | template ID 66, description 컬럼 추가 |
| 2.2 | React 동적 렌더링 컴포넌트 구현 | ✅ | FqcRequestDocumentContent, InspectionRequestModal FQC 모드 |
| 2.3 | 요청서 문서 생성 API 연동 | ✅ | syncRequestDocument(), store/update/attachOrders 연동 |
| 2.4 | rendered_html 스냅샷 저장 적용 (Lazy Snapshot) | ✅ | patchDocumentSnapshot, contentWrapperRef 캡처 |
### Phase 3: 통합 및 정리
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 3.1 | InspectionDetail.tsx 모달 연동 통합 테스트 | ✅ | 요청서(legacy fallback) + 성적서(FQC 8컬럼) 정상 |
| 3.2 | mng 문서 보기에서 스냅샷 출력 확인 | ✅ | show.blade.php rendered_html 우선 출력 패턴 코드 검증 |
| 3.3 | 하드코딩 컴포넌트 fallback 유지 확인 | ✅ | requestDocumentId 없으면 legacy fallback 정상 동작 |
| 3.4 | 기존 FQC 데이터 호환성 확인 | ✅ | 기존 EAV 데이터(basic_fields) 정상 표시 |
---
## 4. 작업 절차
### 4.1 단계별 절차
```
Phase 1: 제품검사 성적서 보완
├── Step 1.1: mng template ID 65 시더 보완
│ ├── template columns 재정의: 4→2개 (측정값+판정만, 나머지는 section_item 필드)
│ ├── section_items 교체: 11개→22개 (섹션 5.1.2의 상세 항목 데이터)
│ ├── cleanupExisting() → updateOrCreate() 패턴으로 ID 안정성 확보 (섹션 5.1.9)
│ ├── mng에서 시더 실행 후 미리보기 확인
│ └── 파일: mng/database/seeders/ProductInspectionTemplateSeeder.php
├── Step 1.2: FqcDocumentContent.tsx 8컬럼 렌더링 보완
│ ├── [선행] fqcActions.ts 타입 수정: TemplateItemApi, FqcTemplateItem에 category/method/frequency/measurement_type 추가 (섹션 5.1.7)
│ ├── [선행] transformTemplate()에서 새 필드 매핑 추가
│ ├── 8컬럼 시각 레이아웃: 1~6 section_item 읽기전용 + 7~8 template column 편집 (섹션 5.1.1)
│ ├── rowSpan 복합키 병합: category 단독 + method+frequency 복합키 (섹션 5.1.3)
│ ├── 측정값 입력 분기: numeric→input, checkbox→양호/불량, none→비활성 (섹션 5.1.8)
│ ├── 종합판정: measurement_type='none' 제외하고 계산 (섹션 5.1.8)
│ └── 파일: fqcActions.ts + FqcDocumentContent.tsx
├── Step 1.3: rendered_html 스냅샷 적용
│ ├── FqcDocumentContent의 wrapper div에 ref 연결
│ ├── saveFqcDocument 호출 시 contentWrapperRef.current.innerHTML 캡처
│ ├── fqcActions.ts의 saveFqcDocument()에 rendered_html 파라미터 추가
│ ├── API: POST /v1/documents/upsert → DocumentService에서 rendered_html 저장 (이미 지원됨)
│ └── 파일: fqcActions.ts, InspectionReportModal.tsx
└── Step 1.4: 양식 모드 기본값 전환
├── InspectionReportModal.tsx에서 FQC 모드 우선 표시
├── fqcDocumentMap 없어도 template 로드 시도
└── 레거시 모드 fallback 유지
Phase 2: 제품검사 요청서 신규
├── Step 2.1: mng template 등록 (시더 방식 확정)
│ ├── 양식 구조 설계 (섹션 5.2의 상세 구조 참조)
│ ├── 시더 작성 (ProductInspectionRequestTemplateSeeder)
│ ├── mng 시더 실행 후 미리보기 확인
│ └── 파일: mng/database/seeders/ProductInspectionRequestTemplateSeeder.php (신규)
├── Step 2.2: React 동적 렌더링
│ ├── FqcRequestDocumentContent.tsx 신규 생성 (또는 기존 FqcDocumentContent 확장)
│ ├── quality_documents 데이터 → 문서 데이터 매핑
│ ├── 사전 고지 정보 테이블 (quality_document_locations → 테이블 행)
│ ├── InspectionRequestModal에 양식 기반 모드 추가
│ └── 파일: react/.../documents/ 디렉토리
├── Step 2.3: API 연동 (quality_document 생성 시 자동 생성 확정)
│ ├── QualityDocumentService::store()에서 요청서 document 자동 생성
│ ├── 기본필드 자동 매핑 (quality_document → document basic_fields)
│ ├── 개소 데이터 자동 매핑 (quality_document_locations → 사전고지 테이블)
│ └── 파일: api/app/Services/QualityDocumentService.php
└── Step 2.4: rendered_html 스냅샷 (Lazy Snapshot 패턴)
├── 요청서는 readonly 문서 → "입력 시 저장" 캡처 불가
├── Lazy Snapshot 적용: 요청서 최초 조회 시 rendered_html 없으면 자동 캡처
├── captureRenderedHtml 유틸리티 활용 (react/src/lib/utils/capture-rendered-html.tsx)
├── 또는 contentWrapperRef.innerHTML로 렌더링 완료 후 캡처
├── 백그라운드 API 호출로 rendered_html 저장 (PATCH /v1/documents/{id} 또는 upsert)
└── 참조: document-snapshot-architecture-plan.md 캡처 원칙 B
Phase 3: 통합 및 정리
├── InspectionDetail.tsx에서 모달 연동 통합 테스트
├── mng show.blade.php에서 스냅샷 출력 확인
├── 하드코딩 fallback 정상 동작 확인
└── 기존 FQC 데이터 호환성 확인
```
---
## 5. 상세 작업 내용
### 5.1 제품검사 성적서 양식 보완 상세 (Phase 1.1)
#### 5.1.1 컬럼 구조 — Template columns vs Display columns (핵심 구분)
> **핵심**: Template columns는 EAV 편집 가능 필드만 정의. 8컬럼 시각 레이아웃은 React에서 section_item 필드 + template columns를 조합하여 렌더링.
**Template columns = EAV 데이터 저장용 (편집 가능 필드만)**
현재 4컬럼 중 실제 EAV로 저장하는 것은 `판정`(select) 하나뿐. 나머지 3개(NO, 검사항목, 검사기준)는 section_item 필드의 읽기 전용 표시.
**목표 template columns (2~3개)** — 시더에 반영:
| 컬럼 | label | column_type | width | 비고 |
|------|-------|-------------|-------|------|
| 1 | 측정값 | measurement | 70px | measurement_type별 입력 (numeric/checkbox) |
| 2 | 판정 | select | 50px | 적합/부적합 |
> `measurement_type='none'`인 항목(작동테스트)은 측정값/판정 컬럼 모두 비활성.
**8컬럼 시각 레이아웃 (React 렌더링)**:
| 시각 컬럼 | 데이터 소스 | 편집 가능 |
|-----------|-----------|:---------:|
| 1. No. | section_item 순번 (category별 그룹 번호) | ❌ |
| 2. 검사항목 | section_item.`category` | ❌ |
| 3. 세부항목 | section_item.`item` | ❌ |
| 4. 검사기준 | section_item.`standard` | ❌ |
| 5. 검사방법 | section_item.`method` | ❌ |
| 6. 검사주기 | section_item.`frequency` | ❌ |
| 7. 측정값 | template column `measurement` → document_data EAV | ✅ |
| 8. 판정 | template column `select` → document_data EAV | ✅ |
> 1~6번은 section_item의 읽기 전용 필드를 React에서 직접 렌더링. 7~8번만 template columns로 정의하여 document_data에 EAV 저장.
#### 5.1.2 검사항목 데이터 (시더에 반영할 section_items)
**방안 C 확정** — 하드코딩 `mockReportInspectionItems` (mockData.ts:426-458)을 template section_items로 이관.
`DocumentTemplateSectionItem` 필드 매핑:
| mock 필드 | DB 필드 | 설명 |
|-----------|---------|------|
| category | `category` | 검사 그룹명 (겉모양, 모터, 치수 등) |
| subCategory | `item` | 세부 항목명 (가공상태, 길이 등) |
| criteria | `standard` | 검사 기준 |
| method | `method` | 검사 방법 |
| frequency | `frequency` | 검사 주기 |
| measurement_type | `measurement_type` | checkbox/numeric/none |
**시더 section_items 데이터** (검사 DATA 섹션):
```php
// 섹션: 검사 DATA
$items = [
// === 1. 겉모양 (5개 세부항목, method/freq 동일: 육안검사/전수검사) ===
['category' => '겉모양', 'item' => '가공상태', 'standard' => '흠, 녹 등 확인', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
['category' => '겉모양', 'item' => '재봉상태', 'standard' => '이중 재봉 상태', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
['category' => '겉모양', 'item' => '조립상태', 'standard' => '개폐 작동', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
['category' => '겉모양', 'item' => '연기차단재', 'standard' => '접착력 확인', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
['category' => '겉모양', 'item' => '하단마감재', 'standard' => '접착력 확인', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
// === 2. 모터 ===
['category' => '모터', 'item' => '-', 'standard' => '인정제품과 동일사양', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
// === 3. 재질 ===
['category' => '재질', 'item' => '-', 'standard' => 'WY-SC780 인쇄상태 확인', 'method' => '육안검사', 'frequency' => '전수검사', 'measurement_type' => 'checkbox'],
// === 4. 치수(오픈사이즈) (4개 세부항목, method: 체크검사) ===
['category' => '치수(오픈사이즈)', 'item' => '길이(W)', 'standard' => '수주치수 +-30mm', 'method' => '체크검사', 'frequency' => '전수검사', 'measurement_type' => 'numeric'],
['category' => '치수(오픈사이즈)', 'item' => '높이(H)', 'standard' => '수주치수 +-20mm', 'method' => '체크검사', 'frequency' => '전수검사', 'measurement_type' => 'numeric'],
['category' => '치수(오픈사이즈)', 'item' => '가이드레일 간격', 'standard' => '수주치수 +-10mm', 'method' => '체크검사', 'frequency' => '전수검사', 'measurement_type' => 'numeric'],
['category' => '치수(오픈사이즈)', 'item' => '하단막대 간격', 'standard' => '수주치수 +-10mm', 'method' => '체크검사', 'frequency' => '전수검사', 'measurement_type' => 'numeric'],
// === 5. 작동테스트 ===
['category' => '작동테스트', 'item' => '개폐성능', 'standard' => '작동 유무 확인', 'method' => '', 'frequency' => '', 'measurement_type' => 'none'],
// === 6. 내화시험 (3개 세부항목, method: 공인시험기관, freq: 1회/5년) ===
['category' => '내화시험', 'item' => '비차열 1시간', 'standard' => '균열게이지 불관통', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '내화시험', 'item' => '차열성', 'standard' => '이면온도 상승제한', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '내화시험', 'item' => '비손상성', 'standard' => '화염 비관통', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
// === 7. 차연시험 ===
['category' => '차연시험', 'item' => '공기누설량', 'standard' => '25Pa 기준 0.9m3/min.m2 이하', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
// === 8. 개폐시험 (5개 세부항목) ===
['category' => '개폐시험', 'item' => '개폐 작동', 'standard' => '10회 이상 개폐작동 이상없음', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '개폐시험', 'item' => '폐쇄 시간', 'standard' => '5초 이내 완전 폐쇄', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '개폐시험', 'item' => '평균속도(상한)', 'standard' => '0.3m/sec 이하', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '개폐시험', 'item' => '평균속도(하한)', 'standard' => '0.07m/sec 이상', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
['category' => '개폐시험', 'item' => '과부하', 'standard' => '과부하 작동 이상없음', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
// === 9. 내충격시험 ===
['category' => '내충격시험', 'item' => '-', 'standard' => '낙하높이 기준 이상없음', 'method' => '공인시험기관', 'frequency' => '1회/5년', 'measurement_type' => 'checkbox'],
];
```
#### 5.1.3 rowSpan 자동 병합 로직 (React 구현)
하드코딩 `InspectionReportDocument.tsx``buildCoverageMap()` (라인 42-57) 패턴을 **template 데이터 기반으로 자동화**.
> **주의**: `method`와 `frequency`는 **복합 키**(method+frequency)로 병합해야 함.
> 단일 필드 비교 시 frequency='전수검사'가 7+4=11행 연속으로 잘못 병합됨.
> 실제로는 method가 다르면 (육안검사 7행 vs 체크검사 4행) 별도 그룹이어야 함.
```typescript
// category는 단일 필드 병합 (같은 category 연속이면 병합)
function buildFieldRowSpan(items: SectionItem[], field: 'category') {
const spans: Map<number, number> = new Map();
const covered: Set<number> = new Set();
let i = 0;
while (i < items.length) {
const value = items[i][field];
if (!value) { i++; continue; }
let span = 1;
while (i + span < items.length && items[i + span][field] === value) {
covered.add(i + span);
span++;
}
if (span > 1) spans.set(i, span);
i += span;
}
return { spans, covered };
}
// method+frequency는 복합 키 병합
function buildCompositeRowSpan(items: SectionItem[]) {
const spans: Map<number, number> = new Map();
const covered: Set<number> = new Set();
let i = 0;
while (i < items.length) {
const key = `${items[i].method || ''}|${items[i].frequency || ''}`;
if (!items[i].method && !items[i].frequency) { i++; continue; }
let span = 1;
while (i + span < items.length) {
const nextKey = `${items[i + span].method || ''}|${items[i + span].frequency || ''}`;
if (nextKey !== key) break;
covered.add(i + span);
span++;
}
if (span > 1) spans.set(i, span);
i += span;
}
return { spans, covered };
}
// 사용: 렌더링 시
const categoryCoverage = buildFieldRowSpan(sectionItems, 'category');
const methodFreqCoverage = buildCompositeRowSpan(sectionItems);
// method 컬럼과 frequency 컬럼 모두 methodFreqCoverage로 렌더링
// 셀 렌더링 예시
{!categoryCoverage.covered.has(idx) && (
<td rowSpan={categoryCoverage.spans.get(idx) || 1}>{item.category}</td>
)}
{!methodFreqCoverage.covered.has(idx) && (
<td rowSpan={methodFreqCoverage.spans.get(idx) || 1}>{item.method}</td>
)}
{!methodFreqCoverage.covered.has(idx) && (
<td rowSpan={methodFreqCoverage.spans.get(idx) || 1}>{item.frequency}</td>
)}
```
**병합 결과 예시** (시더 데이터 기준):
- category `겉모양` → 5행 병합
- category `치수(오픈사이즈)` → 4행 병합
- method+frequency `육안검사|전수검사` → 7행 (겉모양5 + 모터1 + 재질1)
- method+frequency `체크검사|전수검사` → 4행 (치수 4개)
- method+frequency `공인시험기관|1회/5년` → 9행 (내화3 + 차연1 + 개폐5)
- `작동테스트`는 method/frequency 빈값 → 병합 대상 아님
#### 5.1.4 rendered_html 캡처 패턴
> **캡처 원칙** (document-snapshot-architecture-plan.md 참조):
> - **Active Capture**: 입력 화면에서 저장할 때 캡처. readOnly에서는 캡처하지 않음.
> - **Lazy Snapshot**: 조회 시 rendered_html 없으면 자동 캡처/저장 (점진적 전환).
> FQC 성적서는 Active Capture (편집 모드 저장 시), 요청서는 Lazy Snapshot (readonly 자동 캡처) 적용.
**패턴 1: innerHTML 직접 캡처** (참조: 중간검사 `InspectionReportModal.tsx` 라인 341-366)
```typescript
// 1. ref 정의
const contentWrapperRef = useRef<HTMLDivElement>(null);
// 2. 저장 시 캡처
const handleSave = async () => {
const renderedHtml = contentWrapperRef.current?.innerHTML || undefined;
const result = await saveInspectionDocument({
...data,
rendered_html: renderedHtml,
});
};
// 3. JSX에서 ref 연결
<div ref={contentWrapperRef}>
<TemplateInspectionContent ... />
</div>
```
**패턴 2: 오프스크린 렌더링** (참조: 수입검사 `ImportInspectionInputModal.tsx`)
```typescript
import { captureRenderedHtml } from '@/lib/utils/capture-rendered-html';
// 문서 컴포넌트를 오프스크린으로 렌더링 → HTML 캡처
const html = await captureRenderedHtml(DocumentComponent, documentProps);
```
- 유틸리티 위치: `react/src/lib/utils/capture-rendered-html.tsx` (flushSync + createRoot)
- 문서가 화면에 렌더링되지 않는 상황에서 캡처할 때 사용 (Lazy Snapshot에 활용 가능)
**FQC 성적서 적용 (Phase 1.3)**: 패턴 1 사용. `InspectionReportModal.tsx`의 FQC 모드 저장 로직에 동일 패턴 추가.
현재 `fqcActions.ts``saveFqcDocument()``POST /v1/documents/upsert` 사용 → body에 `rendered_html` 추가.
#### 5.1.5 FqcDocumentContent.tsx 현재 구조 (보완 대상)
**현재 상태** (라인 311-375):
- 4컬럼 테이블: NO(30px) | 검사항목 | 검사기준 | 판정(28px)
- `InspectionRow` 컴포넌트 (라인 399-454): 4개 셀 렌더링
- 판정: readonly면 텍스트 표시, 편집모드면 양호/불량 토글 버튼
- 종합판정: 라인 117-128 자동 계산 (하나라도 부적합이면 불합격)
**보완 방향**:
1. template columns 2개(측정값+판정)로 재정의 + 8컬럼 시각 레이아웃 (섹션 5.1.1)
2. `InspectionRow` → 8셀 렌더링 + rowSpan 복합키 병합 (섹션 5.1.3)
3. measurement_type에 따른 셀 분기 (섹션 5.1.8):
- `checkbox`: 양호/불량 토글 (기존)
- `numeric`: 숫자 입력 필드 (신규)
- `none`: 측정값/판정 모두 비활성 + 종합판정에서 제외 (작동테스트)
4. method/frequency: section_item 필드에서 직접 렌더링 (readonly)
5. [선행] fqcActions.ts 타입에 category/method/frequency/measurement_type 추가 (섹션 5.1.7)
#### 5.1.6 saveFqcDocument API 현재 구조
**`fqcActions.ts`** (라인 429-461):
```typescript
// 현재: rendered_html 미포함
const body: Record<string, unknown> = {
template_id: templateId,
data: data,
};
if (documentId) body.document_id = documentId;
if (itemId) body.item_id = itemId;
if (title) body.title = title;
// → rendered_html 추가 필요
const res = await fetchAPI('/api/v1/documents/upsert', { method: 'POST', body });
```
**data 구조** (EAV):
```typescript
data: Array<{
section_id?: number | null;
column_id?: number | null;
row_index: number;
field_key: string; // 예: 'judgment', 'measured_value'
field_value: string | null;
}>
```
#### 5.1.7 Frontend 타입 갭 수정 (Phase 1.2 선행 작업)
> **문제**: API는 section_items에 category, method, frequency, measurement_type 필드를 반환하지만,
> React의 `TemplateItemApi`와 `FqcTemplateItem` 타입에 이 필드들이 누락되어 있음.
**현재 타입** (`fqcActions.ts:20-29`):
```typescript
interface TemplateItemApi {
id: number;
section_id: number;
item: string;
standard: string;
sort_order: number;
// ❌ category, method, frequency, measurement_type 누락
}
```
**수정 필요**:
```typescript
interface TemplateItemApi {
id: number;
section_id: number;
item: string;
standard: string;
sort_order: number;
category: string | null; // 추가
method: string | null; // 추가
frequency: string | null; // 추가
measurement_type: string | null; // 추가 (checkbox/numeric/none)
}
```
**`FqcTemplateItem`** (`fqcActions.ts:152-161`)도 동일하게 필드 추가.
**`transformTemplate()`** (`fqcActions.ts:254-300`)에서 매핑 추가:
```typescript
// items 변환 시 category, method, frequency, measurement_type 포함
items: section.items.map((item: TemplateItemApi) => ({
...existingMapping,
category: item.category || '',
method: item.method || '',
frequency: item.frequency || '',
measurement_type: item.measurement_type || 'checkbox',
}))
```
#### 5.1.8 measurement_type='none' 처리 (작동테스트)
> **문제**: 작동테스트 항목은 `measurement_type='none'`인데, 현재 코드는 checkbox/numeric만 처리.
> none 항목에 판정 UI를 표시하면 종합판정에 영향을 주게 됨.
**처리 방안**:
1. **측정값 셀**: 빈 셀 또는 '-' 표시 (입력 불가)
2. **판정 셀**: 비활성화 (hideJudgment)
3. **종합판정 계산에서 제외**: `measurement_type='none'` 항목은 적합/부적합 집계에서 제외
**React 구현** (`FqcDocumentContent.tsx``InspectionRow`):
```typescript
// measurement_type에 따른 렌더링 분기
if (item.measurement_type === 'none') {
// 측정값: 빈 셀
// 판정: 빈 셀 (토글 버튼 미표시)
return <><td>-</td><td>-</td></>;
}
// 종합판정 계산 (라인 117-128 수정)
const judgmentItems = records.filter(r =>
r.field_key === 'judgment' &&
// none 타입 항목 제외
sectionItems.find(item => item.sort_order === r.row_index)?.measurement_type !== 'none'
);
```
#### 5.1.9 Template ID 안정성 대책
> **문제**: 시더가 `cleanupExisting()`으로 삭제 후 재생성하면 auto_increment로 ID가 변경됨.
> `FQC_TEMPLATE_ID = 65` 하드코딩이 깨질 수 있음.
**대책 (2단계)**:
**1단계 (즉시)** — 시더 개선:
- `cleanupExisting()` 대신 `updateOrCreate()` 패턴 사용
- template ID를 명시적으로 지정하여 재실행 시에도 동일 ID 유지
```php
$template = DocumentTemplate::updateOrCreate(
['id' => 65, 'tenant_id' => $this->tenantId],
['name' => '제품검사 성적서', ...]
);
// 하위 데이터도 sync 방식으로 처리
```
**2단계 (권장, 후순위)** — category 기반 조회:
- `FQC_TEMPLATE_ID = 65` 대신 API에서 category='품질/제품검사' + name='제품검사 성적서'로 조회
- 환경에 따라 ID가 달라도 동작하도록 유연성 확보
- **이번 작업에서는 1단계만 적용**, 2단계는 추후 리팩토링 시 적용
---
### 5.2 제품검사 요청서 양식 설계 (Phase 2.1)
#### 5.2.1 양식 구조
```
DocumentTemplate (제품검사 요청서)
├── name: '제품검사 요청서'
├── category: '품질/제품검사'
├── title: '제 품 검 사 요 청 서'
├── approval_lines: [
│ { name: '작성', dept: '품질', role: '담당자', sort_order: 1 },
│ { name: '승인', dept: '경영', role: '대표', sort_order: 2 },
│ ]
├── basic_fields: [
│ { label: '수주처', field_key: 'client', field_type: 'text' },
│ { label: '업체명', field_key: 'company_name', field_type: 'text' },
│ { label: '담당자', field_key: 'manager', field_type: 'text' },
│ { label: '수주번호', field_key: 'order_number', field_type: 'text' },
│ { label: '담당자 연락처', field_key: 'manager_contact', field_type: 'text' },
│ { label: '현장명', field_key: 'site_name', field_type: 'text' },
│ { label: '납품일', field_key: 'delivery_date', field_type: 'date' },
│ { label: '현장 주소', field_key: 'site_address', field_type: 'text' },
│ { label: '총 개소', field_key: 'total_locations', field_type: 'number' },
│ { label: '접수일', field_key: 'receipt_date', field_type: 'date' },
│ { label: '검사방문요청일', field_key: 'inspection_request_date', field_type: 'date' },
│ ]
├── sections: [
│ { title: '건축공사장 정보', items: [
│ { item: '현장명', measurement_type: 'text_input' },
│ { item: '대지위치', measurement_type: 'text_input' },
│ { item: '지번', measurement_type: 'text_input' },
│ ]},
│ { title: '자재유통업자 정보', items: [
│ { item: '회사명', measurement_type: 'text_input' },
│ { item: '주소', measurement_type: 'text_input' },
│ { item: '대표자', measurement_type: 'text_input' },
│ { item: '전화번호', measurement_type: 'text_input' },
│ ]},
│ { title: '공사시공자 정보', items: [...] }, // 회사명, 주소, 성명, 전화
│ { title: '공사감리자 정보', items: [...] }, // 사무소명, 주소, 성명, 전화
│ { title: '검사대상 사전 고지 정보', items: [] }, // 동적 테이블 (locations)
│ ]
└── columns: [ // 사전 고지 정보 테이블용
{ label: 'No.', column_type: 'text', width: '40px' },
{ label: '층수', column_type: 'text', width: '60px' },
{ label: '부호', column_type: 'text', width: '60px' },
{ label: '발주 가로', column_type: 'text', width: '70px', group_name: '오픈사이즈(발주규격)' },
{ label: '발주 세로', column_type: 'text', width: '70px', group_name: '오픈사이즈(발주규격)' },
{ label: '시공 가로', column_type: 'text', width: '70px', group_name: '오픈사이즈(시공후규격)' },
{ label: '시공 세로', column_type: 'text', width: '70px', group_name: '오픈사이즈(시공후규격)' },
{ label: '변경사유', column_type: 'text' },
]
```
#### 5.2.2 데이터 매핑 (quality_document → 요청서 필드)
| 요청서 필드 | 데이터 소스 | 경로 |
|------------|-----------|------|
| 수주처 | quality_document → order → client | `qualityDocument.orders[0].order.client.name` |
| 업체명 | tenant | `tenant.name` |
| 담당자 | quality_document.created_by | user name |
| 수주번호 | quality_document → order | `qualityDocument.orders[0].order.order_number` |
| 현장명 | quality_document → order | `qualityDocument.orders[0].order.site_name` |
| 납품일 | quality_document → order | `qualityDocument.orders[0].order.delivery_date` |
| 총 개소 | quality_document_locations count | `qualityDocument.orders.flatMap(o => o.locations).length` |
| 사전고지 테이블 | quality_document_locations | location별 floor/symbol/width/height/post_width/post_height/change_reason |
#### 5.2.3 특이사항
- 요청서는 검사 입력이 아닌 **정보 표시 문서** (readonly)
- 개소(location) 데이터는 `quality_document_locations` 테이블에서 가져옴
- "검사 요청 시 필독" 고정 텍스트는 section description으로 처리
- quality_document 생성 시 요청서 document 자동 생성 (bulkCreate 패턴 참조)
---
## 6. 확정 사항
| # | 항목 | 결정 내용 | 영향 범위 | 상태 |
|---|------|----------|----------|------|
| 1 | 성적서 양식 보완 방안 | **방안 C 확정** — 하드코딩 데이터를 template items로 완전 이관. SectionItem에 method/frequency/measurement_type 이미 존재 | mng, react | ✅ 확정 |
| 2 | 요청서 template 생성 방법 | **시더 확정** — ProductInspectionRequestTemplateSeeder 작성 (mng/database/seeders/). 재현성 + 버전 관리 | mng | ✅ 확정 |
| 3 | 요청서 문서 생성 시점 | **자동 생성 확정** — quality_document 생성 시 자동. 기존 성적서 bulk create 패턴과 동일 | api | ✅ 확정 |
| 4 | 요청서 rendered_html 캡처 방식 | **Lazy Snapshot 확정** — 요청서는 readonly 문서이므로 최초 조회 시 rendered_html 없으면 자동 캡처/저장. `captureRenderedHtml` 유틸리티 또는 `contentWrapperRef.innerHTML` 활용 | react | ✅ 확정 |
---
## 6.1 에이전트 조사 결과 (2026-03-06)
### Template ID 65 현황
- Seeder: MNG에만 존재 (`mng/database/seeders/ProductInspectionTemplateSeeder.php`, tenant_id=287 하드코딩)
- 정규 관리 안됨 (migration/seeder로 재현 불가)
- React: `FQC_TEMPLATE_ID = 65` 하드코딩 (`fqcActions.ts:348`)
### rendered_html 스냅샷 아키텍처 (~95% 완성)
| 계층 | 상태 | 비고 |
|------|------|------|
| DB (documents.rendered_html) | ✅ | LONGTEXT, migration 완료 |
| API (DocumentService create/update) | ✅ | rendered_html 조건부 저장 |
| React (InspectionReportModal) | ✅ | contentWrapperRef.innerHTML 캡처 |
| MNG (show/print.blade.php) | ✅ | rendered_html 우선 출력 + fallback |
| FormRequest 검증 | ✅ | StoreRequest/UpdateRequest/UpsertRequest 모두 nullable string 검증 완료 |
### mng template 구조 핵심
- `DocumentTemplateSectionItem` 필드: category, item, standard, tolerance, standard_criteria, method, measurement_type, frequency_n, frequency_c, frequency, regulation, field_values(JSON)
- **8컬럼 구조를 추가 스키마 변경 없이 지원 가능** — 방안 C 근거
- 저장 방식: Legacy EAV (approval_lines, basic_fields, sections+items, columns, section_fields, links)
- 저장 API: `POST /api/admin/document-templates``saveRelations()` 트랜잭션
### QualityDocumentLocation 모델
- 테이블: `quality_document_locations`
- 필드: `quality_document_id`, `quality_document_order_id`, `order_item_id`, `post_width`, `post_height`, `change_reason`, `inspection_data`(JSON), `document_id`, `inspection_status`
- 관계: qualityDocument, qualityDocumentOrder, orderItem, document
- 상태: pending / completed
---
## 7. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-03-06 | - | 문서 초안 작성 | - | - |
| 2026-03-06 | 컨펌 3건 | 방안 C 확정, 시더 확정, 자동 생성 확정 | - | ✅ 사용자 승인 |
| 2026-03-06 | 에이전트 조사 | template ID 65 현황, rendered_html 현황, mng 구조 분석 반영 | - | - |
| 2026-03-06 | 자기완결성 보완 | 시더 데이터, rowSpan 로직, rendered_html 캡처 패턴, API 구조, 타입 정의 추가 | - | - |
| 2026-03-06 | 방법론 수정 6건 | ①컬럼 아키텍처 재설계(template 2개+시각 8컬럼) ②프론트 타입 갭 보완 ③rowSpan 복합키 알고리즘 ④Template ID 안정성 대책 ⑤measurement_type=none 처리 ⑥시더 위치 명확화(mng) | - | - |
| 2026-03-06 | 스냅샷 아키텍처 정합성 검토 반영 | ①FormRequest 검증 ✅ 확인 ②Phase 2.4 Lazy Snapshot 전략 확정 ③참고 파일 추가(capture-rendered-html.tsx, UpsertRequest) ④캡처 원칙(Active/Lazy) 명시 ⑤오프스크린 렌더링 참조 추가 | 5.1.4, 6.1, 9 | - |
---
## 8. 참고 문서
- **문서관리 시스템 마스터**: `docs/dev/dev_plans/document-system-master.md`
- **스냅샷 아키텍처**: `docs/dev/dev_plans/document-snapshot-architecture-plan.md`
- **FQC Phase 5.2 아카이브**: `docs/dev/dev_plans/archive/document-system-product-inspection.md`
- **문서관리 기능 스펙**: `docs/features/documents/README.md`
- **DB 스키마**: `docs/system/database/documents.md`
- **품질 체크리스트**: `docs/dev/standards/quality-checklist.md`
---
## 9. 참고 파일 경로
### React (수정 대상)
| 파일 | 역할 | 핵심 라인 |
|------|------|----------|
| `react/src/components/quality/InspectionManagement/documents/FqcDocumentContent.tsx` | FQC 양식 기반 동적 렌더링 (보완 대상) | 311-375: 4컬럼 테이블, 399-454: InspectionRow |
| `react/src/components/quality/InspectionManagement/documents/InspectionReportDocument.tsx` | 하드코딩 성적서 (참조/fallback) | 42-57: buildCoverageMap(), 198-376: 8컬럼 테이블 |
| `react/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx` | 성적서 모달 (듀얼 모드) | 61: useFqcMode, 106-116: FQC 문서 로드, 233-262: fallback |
| `react/src/components/quality/InspectionManagement/documents/InspectionRequestDocument.tsx` | 하드코딩 요청서 (참조/fallback) | 47-62: 결재, 66-103: 기본정보, 211-255: 사전고지 테이블 |
| `react/src/components/quality/InspectionManagement/documents/InspectionRequestModal.tsx` | 요청서 모달 | 26-37: DocumentViewer 래퍼 |
| `react/src/components/quality/InspectionManagement/fqcActions.ts` | FQC 서버 액션 | 348: FQC_TEMPLATE_ID=65, 429-461: saveFqcDocument() |
| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 상세 페이지 (모달 연동) | 1230-1275: 모달 통합 |
| `react/src/components/quality/InspectionManagement/mockData.ts` | 데이터 변환 함수 | 398-423: buildRequestDocumentData(), 426-458: mockReportInspectionItems |
| `react/src/components/quality/InspectionManagement/types.ts` | 타입 정의 | 224-247: InspectionRequestDocument, 250-266: ReportInspectionItem, 269-285: InspectionReportDocument |
| `react/src/components/quality/InspectionManagement/actions.ts` | 서버 액션 (CRUD) | |
### React (참조 - 기존 동적 렌더링 패턴)
| 파일 | 역할 | 핵심 라인 |
|------|------|----------|
| `react/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx` | 중간검사 동적 렌더링 (참조 모델) | 917-999: 컬럼타입별 렌더링, 1105-1183: renderComplexCells() |
| `react/src/components/production/WorkOrders/documents/InspectionReportModal.tsx` | 중간검사 모달 (rendered_html 캡처 패턴) | 167: contentWrapperRef, 341-366: handleSave+innerHTML |
| `react/src/lib/utils/capture-rendered-html.tsx` | 오프스크린 렌더링 유틸리티 (flushSync+createRoot) | Phase 2.4 Lazy Snapshot에서 활용 가능 |
| `react/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx` | 수입검사 오프스크린 캡처 참조 | captureRenderedHtml 사용 예시 |
### API (수정 대상)
| 파일 | 역할 | 핵심 라인 |
|------|------|----------|
| `api/app/Services/DocumentService.php` | 문서 저장/조회 | 125: create(rendered_html), 180: update(rendered_html) |
| `api/app/Services/QualityDocumentService.php` | 품질관리 문서 서비스 | 176-209: store(), 749-799: requestDocument(), 804-868: resultDocument() |
| `api/app/Models/Documents/Document.php` | 문서 모델 | 76: $fillable에 rendered_html |
| `api/app/Models/Qualitys/QualityDocumentLocation.php` | 개소 모델 | fillable: post_width, post_height, change_reason, inspection_data |
| `api/app/Http/Requests/Document/UpsertRequest.php` | documents/upsert 검증 | rendered_html nullable string 검증 완료 |
### MNG (확인/수정 대상)
| 파일 | 역할 |
|------|------|
| `mng/app/Http/Controllers/DocumentTemplateController.php` | 양식 CRUD |
| `mng/resources/views/document-templates/edit.blade.php` | 양식 편집 |
| `mng/resources/views/documents/show.blade.php` | 문서 보기 (rendered_html 우선 출력, 29-32행) |
| `mng/resources/views/documents/print.blade.php` | 문서 인쇄 (28-32행) |
| `mng/database/seeders/ProductInspectionTemplateSeeder.php` | template ID 65 시더 (49-182: 항목 데이터) |
---
## 10. 미커밋 변경사항 (이전 세션, 2026-03-06)
**api/** — order_ids 영속성, location count 수정, location data 저장:
- `app/Http/Requests/Quality/QualityDocumentStoreRequest.php` — 수정
- `app/Http/Requests/Quality/QualityDocumentUpdateRequest.php` — 수정
- `app/Services/QualityDocumentService.php` — 수정
- `app/Models/Qualitys/QualityDocumentLocation.php` — 수정
- `database/migrations/2026_03_06_094425_add_inspection_data_to_quality_document_locations.php` — 신규
**react/**`src/components/quality/InspectionManagement/actions.ts` — 수정
> 이 작업 시작 전에 먼저 커밋 필요
---
## 11. 세션 및 메모리 관리 정책 (Serena Optimized)
### 11.1 세션 시작 시 (Load Strategy)
```javascript
read_memory("fqc-doc-state") // 1. 상태 파악
read_memory("fqc-doc-snapshot") // 2. 사고 흐름 복구
read_memory("fqc-doc-active-symbols") // 3. 작업 대상 파악
```
### 11.2 작업 중 관리 (Context Defense)
| 컨텍스트 잔량 | Action | 내용 |
|--------------|--------|------|
| **30% 이하** | Snapshot | `write_memory("fqc-doc-snapshot", "코드변경+논의요약")` |
| **20% 이하** | Context Purge | `write_memory("fqc-doc-active-symbols", "주요 수정 파일/함수")` |
| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 |
### 11.3 Serena 메모리 구조
- `fqc-doc-state`: { phase, progress, next_step, last_decision } (JSON)
- `fqc-doc-snapshot`: 현재까지의 논의 및 코드 변경점 요약 (Text)
- `fqc-doc-rules`: 해당 작업에서 결정된 불변의 규칙들 (Text)
- `fqc-doc-active-symbols`: 현재 수정 중인 파일/심볼 리스트 (List)
---
## 12. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 12.1 성공 기준
| # | 기준 | 달성 | 비고 |
|---|------|:----:|------|
| 1 | mng에서 성적서 양식 편집/미리보기 정상 | ✅ | 시더 실행 확인 |
| 2 | mng에서 요청서 양식 편집/미리보기 정상 | ✅ | 시더 실행 확인 |
| 3 | React에서 성적서 양식 기반 동적 렌더링 (8컬럼+rowSpan) | ✅ | 브라우저 테스트 통과 |
| 4 | React에서 요청서 양식 기반 동적 렌더링 | ✅ | requestDocumentId 없으면 legacy fallback |
| 5 | 저장 시 rendered_html 스냅샷 저장됨 | ✅ | Active Capture 코드 구현 완료 (검사 저장 시 동작) |
| 6 | mng 문서 보기에서 스냅샷 정상 출력 | ✅ | show.blade.php 코드 검증 |
| 7 | 기존 하드코딩 fallback 정상 동작 | ✅ | 요청서 legacy fallback 브라우저 테스트 통과 |
| 8 | 기존 FQC 데이터 호환성 유지 | ✅ | 기존 EAV basic_fields 정상 표시 확인 |
### 12.2 테스트 시나리오
| 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|---------|----------|----------|------|
| `/quality/inspections/1?mode=view` → 검사제품요청서 클릭 | 양식 기반 요청서 표시 | legacy fallback 정상 (EAV 문서 미생성 상태) | ✅ |
| `/quality/inspections/1?mode=view` → 제품검사하기 클릭 | 양식 기반 성적서 표시 (편집 모드, 8컬럼) | FQC 8컬럼 + rowSpan 정상 | ✅ |
| 성적서 검사 완료 후 저장 | document_data + rendered_html 저장 | 코드 구현 완료 (실 저장은 검사 진행 시) | ✅ |
| `mng.sam.kr/documents/{id}` | rendered_html 스냅샷 출력 | show.blade.php 코드 검증 | ✅ |
| template ID 65 없는 환경 | 하드코딩 fallback 동작 | templateLoadFailed 시 legacy 렌더링 | ✅ |
| 치수 검사항목에 측정값 입력 | numeric input → document_data에 저장 | UI 표시 확인 (저장은 검사 진행 시) | ✅ |
---
## 13. 자기완결성 점검 결과
### 13.1 체크리스트 검증
| # | 검증 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 |
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 12.1 |
| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 |
| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서, 미커밋 사항(섹션 10) |
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 (라인 번호 포함) |
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4+5 (시더 데이터, 코드 패턴 포함) |
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 12.2 |
| 8 | 모호한 표현이 없는가? | ✅ | 방안 C 확정, 시더 확정, 자동 생성 확정 |
### 13.2 새 세션 시뮬레이션 테스트
| 질문 | 답변 가능 | 참조 섹션 |
|------|:--------:|----------|
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.1 단계별 절차 + 10. 미커밋 사항(먼저 커밋) |
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 파일 경로 (라인 번호 포함) |
| Q4. 시더에 어떤 데이터를 넣어야 하는가? | ✅ | 5.1.2 검사항목 데이터 (PHP 배열) |
| Q5. React rowSpan은 어떻게 구현하는가? | ✅ | 5.1.3 자동 병합 로직 (TypeScript 코드) |
| Q6. rendered_html은 어떻게 캡처하는가? | ✅ | 5.1.4 캡처 패턴 (참조 코드) |
| Q7. 요청서 필드는 어디서 가져오는가? | ✅ | 5.2.2 데이터 매핑 테이블 |
| Q8. 작업 완료 확인 방법은? | ✅ | 12. 검증 결과 |
| Q9. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 |
---
*이 문서는 /plan 스킬로 생성되었습니다.*

View File

@@ -0,0 +1,245 @@
# MNG 절곡 문서 React 매칭 계획
> **작성일**: 2026-03-05
> **목적**: MNG 절곡 문서(중간검사성적서 #36, 작업일지 #39)의 상세보기를 React와 동일하게 수정
> **상태**: 🔄 진행중
> **영향 범위**: mng만 (React/API 변경 없음)
---
## 현재 진행 상태
| 항목 | 내용 |
|------|------|
| **마지막 완료 작업** | Task 2.2: 절곡 작업일지 전용 렌더링 블록 추가 |
| **다음 작업** | 브라우저 검증 (사용자 확인 필요) |
| **진행률** | 5/6 (83%) |
| **마지막 업데이트** | 2026-03-05 |
---
## 1. 개요
### 1.1 배경
MNG(`mng.sam.kr/documents/36`, `/documents/39`)에서 절곡 문서의 상세보기가 React(`dev.sam.kr/production/worker-screen` > 절곡공정)와 다르다.
- **MNG**: DB 템플릿(`document_templates`, `document_template_columns`)을 동적으로 읽어 범용 렌더링
- **React**: 전용 하드코딩 컴포넌트(`BendingInspectionContent.tsx`, `BendingWorkLogContent.tsx`)로 렌더링
React가 더 상세하고 정확하므로, MNG를 React에 맞춰 수정한다.
### 1.2 핵심 원칙
- MNG only 수정 (React/API 변경 없음)
- DB 템플릿 컬럼 수정 + show.blade.php 전용 렌더링 블록 추가
- 기존 범용 렌더링 로직은 유지 (다른 문서에 영향 없도록)
### 1.3 변경 승인 정책
| 분류 | 예시 | 승인 |
|------|------|------|
| 즉시 가능 | blade 템플릿 수정, DB 시더/컬럼 업데이트 | 불필요 |
| 컨펌 필요 | DocumentController 로직 변경, 새 쿼리 추가 | **필수** |
| 금지 | 테이블 구조 변경, API 엔드포인트 변경 | 별도 협의 |
---
## 2. 차이점 분석
### 2.1 중간검사성적서 (#36) - 중간검사 DATA 테이블
**React 컬럼 구조** (`BendingInspectionContent.tsx`):
| 컬럼 | 타입 | 비고 |
|------|------|------|
| 분류 | text | KWE01 등 제품코드 |
| 제품명 | text | 가이드레일, 케이스 등 |
| 타입 | text | 벽면형, 측면형 등 |
| 겉모양/절곡상태 | check | 양호/불량 체크 |
| 길이 - 도면치수 | text | 설계값 |
| 길이 - 측정값 | input | 입력값 |
| 너비 - 도면치수 | text | 설계값 |
| 너비 - 측정값 | input | 입력값 |
| 간격 - 포인트 | text | 1~5 등 |
| 간격 - 도면치수 | text | 설계값 |
| 간격 - 측정값 | input | 입력값 |
| 판정 | auto | 적/부 자동판정 |
**핵심 차이**:
- React는 **7개 제품** x **다중 gapPoints**(간격 측정점)로 rowSpan 사용
- MNG 현재 DB에는 "간격" 컬럼, "판정" 컬럼 없음
- MNG "분류/제품명"이 하나로 합쳐져 있을 수 있음
**React 제품 목록** (INITIAL_PRODUCTS):
1. 가이드레일 벽면형 (gapPoints: 5개)
2. 가이드레일 측면형 (gapPoints: 5개)
3. 케이스 (gapPoints: 2개)
4. 하단마감재 (gapPoints: 2개)
5. 하단L-BAR (gapPoints: 1개)
6. 연기차단재 W50 (gapPoints: 1개)
7. 연기차단재 W80 (gapPoints: 2개)
### 2.2 작업일지 (#39) - 절곡 전용 구조
**React 구조** (`BendingWorkLogContent.tsx`):
```
헤더: "작업일지 (절곡)" + 문서번호/작성일자 + 결재란
신청업체/신청내용 테이블 (수주일, 현장명, 수주처, 작업일자, 담당자, LOT NO, 연락처, 생산담당자, 출고예정일)
제품 정보 테이블 (제품명, 재질, 마감, 유형)
4개 카테고리 섹션:
1. GuideRailSection (가이드레일 벽면형/측면형)
2. BottomBarSection (하단마감재)
3. ShutterBoxSection (셔터박스)
4. SmokeBarrierSection (연기차단재 W50/W80)
생산량 합계 [kg] (SUS/EGI)
비고
```
**핵심 차이**:
- MNG 현재: 범용 work_order_items 플랫 테이블 (line 137, 섹션 없는 문서)
- React: 절곡 전용 4개 카테고리 섹션 + 제품정보 + 생산량합계
- React는 `bendingInfo` 데이터(work_order.options.bending_info)를 사용
- MNG Controller에서 bending_info를 아직 view에 전달하지 않음
---
## 3. 작업 범위
### Phase 1: 중간검사성적서 (#36)
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 1.1 | 현재 DB 템플릿 컬럼 조회 (tinker) | ✅ | template_id=60, 7컬럼 (간격/판정 포함) |
| 1.2 | show.blade.php 절곡 검사 전용 렌더링 블록 추가 | ✅ | partials/bending-inspection-data.blade.php |
| 1.3 | 브라우저 검증 | ⏳ | mng.sam.kr/documents/36 |
### Phase 2: 작업일지 (#39)
| # | 작업 항목 | 상태 | 비고 |
|---|----------|:----:|------|
| 2.1 | DocumentController에 bending_info 전달 추가 | ✅ | work_order.options에서 추출 |
| 2.2 | show.blade.php 절곡 작업일지 전용 렌더링 블록 추가 | ✅ | partials/bending-worklog.blade.php |
| 2.3 | 브라우저 검증 | ⏳ | mng.sam.kr/documents/39 |
---
## 4. 상세 작업 내용
### 4.1 Task 1.2: show.blade.php 절곡 검사 전용 블록
**위치**: `show.blade.php` 섹션 렌더링 영역 (line ~400)
**조건**: template category가 '절곡' 또는 template name에 '절곡' 포함
**렌더링 구조**:
```html
<table>
<thead>
<tr>
<th rowSpan=2>분류</th>
<th rowSpan=2>제품명</th>
<th rowSpan=2>타입</th>
<th rowSpan=2>겉모양/절곡상태</th>
<th colSpan=2>길이</th>
<th colSpan=2>너비</th>
<th colSpan=3>간격</th>
<th rowSpan=2>판정</th>
</tr>
<tr>
<th>도면치수</th><th>측정값</th>
<th>도면치수</th><th>측정값</th>
<th>포인트</th><th>도면치수</th><th>측정값</th>
</tr>
</thead>
<tbody>
<!-- 각 제품별 gapPoints 수만큼 행 (rowSpan 사용) -->
</tbody>
</table>
```
**데이터 소스**: `document_data` EAV + INITIAL_PRODUCTS 정적 구조
- section_id, column_id, row_index, field_key로 조회
- 간격 데이터: field_key `gap_point_{n}`, `gap_design_{n}`, `gap_measured_{n}`
### 4.2 Task 2.1: Controller bending_info 전달
**파일**: `mng/app/Http/Controllers/DocumentController.php` show()
**추가 로직**:
```php
// 절곡 작업일지용: bending_info 추출
$bendingInfo = null;
if ($workOrder) {
$woOptions = json_decode($workOrder->options, true) ?? [];
$bendingInfo = $woOptions['bending_info'] ?? null;
}
```
**view 전달**: `'bendingInfo' => $bendingInfo`
### 4.3 Task 2.2: show.blade.php 절곡 작업일지 전용 블록
**위치**: `show.blade.php` line ~137 (작업일지 전용 영역)
**조건**: 절곡 공정인 경우 (template name에 '절곡' 포함 또는 공정 확인)
**렌더링 구조**:
1. 신청업체/신청내용 테이블
2. 제품 정보 테이블 (제품명, 재질, 마감, 유형)
3. 4개 카테고리 섹션 (가이드레일, 하단마감재, 셔터박스, 연기차단재)
4. 생산량 합계 [kg] (SUS/EGI)
5. 비고
**PHP 유틸 함수 필요**:
- `getMaterialMapping($productCode, $finishMaterial)` - React utils.ts 포팅
- `calculateProductionSummary($bendingInfo, $mapping)` - React utils.ts 포팅
---
## 5. 참고 파일
### React (타겟 - 읽기 전용)
- `react/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx` (547행)
- `react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx` (207행)
- `react/src/components/production/WorkOrders/documents/bending/types.ts`
- `react/src/components/production/WorkOrders/documents/bending/utils.ts`
- `react/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx`
- `react/src/components/production/WorkOrders/documents/bending/BottomBarSection.tsx`
- `react/src/components/production/WorkOrders/documents/bending/ShutterBoxSection.tsx`
- `react/src/components/production/WorkOrders/documents/bending/SmokeBarrierSection.tsx`
- `react/src/components/production/WorkOrders/documents/bending/ProductionSummarySection.tsx`
### MNG (수정 대상)
- `mng/resources/views/documents/show.blade.php` (메인 렌더링)
- `mng/app/Http/Controllers/DocumentController.php` (show 메서드)
### DB 참고
- `mng/database/seeders/MidInspectionTemplateSeeder.php` (절곡 검사 템플릿)
- `mng/database/seeders/WorkLogTemplateSeeder.php` (절곡 작업일지 템플릿)
### 문서
- `docs/features/documents/README.md` - 문서관리 시스템
- `docs/system/database/documents.md` - DB 스키마
- `docs/dev/dev_plans/document-system-mid-inspection.md` - 중간검사 계획
- `docs/dev/dev_plans/document-system-work-log.md` - 작업일지 계획
---
## 6. 변경 이력
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|------|------|----------|------|------|
| 2026-03-05 | - | 계획 문서 작성 | - | - |
---
## 7. 검증 결과
> 작업 완료 후 이 섹션에 검증 결과 추가
### 7.1 성공 기준
| 기준 | 달성 | 비고 |
|------|------|------|
| mng.sam.kr/documents/36 검사 DATA 테이블이 React와 동일 구조 | ⏳ | rowSpan, 간격, 판정 포함 |
| mng.sam.kr/documents/39 작업일지가 React와 동일 구조 | ⏳ | 4개 카테고리, 생산량합계 포함 |
| 기존 다른 문서 렌더링에 영향 없음 | ⏳ | 조건 분기로 절곡 전용만 |

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,794 @@
# SAM 로컬 개발 환경 셋팅 가이드
> 최종 업데이트: 2026-03-09
---
## 1. 사전 준비
### 필수 소프트웨어
| 소프트웨어 | 버전 | 용도 | 설치 |
|-----------|------|------|---------------------------------------------------------------|
| **Docker Desktop** | 최신 | 컨테이너 실행 | [docker.com](https://www.docker.com/products/docker-desktop/) |
| **Git** | 최신 | 버전 관리 | `brew install git` |
| **mkcert** | 최신 | 로컬 SSL 인증서 | `brew install mkcert` |
| **텍스트 에디터** | - | 코드 편집 | JetBrains IDE 권장 |
> Docker Desktop이 설치되면 PHP, Node.js, MySQL 등은 **별도 설치 불필요** (Docker 컨테이너 내부에서 실행)
### Git 서버 정보
| 항목 | 값 |
|------|-----|
| Git 서버 | Gitea (자체 호스팅) |
| 서버 주소 | `http://114.203.209.83:3000` |
| 조직 | `SamProject` |
> Gitea 계정이 필요합니다. 팀장에게 계정 생성을 요청하세요.
---
## 2. 저장소 클론
### 디렉토리 구조
```
Works/@KD_SAM/SAM/ ← 루트 (원하는 경로에 생성)
├── api/ ← Laravel REST API
├── mng/ ← Laravel 관리자 패널
├── react/ ← Next.js 프론트엔드
├── docs/ ← 기술 문서
├── hotfix/ ← 테스트/QA 문서
├── docker/ ← Docker 설정 (api 저장소에 포함되지 않음)
├── design/ ← 디자인 시스템 (선택)
└── planning/ ← 기획 문서 (선택)
```
### 클론 명령어
```bash
# 작업 디렉토리 생성
mkdir -p ~/Works/@KD_SAM/SAM
cd ~/Works/@KD_SAM/SAM
# 5개 저장소 클론
git clone http://114.203.209.83:3000/SamProject/sam-api.git api
git clone http://114.203.209.83:3000/SamProject/sam-manage.git mng
git clone http://114.203.209.83:3000/SamProject/sam-react-prod.git react
git clone http://114.203.209.83:3000/SamProject/sam-docs.git docs
git clone http://114.203.209.83:3000/SamProject/sam-hotfix.git hotfix
```
### 기본 브랜치 전환
```bash
# api, react는 develop 브랜치에서 작업
cd api && git checkout develop && cd ..
cd react && git checkout develop && cd ..
```
> **중요**: `main` 브랜치에서 직접 작업하지 않습니다. 항상 `develop`에서 작업합니다.
---
## 3. Docker 환경 구성
### 3-1. Docker 설정 파일 확인
`docker/` 디렉토리에 모든 Docker 설정이 포함되어 있습니다.
```
docker/
├── docker-compose.yml ← 메인 Compose 파일
├── .env ← Compose 환경변수
├── api/
│ ├── Dockerfile ← PHP 8.4 + Nginx
│ ├── nginx.conf
│ ├── supervisord.conf
│ └── uploads.ini
├── mng/
│ ├── Dockerfile ← PHP 8.4 + Nginx
│ ├── nginx.conf
│ ├── supervisord.conf
│ └── uploads.ini
├── react/
│ └── Dockerfile ← Node 20 + Chromium (PDF용)
├── mysql/
│ ├── init.sql ← 초기 DB/유저 생성
│ └── my.cnf ← MySQL 설정
├── nginx/
│ ├── nginx.conf ← 리버스 프록시 설정
│ └── ssl/ ← SSL 인증서
└── 5130/
└── Dockerfile ← 레거시 PHP 7.3
```
### 3-2. 서비스 구성
| 서비스 | 이미지 | 내부 포트 | 역할 |
|--------|--------|-----------|------|
| **nginx** | nginx:latest | 80, 443 | 리버스 프록시, SSL 종료 |
| **api** | PHP 8.4-fpm | 9000 | REST API 백엔드 |
| **mng** | PHP 8.4-fpm | 9000 | 관리자 패널 |
| **react** | node:20-alpine | 3000 | Next.js 프론트엔드 |
| **mysql** | mysql:8.0 | 3306 | 데이터베이스 |
| **php73** | PHP 7.3-fpm | 9000 | 레거시 5130 앱 |
### 3-3. 네트워크
모든 컨테이너는 `samnet` 브릿지 네트워크로 연결됩니다.
```
[브라우저] → [nginx:443] → api / mng / react (내부 라우팅)
→ mysql (컨테이너명: sam-mysql-1)
```
---
## 4. hosts 파일 설정
로컬 도메인을 사용하기 위해 hosts 파일을 수정합니다.
```bash
sudo nano /etc/hosts
```
아래 내용을 추가:
```
127.0.0.1 api.sam.kr mng.sam.kr admin.sam.kr dev.sam.kr design.sam.kr plan.sam.kr 5130.sam.kr
127.0.0.1 sam.kr www.sam.kr sales.sam.kr demo.sam.kr
```
---
## 5. SSL 인증서 설정
로컬 HTTPS를 위한 자체 서명 인증서를 생성합니다.
### 5-1. mkcert 설치 및 초기화
```bash
# mkcert 설치 (처음 한 번)
brew install mkcert
mkcert -install # 로컬 CA를 시스템에 등록
```
### 5-2. 와일드카드 인증서 생성
```bash
cd docker/nginx/ssl/
# *.sam.kr 와일드카드 인증서 생성
mkcert "*.sam.kr" localhost 127.0.0.1 ::1
# 파일명 변경 (nginx.conf에서 참조하는 이름으로)
mv _wildcard.sam.kr+3.pem sam.kr.crt
mv _wildcard.sam.kr+3-key.pem sam.kr.key
```
### 5-3. 포트 포워딩 설정 (macOS)
Docker가 443 포트를 4443으로 매핑하므로, 브라우저에서 표준 443 포트로 접속하려면 포트 포워딩이 필요합니다.
```bash
# pfctl 규칙 적용 (443 → 4443 포워딩)
sudo pfctl -ef docker/nginx/ssl/pf-sam.conf
```
> **참고**: macOS 재부팅 시 이 설정은 초기화되므로 재부팅 후 다시 실행해야 합니다.
**포트 포워딩 없이 사용하려면** `https://api.sam.kr:4443` 처럼 포트 번호를 직접 지정합니다.
---
## 6. 환경변수 (.env) 설정
### 6-1. API (.env)
```bash
cd api
cp .env.example .env
```
`.env` 파일에서 확인/수정할 항목:
```env
# 앱 키 생성은 Docker 실행 후 컨테이너 내에서 수행
# docker exec sam-api-1 php artisan key:generate
APP_NAME="SAM API"
APP_ENV=local
APP_DEBUG=true
APP_URL=https://api.sam.kr/
# DB (Docker 환경에서는 docker-compose가 오버라이드)
DB_HOST=127.0.0.1 # 로컬 직접 접속 시
# DB_HOST=sam-mysql-1 # Docker 환경 (자동 오버라이드)
DB_DATABASE=samdb
DB_USERNAME=samuser
DB_PASSWORD=sampass
# Swagger
L5_SWAGGER_GENERATE_ALWAYS=true
L5_SWAGGER_CONST_HOST=https://api.sam.kr/
# Sanctum 토큰 (분 단위)
SANCTUM_ACCESS_TOKEN_EXPIRATION=120
SANCTUM_REFRESH_TOKEN_EXPIRATION=10080
# 내부 통신 키 (MNG ↔ API)
INTERNAL_EXCHANGE_SECRET= # 팀 내부 문서에서 확인
```
> **API 키, Firebase, AI 서비스 키** 등 민감한 값은 팀 내부 문서(노션)에서 별도 공유합니다.
### 6-2. MNG (.env)
```bash
cd mng
cp .env.example .env
```
```env
APP_NAME=SAM-MNG
APP_ENV=local
APP_DEBUG=true
APP_URL=https://mng.sam.kr
DB_HOST=sam-mysql-1
DB_DATABASE=samdb
DB_USERNAME=samuser
DB_PASSWORD=sampass
# API 서버 연동
API_BASE_URL=https://api.sam.kr
# 내부 통신 키 (API와 동일한 값)
INTERNAL_EXCHANGE_SECRET= # API의 값과 동일하게 설정
```
### 6-3. React (.env.local)
```bash
cd react
cp .env.example .env.local
```
```env
NEXT_PUBLIC_APP_ENV=local
NEXT_PUBLIC_API_URL=https://api.sam.kr
NEXT_PUBLIC_FRONTEND_URL=https://dev.sam.kr
NEXT_PUBLIC_AUTH_MODE=sanctum
# API Key (서버 사이드 전용 - NEXT_PUBLIC_ 접두사 붙이지 말 것!)
API_KEY= # 팀 내부 문서에서 확인
# 개발 도구
NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=false
# Puppeteer (PDF 생성 - Docker에서는 자동 설정)
PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
```
### 6-4. docs / hotfix
별도 환경변수 설정 불필요 (순수 마크다운 문서 저장소)
---
## 7. Docker 실행
### 7-1. 최초 실행
```bash
cd docker
# 이미지 빌드 + 컨테이너 시작
docker compose up -d --build
```
> 첫 실행 시 이미지 빌드에 5~10분 소요될 수 있습니다.
### 7-2. 초기 설정 (최초 1회)
```bash
# API: 앱 키 생성 + 의존성 설치 + 마이그레이션
docker exec sam-api-1 php artisan key:generate
docker exec sam-api-1 composer install
docker exec sam-api-1 php artisan migrate
docker exec sam-api-1 php artisan l5-swagger:generate
# MNG: 앱 키 생성 + 의존성 설치
docker exec sam-mng-1 php artisan key:generate
docker exec sam-mng-1 composer install
docker exec sam-mng-1 npm install
docker exec sam-mng-1 npm run build
```
> React는 Docker 컨테이너 시작 시 자동으로 `npm install` + `npm run dev`가 실행됩니다.
### 7-3. 일상적인 시작/종료
```bash
cd docker
# 시작
docker compose up -d
# 종료
docker compose down
# 로그 확인
docker compose logs -f # 전체
docker compose logs -f api # API만
docker compose logs -f react # React만
# 컨테이너 상태 확인
docker compose ps
```
---
## 8. 접속 확인
### 8-1. 로컬 도메인
| 서비스 | URL | 설명 |
|--------|-----|------|
| **프론트엔드** | `https://dev.sam.kr` | Next.js 사용자 화면 |
| **API 문서** | `https://api.sam.kr/api-docs/index.html` | Swagger UI |
| **관리자 패널** | `https://mng.sam.kr` | MNG 관리자 화면 |
| **관리자 (별칭)** | `https://admin.sam.kr` | MNG와 동일 |
| **디자인 시스템** | `https://design.sam.kr` | 컴포넌트 스토리북 |
| **레거시** | `https://5130.sam.kr` | 기존 PHP 시스템 |
### 8-2. DB 접속 정보
| 항목 | 값 |
|------|-----|
| Host | `127.0.0.1` |
| Port | `3306` |
| Database | `samdb` |
| Username | `samuser` |
| Password | `sampass` |
| Root Password | `root` |
| 레거시 DB | `chandj` |
> DBeaver, DataGrip, MySQL Workbench 등 원하는 DB 클라이언트로 접속 가능
### 8-3. 접속 테스트
```bash
# API 응답 확인
curl -k https://api.sam.kr/api-docs/index.html
# MySQL 접속 확인
docker exec sam-mysql-1 mysql -u samuser -psampass -e "SHOW DATABASES;"
```
---
## 9. 저장소별 상세 정보
### 9-1. api/ (REST API 서버)
| 항목 | 값 |
|------|-----|
| 프레임워크 | Laravel 12 (PHP 8.4) |
| 인증 | Sanctum (토큰 기반) |
| API 문서 | Swagger (l5-swagger) |
| DB | MySQL 8.0 (samdb) |
| 역할 | 모든 비즈니스 로직의 중심, 프론트/관리자 모두 이 API 사용 |
**주요 명령어:**
```bash
# 컨테이너 진입
docker exec -it sam-api-1 bash
# 마이그레이션
php artisan migrate
php artisan migrate:status
# Swagger 재생성
php artisan l5-swagger:generate
# 코드 포매터
./vendor/bin/pint
# 캐시 초기화
php artisan cache:clear
php artisan config:clear
php artisan route:clear
```
**핵심 디렉토리:**
```
api/
├── app/
│ ├── Http/Controllers/Api/V1/ ← API 컨트롤러
│ ├── Http/Requests/ ← FormRequest (검증)
│ ├── Models/ ← Eloquent 모델
│ ├── Services/ ← 비즈니스 로직 (핵심)
│ ├── Swagger/v1/ ← Swagger 문서 클래스
│ └── Helpers/ApiResponse.php ← 응답 헬퍼
├── database/migrations/ ← DB 마이그레이션
├── routes/api.php ← API 라우트
└── lang/ko/ ← 한국어 메시지
```
### 9-2. mng/ (관리자 패널)
| 항목 | 값 |
|------|-----|
| 프레임워크 | Laravel 12 (PHP 8.4) |
| 프론트엔드 | Blade + Tailwind CSS + HTMX + DaisyUI |
| 역할 | 시스템 관리, 메뉴/권한 관리, 테넌트 관리 |
**주요 명령어:**
```bash
# 컨테이너 진입
docker exec -it sam-mng-1 bash
# 프론트 에셋 빌드
npm run build # 프로덕션
npm run dev # 개발 (HMR)
# 코드 포매터
./vendor/bin/pint
```
**핵심 디렉토리:**
```
mng/
├── app/
│ ├── Http/Controllers/ ← 웹 컨트롤러
│ ├── Models/ ← 독립 모델 (API와 별개)
│ └── Services/ ← 비즈니스 로직
├── resources/views/ ← Blade 템플릿
├── routes/web.php ← 웹 라우트
└── public/ ← 정적 파일
```
> **주의**: MNG에서는 DB 마이그레이션을 생성/실행하지 않습니다. DB 변경은 반드시 API 프로젝트에서 수행합니다.
### 9-3. react/ (Next.js 프론트엔드)
| 항목 | 값 |
|------|-----|
| 프레임워크 | Next.js 15 (React 19, TypeScript) |
| 스타일링 | Tailwind CSS 4 |
| UI 라이브러리 | shadcn/ui (Radix UI 기반) |
| 상태관리 | Zustand |
| 폼 검증 | Zod + React Hook Form |
| 역할 | 사용자용 ERP 프론트엔드 |
**주요 명령어:**
```bash
# React는 Docker 컨테이너가 자동으로 dev 서버를 실행합니다.
# 수동 실행이 필요한 경우:
docker exec -it sam-react-1 sh
npm run dev # 개발 서버
npm run build # 프로덕션 빌드
npm run lint # ESLint
```
**핵심 디렉토리:**
```
react/
├── src/
│ ├── app/ ← Next.js App Router 페이지
│ ├── components/
│ │ ├── ui/ ← shadcn/ui 원자 컴포넌트
│ │ ├── molecules/ ← 조합 컴포넌트
│ │ ├── organisms/ ← 복합 컴포넌트
│ │ └── [도메인]/ ← 도메인별 컴포넌트
│ ├── lib/ ← 유틸리티, API 헬퍼
│ └── stores/ ← Zustand 스토어
├── public/ ← 정적 파일
└── next.config.ts ← Next.js 설정
```
**핵심 규칙:**
- 모든 페이지는 `'use client'` 선언 필수 (폐쇄형 ERP, SSR 불필요)
- API 호출은 Server Action을 통해 수행 (HttpOnly 쿠키 프록시)
- `buildApiUrl()` 유틸리티 필수 사용
### 9-4. docs/ (기술 문서)
| 항목 | 값 |
|------|-----|
| 내용 | 개발 가이드, API 스펙, 기획 문서, 표준 |
| 형식 | Markdown |
| 설치 | 없음 (문서만 관리) |
```
docs/
├── INDEX.md ← 문서 인덱스 (여기부터 시작)
├── dev/
│ ├── dev_plans/ ← 개발 계획 문서
│ ├── standards/ ← 코드/아키텍처 표준
│ ├── guides/ ← 셋업/사용 가이드
│ ├── changes/ ← 변경 로그
│ └── deploys/ ← 배포 문서
├── features/ ← 기능 스펙
├── frontend/ ← 프론트엔드 API 스펙
├── rules/ ← 비즈니스 규칙
└── system/ ← 시스템 아키텍처
```
### 9-5. hotfix/ (테스트/QA)
| 항목 | 값 |
|------|-----|
| 내용 | E2E 테스트 결과, 버그 리포트, 테스트 케이스 |
| 형식 | Markdown + 스크린샷 |
| 설치 | 없음 (문서만 관리) |
```
hotfix/
├── e2e/ ← E2E 테스트 정의
├── testcase/ ← 테스트 케이스 문서
├── reports/ ← 테스트 실행 결과
├── screenshots/ ← 시각적 테스트 증거
├── research/ ← 조사 자료
├── sam.code-workspace ← VS Code 워크스페이스
├── *-Test-Report_*.md ← 개별 테스트 보고서
└── Fail-*_*.md ← 실패 케이스 보고서
```
---
## 10. 주요 아키텍처 개념
### 10-1. 멀티테넌트
- 모든 데이터에 `tenant_id` 컬럼이 존재
- `BelongsToTenant` 글로벌 스코프가 자동으로 테넌트 필터링
- 하나의 DB에서 여러 회사(테넌트)의 데이터를 격리
### 10-2. 데이터 흐름
```
[사용자 브라우저]
[Next.js (react/)] ──Server Action──▶ [Laravel API (api/)] ──▶ [MySQL]
[관리자 브라우저] │
│ │
▼ │
[Laravel MNG (mng/)] ──내부 API 호출──────────┘
```
### 10-3. 인증 방식
| 클라이언트 | 인증 방식 | 설명 |
|-----------|----------|------|
| React (웹) | Sanctum Cookie | HttpOnly 쿠키, Server Action 프록시 |
| MNG (관리자) | 세션 + 내부 HMAC | Laravel 세션 + API 내부 통신 키 |
| 외부 연동 | API Key + Bearer | Sanctum 토큰 |
---
## 11. 자주 쓰는 명령어
### Docker 관련
```bash
# 전체 시작/종료
cd docker && docker compose up -d
cd docker && docker compose down
# 컨테이너 재시작
docker restart sam-api-1
docker restart sam-react-1
# 이미지 재빌드 (Dockerfile 수정 후)
docker compose build --no-cache api
docker compose up -d api
# 볼륨 포함 완전 초기화 (⚠️ DB 데이터 삭제됨)
docker compose down -v
```
### API 개발
```bash
docker exec sam-api-1 php artisan migrate # 마이그레이션
docker exec sam-api-1 php artisan migrate:rollback # 롤백
docker exec sam-api-1 php artisan l5-swagger:generate # Swagger 재생성
docker exec sam-api-1 ./vendor/bin/pint # 코드 포매팅
docker exec sam-api-1 php artisan tinker # REPL
docker exec sam-api-1 php artisan route:list # 라우트 목록
```
### MNG 개발
```bash
docker exec sam-mng-1 php artisan tinker
docker exec sam-mng-1 npm run dev # Vite HMR
docker exec sam-mng-1 npm run build # 에셋 빌드
docker exec sam-mng-1 ./vendor/bin/pint
```
### MySQL 접속
```bash
# CLI 접속
docker exec -it sam-mysql-1 mysql -u samuser -psampass samdb
# 특정 쿼리 실행
docker exec sam-mysql-1 mysql -u samuser -psampass samdb -e "SHOW TABLES;"
```
---
## 12. 트러블슈팅
### SSL 인증서 오류 (브라우저에서 "안전하지 않음")
```bash
# mkcert CA가 설치되지 않은 경우
mkcert -install
# 인증서 재생성
cd docker/nginx/ssl
mkcert "*.sam.kr" localhost 127.0.0.1 ::1
mv _wildcard.sam.kr+3.pem sam.kr.crt
mv _wildcard.sam.kr+3-key.pem sam.kr.key
# Nginx 재시작
docker restart sam-nginx-1
```
### 도메인 접속이 안 될 때
```bash
# hosts 파일 확인
cat /etc/hosts | grep sam
# DNS 캐시 초기화 (macOS)
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
# Nginx 설정 유효성 확인
docker exec sam-nginx-1 nginx -t
```
### 포트 포워딩 확인 (443 → 4443)
```bash
# 현재 규칙 확인
sudo pfctl -s rules | grep 4443
# 규칙 재적용
sudo pfctl -ef docker/nginx/ssl/pf-sam.conf
# 규칙 비활성화 (필요 시)
sudo pfctl -d
```
### DB 접속 오류
```bash
# MySQL 컨테이너 상태 확인
docker compose ps mysql
# MySQL 로그 확인
docker compose logs mysql
# 수동 접속 테스트
docker exec sam-mysql-1 mysql -u root -proot -e "SELECT 1;"
```
### React 빌드/HMR 오류
```bash
# node_modules 초기화
docker exec sam-react-1 rm -rf node_modules .next
docker exec sam-react-1 npm install
docker restart sam-react-1
```
### composer/npm 의존성 문제
```bash
# API
docker exec sam-api-1 composer install --no-cache
docker exec sam-api-1 composer dump-autoload
# MNG
docker exec sam-mng-1 composer install --no-cache
docker exec sam-mng-1 npm ci
```
---
## 13. Git 워크플로우
### 브랜치 전략
| 브랜치 | 역할 | 규칙 |
|--------|------|------|
| `main` | 배포용 | squash merge로만 올림, 직접 커밋 금지 |
| `develop` | 일상 작업 | 자유롭게 커밋 |
| `feature/*` | 대형 기능 | 선택적 사용 (1주일+ 작업) |
### 커밋 메시지 형식
```
type: 간결한 설명
type 종류:
feat: 새 기능
fix: 버그 수정
refactor: 리팩토링
docs: 문서
chore: 설정, 빌드
style: 코드 포매팅
```
### 일상 워크플로우
```bash
# 1. 작업 시작
cd api
git checkout develop
git pull origin develop
# 2. 작업 수행 + 커밋
git add <파일들>
git commit -m "feat: 수주관리 삭제 기능 추가"
# 3. 푸시
git push origin develop
```
---
## 14. 셋팅 체크리스트
아래 항목을 순서대로 확인하며 셋팅을 완료합니다.
- [ ] Docker Desktop 설치 및 실행
- [ ] Git 설치 및 Gitea 계정 발급
- [ ] 5개 저장소 클론 완료
- [ ] `/etc/hosts` 파일 수정
- [ ] mkcert 설치 + SSL 인증서 생성
- [ ] pfctl 포트 포워딩 설정
- [ ] api/.env 설정 (`.env.example``.env`)
- [ ] mng/.env 설정
- [ ] react/.env.local 설정
- [ ] API Key, HMAC Key 등 민감 값 입력 (팀 공유 문서 참조)
- [ ] `docker compose up -d --build` 실행
- [ ] 초기 설정 실행 (key:generate, composer install, migrate)
- [ ] `https://api.sam.kr/api-docs/index.html` 접속 확인
- [ ] `https://mng.sam.kr` 접속 확인
- [ ] `https://dev.sam.kr` 접속 확인
- [ ] DB 클라이언트로 `127.0.0.1:3306` 접속 확인
---
## 15. 참고 문서
| 문서 | 경로 | 설명 |
|------|------|------|
| 문서 인덱스 | `docs/INDEX.md` | 전체 문서 목록 |
| API 규칙 | `API_RULES.md` | API 개발 규칙 |
| 개발 명령어 | `DEV_COMMANDS.md` | 자주 쓰는 명령어 모음 |
| 품질 체크리스트 | `QUALITY_CHECKLIST.md` | 코드 품질 체크 항목 |
| 빠른 참조 | `SAM_QUICK_REFERENCE.md` | 핵심 규칙 요약 |
| SSL 가이드 | `docker/nginx/ssl/SSL_SETUP_GUIDE.md` | SSL 상세 설정 |
---
> 문의사항은 팀 슬랙 채널 또는 팀장에게 문의하세요.

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,999 @@
# 결재 양식 기술 명세
> **작성일**: 2026-03-06
> **상태**: Phase 2 구현 완료
> **관련**: [README.md](README.md) | [워크플로우](workflows.md) | [API 명세](api-reference.md) | [UI 화면](ui-screens.md)
---
## 1. 개요
### 1.1 목적
SAM MNG 결재관리의 **기안함 양식** 기술 명세. 각 양식의 필드 구조, JSON Content 데이터 형식, UI 인터랙션, 특수 로직을 정의한다.
### 1.2 양식 목록
| 코드 | 양식명 | 분류 | Blade 파일 | 설명 |
|------|--------|------|------------|------|
| `BUSINESS_DRAFT` | 업무기안서 | 일반 | (body 편집기) | 일반 업무 보고·요청 |
| `leave` | 휴가신청 | 인사/근태 | `_leave-form.blade.php` | 연차, 휴가, 근태 신청 |
| `attendance_request` | 근태신청 | 인사/근태 | `_leave-form.blade.php` | 외근, 출장, 조퇴 등 |
| `reason_report` | 사유서 | 인사/근태 | `_leave-form.blade.php` | 지각, 결근 등 사유 소명 |
| `resignation` | 사직서 | 인사/근태 | `_resignation-form.blade.php` | 퇴직 서류 |
| `employment_cert` | 재직증명서 | 증명서 | `_certificate-form.blade.php` | 재직 증명 발급 (PDF) |
| `career_cert` | 경력증명서 | 증명서 | `_career-cert-form.blade.php` | 경력 증명 발급 (PDF) |
| `appointment_cert` | 위촉증명서 | 증명서 | `_appointment-cert-form.blade.php` | 위촉/임명 증명 발급 (PDF) |
| `pr_expense` | 지출품의서 | 품의 | `_purchase-request-form.blade.php` | 지출 전 사전 승인 |
| `pr_contract` | 계약체결품의서 | 품의 | `_purchase-request-form.blade.php` | 계약 체결 전 승인 |
| `pr_purchase` | 구매품의서 | 품의 | `_purchase-request-form.blade.php` | 물품 구매 전 승인 |
| `pr_trip` | 출장품의서 | 품의 | `_purchase-request-form.blade.php` | 출장 계획 승인 |
| `pr_settlement` | 비용정산품의서 | 품의 | `_purchase-request-form.blade.php` | 비용 정산 승인 |
| `expense` | 지출결의서 | 재무 | `_expense-form.blade.php` | 법인카드/송금/자동이체 지출 |
### 1.3 공통 구조
모든 양식은 동일한 패턴으로 동작한다:
```
양식 선택 (form_id)
양식별 Blade 파셜 렌더링 (create.blade.php 내 조건부 display)
사용자 입력 → Alpine.js / JavaScript 인터랙션
getFormData() → JSON content 생성
ApprovalService::createApproval() → Approval.content (JSON 컬럼) 저장
```
### 1.4 양식 선택 UI (2단계 분류 + 설명 카드)
양식 선택은 **2단계 드롭다운 + 설명 카드** 레이아웃으로 구성된다.
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 양식 * │
│ ┌──── 30% ────────┐ ┌─────────────── 70% ───────────────────────────┐ │
│ │ 📋 품의 ▼ │ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ │ │ 📋 지출품의서 │ │ │
│ │ 지출품의서 ▼ │ │ │ 지출이 발생하기 전 사전 승인을 받는 │ │ │
│ │ │ │ │ 문서입니다. 예산 범위 내에서 지출 항목과 │ │ │
│ │ │ │ │ 금액을 기재하여 사전에 승락을 받습니다. │ │ │
│ └──────────────────┘ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
#### 1단계: 분류 선택 (`form_category`)
| 분류 | 아이콘 | 포함 양식 |
|------|--------|----------|
| 일반 | 📄 | 업무기안서 |
| 인사/근태 | 👤 | 휴가신청, 근태신청, 사유서, 사직서 |
| 증명서 | 📜 | 재직증명서, 경력증명서, 위촉증명서 |
| 품의 | 📋 | 지출품의서, 계약체결품의서, 구매품의서, 출장품의서, 비용정산품의서 |
| 재무 | 💰 | 지출결의서 |
#### 2단계: 양식 선택 (`form_id`)
- 1단계 분류 선택 시 해당 분류에 속하지 않는 양식은 `display:none` + `disabled`
- 분류 내 첫 번째 양식 자동 선택
#### 설명 카드 (`formDescriptions`)
- 양식 선택 시 우측에 해당 양식의 아이콘/제목/설명 텍스트 표시
- 14종 전체 양식에 대한 설명 정의 (create/edit 공통)
- 색상: 양식별 Tailwind 테마 색상 (`border-*-200 bg-*-50`)
#### 핵심 JavaScript 함수
| 함수 | 설명 |
|------|------|
| `buildCategoryOptions()` | 사용 가능한 카테고리만 `form_category` 옵션으로 생성 |
| `filterFormsByCategory(cat)` | 선택된 분류 외 양식 옵션 숨김/비활성화 |
| `selectCategoryByFormId(formId)` | formId로 카테고리 역산하여 자동 선택 |
| `updateFormDescription(formId)` | 설명 카드 DOM 업데이트 |
### 1.5 파일 구조
```
resources/views/approvals/
├── create.blade.php ← 기안 작성 (2단계 양식 선택 + 설명 카드 + 동적 폼)
├── edit.blade.php ← 기안 수정 (create와 동일한 2단계 선택 구조)
├── show.blade.php ← 상세 조회 (양식별 조회 컴포넌트)
└── partials/
├── _leave-form.blade.php ← 휴가신청 폼
├── _expense-form.blade.php ← 지출결의서 폼
├── _expense-show.blade.php ← 지출결의서 조회
├── _purchase-request-form.blade.php ← 품의서 5종 통합 폼 (Alpine.js)
├── _purchase-request-show.blade.php ← 품의서 5종 통합 조회
├── _certificate-form.blade.php ← 재직증명서 폼
├── _certificate-show.blade.php ← 재직증명서 조회
├── _career-cert-form.blade.php ← 경력증명서 폼
├── _career-cert-show.blade.php ← 경력증명서 조회
├── _appointment-cert-form.blade.php ← 위촉증명서 폼
├── _appointment-cert-show.blade.php ← 위촉증명서 조회
├── _resignation-form.blade.php ← 사직서 폼
├── _resignation-show.blade.php ← 사직서 조회
├── _approval-stamp-table.blade.php ← 결재 도장 테이블
└── _approval-line-editor.blade.php ← 결재선 편집기
```
---
## 2. 휴가신청 (leave)
### 2.1 폼 필드
| 필드 ID | 라벨 | 타입 | 필수 | 기본값 | 설명 |
|---------|------|------|------|--------|------|
| `leave-user-id` | 신청자 | select | 필수 | `auth()->id()` | 활성 사원 목록 |
| `leave-type` | 유형 | select | 필수 | - | 휴가/근태신청/사유서 |
| `leave-start-date` | 시작일 | date | 필수 | - | `YYYY-MM-DD` |
| `leave-end-date` | 종료일 | date | 필수 | - | `YYYY-MM-DD` |
| `leave-reason` | 사유 | textarea | 선택 | - | 자유 텍스트 |
### 2.2 Content JSON
```json
{
"user_id": "10",
"leave_type": "연차",
"start_date": "2026-03-06",
"end_date": "2026-03-07",
"reason": "개인 사유"
}
```
### 2.3 특수 로직
- **자동 선택**: 로그인 사용자가 기본 선택 (`auth()->id()`)
- **직원 목록**: `$employees` Props로 전달 (활성 사원만)
- **단순 구조**: Alpine.js 없이 Blade 폼으로 구현
---
## 3. 지출결의서 (expense)
### 3.1 폼 구조 (Alpine.js 기반)
```javascript
x-data="expenseForm(initialData, authUserName, initialFiles, cardsData, accountsData)"
```
### 3.2 기본 정보 필드
| 필드 | 라벨 | 타입 | 필수 | 기본값 |
|------|------|------|------|--------|
| `expense_type` | 지출형식 | radio | 필수 | `corporate_card` |
| `tax_invoice` | 세금계산서 | radio | 필수 | `normal` |
| `write_date` | 작성일자 | date | 선택 | 오늘 |
| `approval_date` | 결재일자 | date | 선택 | 오늘 |
| `department` | 부서 | text | 선택 | `경리부` |
| `writer_name` | 이름 | text | 선택 | 인증 사용자명 |
### 3.3 지출형식별 선택
| 지출형식 | 코드 | 연결 데이터 |
|---------|------|------------|
| 법인카드 | `corporate_card` | `$cards``selected_card` |
| 송금 | `transfer` | `$accounts``selected_account` |
| 자동이체 출금 | `auto_transfer` | `$accounts``selected_account` |
| 현금/가지급정산 | `cash_advance` | 없음 |
**법인카드 선택 시 저장 구조:**
```json
{
"selected_card": {
"id": 1,
"card_name": "삼성카드",
"card_company": "삼성",
"card_number_last4": "1234",
"card_holder_name": "홍길동"
}
}
```
**계좌 선택 시 저장 구조:**
```json
{
"selected_account": {
"id": 1,
"bank_name": "국민은행",
"account_number": "123-456-789012",
"account_holder": "주일기업"
}
}
```
### 3.4 세금계산서 옵션
| 옵션 | 코드 |
|------|------|
| 일반 | `normal` |
| 이월발행 | `deferred` |
| 없음 | `none` |
### 3.5 내역 테이블
**동적 rows** (`.items` 배열):
| 필드 | 라벨 | 타입 | 설명 |
|------|------|------|------|
| `date` | 일자 | date | `YYYY-MM-DD` |
| `description` | 적요 | text | 지출 설명 |
| `amount` | 금액 | number | 콤마 제거 정수 |
| `vendor` | 거래처 | text | Autocomplete 검색 |
| `vendor_id` | 거래처 ID | hidden | API 연결 ID |
| `vendor_biz_no` | 사업자번호 | hidden | 자동 채움 |
| `bank` | 은행명 | text | 수동 입력 |
| `account_no` | 계좌번호 | text | 수동 입력 |
| `depositor` | 예금주 | text | 수동 입력 |
| `remark` | 비고 | text | 메모 |
### 3.6 Content JSON (전체)
```json
{
"expense_type": "corporate_card",
"tax_invoice": "normal",
"write_date": "2026-03-06",
"approval_date": "2026-03-06",
"department": "경리부",
"writer_name": "홍길동",
"items": [
{
"date": "2026-03-05",
"description": "사무용품 구매",
"amount": 150000,
"vendor": "오피스디포",
"vendor_id": 123,
"vendor_biz_no": "123-45-67890",
"bank": "",
"account_no": "",
"depositor": "",
"remark": ""
}
],
"total_amount": 150000,
"attachment_memo": "영수증 첨부",
"selected_card": { ... },
"selected_account": null
}
```
### 3.7 특수 기능
#### 거래처 검색 (Autocomplete)
```
입력 → 250ms 디바운싱 → API 호출 → 드롭다운 렌더링
API: /barobill/tax-invoice/search-partners?keyword=...
키보드: ↑↓(네비게이션), Enter(선택), Esc(닫기)
마우스: 항목 클릭(선택)
```
#### 금액 입력 포맷팅
```
입력 시: 콤마 제거 → 정수 저장 (parseMoney)
표시 시: 콤마 포맷 (formatMoney)
합계: totalAmount getter → footer 실시간 업데이트
```
#### 파일 업로드
```
드래그 앤 드롭 + 파일 입력
최대: 20MB
형식: pdf, doc, docx, xls, xlsx, ppt, pptx, txt, jpg, jpeg, png, gif, zip, rar
API: POST /api/admin/approvals/upload-file
진행률: XHR 업로드 이벤트
```
#### 카드/계좌 연동
```
카드 선택 → 모든 내역 행에 "결제카드" 자동 표시
계좌 선택 → 모든 내역 행에 "은행/계좌/예금주" 자동 채움
```
### 3.8 조회 화면 (_expense-show.blade.php)
| 섹션 | 내용 |
|------|------|
| 기본 정보 | 지출형식, 세금계산서, 작성일, 결재일, 부서, 이름 |
| 선택 카드/계좌 | 유색 박스로 표시 |
| 내역 테이블 | 읽기 전용, `number_format()` 금액 |
| 첨부서류 메모 | `whitespace-pre-wrap` |
| 첨부파일 목록 | 다운로드 링크 + 파일 크기 |
---
## 4. 증명서 양식 공통
### 4.1 공통 패턴
모든 증명서 양식은 동일한 패턴을 따른다:
```
사원 선택 → loadXxxInfo(userId) → API 호출 → 읽기 전용 필드 자동 채움
일부 필드만 수정 가능
미리보기 모달 (인쇄 가능)
```
### 4.2 공통 함수
| 함수 | 설명 |
|------|------|
| `loadXxxInfo(userId)` | 사원 선택 시 인적/재직 정보 로드 |
| `openXxxPreview()` | 미리보기 모달 열기 |
| `printXxxPreview()` | 미리보기 인쇄 (`window.print()`) |
| `closeXxxPreview()` | 미리보기 닫기 |
| `onXxxPurposeChange()` | 용도 선택 시 직접입력 필드 표시 |
### 4.3 조회 화면 공통
- 읽기 전용 필드 표시
- PDF 다운로드: `route('api.admin.approvals.cert-pdf', $approval->id)`
---
## 5. 재직증명서 (employment_cert)
### 5.1 폼 필드
| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `cert-name` | 성명 | text | readonly | DB 자동 채움 |
| | `cert-resident` | 주민등록번호 | text | readonly | DB 자동 채움 |
| | `cert-address` | 주소 | text | editable | 직접 입력 |
| 재직사항 | `cert-company` | 회사명 | text | readonly | DB 자동 채움 |
| | `cert-business-num` | 사업자번호 | text | readonly | DB 자동 채움 |
| | `cert-department` | 근무부서 | text | readonly | DB 자동 채움 |
| | `cert-position` | 직급 | text | readonly | DB 자동 채움 |
| | `cert-hire-date` | 재직기간 | text | readonly | DB 자동 채움 |
| 발급정보 | `cert-purpose-select` | 사용용도 | select | editable | 드롭다운 선택 |
| | (custom) | 기타 용도 | text | editable | "기타" 선택 시 표시 |
| | `cert-issue-date` | 발급일 | text | readonly | `now()->format('Y-m-d')` |
### 5.2 Content JSON
```json
{
"name": "홍길동",
"resident_number": "900101-1XXXXXX",
"address": "서울특별시 강남구",
"company_name": "(주)코드브릿지엑스",
"business_num": "123-45-67890",
"department": "개발팀",
"position": "과장",
"hire_date": "2020-03-01",
"purpose": "은행 제출용",
"issue_date": "2026-03-06"
}
```
---
## 6. 경력증명서 (career_cert)
### 6.1 폼 필드 (재직증명서 대비 추가/변경)
| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `cc-birth-date` | 생년월일 | text | readonly | DB 자동 채움 |
| 경력사항 | `cc-ceo-name` | 대표자 | text | readonly | DB 자동 채움 |
| | `cc-phone` | 대표전화 | text | readonly | DB 자동 채움 |
| | `cc-company-address` | 소재지 | text | readonly | DB 자동 채움 |
| | `cc-department` | 소속부서 | text | readonly | DB 자동 채움 |
| | `cc-position` | 직위/직급 | text | readonly | DB 자동 채움 |
| | `cc-hire-date` | 근무기간 시작 | text | readonly | DB 자동 채움 |
| | `cc-resign-date` | 근무기간 종료 | date | editable | 직접 입력 |
| | `cc-job-description` | 담당업무 | text | editable | 직접 입력 |
| 발급정보 | 용도 | select | editable | + "이직 제출용" 옵션 |
### 6.2 Content JSON
```json
{
"name": "홍길동",
"birth_date": "1990-01-01",
"address": "서울특별시 강남구",
"company_name": "(주)코드브릿지엑스",
"business_num": "123-45-67890",
"ceo_name": "김대표",
"phone": "02-1234-5678",
"company_address": "서울특별시 강남구 테헤란로",
"department": "개발팀",
"position": "과장",
"hire_date": "2020-03-01",
"resign_date": "2026-02-28",
"job_description": "웹 애플리케이션 개발",
"purpose": "이직 제출용",
"issue_date": "2026-03-06"
}
```
---
## 7. 위촉증명서 (appointment_cert)
### 7.1 폼 필드
| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 설명 |
|------|---------|------|------|------|------|
| 인적사항 | `ac-name` | 성명 | text | readonly | DB 자동 채움 |
| | `ac-resident` | 주민등록번호 | text | readonly | DB 자동 채움 |
| | `ac-department` | 소속 | text | readonly | DB 자동 채움 |
| | `ac-phone` | 연락처 | text | editable | 직접 입력 |
| 위촉정보 | `ac-hire-date` | 위촉기간 시작 | text | readonly | DB 자동 채움 |
| | `ac-resign-date` | 위촉기간 종료 | date | editable | 직접 입력 |
| | `ac-contract-type` | 계약자격 | text | editable | 직접 입력 |
| 발급정보 | `ac-purpose-select` | 용도 | select | editable | 드롭다운 선택 |
| | `ac-issue-date` | 발급일 | text | readonly | 자동 설정 |
| (숨김) | `ac-company-name` | 회사명 | hidden | - | 미리보기용 |
| | `ac-ceo-name` | 대표자명 | hidden | - | 미리보기용 |
### 7.2 Content JSON
```json
{
"name": "홍길동",
"resident_number": "900101-1XXXXXX",
"department": "기술자문팀",
"phone": "010-1234-5678",
"hire_date": "2024-01-01",
"resign_date": "2026-12-31",
"contract_type": "기술자문위원",
"purpose": "관공서 제출용",
"issue_date": "2026-03-06"
}
```
---
## 8. 사직서 (resignation)
### 8.1 폼 필드
| 섹션 | 필드 ID | 라벨 | 타입 | 수정 | 필수 |
|------|---------|------|------|------|------|
| 인적사항 | `rg-department` | 소속 | text | readonly | - |
| | `rg-position` | 직위 | text | readonly | - |
| | `rg-name` | 성명 | text | readonly | - |
| | `rg-resident` | 주민등록번호 | text | readonly | - |
| | `rg-hire-date` | 입사일 | text | readonly | - |
| | `rg-resign-date` | 퇴사(예정)일 | date | editable | 필수 |
| | `rg-address` | 주소 | text | editable | - |
| 사직사유 | `rg-reason-select` | 사유 | select | editable | 필수 |
| | (custom) | 기타 사유 | text | editable | - |
| 제출일 | `rg-issue-date` | 제출일 | text | readonly | - |
### 8.2 사직사유 옵션
| 옵션 |
|------|
| 일신상의 사유 |
| 가사 사정 |
| 건강상의 이유 |
| 진학/학업 |
| 이직 |
| 기타 (직접입력) |
### 8.3 Content JSON
```json
{
"department": "개발팀",
"position": "대리",
"name": "홍길동",
"resident_number": "900101-1XXXXXX",
"hire_date": "2020-03-01",
"resign_date": "2026-04-01",
"address": "서울특별시 강남구",
"reason": "이직",
"issue_date": "2026-03-06"
}
```
---
## 9. 품의서 5종 공통 (_purchase-request-form/show)
### 9.1 통합 Alpine.js 컴포넌트
품의서 5종은 **단일 Blade 파일**(`_purchase-request-form.blade.php`)에서 `prType` 프로퍼티로 동적 전환된다.
```javascript
x-data="purchaseRequestForm(initialData, authUserName, initialFiles)"
```
#### 타입 전환 메커니즘
```
create.blade.php → switchFormMode()
code.startsWith('pr_') 감지
#purchase-request-form-container display: block
setTimeout(50ms) → _x_dataStack[0].setPrType(code)
Alpine.js x-if 분기 → 해당 폼 렌더링
```
#### prType 코드 및 라벨
| prType | 라벨 | 색상 |
|--------|------|------|
| `pr_expense` | 지출품의서 | `bg-orange-50 text-orange-700` |
| `pr_contract` | 계약체결품의서 | `bg-purple-50 text-purple-700` |
| `pr_purchase` | 구매품의서 | `bg-blue-50 text-blue-700` |
| `pr_trip` | 출장품의서 | `bg-green-50 text-green-700` |
| `pr_settlement` | 비용정산품의서 | `bg-teal-50 text-teal-700` |
### 9.2 공통 필드 (모든 품의서)
| 필드 | 라벨 | 타입 | 기본값 |
|------|------|------|--------|
| `write_date` | 작성일자 | date | 오늘 |
| `department` | 요청부서 | text | - |
| `writer_name` | 요청자 | text | 인증 사용자명 |
| `attachment_memo` | 첨부서류 메모 | textarea | - |
| `files` | 파일 업로드 | file[] | - |
### 9.3 공통 함수
| 함수 | 설명 |
|------|------|
| `setPrType(type)` | 외부에서 prType 설정 (switchFormMode에서 호출) |
| `getFormData()` | prType별 다른 JSON 구조 반환 (base에 `pr_type` 포함) |
| `addItem()` | 내역 행 추가 |
| `removeItem(index)` | 내역 행 삭제 |
| `formatMoney(val)` | 숫자 → 콤마 포맷 |
| `parseMoney(str)` | 콤마 문자열 → 정수 |
| `prVendorSearch(target, fieldName)` | 범용 거래처 Autocomplete 검색 |
### 9.4 조회 화면 분기 (show.blade.php)
```php
// show.blade.php에서 pr_ prefix로 분기
@if(str_starts_with($approval->form?->code ?? '', 'pr_'))
@include('approvals.partials._purchase-request-show', ['content' => $content])
@endif
```
`_purchase-request-show.blade.php`에서 `$content['pr_type']`으로 5종 분기 렌더링.
---
## 10. 지출품의서 (pr_expense)
### 10.1 추가 필드
| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `expense_category` | 지출항목 | text | 선택 |
| `usage_date` | 사용일자 | date | 선택 |
| `purpose` | 사용목적 | textarea | 필수 |
### 10.2 내역 테이블
| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `description` | 항목 | text |
| `amount` | 금액 | number (콤마 포맷) |
| `remark` | 비고 | text |
### 10.3 Content JSON
```json
{
"pr_type": "pr_expense",
"write_date": "2026-03-06",
"department": "개발팀",
"writer_name": "홍길동",
"expense_category": "사무용품",
"usage_date": "2026-03-05",
"purpose": "업무용 모니터 구매",
"items": [
{ "description": "27인치 모니터", "amount": 350000, "remark": "LG전자" }
],
"total_amount": 350000,
"attachment_memo": "견적서 첨부"
}
```
---
## 11. 계약체결품의서 (pr_contract)
### 11.1 추가 필드
| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `contract_party` | 계약상대방 | text + Autocomplete | 필수 |
| `contract_party_biz_no` | 사업자번호 | text (자동) | - |
| `contract_content` | 계약내용 | textarea | 필수 |
| `contract_period_start` | 계약기간 시작 | date | 선택 |
| `contract_period_end` | 계약기간 종료 | date | 선택 |
| `contract_amount` | 계약금액 | number (콤마) | 필수 |
| `contract_conditions` | 주요조건 | textarea | 선택 |
### 11.2 Content JSON
```json
{
"pr_type": "pr_contract",
"write_date": "2026-03-06",
"department": "경영지원팀",
"writer_name": "홍길동",
"contract_party": "(주)에이비씨",
"contract_party_biz_no": "123-45-67890",
"contract_content": "연간 IT 유지보수 계약",
"contract_period_start": "2026-04-01",
"contract_period_end": "2027-03-31",
"contract_amount": 12000000,
"contract_conditions": "월 1회 정기점검, 장애 발생 시 4시간 내 대응",
"attachment_memo": "계약서 초안 첨부"
}
```
### 11.3 특수 로직
- **거래처 검색**: `prVendorSearch(formData, 'contract_party')` — 계약상대방 필드에 Autocomplete 적용
- 선택 시 `contract_party_biz_no` 자동 채움
---
## 12. 구매품의서 (pr_purchase)
### 12.1 추가 필드
| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `vendor` | 납품업체 | text + Autocomplete | 선택 |
| `vendor_biz_no` | 사업자번호 | text (자동) | - |
| `delivery_date` | 납품예정일 | date | 선택 |
| `delivery_location` | 납품장소 | text | 선택 |
### 12.2 내역 테이블
| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `name` | 품목 | text |
| `spec` | 규격 | text |
| `quantity` | 수량 | number |
| `unit_price` | 단가 | number (콤마) |
| `amount` | 금액 | number (자동: 수량×단가) |
| `remark` | 비고 | text |
### 12.3 Content JSON
```json
{
"pr_type": "pr_purchase",
"write_date": "2026-03-06",
"department": "생산팀",
"writer_name": "홍길동",
"vendor": "(주)공급사",
"vendor_biz_no": "987-65-43210",
"delivery_date": "2026-03-20",
"delivery_location": "본사 1층 창고",
"items": [
{ "name": "A4용지", "spec": "80g 500매", "quantity": 10, "unit_price": 25000, "amount": 250000, "remark": "" }
],
"total_amount": 250000,
"attachment_memo": ""
}
```
### 12.4 특수 로직
- **금액 자동 계산**: `quantity × unit_price → amount` (x-effect 반응)
- **거래처 검색**: `prVendorSearch(formData, 'vendor')` — 납품업체 필드에 Autocomplete 적용
---
## 13. 출장품의서 (pr_trip)
### 13.1 추가 필드
| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `destination` | 출장지 | text | 필수 |
| `trip_period_start` | 출장기간 시작 | date | 필수 |
| `trip_period_end` | 출장기간 종료 | date | 필수 |
| `trip_purpose` | 출장목적 | textarea | 필수 |
### 13.2 일정표 (items)
| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `date` | 일자 | date |
| `schedule` | 일정 | text |
| `remark` | 비고 | text |
### 13.3 경비 내역 (expenses)
| 필드 | 라벨 | 타입 |
|------|------|------|
| `transport` | 교통비 | number (콤마) |
| `accommodation` | 숙박비 | number (콤마) |
| `meals` | 식비 | number (콤마) |
| `others` | 기타 | number (콤마) |
| (자동) | 합계 | number (합산) |
### 13.4 Content JSON
```json
{
"pr_type": "pr_trip",
"write_date": "2026-03-06",
"department": "영업팀",
"writer_name": "홍길동",
"destination": "부산 해운대",
"trip_period_start": "2026-03-10",
"trip_period_end": "2026-03-11",
"trip_purpose": "거래처 방문 및 현장 점검",
"items": [
{ "date": "2026-03-10", "schedule": "거래처 미팅", "remark": "오전 10시" },
{ "date": "2026-03-11", "schedule": "현장 점검 및 복귀", "remark": "" }
],
"expenses": {
"transport": 120000,
"accommodation": 80000,
"meals": 40000,
"others": 0
},
"total_amount": 240000,
"attachment_memo": ""
}
```
### 13.5 조회 화면 특수 구조
- **일정표**: 테이블 형태로 일자/일정/비고 렌더링
- **경비 카드**: 교통비/숙박비/식비/기타 4개 항목 + 합계를 카드 그리드로 표시
---
## 14. 비용정산품의서 (pr_settlement)
### 14.1 추가 필드
| 필드 | 라벨 | 타입 | 필수 |
|------|------|------|------|
| `settlement_period_start` | 정산기간 시작 | date | 선택 |
| `settlement_period_end` | 정산기간 종료 | date | 선택 |
| `payment_method` | 지급방법 | radio | 필수 |
### 14.2 지급방법 옵션
| 값 | 라벨 |
|----|------|
| `corporate_card` | 법인카드 사용 |
| `personal_advance` | 개인 선지출 (환급 요청) |
### 14.3 내역 테이블
| 컬럼 | 라벨 | 타입 |
|------|------|------|
| `date` | 사용일자 | date |
| `description` | 항목 | text |
| `amount` | 금액 | number (콤마) |
| `remark` | 비고 | text |
### 14.4 Content JSON
```json
{
"pr_type": "pr_settlement",
"write_date": "2026-03-06",
"department": "개발팀",
"writer_name": "홍길동",
"settlement_period_start": "2026-02-01",
"settlement_period_end": "2026-02-28",
"payment_method": "personal_advance",
"items": [
{ "date": "2026-02-15", "description": "택시비", "amount": 25000, "remark": "야근 귀가" },
{ "date": "2026-02-20", "description": "회의 다과", "amount": 15000, "remark": "팀 미팅" }
],
"total_amount": 40000,
"attachment_memo": "영수증 첨부"
}
```
### 14.5 조회 화면 특수 구조
- **지급방법 표시**: `corporate_card` → "법인카드 사용", `personal_advance` → "개인 선지출 (환급 요청)"
- 해당 라벨을 뱃지 형태로 표시
---
## 15. 결재 도장 테이블 (_approval-stamp-table.blade.php)
### 15.1 구조
전통 한글 결재 양식의 도장 테이블을 구현한다.
```
┌──────┬────────┬────────┬────────┐
│ │ 과장 │ 부장 │ 이사 │ ← 1행: 직급 헤더
│ 결재 ├────────┼────────┼────────┤
│ │ [승인] │ [대기] │ [대기] │ ← 2행: 서명/도장 영역
│ ├────────┼────────┼────────┤
│ │ 김과장 │ 박부장 │ 이이사 │ ← 3행: 이름 + 처리일
│ │ 03/06 │ │ │
└──────┴────────┴────────┴────────┘
```
### 15.2 상태별 표시
| 상태 | approval_type | 표시 | 색상 |
|------|---------------|------|------|
| 승인 | `normal` | 빨간 원형 "승인" | `bg-red-500` |
| 전결 | `pre_decided` | 파란 원형 "전결" | `bg-blue-500` |
| 반려 | - | 빨간 원형 "반려" | `bg-red-500` |
| 보류 | - | 주황 원형 "보류" | `bg-amber-500` |
| 건너뜀 | - | 회색 "-" | `bg-gray-300` |
---
## 16. 결재선 편집기 (_approval-line-editor.blade.php)
### 16.1 2패널 구조
```
┌─────────────────────┬─────────────────────┐
│ 인원 목록 │ 결재선 │
│ │ │
│ [검색 input] │ [템플릿 선택 ▼] │
│ │ │
│ ▼ 개발팀 │ ① 김과장 (결재) [✗] │
│ 홍길동 과장 [+] │ ② 박부장 (합의) [✗] │
│ 김영희 대리 [+] │ ③ 이대리 (참조) [✗] │
│ │ │
│ ▼ 경영지원팀 │ (드래그로 순서 변경) │
│ 박부장 부장 [+] │ │
│ │ │
├─────────────────────┴─────────────────────┤
│ 결재: 1명 합의: 1명 참조: 1명 합계: 3명 │
└───────────────────────────────────────────┘
```
### 16.2 기능
| 기능 | 설명 |
|------|------|
| **인원 검색** | 이름/부서 실시간 검색 |
| **부서별 접기** | 부서 헤더 클릭으로 인원 접기/펼치기 |
| **드래그 정렬** | SortableJS로 결재선 순서 변경 |
| **유형 선택** | 각 단계별 approval/agreement/reference 선택 |
| **템플릿 로드** | 저장된 결재선 템플릿 드롭다운 |
### 16.3 데이터 소스
```
API: /api/admin/tenant-users/list
응답:
[
{
"department_id": 1,
"department_name": "개발팀",
"users": [
{ "id": 10, "name": "홍길동", "position": "과장", "job_title": "팀장" }
]
}
]
```
### 16.4 Hidden Inputs (form 전송)
```html
<input type="hidden" name="steps[0][user_id]" value="10">
<input type="hidden" name="steps[0][step_type]" value="approval">
<input type="hidden" name="steps[1][user_id]" value="20">
<input type="hidden" name="steps[1][step_type]" value="agreement">
```
---
## 17. ApprovalForm 모델
### 17.1 테이블 스키마
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | BIGINT PK | |
| `tenant_id` | BIGINT FK | 테넌트 격리 |
| `name` | VARCHAR | 양식명 (예: "휴가신청서") |
| `code` | VARCHAR UNIQUE | 양식 코드 (예: `leave`) |
| `category` | ENUM | `request`, `expense`, `certificate`, `expense_estimate` |
| `template` | JSON | 필드 정의 메타데이터 |
| `body_template` | LONGTEXT NULL | HTML 본문 템플릿 |
| `is_active` | BOOLEAN | 활성 여부 |
### 17.2 카테고리
#### DB 카테고리 (ApprovalForm.category)
| 카테고리 | 설명 | 양식 코드 |
|---------|------|----------|
| `request` | 신청서 | `leave`, `attendance_request`, `reason_report` |
| `expense` | 지출결의서 | `expense` |
| `certificate` | 증명서/서류 | `employment_cert`, `career_cert`, `appointment_cert`, `resignation` |
| `expense_estimate` | 품의서 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` |
#### UI 분류 (formCategoryMap — 2단계 선택용)
| UI 분류 | 양식 코드 |
|---------|----------|
| 일반 | `BUSINESS_DRAFT` |
| 인사/근태 | `leave`, `attendance_request`, `reason_report`, `resignation` |
| 증명서 | `employment_cert`, `career_cert`, `appointment_cert` |
| 품의 | `pr_expense`, `pr_contract`, `pr_purchase`, `pr_trip`, `pr_settlement` |
| 재무 | `expense` |
> **참고**: DB 카테고리와 UI 분류는 별도 매핑이다. DB는 `approval_forms.category` ENUM이고, UI 분류는 JavaScript `formCategoryMap` 객체로 정의된다.
---
## 18. 양식별 저장/조회 흐름
### 18.1 저장 (create/update)
```
사용자 입력
getFormData() (JavaScript)
POST /api/admin/approvals
body: { form_id, title, content: {...}, body, steps: [...] }
ApprovalService::createApproval()
Approval.content = JSON encode → DB 저장
```
### 18.2 조회 (show)
```
GET /approval-mgmt/{id}
ApprovalController::show()
Blade: show.blade.php
양식 코드별 분기:
leave → (본문에 인라인 표시)
expense → @include('_expense-show')
pr_* → @include('_purchase-request-show') ← str_starts_with 매칭
employment_cert → @include('_certificate-show')
career_cert → @include('_career-cert-show')
appointment_cert → @include('_appointment-cert-show')
resignation → @include('_resignation-show')
```
> **품의서 분기**: `str_starts_with($approval->form?->code ?? '', 'pr_')` 조건으로 5종 모두 단일 include로 처리. `_purchase-request-show.blade.php` 내부에서 `$content['pr_type']`으로 세부 분기.
---
## 관련 문서
- [README.md](README.md) — 결재관리 시스템 전체 개요
- [워크플로우 상세](workflows.md) — 승인/반려/회수/보류/전결 흐름
- [API 명세](api-reference.md) — 엔드포인트별 요청/응답
- [UI 화면 구성](ui-screens.md) — 화면별 UI 요소 및 동작
---
**최종 업데이트**: 2026-03-06

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
# 바로빌 카카오톡 (알림톡/친구톡) 연동
> **문서 버전**: 1.0
> **문서 버전**: 1.1
> **작성일**: 2026-02-14
> **최종 수정**: 2026-02-24
> **상태**: 운영 중 (전자계약 알림톡 발송 완료)
> **최종 수정**: 2026-02-27
> **상태**: 운영 중 (알림톡 + SMS + 환경별 분기 완료)
> **대상 프로젝트**: MNG
---
@@ -25,7 +25,11 @@
| 채널 연동 (바로빌↔카카오) | **완료** (2026-02-20) | 바로빌 관리 URL에서 채널 연동 처리 |
| 바로빌 파트너 과금 설정 | **완료** (2026-02-23) | 바로빌 측에서 파트너사 과금 설정 완료 |
| 알림톡 템플릿 v1 검수 | **완료** (2026-02-22) | `전자계약_서명요청`, `전자계약_리마인드` 2종 승인 |
| 알림톡 템플릿 v2 검수 | **심사 중** (2026-02-24 접수) | 버튼 URL에 `#{토큰}` 변수 포함 2재등록 |
| 알림톡 템플릿 v2 검수 | **완료** (2026-02-25) | 버튼 URL에 `#{토큰}` 변수 포함 3승인 |
| 알림톡 `전자계약_완료` | **완료** (2026-02-26) | 서명 완료 알림 발송용 템플릿 승인 |
| 역할 기반 알림 분기 | **완료** (2026-02-26) | 본사=이메일, 상대방=알림톡/SMS |
| 환경별 템플릿 분기 | **완료** (2026-02-27) | `_DEV` 접미사 개발 템플릿 등록 |
| DEV 템플릿 검수 | **심사 중** (2026-02-27 접수) | 개발서버용 3종 (`admin.codebridge-x.com`) |
> 상세 등록 가이드: [카카오톡 알림톡 채널 및 템플릿 등록 가이드](../../guides/카카오톡-알림톡-채널-템플릿-등록.md)
@@ -310,14 +314,24 @@ $params = [
| 전달 결과 확인 (2단계) | **구현 완료** | 2026-02-24 |
| 로그인 페이지 서명 확인 | **성공** | 2026-02-24 |
### 5.3 대기 중인 항목
### 5.3 완료된 추가 항목 (2026-02-26~27)
| 항목 | 상태 | 비고 |
|------|------|------|
| 템플릿 v2 승인 | **심사 중** | 버튼 URL에 `#{토큰}` 변수 포함 |
| v2 승인 후 코드 수정 | **대기** | 동적 서명 URL을 버튼에 전달 |
| `전자계약_완료` 템플릿 | **미등록** | 서명 완료 알림 발송용 |
| SMS 대체발송 | **미설정** | 발신번호 등록 필요 |
| 템플릿 v2 승인 | **완료** | 버튼 URL에 `#{토큰}` 변수 포함 3종 승인 |
| `전자계약_완료` 템플릿 | **완료** | 서명 완료 알림 발송 — PDF 다운로드 버튼 |
| 역할 기반 알림 분기 | **완료** | 본사(creator)=이메일, 상대방(counterpart)=알림톡 |
| OTP SMS 발송 | **완료** | 상대방에게 SMS로 인증코드 발송 |
| 환경별 템플릿 분기 | **완료** | `resolveTemplateName()``_DEV` 접미사 자동 적용 |
| 서명 PDF 재생성 | **완료** | `downloadDocument()`에서 완료 계약 PDF 자동 재생성 |
> 상세 가이드: [전자계약 알림톡/SMS 환경별 설정 가이드](./esign-notification-guide.md)
### 5.4 대기 중인 항목
| 항목 | 상태 | 비고 |
|------|------|------|
| DEV 템플릿 검수 | **심사 중** | `admin.codebridge-x.com` 도메인 3종 |
| 친구톡 발송 | **대기** | 채널 친구 추가 후 가능 |
| 대량 발송 | **대기** | 단건 안정화 후 |
@@ -377,37 +391,7 @@ $buttons = [
---
## 8. API 측 바로빌 연동 (세금계산서)
MNG의 카카오톡 연동 외에, API(`api/`)에서도 바로빌 서비스를 사용한다:
### 8.1 바로빌 설정 API
| HTTP | URI | 설명 |
|------|-----|------|
| GET | `/v1/barobill-settings` | 바로빌 설정 조회 |
| PUT | `/v1/barobill-settings` | 바로빌 설정 저장 (사업자번호, 인증키, 자동발행 등) |
| POST | `/v1/barobill-settings/test-connection` | 연동 테스트 |
### 8.2 세금계산서 발행
`BarobillService`는 세금계산서 발행/취소/국세청 전송 상태 조회도 담당한다:
| API 메서드 | 설명 |
|-----------|------|
| `issueTaxInvoice()` | 세금계산서 발행 (RegistAndIssueTaxInvoice) |
| `cancelTaxInvoice()` | 세금계산서 취소 |
| `checkNtsSendStatus()` | 국세청 전송 상태 조회 |
| `checkBusinessNumber()` | 사업자번호 휴폐업 조회 |
| `testConnection()` | GetAccessToken으로 연동 테스트 |
**인증키 보안:** `cert_key`는 Laravel `Crypt` 파사드로 자동 암/복호화
→ 상세: [세금계산서 관리](../finance/tax-invoices.md)
---
## 9. 참고 자료
## 8. 참고 자료
- [바로빌 API 문서](https://dev.barobill.co.kr)
- [카카오비즈니스 채널 관리](https://business.kakao.com)
@@ -420,6 +404,7 @@ MNG의 카카오톡 연동 외에, API(`api/`)에서도 바로빌 서비스를
| 날짜 | 버전 | 변경 내용 |
|------|------|----------|
| 2026-02-27 | 1.1 | 역할 기반 알림, OTP SMS, 환경별 템플릿 분기, 완료 알림톡 추가 |
| 2026-02-24 | 1.0 | 전자계약 알림톡 연동 완료, 트러블슈팅 문서화, v2 템플릿 가이드 추가 |
| 2026-02-14 | 0.2 | 전자계약(E-Sign) 알림톡 연동 활용 계획 추가 |
| 2026-02-14 | 0.1 | 초안 작성 - 코드 구현 완료, 실 서비스 연동 대기 |

View File

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

View File

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

View File

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

View File

@@ -111,10 +111,12 @@ DRAFT → PENDING → APPROVED
## 관련 문서
- [MNG 문서관리 시스템 상세](mng-document-system.md) — MNG 화면 구성, 탭별 기능, 서식 빌더, EAV 저장 패턴 상세
- [MNG 문서양식관리](mng-document-template.md) — 서식 생성/편집, Legacy/Block Builder, 프리셋, 연결품목 관리
- [DB 스키마 — 문서/전자서명](../../system/database/documents.md)
- [게시판 시스템](../boards/README.md) — 유사한 EAV 패턴 적용
- Swagger: `/api-docs` → Documents 섹션
---
**최종 업데이트**: 2026-02-27
**최종 업데이트**: 2026-03-06

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,362 @@
# 품질관리 시스템
> **작성일**: 2026-03-09
> **상태**: 운영 중
---
## 1. 개요
### 1.1 목적
SAM 품질관리 시스템은 **방화문 제품검사 → 실적신고 → 건기원 제출**의 전체 흐름을 자동화한다.
건축자재 품질관리 법규(건설기술진흥법)에 따른 제품검사 수행 및 분기별 실적신고를 관리한다.
### 1.2 역할별 프로세스 플로우
> 스토리보드 슬라이드 3 기준
```
매출거래처 견적/수주 담당자 생산 담당자 출고 담당자 품질 담당자
─────────── ────────────── ────────── ────────── ──────────
주문 견적 작성 생산 부서 할당 출고 대기 제품검사 신청
(전화/카톡/메일) │ │ │ (거래처 요청)
│ 수주 전환?──No──→ 재고 소요량 출고 완료 │
│ │ Yes 충분? 검사원 일정 관리
주문 확인 및 수주서 작성 │ │
내역 확정 (견적서 선택) 원자재 투입 체크 검사원 현장 방문
│ │ 및 검사 진행
생산지시 생성 공정 작업 진행 │
│ │ 합격? ──No──→
재고 소요량 중간검사 │ Yes
충분?──No──→ │ 실적 신고 관리
│ 생산 완료 │
(자재 담당자 품질 인증 심사
입고 등록
수입검사)
* 분할 수주/생산/출고는 1차 이후 반복 가능
```
### 1.3 핵심 흐름 (시스템)
```
품질관리서 생성 → 수주 연결 → 개소별 검사 → 검사완료
실적신고 자동생성 (분기별)
필수정보 확인 → 확정 → 건기원 신고
품질인정심사
(기준/매뉴얼 + 로트추적)
```
### 1.4 메뉴 구조
| 메뉴 | URL | 설명 | 상태 |
|------|-----|------|------|
| 제품검사관리 | `/quality/inspections` | 품질관리서 목록/생성/상세 | 운영 중 |
| 실적신고관리 | `/quality/performance-reports` | 분기별 실적신고 관리 | 운영 중 |
| 품질인정심사 | `/quality/qms` | 기준/매뉴얼 심사 + 로트 추적 심사 | 개발 예정 |
---
## 2. 제품검사 (QualityDocument)
> 상세 문서: [inspection-management.md](./inspection-management.md)
### 2.1 품질관리서 생성
- **채번**: `KD-QD-YYYYMM-0001` (자동)
- **필수 입력**: 현장명, 접수일, 검사자
- **선택 입력**: 수주처(client_id), 관련자 정보(options JSON)
### 2.2 수주 연결
- 품질관리서에 수주(Order)를 연결하면 **개소(Location)가 자동 생성**
- 개소 = 수주의 root node (층/부호 단위)
- 시공규격(post_width, post_height)은 발주규격과 다를 수 있음
### 2.3 검사 수행
각 개소에 대해 **15개 검사항목** + **제품 사진**을 입력:
| 분류 | 검사항목 | 판정 |
|------|---------|------|
| 외관 | 가공, 봉제, 조립, 차연재, 하부마감 | pass/fail |
| 기능 | 모터, 소재 | pass/fail |
| 치수 | 가로, 세로, 가이드레일 간격, 하부마감 간격 | OK/NG |
| 시험 | 내화, 차연, 개폐, 충격 | pass/fail |
### 2.4 상태 전이
```
received (접수) → in_progress (검사 중) → completed (검사 완료)
```
**개소별 상태 자동 판정**:
- `pending`: 검사 데이터 없음
- `in_progress`: 일부 항목 입력 또는 사진 미등록
- `completed`: 15개 항목 전부 + 사진 등록
---
## 3. 생산실적신고 (PerformanceReport)
> 상세 문서: [performance-reports.md](./performance-reports.md)
### 3.1 자동 생성
- **트리거**: 품질관리서 검사완료(`complete()`) 시
- **생성 기준**: 현재 연도 + 분기 (`year`, `quarter`)
- **초기 상태**: `unconfirmed`
### 3.2 확정 프로세스
```
unconfirmed (미확정)
↓ confirm() — 필수정보 검증 통과 시
confirmed (확정)
↓ distribute() — 건기원 신고 시 (미구현)
reported (신고완료)
```
**확정 해제**: `confirmed``unconfirmed` (unconfirm)
### 3.3 필수정보 검증
확정 전 4가지 섹션의 필수필드가 모두 입력되어야 함:
| 섹션 | 필수필드 |
|------|---------|
| 건축공사장 | 현장명, 대지위치, 지번 |
| 자재유통업자 | 업체명, 주소, 대표자, 전화번호 |
| 공사시공자 | 업체명, 주소, 담당자, 연락처 |
| 공사감리자 | 사무소명, 주소, 담당자, 연락처 |
### 3.4 누락체크
- 출고완료(배송 완료)된 수주 중 품질관리서가 미등록된 건 탐지
- 별도 탭에서 조회 가능
### 3.5 건기원 실적신고 비즈니스 컨텍스트
**건기원(한국건설기술연구원)** 실적신고는 법적 의무:
- **주기**: 분기별 (1~3월, 4~6월, 7~9월, 10~12월)
- **대상**: 해당 분기에 검사 완료된 모든 건
- **내용**: 품질관리서 번호, LOT 번호, 현장 정보, 검사 결과
- **제출처**: 건기원 온라인 시스템 (수기 입력 또는 엑셀 업로드)
**SAM 시스템 역할**:
1. 제품검사 완료 시 실적신고 데이터 자동 수집
2. 필수정보 누락 여부 사전 검증
3. 확정 후 건기원 제출 양식으로 데이터 정리
4. (향후) 건기원 시스템 연동 자동 배포
---
## 4. 데이터 구조
### 4.1 테이블 관계
```
quality_documents (품질관리서)
├── quality_document_orders (수주 연결, M:N)
│ └── orders
├── quality_document_locations (개소별 검사)
│ ├── order_items (대표 품목)
│ └── documents (EAV 성적서)
└── performance_reports (실적신고, 1:1)
```
### 4.2 주요 테이블
| 테이블 | 설명 | 주요 컬럼 |
|--------|------|----------|
| `quality_documents` | 품질관리서 | quality_doc_number, status, client_id, options(JSON) |
| `quality_document_orders` | 품질-수주 연결 | quality_document_id, order_id |
| `quality_document_locations` | 개소별 검사 | inspection_data(JSON), inspection_status, post_width/height |
| `performance_reports` | 실적신고 | year, quarter, confirmation_status, confirmed_date |
### 4.3 options JSON 구조 (quality_documents)
```json
{
"construction_site": {
"name": "현장명",
"land_location": "대지위치",
"lot_no": "지번"
},
"material_distributor": {
"company": "업체명",
"address": "주소",
"ceo": "대표자",
"tel": "전화번호"
},
"contractor": {
"company": "업체명",
"address": "주소",
"name": "담당자",
"phone": "연락처"
},
"supervisor": {
"office": "사무소명",
"address": "주소",
"name": "담당자",
"phone": "연락처"
}
}
```
---
## 5. API 엔드포인트
### 5.1 제품검사 (`/api/v1/quality/documents`)
| Method | Path | 설명 |
|--------|------|------|
| GET | `/quality/documents` | 목록 (상태/날짜 필터) |
| GET | `/quality/documents/stats` | 상태별 통계 |
| GET | `/quality/documents/calendar` | 캘린더 스케줄 |
| GET | `/quality/documents/available-orders` | 미등록 수주 조회 |
| POST | `/quality/documents` | 생성 |
| GET | `/quality/documents/{id}` | 상세 |
| PUT | `/quality/documents/{id}` | 수정 |
| DELETE | `/quality/documents/{id}` | 삭제 |
| PATCH | `/quality/documents/{id}/complete` | 검사완료 (→ 실적신고 자동생성) |
| POST | `/quality/documents/{id}/orders` | 수주 연결 |
| DELETE | `/quality/documents/{id}/orders/{orderId}` | 수주 해제 |
| POST | `/quality/documents/{id}/locations/{locId}/inspect` | 개소별 검사 저장 |
### 5.2 실적신고 (`/api/v1/quality/performance-reports`)
| Method | Path | 설명 |
|--------|------|------|
| GET | `/quality/performance-reports` | 목록 (연도/분기/상태 필터) |
| GET | `/quality/performance-reports/stats` | 확정/미확정 통계 |
| GET | `/quality/performance-reports/missing` | 누락체크 |
| PATCH | `/quality/performance-reports/confirm` | 일괄 확정 |
| PATCH | `/quality/performance-reports/unconfirm` | 확정 해제 |
| PATCH | `/quality/performance-reports/memo` | 메모 일괄 업데이트 |
---
## 6. 프론트엔드 구조
### 6.1 페이지
```
react/src/app/[locale]/(protected)/quality/
├── page.tsx # 대시보드
├── inspections/
│ ├── page.tsx # 검사 목록
│ ├── new/page.tsx # 검사 생성
│ └── [id]/page.tsx # 검사 상세/수정
└── performance-reports/
└── page.tsx # 실적신고 목록
```
### 6.2 컴포넌트
```
react/src/components/quality/
├── InspectionManagement/
│ ├── InspectionList.tsx # 검사 목록
│ ├── InspectionCreate.tsx # 검사 생성
│ ├── InspectionDetail.tsx # 검사 상세 (수정 포함)
│ ├── OrderSelectModal.tsx # 수주 선택 모달
│ ├── ProductInspectionInputModal.tsx # 검사 입력 모달
│ ├── actions.ts # Server Actions
│ ├── types.ts # TypeScript 타입
│ └── documents/ # 요청서/성적서 문서
└── PerformanceReportManagement/
├── PerformanceReportList.tsx # 실적신고 목록 (2탭)
├── MemoModal.tsx # 메모 모달
└── actions.ts # Server Actions
```
### 6.3 실적신고 화면 기능
**탭 1: 분기별 실적신고**
- 연도/분기 필터
- 통계: 전체, 확정, 미확정, 총 개소
- 테이블: 품질관리서번호, 작성일, 현장명, 수주처, 개소수, 필수정보 상태, 확정상태, 확정일, 메모
- 액션: 선택 확정, 확정 해제, 메모 일괄 작성
**탭 2: 누락체크**
- 출고완료 수주 중 품질관리서 미등록 건 조회
- 빠른 누락 확인으로 법규 준수 지원
---
## 7. 품질인정심사 (QMS)
> 상세 문서: [quality-certification-audit.md](./quality-certification-audit.md)
### 7.1 개요
품질 인정 심사 자료를 관리하는 기능. 두 가지 심사 영역으로 구성:
| 심사 영역 | 설명 | 진행률 추적 |
|----------|------|-----------|
| 기준/매뉴얼 심사 | 품질 기준 문서 및 매뉴얼 점검표 체크 | 완료 항목 / 전체 항목 |
| 로트 추적 심사 | 품질관리서 → 수주코드 → 개소별 제품로트 → 관련 서류 추적 확인 | 확인 개소 / 전체 개소 |
### 7.2 로트 추적 심사 구조
```
품질관리서 목록 → 수주코드 목록 → 관련 서류
(1단계 선택) (2단계 선택) (3단계 확인)
```
- 해당 분기 실적신고 확정 건 기준
- 수입검사, 중간검사, 납품확인서, 출고증, 제품검사 성적서, 품질관리서 등 서류 연결 확인
### 7.3 구현 상태
**개발 예정** — 현재 페이지 구조만 존재
---
## 8. 미구현 기능 요약
| 기능 | 상태 | 설명 |
|------|------|------|
| 배포(distribute) API | 미구현 | 건기원 시스템 연동 자동 배포 |
| 확정건 엑셀 다운로드 | 미구현 | 확정된 실적 건기원 양식 엑셀 내보내기 |
| 품질인정심사 | 미구현 | 기준/매뉴얼 심사 + 로트 추적 심사 |
---
## 9. 스토리보드 참조
> **출처**: `SAM_MES_경동기업_품질관리_Storyboard_D1.9_260224`
| 슬라이드 | 화면 | 기능 영역 |
|---------|------|----------|
| 3 | 프로젝트 진행 플로우차트 | 전체 프로세스 |
| 5~6 | 제품검사 목록 + 캘린더 | 제품검사관리 |
| 7~9 | 제품검사 상세 | 제품검사관리 |
| 10 | 수주 선택 팝업 | 제품검사관리 |
| 11 | 제품검사 팝업 (검사 입력) | 제품검사관리 |
| 12~13 | 제품검사요청서 | 문서 출력 |
| 14~15 | 제품검사성적서 | 문서 출력 |
| 16 | 실적신고 목록 | 실적신고관리 |
| 17 | 메모 팝업 | 실적신고관리 |
| 18 | 누락체크 | 실적신고관리 |
| 19 | 품질인정심사 (기준/매뉴얼) | 품질인정심사 |
| 20 | 품질인정심사 (로트 추적) | 품질인정심사 |
---
## 관련 문서
- [DB 스키마 — 생산/품질](../../system/database/production.md)
- [API 규칙](../../dev/standards/api-rules.md)
- [채번 규칙](../../rules/numbering-rules.md)
---
**최종 업데이트**: 2026-03-09

View File

@@ -0,0 +1,317 @@
# 제품검사 관리 (Inspection Management)
> **작성일**: 2026-03-09
> **상태**: 운영 중
> **URL**: `/quality/inspections`
---
## 1. 개요
### 1.1 목적
방화문 제품의 현장 출고 전 품질검사를 수행하고 검사성적서를 발행한다.
검사 완료 시 실적신고(PerformanceReport)를 자동 생성한다.
### 1.2 품질관리서 구조
```
품질관리서 (QualityDocument)
├── 기본정보: 채번, 현장명, 접수일, 검사자, 수주처
├── 관련자 정보 (options JSON):
│ ├── 건축공사장 정보
│ ├── 자재유통업자 정보
│ ├── 공사시공자 정보
│ └── 공사감리자 정보
├── 수주 연결 (QualityDocumentOrder, 1:N)
│ └── 개소 (QualityDocumentLocation, 1:N)
│ ├── 시공규격 (post_width, post_height)
│ ├── 검사 데이터 (inspection_data JSON)
│ └── 검사성적서 (Document EAV)
└── 실적신고 (PerformanceReport, 1:1)
```
### 1.3 화면 구성
**목록 페이지** (슬라이드 5~6):
- 상단: 날짜 필터 (전체/전전월/전월/금월/어제/오늘) + 검색
- 통계 카드: 접수, 진행중, 완료 건수
- 테이블: 품질관리서번호, 번호, 수주처, 개소, 실적신고 필수정보, 검사시기, 검사자, 상태, 작성자, 접수일
- 하단: **캘린더 스케줄** (월간 뷰)
**캘린더 뷰**:
- 월 단위 표시, 상태 필터(전체/진행중/완료)
- 색상 구분: 완료(초록 배지), 진행중(파란 바)
- 검사 완료 건: `홍길동 - 현장명 / 완료` 형태
- 검사 진행 건: `홍길동 - 현장명 / 진행중` 형태 (날짜 범위 바)
- 클릭 시 해당 제품검사 상세 화면으로 이동
**상세 페이지** (슬라이드 7~9):
- 기본 정보: 품질관리서번호, 현장명, 수주처, 접수일, 담당자, 연락처, 상태, 작성자
- 관련자 정보 4개 섹션 (실적 신고 시 필수 정보)
- 검사 정보: 검사방문요청일, 검사시작일, 검사종료일, 검사자
- 현장 주소: 우편번호 찾기 + 상세주소
- 수주 설정 정보: 수주 선택 버튼 → 수주별 개소 목록 (층수/부호/수주규격/시공규격/변경사유)
- 하단 버튼: 검사제품요청서 보기, 제품검사성적서 보기, **검사 완료**, 수정
---
## 2. 상태 관리
### 2.1 품질관리서 상태
| 상태 | 코드 | 조건 |
|------|------|------|
| 접수 | `received` | 생성 직후, 수주 미연결 |
| 진행중 | `in_progress` | 수주 연결됨 또는 일부 검사 진행 |
| 완료 | `completed` | 모든 개소 검사 완료 후 `complete()` 호출 |
### 2.2 개소별 검사 상태 (자동 판정)
| 상태 | 코드 | 판정 기준 |
|------|------|----------|
| 대기 | `pending` | 검사 데이터 없음 (15개 항목 0개 + 사진 없음) |
| 진행중 | `in_progress` | 일부 항목 입력 또는 사진 미등록 |
| 완료 | `completed` | 15개 항목 전부 입력 + 사진 1장 이상 |
### 2.3 상태 자동 재계산
개소별 검사 저장 시 → 개소 상태 자동 판정 → 품질관리서 상태 재계산:
- 전부 `pending``received`
- 하나라도 `completed` 또는 `in_progress``in_progress`
- 전부 `completed``in_progress` (수동 `complete()` 필요)
---
## 3. 검사 항목
### 3.1 15개 검사 항목
| # | 키 | 분류 | 설명 | 판정값 |
|---|-----|------|------|--------|
| 1 | `appearanceProcessing` | 외관 | 가공 상태 | pass/fail |
| 2 | `appearanceSewing` | 외관 | 봉제 상태 | pass/fail |
| 3 | `appearanceAssembly` | 외관 | 조립 상태 | pass/fail |
| 4 | `appearanceSmokeBarrier` | 외관 | 차연재 상태 | pass/fail |
| 5 | `appearanceBottomFinish` | 외관 | 하부마감 상태 | pass/fail |
| 6 | `motor` | 기능 | 모터 작동 | pass/fail |
| 7 | `material` | 기능 | 소재 적합성 | pass/fail |
| 8 | `lengthJudgment` | 치수 | 가로 치수 | OK/NG |
| 9 | `heightJudgment` | 치수 | 세로 치수 | OK/NG |
| 10 | `guideRailGap` | 치수 | 가이드레일 간격 | OK/NG |
| 11 | `bottomFinishGap` | 치수 | 하부마감 간격 | OK/NG |
| 12 | `fireResistanceTest` | 시험 | 내화 시험 | pass/fail |
| 13 | `smokeLeakageTest` | 시험 | 차연 시험 | pass/fail |
| 14 | `openCloseTest` | 시험 | 개폐 시험 | pass/fail |
| 15 | `impactTest` | 시험 | 충격 시험 | pass/fail |
### 3.2 추가 데이터
| 키 | 설명 | 필수 |
|----|------|------|
| `productImages` | 제품 사진 URL 배열 | 완료 판정에 필수 |
---
## 4. 수주 연결
### 4.1 수주 선택
- `availableOrders()`: 해당 수주처(client_id)의 미등록 수주 조회
- 모달에서 복수 수주 선택 가능
### 4.2 개소 자동생성
수주 연결 시 각 수주의 root node(층/부호)마다 개소(Location) 자동생성:
```
수주 A (3개 root node)
├── 1F A호 → Location 1
├── 2F B호 → Location 2
└── 3F C호 → Location 3
수주 B (2개 root node)
├── 지하1F → Location 4
└── 1F → Location 5
```
### 4.3 개소 데이터
| 필드 | 설명 | 출처 |
|------|------|------|
| `order_item_id` | 대표 OrderItem | root node의 첫 번째 품목 |
| `post_width` | 시공 가로 | 발주 규격에서 복사 (수정 가능) |
| `post_height` | 시공 세로 | 발주 규격에서 복사 (수정 가능) |
| `change_reason` | 변경 사유 | 규격 변경 시 입력 |
---
## 5. 문서 자동생성 (EAV)
### 5.1 제품검사요청서 (슬라이드 12~13)
- **Template ID**: 66 (제품검사 요청서)
- **트리거**: 품질관리서 생성/수정 시 `syncRequestDocument()` 호출
- **인쇄용 페이지 형태로 구분**되어 표시 (인쇄, 공유, 닫기 버튼)
**문서 구성**:
```
┌─────────────────────────────────────────┐
│ 제품검사요청서 │
│ 문서번호: ABC123 | 작성일자: 2025.11.11 │
│ │
│ 승인라인: 작성 → 승인 → 승인 → 승인 │
│ 홍길동 이름 이름 이름 │
│ │
│ ── 기본정보 ── │
│ 수주처, 수주번호, 담당자, 연락처 │
│ 현장명, 납품일, 총 개소, 접수일 │
│ │
│ ── 입력사항 (실적신고 필수 정보) ── │
│ 건축공사장: 현장명, 대지위치, 지번 │
│ 자재유통업자: 회사명, 회사주소, 대표자명, │
│ 전화번호 │
│ 공사시공자: 회사명, 회사주소, 성명, 전화번호 │
│ 공사감리자: 사무소명, 사무소주소, 성명, │
│ 전화번호 │
│ │
│ ── 검사대상 사전 고지 정보 ── │
│ No. 층수 부호 발주규격(가로/세로) │
│ 시공후규격(가로/세로) 변경사유 │
└─────────────────────────────────────────┘
```
**주의 문구** (빨간색):
- 발주 사이즈와 시공 완료된 사이즈가 다를 시 **실질 범위를 넣어야 한다**
- 변경사유를 고지하여야 인정마을을 부착할 수 있다
- 사전고지를 하지 않음으로 발생하는 문제의 귀책은 신청업체에 있다
### 5.2 제품검사성적서 (슬라이드 14~15)
- 각 개소별 `document_id`로 EAV Document 참조
- 검사 결과(inspection_data)를 EAV 필드로 저장
- **개소별 페이지 단위**: 1/50 형태의 페이지 네비게이션 (이전/이동/다음 버튼)
**문서 구성**:
```
┌─────────────────────────────────────────┐
│ 제품검사성적서 │
│ 문서번호: ABC123 | 작성일자: 2025.11.11 │
│ │
│ 제품명, 제품 LOT NO, 로트크기 │
│ 제품코드, 검사일자 │
│ 수주처, 검사자 │
│ 현장명 │
│ │
│ ── 제품 사진 ── │
│ [IMG] [IMG] │
│ │
│ ── 검사 항목 ── │
│ No. 검사항목 검사기준 검사 특정값 판정│
│ 1 외모양 │
│ 가공상태 사용상 해로운 결함이 없을 것 │
│ 재봉상태 내화심에 의해 견고하게 접합 │
│ 조립상태 핸드바 견고하게 조립되어야 함 │
│ 연기차단재 연기차단재 가이드레일 W60, │
│ 가이드레일 W50 (분체 설치) │
│ 하단마감재 내부 부재형상 설치 유무 │
│ 2 모터 인정제품과 동일사양 │
│ 3 재질 WY-SC780 인쇄상태 확인 │
│ 4 치수 │
│ 길이 수주 치수 ± 30mm │
│ 높이 수주 치수 ± 30mm │
│ 가이드레일 10 ± 5mm (측정부위 길이 100 이내)│
│ 간격 가이드레일갑과 하단마감재 25mm 이내│
│ 5 작동테스트 6mm 관절게이지 관통 여 150mm │
│ 6 내화시험 25mm 관절게이지 관통 유무 │
│ 7 차연시험 10초 이상 자속되는 화염 발생 유무 │
│ 8 개폐시험 전도/개폐 2.5~6.5m/min 등 │
│ │
│ 특이사항: │
│ 종합판정: 합격 │
│ │
│ [이전] [1] /50 [이동] [다음] │
└─────────────────────────────────────────┘
```
---
## 6. API
### 6.1 주요 엔드포인트
| Method | Path | 설명 |
|--------|------|------|
| GET | `/quality/documents` | 목록 |
| POST | `/quality/documents` | 생성 |
| GET | `/quality/documents/{id}` | 상세 |
| PUT | `/quality/documents/{id}` | 수정 (개소/규격 포함) |
| PATCH | `/quality/documents/{id}/complete` | 검사완료 |
| POST | `/quality/documents/{id}/orders` | 수주 연결 |
| DELETE | `/quality/documents/{id}/orders/{orderId}` | 수주 해제 |
| POST | `/quality/documents/{id}/locations/{locId}/inspect` | 개소별 검사 저장 |
### 6.2 검사 저장 요청 예시
```json
POST /quality/documents/1/locations/5/inspect
{
"inspection_data": {
"appearanceProcessing": "pass",
"appearanceSewing": "pass",
"appearanceAssembly": "pass",
"appearanceSmokeBarrier": "pass",
"appearanceBottomFinish": "pass",
"motor": "pass",
"material": "pass",
"lengthJudgment": "OK",
"heightJudgment": "OK",
"guideRailGap": "OK",
"bottomFinishGap": "OK",
"fireResistanceTest": "pass",
"smokeLeakageTest": "pass",
"openCloseTest": "pass",
"impactTest": "pass",
"productImages": ["https://..."]
}
}
```
---
## 7. 소스 파일
### 7.1 Backend
| 파일 | 역할 |
|------|------|
| `api/app/Models/Qualitys/QualityDocument.php` | 품질관리서 모델 |
| `api/app/Models/Qualitys/QualityDocumentLocation.php` | 개소 모델 |
| `api/app/Models/Qualitys/QualityDocumentOrder.php` | 수주 연결 모델 |
| `api/app/Services/QualityDocumentService.php` | 서비스 (770줄) |
| `api/app/Http/Controllers/Api/V1/QualityDocumentController.php` | 컨트롤러 |
### 7.2 Frontend
| 파일 | 역할 |
|------|------|
| `react/src/components/quality/InspectionManagement/InspectionList.tsx` | 목록 |
| `react/src/components/quality/InspectionManagement/InspectionCreate.tsx` | 생성 |
| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 상세/수정 |
| `react/src/components/quality/InspectionManagement/OrderSelectModal.tsx` | 수주 선택 |
| `react/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx` | 검사 입력 |
| `react/src/components/quality/InspectionManagement/actions.ts` | Server Actions |
| `react/src/components/quality/InspectionManagement/types.ts` | 타입 정의 |
---
## 관련 문서
- [품질관리 시스템 개요](./README.md)
- [생산실적신고](./performance-reports.md)
---
**최종 업데이트**: 2026-03-09

View File

@@ -0,0 +1,268 @@
# 생산실적신고 (Performance Reports)
> **작성일**: 2026-03-09
> **상태**: 운영 중
> **URL**: `/quality/performance-reports`
---
## 1. 개요
### 1.1 목적
건설기술진흥법에 따라 방화문 제품검사 완료 건에 대해 **분기별 실적신고**를 관리한다.
건기원(한국건설기술연구원) 온라인 시스템에 제출할 데이터를 자동 수집하고, 필수정보 검증 후 확정 처리한다.
### 1.2 비즈니스 배경
- **법적 의무**: 방화문은 건축자재 품질관리 대상으로, 제품검사 후 실적을 건기원에 신고해야 함
- **신고 주기**: 분기별 (Q1: 1~3월, Q2: 4~6월, Q3: 7~9월, Q4: 10~12월)
- **신고 내용**: 품질관리서 번호, 현장정보, LOT, 규격, 검사결과
- **제출 방식**: 건기원 온라인 시스템 수기 입력 또는 엑셀 업로드
### 1.3 SAM에서의 역할
```
제품검사 완료 → 실적신고 자동생성 → 필수정보 검증 → 확정 → 건기원 제출
(SAM 자동) (SAM 자동) (담당자) (수동/향후 자동)
```
---
## 2. 상태 흐름
### 2.1 상태 정의
| 상태 | 코드 | 설명 |
|------|------|------|
| 미확정 | `unconfirmed` | 자동생성 직후, 필수정보 미완료 가능 |
| 확정 | `confirmed` | 필수정보 완료 후 담당자가 확정 |
| 신고완료 | `reported` | 건기원에 신고 완료 (미구현) |
### 2.2 상태 전이
```
confirm()
unconfirmed ─────────────────→ confirmed
↑ │
│ unconfirm() │
└──────────────────────────────┘
distribute()
reported (미구현)
```
### 2.3 확정 조건
확정(`confirm`) 시 **필수정보 4개 섹션** 검증:
```
✅ 건축공사장: name, land_location, lot_no
✅ 자재유통업자: company, address, ceo, tel
✅ 공사시공자: company, address, name, phone
✅ 공사감리자: office, address, name, phone
```
하나라도 누락되면 확정 불가 → `cannotConfirmWithMissingInfo` 에러 반환
---
## 3. 자동 생성 로직
### 3.1 트리거
`QualityDocumentService::complete()` 호출 시:
```php
PerformanceReport::firstOrCreate(
[
'tenant_id' => $tenantId,
'quality_document_id' => $qualityDocument->id,
],
[
'year' => now()->year, // 현재 연도
'quarter' => ceil(now()->month / 3), // 현재 분기
'confirmation_status' => 'unconfirmed',
'created_by' => $userId,
]
);
```
### 3.2 특징
- `firstOrCreate`: 동일 품질관리서에 대해 중복 생성 방지
- 검사완료 시점의 연도/분기로 자동 배정
- Unique 제약: `(tenant_id, quality_document_id)`
---
## 4. 화면 구성
### 4.1 탭 구조
| 탭 | 내용 |
|----|------|
| **분기별 실적신고** | 기본 탭. 연도/분기별 실적 목록 |
| **누락체크** | 출고완료 수주 중 품질관리서 미등록 건 |
### 4.2 통계 카드
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 전체 │ │ 확정 │ │ 미확정 │ │ 총 개소 │
│ 12건 │ │ 8건 │ │ 4건 │ │ 48개소 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
### 4.3 테이블 컬럼 (분기별 실적신고)
| 컬럼 | 설명 |
|------|------|
| 선택 | 체크박스 (일괄 처리용) |
| 번호 | 순번 |
| 품질관리서번호 | KD-QD-YYYYMM-NNNN |
| 작성일 | 품질관리서 접수일 |
| 현장명 | 시공 현장명 |
| 수주처 | 거래처 |
| 개소 | 검사 개소 수 |
| 필수정보 | "완료" 또는 "N건 누락" |
| 확정상태 | 미확정/확정/신고완료 |
| 확정일 | 확정 처리일 |
| 메모 | 메모 내용 |
### 4.4 액션 버튼
| 버튼 | 조건 | 설명 |
|------|------|------|
| 선택 확정 | 미확정 건 선택 시 | 필수정보 완료된 건만 일괄 확정 |
| 확정 해제 | 확정 건 선택 시 | 확정 → 미확정 되돌리기 |
| 배포 | 확정 건 선택 시 | 건기원 신고 (미구현) |
| 메모 | 건 선택 시 | 메모 일괄 작성 |
---
## 5. 누락체크 (슬라이드 18)
### 5.1 목적
실적신고 기간이 지났지만 확정이 안된 목록을 확인한다.
출고(배송)가 완료되었지만 품질관리서가 등록되지 않은 수주를 탐지한다.
### 5.2 누락 발생 원인
| 원인 | 설명 |
|------|------|
| 품질관리서 발행일 기준 분기 불일치 | 품질관리서가 해당 분기에 포함되어야 하나, 공사 미완료로 다음 분기로 이월 |
| 수주 중복 등록 | 수주통 시 다른 현장명으로 등록되어 제품검사 발행 시 누락 |
| 납품 후 미등록 | 납품 후 제품검사 등록이 누락된 경우 |
### 5.3 누락체크 목록 표시
| 컬럼 | 설명 |
|------|------|
| 품질관리서 번호 | 관련 품질관리서 (있는 경우) |
| 현장명 | 수주 현장명 |
| 수주처 | 거래처 |
| 개소 | 수주 개소 수 |
| 제품검사완료일 | 검사 완료 일자 |
| 메모 | 누락 사유 메모 |
### 5.4 로직
```sql
-- 출고완료 수주 중 quality_document_orders에 미등록된 건
SELECT orders.*
FROM orders
WHERE delivery_status = 'completed'
AND NOT EXISTS (
SELECT 1 FROM quality_document_orders
WHERE order_id = orders.id
)
```
---
## 6. API
### 6.1 엔드포인트
| Method | Path | 설명 |
|--------|------|------|
| GET | `/quality/performance-reports` | 목록 (year, quarter, status 필터) |
| GET | `/quality/performance-reports/stats` | 확정/미확정 통계 |
| GET | `/quality/performance-reports/missing` | 누락체크 |
| PATCH | `/quality/performance-reports/confirm` | 일괄 확정 |
| PATCH | `/quality/performance-reports/unconfirm` | 확정 해제 |
| PATCH | `/quality/performance-reports/memo` | 메모 일괄 업데이트 |
### 6.2 요청/응답 예시
**목록 조회**:
```
GET /quality/performance-reports?year=2026&quarter=1&status=unconfirmed
```
**일괄 확정**:
```json
PATCH /quality/performance-reports/confirm
{
"ids": [1, 2, 3]
}
```
**응답**:
```json
{
"success": true,
"message": "message.performance_report.confirmed",
"data": {
"confirmed_count": 2,
"skipped_count": 1,
"skipped_ids": [3]
}
}
```
---
## 7. 소스 파일
### 7.1 Backend
| 파일 | 역할 |
|------|------|
| `api/app/Models/Qualitys/PerformanceReport.php` | 모델 |
| `api/app/Services/PerformanceReportService.php` | 서비스 (280줄) |
| `api/app/Http/Controllers/Api/V1/PerformanceReportController.php` | 컨트롤러 |
| `api/routes/api/v1/quality.php` | 라우트 |
### 7.2 Frontend
| 파일 | 역할 |
|------|------|
| `react/src/app/[locale]/(protected)/quality/performance-reports/page.tsx` | 페이지 |
| `react/src/components/quality/PerformanceReportManagement/PerformanceReportList.tsx` | 목록 컴포넌트 |
| `react/src/components/quality/PerformanceReportManagement/MemoModal.tsx` | 메모 모달 |
| `react/src/components/quality/PerformanceReportManagement/actions.ts` | Server Actions |
---
## 8. 미구현 기능
| 기능 | 설명 | 우선순위 |
|------|------|---------|
| 배포(distribute) API | 건기원 시스템 연동 자동 신고 | 중 |
| 엑셀 다운로드 | 확정건 건기원 양식 엑셀 내보내기 | 높음 |
| 분기 마감 알림 | 분기 종료 전 미확정건 알림 | 낮음 |
---
## 관련 문서
- [품질관리 시스템 개요](./README.md)
- [제품검사 관리](./inspection-management.md)
---
**최종 업데이트**: 2026-03-09

View File

@@ -0,0 +1,169 @@
# 품질인정심사 (Quality Certification Audit)
> **작성일**: 2026-03-09
> **상태**: 개발 예정
> **URL**: `/quality/qms`
> **스토리보드**: 슬라이드 19~20
---
## 1. 개요
### 1.1 목적
품질인정심사 자료를 조회하고 관리한다. 기준/매뉴얼 심사와 로트 추적 심사 두 가지 영역으로 구성된다.
### 1.2 탭 구조
| 탭 | 설명 | 진행률 예시 |
|----|------|-----------|
| 기준/매뉴얼 심사 | 품질 기준 문서 및 매뉴얼 점검 | 2/15 |
| 로트 추적 심사 | 개소별 제품로트 추적 및 서류 확인 | 7/12 |
**전체 심사 진행률**: 기준/매뉴얼 + 로트추적 합산 (예: 9/27)
---
## 2. 기준/매뉴얼 심사
### 2.1 화면 구성
```
┌─────────────────────────────────────────────────────┐
│ 필터: 연도, 분기(전체/1~4), 검색 │
├──────────────────┬──────────────────────────────────┤
│ 점검표 항목 │ 기준 문서화 │
│ │ │
│ ▼ 1. 제목 │ 항목명 ─────────── │
│ ☑ 1. 항목 [완료] │ 소개 ─────────── │
│ ☑ 2. 항목 [완료] │ │
│ □ 3. 항목 │ 관련 기준 문서 │
│ │ 📄 문서명 R025 2025-01-01 │
│ ▼ 2. 제목 │ 📄 문서명 R025 2025-01-01 │
│ ☑ 1. 항목 │ │
│ ☑ 2. 항목 │ 📎 기준/매뉴얼 확인 │
│ □ 3. 항목 │ │
│ │ 문서 미리보기 영역 │
│ 완료 체크 박스 │ (PDF 등) │
│ □ 완료 │ 파일명: 파일명.pdf 1/1 페이지 │
└──────────────────┴──────────────────────────────────┘
```
### 2.2 점검표 항목
- 계층 구조: 대분류(제목) → 세부 항목
- 각 항목에 완료/미완료 체크
- 항목 클릭 시 우측에 해당 항목의 기준 문서 정보 표시
### 2.3 기준 문서화
| 영역 | 설명 |
|------|------|
| 항목명 및 소개 | 선택한 점검표 항목의 상세 정보 |
| 관련 기준 문서 | 해당 항목에 연결된 기준 문서 목록 (문서명, 문서번호, 날짜) |
| 문서 미리보기 | PDF 등 첨부 문서 미리보기 |
| 완료 체크 박스 | 기본값: 완료 해제 상태 |
### 2.4 기준/매뉴얼 확인 버튼
- 클릭 시 확인 완료 스티커로 변경
- 전체 항목 확인 완료 시 탭 진행률 갱신
---
## 3. 로트 추적 심사
### 3.1 화면 구성
```
┌───────────────────────────────────────────────────────────┐
│ 필터: 연도, 분기(전체/1~4), 검색 │
├─────────────────┬─────────────────┬───────────────────────┤
│ 품질관리서 목록 │ 수주코드 목록 │ 관련 서류 │
│ │ │ │
│ KD-SS-2024- │ KD-SS-240921-19 │ 수입검사 성적서 │
│ 2025년 3분기 │ 수주: 2024-09-24│ ┌──────────────────┐ │
│ 현장명 │ 7개소 / 완료 │ │ 전반 수입검사 성적서│ │
│ 인정특성, 실리카 │ │ │ 절반 수입검사 성적서│ │
│ 수주코드 2건 │ 개소별 제품로트 │ └──────────────────┘ │
│ 14개소 │ KD-SS-240921-19 │ │
│ │ -01 │ 개소별 제품로트 목록 │
│ KD-SS-2024- │ 보조 │ 수주코드번호 │
│ 현장명 │ │ 가로, 세로 │
│ 수주코드 2건 │ KD-SS-240921-19 │ 3건의 서류 │
│ 7개소 │ -19 [확인] │ │
│ │ 보조 │ 확인 서류 목록 │
│ │ │ 수입검사, 일반전표, │
│ │ │ 중간검사, 납품확인서, │
│ │ │ 출고증, 제품검사, │
│ │ │ 검사 성적서, 품질관리서│
├─────────────────┴─────────────────┤ │
│ 문서 정보 영역 │ 확인 버튼 │
│ 품질: 해당 문서 열람/닫힘 토글 │ [확인] [완료] │
└────────────────────────────────────┴───────────────────────┘
```
### 3.2 3단 드릴다운 구조
| 단계 | 영역 | 표시 내용 |
|------|------|---------|
| 1단계 | 품질관리서 목록 | 해당 분기 확정된 품질관리서, 인정특성, 수주코드 건수, 개소수 |
| 2단계 | 수주코드 목록 | 선택한 품질관리서의 수주코드, 수주일, 개소수, 완료 상태 |
| 3단계 | 관련 서류 | 해당 수주코드의 개소별 제품로트, 확인 서류 목록 |
### 3.3 품질관리서 목록 (1단계)
- 해당 분기로 실적신고 확정된 품질관리서
- 표시: 품질관리서 번호, 해당 분기, 현장명, 인정특성, 수주코드 건수, 개소수
- 클릭 시 2단계(수주코드 목록) 표시
### 3.4 수주코드 목록 (2단계)
- 해당 품질관리서에 연결된 수주코드 목록
- 품질관리서는 제품검사 시 수주코드 연결
- 표시: 수주코드 번호, 개소 수, 수주일, 현장, 개소수, 개소별 제품로트 확인 여부
- 클릭 시 5단계 영역에 해당 수주코드의 관련 서류 표시
### 3.5 관련 서류 (3단계)
개소별 제품로트에 연결된 서류:
| 서류 종류 | 설명 |
|---------|------|
| 수입검사 성적서 | 원자재 수입검사 결과 |
| 일반전표 | 회계 전표 |
| 중간검사 성적서 | 공정 중간검사 |
| 납품확인서 | 납품 확인 |
| 출고증 | 출고 기록 |
| 제품검사 성적서 | 완제품 검사 |
| 품질관리서 | 품질관리서 원본 |
### 3.6 확인 프로세스
1. 품질관리서 선택 → 수주코드 선택 → 관련 서류 조회
2. 각 서류를 열람하고 **확인** 버튼 클릭
3. 모든 서류 확인 완료 시 해당 로트 **완료** 처리
4. 전체 로트 완료 시 로트 추적 심사 진행률 갱신
---
## 4. 구현 상태
| 기능 | 상태 | 비고 |
|------|------|------|
| 기준/매뉴얼 심사 점검표 | 미구현 | 페이지만 존재 |
| 기준 문서 관리 | 미구현 | |
| 로트 추적 심사 | 미구현 | |
| 서류 연결/확인 | 미구현 | |
---
## 관련 문서
- [품질관리 시스템 개요](./README.md)
- [제품검사 관리](./inspection-management.md)
- [생산실적신고](./performance-reports.md)
---
**최종 업데이트**: 2026-03-09

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

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

View File

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

View File

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

View File

@@ -0,0 +1,244 @@
# 사운드 로고 스튜디오
> **작성일**: 2026-03-08
> **상태**: 운영 중
> **라우트**: `/rd/sound-logo`
> **뷰**: `resources/views/rd/sound-logo/index.blade.php`
---
## 1. 개요
사운드 로고 스튜디오는 기업 시그니처 사운드(사운드 로고)를 제작하는 올인원 도구이다. Web Audio API 기반 시퀀서, Gemini AI 어시스트, TTS 음성 오버레이, Lyria RealTime BGM 생성을 하나의 SPA에서 통합 제공한다.
**핵심 기능:**
- 시퀀서 기반 사운드 로고 수동/프리셋 제작
- Gemini AI가 프롬프트 기반으로 음표 시퀀스 자동 설계
- Gemini TTS로 나레이션 음성 생성 (여성/남성/아이, 30종 음성, 속도 조절)
- Lyria RealTime WebSocket으로 AI 배경음악 실시간 생성
- 시퀀서/BGM 상호 배타적 재생 + TTS 공통 합성
- WAV 내보내기
---
## 2. 아키텍처
### 2.1 3레이어 오디오 구조
```
┌─────────────────────────────────────────────────────────┐
│ Layer 1: 사운드 로고 (시퀀서 또는 AI BGM — 상호 배타적) │
│ ├── A) 시퀀서 (수동 편집 / 프리셋 / AI 생성) │
│ └── B) AI 배경음악 (Lyria RealTime) │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 음성 TTS (Gemini) ─── 양쪽 모드 공통 │
├─────────────────────────────────────────────────────────┤
│ DynamicsCompressor (클리핑 방지) → AudioContext.dest │
└─────────────────────────────────────────────────────────┘
```
- **시퀀서 모드**: 수동/프리셋/AI 생성 음표 + TTS 합성. BGM 제외.
- **BGM 모드**: AI 배경음악 + TTS 합성. 시퀀서 음표 제외.
- 판단 기준: `bgmBuffer` 존재 여부
### 2.2 기술 스택
| 계층 | 기술 | 설명 |
|------|------|------|
| 프론트엔드 | Blade + Alpine.js | 단일 SPA |
| 오디오 엔진 | Web Audio API | OscillatorNode, BufferSourceNode, DynamicsCompressorNode |
| AI 시퀀서 | Gemini 2.5 Flash | 프롬프트 → JSON 음표 시퀀스 |
| TTS | `gemini-2.5-flash-preview-tts` | 30종 음성, Director's Note 스타일 제어 |
| BGM | Lyria RealTime (WebSocket) | `models/lyria-realtime-exp`, 48kHz 스테레오 PCM |
| 저장 | 없음 (클라이언트 전용) | WAV 내보내기로 결과물 보존 |
---
## 3. 시퀀서 (Layer 1-A)
### 3.1 입력 모드
| 모드 | 설명 |
|------|------|
| **수동** | 음표 그리드에서 직접 추가/삭제/편집 |
| **프리셋** | 사전 정의된 사운드 패턴 선택 (기업 시그널, 알림음 등) |
| **AI 생성** | 프롬프트 입력 → Gemini가 음표 시퀀스 JSON 반환 |
### 3.2 음표 데이터 구조
```json
{
"type": "note | chord | rest",
"note": "C5",
"chord": ["C4", "E4", "G4"],
"duration": 0.2,
"velocity": 0.8
}
```
### 3.3 신디사이저 설정
| 파라미터 | 범위 | 설명 |
|---------|------|------|
| `synth` | sine, triangle, square, sawtooth | 파형 |
| `volume` | 0~1 | 마스터 볼륨 |
| `adsr.attack` | 1~500ms | 어택 |
| `adsr.decay` | 10~1000ms | 디케이 |
| `adsr.sustain` | 0~1.0 | 서스테인 |
| `adsr.release` | 10~3000ms | 릴리즈 |
| `reverb` | 0~1 | 리버브 양 |
---
## 4. 음성 오버레이 — TTS (Layer 2)
### 4.1 음성 카테고리
| 카테고리 | 음성 수 | 주요 음성 |
|---------|---------|----------|
| **여성** | 9종 | Kore(단정), Aoede(산뜻), Leda(따뜻), Zephyr(차분) 등 |
| **남성** | 9종 | Puck(밝음), Charon(깊음), Orus(안정), Fenrir(무게) 등 |
| **아이** | 5종 | Leda 기반(여아), Puck 기반(남아) 등 |
### 4.2 속도 조절
| 단계 | Director's Note |
|------|----------------|
| 1 (매우 느림) | "아주 천천히 또박또박 말해주세요." |
| 2 (느림) | "조금 느린 속도로 말해주세요." |
| 3 (보통) | (지시문 없음) |
| 4 (빠름) | "조금 빠른 속도로 말해주세요." |
| 5 (매우 빠름) | "아주 빠른 속도로 말해주세요." |
### 4.3 오디오 형식
- 출력: `audio/L16;rate=24000` (16-bit PCM, 24kHz, 모노, little-endian)
- 디코딩: `DataView.getInt16(offset, true)` → Float32 변환
---
## 5. AI 배경음악 — Lyria (Layer 1-B)
### 5.1 WebSocket 프로토콜
```
브라우저 ──WebSocket──→ Google Lyria RealTime API
(wss://generativelanguage.googleapis.com/ws/...BidiGenerateMusic)
```
서버는 API 키만 전달(`GET /lyria-config`), 실제 WebSocket 통신은 브라우저에서 직접 수행한다.
### 5.2 메시지 흐름
```
1. setup → { setup: { model: "models/lyria-realtime-exp" } }
2. (수신) ← { setupComplete: {} }
3. 프롬프트 → { clientContent: { weightedPrompts: [...] } }
4. 설정 → { musicGenerationConfig: { bpm, density, brightness, scale, temperature } }
5. 재생 → { playbackControl: "PLAY" }
6. (수신 반복) ← { serverContent: { audioChunks: [{ data: "base64..." }] } }
7. 정지 → { playbackControl: "STOP" }
8. (종료) ← WebSocket close
```
### 5.3 BGM 파라미터
| 파라미터 | 범위 | 설명 |
|---------|------|------|
| `bgmPrompt` | 텍스트 | 음악 분위기 설명 |
| `bgmBpm` | 60~180 | BPM |
| `bgmDensity` | 0~100 | 밀도 (0~1 변환) |
| `bgmBrightness` | 0~100 | 밝기 (0~1 변환) |
| `bgmScale` | C_MAJOR 등 | 음계 |
| `bgmDuration` | 5~60초 | 생성 길이 |
### 5.4 오디오 형식
- 출력: 16-bit PCM, 48kHz, 스테레오, little-endian
- WAV 헤더 감지 시 `decodeAudioData` fallback
---
## 6. API 엔드포인트
### 6.1 사운드 로고 (RdController)
| HTTP | URI | 메서드 | 설명 |
|------|-----|--------|------|
| GET | `/rd/sound-logo` | `soundLogo()` | 스튜디오 페이지 |
| POST | `/rd/sound-logo/generate` | `soundLogoGenerate()` | AI 음표 시퀀스 생성 (Gemini) |
| POST | `/rd/sound-logo/tts` | `soundLogoTts()` | TTS 음성 생성 (Gemini TTS) |
| GET | `/rd/sound-logo/lyria-config` | `soundLogoLyriaConfig()` | Lyria WebSocket 접속 설정 반환 |
### 6.2 CM송/나레이션 (CmSongController)
| HTTP | URI | 메서드 | 설명 |
|------|-----|--------|------|
| GET | `/rd/cm-song` | `index()` | 나레이션 목록 |
| GET | `/rd/cm-song/create` | `create()` | 나레이션 제작 |
| POST | `/rd/cm-song` | `store()` | 나레이션 저장 |
| GET | `/rd/cm-song/{id}` | `show()` | 나레이션 상세 |
| DELETE | `/rd/cm-song/{id}` | `destroy()` | 나레이션 삭제 |
| GET | `/rd/cm-song/{id}/download` | `download()` | 음성 파일 다운로드 |
| POST | `/rd/cm-song/generate-lyrics` | `generateLyrics()` | AI 가사 생성 (Gemini) |
| POST | `/rd/cm-song/generate-audio` | `generateAudio()` | TTS 음성 생성 |
### 6.3 CM송 데이터 모델
| 모델 | 테이블 | 설명 |
|------|--------|------|
| `CmSong` | `cm_songs` | 나레이션 (회사명, 업종, 가사, 음성파일) |
**저장 경로**: `tenant` 디스크 → `cm-songs/{tenant_id}/{filename}.wav`
---
## 7. 오디오 엔진 상세
### 7.1 마스터 출력 체인
```
각 소스 (Oscillator / BufferSource)
→ GainNode (개별 볼륨)
→ DynamicsCompressorNode (마스터 리미터)
→ AudioContext.destination
```
**컴프레서 설정:**
- threshold: -6dB
- knee: 10dB
- ratio: 12:1
- attack: 3ms
- release: 150ms
### 7.2 WAV 내보내기
`OfflineAudioContext`로 오프라인 렌더링 후 `bufferToWav()` 변환.
- 샘플레이트: 44,100Hz
- 채널: 2 (스테레오)
- 비트: 16-bit PCM
- 오프라인 컨텍스트에도 동일한 DynamicsCompressor 적용
---
## 8. 관련 파일
| 파일 | 설명 |
|------|------|
| `resources/views/rd/sound-logo/index.blade.php` | SPA 뷰 (Alpine.js, ~2100줄) |
| `app/Http/Controllers/RdController.php` | 사운드 로고 API (4 메서드) |
| `app/Http/Controllers/Rd/CmSongController.php` | CM송/나레이션 CRUD (8 메서드) |
| `app/Models/Rd/CmSong.php` | CM송 모델 |
| `app/Helpers/AiTokenHelper.php` | Gemini 토큰 사용량 추적 |
---
## 관련 문서
- [R&D 메뉴 개요](README.md) — R&D 전체 메뉴 구조
- [AI 분석 리포트](../ai/README.md) — Gemini API 활용 패턴 참고
- [사운드 로고 생성기 기획서](../../plans/sound-logo-generator-plan.md) — 초기 기획
---
**최종 업데이트**: 2026-03-08

96
frontend/_index.md Normal file
View File

@@ -0,0 +1,96 @@
# SAM ERP Frontend Documentation
> **프로젝트**: SAM ERP Next.js 프론트엔드
> **최종 갱신**: 2026-03-09
> **현재 문서 버전**: v1
---
## 문서 구조
```
frontend/
├── _index.md ← 현재 문서 (목록 + 버전 관리)
├── v1/ ← 현재 활성 버전
│ ├── 01 ~ 09 ← 프론트엔드 아키텍처/가이드
│ └── 10 ← API 연동 스펙
└── api-specs/ ← (레거시, v1/10으로 이관됨)
```
---
## 문서 목록 및 버전 현황
| # | 문서 | 버전 | 최종 수정 | 담당 | 대상 | 설명 |
|---|------|------|----------|------|------|------|
| 01 | [architecture](v1/01-architecture.md) | 1.0.0 | 2026-03-09 | Frontend | 전체 | 프로젝트 구조, 기술 스택, 디렉토리 설계 |
| 02 | [api-pattern](v1/02-api-pattern.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | API 통신 패턴 (프록시, Server Action, buildApiUrl) |
| 03 | [component-design](v1/03-component-design.md) | 1.0.0 | 2026-03-09 | Frontend | FE/기획 | 컴포넌트 계층 (atoms → templates), 페이지 유형 |
| 04 | [common-components](v1/04-common-components.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 공통 컴포넌트 사용법 (UniversalListPage 등) |
| 05 | [form-pattern](v1/05-form-pattern.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 폼 패턴 (Zod, FormField, react-hook-form) |
| 06 | [styling-guide](v1/06-styling-guide.md) | 1.0.0 | 2026-03-09 | Frontend | FE/디자인 | CSS 규칙 (Tailwind, shadcn/ui, 색상 시스템) |
| 07 | [auth-flow](v1/07-auth-flow.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | 인증 흐름 (HttpOnly cookie, 토큰 갱신) |
| 08 | [dashboard-system](v1/08-dashboard-system.md) | 1.0.0 | 2026-03-09 | Frontend | FE/BE | CEO 대시보드 아키텍처 (invalidation, hooks) |
| 09 | [conventions](v1/09-conventions.md) | 1.0.0 | 2026-03-09 | Frontend | FE | 네이밍, import, 파일 배치, Git 규칙 |
| 10 | [document-api-integration](v1/10-document-api-integration.md) | 1.0.0 | 2026-02-05 | API Team | FE/BE | 문서 관리 API 연동 (검사 성적서 resolve/upsert) |
### 대상 범례
- **FE**: 프론트엔드 개발자
- **BE**: 백엔드 개발자
- **기획**: 기획자/PM
- **디자인**: 디자이너
- **전체**: 모든 역할
---
## 버전 변경 이력
### v1 (2026-03-09 ~)
| 날짜 | 문서 | 변경 | 버전 |
|------|------|------|------|
| 2026-03-09 | 01~09 | 초기 작성 | 1.0.0 |
| 2026-02-05 | 10 | 문서 API 연동 가이드 작성 (api-specs에서 이관) | 1.0.0 |
---
## 버전 관리 규칙
### 문서 버전 (Semantic Versioning)
```
MAJOR.MINOR.PATCH
MAJOR: 문서 구조 변경, 기존 내용 대폭 수정
MINOR: 새로운 섹션 추가, 기존 내용 보완
PATCH: 오탈자, 코드 예시 수정, 사소한 수정
```
### 업데이트 절차
1. 해당 문서 내용 수정
2. 문서 상단 `버전``최종 수정` 날짜 갱신
3.`_index.md`의 문서 목록 테이블 버전/날짜 갱신
4. 변경 이력 테이블에 행 추가
### 새 문서 추가 시
1. `v1/` 폴더에 `{번호}-{주제}.md` 형식으로 생성
2. 문서 상단에 버전/날짜/대상 헤더 포함
3. `_index.md` 문서 목록 테이블에 행 추가
---
## 빠른 참고
| 할 일 | 읽을 문서 |
|-------|----------|
| 프로젝트 전체 구조 이해 | 01-architecture |
| API 호출 방법 알기 | 02-api-pattern |
| 새 리스트 페이지 만들기 | 03-component-design → 04-common-components |
| 새 폼 페이지 만들기 | 05-form-pattern |
| 디자인/스타일 규칙 확인 | 06-styling-guide |
| 인증 동작 이해 | 07-auth-flow |
| 대시보드 연동 작업 | 08-dashboard-system |
| 코딩 컨벤션 확인 | 09-conventions |
| 문서 관리 API 연동 | 10-document-api-integration |

Some files were not shown because too many files have changed in this diff Show More