fix:계약 생성 폼 UX 전면 개선

- max-w-3xl 제거하여 전체 너비 사용
- flex 기반 반응형 레이아웃 (PC: 한 행, 모바일: 자동 줄바꿈)
- 이름(1):이메일(2.5):전화번호(1) flex-grow 비율 적용
- 작성자/상대방을 하나의 카드에 통합 (구분선으로 분리)
- SignerRow 컴포넌트 분리로 중복 제거
- 서명자 컬러 표시 (파란색: 작성자, 빨간색: 상대방)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-12 18:33:26 +09:00
parent ebee3f195c
commit 203d52300a

View File

@@ -15,17 +15,34 @@
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
// Input 컴포넌트를 App 바깥에 정의 (한글 IME 조합 깨짐 방지)
const Input = ({ label, name, value, error, onChange, type = 'text', required = false, placeholder = '' }) => (
<div>
const Input = ({ label, name, value, error, onChange, type = 'text', required = false, placeholder = '', style }) => (
<div style={style}>
<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={value} onChange={e => onChange(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" />
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors" />
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
</div>
);
const SignerRow = ({ prefix, title, subtitle, color, form, errors, onChange }) => (
<div>
<div className="flex items-center gap-2 mb-3">
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: color }}></span>
<h3 className="text-sm font-semibold text-gray-800">{title}</h3>
<span className="text-xs text-gray-400">{subtitle}</span>
</div>
<div className="flex gap-3" style={{ flexWrap: 'wrap' }}>
<Input label="이름" name={`${prefix}_name`} value={form[`${prefix}_name`]} error={errors[`${prefix}_name`]}
onChange={onChange} required placeholder="홍길동" style={{ flex: '1 1 140px', minWidth: 120 }} />
<Input label="이메일" name={`${prefix}_email`} value={form[`${prefix}_email`]} error={errors[`${prefix}_email`]}
onChange={onChange} type="email" required placeholder="hong@example.com" style={{ flex: '2.5 1 200px', minWidth: 180 }} />
<Input label="전화번호" name={`${prefix}_phone`} value={form[`${prefix}_phone`]} error={errors[`${prefix}_phone`]}
onChange={onChange} placeholder="010-1234-5678" style={{ flex: '1 1 140px', minWidth: 120 }} />
</div>
</div>
);
const App = () => {
const [form, setForm] = useState({
title: '', description: '', sign_order_type: 'counterpart_first', expires_at: '',
@@ -77,83 +94,65 @@ className="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-bl
};
return (
<div className="p-6 max-w-3xl mx-auto">
<div className="p-4 sm:p-6">
<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>
<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>
</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>}
{errors.general && <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4 text-sm">{errors.general}</div>}
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
{/* 계약 정보 */}
<div className="bg-white rounded-lg border p-6">
<h2 className="text-lg font-semibold mb-4">계약 정보</h2>
<div className="bg-white rounded-lg border p-5">
<h2 className="text-base font-semibold text-gray-900 mb-4">계약 정보</h2>
<div className="space-y-4">
<Input label="계약 제목" name="title" value={form.title} error={errors.title} onChange={handleChange} 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" />
placeholder="계약에 대한 간단한 설명 (선택)" rows={2}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors" />
</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" />
className="w-full border border-gray-300 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:cursor-pointer" />
{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>
<div className="flex gap-4" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 200px' }}>
<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">
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
<option value="counterpart_first">상대방 먼저 서명</option>
<option value="creator_first">작성자 먼저 서명</option>
</select>
</div>
<Input label="만료일" name="expires_at" value={form.expires_at} error={errors.expires_at} onChange={handleChange} type="datetime-local" />
<div style={{ flex: '1 1 200px' }}>
<Input label="만료일" name="expires_at" value={form.expires_at} error={errors.expires_at} onChange={handleChange} type="datetime-local" />
</div>
</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-8 gap-4">
<div className="col-span-2">
<Input label="이름" name="creator_name" value={form.creator_name} error={errors.creator_name} onChange={handleChange} required placeholder="홍길동" />
</div>
<div className="col-span-4">
<Input label="이메일" name="creator_email" value={form.creator_email} error={errors.creator_email} onChange={handleChange} type="email" required placeholder="hong@example.com" />
</div>
<div className="col-span-2">
<Input label="전화번호" name="creator_phone" value={form.creator_phone} error={errors.creator_phone} onChange={handleChange} placeholder="010-1234-5678" />
</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-8 gap-4">
<div className="col-span-2">
<Input label="이름" name="counterpart_name" value={form.counterpart_name} error={errors.counterpart_name} onChange={handleChange} required placeholder="김철수" />
</div>
<div className="col-span-4">
<Input label="이메일" name="counterpart_email" value={form.counterpart_email} error={errors.counterpart_email} onChange={handleChange} type="email" required placeholder="kim@example.com" />
</div>
<div className="col-span-2">
<Input label="전화번호" name="counterpart_phone" value={form.counterpart_phone} error={errors.counterpart_phone} onChange={handleChange} placeholder="010-5678-1234" />
</div>
{/* 서명자 정보 */}
<div className="bg-white rounded-lg border p-5">
<h2 className="text-base font-semibold text-gray-900 mb-4">서명자 정보</h2>
<div className="space-y-5">
<SignerRow prefix="creator" title="작성자 (갑)" subtitle="" color="#3B82F6"
form={form} errors={errors} onChange={handleChange} />
<div className="border-t"></div>
<SignerRow prefix="counterpart" title="상대방 (을)" subtitle="서명 요청 대상" color="#EF4444"
form={form} errors={errors} onChange={handleChange} />
</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>
<div className="flex justify-end gap-3 pt-1">
<a href="/esign" className="px-5 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm transition-colors" 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">
className="px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium disabled:opacity-50 transition-colors">
{submitting ? '생성 중...' : '계약 생성 및 서명 위치 설정'}
</button>
</div>