Skip to content

Commit 59947e6

Browse files
카카오 브랜드 메시지 및 음성 메시지 발송 예제를 추가하고, 관련 스키마를 개선하여 메시지 전송 기능을 확장함. Next.js 예제의 패키지 의존성을 업데이트하여 최신 버전으로 유지함.
1 parent b3b90d6 commit 59947e6

6 files changed

Lines changed: 102 additions & 21 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* 카카오 브랜드 메시지 발송 예제
3+
* 현재 targeting 타입 중 M, N의 경우는 카카오 측에서 인허가된 채널만 사용하실 수 있습니다.
4+
* 그 외의 모든 채널은 I 타입만 사용 가능합니다.
5+
*/
6+
const {SolapiMessageService} = require('solapi');
7+
const messageService = new SolapiMessageService(
8+
'ENTER_YOUR_API_KEY',
9+
'ENTER_YOUR_API_SECRET',
10+
);
11+
12+
// 단일 발송 예제
13+
messageService
14+
.send({
15+
to: '수신번호',
16+
from: '계정에서 등록한 발신번호 입력',
17+
kakaoOptions: {
18+
pfId: '연동한 비즈니스 채널의 pfId',
19+
templateId: '등록한 브랜드 메시지 템플릿의 ID',
20+
variables: {},
21+
// 템플릿 내 치환문구(변수)가 있는 경우 추가, 반드시 key, value 모두 string으로 기입해야 합니다.
22+
/*
23+
variables: {
24+
"변수명": "임의의 값"
25+
}
26+
*/
27+
// 현재 BMS(브랜드 메시지)는 문자로의 대체발송이 비활성화 됩니다.
28+
bms: {
29+
targeting: 'I',
30+
},
31+
},
32+
})
33+
.then(res => console.log(res));
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* 음성 메시지 발송 예제
3+
* 음성 메시지에 대한 자세한 내용은 아래 링크를 참고해주세요.
4+
* @see https://developers.solapi.com/references/voice
5+
* 문서상 DTMF는 유선 전화등에서 들을 수 있는 다이얼 신호들을 뜻합니다.
6+
* @see https://en.wikipedia.org/wiki/DTMF_signaling
7+
*/
8+
const {SolapiMessageService} = require('solapi');
9+
const messageService = new SolapiMessageService(
10+
'ENTER_YOUR_API_KEY',
11+
'ENTER_YOUR_API_SECRET',
12+
);
13+
14+
messageService.send({
15+
to: '수신번호',
16+
from: '발신번호',
17+
text: '음성 메시지 테스트입니다, 실제 수신자에게 들리는 내용입니다.',
18+
voiceOptions: {
19+
voiceType: 'FEMALE', // 필수값, MALE 혹은 FEMALE만 선택 가능합니다
20+
// headerMessage: '보이스 메시지 테스트', // 메시지 시작에 나오는 머릿말, 최대 135자까지 가능합니다.
21+
// tailMessage: '보이스 메시지 테스트', // 통화가 끝나고 나오는 꼬릿말, 상담원 연결 시 나오지 않습니다, 머리말(headerMessage)이 있어야 적용됩니다.
22+
// replyRange: 1, // 수신자가 누를 수 있는 다이얼 값의 범위, 1~9까지 가능하며 counselorNumber와 함께 사용할 수 없습니다.
23+
// counselorNumber: '상담번호, 예) 029302266', // 수신자가 0번을 눌렀을 때 연결되는 상담번호, replyRange와 함께 사용할 수 없습니다.
24+
},
25+
});

examples/nextjs/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"@next/env": "^15.2.1",
13-
"jotai": "^2.12.1",
14-
"motion": "^12.4.10",
15-
"next": "^15.2.1",
16-
"react": "^19.0.0",
17-
"react-dom": "^19.0.0",
18-
"solapi": "^5",
19-
"zod": "^3.24.2"
12+
"@next/env": "^15.3.5",
13+
"jotai": "^2.12.5",
14+
"motion": "^12.23.3",
15+
"next": "^15.3.5",
16+
"react": "^19.1.0",
17+
"react-dom": "^19.1.0",
18+
"solapi": "latest",
19+
"zod": "^4.0.5"
2020
},
2121
"devDependencies": {
2222
"@tailwindcss/postcss": "^4",
23-
"@types/node": "^20",
23+
"@types/node": "^24",
2424
"@types/react": "^19",
2525
"@types/react-dom": "^19",
2626
"tailwindcss": "^4",

src/lib/effectErrorHandler.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,43 @@ export const runSafePromise = <E, A>(
124124
);
125125
};
126126

127+
// MessageNotReceivedError의 프로퍼티를 포함한 확장 Error 타입
128+
interface MessageNotReceivedErrorCompat extends Error {
129+
readonly failedMessageList: ReadonlyArray<
130+
import('../models/responses/sendManyDetailResponse').FailedMessage
131+
>;
132+
readonly totalCount: number;
133+
}
134+
127135
// Effect 에러를 기존 Error로 변환 (하위 호환성)
128136
export const toCompatibleError = (effectError: unknown): Error => {
137+
// MessageNotReceivedError의 경우 특별 처리하여 원본 프로퍼티 보존
138+
if (effectError instanceof EffectError.MessageNotReceivedError) {
139+
const error = new Error(
140+
effectError.message,
141+
) as MessageNotReceivedErrorCompat;
142+
error.name = 'MessageNotReceivedError';
143+
// failedMessageList와 totalCount 프로퍼티 보존
144+
Object.defineProperty(error, 'failedMessageList', {
145+
value: effectError.failedMessageList,
146+
writable: false,
147+
enumerable: true,
148+
configurable: false,
149+
});
150+
Object.defineProperty(error, 'totalCount', {
151+
value: effectError.totalCount,
152+
writable: false,
153+
enumerable: true,
154+
configurable: false,
155+
});
156+
delete error.stack;
157+
return error;
158+
}
159+
129160
const formatted = formatError(effectError);
130161
// 하위 호환성을 위해 여전히 Error 사용하지만 스택 제거
131162
const error = new Error(formatted);
132-
error.name = 'ApplicationError';
163+
error.name = 'FromSolapiError';
133164
delete error.stack;
134165
return error;
135166
};

src/models/base/kakao/kakaoOption.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,7 @@ export class VariableValidationError extends Data.TaggedError(
1616
}
1717

1818
const kakaoOptionBmsSchema = Schema.Struct({
19-
targeting: Schema.optional(
20-
Schema.Enums({
21-
M: 'M',
22-
N: 'N',
23-
I: 'I',
24-
}),
25-
),
19+
targeting: Schema.Literal('I', 'M', 'N'),
2620
});
2721

2822
// Constants for variable validation

src/models/requests/voice/voiceOption.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import {Schema} from 'effect';
22

33
export const voiceOptionSchema = Schema.Struct({
4-
voiceType: Schema.Literal('FEMALE', 'MALE').pipe(
5-
Schema.optionalWith({default: () => 'FEMALE' as const}),
6-
),
4+
voiceType: Schema.Literal('FEMALE', 'MALE'),
75
headerMessage: Schema.optional(Schema.String),
86
tailMessage: Schema.optional(Schema.String),
9-
replyRate: Schema.optional(Schema.Literal(1, 2, 3)),
7+
replyRange: Schema.optional(Schema.Literal(1, 2, 3, 4, 5, 6, 7, 8, 9)),
108
counselorNumber: Schema.optional(Schema.String),
119
});
1210

0 commit comments

Comments
 (0)