Skip to content

Commit 80a363a

Browse files
Merge pull request #67 from Palbahngmiyine/master
Update next.js examples
2 parents 5104364 + fff8514 commit 80a363a

14 files changed

Lines changed: 633 additions & 160 deletions

File tree

examples/nextjs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ bun dev
4040
```
4141
## 주의사항
4242

43-
반드시 .env 파일에 등록 및 활성화하신 API Key, API Secret Key를 설정하셔야 합니다!
43+
* 반드시 .env 파일에 등록 및 활성화하신 API Key, API Secret Key를 설정하셔야 합니다!
44+
* 본 예제는 Node.js 18.18 버전 이상을 호환하는 [Next.js 15.2.1](https://nextjs.org/docs/app/getting-started/installation) 버전으로 만들어졌습니다. 반드시 Node.js 18.18 이상의 버전으로 진행해주세요!

examples/nextjs/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
},
1111
"dependencies": {
1212
"@next/env": "^15.2.1",
13-
"next": "15.2.1",
13+
"jotai": "^2.12.1",
14+
"motion": "^12.4.10",
15+
"next": "^15.2.1",
1416
"react": "^19.0.0",
1517
"react-dom": "^19.0.0",
16-
"solapi": "latest"
18+
"solapi": "^5",
19+
"zod": "^3.24.2"
1720
},
1821
"devDependencies": {
1922
"@tailwindcss/postcss": "^4",

examples/nextjs/src/app/actions.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,91 @@
11
'use server';
22

33
import '../../envConfig';
4-
import {BadRequestError, SolapiMessageService} from 'solapi';
4+
import {SolapiMessageService} from 'solapi';
55
import {redirect} from 'next/navigation';
6+
import {
7+
alimtalkFormSchema,
8+
AlimtalkFormType,
9+
ApiKeysType,
10+
messageFormSchema,
11+
MessageFormType,
12+
} from '@/types/types';
13+
import {extractVariablesFields, formDataToObject} from '@/lib/formData';
14+
15+
// 주의!! 실제 발송 연동시에는 오로지 서버에서 api key를 관리해서 MessageService를 호출 하도록 해주세요.
16+
// 본인의 Api Key가 유출되어 발생하는 피해는 오로지 개인에게 있습니다!
17+
export async function getApiKeys(): Promise<Array<string | undefined>> {
18+
return [process.env.SOLAPI_API_KEY, process.env.SOLAPI_API_SECRET];
19+
}
620

721
export async function sendMessage(formData: FormData) {
8-
if (!formData.has('from') || !formData.has('to') || !formData.has('text')) {
9-
throw new BadRequestError(
10-
'반드시 발신번호, 수신번호, 메시지 내용을 입력해주세요!',
11-
);
12-
}
13-
const from = formData.get('from') as string;
14-
const to = formData.get('to') as string;
15-
const text = formData.get('text') as string;
16-
17-
const messageService = new SolapiMessageService(
18-
process.env.SOLAPI_API_KEY!,
19-
process.env.SOLAPI_API_SECRET!,
20-
);
22+
const rawFormData = {
23+
apiKey: formData.get('apiKey'),
24+
apiSecret: formData.get('apiSecret'),
25+
from: formData.get('from'),
26+
to: formData.get('to'),
27+
text: formData.get('text'),
28+
};
29+
30+
const {from, to, text, apiKey, apiSecret}: MessageFormType =
31+
messageFormSchema.parse(rawFormData);
32+
33+
// 주의!! 실제 발송 연동시에는 오로지 서버에서 api key를 관리해서 MessageService를 호출 하도록 해주세요.
34+
// 본인의 Api Key가 유출되어 발생하는 피해는 오로지 개인에게 있습니다!
35+
const messageService = new SolapiMessageService(apiKey, apiSecret);
36+
await messageService.send({from, to, text}).then(console.log);
37+
38+
redirect('/?success=true');
39+
}
40+
41+
export async function getKakaoChannels({apiKey, apiSecret}: ApiKeysType) {
42+
const messageService = new SolapiMessageService(apiKey, apiSecret);
43+
return await messageService
44+
.getKakaoChannels()
45+
.then(res => JSON.parse(JSON.stringify(res.channelList)));
46+
}
47+
48+
export async function getKakaoAlimtalkTemplates(
49+
{apiKey, apiSecret}: ApiKeysType,
50+
channelId: string,
51+
) {
52+
const messageService = new SolapiMessageService(apiKey, apiSecret);
53+
return await messageService
54+
.getKakaoAlimtalkTemplates({
55+
channelId,
56+
status: 'APPROVED',
57+
})
58+
.then(res => JSON.parse(JSON.stringify(res.templateList)));
59+
}
60+
61+
export async function getKakaoAlimtalkTemplate(
62+
{apiKey, apiSecret}: ApiKeysType,
63+
templateId: string,
64+
) {
65+
const messageService = new SolapiMessageService(apiKey, apiSecret);
66+
return await messageService
67+
.getKakaoAlimtalkTemplate(templateId)
68+
.then(res => JSON.parse(JSON.stringify(res)));
69+
}
70+
71+
export async function sendAlimtalk(formData: FormData) {
72+
const data: AlimtalkFormType = formDataToObject(formData);
73+
alimtalkFormSchema.parse(data);
74+
75+
const variables = extractVariablesFields(data);
2176

77+
const messageService = new SolapiMessageService(data.apiKey, data.apiSecret);
2278
await messageService
2379
.send({
24-
from,
25-
to,
26-
text,
80+
from: data.from,
81+
to: data.to,
82+
kakaoOptions: {
83+
pfId: data.channelId,
84+
templateId: data.templateId,
85+
variables: {
86+
...variables,
87+
},
88+
},
2789
})
2890
.then(console.log);
2991

examples/nextjs/src/app/layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {Metadata} from 'next';
22
import localFont from 'next/font/local';
33
import './globals.css';
4+
import type React from 'react';
45

56
const pretendard = localFont({
67
src: '../fonts/PretendardVariable.woff2',
@@ -20,9 +21,7 @@ export default function RootLayout({
2021
}>) {
2122
return (
2223
<html lang="ko">
23-
<body className={`${pretendard.className}`}>
24-
{children}
25-
</body>
24+
<body className={`${pretendard.className}`}>{children}</body>
2625
</html>
2726
);
2827
}

examples/nextjs/src/app/page.tsx

Lines changed: 30 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,45 @@
11
'use client';
22

3-
import Form from 'next/form';
4-
import {sendMessage} from '@/app/actions';
53
import {useSearchParams} from 'next/navigation';
6-
import {useRouter} from 'next/navigation';
4+
import SMSForm from '@/components/SMSForm';
5+
import Toast from '@/components/Toast';
6+
import Tabs from '@/components/Tabs';
7+
import {useAtom, useStore, Provider} from 'jotai';
8+
import {apiKeyAtom, apiSecretAtom, tabAtom} from '@/atoms/CommonAtom';
9+
import AlimtalkForm from '@/components/AlimtalkForm';
10+
import {useEffect} from 'react';
11+
import {getApiKeys} from '@/app/actions';
712

813
export default function Home() {
9-
const router = useRouter();
1014
const searchParams = useSearchParams();
1115
const isSuccess = !!searchParams.get('success');
16+
const [activeTab] = useAtom(tabAtom);
17+
const store = useStore();
18+
const [, setApiKey] = useAtom(apiKeyAtom);
19+
const [, setApiSecret] = useAtom(apiSecretAtom);
1220

13-
const handleCloseEvent = async () => {
14-
router.replace('/');
15-
};
21+
useEffect(() => {
22+
getApiKeys().then(keys => {
23+
setApiKey(keys[0] ?? '');
24+
setApiSecret(keys[1] ?? '');
25+
});
26+
}, []);
1627

1728
return (
18-
<>
19-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
29+
<Provider store={store}>
30+
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
31+
<Tabs />
2032
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
21-
<Form
22-
action={sendMessage}
23-
formMethod="POST"
24-
className="flex flex-col gap-6 row-start-2 items-center sm:items-start">
25-
<div className="flex justify-center flex-auto w-full">
26-
<h1 className="text-4xl font-black text-center">
27-
Next.js + SOLAPI 발송 예제
28-
</h1>
29-
</div>
30-
<p className="flex flex-auto justify-center w-full">
31-
발신번호, 수신번호, 텍스트만 입력해서 문자 발송 테스트를
32-
진행해보실 수 있습니다!
33-
</p>
34-
<div className="w-full">
35-
<label
36-
className="block text-gray-700 text-sm mb-2"
37-
htmlFor="from">
38-
발신번호 /{' '}
39-
<b>
40-
반드시 사용할 API Key 계정 내 등록된 발신번호를 입력해주세요!
41-
예) 15771603
42-
</b>
43-
</label>
44-
<input
45-
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
46-
id="from"
47-
type="text"
48-
name="from"
49-
placeholder="발신번호"
50-
required={true}
51-
/>
52-
</div>
53-
<div className="w-full">
54-
<label className="block text-gray-700 text-sm mb-2" htmlFor="to">
55-
수신번호 /{' '}
56-
<b>
57-
실제 발송 테스트 시, 반드시 올바른 수신번호를 입력해주세요!
58-
예) 01012345678
59-
</b>
60-
</label>
61-
<input
62-
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
63-
id="to"
64-
type="text"
65-
name="to"
66-
placeholder="수신번호"
67-
required={true}
68-
/>
69-
</div>
70-
<div className="w-full">
71-
<label className="block text-gray-700 text-sm mb-2" htmlFor="to">
72-
발송내용 /{' '}
73-
<b>
74-
한글 45자, 영자 90자 이하 입력되면 자동으로 SMS 타입의
75-
메시지가 발송됩니다, 그 이상을 입력하면 LMS 타입의 메시지가
76-
발송됩니다!
77-
</b>
78-
</label>
79-
<textarea
80-
className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
81-
id="text"
82-
name="text"
83-
draggable={false}
84-
rows={8}
85-
inputMode="text"
86-
placeholder="발송할 문자내용 입력"
87-
required={true}
88-
/>
89-
</div>
90-
<div className="flex flex-auto w-full gap-4 items-center justify-center flex-col sm:flex-row">
91-
<button
92-
type="submit"
93-
className="w-40 rounded-full border cursor-pointer border-solid border-transparent transition-colors flex items-center justify-center bg-[#4541FF] hover:bg-[#0035ef] text-background gap-2 dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5">
94-
발송하기
95-
</button>
96-
</div>
97-
</Form>
33+
<div className="flex justify-center flex-auto w-full">
34+
<h1 className="text-4xl font-black text-center">
35+
Next.js + SOLAPI {activeTab === 'sms' ? '문자' : '알림톡'} 발송
36+
예제
37+
</h1>
38+
</div>
39+
{activeTab === 'sms' ? <SMSForm /> : <AlimtalkForm />}
9840
</main>
41+
<Toast isSuccess={isSuccess} />
9942
</div>
100-
<div
101-
id="toast-default"
102-
className={`flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-sm dark:text-gray-400 dark:bg-gray-800 fixed top-5 inset-x-0 max-w mx-auto transition-opacity duration-500 ${isSuccess ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
103-
role="alert">
104-
<div className="inline-flex items-center justify-center shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
105-
<svg
106-
className="w-5 h-5"
107-
aria-hidden="true"
108-
xmlns="http://www.w3.org/2000/svg"
109-
fill="currentColor"
110-
viewBox="0 0 20 20">
111-
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
112-
</svg>
113-
<span className="sr-only">Check icon</span>
114-
</div>
115-
<div className="ms-3 text-sm font-normal">
116-
발송 완료되었습니다!
117-
<br />
118-
<a
119-
href="https://console.solapi.com/message-log"
120-
className="text-sky-500"
121-
target="_blank"
122-
rel="noreferrer noopener">
123-
콘솔
124-
</a>
125-
에서 확인해보세요!
126-
</div>
127-
<button
128-
type="button"
129-
className="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700 cursor-pointer"
130-
data-dismiss-target="#toast-default"
131-
aria-label="Close"
132-
onClick={handleCloseEvent}>
133-
<span className="sr-only">Close</span>
134-
<svg
135-
className="w-3 h-3"
136-
aria-hidden="true"
137-
xmlns="http://www.w3.org/2000/svg"
138-
fill="none"
139-
viewBox="0 0 14 14">
140-
<path
141-
stroke="currentColor"
142-
strokeLinecap="round"
143-
strokeLinejoin="round"
144-
strokeWidth={2}
145-
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
146-
/>
147-
</svg>
148-
</button>
149-
</div>
150-
</>
43+
</Provider>
15144
);
15245
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {atom} from 'jotai';
2+
3+
export const tabAtom = atom<string>('sms');
4+
5+
export const apiKeyAtom = atom<string>('');
6+
7+
export const apiSecretAtom = atom<string>('');

0 commit comments

Comments
 (0)