package itn.com.cmm.util; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import itn.com.cmm.MjonMsgSendVO; import itn.com.cmm.OptimalMsgResultDTO; import itn.let.mail.service.StatusResponse; import itn.let.mjo.event.service.MjonEventVO; import itn.let.mjo.msg.service.MjonMsgVO; import itn.let.mjo.msgagent.service.MjonMsgAgentStsVO; import itn.let.mjo.spammsg.web.ComGetSpamStringParser; import itn.let.sym.site.service.JoinSettingVO; import itn.let.uss.umt.service.MberManageVO; import itn.let.uss.umt.service.UserManageVO; import lombok.extern.slf4j.Slf4j; /** * * @author : 이호영 * @fileName : MsgSendUtils.java * @date : 2024.09.23 * @description : 메세지발송 데이터 다루는 유틸 * =========================================================== * DATE AUTHOR NOTE * ----------------------------------------------------------- * * 2024.09.23 이호영 최초 생성 * * * */ @Slf4j public final class MsgSendUtils { // 단문 메세지 타입 public static final String SHORT_MSG_TYPE = "4"; // 장문 메세지 타입 public static final String LONG_MSG_TYPE = "6"; // 이벤트 최저 잔액 public static final double MIN_EVENT_REMAIN_CASH = 7.5; // 이벤트 최소 잔액 /** * @methodName : getSmsTxtBytes * @author : 이호영 * @date : 2024.09.23 * @description : sms 텍스트 바이트 계산 후 return; * @param smsTxt * @return * @throws UnsupportedEncodingException */ public static int getSmsTxtBytes(String smsTxt) throws UnsupportedEncodingException { //문자열 길이 체크 해주기 int smsBytes = 0; //문자 바이트 계산에 필요한 캐릭터 셋 : 한글 2Byte로 계산 String charset = "euc-kr"; if(StringUtils.isNotEmpty(smsTxt)) { String smsCont = smsTxt.replace("\r\n", "\n"); smsBytes = smsCont.getBytes(charset).length; } // log.info(" + smsBytes :: [{}]", smsBytes); return smsBytes; } /** * @methodName : getMsgType * @author : 이호영 * @date : 2024.09.23 * @description : msgType 재정의 * @param mjonMsgVO * @param smsTxtByte * @return * @throws UnsupportedEncodingException */ public static String getMsgTypeWithByteValidation(MjonMsgSendVO sendVO, String p_smsTxt) throws UnsupportedEncodingException { // // 내문자저장함에 저장 후 문자를 발송하는 경우 문자 타입이 숫자가 아닌 문자로 넘어와서 변경 처리함 // if ("P".equals(msgType) || "L".equals(msgType)) { // msgType = "6"; // } else if ("S".equals(msgType)) { // msgType = "4"; // } int smsTxtByte = getSmsTxtBytes(p_smsTxt); String msgType = SHORT_MSG_TYPE; // 1. 2000 Byte 초과는 에러 처리 if (smsTxtByte > 2000) { return "INVALID"; } // 2. 첨부파일 여부 확인 (첨부파일이 있으면 장문으로 설정) if (StringUtils.isNotEmpty(sendVO.getFilePath1())) { msgType = LONG_MSG_TYPE; } // 3. 문자 길이에 따라 메시지 타입 설정 (90 Byte 초과는 장문) else if (smsTxtByte > 90) { msgType = LONG_MSG_TYPE; } return msgType; } public static float getValidPrice(Float personalPrice, Float defaultPrice) { return (personalPrice != null && personalPrice > 0) ? personalPrice : defaultPrice; } /** * @methodName : determinePriceByMsgType * @author : 이호영 * @date : 2024.09.24 * @description : 문자 메시지 타입에 따라 단가를 결정하는 메서드 * @param mjonMsgVO * @param shortPrice * @param longPrice * @param picturePrice * @param picture2Price * @param picture3Price * @return */ public static float determinePriceByMsgType(MjonMsgVO mjonMsgVO, float shortPrice, float longPrice, float picturePrice, float picture2Price, float picture3Price) { float price = 0; String msgType = mjonMsgVO.getMsgType(); if (SHORT_MSG_TYPE.equals(msgType)) { price = shortPrice; } else if (LONG_MSG_TYPE.equals(msgType)) { // 파일 첨부 여부에 따라 장문 단가 설정 if (mjonMsgVO.getFileName3() != null) { price = picture3Price; } else if (mjonMsgVO.getFileName2() != null) { price = picture2Price; } else if (mjonMsgVO.getFileName1() != null) { price = picturePrice; } else { price = longPrice; } } return price; } /** * @methodName : isReplacementRequired * @author : 이호영 * @date : 2024.11.12 * @description : 치환데이터가 있는지 확인 * @param mjonMsgVO * @return */ public static boolean isRepleasYN(MjonMsgVO mjonMsgVO) { // 치환 구문 패턴 리스트 String[] placeholders = {"\\[\\*이름\\*\\]", "\\[\\*1\\*\\]", "\\[\\*2\\*\\]", "\\[\\*4\\*\\]", "\\[\\*3\\*\\]"}; for (String placeholder : placeholders) { Pattern pattern = Pattern.compile(placeholder); Matcher matcher = pattern.matcher(mjonMsgVO.getSmsTxt()); // 해당 패턴이 존재하면 true 반환 if (matcher.find()) { return true; } } return false; } /** * @methodName : populateReplacementLists * @author : 이호영 * @date : 2024.09.26 * @description : 배열에 데이터를 채우는 메서드 * 1. 치환 문자열 데이터 확인 및 문자 치환 * 2. 스팸 문자 체크 * 3. 메세지 타입 구하기 * 4. 제목 셋팅 : 타입에 따라 분기 * 5. 이미지 갯수 셋팅 * 6. 예약 및 분할에 따른 시간 설정 * 7. 전송사 코드 셋팅 * @param mjonMsgVO * @param lists * @param statusResponse * @param agentSendCounts * @param sendRateList * @param isHolidayNotified * @return call by reference * @throws Exception */ public static Boolean populateSendLists(MjonMsgVO mjonMsgVO, List mjonMsgSendListVO , StatusResponse statusResponse, List resultSpamTxt , Map agentSendCounts, List sendRateList, boolean isHolidayNotified, String smishingYn) throws Exception{ log.info(" :: populateSendLists :: "); // 예약 시간 기본값 설정 Date now = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); // ReqDate가 비어 있으면 현재 시간으로 설정, 그렇지 않으면 ReqDate로 설정 // 화면에서 예약문자면 예약시간을 regDate로 설정한다. Date baseDate; if (StringUtils.isEmpty(mjonMsgVO.getReqDate())) { mjonMsgVO.setReqDate(sdf.format(now)); // ReqDate에 현재 시간 설정 baseDate = now; } else { baseDate = sdf.parse(mjonMsgVO.getReqDate()); // ReqDate를 baseDate로 설정 } Calendar calendar = Calendar.getInstance(); calendar.setTime(baseDate); // calendar에 baseDate 설정 int counter = 0; // 분할 건수 카운터 // 데이터 모두 스팸 체크를 안하고 건별로 갯수를 정해서 스팸체크를 한다. int spamChkSize = getSpamChkSize(mjonMsgSendListVO.size()); int sampleCounter = 0; // 치환 구문과 필드 getter 매핑 Map> placeholders = new HashMap<>(); placeholders.put("[*이름*]", MjonMsgSendVO::getName); placeholders.put("[*1*]", MjonMsgSendVO::getRep1); placeholders.put("[*2*]", MjonMsgSendVO::getRep2); placeholders.put("[*3*]", MjonMsgSendVO::getRep3); placeholders.put("[*4*]", MjonMsgSendVO::getRep4); boolean hasPerformedSpamCheck = false; // 치환 문자가 없는 경우, 스팸 체크가 한 번만 수행되도록 제어 boolean hasPerformedMsgType = false; // 치환 문자가 없는 경우, 스팸 체크가 한 번만 수행되도록 제어 boolean hasPerformedDelayYn = false; // 치환 문자가 없는 경우, 스팸 체크가 한 번만 수행되도록 제어 String msgKind = mjonMsgVO.getMsgKind(); String smsTxtTemp = mjonMsgVO.getSmsTxt(); String mmsSubject = mjonMsgVO.getMmsSubject(); Boolean replaceYN = getReplaceYN(smsTxtTemp); String msgTypeResult = null; for (MjonMsgSendVO sendVO : mjonMsgSendListVO) { sendVO.setCallFrom(mjonMsgVO.getCallFrom()); sendVO.setCallTo(sendVO.getPhone()); sendVO.setUserId(mjonMsgVO.getUserId()); String smsTxt = smsTxtTemp; // 치환 문자면 if(replaceYN) { // 각 치환 구문을 확인하고 치환할 값이 없으면 오류 반환 for (Map.Entry> entry : placeholders.entrySet()) { String placeholder = entry.getKey(); String value = entry.getValue().apply(sendVO); System.out.println(""); // log.info(" + smsTxtTemp [{}]", smsTxtTemp); // log.info(" + placeholder [{}]", placeholder); // log.info(" + value [{}]", value); // log.info(" + smsTxtTemp.contains(placeholder) [{}]", smsTxtTemp.contains(placeholder)); if (smsTxt.contains(placeholder)) { if (StringUtils.isEmpty(value)) { statusResponseSet(statusResponse, HttpStatus.BAD_REQUEST, "치환 문구중 " + placeholder + " 데이터가 없습니다."); return false; } smsTxt = smsTxt.replace(placeholder, value); // log.info(" + smsTxt [{}]", smsTxt); } } } String smsSpamChkTxt = smsTxt; if(StringUtils.isNotEmpty(smsTxt)) { smsSpamChkTxt = smsTxt.replaceAll(String.valueOf((char) 13), ""); } // 이미지 셋팅 setImagePathsForMsgSendVO(mjonMsgVO, sendVO); // == 치환 여부에 따라 처리 로직 분기 == // 치환 문자가 아닌 경우 if (!replaceYN) { // 스팸 체크와 메시지 타입 체크 각각 한 번만 수행 if (!hasPerformedSpamCheck) { checkSpamAndSetStatus(mjonMsgVO, smsSpamChkTxt, resultSpamTxt, isHolidayNotified); hasPerformedSpamCheck = true; } if (!hasPerformedMsgType) { msgTypeResult = getMsgTypeWithByteValidation(sendVO, smsTxt); if ("INVALID".equals(msgTypeResult)) { statusResponseSet(statusResponse, HttpStatus.BAD_REQUEST, "문자 치환 후 전송 문자 길이를 초과하였습니다."); return false; } hasPerformedMsgType = true; } } else {// 치환 문자인 경우 // 스팸 체크는 `spamChkSize`만큼 반복 수행 if (sampleCounter < spamChkSize && !"Y".equals(mjonMsgVO.getSpamStatus())) { checkSpamAndSetStatus(mjonMsgVO, smsSpamChkTxt, resultSpamTxt, isHolidayNotified); sampleCounter++; } // 메시지 타입 체크는 매번 수행 msgTypeResult = getMsgTypeWithByteValidation(sendVO, smsTxt); if ("INVALID".equals(msgTypeResult)) { statusResponseSet(statusResponse, HttpStatus.BAD_REQUEST, "문자 치환 후 전송 문자 길이를 초과하였습니다."); return false; } } sendVO.setSmsTxt(smsTxt); sendVO.setMsgType(msgTypeResult); // 제목 셋팅 if (LONG_MSG_TYPE.equals(msgTypeResult)){ String mmsTitleTemp = ""; // 제목이 비어 있고 전송할 SMS 텍스트가 존재하는 경우에만 처리 if (StringUtils.isEmpty(mmsSubject) && StringUtils.isNotEmpty(smsTxt)) { // SMS 텍스트를 줄 단위로 나누기 mmsTitleTemp = getMmsgSubject(smsTxt, msgKind); // 설정된 제목을 현재 메시지 객체에 적용 sendVO.setSubject(mmsTitleTemp); }else { // 설정된 제목을 현재 메시지 객체에 적용 sendVO.setSubject(mmsSubject); } } /* @isHolidayNotified * - 관리자 알림 설정으로 인해 * - 해당 시간이면 지연 미처리 * @smishingYn * - 회원 별 '스미싱 온' 상태값 * - Y면 알림, 지연 처리해야 함 * */ if("Y".equalsIgnoreCase(smishingYn) && isHolidayNotified) { mjonMsgVO.setSpamStatus("Y"); mjonMsgVO.setSmishingYn("Y"); mjonMsgVO.setDelayYn("Y"); } // 지연 여부 처리 if (( "Y".equalsIgnoreCase(smishingYn) || "Y".equalsIgnoreCase(mjonMsgVO.getDelayYn())) && !hasPerformedDelayYn && isHolidayNotified) { calendar.add(Calendar.MINUTE, 30); // 모든 시간을 30분 뒤로 미룸 // TEST // calendar.add(Calendar.MINUTE, 5); // 모든 시간을 30분 뒤로 미룸 hasPerformedDelayYn = true; } // 예약 여부 확인 if ("Y".equalsIgnoreCase(mjonMsgVO.getReserveYn())) { // 분할 발송일 경우 if ("on".equalsIgnoreCase(mjonMsgVO.getDivideChk())) { if (counter == Integer.parseInt(mjonMsgVO.getDivideCnt())) { // 지정된 건수마다 간격 추가 counter = 0; calendar.add(Calendar.MINUTE, Integer.parseInt(mjonMsgVO.getDivideTime())); } counter++; } // 예약 시간 설정 sendVO.setReqDate(sdf.format(calendar.getTime())); // 분할된 시간 설정 또는 기본 예약 시간 사용 } else { // 예약 여부가 N일 경우에도 기본 예약 시간 설정 sendVO.setReqDate(sdf.format(calendar.getTime())); } // 전송사 코드 셋팅 String agentCode = "00".equals(mjonMsgVO.getAgentCode()) ? MsgSendUtils.assignAgentBasedOnCount(agentSendCounts, sendRateList) : mjonMsgVO.getAgentCode(); sendVO.setAgentCode(agentCode); } // Group TB에 넣어줄 제목 // 치환안된 sms 데이터로 넣어야함 log.info(" mjonMsgSendListVO size :: [{}]", mjonMsgSendListVO.size()); if (LONG_MSG_TYPE.equals(mjonMsgSendListVO.get(0).getMsgType())){ mjonMsgVO.setMmsSubject(getMmsgSubject(smsTxtTemp, msgKind)); } return true; } /** * @methodName : setImagePathsForMsgSendVO * @author : 이호영 * @date : 2024.11.26 * @description : vo에 이미지 셋팅 * @param mjonMsgVO * @param sendVO */ private static void setImagePathsForMsgSendVO(MjonMsgVO mjonMsgVO, MjonMsgSendVO sendVO) { int fileCount = Integer.parseInt(mjonMsgVO.getFileCnt()); switch (fileCount) { case 3: sendVO.setFilePath3(mjonMsgVO.getFileName3()); case 2: sendVO.setFilePath2(mjonMsgVO.getFileName2()); case 1: sendVO.setFilePath1(mjonMsgVO.getFileName1()); break; default: // fileCount가 0이거나 설정할 파일이 없는 경우 break; } sendVO.setFileCnt(mjonMsgVO.getFileCnt()); } private static void checkSpamAndSetStatus(MjonMsgVO mjonMsgVO, String personalizedSmsTxt, List resultSpamTxt, boolean isHolidayNotified) throws Exception { mjonMsgVO.setSpamStatus("N"); mjonMsgVO.setDelayYn("N"); if(StringUtils.isNotEmpty(personalizedSmsTxt)) { String resultParser = ComGetSpamStringParser.getSpamTextParse(personalizedSmsTxt).trim(); int spmCnt = 0; String spmFilterTxt = ""; for (String spmTxt : resultSpamTxt) { String parserStr = ComGetSpamStringParser.getSpamTextParse(spmTxt).trim(); if (resultParser.contains(parserStr)) { spmCnt++; spmFilterTxt += spmTxt + ","; } } if (spmCnt > 0) { // 스팸 문자가 포함된 경우 if (StringUtil.getWordRight(spmFilterTxt.trim(), 1).equals(",")) { // 처음부터 idx 만큼 잘라낸 나머지 글자 spmFilterTxt = StringUtil.getWordLeft(spmFilterTxt.trim(), 1); } /* @isHolidayNotified * - 관리자 알림 설정으로 인해 * - 해당 시간이면 지연 미처리 * */ mjonMsgVO.setSpamStatus("Y"); if(isHolidayNotified) { mjonMsgVO.setDelayYn("Y"); } } } } private static Boolean getReplaceYN(String smsTxtTemplate) {// 여러 치환 구문이 포함된 정규식 패턴 if (smsTxtTemplate == null) { return false; // null일 경우 false 반환 } Boolean replaceYN = false; Pattern pattern = Pattern.compile("\\[\\*이름\\*\\]|\\[\\*1\\*\\]|\\[\\*2\\*\\]|\\[\\*3\\*\\]|\\[\\*4\\*\\]"); Matcher matcher = pattern.matcher(smsTxtTemplate); // 정규식 패턴에 해당하는 치환 구문이 존재하는지 확인 if (matcher.find()) { replaceYN = true; } return replaceYN; } /** * @methodName : getSpamChkSize * @author : 이호영 * @date : 2024.11.13 * @description : 수신자 건수별로 스팸체크하는 갯수 설정 * @param size * @return */ private static int getSpamChkSize(int size) { int chkSize = 1; // 기본 샘플 크기 // 수신자 수에 따른 샘플 크기 결정 if (size > 100 && size <= 1000) { chkSize = 10; } else if (size > 1000) { chkSize = 50; } return chkSize; } /** * 특정 플레이스홀더를 리스트에서 가져온 값으로 치환합니다. * * @param smsTxt 문자 내용 * @param placeholder 치환할 플레이스홀더 (예: [*이름*]) * @param list 치환할 데이터 리스트 * @param index 현재 인덱스 * @return 치환된 문자 내용 */ private static String replacePlaceholder(String smsTxt, String placeholder, String[] list, int index) { if (smsTxt.contains(placeholder)) { if (list.length > index && StringUtil.isNotEmpty(list[index])) { smsTxt = smsTxt.replaceAll(placeholder, StringUtil.getString(list[index].replaceAll("§", ","))); } else { smsTxt = smsTxt.replaceAll(placeholder, ""); } } return smsTxt; } // 배열 인덱스를 안전하게 접근하는 메서드 private static String getSafeValue(String[] array, int index) { return (array != null && array.length > index) ? array[index] : " "; } // 치환 처리에 특수문자 제거 로직이 포함된 메서드 private static String getReplacementValue(String[] array, int index, String placeholder) { if (array != null && array.length > index && StringUtil.isNotEmpty(array[index])) { return StringUtil.getString(array[index].replaceAll("§", ",")); } else { return " "; } } public static void statusResponseSet (StatusResponse statusResponse, HttpStatus httpStatus, String msg ) { statusResponse.setStatus(httpStatus); statusResponse.setMessage(msg); } public static StatusResponse validateFilesForMessageSending(int fileCount, MjonMsgVO mjonMsgVO) { if (fileCount > 0) { boolean allFilesAreNull = Stream .of(mjonMsgVO.getFileName1(), mjonMsgVO.getFileName2(), mjonMsgVO.getFileName3()) .allMatch(Objects::isNull); if (allFilesAreNull) { return new StatusResponse(HttpStatus.NO_CONTENT, "문자 메세지 이미지 추가에 오류가 발생하여 문자 발송이 취소 되었습니다."); } } return null; // 유효성 검사를 통과한 경우 } /** * @methodName : validateReplacementData * @author : 이호영 * @date : 2024.09.25 * @description : 치환문자가 사용될 때 데이터가 올바른지 확인하는 메서드 * @param mjonMsgVO * @param list * @return boolean */ /* * public static boolean validateReplacementData(MjonMsgVO mjonMsgVO, * List list) { // 치환 구문과 필드 getter 매핑 Map> placeholders = new HashMap<>(); * placeholders.put("\\[\\*이름\\*\\]", MjonMsgSendVO::getName); * placeholders.put("\\[\\*1\\*\\]", MjonMsgSendVO::getRep1); * placeholders.put("\\[\\*2\\*\\]", MjonMsgSendVO::getRep2); * placeholders.put("\\[\\*3\\*\\]", MjonMsgSendVO::getRep3); * placeholders.put("\\[\\*4\\*\\]", MjonMsgSendVO::getRep4); * * // smsTxt 에서 필요한 치환 구문이 포함되어 있는지 확인 String smsTxt = mjonMsgVO.getSmsTxt(); * for (Map.Entry> entry : * placeholders.entrySet()) { if * (Pattern.compile(entry.getKey()).matcher(smsTxt).find()) { // 해당 치환 구문이 존재할 * 경우 모든 수신자에서 필드 값 확인 for (MjonMsgSendVO recipient : list) { if * (StringUtils.isEmpty(entry.getValue().apply(recipient))) { return false; // * 데이터가 없는 경우 } } } } // 모든 치환 구문이 유효한 데이터를 가지고 있으면 true 반환 return true; } */ // 배열이 비어있는지 확인하는 메서드 public static boolean isEmpty(String[] array) { return array == null || array.length == 0; } public static void setPriceforVO(MjonMsgVO mjonMsgVO, List mjonMsgSendVOList, JoinSettingVO sysJoinSetVO, MberManageVO mberManageVO) { // 사용자 단가 정보 설정 (협의 단가가 없을 경우 시스템 단가를 적용) float shortPrice = MsgSendUtils.getValidPrice(mberManageVO.getShortPrice(), sysJoinSetVO.getShortPrice()); float longPrice = MsgSendUtils.getValidPrice(mberManageVO.getLongPrice(), sysJoinSetVO.getLongPrice()); float picturePrice = MsgSendUtils.getValidPrice(mberManageVO.getPicturePrice(), sysJoinSetVO.getPicturePrice()); float picture2Price = MsgSendUtils.getValidPrice(mberManageVO.getPicture2Price(), sysJoinSetVO.getPicture2Price()); float picture3Price = MsgSendUtils.getValidPrice(mberManageVO.getPicture3Price(), sysJoinSetVO.getPicture3Price()); // 각 메시지 타입에 따라 사용자 단가 설정 및 총 단가 계산 float totalPrice = 0.0f; for (MjonMsgSendVO sendVO : mjonMsgSendVOList) { String msgType = sendVO.getMsgType(); String eachPrice; switch (msgType) { case SHORT_MSG_TYPE: // 단문 메시지 타입 eachPrice = Float.toString(shortPrice); break; case LONG_MSG_TYPE: // 장문 또는 이미지 메시지 타입 eachPrice = getPicturePrice(sendVO, longPrice, picturePrice, picture2Price, picture3Price); break; default: // 기본값이 필요하다면 추가 가능 throw new IllegalArgumentException("setPriceforVO :: type Exception "); } sendVO.setEachPrice(eachPrice); // log.info(" eachPrice :: [{}]", eachPrice); // 각 가격을 합산 totalPrice += Float.parseFloat(eachPrice); } } /** * @methodName : setPriceforVO * @author : 이호영 * @date : 2024.12.02 * @description : total금액 구하기 * @param mjonMsgSendVOList * @return */ public static float setPriceforVO(List mjonMsgSendVOList) { float totalPrice = 0.0f; for (MjonMsgSendVO sendVO : mjonMsgSendVOList) { String priceStr = sendVO.getEachPrice(); if(StringUtils.isNotEmpty(priceStr)) { totalPrice += Float.parseFloat(priceStr); } } return totalPrice; } /** * 이미지 파일 경로를 기준으로 적절한 가격을 반환하는 헬퍼 메소드. */ private static String getPicturePrice(MjonMsgSendVO sendVO, float longPrice, float picturePrice, float picture2Price, float picture3Price) { switch (sendVO.getFilePath3() != null ? "FILE3" : sendVO.getFilePath2() != null ? "FILE2" : sendVO.getFilePath1() != null ? "FILE1" : "NONE") { case "FILE3": return Float.toString(picture3Price); case "FILE2": return Float.toString(picture2Price); case "FILE1": return Float.toString(picturePrice); default: return Float.toString(longPrice); } } /** * @methodName : getOptimalMsgList * @author : 이호영 * @date : 2024.11.26 * @description : 주어진 이벤트 정보와 메시지 리스트를 기반으로, 잔액 내에서 최적의 메시지 리스트를 생성하고 결과를 반환합니다. * 만약 잔액이 부족하거나 이벤트가 종료된 상태라면, 이벤트 종료 정보를 포함하여 반환합니다. * * @param eventMberInfo 이벤트 정보 객체 (MjonEventVO). 이벤트 상태 및 잔액 정보를 포함. * @param mjonMsgSendVOList 최적화를 수행할 메시지 리스트 (List). * @return OptimalMsgResultDTO 최적 메시지 리스트와 이벤트 상태 정보를 포함한 DTO 객체. * @throws Exception 메시지 단가 계산 오류 또는 기타 처리 중 발생한 예외를 상위로 전달. */ public static OptimalMsgResultDTO getOptimalMsgList( MjonEventVO eventMberInfo, List mjonMsgSendVOList) throws Exception { // 최적의 메시지 리스트를 담을 리스트 List optimalList = new ArrayList<>(); // 이벤트 정보가 없는 경우 if (eventMberInfo == null) { return OptimalMsgResultDTO.builder() .optimalMsgList(optimalList) .eventInfo(null) .build(); } // 이벤트 잔액과 종료 조건 확인 double targetCash = eventMberInfo.getEventRemainCash(); String eventEndDate = eventMberInfo.getEventEndDate(); if (isEventExpired(targetCash, eventEndDate)) { MjonEventVO returnEventMberInfo = terminateEvent(eventMberInfo, targetCash); return OptimalMsgResultDTO.builder() .optimalMsgList(optimalList) .eventInfo(returnEventMberInfo) .build(); } // 단가 설정 float eventShortPrice = Float.parseFloat(eventMberInfo.getEventShortPrice()); float eventLongPrice = Float.parseFloat(eventMberInfo.getEventLongPrice()); float eventPicturePrice = Float.parseFloat(eventMberInfo.getEventPicturePrice()); float eventPicture2Price = Float.parseFloat(eventMberInfo.getEventPicture2Price()); float eventPicture3Price = Float.parseFloat(eventMberInfo.getEventPicture3Price()); // 최적의 메시지 리스트 생성 double sum = 0.0; Iterator iterator = mjonMsgSendVOList.iterator(); while (iterator.hasNext()) { MjonMsgSendVO msg = iterator.next(); // 단가 계산 및 예외 처리 String eachPrice = getEachPriceOrThrow(msg, eventShortPrice, eventLongPrice, eventPicturePrice, eventPicture2Price, eventPicture3Price); float floatEachPrice = Float.parseFloat(eachPrice); // 최적의 메시지 리스트에 추가 if (sum + floatEachPrice <= targetCash) { sum += floatEachPrice; msg.setEachPrice(eachPrice); msg.setEventYn("Y"); optimalList.add(msg); iterator.remove(); } else { break; // 예산 초과로 추가 불가능 } } // 잔액 부족 시 이벤트 종료 처리하는 VO 생성 double remainAmt = targetCash - sum; log.info("remainAmt :: [{}]", remainAmt); if (remainAmt < MIN_EVENT_REMAIN_CASH) { MjonEventVO returnEventMberInfo = terminateEvent(eventMberInfo, remainAmt); return OptimalMsgResultDTO.builder() .optimalMsgList(optimalList) .eventInfo(returnEventMberInfo) .build(); } // 결과 반환 return OptimalMsgResultDTO.builder() .optimalMsgList(optimalList) .eventInfo(null) .build(); } /** * @methodName : getEachPriceOrThrow * @author : 이호영 * @date : 2024.11.26 * @description : * @param msg * @param shortPrice * @param longPrice * @param picturePrice * @param picture2Price * @param picture3Price * @return */ private static String getEachPriceOrThrow( MjonMsgSendVO msg, float shortPrice, float longPrice, float picturePrice, float picture2Price, float picture3Price) { switch (msg.getMsgType()) { case SHORT_MSG_TYPE: return Float.toString(shortPrice); case LONG_MSG_TYPE: return getPicturePrice(msg, longPrice, picturePrice, picture2Price, picture3Price); default: throw new IllegalArgumentException("Unsupported message type: " + msg.getMsgType()); } } private static boolean isEventExpired(double targetCash, String eventEndDate) throws Exception { return !MJUtil.getCompareDate(eventEndDate) || targetCash < MIN_EVENT_REMAIN_CASH; } /** * @methodName : terminateEvent * @author : 이호영 * @date : 2024.11.26 * @description : 이벤트 종료 VO 생성 * @param eventMberInfo * @param targetCash * @return */ private static MjonEventVO terminateEvent(MjonEventVO eventMberInfo, double targetCash) { // TODO Auto-generated method stub // 이벤트 상태를 종료로 변경 MjonEventVO returnEventMberInfo = new MjonEventVO(); returnEventMberInfo.setEventInfoId(eventMberInfo.getEventInfoId()); returnEventMberInfo.setEventStatus("E"); returnEventMberInfo.setEventRemainCash(targetCash); returnEventMberInfo.setEventMemo("발송 최소 금액("+ MIN_EVENT_REMAIN_CASH +") 부족 혹은 이벤트 종료일 초과되어 이벤트 종료 시킴"); return returnEventMberInfo; } /** * @methodName : handleHotlineAgentRate * @author : 이호영 * @date : 2024.11.26 * @description : 전용 전송사 비율이 0이면 대표 전송사로 수정 * @param userInfo * @param tmp * @param hotlineAgentCode * @return */ public static String handleHotlineAgentRate(MberManageVO userInfo, MjonMsgAgentStsVO tmp, String hotlineAgentCode) { String sendRate = tmp.getSendRate(); String repAgent = tmp.getRepAgent(); String useYn = tmp.getUseYn(); // 블라인드 코드가 'N'인 경우 전송 비율 및 사용 가능 여부 확인 if ("N".equals(userInfo.getBlineCode())) { // 전송 비율이 0이거나 사용 불가인 경우 대표 전송사로 변경 if ("0".equals(sendRate) || "N".equals(useYn)) { hotlineAgentCode = repAgent; } } return hotlineAgentCode; // 변경된 hotlineAgentCode 반환 } /** * @methodName : initializeAgentSendCounts * @author : 이호영 * @date : 2024.11.26 * @description : 전송사별 코드로 map 초기화 하기 * @return */ public static Map initializeAgentSendCounts() { Map agentSendCounts = new HashMap<>(); agentSendCounts.put("01", 0); // 아이하트 agentSendCounts.put("02", 0); // 현대 퓨쳐넷 agentSendCounts.put("03", 0); // 아이엠오 agentSendCounts.put("04", 0); // 비즈뿌리오 agentSendCounts.put("05", 0); // 제이제이 agentSendCounts.put("07", 0); // 인비토 return agentSendCounts; } /** * @methodName : calculateSendCounts * @author : 이호영 * @date : 2024.11.26 * @description : agentSendCounts에 있는 에이젼트 map에 전송사 비율에 따라 카운트 put 하기 * @param agentSendCounts * @param sendRate * @param finalSize */ public static void calculateSendCounts(Map agentSendCounts, MjonMsgVO sendRate, int finalSize) { String agentCode = sendRate.getAgentCode(); float sendRatePercentage = Float.parseFloat(sendRate.getSendRate()); int sendCount = Math.round(finalSize * sendRatePercentage); if (agentSendCounts.containsKey(agentCode)) { agentSendCounts.put(agentCode, sendCount); } } /** * @methodName : assignAgentBasedOnCount * @author : 이호영 * @date : 2024.11.26 * @description : initializeAgentSendCounts()에서 만든 map으로 발송 agent 데이터 Set * @param agentSendCounts * @param sendRateList * @return */ public static String assignAgentBasedOnCount(Map agentSendCounts, List sendRateList) { // 전송사 코드 순서대로 확인 :: initializeAgentSendCounts 메소드와 맞춰야함 List agentCodes = Arrays.asList("01", "02", "03", "04", "05", "07"); // 전송 가능한 코드 찾기 for (String agentCode : agentCodes) { if (agentSendCounts.get(agentCode) > 0) { agentSendCounts.put(agentCode, agentSendCounts.get(agentCode) - 1); return agentCode; } } // 모든 코드의 전송 가능 횟수가 0이면 대표 전송사로 할당 return sendRateList.get(0).getRepAgent(); } /** * @methodName : getMmsgSubject * @author : 이호영 * @date : 2024.12.02 * @description : 타이틀 생성 * @param smsTxt * @param msgKind * @return */ public static String getMmsgSubject(String smsTxt, String msgKind) { String mmsTitleTemp = ""; if(StringUtils.isEmpty(smsTxt)) { return mmsTitleTemp; } // SMS 텍스트를 줄 단위로 나누기 String[] split = smsTxt.split("\n"); if (split.length > 0) { // "C" 메시지 종류인 경우 두 번째 줄, 그렇지 않으면 첫 번째 줄을 제목으로 설정 mmsTitleTemp = "C".equals(msgKind) && split.length > 1 ? split[1].trim() : split[0].trim(); // 제목 글자 수를 20자로 제한 (초과 시 잘라냄) mmsTitleTemp = mmsTitleTemp.length() > 20 ? mmsTitleTemp.substring(0, 20) : mmsTitleTemp; } return mmsTitleTemp; } }