[Spring] Naver Cloud 메시지, 이메일 연동

NameJM·2024년 3월 4일

Spring Boot 2.7.10, JDK 1.8 기준

아래 서비스 연동하는 방법입니다.

  • Cloud Outbound Mailer
  • Simple & Easy Notification Service

실제 호출은 비동기 형식으로 진행됩니다.

property 추가

app:
  ncloud:
    accessKey: {accessKey}
    secretKey: {secretKey}
    mail:
      url: https://mail.apigw.ntruss.com/api/v1/mails
    alimtalk:
      url: https://sens.apigw.ntruss.com/alimtalk/v2/services/{serviceId}/messages
      plusFriendId: "@{plusFriendId}"
      serviceId: {serviceId}
    sms:
      url: https://sens.apigw.ntruss.com/sms/v2/services/{serviceId}/messages
      from: "{fromPhone}"
      serviceId: {serviceId}

소스코드

VO, DTO 추가

AlimTalkReqMessageCommon.java

@Data
public class AlimTalkReqMessageCommon {
    private String title;
    private String description;
}

AlimTalkReqMessageItem.java

@Data
public class AlimTalkReqMessageItem {
    private List<AlimTalkReqMessageCommon> list;
    private AlimTalkReqMessageCommon summary;
}

AlimTalkReqMessageFailoverConfig.java

@Data
public class AlimTalkReqMessageFailoverConfig {
    private String reserveTime;
    private String reserveTimeZone;
    private String scheduleCode;
}

AlimTalkReqMessageButton.java

@Data
public class AlimTalkReqMessageButton {
    private String type;
    private String name;
    private String linkMobile;
    private String linkPc;
    private String schemeIos;
    private String schemeAndroid;
}

AlimTalkReqMessage.java

@Data
public class AlimTalkReqMessage {
    private String countryCode;                                //수신자 국가번호
    private String to;                                         //수신자번호
    private String title;                                      //알림톡 강조표시 내용
    private String content;                                    //알림톡 메시지 내용
    private String headerContent;                              //알림톡 헤더 내용
    private AlimTalkReqMessageCommon itemHighlight;            //아이템 하이라이트
    private AlimTalkReqMessageItem item;                       //아이템 리스트
    private List<AlimTalkReqMessageButton> buttons;            //알림톡 메시지 버튼
    private boolean useSmsFailover;                            //SMS Failover 사용 여부
    private AlimTalkReqMessageFailoverConfig failoverConfig;   //Failover 설정
}

AlimTalkReqBody.java

@Data
public class AlimTalkReqBody {
    private String plusFriendId;                   //카카오톡 채널명 ((구)플러스친구 아이디)
    private String templateCode;                   //템플릿 코드
    private List<AlimTalkReqMessage> messages;     //메시지 정보
//    private String reserveTime;                    //예약 일시
//    private String reserveTimeZone;                //예약 일시 타임존
//    private String scheduleCode;                   //스케줄 코드
}

RecipientForRequest.java

@Data
public class RecipientForRequest {
    private String address = null;
    private String name = null;
    private String type = "R";
    private Object parameters = null;
}

RecipientGroupFilter.java

public class RecipientGroupFilter {
    private Boolean andFilter;
    private List<String> groups;
}

CreateMailRequest.java

@Data
public class CreateMailRequest {

    @Required
    private String senderAddress; // 발송자 Email 주소

    private String senderName; // 발송자 이름

    private int templateSid; // 템플릿 ID

    @Required
    private String title; // 메일 제목

    @Required
    private String body = ""; // 메일 본문

    private boolean individual = true; // 개인별 발송 혹은 일반 발송 여부

    private boolean confirmAndSend; //확인 후 발송 여부

    private boolean advertising = false; // 광고메일여부

    private Object parameters; // 치환 파라미터

    private String referencesHeader; // 특정 메일을 모아서 보기 위해 네이버 메일에서 지원하는 기능

    // private long reservationUtc; // 예약 발송 일시

    // private String reservationDateTime; // 예약 발송 일시 (reservationUtc 값 우선)

    private List<String> attachFileIds; // 첨부파일 ID 목록

    @Required
    private List<RecipientForRequest> recipients; // 수신자목록

    private RecipientGroupFilter recipientGroupFilter; // 수신자 그룹 조합 발송 조건

    private boolean useBasicUnsubscribeMsg; // 광고 메일일 경우 기본 수신 거부 문구 사용 여부

    @Required
    private String unsubscribeMessage; // 사용자 정의 수신 거부 문구

}

SmsContentType.java

public enum SmsContentType {
    COMM, //일반 메시지
    AD // 광고메시지
}

SmsFiles.java

@Getter
@Setter
public class SmsFiles {
    private String fileId;
}

SmsMessage.java

@Data
public class SmsMessage {
    private String to;
    private String subject;
    private String content;
}

SmsRequestBody.java

@Data
public class SmsRequestBody {
    private String requestId;
    private String requestStartTime;
    private String requestEndTime;
    private String completeStartTime;
    private String completeEndTime;
    private SmsType type;
    private SmsContentType contentType;
    private String countryCode; // 국가코드 (한국 82)
    private String from;
    private String subject;
    private String content;
    private List<SmsMessage> messages;
    private List<SmsFiles> files;
    // private String reserveTime; // 예약발송 예)yyyy-MM-dd HH:mm
    // private String reserveTimeZone;
    // private String scheduleCode;
}

SmsType.java

public enum SmsType {
    SMS, LMS, MMS
}

NCloudService.java

