1625 lines
97 KiB
PHP
1625 lines
97 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '전자계약 문서')
|
|
|
|
@section('content')
|
|
<div id="esign-docs-root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
@include('partials.react-cdn')
|
|
<script src="https://unpkg.com/lucide@0.469.0"></script>
|
|
@verbatim
|
|
<script type="text/babel">
|
|
const { useState, useEffect } = React;
|
|
|
|
// ============================================================
|
|
// Tab Definitions
|
|
// ============================================================
|
|
const TABS = [
|
|
{ id: 'overview', label: '개요', icon: 'book-open' },
|
|
{ id: 'manual', label: '사용자 매뉴얼', icon: 'help-circle' },
|
|
{ id: 'workflow', label: '워크플로우', icon: 'git-branch' },
|
|
{ id: 'architecture',label: '아키텍처', icon: 'layers' },
|
|
{ id: 'api', label: 'API 명세', icon: 'code' },
|
|
{ id: 'security', label: '보안', icon: 'shield' },
|
|
{ id: 'screens', label: '화면 설계', icon: 'monitor' },
|
|
{ id: 'operations', label: '운영 가이드', icon: 'settings' },
|
|
{ id: 'changelog', label: '변경 이력', icon: 'clock' },
|
|
];
|
|
|
|
// ============================================================
|
|
// Shared UI Components
|
|
// ============================================================
|
|
const Badge = ({ children, color = 'blue' }) => {
|
|
const colors = {
|
|
blue: 'bg-blue-100 text-blue-700',
|
|
green: 'bg-green-100 text-green-700',
|
|
yellow: 'bg-yellow-100 text-yellow-700',
|
|
red: 'bg-red-100 text-red-700',
|
|
gray: 'bg-gray-100 text-gray-600',
|
|
purple: 'bg-purple-100 text-purple-700',
|
|
indigo: 'bg-indigo-100 text-indigo-700',
|
|
};
|
|
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${colors[color] || colors.blue}`}>{children}</span>;
|
|
};
|
|
|
|
const SectionTitle = ({ children }) => (
|
|
<h2 className="text-lg font-bold text-gray-900 mb-4 pb-2 border-b border-gray-200">{children}</h2>
|
|
);
|
|
|
|
const SubTitle = ({ children }) => (
|
|
<h3 className="text-base font-semibold text-gray-800 mt-6 mb-3">{children}</h3>
|
|
);
|
|
|
|
const CodeBlock = ({ children, title }) => (
|
|
<div className="my-3">
|
|
{title && <p className="text-xs font-medium text-gray-500 mb-1">{title}</p>}
|
|
<pre className="bg-gray-900 text-gray-100 rounded-lg p-4 text-sm overflow-x-auto leading-relaxed"><code>{children}</code></pre>
|
|
</div>
|
|
);
|
|
|
|
const InfoCard = ({ title, children, color = 'blue' }) => {
|
|
const colors = {
|
|
blue: 'border-blue-200 bg-blue-50',
|
|
green: 'border-green-200 bg-green-50',
|
|
yellow: 'border-yellow-200 bg-yellow-50',
|
|
red: 'border-red-200 bg-red-50',
|
|
gray: 'border-gray-200 bg-gray-50',
|
|
};
|
|
return (
|
|
<div className={`border rounded-lg p-4 ${colors[color] || colors.blue}`}>
|
|
{title && <p className="font-semibold text-sm mb-2">{title}</p>}
|
|
<div className="text-sm text-gray-700">{children}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Table = ({ headers, rows }) => (
|
|
<div className="overflow-x-auto my-3">
|
|
<table className="w-full text-sm border border-gray-200 rounded-lg overflow-hidden">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
{headers.map((h, i) => (
|
|
<th key={i} className="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider border-b">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{rows.map((row, ri) => (
|
|
<tr key={ri} className="hover:bg-gray-50">
|
|
{row.map((cell, ci) => (
|
|
<td key={ci} className="px-3 py-2 text-gray-700 whitespace-nowrap">{cell}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 1: Overview
|
|
// ============================================================
|
|
const OverviewTab = () => (
|
|
<div>
|
|
<SectionTitle>프로젝트 개요</SectionTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<InfoCard title="프로젝트" color="blue">
|
|
<p>SAM E-Sign</p>
|
|
<p className="text-xs text-gray-500 mt-1">v1.0 / 2026-02-12</p>
|
|
</InfoCard>
|
|
<InfoCard title="규모" color="green">
|
|
<p>총 33개 파일 (신규 29 + 수정 4)</p>
|
|
<p className="text-xs text-gray-500 mt-1">+3,273 라인</p>
|
|
</InfoCard>
|
|
<InfoCard title="기술 스택" color="gray">
|
|
<p>Laravel 11 + React 18 + MySQL 8</p>
|
|
<p className="text-xs text-gray-500 mt-1">PDF.js + SignaturePad + 카카오 알림톡</p>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
<SubTitle>파일 구성</SubTitle>
|
|
<Table
|
|
headers={['영역', '파일 수', '설명']}
|
|
rows={[
|
|
['DB 마이그레이션', '4', 'esign_ 접두사 테이블'],
|
|
['Models', '4', 'Contract, Signer, SignField, AuditLog'],
|
|
['Services', '4', 'Contract, Sign, Pdf, Audit'],
|
|
['API Controllers', '2', '계약관리 10 + 서명 6 엔드포인트'],
|
|
['Form Requests', '4', 'Store, Configure, Submit, Reject'],
|
|
['Mail Templates', '1', 'EsignRequestMail'],
|
|
['알림톡 Templates', '3', '서명요청/OTP인증/완료 알림톡'],
|
|
['MNG Controllers', '2', '인증 5화면 + 공개 3화면'],
|
|
['MNG Views', '8', 'React 하이브리드'],
|
|
['API Routes', '16', '계약 + 서명 라우트'],
|
|
['MNG Routes', '8', '화면 + 공개 라우트'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>기술 스택 상세</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InfoCard title="Backend" color="gray">
|
|
<ul className="space-y-1">
|
|
<li>Laravel 11 (PHP 8.3)</li>
|
|
<li>MySQL 8.0 (Multi-tenant)</li>
|
|
<li>FPDI/FPDF (PDF 합성 - 예정)</li>
|
|
<li>Laravel Mail (이메일 발송)</li>
|
|
<li>카카오 알림톡 API (서명 요청/OTP/알림 발송)</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="Frontend" color="gray">
|
|
<ul className="space-y-1">
|
|
<li>React 18 + Babel (CDN, 브라우저 트랜스파일링)</li>
|
|
<li>HTMX (네비게이션)</li>
|
|
<li>Tailwind CSS (스타일링)</li>
|
|
<li>PDF.js (PDF 렌더링)</li>
|
|
<li>signature_pad.js (서명 캔버스)</li>
|
|
<li>Lucide (아이콘)</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab: User Manual (사용자 매뉴얼)
|
|
// ============================================================
|
|
const StepCard = ({ step, title, children, color = 'blue' }) => {
|
|
const bg = { blue: 'bg-blue-600', green: 'bg-green-600', yellow: 'bg-yellow-500', purple: 'bg-purple-600', indigo: 'bg-indigo-600', red: 'bg-red-600' };
|
|
return (
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div className={`flex items-center gap-3 px-4 py-3 ${bg[color] || bg.blue} text-white`}>
|
|
<span className="flex-shrink-0 w-7 h-7 bg-white/20 rounded-full flex items-center justify-center text-sm font-bold">{step}</span>
|
|
<span className="font-semibold">{title}</span>
|
|
</div>
|
|
<div className="p-4 text-sm text-gray-700 space-y-2">{children}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const TipBox = ({ children, type = 'tip' }) => {
|
|
const styles = {
|
|
tip: { border: 'border-blue-300', bg: 'bg-blue-50', icon: 'bg-blue-500', emoji: 'i', text: 'text-blue-800' },
|
|
warning: { border: 'border-yellow-300', bg: 'bg-yellow-50', icon: 'bg-yellow-500', emoji: '!', text: 'text-yellow-800' },
|
|
success: { border: 'border-green-300', bg: 'bg-green-50', icon: 'bg-green-500', emoji: '\u2713', text: 'text-green-800' },
|
|
};
|
|
const s = styles[type] || styles.tip;
|
|
return (
|
|
<div className={`flex gap-3 ${s.bg} ${s.border} border rounded-lg p-3 my-3`}>
|
|
<span className={`flex-shrink-0 w-5 h-5 ${s.icon} text-white rounded-full flex items-center justify-center text-xs font-bold`}>{s.emoji}</span>
|
|
<div className={`text-sm ${s.text}`}>{children}</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FaqItem = ({ q, children }) => {
|
|
const [open, setOpen] = useState(false);
|
|
return (
|
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
<button onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 transition-colors">
|
|
<span className="text-sm font-medium text-gray-900">Q. {q}</span>
|
|
<span className="text-gray-400 text-lg flex-shrink-0 ml-2">{open ? '\u2212' : '+'}</span>
|
|
</button>
|
|
{open && <div className="px-4 pb-3 text-sm text-gray-600 border-t border-gray-100 pt-3">{children}</div>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const UserManualTab = () => (
|
|
<div>
|
|
<SectionTitle>사용자 매뉴얼</SectionTitle>
|
|
|
|
{/* 소개 */}
|
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-5 mb-6">
|
|
<h3 className="text-base font-bold text-blue-900 mb-2">SAM E-Sign이란?</h3>
|
|
<p className="text-sm text-blue-800 mb-3">
|
|
PDF 계약서에 온라인으로 전자서명하는 솔루션입니다.<br/>
|
|
종이 계약서를 인쇄하고 대면으로 서명받을 필요 없이, <strong>카카오 알림톡 또는 이메일 링크 하나로</strong> 간편하게 계약을 체결할 수 있습니다.
|
|
</p>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
|
|
{[
|
|
{ icon: 'upload', label: 'PDF 업로드', desc: '계약서를 업로드' },
|
|
{ icon: 'mouse-pointer', label: '서명란 배치', desc: '위치 지정' },
|
|
{ icon: 'send', label: '알림톡/이메일', desc: '서명 요청 발송' },
|
|
{ icon: 'check-circle', label: '서명 완료', desc: '양쪽 서명' },
|
|
].map((item, i) => (
|
|
<div key={i} className="bg-white rounded-lg p-3 text-center border border-blue-100">
|
|
<i data-lucide={item.icon} className="w-5 h-5 mx-auto text-blue-600 mb-1"></i>
|
|
<p className="text-xs font-semibold text-gray-800">{item.label}</p>
|
|
<p className="text-xs text-gray-500">{item.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 사용자 역할 */}
|
|
<SubTitle>누가 사용하나요?</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<InfoCard title="계약 생성자 (갑)" color="blue">
|
|
<p className="mb-1">SAM MNG에 로그인하여 사용합니다.</p>
|
|
<ul className="space-y-0.5 text-gray-600">
|
|
<li>- 계약서 PDF 업로드</li>
|
|
<li>- 서명란 위치 지정</li>
|
|
<li>- 서명 요청 알림톡/이메일 발송</li>
|
|
<li>- 계약 진행 상황 관리</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="서명 상대방 (을)" color="green">
|
|
<p className="mb-1">카카오 알림톡 또는 이메일 링크로 접속합니다. (로그인 불필요)</p>
|
|
<ul className="space-y-0.5 text-gray-600">
|
|
<li>- 알림톡/이메일 OTP 본인인증</li>
|
|
<li>- 계약서 내용 확인</li>
|
|
<li>- 전자서명 또는 거절</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
{/* 사전 준비물 */}
|
|
<SubTitle>시작하기 전에 준비하세요</SubTitle>
|
|
<Table
|
|
headers={['준비물', '형식', '비고']}
|
|
rows={[
|
|
['계약서 PDF 파일', '.pdf 파일', '최대 20MB'],
|
|
['작성자(갑) 이름/이메일', '텍스트 / 이메일', 'OTP 인증에 사용'],
|
|
['상대방(을) 이름/이메일', '텍스트 / 이메일', '서명 링크 발송용'],
|
|
['상대방(을) 전화번호', '숫자 (010-xxxx-xxxx)', '카카오 알림톡 발송용 (선택)'],
|
|
]}
|
|
/>
|
|
<TipBox type="tip">Word, 한글 등의 파일은 PDF로 변환한 후 업로드하세요. PDF 형식만 지원됩니다.</TipBox>
|
|
|
|
{/* ===== 계약 생성자(갑) 가이드 ===== */}
|
|
<div className="mt-8 mb-4">
|
|
<h3 className="text-base font-bold text-blue-700 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center text-blue-700 text-xs font-bold">갑</span>
|
|
계약 생성자 가이드 (갑)
|
|
</h3>
|
|
<p className="text-sm text-gray-500 mt-1">SAM MNG에 로그인 후 진행합니다.</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<StepCard step="1" title="새 계약 생성" color="blue">
|
|
<p><strong>메뉴 이동:</strong> 사이드바 → SAM E-Sign → 새 계약 생성</p>
|
|
<div className="bg-gray-50 rounded-lg p-3 mt-2 space-y-2">
|
|
<p className="font-medium text-gray-800">입력 항목:</p>
|
|
<ul className="space-y-1 ml-2">
|
|
<li><Badge color="red">필수</Badge> <strong>계약 제목</strong> - 계약서 이름 (최대 200자)</li>
|
|
<li><Badge color="gray">선택</Badge> <strong>계약 설명</strong> - 부가 설명 (최대 2,000자)</li>
|
|
<li><Badge color="red">필수</Badge> <strong>PDF 파일</strong> - 계약서 PDF 업로드 (최대 20MB)</li>
|
|
<li><Badge color="gray">선택</Badge> <strong>서명 기한</strong> - 기본 7일</li>
|
|
<li><Badge color="gray">선택</Badge> <strong>서명 순서</strong> - 상대방 먼저(기본) 또는 작성자 먼저</li>
|
|
</ul>
|
|
<div className="border-t border-gray-200 pt-2 mt-2">
|
|
<p className="font-medium text-gray-800">서명자 정보:</p>
|
|
<ul className="space-y-1 ml-2">
|
|
<li><Badge color="red">필수</Badge> 작성자(갑) 이름, 이메일</li>
|
|
<li><Badge color="red">필수</Badge> 상대방(을) 이름, 이메일</li>
|
|
<li><Badge color="gray">선택</Badge> 전화번호</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<TipBox type="tip">계약 제목은 서명 요청 알림톡/이메일의 제목에 포함됩니다. 상대방이 알아보기 쉽게 작성하세요. 전화번호를 입력하면 카카오 알림톡으로도 서명 요청이 발송됩니다.</TipBox>
|
|
<p className="text-gray-600">"계약 생성" 버튼을 누르면 계약 코드(예: <code className="bg-gray-100 px-1 rounded text-xs">ES-20260212-A1B2C3</code>)가 자동 생성되고, 서명 위치 지정 화면으로 이동합니다.</p>
|
|
</StepCard>
|
|
|
|
<StepCard step="2" title="서명 위치 지정" color="indigo">
|
|
<p>업로드한 PDF가 화면에 표시됩니다. 서명이 필요한 위치에 필드를 배치합니다.</p>
|
|
<div className="bg-gray-50 rounded-lg p-3 mt-2">
|
|
<p className="font-medium text-gray-800 mb-2">사용 방법:</p>
|
|
<ol className="list-decimal list-inside space-y-1.5 text-gray-700">
|
|
<li>오른쪽 패널에서 <strong>필드 타입</strong>을 선택합니다 (서명, 도장, 텍스트, 날짜, 체크박스)</li>
|
|
<li>PDF 문서 위의 원하는 위치를 <strong>클릭</strong>하여 필드를 배치합니다</li>
|
|
<li>필드를 <strong>드래그</strong>하여 위치를 조정하고, 모서리를 드래그하여 크기를 조정합니다</li>
|
|
<li>여러 페이지에 걸쳐 배치할 수 있습니다</li>
|
|
</ol>
|
|
</div>
|
|
<div className="flex gap-4 mt-3">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="w-4 h-4 bg-blue-500 rounded"></span>
|
|
<span className="text-gray-600">파란색 = 작성자(갑) 서명란</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="w-4 h-4 bg-red-500 rounded"></span>
|
|
<span className="text-gray-600">빨간색 = 상대방(을) 서명란</span>
|
|
</div>
|
|
</div>
|
|
<TipBox type="warning">서명 요청 발송 후에는 위치를 변경할 수 없습니다. 신중하게 배치하세요.</TipBox>
|
|
</StepCard>
|
|
|
|
<StepCard step="3" title="서명 요청 발송" color="purple">
|
|
<p>서명 위치 지정을 완료하면 발송 확인 화면이 표시됩니다.</p>
|
|
<div className="bg-gray-50 rounded-lg p-3 mt-2">
|
|
<p className="font-medium text-gray-800 mb-2">발송 전 체크리스트를 확인하세요:</p>
|
|
<ul className="space-y-1">
|
|
<li className="flex items-center gap-2"><span className="text-green-500 font-bold">✓</span> PDF 문서 업로드 완료</li>
|
|
<li className="flex items-center gap-2"><span className="text-green-500 font-bold">✓</span> 서명 필드 설정 완료</li>
|
|
<li className="flex items-center gap-2"><span className="text-green-500 font-bold">✓</span> 문서 무결성 확인</li>
|
|
<li className="flex items-center gap-2"><span className="text-green-500 font-bold">✓</span> 서명 순서 확인</li>
|
|
</ul>
|
|
</div>
|
|
<p className="mt-2 text-gray-600">"서명 요청 발송" 버튼을 누르면 첫 번째 서명자에게 <strong>카카오 알림톡과 이메일</strong>이 발송되고, 계약 상태가 <Badge color="blue">진행중</Badge>으로 변경됩니다.</p>
|
|
<TipBox type="tip">전화번호가 등록된 서명자에게는 카카오 알림톡이 우선 발송됩니다. 알림톡 발송 실패 시 자동으로 이메일이 발송됩니다.</TipBox>
|
|
<TipBox type="warning">발송 후에는 계약 내용, 서명 위치, 서명 순서를 변경할 수 없습니다. 수정이 필요하면 취소 후 새로 생성해야 합니다.</TipBox>
|
|
</StepCard>
|
|
|
|
<StepCard step="4" title="계약 관리" color="blue">
|
|
<p>대시보드에서 진행 중인 계약을 관리합니다.</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-2">
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<p className="font-medium text-sm text-gray-800 mb-1">리마인더 발송</p>
|
|
<p className="text-xs text-gray-600">상대방이 서명하지 않으면 리마인더 알림톡/이메일을 보낼 수 있습니다. 여러 번 발송 가능합니다.</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<p className="font-medium text-sm text-gray-800 mb-1">계약 취소</p>
|
|
<p className="text-xs text-gray-600">초안 또는 진행중 상태에서만 취소 가능합니다. 완료된 계약은 취소할 수 없습니다.</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<p className="font-medium text-sm text-gray-800 mb-1">PDF 다운로드</p>
|
|
<p className="text-xs text-gray-600">계약 완료 후 서명이 포함된 최종 PDF를 다운로드할 수 있습니다.</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<p className="font-medium text-sm text-gray-800 mb-1">무결성 검증</p>
|
|
<p className="text-xs text-gray-600">원본 문서의 위변조 여부를 SHA-256 해시로 확인합니다.</p>
|
|
</div>
|
|
</div>
|
|
</StepCard>
|
|
</div>
|
|
|
|
{/* 상태 설명 */}
|
|
<SubTitle>계약 상태별 의미</SubTitle>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
{[
|
|
{ label: '초안', badge: 'gray', desc: '작성 중, 아직 발송 안 함' },
|
|
{ label: '진행중', badge: 'blue', desc: '서명 요청 발송됨' },
|
|
{ label: '부분 서명', badge: 'yellow', desc: '한쪽만 서명 완료' },
|
|
{ label: '완료', badge: 'green', desc: '양쪽 서명 완료!' },
|
|
{ label: '만료', badge: 'red', desc: '서명 기한 초과' },
|
|
{ label: '취소', badge: 'gray', desc: '생성자가 취소함' },
|
|
{ label: '거절', badge: 'red', desc: '서명자가 거절함' },
|
|
].map((s, i) => (
|
|
<div key={i} className="border border-gray-200 rounded-lg p-3">
|
|
<Badge color={s.badge}>{s.label}</Badge>
|
|
<p className="text-xs text-gray-600 mt-1">{s.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* ===== 서명 상대방(을) 가이드 ===== */}
|
|
<div className="mt-10 mb-4">
|
|
<h3 className="text-base font-bold text-green-700 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-green-100 rounded-full flex items-center justify-center text-green-700 text-xs font-bold">을</span>
|
|
서명 상대방 가이드 (을)
|
|
</h3>
|
|
<p className="text-sm text-gray-500 mt-1">카카오 알림톡 또는 이메일로 서명 요청을 받은 분을 위한 안내입니다. SAM 계정이 없어도 됩니다.</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<StepCard step="1" title="서명 요청 알림 확인" color="green">
|
|
<p>카카오 알림톡 또는 이메일로 서명 요청이 도착합니다.</p>
|
|
|
|
{/* 카카오 알림톡 예시 */}
|
|
<p className="text-xs font-semibold text-yellow-700 mt-3 mb-1">카카오 알림톡으로 받은 경우:</p>
|
|
<div className="bg-yellow-50 border border-yellow-300 rounded-lg p-4 mt-1 text-gray-700">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="text-lg">💬</span>
|
|
<span className="text-xs font-bold text-yellow-800">카카오톡 알림톡</span>
|
|
</div>
|
|
<p className="text-sm font-medium mb-1">[SAM E-Sign] 서명 요청</p>
|
|
<p className="text-sm">안녕하세요, 박을동님.</p>
|
|
<p className="text-sm mb-1">김갑순님이 계약 서명을 요청했습니다.</p>
|
|
<p className="text-xs text-gray-500">계약: 소프트웨어 개발 용역 계약서</p>
|
|
<p className="text-xs text-gray-500 mb-2">서명 기한: 2026-02-19</p>
|
|
<div className="bg-yellow-500 text-white text-sm text-center py-2 px-4 rounded-lg inline-block">계약서 확인 및 서명하기</div>
|
|
</div>
|
|
|
|
{/* 이메일 예시 */}
|
|
<p className="text-xs font-semibold text-blue-700 mt-4 mb-1">이메일로 받은 경우:</p>
|
|
<div className="bg-white border border-gray-300 rounded-lg p-4 mt-1 text-gray-700">
|
|
<p className="text-xs text-gray-400 mb-1">제목</p>
|
|
<p className="font-medium text-sm mb-3">[SAM E-Sign] 서명 요청 - 소프트웨어 개발 용역 계약서</p>
|
|
<p className="text-sm">안녕하세요, 박을동님.</p>
|
|
<p className="text-sm mb-2">김갑순님이 계약 서명을 요청했습니다.</p>
|
|
<p className="text-xs text-gray-500">계약 제목: 소프트웨어 개발 용역 계약서</p>
|
|
<p className="text-xs text-gray-500 mb-3">서명 기한: 2026-02-19</p>
|
|
<div className="bg-blue-600 text-white text-sm text-center py-2 px-4 rounded-lg inline-block">계약서 확인 및 서명하기</div>
|
|
</div>
|
|
<p className="mt-2 text-gray-600">버튼을 클릭하면 서명 절차가 시작됩니다.</p>
|
|
<TipBox type="tip">카카오 알림톡은 카카오톡 앱에서 바로 확인할 수 있어 이메일보다 빠르게 서명을 진행할 수 있습니다. 이메일이 보이지 않으면 <strong>스팸/정크 메일함</strong>을 확인해 보세요.</TipBox>
|
|
</StepCard>
|
|
|
|
<StepCard step="2" title="본인인증 (OTP)" color="green">
|
|
{/* OTP 개념 설명 */}
|
|
<div className="bg-green-50 border border-green-200 rounded-xl p-4 mb-4">
|
|
<p className="font-bold text-green-800 mb-2">OTP가 뭔가요?</p>
|
|
<p className="text-green-700 mb-3">
|
|
OTP는 <strong>"일회용 비밀번호"</strong> 입니다. (One-Time Password의 약자)<br/>
|
|
매번 새롭게 만들어지는 숫자 코드로, <strong>딱 한 번만</strong> 사용할 수 있습니다.<br/>
|
|
<strong>카카오 알림톡</strong> 또는 <strong>이메일</strong>로 받을 수 있습니다.
|
|
</p>
|
|
<div className="bg-white rounded-lg p-3 border border-green-100">
|
|
<p className="text-sm font-semibold text-gray-800 mb-2">쉽게 비유하면...</p>
|
|
<p className="text-sm text-gray-600 mb-2">
|
|
택배를 받을 때 무인 보관함에서 <strong>"인증번호 4자리"</strong>를 입력해야 열리죠?<br/>
|
|
그 번호는 내 택배에만 쓸 수 있고, 한 번 쓰면 다시 못 씁니다.
|
|
</p>
|
|
<p className="text-sm text-gray-600">
|
|
OTP도 마찬가지입니다. 이메일로 받은 <strong>6자리 숫자</strong>를 입력하면<br/>
|
|
"이 사람이 진짜 본인이 맞구나!" 하고 확인해 주는 거예요.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 왜 필요한지 */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-4">
|
|
<p className="font-bold text-blue-800 mb-2">왜 OTP가 필요한가요?</p>
|
|
<p className="text-sm text-blue-700">
|
|
계약서에 서명하는 건 아주 중요한 일이에요.<br/>
|
|
만약 누군가 내 서명 링크를 몰래 가져가서 대신 서명하면 큰일이겠죠?
|
|
</p>
|
|
<div className="flex items-start gap-3 mt-3 bg-white rounded-lg p-3 border border-blue-100">
|
|
<span className="text-2xl">🔒</span>
|
|
<div className="text-sm text-gray-700">
|
|
<p className="font-medium">OTP는 "내 카카오톡/이메일을 볼 수 있는 사람 = 진짜 본인"이라는 원리를 이용합니다.</p>
|
|
<p className="mt-1 text-gray-500">서명 링크를 누른 사람이 정말 계약 당사자인지 카카오 알림톡 또는 이메일로 한 번 더 확인하는 과정입니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 실제 진행 절차 */}
|
|
<p className="font-semibold text-gray-800 mb-2">실제 진행 방법 (4단계)</p>
|
|
<div className="space-y-3">
|
|
<div className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
|
|
<span className="flex-shrink-0 w-7 h-7 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold">1</span>
|
|
<div>
|
|
<p className="font-medium text-gray-800">"인증코드 발송" 버튼을 누릅니다</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">전화번호가 등록된 경우 카카오 알림톡으로, 그렇지 않으면 이메일로 발송됩니다</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
|
|
<span className="flex-shrink-0 w-7 h-7 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold">2</span>
|
|
<div>
|
|
<p className="font-medium text-gray-800">카카오톡 또는 이메일함을 확인합니다</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">몇 초 안에 인증코드가 도착합니다:</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded p-2 text-xs">
|
|
<p className="text-yellow-700 font-semibold mb-1">💬 카카오 알림톡</p>
|
|
<p className="text-gray-600">[SAM E-Sign] 인증코드</p>
|
|
<p className="text-gray-700 mt-1">인증코드: <strong className="text-lg text-blue-600 font-mono">4 8 2 9 1 7</strong></p>
|
|
</div>
|
|
<div className="bg-white border border-gray-200 rounded p-2 text-xs">
|
|
<p className="text-blue-700 font-semibold mb-1">✉ 이메일</p>
|
|
<p className="text-gray-400">제목: [SAM E-Sign] 인증코드 안내</p>
|
|
<p className="text-gray-700 mt-1">인증코드: <strong className="text-lg text-blue-600 font-mono">4 8 2 9 1 7</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
|
|
<span className="flex-shrink-0 w-7 h-7 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold">3</span>
|
|
<div>
|
|
<p className="font-medium text-gray-800">받은 6자리 숫자를 입력합니다</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">위 예시라면 4, 8, 2, 9, 1, 7 을 차례로 입력합니다</p>
|
|
<div className="flex gap-1 mt-1">
|
|
{['4','8','2','9','1','7'].map((n, i) => (
|
|
<span key={i} className="w-8 h-8 bg-white border-2 border-blue-300 rounded flex items-center justify-center text-sm font-bold text-blue-700">{n}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
|
|
<span className="flex-shrink-0 w-7 h-7 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold">4</span>
|
|
<div>
|
|
<p className="font-medium text-gray-800">"확인" 버튼을 누릅니다</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">코드가 맞으면 자동으로 계약서 확인 화면으로 넘어갑니다</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 주의사항 */}
|
|
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-xl p-4">
|
|
<p className="font-bold text-yellow-800 mb-2">주의하세요!</p>
|
|
<div className="space-y-2 text-sm text-yellow-700">
|
|
<div className="flex items-start gap-2">
|
|
<span className="font-bold text-yellow-600">⏰</span>
|
|
<p><strong>5분 안에 입력하세요</strong> - 코드는 발송 후 5분이 지나면 만료됩니다. 만료되면 "재발송"을 눌러 새 코드를 받으세요.</p>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<span className="font-bold text-yellow-600">⚠</span>
|
|
<p><strong>5번까지만 입력할 수 있어요</strong> - 틀린 코드를 5번 넣으면 보안을 위해 차단됩니다. 이 경우 계약을 보낸 분에게 연락하세요.</p>
|
|
</div>
|
|
<div className="flex items-start gap-2">
|
|
<span className="font-bold text-yellow-600">📩</span>
|
|
<p><strong>알림톡/이메일이 안 오나요?</strong> - 카카오톡 알림 설정을 확인하거나 이메일 스팸/정크 메일함을 확인해 보세요. 그래도 없으면 1분 후 "재발송" 버튼을 누르세요.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<TipBox type="success">OTP 인증은 한 마디로 "내가 진짜 나"임을 증명하는 단계입니다. 카카오 알림톡 또는 이메일로 받은 숫자 6개만 입력하면 끝!</TipBox>
|
|
</StepCard>
|
|
|
|
<StepCard step="3" title="계약서 확인" color="green">
|
|
<p>인증이 완료되면 계약서 PDF를 확인하는 화면으로 이동합니다.</p>
|
|
<ul className="mt-2 space-y-1">
|
|
<li>- 계약서 전체 내용을 <strong>꼼꼼히 확인</strong>하세요</li>
|
|
<li>- 여러 페이지인 경우 <strong>모든 페이지</strong>를 확인하세요</li>
|
|
<li>- 서명이 필요한 위치에 <strong>빨간색 표시</strong>가 되어 있습니다</li>
|
|
</ul>
|
|
<p className="mt-2 text-gray-600">확인 후 <strong>"서명하기"</strong> 또는 <strong>"거절하기"</strong>를 선택합니다.</p>
|
|
</StepCard>
|
|
|
|
<StepCard step="4" title="서명 수행" color="green">
|
|
<p>"서명하기"를 누르면 서명 입력 화면이 나타납니다.</p>
|
|
<div className="bg-gray-50 rounded-lg p-3 mt-2">
|
|
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
|
<li>서명 캔버스에 <strong>마우스 드래그 또는 터치</strong>로 서명을 그립니다</li>
|
|
<li>마음에 들지 않으면 <strong>"다시 쓰기"</strong>를 눌러 초기화합니다</li>
|
|
<li>아래 <strong>2개의 동의 체크박스</strong>를 모두 체크합니다
|
|
<ul className="ml-4 mt-1 text-xs text-gray-500 space-y-0.5">
|
|
<li>☑ "본 계약서의 내용을 확인하였으며 서명에 동의합니다"</li>
|
|
<li>☑ "전자서명의 법적 효력에 동의합니다"</li>
|
|
</ul>
|
|
</li>
|
|
<li><strong>"최종 제출"</strong> 버튼을 클릭합니다</li>
|
|
</ol>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3 mt-3">
|
|
<div className="bg-gray-50 rounded p-2 text-center">
|
|
<p className="text-xs font-medium text-gray-700">PC에서</p>
|
|
<p className="text-xs text-gray-500">마우스를 클릭+드래그</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded p-2 text-center">
|
|
<p className="text-xs font-medium text-gray-700">모바일/태블릿에서</p>
|
|
<p className="text-xs text-gray-500">손가락 또는 펜으로 터치</p>
|
|
</div>
|
|
</div>
|
|
<TipBox type="success">서명이 제출되면 완료 화면이 표시됩니다. 상대방 서명이 완료되면 계약이 최종 확정됩니다.</TipBox>
|
|
</StepCard>
|
|
</div>
|
|
|
|
{/* 서명 거절 */}
|
|
<SubTitle>서명을 거절하려면?</SubTitle>
|
|
<InfoCard title="서명 거절 절차" color="red">
|
|
<ol className="list-decimal list-inside space-y-1">
|
|
<li>계약서 확인 화면에서 <strong>"거절하기"</strong> 버튼을 클릭합니다</li>
|
|
<li>거절 사유를 입력합니다 (필수, 최대 1,000자)</li>
|
|
<li><strong>"거절 확인"</strong>을 클릭합니다</li>
|
|
</ol>
|
|
<p className="mt-2 text-xs text-red-600">거절하면 계약이 "거절" 상태로 변경되며, 생성자에게 알림톡/이메일로 사유가 전달됩니다. 거절 후에는 취소할 수 없으니 신중하게 결정하세요.</p>
|
|
</InfoCard>
|
|
|
|
{/* FAQ */}
|
|
<div className="mt-10">
|
|
<SectionTitle>자주 묻는 질문 (FAQ)</SectionTitle>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<FaqItem q="PDF 외 다른 형식의 파일을 업로드할 수 있나요?">
|
|
<p>현재 v1.0에서는 PDF 형식만 지원합니다. Word, 한글 등의 문서는 PDF로 변환한 후 업로드해 주세요.</p>
|
|
</FaqItem>
|
|
<FaqItem q="PDF 파일 크기 제한은 얼마인가요?">
|
|
<p>최대 <strong>20MB</strong>까지 업로드할 수 있습니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="업로드한 PDF를 수정할 수 있나요?">
|
|
<p>아니요. 문서 무결성 보장을 위해 업로드 후 PDF 내용은 변경할 수 없습니다. 수정이 필요하면 <strong>계약을 취소하고 수정된 PDF로 새 계약을 생성</strong>하세요.</p>
|
|
</FaqItem>
|
|
<FaqItem q="서명 기한을 연장할 수 있나요?">
|
|
<p>현재 v1.0에서는 기한 연장 기능을 지원하지 않습니다. 기한이 만료되면 새 계약을 생성해야 합니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="상대방이 서명 요청을 받지 못했다고 합니다.">
|
|
<ul className="list-disc list-inside space-y-1">
|
|
<li><strong>카카오 알림톡</strong>인 경우: 카카오톡 채널 차단 여부, 전화번호 정확성을 확인하세요</li>
|
|
<li><strong>이메일</strong>인 경우: <strong>스팸/정크 메일함</strong>을 확인하도록 안내하세요</li>
|
|
<li>이메일 주소/전화번호가 정확한지 확인하세요</li>
|
|
<li><strong>리마인더 발송</strong> 기능을 사용하여 알림톡/이메일을 재발송하세요</li>
|
|
</ul>
|
|
</FaqItem>
|
|
<FaqItem q="인증코드(OTP)를 5회 이상 틀렸습니다. 어떻게 하나요?">
|
|
<p className="mb-2">보안을 위해 5번 틀리면 더 이상 코드를 입력할 수 없게 됩니다.</p>
|
|
<p><strong>해결 방법:</strong> 계약을 보낸 분에게 연락하여 "계약을 취소하고 다시 보내달라"고 요청하세요. 새 링크가 이메일로 오면 처음부터 다시 시도하면 됩니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="OTP 인증코드가 안 와요.">
|
|
<p className="font-semibold text-gray-700 mb-2">카카오 알림톡으로 안 오는 경우:</p>
|
|
<ul className="list-disc list-inside space-y-1 mb-3">
|
|
<li>카카오톡 앱에서 <strong>알림 설정</strong>이 켜져 있는지 확인하세요.</li>
|
|
<li>SAM E-Sign <strong>카카오 채널을 차단</strong>하지 않았는지 확인하세요.</li>
|
|
<li>전화번호가 정확하게 등록되었는지 계약 보낸 분에게 확인 요청하세요.</li>
|
|
</ul>
|
|
<p className="font-semibold text-gray-700 mb-2">이메일로 안 오는 경우:</p>
|
|
<ul className="list-disc list-inside space-y-1">
|
|
<li><strong>스팸/정크 메일함</strong>을 먼저 확인해 보세요 (가장 흔한 원인!)</li>
|
|
<li>1~2분 정도 기다려 보세요. 서버 상황에 따라 약간 지연될 수 있습니다.</li>
|
|
<li>그래도 안 오면 화면의 <strong>"재발송"</strong> 버튼을 눌러 새 코드를 받으세요.</li>
|
|
<li>계속 안 된다면 계약을 보낸 분에게 내 이메일/전화번호가 맞는지 확인 요청하세요.</li>
|
|
</ul>
|
|
</FaqItem>
|
|
<FaqItem q="모바일에서도 서명할 수 있나요?">
|
|
<p>네! 모바일 브라우저(Chrome, Safari 등)에서 서명할 수 있습니다. 손가락으로 터치하여 서명하세요.</p>
|
|
</FaqItem>
|
|
<FaqItem q="서명을 잘못 했습니다. 다시 할 수 있나요?">
|
|
<p>서명 <strong>제출 전</strong>에는 "다시 쓰기" 버튼으로 서명을 초기화할 수 있습니다. 이미 제출한 서명은 변경할 수 없습니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="서명을 거절하면 어떻게 되나요?">
|
|
<p>계약이 "거절" 상태로 변경되며, 생성자에게 알림톡/이메일로 거절 알림과 사유가 전달됩니다. 거절된 계약은 복구할 수 없으므로, 협의 후 새 계약을 생성해야 합니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="완료된 계약서를 취소할 수 있나요?">
|
|
<p>양쪽 서명이 완료된 계약은 취소할 수 없습니다. 법적으로 유효한 전자서명이 완료된 상태이므로, 필요한 경우 별도의 <strong>해지 계약</strong>을 체결해야 합니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="완료 PDF는 언제까지 다운로드할 수 있나요?">
|
|
<p>완료된 PDF는 시스템에 보관되며, 로그인 후 언제든지 다운로드할 수 있습니다. 관련 법규에 따라 <strong>최소 5년간</strong> 보관됩니다.</p>
|
|
</FaqItem>
|
|
<FaqItem q="전자서명의 법적 효력은 어떻게 되나요?">
|
|
<p className="mb-2">SAM E-Sign의 전자서명은 한국 전자서명법 제2조에 따른 전자서명 요건을 충족합니다:</p>
|
|
<ul className="list-disc list-inside space-y-1">
|
|
<li><strong>서명자 확인</strong> → 이메일 OTP 본인인증</li>
|
|
<li><strong>서명 의사 확인</strong> → 동의 체크박스 2개</li>
|
|
<li><strong>문서 변경 감지</strong> → SHA-256 해시 비교</li>
|
|
<li><strong>서명 후 불변</strong> → 서명 완료 후 수정 차단</li>
|
|
</ul>
|
|
</FaqItem>
|
|
<FaqItem q="다른 사람이 내 서명 링크를 사용할 수 있나요?">
|
|
<p>서명 링크에 접속하더라도 <strong>등록된 카카오톡 또는 이메일로 OTP 인증</strong>을 통과해야만 서명할 수 있습니다. 카카오톡 및 이메일 계정을 안전하게 관리하고 있다면 걱정하지 않으셔도 됩니다.</p>
|
|
</FaqItem>
|
|
</div>
|
|
|
|
{/* ===== 법률 안내 ===== */}
|
|
<div className="mt-10">
|
|
<SectionTitle>법률 안내</SectionTitle>
|
|
</div>
|
|
|
|
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 border border-indigo-200 rounded-xl p-5 mb-6">
|
|
<p className="font-bold text-indigo-900 mb-2">SAM E-Sign으로 체결한 계약은 법적으로 유효합니다</p>
|
|
<p className="text-sm text-indigo-700">
|
|
SAM E-Sign의 전자서명은 한국 <strong>전자서명법</strong>과 <strong>전자문서 및 전자거래 기본법</strong>에 근거하여
|
|
종이 계약서의 서명/날인과 <strong>동일한 법적 효력</strong>을 가집니다.
|
|
</p>
|
|
</div>
|
|
|
|
{/* 법적 근거 */}
|
|
<SubTitle>1. 법적 근거</SubTitle>
|
|
|
|
<div className="space-y-3 mb-4">
|
|
<div className="border border-indigo-200 rounded-lg overflow-hidden">
|
|
<div className="bg-indigo-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-indigo-900">전자서명법 제3조 제1항 - 효력 부인 금지</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<blockquote className="border-l-4 border-indigo-300 pl-3 text-sm text-gray-700 italic">
|
|
"전자서명은 전자적 형태라는 이유만으로 서명, 서명날인 또는 기명날인으로서의 효력이 부인되지 아니한다."
|
|
</blockquote>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
즉, "컴퓨터로 서명했으니까 무효"라고 할 수 없습니다. 전자서명도 종이 서명과 같은 법적 효력이 있습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-indigo-200 rounded-lg overflow-hidden">
|
|
<div className="bg-indigo-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-indigo-900">전자서명법 제3조 제3항 - 서명과 동일한 효력</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<blockquote className="border-l-4 border-indigo-300 pl-3 text-sm text-gray-700 italic">
|
|
"법령의 규정이나 당사자 간의 약정에 따라 서명, 서명날인 또는 기명날인의 방식으로 전자서명을 선택한 경우
|
|
그 전자서명은 서명, 서명날인 또는 기명날인의 효력을 가진다."
|
|
</blockquote>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
양쪽이 전자서명으로 계약하기로 합의하면, 그 서명은 직접 손으로 쓴 서명과 동일한 효력을 가집니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-indigo-200 rounded-lg overflow-hidden">
|
|
<div className="bg-indigo-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-indigo-900">전자문서 및 전자거래 기본법 제4조 - 전자문서의 효력</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<blockquote className="border-l-4 border-indigo-300 pl-3 text-sm text-gray-700 italic">
|
|
"전자문서는 전자적 형태로 되어 있다는 이유만으로 법적 효력이 부인되지 아니한다."
|
|
</blockquote>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
PDF로 작성된 계약서도 종이 계약서와 동일하게 법적으로 유효한 문서입니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-indigo-200 rounded-lg overflow-hidden">
|
|
<div className="bg-indigo-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-indigo-900">전자문서 및 전자거래 기본법 제5조 - 서면으로서의 효력</p>
|
|
</div>
|
|
<div className="px-4 py-3">
|
|
<p className="text-sm text-gray-700">전자문서가 다음 3가지 요건을 갖추면 <strong>법률상 "서면"</strong>으로 인정됩니다:</p>
|
|
<ol className="list-decimal list-inside mt-2 space-y-1 text-sm text-gray-600">
|
|
<li><strong>열람 가능</strong> - 내용을 화면에서 볼 수 있음</li>
|
|
<li><strong>재현 가능</strong> - 작성된 그대로의 형태를 다시 볼 수 있음</li>
|
|
<li><strong>보존 가능</strong> - 안전하게 저장/보관할 수 있음</li>
|
|
</ol>
|
|
<TipBox type="success">SAM E-Sign은 PDF 원본 보존, 언제든 열람/다운로드 가능, 해시 기반 무결성 검증으로 이 3가지를 모두 충족합니다.</TipBox>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 공인전자서명 폐지 안내 */}
|
|
<SubTitle>2. 2020년 전자서명법 개정과 SAM E-Sign</SubTitle>
|
|
|
|
<InfoCard title="공인인증서 제도 폐지 (2020년 12월)" color="blue">
|
|
<p className="mb-2">2020년 전자서명법 전면 개정으로 <strong>"공인전자서명"</strong> 제도가 폐지되었습니다.</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
|
<div className="bg-white rounded-lg p-3 border border-gray-200">
|
|
<p className="text-xs font-semibold text-red-600 mb-1">개정 전</p>
|
|
<ul className="text-xs text-gray-600 space-y-0.5">
|
|
<li>- 공인인증서 서명만 법적 추정력 인정</li>
|
|
<li>- 일반 전자서명은 효력 제한</li>
|
|
</ul>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 border border-gray-200">
|
|
<p className="text-xs font-semibold text-green-600 mb-1">개정 후 (현행)</p>
|
|
<ul className="text-xs text-gray-600 space-y-0.5">
|
|
<li>- 모든 전자서명에 기본적 법적 효력 인정</li>
|
|
<li>- 기술 중립성 원칙 적용</li>
|
|
<li>- 당사자 약정으로 효력 확보</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-3">
|
|
SAM E-Sign은 개정된 전자서명법에 따라 OTP 본인인증, SHA-256 무결성 검증, 감사 추적 등
|
|
기술적 신뢰성을 갖춘 전자서명 수단을 제공합니다.
|
|
</p>
|
|
</InfoCard>
|
|
|
|
{/* SAM E-Sign이 법적 효력을 갖추는 방법 */}
|
|
<SubTitle>3. SAM E-Sign이 법적 요건을 충족하는 방법</SubTitle>
|
|
|
|
<p className="text-sm text-gray-600 mb-3">전자서명이 법적 효력을 인정받으려면 4가지 핵심 요건을 충족해야 합니다. SAM E-Sign은 모두 충족합니다.</p>
|
|
|
|
<div className="space-y-3">
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div className="bg-blue-600 px-4 py-2 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold text-white">1</span>
|
|
<span className="font-semibold text-white text-sm">서명자 본인 확인</span>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-1">
|
|
<p className="text-sm text-gray-700 mb-2"><strong>법적 요구:</strong> 서명한 사람이 정말 본인인지 확인해야 합니다.</p>
|
|
<p className="text-sm text-gray-700"><strong>SAM E-Sign 구현:</strong></p>
|
|
<ul className="text-sm text-gray-600 space-y-1 mt-1">
|
|
<li>- <strong>카카오 알림톡 / 이메일 OTP 인증</strong> (6자리 일회용 코드, 5분 유효, 5회 제한)</li>
|
|
<li>- 고유 액세스 토큰 (128자리 랜덤 문자열)</li>
|
|
<li>- 접속 IP 및 브라우저 정보 기록</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div className="bg-green-600 px-4 py-2 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold text-white">2</span>
|
|
<span className="font-semibold text-white text-sm">서명 의사 확인</span>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-sm text-gray-700 mb-2"><strong>법적 요구:</strong> 서명자가 자발적으로, 내용을 이해하고 서명했음을 확인해야 합니다.</p>
|
|
<p className="text-sm text-gray-700"><strong>SAM E-Sign 구현:</strong></p>
|
|
<ul className="text-sm text-gray-600 space-y-1 mt-1">
|
|
<li>- 계약서 전문 열람 단계 (PDF 확인 필수)</li>
|
|
<li>- <strong>2개의 명시적 동의 체크박스</strong></li>
|
|
<li className="ml-4 text-xs text-gray-500">"본 계약서의 내용을 확인하였으며 서명에 동의합니다"</li>
|
|
<li className="ml-4 text-xs text-gray-500">"전자서명의 법적 효력에 동의합니다"</li>
|
|
<li>- 직접 서명 캔버스 입력 (터치/마우스)</li>
|
|
<li>- 거절 옵션 제공 (사유 기재 포함)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div className="bg-purple-600 px-4 py-2 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold text-white">3</span>
|
|
<span className="font-semibold text-white text-sm">문서 무결성 보장 (위변조 방지)</span>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-sm text-gray-700 mb-2"><strong>법적 요구:</strong> 서명 후 문서가 변경되지 않았음을 증명할 수 있어야 합니다.</p>
|
|
<p className="text-sm text-gray-700"><strong>SAM E-Sign 구현:</strong></p>
|
|
<ul className="text-sm text-gray-600 space-y-1 mt-1">
|
|
<li>- <strong>SHA-256 해시</strong>로 문서의 고유 지문 생성 및 DB 저장</li>
|
|
<li>- <code className="bg-gray-100 px-1 rounded text-xs">hash_equals()</code> 타이밍 공격 방지 검증</li>
|
|
<li>- 서명 완료 후 문서 수정 차단</li>
|
|
<li>- 언제든 "무결성 검증" 기능으로 원본 확인 가능</li>
|
|
</ul>
|
|
<TipBox type="tip">해시(Hash)란 파일의 "디지털 지문"입니다. 문서를 단 1글자만 바꿔도 해시값이 완전히 달라지므로, 해시값이 같으면 원본과 100% 동일함을 보장합니다.</TipBox>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
<div className="bg-red-600 px-4 py-2 flex items-center gap-2">
|
|
<span className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center text-xs font-bold text-white">4</span>
|
|
<span className="font-semibold text-white text-sm">감사 추적 (Audit Trail)</span>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-sm text-gray-700 mb-2"><strong>법적 요구:</strong> 계약 과정 전체를 시간순으로 기록하여 추후 분쟁 시 증거로 활용할 수 있어야 합니다.</p>
|
|
<p className="text-sm text-gray-700"><strong>SAM E-Sign 구현:</strong></p>
|
|
<ul className="text-sm text-gray-600 space-y-1 mt-1">
|
|
<li>- 모든 행위를 <strong>삭제 불가능한 로그</strong>로 영구 기록</li>
|
|
<li>- 각 기록에 <strong>정확한 시각, IP 주소, 브라우저 정보</strong> 포함</li>
|
|
<li>- 기록 항목: 계약 생성, 서명 요청 발송, 링크 접속, OTP 발송/인증, 문서 열람, 서명 수행, 완료, 다운로드 등</li>
|
|
</ul>
|
|
<div className="bg-gray-50 rounded-lg p-3 mt-3 text-xs text-gray-600">
|
|
<p className="font-medium text-gray-800 mb-1">감사 추적 기록 예시:</p>
|
|
<div className="font-mono space-y-0.5">
|
|
<p>2026-02-12 10:00:15 | 계약 생성 | IP: 192.168.1.100</p>
|
|
<p>2026-02-12 10:05:22 | 서명 요청 발송 | 수신: park@example.com</p>
|
|
<p>2026-02-12 14:30:08 | 서명 링크 접속 | IP: 211.xxx.xxx.xxx</p>
|
|
<p>2026-02-12 14:30:45 | OTP 인증 성공 | 시도: 1회</p>
|
|
<p>2026-02-12 14:32:10 | 전자서명 수행 | Chrome 120, Windows 11</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 체결 가능/불가능한 계약 */}
|
|
<SubTitle>4. 전자서명으로 체결할 수 있는 계약</SubTitle>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div className="border border-green-200 rounded-xl overflow-hidden">
|
|
<div className="bg-green-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-green-800">✓ 전자서명 가능</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<ul className="text-sm text-gray-700 space-y-1.5">
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 물품 공급/납품 계약</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 용역 계약 (개발, 컨설팅 등)</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 비밀유지계약 (NDA)</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 프랜차이즈/대리점 계약</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 근로 계약서</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 라이선스 계약</li>
|
|
<li className="flex items-start gap-2"><span className="text-green-500 mt-0.5">✓</span> 부동산 매매/임대차 계약 *</li>
|
|
</ul>
|
|
<p className="text-xs text-gray-400 mt-2">* 부동산 계약 자체는 가능하나, 등기 시 별도 절차 필요</p>
|
|
</div>
|
|
</div>
|
|
<div className="border border-red-200 rounded-xl overflow-hidden">
|
|
<div className="bg-red-50 px-4 py-2">
|
|
<p className="font-semibold text-sm text-red-800">✗ 전자서명 제한/불가</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<ul className="text-sm text-gray-700 space-y-1.5">
|
|
<li className="flex items-start gap-2"><span className="text-red-500 mt-0.5">✗</span> 부동산 등기 신청 서류</li>
|
|
<li className="flex items-start gap-2"><span className="text-red-500 mt-0.5">✗</span> 공증이 필요한 문서</li>
|
|
<li className="flex items-start gap-2"><span className="text-red-500 mt-0.5">✗</span> 어음/수표 (유가증권)</li>
|
|
<li className="flex items-start gap-2"><span className="text-red-500 mt-0.5">✗</span> 공정증서 (강제집행 목적)</li>
|
|
<li className="flex items-start gap-2"><span className="text-red-500 mt-0.5">✗</span> 법원 제출 소송 서류</li>
|
|
</ul>
|
|
<p className="text-xs text-gray-400 mt-2">이러한 문서는 법령에서 별도의 형식/절차를 요구합니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<TipBox type="tip">
|
|
SAM E-Sign은 주로 <strong>물품 공급, 용역, NDA, 납품 계약</strong> 등 일반 상사 계약에 적합합니다.
|
|
대부분의 비즈니스 계약은 전자서명으로 유효하게 체결할 수 있습니다.
|
|
</TipBox>
|
|
|
|
{/* 분쟁 발생 시 */}
|
|
<SubTitle>5. 분쟁이 발생하면?</SubTitle>
|
|
|
|
<InfoCard title="전자서명 계약서의 증거 능력" color="gray">
|
|
<p className="mb-3">SAM E-Sign으로 체결한 계약서는 분쟁 시 <strong>법적 증거로 활용</strong>할 수 있습니다.</p>
|
|
<div className="space-y-3">
|
|
<div className="bg-white rounded-lg p-3 border border-gray-200">
|
|
<p className="text-xs font-semibold text-gray-800 mb-1">1. 문서 원본 제출</p>
|
|
<p className="text-xs text-gray-600">서명이 완료된 PDF 원본을 증거로 제출합니다. SHA-256 해시값으로 위변조 없음을 객관적으로 증명합니다.</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 border border-gray-200">
|
|
<p className="text-xs font-semibold text-gray-800 mb-1">2. 감사 추적 기록 제출</p>
|
|
<p className="text-xs text-gray-600">누가, 언제, 어디서(IP), 어떤 기기로 서명했는지 상세 기록을 제출합니다. 서명 과정 전체를 재구성할 수 있어 <strong>부인 방지(Non-repudiation)</strong> 효과가 있습니다.</p>
|
|
</div>
|
|
<div className="bg-white rounded-lg p-3 border border-gray-200">
|
|
<p className="text-xs font-semibold text-gray-800 mb-1">3. 본인인증 기록 제출</p>
|
|
<p className="text-xs text-gray-600">OTP 인증 성공 기록, 접속 IP, 시각 등으로 서명자가 본인임을 입증합니다.</p>
|
|
</div>
|
|
</div>
|
|
</InfoCard>
|
|
|
|
<TipBox type="warning">
|
|
<strong>참고:</strong> SAM E-Sign의 전자서명은 종이 계약서의 서명과 동일한 법적 효력을 가지지만,
|
|
<strong>내용증명이나 공정증서</strong>와 동일한 강제집행력을 갖는 것은 아닙니다.
|
|
강제집행이 필요한 경우 별도의 법적 절차를 진행해야 합니다.
|
|
</TipBox>
|
|
|
|
{/* 민법상 계약 성립 */}
|
|
<SubTitle>6. 전자서명과 민법상 계약 성립</SubTitle>
|
|
|
|
<InfoCard title="계약은 어떻게 성립되나요?" color="blue">
|
|
<p className="mb-2">한국 민법에서 계약은 <strong>"낙성 불요식 계약"</strong> 원칙을 따릅니다.</p>
|
|
<p className="text-xs text-gray-500 mb-3">즉, 별도의 형식을 요구하지 않고, 당사자간의 합의만으로 계약이 성립합니다.</p>
|
|
<div className="bg-white rounded-lg p-3 border border-blue-100">
|
|
<p className="text-sm text-gray-700">따라서 계약서의 형태가 <strong>종이든, PDF든, 전자문서든</strong> 상관없이,
|
|
양쪽이 계약 내용에 합의했다는 사실을 증명할 수 있으면 법적으로 유효합니다.</p>
|
|
</div>
|
|
<div className="mt-3 space-y-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Badge color="blue">청약</Badge>
|
|
<span className="text-gray-600">계약 생성자가 계약서를 작성하고 서명 요청을 보냄</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Badge color="green">승낙</Badge>
|
|
<span className="text-gray-600">상대방이 계약서 내용을 확인하고 전자서명을 수행</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Badge color="purple">성립</Badge>
|
|
<span className="text-gray-600">양쪽 모두 서명 완료 → 계약 성립!</span>
|
|
</div>
|
|
</div>
|
|
</InfoCard>
|
|
|
|
{/* 관련 법령 요약 */}
|
|
<SubTitle>7. 관련 법령 요약</SubTitle>
|
|
|
|
<Table
|
|
headers={['법령', '조문', '핵심 내용']}
|
|
rows={[
|
|
['전자서명법', '제2조 제2호', '전자서명의 정의 (서명자 신원 + 서명 사실 표시)'],
|
|
['전자서명법', '제3조 제1항', '전자서명의 효력 부인 금지'],
|
|
['전자서명법', '제3조 제3항', '당사자 약정 시 서명/날인과 동일 효력'],
|
|
['전자문서법', '제4조 제1항', '전자문서의 법적 효력 인정'],
|
|
['전자문서법', '제5조', '전자문서의 서면 효력 (열람/재현/보존 요건)'],
|
|
['민법', '제527~532조', '계약의 성립 (청약과 승낙)'],
|
|
]}
|
|
/>
|
|
|
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mt-4 text-xs text-gray-500">
|
|
<p className="font-semibold text-gray-700 mb-2">참고 자료</p>
|
|
<ul className="space-y-0.5">
|
|
<li>- 전자서명법 (법률 제17354호, 2020.6.9. 전부개정, 2020.12.10. 시행)</li>
|
|
<li>- 전자문서 및 전자거래 기본법 (법률 제17799호)</li>
|
|
<li>- 국가법령정보센터 (law.go.kr)</li>
|
|
</ul>
|
|
<p className="mt-2 text-gray-400">
|
|
* 이 안내는 법률 자문을 대체하지 않습니다. 구체적인 법률 문제는 전문 변호사와 상담하세요.
|
|
</p>
|
|
</div>
|
|
|
|
{/* 용어 사전 */}
|
|
<SubTitle>용어 사전</SubTitle>
|
|
<Table
|
|
headers={['용어', '설명']}
|
|
rows={[
|
|
['갑 (작성자)', '계약서를 작성하고 서명을 요청하는 사람'],
|
|
['을 (상대방)', '이메일 링크를 통해 서명하는 사람'],
|
|
['OTP', '일회용 인증 코드 (One-Time Password) - 카카오 알림톡 또는 이메일로 전송'],
|
|
['카카오 알림톡', '카카오톡 비즈니스 채널을 통한 알림 메시지 서비스'],
|
|
['전자서명', '종이 서명을 대체하는 디지털 서명'],
|
|
['감사 추적', '누가, 언제, 무엇을 했는지 기록하는 시스템'],
|
|
['무결성 검증', '문서가 위변조되지 않았는지 확인하는 절차'],
|
|
['전자서명법', '전자서명의 법적 효력과 인증 체계를 규정하는 법률 (2020년 전면 개정)'],
|
|
['전자문서법', '전자문서 및 전자거래 기본법 — 전자문서의 법적 효력을 규정'],
|
|
['부인 방지', '서명 후 "나는 서명한 적 없다"고 부인하지 못하게 하는 보안 기능'],
|
|
['본인확인', '카카오 알림톡/이메일 OTP 인증을 통해 서명자가 본인임을 확인하는 절차'],
|
|
['해시값', '문서 내용을 고유한 암호 코드로 변환한 값 (위변조 탐지용)'],
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 2: Workflow
|
|
// ============================================================
|
|
const FlowStep = ({ number, title, desc, color = 'blue' }) => {
|
|
const bg = { blue: 'bg-blue-600', green: 'bg-green-600', yellow: 'bg-yellow-500', red: 'bg-red-600', purple: 'bg-purple-600', indigo: 'bg-indigo-600' };
|
|
return (
|
|
<div className="flex items-start gap-3">
|
|
<div className={`flex-shrink-0 w-8 h-8 ${bg[color] || bg.blue} text-white rounded-full flex items-center justify-center text-sm font-bold`}>{number}</div>
|
|
<div>
|
|
<p className="font-semibold text-gray-900">{title}</p>
|
|
<p className="text-sm text-gray-600">{desc}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const WorkflowTab = () => (
|
|
<div>
|
|
<SectionTitle>계약 생성 ~ 서명 완료 플로우</SectionTitle>
|
|
|
|
<SubTitle>전체 프로세스 (5단계)</SubTitle>
|
|
<div className="space-y-4 mb-8">
|
|
<FlowStep number="1" title="계약 생성 (Create)" desc="제목, PDF 업로드, 서명자 정보 입력 → status: draft" color="blue" />
|
|
<div className="ml-4 border-l-2 border-gray-200 h-4"></div>
|
|
<FlowStep number="2" title="서명 필드 배치 (Configure)" desc="PDF 위에 서명/날인/텍스트 필드 드래그&드롭 배치" color="indigo" />
|
|
<div className="ml-4 border-l-2 border-gray-200 h-4"></div>
|
|
<FlowStep number="3" title="서명 요청 발송 (Send)" desc="카카오 알림톡/이메일로 서명 링크 발송 → status: pending" color="purple" />
|
|
<div className="ml-4 border-l-2 border-gray-200 h-4"></div>
|
|
<FlowStep number="4" title="서명 수행 (Sign)" desc="알림톡/이메일 OTP 인증 → 문서 확인 → 서명 제출 → status: partially_signed / completed" color="yellow" />
|
|
<div className="ml-4 border-l-2 border-gray-200 h-4"></div>
|
|
<FlowStep number="5" title="완료 (Complete)" desc="모든 서명자 서명 완료 → status: completed, 완료 알림톡/이메일 발송" color="green" />
|
|
</div>
|
|
|
|
<SubTitle>계약 상태 전이</SubTitle>
|
|
<div className="bg-gray-50 rounded-lg p-6 my-4">
|
|
<div className="flex flex-wrap items-center gap-2 justify-center text-sm">
|
|
<Badge color="gray">draft</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="blue">pending</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="yellow">partially_signed</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="green">completed</Badge>
|
|
</div>
|
|
<div className="flex flex-wrap gap-6 justify-center mt-4 text-xs text-gray-500">
|
|
<span>draft → <Badge color="gray">cancelled</Badge></span>
|
|
<span>pending → <Badge color="red">expired</Badge></span>
|
|
<span>partially_signed → <Badge color="red">rejected</Badge></span>
|
|
</div>
|
|
</div>
|
|
|
|
<SubTitle>서명자 상태 전이</SubTitle>
|
|
<div className="bg-gray-50 rounded-lg p-6 my-4">
|
|
<div className="flex flex-wrap items-center gap-2 justify-center text-sm">
|
|
<Badge color="gray">waiting</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="blue">notified</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="yellow">authenticated</Badge>
|
|
<span className="text-gray-400">→</span>
|
|
<Badge color="green">signed</Badge>
|
|
</div>
|
|
<div className="flex flex-wrap gap-6 justify-center mt-4 text-xs text-gray-500">
|
|
<span>notified → <Badge color="red">rejected</Badge></span>
|
|
<span>authenticated → <Badge color="red">rejected</Badge></span>
|
|
</div>
|
|
</div>
|
|
|
|
<SubTitle>서명 순서 옵션</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InfoCard title="counterpart_first (기본)" color="blue">
|
|
<p>상대방이 먼저 서명 → 작성자가 나중에 서명</p>
|
|
</InfoCard>
|
|
<InfoCard title="creator_first" color="green">
|
|
<p>작성자가 먼저 서명 → 상대방이 나중에 서명</p>
|
|
</InfoCard>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 3: Architecture
|
|
// ============================================================
|
|
const ArchitectureTab = () => (
|
|
<div>
|
|
<SectionTitle>시스템 아키텍처</SectionTitle>
|
|
|
|
<SubTitle>DB 스키마 (4개 테이블)</SubTitle>
|
|
<Table
|
|
headers={['테이블', '주요 필드', '용도']}
|
|
rows={[
|
|
['esign_contracts', 'id, contract_code, status, title, original_file_path, original_file_hash, sign_order, expires_at', '계약 마스터'],
|
|
['esign_signers', 'id, contract_id, role, name, email, phone, access_token, otp_code, otp_expires_at, otp_attempts, signature_image_path, sign_session_token', '서명자 정보 & 인증'],
|
|
['esign_sign_fields', 'id, contract_id, signer_id, page_number, position_x, position_y, width, height, field_type', '서명 위치 좌표'],
|
|
['esign_audit_logs', 'id, contract_id, signer_id, action, ip_address, user_agent, metadata', '감사 추적 (삭제 불가)'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>서비스 레이어</SubTitle>
|
|
<Table
|
|
headers={['Service', '주요 메서드', '역할']}
|
|
rows={[
|
|
['EsignContractService', 'list, stats, create, show, cancel, send, remind, configureFields', '계약 CRUD + 비즈니스 로직'],
|
|
['EsignSignService', 'getByToken, sendOtp, verifyOtp, submitSignature, reject', '서명 프로세스 처리'],
|
|
['EsignPdfService', 'generateHash, verifyIntegrity, composeSigned*, addAuditPage*', 'PDF 무결성 + 합성 (* stub)'],
|
|
['EsignAuditService', 'log, logPublic, getContractLogs', '감사 로그 기록/조회'],
|
|
['KakaoAlimtalkService', 'sendSignRequest, sendOtp, sendCompletion, sendReminder', '카카오 알림톡 발송 (예정)'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>멀티테넌트 구조</SubTitle>
|
|
<InfoCard title="tenant_id 기반 데이터 격리" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>모든 esign_ 테이블에 <code className="bg-blue-100 px-1 rounded">tenant_id</code> 컬럼 포함</li>
|
|
<li><code className="bg-blue-100 px-1 rounded">BelongsToTenant</code> trait 으로 글로벌 스코프 적용</li>
|
|
<li>공개 서명 API에서는 <code className="bg-blue-100 px-1 rounded">withoutGlobalScopes()</code> 사용</li>
|
|
</ul>
|
|
</InfoCard>
|
|
|
|
<SubTitle>컨트롤러 구조</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InfoCard title="API (sam-api)" color="gray">
|
|
<ul className="space-y-1">
|
|
<li><strong>EsignContractController</strong> - 계약 관리 10 엔드포인트</li>
|
|
<li><strong>EsignSignController</strong> - 서명 프로세스 6 엔드포인트</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="MNG (sam-mng)" color="gray">
|
|
<ul className="space-y-1">
|
|
<li><strong>EsignController</strong> - 인증 필요 화면 5개</li>
|
|
<li><strong>EsignPublicController</strong> - 공개 서명 화면 3개</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
<SubTitle>파일 저장 구조</SubTitle>
|
|
<CodeBlock>{`storage/app/esign/
|
|
├── contracts/
|
|
│ └── {contract_id}/
|
|
│ ├── original.pdf # 원본 PDF
|
|
│ └── signed.pdf # 서명 완료 PDF (v1.1)
|
|
└── signatures/
|
|
└── {signer_id}/
|
|
└── signature.png # 서명 이미지`}</CodeBlock>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 4: API Specification
|
|
// ============================================================
|
|
const ApiTab = () => (
|
|
<div>
|
|
<SectionTitle>API 명세 (총 16 엔드포인트)</SectionTitle>
|
|
|
|
<SubTitle>계약 관리 API (인증 필요 - 10개)</SubTitle>
|
|
<Table
|
|
headers={['Method', 'Path', '설명']}
|
|
rows={[
|
|
[<Badge color="green">GET</Badge>, '/esign/contracts', '계약 목록 (페이지네이션, 필터)'],
|
|
[<Badge color="green">GET</Badge>, '/esign/contracts/stats', '상태별 통계'],
|
|
[<Badge color="green">GET</Badge>, '/esign/contracts/{id}', '계약 상세'],
|
|
[<Badge color="blue">POST</Badge>, '/esign/contracts', '계약 생성 (multipart/form-data)'],
|
|
[<Badge color="blue">POST</Badge>, '/esign/contracts/{id}/fields', '서명 필드 설정'],
|
|
[<Badge color="blue">POST</Badge>, '/esign/contracts/{id}/send', '서명 요청 발송'],
|
|
[<Badge color="blue">POST</Badge>, '/esign/contracts/{id}/remind', '리마인드 발송'],
|
|
[<Badge color="blue">POST</Badge>, '/esign/contracts/{id}/cancel', '계약 취소'],
|
|
[<Badge color="green">GET</Badge>, '/esign/contracts/{id}/download', 'PDF 다운로드'],
|
|
[<Badge color="green">GET</Badge>, '/esign/contracts/{id}/verify', '무결성 검증'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>서명 프로세스 API (토큰 기반 - 6개)</SubTitle>
|
|
<Table
|
|
headers={['Method', 'Path', '설명']}
|
|
rows={[
|
|
[<Badge color="green">GET</Badge>, '/api/esign/sign/{token}', '계약 정보 조회'],
|
|
[<Badge color="blue">POST</Badge>, '/api/esign/sign/{token}/otp/send', 'OTP 발송'],
|
|
[<Badge color="blue">POST</Badge>, '/api/esign/sign/{token}/otp/verify', 'OTP 검증'],
|
|
[<Badge color="green">GET</Badge>, '/api/esign/sign/{token}/document', 'PDF 문서 조회'],
|
|
[<Badge color="blue">POST</Badge>, '/api/esign/sign/{token}/submit', '서명 제출'],
|
|
[<Badge color="blue">POST</Badge>, '/api/esign/sign/{token}/reject', '서명 거절'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>요청/응답 예시</SubTitle>
|
|
|
|
<p className="text-sm font-medium text-gray-700 mb-2">계약 생성 (POST /esign/contracts)</p>
|
|
<CodeBlock title="Response 201">{`{
|
|
"success": true,
|
|
"message": "계약이 생성되었습니다",
|
|
"data": {
|
|
"id": 1,
|
|
"contract_code": "ES-20260212-A1B2C3",
|
|
"status": "draft",
|
|
"title": "2026년 블라인드 납품 계약서",
|
|
"original_file_hash": "e3b0c44298fc1c14...",
|
|
"signers": [
|
|
{ "id": 1, "role": "creator", "name": "김담당", "status": "waiting" },
|
|
{ "id": 2, "role": "counterpart", "name": "이대표", "status": "waiting" }
|
|
]
|
|
}
|
|
}`}</CodeBlock>
|
|
|
|
<p className="text-sm font-medium text-gray-700 mb-2 mt-4">OTP 검증 (POST /api/esign/sign/{'{token}'}/otp/verify)</p>
|
|
<CodeBlock title="Response 200">{`{
|
|
"success": true,
|
|
"message": "OTP 인증이 완료되었습니다",
|
|
"data": {
|
|
"sign_session_token": "a3f8b2c1d4e5f6...",
|
|
"signer": { "id": 2, "status": "authenticated" }
|
|
}
|
|
}`}</CodeBlock>
|
|
|
|
<SubTitle>Enum 값</SubTitle>
|
|
<Table
|
|
headers={['카테고리', '값']}
|
|
rows={[
|
|
['계약 상태', 'draft, pending, partially_signed, completed, expired, cancelled, rejected'],
|
|
['서명 순서', 'counterpart_first, creator_first'],
|
|
['서명자 역할', 'creator, counterpart'],
|
|
['서명자 상태', 'waiting, notified, authenticated, signed, rejected'],
|
|
['필드 타입', 'signature, stamp, text, date, checkbox'],
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 5: Security
|
|
// ============================================================
|
|
const SecurityTab = () => (
|
|
<div>
|
|
<SectionTitle>보안 아키텍처</SectionTitle>
|
|
|
|
<SubTitle>OTP 인증</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<InfoCard title="OTP 스펙" color="yellow">
|
|
<ul className="space-y-1">
|
|
<li>6자리 숫자 코드</li>
|
|
<li>유효 시간: <strong>5분</strong></li>
|
|
<li>최대 시도: <strong>5회</strong></li>
|
|
<li>저장: bcrypt 해시 (v1.1 예정, 현재 평문)</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="인증 프로세스" color="blue">
|
|
<ol className="space-y-1 list-decimal list-inside">
|
|
<li>서명자에게 카카오 알림톡(우선) 또는 이메일로 OTP 발송</li>
|
|
<li>서명자가 6자리 코드 입력</li>
|
|
<li>검증 성공 → sign_session_token 발급</li>
|
|
<li>이후 요청에 세션 토큰 사용</li>
|
|
</ol>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
<SubTitle>토큰 보안</SubTitle>
|
|
<Table
|
|
headers={['토큰', '용도', '길이', '만료']}
|
|
rows={[
|
|
['access_token', '서명 링크', '128자', '계약 만료일까지'],
|
|
['sign_session_token', 'OTP 인증 후 세션', '64자', '별도 만료 없음'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>문서 무결성</SubTitle>
|
|
<InfoCard title="SHA-256 해시 검증" color="green">
|
|
<ul className="space-y-1">
|
|
<li>PDF 업로드 시 SHA-256 해시 생성 및 DB 저장</li>
|
|
<li><code className="bg-green-100 px-1 rounded">hash_equals()</code> 사용 (타이밍 공격 방지)</li>
|
|
<li>서명 완료 PDF 별도 해시 생성</li>
|
|
<li>다운로드 시 무결성 검증 가능</li>
|
|
</ul>
|
|
</InfoCard>
|
|
|
|
<SubTitle>감사 로그 (Audit Trail)</SubTitle>
|
|
<InfoCard title="삭제 불가 - 법적 증적" color="red">
|
|
<p className="mb-2">모든 주요 액션이 자동 기록됩니다:</p>
|
|
<div className="flex flex-wrap gap-1">
|
|
{['created','sent','viewed','otp_sent','authenticated','signed','rejected','completed','cancelled','reminded','downloaded'].map(a => (
|
|
<Badge key={a} color="gray">{a}</Badge>
|
|
))}
|
|
</div>
|
|
<p className="mt-2 text-xs text-gray-500">각 로그에 IP, User-Agent, 메타데이터 포함</p>
|
|
</InfoCard>
|
|
|
|
<SubTitle>법적 준거</SubTitle>
|
|
<Table
|
|
headers={['항목', '구현']}
|
|
rows={[
|
|
['전자서명법 제2조', '전자 서명 + 본인 확인 (카카오 알림톡/이메일 OTP)'],
|
|
['본인 확인', '카카오 알림톡/이메일 OTP 6자리, 5분, 5회 제한'],
|
|
['동의 확인', '체크박스 2개 (문서 확인 + 서명 동의)'],
|
|
['위변조 방지', 'SHA-256 해시 + 무결성 검증 API'],
|
|
['서명 후 불변', '상태 변경 불가, 감사 로그 영구 보존'],
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 6: Screens
|
|
// ============================================================
|
|
const ScreensTab = () => (
|
|
<div>
|
|
<SectionTitle>화면 설계 (8개 화면)</SectionTitle>
|
|
|
|
<SubTitle>인증 필요 화면 (5개)</SubTitle>
|
|
<div className="space-y-4 mb-6">
|
|
<InfoCard title="1. 대시보드 (/esign)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>상태별 통계 카드 (전체, 진행중, 완료, 만료)</li>
|
|
<li>계약 목록 테이블 (페이지네이션, 기본 20건)</li>
|
|
<li>필터: 상태, 검색어, 기간</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="2. 계약 생성 (/esign/create)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>제목 입력 (필수, 최대 200자)</li>
|
|
<li>PDF 업로드 (최대 20MB)</li>
|
|
<li>서명 순서 선택 (상대방 우선 / 작성자 우선)</li>
|
|
<li>작성자 & 상대방 정보 (이름, 이메일, 전화번호)</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="3. 계약 상세 (/esign/{id})" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>기본 정보 표시 (제목, 코드, 상태, 날짜)</li>
|
|
<li>서명자별 상태 카드</li>
|
|
<li>감사 로그 타임라인</li>
|
|
<li>액션 버튼: 발송, 리마인드, 취소, 다운로드, 무결성 검증</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="4. 서명 필드 배치 (/esign/{id}/fields)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>PDF.js로 문서 렌더링</li>
|
|
<li>드래그 & 드롭으로 서명 필드 배치</li>
|
|
<li>색상 구분: 작성자(파란색), 상대방(빨간색)</li>
|
|
<li>필드 타입: signature, stamp, text, date, checkbox</li>
|
|
<li>좌표는 % 단위 (0-100)</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="5. 서명 요청 발송 (/esign/{id}/send)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>발송 전 체크리스트</li>
|
|
<li>서명 순서 최종 확인</li>
|
|
<li>발송 버튼</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
<SubTitle>공개 화면 - 서명자용 (3개)</SubTitle>
|
|
<div className="space-y-4 mb-6">
|
|
<InfoCard title="6. OTP 인증 (/esign/sign/{token})" color="green">
|
|
<ul className="space-y-1">
|
|
<li>계약 정보 표시</li>
|
|
<li>OTP 발송 버튼 (카카오 알림톡 우선, 이메일 폴백)</li>
|
|
<li>6자리 입력 필드</li>
|
|
<li>카운트다운 타이머 (5분)</li>
|
|
<li>남은 시도 횟수 표시</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="7. 서명 수행 (/esign/sign/{token}/sign)" color="green">
|
|
<ul className="space-y-1">
|
|
<li>3단계 프로세스: 문서 확인 → 서명 → 확인</li>
|
|
<li>PDF 다운로드 링크</li>
|
|
<li>SignaturePad 캔버스 (터치/마우스)</li>
|
|
<li>동의 체크박스 2개</li>
|
|
<li>제출 / 거절 옵션</li>
|
|
</ul>
|
|
</InfoCard>
|
|
<InfoCard title="8. 완료 (/esign/sign/{token}/done)" color="green">
|
|
<ul className="space-y-1">
|
|
<li>서명 완료 메시지</li>
|
|
<li>계약 요약 정보</li>
|
|
<li>서명 일시 표시</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
|
|
<SubTitle>스토리보드</SubTitle>
|
|
<InfoCard title="화면 설계 스토리보드 (PPTX)" color="gray">
|
|
<p className="mb-2">8개 화면의 상세 UI 와이어프레임이 포함된 스토리보드입니다.</p>
|
|
<a href="/docs/esign/esign-storyboard.pptx" download
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
esign-storyboard.pptx 다운로드
|
|
</a>
|
|
</InfoCard>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 7: Operations Guide
|
|
// ============================================================
|
|
const OperationsTab = () => (
|
|
<div>
|
|
<SectionTitle>운영 가이드</SectionTitle>
|
|
|
|
<SubTitle>초기 배포</SubTitle>
|
|
<CodeBlock title="배포 절차">{`# 1. 마이그레이션
|
|
docker exec sam-api-1 php artisan migrate
|
|
|
|
# 2. 스토리지 설정
|
|
docker exec sam-api-1 mkdir -p storage/app/esign
|
|
chmod 775 storage/app/esign
|
|
|
|
# 3. 라우트 캐시
|
|
docker exec sam-api-1 php artisan route:cache
|
|
docker exec sam-mng-1 php artisan route:cache`}</CodeBlock>
|
|
|
|
<SubTitle>환경 변수</SubTitle>
|
|
<Table
|
|
headers={['변수', '개발 환경', '운영 환경']}
|
|
rows={[
|
|
['APP_ENV', 'local', 'production'],
|
|
['APP_DEBUG', 'true', 'false'],
|
|
['MAIL_MAILER', 'log', 'smtp'],
|
|
['QUEUE_CONNECTION', 'sync', 'database'],
|
|
['FILESYSTEM_DISK', 'local', 'local / s3'],
|
|
['KAKAO_ALIMTALK_SENDER_KEY', '-', '카카오 비즈니스 채널 발신 키'],
|
|
['KAKAO_ALIMTALK_TEMPLATE_*', '-', '알림톡 템플릿 코드 (서명요청/OTP/완료)'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>모니터링</SubTitle>
|
|
<Table
|
|
headers={['지표', '확인 방법', '임계치']}
|
|
rows={[
|
|
['API 응답 시간', 'Nginx 로그', '> 3초 경고'],
|
|
['에러율', 'Laravel 로그', '> 10건/시간'],
|
|
['디스크 사용량', 'df -h', '> 80% 경고'],
|
|
['메일 큐', 'queue:failed', '> 0 알림'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>백업 전략</SubTitle>
|
|
<Table
|
|
headers={['대상', '방법', '주기']}
|
|
rows={[
|
|
['DB (esign_ 테이블)', 'mysqldump', '매일'],
|
|
['PDF 파일', 'rsync', '매일'],
|
|
['감사 로그', '영구 보존', '삭제 불가'],
|
|
]}
|
|
/>
|
|
|
|
<SubTitle>장애 대응</SubTitle>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InfoCard title="카카오 알림톡 발송 실패" color="yellow">
|
|
<ol className="list-decimal list-inside space-y-1">
|
|
<li>카카오 비즈니스 채널 상태 확인</li>
|
|
<li>발신 키(SENDER_KEY) 유효성 확인</li>
|
|
<li>템플릿 승인 상태 확인</li>
|
|
<li>수신자 전화번호 형식 확인</li>
|
|
<li>알림톡 실패 시 이메일 폴백 자동 발송 확인</li>
|
|
</ol>
|
|
</InfoCard>
|
|
<InfoCard title="이메일 발송 실패" color="yellow">
|
|
<ol className="list-decimal list-inside space-y-1">
|
|
<li>Laravel 로그 확인</li>
|
|
<li>SMTP 설정 확인</li>
|
|
<li>큐 실패 건 재시도: <code className="bg-yellow-100 px-1 rounded">queue:retry</code></li>
|
|
</ol>
|
|
</InfoCard>
|
|
<InfoCard title="PDF 업로드 실패" color="yellow">
|
|
<ol className="list-decimal list-inside space-y-1">
|
|
<li>파일 크기 제한 확인 (20MB)</li>
|
|
<li>디스크 공간 확인</li>
|
|
<li>storage 디렉토리 권한 확인</li>
|
|
</ol>
|
|
</InfoCard>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab 8: Changelog
|
|
// ============================================================
|
|
const ChangelogTab = () => (
|
|
<div>
|
|
<SectionTitle>변경 이력</SectionTitle>
|
|
|
|
<SubTitle>v1.0.0 (2026-02-12) - 최초 릴리즈</SubTitle>
|
|
<InfoCard title="SAM E-Sign 전체 구현" color="green">
|
|
<Table
|
|
headers={['저장소', '커밋', '파일', '라인']}
|
|
rows={[
|
|
['API', '2', '24', '+1,709'],
|
|
['MNG', '1', '11', '+1,564'],
|
|
['Docs', '8', '10', '-'],
|
|
['합계', '11', '45', '+3,273'],
|
|
]}
|
|
/>
|
|
</InfoCard>
|
|
|
|
<div className="mt-4 space-y-2">
|
|
<p className="text-sm text-gray-700"><Badge color="green">API</Badge> 마이그레이션 4개, 모델 4개, 서비스 4개, 컨트롤러 2개, FormRequest 4개, Mail 1개</p>
|
|
<p className="text-sm text-gray-700"><Badge color="blue">MNG</Badge> 컨트롤러 2개, 뷰 8개, 라우트 설정</p>
|
|
<p className="text-sm text-gray-700"><Badge color="gray">Docs</Badge> 기획서, 기술 설계서, API 명세서, 사용자 매뉴얼, 운영 가이드 등 8개 문서</p>
|
|
</div>
|
|
|
|
<SubTitle>향후 계획</SubTitle>
|
|
|
|
<div className="space-y-4">
|
|
<InfoCard title="v1.1 (계획)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>PDF 서명 합성 (FPDI/FPDF)</li>
|
|
<li>감사 추적 페이지 PDF 포함</li>
|
|
<li><strong>카카오 알림톡 연동</strong> (서명 요청/OTP/리마인더/완료 알림)</li>
|
|
<li>자동 만료 스케줄러</li>
|
|
<li>큐 워커 설정</li>
|
|
</ul>
|
|
</InfoCard>
|
|
|
|
<InfoCard title="v1.2 (계획)" color="blue">
|
|
<ul className="space-y-1">
|
|
<li>대량 계약 발송</li>
|
|
<li>계약 템플릿 관리</li>
|
|
<li>알림톡 발송 통계 대시보드</li>
|
|
</ul>
|
|
</InfoCard>
|
|
|
|
<InfoCard title="v2.0 (계획)" color="purple">
|
|
<ul className="space-y-1">
|
|
<li>AWS S3 파일 저장소 전환</li>
|
|
<li>공인 인증서 연동</li>
|
|
<li>모바일 반응형 UI</li>
|
|
<li>다국어 지원</li>
|
|
</ul>
|
|
</InfoCard>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
// ============================================================
|
|
// Tab Content Map
|
|
// ============================================================
|
|
const TAB_CONTENT = {
|
|
overview: OverviewTab,
|
|
manual: UserManualTab,
|
|
workflow: WorkflowTab,
|
|
architecture: ArchitectureTab,
|
|
api: ApiTab,
|
|
security: SecurityTab,
|
|
screens: ScreensTab,
|
|
operations: OperationsTab,
|
|
changelog: ChangelogTab,
|
|
};
|
|
|
|
// ============================================================
|
|
// Main App
|
|
// ============================================================
|
|
const App = () => {
|
|
const [activeTab, setActiveTab] = useState('overview');
|
|
const TabContent = TAB_CONTENT[activeTab];
|
|
|
|
useEffect(() => {
|
|
if (window.lucide) lucide.createIcons();
|
|
}, [activeTab]);
|
|
|
|
return (
|
|
<div className="p-4 sm:p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">SAM E-Sign 문서</h1>
|
|
<p className="text-sm text-gray-500 mt-1">SAM E-Sign 기술 문서</p>
|
|
</div>
|
|
<a href="/esign"
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium"
|
|
hx-boost="false">
|
|
← 대시보드로 돌아가기
|
|
</a>
|
|
</div>
|
|
|
|
<div className="flex flex-col lg:flex-row gap-6">
|
|
{/* Desktop: Left sidebar tabs */}
|
|
<nav className="hidden lg:block w-56 flex-shrink-0">
|
|
<div className="sticky top-6 space-y-1">
|
|
{TABS.map(tab => (
|
|
<button key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition-colors
|
|
${activeTab === tab.id
|
|
? 'bg-blue-50 text-blue-700 font-semibold'
|
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'}`}>
|
|
<i data-lucide={tab.icon} className="w-4 h-4"></i>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Mobile: Horizontal scroll tabs */}
|
|
<nav className="lg:hidden overflow-x-auto -mx-6 px-6">
|
|
<div className="flex gap-1 pb-2 min-w-max">
|
|
{TABS.map(tab => (
|
|
<button key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm whitespace-nowrap transition-colors
|
|
${activeTab === tab.id
|
|
? 'bg-blue-50 text-blue-700 font-semibold'
|
|
: 'text-gray-600 hover:bg-gray-100'}`}>
|
|
<i data-lucide={tab.icon} className="w-4 h-4"></i>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Tab content */}
|
|
<div className="flex-1 min-w-0 bg-white rounded-xl border border-gray-200 p-6">
|
|
<TabContent />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
ReactDOM.createRoot(document.getElementById('esign-docs-root')).render(<App />);
|
|
</script>
|
|
@endverbatim
|
|
@endpush
|