Files
sam-manage/resources/views/esign/create.blade.php
김보곤 440cd11ece refactor:esign 페이지 및 전역 레이아웃 React CDN 통합
- esign 전자서명 관련 9개 파일 업데이트
- layouts/app.blade.php 업데이트
- fcm.js React 관련 변경사항 반영
2026-02-12 10:35:04 +09:00

156 lines
8.0 KiB
PHP

@extends('layouts.app')
@section('title', '새 전자계약 생성')
@section('content')
<meta name="csrf-token" content="{{ csrf_token() }}">
<div id="esign-create-root"></div>
@endsection
@push('scripts')
@include('partials.react-cdn')
@verbatim
<script type="text/babel">
const { useState, useRef } = React;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
const App = () => {
const [form, setForm] = useState({
title: '', description: '', sign_order_type: 'counterpart_first', expires_at: '',
creator_name: '', creator_email: '', creator_phone: '',
counterpart_name: '', counterpart_email: '', counterpart_phone: '',
});
const [file, setFile] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [errors, setErrors] = useState({});
const fileRef = useRef(null);
const handleChange = (key, val) => setForm(f => ({...f, [key]: val}));
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
setErrors({});
const fd = new FormData();
Object.entries(form).forEach(([k, v]) => { if (v) fd.append(k, v); });
if (file) fd.append('file', file);
try {
fd.append('_token', csrfToken);
fd.append('signers[0][name]', form.creator_name);
fd.append('signers[0][email]', form.creator_email);
fd.append('signers[0][phone]', form.creator_phone || '');
fd.append('signers[0][role]', 'creator');
fd.append('signers[1][name]', form.counterpart_name);
fd.append('signers[1][email]', form.counterpart_email);
fd.append('signers[1][phone]', form.counterpart_phone || '');
fd.append('signers[1][role]', 'counterpart');
const res = await fetch('/esign/contracts/store', {
method: 'POST',
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: fd,
});
const json = await res.json();
if (json.success) {
location.href = `/esign/${json.data.id}/fields`;
} else {
setErrors(json.errors || { general: json.message });
}
} catch (e) {
setErrors({ general: '서버 오류가 발생했습니다.' });
}
setSubmitting(false);
};
const Input = ({ label, name, type = 'text', required = false, placeholder = '' }) => (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{label} {required && <span className="text-red-500">*</span>}</label>
<input type={type} value={form[name] || ''} onChange={e => handleChange(name, e.target.value)}
placeholder={placeholder} required={required}
className="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{errors[name] && <p className="text-red-500 text-xs mt-1">{errors[name]}</p>}
</div>
);
return (
<div className="p-6 max-w-3xl mx-auto">
<div className="flex items-center gap-3 mb-6">
<a href="/esign" className="text-gray-400 hover:text-gray-600" hx-boost="false">&larr;</a>
<h1 className="text-2xl font-bold text-gray-900"> 전자계약 생성</h1>
</div>
{errors.general && <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">{errors.general}</div>}
<form onSubmit={handleSubmit} className="space-y-6">
{/* 계약 정보 */}
<div className="bg-white rounded-lg border p-6">
<h2 className="text-lg font-semibold mb-4">계약 정보</h2>
<div className="space-y-4">
<Input label="계약 제목" name="title" required placeholder="예: 2026년 공급 계약서" />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">설명</label>
<textarea value={form.description} onChange={e => handleChange('description', e.target.value)}
placeholder="계약에 대한 간단한 설명 (선택)" rows={3}
className="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">PDF 파일 <span className="text-red-500">*</span></label>
<input ref={fileRef} type="file" accept=".pdf" onChange={e => setFile(e.target.files[0])} required
className="w-full border rounded-lg px-3 py-2 text-sm file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:bg-blue-50 file:text-blue-700 file:text-sm file:font-medium" />
{file && <p className="text-xs text-gray-500 mt-1">{file.name} ({(file.size / 1024 / 1024).toFixed(2)} MB)</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">서명 순서</label>
<select value={form.sign_order_type} onChange={e => handleChange('sign_order_type', e.target.value)}
className="w-full border rounded-lg px-3 py-2 text-sm">
<option value="counterpart_first">상대방 먼저 서명</option>
<option value="creator_first">작성자 먼저 서명</option>
</select>
</div>
<Input label="만료일" name="expires_at" type="datetime-local" />
</div>
</div>
</div>
{/* 작성자 정보 */}
<div className="bg-white rounded-lg border p-6">
<h2 className="text-lg font-semibold mb-4">작성자 () 정보</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input label="이름" name="creator_name" required placeholder="홍길동" />
<Input label="이메일" name="creator_email" type="email" required placeholder="hong@example.com" />
<Input label="전화번호" name="creator_phone" placeholder="010-1234-5678" />
</div>
</div>
{/* 상대방 정보 */}
<div className="bg-white rounded-lg border p-6">
<h2 className="text-lg font-semibold mb-4">상대방 정보</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Input label="이름" name="counterpart_name" required placeholder="김철수" />
<Input label="이메일" name="counterpart_email" type="email" required placeholder="kim@example.com" />
<Input label="전화번호" name="counterpart_phone" placeholder="010-5678-1234" />
</div>
</div>
{/* 버튼 */}
<div className="flex justify-end gap-3">
<a href="/esign" className="px-6 py-2 border rounded-lg text-gray-700 hover:bg-gray-50 text-sm" hx-boost="false">취소</a>
<button type="submit" disabled={submitting}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium disabled:opacity-50">
{submitting ? '생성 중...' : '계약 생성 및 서명 위치 설정'}
</button>
</div>
</form>
</div>
);
};
ReactDOM.createRoot(document.getElementById('esign-create-root')).render(<App />);
</script>
@endverbatim
@endpush