@EnableAsync
@Component
@Slf4j
public class NCloudService {
    @Value("${app.ncloud.mail.url}") String mailUrl;
    @Value("${app.ncloud.sms.url}") String smsUrl;
    @Value("${app.ncloud.alimtalk.url}") String alimtalkUrl;
    @Value("${app.ncloud.alimtalk.plusFriendId}") String alimtalkId;
    @Value("${app.ncloud.alimtalk.serviceId}") String alimtalkServiceId;
    @Value("${app.ncloud.sms.from}") String smsFrom;
    @Value("${app.ncloud.sms.serviceId}") String smsServiceId;
    @Value("${app.ncloud.accessKey}") String accessKey;
    @Value("${app.ncloud.secretKey}") String secretKey;
    
    public JSONObject callApi(String obj, String urlParam) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        return callApi(obj, urlParam, "POST");
    }
    
    
    /**
     * 이메일 전송 요청
     * @param tid nCloud에 등록되어있는 템플릿 ID
     * @param recipientForRequest 수신자목록
     */
    @Async
    public void createMailRequest(int tid, List<RecipientForRequest> recipientForRequest) throws Exception {
        CreateMailRequest createMailRequest = new CreateMailRequest();
        createMailRequest.setTemplateSid(tid);
        createMailRequest.setSenderAddress("test@gmail.com");
        createMailRequest.setRecipients(recipientForRequest);

        ObjectMapper mapper = new ObjectMapper();
        JSONObject response = callApi(mapper.writeValueAsString(createMailRequest), mailUrl);
    }
    
    
    /**
     * 카카오톡 메시지 전송 요청
     * @param templateCode nCloud에 등록되어있는 템플릿 ID
     * @param messages 수신자 및 수신내용 목록
     */
     @Async
    public void sendAlimTalkForKakao(String templateCode, List<AlimTalkReqMessage> messages) throws Exception {
        AlimTalkReqBody body = new AlimTalkReqBody();
        body.setPlusFriendId(alimtalkId);
        if(templateCode != null)
            body.setTemplateCode(templateCode);
        body.setMessages(messages);

        ObjectMapper mapper = new ObjectMapper();
        callApi(mapper.writeValueAsString(body), alimtalkUrl.replaceAll("\\{serviceId}", alimtalkServiceId));
    }
    
    public JSONObject sendSms(SmsType type, List<SmsMessage> messages) throws Exception {
        return sendSms(type, messages, "82");
    }

    /**
     * SMS 전송 요청
     * @param type SMS, MMS, LMS 여부
     * @param messages 수신자 및 수신내용 목록
     * @param countryCode 수신국가
     */
    @Async
    public JSONObject sendSms(SmsType type, List<SmsMessage> messages, String countryCode) throws Exception {
        SmsRequestBody body = new SmsRequestBody();
        body.setType(type);
        body.setContentType(SmsContentType.COMM);
        body.setCountryCode(countryCode);
        body.setFrom(smsFrom);
        body.setSubject("null");
        body.setContent("null");

        body.setMessages(messages);

        ObjectMapper mapper = new ObjectMapper();
        JSONObject response = callApi(mapper.writeValueAsString(body), smsUrl.replaceAll("\\{serviceId}", smsServiceId));

        return response;
    }
    
    
    /**
     * SMS전송 결과 조회
     * @param messageId 메시지 ID
     */
    public JSONObject getSendSmsResultInfo(String messageId) throws Exception {
        String url = new StringBuilder()
                .append(smsUrl.replaceAll("\\{serviceId}", smsServiceId))
                .append("/")
                .append(messageId).toString();

        return callApi(null, url, "GET");
    }
    
    public JSONObject callApi(String obj, String urlParam, String method) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        HttpURLConnection conn = null;
        JSONObject responseJson = null;

        URL url = new URL(urlParam);

        Long timestamp = System.currentTimeMillis();

        conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setRequestProperty("cache-control", "no-cache");
        conn.setRequestProperty("pragma", "no-cache");
        conn.setRequestProperty("x-ncp-apigw-timestamp", Long.toString(timestamp));
        conn.setRequestProperty("x-ncp-iam-access-key", accessKey);
        conn.setRequestProperty("x-ncp-apigw-signature-v2", makeSignature(url, method, timestamp));
        conn.setDoOutput(true);

        if(obj != null) {
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));

            try {
                bw.write(obj);
                bw.flush();
                bw.close();
            } catch (IOException e) {
                log.error("Error : {}", e.getMessage());
            } finally {
                if (bw != null) {
                    bw.close();
                }
            }
        }

        int responseCode = conn.getResponseCode();
        if (100 <= responseCode && responseCode <= 399) {
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            StringBuilder sb = new StringBuilder();
            String line = "";
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            try {
                br.close();
            }catch (IOException e){
                log.error("Error : {}", e.getMessage());
            }finally {
                if(br != null){
                    br.close();
                }
            }
            responseJson = new JSONObject(sb.toString());
        } else{
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
            StringBuilder sb = new StringBuilder();
            String line = "";
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            try {
                br.close();
            }catch (IOException e){
                log.error("Error : {}", e.getMessage());
            }finally {
                if(br != null){
                    br.close();
                }
            }
            responseJson = new JSONObject(sb.toString());
        }
        log.info("responseJson :: " + responseJson);

        return responseJson;
    }
    
    public String makeSignature(URL paramUrl, String requestType, Long timestamp) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        String space = " ";
        String newLine = "\n";
        String method = requestType;
        String url = paramUrl.toString().replaceAll("^((http[s]?|ftp):\\/)?\\/?([^:\\/\\s]+)", "");

        String message = new StringBuilder()
                .append(method)
                .append(space)
                .append(url)
                .append(newLine)
                .append(timestamp)
                .append(newLine)
                .append(accessKey)
                .toString();

        SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);

        byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));
        String encodeBase64String = Base64.getEncoder().encodeToString(rawHmac);

        return encodeBase64String;
    }
}

0개의 댓글