feat: support internal SMS API
This commit is contained in:
@@ -12,8 +12,15 @@ export interface SmsMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
type SmsApiResponse = {
|
||||
requestId?: string;
|
||||
requestTime?: string;
|
||||
statusCode?: string | number;
|
||||
statusName?: string;
|
||||
};
|
||||
|
||||
function normalizePhone(value: string | null | undefined): string {
|
||||
return String(value ?? '').replace(/[^\d+]/g, '');
|
||||
return String(value ?? '').replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function fail(channel: string, recipient: string, error: unknown): SmsSendResult {
|
||||
@@ -26,8 +33,29 @@ function fail(channel: string, recipient: string, error: unknown): SmsSendResult
|
||||
};
|
||||
}
|
||||
|
||||
function smsProvider(): string {
|
||||
return process.env.ACS_SMS_PROVIDER ?? env.smsProvider;
|
||||
}
|
||||
|
||||
function smsApiUrl(): string | undefined {
|
||||
return process.env.ACS_SMS_API_URL ?? env.smsApiUrl;
|
||||
}
|
||||
|
||||
function smsApiKey(): string | undefined {
|
||||
return process.env.ACS_SMS_API_KEY ?? env.smsApiKey;
|
||||
}
|
||||
|
||||
function smsSender(): string | undefined {
|
||||
return process.env.ACS_SMS_SENDER ?? env.smsSender;
|
||||
}
|
||||
|
||||
function smsTimeoutMs(): number {
|
||||
return Number(process.env.ACS_SMS_TIMEOUT_MS ?? env.smsTimeoutMs);
|
||||
}
|
||||
|
||||
export async function sendSms(message: SmsMessage): Promise<SmsSendResult> {
|
||||
const provider = env.smsProvider.toLowerCase();
|
||||
const configuredProvider = smsProvider();
|
||||
const provider = configuredProvider.toLowerCase();
|
||||
const recipient = normalizePhone(message.to);
|
||||
|
||||
if (!recipient) {
|
||||
@@ -39,27 +67,33 @@ export async function sendSms(message: SmsMessage): Promise<SmsSendResult> {
|
||||
return { channel: 'DEV', recipient, status: 'SENT', lastError: null };
|
||||
}
|
||||
|
||||
if (provider !== 'http') {
|
||||
return fail(provider.toUpperCase(), recipient, `지원하지 않는 SMS provider입니다: ${env.smsProvider}`);
|
||||
if (provider === 'hanbank' || provider === 'bok') {
|
||||
return sendHanbankSms(recipient, message.text);
|
||||
}
|
||||
|
||||
if (!env.smsApiUrl) {
|
||||
if (provider !== 'http') {
|
||||
return fail(provider.toUpperCase(), recipient, `지원하지 않는 SMS provider입니다: ${configuredProvider}`);
|
||||
}
|
||||
|
||||
const apiUrl = smsApiUrl();
|
||||
if (!apiUrl) {
|
||||
return fail('SMS', recipient, 'ACS_SMS_API_URL이 설정되지 않았습니다.');
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), env.smsTimeoutMs);
|
||||
const timeout = setTimeout(() => controller.abort(), smsTimeoutMs());
|
||||
|
||||
try {
|
||||
const response = await fetch(env.smsApiUrl, {
|
||||
const apiKey = smsApiKey();
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(env.smsApiKey ? { Authorization: `Bearer ${env.smsApiKey}` } : {}),
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: recipient,
|
||||
from: env.smsSender,
|
||||
from: smsSender(),
|
||||
message: message.text,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
@@ -77,3 +111,60 @@ export async function sendSms(message: SmsMessage): Promise<SmsSendResult> {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHanbankSmsUrl(): string {
|
||||
const baseUrl = smsApiUrl() ?? 'http://210.104.132.59:8000';
|
||||
if (/\/sens\/sms\/?$/i.test(baseUrl)) {
|
||||
return baseUrl;
|
||||
}
|
||||
return `${baseUrl.replace(/\/+$/, '')}/sens/sms`;
|
||||
}
|
||||
|
||||
function isSuccessResponse(response: SmsApiResponse): boolean {
|
||||
const statusCode = String(response.statusCode ?? '').trim();
|
||||
const statusName = String(response.statusName ?? '').trim().toLowerCase();
|
||||
return statusCode === '202' || statusName === 'success';
|
||||
}
|
||||
|
||||
async function sendHanbankSms(recipient: string, text: string): Promise<SmsSendResult> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), smsTimeoutMs());
|
||||
|
||||
try {
|
||||
const response = await fetch(resolveHanbankSmsUrl(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
receive_number: recipient,
|
||||
content: text,
|
||||
msg_type: 'LMS',
|
||||
reserve_time: '',
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const rawBody = await response.text().catch(() => '');
|
||||
let body: SmsApiResponse = {};
|
||||
if (rawBody) {
|
||||
try {
|
||||
body = JSON.parse(rawBody) as SmsApiResponse;
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok || (body.statusCode !== undefined && !isSuccessResponse(body))) {
|
||||
const detail = rawBody ? `: ${rawBody.slice(0, 300)}` : '';
|
||||
throw new Error(`사내 SMS API 발송 실패 HTTP ${response.status}${detail}`);
|
||||
}
|
||||
|
||||
return { channel: 'HANBANK', recipient, status: 'SENT', lastError: null };
|
||||
} catch (error) {
|
||||
return fail('HANBANK', recipient, error);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user