diff --git a/projects/e-sign/esign-storyboard.pptx b/projects/e-sign/esign-storyboard.pptx new file mode 100644 index 0000000..a407e0f Binary files /dev/null and b/projects/e-sign/esign-storyboard.pptx differ diff --git a/projects/e-sign/storyboard-config.json b/projects/e-sign/storyboard-config.json new file mode 100644 index 0000000..ac14d00 --- /dev/null +++ b/projects/e-sign/storyboard-config.json @@ -0,0 +1,302 @@ +{ + "projectName": "SAM E-Sign 전자계약 서명 솔루션", + "company": "SAM (주일/경동)", + "author": "DX 추진팀", + "date": "2026.02.12", + "version": "v1.0", + "purpose": "모두싸인과 유사한 간편 전자계약 서명 솔루션을 SAM 시스템에 자체 구축하여, 두 당사자(계약 생성자 A, 상대방 B)가 온라인으로 계약서에 서명하고 법적 효력이 있는 형태로 보관하는 시스템", + "features": [ + "PDF 업로드 기반 전자계약 생성", + "드래그&드롭 서명 위치 지정 (PDF.js)", + "이메일 OTP 본인인증 (6자리, 5분 유효)", + "캔버스 직접 서명 (SignaturePad)", + "SHA-256 문서 무결성 검증", + "감사 추적 로그 (Audit Trail)", + "순차 서명 (A→B 또는 B→A)", + "이메일 서명 요청/리마인더 발송" + ], + "effects": [ + { "icon": "📝", "title": "계약 간소화", "desc": "PDF 업로드 → 서명 위치 지정 → 링크 발송, 3단계 완료" }, + { "icon": "🔒", "title": "보안 강화", "desc": "문서 해시 검증, OTP 본인인증, IP/UA 기록, 감사 추적" }, + { "icon": "⚖️", "title": "법적 효력", "desc": "전자서명법 제2조에 부합하는 전자서명 요건 충족" }, + { "icon": "⏱️", "title": "업무 효율", "desc": "대면 서명 불필요, 이메일 링크로 언제 어디서든 서명" } + ], + "tocItems": [ + { "num": "01", "title": "프로젝트 개요", "desc": "목적, 주요 기능, 기대 효과" }, + { "num": "02", "title": "메뉴 구조 (IA)", "desc": "관리자 화면 + 공개 서명 화면" }, + { "num": "03", "title": "관리자 화면", "desc": "대시보드, 계약 생성, 상세, 필드 설정, 발송" }, + { "num": "04", "title": "서명자 화면", "desc": "본인인증(OTP), 서명 수행, 완료" } + ], + "mainMenus": [ + { "title": "계약 대시보드", "children": ["계약 현황 통계", "계약 목록 조회"] }, + { "title": "계약 생성", "children": ["PDF 업로드", "서명자 정보 입력"] }, + { "title": "계약 상세", "children": ["서명 현황", "감사 로그"] }, + { "title": "서명 위치 지정", "children": ["PDF 뷰어", "필드 배치"] }, + { "title": "서명 요청 발송", "children": ["발송 전 확인", "이메일 발송"] } + ], + "screens": [ + { + "taskName": "계약 대시보드", + "route": "/esign", + "screenName": "계약 대시보드", + "screenId": "ES_DASH", + "descriptions": [ + { "title": "상태별 통계 카드", "content": "전체/진행중/대기/완료/만료 건수를 카드 형태로 표시. API: GET /api/v1/esign/contracts/stats", "markerX": 1.8, "markerY": 1.5 }, + { "title": "필터/검색 영역", "content": "상태 필터(드롭다운), 키워드 검색, 날짜 범위 필터 제공", "markerX": 1.8, "markerY": 2.3 }, + { "title": "계약 목록 테이블", "content": "계약코드, 제목, 상대방, 상태(배지), 기한, 생성일 컬럼. 페이지네이션 포함. API: GET /api/v1/esign/contracts", "markerX": 1.8, "markerY": 3.0 }, + { "title": "새 계약 생성 버튼", "content": "우측 상단 '+ 새 계약' 버튼 → /esign/create로 이동", "markerX": 6.2, "markerY": 1.5 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.4, "h": 0.35, "fill": "1e293b", "text": "계약 대시보드", "color": "FFFFFF", "fontSize": 12, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.8, "y": 1.3, "w": 1.2, "h": 0.35, "fill": "0d9488", "text": "+ 새 계약", "color": "FFFFFF", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 1.8, "w": 1.2, "h": 0.7, "fill": "e0f2fe", "text": "전체\n50건", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 2.9, "y": 1.8, "w": 1.2, "h": 0.7, "fill": "fef3c7", "text": "진행중\n15건", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 4.2, "y": 1.8, "w": 1.2, "h": 0.7, "fill": "dcfce7", "text": "완료\n28건", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 5.5, "y": 1.8, "w": 1.2, "h": 0.7, "fill": "fee2e2", "text": "만료/취소\n7건", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 2.65, "w": 1.5, "h": 0.3, "fill": "f1f5f9", "text": "상태 필터 ▼", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 3.2, "y": 2.65, "w": 2.5, "h": 0.3, "fill": "f1f5f9", "text": "🔍 제목 검색...", "fontSize": 8, "align": "left", "color": "94a3b8"}, + {"type": "rect", "x": 5.8, "y": 2.65, "w": 1.2, "h": 0.3, "fill": "f1f5f9", "text": "날짜 범위", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 3.1, "w": 5.4, "h": 0.3, "fill": "1e293b", "text": "계약코드 제목 상대방 상태 기한 생성일", "color": "FFFFFF", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.4, "w": 5.4, "h": 0.25, "fill": "FFFFFF", "text": "ES-20260212-A3F 소프트웨어 개발 용역 박을동 서명대기 02-19 02-12", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.65, "w": 5.4, "h": 0.25, "fill": "f8fafc", "text": "ES-20260211-B7K 디자인 외주 계약 이상미 완료 02-18 02-11", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.9, "w": 5.4, "h": 0.25, "fill": "FFFFFF", "text": "ES-20260210-C2M 유지보수 계약서 김철수 초안 02-17 02-10", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 4.15, "w": 5.4, "h": 0.25, "fill": "f8fafc", "text": "ES-20260209-D5P 컨설팅 용역 계약 정미경 부분서명 02-16 02-09", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 4.6, "y": 4.55, "w": 2.4, "h": 0.25, "fill": "f1f5f9", "text": "< 1 2 3 4 5 >", "fontSize": 8, "align": "center"} + ] + }, + { + "taskName": "계약 생성", + "route": "/esign/create", + "screenName": "새 계약 생성", + "screenId": "ES_CREATE", + "descriptions": [ + { "title": "계약 정보 입력", "content": "계약 제목(필수), 설명(선택), 서명 순서 선택(상대방 먼저/작성자 먼저), 서명 기한(기본 7일)", "markerX": 1.8, "markerY": 1.5 }, + { "title": "PDF 파일 업로드", "content": "드래그&드롭 영역으로 PDF 업로드. 최대 20MB. 업로드 시 SHA-256 해시 자동 생성", "markerX": 1.8, "markerY": 2.6 }, + { "title": "작성자(갑) 정보", "content": "이름, 이메일(필수), 전화번호(선택). 로그인 사용자 정보 자동 입력", "markerX": 1.8, "markerY": 3.4 }, + { "title": "상대방(을) 정보", "content": "이름, 이메일(필수), 전화번호(선택). 서명 요청 이메일 발송 대상. API: POST /api/v1/esign/contracts", "markerX": 4.5, "markerY": 3.4 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.4, "h": 0.35, "fill": "1e293b", "text": "새 계약 생성", "color": "FFFFFF", "fontSize": 12, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 1.8, "w": 5.4, "h": 0.6, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 1.85, "w": 1.0, "h": 0.2, "text": "계약 제목 *", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.8, "y": 1.85, "w": 4.1, "h": 0.25, "fill": "f1f5f9", "text": "계약 제목을 입력하세요", "fontSize": 8, "color": "94a3b8", "align": "left"}, + {"type": "rect", "x": 1.7, "y": 2.15, "w": 1.0, "h": 0.2, "text": "서명 순서", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.8, "y": 2.15, "w": 2.0, "h": 0.25, "fill": "f1f5f9", "text": "상대방 먼저 ▼", "fontSize": 8, "align": "left"}, + {"type": "rect", "x": 5.0, "y": 2.15, "w": 0.8, "h": 0.2, "text": "서명 기한", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.9, "y": 2.15, "w": 1.0, "h": 0.25, "fill": "f1f5f9", "text": "7일 ▼", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 2.55, "w": 5.4, "h": 0.7, "fill": "f1f5f9"}, + {"type": "rect", "x": 3.3, "y": 2.7, "w": 2.0, "h": 0.2, "text": "PDF 파일을 여기에 드래그하세요", "fontSize": 8, "color": "64748b", "align": "center"}, + {"type": "rect", "x": 3.7, "y": 2.95, "w": 1.2, "h": 0.25, "fill": "0d9488", "text": "파일 선택", "color": "FFFFFF", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 3.4, "w": 2.6, "h": 1.2, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 3.45, "w": 2.4, "h": 0.25, "fill": "e0f2fe", "text": "작성자 (갑) 정보", "fontSize": 8, "bold": true, "align": "center"}, + {"type": "rect", "x": 1.7, "y": 3.75, "w": 0.6, "h": 0.18, "text": "이름 *", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 2.4, "y": 3.75, "w": 1.7, "h": 0.2, "fill": "f1f5f9", "text": "김갑순", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.7, "y": 4.0, "w": 0.6, "h": 0.18, "text": "이메일 *", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 2.4, "y": 4.0, "w": 1.7, "h": 0.2, "fill": "f1f5f9", "text": "kim@sam.kr", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.7, "y": 4.25, "w": 0.6, "h": 0.18, "text": "전화번호", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 2.4, "y": 4.25, "w": 1.7, "h": 0.2, "fill": "f1f5f9", "text": "010-1234-5678", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 4.4, "y": 3.4, "w": 2.6, "h": 1.2, "fill": "FFFFFF"}, + {"type": "rect", "x": 4.5, "y": 3.45, "w": 2.4, "h": 0.25, "fill": "fee2e2", "text": "상대방 (을) 정보", "fontSize": 8, "bold": true, "align": "center"}, + {"type": "rect", "x": 4.5, "y": 3.75, "w": 0.6, "h": 0.18, "text": "이름 *", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 5.2, "y": 3.75, "w": 1.7, "h": 0.2, "fill": "f1f5f9", "text": "", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 4.5, "y": 4.0, "w": 0.6, "h": 0.18, "text": "이메일 *", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 5.2, "y": 4.0, "w": 1.7, "h": 0.2, "fill": "f1f5f9", "text": "", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 5.6, "y": 4.7, "w": 1.4, "h": 0.35, "fill": "0d9488", "text": "계약 생성", "color": "FFFFFF", "fontSize": 9, "bold": true, "align": "center"} + ] + }, + { + "taskName": "계약 상세", + "route": "/esign/{id}", + "screenName": "계약 상세", + "screenId": "ES_DETAIL", + "descriptions": [ + { "title": "계약 기본 정보", "content": "계약코드, 제목, 설명, 서명 순서, 기한, 상태 배지 표시. 상태별 색상 구분", "markerX": 1.8, "markerY": 1.5 }, + { "title": "서명자 현황 카드", "content": "작성자(갑)/상대방(을) 각각의 서명 상태, 인증 시각, 서명 시각 표시. 프로그레스 바로 진행률 시각화", "markerX": 1.8, "markerY": 2.5 }, + { "title": "감사 추적 타임라인", "content": "계약 생성→발송→열람→인증→서명 등 모든 이벤트를 타임라인 형태로 표시. IP, 시각 포함", "markerX": 1.8, "markerY": 3.5 }, + { "title": "액션 버튼 영역", "content": "상태에 따라 발송/리마인더/취소/다운로드/검증 버튼 동적 표시", "markerX": 5.2, "markerY": 1.5 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.3, "w": 3.8, "h": 0.35, "fill": "1e293b", "text": "계약 상세 - ES-20260212-A3F", "color": "FFFFFF", "fontSize": 11, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.5, "y": 1.3, "w": 0.55, "h": 0.3, "fill": "3b82f6", "text": "발송", "color": "FFFFFF", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 6.1, "y": 1.3, "w": 0.55, "h": 0.3, "fill": "f59e0b", "text": "리마인더", "color": "FFFFFF", "fontSize": 7, "align": "center"}, + {"type": "rect", "x": 6.7, "y": 1.3, "w": 0.3, "h": 0.3, "fill": "dc2626", "text": "취소", "color": "FFFFFF", "fontSize": 7, "align": "center"}, + {"type": "rect", "x": 1.6, "y": 1.8, "w": 5.4, "h": 0.6, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 1.85, "w": 0.6, "h": 0.18, "text": "제목", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.4, "y": 1.85, "w": 2.5, "h": 0.18, "text": "소프트웨어 개발 용역 계약서", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.3, "y": 1.85, "w": 0.3, "h": 0.18, "text": "상태", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 5.7, "y": 1.82, "w": 0.7, "h": 0.22, "fill": "dbeafe", "text": "서명대기", "fontSize": 7, "color": "1d4ed8", "align": "center"}, + {"type": "rect", "x": 1.7, "y": 2.1, "w": 0.6, "h": 0.18, "text": "기한", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.4, "y": 2.1, "w": 1.5, "h": 0.18, "text": "2026-02-19 (D-7)", "fontSize": 8, "color": "dc2626", "align": "left"}, + {"type": "rect", "x": 1.6, "y": 2.55, "w": 2.6, "h": 0.8, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 2.6, "w": 2.4, "h": 0.22, "fill": "e0f2fe", "text": "작성자 (갑) - 김갑순", "fontSize": 8, "bold": true, "align": "center"}, + {"type": "rect", "x": 1.7, "y": 2.85, "w": 0.8, "h": 0.15, "text": "상태: 대기중", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 4.4, "y": 2.55, "w": 2.6, "h": 0.8, "fill": "FFFFFF"}, + {"type": "rect", "x": 4.5, "y": 2.6, "w": 2.4, "h": 0.22, "fill": "fee2e2", "text": "상대방 (을) - 박을동", "fontSize": 8, "bold": true, "align": "center"}, + {"type": "rect", "x": 4.5, "y": 2.85, "w": 1.2, "h": 0.15, "text": "상태: 이메일 발송됨", "fontSize": 7, "color": "f59e0b", "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.5, "w": 5.4, "h": 0.25, "fill": "1e293b", "text": " 감사 추적 로그 (Audit Trail)", "color": "FFFFFF", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.75, "w": 5.4, "h": 0.2, "fill": "FFFFFF", "text": " 02-12 10:00 계약 생성 김갑순 192.168.1.100", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 3.95, "w": 5.4, "h": 0.2, "fill": "f8fafc", "text": " 02-12 10:05 서명 요청 발송 시스템 -", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 4.15, "w": 5.4, "h": 0.2, "fill": "FFFFFF", "text": " 02-12 11:20 서명 링크 접속 박을동 121.xxx.xxx.55", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 4.35, "w": 5.4, "h": 0.2, "fill": "f8fafc", "text": " 02-12 11:21 OTP 인증 완료 박을동 121.xxx.xxx.55", "fontSize": 7, "align": "left"} + ] + }, + { + "taskName": "서명 위치 지정", + "route": "/esign/{id}/fields", + "screenName": "서명 위치 지정", + "screenId": "ES_FIELDS", + "descriptions": [ + { "title": "PDF 뷰어 (PDF.js)", "content": "원본 PDF를 브라우저에서 렌더링. 페이지 네비게이션 제공. 확대/축소 가능", "markerX": 1.8, "markerY": 1.8 }, + { "title": "서명 필드 추가", "content": "서명자 선택 후 PDF 위에 클릭하여 필드 추가. 타입: 서명/도장/텍스트/날짜/체크박스", "markerX": 2.5, "markerY": 3.0 }, + { "title": "필드 속성 패널", "content": "우측에 선택된 필드의 속성 표시. 필드 타입, 라벨, 필수 여부, 좌표값(%) 편집", "markerX": 5.5, "markerY": 2.5 }, + { "title": "저장 버튼", "content": "설정 완료 후 저장 → API: POST /api/v1/esign/contracts/{id}/fields", "markerX": 5.5, "markerY": 4.5 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.4, "h": 0.35, "fill": "1e293b", "text": "서명 위치 지정 - 소프트웨어 개발 용역 계약서", "color": "FFFFFF", "fontSize": 10, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 1.8, "w": 3.8, "h": 3.0, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 1.85, "w": 3.6, "h": 0.3, "fill": "f1f5f9", "text": "◀ 1 / 3 페이지 ▶ 🔍 100%", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 1.8, "y": 2.25, "w": 3.4, "h": 2.3, "fill": "f8fafc"}, + {"type": "rect", "x": 2.0, "y": 2.4, "w": 3.0, "h": 0.15, "fill": "e2e8f0", "text": "제 1 조 (목적)", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 2.0, "y": 2.6, "w": 3.0, "h": 0.08, "fill": "e2e8f0"}, + {"type": "rect", "x": 2.0, "y": 2.72, "w": 3.0, "h": 0.08, "fill": "e2e8f0"}, + {"type": "rect", "x": 2.2, "y": 3.7, "w": 1.2, "h": 0.5, "fill": "bfdbfe", "text": "갑 서명\n(클릭하여 이동)", "fontSize": 7, "color": "1d4ed8", "align": "center"}, + {"type": "rect", "x": 3.8, "y": 3.7, "w": 1.2, "h": 0.5, "fill": "fecaca", "text": "을 서명\n(클릭하여 이동)", "fontSize": 7, "color": "dc2626", "align": "center"}, + {"type": "rect", "x": 5.6, "y": 1.8, "w": 1.4, "h": 0.3, "fill": "1e293b", "text": "필드 속성", "color": "FFFFFF", "fontSize": 8, "bold": true, "align": "center"}, + {"type": "rect", "x": 5.6, "y": 2.1, "w": 1.4, "h": 2.0, "fill": "FFFFFF"}, + {"type": "rect", "x": 5.65, "y": 2.15, "w": 0.5, "h": 0.15, "text": "서명자", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 6.2, "y": 2.15, "w": 0.75, "h": 0.18, "fill": "f1f5f9", "text": "작성자(갑)", "fontSize": 7, "align": "center"}, + {"type": "rect", "x": 5.65, "y": 2.4, "w": 0.5, "h": 0.15, "text": "타입", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 6.2, "y": 2.4, "w": 0.75, "h": 0.18, "fill": "f1f5f9", "text": "서명 ▼", "fontSize": 7, "align": "center"}, + {"type": "rect", "x": 5.65, "y": 2.65, "w": 0.5, "h": 0.15, "text": "라벨", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 6.2, "y": 2.65, "w": 0.75, "h": 0.18, "fill": "f1f5f9", "text": "갑 서명", "fontSize": 7, "align": "center"}, + {"type": "rect", "x": 5.65, "y": 2.9, "w": 0.5, "h": 0.15, "text": "필수", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 6.2, "y": 2.9, "w": 0.75, "h": 0.18, "text": "☑ 필수 항목", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 5.6, "y": 4.2, "w": 0.65, "h": 0.3, "fill": "e2e8f0", "text": "+ 갑 필드", "fontSize": 7, "color": "1d4ed8", "align": "center"}, + {"type": "rect", "x": 6.3, "y": 4.2, "w": 0.65, "h": 0.3, "fill": "e2e8f0", "text": "+ 을 필드", "fontSize": 7, "color": "dc2626", "align": "center"}, + {"type": "rect", "x": 5.6, "y": 4.6, "w": 1.4, "h": 0.35, "fill": "0d9488", "text": "필드 설정 저장", "color": "FFFFFF", "fontSize": 9, "bold": true, "align": "center"} + ] + }, + { + "taskName": "서명 요청 발송", + "route": "/esign/{id}/send", + "screenName": "서명 요청 발송", + "screenId": "ES_SEND", + "descriptions": [ + { "title": "발송 전 체크리스트", "content": "서명 필드 설정 여부, 서명자 정보 완료 여부, PDF 무결성 검증 결과를 체크리스트로 표시", "markerX": 1.8, "markerY": 1.8 }, + { "title": "서명 순서 확인", "content": "설정된 서명 순서와 각 서명자 정보를 시각적으로 확인. 순서 변경 불가 (생성 시 결정)", "markerX": 1.8, "markerY": 2.8 }, + { "title": "발송 확인 버튼", "content": "최종 확인 후 발송 → 상태 draft→pending, 첫 서명자에게 이메일 발송", "markerX": 4.0, "markerY": 4.3 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.3, "w": 5.4, "h": 0.35, "fill": "1e293b", "text": "서명 요청 발송", "color": "FFFFFF", "fontSize": 12, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.6, "y": 1.8, "w": 5.4, "h": 0.8, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 1.85, "w": 2.0, "h": 0.22, "text": "발송 전 확인사항", "fontSize": 9, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.8, "y": 2.1, "w": 4.0, "h": 0.15, "text": "✅ 서명 필드가 설정되었습니다 (갑: 1개, 을: 1개)", "fontSize": 8, "color": "16a34a", "align": "left"}, + {"type": "rect", "x": 1.8, "y": 2.28, "w": 4.0, "h": 0.15, "text": "✅ 서명자 정보가 완료되었습니다", "fontSize": 8, "color": "16a34a", "align": "left"}, + {"type": "rect", "x": 1.8, "y": 2.46, "w": 4.0, "h": 0.15, "text": "✅ PDF 문서 무결성이 확인되었습니다 (SHA-256)", "fontSize": 8, "color": "16a34a", "align": "left"}, + {"type": "rect", "x": 1.6, "y": 2.8, "w": 5.4, "h": 1.3, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 2.85, "w": 2.0, "h": 0.22, "text": "서명 순서", "fontSize": 9, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.0, "y": 3.15, "w": 1.8, "h": 0.7, "fill": "fee2e2"}, + {"type": "rect", "x": 2.1, "y": 3.2, "w": 0.3, "h": 0.25, "fill": "dc2626", "text": "1", "color": "FFFFFF", "fontSize": 10, "bold": true, "align": "center"}, + {"type": "rect", "x": 2.5, "y": 3.2, "w": 1.2, "h": 0.18, "text": "상대방 (을)", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.5, "y": 3.42, "w": 1.2, "h": 0.15, "text": "박을동", "fontSize": 8, "align": "left"}, + {"type": "rect", "x": 2.5, "y": 3.6, "w": 1.2, "h": 0.15, "text": "park@corp.com", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 4.0, "y": 3.45, "w": 0.6, "h": 0.2, "text": "→", "fontSize": 14, "color": "64748b", "align": "center"}, + {"type": "rect", "x": 4.8, "y": 3.15, "w": 1.8, "h": 0.7, "fill": "e0f2fe"}, + {"type": "rect", "x": 4.9, "y": 3.2, "w": 0.3, "h": 0.25, "fill": "3b82f6", "text": "2", "color": "FFFFFF", "fontSize": 10, "bold": true, "align": "center"}, + {"type": "rect", "x": 5.3, "y": 3.2, "w": 1.2, "h": 0.18, "text": "작성자 (갑)", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.3, "y": 3.42, "w": 1.2, "h": 0.15, "text": "김갑순", "fontSize": 8, "align": "left"}, + {"type": "rect", "x": 5.3, "y": 3.6, "w": 1.2, "h": 0.15, "text": "kim@sam.kr", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.5, "y": 4.3, "w": 3.5, "h": 0.4, "fill": "0d9488", "text": "서명 요청 발송", "color": "FFFFFF", "fontSize": 11, "bold": true, "align": "center"} + ] + }, + { + "taskName": "본인인증 (OTP)", + "route": "/esign/sign/{token}", + "screenName": "본인인증 (서명자용)", + "screenId": "ES_AUTH", + "descriptions": [ + { "title": "계약 정보 확인", "content": "계약 제목, 서명자 이름/이메일, 서명 기한 표시. 토큰 기반 조회 (비로그인)", "markerX": 2.8, "markerY": 1.8 }, + { "title": "OTP 발송 버튼", "content": "등록된 이메일로 6자리 인증 코드 발송. 5분 유효, 최대 5회 시도", "markerX": 2.8, "markerY": 3.0 }, + { "title": "OTP 입력 폼", "content": "6자리 숫자 입력 (대형 폰트). 인증 성공 시 sign_session_token 발급 → 서명 화면 이동", "markerX": 2.8, "markerY": 3.7 } + ], + "wireframeElements": [ + {"type": "rect", "x": 2.3, "y": 1.15, "w": 4.0, "h": 0.4, "text": "SAM E-Sign", "fontSize": 16, "bold": true, "align": "center"}, + {"type": "rect", "x": 2.3, "y": 1.5, "w": 4.0, "h": 0.2, "text": "전자계약 서명", "fontSize": 9, "color": "64748b", "align": "center"}, + {"type": "rect", "x": 2.5, "y": 1.9, "w": 3.6, "h": 3.0, "fill": "FFFFFF"}, + {"type": "rect", "x": 2.6, "y": 1.95, "w": 3.4, "h": 0.25, "text": "계약 정보 확인", "fontSize": 10, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.6, "y": 2.25, "w": 3.4, "h": 0.35, "fill": "f8fafc"}, + {"type": "rect", "x": 2.7, "y": 2.27, "w": 0.8, "h": 0.12, "text": "계약 제목", "fontSize": 6, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.7, "y": 2.4, "w": 3.2, "h": 0.15, "text": "소프트웨어 개발 용역 계약서", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.6, "y": 2.65, "w": 3.4, "h": 0.35, "fill": "f8fafc"}, + {"type": "rect", "x": 2.7, "y": 2.67, "w": 0.8, "h": 0.12, "text": "서명자", "fontSize": 6, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.7, "y": 2.8, "w": 3.2, "h": 0.15, "text": "박을동 (park@corp.com)", "fontSize": 8, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.6, "y": 3.05, "w": 3.4, "h": 0.35, "fill": "f8fafc"}, + {"type": "rect", "x": 2.7, "y": 3.07, "w": 0.8, "h": 0.12, "text": "서명 기한", "fontSize": 6, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.7, "y": 3.2, "w": 3.2, "h": 0.15, "text": "2026-02-19", "fontSize": 8, "bold": true, "color": "dc2626", "align": "left"}, + {"type": "rect", "x": 2.6, "y": 3.5, "w": 3.4, "h": 0.3, "text": "본인인증을 위해 등록된 이메일로 인증 코드를 발송합니다.", "fontSize": 8, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.6, "y": 3.85, "w": 3.4, "h": 0.4, "fill": "3b82f6", "text": "인증 코드 발송", "color": "FFFFFF", "fontSize": 10, "bold": true, "align": "center"}, + {"type": "rect", "x": 2.6, "y": 4.35, "w": 3.4, "h": 0.2, "text": "OTP 입력 화면 (발송 후 전환)", "fontSize": 7, "color": "94a3b8", "align": "center"}, + {"type": "rect", "x": 3.2, "y": 4.55, "w": 2.2, "h": 0.3, "fill": "f1f5f9", "text": "4 8 2 9 1 7", "fontSize": 14, "align": "center"} + ] + }, + { + "taskName": "서명 수행", + "route": "/esign/sign/{token}/sign", + "screenName": "전자서명 수행 (서명자용)", + "screenId": "ES_SIGN", + "descriptions": [ + { "title": "문서 확인 단계", "content": "계약서 PDF 다운로드 링크 제공. 동의 체크박스로 전자서명 법적 효력 동의 확인", "markerX": 2.8, "markerY": 1.8 }, + { "title": "서명 입력 (SignaturePad)", "content": "캔버스에 터치/마우스로 서명 입력. '지우기' 버튼으로 초기화. 3단계: 문서확인→서명→확인", "markerX": 2.8, "markerY": 2.8 }, + { "title": "서명 확인/제출", "content": "입력된 서명 미리보기. '다시 서명' 또는 '서명 제출' 선택. base64 PNG로 전송", "markerX": 2.8, "markerY": 3.8 }, + { "title": "거절 기능", "content": "상단 '거절' 버튼으로 서명 거절 가능. 거절 사유 입력 필수", "markerX": 5.5, "markerY": 1.3 } + ], + "wireframeElements": [ + {"type": "rect", "x": 1.6, "y": 1.2, "w": 5.4, "h": 0.4, "fill": "FFFFFF"}, + {"type": "rect", "x": 1.7, "y": 1.25, "w": 3.0, "h": 0.15, "text": "소프트웨어 개발 용역 계약서", "fontSize": 9, "bold": true, "align": "left"}, + {"type": "rect", "x": 1.7, "y": 1.42, "w": 2.0, "h": 0.12, "text": "박을동 님의 전자서명", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 6.2, "y": 1.25, "w": 0.7, "h": 0.28, "fill": "FFFFFF", "text": "거절", "fontSize": 8, "color": "dc2626", "align": "center"}, + {"type": "rect", "x": 2.3, "y": 1.8, "w": 4.0, "h": 1.5, "fill": "FFFFFF"}, + {"type": "rect", "x": 2.4, "y": 1.85, "w": 2.0, "h": 0.22, "text": "계약 문서 확인", "fontSize": 10, "bold": true, "align": "left"}, + {"type": "rect", "x": 2.5, "y": 2.15, "w": 3.8, "h": 0.5, "fill": "f1f5f9"}, + {"type": "rect", "x": 3.5, "y": 2.2, "w": 1.5, "h": 0.15, "text": "PDF 문서", "fontSize": 8, "color": "64748b", "align": "center"}, + {"type": "rect", "x": 3.3, "y": 2.4, "w": 1.8, "h": 0.25, "fill": "3b82f6", "text": "문서 열기 / 다운로드", "color": "FFFFFF", "fontSize": 8, "align": "center"}, + {"type": "rect", "x": 2.5, "y": 2.75, "w": 0.2, "h": 0.2, "fill": "3b82f6"}, + {"type": "rect", "x": 2.75, "y": 2.75, "w": 3.5, "h": 0.35, "text": "위 계약서의 내용을 확인하였으며, 전자서명에\n동의합니다. 전자서명은 법적 효력을 가집니다.", "fontSize": 7, "align": "left"}, + {"type": "rect", "x": 2.3, "y": 3.35, "w": 4.0, "h": 1.5, "fill": "FFFFFF"}, + {"type": "rect", "x": 2.4, "y": 3.4, "w": 1.5, "h": 0.22, "text": "서명 입력", "fontSize": 10, "bold": true, "align": "left"}, + {"type": "rect", "x": 5.5, "y": 3.4, "w": 0.7, "h": 0.2, "text": "지우기", "fontSize": 8, "color": "64748b", "align": "right"}, + {"type": "rect", "x": 2.4, "y": 3.65, "w": 3.0, "h": 0.15, "text": "아래 영역에 서명을 입력해 주세요.", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 2.5, "y": 3.85, "w": 3.6, "h": 0.9, "fill": "FFFFFF"}, + {"type": "rect", "x": 2.3, "y": 4.85, "w": 1.9, "h": 0.3, "fill": "e2e8f0", "text": "이전", "fontSize": 9, "align": "center"}, + {"type": "rect", "x": 4.3, "y": 4.85, "w": 2.0, "h": 0.3, "fill": "3b82f6", "text": "서명 확인", "color": "FFFFFF", "fontSize": 9, "bold": true, "align": "center"} + ] + }, + { + "taskName": "서명 완료", + "route": "/esign/sign/{token}/done", + "screenName": "서명 완료 (서명자용)", + "screenId": "ES_DONE", + "descriptions": [ + { "title": "완료 상태 표시", "content": "서명 완료 시 녹색 체크 아이콘 + 메시지. 모든 서명 완료 시 계약 체결 안내", "markerX": 3.5, "markerY": 2.0 }, + { "title": "거절 상태 표시", "content": "서명 거절 시 빨간 X 아이콘 + 메시지. 계약 담당자 알림 발송 안내", "markerX": 3.5, "markerY": 2.8 }, + { "title": "계약 요약 정보", "content": "계약 제목, 서명자 이름, 서명 일시를 카드 형태로 표시", "markerX": 3.5, "markerY": 3.5 } + ], + "wireframeElements": [ + {"type": "rect", "x": 2.8, "y": 1.2, "w": 3.0, "h": 0.35, "text": "SAM E-Sign", "fontSize": 16, "bold": true, "align": "center"}, + {"type": "rect", "x": 2.8, "y": 1.7, "w": 3.0, "h": 3.0, "fill": "FFFFFF"}, + {"type": "rect", "x": 3.8, "y": 1.85, "w": 0.8, "h": 0.8, "fill": "dcfce7"}, + {"type": "rect", "x": 3.95, "y": 2.0, "w": 0.5, "h": 0.5, "text": "✓", "fontSize": 22, "color": "16a34a", "bold": true, "align": "center"}, + {"type": "rect", "x": 2.9, "y": 2.75, "w": 2.8, "h": 0.25, "text": "서명이 완료되었습니다", "fontSize": 12, "bold": true, "align": "center"}, + {"type": "rect", "x": 2.9, "y": 3.05, "w": 2.8, "h": 0.35, "text": "서명이 정상적으로 접수되었습니다.\n다른 서명자의 서명이 완료되면\n알려드리겠습니다.", "fontSize": 8, "color": "64748b", "align": "center"}, + {"type": "rect", "x": 3.0, "y": 3.55, "w": 2.6, "h": 0.8, "fill": "f8fafc"}, + {"type": "rect", "x": 3.1, "y": 3.6, "w": 0.5, "h": 0.15, "text": "계약", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 4.0, "y": 3.6, "w": 1.5, "h": 0.15, "text": "소프트웨어 개발 용역", "fontSize": 8, "bold": true, "align": "right"}, + {"type": "rect", "x": 3.1, "y": 3.8, "w": 0.5, "h": 0.15, "text": "서명자", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 4.0, "y": 3.8, "w": 1.5, "h": 0.15, "text": "박을동", "fontSize": 8, "bold": true, "align": "right"}, + {"type": "rect", "x": 3.1, "y": 4.0, "w": 0.5, "h": 0.15, "text": "서명일시", "fontSize": 7, "color": "64748b", "align": "left"}, + {"type": "rect", "x": 4.0, "y": 4.0, "w": 1.5, "h": 0.15, "text": "2026-02-12 11:23", "fontSize": 8, "bold": true, "align": "right"}, + {"type": "rect", "x": 3.2, "y": 4.55, "w": 2.2, "h": 0.2, "text": "SAM E-Sign 전자계약 서명 시스템", "fontSize": 7, "color": "94a3b8", "align": "center"} + ] + } + ] +} diff --git a/projects/e-sign/technical-design.md b/projects/e-sign/technical-design.md new file mode 100644 index 0000000..99f81e3 --- /dev/null +++ b/projects/e-sign/technical-design.md @@ -0,0 +1,950 @@ +# 전자계약 서명 솔루션 (E-Sign) - 기술 설계 문서 + +> **프로젝트명**: SAM E-Sign (가칭) +> **작성일**: 2026-02-12 +> **버전**: v1.0 Draft +> **작성자**: DX 추진팀 + +--- + +## 1. 프로젝트 개요 + +### 1.1 목적 + +모두싸인과 유사한 **간편 전자계약 서명 솔루션**을 자체 구축한다. +두 당사자(계약 생성자 A, 상대방 B)가 온라인으로 계약서에 서명하고, +서명 완료된 문서를 법적 효력이 있는 형태로 보관하는 시스템이다. + +### 1.2 핵심 가치 + +| 가치 | 설명 | +|------|------| +| **간편함** | PDF 업로드 → 서명 위치 지정 → 링크 발송, 3단계 완료 | +| **보안** | 문서 해시 검증, 본인인증, 감사 추적(Audit Trail) | +| **법적 효력** | 전자서명법 제2조에 부합하는 전자서명 요건 충족 | + +### 1.3 범위 (v1) + +| 포함 | 미포함 (v2 이후) | +|------|------------------| +| 2인 서명 (생성자/상대방) | N명 다자간 서명 | +| PDF 문서 기반 | 워드/한글 문서 직접 편집 | +| 이메일 OTP 인증 | 카카오/PASS 본인인증 | +| 순차 서명 (A→B 또는 B→A) | 동시 서명 | +| 캔버스 직접 서명 | 공인인증서 연동 | +| 감사 추적 로그 | 블록체인 기반 공증 | +| 완료 문서 PDF 다운로드 | API 외부 연동 | + +### 1.4 기술 스택 + +| 영역 | 기술 | 비고 | +|------|------|------| +| Backend | Laravel 11 (PHP 8.3) | SAM API 프로젝트 확장 | +| Frontend | HTMX + Alpine.js + Tailwind CSS | SAM MNG 프로젝트 확장 | +| Database | MySQL 8.0 | 기존 SAM DB 공유 | +| PDF 렌더링 | pdf.js (프론트) | 브라우저 PDF 표시 | +| 서명 캡처 | signature_pad.js | 터치/마우스 서명 | +| PDF 합성 | FPDI + FPDF (백엔드) | 원본 PDF에 서명 삽입 | +| 파일 저장 | Laravel Storage (local/S3) | 암호화 저장 | +| 알림 | Laravel Notification (Mail) | 이메일 발송 | + +--- + +## 2. 시스템 아키텍처 + +### 2.1 전체 구조 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 사용자 (브라우저) │ +├────────────────────────┬────────────────────────────────────────┤ +│ 계약 생성자 (A) │ 상대방 (B) │ +│ - 로그인 사용자 │ - 비로그인 (토큰 기반 접근) │ +│ - 계약서 업로드 │ - 이메일 링크로 접속 │ +│ - 서명 위치 지정 │ - 본인인증 후 서명 │ +└────────┬───────────────┴──────────────────┬─────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Nginx (sam-nginx-1) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────────────┐ │ +│ │ MNG (sam-mng-1) │ │ API (sam-api-1) │ │ +│ │ │ │ │ │ +│ │ - 계약 관리 화면 │ │ - 계약 CRUD API │ │ +│ │ - PDF 뷰어/서명 UI │ │ - 서명 처리 API │ │ +│ │ - 대시보드 │ │ - 인증 API (OTP) │ │ +│ │ │ │ - PDF 합성 서비스 │ │ +│ │ HTMX + Alpine.js │ │ - 알림 서비스 (이메일) │ │ +│ │ + pdf.js │ │ - 감사 로그 서비스 │ │ +│ │ + signature_pad │ │ - 문서 해시 검증 서비스 │ │ +│ └──────────┬───────────┘ └──────────────┬─────────────────┘ │ +│ │ │ │ +│ └────────────┬───────────────────┘ │ +│ ▼ │ +│ ┌──────────────────────────┐ │ +│ │ MySQL (sam-mysql-1) │ │ +│ │ │ │ +│ │ - contracts │ │ +│ │ - contract_signers │ │ +│ │ - contract_sign_fields │ │ +│ │ - contract_audit_logs │ │ +│ └──────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────┐ │ +│ │ File Storage │ │ +│ │ - 원본 PDF (암호화) │ │ +│ │ - 서명 이미지 │ │ +│ │ - 완료 PDF │ │ +│ └──────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 서비스 레이어 구조 + +``` +app/Services/ESign/ +├── ContractService.php # 계약 CRUD, 상태 관리 +├── SignatureService.php # 서명 처리, 이미지 저장 +├── PdfService.php # PDF 합성, 해시 생성/검증 +├── AuthenticationService.php # OTP 생성/검증, 토큰 관리 +├── NotificationService.php # 이메일 발송 (서명 요청, 완료 알림) +└── AuditLogService.php # 감사 추적 로그 기록 +``` + +--- + +## 3. 핵심 플로우 + +### 3.1 계약 생성 플로우 + +``` +[A: 계약 생성자] + +1. 로그인 상태에서 "새 계약" 클릭 +2. 계약 정보 입력 + - 계약 제목 + - 계약 설명 (선택) + - 서명 기한 (기본 7일) +3. PDF 파일 업로드 + → 서버: 파일 저장 + SHA-256 해시 생성 +4. 서명 위치 지정 화면으로 이동 + → pdf.js로 PDF 렌더링 + → 드래그&드롭으로 서명란 배치 + → A의 서명란 (파란색) + → B의 서명란 (빨간색) + → 날짜 필드, 텍스트 필드 추가 가능 +5. 상대방(B) 정보 입력 + - 이름 + - 이메일 (필수) + - 전화번호 (선택) +6. 서명 순서 선택 + - B 먼저 → A 확인 서명 + - A 먼저 → B 확인 서명 +7. "서명 요청 발송" 클릭 + → 서버: contract 상태를 'pending'으로 변경 + → 서버: 상대방에게 이메일 발송 (서명 링크 포함) + → 감사 로그: 'contract_created', 'sign_requested' +``` + +### 3.2 서명 수행 플로우 (상대방 B) + +``` +[B: 서명 상대방] + +1. 이메일에서 서명 링크 클릭 + → URL: /esign/sign/{access_token} + → 서버: 토큰 유효성 검증 (만료, 사용 여부) +2. 본인인증 게이트 + - 이메일로 6자리 OTP 발송 + - OTP 입력 (3회 제한, 5분 유효) + → 서버: 인증 성공 시 세션에 verified 상태 저장 + → 감사 로그: 'identity_verified' +3. 계약서 열람 + → pdf.js로 PDF 렌더링 + → 서명이 필요한 위치에 하이라이트 표시 + → 감사 로그: 'document_viewed' +4. 서명 수행 + - 서명란 클릭 → 캔버스 서명 모달 팝업 + - 터치/마우스로 서명 + - "서명 완료" 클릭 + → 서버: 서명 이미지 저장 (PNG, base64→file) + → 서버: 서명 시각, IP, User-Agent 기록 + → 감사 로그: 'signed' +5. 동의 확인 + - [✓] 본 계약서의 내용을 확인하였으며 서명에 동의합니다 + - [✓] 전자서명의 법적 효력에 동의합니다 + - "최종 제출" 클릭 + → 서버: signer 상태를 'signed'로 변경 + → 서버: 다음 서명자(A)에게 알림 발송 + → 감사 로그: 'consent_agreed', 'submission_completed' +``` + +### 3.3 최종 서명 플로우 (생성자 A) + +``` +[A: 계약 생성자 - 최종 서명] + +1. 알림 수신 (이메일 또는 대시보드) + "상대방 OOO님이 서명을 완료했습니다" +2. 본인인증 (동일 OTP 절차) +3. 계약서 열람 (B의 서명이 표시된 상태) +4. A의 서명란에 서명 +5. 최종 제출 + → 서버: contract 상태를 'completed'로 변경 + → 서버: PDF 합성 (원본 + A서명 + B서명 + 감사정보) + → 서버: 완료 PDF에 SHA-256 해시 생성 + → 서버: 양쪽에 완료 알림 + PDF 다운로드 링크 발송 + → 감사 로그: 'contract_completed' +``` + +### 3.4 상태 전이 다이어그램 + +``` + ┌───────┐ + │ draft │ 계약서 작성 중 (저장만, 발송 전) + └───┬───┘ + │ 서명 요청 발송 + ▼ + ┌─────────┐ + │ pending │ 서명 요청됨, 첫 서명자 대기 + └────┬────┘ + │ 첫 번째 서명 완료 + ▼ + ┌────────────────────┐ + │ partially_signed │ 한쪽만 서명 완료 + └─────────┬──────────┘ + │ 두 번째 서명 완료 + ▼ + ┌───────────┐ + │ completed │ 양쪽 서명 완료, PDF 합성됨 + └───────────┘ + + [만료 시] + pending / partially_signed ──→ expired (서명 기한 초과) + + [취소 시] + draft / pending ──→ cancelled (생성자가 취소) + + [거절 시] + pending / partially_signed ──→ rejected (서명자가 거절) +``` + +--- + +## 4. 데이터베이스 스키마 + +### 4.1 ER 다이어그램 (텍스트) + +``` +contracts (1) ──── (N) contract_signers + │ │ + │ │ + (1) (1) + │ │ + (N) (N) +contract_sign_fields contract_audit_logs +``` + +### 4.2 contracts (계약서) + +```sql +CREATE TABLE contracts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + + -- 계약 정보 + contract_code VARCHAR(30) NOT NULL UNIQUE, -- 'ESIGN-2026-000001' + title VARCHAR(255) NOT NULL, -- 계약 제목 + description TEXT NULL, -- 계약 설명 + sign_order_type ENUM('counterpart_first', 'creator_first') DEFAULT 'counterpart_first', + + -- 문서 파일 + original_file_path VARCHAR(500) NOT NULL, -- 원본 PDF 경로 (암호화 저장) + original_file_name VARCHAR(255) NOT NULL, -- 원본 파일명 + original_file_hash VARCHAR(64) NOT NULL, -- SHA-256 해시 + original_file_size INT UNSIGNED NOT NULL, -- 파일 크기 (bytes) + signed_file_path VARCHAR(500) NULL, -- 서명 완료 PDF 경로 + signed_file_hash VARCHAR(64) NULL, -- 서명 완료 PDF 해시 + + -- 상태 + status ENUM('draft', 'pending', 'partially_signed', 'completed', 'expired', 'cancelled', 'rejected') + DEFAULT 'draft', + expires_at DATETIME NOT NULL, -- 서명 기한 + completed_at DATETIME NULL, -- 완료 시각 + + -- 생성자 + created_by BIGINT UNSIGNED NOT NULL, -- 생성자 user_id + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, -- soft delete + + INDEX idx_tenant_status (tenant_id, status), + INDEX idx_created_by (created_by), + INDEX idx_expires_at (expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 4.3 contract_signers (서명자) + +```sql +CREATE TABLE contract_signers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + contract_id BIGINT UNSIGNED NOT NULL, + + -- 서명자 정보 + role ENUM('creator', 'counterpart') NOT NULL, + sign_order TINYINT UNSIGNED NOT NULL DEFAULT 1, -- 서명 순서 (1 or 2) + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(20) NULL, + + -- 접근 토큰 + access_token VARCHAR(128) NOT NULL UNIQUE, -- 서명 링크용 1회성 토큰 + token_expires_at DATETIME NOT NULL, -- 토큰 만료 시각 + + -- 인증 정보 + otp_code VARCHAR(10) NULL, -- OTP 코드 (해시 저장) + otp_expires_at DATETIME NULL, -- OTP 만료 시각 + otp_attempts TINYINT UNSIGNED DEFAULT 0, -- OTP 시도 횟수 + auth_verified_at DATETIME NULL, -- 본인인증 완료 시각 + auth_method VARCHAR(20) DEFAULT 'email_otp', -- 인증 방식 + + -- 서명 정보 + signature_image_path VARCHAR(500) NULL, -- 서명 이미지 경로 + signed_at DATETIME NULL, -- 서명 시각 + consent_agreed_at DATETIME NULL, -- 동의 시각 + + -- 서명 시점 환경 정보 + sign_ip_address VARCHAR(45) NULL, -- IPv4/IPv6 + sign_user_agent VARCHAR(500) NULL, -- 브라우저 정보 + + -- 상태 + status ENUM('waiting', 'notified', 'authenticated', 'signed', 'rejected') + DEFAULT 'waiting', + rejected_reason TEXT NULL, -- 거절 사유 + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, + INDEX idx_access_token (access_token), + INDEX idx_contract_role (contract_id, role) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 4.4 contract_sign_fields (서명 위치/필드) + +```sql +CREATE TABLE contract_sign_fields ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + contract_id BIGINT UNSIGNED NOT NULL, + signer_id BIGINT UNSIGNED NOT NULL, -- contract_signers.id + + -- 위치 정보 + page_number INT UNSIGNED NOT NULL, -- PDF 페이지 번호 (1부터) + position_x DECIMAL(8,2) NOT NULL, -- X 좌표 (pt 단위) + position_y DECIMAL(8,2) NOT NULL, -- Y 좌표 (pt 단위) + width DECIMAL(8,2) NOT NULL, -- 너비 (pt) + height DECIMAL(8,2) NOT NULL, -- 높이 (pt) + + -- 필드 정보 + field_type ENUM('signature', 'stamp', 'text', 'date', 'checkbox') NOT NULL DEFAULT 'signature', + field_label VARCHAR(100) NULL, -- 필드 라벨 (예: "갑 서명") + field_value TEXT NULL, -- 입력된 값 (텍스트/날짜) + is_required BOOLEAN DEFAULT TRUE, + + sort_order INT UNSIGNED DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, + FOREIGN KEY (signer_id) REFERENCES contract_signers(id) ON DELETE CASCADE, + INDEX idx_contract_page (contract_id, page_number) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 4.5 contract_audit_logs (감사 추적 로그) + +```sql +CREATE TABLE contract_audit_logs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + contract_id BIGINT UNSIGNED NOT NULL, + signer_id BIGINT UNSIGNED NULL, -- NULL이면 시스템 이벤트 + + -- 이벤트 정보 + action VARCHAR(50) NOT NULL, + -- 가능한 값: + -- 'contract_created' : 계약서 생성 + -- 'document_uploaded' : PDF 업로드 + -- 'fields_configured' : 서명 위치 설정 + -- 'sign_requested' : 서명 요청 발송 + -- 'link_accessed' : 서명 링크 접속 + -- 'otp_sent' : OTP 발송 + -- 'otp_verified' : OTP 인증 성공 + -- 'otp_failed' : OTP 인증 실패 + -- 'document_viewed' : 계약서 열람 + -- 'signed' : 서명 수행 + -- 'consent_agreed' : 동의 체크 + -- 'submission_completed' : 최종 제출 + -- 'contract_completed' : 계약 완료 (양쪽 서명) + -- 'pdf_generated' : 완료 PDF 생성 + -- 'document_downloaded' : 문서 다운로드 + -- 'contract_cancelled' : 계약 취소 + -- 'contract_rejected' : 서명 거절 + -- 'contract_expired' : 계약 만료 + -- 'reminder_sent' : 리마인더 발송 + + -- 환경 정보 + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(500) NULL, + metadata JSON NULL, -- 추가 데이터 (유연한 확장) + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE, + INDEX idx_contract_action (contract_id, action), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +--- + +## 5. API 명세 + +### 5.1 계약 관리 API + +#### 계약 목록 조회 +``` +GET /api/v1/esign/contracts +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| page | int | N | 페이지 번호 (기본 1) | +| size | int | N | 페이지 크기 (기본 20) | +| status | string | N | 상태 필터 | +| search | string | N | 제목 검색 | +| date_from | string | N | 시작일 | +| date_to | string | N | 종료일 | + +**Response 200:** +```json +{ + "data": [ + { + "id": 1, + "contract_code": "ESIGN-2026-000001", + "title": "소프트웨어 개발 용역 계약서", + "status": "pending", + "signers": [ + { "name": "김갑순", "role": "creator", "status": "waiting" }, + { "name": "박을동", "role": "counterpart", "status": "notified" } + ], + "expires_at": "2026-02-19T23:59:59", + "created_at": "2026-02-12T10:00:00" + } + ], + "meta": { "total": 25, "page": 1, "size": 20 } +} +``` + +#### 계약 생성 +``` +POST /api/v1/esign/contracts +Content-Type: multipart/form-data +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| title | string | Y | 계약 제목 | +| description | string | N | 계약 설명 | +| document | file | Y | PDF 파일 (max 20MB) | +| expires_days | int | N | 서명 기한 일수 (기본 7) | +| sign_order_type | string | N | 'counterpart_first' 또는 'creator_first' | +| counterpart_name | string | Y | 상대방 이름 | +| counterpart_email | string | Y | 상대방 이메일 | +| counterpart_phone | string | N | 상대방 전화번호 | + +**Response 201:** +```json +{ + "data": { + "id": 1, + "contract_code": "ESIGN-2026-000001", + "status": "draft", + "original_file_hash": "a1b2c3d4e5f6...", + "signers": [ + { "id": 1, "role": "creator", "name": "김갑순" }, + { "id": 2, "role": "counterpart", "name": "박을동" } + ] + } +} +``` + +#### 계약 상세 조회 +``` +GET /api/v1/esign/contracts/{id} +``` + +#### 계약 취소 +``` +POST /api/v1/esign/contracts/{id}/cancel +``` + +#### 계약 통계 +``` +GET /api/v1/esign/contracts/stats +``` + +**Response 200:** +```json +{ + "data": { + "total": 50, + "draft": 3, + "pending": 10, + "partially_signed": 5, + "completed": 28, + "expired": 3, + "cancelled": 1 + } +} +``` + +### 5.2 서명 필드 API + +#### 서명 위치 설정 +``` +POST /api/v1/esign/contracts/{id}/fields +Content-Type: application/json +``` + +```json +{ + "fields": [ + { + "signer_id": 1, + "page_number": 3, + "position_x": 120.5, + "position_y": 650.0, + "width": 150, + "height": 60, + "field_type": "signature", + "field_label": "갑 (생성자) 서명", + "is_required": true + }, + { + "signer_id": 2, + "page_number": 3, + "position_x": 350.5, + "position_y": 650.0, + "width": 150, + "height": 60, + "field_type": "signature", + "field_label": "을 (상대방) 서명", + "is_required": true + }, + { + "signer_id": 1, + "page_number": 3, + "position_x": 120.5, + "position_y": 720.0, + "width": 100, + "height": 25, + "field_type": "date", + "field_label": "서명일", + "is_required": true + } + ] +} +``` + +#### 서명 위치 조회 +``` +GET /api/v1/esign/contracts/{id}/fields +``` + +### 5.3 서명 요청 API + +#### 서명 요청 발송 +``` +POST /api/v1/esign/contracts/{id}/send +``` +> 상대방에게 이메일 발송, 상태를 `pending`으로 변경 + +#### 리마인더 발송 +``` +POST /api/v1/esign/contracts/{id}/remind +``` + +### 5.4 서명 수행 API (토큰 기반, 비로그인) + +#### 서명 페이지 접속 +``` +GET /api/v1/esign/sign/{access_token} +``` +> 토큰 검증 → 계약 정보 + 서명 필드 반환 + +#### OTP 발송 요청 +``` +POST /api/v1/esign/sign/{access_token}/otp/send +``` + +**Response 200:** +```json +{ + "message": "인증코드가 이메일로 발송되었습니다", + "expires_in": 300, + "remaining_attempts": 3 +} +``` + +#### OTP 인증 +``` +POST /api/v1/esign/sign/{access_token}/otp/verify +``` + +```json +{ + "otp_code": "482917" +} +``` + +**Response 200 (성공):** +```json +{ + "verified": true, + "sign_session_token": "eyJ..." +} +``` + +**Response 401 (실패):** +```json +{ + "verified": false, + "remaining_attempts": 2, + "message": "인증코드가 일치하지 않습니다" +} +``` + +#### 서명 제출 +``` +POST /api/v1/esign/sign/{access_token}/submit +Authorization: Bearer {sign_session_token} +Content-Type: application/json +``` + +```json +{ + "signatures": [ + { + "field_id": 1, + "signature_image": "data:image/png;base64,iVBORw0KGgo...", + "field_type": "signature" + }, + { + "field_id": 3, + "field_value": "2026-02-12", + "field_type": "date" + } + ], + "consent_electronic_signature": true, + "consent_contract_content": true +} +``` + +#### 서명 거절 +``` +POST /api/v1/esign/sign/{access_token}/reject +``` + +```json +{ + "reason": "계약 조건 수정이 필요합니다" +} +``` + +### 5.5 문서 API + +#### 원본 PDF 조회 (인증 후) +``` +GET /api/v1/esign/sign/{access_token}/document +Authorization: Bearer {sign_session_token} +``` +> Content-Type: application/pdf + +#### 완료 PDF 다운로드 +``` +GET /api/v1/esign/contracts/{id}/download +``` +> 로그인 사용자만 접근, 완료 상태인 계약만 + +#### 문서 무결성 검증 +``` +GET /api/v1/esign/contracts/{id}/verify +``` + +**Response 200:** +```json +{ + "original_hash": "a1b2c3...", + "signed_hash": "d4e5f6...", + "original_integrity": true, + "signed_integrity": true, + "verification_time": "2026-02-12T15:30:00" +} +``` + +### 5.6 감사 로그 API + +#### 감사 로그 조회 +``` +GET /api/v1/esign/contracts/{id}/audit-logs +``` + +**Response 200:** +```json +{ + "data": [ + { + "action": "contract_created", + "signer_name": "김갑순", + "ip_address": "192.168.1.100", + "created_at": "2026-02-12T10:00:00" + }, + { + "action": "sign_requested", + "signer_name": null, + "metadata": { "sent_to": "park@example.com" }, + "created_at": "2026-02-12T10:05:00" + } + ] +} +``` + +--- + +## 6. 보안 설계 + +### 6.1 문서 무결성 (Document Integrity) + +```php +// 업로드 시 해시 생성 +$hash = hash_file('sha256', $uploadedFile->getRealPath()); + +// 검증 시 해시 비교 +$currentHash = hash_file('sha256', Storage::path($contract->original_file_path)); +$isValid = hash_equals($contract->original_file_hash, $currentHash); +``` + +- 원본 PDF 업로드 시 SHA-256 해시 생성 및 DB 저장 +- 서명 완료 PDF 생성 시에도 별도 해시 저장 +- 문서 다운로드/열람 시 해시 비교로 위변조 여부 확인 + +### 6.2 서명자 인증 (Signer Authentication) + +``` +인증 플로우: +1. 서명 링크 접속 (access_token 검증) +2. OTP 발송 (이메일) +3. OTP 입력 (6자리, 5분 유효, 3회 제한) +4. 인증 성공 → sign_session_token 발급 (JWT, 30분 유효) +5. 이후 모든 서명 API 호출 시 sign_session_token 필요 +``` + +**OTP 보안 규칙:** + +| 규칙 | 값 | 설명 | +|------|-----|------| +| OTP 길이 | 6자리 숫자 | 무작위 생성 | +| 유효 시간 | 5분 | 초과 시 재발송 필요 | +| 시도 제한 | 3회 | 초과 시 토큰 무효화 | +| 재발송 간격 | 60초 | 연속 발송 방지 | +| 저장 방식 | bcrypt 해시 | DB에 평문 저장 금지 | + +### 6.3 접근 제어 (Access Control) + +**토큰 정책:** + +| 토큰 | 용도 | 유효기간 | 특성 | +|------|------|---------|------| +| access_token | 서명 링크 URL | 계약 만료일까지 | 128자 랜덤, URL-safe | +| sign_session_token | OTP 인증 후 세션 | 30분 | JWT, 갱신 불가 | + +**접근 규칙:** +- 서명 링크는 해당 서명자만 접근 가능 (토큰 + 이메일 검증) +- 완료/취소/만료 상태 계약은 서명 접근 차단 +- 서명 순서가 아닌 서명자는 대기 화면 표시 +- 모든 API 호출 시 IP/UA 기록 + +### 6.4 감사 추적 (Audit Trail) + +모든 주요 행위를 `contract_audit_logs`에 기록: + +``` +기록 대상 행위: +- 계약서 생성/수정/삭제 +- PDF 업로드 +- 서명 요청 발송 +- 서명 링크 접속 (성공/실패) +- OTP 발송/검증 (성공/실패) +- 계약서 열람 +- 서명 수행 +- 동의 체크 +- 문서 다운로드 +- 계약 취소/거절/만료 +``` + +**감사 로그는 삭제 불가** (soft delete 미적용) + +### 6.5 파일 보안 + +``` +저장 구조: +storage/app/esign/ +├── originals/ # 원본 PDF (AES-256 암호화) +│ └── {contract_id}/ +│ └── {hash}.pdf.enc +├── signatures/ # 서명 이미지 +│ └── {signer_id}/ +│ └── {timestamp}.png +└── completed/ # 완료 PDF + └── {contract_id}/ + └── {contract_code}_signed.pdf +``` + +- 원본 PDF는 AES-256으로 암호화 저장 +- 서명 이미지는 별도 디렉토리에 격리 +- 완료 PDF는 감사 증적 페이지를 포함하여 생성 +- 파일 경로에 직접 접근 불가 (Controller를 통한 스트리밍만 허용) + +### 6.6 완료 PDF에 포함되는 감사 정보 + +서명 완료 PDF의 마지막 페이지에 자동 추가: + +``` +┌──────────────────────────────────────────┐ +│ 전자서명 감사 증적 (Audit Trail) │ +├──────────────────────────────────────────┤ +│ 계약 번호: ESIGN-2026-000001 │ +│ 문서 해시: a1b2c3d4e5f6... │ +│ │ +│ [서명자 A - 갑] │ +│ 이름: 김갑순 │ +│ 인증 방식: 이메일 OTP │ +│ 인증 시각: 2026-02-12 14:30:22 KST │ +│ 서명 시각: 2026-02-12 14:32:15 KST │ +│ IP: 203.xxx.xxx.100 │ +│ │ +│ [서명자 B - 을] │ +│ 이름: 박을동 │ +│ 인증 방식: 이메일 OTP │ +│ 인증 시각: 2026-02-12 11:20:05 KST │ +│ 서명 시각: 2026-02-12 11:23:41 KST │ +│ IP: 121.xxx.xxx.55 │ +│ │ +│ 계약 완료: 2026-02-12 14:32:15 KST │ +│ 본 문서는 전자서명법에 의거하여 │ +│ 법적 효력을 가집니다. │ +└──────────────────────────────────────────┘ +``` + +--- + +## 7. 화면 목록 + +### 7.1 계약 생성자(A) 화면 + +| # | 화면ID | 화면명 | 경로 | 설명 | +|---|--------|--------|------|------| +| 1 | ES_DASH | 대시보드 | /esign | 계약 현황 통계 + 목록 | +| 2 | ES_CREATE | 계약 생성 | /esign/create | PDF 업로드 + 정보 입력 | +| 3 | ES_FIELDS | 서명 위치 지정 | /esign/{id}/fields | PDF 위에 서명란 배치 | +| 4 | ES_SEND | 서명 요청 발송 | /esign/{id}/send | 상대방 정보 입력 + 발송 | +| 5 | ES_DETAIL | 계약 상세 | /esign/{id} | 진행 상태 + 감사 로그 | + +### 7.2 서명 상대방(B) 화면 + +| # | 화면ID | 화면명 | 경로 | 설명 | +|---|--------|--------|------|------| +| 6 | ES_AUTH | 본인인증 | /esign/sign/{token} | OTP 인증 게이트 | +| 7 | ES_SIGN | 서명 수행 | /esign/sign/{token}/sign | PDF 열람 + 서명 | +| 8 | ES_DONE | 서명 완료 | /esign/sign/{token}/done | 완료 안내 | + +--- + +## 8. 구현 로드맵 + +### Phase 1: 기본 기능 (2주) + +| 주차 | 작업 | 담당 | +|------|------|------| +| 1주차 | DB 마이그레이션 생성 | API | +| 1주차 | Contract 모델/서비스/컨트롤러 | API | +| 1주차 | PDF 업로드 + 해시 생성 | API | +| 1주차 | 대시보드 + 계약 생성 화면 | MNG | +| 2주차 | 서명 위치 지정 화면 (pdf.js + 드래그) | MNG | +| 2주차 | OTP 인증 + 서명 캡처 (signature_pad) | MNG + API | +| 2주차 | 이메일 발송 (서명 요청/완료) | API | +| 2주차 | PDF 합성 (FPDI + 서명 이미지) | API | + +### Phase 2: 보안 강화 (1주) + +| 작업 | 담당 | +|------|------| +| 감사 추적 로그 전체 구현 | API | +| 파일 암호화 저장 (AES-256) | API | +| 완료 PDF 감사 증적 페이지 추가 | API | +| 문서 무결성 검증 API | API | +| 토큰 만료/사용 횟수 제한 | API | +| Rate Limiting (OTP 발송 등) | API | + +### Phase 3: UX 개선 (1주) + +| 작업 | 담당 | +|------|------| +| 리마인더 자동 발송 (만료 3일 전) | API (Scheduler) | +| 만료 자동 처리 배치 | API (Scheduler) | +| 모바일 반응형 서명 UI | MNG | +| 계약 목록 필터/정렬/검색 | MNG + API | +| 서명 거절 + 사유 입력 | MNG + API | + +### Phase 4: 확장 기능 (v2, 추후) + +| 기능 | 설명 | +|------|------| +| SMS 인증 | Coolsms/NHN Cloud 연동 | +| 카카오 알림톡 | 카카오 비즈메시지 연동 | +| 다자간 서명 (3인 이상) | signers 테이블 확장 | +| 템플릿 관리 | 자주 쓰는 계약서 양식 저장 | +| 외부 API 제공 | 타 시스템에서 전자계약 호출 | +| 블록체인 공증 | 계약 해시를 블록체인에 기록 | + +--- + +## 9. 법적 고려사항 + +### 9.1 전자서명법 요건 + +한국 전자서명법 제2조에 따른 전자서명 요건: + +| 요건 | 충족 방안 | +|------|-----------| +| 서명자 확인 | 이메일 OTP 본인인증 | +| 서명 의사 확인 | 동의 체크박스 2개 (내용 확인 + 법적 효력 동의) | +| 문서 변경 감지 | SHA-256 해시 비교 | +| 서명 후 변경 불가 | 서명 완료 후 계약 수정 차단 + 별도 PDF 생성 | + +### 9.2 개인정보보호 + +| 항목 | 조치 | +|------|------| +| 수집 정보 | 이름, 이메일, 전화번호 (선택), IP, 서명 이미지 | +| 보관 기간 | 계약 완료 후 5년 (전자상거래법) | +| 암호화 | 파일 AES-256, OTP bcrypt, 통신 HTTPS | +| 접근 통제 | 계약 당사자 + 테넌트 관리자만 접근 | + +--- + +*이 문서는 SAM E-Sign 솔루션의 초기 기술 설계입니다. 구현 과정에서 상세 내용이 변경될 수 있습니다.*