- 전자계약 (E-Sign), 전자계약 대시보드, 전자계약 상세 등 → SAM E-Sign - 9개 파일, 19곳 수정 (코드 식별자/URL은 유지) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
6.1 KiB
PHP
130 lines
6.1 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'SAM E-Sign - 서명 요청 발송')
|
|
|
|
@section('content')
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<div id="esign-send-root" data-contract-id="{{ $contractId }}"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
@include('partials.react-cdn')
|
|
@verbatim
|
|
<script type="text/babel">
|
|
const { useState, useEffect, useCallback } = React;
|
|
|
|
const CONTRACT_ID = document.getElementById('esign-send-root')?.dataset.contractId;
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
|
|
const getHeaders = () => ({
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
});
|
|
|
|
const App = () => {
|
|
const [contract, setContract] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [sending, setSending] = useState(false);
|
|
|
|
const fetchContract = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/esign/contracts/${CONTRACT_ID}`, { headers: getHeaders() });
|
|
const json = await res.json();
|
|
if (json.success) setContract(json.data);
|
|
} catch (e) { console.error(e); }
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { fetchContract(); }, [fetchContract]);
|
|
|
|
const handleSend = async () => {
|
|
if (!window.confirm('서명 요청을 발송하시겠습니까?\n첫 번째 서명자에게 이메일이 발송됩니다.')) return;
|
|
setSending(true);
|
|
try {
|
|
const res = await fetch(`/esign/contracts/${CONTRACT_ID}/send`, {
|
|
method: 'POST', headers: getHeaders(),
|
|
});
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
alert('서명 요청이 발송되었습니다.');
|
|
location.href = `/esign/${CONTRACT_ID}`;
|
|
} else {
|
|
alert(json.message || '발송에 실패했습니다.');
|
|
}
|
|
} catch (e) { alert('서버 오류가 발생했습니다.'); }
|
|
setSending(false);
|
|
};
|
|
|
|
if (loading) return <div className="p-6 text-center text-gray-400">로딩 중...</div>;
|
|
if (!contract) return <div className="p-6 text-center text-red-500">계약을 찾을 수 없습니다.</div>;
|
|
|
|
const signers = contract.signers || [];
|
|
const fieldsCount = contract.sign_fields?.length || 0;
|
|
|
|
return (
|
|
<div className="p-6 max-w-2xl mx-auto">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<a href={`/esign/${CONTRACT_ID}`} className="text-gray-400 hover:text-gray-600" hx-boost="false">←</a>
|
|
<h1 className="text-2xl font-bold text-gray-900">서명 요청 발송</h1>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg border p-6 mb-6">
|
|
<h2 className="text-lg font-semibold mb-4">발송 전 확인</h2>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs text-white ${contract.title ? 'bg-green-500' : 'bg-red-500'}`}>
|
|
{contract.title ? '✓' : '!'}
|
|
</span>
|
|
<span className="text-sm">계약 제목: <strong>{contract.title}</strong></span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs text-white ${contract.original_file_name ? 'bg-green-500' : 'bg-red-500'}`}>
|
|
{contract.original_file_name ? '✓' : '!'}
|
|
</span>
|
|
<span className="text-sm">PDF 파일: <strong>{contract.original_file_name || '미업로드'}</strong></span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs text-white ${fieldsCount > 0 ? 'bg-green-500' : 'bg-red-500'}`}>
|
|
{fieldsCount > 0 ? '✓' : '!'}
|
|
</span>
|
|
<span className="text-sm">서명 필드: <strong>{fieldsCount}개 설정됨</strong></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg border p-6 mb-6">
|
|
<h2 className="text-lg font-semibold mb-4">서명 순서</h2>
|
|
<div className="space-y-3">
|
|
{signers.sort((a, b) => a.sign_order - b.sign_order).map((s, i) => (
|
|
<div key={s.id} className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg">
|
|
<span className="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-sm font-bold">{s.sign_order || i + 1}</span>
|
|
<div>
|
|
<p className="font-medium text-sm">{s.name} <span className="text-gray-400 text-xs">({s.role === 'creator' ? '작성자' : '상대방'})</span></p>
|
|
<p className="text-xs text-gray-500">{s.email}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<a href={`/esign/${CONTRACT_ID}`} className="px-6 py-2 border rounded-lg text-gray-700 hover:bg-gray-50 text-sm" hx-boost="false">돌아가기</a>
|
|
<button onClick={handleSend} disabled={sending || fieldsCount === 0}
|
|
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium disabled:opacity-50">
|
|
{sending ? '발송 중...' : '서명 요청 발송'}
|
|
</button>
|
|
</div>
|
|
|
|
{fieldsCount === 0 && (
|
|
<p className="text-red-500 text-sm mt-3 text-right">서명 필드를 먼저 설정해 주세요.</p>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
ReactDOM.createRoot(document.getElementById('esign-send-root')).render(<App />);
|
|
</script>
|
|
@endverbatim
|
|
@endpush
|