feat: [esign] 알림톡 템플릿 선택 기능 추가

- 바로빌 승인된 알림톡 템플릿 목록 조회 API 추가
- 서명 요청 발송 시 템플릿 선택 드롭다운 UI 추가
- 템플릿 미리보기 (본문 + 버튼) 표시
- send()에 template_name 파라미터 전달 지원
- 미선택 시 기존 하드코딩 폴백 유지
This commit is contained in:
김보곤
2026-02-25 22:42:29 +09:00
parent 8c24b0ae24
commit e9325ff74d
3 changed files with 192 additions and 5 deletions

View File

@@ -34,6 +34,11 @@
const [sending, setSending] = useState(false);
const [sendMethod, setSendMethod] = useState('email');
const [smsFallback, setSmsFallback] = useState(true);
const [templates, setTemplates] = useState([]);
const [selectedTemplate, setSelectedTemplate] = useState('');
const [templateLoading, setTemplateLoading] = useState(false);
const [templateError, setTemplateError] = useState('');
const [templatePreview, setTemplatePreview] = useState(null);
const fetchContract = useCallback(async () => {
try {
@@ -46,6 +51,36 @@
useEffect(() => { fetchContract(); }, [fetchContract]);
const fetchTemplates = useCallback(async () => {
setTemplateLoading(true);
setTemplateError('');
setTemplates([]);
setSelectedTemplate('');
setTemplatePreview(null);
try {
const res = await fetch('/esign/contracts/alimtalk-templates', { headers: getHeaders() });
const json = await res.json();
if (json.success && json.data?.templates?.length > 0) {
setTemplates(json.data.templates);
if (json.data.templates.length === 1) {
setSelectedTemplate(json.data.templates[0].name);
setTemplatePreview(json.data.templates[0]);
}
} else {
setTemplateError(json.message || '승인된 알림톡 템플릿이 없습니다.');
}
} catch (e) {
setTemplateError('템플릿 목록 조회 중 오류가 발생했습니다.');
}
setTemplateLoading(false);
}, []);
useEffect(() => {
if (sendMethod === 'alimtalk' || sendMethod === 'both') {
if (templates.length === 0 && !templateLoading) fetchTemplates();
}
}, [sendMethod]);
const handleSend = async () => {
const methodLabel = SEND_METHODS.find(m => m.value === sendMethod)?.label || sendMethod;
if (!window.confirm(`서명 요청을 발송하시겠습니까?\n발송 방식: ${methodLabel}`)) return;
@@ -53,7 +88,11 @@
try {
const res = await fetch(`/esign/contracts/${CONTRACT_ID}/send`, {
method: 'POST', headers: getHeaders(),
body: JSON.stringify({ send_method: sendMethod, sms_fallback: smsFallback }),
body: JSON.stringify({
send_method: sendMethod,
sms_fallback: smsFallback,
template_name: (sendMethod === 'alimtalk' || sendMethod === 'both') ? selectedTemplate : undefined,
}),
});
const json = await res.json();
if (json.success) {
@@ -138,6 +177,52 @@ className="rounded text-blue-600 focus:ring-blue-500" />
<span className="text-xs text-gray-400">(카카오톡 미사용자에게 SMS로 자동 전환)</span>
</label>
)}
{/* 알림톡 템플릿 선택 */}
{needsAlimtalk && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
<label className="block text-sm font-medium text-gray-700 mb-2">알림톡 템플릿 선택</label>
{templateLoading ? (
<div className="text-sm text-gray-400 py-2">템플릿 목록 조회 ...</div>
) : templateError ? (
<div className="text-sm text-red-500 py-2">
{templateError}
<button onClick={fetchTemplates} className="ml-2 text-blue-500 hover:underline text-xs">다시 시도</button>
</div>
) : (
<>
<select
value={selectedTemplate}
onChange={e => {
setSelectedTemplate(e.target.value);
const tpl = templates.find(t => t.name === e.target.value);
setTemplatePreview(tpl || null);
}}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="">-- 템플릿을 선택하세요 --</option>
{templates.map(t => (
<option key={t.name} value={t.name}>{t.name}</option>
))}
</select>
{templatePreview && (
<div className="mt-3 p-3 bg-white border border-gray-200 rounded-lg">
<p className="text-xs font-medium text-gray-500 mb-2">미리보기</p>
<pre className="text-xs text-gray-700 whitespace-pre-wrap leading-relaxed">{templatePreview.content}</pre>
{templatePreview.buttons?.length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-100">
{templatePreview.buttons.map((btn, i) => (
<span key={i} className="inline-block px-3 py-1 mr-1 text-xs bg-yellow-100 text-yellow-800 rounded">{btn.Name}</span>
))}
</div>
)}
</div>
)}
</>
)}
</div>
)}
</div>
{/* 서명자 연락처 확인 */}
@@ -171,7 +256,7 @@ className="rounded text-blue-600 focus:ring-blue-500" />
{/* 발송 버튼 */}
<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}
<button onClick={handleSend} disabled={sending || fieldsCount === 0 || (needsAlimtalk && !selectedTemplate)}
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>
@@ -180,6 +265,9 @@ className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-
{fieldsCount === 0 && (
<p className="text-red-500 text-sm mt-3 text-right">서명 필드를 먼저 설정해 주세요.</p>
)}
{needsAlimtalk && !selectedTemplate && fieldsCount > 0 && (
<p className="text-amber-600 text-sm mt-3 text-right">알림톡 템플릿을 선택해 주세요.</p>
)}
</div>
);
};