feat:새 계약 생성 - 계약제목 드롭박스 + 번개 버튼 랜덤 상대방

- 계약 제목을 드롭박스로 변경 (영업파트너 계약서/비밀유지 서약서/고객 서비스이용 계약서/직접입력)
- 직접입력 선택 시 텍스트 입력 필드 표시
- 번개마크 2개 → 1개로 축소
- 번개마크 클릭 시 영업파트너 목록에서 랜덤으로 상대방 정보 자동 채우기

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-19 23:13:16 +09:00
parent f1c557b37c
commit abf424d10e

View File

@@ -36,6 +36,12 @@
{ id: 2, title: '템플릿 & 미리보기' },
{ id: 3, title: '확인 & 생성' },
];
const TITLE_PRESETS = [
{ value: '영업파트너 계약서', label: '영업파트너 계약서' },
{ value: '비밀유지 서약서', label: '비밀유지 서약서' },
{ value: '고객 서비스이용 계약서', label: '고객 서비스이용 계약서' },
{ value: '__custom__', label: '직접입력' },
];
// ─── Input ───
const Input = ({ label, name, value, error, onChange, type = 'text', required = false, placeholder = '', style }) => (
@@ -350,11 +356,12 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
const App = () => {
const [step, setStep] = useState(1);
const [form, setForm] = useState({
title: '', description: '', sign_order_type: 'counterpart_first',
title: '영업파트너 계약서', description: '', sign_order_type: 'counterpart_first',
expires_at: (() => { const d = new Date(); d.setDate(d.getDate() + 7); return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0') + 'T23:59'; })(),
creator_name: '(주) 코드브릿지엑스', creator_email: 'contact@codebridge-x.com', creator_phone: '02-6347-0005',
counterpart_name: '', counterpart_email: '', counterpart_phone: '',
});
const [titleType, setTitleType] = useState('영업파트너 계약서');
const [file, setFile] = useState(null);
const [registeredStamp, setRegisteredStamp] = useState(null);
const [submitting, setSubmitting] = useState(false);
@@ -415,33 +422,22 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
} catch (_) { setTemplateItems([]); }
};
const fillTestData = () => {
const year = new Date().getFullYear();
setForm(f => ({
...f,
title: `${year}년 영업파트너 계약서`,
description: '영업파트너 위촉 및 수수료 지급에 관한 계약',
creator_name: '(주) 코드브릿지엑스',
creator_email: 'contact@codebridge-x.com',
creator_phone: '02-6347-0005',
counterpart_name: '김지훈',
counterpart_email: 'awesomemyword@gmail.com',
counterpart_phone: '010-5123-8210',
}));
};
const fillTestDataNda = () => {
setForm(f => ({
...f,
title: '비밀유지서약서',
description: '영업파트너 등 비밀유지서약서',
creator_name: '(주) 코드브릿지엑스',
creator_email: 'contact@codebridge-x.com',
creator_phone: '02-6347-0005',
counterpart_name: '김지훈',
counterpart_email: 'awesomemyword@gmail.com',
counterpart_phone: '010-5123-8210',
}));
const fillRandomCounterpart = async () => {
try {
const res = await fetch(`/esign/contracts/search-partners?q=`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
});
const json = await res.json();
if (json.success && json.data.length > 0) {
const partner = json.data[Math.floor(Math.random() * json.data.length)];
setForm(f => ({
...f,
counterpart_name: partner.name || '',
counterpart_email: partner.email || '',
counterpart_phone: partner.phone || '',
}));
}
} catch (_) {}
};
// 파트너 선택 시 자동 채우기
@@ -567,7 +563,24 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
<div className="bg-white rounded-lg border p-4">
<h2 className="text-sm font-semibold text-gray-900 mb-3">계약 정보</h2>
<div className="space-y-3">
<Input label="계약 제목" name="title" value={form.title} error={errors.title} onChange={handleChange} required placeholder="예: 2026년 공급 계약서" />
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">계약 제목 <span className="text-red-500">*</span></label>
<select value={titleType} onChange={e => {
const val = e.target.value;
setTitleType(val);
if (val !== '__custom__') handleChange('title', val);
else handleChange('title', '');
}}
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none mb-1">
{TITLE_PRESETS.map(p => <option key={p.value} value={p.value}>{p.label}</option>)}
</select>
{titleType === '__custom__' && (
<input type="text" value={form.title} onChange={e => handleChange('title', e.target.value)}
placeholder="계약 제목을 직접 입력하세요"
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors mt-1" />
)}
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title}</p>}
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">설명</label>
<textarea value={form.description} onChange={e => handleChange('description', e.target.value)}
@@ -875,20 +888,14 @@ className="px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-
<div className="flex items-center gap-3 mb-2">
<a href="/esign" className="text-gray-400 hover:text-gray-600 text-lg" hx-boost="false">&larr;</a>
<h1 className="text-xl font-bold text-gray-900"> 계약 생성</h1>
{IS_ADMIN && (<>
<button type="button" onClick={fillTestData} title="영업파트너 계약서 테스트 데이터"
{IS_ADMIN && (
<button type="button" onClick={fillRandomCounterpart} title="랜덤 상대방 정보 채우기"
className="w-8 h-8 flex items-center justify-center rounded-lg text-amber-500 hover:bg-amber-50 hover:text-amber-600 transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
</button>
<button type="button" onClick={fillTestDataNda} title="비밀유지서약서 테스트 데이터"
className="w-8 h-8 flex items-center justify-center rounded-lg text-purple-500 hover:bg-purple-50 hover:text-purple-600 transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
</button>
</>)}
)}
</div>
{/* 스텝 인디케이터 (템플릿 있을 때만) */}