import { env } from '../config/env.js'; export interface SmsSendResult { channel: string; recipient: string; status: 'SENT' | 'FAILED'; lastError: string | null; } export interface SmsMessage { to: string; text: string; } function normalizePhone(value: string | null | undefined): string { return String(value ?? '').replace(/[^\d+]/g, ''); } function fail(channel: string, recipient: string, error: unknown): SmsSendResult { const message = error instanceof Error ? error.message : String(error); return { channel, recipient, status: 'FAILED', lastError: message.slice(0, 500), }; } export async function sendSms(message: SmsMessage): Promise { const provider = env.smsProvider.toLowerCase(); const recipient = normalizePhone(message.to); if (!recipient) { return fail(provider.toUpperCase(), recipient, '수신자 휴대폰 번호가 없습니다.'); } if (provider === 'dev') { console.log('[sms:dev]', { to: recipient, text: message.text }); return { channel: 'DEV', recipient, status: 'SENT', lastError: null }; } if (provider !== 'http') { return fail(provider.toUpperCase(), recipient, `지원하지 않는 SMS provider입니다: ${env.smsProvider}`); } if (!env.smsApiUrl) { return fail('SMS', recipient, 'ACS_SMS_API_URL이 설정되지 않았습니다.'); } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), env.smsTimeoutMs); try { const response = await fetch(env.smsApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(env.smsApiKey ? { Authorization: `Bearer ${env.smsApiKey}` } : {}), }, body: JSON.stringify({ to: recipient, from: env.smsSender, message: message.text, }), signal: controller.signal, }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`SMS API HTTP ${response.status}${text ? `: ${text.slice(0, 300)}` : ''}`); } return { channel: 'SMS', recipient, status: 'SENT', lastError: null }; } catch (error) { return fail('SMS', recipient, error); } finally { clearTimeout(timeout); } }