전화 관련 commit

This commit is contained in:
pjs
2026-02-19 21:00:48 +09:00
parent a41b072d99
commit ae1ac1ea0f
22 changed files with 3317 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.madeu.crm.callLog.map.CallLogMAP">
<!-- 통화 로그 목록 건수 조회 -->
<select id="getCallLogCnt" parameterType="com.madeu.crm.callLog.dto.CallLogSearchDTO" resultType="Integer">
/** CallLogMAP.getCallLogCnt **/
SELECT COUNT(*) AS TOTAL_COUNT
FROM MU_CTI_LOG A
WHERE A.USE_YN = 'Y'
AND A.FULLDNIS = #{fulldnis}
AND A.REG_DATE &gt;= CONCAT(#{sDate}, ' 00:00:00')
AND A.REG_DATE &lt;= CONCAT(#{eDate}, ' 23:59:59')
<if test="callType != null and callType != '' and callType != 'callback'">
AND A.MENU_NO = #{callType}
AND A.STATE_TYPE IN ('start', 'ringout')
</if>
<if test="callType == 'callback'">
AND A.STATE_TYPE = 'callback'
</if>
<if test="callType == null or callType == ''">
AND (A.STATE_TYPE = 'start' OR A.STATE_TYPE = 'ringout')
</if>
<if test="callType1 != null and callType1 != ''">
AND A.C_TYPE1 = #{callType1}
</if>
<if test="callType2 != null and callType2 != ''">
AND A.C_TYPE2 = #{callType2}
</if>
<if test="mCid != null and mCid != ''">
AND A.CID LIKE CONCAT('%', #{mCid}, '%')
</if>
<if test="incallSel != null and incallSel != ''">
AND A.INCALL_SEL LIKE CONCAT('%|=|', #{incallSel}, '|=|%')
</if>
</select>
<!-- 통화 로그 통계 집계 -->
<select id="getCallLogStats" parameterType="com.madeu.crm.callLog.dto.CallLogSearchDTO" resultType="com.madeu.crm.callLog.dto.CallLogStatsDTO">
/** CallLogMAP.getCallLogStats **/
SELECT COUNT(*) AS "totalCnt"
,SUM(IF(A.BOUND = 'IN', 1, 0)) AS "inCnt"
,SUM(IF(A.BOUND = 'OUT', 1, 0)) AS "outCnt"
,SUM(IF(A.C_TYPE1 = '초진', 1, 0)) AS "type1Cnt"
,SUM(IF(A.C_TYPE1 = '재진', 1, 0)) AS "type2Cnt"
,SUM(IF(A.C_TYPE2 = '상담', 1, 0)) AS "type11Cnt"
,SUM(IF(A.C_TYPE2 = '상담후예약', 1, 0)) AS "type12Cnt"
,SUM(IF(A.C_TYPE2 = '회차예약', 1, 0)) AS "type13Cnt"
FROM MU_CTI_LOG A
WHERE A.USE_YN = 'Y'
AND A.FULLDNIS = #{fulldnis}
AND A.REG_DATE &gt;= CONCAT(#{sDate}, ' 00:00:00')
AND A.REG_DATE &lt;= CONCAT(#{eDate}, ' 23:59:59')
<if test="callType != null and callType != '' and callType != 'callback'">
AND A.MENU_NO = #{callType}
AND A.STATE_TYPE IN ('start', 'ringout')
</if>
<if test="callType == 'callback'">
AND A.STATE_TYPE = 'callback'
</if>
<if test="callType == null or callType == ''">
AND (A.STATE_TYPE = 'start' OR A.STATE_TYPE = 'ringout')
</if>
<if test="callType1 != null and callType1 != ''">
AND A.C_TYPE1 = #{callType1}
</if>
<if test="callType2 != null and callType2 != ''">
AND A.C_TYPE2 = #{callType2}
</if>
<if test="mCid != null and mCid != ''">
AND A.CID LIKE CONCAT('%', #{mCid}, '%')
</if>
</select>
<!-- 통화 연결 건수 (통계용) -->
<select id="getCallLogLinkedStats" parameterType="com.madeu.crm.callLog.dto.CallLogSearchDTO" resultType="hashmap">
/** CallLogMAP.getCallLogLinkedStats **/
SELECT COUNT(*) AS "totalOkCnt"
,SUM(IF(A.BOUND = 'IN', 1, 0)) AS "inOkCnt"
,SUM(IF(A.BOUND = 'OUT', 1, 0)) AS "outOkCnt"
FROM MU_CTI_LOG A
WHERE A.USE_YN = 'Y'
AND A.FULLDNIS = #{fulldnis}
AND A.REG_DATE &gt;= CONCAT(#{sDate}, ' 00:00:00')
AND A.REG_DATE &lt;= CONCAT(#{eDate}, ' 23:59:59')
AND A.STATE_TYPE = 'link'
</select>
<!-- 통화 로그 목록 조회 -->
<select id="getCallLogList" parameterType="com.madeu.crm.callLog.dto.CallLogSearchDTO" resultType="com.madeu.crm.callLog.dto.CallLogDTO">
/** CallLogMAP.getCallLogList **/
SELECT L.*
FROM (
SELECT L.*
,CAST(@RNUM:=@RNUM + 1 AS CHAR) AS "rowNum"
FROM (
SELECT A.MU_CTI_LOG_ID AS "muCtiLogId"
,A.RECORD_NO AS "recordNo"
,A.CID AS "cid"
,A.BOUND AS "bound"
,A.MENU_NO AS "menuNo"
,A.STATE_TYPE AS "stateType"
,A.C_TYPE1 AS "cType1"
,A.C_TYPE2 AS "cType2"
,A.CRM_MB_PID AS "crmMbPid"
,A.STORE_PID AS "storePid"
,A.INCALL_SEL AS "incallSel"
,A.CALL_BACK_CHK AS "callBackChk"
,DATE_FORMAT(A.REG_DATE, '%Y-%m-%d %H:%i:%s') AS "regDate"
/* ring - ARS 메뉴 선택 */
,(SELECT B.MENU_NO
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '2'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "ring"
/* 통화 연결 시각 */
,(SELECT DATE_FORMAT(B.REG_DATE, '%Y-%m-%d %H:%i:%s')
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '3'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "linkDate"
/* 통화 연결 상담원 */
,(SELECT B.LOGINCID
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '3'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "linkCid"
/* 통화 종료 시각 (step 4) */
,(SELECT DATE_FORMAT(B.REG_DATE, '%Y-%m-%d %H:%i:%s')
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '4'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "linkEndDate"
/* 통화 종료 시각 (step 5) */
,(SELECT DATE_FORMAT(B.REG_DATE, '%Y-%m-%d %H:%i:%s')
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '5'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "bendDate"
/* callback CID */
,(SELECT B.CID
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.CTI_STEP = '98'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "callbackCid"
/* SMS 전송 여부 */
,(SELECT B.STATE_TYPE
FROM MU_CTI_LOG B
WHERE B.RECORD_NO = A.RECORD_NO
AND B.STATE_TYPE = 'lms'
AND B.USE_YN = 'Y'
ORDER BY B.REG_DATE DESC LIMIT 1) AS "smsFlag"
/* 통화 메모 */
,(SELECT T.CALL_MSG
FROM MU_CTI_TEXT T
WHERE T.RECORD_NO = A.RECORD_NO
AND T.USE_YN = 'Y'
ORDER BY T.REG_DATE DESC LIMIT 1) AS "callMsg"
/* 고객명 */
,(SELECT MM.NAME
FROM MU_MEMBER MM
WHERE MM.MU_MEMBER_ID = A.CRM_MB_PID
AND MM.USE_YN = 'Y'
LIMIT 1) AS "memberName"
FROM MU_CTI_LOG A
WHERE A.USE_YN = 'Y'
AND A.FULLDNIS = #{fulldnis}
AND A.REG_DATE &gt;= CONCAT(#{sDate}, ' 00:00:00')
AND A.REG_DATE &lt;= CONCAT(#{eDate}, ' 23:59:59')
<if test="callType != null and callType != '' and callType != 'callback'">
AND A.MENU_NO = #{callType}
AND A.STATE_TYPE IN ('start', 'ringout')
</if>
<if test="callType == 'callback'">
AND A.STATE_TYPE = 'callback'
</if>
<if test="callType == null or callType == ''">
AND (A.STATE_TYPE = 'start' OR A.STATE_TYPE = 'ringout')
</if>
<if test="callType1 != null and callType1 != ''">
AND A.C_TYPE1 = #{callType1}
</if>
<if test="callType2 != null and callType2 != ''">
AND A.C_TYPE2 = #{callType2}
</if>
<if test="mCid != null and mCid != ''">
AND A.CID LIKE CONCAT('%', #{mCid}, '%')
</if>
<if test="incallSel != null and incallSel != ''">
AND A.INCALL_SEL LIKE CONCAT('%|=|', #{incallSel}, '|=|%')
</if>
ORDER BY A.REG_DATE DESC
LIMIT 18446744073709551615
) L, (SELECT @RNUM:=0) R
WHERE 1 = 1
) L
WHERE 1 = 1
<if test="gridLimitEnd != null and gridLimitEnd != ''">
LIMIT ${gridLimitStart}, ${gridLimitEnd}
</if>
</select>
<!-- 통화 메모 존재 여부 -->
<select id="getCallMemoCnt" parameterType="com.madeu.crm.callLog.dto.CallMemoDTO" resultType="Integer">
/** CallLogMAP.getCallMemoCnt **/
SELECT COUNT(*)
FROM MU_CTI_TEXT
WHERE RECORD_NO = #{recordNo}
AND USE_YN = 'Y'
</select>
<!-- 통화 메모 신규 저장 -->
<insert id="saveCallMemo" parameterType="com.madeu.crm.callLog.dto.CallMemoDTO">
<selectKey resultType="string" keyProperty="muCtiTextId" order="BEFORE">
SELECT CONCAT('CTXT', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_CTI_TEXT_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0'))
FROM MU_CTI_TEXT
</selectKey>
/** 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()
)
</insert>
<!-- 통화 메모 수정 -->
<update id="modCallMemo" parameterType="com.madeu.crm.callLog.dto.CallMemoDTO">
/** 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'
</update>
<!-- CTI 로그 저장 (Webhook용) -->
<insert id="putCtiLog" parameterType="com.madeu.crm.callLog.dto.CallLogDTO">
<selectKey resultType="string" keyProperty="muCtiLogId" order="BEFORE">
SELECT CONCAT('CLOG', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_CTI_LOG_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0'))
FROM MU_CTI_LOG
</selectKey>
/** 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()
)
</insert>
<!-- CTI 로그 회원 매핑 업데이트 -->
<update id="modCtiLogMember" parameterType="hashmap">
/** 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'
</update>
</mapper>

View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.madeu.crm.smsTemplate.map.SmsTemplateMAP">
<!-- 상용구 목록 건수 조회 -->
<select id="getSmsTemplateCnt" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateSearchDTO" resultType="Integer">
/** SmsTemplateMAP.getSmsTemplateCnt **/
SELECT COUNT(*) AS TOTAL_COUNT
FROM MU_SMS_TEMPLATE
WHERE USE_YN = 'Y'
<if test="searchKeyword != null and searchKeyword != ''">
AND (TITLE LIKE CONCAT('%', TRIM(#{searchKeyword}), '%')
OR CONTENT LIKE CONCAT('%', TRIM(#{searchKeyword}), '%'))
</if>
</select>
<!-- 상용구 목록 조회 -->
<select id="getSmsTemplateList" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateSearchDTO" resultType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO">
/** SmsTemplateMAP.getSmsTemplateList **/
SELECT ST.*
FROM (
SELECT ST.*
,CAST(@RNUM:=@RNUM + 1 AS CHAR) AS "rowNum"
FROM (
SELECT ST.MU_SMS_TEMPLATE_ID AS "muSmsTemplateId"
,ST.TITLE AS "title"
,ST.CONTENT AS "content"
,DATE_FORMAT(ST.REG_DATE, '%Y-%m-%d %H:%i') AS "regDate"
,DATE_FORMAT(ST.MOD_DATE, '%Y-%m-%d %H:%i') AS "modDate"
,(SELECT MM.NAME
FROM MU_MEMBER AS MM
WHERE MM.USE_YN = 'Y'
AND MM.MU_MEMBER_ID = ST.REG_ID
LIMIT 0, 1) AS "regName"
FROM MU_SMS_TEMPLATE AS ST
WHERE ST.USE_YN = 'Y'
<if test="searchKeyword != null and searchKeyword != ''">
AND (ST.TITLE LIKE CONCAT('%', TRIM(#{searchKeyword}), '%')
OR ST.CONTENT LIKE CONCAT('%', TRIM(#{searchKeyword}), '%'))
</if>
<choose>
<when test="gridSort != null and gridSort != ''">
ORDER BY ${gridSort}
</when>
<otherwise>
ORDER BY ST.REG_DATE DESC
</otherwise>
</choose>
LIMIT 18446744073709551615
) ST, (SELECT @RNUM:=0) R
WHERE 1 = 1
) ST
WHERE 1 = 1
<if test="gridLimitEnd != null and gridLimitEnd != ''">
LIMIT ${gridLimitStart}, ${gridLimitEnd}
</if>
</select>
<!-- 상용구 상세 조회 -->
<select id="getSmsTemplate" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO" resultType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO">
/** SmsTemplateMAP.getSmsTemplate **/
SELECT ST.MU_SMS_TEMPLATE_ID AS "muSmsTemplateId"
,ST.TITLE AS "title"
,ST.CONTENT AS "content"
,DATE_FORMAT(ST.REG_DATE, '%Y-%m-%d %H:%i') AS "regDate"
,DATE_FORMAT(ST.MOD_DATE, '%Y-%m-%d %H:%i') AS "modDate"
,(SELECT MM.NAME
FROM MU_MEMBER AS MM
WHERE MM.USE_YN = 'Y'
AND MM.MU_MEMBER_ID = ST.REG_ID
LIMIT 0, 1) AS "regName"
FROM MU_SMS_TEMPLATE AS ST
WHERE ST.USE_YN = 'Y'
AND ST.MU_SMS_TEMPLATE_ID = #{muSmsTemplateId}
LIMIT 0, 1
</select>
<!-- 상용구 등록 -->
<insert id="putSmsTemplate" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO">
<selectKey resultType="string" keyProperty="muSmsTemplateId" order="BEFORE">
SELECT CONCAT('SMST', LPAD(IFNULL(MAX(CAST(SUBSTRING(MU_SMS_TEMPLATE_ID, 5) AS UNSIGNED)), 0) + 1, 11, '0'))
FROM MU_SMS_TEMPLATE
</selectKey>
/** 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()
)
</insert>
<!-- 상용구 수정 -->
<update id="modSmsTemplate" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO">
/** 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}
</update>
<!-- 상용구 삭제 (논리 삭제) -->
<update id="delSmsTemplate" parameterType="com.madeu.crm.smsTemplate.dto.SmsTemplateDTO">
/** 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}
</update>
</mapper>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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 '<span style="color:#0000ff;font-weight:600">OUT</span>';
}
if (params.data.callbackCid) {
return 'Call Back<br><small>' + params.data.callbackCid + '</small>';
}
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 = '<button type="button" class="cl_play_btn" onclick="fn_playRecord(\'' + params.data.recordNo + '\')">▶</button>';
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 '<span class="cl_incall_badge">' + item + '</span>';
}).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 || '<span style="color:#B5BDC4">클릭하여 메모 입력</span>';
}
}
];
var gridOptions = {
columnDefs: columnDefs,
rowData: [],
rowSelection: 'single',
animateRows: true,
headerHeight: 36,
rowHeight: 40,
suppressCellFocus: true,
overlayNoRowsTemplate: '<span style="color:#B5BDC4; font-size:13px;">검색된 통화 내역이 없습니다.</span>',
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('녹음 파일 조회 중 오류가 발생하였습니다.');
}
});
}

View File

@@ -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: '<span style="color:#B5BDC4; font-size:13px;">등록된 상용구가 없습니다.</span>',
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;
}

View File

@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link rel="stylesheet" href="/css/web/webFeedbackSelectList.css">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
<link rel="stylesheet" href="/css/web/call_log.css">
</th:block>
<th:block layout:fragment="layout_top_script">
<script>
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]" == "" ? "N" : "[[${insertUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]" == "" ? "N" : "[[${updateUseYn}]]";
let deleteUseYn = "[[${deleteUseYn}]]" == "" ? "N" : "[[${deleteUseYn}]]";
let downloadUseYn = "[[${downloadUseYn}]]" == "" ? "N" : "[[${downloadUseYn}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
<!-- 센터쪽 -->
<div class="center_box">
<p class="page_title">통화 로그 관리</p>
<!-- 검색 필터 영역 -->
<div class="cl_filter_wrap">
<!-- 1행: 구분 필터 -->
<div class="cl_filter_row">
<div class="cl_filter_item">
<label>구분1</label>
<select id="callType" class="cl_select">
<option value="">전체</option>
<option value="1">상담</option>
<option value="2">예약</option>
<option value="3">위치안내</option>
<option value="callback">Call Back</option>
</select>
</div>
<div class="cl_filter_item">
<label>구분2</label>
<select id="callType1" class="cl_select">
<option value="">전체</option>
<option value="초진">초진</option>
<option value="재진">재진</option>
</select>
</div>
<div class="cl_filter_item">
<label>구분3</label>
<select id="callType2" class="cl_select">
<option value="">전체</option>
<option value="상담">상담</option>
<option value="상담후예약">상담후예약</option>
<option value="회차예약">회차예약</option>
</select>
</div>
<div class="cl_filter_item">
<label>고객전화번호</label>
<input type="text" id="mCid" placeholder="전화번호 입력" />
</div>
</div>
<!-- 2행: 날짜 필터 -->
<div class="cl_filter_row">
<div class="cl_filter_item cl_date_item">
<label>검색일자</label>
<input type="date" id="sDate" />
<span class="cl_date_sep">~</span>
<input type="date" id="eDate" />
</div>
<div class="cl_quick_btns">
<button type="button" onclick="fn_dayChk('t')">오늘</button>
<button type="button" onclick="fn_dayChk('p')">어제</button>
<button type="button" onclick="fn_dayChk('w')">이번주</button>
<button type="button" onclick="fn_dayChk('pw')">지난주</button>
<button type="button" onclick="fn_dayChk('m')">이번달</button>
<button type="button" onclick="fn_dayChk('pm')">지난달</button>
</div>
<button type="button" id="searchBtn" class="cl_search_btn">조회</button>
</div>
</div>
<!-- 통계 요약 바 -->
<div class="cl_stats_bar">
<span>전체: <strong id="statTotal">0</strong></span>
<span class="cl_stats_sep">|</span>
<span>IN: <strong id="statIn">0</strong></span>
<span class="cl_stats_detail">[초진: <strong id="statType1">0</strong> / 재진: <strong
id="statType2">0</strong>]</span>
<span class="cl_stats_detail">[상담: <strong id="statType11">0</strong> / 상담후예약: <strong
id="statType12">0</strong> / 회차예약: <strong id="statType13">0</strong>]</span>
<span class="cl_stats_sep">|</span>
<span>OUT: <strong id="statOut">0</strong></span>
</div>
<!-- ag-Grid 테이블 -->
<div id="callLogGrid" class="cl_grid_box ag-theme-balham"></div>
<!-- 메모 팝업 -->
<div class="cl_memo_overlay" id="memoOverlay" style="display:none;">
<div class="cl_memo_popup">
<div class="cl_memo_header">
<h3>통화 메모</h3>
<span class="cl_memo_record" id="memoRecordNo"></span>
<button type="button" class="cl_memo_close" onclick="fn_closeMemo()">×</button>
</div>
<div class="cl_memo_body">
<input type="hidden" id="memoRecordNoVal" />
<textarea id="memoContent" placeholder="메모를 입력하세요" rows="6"></textarea>
</div>
<div class="cl_memo_footer">
<button type="button" class="cl_memo_save_btn" onclick="fn_saveMemo()">저장</button>
<button type="button" class="cl_memo_cancel_btn" onclick="fn_closeMemo()">취소</button>
</div>
</div>
</div>
</div>
<form id="callLogForm" onsubmit="return false;"></form>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="/js/web/ag-grid-community-29.3.5.min.js"></script>
<script src="/js/web/callLog/callLogSelectList.js"></script>
</th:block>
</html>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{/web/layout/homeLayout}">
<th:block layout:fragment="layout_css">
<link rel="stylesheet" href="/css/web/webFeedbackSelectList.css">
<link rel="stylesheet" href="/css/web/grid.css?v1.1">
<link rel="stylesheet" href="/css/web/sms_template.css">
</th:block>
<th:block layout:fragment="layout_top_script">
<script>
let menuClass = "[[${param.menuClass}]]" == "" ? "" : "[[${param.menuClass}]]";
let selectUseYn = "[[${selectUseYn}]]" == "" ? "N" : "[[${selectUseYn}]]";
let insertUseYn = "[[${insertUseYn}]]" == "" ? "N" : "[[${insertUseYn}]]";
let updateUseYn = "[[${updateUseYn}]]" == "" ? "N" : "[[${updateUseYn}]]";
let deleteUseYn = "[[${deleteUseYn}]]" == "" ? "N" : "[[${deleteUseYn}]]";
let downloadUseYn = "[[${downloadUseYn}]]" == "" ? "N" : "[[${downloadUseYn}]]";
</script>
</th:block>
<th:block layout:fragment="layout_content">
<!-- 센터쪽 -->
<div class="center_box">
<p class="page_title">문자 상용구 관리</p>
<!-- 2패널 컨테이너 -->
<div class="sms_two_panel">
<!-- 좌측: 목록 -->
<div class="sms_left_panel">
<div class="sms_search_area">
<div class="sms_search_box">
<img src="/image/web/search_G.svg" alt="search" />
<input type="text" id="searchKeyword" placeholder="제목 또는 내용" />
</div>
<button id="searchBtn" class="sms_search_btn">조회</button>
<button type="button" id="newTemplateBtn" class="sms_new_btn">등록</button>
</div>
<div id="smsTemplateGrid" class="sms_grid_box ag-theme-balham"></div>
</div>
<!-- 우측: 상세/편집 -->
<div class="sms_right_panel">
<!-- 빈 상태 -->
<div class="sms_empty_state" id="emptyState">
<p>좌측 목록에서 상용구를 선택하거나<br />"등록" 버튼을 클릭하세요.</p>
</div>
<!-- 상세 폼 -->
<div class="sms_detail_form" id="detailForm" style="display:none;">
<div class="sms_detail_title_bar">
<h3 id="detailTitle">상용구 등록</h3>
</div>
<input type="hidden" id="muSmsTemplateId" />
<div class="sms_form_row">
<label for="templateTitle">제목</label>
<input type="text" id="templateTitle" placeholder="상용구 제목을 입력하세요" maxlength="100" />
</div>
<div class="sms_form_row">
<label for="templateContent">내용</label>
<textarea id="templateContent" placeholder="상용구 내용을 입력하세요" rows="8"></textarea>
<div class="byte_info">
<span><span id="byteCount">0</span> byte</span>
<span class="type_badge sms" id="typeBadge">SMS</span>
</div>
</div>
<div class="sms_form_row sms_info_row" id="infoRow" style="display:none;">
<span>등록자: <strong id="regName">-</strong></span>
<span>등록일: <strong id="regDate">-</strong></span>
<span>수정일: <strong id="modDate">-</strong></span>
</div>
<div class="sms_btn_group">
<button type="button" id="saveBtn" class="sms_save_btn">저장</button>
<button type="button" id="deleteBtn" class="sms_delete_btn" style="display:none;">삭제</button>
<button type="button" id="cancelBtn" class="sms_cancel_btn">취소</button>
</div>
</div>
</div>
</div>
</div>
<form id="smsTemplateForm" onsubmit="return false;"></form>
</th:block>
<th:block layout:fragment="layout_popup">
</th:block>
<th:block layout:fragment="layout_script">
<script src="/js/web/ag-grid-community-29.3.5.min.js"></script>
<script src="/js/web/smsTemplate/smsTemplateSelectList.js"></script>
</th:block>
</html>