@Service
@RequiredArgsConstructor
@Slf4j
public class KakaoMessageService {
@Value("${kakao.api}")
private String kakaoApiKey;
private final WebClient webClient;
private final String BASE_URL = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
public String sendTextMessage(String accessToken, KakaoMessageTextRequest request) {
/**
* 메시지 보낼 때 필요한 정보들
*
*/
// Map<String, Object> linkData = new HashMap<>();
// linkData.put("web_url", request.getLink().get("web_url"));
// linkData.put("mobile_web_url", request.getLink().get("mobile_web_url"));
//
// Map<String, Object> requestBody = new HashMap<>();
// requestBody.put("object_type", "text"); //text로 고정
// requestBody.put("text", request.getText());
// requestBody.put("link", linkData);
// requestBody.put("button_title", request.getButton_title());
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
/**
* 피드 메시지 보내기.
*/
public String sendFeedMessage(String accessToken, KakaoMessageFeedRequest request) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
/**
* 만약 커스텀을 해야하는 경우에.
*/
public String sendFeedCustomMessage(String accessToken, String con) {
KakaoMessageFeedRequest request = new KakaoMessageFeedRequest();
request.setObject_type("feed"); //feed 메시지라 여기는 고정으로 넣어야함.
KakaoMessageFeedRequest.Content content = new KakaoMessageFeedRequest.Content();
content.setTitle(con);
// 여기서 나머지 정보들을 내가 직접 구인 구직 정보 레포지토리에서 꺼내와서 설정해주면 됨.
KakaoMessageFeedRequest.Button webButton = new KakaoMessageFeedRequest.Button();
webButton.setTitle("웹으로 이동");
KakaoMessageFeedRequest.Link webLink = new KakaoMessageFeedRequest.Link();
webLink.setWeb_url("http://www.daum.net");
webLink.setMobile_web_url("http://m.daum.net");
webButton.setLink(webLink);
KakaoMessageFeedRequest.Button appButton = new KakaoMessageFeedRequest.Button();
appButton.setTitle("앱으로 이동");
KakaoMessageFeedRequest.Link appLink = new KakaoMessageFeedRequest.Link();
appLink.setAndroid_execution_params("contentId=100");
appLink.setIos_execution_params("contentId=100");
appButton.setLink(appLink);
List<KakaoMessageFeedRequest.Button> buttons = new ArrayList<>();
buttons.add(webButton);
buttons.add(appButton);
request.setContent(content);
request.setButtons(buttons);
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
}
위와 같이 메시지 구성 요소를 담은 객체(Object) template_object를 Object 타입으로 요청 보내야 해 !!
public String sendTextMessage(String accessToken, KakaoMessageTextRequest request) {
/**
* 메시지 보낼 때 필요한 정보들
*
*/
// Map<String, Object> linkData = new HashMap<>();
// linkData.put("web_url", request.getLink().get("web_url"));
// linkData.put("mobile_web_url", request.getLink().get("mobile_web_url"));
//
// Map<String, Object> requestBody = new HashMap<>();
// requestBody.put("object_type", "text"); //text로 고정
// requestBody.put("text", request.getText());
// requestBody.put("link", linkData);
// requestBody.put("button_title", request.getButton_title());
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
이 코드는 KakaoTalk 플랫폼에서 텍스트 메시지
를 보내는 메서드이다. 메시지 내용과 다양한 설정을 포함하는 KakaoMessageTextRequest
객체를 사용하여 메시지를 작성하고, 이를 KakaoTalk API
로 보내기 위해 필요한 작업을 수행한다.
🖤 formData
변수를 초기화한다. 이 변수는 KakaoTalk API로 전송할 데이터를 저장하는 데 사용될 것.
🖤 webClient
를 사용하여 KakaoTalk API와 통신을 설정한다.
🖤 HTTP 요청을 빌드하고, 요청 헤더에 KakaoTalk API에 대한 인증 정보를 추가한다. accessToken은 이 API를 호출하기 위한 액세스 토큰 !
🖤 POST 요청을 구성하고, 요청 본문에는 formData의 내용을 추가한다. 요청 본문 형식은 MediaType.APPLICATION_FORM_URLENCODED
로 설정되어 있다.
🖤 retrieve()
메서드를 호출하여 요청을 실행하고, KakaoTalk API로부터의 응답을 가져온다. 응답 내용은 JSON 형식의 문자열 !
🖤 bodyToMono(String.class)를 사용하여 응답 본문을 문자열로 변환하고, block() 메서드를 호출하여 Mono를 동기적으로 처리하여 최종 응답을 문자열로 반환한다.
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
⚽ MultivalueMap
을 사용하여 HTTP POST 요청의 본문을 구성하는데, 해당 자료구조는 키와 값의 여러 쌍을 저장할 수 있는 자료구조이다.
⚽ LinkedMultiValueMap
는 하나의 키에 여러 값을 저장할 수 있다. (다중 값 저장)
⚽ new ObjectMapper().writeValueAsString(request)
: ObjectMapper는 Jackson 라이브러리의 일부이며, Java 객체를 JSON 문자열로 직렬화(변환)하는 데 사용된다. request 객체는 KakaoMessageTextRequest 타입으로, 이 객체의 내용을 JSON 문자열로 변환한다. writeValueAsString()
메서드는 객체를 JSON 문자열로 변환하는 역할을 합니다.
⚽ formData.add("template_object", ...)
: 이제 formData에 template_object
라는 키로 JSON 문자열을 추가된다. 이렇게 하면 요청 본문에 template_object 키 아래에 JSON 문자열이 들어가게 된다.
template_object
라는 키로 KakaoMessageTextRequest 객체의 내용이 JSON 문자열로 저장되어 있다. 이후 HTTP POST 요청에서 이 데이터를 본문으로 전송하여 KakaoTalk API에 메시지를 전송하게 된다.String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
이 코드는 Spring WebClient를 사용하여 HTTP POST 요청을 만들고, 해당 요청을 KakaoTalk API로 보내고 응답을 처리하는 부분.
⚽ webClient.mutate()
: WebClient의 mutate() 메서드를 호출하여 새로운 WebClient.Builder 인스턴스
를 생성한다. WebClient.Builder는 웹 클라이언트를 구성하고 사용자 정의할 때 사용된다.
⚽ .baseUrl(BASE_URL)
: baseUrl(BASE_URL) 메서드를 사용하여 기본 URL을 설정, BASE_URL은 KakaoTalk API의 기본 URL이며, 이 URL에 다른 경로 및 매개변수가 추가될 것.
⚽ .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
: 기본 요청 헤더에 Authorization 헤더를 추가한다. 이 헤더에는 Bearer 토큰을 사용하여 KakaoTalk API에 대한 인증이 포함되어 있다. accessToken은 이전에 전달된 액세스 토큰이다.
⚽ .build()
: 이전 설정을 기반으로하여 WebClient.Builder를 사용하여 실제 WebClient 인스턴스를 생성한다.
⚽ .post()
: HTTP POST 요청을 설정하고 이 메서드를 호출하면 POST 메서드를 사용하는 요청이 설정된다.
⚽ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
: 요청 본문의 콘텐츠 타입을 MediaType.APPLICATION_FORM_URLENCODED로 설정한다. 이것은 HTTP 요청의 본문이 URL-encoded 폼 데이터임을 나타낸다.
⚽ .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
: 요청 헤더에 Authorization 헤더를 추가한다. 이번에도 accessToken을 사용하여 API에 대한 인증을 제공한다. (굳이 추가 안했어도 됐는데..)
⚽ .body(BodyInserters.fromFormData(formData))
: 요청의 본문 데이터를 설정한다. 앞에서 구성한 formData를 사용하여 URL-encoded 폼 데이터를 요청에 추가한다.
🖤 여기서 헷갈리면 안되는게 .contentType(MediaType.APPLICATION_FORM_URLENCODED)
은 요청의 콘텐츠 타입을 설정하고, .body(BodyInserters.fromFormData(formData))
는 해당 콘텐츠 타입에 맞는 데이터를 요청 본문에 추가한다.
⚽ .retrieve()
: 설정된 요청을 실행하고, KakaoTalk API로부터의 응답을 가져오기 위해 retrieve() 메서드를 호출
⚽ .bodyToMono(String.class)
: 응답 본문을 문자열로 변환하기 위해 bodyToMono(String.class)를 호출한다. 이 메서드는 응답 데이터를 Mono 형식으로 가져오고, 이 Mono를 문자열로 변환
⚽ .block()
: Mono를 동기적으로 처리하여 최종 응답을 얻기 위해 block() 메서드를 호출합니다. 이것은 비동기 프로그래밍에서 결과를 기다리는 용도로 사용
--data-urlencoded
를 사영하여 데이터를 요청하기 때문에 데이터를 URL-encoded 형식으로 제공해야 했다. 그러므로.body(BodyInserters.fromFormData(formData))
부분은 필수적.
--data-urlencode
를 사용하는 것은 데이터를 URL-encoded 형식으로 전송하겠다는 의미이며, Java의BodyInserters.fromFormData(formData)
는 이를 수행하는 데 사용됩니다. 이 부분이 없으면 데이터가 제대로 URL-encoded 되지 않고 전송될 수 있으며, API 서버에서 요청을 처리하지 못할 수 있습니다.
요약하면, Kakao API에서 --data-urlencode와 같은 방식으로 데이터를 요청할 때는 요청 본문을 URL-encoded 폼 데이터로 제공해야 하므로, .body(BodyInserters.fromFormData(formData)) 부분은 필수적입니다. 이 부분을 통해 요청 데이터가 올바른 형식으로 API에 전달됩니다.
curl -v -X POST "https://kapi.kakao.com/v2/api/talk/memo/default/send" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data-urlencode 'template_object={
"object_type": "feed",
"content": {
"title": "오늘의 디저트",
"description": "아메리카노, 빵, 케익",
"image_url": "https://mud-kage.kakao.com/dn/NTmhS/btqfEUdFAUf/FjKzkZsnoeE4o19klTOVI1/openlink_640x640s.jpg",
"image_width": 640,
"image_height": 640,
"link": {
"web_url": "http://www.daum.net",
"mobile_web_url": "http://m.daum.net",
"android_execution_params": "contentId=100",
"ios_execution_params": "contentId=100"
}
},
"item_content" : {
"profile_text" :"Kakao",
"profile_image_url" :"https://mud-kage.kakao.com/dn/Q2iNx/btqgeRgV54P/VLdBs9cvyn8BJXB3o7N8UK/kakaolink40_original.png",
"title_image_url" : "https://mud-kage.kakao.com/dn/Q2iNx/btqgeRgV54P/VLdBs9cvyn8BJXB3o7N8UK/kakaolink40_original.png",
"title_image_text" :"Cheese cake",
"title_image_category" : "Cake",
"items" : [
{
"item" :"Cake1",
"item_op" : "1000원"
},
{
"item" :"Cake2",
"item_op" : "2000원"
},
{
"item" :"Cake3",
"item_op" : "3000원"
},
{
"item" :"Cake4",
"item_op" : "4000원"
},
{
"item" :"Cake5",
"item_op" : "5000원"
}
],
"sum" :"Total",
"sum_op" : "15000원"
},
"social": {
"like_count": 100,
"comment_count": 200,
"shared_count": 300,
"view_count": 400,
"subscriber_count": 500
},
"buttons": [
{
"title": "웹으로 이동",
"link": {
"web_url": "http://www.daum.net",
"mobile_web_url": "http://m.daum.net"
}
},
{
"title": "앱으로 이동",
"link": {
"android_execution_params": "contentId=100",
"ios_execution_params": "contentId=100"
}
}
]
}
내용은 많지만 결국 비슷하다.
/**
* 피드 메시지 보내기.
*/
public String sendFeedMessage(String accessToken, KakaoMessageFeedRequest request) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
💨 만약 메시지를 커스텀해서 보내야 할 경우에는
/**
* 만약 커스텀을 해야하는 경우에.
*/
public String sendFeedCustomMessage(String accessToken, String con) {
KakaoMessageFeedRequest request = new KakaoMessageFeedRequest();
request.setObject_type("feed"); //feed 메시지라 여기는 고정으로 넣어야함.
KakaoMessageFeedRequest.Content content = new KakaoMessageFeedRequest.Content();
content.setTitle(con);
// 여기서 나머지 정보들을 내가 직접 구인 구직 정보 레포지토리에서 꺼내와서 설정해주면 됨.
KakaoMessageFeedRequest.Button webButton = new KakaoMessageFeedRequest.Button();
webButton.setTitle("웹으로 이동");
KakaoMessageFeedRequest.Link webLink = new KakaoMessageFeedRequest.Link();
webLink.setWeb_url("http://www.daum.net");
webLink.setMobile_web_url("http://m.daum.net");
webButton.setLink(webLink);
KakaoMessageFeedRequest.Button appButton = new KakaoMessageFeedRequest.Button();
appButton.setTitle("앱으로 이동");
KakaoMessageFeedRequest.Link appLink = new KakaoMessageFeedRequest.Link();
appLink.setAndroid_execution_params("contentId=100");
appLink.setIos_execution_params("contentId=100");
appButton.setLink(appLink);
List<KakaoMessageFeedRequest.Button> buttons = new ArrayList<>();
buttons.add(webButton);
buttons.add(appButton);
request.setContent(content);
request.setButtons(buttons);
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
try {
formData.add("template_object", new ObjectMapper().writeValueAsString(request));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
String res = webClient.mutate()
.baseUrl(BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.build()
.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.bodyToMono(String.class)
.block();
return res;
}
위처럼 따로 세팅을 해주면 된다!
/**
* 카카오톡 피드 메시지 보내기
*/
@PostMapping("/kakao/send_message/feed")
public String returnFeedMessage(@RequestBody KakaoMessageFeedRequest request) {
log.info("access_token is {}", access_token);
String res = kakaoMessageService.sendFeedMessage(access_token, request);
return res;
}
/**
* 노인 구직 정보에서 하트 클릭 시 해당 구인 구직 정보 카카오톡 메시지로 보내기
* 프론트에서 양식 다 맞춰서 보냄.
*/
@PostMapping("/kakao/send_message/want")
public String returnJobSearchMessage(@RequestBody KakaoMessageFeedRequest request, @RequestParam String accessToken) {
String res = kakaoMessageService.sendFeedMessage(accessToken, request);
log.info("res = {}", res);
log.info("accessToken = {}", accessToken);
return res;
}
위처럼 컨트롤러에서 진행하면 됐다.