feat: [esign] 계약 수정 화면에 서명 필드 값 편집 기능 추가
- 수정 화면에서 text/date/checkbox 필드 값 인라인 편집 - 필드별 서명자 구분(작성자/상대방), 페이지 번호 표시 - update API에서 필드 값 일괄 업데이트 처리
This commit is contained in:
@@ -595,6 +595,9 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'signers.*.phone' => 'nullable|string|max:20',
|
||||
'signers.*.role' => 'required|in:creator,counterpart',
|
||||
'file' => 'nullable|file|mimes:pdf,doc,docx|max:20480',
|
||||
'fields' => 'nullable|array',
|
||||
'fields.*.id' => 'required|integer',
|
||||
'fields.*.field_value' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$userId = auth()->id();
|
||||
@@ -645,6 +648,16 @@ public function update(Request $request, int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
// 필드 값 업데이트
|
||||
if ($request->has('fields')) {
|
||||
foreach ($request->input('fields') as $fieldData) {
|
||||
EsignSignField::withoutGlobalScopes()
|
||||
->where('id', $fieldData['id'])
|
||||
->where('contract_id', $contract->id)
|
||||
->update(['field_value' => $fieldData['field_value'] ?? null]);
|
||||
}
|
||||
}
|
||||
|
||||
// 감사 로그
|
||||
EsignAuditLog::create([
|
||||
'tenant_id' => $tenantId,
|
||||
@@ -659,7 +672,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '계약이 수정되었습니다.',
|
||||
'data' => $contract->load('signers'),
|
||||
'data' => $contract->load(['signers', 'signFields']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -460,6 +460,8 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
|
||||
const [tenantModalOpen, setTenantModalOpen] = useState(false);
|
||||
const [editLoading, setEditLoading] = useState(IS_EDIT);
|
||||
const [existingFileName, setExistingFileName] = useState('');
|
||||
const [editFields, setEditFields] = useState([]);
|
||||
const [editSigners, setEditSigners] = useState([]);
|
||||
const fileRef = useRef(null);
|
||||
|
||||
const hasTemplates = templates.length > 0;
|
||||
@@ -511,6 +513,9 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
|
||||
const preset = TITLE_PRESETS.find(p => p.value === c.title);
|
||||
setTitleType(preset ? c.title : '__custom__');
|
||||
if (c.original_file_name) setExistingFileName(c.original_file_name);
|
||||
// 필드 및 서명자 정보 로드
|
||||
if (c.sign_fields?.length) setEditFields(c.sign_fields);
|
||||
if (c.signers?.length) setEditSigners(c.signers);
|
||||
}
|
||||
})
|
||||
.catch(() => { alert('계약 정보를 불러오지 못했습니다.'); })
|
||||
@@ -787,6 +792,14 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
|
||||
fd.append('signers[1][phone]', form.counterpart_phone || '');
|
||||
fd.append('signers[1][role]', 'counterpart');
|
||||
|
||||
// 수정 모드: 필드 값 전송
|
||||
if (IS_EDIT && editFields.length > 0) {
|
||||
editFields.forEach((f, i) => {
|
||||
fd.append(`fields[${i}][id]`, f.id);
|
||||
fd.append(`fields[${i}][field_value]`, f.field_value || '');
|
||||
});
|
||||
}
|
||||
|
||||
let url, method;
|
||||
if (IS_EDIT) {
|
||||
url = `/esign/contracts/${EDIT_CONTRACT_ID}`;
|
||||
@@ -920,6 +933,77 @@ className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm focus:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수정 모드: 서명 필드 값 편집 */}
|
||||
{IS_EDIT && editFields.length > 0 && (
|
||||
<div className="bg-white rounded-lg border p-4">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-3">서명 필드 값</h2>
|
||||
<p className="text-xs text-gray-500 mb-3">각 필드의 값을 수정할 수 있습니다. 서명/도장 필드는 서명 시 입력됩니다.</p>
|
||||
<div className="space-y-2">
|
||||
{editFields.map((field, idx) => {
|
||||
const signerInfo = editSigners.find(s => s.id === field.signer_id);
|
||||
const signerLabel = signerInfo ? (signerInfo.role === 'creator' ? '작성자' : '상대방') : '';
|
||||
const signerColor = signerInfo?.role === 'creator' ? '#3B82F6' : '#EF4444';
|
||||
const isEditable = ['text', 'date'].includes(field.field_type);
|
||||
|
||||
return (
|
||||
<div key={field.id} className="flex items-center gap-3 p-2 rounded-lg bg-gray-50">
|
||||
<span className="shrink-0 w-5 h-5 rounded flex items-center justify-center text-white text-[10px] font-bold"
|
||||
style={{ backgroundColor: signerColor }}>
|
||||
{FIELD_TYPE_INFO[field.field_type]?.icon || '?'}
|
||||
</span>
|
||||
<div className="shrink-0" style={{ width: 90 }}>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full text-white font-medium" style={{ backgroundColor: signerColor }}>
|
||||
{signerLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="shrink-0 text-xs text-gray-600" style={{ width: 100 }}>
|
||||
{field.field_label || FIELD_TYPE_INFO[field.field_type]?.label || field.field_type}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
{isEditable ? (
|
||||
<input
|
||||
type={field.field_type === 'date' ? 'date' : 'text'}
|
||||
value={field.field_value || ''}
|
||||
onChange={e => {
|
||||
const updated = [...editFields];
|
||||
updated[idx] = { ...updated[idx], field_value: e.target.value };
|
||||
setEditFields(updated);
|
||||
}}
|
||||
placeholder={field.field_variable ? `{{${field.field_variable}}}` : '값 입력'}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
) : field.field_type === 'checkbox' ? (
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.field_value === '1' || field.field_value === 'true'}
|
||||
onChange={e => {
|
||||
const updated = [...editFields];
|
||||
updated[idx] = { ...updated[idx], field_value: e.target.checked ? '1' : '0' };
|
||||
setEditFields(updated);
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">{field.field_label || '체크'}</span>
|
||||
</label>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 italic">서명 시 입력</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="shrink-0 text-[10px] text-gray-400">P{field.page_number}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{IS_EDIT && editFields.length === 0 && (
|
||||
<div className="bg-gray-50 rounded-lg border border-gray-200 p-4">
|
||||
<p className="text-xs text-gray-500">설정된 서명 필드가 없습니다. 수정 완료 후 서명 위치 설정에서 필드를 추가하세요.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 네비게이션 */}
|
||||
<div className="flex justify-between">
|
||||
<a href={IS_EDIT ? `/esign/${EDIT_CONTRACT_ID}` : '/esign'} className="px-4 py-1.5 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 text-sm transition-colors" hx-boost="false">취소</a>
|
||||
|
||||
Reference in New Issue
Block a user