feat: send visitor pass link on approval
This commit is contained in:
79
server/services/sms.ts
Normal file
79
server/services/sms.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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<SmsSendResult> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user