diff --git a/src/main/java/com/madeu/crm/callLog/ctrl/CallLogController.java b/src/main/java/com/madeu/crm/callLog/ctrl/CallLogController.java new file mode 100644 index 0000000..602b79a --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/ctrl/CallLogController.java @@ -0,0 +1,215 @@ +package com.madeu.crm.callLog.ctrl; + +import java.util.HashMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import com.madeu.constants.Constants; +import com.madeu.crm.callLog.dto.CallLogSearchDTO; +import com.madeu.crm.callLog.dto.CallMemoDTO; +import com.madeu.crm.callLog.service.CallLogService; +import com.madeu.crm.callLog.service.GoodArsService; +import com.madeu.init.ManagerDraftAction; +import com.madeu.util.HttpUtil; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequestMapping("/callLog") +public class CallLogController extends ManagerDraftAction { + + @Autowired + private CallLogService callLogService; + + @Autowired + private GoodArsService goodArsService; + + /** + * 통화 로그 관리 화면으로 이동 + */ + @RequestMapping("/moveCallLogList.do") + public String moveCallLogList(HttpSession session, HttpServletRequest request, HttpServletResponse response, + Model model) { + log.debug("CallLogController moveCallLogList START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = null; + + try { + if (!webCheckLogin(session)) { + return "/web/login/logout"; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = callLogService.moveCallLogList(paramMap); + model.addAttribute("selectUseYn", map.get("selectUseYn")); + model.addAttribute("insertUseYn", map.get("insertUseYn")); + model.addAttribute("updateUseYn", map.get("updateUseYn")); + model.addAttribute("deleteUseYn", map.get("deleteUseYn")); + model.addAttribute("downloadUseYn", map.get("downloadUseYn")); + } + } catch (Exception e) { + e.printStackTrace(); + return "/web/login/logout"; + } + log.debug("CallLogController moveCallLogList END"); + + return "/web/callLog/callLogSelectList"; + } + + /** + * 통화 로그 목록 조회 (통계 포함) + */ + @PostMapping("/getCallLogList.do") + public ModelAndView getCallLogList(HttpSession session, HttpServletRequest request, + HttpServletResponse response, CallLogSearchDTO searchDTO) { + log.debug("CallLogController getCallLogList START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + searchDTO.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = callLogService.getCallLogList(searchDTO); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } finally { + if (Constants.OK != map.get("msgCode")) { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + } + log.debug("CallLogController getCallLogList END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 통화 메모 저장 + */ + @PostMapping("/saveCallMemo.do") + public ModelAndView saveCallMemo(HttpSession session, HttpServletRequest request, + HttpServletResponse response, CallMemoDTO memoDTO) { + log.debug("CallLogController saveCallMemo START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + memoDTO.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = callLogService.saveCallMemo(memoDTO); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "메모 저장 중 오류가 발생하였습니다."); + } + log.debug("CallLogController saveCallMemo END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 전화 걸기 (아웃바운드) + */ + @PostMapping("/makeOutboundCall.do") + public ModelAndView makeOutboundCall(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("CallLogController makeOutboundCall START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + String loginCid = String.valueOf(paramMap.get("loginCid")); + String sendCid = String.valueOf(paramMap.get("sendCid")); + String userCid = String.valueOf(paramMap.get("userCid")); + + map = goodArsService.makeOutboundCall(loginCid, sendCid, userCid); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("success", false); + map.put("msgDesc", "전화 발신 중 오류가 발생하였습니다."); + } + log.debug("CallLogController makeOutboundCall END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 전화 돌려주기 (Push) + */ + @PostMapping("/pushCall.do") + public ModelAndView pushCall(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("CallLogController pushCall START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + String loginCid = String.valueOf(paramMap.get("loginCid")); + String pushCid = String.valueOf(paramMap.get("pushCid")); + + map = goodArsService.pushCall(loginCid, pushCid); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("success", false); + map.put("msgDesc", "전화 연결 중 오류가 발생하였습니다."); + } + log.debug("CallLogController pushCall END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 녹음 파일 URL 조회 + */ + @PostMapping("/getRecordFileUrl.do") + public ModelAndView getRecordFileUrl(HttpSession session, HttpServletRequest request, + HttpServletResponse response) { + log.debug("CallLogController getRecordFileUrl START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + String recordNo = String.valueOf(paramMap.get("recordNo")); + String url = goodArsService.getRecordFileUrl(recordNo); + map.put("success", true); + map.put("recordUrl", url); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("success", false); + map.put("msgDesc", "녹음 파일 조회 중 오류가 발생하였습니다."); + } + log.debug("CallLogController getRecordFileUrl END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } +} diff --git a/src/main/java/com/madeu/crm/callLog/ctrl/CtiWebhookController.java b/src/main/java/com/madeu/crm/callLog/ctrl/CtiWebhookController.java new file mode 100644 index 0000000..9feb3b9 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/ctrl/CtiWebhookController.java @@ -0,0 +1,199 @@ +package com.madeu.crm.callLog.ctrl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.madeu.crm.callLog.dto.CallLogDTO; +import com.madeu.crm.callLog.service.CallLogService; + +import lombok.extern.slf4j.Slf4j; + +/** + * GoodARS Webhook 수신 Controller + * + * ARS 서버에서 통화 단계별로 이 엔드포인트를 호출하여 통화 로그를 저장합니다. + * 인증 없이(public) 접근 가능해야 합니다. + * + * 통화 흐름: + * step01 (수신) → step02 (ARS 메뉴 선택) → step03 (통화 연결) + * → step04 (통화 종료) → step05 (기타 종료) + * callback (콜백 요청) + */ +@Slf4j +@RestController +@RequestMapping("/api/cti/webhook") +public class CtiWebhookController { + + @Autowired + private CallLogService callLogService; + + /** + * Step 01 - 전화 수신 (통화 시작) + */ + @PostMapping("/step01") + public String step01( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook step01 - RECORD_NO:{}, CID:{}, FULLDNIS:{}", recordNo, cid, fulldnis); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, stateType, "1"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook step01 error", e); + return "fail"; + } + } + + /** + * Step 02 - ARS 메뉴 선택 (Ring) + */ + @PostMapping("/step02") + public String step02( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook step02 - RECORD_NO:{}, MENU_NO:{}", recordNo, menuNo); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, stateType, "2"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook step02 error", e); + return "fail"; + } + } + + /** + * Step 03 - 통화 연결 (Link) + */ + @PostMapping("/step03") + public String step03( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook step03 - RECORD_NO:{}, LOGINCID:{}", recordNo, logincid); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, stateType, "3"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook step03 error", e); + return "fail"; + } + } + + /** + * Step 04 - 통화 종료 (Link End) + */ + @PostMapping("/step04") + public String step04( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook step04 - RECORD_NO:{}", recordNo); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, stateType, "4"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook step04 error", e); + return "fail"; + } + } + + /** + * Step 05 - 기타 종료 (Bend) + */ + @PostMapping("/step05") + public String step05( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook step05 - RECORD_NO:{}", recordNo); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, stateType, "5"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook step05 error", e); + return "fail"; + } + } + + /** + * Callback - 콜백 요청 + */ + @PostMapping("/callback") + public String callback( + @RequestParam(value = "FULLDNIS", required = false, defaultValue = "") String fulldnis, + @RequestParam(value = "CID", required = false, defaultValue = "") String cid, + @RequestParam(value = "MENU_NO", required = false, defaultValue = "") String menuNo, + @RequestParam(value = "LOGINCID", required = false, defaultValue = "") String logincid, + @RequestParam(value = "RECORD_NO", required = false, defaultValue = "") String recordNo, + @RequestParam(value = "BOUND", required = false, defaultValue = "") String bound, + @RequestParam(value = "STATE_TYPE", required = false, defaultValue = "") String stateType) { + + log.debug("CtiWebhook callback - RECORD_NO:{}, CID:{}", recordNo, cid); + + try { + CallLogDTO logDTO = buildLogDTO(fulldnis, cid, menuNo, logincid, recordNo, bound, "callback", "98"); + callLogService.putCtiLog(logDTO); + return "success"; + } catch (Exception e) { + log.error("CtiWebhook callback error", e); + return "fail"; + } + } + + /** + * 공통 DTO 빌드 메서드 + */ + private CallLogDTO buildLogDTO(String fulldnis, String cid, String menuNo, String logincid, + String recordNo, String bound, String stateType, String ctiStep) { + CallLogDTO dto = new CallLogDTO(); + dto.setFulldnis(fulldnis); + dto.setCid(cid); + dto.setMenuNo(menuNo); + dto.setLogincid(logincid); + dto.setRecordNo(recordNo); + dto.setBound(bound); + dto.setStateType(stateType); + dto.setCtiStep(ctiStep); + return dto; + } +} diff --git a/src/main/java/com/madeu/crm/callLog/dto/CallLogDTO.java b/src/main/java/com/madeu/crm/callLog/dto/CallLogDTO.java new file mode 100644 index 0000000..659c598 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/dto/CallLogDTO.java @@ -0,0 +1,44 @@ +package com.madeu.crm.callLog.dto; + +import lombok.Data; + +/** + * 통화 로그 결과 DTO + */ +@Data +public class CallLogDTO { + /* ---- PK / 기본 필드 ---- */ + private String muCtiLogId; + private String recordNo; + private String cid; // 발신자 전화번호 + private String fulldnis; // 매장 수신번호 + private String bound; // IN/OUT + private String menuNo; // ARS 메뉴 번호 + private String stateType; // 상태유형 + private String ctiStep; // 통화단계 + private String logincid; // 상담원 전화번호 + private String cType1; // 초진/재진 + private String cType2; // 상담/상담후예약/회차예약 + private String crmMbPid; // CRM 회원 PID + private String storePid; // 매장 PID + private String incallSel; // In Call 선택값 + private String callBackChk; // 콜백 확인 여부 + private String useYn; + private String cudFlag; + private String regId; + private String regDate; + private String modId; + private String modDate; + + /* ---- 조인/서브쿼리 결과 필드 ---- */ + private String ring; // ARS 메뉴 선택 (step2) + private String linkDate; // 통화 연결 시각 (step3) + private String linkCid; // 통화 연결 상담원 (step3) + private String linkEndDate; // 통화 종료 시각 (step4) + private String bendDate; // 기타 종료 시각 (step5) + private String callbackCid; // 콜백 전화번호 (step98) + private String smsFlag; // SMS 전송 여부 + private String callMsg; // 통화 메모 + private String memberName; // 고객명 + private String rowNum; // 행 번호 +} diff --git a/src/main/java/com/madeu/crm/callLog/dto/CallLogSearchDTO.java b/src/main/java/com/madeu/crm/callLog/dto/CallLogSearchDTO.java new file mode 100644 index 0000000..25756c1 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/dto/CallLogSearchDTO.java @@ -0,0 +1,28 @@ +package com.madeu.crm.callLog.dto; + +import lombok.Data; + +/** + * 통화 로그 검색 조건 DTO + */ +@Data +public class CallLogSearchDTO { + /* ---- 검색 조건 ---- */ + private String sDate; // 검색 시작일 + private String eDate; // 검색 종료일 + private String callType; // 구분1 (ARS 메뉴: 1=상담, 2=예약, 3=위치안내, callback) + private String callType1; // 구분2 (초진/재진) + private String callType2; // 구분3 (상담/상담후예약/회차예약) + private String mCid; // 고객 전화번호 검색 + private String fulldnis; // 매장 수신번호 + private String incallSel; // In Call 선택값 필터 + + /* ---- 페이징 ---- */ + private Integer gridLimitStart; + private Integer gridLimitEnd; + private String gridSort; + + /* ---- 권한/세션 ---- */ + private String menuClass; + private String loginMemberId; +} diff --git a/src/main/java/com/madeu/crm/callLog/dto/CallLogStatsDTO.java b/src/main/java/com/madeu/crm/callLog/dto/CallLogStatsDTO.java new file mode 100644 index 0000000..e66c3a3 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/dto/CallLogStatsDTO.java @@ -0,0 +1,18 @@ +package com.madeu.crm.callLog.dto; + +import lombok.Data; + +/** + * 통화 로그 통계 DTO + */ +@Data +public class CallLogStatsDTO { + private int totalCnt; // 전체 건수 + private int inCnt; // IN 건수 + private int outCnt; // OUT 건수 + private int type1Cnt; // 초진 건수 + private int type2Cnt; // 재진 건수 + private int type11Cnt; // 상담 건수 + private int type12Cnt; // 상담후예약 건수 + private int type13Cnt; // 회차예약 건수 +} diff --git a/src/main/java/com/madeu/crm/callLog/dto/CallMemoDTO.java b/src/main/java/com/madeu/crm/callLog/dto/CallMemoDTO.java new file mode 100644 index 0000000..081a6ac --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/dto/CallMemoDTO.java @@ -0,0 +1,14 @@ +package com.madeu.crm.callLog.dto; + +import lombok.Data; + +/** + * 통화 메모 DTO + */ +@Data +public class CallMemoDTO { + private String muCtiTextId; + private String recordNo; + private String callMsg; + private String loginMemberId; +} diff --git a/src/main/java/com/madeu/crm/callLog/map/CallLogMAP.java b/src/main/java/com/madeu/crm/callLog/map/CallLogMAP.java new file mode 100644 index 0000000..5ea4571 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/map/CallLogMAP.java @@ -0,0 +1,38 @@ +package com.madeu.crm.callLog.map; + +import java.util.HashMap; +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; + +import com.madeu.crm.callLog.dto.CallLogDTO; +import com.madeu.crm.callLog.dto.CallLogSearchDTO; +import com.madeu.crm.callLog.dto.CallLogStatsDTO; +import com.madeu.crm.callLog.dto.CallMemoDTO; + +@Mapper +public interface CallLogMAP { + // 통화 로그 목록 건수 조회 + int getCallLogCnt(CallLogSearchDTO searchDTO); + + // 통화 로그 목록 조회 + List getCallLogList(CallLogSearchDTO searchDTO); + + // 통화 로그 통계 집계 + CallLogStatsDTO getCallLogStats(CallLogSearchDTO searchDTO); + + // 통화 메모 존재 여부 확인 + int getCallMemoCnt(CallMemoDTO memoDTO); + + // 통화 메모 신규 저장 + int saveCallMemo(CallMemoDTO memoDTO); + + // 통화 메모 수정 + int modCallMemo(CallMemoDTO memoDTO); + + // CTI 로그 저장 (Webhook용) + int putCtiLog(CallLogDTO logDTO); + + // CTI 로그 회원 매핑 업데이트 + int modCtiLogMember(HashMap paramMap); +} diff --git a/src/main/java/com/madeu/crm/callLog/service/CallLogService.java b/src/main/java/com/madeu/crm/callLog/service/CallLogService.java new file mode 100644 index 0000000..3a4c874 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/service/CallLogService.java @@ -0,0 +1,134 @@ +package com.madeu.crm.callLog.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.madeu.common.service.MenuAuthService; +import com.madeu.constants.Constants; +import com.madeu.crm.callLog.dto.CallLogDTO; +import com.madeu.crm.callLog.dto.CallLogSearchDTO; +import com.madeu.crm.callLog.dto.CallLogStatsDTO; +import com.madeu.crm.callLog.dto.CallMemoDTO; +import com.madeu.crm.callLog.map.CallLogMAP; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class CallLogService { + + @Autowired + private CallLogMAP callLogMAP; + + @Autowired + private MenuAuthService menuAuthService; + + /** + * 통화 로그 화면 이동 + */ + public HashMap moveCallLogList(HashMap paramMap) throws Exception { + log.debug("moveCallLogList START"); + + HashMap map = menuAuthService.getMenuAuthority(paramMap); + + log.debug("loginMemberId : " + paramMap.get("loginMemberId")); + log.debug("menuClass : " + paramMap.get("menuClass")); + log.debug("moveCallLogList END"); + return map; + } + + /** + * 통화 로그 목록 조회 (통계 포함) + */ + public HashMap getCallLogList(CallLogSearchDTO searchDTO) throws Exception { + log.debug("getCallLogList START"); + + HashMap map = new HashMap<>(); + + List listMap = new ArrayList<>(); + + int totalCount = callLogMAP.getCallLogCnt(searchDTO); + + if (0 < totalCount) { + listMap = callLogMAP.getCallLogList(searchDTO); + } + + // 통계 집계 + CallLogStatsDTO stats = callLogMAP.getCallLogStats(searchDTO); + + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("totalCount", totalCount); + map.put("rows", listMap); + map.put("stats", stats); + + log.debug("getCallLogList END"); + return map; + } + + /** + * 통화 메모 저장 + */ + public HashMap saveCallMemo(CallMemoDTO memoDTO) throws Exception { + log.debug("saveCallMemo START"); + + HashMap map = new HashMap<>(); + + int existCnt = callLogMAP.getCallMemoCnt(memoDTO); + int result = 0; + + if (existCnt > 0) { + // 기존 메모 수정 + result = callLogMAP.modCallMemo(memoDTO); + } else { + // 신규 메모 등록 + result = callLogMAP.saveCallMemo(memoDTO); + } + + if (0 < result) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("msgDesc", "메모가 저장되었습니다."); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + map.put("msgDesc", "메모 저장에 실패하였습니다."); + } + + log.debug("saveCallMemo END"); + return map; + } + + /** + * CTI 로그 저장 (Webhook에서 호출) + */ + public HashMap putCtiLog(CallLogDTO logDTO) throws Exception { + log.debug("putCtiLog START"); + + HashMap map = new HashMap<>(); + + int result = callLogMAP.putCtiLog(logDTO); + + if (0 < result) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + } + + log.debug("putCtiLog END"); + return map; + } + + /** + * CTI 로그 회원 매핑 + */ + public void updateCtiLogMember(HashMap paramMap) throws Exception { + callLogMAP.modCtiLogMember(paramMap); + } +} diff --git a/src/main/java/com/madeu/crm/callLog/service/GoodArsService.java b/src/main/java/com/madeu/crm/callLog/service/GoodArsService.java new file mode 100644 index 0000000..483d173 --- /dev/null +++ b/src/main/java/com/madeu/crm/callLog/service/GoodArsService.java @@ -0,0 +1,129 @@ +package com.madeu.crm.callLog.service; + +import java.util.HashMap; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import lombok.extern.slf4j.Slf4j; + +/** + * GoodARS CTI API 연동 서비스 + * + * 연동 서버: ars.goodars.co.kr + * 고객사 ID(CPID): application.properties에 설정 + */ +@Slf4j +@Service +public class GoodArsService { + + @Value("${goodars.cpid:5000036030017}") + private String arsCpid; + + @Value("${goodars.base-url:http://ars.goodars.co.kr/LINK/MADEU}") + private String arsBaseUrl; + + @Value("${goodars.base-url-ssl:https://ars.goodars.co.kr/LINK/MADEU}") + private String arsBaseUrlSsl; + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 아웃바운드 전화 발신 + * + * @param loginCid 상담원 전화번호 (내선번호) + * @param sendCid 매장 대표번호 + * @param userCid 고객 전화번호 + * @return 성공 여부 ("SUCCESS" = 성공) + */ + public HashMap makeOutboundCall(String loginCid, String sendCid, String userCid) { + log.debug("makeOutboundCall START - loginCid:{}, sendCid:{}, userCid:{}", loginCid, sendCid, userCid); + + HashMap result = new HashMap<>(); + + try { + String url = UriComponentsBuilder.fromHttpUrl(arsBaseUrl + "/wid_outcall.asp") + .queryParam("CPID", arsCpid) + .queryParam("LOGINCID", loginCid) + .queryParam("SENDCID", sendCid) + .queryParam("USERCID", userCid.replaceAll("[^0-9]", "")) + .toUriString(); + + log.debug("makeOutboundCall URL: {}", url); + + String response = restTemplate.getForObject(url, String.class); + log.debug("makeOutboundCall response: {}", response); + + if ("SUCCESS".equals(response)) { + result.put("success", true); + result.put("msgDesc", "전화 발신 중입니다."); + } else { + result.put("success", false); + result.put("msgDesc", "전화 발신에 실패하였습니다. (" + response + ")"); + } + } catch (Exception e) { + log.error("makeOutboundCall error", e); + result.put("success", false); + result.put("msgDesc", "전화 발신 중 오류가 발생하였습니다."); + } + + log.debug("makeOutboundCall END"); + return result; + } + + /** + * 녹음 파일 URL 생성 + * + * @param recordNo CTI 레코드 번호 + * @return 녹음 파일 URL + */ + public String getRecordFileUrl(String recordNo) { + return UriComponentsBuilder.fromHttpUrl(arsBaseUrl + "/wid_file.asp") + .queryParam("CPID", arsCpid) + .queryParam("SN", recordNo) + .toUriString(); + } + + /** + * 전화 돌려주기 (Push) + * + * @param loginCid 상담원 전화번호 + * @param pushCid 전달받을 상담원 전화번호 + * @return 성공 여부 + */ + public HashMap pushCall(String loginCid, String pushCid) { + log.debug("pushCall START - loginCid:{}, pushCid:{}", loginCid, pushCid); + + HashMap result = new HashMap<>(); + + try { + String url = UriComponentsBuilder.fromHttpUrl(arsBaseUrlSsl + "/wid_push.asp") + .queryParam("CPID", arsCpid) + .queryParam("LOGINCID", loginCid) + .queryParam("PUSHCID", pushCid) + .toUriString(); + + log.debug("pushCall URL: {}", url); + + String response = restTemplate.getForObject(url, String.class); + log.debug("pushCall response: {}", response); + + if ("1".equals(response)) { + result.put("success", true); + result.put("msgDesc", "전화가 연결되었습니다."); + } else { + result.put("success", false); + result.put("msgDesc", "연결에 실패하였습니다."); + } + } catch (Exception e) { + log.error("pushCall error", e); + result.put("success", false); + result.put("msgDesc", "전화 연결 중 오류가 발생하였습니다."); + } + + log.debug("pushCall END"); + return result; + } +} diff --git a/src/main/java/com/madeu/crm/smsTemplate/ctrl/SmsTemplateController.java b/src/main/java/com/madeu/crm/smsTemplate/ctrl/SmsTemplateController.java new file mode 100644 index 0000000..d15a787 --- /dev/null +++ b/src/main/java/com/madeu/crm/smsTemplate/ctrl/SmsTemplateController.java @@ -0,0 +1,201 @@ +package com.madeu.crm.smsTemplate.ctrl; + +import java.util.HashMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import com.madeu.constants.Constants; +import com.madeu.crm.smsTemplate.dto.SmsTemplateDTO; +import com.madeu.crm.smsTemplate.dto.SmsTemplateSearchDTO; +import com.madeu.crm.smsTemplate.service.SmsTemplateService; +import com.madeu.init.ManagerDraftAction; +import com.madeu.util.HttpUtil; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Controller +@RequestMapping("/smsTemplate") +public class SmsTemplateController extends ManagerDraftAction { + + @Autowired + private SmsTemplateService smsTemplateService; + + /** + * 문자 상용구 관리 화면으로 이동 + */ + @RequestMapping("/moveSmsTemplateList.do") + public String moveSmsTemplateList(HttpSession session, HttpServletRequest request, HttpServletResponse response, + Model model) { + log.debug("SmsTemplateController moveSmsTemplateList START"); + + HashMap paramMap = HttpUtil.getParameterMap(request); + HashMap map = null; + + try { + if (!webCheckLogin(session)) { + return "/web/login/logout"; + } else { + paramMap.put("loginMemberId", String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.moveSmsTemplateList(paramMap); + model.addAttribute("selectUseYn", map.get("selectUseYn")); + model.addAttribute("insertUseYn", map.get("insertUseYn")); + model.addAttribute("updateUseYn", map.get("updateUseYn")); + model.addAttribute("deleteUseYn", map.get("deleteUseYn")); + model.addAttribute("downloadUseYn", map.get("downloadUseYn")); + } + } catch (Exception e) { + e.printStackTrace(); + return "/web/login/logout"; + } + log.debug("SmsTemplateController moveSmsTemplateList END"); + + return "/web/smsTemplate/smsTemplateSelectList"; + } + + /** + * 문자 상용구 목록 조회 + */ + @PostMapping("/getSmsTemplateList.do") + public ModelAndView getSmsTemplateList(HttpSession session, HttpServletRequest request, + HttpServletResponse response, SmsTemplateSearchDTO searchDTO) { + log.debug("SmsTemplateController getSmsTemplateList START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + searchDTO.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.getSmsTemplateList(searchDTO); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "서버 오류가 발생했습니다."); + } finally { + if (Constants.OK != map.get("msgCode")) { + map.put("msgCode", Constants.FAIL); + map.put("success", false); + if (null == map.get("msgDesc") || ("").equals(map.get("msgDesc"))) { + map.put("msgDesc", "정상적으로 수행되지 않았습니다. 관리자에게 문의하시기 바랍니다."); + } + } + } + log.debug("SmsTemplateController getSmsTemplateList END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 문자 상용구 상세 조회 + */ + @PostMapping("/getSmsTemplate.do") + public ModelAndView getSmsTemplate(HttpSession session, HttpServletRequest request, + HttpServletResponse response, SmsTemplateDTO dto) { + log.debug("SmsTemplateController getSmsTemplate START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + dto.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.getSmsTemplate(dto); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "조회 중 오류가 발생하였습니다."); + } + log.debug("SmsTemplateController getSmsTemplate END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 문자 상용구 등록 + */ + @PostMapping("/putSmsTemplate.do") + public ModelAndView putSmsTemplate(HttpSession session, HttpServletRequest request, + HttpServletResponse response, SmsTemplateDTO dto) { + log.debug("SmsTemplateController putSmsTemplate START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + dto.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.putSmsTemplate(dto); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "등록 중 오류가 발생하였습니다."); + } + log.debug("SmsTemplateController putSmsTemplate END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 문자 상용구 수정 + */ + @PostMapping("/modSmsTemplate.do") + public ModelAndView modSmsTemplate(HttpSession session, HttpServletRequest request, + HttpServletResponse response, SmsTemplateDTO dto) { + log.debug("SmsTemplateController modSmsTemplate START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + dto.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.modSmsTemplate(dto); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "수정 중 오류가 발생하였습니다."); + } + log.debug("SmsTemplateController modSmsTemplate END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } + + /** + * 문자 상용구 삭제 + */ + @PostMapping("/delSmsTemplate.do") + public ModelAndView delSmsTemplate(HttpSession session, HttpServletRequest request, + HttpServletResponse response, SmsTemplateDTO dto) { + log.debug("SmsTemplateController delSmsTemplate START"); + + HashMap map = new HashMap<>(); + + try { + if (!webCheckLogin(session)) { + return null; + } else { + dto.setLoginMemberId(String.valueOf(session.getAttribute("loginMemberId"))); + map = smsTemplateService.delSmsTemplate(dto); + } + } catch (Exception e) { + e.printStackTrace(); + map.put("msgCode", Constants.FAIL); + map.put("msgDesc", "삭제 중 오류가 발생하였습니다."); + } + log.debug("SmsTemplateController delSmsTemplate END"); + return HttpUtil.makeHashToJsonModelAndView(map); + } +} diff --git a/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateDTO.java b/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateDTO.java new file mode 100644 index 0000000..35d27b3 --- /dev/null +++ b/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateDTO.java @@ -0,0 +1,27 @@ +package com.madeu.crm.smsTemplate.dto; + +import lombok.Data; + +/** + * SMS 상용구 결과 DTO + */ +@Data +public class SmsTemplateDTO { + /* ---- 테이블 필드 ---- */ + private String muSmsTemplateId; + private String title; + private String content; + private String useYn; + private String cudFlag; + private String regId; + private String regDate; + private String modId; + private String modDate; + + /* ---- 조인/서브쿼리 결과 필드 ---- */ + private String regName; // 등록자명 + private String rowNum; // 행 번호 + + /* ---- 세션/공통 ---- */ + private String loginMemberId; +} diff --git a/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateSearchDTO.java b/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateSearchDTO.java new file mode 100644 index 0000000..e2d6c4e --- /dev/null +++ b/src/main/java/com/madeu/crm/smsTemplate/dto/SmsTemplateSearchDTO.java @@ -0,0 +1,21 @@ +package com.madeu.crm.smsTemplate.dto; + +import lombok.Data; + +/** + * SMS 상용구 검색 조건 DTO + */ +@Data +public class SmsTemplateSearchDTO { + /* ---- 검색 조건 ---- */ + private String searchKeyword; + + /* ---- 페이징 ---- */ + private Integer gridLimitStart; + private Integer gridLimitEnd; + private String gridSort; + + /* ---- 권한/세션 ---- */ + private String menuClass; + private String loginMemberId; +} diff --git a/src/main/java/com/madeu/crm/smsTemplate/map/SmsTemplateMAP.java b/src/main/java/com/madeu/crm/smsTemplate/map/SmsTemplateMAP.java new file mode 100644 index 0000000..382f7f6 --- /dev/null +++ b/src/main/java/com/madeu/crm/smsTemplate/map/SmsTemplateMAP.java @@ -0,0 +1,29 @@ +package com.madeu.crm.smsTemplate.map; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; + +import com.madeu.crm.smsTemplate.dto.SmsTemplateDTO; +import com.madeu.crm.smsTemplate.dto.SmsTemplateSearchDTO; + +@Mapper +public interface SmsTemplateMAP { + // 상용구 목록 건수 조회 + int getSmsTemplateCnt(SmsTemplateSearchDTO searchDTO); + + // 상용구 목록 조회 + List getSmsTemplateList(SmsTemplateSearchDTO searchDTO); + + // 상용구 상세 조회 + SmsTemplateDTO getSmsTemplate(SmsTemplateDTO dto); + + // 상용구 등록 + int putSmsTemplate(SmsTemplateDTO dto); + + // 상용구 수정 + int modSmsTemplate(SmsTemplateDTO dto); + + // 상용구 삭제 (논리 삭제) + int delSmsTemplate(SmsTemplateDTO dto); +} diff --git a/src/main/java/com/madeu/crm/smsTemplate/service/SmsTemplateService.java b/src/main/java/com/madeu/crm/smsTemplate/service/SmsTemplateService.java new file mode 100644 index 0000000..4672699 --- /dev/null +++ b/src/main/java/com/madeu/crm/smsTemplate/service/SmsTemplateService.java @@ -0,0 +1,162 @@ +package com.madeu.crm.smsTemplate.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.madeu.common.service.MenuAuthService; +import com.madeu.constants.Constants; +import com.madeu.crm.smsTemplate.dto.SmsTemplateDTO; +import com.madeu.crm.smsTemplate.dto.SmsTemplateSearchDTO; +import com.madeu.crm.smsTemplate.map.SmsTemplateMAP; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class SmsTemplateService { + + @Autowired + private SmsTemplateMAP smsTemplateMAP; + + @Autowired + private MenuAuthService menuAuthService; + + /** + * 상용구 관리 화면 이동 + */ + public HashMap moveSmsTemplateList(HashMap paramMap) throws Exception { + log.debug("moveSmsTemplateList START"); + + HashMap map = menuAuthService.getMenuAuthority(paramMap); + + log.debug("loginMemberId : " + paramMap.get("loginMemberId")); + log.debug("menuClass : " + paramMap.get("menuClass")); + log.debug("moveSmsTemplateList END"); + return map; + } + + /** + * 상용구 목록 조회 + */ + public HashMap getSmsTemplateList(SmsTemplateSearchDTO searchDTO) throws Exception { + log.debug("getSmsTemplateList START"); + + HashMap map = new HashMap<>(); + + List listMap = new ArrayList<>(); + + int totalCount = smsTemplateMAP.getSmsTemplateCnt(searchDTO); + + if (0 < totalCount) { + listMap = smsTemplateMAP.getSmsTemplateList(searchDTO); + } + + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("totalCount", totalCount); + map.put("rows", listMap); + + log.debug("getSmsTemplateList END"); + return map; + } + + /** + * 상용구 상세 조회 + */ + public HashMap getSmsTemplate(SmsTemplateDTO dto) throws Exception { + log.debug("getSmsTemplate START"); + + HashMap map = new HashMap<>(); + + SmsTemplateDTO detail = smsTemplateMAP.getSmsTemplate(dto); + + if (detail != null) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("data", detail); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + map.put("msgDesc", "상용구 정보를 찾을 수 없습니다."); + } + + log.debug("getSmsTemplate END"); + return map; + } + + /** + * 상용구 등록 + */ + public HashMap putSmsTemplate(SmsTemplateDTO dto) throws Exception { + log.debug("putSmsTemplate START"); + + HashMap map = new HashMap<>(); + + int result = smsTemplateMAP.putSmsTemplate(dto); + + if (0 < result) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("msgDesc", "상용구가 등록되었습니다."); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + map.put("msgDesc", "상용구 등록에 실패하였습니다."); + } + + log.debug("putSmsTemplate END"); + return map; + } + + /** + * 상용구 수정 + */ + public HashMap modSmsTemplate(SmsTemplateDTO dto) throws Exception { + log.debug("modSmsTemplate START"); + + HashMap map = new HashMap<>(); + + int result = smsTemplateMAP.modSmsTemplate(dto); + + if (0 < result) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("msgDesc", "상용구가 수정되었습니다."); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + map.put("msgDesc", "상용구 수정에 실패하였습니다."); + } + + log.debug("modSmsTemplate END"); + return map; + } + + /** + * 상용구 삭제 (논리 삭제) + */ + public HashMap delSmsTemplate(SmsTemplateDTO dto) throws Exception { + log.debug("delSmsTemplate START"); + + HashMap map = new HashMap<>(); + + int result = smsTemplateMAP.delSmsTemplate(dto); + + if (0 < result) { + map.put("msgCode", Constants.OK); + map.put("success", "true"); + map.put("msgDesc", "상용구가 삭제되었습니다."); + } else { + map.put("msgCode", Constants.FAIL); + map.put("success", "false"); + map.put("msgDesc", "상용구 삭제에 실패하였습니다."); + } + + log.debug("delSmsTemplate END"); + return map; + } +} diff --git a/src/main/resources/mappers/crm/callLog/CallLogSql.xml b/src/main/resources/mappers/crm/callLog/CallLogSql.xml new file mode 100644 index 0000000..f9c54ca --- /dev/null +++ b/src/main/resources/mappers/crm/callLog/CallLogSql.xml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + SELECT CONCAT('CTXT', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_CTI_TEXT_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0')) + FROM MU_CTI_TEXT + + /** CallLogMAP.saveCallMemo **/ + INSERT INTO MU_CTI_TEXT ( + MU_CTI_TEXT_ID + ,RECORD_NO + ,CALL_MSG + ,USE_YN + ,CUD_FLAG + ,REG_ID + ,REG_DATE + ,MOD_ID + ,MOD_DATE + ) VALUES ( + #{muCtiTextId} + ,#{recordNo} + ,#{callMsg} + ,'Y' + ,'C' + ,#{loginMemberId} + ,NOW() + ,#{loginMemberId} + ,NOW() + ) + + + + + /** CallLogMAP.modCallMemo **/ + UPDATE MU_CTI_TEXT + SET CALL_MSG = #{callMsg} + ,CUD_FLAG = 'U' + ,MOD_ID = #{loginMemberId} + ,MOD_DATE = NOW() + WHERE RECORD_NO = #{recordNo} + AND USE_YN = 'Y' + + + + + + SELECT CONCAT('CLOG', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_CTI_LOG_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0')) + FROM MU_CTI_LOG + + /** CallLogMAP.putCtiLog **/ + INSERT INTO MU_CTI_LOG ( + MU_CTI_LOG_ID + ,RECORD_NO + ,CID + ,FULLDNIS + ,BOUND + ,MENU_NO + ,STATE_TYPE + ,CTI_STEP + ,LOGINCID + ,C_TYPE1 + ,C_TYPE2 + ,CRM_MB_PID + ,STORE_PID + ,INCALL_SEL + ,USE_YN + ,CUD_FLAG + ,REG_DATE + ,MOD_DATE + ) VALUES ( + #{muCtiLogId} + ,#{recordNo} + ,#{cid} + ,#{fulldnis} + ,#{bound} + ,#{menuNo} + ,#{stateType} + ,#{ctiStep} + ,#{logincid} + ,#{cType1} + ,#{cType2} + ,#{crmMbPid} + ,#{storePid} + ,#{incallSel} + ,'Y' + ,'C' + ,NOW() + ,NOW() + ) + + + + + /** CallLogMAP.modCtiLogMember **/ + UPDATE MU_CTI_LOG + SET STORE_PID = #{storePid} + ,CRM_MB_PID = #{crmMbPid} + ,MOD_DATE = NOW() + WHERE RECORD_NO = #{recordNo} + AND USE_YN = 'Y' + + + diff --git a/src/main/resources/mappers/crm/smsTemplate/SmsTemplateSql.xml b/src/main/resources/mappers/crm/smsTemplate/SmsTemplateSql.xml new file mode 100644 index 0000000..d167c9e --- /dev/null +++ b/src/main/resources/mappers/crm/smsTemplate/SmsTemplateSql.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + SELECT CONCAT('SMST', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_SMS_TEMPLATE_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0')) + FROM MU_SMS_TEMPLATE + + /** SmsTemplateMAP.putSmsTemplate **/ + INSERT INTO MU_SMS_TEMPLATE ( + MU_SMS_TEMPLATE_ID + ,TITLE + ,CONTENT + ,USE_YN + ,CUD_FLAG + ,REG_ID + ,REG_DATE + ,MOD_ID + ,MOD_DATE + ) VALUES ( + #{muSmsTemplateId} + ,#{title} + ,#{content} + ,'Y' + ,'C' + ,#{loginMemberId} + ,NOW() + ,#{loginMemberId} + ,NOW() + ) + + + + + /** SmsTemplateMAP.modSmsTemplate **/ + UPDATE MU_SMS_TEMPLATE + SET TITLE = #{title} + ,CONTENT = #{content} + ,CUD_FLAG = 'U' + ,MOD_ID = #{loginMemberId} + ,MOD_DATE = NOW() + WHERE USE_YN = 'Y' + AND MU_SMS_TEMPLATE_ID = #{muSmsTemplateId} + + + + + /** SmsTemplateMAP.delSmsTemplate **/ + UPDATE MU_SMS_TEMPLATE + SET USE_YN = 'N' + ,CUD_FLAG = 'D' + ,MOD_ID = #{loginMemberId} + ,MOD_DATE = NOW() + WHERE USE_YN = 'Y' + AND MU_SMS_TEMPLATE_ID = #{muSmsTemplateId} + + + diff --git a/src/main/resources/static/css/web/call_log.css b/src/main/resources/static/css/web/call_log.css new file mode 100644 index 0000000..671d69b --- /dev/null +++ b/src/main/resources/static/css/web/call_log.css @@ -0,0 +1,350 @@ +/* ============================================ + 통화 로그 관리 (call_log.css) + ============================================ */ + +/* ---- 검색 필터 영역 ---- */ +.cl_filter_wrap { + background: #fff; + border: 1px solid #E9ECF0; + border-radius: 5px; + padding: 12px 16px; + margin-bottom: 10px; + clear: both; +} + +.cl_filter_row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.cl_filter_row+.cl_filter_row { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #F0F1F3; +} + +.cl_filter_item { + display: flex; + align-items: center; + gap: 6px; +} + +.cl_filter_item label { + font-size: 13px; + font-weight: 600; + color: #555; + white-space: nowrap; + min-width: 55px; +} + +.cl_filter_item select, +.cl_filter_item .cl_select { + height: 32px; + padding: 0 8px; + border: 1px solid #E9ECF0; + border-radius: 4px; + font-size: 13px; + color: #333; + background: #fff url(/image/web/select_arrow.svg) no-repeat 95% 55%/18px auto; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding-right: 28px; + min-width: 90px; +} + +.cl_filter_item input[type="text"] { + height: 32px; + padding: 0 10px; + border: 1px solid #E9ECF0; + border-radius: 4px; + font-size: 13px; + width: 140px; +} + +.cl_filter_item input[type="text"]::placeholder { + color: #B5BDC4; +} + +/* 날짜 필터 */ +.cl_date_item input[type="date"] { + height: 32px; + padding: 0 8px; + border: 1px solid #E9ECF0; + border-radius: 4px; + font-size: 13px; + color: #333; +} + +.cl_date_sep { + color: #888; + font-size: 13px; + margin: 0 2px; +} + +/* 날짜 단축 버튼 */ +.cl_quick_btns { + display: flex; + gap: 4px; +} + +.cl_quick_btns button { + height: 30px; + padding: 0 10px; + border: 1px solid #D5D8DC; + border-radius: 4px; + background: #FAFBFC; + color: #555; + font-size: 12px; + cursor: pointer; + white-space: nowrap; + transition: all 0.15s; +} + +.cl_quick_btns button:hover { + background: #3985EA; + color: #fff; + border-color: #3985EA; +} + +/* 조회 버튼 */ +.cl_search_btn { + height: 32px; + padding: 0 20px; + background: #3985EA; + color: #fff; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + margin-left: auto; +} + +.cl_search_btn:hover { + background: #2D6CC0; +} + +/* ---- 통계 요약 바 ---- */ +.cl_stats_bar { + display: flex; + align-items: center; + gap: 10px; + background: #F7F9FC; + border: 1px solid #E9ECF0; + border-radius: 5px; + padding: 10px 16px; + margin-bottom: 10px; + font-size: 13px; + color: #555; + flex-wrap: wrap; +} + +.cl_stats_bar strong { + color: #3985EA; + font-weight: 700; +} + +.cl_stats_sep { + color: #D5D8DC; +} + +.cl_stats_detail { + font-size: 12px; + color: #888; +} + +.cl_stats_detail strong { + color: #333; + font-weight: 600; +} + +/* ---- ag-Grid 영역 ---- */ +.cl_grid_box { + width: 100%; + height: calc(100% - 230px); + min-height: 300px; + background: #fff; + border: solid 1px #E9ECF0; + border-radius: 5px; + overflow: hidden; +} + +/* ---- 메모 팝업 ---- */ +.cl_memo_overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.35); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} + +.cl_memo_popup { + background: #fff; + border-radius: 8px; + width: 480px; + max-width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); + overflow: hidden; +} + +.cl_memo_header { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 20px; + border-bottom: 1px solid #E9ECF0; +} + +.cl_memo_header h3 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #333; +} + +.cl_memo_record { + font-size: 12px; + color: #888; +} + +.cl_memo_close { + margin-left: auto; + background: none; + border: none; + font-size: 22px; + color: #999; + cursor: pointer; + line-height: 1; +} + +.cl_memo_close:hover { + color: #333; +} + +.cl_memo_body { + padding: 16px 20px; +} + +.cl_memo_body textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid #E9ECF0; + border-radius: 5px; + font-size: 14px; + resize: vertical; + min-height: 120px; + line-height: 1.6; + box-sizing: border-box; +} + +.cl_memo_body textarea:focus { + border-color: #3985EA; + outline: none; + box-shadow: 0 0 0 2px rgba(57, 133, 234, 0.12); +} + +.cl_memo_footer { + display: flex; + gap: 8px; + padding: 12px 20px 16px; + justify-content: flex-end; +} + +.cl_memo_save_btn { + height: 34px; + padding: 0 20px; + border: none; + border-radius: 5px; + background: #3985EA; + color: #fff; + font-size: 13px; + cursor: pointer; +} + +.cl_memo_save_btn:hover { + background: #2D6CC0; +} + +.cl_memo_cancel_btn { + height: 34px; + padding: 0 16px; + border: 1px solid #E9ECF0; + border-radius: 5px; + background: #fff; + color: #666; + font-size: 13px; + cursor: pointer; +} + +.cl_memo_cancel_btn:hover { + background: #F7F8FA; +} + +/* ---- 그리드 내 버튼/뱃지 ---- */ +.cl_play_btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: 1px solid #3985EA; + border-radius: 50%; + background: #fff; + color: #3985EA; + font-size: 11px; + cursor: pointer; + transition: all 0.15s; +} + +.cl_play_btn:hover { + background: #3985EA; + color: #fff; +} + +.cl_incall_badge { + display: inline-block; + padding: 1px 6px; + margin: 1px 2px; + background: #EEF3FB; + color: #3366AA; + border-radius: 3px; + font-size: 11px; + white-space: nowrap; +} + +/* ---- 반응형 ---- */ +@media screen and (max-width: 1500px) { + .cl_filter_row { + gap: 8px; + } + + .cl_filter_item label { + min-width: 45px; + font-size: 12px; + } + + .cl_filter_item select, + .cl_filter_item .cl_select { + min-width: 80px; + font-size: 12px; + } + + .cl_filter_item input[type="text"] { + width: 120px; + font-size: 12px; + } + + .cl_quick_btns button { + padding: 0 7px; + font-size: 11px; + } +} \ No newline at end of file diff --git a/src/main/resources/static/css/web/sms_template.css b/src/main/resources/static/css/web/sms_template.css new file mode 100644 index 0000000..6d92076 --- /dev/null +++ b/src/main/resources/static/css/web/sms_template.css @@ -0,0 +1,285 @@ +/* ============================================ + 문자 상용구 관리 - 좌측 목록 / 우측 상세 + ============================================ */ + +/* ---- 2패널 컨테이너 ---- */ +.sms_two_panel { + display: flex; + gap: 10px; + height: calc(100% - 60px); + min-height: 400px; + clear: both; +} + +/* ---- 좌측 목록 패널 ---- */ +.sms_left_panel { + width: 45%; + min-width: 360px; + display: flex; + flex-direction: column; + border: 1px solid #E9ECF0; + border-radius: 5px; + background: #fff; + overflow: hidden; +} + +/* 검색 영역 */ +.sms_search_area { + display: flex; + gap: 5px; + padding: 10px; + border-bottom: 1px solid #E9ECF0; + align-items: center; +} + +.sms_search_box { + flex: 1; + position: relative; + height: 36px; +} + +.sms_search_box img { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 8px; + z-index: 1; + width: 18px; +} + +.sms_search_box input { + width: 100%; + height: 36px; + border: 1px solid #E9ECF0; + border-radius: 5px; + padding: 0 10px 0 32px; + font-size: 13px; + background: #fff; +} + +.sms_search_box input::placeholder { + color: #B5BDC4; +} + +.sms_search_btn { + height: 36px; + padding: 0 14px; + background: #3985EA; + color: #fff; + border: none; + border-radius: 5px; + font-size: 13px; + cursor: pointer; + white-space: nowrap; +} + +.sms_search_btn:hover { + background: #2D6CC0; +} + +.sms_new_btn { + height: 36px; + padding: 0 14px; + background: #fff; + color: #3985EA; + border: 1px solid #3985EA; + border-radius: 5px; + font-size: 13px; + cursor: pointer; + white-space: nowrap; +} + +.sms_new_btn:hover { + background: #3985EA; + color: #fff; +} + +/* ag-Grid 영역 */ +.sms_grid_box { + flex: 1; + width: 100%; +} + +/* ---- 우측 상세 패널 ---- */ +.sms_right_panel { + flex: 1; + display: flex; + flex-direction: column; + border: 1px solid #E9ECF0; + border-radius: 5px; + background: #fff; + overflow: hidden; +} + +/* 빈 상태 */ +.sms_empty_state { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.sms_empty_state p { + text-align: center; + color: #B5BDC4; + font-size: 14px; + line-height: 2; +} + +/* 상세 폼 */ +.sms_detail_form { + flex: 1; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.sms_detail_title_bar { + padding: 14px 20px; + border-bottom: 1px solid #E9ECF0; +} + +.sms_detail_title_bar h3 { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #333; +} + +.sms_form_row { + padding: 0 20px; + margin-top: 14px; +} + +.sms_form_row label { + display: block; + margin-bottom: 5px; + font-size: 13px; + font-weight: 600; + color: #555; +} + +.sms_form_row input[type="text"] { + width: 100%; + height: 38px; + padding: 0 12px; + border: 1px solid #E9ECF0; + border-radius: 5px; + font-size: 14px; + box-sizing: border-box; +} + +.sms_form_row textarea { + width: 100%; + padding: 10px 12px; + border: 1px solid #E9ECF0; + border-radius: 5px; + font-size: 14px; + resize: vertical; + min-height: 160px; + line-height: 1.6; + box-sizing: border-box; +} + +.sms_form_row input:focus, +.sms_form_row textarea:focus { + border-color: #3985EA; + outline: none; + box-shadow: 0 0 0 2px rgba(57, 133, 234, 0.12); +} + +.byte_info { + text-align: right; + margin-top: 5px; + font-size: 12px; + color: #888; +} + +.type_badge { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + margin-left: 4px; + vertical-align: middle; +} + +.type_badge.sms { + background: #E8F5E9; + color: #2E7D32; +} + +.type_badge.lms { + background: #FFF3E0; + color: #E65100; +} + +.sms_info_row { + display: flex; + gap: 16px; + padding: 10px 20px !important; + margin-top: 10px !important; + border-top: 1px solid #F0F1F3; + font-size: 12px; + color: #888; +} + +.sms_info_row strong { + font-weight: 500; + color: #555; +} + +.sms_btn_group { + display: flex; + gap: 8px; + padding: 14px 20px; + border-top: 1px solid #E9ECF0; + margin-top: auto; +} + +.sms_save_btn { + height: 36px; + padding: 0 24px; + border: none; + border-radius: 5px; + background: #3985EA; + color: #fff; + font-size: 13px; + cursor: pointer; +} + +.sms_save_btn:hover { + background: #2D6CC0; +} + +.sms_delete_btn { + height: 36px; + padding: 0 18px; + border: 1px solid #FF2222; + border-radius: 5px; + background: transparent; + color: #FF2222; + font-size: 13px; + cursor: pointer; +} + +.sms_delete_btn:hover { + background: #FF2222; + color: #fff; +} + +.sms_cancel_btn { + height: 36px; + padding: 0 18px; + border: 1px solid #E9ECF0; + border-radius: 5px; + background: #fff; + color: #666; + font-size: 13px; + cursor: pointer; +} + +.sms_cancel_btn:hover { + background: #F7F8FA; +} \ No newline at end of file diff --git a/src/main/resources/static/js/web/callLog/callLogSelectList.js b/src/main/resources/static/js/web/callLog/callLogSelectList.js new file mode 100644 index 0000000..a6517d5 --- /dev/null +++ b/src/main/resources/static/js/web/callLog/callLogSelectList.js @@ -0,0 +1,427 @@ +/** + * 통화 로그 관리 - ag-Grid + */ +$(document).ready(function () { + fn_initGrid(); + fn_init(); + fn_setToday(); + fn_loadCallLogList(); +}); + +/* ============================================ + 전역 변수 + ============================================ */ +var gridApi = null; + +/* ARS 메뉴 번호 → 텍스트 매핑 */ +var arsMenuMap = { + '1': '상담', + '2': '예약', + '3': '위치안내' +}; + +/* ============================================ + ag-Grid 초기화 + ============================================ */ +function fn_initGrid() { + var columnDefs = [ + { + headerName: 'No', + valueGetter: function (params) { + return params.node.rowIndex + 1; + }, + width: 60, + maxWidth: 60, + cellStyle: { textAlign: 'center' }, + suppressSizeToFit: true + }, + { + headerName: 'RECORD NO', + field: 'recordNo', + width: 100, + maxWidth: 110, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '구분1', + field: 'menuText', + width: 100, + maxWidth: 120, + cellStyle: { textAlign: 'center' }, + valueGetter: function (params) { + var d = params.data; + if (!d) return ''; + if (d.bound === 'OUT') return 'OUT'; + if (d.callbackCid) return 'Call Back'; + return arsMenuMap[d.ring] || ''; + }, + cellRenderer: function (params) { + if (!params.data) return ''; + if (params.data.bound === 'OUT') { + return 'OUT'; + } + if (params.data.callbackCid) { + return 'Call Back
' + params.data.callbackCid + ''; + } + return params.value || ''; + } + }, + { + headerName: '구분2', + field: 'cType1', + width: 65, + maxWidth: 80, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '구분3', + field: 'cType2', + width: 80, + maxWidth: 100, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '고객명', + field: 'memberName', + width: 90, + maxWidth: 120, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '전화번호', + field: 'cid', + width: 120, + maxWidth: 140, + cellStyle: { textAlign: 'center' }, + valueFormatter: function (params) { + return fn_formatPhone(params.value); + } + }, + { + headerName: '전화 온 시간', + field: 'regDate', + width: 145, + maxWidth: 160, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '통화 시작', + field: 'linkDate', + width: 145, + maxWidth: 160, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '통화 종료', + valueGetter: function (params) { + if (!params.data) return ''; + return params.data.linkEndDate || params.data.bendDate || ''; + }, + width: 145, + maxWidth: 160, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '상담 전화번호', + field: 'linkCid', + width: 110, + maxWidth: 130, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '통화파일', + field: 'recordNo', + width: 80, + maxWidth: 90, + cellStyle: { textAlign: 'center' }, + cellRenderer: function (params) { + if (!params.data || !params.data.recordNo || !params.data.linkDate) return ''; + var btn = ''; + return btn; + } + }, + { + headerName: 'In Call', + field: 'incallSel', + width: 150, + minWidth: 100, + cellStyle: { textAlign: 'left' }, + cellRenderer: function (params) { + var val = params.value; + if (!val) return ''; + // incallSel은 '|=|값1|=||=|값2|=|' 형식 + var items = val.split('|=|').filter(function (v) { return v.trim() !== ''; }); + if (items.length === 0) return ''; + return items.map(function (item) { + return '' + item + ''; + }).join(' '); + } + }, + { + headerName: '메모', + field: 'callMsg', + flex: 1, + minWidth: 120, + cellStyle: { textAlign: 'left', cursor: 'pointer' }, + cellRenderer: function (params) { + var msg = params.value || ''; + // HTML 태그 제거하여 표시 + var plainText = msg.replace(/<[^>]+>/g, '').trim(); + if (plainText.length > 30) { + plainText = plainText.substring(0, 30) + '...'; + } + return plainText || '클릭하여 메모 입력'; + } + } + ]; + + var gridOptions = { + columnDefs: columnDefs, + rowData: [], + rowSelection: 'single', + animateRows: true, + headerHeight: 36, + rowHeight: 40, + suppressCellFocus: true, + overlayNoRowsTemplate: '검색된 통화 내역이 없습니다.', + onCellClicked: function (event) { + if (event.colDef.field === 'callMsg') { + fn_openMemo(event.data); + } + }, + defaultColDef: { + resizable: true, + sortable: true + } + }; + + var gridDiv = document.querySelector('#callLogGrid'); + gridApi = new agGrid.Grid(gridDiv, gridOptions); + gridApi = gridOptions.api; +} + +/* ============================================ + 초기화 (이벤트 바인딩) + ============================================ */ +function fn_init() { + $('#searchBtn').on('click', function () { + fn_loadCallLogList(); + }); + + $('#mCid').on('keypress', function (e) { + if (e.which === 13) fn_loadCallLogList(); + }); + + $('#sDate, #eDate').on('change', function () { + // 날짜 변경 시 자동 검색하지 않음 (사용자가 조회 버튼 클릭) + }); +} + +/* ============================================ + 오늘 날짜 설정 + ============================================ */ +function fn_setToday() { + var today = fn_getDateStr(new Date()); + $('#sDate').val(today); + $('#eDate').val(today); +} + +/* ============================================ + 날짜 단축 버튼 + ============================================ */ +function fn_dayChk(type) { + var now = new Date(); + var sDate, eDate; + + switch (type) { + case 't': // 오늘 + sDate = new Date(); + eDate = new Date(); + break; + case 'p': // 어제 + sDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + eDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + break; + case 'w': // 이번주 (월요일~오늘) + var dayOfWeek = now.getDay(); + var diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + sDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff); + eDate = new Date(); + break; + case 'pw': // 지난주 (월요일~일요일) + var dayOfWeek = now.getDay(); + var diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + var thisMonday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff); + sDate = new Date(thisMonday.getFullYear(), thisMonday.getMonth(), thisMonday.getDate() - 7); + eDate = new Date(thisMonday.getFullYear(), thisMonday.getMonth(), thisMonday.getDate() - 1); + break; + case 'm': // 이번달 + sDate = new Date(now.getFullYear(), now.getMonth(), 1); + eDate = new Date(); + break; + case 'pm': // 지난달 + sDate = new Date(now.getFullYear(), now.getMonth() - 1, 1); + eDate = new Date(now.getFullYear(), now.getMonth(), 0); + break; + default: + return; + } + + $('#sDate').val(fn_getDateStr(sDate)); + $('#eDate').val(fn_getDateStr(eDate)); + fn_loadCallLogList(); +} + +/* ============================================ + 목록 조회 + ============================================ */ +function fn_loadCallLogList() { + var param = { + sDate: $('#sDate').val(), + eDate: $('#eDate').val(), + callType: $('#callType').val(), + callType1: $('#callType1').val(), + callType2: $('#callType2').val(), + mCid: $('#mCid').val().replace(/[^0-9]/g, ''), + fulldnis: '', // TODO: 매장별 수신번호 설정 필요 + menuClass: menuClass + }; + + $.ajax({ + url: '/callLog/getCallLogList.do', + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + var rows = data.rows || []; + gridApi.setRowData(rows); + + if (rows.length === 0) { + gridApi.showNoRowsOverlay(); + } + + // 통계 업데이트 + fn_updateStats(data.stats); + }, + error: function () { + alert('목록 조회 중 오류가 발생하였습니다.'); + } + }); +} + +/* ============================================ + 통계 업데이트 + ============================================ */ +function fn_updateStats(stats) { + if (!stats) { + $('#statTotal, #statIn, #statOut, #statType1, #statType2, #statType11, #statType12, #statType13').text('0'); + return; + } + + $('#statTotal').text(fn_numberFormat(stats.totalCnt || 0)); + $('#statIn').text(fn_numberFormat(stats.inCnt || 0)); + $('#statOut').text(fn_numberFormat(stats.outCnt || 0)); + $('#statType1').text(fn_numberFormat(stats.type1Cnt || 0)); + $('#statType2').text(fn_numberFormat(stats.type2Cnt || 0)); + $('#statType11').text(fn_numberFormat(stats.type11Cnt || 0)); + $('#statType12').text(fn_numberFormat(stats.type12Cnt || 0)); + $('#statType13').text(fn_numberFormat(stats.type13Cnt || 0)); +} + +/* ============================================ + 메모 팝업 + ============================================ */ +function fn_openMemo(data) { + if (!data || !data.recordNo) return; + + $('#memoRecordNoVal').val(data.recordNo); + $('#memoRecordNo').text('RECORD NO: ' + data.recordNo); + $('#memoContent').val(data.callMsg || ''); + $('#memoOverlay').fadeIn(200); + $('#memoContent').focus(); +} + +function fn_closeMemo() { + $('#memoOverlay').fadeOut(200); +} + +function fn_saveMemo() { + var recordNo = $('#memoRecordNoVal').val(); + var callMsg = $.trim($('#memoContent').val()); + + if (!recordNo) return; + + var param = { + recordNo: recordNo, + callMsg: callMsg, + menuClass: menuClass + }; + + $.ajax({ + url: '/callLog/saveCallMemo.do', + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + alert(data.msgDesc || '저장되었습니다.'); + fn_closeMemo(); + fn_loadCallLogList(); + }, + error: function () { + alert('메모 저장 중 오류가 발생하였습니다.'); + } + }); +} + +/* ============================================ + 유틸리티 함수 + ============================================ */ +function fn_getDateStr(date) { + var y = date.getFullYear(); + var m = ('0' + (date.getMonth() + 1)).slice(-2); + var d = ('0' + date.getDate()).slice(-2); + return y + '-' + m + '-' + d; +} + +function fn_numberFormat(num) { + if (!num && num !== 0) return '0'; + return Number(num).toLocaleString(); +} + +function fn_formatPhone(phone) { + if (!phone) return ''; + phone = phone.replace(/[^0-9]/g, ''); + if (phone.length === 11) { + return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); + } else if (phone.length === 10) { + return phone.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3'); + } else if (phone.length === 9) { + return phone.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3'); + } + return phone; +} + +/* ============================================ + 녹음 파일 재생 + ============================================ */ +function fn_playRecord(recordNo) { + if (!recordNo) return; + + $.ajax({ + url: '/callLog/getRecordFileUrl.do', + type: 'POST', + data: { recordNo: recordNo }, + dataType: 'json', + success: function (data) { + if (data.success && data.recordUrl) { + window.open(data.recordUrl, '_blank', 'width=400,height=200'); + } else { + alert('녹음 파일을 불러올 수 없습니다.'); + } + }, + error: function () { + alert('녹음 파일 조회 중 오류가 발생하였습니다.'); + } + }); +} + diff --git a/src/main/resources/static/js/web/smsTemplate/smsTemplateSelectList.js b/src/main/resources/static/js/web/smsTemplate/smsTemplateSelectList.js new file mode 100644 index 0000000..5a40ee8 --- /dev/null +++ b/src/main/resources/static/js/web/smsTemplate/smsTemplateSelectList.js @@ -0,0 +1,329 @@ +/** + * 문자 상용구 관리 - ag-Grid + */ +$(document).ready(function () { + fn_initGrid(); + fn_init(); + fn_loadTemplateList(); +}); + +/* ============================================ + 전역 변수 + ============================================ */ +var gridApi = null; +var currentMode = ''; // 'new' | 'edit' +var currentTemplateId = ''; + +/* ============================================ + ag-Grid 초기화 + ============================================ */ +function fn_initGrid() { + var columnDefs = [ + { + headerName: 'No', + valueGetter: function (params) { + return params.node.rowIndex + 1; + }, + width: 55, + maxWidth: 55, + cellStyle: { textAlign: 'center' }, + suppressSizeToFit: true + }, + { + headerName: '제목', + field: 'title', + flex: 2, + minWidth: 120, + cellStyle: { textAlign: 'left' } + }, + { + headerName: '등록자', + field: 'regName', + width: 80, + maxWidth: 100, + cellStyle: { textAlign: 'center' } + }, + { + headerName: '등록일', + field: 'regDate', + width: 100, + maxWidth: 120, + cellStyle: { textAlign: 'center' } + } + ]; + + var gridOptions = { + columnDefs: columnDefs, + rowData: [], + rowSelection: 'single', + animateRows: true, + headerHeight: 36, + rowHeight: 38, + suppressCellFocus: true, + overlayNoRowsTemplate: '등록된 상용구가 없습니다.', + onRowClicked: function (event) { + var id = event.data.muSmsTemplateId; + if (id) fn_selectTemplate(id); + }, + defaultColDef: { + resizable: true, + sortable: true + } + }; + + var gridDiv = document.querySelector('#smsTemplateGrid'); + gridApi = new agGrid.Grid(gridDiv, gridOptions); + gridApi = gridOptions.api; +} + +/* ============================================ + 초기화 + ============================================ */ +function fn_init() { + $('#searchBtn').on('click', function () { + fn_loadTemplateList(); + }); + + $('#searchKeyword').on('keypress', function (e) { + if (e.which === 13) fn_loadTemplateList(); + }); + + $('#newTemplateBtn').on('click', function () { + fn_newTemplate(); + }); + + $('#saveBtn').on('click', function () { + fn_saveTemplate(); + }); + + $('#deleteBtn').on('click', function () { + fn_deleteTemplate(); + }); + + $('#cancelBtn').on('click', function () { + fn_cancelEdit(); + }); + + $('#templateContent').on('input', function () { + fn_updateByteCount(); + }); +} + +/* ============================================ + 목록 조회 + ============================================ */ +function fn_loadTemplateList() { + var param = { + searchKeyword: $('#searchKeyword').val(), + menuClass: menuClass + }; + + $.ajax({ + url: '/smsTemplate/getSmsTemplateList.do', + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + var rows = data.rows || []; + gridApi.setRowData(rows); + + if (rows.length === 0) { + gridApi.showNoRowsOverlay(); + } + }, + error: function () { + alert('목록 조회 중 오류가 발생하였습니다.'); + } + }); +} + +/* ============================================ + 상세 조회 + ============================================ */ +function fn_selectTemplate(templateId) { + var param = { + muSmsTemplateId: templateId, + menuClass: menuClass + }; + + $.ajax({ + url: '/smsTemplate/getSmsTemplate.do', + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + if (data.data) { + fn_showDetail(data.data); + } else { + alert(data.msgDesc || '상세 조회에 실패하였습니다.'); + } + }, + error: function () { + alert('상세 조회 중 오류가 발생하였습니다.'); + } + }); +} + +function fn_showDetail(detail) { + currentMode = 'edit'; + currentTemplateId = detail.muSmsTemplateId; + + $('#detailTitle').text('상용구 수정'); + $('#muSmsTemplateId').val(detail.muSmsTemplateId); + $('#templateTitle').val(detail.title || ''); + $('#templateContent').val(detail.content || ''); + $('#regName').text(detail.regName || '-'); + $('#regDate').text(detail.regDate || '-'); + $('#modDate').text(detail.modDate || '-'); + + $('#infoRow').show(); + $('#deleteBtn').show(); + $('#emptyState').hide(); + $('#detailForm').show(); + + fn_updateByteCount(); +} + +/* ============================================ + 새 상용구 + ============================================ */ +function fn_newTemplate() { + currentMode = 'new'; + currentTemplateId = ''; + + $('#detailTitle').text('상용구 등록'); + $('#muSmsTemplateId').val(''); + $('#templateTitle').val(''); + $('#templateContent').val(''); + + $('#infoRow').hide(); + $('#deleteBtn').hide(); + $('#emptyState').hide(); + $('#detailForm').show(); + + fn_updateByteCount(); + + gridApi.deselectAll(); + $('#templateTitle').focus(); +} + +/* ============================================ + 취소 + ============================================ */ +function fn_cancelEdit() { + currentMode = ''; + currentTemplateId = ''; + + $('#detailForm').hide(); + $('#emptyState').show(); + + gridApi.deselectAll(); +} + +/* ============================================ + 저장 (등록/수정) + ============================================ */ +function fn_saveTemplate() { + var title = $.trim($('#templateTitle').val()); + var content = $.trim($('#templateContent').val()); + + if (!title) { + alert('제목을 입력하세요.'); + $('#templateTitle').focus(); + return; + } + + if (!content) { + alert('내용을 입력하세요.'); + $('#templateContent').focus(); + return; + } + + var url = ''; + var param = { + title: title, + content: content, + menuClass: menuClass + }; + + if (currentMode === 'new') { + url = '/smsTemplate/putSmsTemplate.do'; + } else if (currentMode === 'edit') { + url = '/smsTemplate/modSmsTemplate.do'; + param.muSmsTemplateId = currentTemplateId; + } + + if (!url) return; + + $.ajax({ + url: url, + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + alert(data.msgDesc || '처리되었습니다.'); + fn_cancelEdit(); + fn_loadTemplateList(); + }, + error: function () { + alert('저장 중 오류가 발생하였습니다.'); + } + }); +} + +/* ============================================ + 삭제 + ============================================ */ +function fn_deleteTemplate() { + if (!currentTemplateId) return; + if (!confirm('이 상용구를 삭제하시겠습니까?')) return; + + var param = { + muSmsTemplateId: currentTemplateId, + menuClass: menuClass + }; + + $.ajax({ + url: '/smsTemplate/delSmsTemplate.do', + type: 'POST', + data: param, + dataType: 'json', + success: function (data) { + alert(data.msgDesc || '삭제되었습니다.'); + fn_cancelEdit(); + fn_loadTemplateList(); + }, + error: function () { + alert('삭제 중 오류가 발생하였습니다.'); + } + }); +} + +/* ============================================ + 바이트 카운트 + ============================================ */ +function fn_updateByteCount() { + var content = $('#templateContent').val() || ''; + var byteLen = fn_getByteLength(content); + + $('#byteCount').text(byteLen); + + var $badge = $('#typeBadge'); + if (byteLen > 90) { + $badge.text('LMS').removeClass('sms').addClass('lms'); + } else { + $badge.text('SMS').removeClass('lms').addClass('sms'); + } +} + +function fn_getByteLength(str) { + var byte = 0; + for (var i = 0; i < str.length; i++) { + if (str.charCodeAt(i) <= 0x7F) { + byte += 1; + } else { + byte += 2; + } + } + return byte; +} diff --git a/src/main/resources/templates/web/callLog/callLogSelectList.html b/src/main/resources/templates/web/callLog/callLogSelectList.html new file mode 100644 index 0000000..690e758 --- /dev/null +++ b/src/main/resources/templates/web/callLog/callLogSelectList.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + +
+

통화 로그 관리

+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + ~ + +
+
+ + + + + + +
+ +
+
+ + +
+ 전체: 0 + | + IN: 0 + [초진: 0 / 재진: 0] + [상담: 0 / 상담후예약: 0 / 회차예약: 0] + | + OUT: 0 +
+ + +
+ + + +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/web/smsTemplate/smsTemplateSelectList.html b/src/main/resources/templates/web/smsTemplate/smsTemplateSelectList.html new file mode 100644 index 0000000..9c491d3 --- /dev/null +++ b/src/main/resources/templates/web/smsTemplate/smsTemplateSelectList.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + +
+

문자 상용구 관리

+ + +
+ +
+
+ + + +
+ +
+
+ + +
+ +
+

좌측 목록에서 상용구를 선택하거나
"등록" 버튼을 클릭하세요.

+
+ + + +
+
+
+ +
+
+ + + + + + + + \ No newline at end of file