Merge branch 'api_advc'

This commit is contained in:
hehihoho3@gmail.com 2025-10-27 11:14:13 +09:00
commit 6700d30ba6
98 changed files with 4313 additions and 1112 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
HELP.md
target/
log/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
@ -198,3 +199,4 @@ fabric.properties
rebel.xml
/mvnw
/mvnw.cmd
/log.config.path_IS_UNDEFINED/

View File

@ -38,6 +38,11 @@
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>

View File

@ -4,10 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.idgen.service.IdgenService;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.access.mapper.domain.AccessKeyVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.SendSucRestResponse;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.SendSucRestResponse;
import com.itn.mjonApi.mjon.log.service.mapper.LettnAccessLogMapper;
import com.itn.mjonApi.mjon.log.service.mapper.LettnApiSendMsgLogMapper;
import com.itn.mjonApi.mjon.log.service.mapper.MjMsgGroupDataMapper;
@ -59,6 +56,7 @@ public class LogAspect {
@Autowired
MjMsgGroupDataMapper mjMsgGroupDataMapper;
@Resource(name = "apiAccessLog")
private IdgenService idgenApiAccessLogId;
@ -72,16 +70,28 @@ public class LogAspect {
* - mberId
* - accessKey
*/
@Before(value = "execution(* com.itn.mjonApi.mjon.api.*..*Impl.*(..))" )
// @Before(value = "execution(* com.itn.mjonApi.mjon.api.*..*Impl.*(..))" )
@Before("execution(* com.itn.mjonApi.mjon.api..*Controller.*(..)) || execution(* com.itn.mjonApi.mjon.api..*Impl.*(..))")
public void before(JoinPoint joinPoint) throws IllegalAccessException, JsonProcessingException {
log.info(" :: AOP before :: ");
//
// for (Object arg : joinPoint.getArgs()) {
// if (arg == null) continue;
//
// // JSON 직렬화해서 VO 전체 출력
// log.info("VO 전체: {}", new ObjectMapper().writeValueAsString(arg));
// log.info("VO 클래스명: {}", arg.getClass().getName());
//
//
// }
HttpServletRequest request = this.getHttpServletRequest();
// VO 객체를 가져옴
Object objectVO = joinPoint.getArgs()[0];
log.info("joinPoint.getArgs()[0] :: [{}]", joinPoint.getArgs()[0]);
// log.info("joinPoint.getArgs()[0] :: [{}]", joinPoint.getArgs()[0]);
@ -91,16 +101,27 @@ public class LogAspect {
String mberId = "";
String accessKey = "";
for(Field field : objectVO.getClass().getDeclaredFields()){;
field.setAccessible(true);
log.info("field.getName() :: [{}]", field.getName());
log.info("field.get(objectVO) :: [{}]", field.get(objectVO));
try {
// JDK 내부 클래스 필드는 무시
if (field.getDeclaringClass().getName().startsWith("java.")) continue;
field.setAccessible(true);
if ("mberId".equals(field.getName())) {
mberId = String.valueOf(field.get(objectVO));
log.info("mberId :: [{}]", mberId);
}
// log.info("field.get(objectVO) :: [{}]", field.get(objectVO));
if("mberId".equals(field.getName())){ mberId=field.get(objectVO).toString(); }
else if("accessKey".equals(field.getName())){ accessKey=field.get(objectVO).toString(); }
if("mberId".equals(field.getName())){ mberId=field.get(objectVO).toString(); }
else if("accessKey".equals(field.getName())){ accessKey=field.get(objectVO).toString(); }
if(StringUtils.isNotEmpty(mberId) && StringUtils.isNotEmpty(accessKey)){ break; }
if(StringUtils.isNotEmpty(mberId) && StringUtils.isNotEmpty(accessKey)){ break; }
} catch (Exception e) {
e.printStackTrace();
log.warn("접근 불가 필드 스킵: {}", field.getName());
continue;
}
}
String nextStringId = idgenApiAccessLogId.getNextStringId();
@ -162,7 +183,9 @@ public class LogAspect {
lettnApiSendMsgLogMapper.insert(apiSendMsgLogVO);
// 메세지 그룹 테이블에 발송 구분 업데이트
this.updateMsgGroupTbSendKind(apiSendMsgLogVO);
// mjon_git에서 처리하는걸로 수정
// 20250729 이호영
// this.updateMsgGroupTbSendKind(apiSendMsgLogVO);
}
@ -172,18 +195,18 @@ public class LogAspect {
* @description 메세지 그룹 테이블에 발송 구분 업데이트
* @param apiSendMsgLogVO
*/
private void updateMsgGroupTbSendKind(LettnApiSendMsgLogVO apiSendMsgLogVO) {
if(StringUtils.isNotEmpty(apiSendMsgLogVO.getMsgGroupId()))
{
String[] msgGroupIds = null;
if(apiSendMsgLogVO.getMsgGroupId().indexOf(",") > -1){
msgGroupIds = apiSendMsgLogVO.getMsgGroupId().split(",");
}else{
msgGroupIds = new String[]{apiSendMsgLogVO.getMsgGroupId()};
}
mjMsgGroupDataMapper.update(msgGroupIds);
}
}
// private void updateMsgGroupTbSendKind(LettnApiSendMsgLogVO apiSendMsgLogVO) {
// if(StringUtils.isNotEmpty(apiSendMsgLogVO.getMsgGroupId()))
// {
// String[] msgGroupIds = null;
// if(apiSendMsgLogVO.getMsgGroupId().indexOf(",") > -1){
// msgGroupIds = apiSendMsgLogVO.getMsgGroupId().split(",");
// }else{
// msgGroupIds = new String[]{apiSendMsgLogVO.getMsgGroupId()};
// }
// mjMsgGroupDataMapper.update(msgGroupIds);
// }
// }
/**
* @description lettngnrlmber_api_send_msg_log 테이블에 저장할 데이터 만들기
@ -252,6 +275,7 @@ public class LogAspect {
@NotNull
private static String getClassNmFromObject(Object returnValue) {
String classNmTemp = returnValue.getClass().getName();
log.info("getClassNmFromObject :: [{}]", classNmTemp);
String classNm = classNmTemp.substring(classNmTemp.lastIndexOf(".")+1, classNmTemp.length());
return classNm;
}
@ -312,37 +336,29 @@ public class LogAspect {
* @return
*/
public String getJsonToString(Object returnValue) throws JsonProcessingException {
if(ObjectUtils.isEmpty(returnValue)){
if (ObjectUtils.isEmpty(returnValue)) {
return null;
}
String classNm = this.getClassNmFromObject(returnValue);
/**
* @description : return Class가 추가되면 여기에 추가
*/
if("AccessKeyVO".equals(classNm)) {
AccessKeyVO accessKeyVO = (AccessKeyVO) returnValue;
return ApiObjectUtil.getAccessKeyVOToJsonString(accessKeyVO);
// 처리 대상 클래스 목록 (선택적으로 필터링하고 싶다면 여기에 조건)
switch (classNm) {
case "AccessKeyVO":
case "RestResponse":
case "MsgsRequestVO":
case "MsgRequestVO":
case "MjKakaoProfileInfoVO":
case "BizTemplateRequest":
case "MsgAtRequestVO":
case "MsgFtRequestVO":
case "String":
case "ArrayList":
return ApiObjectUtil.toJson(returnValue);
default:
log.info("데이터를 추가해 주세요 [{}]", classNm);
return "데이터를 추가해 주세요";
}
else if("RestResponse".equals(classNm)){
RestResponse restResponse = (RestResponse) returnValue;
return ApiObjectUtil.getRestResponseToJsonString(restResponse);
}
else if("MsgsRequestVO".equals(classNm)){
MsgsRequestVO msgsRequestVO = (MsgsRequestVO) returnValue;
return ApiObjectUtil.getMsgsRequestVOToJsonString(msgsRequestVO);
}
else if("MsgRequestVO".equals(classNm)){
MsgRequestVO restResponse = (MsgRequestVO) returnValue;
return ApiObjectUtil.getMsgRequestVOToJsonString(restResponse);
}
else{
return "데이터를 추가해 주세요";
}
}
}

View File

@ -1,10 +1,22 @@
package com.itn.mjonApi.cmn.apiServer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MjonResponseVO;
import org.springframework.http.ResponseEntity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itn.mjonApi.cmn.domain.StatusResponse;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.cmn.domain.biz.template.detail.TemplateDetailResponse;
import com.itn.mjonApi.cmn.domain.biz.template.list.TemplateListResponse;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MjonResponseVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
/**
* packageName : com.itn.mjonApi.cmn.apiServer
@ -18,6 +30,7 @@ import org.springframework.web.client.RestTemplate;
* -----------------------------------------------------------
* 2023-05-15 hylee 최초 생성
*/
@Slf4j
@Service
public class ApiService <T> {
@ -37,13 +50,120 @@ public class ApiService <T> {
* @throws JsonProcessingException
*/
public MjonResponseVO postForEntity(String url, Object request, Class<String> responseType) throws JsonProcessingException {
ResponseEntity<String> spamChkEntity = (ResponseEntity<String>) restTemplate.postForEntity(
ResponseEntity<String> returnEntity = (ResponseEntity<String>) restTemplate.postForEntity(
url
, request
, responseType
);
MjonResponseVO spamResponse = MjonResponseVO.getMjonResponse(spamChkEntity);
return spamResponse;
log.info("returnEntity :: [{}]", returnEntity.toString());
// ObjectMapper로 JSON 파싱
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(returnEntity.getBody());
// log.info("rootNode :: [{}]", rootNode.toString());
// "apiReturn" 필드만 추출
JsonNode apiReturnNode = rootNode.get("apiReturn");
if (apiReturnNode == null || apiReturnNode.isNull()) {
log.warn("apiReturn 필드가 응답에 존재하지 않습니다.");
return null;
}
return MjonResponseVO.getMjonResponse(apiReturnNode);
}
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 알림톡 템플릿 리스트 조회
* @author hylee
*/
public TemplateListResponse postForBizTemplateListEntity(String url, BizTemplateRequest requestDto) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<BizTemplateRequest> requestEntity = new HttpEntity<>(requestDto, headers);
TemplateListResponse returnEntity = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
TemplateListResponse.class
).getBody();
return returnEntity;
}
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 알림톡 템플릿 상세 조회
* @author hylee
*/
public TemplateDetailResponse postForBizTemplateDetailEntity(String url, BizTemplateRequest requestDto) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<BizTemplateRequest> requestEntity = new HttpEntity<>(requestDto, headers);
TemplateDetailResponse returnEntity = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
TemplateDetailResponse.class
).getBody();
log.info("returnEntity :: [{}]", returnEntity.toString());
return returnEntity;
}
public StatusResponse postMultipartForEntity(String url, MsgFtRequestVO msgFtRequestVO, MultipartFile templateImage) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 파일 추가
body.add("templateImage", templateImage.getResource());
// MsgFtRequestVO 필드들을 kakaoVO에 맞게 매핑
if (msgFtRequestVO.getSendKind() != null) {
body.add("sendKind", msgFtRequestVO.getSendKind());
}
if ( msgFtRequestVO.getTemplateMsgType() != null) {
body.add("imageType", msgFtRequestVO.getTemplateMsgType());
}
if (msgFtRequestVO.getTemplateCode() != null) {
body.add("templateCode", msgFtRequestVO.getTemplateCode());
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<StatusResponse> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
StatusResponse.class
);
// log.info("Upload response :: [{}]", response.getBody());
return response.getBody();
}
}

View File

@ -0,0 +1,19 @@
package com.itn.mjonApi.cmn.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class CmnVO {
private String mberId; // 사용자 ID
private String accessKey; // Api Key
}

View File

@ -1,104 +1,15 @@
package com.itn.mjonApi.mjon.api.send.mapper.domain;
package com.itn.mjonApi.cmn.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* packageName : com.itn.mjonApi.mjon.api.send.mapper.domain
* fileName : MjonMsgVO
* author : hylee
* date : 2023-05-23
* description : 문자 발송에 필요한 값들을 받는 vo
* 1건~500건 대량 문자 개인별로 발송
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-05-09 hylee 최초 생성
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MsgsRequestVO implements Serializable {
/**
* 값이 있는 경우
* 문자온 프로젝트에서 처리해줌
* null이면 에러
*/
private static final long serialVersionUID = 1L;
private String mberId; // 사용자 ID
private String accessKey; // Api Key
private String smsTxt; // SMS용 메시지본문
private String[] callToList; // 수신번호리스트
private String callFrom; // 발신번호 :: 정책이 필요함
private String eachPrice = "0"; // 전송문자 개별가격
private String sPrice = "0"; // 임시
private String totPrice = "0"; // 전송문자 토탈가격
private String fileCnt = "0"; // 첨부파일 갯수
private String msgType = "4"; // 메시지의 (4: SMS 전송, 5: URL 전송, 6: MMS전송, 7: BARCODE전송, 8: 카카오 알림톡 전송)
// ==== 단가 ====
private float smsPrice = 0; // sms 단가 null 이면 에러
private float mmsPrice = 0; // mms 단가 null 이면 에러
// private float kakaoAtPrice; // 카카오 알림톡 단가
// private float kakaoFtPrice; // 카카오 친구톡 단가
// private float kakaoFtImgPrice;// 카카오 이미지 단가
// private float kakaoFtWideImgPrice; // 카카오 와이드 이미지 단가
private String[] imgFilePath = new String[0]; // 그림 이미지 경로
private String spamStatus; // 스팸문자 유무 (Y/N) - 서비스단에서 처리
private String txtReplYn = "N"; // 변환문자 유무 (Y/N) - 서비스단에서 처리
// private String nameStr; // value = "치환 이름 리스트 |로 구분", example = "홍길동1|홍길동2|홍길동3"
// private String rep1Str; // value = "치환 문자1 리스트 |로 구분", example = ""
// private String rep2Str; // value = "치환 문자2 리스트 |로 구분", example = ""
// private String rep3Str; // value = "치환 문자3 리스트 |로 구분", example = ""
// private String rep4Str; // value = "치환 문자4 리스트 |로 구분", example = ""
// private String[] nameList= new String[0]; // value = "nameStr 을 |로 split 후 담는 변수", example = ""
// private String[] rep1List= new String[0]; // value = "rep1Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep2List= new String[0]; // value = "rep2Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep3List= new String[0]; // value = "rep3Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep4List= new String[0]; // value = "rep4Str 을 |로 split 후 담는 변수", example = ""
private String reserveYn = "N"; // value = "예약 유무 (Y/N)", example = "N"
// 치환 있을 경우 사용
// private String shortMsgCnt; // value = "치환 후 단문 건수", example = ""
// private String longMsgCnt; // value = "치환 후 장문 건수", example = ""
// @ApiModelProperty(value = "문자 종류 일반:N, 광고:A, 선거:C", example = "N", hidden = true)
private String msgKind = "N"; // '문자 종류 일반:N, 광고:A, 선거:C',
private String test_yn; // 테스트 여부
public class SendRequestCmnVO {
// 수신자 발송txt 각각 _1~_100
// 교차로 있어야 로직이 가능함
@ -1105,6 +1016,4 @@ public class MsgsRequestVO implements Serializable {
// private String smsTxt_498;
// private String smsTxt_499;
// private String smsTxt_500;
}

View File

@ -0,0 +1,41 @@
package com.itn.mjonApi.cmn.domain;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.http.HttpStatus;
/*
* 1XX : 조건부 응답
* 2XX : 성공
* 3XX : 리다이렉션 완료
* 4XX : 요청 오류
* 500 : 서버 오류
*
* 참고 : https://km0830.tistory.com/33
*
* ====== 자주 사용하는 코드 =====
* 200 : Ok : 서버가 클라이언트의 요청을 성공적으로 처리, 페이지에서는 페이지 요청이 정상적으로 완료 (Ok)
* 400 : Bad Request : 잘못 요청 (Bad Request)
* 401 : Unauthorized : 권한 없음, 예를 들면, 로그인 페이지가 필요한 페이지를 로그인 없이 접속하려는 경우 반환되는 코드 (인증 실패) (Unauthorized)
*
* */
@Getter
@Setter
@NoArgsConstructor
@ToString
public class StatusResponse {
private HttpStatus status;
private String message;
private Object object;
private Object apiReturn;
private String messageTemp;
// private String timestamp;
}

View File

@ -0,0 +1,20 @@
package com.itn.mjonApi.cmn.domain.biz.template;
import com.itn.mjonApi.cmn.domain.CmnVO;
import lombok.*;
import lombok.experimental.SuperBuilder;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class BizTemplateRequest extends CmnVO {
private String bizId;
private String apiKey;
private String senderKey;
private String templateCode;
private String test_yn;
}

View File

@ -0,0 +1,16 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CommentsAttachment {
private String originalFileName;
private String filePath;
}

View File

@ -0,0 +1,23 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateButton {
private String bizFormId;
private String linkAnd;
private String linkIos;
private String linkMo;
private String linkPc;
private String linkType;
private String name;
private String ordering;
private String pluginId;
}

View File

@ -0,0 +1,20 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
import java.util.List;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateComments {
private String content;
private String createdAt;
private String status;
private List<CommentsAttachment> attachment;
}

View File

@ -0,0 +1,41 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
import java.util.List;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateDetail {
private String block;
private String categoryCode;
private String createdAt;
private String dormant;
private String inspectionStatus;
private String modifiedAt;
private String securityFlag;
private String senderKey;
private String senderKeyType;
private String status;
private String templateCode;
private String templateContent;
private String templateEmphasizeType;
private String templateExtra;
private String templateHeader;
private String templateImageName;
private String templateImageUrl;
private String templateMessageType;
private String templateName;
private String templateSubtitle;
private String templateTitle;
private TemplateItemHighlight templateItemHighlight;
private TemplateItem templateItem;
private List<TemplateButton> buttons;
private List<TemplateComments> comments;
private List<TemplateQuickReplies> quickReplies;
}

View File

@ -0,0 +1,16 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateDetailResponse {
private String code;
private String message;
private TemplateDetail data;
}

View File

@ -0,0 +1,16 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
import java.util.List;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateItem {
private List<TemplateItemItem> list;
private TemplateItemSummary summary;
}

View File

@ -0,0 +1,16 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateItemHighlight {
private String title; // 아이템 타이틀
private String description; // 상세 설명
private String imageUrl; // 썸네일 이미지 주소
}

View File

@ -0,0 +1,14 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateItemItem {
private String title;
private String description;
}

View File

@ -0,0 +1,15 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateItemSummary {
private String title;
private String description;
}

View File

@ -0,0 +1,19 @@
package com.itn.mjonApi.cmn.domain.biz.template.detail;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TemplateQuickReplies {
private String name;
private String linkType;
private String linkAnd;
private String linkIos;
private String linkMo;
private String linkPc;
}

View File

@ -0,0 +1,46 @@
package com.itn.mjonApi.cmn.domain.biz.template.list;
/**
* 템플릿 상태를 나타내는 열거형 클래스.
* - API 응답의 상태 코드(EX: REG) 사람이 읽기 쉽게 변환하는 사용됨.
*/
public enum ServiceStatusEnum {
REG("등록완료"), // REG: 등록 완료된 템플릿
RDY("대기"), // RDY: 대기 상태
ACT("활성"), // ACT: 활성화 상태
DMT("중단"), // DMT: 중단된 템플릿
REJ("반려"), // REJ: 반려된 템플릿
UNKNOWN("알 수 없음"); // 정의되지 않은 상태
private final String label; // 한글 설명
/**
* 생성자
* @param label 상태의 한글 설명
*/
ServiceStatusEnum(String label) {
this.label = label;
}
/**
* 한글 설명 반환
*/
public String getLabel() {
return this.label;
}
/**
* 코드값을 받아 "[코드](한글설명)" 형태 문자열로 반환
* @param code 상태 코드 (: REG, ACT )
* @return : "REG(등록완료)", "DMT(중단)", "FOO(알 수 없음)"
*/
public static String getLabelByCode(String code) {
for (ServiceStatusEnum status : values()) {
if (status.name().equalsIgnoreCase(code)) {
return status.name() + "(" + status.getLabel() + ")";
}
}
return code + "(알 수 없음)";
}
}

View File

@ -0,0 +1,20 @@
package com.itn.mjonApi.cmn.domain.biz.template.list;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class TemplateInfo {
private String senderKey;
private String senderKeyType;
private String templateCode;
private String templateName;
private String createdAt;
private String modifiedAt;
private String categoryCode;
private String serviceStatus;
}

View File

@ -0,0 +1,15 @@
package com.itn.mjonApi.cmn.domain.biz.template.list;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class TemplateListData {
private List<TemplateInfo> list;
private boolean hasNext;
}

View File

@ -0,0 +1,17 @@
package com.itn.mjonApi.cmn.domain.biz.template.list;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class TemplateListResponse {
private String code;
private String message;
private int totalCount;
private int totalPage;
private int currentPage;
private TemplateListData data;
}

View File

@ -58,7 +58,6 @@ public class IdgenServiceImpl implements IdgenService {
// nextId 만들기
String nextId = prefixTemp + idgenVO.getNextId();
log.info(" userId : [{}]", nextId);
return nextId;
}

View File

@ -53,6 +53,33 @@ public class CertifInterceptor implements HandlerInterceptor{
String serverIp = ""; //접속 server IP
try{
/*
String contentType = request.getContentType();
log.info("요청 contentType: {}", contentType);
//
if("application/json".equals(contentType) )
{
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String body = sb.toString();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(body);
String mberId = jsonNode.get("mberId").asText();
String accessKey = jsonNode.get("accessKey").asText();
log.info("mberId :: [{}]", mberId);
log.info("accessKey :: [{}]", accessKey);
}
*/
String clientIp = null;
boolean isIpInHeader = false;
@ -150,8 +177,9 @@ public class CertifInterceptor implements HandlerInterceptor{
//referer 값이 없으면 serverIP 값으로 대체한다.
if ("".equals(referer) || referer==null) {
referer = serverIp;
}
log.info("certi request.getParameter(\"accessKey\") :: [{}]", request.getParameter("accessKey"));
}
referer= "119.193.215.98";
// log.info("certi request.getParameter(\"accessKey\") :: [{}]", request.getParameter("accessKey"));
// hylee Builder 패턴으로 변경 => 20230516
AccessKeyVO accessKeyVO = accessKeyService.selectRKey(
new AccessKeyVO().builder()

View File

@ -1,7 +1,8 @@
package com.itn.mjonApi.cmn.model;
import com.itn.mjonApi.mjon.api.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.PriceVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@ -19,6 +20,7 @@ import java.util.Map;
* 2023-07-03 hylee 최초 생성
*/
@Slf4j
@Component
public class Price {
@ -29,48 +31,88 @@ public class Price {
double mberMoney = priceMapper.selectMberMoney(mberId);
//시스템 단가 변수
double sys_shortPrice = 0.0f;
double sys_longPrice = 0.0f;
double sys_picturePrice = 0.0f;
double sys_shortPrice = 0.0f;
double sys_longPrice = 0.0f;
double sys_picturePrice = 0.0f;
double sys_kakaoAtPrice = 0.0f;
double sys_kakaoFtPrice = 0.0f;
double sys_kakaoFtImgPrice = 0.0f;
double sys_kakaoFtWideImgPrice = 0.0f;
//최종 단가 변수
double shortPrice = 0.0f;
double longPrice = 0.0f;
double picturePrice = 0.0f;
double shortPrice = 0.0f;
double longPrice = 0.0f;
double picturePrice = 0.0f;
double kakaoAtPrice = 0.0f;
double kakaoFtPrice = 0.0f;
double kakaoFtImgPrice = 0.0f;
double kakaoFtWideImgPrice = 0.0f;
//1.시스템 기본 단가, 사용자 개인단가 정보 불러오기
Map<String, String> priceMap = priceMapper.selectMberPriceInfo(mberId);
//1-2.단가 계산을 위한 set
sys_shortPrice = Double.parseDouble(String.valueOf(priceMap.get("sysShortPrice")));
sys_longPrice = Double.parseDouble(String.valueOf(priceMap.get("sysLongPrice")));
sys_picturePrice = Double.parseDouble(String.valueOf(priceMap.get("sysPicturePrice")));
sys_shortPrice = Double.parseDouble(String.valueOf(priceMap.get("sysShortPrice")));
sys_longPrice = Double.parseDouble(String.valueOf(priceMap.get("sysLongPrice")));
sys_picturePrice = Double.parseDouble(String.valueOf(priceMap.get("sysPicturePrice")));
sys_kakaoAtPrice = Double.parseDouble(String.valueOf(priceMap.get("sysKakaoAtPrice")));
sys_kakaoFtPrice = Double.parseDouble(String.valueOf(priceMap.get("sysKakaoFtPrice")));
sys_kakaoFtImgPrice = Double.parseDouble(String.valueOf(priceMap.get("sysKakaoFtImgPrice")));
sys_kakaoFtWideImgPrice = Double.parseDouble(String.valueOf(priceMap.get("sysKakaoFtWideImgPrice")));
shortPrice = Double.parseDouble(String.valueOf(priceMap.get("shortPrice")));
longPrice = Double.parseDouble(String.valueOf(priceMap.get("longPrice")));
picturePrice = Double.parseDouble(String.valueOf(priceMap.get("picturePrice")));
kakaoAtPrice = Double.parseDouble(String.valueOf(priceMap.get("kakaoAtPrice")));
kakaoFtPrice = Double.parseDouble(String.valueOf(priceMap.get("kakaoFtPrice")));
kakaoFtImgPrice = Double.parseDouble(String.valueOf(priceMap.get("kakaoFtImgPrice")));
kakaoFtWideImgPrice = Double.parseDouble(String.valueOf(priceMap.get("kakaoFtWideImgPrice")));
//1-3. 최종 단가 계산
shortPrice = shortPrice == 0.0f ? sys_shortPrice : shortPrice;
longPrice = longPrice == 0.0f ? sys_longPrice : longPrice;
picturePrice = picturePrice == 0.0f ? sys_picturePrice : picturePrice;
kakaoAtPrice = kakaoAtPrice == 0.0f ? sys_kakaoAtPrice : kakaoAtPrice;
kakaoFtPrice = kakaoFtPrice == 0.0f ? sys_kakaoFtPrice : kakaoFtPrice;
kakaoFtImgPrice = kakaoFtImgPrice == 0.0f ? sys_kakaoFtImgPrice : kakaoFtImgPrice;
kakaoFtWideImgPrice = kakaoFtWideImgPrice == 0.0f ? sys_kakaoFtWideImgPrice : kakaoFtWideImgPrice;
//2. 단가별 발송 가능건수 계산을위한 변수 set
int shortSendPsbltEa = 0;
int longSendPsbltEa = 0;
int pictureSendPsbltEa = 0;
int kakaoAtSendPsbltEa = 0;
int kakaoFtSendPsbltEa = 0;
int kakaoFtImgSendPsbltEa = 0;
int kakaoFtWideImgSendPsbltEa = 0;
//2-1. 소수점 연산을 위한 BigDecimal Casting
BigDecimal mberMoney_big = new BigDecimal(String.valueOf(mberMoney));
BigDecimal shortPrice_big = new BigDecimal(String.valueOf(priceMap.get("sysShortPrice")));
BigDecimal longPrice_big = new BigDecimal(String.valueOf(priceMap.get("sysLongPrice")));
BigDecimal picturePrice_big = new BigDecimal(String.valueOf(priceMap.get("sysPicturePrice")));
BigDecimal shortPrice_big = new BigDecimal(String.valueOf(shortPrice));
BigDecimal longPrice_big = new BigDecimal(String.valueOf(longPrice));
BigDecimal picturePrice_big = new BigDecimal(String.valueOf(picturePrice));
BigDecimal kakaoAtPrice_big = new BigDecimal(String.valueOf(kakaoAtPrice));
BigDecimal kakaoFtPrice_big = new BigDecimal(String.valueOf(kakaoFtPrice));
BigDecimal kakaoFtImgPrice_big = new BigDecimal(String.valueOf(kakaoFtImgPrice));
BigDecimal kakaoFtWideImgPrice_big = new BigDecimal(String.valueOf(kakaoFtWideImgPrice));
//2-2. mberMoney가 0일경우 제외
if(mberMoney_big.compareTo(BigDecimal.ZERO) != 0) {
shortSendPsbltEa = mberMoney_big.divide(shortPrice_big, BigDecimal.ROUND_DOWN).intValue();
longSendPsbltEa = mberMoney_big.divide(longPrice_big, BigDecimal.ROUND_DOWN).intValue();
pictureSendPsbltEa = mberMoney_big.divide(picturePrice_big, BigDecimal.ROUND_DOWN).intValue();
shortSendPsbltEa = mberMoney_big.divide(shortPrice_big, BigDecimal.ROUND_DOWN).intValue();
longSendPsbltEa = mberMoney_big.divide(longPrice_big, BigDecimal.ROUND_DOWN).intValue();
pictureSendPsbltEa = mberMoney_big.divide(picturePrice_big, BigDecimal.ROUND_DOWN).intValue();
kakaoAtSendPsbltEa = mberMoney_big.divide(kakaoAtPrice_big, BigDecimal.ROUND_DOWN).intValue();
kakaoFtSendPsbltEa = mberMoney_big.divide(kakaoFtPrice_big, BigDecimal.ROUND_DOWN).intValue();
kakaoFtImgSendPsbltEa = mberMoney_big.divide(kakaoFtImgPrice_big, BigDecimal.ROUND_DOWN).intValue();
kakaoFtWideImgSendPsbltEa = mberMoney_big.divide(kakaoFtWideImgPrice_big, BigDecimal.ROUND_DOWN).intValue();
}
//result set
@ -78,11 +120,28 @@ public class Price {
.shortPrice(shortPrice)
.longPrice(longPrice)
.picturePrice(picturePrice)
.kakaoAtPrice(kakaoAtPrice)
.kakaoFtPrice(kakaoFtPrice)
.kakaoFtImgPrice(kakaoFtImgPrice)
.kakaoFtWideImgPrice(kakaoFtWideImgPrice)
.shortSendPsbltEa(shortSendPsbltEa)
.longSendPsbltEa(longSendPsbltEa)
.pictureSendPsbltEa(pictureSendPsbltEa)
.kakaoAtSendPsbltEa(kakaoAtSendPsbltEa)
.kakaoFtSendPsbltEa(kakaoFtSendPsbltEa)
.kakaoFtImgSendPsbltEa(kakaoFtImgSendPsbltEa)
.kakaoFtWideImgSendPsbltEa(kakaoFtWideImgSendPsbltEa)
.mberMoney(mberMoney)
.build();
log.info(" + priceVO :: [{}]", priceVO);
return priceVO;
}

View File

@ -1,9 +1,6 @@
package com.itn.mjonApi.cmn.msg;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import java.time.LocalDateTime;
@ -11,6 +8,7 @@ import java.time.LocalDateTime;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class RestResponse{
private String resultCode = "0";

View File

@ -33,7 +33,8 @@ public enum StatMsg {
// 문자보내기 ======================================================================
STAT_0("0","")
, STAT_1010("1010","발신자 전화번호 사용 불가")
, STAT_1020("1020","수신자 전화번호 오류")
, STAT_1020("1020","수신자 전화번호 오류") //
, STAT_1021("1021","수신거부 번호 제거 후 수신자 목록 없음")
, STAT_1030("1030","문자 내용 발송 불가")
, STAT_1040("1040","치환 데이터 오류")
, STAT_1050("1050","치환 후 문자 길이 초과")
@ -43,6 +44,28 @@ public enum StatMsg {
, STAT_1090("1090","요청 발송일시에 발송 불가") // 아직 사용 안함
, STAT_1099("1099","기타 시스템 오류")
//카톡발송======================================================================
, STAT_2010("2010","발신프로필 KEY 오류")
, STAT_2030("2030","템플릿 코드 오류")
, STAT_2040("2040","본문 데이터 오류")
, STAT_2041("2041","타이틀 데이터 오류")
, STAT_2042("2042","대체문자 데이터 오류")
, STAT_2043("2043","대체문자 발송 시 발신번호가 필요합니다.")
, STAT_2050("2050","지원하지 않는 이미지 형식입니다.")
, STAT_2051("2051","이미지 용량은 5MB 이내여야 합니다.")
, STAT_2052("2052","이미지를 읽을 수 없습니다.")
, STAT_2053("2053","지원하지 않는 이미지 형식입니다.")
, STAT_2054("2054","이미지 가로폭인 500px 미만입니다.")
, STAT_2055("2055","유효한 이미지 파일이 없습니다.")
, STAT_2056("2056","비율 2:1 이상 또는 3:4 이하만 허용됩니다.")
, STAT_2057("2057","대체문자(MMS) 이미지는 10MB를 초과할 수 없습니다.")
, STAT_2080("2080","친구톡은 20시 50분부터 익일 08시까지 발송이 제한됩니다.")
, STAT_2099("2099","기타 시스템 오류")
//전체발송======================================================================
, STAT_3099("3099","기타 시스템 오류")
@ -52,9 +75,12 @@ public enum StatMsg {
//발송가능건수======================================================================
, STAT_5099("5099","기타 시스템 오류")
//======================================================================
, msgType4("단문","SMS")
, msgType6("장문","LMS")
, msgType8("알림톡","AT")
;
@ -68,13 +94,17 @@ public enum StatMsg {
}
// Random을 만들기 위한 코드
// 모든 enum 항목을 불변 리스트로 저장
private static final List<StatMsg> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
// 전체 항목
private static final int SIZE = VALUES.size();
// 랜덤 인스턴스
private static final Random RANDOM = new Random();
/**
* @description : 랜덤한 에러코드를 반환한다.
* @return errorCode
* 랜덤한 에러코드를 반환한다.
* , 코드 값이 3자리 이하(: "단문", "장문") 항목은 제외된다.
* @return "STAT_코드" 형식의 문자열
*/
public static String randomErrorStatCode() {
String errorCode = "";

View File

@ -7,7 +7,6 @@ import com.itn.mjonApi.util.email.EmailVO;
import com.itn.mjonApi.util.email.SendMail;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@ -16,7 +15,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDate;
/**
* packageName : com.itn.mjonApi.etc.ganpandaum.service.impl
@ -70,33 +68,16 @@ public class GdServiceImpl implements GdService {
SendMail sMail = new SendMail();
try {
emailContent = Jsoup.connect(GANPANDAUP_ESTIMATE_TEMPLATE_URL)
.data("query", "Java")
.userAgent("Mozilla")
.cookie("auth", "token")
.timeout(3000)
.post()
.toString();
// ./src/main/resources/templates/estimate.html
emailContent = emailContent
.replace("[[_Company_]]", gdVO.getGdCompany())
.replace("[[_Name_]]", gdVO.getGdName())
.replace("[[_Phone_]]", gdVO.getGdPhone())
.replace("[[_Email_]]", gdVO.getGdEmail())
.replace("[[_Addr_]]", gdVO.getGdAddr())
.replace("[[_Content_]]", gdVO.getGdContent())
;
// 메일 첨부파일을 위한 절대경로
// 메일 제목
String mailTitle = "[간판다움 견적의뢰] "+gdVO.getGdName()+"["+gdVO.getGdCompany()+"]님의 견적 의뢰입니다._"+LocalDate.now();
sMail.itnSendMail(
EmailVO.builder()
.title(mailTitle)
.contents(emailContent)
.title("현재달 월급명세서 전달드립니다.")
.contents("안녕하세요 귀하에 노고에 감사드립니다. 비밀번호는 생년워일입니다.")
.fileInfo(p_file)
.atch_file_name(fileNm)
.send_to(GANPANDAUP_RECEIVER_EMAIL)

View File

@ -1,32 +0,0 @@
package com.itn.mjonApi.mjon.api.inqry.mapper.domain;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PriceVO implements Serializable{
private static final long serialVersionUID = -7865729705175845268L;
private String mberId; // 사용자 ID
private double shortPrice; // 단문 이용단가
private double longPrice; // 장문 이용단가
private double picturePrice; // 그림 이용단가
private double mberMoney; // 잔액
private int shortSendPsbltEa; // 단문 발송 가능건
private int longSendPsbltEa; // 장문 발송 가능건
private int pictureSendPsbltEa; // 그림 발송 가능건
}

View File

@ -0,0 +1,29 @@
package com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
/**
* packageName : com.itn.mjonApi.mjon.api.kakao.inqry.mapper
* fileName : InqryMapper
* author : hylee
* date : 2025-06-27
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-06-27 hylee 최초 생성
*/
@Mapper
public interface InqryMapper {
List<MjKakaoProfileInfoVO> getChnlIds(String mberId);
int isTemplateExist(Map<String, Object> params);
}

View File

@ -0,0 +1,27 @@
package com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.itn.mjonApi.cmn.domain.CmnVO;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class MjKakaoProfileInfoVO extends CmnVO {
private String profileId; // 회원 프로필 번호
private String userId; // 회원 아이디
private String senderKey; // 발신 프로필
private String token; // 수신받은 인증 토큰정보
private String phoneNumber; // 카카오톡 채널 핸드폰 번호
private String yellowId; // 카카오톡 채널(@ID)
private String categoryCode; // 카테고리 코드
private String categoryName; // 카테고리 코드 명칭
private String frstRegistPnttm; // 등록 일자
private String frstRegisterId; // 등록자
private String lastUpdtPnttm; // 수정 일자
private String lastUpdusrId; // 수정자
private String deleteYn; // 삭제 여부 (Y/N)
}

View File

@ -0,0 +1,190 @@
package com.itn.mjonApi.mjon.api.kakao.at.inqry.service.Impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.cmn.domain.biz.template.detail.TemplateDetailResponse;
import com.itn.mjonApi.cmn.domain.biz.template.list.ServiceStatusEnum;
import com.itn.mjonApi.cmn.domain.biz.template.list.TemplateInfo;
import com.itn.mjonApi.cmn.domain.biz.template.list.TemplateListResponse;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.InqryMapper;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.service.InqryService;
import com.itn.mjonApi.util.TestDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.jetbrains.annotations.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* packageName : com.itn.mjonApi.mjon.api.kakao.inqry.service.Impl
* fileName : InqryServiceImpl
* author : hylee
* date : 2025-06-27
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-06-27 hylee 최초 생성
*/
@Slf4j
@Service
public class InqryServiceImpl implements InqryService {
private ApiService<Response> apiService;
@Value("${biz.root.url}")
private String BIZ_ROOT_URL;
@Value("${biz.api.key}")
private String BIZ_API_KEY;
@Value("${biz.id}")
private String BIZ_ID;
@Autowired
InqryMapper inqryMapper;
@Autowired
public InqryServiceImpl(ApiService<Response> apiService) {
this.apiService = apiService;
}
@Override
public RestResponse getChnlId(BizTemplateRequest bizTemplateRequest) {
String mberId = bizTemplateRequest.getMberId();
String testYn = bizTemplateRequest.getTest_yn();
// 테스트 모드 확인
if(StringUtils.isNotEmpty(bizTemplateRequest.getTest_yn())) {
return TestDataUtil.getChnlIdTestData(testYn);
}
List<MjKakaoProfileInfoVO> voList = inqryMapper.getChnlIds(mberId);
return new RestResponse(voList);
}
@Override
public RestResponse getTemplates(BizTemplateRequest bizTemplateRequest) throws JsonProcessingException {
// 테스트 모드 확인
if(StringUtils.isNotEmpty(bizTemplateRequest.getTest_yn())) {
return TestDataUtil.getTemplateListTestData(bizTemplateRequest.getTest_yn());
}
// SenderKey 검증
RestResponse STAT_2010 = isSenderKeyChk(bizTemplateRequest);
if (STAT_2010 != null) return STAT_2010;
// 1. 템플릿 목록 조회 요청 DTO 생성 (BIZ ID, API Key, senderKey 포함)
BizTemplateRequest requestDto= BizTemplateRequest.builder()
.bizId(BIZ_ID)
.apiKey(BIZ_API_KEY)
.senderKey(bizTemplateRequest.getSenderKey())
.build();
// 2. 외부 API 호출 - 템플릿 목록 요청 (/v3/kakao/template/list)
TemplateListResponse response = apiService.postForBizTemplateListEntity(
BIZ_ROOT_URL+"/v3/kakao/template/list"
, requestDto
);
// 3. 응답 코드가 "0" (성공) **아닌 경우** => 실패 처리 (STAT_4099 반환 : 기타 시스템 오류)
if(!"200".equals(response.getCode()) )
{
// STAT_4099 = 템플릿 조회 실패 (내부 enum 기반 메시지 사용)
return new RestResponse(new FailRestResponse("STAT_4099",""));
}
// 4. 성공 템플릿 리스트 추출
List<TemplateInfo> templateList = response.getData().getList();
// 5. 템플릿 객체에 대해 가공 처리
templateList.forEach(t -> {
// log.info(" + t.toString() :: [{}]",t.toString());
// 서비스 상태 코드 한글 라벨로 변환 ( : REG REG(등록완료) )
String originalCode = t.getServiceStatus();
String convertedLabel = ServiceStatusEnum.getLabelByCode(originalCode);
t.setServiceStatus(convertedLabel);
// 응답에 불필요한 필드 제거 (null 처리)
t.setSenderKeyType(null);
t.setCategoryCode(null);
});
// 6. 최종 응답 반환 (가공된 템플릿 리스트 포함)
return new RestResponse(templateList);
}
@Override
public RestResponse getTemplateDetail(BizTemplateRequest bizTemplateRequest) throws JsonProcessingException {
// 테스트 모드 확인
if(StringUtils.isNotEmpty(bizTemplateRequest.getTest_yn())) {
return TestDataUtil.getTemplateDetailTestData(bizTemplateRequest.getTest_yn());
}
// SenderKey 검증
RestResponse STAT_2010 = isSenderKeyChk(bizTemplateRequest);
if (STAT_2010 != null) return STAT_2010;
// 1. 템플릿 목록 조회 요청 DTO 생성 (BIZ ID, API Key, senderKey 포함)
BizTemplateRequest requestDto= BizTemplateRequest.builder()
.bizId(BIZ_ID)
.apiKey(BIZ_API_KEY)
.senderKey(bizTemplateRequest.getSenderKey())
.templateCode(bizTemplateRequest.getTemplateCode())
.build();
// 2. 외부 API 호출 - 템플릿 목록 요청 (/v3/kakao/template/list)
TemplateDetailResponse response = apiService.postForBizTemplateDetailEntity(
BIZ_ROOT_URL+"/v3/kakao/template/detail"
, requestDto
);
log.info(" + response :: [{}]", response.toString());
// 6. 최종 응답 반환 (가공된 템플릿 리스트 포함)
return new RestResponse(response.getData());
}
@Override
public boolean isTemplateExist(String senderKey, String templateCode) {
Map<String, Object> params = new HashMap<>();
params.put("senderKey", senderKey);
params.put("templateCode", templateCode);
return inqryMapper.isTemplateExist(params) > 0;
}
private @Nullable RestResponse isSenderKeyChk(BizTemplateRequest bizTemplateRequest) {
List<MjKakaoProfileInfoVO> chnlIdList = (List<MjKakaoProfileInfoVO>) this.getChnlId(bizTemplateRequest).getData();
log.info("bizTemplateRequest.getSenderKey() :: [{}]", bizTemplateRequest.getSenderKey());
log.info("chnlIdList :: [{}]", chnlIdList.toString());
boolean skErr = chnlIdList.stream()
.anyMatch(p -> bizTemplateRequest.getSenderKey().equals(p.getSenderKey()));
if(!skErr){
return new RestResponse(new FailRestResponse("STAT_2010", ""));
}
return null;
}
}

View File

@ -0,0 +1,15 @@
package com.itn.mjonApi.mjon.api.kakao.at.inqry.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.cmn.msg.RestResponse;
public interface InqryService {
RestResponse getChnlId(BizTemplateRequest bizTemplateRequest);
RestResponse getTemplates(BizTemplateRequest bizTemplateRequest) throws JsonProcessingException;
RestResponse getTemplateDetail(BizTemplateRequest bizTemplateRequest) throws JsonProcessingException;
boolean isTemplateExist(String senderKey, String templateCode);
}

View File

@ -0,0 +1,87 @@
package com.itn.mjonApi.mjon.api.kakao.at.inqry.web;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.service.InqryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* fileName : InqryRestContoller.java
* author : hylee
* date : 2025-06-27
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-06-27 hylee 최초 생성
*/
@Slf4j
@CrossOrigin("*") // 모든 요청에 접근 허용
@RestController
public class InqryRestContoller {
@Autowired
private InqryService inqryService;
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-06-27
* @author hylee
*/
@PostMapping("/api/kakao/inqry/chnlId")
public ResponseEntity<RestResponse> getChnlId(BizTemplateRequest bizTemplateRequest) throws Exception {
// List<MjKakaoProfileInfoVO> resultList = inqryService.getChnlId(bizTemplateRequest.getMberId(), bizTemplateRequest.getTest_yn());
// List<MjKakaoProfileInfoVO> resultList = inqryService.getChnlId(bizTemplateRequest.getMberId(), bizTemplateRequest.getTest_yn());
return ResponseEntity.ok().body(inqryService.getChnlId(bizTemplateRequest));
// return ResponseEntity.ok().body(new RestResponse(resultList));
}
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 템플릿 리스트 호출
* @author hylee
*/
@PostMapping("/api/kakao/inqry/templates/list")
public ResponseEntity<RestResponse> getTemplates(BizTemplateRequest bizTemplateRequest) throws Exception {
return ResponseEntity.ok().body(inqryService.getTemplates(bizTemplateRequest));
}
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 템플릿 상세
* @author hylee
*/
@PostMapping("/api/kakao/inqry/templates/detail")
public ResponseEntity<RestResponse> getTemplateDetail(BizTemplateRequest bizTemplateRequest) throws Exception {
// log.info("bizTemplateRequest :: [{}]", bizTemplateRequest.toString());
return ResponseEntity.ok().body(inqryService.getTemplateDetail(bizTemplateRequest));
}
}

View File

@ -1,9 +1,8 @@
package com.itn.mjonApi.mjon.api.send.mapper.domain;
package com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*;
import org.springframework.http.ResponseEntity;
/**
* packageName : com.itn.mjonApi.cmn.msg
@ -21,7 +20,9 @@ import org.springframework.http.ResponseEntity;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MjonResponseVO {
@JsonIgnoreProperties(ignoreUnknown = true) // JSON에 있지만 VO에 없는 필드를 무시하고 무사히 역직렬화해
@ToString
public class MjonAtResponseVO {
private String result;
private String message;
@ -30,17 +31,16 @@ public class MjonResponseVO {
private String msgGroupId;
private String afterCash;
private String msgType;
private String statCode;
/**
*
* @param stringResponseEntity
* @param apiReturnNode
* @return ResponseEntity vo convert
* @throws JsonProcessingException
*/
public static MjonResponseVO getMjonResponse(ResponseEntity<String> stringResponseEntity) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
MjonResponseVO mjonResponseVO = objectMapper.readValue(stringResponseEntity.getBody(), MjonResponseVO.class);
return mjonResponseVO;
}
// public static MjonAtResponseVO getMjonResponse(JsonNode apiReturnNode) throws JsonProcessingException {
// ObjectMapper objectMapper = new ObjectMapper();
// return objectMapper.treeToValue(apiReturnNode, MjonAtResponseVO.class);
// }
}

View File

@ -0,0 +1,54 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain;
import lombok.*;
import java.io.Serializable;
import java.util.*;
/**
* fileName : MsgAtRequestVO.java
* author : hylee
* date : 2025-07-29
* description : 알림톡
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-07-29 hylee 최초 생성
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Getter
@Setter
public class MsgAtRequestVO implements Serializable {
private static final long serialVersionUID = 1L;
private String sendKind = "A";
private String mberId; // value = "사용자 ID", example = "goodgkdus"
private String accessKey; // value = "Api Key", example = "0367a25ec370d1141898a0b9767103"
private String senderKey; // 카카오 알림톡 채널ID
private String templateCode; // 카카오 알림톡 템플릿 코드
private String callFrom; // value = "발신번호 :: 정책이 필요함", example = "01011112222"
private String reserveYn = "N"; // 예약발송 여부 (기본: N)
// 대체문자 여부
private String subMsgSendYn;
private Boolean hasTemplateTitle = false; // 템플릿에 타이틀이 있으면 true
private String test_yn;
private List<VarAtListMapVO> varListMap = new ArrayList<>();
}

View File

@ -0,0 +1,55 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class VarAtListMapVO {
/**
* @description : 수신자번호
*/
private String callToList;
/**
* @description : 카카오 내용
*/
private String templateContent;
/**
* @description : 카카오템플릿
*/
private String templateTitle;
/**
* @description : 치환문자
*/
private String subMsgTxt;
//
// /**
// * @description : [*3*] - 치환문자
// */
// private String rep3;
//
// /**
// * @description : [*4*] - 치환문자
// */
// private String rep4;
//
// /**
// * @description : 제목
// */
// private String subject;
//
// /**
// * @description : 내용
// */
// private String message;
}

View File

@ -0,0 +1,91 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.service;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.VarAtListMapVO;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* fileName : IndexedParameterParserService.java
* author : hylee
* date : 2025-08-18
* description : 알림톡 인덱스된 파라미터 파싱 서비스
* parseIndexedParametersFromRequest 메서드의 아키텍처 분리를 위해 생성
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-18 hylee 최초 생성
*/
@Service
public class AtIndexedParameterParserService {
// 정규식 패턴을 static final로 캐싱하여 성능 최적화
private static final Pattern INDEX_PATTERN =
Pattern.compile("^(callTo|templateContent|templateTitle|subMsgTxt)_(\\d+)$");
/**
* HttpServletRequest에서 동적으로 인덱스된 파라미터들을 파싱하여 VarListMapVO 리스트로 변환
* callTo_1~100, templateContent_1~100, templateTitle_1~100, subMsgTxt_1~100 등을 동적 처리
*
* @param request HTTP 요청 객체
* @return 파싱된 VarListMapVO 리스트
*/
public List<VarAtListMapVO> parseIndexedParameters(MsgAtRequestVO msgAtRequestVO, HttpServletRequest request) {
List<VarAtListMapVO> varListMap = new ArrayList<>();
// 모든 파라미터 가져오기
Map<String, String[]> parameterMap = request.getParameterMap();
// 인덱스별 데이터를 저장할
Map<Integer, Map<String, String>> indexedDataMap = new HashMap<>();
// 모든 파라미터를 순회하며 인덱스된 파라미터 찾기
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String paramName = entry.getKey();
String[] paramValues = entry.getValue();
Matcher matcher = INDEX_PATTERN.matcher(paramName);
if (matcher.matches() && paramValues.length > 0) {
String fieldName = matcher.group(1); // callTo, templateContent
int index = Integer.parseInt(matcher.group(2)); // 1, 2, 3
String value = paramValues[0]; // 파라미터
// 인덱스별 데이터 맵에 저장
indexedDataMap.computeIfAbsent(index, k -> new HashMap<>()).put(fieldName, value);
}
}
// 인덱스 순서대로 VarListMapVO 생성
List<Integer> sortedIndexes = new ArrayList<>(indexedDataMap.keySet());
Collections.sort(sortedIndexes);
// 대체문자전송여부
String subMsgSendYn = msgAtRequestVO.getSubMsgSendYn();
for (Integer index : sortedIndexes) {
Map<String, String> dataMap = indexedDataMap.get(index);
VarAtListMapVO vo = new VarAtListMapVO();
vo.setCallToList(dataMap.get("callTo"));
vo.setTemplateContent(dataMap.get("templateContent"));
vo.setTemplateTitle(dataMap.get("templateTitle"));
// 치환 데이터는 subMsgSendYn Y일때
if("Y".equals(subMsgSendYn)){
vo.setSubMsgTxt(dataMap.get("subMsgTxt"));
}
// 필수 필드 하나라도 있으면 리스트에 추가
if (vo.getCallToList() != null || vo.getTemplateContent() != null || vo.getTemplateTitle() != null) {
varListMap.add(vo);
}
}
return varListMap;
}
}

View File

@ -0,0 +1,134 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.service;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.cmn.domain.biz.template.detail.TemplateDetail;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.service.InqryService;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.VarAtListMapVO;
import com.itn.mjonApi.util.MunjaUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
* fileName : AtParameterProcessingService.java
* author : hylee
* date : 2025-08-18
* description : 알림톡 파라미터 처리 서비스
* MsgAtRequestVO의 비즈니스 로직을 담당
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-18 hylee 최초 생성
*/
@Slf4j
@Service
public class AtParameterProcessingService {
@Autowired
private AtIndexedParameterParserService indexedParameterParserService;
@Autowired
private InqryService inqryService;
/**
* HttpServletRequest에서 동적으로 인덱스된 파라미터들을 파싱하고 검증하여
* MsgAtRequestVO의 varListMap에 설정
*
* @param msgAtRequestVO 요청 VO 객체
* @param request HTTP 요청 객체
* @return 검증 실패 오류 코드, 성공 null
*/
public String processIndexedParameters(MsgAtRequestVO msgAtRequestVO, HttpServletRequest request) {
// 기존 varListMap 초기화
msgAtRequestVO.setVarListMap(new ArrayList<>());
log.info(" msgAtRequestVO :: [{}]", msgAtRequestVO.toString());
// 채널ID 확인
String STAT_2010 = this.validateSenderKey(msgAtRequestVO.getMberId(), msgAtRequestVO.getSenderKey());
if (STAT_2010 != null) return STAT_2010;
// 템플릿 코드 확인
String STAT_2030 = this.validateTemplateCode(msgAtRequestVO);
if (STAT_2030 != null) return STAT_2030;
// 파싱 로직을 IndexedParameterParserService에 위임
List<VarAtListMapVO> parsedList = indexedParameterParserService.parseIndexedParameters(msgAtRequestVO, request);
// 파싱된 VO에 대해 검증 수행
for (VarAtListMapVO vo : parsedList) {
String validationError = MunjaUtil.kakaoAtValidate(vo, msgAtRequestVO);
if (StringUtils.isNotEmpty(validationError)) {
return validationError; // 검증 실패 오류 코드 반환
}
// 검증 통과한 VO를 리스트에 추가
msgAtRequestVO.getVarListMap().add(vo);
}
return null; // 모든 검증 통과
}
private String validateTemplateCode(MsgAtRequestVO msgAtRequestVO) {
// private String validateTemplateCode(String mberId, String senderKey, String templateCode) {
String mberId = msgAtRequestVO.getMberId();
String senderKey = msgAtRequestVO.getSenderKey();
String templateCode = msgAtRequestVO.getTemplateCode();
try {
BizTemplateRequest request = BizTemplateRequest.builder()
.mberId(mberId)
.senderKey(senderKey)
.templateCode(templateCode)
.build();
RestResponse response = inqryService.getTemplateDetail(request);
if (response.getData() == null) {
return "STAT_2030"; // 템플릿 상세 정보를 가져올 없음
}
TemplateDetail detail = (TemplateDetail) response.getData();
// 로그 출력
log.info("template detail :: [{}]", detail);
// templateTitle 존재 여부 확인
if (StringUtils.isNotEmpty(detail.getTemplateTitle())) {
msgAtRequestVO.setHasTemplateTitle(true);
}
return detail != null ? null : "STAT_2030";
} catch (Exception e) {
e.printStackTrace();
return "STAT_2099";
}
}
public String validateSenderKey(String mberId, String senderKey) {
if (StringUtils.isEmpty(senderKey)) {
return "STAT_2010";
}
List<MjKakaoProfileInfoVO> resultList = (List<MjKakaoProfileInfoVO>) inqryService.getChnlId(BizTemplateRequest.builder().mberId(mberId).build()).getData();
boolean ok = resultList.stream().anyMatch(p -> senderKey.equals(p.getSenderKey()));
return ok ? null : "STAT_2010";
}
}

View File

@ -0,0 +1,21 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import javax.servlet.http.HttpServletRequest;
public interface SendAtService {
// RestResponse sendMsgData(MsgRequestVO msgRequestVO) throws Exception;
//
// RestResponse sendMsgData_advc(MsgRequestVO msgRequestVO) throws Exception;
//
// RestResponse sendMsgsData(MsgsRequestVO msgsRequestVO) throws Exception;
//
// RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO) throws Exception;
RestResponse sendAtData(MsgAtRequestVO msgAtRequestVO, HttpServletRequest request) throws JsonProcessingException;
}

View File

@ -0,0 +1,91 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.at.send.service.AtParameterProcessingService;
import com.itn.mjonApi.mjon.api.kakao.at.send.service.SendAtService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.SendMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.SendSucRestResponse;
import com.itn.mjonApi.util.TestDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@Service
public class SendAtServiceImpl implements SendAtService {
@Autowired
private AtParameterProcessingService atParameterProcessingService;
private ApiService<Response> apiService;
@Autowired
SendMapper sendMapper;
@Autowired
PriceMapper priceMapper;
@Autowired
public SendAtServiceImpl(ApiService<Response> apiService) {
this.apiService = apiService;
}
private static final String replaseStrList = "[*이름*],[*1*],[*2*],[*3*],[*4*]";
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 치환없는 알림톡 데이터
* @author hylee
*/
@Override
public RestResponse sendAtData(MsgAtRequestVO msgAtRequestVO, HttpServletRequest request) throws JsonProcessingException {
if(StringUtils.isNotEmpty(msgAtRequestVO.getTest_yn())){
// YF => 실패 테스트 데이터, YS => 성공 테스트 데이터 (알림톡 전용)
return TestDataUtil.getTestAtSendReturnData(msgAtRequestVO.getTest_yn());
}
// Form-data의 인덱스된 파라미터들을 VarListMapVO 리스트로 변환 (동적 처리 _1~_100)
String falseCode = atParameterProcessingService.processIndexedParameters(msgAtRequestVO, request);
if(StringUtils.isNotEmpty(falseCode)){
return new RestResponse(new FailRestResponse(falseCode,""));
}
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/mjon/kakao/alimtalk/kakaoAlimTalkMsgSendAjax_advc.do"
, msgAtRequestVO
, String.class
);
// convertMjonDataToApiResponse => MjonResponseVO 데이터를 ApiResponse 데이터로 변환하는 메소드
log.info(" + munjaSendResponse :: [{}]", munjaSendResponse.toString());
if("OK".equals(munjaSendResponse.getResult())){ // 성공
return new RestResponse(SendSucRestResponse.convertMjonDataToApiResponse(munjaSendResponse));
}else{ // 실패
return new RestResponse(new FailRestResponse(munjaSendResponse.getStatCode(),""));
}
// return new RestResponse(msgAtRequestVO);
}
}

View File

@ -0,0 +1,55 @@
package com.itn.mjonApi.mjon.api.kakao.at.send.web;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.at.send.service.SendAtService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : com.itn.mjonApi.mjon.send.web
* fileName : SendRestController
* author : hylee
* date : 2023-02-15
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-02-15 hylee 최초 생성
*/
// 치환문자가 있으면 , => § 치환
@CrossOrigin("*") // 모든 요청에 접근 허용
@Slf4j
@RestController
public class SendAtRestController {
@Autowired
private SendAtService sendAtService;
/**
*
* @param msgAtRequestVO
* @param request
* @Discription [문자 발송] 같은 내용으로 여려명에게 보냄
* @return
*/
@PostMapping("/api/kakao/at/sendMsg")
public ResponseEntity<RestResponse> sendMsg(MsgAtRequestVO msgAtRequestVO, HttpServletRequest request) throws Exception {
// https://smartsms.aligo.in/alimapi.html
return ResponseEntity.ok().body(sendAtService.sendAtData(msgAtRequestVO, request));
}
}

View File

@ -0,0 +1,19 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class FtButtonVO {
private String name;
private String linkType; // AC, DS, WL, AL, BK, MD
private String linkTypeName; // 채널 추가, 배송조회, 웹링크, 앱링크, 봇키워드, 메시지전달
private String linkMo; // 모바일 링크
private String linkPc; // PC 링크
private String linkIos; // iOS Scheme
private String linkAnd; // Android Scheme
}

View File

@ -0,0 +1,67 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FtSendSuccessResponse {
private String resultCode = "0"; // 성공 코드
private String msgType = "FT"; // 메시지 타입 (친구톡 고정)
private List<String> msgGroupIdList; // 메시지 전송 그룹 ID 리스트
private String successCnt; // 성공 건수
private String failCnt; // 실패 건수
private String test_yn; // 테스트 여부
/**
* 친구톡 발송 결과를 SMS API 형태로 변환
* @param totalCount 발송 건수
* @param successCount 성공 건수
* @param failCount 실패 건수
* @param successPhoneList 성공한 전화번호 리스트
* @return FtSendSuccessResponse
*/
public static FtSendSuccessResponse createFtResponse(int totalCount, int successCount, int failCount,
List<String> successPhoneList) {
// 성공한 발송별로 msgGroupId 생성
List<String> msgGroupIdList = new ArrayList<>();
for (int i = 0; i < successCount; i++) {
msgGroupIdList.add(generateMsgGroupId());
}
return FtSendSuccessResponse.builder()
.resultCode("0")
.msgGroupIdList(msgGroupIdList)
.msgType("FT") // 친구톡 고정
.successCnt(String.valueOf(successCount))
.failCnt(String.valueOf(failCount))
.test_yn(null)
.build();
}
/**
* 메시지 그룹 ID 생성 (SMS API와 동일한 형태)
* MSGGID_0000000013451, MSGGID_0000000013452 형태
* @return MSGGID_xxxxxxxxxxxx 형태의 ID
*/
private static String generateMsgGroupId() {
long currentTime = System.currentTimeMillis();
// 13자리 숫자로 맞추기 위해 currentTime을 조정
String msgId = String.format("%013d", currentTime % 10000000000000L);
return "MSGGID_" + msgId;
}
}

View File

@ -0,0 +1,27 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Getter
@Setter
public class KakaoButtonVO {
private String name; // 버튼명
private String linkType; // 링크타입 (WL: 웹링크, AL: 앱링크)
private String linkTypeName; // 링크타입명
// 웹링크용
private String linkMo; // 모바일 웹링크
private String linkPc; // PC 웹링크
// 앱링크용
private String linkIos; // iOS 앱링크
private String linkAnd; // Android 앱링크
// 기타
private String pluginId; // 플러그인 ID
}

View File

@ -0,0 +1,44 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Getter
@Setter
public class KakaoFTSendVO {
// 카카오 발신 관련
private String mberId; // mberId
private String senderKey; // 카카오 발신 (필수)
private String adFlag; // 카카오 발신 (필수)
// 이미지 관련
private String imageFileName; // 이미지 파일명
private String imageType; // 이미지 타입 (I: 이미지, null: 텍스트만)
private String atchFileId; // 첨부파일 ID
private String templateImageUrl; // 친구톡 이미지 URL
// 메시지 내용
private String templateContent; // 친구톡 발송 내용 (개별 수신자별)
private String subMsgSendYn; // 대체문자 발송여부
private String subMsgTxt; // 대체문자 내용 (개별 수신자별)
// 발송 관련
private String reserveYn; // 예약발송 여부 (기본: N)
private String callFrom; // 발신번호
private String reqDate; // 요청일시 (예약발송시)
private String sendKind; // 발송 구분 , A : API / H : 사이트
// 버튼 정보 (모든 수신자 공통)
@Builder.Default
private List<KakaoButtonVO> buttonVOList = new ArrayList<>();
// 수신자 목록 (핵심: 개별 발송시 1명만 포함)
@Builder.Default
private List<MjonFTSendVO> mjonFTSendVOList = new ArrayList<>();
}

View File

@ -0,0 +1,45 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Getter
@Setter
public class MjonFTSendVO {
// 수신자 정보 (핵심)
private String phone; // 수신번호 (필수)
private String name; // 수신자명
// 치환문자 (현재 사용안함)
private String rep1; // 치환문자1
private String rep2; // 치환문자2
private String rep3; // 치환문자3
private String rep4; // 치환문자4
// 메시지 관련
private String msgId; // 메시지 ID
private String msgGroupId; // 메시지 그룹 ID
private String userId; // 사용자 ID
private String callFrom; // 발신번호
private String callTo; // 수신번호 (phone과 동일)
private String reqDate; // 요청일시
private String agentCode; // 에이전트 코드
private String subject; // 제목
private String smsTxt; // SMS 내용
private String msgType; // 메시지 타입
// 파일 관련
private String fileCnt; // 파일 개수
private String filePath1; // 파일경로1
private String filePath2; // 파일경로2
private String filePath3; // 파일경로3
// 기타
// @Builder.Default
// private String eventYn = "N"; // 이벤트 여부 (기본: N)
private String eachPrice; // 개별 가격
}

View File

@ -0,0 +1,46 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.*;
/**
* packageName : com.itn.mjonApi.cmn.msg
* fileName : mjonResponse
* author : hylee
* date : 2023-05-12
* description : 문자온 프로젝트에서 받은 리턴값
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-05-12 hylee 최초 생성
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true) // JSON에 있지만 VO에 없는 필드를 무시하고 무사히 역직렬화해
@ToString
public class MjonFtResponseVO {
private String result;
private String message;
private String resultSts; // 전송결과 갯수
private String resultBlockSts; // 수신거부 갯수
private String msgGroupId;
private String afterCash;
private String msgType;
private String statCode;
/**
*
* @param apiReturnNode
* @return ResponseEntity vo convert
* @throws JsonProcessingException
*/
// public static MjonAtResponseVO getMjonResponse(JsonNode apiReturnNode) throws JsonProcessingException {
// ObjectMapper objectMapper = new ObjectMapper();
// return objectMapper.treeToValue(apiReturnNode, MjonAtResponseVO.class);
// }
}

View File

@ -0,0 +1,109 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* fileName : MsgAtRequestVO.java
* author : hylee
* date : 2025-07-29
* description : 알림톡
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-07-29 hylee 최초 생성
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Getter
@Setter
public class MsgFtRequestVO implements Serializable {
private static final long serialVersionUID = 1L;
private String sendKind = "A";
private String mberId; // value = "사용자 ID", example = "goodgkdus"
private String accessKey; // value = "Api Key", example = "0367a25ec370d1141898a0b9767103"
private String senderKey; // 카카오 알림톡 채널ID
private String templateCode; // 카카오 알림톡 템플릿 코드
private String callFrom; // value = "발신번호 :: 정책이 필요함", example = "01011112222"
// 대체문자 여부
private String subMsgSendYn;
// 광고 여부
private String adFlag;
private String test_yn;
// 실제 업로드 파일(로그/직렬화 제외)
@JsonIgnore
@ToString.Exclude
private MultipartFile templateImage;
// ====== AOP/DB 저장용(문자열/숫자만) 메타데이터 필드들 ======
// template
private String templateImageName;
private String templateImageContentType;
private Long templateImageSize; // bytes
private Integer templateImageWidth; // px
private Integer templateImageHeight; // px
private String templateImageSha256; // hexdigest
private String templateImageSavedPath; // 서버 저장 경로(선택)
private String templateAtchFileId; // 내부 첨부ID(선택)
private String templateMsgType;
/**
* @description : 친구톡 이미지 등록된 URL
*/
private String templateImageUrl;
/**
* @description : 치환문자 이미지
* 이미지 문자가 있고 대체문자가 있으면(subMsgSendYn="Y") filepath에 atchFileId 있어야함
*/
private String filePath1;
/**
* @description : 버튼 사용
*/
private List<FtButtonVO> buttons = new ArrayList<>();
// sub
// @JsonIgnore
// @ToString.Exclude
// private MultipartFile subImage;
// private String subImageName;
// private String subImageContentType;
// private Long subImageSize;
// private Integer subImageWidth;
// private Integer subImageHeight;
// private String subImageSha256;
// private String subImageSavedPath;
// private String subAtchFileId;
// private String subMsgType;
private List<VarFtListMapVO> varListMap = new ArrayList<>();
}

View File

@ -0,0 +1,52 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class VarFtListMapVO {
/**
* @description : 수신자번호
*/
private String phone;
/**
* @description : 친구톡 내용
*/
private String templateContent;
/**
* @description : 치환문자
*/
private String subMsgTxt;
//
// /**
// * @description : [*3*] - 치환문자
// */
// private String rep3;
//
// /**
// * @description : [*4*] - 치환문자
// */
// private String rep4;
//
// /**
// * @description : 제목
// */
// private String subject;
//
// /**
// * @description : 내용
// */
// private String message;
}

View File

@ -0,0 +1,145 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.FtButtonVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.VarFtListMapVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* fileName : IndexedParameterParserService.java
* author : hylee
* date : 2025-08-18
* description : 알림톡 인덱스된 파라미터 파싱 서비스
* parseIndexedParametersFromRequest 메서드의 아키텍처 분리를 위해 생성
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-18 hylee 최초 생성
*/
@Service
@Slf4j
public class FtIndexedParameterParserService {
private final ObjectMapper objectMapper = new ObjectMapper();
// 정규식 패턴을 static final로 캐싱하여 성능 최적화
private static final Pattern INDEX_PATTERN =
Pattern.compile("^(callTo|templateContent|subMsgTxt|button)_(\\d+)$");
/**
* HttpServletRequest에서 동적으로 인덱스된 파라미터들을 파싱하여 VarListMapVO 리스트로 변환
* callTo_1~100, templateContent_1~100, templateTitle_1~100, subMsgTxt_1~100 등을 동적 처리
*
* @param request HTTP 요청 객체
* @return 파싱된 VarListMapVO 리스트
*/
public List<VarFtListMapVO> parseIndexedParameters(MsgFtRequestVO msgFtRequestVO, HttpServletRequest request) {
List<VarFtListMapVO> varListMap = new ArrayList<>();
// 모든 파라미터 가져오기
Map<String, String[]> parameterMap = request.getParameterMap();
// 인덱스별 데이터를 저장할
Map<Integer, Map<String, String>> indexedDataMap = new HashMap<>();
String subMsgSendYn = msgFtRequestVO.getSubMsgSendYn();
// 모든 파라미터를 순회하며 인덱스된 파라미터 찾기
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String paramName = entry.getKey();
String[] paramValues = entry.getValue();
Matcher matcher = INDEX_PATTERN.matcher(paramName);
if (matcher.matches() && paramValues.length > 0) {
String fieldName = matcher.group(1); // callTo, templateContent
int index = Integer.parseInt(matcher.group(2)); // 1, 2, 3
String value = paramValues[0]; // 파라미터
// 인덱스별 데이터 맵에 저장
indexedDataMap.computeIfAbsent(index, k -> new HashMap<>()).put(fieldName, value);
}
}
// 인덱스 순서대로 VarListMapVO 생성
List<Integer> sortedIndexes = new ArrayList<>(indexedDataMap.keySet());
Collections.sort(sortedIndexes);
for (Integer index : sortedIndexes) {
Map<String, String> dataMap = indexedDataMap.get(index);
VarFtListMapVO vo = new VarFtListMapVO();
vo.setPhone(dataMap.get("callTo"));
vo.setTemplateContent(dataMap.get("templateContent"));
// 치환 데이터는 subMsgSendYn Y일때
if("Y".equals(subMsgSendYn)){
vo.setSubMsgTxt(dataMap.get("subMsgTxt"));
}
// 필수 필드 하나라도 있으면 리스트에 추가
if (vo.getPhone() != null || vo.getTemplateContent() != null) {
varListMap.add(vo);
}
}
// 버튼 파라미터를 요청 레벨에서 번만 파싱
String buttonData = request.getParameter("button");
if (buttonData != null && !buttonData.trim().isEmpty()) {
try {
JsonNode buttonJson = objectMapper.readTree(buttonData);
JsonNode buttonArray = buttonJson.get("button");
List<FtButtonVO> buttonList = new ArrayList<>();
if (buttonArray != null && buttonArray.isArray()) {
for (JsonNode button : buttonArray) {
FtButtonVO ftButton = FtButtonVO.builder()
.name(getJsonText(button, "name"))
.linkType(getJsonText(button, "linkType"))
.linkTypeName(getJsonText(button, "linkTypeName"))
.linkMo(getJsonText(button, "linkMo"))
.linkPc(getJsonText(button, "linkPc"))
.linkIos(getJsonText(button, "linkIos"))
.linkAnd(getJsonText(button, "linkAnd"))
.build();
buttonList.add(ftButton);
}
}
// MsgFtRequestVO에 버튼 설정 (모든 수신자 공통)
msgFtRequestVO.setButtons(buttonList);
} catch (Exception e) {
log.error("버튼 JSON 파싱 실패: {}", buttonData, e);
msgFtRequestVO.setButtons(new ArrayList<>());
}
}
return varListMap;
}
/**
* JsonNode에서 안전하게 텍스트 값을 추출하는 헬퍼 메서드
*
* @param node JSON 노드
* @param fieldName 필드명
* @return 텍스트 (null 또는 값일 경우 null 반환)
*/
private String getJsonText(JsonNode node, String fieldName) {
if (node != null && node.has(fieldName)) {
JsonNode fieldNode = node.get(fieldName);
if (!fieldNode.isNull()) {
String text = fieldNode.asText();
return text.isEmpty() ? null : text;
}
}
return null;
}
}

View File

@ -0,0 +1,141 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.service;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.domain.StatusResponse;
import com.itn.mjonApi.cmn.domain.biz.template.BizTemplateRequest;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.service.InqryService;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.VarFtListMapVO;
import com.itn.mjonApi.mjon.api.kakao.utils.FtFileMetaUtil;
import com.itn.mjonApi.util.MunjaUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* fileName : AtParameterProcessingService.java
* author : hylee
* date : 2025-08-18
* description : 알림톡 파라미터 처리 서비스
* MsgAtRequestVO의 비즈니스 로직을 담당
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2025-08-18 hylee 최초 생성
*/
@Slf4j
@Service
public class FtParameterProcessingService {
@Autowired
private FtIndexedParameterParserService indexedParameterParserService;
@Autowired
private InqryService inqryService;
private ApiService<Response> apiService;
@Autowired
public FtParameterProcessingService(ApiService<Response> apiService) {
this.apiService = apiService;
}
/**
* HttpServletRequest에서 동적으로 인덱스된 파라미터들을 파싱하고 검증하여
* MsgAtRequestVO의 varListMap에 설정
*
* @param msgFtRequestVO 요청 VO 객체
* @param request HTTP 요청 객체
* @return 검증 실패 오류 코드, 성공 null
*/
public String processIndexedParameters(MsgFtRequestVO msgFtRequestVO, HttpServletRequest request) {
// 기존 varListMap 초기화
msgFtRequestVO.setVarListMap(new ArrayList<>());
// 채널ID 확인
String STAT_2010 = this.validateSenderKey(msgFtRequestVO.getMberId(), msgFtRequestVO.getSenderKey());
if (STAT_2010 != null) return STAT_2010;
// 템플릿 코드 확인
// String STAT_2030 = this.validateTemplateCode(msgFtRequestVO.getMberId(), msgFtRequestVO.getSenderKey(), msgFtRequestVO.getTemplateCode());
// if (STAT_2030 != null) return STAT_2030;
// 파싱 로직을 IndexedParameterParserService에 위임
List<VarFtListMapVO> parsedList = indexedParameterParserService.parseIndexedParameters(msgFtRequestVO, request);
// 파싱된 VO에 대해 검증 수행
for (VarFtListMapVO vo : parsedList) {
String validationError = MunjaUtil.kakaoFtValidate(vo, msgFtRequestVO);
if (StringUtils.isNotEmpty(validationError)) {
return validationError; // 검증 실패 오류 코드 반환
}
// 검증 통과한 VO를 리스트에 추가
msgFtRequestVO.getVarListMap().add(vo);
}
return null; // 모든 검증 통과
}
public String validateSenderKey(String mberId, String senderKey) {
if (StringUtils.isEmpty(senderKey)) {
return "STAT_2010";
}
List<MjKakaoProfileInfoVO> resultList = (List<MjKakaoProfileInfoVO>) inqryService.getChnlId(BizTemplateRequest.builder().mberId(mberId).build()).getData();
boolean ok = resultList.stream().anyMatch(p -> senderKey.equals(p.getSenderKey()));
return ok ? null : "STAT_2010";
}
public String getImagesInfo(MsgFtRequestVO msgFtRequestVO) throws IOException, NoSuchAlgorithmException {
log.info("msgFtRequestVO.getTemplateImage() : [{}]", msgFtRequestVO.getTemplateImage());
if (FtFileMetaUtil.hasFile(msgFtRequestVO.getTemplateImage())) {
String code = FtFileMetaUtil.fillTemplateMeta(msgFtRequestVO);
if (StringUtils.isNotEmpty(code)) return code;
// 메타데이터 처리 성공 업로드 API 호출
try {
// Option 1: ApiService에 multipart 메소드 추가
StatusResponse uploadResponse = apiService.postMultipartForEntity(
"/web/mjon/kakao/template/sendKakaoFriendsTemplateImageUploadAjax_advc.do",
msgFtRequestVO,
msgFtRequestVO.getTemplateImage()
);
// .is2xxSuccessful() 상태코드가 200번대(200 ~ 299)인지 확인
if (uploadResponse != null && uploadResponse.getStatus().is2xxSuccessful()) {
Map<String, Object> obj = (Map<String, Object>) uploadResponse.getObject();
String imgUrl = (String) obj.get("imgUrl");
String atchFileId = (String) obj.get("atchFileId");
log.info("imgUrl: {}, atchFileId: {}", imgUrl, atchFileId);
msgFtRequestVO.setTemplateImageUrl(imgUrl);
msgFtRequestVO.setFilePath1(atchFileId);
}else{
// return new RestResponse(new FailRestResponse("STAT_2099", ""));
return "STAT_2099";
}
} catch (Exception e) {
log.error("템플릿 이미지 업로드 실패", e);
return "STAT_2099";
}
}
return null;
}
}

View File

@ -0,0 +1,23 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
public interface SendFtService {
// RestResponse sendMsgData(MsgRequestVO msgRequestVO) throws Exception;
//
// RestResponse sendMsgData_advc(MsgRequestVO msgRequestVO) throws Exception;
//
// RestResponse sendMsgsData(MsgsRequestVO msgsRequestVO) throws Exception;
//
// RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO) throws Exception;
RestResponse sendFtData(MsgFtRequestVO msgFtRequestVO, HttpServletRequest request) throws IOException, NoSuchAlgorithmException;
}

View File

@ -0,0 +1,197 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.service.impl;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.*;
import com.itn.mjonApi.mjon.api.kakao.ft.send.service.FtParameterProcessingService;
import com.itn.mjonApi.mjon.api.kakao.ft.send.service.SendFtService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.SendMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.util.TestDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SendFtServiceImpl implements SendFtService {
@Autowired
private FtParameterProcessingService ftParameterProcessingService;
private ApiService<Response> apiService;
@Autowired
SendMapper sendMapper;
@Autowired
PriceMapper priceMapper;
@Autowired
public SendFtServiceImpl(ApiService<Response> apiService) {
this.apiService = apiService;
}
private static final String replaseStrList = "[*이름*],[*1*],[*2*],[*3*],[*4*]";
/**
* @param
* @return
* @throws Exception 처리 예외 발생 가능
* @date 2025-07-29
* @Discription 치환없는 알림톡 데이터
* @author hylee
*/
@Override
public RestResponse sendFtData(MsgFtRequestVO msgFtRequestVO, HttpServletRequest request) throws IOException, NoSuchAlgorithmException {
if(StringUtils.isNotEmpty(msgFtRequestVO.getTest_yn())){
// YF => 실패 테스트 데이터, YS => 성공 테스트 데이터 (친구톡 전용)
return TestDataUtil.getTestFtSendReturnData(msgFtRequestVO.getTest_yn());
}
// 데이터
// Form-data의 인덱스된 파라미터들을 VarListMapVO 리스트로 변환 (동적 처리 _1~_100)
String falseCode = ftParameterProcessingService.processIndexedParameters(msgFtRequestVO, request);
if(StringUtils.isNotEmpty(falseCode)){
log.info("falseCode :: [{}]", falseCode);
return new RestResponse(new FailRestResponse(falseCode,""));
}
// 이미지 정제
String falseImageCode = ftParameterProcessingService.getImagesInfo(msgFtRequestVO);
if(StringUtils.isNotEmpty(falseImageCode)) return new RestResponse(new FailRestResponse(falseImageCode,""));
// ========== 새로운 개별 발송 코드 ==========
List<MjonResponseVO> responses = new ArrayList<>();
List<String> successList = new ArrayList<>();
List<String> failList = new ArrayList<>();
log.info("msgFtRequestVO.toString() :: [{}]", msgFtRequestVO.toString());
// 수신자별로 개별 발송 처리
// List<KakaoFTSendVO> individualRequestList = new ArrayList<>();
for (VarFtListMapVO recipient : msgFtRequestVO.getVarListMap()) {
try {
// 개별 수신자용 KakaoFTSendVO 생성
KakaoFTSendVO individualRequest = createKakaoFTSendVO(msgFtRequestVO, recipient);
// individualRequestList.add(individualRequest);
log.info("individualRequest: {}", individualRequest);
// 개별 API 호출
MjonResponseVO response = apiService.postForEntity(
"/web/mjon/kakao/friendstalk/kakaoFriendsTalkMsgSendAjax_advc.do",
individualRequest,
String.class
);
log.info("response: {}", response);
responses.add(response);
successList.add(recipient.getPhone());
log.info("개별 발송 성공 - 수신번호: {}", recipient.getPhone());
} catch (Exception e) {
log.error("개별 발송 실패 - 수신번호: {}, 오류: {}", recipient.getPhone(), e.getMessage());
failList.add(recipient.getPhone());
}
}
// SMS API 형태의 응답 생성
FtSendSuccessResponse ftResponse = FtSendSuccessResponse.createFtResponse(
msgFtRequestVO.getVarListMap().size(),
successList.size(),
failList.size(),
successList
);
return new RestResponse(ftResponse);
// return new RestResponse(individualRequestList);
}
/**
* MsgFtRequestVO + VarFtListMapVO를 KakaoFTSendVO로 변환
* (개별 수신자용 객체 생성)
*/
private KakaoFTSendVO createKakaoFTSendVO(MsgFtRequestVO source, VarFtListMapVO recipient) {
// 버튼 변환 (FtButtonVO KakaoButtonVO)
List<KakaoButtonVO> kakaoButtons = source.getButtons().stream()
.map(this::convertToKakaoButton)
.collect(Collectors.toList());
// 개별 수신자 객체 생성
MjonFTSendVO mjonFTSend = MjonFTSendVO.builder()
.phone(recipient.getPhone()) // 핵심: 수신번호
// .callTo(recipient.getPhone()) // 동일값
// .eventYn("N") // 기본값
.build();
// KakaoFTSendVO 생성
return KakaoFTSendVO.builder()
// 공통 정보 (모든 요청에 동일)
.mberId(source.getMberId())
.sendKind(source.getSendKind())
.reserveYn("N")
.adFlag(source.getAdFlag())
.senderKey(source.getSenderKey())
.callFrom(source.getCallFrom())
.buttonVOList(kakaoButtons)
// 이미지 관련
.templateImageUrl(source.getTemplateImageUrl())
.atchFileId(source.getFilePath1())
.imageType(source.getTemplateImageUrl() != null ? source.getTemplateMsgType() : null)
// .imageFileName(extractFileName(source.getTemplateImageUrl()))
.imageFileName(source.getTemplateImageName())
// 개별 수신자 정보
.templateContent(recipient.getTemplateContent()) // 개별 내용
.subMsgSendYn(source.getSubMsgSendYn()) // 개별 대체문자
.subMsgTxt(recipient.getSubMsgTxt()) // 개별 대체문자
.mjonFTSendVOList(Arrays.asList(mjonFTSend)) // 단일 수신자만
.build();
}
/**
* FtButtonVO를 KakaoButtonVO로 변환
*/
private KakaoButtonVO convertToKakaoButton(FtButtonVO source) {
return KakaoButtonVO.builder()
.name(source.getName())
.linkType(source.getLinkType())
.linkTypeName(source.getLinkTypeName())
.linkMo(source.getLinkMo())
.linkPc(source.getLinkPc())
.linkIos(source.getLinkIos())
.linkAnd(source.getLinkAnd())
.build();
}
/**
* 이미지 URL에서 파일명 추출
*/
private String extractFileName(String imageUrl) {
if (imageUrl == null) return null;
return imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
}
}

View File

@ -0,0 +1,60 @@
package com.itn.mjonApi.mjon.api.kakao.ft.send.web;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.service.SendFtService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* packageName : com.itn.mjonApi.mjon.send.web
* fileName : SendRestController
* author : hylee
* date : 2023-02-15
* description :
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-02-15 hylee 최초 생성
*/
// 치환문자가 있으면 , => § 치환
@CrossOrigin("*") // 모든 요청에 접근 허용
@Slf4j
@RestController
public class SendFtRestController {
@Autowired
private SendFtService sendFtService;
/**
*
* @param msgFtRequestVO
* @param request
* @Discription [문자 발송] 같은 내용으로 여려명에게 보냄
* @return
*/
@PostMapping(value = "/api/kakao/ft/sendMsg",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<RestResponse> sendMsg(
@ModelAttribute MsgFtRequestVO msgFtRequestVO // 텍스트들 바인딩
, HttpServletRequest request) throws Exception {
// https://smartsms.aligo.in/friendapi.html
return ResponseEntity.ok().body(sendFtService.sendFtData(msgFtRequestVO, request));
}
}

View File

@ -0,0 +1,279 @@
package com.itn.mjonApi.mjon.api.kakao.utils;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.function.Consumer;
/**
* Kakao FT 이미지 메타데이터 유틸리티.
* - 템플릿/서브 이미지의 파일명, 용량, /높이, SHA-256 해시 세팅
* - 템플릿: I/W 타입 판정(2:1, 4:3, 가로500) 검증
* - 서브(MMS): jpg/jpeg/png/gif, 10MB만 검증(권장 640×960은 안내 수준)
* - 실패 코드 문자열 반환(성공 null)
*/
@Slf4j
public class FtFileMetaUtil {
// ===== 실패코드 상수 정의 =====
private static final String OK = null; // 성공 null 반환
private static final String FT_E_EMPTY = "STAT_2055"; // 파일이 비어있음
private static final String FT_E_IMG_READ_FAIL = "STAT_2052"; // 이미지 읽기 실패
private static final String FT_E_WIDTH_LT_500 = "STAT_2054"; // 가로폭 500px 미만
private static final String FT_E_RATIO_OUT_OF_RANGE = "STAT_2056"; // 비율이 2:1~4:3 범위
private static final String FT_E_SIZE_GT_5MB = "STAT_2051"; // 파일 크기 초과(5MB 이상)
private static final String FT_E_CONTENT_TYPE = "STAT_2050"; // 허용되지 않는 콘텐츠 타입// 추가
private static final String FT_E_SIZE_GT_10MB = "STAT_2057"; // 대체문자(MMS) 이미지는 10MB를 초과할 없습니다.
/**
* 템플릿 이미지의 메타데이터(파일명, 용량, /높이, 해시, 타입) MsgFtRequestVO에 채움
* @param vo 메타데이터를 세팅할 VO
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static String fillTemplateMeta(MsgFtRequestVO vo) throws IOException, NoSuchAlgorithmException {
return fillMeta(vo.getTemplateImage(),
(name) -> vo.setTemplateImageName(name),
(ct) -> vo.setTemplateImageContentType(ct),
(size) -> vo.setTemplateImageSize(size),
(w) -> vo.setTemplateImageWidth(w),
(h) -> vo.setTemplateImageHeight(h),
(hash) -> vo.setTemplateImageSha256(hash),
(type) -> vo.setTemplateMsgType(type) // I/W 세팅
);
}
/**
* 서브(와이드) 이미지의 메타데이터를 MsgFtRequestVO에 채움
* - MMS 규격 검증(타입/10MB) 수행, I/W 판정 없음
* @param vo 메타데이터를 세팅할 VO
* @throws IOException
* @throws NoSuchAlgorithmException
*/
// public static String fillSubMeta(MsgFtRequestVO vo) throws IOException, NoSuchAlgorithmException {
// return fillMetaForSubImage(vo.getSubImage(),
// (name) -> vo.setSubImageName(name),
// (ct) -> vo.setSubImageContentType(ct),
// (size) -> vo.setSubImageSize(size),
// (w) -> vo.setSubImageWidth(w),
// (h) -> vo.setSubImageHeight(h),
// (hash) -> vo.setSubImageSha256(hash)
// );
// }
/**
* subImage(MMS) 전용 메타데이터 수집/검증 메서드
* - 검증: 파일 존재 여부, 용량(10MB), Content-Type(jpg/jpeg/png/gif)
* - 세팅: 파일명, Content-Type, 파일 크기, width/height, SHA-256
* - 비율/가로 500px 검증은 하지 않음 (권장 640×960은 안내 수준)
*
* @param f 업로드된 서브 이미지 파일
* @param nameC 파일명 setter
* @param ctC Content-Type setter
* @param sizeC 파일 크기 setter
* @param wC width setter
* @param hC height setter
* @param hashC SHA-256 setter
* @return 실패코드(String), 성공 null(OK)
* @throws IOException
* @throws NoSuchAlgorithmException
*/
private static String fillMetaForSubImage(
MultipartFile f,
Consumer<String> nameC,
Consumer<String> ctC,
Consumer<Long> sizeC,
Consumer<Integer> wC,
Consumer<Integer> hC,
Consumer<String> hashC
) throws IOException, NoSuchAlgorithmException {
// 1) 파일 존재/ 파일 체크
if (f == null || f.isEmpty()) return FT_E_EMPTY;
// 2) 용량(10MB) 제한
if (f.getSize() > 10L * 1024 * 1024) return FT_E_SIZE_GT_10MB;
// 3) 허용 Content-Type 확인 (jpg/jpeg/png/gif)
String ct = f.getContentType();
if (!isAllowedSubCt(ct)) return FT_E_CONTENT_TYPE;
// 4) 기본 메타 세팅
nameC.accept(f.getOriginalFilename());
ctC.accept(ct);
sizeC.accept(f.getSize());
// 5) 이미지 읽기 width/height 세팅 (비율 제한 없음)
int w = 0, h = 0;
try (InputStream is = f.getInputStream()) {
BufferedImage img = ImageIO.read(is);
if (img == null) return FT_E_IMG_READ_FAIL;
w = img.getWidth();
h = img.getHeight();
wC.accept(w);
hC.accept(h);
} catch (Exception e) {
return FT_E_IMG_READ_FAIL;
}
// sha-256
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(f.getBytes());
hashC.accept(bytesToHex(md.digest()));
// 권장 사이즈(640×960) 안내사항이므로 실패코드 미반환
return OK;
}
/**
* 공통 메타데이터 채우기 유틸
* - 파일명, Content-Type, 파일 크기
* - 이미지 /높이
* - 파일 SHA-256 해시
* - 가로/세로 비율 기반의 메시지 타입(I/W) 판정
*
* @param f MultipartFile (이미지 파일)
* @param nameC 파일명 setter
* @param ctC ContentType setter
* @param sizeC 파일 크기 setter
* @param wC width setter
* @param hC height setter
* @param hashC SHA-256 setter
* @param typeC 이미지 타입(I/W) setter
* @return 실패코드(String), 성공 null 반환
*/
private static String fillMeta(MultipartFile f,
Consumer<String> nameC,
Consumer<String> ctC,
Consumer<Long> sizeC,
Consumer<Integer> wC,
Consumer<Integer> hC,
Consumer<String> hashC,
Consumer<String> typeC
) throws IOException, NoSuchAlgorithmException {
if (f == null || f.isEmpty()) return FT_E_EMPTY;
// 용량/콘텐츠타입 1차 검증
if (f.getSize() > 5L * 1024 * 1024) return FT_E_SIZE_GT_5MB;
String ct = f.getContentType();
log.info(" + ct :: [{}]", ct);
// MIME 타입이 application/octet-stream이면 파일명으로 추정
if ("application/octet-stream".equalsIgnoreCase(ct)) {
String filename = f.getOriginalFilename();
if (filename != null) {
String ext = filename.toLowerCase();
if (ext.endsWith(".jpg") || ext.endsWith(".jpeg")) {
ct = "image/jpeg";
} else if (ext.endsWith(".png")) {
ct = "image/png";
}
}
}
if (ct == null || !(ct.equalsIgnoreCase("image/jpeg")
|| ct.equalsIgnoreCase("image/png"))) {
return FT_E_CONTENT_TYPE;
}
nameC.accept(f.getOriginalFilename());
ctC.accept(ct);
sizeC.accept(f.getSize());
int w = 0, h = 0;
try (InputStream is = f.getInputStream()) {
BufferedImage img = ImageIO.read(is);
if (img == null) return FT_E_IMG_READ_FAIL;
w = img.getWidth();
h = img.getHeight();
wC.accept(w);
hC.accept(h);
} catch (Exception e) {
return FT_E_IMG_READ_FAIL;
}
// sha-256
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(f.getBytes());
hashC.accept(bytesToHex(md.digest()));
// 규칙 검증 + 타입(I/W) 판정
String typeOrCode = calcFtMsgTypeOrCode(w, h);
if (typeOrCode.length() == 1) { // "I" or "W"
typeC.accept(typeOrCode);
return OK;
}
return typeOrCode; // 실패코드 리턴
}
/**
* byte[] 16진수 문자열 변환
* @param bytes SHA-256 결과 바이트 배열
* @return Hex 문자열
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
/**
* 가로/세로 크기를 기반으로 이미지 타입 판정
* - < 500px 오류코드 반환
* - 비율이 2:1( 2.0) ±10% "I"
* - 비율이 4:3( 1.333) ±10% "W"
* - 오류코드 반환
*
* @param width 이미지 가로
* @param height 이미지 세로 높이
* @return "I" 또는 "W" / 실패코드
*/
public static String calcFtMsgTypeOrCode(int width, int height) {
if (width < 500 || height <= 0) return FT_E_WIDTH_LT_500;
double r = (double) width / (double) height;
// 2:1 ~= 2.0, 4:3 ~= 1.333... (±10%)
boolean isI = (r >= 1.8 && r <= 2.2);
boolean isW = (r >= 1.28 && r <= 1.36);
if (isI) return "I";
if (isW) return "W";
return FT_E_RATIO_OUT_OF_RANGE;
}
/**
* MultipartFile 존재 여부 헬퍼
* @param f 업로드 파일
* @return 파일이 null이 아니고 비어있지 않으면 true
*/
public static boolean hasFile(MultipartFile f){ return f != null && !f.isEmpty(); }
/**
* 서브(MMS) 허용 Content-Type 검사
* 허용: image/jpeg, image/jpg, image/png, image/gif
* @param ct 요청의 Content-Type
* @return 허용 타입이면 true
*/
private static boolean isAllowedSubCt(String ct) {
return ct != null && (
ct.equalsIgnoreCase("image/jpeg") ||
ct.equalsIgnoreCase("image/jpg") || // 일부 클라이언트 대비
ct.equalsIgnoreCase("image/png") ||
ct.equalsIgnoreCase("image/gif")
);
}
}

View File

@ -1,14 +1,14 @@
package com.itn.mjonApi.mjon.api.inqry.mapper;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.MjonResponseVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @packageName : com.itn.mjonApi.mjon.api.inqry.service.mapper
* @packageName : com.itn.mjonApi.mjon.api.msg.inqry.service.mapper
* @fileName : PriceMapper.java
* @author : JunHo Lee
* @date : 2023.05.15

View File

@ -1,11 +1,11 @@
package com.itn.mjonApi.mjon.api.inqry.mapper;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
/**
* @packageName : com.itn.mjonApi.mjon.api.inqry.service.mapper
* @packageName : com.itn.mjonApi.mjon.api.msg.inqry.service.mapper
* @fileName : PriceMapper.java
* @author : JunHo Lee
* @date : 2023.05.15

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.inqry.service.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.inqry.service.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.*;

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.inqry.service.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.inqry.service.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.*;

View File

@ -1,14 +1,9 @@
package com.itn.mjonApi.mjon.api.inqry.mapper.domain;
import java.time.LocalDateTime;
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.*;
import org.springframework.http.HttpStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Setter
@Getter
@ -27,13 +22,23 @@ public class PriceResponse {
private double shortPrice; // 단문 이용단가
private double longPrice; // 장문 이용단가
private double picturePrice; // 그림 이용단가
private double kakaoAtPrice; // 그림 이용단가
private double kakaoFtPrice; // 그림 이용단가
private double kakaoFtImgPrice; // 그림 이용단가
private double kakaoFtWideImgPrice; // 그림 이용단가
private double mberMoney; // 잔액
private int shortSendPsbltEa; // 단문 발송 가능건
private int longSendPsbltEa; // 장문 발송 가능건
private int pictureSendPsbltEa; // 그림 발송 가능건
private int kakaoAtSendPsbltEa; // 그림 발송 가능건
private int kakaoFtSendPsbltEa; // 그림 발송 가능건
private int kakaoFtImgSendPsbltEa; // 그림 발송 가능건
private int kakaoFtWideImgSendPsbltEa; // 그림 발송 가능건
/*
* 200-OK : 정상접속
* 401-Unauthorized : 인증실패

View File

@ -0,0 +1,39 @@
package com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain;
import lombok.*;
import java.io.Serializable;
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PriceVO implements Serializable{
private static final long serialVersionUID = -7865729705175845268L;
private String mberId; // 사용자 ID
private double shortPrice; // 단문 이용단가
private double longPrice; // 장문 이용단가
private double picturePrice; // 그림 이용단가
private double kakaoAtPrice; // 알림톡 이용단가
private double kakaoFtPrice; // 친구톡 이용단가
private double kakaoFtImgPrice; // 친구톡 그림 이용단가
private double kakaoFtWideImgPrice; // 친구톡 와이드 그림 이용단가
private double mberMoney; // 잔액
private int shortSendPsbltEa; // 단문 발송 가능건
private int longSendPsbltEa; // 장문 발송 가능건
private int pictureSendPsbltEa; // 그림 발송 가능건
private int kakaoAtSendPsbltEa; // 알림톡 발송 가능건
private int kakaoFtSendPsbltEa; // 친구톡 발송 가능건
private int kakaoFtImgSendPsbltEa; // 친구톡 그림 발송 가능건
private int kakaoFtWideImgSendPsbltEa; // 친구톡 와이드 그림 발송 가능건
}

View File

@ -1,8 +1,8 @@
package com.itn.mjonApi.mjon.api.inqry.service;
package com.itn.mjonApi.mjon.api.msg.inqry.service;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryVO;
public interface HstryService {

View File

@ -1,9 +1,9 @@
package com.itn.mjonApi.mjon.api.inqry.service;
package com.itn.mjonApi.mjon.api.msg.inqry.service;
import com.itn.mjonApi.cmn.msg.RestResponse;
/**
* @packageName : com.itn.mjonApi.mjon.api.inqry.service
* @packageName : com.itn.mjonApi.mjon.api.msg.inqry.service
* @fileName : PriceService.java
* @author : JunHo Lee
* @date : 2023.05.15

View File

@ -1,15 +1,15 @@
package com.itn.mjonApi.mjon.api.inqry.service.impl;
package com.itn.mjonApi.mjon.api.msg.inqry.service.impl;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.mjon.api.inqry.mapper.HstryMapper;
import com.itn.mjonApi.mjon.api.inqry.service.HstryService;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryResponse;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.mjon.api.send.mapper.SendMapper;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.HstryMapper;
import com.itn.mjonApi.mjon.api.msg.inqry.service.HstryService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryResponse;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.SendMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.StringUtils;
@ -28,9 +28,7 @@ public class HstryServiceImpl implements HstryService {
@Autowired
HstryMapper hstryMapper;
@Autowired
SendMapper sendMapper;
@Autowired
public HstryServiceImpl(ApiService<Response> apiService) {

View File

@ -1,13 +1,13 @@
package com.itn.mjonApi.mjon.api.inqry.service.impl;
package com.itn.mjonApi.mjon.api.msg.inqry.service.impl;
import com.itn.mjonApi.cmn.model.Price;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.cmn.msg.StatMsg;
import com.itn.mjonApi.mjon.api.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.inqry.mapper.domain.PriceResponse;
import com.itn.mjonApi.mjon.api.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.inqry.service.PriceService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.PriceResponse;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.msg.inqry.service.PriceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -16,7 +16,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @packageName : com.itn.mjonApi.mjon.api.inqry.service.impl
* @packageName : com.itn.mjonApi.mjon.api.msg.inqry.service.impl
* @fileName : PriceServiceImpl.java
* @author : JunHo Lee
* @date : 2023.05.15
@ -59,10 +59,20 @@ public class PriceServiceImpl implements PriceService {
.shortPrice(priceVO.getShortPrice())
.longPrice(priceVO.getLongPrice())
.picturePrice(priceVO.getPicturePrice())
.kakaoAtPrice(priceVO.getKakaoAtPrice())
.kakaoFtPrice(priceVO.getKakaoFtPrice())
.kakaoFtImgPrice(priceVO.getKakaoFtImgPrice())
.kakaoFtWideImgPrice(priceVO.getKakaoFtWideImgPrice())
//3. 발송가능건수
.shortSendPsbltEa(priceVO.getShortSendPsbltEa())
.longSendPsbltEa(priceVO.getLongSendPsbltEa())
.pictureSendPsbltEa(priceVO.getPictureSendPsbltEa())
.kakaoAtSendPsbltEa(priceVO.getKakaoAtSendPsbltEa())
.kakaoFtSendPsbltEa(priceVO.getKakaoFtSendPsbltEa())
.kakaoFtImgSendPsbltEa(priceVO.getKakaoFtImgSendPsbltEa())
.kakaoFtWideImgSendPsbltEa(priceVO.getKakaoFtWideImgSendPsbltEa())
.build();
} catch (Exception e) {

View File

@ -1,9 +1,9 @@
package com.itn.mjonApi.mjon.api.inqry.web;
package com.itn.mjonApi.mjon.api.msg.inqry.web;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.inqry.service.HstryService;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.HstryVO;
import com.itn.mjonApi.mjon.api.msg.inqry.service.HstryService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryDetailVO;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.HstryVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -24,7 +24,6 @@ import org.springframework.web.client.RestTemplate;
* 2023-02-15 hylee 최초 생성
*/
// 치환문자가 있으면 , => § 치환
@Slf4j
@RestController

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.inqry.web;
package com.itn.mjonApi.mjon.api.msg.inqry.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -6,11 +6,11 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.inqry.service.PriceService;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.msg.inqry.service.PriceService;
/**
* @packageName : com.itn.mjonApi.mjon.api.inqry.web
* @packageName : com.itn.mjonApi.mjon.api.msg.inqry.web
* @fileName : PriceRestController.java
* @author : JunHo Lee
* @date : 2023.05.15

View File

@ -1,10 +1,10 @@
package com.itn.mjonApi.mjon.api.send.mapper;
package com.itn.mjonApi.mjon.api.msg.send.mapper;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MsgRequestVO;
import org.apache.ibatis.annotations.Mapper;
/**
* packageName : com.itn.mjonApi.mjon.api.send.mapper.domain
* packageName : com.itn.mjonApi.mjon.api.msg.send.mapper.domain
* fileName : SendMapper
* author : hylee
* date : 2023-05-19

View File

@ -0,0 +1,44 @@
package com.itn.mjonApi.mjon.api.msg.send.mapper.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class MjonMsgSendVO{
/**
* @description : 수신자번호
*/
private String phone;
/**
* @description : [*이름*] - 치환문자
*/
private String name;
/**
* @description : [*1*] - 치환문자
*/
private String rep1;
/**
* @description : [*2*] - 치환문자
*/
private String rep2;
/**
* @description : [*3*] - 치환문자
*/
private String rep3;
/**
* @description : [*4*] - 치환문자
*/
private String rep4;
}

View File

@ -0,0 +1,53 @@
package com.itn.mjonApi.mjon.api.msg.send.mapper.domain;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*;
/**
* packageName : com.itn.mjonApi.cmn.msg
* fileName : mjonResponse
* author : hylee
* date : 2023-05-12
* description : 문자온 프로젝트에서 받은 리턴값
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-05-12 hylee 최초 생성
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true) // JSON에 있지만 VO에 없는 필드를 무시하고 무사히 역직렬화해
@ToString
public class MjonResponseVO {
private String result;
private String message;
private String resultSts; // 전송결과 갯수
private String resultBlockSts; // 수신거부 갯수
private String msgGroupId;
private String afterCash;
private String msgType;
private String statCode;
/**
*
* @param apiReturnNode
* @return ResponseEntity vo convert
* @throws JsonProcessingException
*/
public static MjonResponseVO getMjonResponse(JsonNode apiReturnNode) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.treeToValue(apiReturnNode, MjonResponseVO.class);
}
// public static MjonResponseVO getMjonResponse(ResponseEntity<String> stringResponseEntity) throws JsonProcessingException {
// ObjectMapper objectMapper = new ObjectMapper();
// return objectMapper.readValue(stringResponseEntity.getBody(), MjonResponseVO.class);
// }
}

View File

@ -1,11 +1,12 @@
package com.itn.mjonApi.mjon.api.send.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.send.mapper.domain;
import lombok.*;
import java.io.Serializable;
import java.util.List;
/**
* packageName : com.itn.mjonApi.mjon.api.send.mapper.domain
* packageName : com.itn.mjonApi.mjon.api.msg.send.mapper.domain
* fileName : MjonMsgVO
* author : hylee
* date : 2023-05-09
@ -18,6 +19,7 @@ import java.io.Serializable;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class MsgRequestVO implements Serializable {
/**
@ -33,6 +35,8 @@ public class MsgRequestVO implements Serializable {
private String smsTxt; // value = "SMS용 메시지본문", example = "문자 메세지 본문"
private String smsTxtArea;//문자 작성 화면 본문 내용
private String[] callToList; // value = "수신번호리스트", dataType = "[Ljava.lang.String;", example = "01011112222,01022223333"
private String callFrom; // value = "발신번호 :: 정책이 필요함", example = "01011112222"
@ -98,6 +102,10 @@ public class MsgRequestVO implements Serializable {
private String test_yn; // 테스트 여부
private String sendKind; // 문자종류
List<MjonMsgSendVO> mjonMsgSendVOList;
// private String msgId ;// '문자ID',
// private String userId ; // '문자온 일반회원ID',
@ -113,7 +121,6 @@ public class MsgRequestVO implements Serializable {
// private String rsltCode2; // '결과처리 상세코드',
// private String rsltNet; // '결과처리 통신사',
// private String subject; // 'MMS용 메시지제목',
// private String smsTxtArea;//문자 작성 화면 본문 내용
// private String msgPayCode; // '재전송 기능에 의한 최종전송콘텐트 종류 저장',
// private String contSeq; // COMMENT 'MMS의 콘텐츠 Key(MMS_CONTENTS_INFO의 CONT_SEQ)',
// private String msgTypeResend; // '재전송할 문자 타입. 값이 있으면 재전송. 없으면 전송',
@ -228,6 +235,14 @@ public class MsgRequestVO implements Serializable {
this.smsTxt = smsTxt;
}
public String getSmsTxtArea() {
return smsTxtArea;
}
public void setSmsTxtArea(String smsTxtArea) {
this.smsTxtArea = smsTxtArea;
}
public String[] getCallToList() {
return callToList;
}
@ -475,4 +490,20 @@ public class MsgRequestVO implements Serializable {
public void setTest_yn(String test_yn) {
this.test_yn = test_yn;
}
public String getSendKind() {
return sendKind;
}
public void setSendKind(String sendKind) {
this.sendKind = sendKind;
}
public List<MjonMsgSendVO> getMjonMsgSendVOList() {
return mjonMsgSendVOList;
}
public void setMjonMsgSendVOList(List<MjonMsgSendVO> mjonMsgSendVOList) {
this.mjonMsgSendVOList = mjonMsgSendVOList;
}
}

View File

@ -0,0 +1,106 @@
package com.itn.mjonApi.mjon.api.msg.send.mapper.domain;
import com.itn.mjonApi.cmn.domain.SendRequestCmnVO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* packageName : com.itn.mjonApi.mjon.api.msg.send.mapper.domain
* fileName : MjonMsgVO
* author : hylee
* date : 2023-05-23
* description : 문자 발송에 필요한 값들을 받는 vo
* 1건~500건 대량 문자 개인별로 발송
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2023-05-09 hylee 최초 생성
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MsgsRequestVO extends SendRequestCmnVO implements Serializable {
/**
* 값이 있는 경우
* 문자온 프로젝트에서 처리해줌
* null이면 에러
*/
private static final long serialVersionUID = 1L;
private String mberId; // 사용자 ID
private String accessKey; // Api Key
private String smsTxt; // SMS용 메시지본문
private String[] callToList; // 수신번호리스트
private String callFrom; // 발신번호 :: 정책이 필요함
private String eachPrice = "0"; // 전송문자 개별가격
private String sPrice = "0"; // 임시
private String totPrice = "0"; // 전송문자 토탈가격
private String fileCnt = "0"; // 첨부파일 갯수
private String msgType = "4"; // 메시지의 (4: SMS 전송, 5: URL 전송, 6: MMS전송, 7: BARCODE전송, 8: 카카오 알림톡 전송)
// ==== 단가 ====
private float smsPrice = 0; // sms 단가 null 이면 에러
private float mmsPrice = 0; // mms 단가 null 이면 에러
// private float kakaoAtPrice; // 카카오 알림톡 단가
// private float kakaoFtPrice; // 카카오 친구톡 단가
// private float kakaoFtImgPrice;// 카카오 이미지 단가
// private float kakaoFtWideImgPrice; // 카카오 와이드 이미지 단가
private String[] imgFilePath = new String[0]; // 그림 이미지 경로
private String spamStatus; // 스팸문자 유무 (Y/N) - 서비스단에서 처리
private String txtReplYn = "N"; // 변환문자 유무 (Y/N) - 서비스단에서 처리
// private String nameStr; // value = "치환 이름 리스트 |로 구분", example = "홍길동1|홍길동2|홍길동3"
// private String rep1Str; // value = "치환 문자1 리스트 |로 구분", example = ""
// private String rep2Str; // value = "치환 문자2 리스트 |로 구분", example = ""
// private String rep3Str; // value = "치환 문자3 리스트 |로 구분", example = ""
// private String rep4Str; // value = "치환 문자4 리스트 |로 구분", example = ""
// private String[] nameList= new String[0]; // value = "nameStr 을 |로 split 후 담는 변수", example = ""
// private String[] rep1List= new String[0]; // value = "rep1Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep2List= new String[0]; // value = "rep2Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep3List= new String[0]; // value = "rep3Str 을 |로 split 후 담는 변수", example = ""
// private String[] rep4List= new String[0]; // value = "rep4Str 을 |로 split 후 담는 변수", example = ""
private String reserveYn = "N"; // value = "예약 유무 (Y/N)", example = "N"
// 치환 있을 경우 사용
// private String shortMsgCnt; // value = "치환 후 단문 건수", example = ""
// private String longMsgCnt; // value = "치환 후 장문 건수", example = ""
// @ApiModelProperty(value = "문자 종류 일반:N, 광고:A, 선거:C", example = "N", hidden = true)
private String msgKind = "N"; // '문자 종류 일반:N, 광고:A, 선거:C',
private String test_yn; // 테스트 여부
}

View File

@ -1,4 +1,4 @@
package com.itn.mjonApi.mjon.api.send.mapper.domain;
package com.itn.mjonApi.mjon.api.msg.send.mapper.domain;
import com.itn.mjonApi.cmn.msg.StatMsg;
import lombok.*;
@ -67,13 +67,31 @@ public class SendSucRestResponse {
private static List<String> getMsgType(String msgType) {
List<String> result = new ArrayList<>();
// msgType이 null이거나 문자열인 경우 리스트 반환
if (msgType == null || msgType.trim().isEmpty()) {
return result;
}
if(msgType.indexOf(",") > 0)
{
result = Arrays.stream(msgType.split(","))
.map(s -> StatMsg.valueOf("msgType"+s).getMsg())
.filter(s -> s != null && !s.trim().isEmpty()) // 문자열 필터링
.map(s -> {
try {
return StatMsg.valueOf("msgType" + s.trim()).getMsg();
} catch (IllegalArgumentException e) {
log.warn("Unknown msgType: {}, skipping", s.trim());
return null;
}
})
.filter(s -> s != null) // null 제거
.collect(Collectors.toList());
}else{
result.add(StatMsg.valueOf("msgType"+ msgType).getMsg());
try {
result.add(StatMsg.valueOf("msgType" + msgType.trim()).getMsg());
} catch (IllegalArgumentException e) {
log.warn("Unknown msgType: {}, returning empty list", msgType.trim());
}
}
return result;
}
@ -85,31 +103,41 @@ public class SendSucRestResponse {
*/
public static SendSucRestResponse SendSuccessMsgsRestResponse(List<MjonResponseVO> mjonResponseVOList) {
mjonResponseVOList.forEach(t->log.info(t.toString()));
// 실패 카운트
int failCnt = (int) mjonResponseVOList.stream()
.filter(s->"fail".equals(s.getResult()))
.filter(s->!"OK".equals(s.getResult()))
.count();
// 성공 카운트
int successCnt = mjonResponseVOList.parallelStream()
.filter(s->!"fail".equals(s.getResult()))
.mapToInt(s -> Integer.parseInt(s.getResultSts()))
.sum();
int successCnt = (int) mjonResponseVOList.parallelStream()
.filter(s->"OK".equals(s.getResult()))
.count();
// 수신거부 카운트
int blockCnt = mjonResponseVOList.parallelStream()
.filter(s->!"fail".equals(s.getResult()))
.mapToInt(s -> Integer.parseInt(s.getResultBlockSts()))
.sum();
// int blockCnt = mjonResponseVOList.parallelStream()
// .filter(s->!"OK".equals(s.getResult()))
// .mapToInt(s -> Integer.parseInt(s.getResultBlockSts()))
// .sum();
// 성공한 메세지 그룹 아이디
List<String> msgGroupIdList = mjonResponseVOList.stream()
.filter(s->!"fail".equals(s.getResult()))
.filter(s->"OK".equals(s.getResult()))
.map(s -> s.getMsgGroupId())
.collect(Collectors.toList());
// 성공한 메세지 그룹 아이디
// 메세지 타입
List<String> msgTypeList = mjonResponseVOList.stream()
.map(s -> StatMsg.valueOf("msgType"+s.getMsgType()).getMsg())
.collect(Collectors.toList());
.filter(s -> s.getMsgType() != null && !s.getMsgType().trim().isEmpty())
.map(s -> {
try {
return StatMsg.valueOf("msgType" + s.getMsgType().trim()).getMsg();
} catch (IllegalArgumentException e) {
log.warn("Unknown msgType in list: {}, skipping", s.getMsgType().trim());
return null;
}
})
.filter(s -> s != null)
.collect(Collectors.toList());
@ -118,8 +146,8 @@ public class SendSucRestResponse {
.resultCode(StatMsg.valueOf("STAT_0").getCode()) // 성공 코드 0 - StatMsg 참고
.msgGroupIdList(msgGroupIdList) // 전송 메세지 그룹 ID
.successCnt(Integer.toString(successCnt)) // 성공 건수
.blockCnt(Integer.toString(blockCnt)) // 수신거부 건수
.failCnt(Integer.toString(failCnt)) // 수신거부 건수
// .blockCnt(Integer.toString(blockCnt)) // 수신거부 건수
.failCnt(Integer.toString(failCnt)) // 실패 건수
.msgTypeList(msgTypeList) // msgType List
.build();

View File

@ -0,0 +1,21 @@
package com.itn.mjonApi.mjon.api.msg.send.service;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MsgsRequestVO;
import java.util.Map;
public interface SendService {
// RestResponse sendMsgData(MsgRequestVO msgRequestVO) throws Exception;
RestResponse sendMsgData_advc(MsgRequestVO msgRequestVO) throws Exception;
// RestResponse sendMsgsData(MsgsRequestVO msgsRequestVO) throws Exception;
RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO) throws Exception;
RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO, Map<String, String> allParams) throws Exception;
}

View File

@ -0,0 +1,367 @@
package com.itn.mjonApi.mjon.api.msg.send.service.impl;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.SendMapper;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.*;
import com.itn.mjonApi.mjon.api.msg.send.service.SendService;
import com.itn.mjonApi.util.MunjaUtil;
import com.itn.mjonApi.util.TestDataUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.util.*;
@Slf4j
@Service
public class SendServiceImpl implements SendService {
private ApiService apiService;
@Autowired
SendMapper sendMapper;
@Autowired
PriceMapper priceMapper;
@Autowired
public SendServiceImpl(ApiService apiService) {
this.apiService = apiService;
}
private static final String replaseStrList = "[*이름*],[*1*],[*2*],[*3*],[*4*]";
@Override
public RestResponse sendMsgData_advc(MsgRequestVO msgRequestVO) throws Exception {
log.info(" :: sendMsgData_advc ::");
msgRequestVO.setSendKind("A");
if(StringUtils.isNotEmpty(msgRequestVO.getTest_yn())){
// YF => 실패 테스트 데이터
return TestDataUtil._getTestMsgReturnData(msgRequestVO.getTest_yn());
}
// step2.수신자 전화번호 정상 여부 체크(정상 번호에 대해서만 발송 가능)
// 1020
// 폰번호 확인 - -> 유효성 정규식
if(MunjaUtil.getCallToListChk(msgRequestVO.getCallToList())){
return new RestResponse(new FailRestResponse("STAT_1020",""));
}
// 치환 데이터 수신자 번호 VO 생성
msgRequestVO.setMjonMsgSendVOList(this.buildMsgSendVOList(msgRequestVO));
// sms 변수 변경
msgRequestVO.setSmsTxtArea(msgRequestVO.getSmsTxt());
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/mjon/msgdata/sendMsgDataAjax_advc.do"
, msgRequestVO
, String.class
);
// convertMjonDataToApiResponse => MjonResponseVO 데이터를 ApiResponse 데이터로 변환하는 메소드
log.info(" + munjaSendResponse :: [{}]", munjaSendResponse.toString());
if("OK".equals(munjaSendResponse.getResult())){ // 성공
return new RestResponse(SendSucRestResponse.convertMjonDataToApiResponse(munjaSendResponse));
}else{ // 실패
return new RestResponse(new FailRestResponse(munjaSendResponse.getStatCode(),""));
}
}
/**
* MsgRequestVO nameStr, rep1Str~rep4Str, callToList 기반으로
* 항목을 MjonMsgSendVO 리스트로 매핑한다.
*
* @param msgRequestVO 메시지 요청 VO
* @return 수신자별 메시지 전송 VO 리스트
*/
public List<MjonMsgSendVO> buildMsgSendVOList(MsgRequestVO msgRequestVO) {
// 수신자 이름 치환 문자들 파싱 (null-safe 처리)
String[] nameArr = Optional.ofNullable(msgRequestVO.getNameStr()).orElse("").split("\\|");
String[] rep1Arr = Optional.ofNullable(msgRequestVO.getRep1Str()).orElse("").split("\\|");
String[] rep2Arr = Optional.ofNullable(msgRequestVO.getRep2Str()).orElse("").split("\\|");
String[] rep3Arr = Optional.ofNullable(msgRequestVO.getRep3Str()).orElse("").split("\\|");
String[] rep4Arr = Optional.ofNullable(msgRequestVO.getRep4Str()).orElse("").split("\\|");
// 콤마(,) 기준으로 수신 번호 문자열을 파싱
String[] phoneArr = Optional.ofNullable(msgRequestVO.getCallToList())
.orElse(new String[0]);
List<MjonMsgSendVO> mjonMsgSendVOList = new ArrayList<>();
// 수신 번호 개수만큼 반복
for (int i = 0; i < phoneArr.length; i++) {
MjonMsgSendVO vo = new MjonMsgSendVO();
// 1. 수신 번호 세팅
vo.setPhone(phoneArr[i].trim());
// 2. 이름/치환문자 세팅
if (i < nameArr.length) vo.setName(nameArr[i].trim());
if (i < rep1Arr.length) vo.setRep1(rep1Arr[i].trim());
if (i < rep2Arr.length) vo.setRep2(rep2Arr[i].trim());
if (i < rep3Arr.length) vo.setRep3(rep3Arr[i].trim());
if (i < rep4Arr.length) vo.setRep4(rep4Arr[i].trim());
mjonMsgSendVOList.add(vo);
}
return mjonMsgSendVOList;
}
@Override
public RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO) throws Exception {
log.info(" :: sendMsgData_advc ::");
if(StringUtils.isNotEmpty(msgsRequestVO.getTest_yn())){
return TestDataUtil._getTestMsgsReturnData(msgsRequestVO.getTest_yn());
}
// msgsVO -> msgVO List로 변환
List<MsgRequestVO> msgRequestVOList = this.getDataCleaning_advc(msgsRequestVO);
msgRequestVOList.forEach(t->log.info(" + t.toString() :: [{}]", t.toString()));
log.info("msgRequestVOList :: [{}]", msgRequestVOList.size());
List<MjonResponseVO> mjonResponseVOList = new ArrayList<MjonResponseVO>();
for(MsgRequestVO aa : msgRequestVOList){
aa.setSmsTxtArea(aa.getSmsTxt());
aa.setSendKind("A");
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/mjon/msgdata/sendMsgDataAjax_advc.do"
, aa
, String.class
);
log.info("munjaSendResponse :: [{}]", munjaSendResponse.toString());
mjonResponseVOList.add(munjaSendResponse);
}
return new RestResponse(SendSucRestResponse.SendSuccessMsgsRestResponse(mjonResponseVOList));
}
@Override
public RestResponse sendMsgsData_advc(MsgsRequestVO msgsRequestVO, Map<String, String> allParams) throws Exception {
log.info(" :: sendMsgData_advc with Map params ::");
if(StringUtils.isNotEmpty(msgsRequestVO.getTest_yn())){
return TestDataUtil._getTestMsgsReturnData(msgsRequestVO.getTest_yn());
}
// Map 기반 동적 파라미터 처리
List<MsgRequestVO> msgRequestVOList = this.getDataCleaning_advc_withMap(msgsRequestVO, allParams);
log.info("msgRequestVOList :: [{}]", msgRequestVOList.size());
List<MjonResponseVO> mjonResponseVOList = new ArrayList<MjonResponseVO>();
for(MsgRequestVO aa : msgRequestVOList){
aa.setSmsTxtArea(aa.getSmsTxt());
aa.setSendKind("A");
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/mjon/msgdata/sendMsgDataAjax_advc.do"
, aa
, String.class
);
log.info("munjaSendResponse :: [{}]", munjaSendResponse.toString());
mjonResponseVOList.add(munjaSendResponse);
}
return new RestResponse(SendSucRestResponse.SendSuccessMsgsRestResponse(mjonResponseVOList));
}
private static RestResponse callToErrorReturnData(MsgRequestVO msgRequestVO) {
FailRestResponse stat1020 = new FailRestResponse("STAT_1020","");
String errorMsg = stat1020.getMsg();
errorMsg.replace("수신자", "수신자(" + msgRequestVO.getCallToList()[0] + ")");
stat1020.setMsg(errorMsg);
return new RestResponse(stat1020);
}
private static List<MsgRequestVO> getDataCleaning_advc(MsgsRequestVO msgsRequestVO) {
List<MsgRequestVO> msgRequestVOList = new ArrayList<>();
// Reflection으로 Object Field에 접근한다.
Field[] declaredFields = msgsRequestVO.getClass().getDeclaredFields();
String mberId = msgsRequestVO.getMberId(); // 사용자 ID
String accessKey = msgsRequestVO.getAccessKey(); // accessKey
String callFrom = msgsRequestVO.getCallFrom(); // 발신자 번호
String callTo = null; // 수신자 번호
List<MjonMsgSendVO> mjonMsgSendVOList = new ArrayList<>();
for (Field field : declaredFields) {
Object value = null;
// private Field일 경우 접근을 허용한다.
field.setAccessible(true);
try {
// Field Value를 참조한다.
value = field.get(msgsRequestVO);
} catch (IllegalAccessException e) {
log.info("Reflection Error. {}", e);
}
// nullPointException 방지
if(value != null)
{
log.info("field.getName() : [{}]", field.getName());
log.info("value.toString() : [{}]", value.toString());
/**
* 필드 이름으로 분기하여
* 각각에 맞는 위치에 값을 넣어준다.
*/
if(field.getName().startsWith("callTo")){ // 수신자 번호
callTo = value.toString();
}else if(field.getName().startsWith("smsTxt")){ // 문자 내용
// 값이 비여 있으면 다음 반복문으로 넘어간다.
if(StringUtils.isEmpty(value.toString())){
callTo = "";
continue;
}
MjonMsgSendVO vo = new MjonMsgSendVO();
vo.setPhone(callTo);
mjonMsgSendVOList.add(vo);
msgRequestVOList.add(
MsgRequestVO.builder()
.mberId(mberId)
.accessKey(accessKey)
.callFrom(callFrom)
.smsTxt(value.toString())
.mjonMsgSendVOList(mjonMsgSendVOList)
.build()
);
// 초기화
callTo = "";
}
}
}
return msgRequestVOList;
}
/**
* Map 기반 동적 파라미터 처리를 위한 새로운 메소드
* callTo_1, smsTxt_1, callTo_2, smsTxt_2 등의 동적 파라미터를 처리
*/
private List<MsgRequestVO> getDataCleaning_advc_withMap(MsgsRequestVO msgsRequestVO, Map<String, String> allParams) {
List<MsgRequestVO> msgRequestVOList = new ArrayList<>();
String mberId = msgsRequestVO.getMberId();
String accessKey = msgsRequestVO.getAccessKey();
String callFrom = msgsRequestVO.getCallFrom();
// 동적 파라미터를 순서대로 처리하기 위해 인덱스 기반으로 처리
Map<Integer, String> callToMap = new HashMap<>();
Map<Integer, String> smsTxtMap = new HashMap<>();
// 파라미터를 분석하여 인덱스별로 분류
for (Map.Entry<String, String> entry : allParams.entrySet()) {
String paramName = entry.getKey();
String paramValue = entry.getValue();
log.info("Processing param: [{}] = [{}]", paramName, paramValue);
if (paramName.startsWith("callTo_")) {
try {
int index = Integer.parseInt(paramName.substring(7)); // "callTo_" 이후 숫자
callToMap.put(index, paramValue);
} catch (NumberFormatException e) {
log.warn("Invalid callTo parameter format: [{}]", paramName);
}
} else if (paramName.startsWith("smsTxt_")) {
try {
int index = Integer.parseInt(paramName.substring(7)); // "smsTxt_" 이후 숫자
if (StringUtils.isNotEmpty(paramValue)) {
smsTxtMap.put(index, paramValue);
}
} catch (NumberFormatException e) {
log.warn("Invalid smsTxt parameter format: [{}]", paramName);
}
}
}
// 인덱스별로 메시지 요청 VO 생성
for (Integer index : smsTxtMap.keySet()) {
String callTo = callToMap.get(index);
String smsTxt = smsTxtMap.get(index);
if (StringUtils.isNotEmpty(callTo) && StringUtils.isNotEmpty(smsTxt)) {
List<MjonMsgSendVO> mjonMsgSendVOList = new ArrayList<>();
MjonMsgSendVO vo = new MjonMsgSendVO();
vo.setPhone(callTo);
mjonMsgSendVOList.add(vo);
msgRequestVOList.add(
MsgRequestVO.builder()
.mberId(mberId)
.accessKey(accessKey)
.callFrom(callFrom)
.smsTxt(smsTxt)
.reserveYn("N")
.mjonMsgSendVOList(mjonMsgSendVOList)
.build()
);
log.info("Created MsgRequestVO for index [{}]: callTo=[{}], smsTxt=[{}]",
index, callTo, smsTxt);
}
// else {
// log.warn("Skipping index [{}]: callTo=[{}], smsTxt=[{}]", index, callTo, smsTxt);
// }
}
msgRequestVOList.forEach(t->log.info("+ t.getReserveYn() :: [{}]", t.getReserveYn()));
return msgRequestVOList;
}
}

View File

@ -1,16 +1,18 @@
package com.itn.mjonApi.mjon.api.send.web;
package com.itn.mjonApi.mjon.api.msg.send.web;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
import com.itn.mjonApi.mjon.api.send.service.SendService;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.MsgsRequestVO;
import com.itn.mjonApi.mjon.api.msg.send.service.SendService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* packageName : com.itn.mjonApi.mjon.send.web
@ -24,18 +26,11 @@ import org.springframework.web.client.RestTemplate;
* 2023-02-15 hylee 최초 생성
*/
// 치환문자가 있으면 , => § 치환
@Slf4j
@RestController
public class SendRestController {
private final RestTemplate restTemplate;
public SendRestController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Autowired
private SendService sendService;
@ -49,20 +44,24 @@ public class SendRestController {
@CrossOrigin("*") // 모든 요청에 접근 허용
@PostMapping("/api/send/sendMsg")
public ResponseEntity<RestResponse> sendMsg(MsgRequestVO msgRequestVO) throws Exception {
return ResponseEntity.ok().body(sendService.sendMsgData(msgRequestVO));
return ResponseEntity.ok().body(sendService.sendMsgData_advc(msgRequestVO));
// return ResponseEntity.ok().body(sendService.sendMsgData(msgRequestVO));
}
/**
*
* @param msgsRequestVO
* @description [문자 발송] 다른 내용으로 여려명에게 보냄
* @param allParams
* @description [문자 발송] 다른 내용으로 여려명에게 보냄 - Map 기반 동적 파라미터 처리
* @return
* @throws Exception
*/
@CrossOrigin("*") // 모든 요청에 접근 허용
@PostMapping("/api/send/sendMsgs")
public ResponseEntity<RestResponse> sendMsgs(MsgsRequestVO msgsRequestVO) throws Exception {
return ResponseEntity.ok().body(sendService.sendMsgsData(msgsRequestVO));
public ResponseEntity<RestResponse> sendMsgs(MsgsRequestVO msgsRequestVO,
@RequestParam Map<String, String> allParams) throws Exception {
return ResponseEntity.ok().body(sendService.sendMsgsData_advc(msgsRequestVO, allParams));
// return ResponseEntity.ok().body(sendService.sendMsgsData(msgsRequestVO));
}

View File

@ -1,13 +0,0 @@
package com.itn.mjonApi.mjon.api.send.service;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
public interface SendService {
RestResponse sendMsgData(MsgRequestVO msgRequestVO) throws Exception;
RestResponse sendMsgsData(MsgsRequestVO msgsRequestVO) throws Exception;
}

View File

@ -1,625 +0,0 @@
package com.itn.mjonApi.mjon.api.send.service.impl;
import com.itn.mjonApi.cmn.apiServer.ApiService;
import com.itn.mjonApi.cmn.model.Price;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.cmn.msg.StatMsg;
import com.itn.mjonApi.mjon.api.inqry.mapper.PriceMapper;
import com.itn.mjonApi.mjon.api.inqry.mapper.domain.PriceVO;
import com.itn.mjonApi.mjon.api.send.mapper.SendMapper;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MjonResponseVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.SendSucRestResponse;
import com.itn.mjonApi.mjon.api.send.service.SendService;
import com.itn.mjonApi.util.MunjaUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
public class SendServiceImpl implements SendService {
private ApiService<Response> apiService;
@Autowired
SendMapper sendMapper;
@Autowired
PriceMapper priceMapper;
@Autowired
public SendServiceImpl(ApiService<Response> apiService) {
this.apiService = apiService;
}
private static final String replaseStrList = "[*이름*],[*1*],[*2*],[*3*],[*4*]";
@Override
public RestResponse sendMsgData(MsgRequestVO msgRequestVO) throws Exception {
if(StringUtils.isNotEmpty(msgRequestVO.getTest_yn())){
// YF => 실패 테스트 데이터
return this._getTestMsgReturnData(msgRequestVO.getTest_yn());
}
//sendMsg 문자 발송 체크 사항
//step1.발신자 전화번호 사용 가능 여부 체크(해당 사용자의 등록된 번호만 발송 가능)
// 1010
msgRequestVO.setCallFrom(MunjaUtil.removeCharactersWithRegex(msgRequestVO.getCallFrom()));
if(!sendMapper.findByCallFrom(msgRequestVO)){
return new RestResponse(new FailRestResponse("STAT_1010",""));
}
// step2.수신자 전화번호 정상 여부 체크(정상 번호에 대해서만 발송 가능)
// 1020
// 폰번호 확인 - -> 유효성 정규식
if(this.getCallToListChk(msgRequestVO)){
return new RestResponse(new FailRestResponse("STAT_1020",""));
}
//step3.문자 내용 정상 여부 확인 - 스미싱 문구는 발송 30분 지연으로 처리됨
// 1030 => 현재 사용안함
// 스팸체크 하는 부분
// apiService.postForEntity => restTemplate.postForEntity 호출 MjonResponseVO에 맞게 데이터 정제하는 메소드
MjonResponseVO spamChkEntity = apiService.postForEntity(
"/web/user/login/selectSpamTxtChkAjax.do"
, msgRequestVO
, String.class
);
// 스팸체크 결과값이 spams 이면 스팸문자로 처리
if("spams".equals(spamChkEntity.getResult())){
msgRequestVO.setSpamStatus("Y");
};
//step4.치환명 정상 여부 확인
// 1040
msgRequestVO.setTxtReplYn(this.getTxtReplYn(msgRequestVO));
// 치환데이터가 있을 경우
if("Y".equals(msgRequestVO.getTxtReplYn())){
//일괄변환 문자에 콤마(,) 들어가있으면 배열로 넘길때 문제가 발생하여 특수문자(§) 치환하여 넘겨주도록 한다.
msgRequestVO = this.getReplaceCommaToStrSymbol(msgRequestVO);
// 치환 단문 장문 개수 구하기
msgRequestVO = this.getLengthOfShortAndLongMsg(msgRequestVO);
}
PriceVO priceVO = Price.priceRefine(msgRequestVO.getMberId(), priceMapper);
// +""; => Double to String 같은 기능
msgRequestVO.setsPrice(priceVO.getShortPrice()+"");
msgRequestVO.setmPrice(priceVO.getLongPrice()+"");
msgRequestVO.setpPrice(priceVO.getPicturePrice()+"");
//문자열 길이 체크 해주기
// 문자 바이트 계산에 필요한 캐릭터 : 한글 2Byte로 계산
int FrBytes = getFrBytes(msgRequestVO);
// 단문 장문 단가과 타입 지정
if(FrBytes > 90){
msgRequestVO.setEachPrice(msgRequestVO.getsPrice());
msgRequestVO.setMsgType("6");
}else {
msgRequestVO.setEachPrice(msgRequestVO.getmPrice());
msgRequestVO.setMsgType("4");
}
// 문자 전송하는 부분
// apiService.postForEntity => restTemplate.postForEntity 호출 MjonResponseVO에 맞게 데이터 정제하는 메소드
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/user/login/sendMsgDataAjax.do"
, msgRequestVO
, String.class
);
log.info("munjaSendResponse : [{}]", munjaSendResponse.toString());
log.info("munjaSendResponse : [{}]", munjaSendResponse.getResult());
// convertMjonDataToApiResponse => MjonResponseVO 데이터를 ApiResponse 데이터로 변환하는 메소드
if(!munjaSendResponse.getResult().equals("fail")){ // 성공
return new RestResponse(SendSucRestResponse.convertMjonDataToApiResponse(munjaSendResponse));
}else{ // 실패
return new RestResponse(new FailRestResponse(StatMsg.randomErrorStatCode(),""));
}
//step5.발송일시 정상여부 확인
// 1050
//step6.문자 타입에 따른 비용 처리 가능 여부 확인
// 1060
}
private static int getFrBytes(MsgRequestVO msgRequestVO) throws UnsupportedEncodingException {
String smsCont = msgRequestVO.getSmsTxt().replace("\r\n", "\n");
int FrBytes = smsCont.getBytes("euc-kr").length;
return FrBytes;
}
private RestResponse _getTestMsgReturnData(String testYn)
{
// YF => 실패 테스트 데이터
if("YF".equals(testYn))
{
// 실패 코드 랜덤으로 리턴
return new RestResponse(new FailRestResponse(StatMsg.randomErrorStatCode(),"YF"));
}else{
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgGroupId("MSGGID_0000000000000") // 전송 메세지 그룹 ID
.successCnt("5") // 성공 건수
.blockCnt("2") // 수신거부 건수
.msgType("LMS")
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
private RestResponse _getTestMsgsReturnData(String testYn)
{
// YF => 실패 테스트 데이터
if("YF".equals(testYn))
{
return new RestResponse(new FailRestResponse(StatMsg.randomErrorStatCode(),"YF"));
}else{ // YS => 성공 테스트 데이터
List<String> gIdList = new ArrayList<>();
gIdList.add("MSGGID_0000000000000");
gIdList.add("MSGGID_0000000000001");
gIdList.add("MSGGID_0000000000002");
List<String> msgTypeList = new ArrayList<>();
msgTypeList.add("SMS");
msgTypeList.add("LMS");
msgTypeList.add("LMS");
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgGroupIdList(gIdList) // 전송 메세지 그룹 ID
.successCnt("2") // 성공 건수
.blockCnt("1") // 수신거부 건수
.msgTypeList(msgTypeList)
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
@Override
public RestResponse sendMsgsData(MsgsRequestVO msgsRequestVO) throws Exception {
if(StringUtils.isNotEmpty(msgsRequestVO.getTest_yn())){
return this._getTestMsgsReturnData(msgsRequestVO.getTest_yn());
}
// msgsVO -> msgVO List로 변환
List<MsgRequestVO> msgRequestVOList = this.getDataCleaning(msgsRequestVO);
//step1.발신자 전화번호 사용 가능 여부 체크(해당 사용자의 등록된 번호만 발송 가능)
// 1010
if(!sendMapper.findByCallFrom(msgRequestVOList.get(0))){
return new RestResponse(new FailRestResponse("STAT_1010",""));
}
// step2.수신자 전화번호 정상 여부 체크(정상 번호에 대해서만 발송 가능)
// 1020
// 폰번호 확인 - -> 유효성 정규식
for(MsgRequestVO msgRequestVO : msgRequestVOList){
if(this.getCallToListChk(msgRequestVO)){
// if(StringUtils.isNotEmpty(this.getCallToListChk(msgRequestVO))){
return this.callToErrorReturnData(msgRequestVO);
}
}
//사용자 잔액
// double mberMoney = priceMapper.selectMberMoney(mberId);
// 이용단가, 발송가능 건수
// PriceVO priceVO = this.price_refine(mberId, mberMoney, priceMapper);
PriceVO priceVO = Price.priceRefine(msgsRequestVO.getMberId(), priceMapper);
// +""; => Double to String 같은 기능
String sPrice = priceVO.getShortPrice()+"";
String mPrice = priceVO.getLongPrice()+"";
String pPrice = priceVO.getPicturePrice()+"";
List<MjonResponseVO> mjonResponseVOList = new ArrayList<MjonResponseVO>();
for(MsgRequestVO msgRequestVO : msgRequestVOList){
//문자열 길이 체크 해주기
// 문자 바이트 계산에 필요한 캐릭터 : 한글 2Byte로 계산
int FrBytes = getFrBytes(msgRequestVO);
// 단문 장문 단가과 타입 지정
if(FrBytes > 90){
msgRequestVO.setEachPrice(sPrice);
msgRequestVO.setMsgType("6");
}else {
msgRequestVO.setEachPrice(mPrice);
msgRequestVO.setMsgType("4");
}
// 단가 셋팅
msgRequestVO.setsPrice(sPrice); // 단문
msgRequestVO.setmPrice(mPrice); // 장문
msgRequestVO.setpPrice(pPrice); // 사진
//step3.문자 내용 정상 여부 확인 - 스미싱 문구는 발송 30분 지연으로 처리됨
// 1030 => 현재 사용안함
// 스팸체크 하는 부분
MjonResponseVO spamChkEntity = apiService.postForEntity(
"/web/user/login/selectSpamTxtChkAjax.do"
, msgRequestVO
, String.class
);
// 스팸체크 결과값이 spams 이면 스팸문자로 처리
if("spams".equals(spamChkEntity.getResult())){
msgRequestVO.setSpamStatus("Y");
};
// 문자 전송하는 부분
// apiService.postForEntity => restTemplate.postForEntity 호출 MjonResponseVO에 맞게 데이터 정제하는 메소드
MjonResponseVO munjaSendResponse = apiService.postForEntity(
"/web/user/login/sendMsgDataAjax.do"
, msgRequestVO
, String.class
);
//
mjonResponseVOList.add(munjaSendResponse);
}
return new RestResponse(SendSucRestResponse.SendSuccessMsgsRestResponse(mjonResponseVOList));
}
private static RestResponse callToErrorReturnData(MsgRequestVO msgRequestVO) {
FailRestResponse stat1020 = new FailRestResponse("STAT_1020","");
String errorMsg = stat1020.getMsg();
errorMsg.replace("수신자", "수신자(" + msgRequestVO.getCallToList()[0] + ")");
stat1020.setMsg(errorMsg);
return new RestResponse(stat1020);
}
/**
* @description 최대 1~100개의 수신번호와 메세지를 MsgRequestVO로 정재하는 메소드.
* @param msgsRequestVO
* @return
*/
private static List<MsgRequestVO> getDataCleaning(MsgsRequestVO msgsRequestVO) {
List<MsgRequestVO> msgRequestVOList = new ArrayList<>();
// Reflection으로 Object Field에 접근한다.
Field[] declaredFields = msgsRequestVO.getClass().getDeclaredFields();
String mberId = null; // 사용자 ID
String accessKey = null; // accessKey
String callFrom = null; // 발신자 번호
String callTo = null; // 수신자 번호
for (Field field : declaredFields) {
Object value = null;
// private Field일 경우 접근을 허용한다.
field.setAccessible(true);
try {
// Field Value를 참조한다.
value = field.get(msgsRequestVO);
} catch (IllegalAccessException e) {
log.info("Reflection Error. {}", e);
}
// nullPointException 방지
if(value != null)
{
/**
* 필드 이름으로 분기하여
* 각각에 맞는 위치에 값을 넣어준다.
*/
if("mberId".equals(field.getName())){ // 사용자 ID
mberId = value.toString();
}else if("accessKey".equals(field.getName())){ // accessKey
accessKey = value.toString();
}else if("callFrom".equals(field.getName())){ // 발신자 번호
callFrom = value.toString();
}else if(field.getName().startsWith("callTo")){ // 수신자 번호
callTo = value.toString();
}else if(field.getName().startsWith("smsTxt")){ // 문자 내용
// 값이 비여 있으면 다음 반복문으로 넘어간다.
if(StringUtils.isEmpty(value.toString())){
callTo = "";
continue;
}
msgRequestVOList.add(
MsgRequestVO.builder()
.mberId(mberId)
.accessKey(accessKey)
.callFrom(callFrom)
.callToList(new String[]{callTo})
.smsTxt(value.toString())
.eachPrice("0") // 디폴트
.sPrice("0") // 디폴트
.totPrice("0") // 디폴트
.fileCnt("0") // 디폴트
.msgType("4") // 디폴트
.smsPrice(0) // 디폴트
.mmsPrice(0) // 디폴트
.imgFilePath(new String[0]) // 디폴트
.txtReplYn("N") // 디폴트
.reserveYn("N") // 디폴트
.msgKind("N") // 디폴트
.build()
);
// 초기화
callTo = "";
}
}
}
return msgRequestVOList;
}
/**
* 치환 단문 장문 msg 개수 구하기
* @param msgRequestVO
* @return msgRequestVO
* @throws UnsupportedEncodingException
*/
private static MsgRequestVO getLengthOfShortAndLongMsg(MsgRequestVO msgRequestVO) throws UnsupportedEncodingException {
String charset = "euc-kr";
int totListCnt = msgRequestVO.getCallToList().length;
int shortMsgCnt=0; // 치환 단문 개수
int longMsgCnt=0; // 치환 장문 개수
for(int i=0; i < totListCnt; i ++) {
String smsTxt = msgRequestVO.getSmsTxt().replaceAll(String.valueOf((char)13), ""); //발송 문자 내용
String[] nameList = msgRequestVO.getNameList(); //치환 이름 리스트
String[] phone = msgRequestVO.getCallToList(); //수신자 휴대폰 번호
String[] rep1 = msgRequestVO.getRep1List(); //치환 문자1 리스트
String[] rep2 = msgRequestVO.getRep2List(); //치환 문자2 리스트
String[] rep3 = msgRequestVO.getRep3List(); //치환 문자3 리스트
String[] rep4 = msgRequestVO.getRep4List(); //치환 문자4 리스트
if (smsTxt.indexOf("[*이름*]") > -1) {
if(nameList.length > i && StringUtils.isNotEmpty(nameList[i])) {
smsTxt = smsTxt.replaceAll("\\[\\*이름\\*\\]", MunjaUtil.getString(nameList[i].replaceAll("§", ",")));
}else {
smsTxt = smsTxt.replaceAll("\\[\\*이름\\*\\]", "");
}
}
if (smsTxt.indexOf("[*1*]") > -1) {
if(rep1.length > i && StringUtils.isNotEmpty(rep1[i])) {
smsTxt = smsTxt.replaceAll("\\[\\*1\\*\\]", MunjaUtil.getString(rep1[i].replaceAll("§", ",")));
}else {
smsTxt = smsTxt.replaceAll("\\[\\*1\\*\\]", "");
}
}
if (smsTxt.indexOf("[*2*]") > -1) {
if(rep2.length > i && StringUtils.isNotEmpty(rep2[i])) {
smsTxt = smsTxt.replaceAll("\\[\\*2\\*\\]", MunjaUtil.getString(rep2[i].replaceAll("§", ",")));
}else {
smsTxt = smsTxt.replaceAll("\\[\\*2\\*\\]", "");
}
}
if (smsTxt.indexOf("[*3*]") > -1) {
if(rep3.length > i && StringUtils.isNotEmpty(rep3[i])) {
smsTxt = smsTxt.replaceAll("\\[\\*3\\*\\]", MunjaUtil.getString(rep3[i].replaceAll("§", ",")));
}else {
smsTxt = smsTxt.replaceAll("\\[\\*3\\*\\]", "");
}
}
if (smsTxt.indexOf("[*4*]") > -1) {
if(rep4.length > i && StringUtils.isNotEmpty(rep4[i])) {
smsTxt = smsTxt.replaceAll("\\[\\*4\\*\\]", MunjaUtil.getString(rep4[i].replaceAll("§", ",")));
}else {
smsTxt = smsTxt.replaceAll("\\[\\*4\\*\\]", "");
}
}
int bytes = smsTxt.getBytes(charset).length;
if(bytes > 90) {//장문문자 리스트 만들기
longMsgCnt++;
}else {//단문문자 리스트 만들기
shortMsgCnt++;
}
}
msgRequestVO.setLongMsgCnt(Integer.toString(longMsgCnt));
msgRequestVO.setShortMsgCnt(Integer.toString(shortMsgCnt));
return msgRequestVO;
}
/**
* 치환문자가 있으면 , => § 치환
* @param msgRequestVO
* @return
*/
private static MsgRequestVO getReplaceCommaToStrSymbol(MsgRequestVO msgRequestVO) {
AtomicInteger index = new AtomicInteger();
// 이름 배열
if(StringUtils.isNotEmpty(msgRequestVO.getNameStr()))
{
msgRequestVO.setNameList(msgRequestVO.getNameStr().split("\\|"));
String[] nameList = new String[msgRequestVO.getNameList().length];
Arrays.stream(msgRequestVO.getNameList()).forEach(name -> {
nameList[index.getAndIncrement()] = MunjaUtil.replaceCommaToStrSymbol(name);
});
msgRequestVO.setNameList(nameList);
}
// Rep1 배열
if(StringUtils.isNotEmpty(msgRequestVO.getRep1Str()))
{
index.set(0);
msgRequestVO.setRep1List(msgRequestVO.getRep1Str().split("\\|"));
String[] rep1List = new String[msgRequestVO.getRep1List().length];
Arrays.stream(msgRequestVO.getRep1List()).forEach(str -> {
rep1List[index.getAndIncrement()] = MunjaUtil.replaceCommaToStrSymbol(str);
});
msgRequestVO.setRep1List(rep1List);
}
// Rep2 배열
if(StringUtils.isNotEmpty(msgRequestVO.getRep2Str()))
{
index.set(0);
msgRequestVO.setRep2List(msgRequestVO.getRep2Str().split("\\|"));
String[] rep2List = new String[msgRequestVO.getRep2List().length];
Arrays.stream(msgRequestVO.getRep2List()).forEach(str -> {
rep2List[index.getAndIncrement()] = MunjaUtil.replaceCommaToStrSymbol(str);
});
msgRequestVO.setRep2List(rep2List);
}
// Rep3 배열
if(StringUtils.isNotEmpty(msgRequestVO.getRep3Str()))
{
index.set(0);
msgRequestVO.setRep3List(msgRequestVO.getRep3Str().split("\\|"));
String[] rep3List = new String[msgRequestVO.getRep3List().length];
Arrays.stream(msgRequestVO.getRep3List()).forEach(str -> {
rep3List[index.getAndIncrement()] = MunjaUtil.replaceCommaToStrSymbol(str);
});
msgRequestVO.setRep3List(rep3List);
}
// Rep4 배열
if(StringUtils.isNotEmpty(msgRequestVO.getRep4Str()))
{
index.set(0);
msgRequestVO.setRep4List(msgRequestVO.getRep4Str().split("\\|"));
String[] rep4List = new String[msgRequestVO.getRep4List().length];
Arrays.stream(msgRequestVO.getRep4List()).forEach(str -> {
rep4List[index.getAndIncrement()] = MunjaUtil.replaceCommaToStrSymbol(str);
});
msgRequestVO.setRep4List(rep4List);
}
return msgRequestVO;
}
/**
* 치환 문자 여부 확인
* @param msgRequestVO
* @return
*/
private static String getTxtReplYn(MsgRequestVO msgRequestVO) {
int callLen = msgRequestVO.getCallToList().length;
// 치환 데이터 확인
Arrays.stream(replaseStrList.split(",")).forEach(
str -> {
if(msgRequestVO.getSmsTxt().indexOf(str) > -1){
msgRequestVO.setTxtReplYn("Y");
}
}
);
return msgRequestVO.getTxtReplYn();
}
/**
* 수신자 목록 번호 검증
* message가 없으면 정상
* @param msgRequestVO
* @return String
*/
private static Boolean getCallToListChk(MsgRequestVO msgRequestVO) {
Boolean returnData = false;
for(String callTo : msgRequestVO.getCallToList()){
/*
if(!MunjaUtil.checkPhoneNumberEmpty(callTo)){
message = "수신 목록에 핸드폰 번호가 없는 항목이 있습니다."; break;};
if(!MunjaUtil.validatePNumWithRegex(callTo)){
message = "휴대폰 번호가 올바르지 않습니다. : " + callTo; break;};
*/
if(!MunjaUtil.validatePNumWithRegex(callTo) // 비여있는지 체크
|| !MunjaUtil.validatePNumWithRegex(callTo) // 정규식으로 번호 체크
)
{
returnData = true;
break;
};
}
return returnData;
}
/**
* MjonResponseVO -> 변환 -> SendFailRestResponse
* @param mjonResponseVO
* @return
*/
public static String convertMjonDataToApiResponse(MjonResponseVO mjonResponseVO) {
String result = mjonResponseVO.getResult();
log.info("convertMjonDataToApiResponse : [{}]", result);
String message = mjonResponseVO.getMessage();
log.info("convertMjonDataToApiResponse : [{}]", message);
String statCode = "";
switch (result) {
case "statusFail" : statCode = "1070"; // 회원 정지
break;
case "smsLengFail" : statCode = "1080"; // 문자 길이 초과
break;
case "fail" : // 문자온 프로젝트에서 result가 fail로 다양한 에러가 리턴하여 분기처리함
if(message.indexOf("문자 치환 후 전송 문자 길이를 초과하였습니다.")>-1)
statCode = "1050"; // 치환 문자 길이 초과
else if(message.indexOf("치환문자 데이터가 없습니다")>-1)
statCode = "1040"; // 치환 데이터 오류
else if(message.indexOf("치환 후 전송 문자 길이를 초과")>-1)
statCode = "1050"; // 치환 데이터 오류
else
statCode = "1099"; // 기타 시스템 오류
break;
default: statCode = "1099"; // 기타 시스템 오류
break;
}
return "STAT_"+statCode;
}
}

View File

@ -3,10 +3,6 @@ package com.itn.mjonApi.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.mjon.api.access.mapper.domain.AccessKeyVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgRequestVO;
import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
/**
* packageName : com.itn.mjonApi.util
@ -21,45 +17,10 @@ import com.itn.mjonApi.mjon.api.send.mapper.domain.MsgsRequestVO;
*/
public class ApiObjectUtil {
private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
/**
* @description : VO를 json으로 변환
* @param restResponse
* @return String
* @throws JsonProcessingException
*/
public static String getRestResponseToJsonString(RestResponse restResponse) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// .registerModule(new JavaTimeModule()) : LocalDateTime을 json으로 변환하기 위함
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(restResponse);
public static String toJson(Object obj) throws JsonProcessingException {
if (obj == null) return null;
return objectMapper.writeValueAsString(obj);
}
public static String getMsgsRequestVOToJsonString(MsgsRequestVO msgsRequestVO) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// .registerModule(new JavaTimeModule()) : LocalDateTime을 json으로 변환하기 위함
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(msgsRequestVO);
}
public static String getMsgRequestVOToJsonString(MsgRequestVO msgRequestVO) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// .registerModule(new JavaTimeModule()) : LocalDateTime을 json으로 변환하기 위함
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(msgRequestVO);
}
/**
* @description : VO를 json으로 변환
* @param accessKeyVO
* @return String
* @throws JsonProcessingException
*/
public static String getAccessKeyVOToJsonString(AccessKeyVO accessKeyVO) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// .registerModule(new JavaTimeModule()) : LocalDateTime을 json으로 변환하기 위함
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(accessKeyVO);
}
}

View File

@ -1,5 +1,11 @@
package com.itn.mjonApi.util;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.MsgAtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.at.send.mapper.domain.VarAtListMapVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.MsgFtRequestVO;
import com.itn.mjonApi.mjon.api.kakao.ft.send.mapper.domain.VarFtListMapVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
/**
@ -13,19 +19,10 @@ import org.apache.commons.lang3.StringUtils;
* -----------------------------------------------------------
* 2023-05-17 hylee 최초 생성
*/
@Slf4j
public class MunjaUtil {
/**
* 폰번호 유효성 검사
* @param callTo
* @return
*/
public static Boolean validatePNumWithRegex(String callTo){
// 핸드폰 정규식
String regex = "^01(?:0|1|[6-9])(?:\\d{3}|\\d{4})\\d{4}$";
return callTo.matches(regex) ? true : false;
}
/**
* 폰번호 빈값 검사
* @param str
@ -49,7 +46,7 @@ public class MunjaUtil {
* 파라미터를 String 타입으로 가져옵니다.<br>
* - null일 경우 문자열을 가져옵니다.<br>
* Get String(if object is null, return empty string).
* @param Object
* @param obj
* @return String
*/
public static String getString(Object obj) {
@ -59,5 +56,128 @@ public class MunjaUtil {
return String.valueOf(obj);
}
/**
* 수신자 목록 번호 검증
* message가 없으면 정상
* @param callToList
* @return String
*/
public static Boolean getCallToListChk(String[] callToList) {
Boolean returnData = false;
if (ArrayUtils.isEmpty(callToList)) {
return true;
}
for(String callTo : callToList){
if(!MunjaUtil.checkPhoneNumberEmpty(callTo) // 비여있는지 체크
|| !MunjaUtil.validatePNumWithRegex(callTo) // 정규식으로 번호 체크
)
{
return true;
};
}
return returnData;
}
public static Boolean getCallToChk(String callTo) {
Boolean returnData = false;
if(!MunjaUtil.checkPhoneNumberEmpty(callTo) // 비여있는지 체크
|| !MunjaUtil.validatePNumWithRegex(callTo) // 정규식으로 번호 체크
)
{
return true;
};
return returnData;
}
// 기존 정규식 검사 메서드 (변경 없음)
public static boolean validatePNumWithRegex(String pNum) {
return pNum != null && pNum.matches("^\\d{10,11}$");
}
/**
* VarListMapVO의 필드들을 검증
*
* @param vo 검증할 VarListMapVO 객체
* @param msgAtRequestVO
* @return 검증 실패 오류 코드, 성공 null
*/
public static String kakaoAtValidate(VarAtListMapVO vo, MsgAtRequestVO msgAtRequestVO) {
String subMsgSendYn = msgAtRequestVO.getSubMsgSendYn();
Boolean hasTemplateTitle = msgAtRequestVO.getHasTemplateTitle();
// 수신번호 검증
String callTo = vo.getCallToList();
if (MunjaUtil.getCallToChk(callTo)) {
return "STAT_1020"; // 수신자 전화번호 오류
}
// 본문 데이터 검증
String templateContent = vo.getTemplateContent();
if (StringUtils.isEmpty(templateContent)) {
return "STAT_2040"; // 본문 데이터 오류
}
// 템플릿에 타이틀이 있으면 확인
if (hasTemplateTitle && StringUtils.isEmpty(vo.getTemplateTitle())) {
return "STAT_2041"; // 타이틀 데이터 오류
}
// 대체문자 검증 (대체문자 발송이 활성화된 경우에만)
if ("Y".equals(subMsgSendYn)) {
String subMsgTxt = vo.getSubMsgTxt();
if (StringUtils.isEmpty(subMsgTxt)) {
return "STAT_2042"; // 대체문자 데이터 오류
}
}
// 모든 검증 통과
return null;
}
public static String kakaoFtValidate(VarFtListMapVO vo, MsgFtRequestVO msgFtRequestVO) {
log.info(" vo.toString() [{}]", vo.toString());
String ok = null;
// 수신번호 검증
String callTo = vo.getPhone();
if (MunjaUtil.getCallToChk(callTo)) {
return "STAT_1020"; // 수신자 전화번호 오류
}
// 본문 데이터 검증
String smsTxt = vo.getTemplateContent();
if (StringUtils.isEmpty(smsTxt)) {
return "STAT_2040"; // 본문 데이터 오류
}
// 대체문자 검증 (대체문자 발송이 활성화된 경우에만)
if ("Y".equals(msgFtRequestVO.getSubMsgSendYn())) {
String subMsgTxt = vo.getSubMsgTxt();
if (StringUtils.isEmpty(subMsgTxt)) {
return "STAT_2042"; // 대체문자 데이터 오류
}
String callFrom = msgFtRequestVO.getCallFrom();
if (StringUtils.isEmpty(callFrom)) {
return "STAT_2043"; // 대체문자 발송 발신번호 필요
}
}
// 모든 검증 통과
return ok;
}
}

View File

@ -0,0 +1,299 @@
package com.itn.mjonApi.util;
import com.itn.mjonApi.cmn.domain.biz.template.detail.TemplateComments;
import com.itn.mjonApi.cmn.domain.biz.template.detail.TemplateDetail;
import com.itn.mjonApi.cmn.domain.biz.template.list.TemplateInfo;
import com.itn.mjonApi.cmn.msg.FailRestResponse;
import com.itn.mjonApi.cmn.msg.RestResponse;
import com.itn.mjonApi.cmn.msg.StatMsg;
import com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO;
import com.itn.mjonApi.mjon.api.msg.send.mapper.domain.SendSucRestResponse;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 카카오 조회 API 테스트 데이터 유틸리티 클래스
* 기존 SMS/LMS 테스트 패턴을 재사용하여 일관성 유지
*
* @author system
* @date 2025-01-09
*/
public class TestDataUtil {
private static FailRestResponse randomFail(){
return new FailRestResponse(StatMsg.randomErrorStatCode(),"YF");
}
public static RestResponse _getTestMsgReturnData(String testYn)
{
// YF => 실패 테스트 데이터
if("YF".equals(testYn))
{
// 실패 코드 랜덤으로 리턴
return new RestResponse(randomFail());
}else{
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgGroupId("MSGGID_0000000000000") // 전송 메세지 그룹 ID
.successCnt("5") // 성공 건수
.blockCnt("2") // 수신거부 건수
.msgType("LMS")
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
/**
* 다중문자발송
*
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse _getTestMsgsReturnData(String testYn)
{
// YF => 실패 테스트 데이터
if("YF".equals(testYn))
{
return new RestResponse(randomFail());
}else{ // YS => 성공 테스트 데이터
List<String> gIdList = new ArrayList<>();
gIdList.add("MSGGID_0000000000000");
gIdList.add("MSGGID_0000000000001");
gIdList.add("MSGGID_0000000000002");
List<String> msgTypeList = new ArrayList<>();
msgTypeList.add("SMS");
msgTypeList.add("LMS");
msgTypeList.add("LMS");
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgGroupIdList(gIdList) // 전송 메세지 그룹 ID
.successCnt("2") // 성공 건수
.blockCnt("1") // 수신거부 건수
.msgTypeList(msgTypeList)
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
/**
* 알림톡 발송 테스트 데이터 생성
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse getTestAtSendReturnData(String testYn) {
if("YF".equals(testYn)) {
// 실패 테스트 데이터
return new RestResponse(randomFail());
} else {
// 성공 테스트: 알림톡 발송 성공 응답
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgGroupId("MSGGID_AT_" + System.currentTimeMillis()) // 알림톡 전용 그룹 ID
.successCnt("2") // 성공 건수
.blockCnt("0") // 수신거부 건수
.msgType("AT") // 알림톡 타입
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
/**
* 친구톡 발송 테스트 데이터 생성
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse getTestFtSendReturnData(String testYn) {
if("YF".equals(testYn)) {
// 실패 테스트 데이터
return new RestResponse(randomFail());
} else {
// 친구톡용 msgGroupIdList 생성
List<String> msgGroupIdList = new ArrayList<>();
long timestamp = System.currentTimeMillis();
msgGroupIdList.add("MSGGID_" + timestamp);
msgGroupIdList.add("MSGGID_" + timestamp);
// 성공 테스트: 친구톡 발송 성공 응답
return new RestResponse(
SendSucRestResponse.builder()
.resultCode("0")
.msgType("FT") // 친구톡 타입
.msgGroupIdList(msgGroupIdList) // 친구톡은 msgGroupIdList 사용
.successCnt("2") // 성공 건수
.failCnt("0")
.test_yn("YS")
.build()
);
}
}
/**
* 채널 ID 조회 테스트 데이터 생성
*
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse getChnlIdTestData(String testYn) {
if ("YF".equals(testYn)) {
return new RestResponse(randomFail());
} else {
Date now = new Date();
// 원하는 포맷
String nowDate = DateFormatUtils.format(now, "yyyy-MM-dd HH:mm:ss");
// 성공 테스트: 모킹된 채널 정보 반환
List<MjKakaoProfileInfoVO> mockData = new ArrayList<>();
MjKakaoProfileInfoVO channel1 = new MjKakaoProfileInfoVO();
channel1.setSenderKey("test_sender_key_001");
channel1.setPhoneNumber("02-1234-0000");
channel1.setYellowId("@test_channel_001");
channel1.setFrstRegisterId("test_id_one");
channel1.setFrstRegistPnttm(nowDate);
MjKakaoProfileInfoVO channel2 = new MjKakaoProfileInfoVO();
channel2.setSenderKey("test_sender_key_002");
channel2.setPhoneNumber("02-1234-0001");
channel2.setYellowId("@test_channel_002");
channel2.setFrstRegisterId("test_id_tow");
channel2.setFrstRegistPnttm(nowDate);
mockData.add(channel1);
mockData.add(channel2);
return new RestResponse(mockData);
}
}
/**
* 템플릿 목록 조회 테스트 데이터 생성
*
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse getTemplateListTestData(String testYn) {
if ("YF".equals(testYn)) {
return new RestResponse(randomFail());
} else {
// 성공 테스트: 모킹된 템플릿 목록 반환
List<TemplateInfo> mockData = new ArrayList<>();
TemplateInfo template1 = new TemplateInfo();
template1.setSenderKey("test_sender_key_001");
template1.setTemplateCode("TEST_TEMPLATE_001");
template1.setTemplateName("테스트 템플릿 001");
template1.setCreatedAt("2025-01-01 10:00:00");
template1.setModifiedAt("2025-01-01 10:00:00");
template1.setServiceStatus("REG(등록완료)");
TemplateInfo template2 = new TemplateInfo();
template2.setSenderKey("test_sender_key_001");
template2.setTemplateCode("TEST_TEMPLATE_002");
template2.setTemplateName("테스트 템플릿 002");
template2.setCreatedAt("2025-01-01 11:00:00");
template2.setModifiedAt("2025-01-01 11:00:00");
template2.setServiceStatus("REG(등록완료)");
TemplateInfo template3 = new TemplateInfo();
template3.setSenderKey("test_sender_key_002");
template3.setTemplateCode("TEST_TEMPLATE_003");
template3.setTemplateName("테스트 템플릿 003");
template3.setCreatedAt("2025-01-01 12:00:00");
template3.setModifiedAt("2025-01-01 12:00:00");
template3.setServiceStatus("REJ(반려)");
mockData.add(template1);
mockData.add(template2);
mockData.add(template3);
return new RestResponse(mockData);
}
}
/**
* 템플릿 상세 조회 테스트 데이터 생성
*
* @param testYn 테스트 모드 (YS: 성공, YF: 실패)
* @return RestResponse 래핑된 테스트 데이터
*/
public static RestResponse getTemplateDetailTestData(String testYn) {
if ("YF".equals(testYn)) {
return new RestResponse(randomFail());
} else {
// 성공 테스트: 실제 API 응답 구조를 반영한 템플릿 상세 반환
TemplateDetail templateDetail = TemplateDetail.builder()
.block("false")
.categoryCode("0000000")
.createdAt("2024-08-29 10:10:06")
.dormant("false")
.inspectionStatus("APR")
.modifiedAt("2025-03-27 15:41:57")
.securityFlag("false")
.senderKey("test_sender_key_1215123251234234234")
.senderKeyType("S")
.status("A")
.templateCode("bizp_test_template_0000000000000000")
.templateContent("[테스트]\\n안녕하세요 #{이름}님\\n테스트 물건1 #{물건1}\\n테스트 물건2 #{물건2}\\n테스트 물건3 #{물건3}\\n테스트 물건4 #{물건4}\\n테스트 물건5 #{물건5}\\n테스트 물건6 #{물건6}\\n입니다.")
.templateEmphasizeType("NONE")
.templateExtra("")
.templateHeader("")
.templateImageName("")
.templateImageUrl("")
.templateMessageType("BA")
.templateName("테스트 변수 템플릿")
.templateSubtitle("")
.templateTitle("")
.templateItemHighlight(null)
.templateItem(null)
.buttons(new ArrayList<>())
.comments(createMockComments())
.quickReplies(new ArrayList<>())
.build();
return new RestResponse(templateDetail);
}
}
/**
* 테스트용 Mock Comments 생성 헬퍼 메서드
* 실제 API 응답의 comments 구조를 반영
*/
private static List<TemplateComments> createMockComments() {
List<TemplateComments> comments = new ArrayList<>();
// 실제 API 응답과 유사한 comment 구조
TemplateComments comment = new TemplateComments();
comment.setContent("안녕하세요. 카카오톡 알림톡 검수 담당자입니다.\\r\\n\\r\\n테스트 템플릿으로 확인되어 승인합니다.\\r\\n\\r\\n감사합니다.");
comment.setCreatedAt("2024-08-29 13:44:13");
comment.setStatus("APR");
comment.setAttachment(new ArrayList<>());
comments.add(comment);
return comments;
}
}

View File

@ -2,7 +2,7 @@
# DB INFO
spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://192.168.0.125:3306/mjon?serverTimezone=Asia/Seoul
spring.datasource.url=jdbc:log4jdbc:mysql://192.168.0.60:3308/mjon_advc?serverTimezone=Asia/Seoul
#spring.datasource.url=jdbc:log4jdbc:mysql://139.150.72.157:3306/mjon?serverTimezone=Asia/Seoul
spring.datasource.username=mjonUr
@ -10,11 +10,17 @@ spring.datasource.password=mjon!@#$
server.port=8088
#logging.level.root=info
# Logging Configuration
logging.level.root=info
logging.level.jdbc.sqltiming=info
logging.level.jdbc.resultset=info
logging.level.jdbc.resultsettable=info
logging.level.jdbc.sqlonly=debug
logging.level.jdbc.audit=warn
logging.level.jdbc.connection=warn
#??? ?? ??
api.root.url=http://192.168.0.60:8085/
#api.root.url=https://www.munjaon.co.kr/
api.root.url=http://192.168.0.176:9080/
Ganpandaup.receiver.email=hylee250@kakao.com

View File

@ -0,0 +1,30 @@
# DB INFO
spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://192.168.0.60:3308/mjon_advc?serverTimezone=Asia/Seoul
#spring.datasource.url=jdbc:log4jdbc:mysql://139.150.72.157:3306/mjon?serverTimezone=Asia/Seoul
spring.datasource.username=mjonUr
spring.datasource.password=mjon!@#$
server.port=8088
# Logging Configuration
logging.level.root=info
logging.level.jdbc.sqltiming=info
logging.level.jdbc.resultset=info
logging.level.jdbc.resultsettable=info
logging.level.jdbc.sqlonly=debug
logging.level.jdbc.audit=warn
logging.level.jdbc.connection=warn
#??? ?? ??
api.root.url=http://localhost:8080/
#api.root.url=http://192.168.0.60:8085/
#api.root.url=https://www.munjaon.co.kr/
Ganpandaup.receiver.email=hylee250@kakao.com
server.tomcat.ajp.port=8009

View File

@ -1,12 +1,14 @@
spring.profiles.active=dev
spring.profiles.active=local
# mybatis setting
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations=classpath:mapper/**/*.xml
# model camel case set
mybatis.configuration.map-underscore-to-camel-case=true
spring.jackson.default-property-inclusion=non_null
#sql \ucd9c\ub825 log \uc124\uc815
logging.level.jdbc.sqlonly=off
@ -34,4 +36,21 @@ spring.servlet.multipart.max-request-size=20MB
#management.endpoints.web.exposure.include=*
#management.endpoint.health.show-details=always
#
Ganpandaup.estimate.template.url=https://www.munjaon.co.kr/publish/email_form_ganpandaum_contact.html
Ganpandaup.estimate.template.url=https://www.munjaon.co.kr/publish/email_form_ganpandaum_contact.html
biz.root.url=https://kapi.ppurio.com
biz.api.key=dheBWCONP6J5
biz.id=itn0202
# ?? ?? actuator ?? ???? ?? ?
# management.server.port=8081
# actuator base path ?? (???: /actuator)
# management.endpoints.web.base-path=/manage
# health ?? ?? ??
management.endpoint.health.show-details=always
# actuator endpoint ?? ??
management.endpoints.web.exposure.include=health,info,metrics

View File

@ -1,2 +1,20 @@
# log4jdbc 설정
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
# SQL 출력 설정
log4jdbc.dump.sql.maxlinelength=0
log4jdbc.trim.sql=true
log4jdbc.trim.sql.extrablanklines=false
# ResultSet 출력 활성화 (가장 중요한 설정)
log4jdbc.dump.sql.select=true
log4jdbc.dump.sql.insert=true
log4jdbc.dump.sql.update=true
log4jdbc.dump.sql.delete=true
# ResultSet 테이블 형태 출력을 위한 추가 설정
log4jdbc.dump.fulldebugstacktrace=false
log4jdbc.suppress.generated.keys.exception=false
# 에러 억제 설정
log4jdbc.auto.load.popular.drivers=false

View File

@ -81,6 +81,23 @@
</appender>
<!-- Loggers -->
<!-- log4jdbc 로거 설정 - SQL 쿼리와 결과값 출력 -->
<logger name="jdbc.sqltiming" level="INFO" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="jdbc.sqlonly" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="jdbc.audit" level="WARN" />
<logger name="jdbc.resultset" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="jdbc.resultsettable" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<logger name="jdbc.connection" level="WARN" />
<!-- <logger name="org.apache.catalina" level="ERROR">
</logger>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.mjonApi.mjon.api.inqry.mapper.PriceMapper">
<select id="selectMberMoney"
resultType="double"
>
SELECT a.USER_MONEY AS mberMoney
FROM lettngnrlmber a
WHERE a.MBER_ID = #{mberId}
</select>
<select id="selectMberPriceInfo"
resultType="hashmap"
>
SELECT a.SHORT_PRICE AS sysShortPrice,
a.LONG_PRICE AS sysLongPrice,
a.PICTURE_PRICE AS sysPicturePrice,
a.PICTURE2_PRICE AS sysPicturePrice2,
a.PICTURE3_PRICE AS sysPicturePrice3,
b.SHORT_PRICE AS shortPrice,
b.LONG_PRICE AS longPrice,
b.PICTURE_PRICE AS picturePrice,
b.PICTURE2_PRICE AS picturePrice2,
b.PICTURE3_PRICE AS picturePrice3
FROM mj_mber_setting a ,
lettngnrlmber b
WHERE b.mber_id = #{mberId}
</select>
</mapper>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.InqryMapper">
<select id="getChnlIds" resultType="com.itn.mjonApi.mjon.api.kakao.at.inqry.mapper.domain.MjKakaoProfileInfoVO">
/* kakao at getChnlIds */
SELECT
-- USER_ID
-- , PROFILE_ID
SENDER_KEY
-- , TOKEN
, PHONE_NUMBER
, YELLOW_ID
-- , CATEGORY_CODE
-- , CATEGORY_NAME
, FRST_REGIST_PNTTM
, FRST_REGISTER_ID
-- , LAST_UPDT_PNTTM
-- , LAST_UPDUSR_ID
-- , DELETE_YN
FROM mj_kakao_profile_info
WHERE DELETE_YN = 'N'
AND USER_ID = #{mberId}
</select>
<select id="isTemplateExist" parameterType="map" resultType="int">
/* kakao at isTemplateExist */
SELECT
COUNT(*)
FROM
mj_kakao_profile_info
WHERE
SENDER_KEY = #{senderKey}
AND TEMPLATE_CODE = #{templateCode}
AND DELETE_YN = 'N'
</select>
</mapper>

View File

@ -3,9 +3,9 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.mjonApi.mjon.api.inqry.mapper.HstryMapper">
<mapper namespace="com.itn.mjonApi.mjon.api.msg.inqry.mapper.HstryMapper">
<select id="selectApiInqryHstry" resultType="com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.MjonResponseVO">
<select id="selectApiInqryHstry" resultType="com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.MjonResponseVO">
/* 문자온 발송결과 페이지 쿼리를 그대로 사용함 - http://localhost:9080/web/mjon/msgsent/selectMsgSentView.do */
SELECT
@ -102,23 +102,21 @@
A.RSLT_CODE ,
A.RSLT_CODE2 ,
CASE
WHEN B.MSG_TYPE = '6'
AND B.FILE_CNT > 0
THEN '그림(MMS)'
WHEN B.MSG_TYPE = '6'
AND B.FILE_CNT = 0
THEN '장문(LMS)'
ELSE '단문(SMS)'
END msgTypeName ,
WHEN B.MSG_TYPE = '6' AND B.FILE_CNT > 0 THEN '그림(MMS)'
WHEN B.MSG_TYPE = '6' AND B.FILE_CNT = 0 THEN '장문(LMS)'
WHEN B.MSG_TYPE = '4' THEN '단문(SMS)'
WHEN B.MSG_TYPE = '8' THEN '알림톡(AT)'
WHEN B.MSG_TYPE = '9' THEN '친구톡(FT)'
ELSE '기타'
END msgTypeName ,
CASE
WHEN B.MSG_TYPE = '6'
AND B.FILE_CNT > 0
THEN '3'
WHEN B.MSG_TYPE = '6'
AND B.FILE_CNT = 0
THEN '2'
ELSE '1'
END orderByCode , (
WHEN B.MSG_TYPE = '6' AND B.FILE_CNT > 0 THEN '3'
WHEN B.MSG_TYPE = '6' AND B.FILE_CNT = 0 THEN '2'
WHEN B.MSG_TYPE = '4' THEN '1'
WHEN B.MSG_TYPE = '8' THEN '4'
WHEN B.MSG_TYPE = '9' THEN '5'
ELSE '9'
END orderByCode ,(
CASE
WHEN A.AGENT_CODE = '01'
AND
@ -182,14 +180,23 @@
AND DATE_ADD(NOW(), INTERVAL 60 MINUTE) >= B.REQ_DATE
<if test="startDate != null and startDate != ''">
AND b.regdate >= STR_TO_DATE(CONCAT(#{startDate}, '000000'), '%Y%m%d%H%i%s')
AND (
(b.regdate <![CDATA[ >= ]]> STR_TO_DATE(CONCAT(#{startDate}, '000000'), '%Y%m%d%H%i%s'))
OR
(b.req_date <![CDATA[ >= ]]> STR_TO_DATE(CONCAT(#{startDate}, '000000'), '%Y%m%d%H%i%s'))
)
</if>
<if test="startDate == null or startDate == ''">
AND b.regdate >= STR_TO_DATE(date_format(now(),'%Y%m%d000000'), '%Y%m%d%H%i%s')
<!-- AND b.regdate >= STR_TO_DATE(date_format(now(),'%Y%m%d000000'), '%Y%m%d%H%i%s')-->
AND b.regdate <![CDATA[ < ]]> DATE_ADD(CURDATE(), INTERVAL 1 DAY)
</if>
<if test="endDate != null and endDate != ''">
AND STR_TO_DATE(CONCAT(#{endDate}, '235959'), '%Y%m%d%H%i%s') >= b.regdate
AND (
(b.regdate <![CDATA[ <= ]]> STR_TO_DATE(CONCAT(#{endDate}, '235959'), '%Y%m%d%H%i%s'))
OR
(b.req_date <![CDATA[ <= ]]> STR_TO_DATE(CONCAT(#{endDate}, '235959'), '%Y%m%d%H%i%s'))
)
</if>
<if test="startDate == null or startDate == ''">
AND STR_TO_DATE(date_format(DATE_ADD(NOW(), INTERVAL 1 day),'%Y%m%d000000'), '%Y%m%d%H%i%s') >= b.regdate
@ -214,7 +221,10 @@
'2',
'3')
AND MSG_TYPE IN ('4',
'6')
'6',
'8',
'9'
)
GROUP BY MSG_GROUP_ID
/*
ORDER BY 1=1,
@ -301,7 +311,10 @@
WHERE 1 =1
AND MD.DEL_FLAG = 'N'
AND MD.MSG_TYPE IN ('4',
'6')
'6',
'8',
'9'
)
)
A
GROUP BY A.MSG_GROUP_ID,
@ -336,7 +349,7 @@
<select id="selectApiInqryHstryDetail"
resultType="com.itn.mjonApi.mjon.api.inqry.service.mapper.domain.MjonResponseVO"
resultType="com.itn.mjonApi.mjon.api.msg.inqry.mapper.domain.MjonResponseVO"
>
/* 문자온 발송결과 페이지 실패 건수 팝업을 변형해서 사용함 - http://localhost:9080/web/mjon/msgsent/selectMsgSFDetailListAjax.do */
@ -366,24 +379,23 @@
DEL_FLAG AS delFlag ,
*/
MSG_TYPE AS msgType ,
CASE
WHEN MSG_TYPE = '6' AND FILE_CNT > 0 THEN '그림(MMS)'
WHEN MSG_TYPE = '6' AND FILE_CNT = 0 THEN '장문(LMS)'
WHEN MSG_TYPE = '4' THEN '단문(SMS)'
WHEN MSG_TYPE = '8' THEN '알림톡(AT)'
WHEN MSG_TYPE = '9' THEN '친구톡(FT)'
ELSE '기타'
END msgTypeName ,
CASE
WHEN MSG_TYPE = '6'
AND FILE_CNT > 0
THEN '그림(MMS)'
WHEN MSG_TYPE = '6'
AND FILE_CNT = 0
THEN '장문(LMS)'
ELSE '단문(SMS)'
END msgTypeName ,
CASE
WHEN MSG_TYPE = '6'
AND FILE_CNT > 0
THEN '3'
WHEN MSG_TYPE = '6'
AND FILE_CNT = 0
THEN '2'
ELSE '1'
WHEN MSG_TYPE = '6' AND FILE_CNT > 0 THEN '3'
WHEN MSG_TYPE = '6' AND FILE_CNT = 0 THEN '2'
WHEN MSG_TYPE = '4' THEN '1'
WHEN MSG_TYPE = '8' THEN '4'
WHEN MSG_TYPE = '9' THEN '5'
ELSE '9'
END orderByCode ,
/*

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.mjonApi.mjon.api.msg.inqry.mapper.PriceMapper">
<select id="selectMberMoney"
resultType="double"
>
SELECT a.USER_MONEY AS mberMoney
FROM lettngnrlmber a
WHERE a.MBER_ID = #{mberId}
</select>
<select id="selectMberPriceInfo"
resultType="hashmap"
>
SELECT a.SHORT_PRICE AS sysShortPrice
, a.LONG_PRICE AS sysLongPrice
, a.PICTURE_PRICE AS sysPicturePrice
, a.PICTURE2_PRICE AS sysPicturePrice2
, a.PICTURE3_PRICE AS sysPicturePrice3
, a.KAKAO_AT_PRICE AS sysKakaoAtPrice
, a.KAKAO_FT_PRICE AS sysKakaoFtPrice
, a.KAKAO_FT_IMG_PRICE AS sysKakaoFtImgPrice
, a.KAKAO_FT_WIDE_IMG_PRICE AS sysKakaoFtWideImgPrice
, b.SHORT_PRICE AS shortPrice
, b.LONG_PRICE AS longPrice
, b.PICTURE_PRICE AS picturePrice
, b.PICTURE2_PRICE AS picturePrice2
, b.PICTURE3_PRICE AS picturePrice3
, b.KAKAO_AT_PRICE AS kakaoAtPrice
, b.KAKAO_FT_PRICE AS kakaoFtPrice
, b.KAKAO_FT_IMG_PRICE AS kakaoFtImgPrice
, b.KAKAO_FT_WIDE_IMG_PRICE AS kakaoFtWideImgPrice
FROM mj_mber_setting a ,
lettngnrlmber b
WHERE b.mber_id = #{mberId}
</select>
</mapper>

View File

@ -3,7 +3,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itn.mjonApi.mjon.api.send.mapper.SendMapper">
<mapper namespace="com.itn.mjonApi.mjon.api.msg.send.mapper.SendMapper">
<select id="findByCallFrom" resultType="boolean">
SELECT IF(COUNT(*), 1, 0)

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.itn.mjonApi.mjon.api.kakao.inqry.mapper.domain"/>
</typeAliases>
</configuration>