본문으로 건너뛰기

SMS 발송 보안 강화

PHP 8 · GODO26
변경 파일 수: 214개
변경 파일 목록
총 214개
2026-10-06 · 문자 발신번호 관리 프로세스 개선.
layer_reject_recipient_member_info.phpAdmin/crm/layer_caller_connector.phpAdmin/crm/message_config/layer_call_number.phpAdmin/crm/message_config/layer_call_number_result.phpAdmin/crm/message_config/layer_manage_call_number.phpAdmin/crm/message_config/layer_manage_call_number_result.phpAdmin/crm/message_config/popup_manage_call_number.phpAdmin/crm/sms_auto.phpAdmin/member/sms_send.phpAdmin/member/admin_log_list.phpAdmin/policy/manage_list.phpAdmin/policy/_manage_permission_manager_list.phpAdmin/policy/_manage_permission_menu_list.phpAdmin/policy/manage_permission.phpAdmin/policy/manage_register.phpAdmin/policy/manage_security.phpAdmin/policy/cart_remind_list.phpAdmin/promotion/comeback_coupon_list.phpAdmin/promotion/layer_message_representative_register_guide.phpAdmin/share/layer_sms_personal_info_access_log.phpAdmin/share/manager_certification_result.phpAdmin/share/AdminLogDAO.phpComponent/Admin/AdminMenu.phpComponent/Admin/CartRemind.phpComponent/CartRemind/CouponAdmin.phpComponent/Coupon/DBTableField.phpComponent/Database/ManagerCs.phpComponent/Member/Manager.phpComponent/Member/SmsAdmin.phpComponent/Sms/SmsAuto.phpComponent/Sms/SmsSender.phpComponent/Sms/LayerSmsAuthController.phpController/Admin/Base/LoginPsController.phpController/Admin/Base/CrmRecipeSendController.phpController/Admin/Crm/LayerCallerConnectorController.phpController/Admin/Crm/MessageConfig/LayerCallNumberController.phpController/Admin/Crm/MessageConfig/LayerManageCallNumberResultController.phpController/Admin/Crm/MessageConfig/MessageConfigPsController.phpController/Admin/Crm/MobileSendController.phpController/Admin/Crm/PopupManageCallNumberController.phpController/Admin/Crm/PopupManageCallNumberPsController.phpController/Admin/Crm/SmsAutoController.phpController/Admin/Member/SmsPsController.phpController/Admin/Member/SmsSendController.phpController/Admin/Member/AdminLogListController.phpController/Admin/Policy/ManageListController.phpController/Admin/Policy/ManagePermissionController.phpController/Admin/Policy/ManagePsController.phpController/Admin/Policy/ManageRegisterController.phpController/Admin/Policy/ManageSecurityController.phpController/Admin/Policy/CartRemindListController.phpController/Admin/Promotion/ComebackCouponListController.phpController/Admin/Promotion/LayerExcelAuthController.phpController/Admin/Share/LayerManagePermissionController.phpController/Admin/Share/
2026-10-26 · SMS 발송 보안 강화
auto_send_config_contents.phpAdmin/crm/auto_send_config/auto_send_config_preview.phpAdmin/crm/auto_send_config/auto_send_config_recipient_section.phpAdmin/crm/auto_send_config/layer_auto_send_modify_kakao_template.phpAdmin/crm/auto_send_config/auto_send_config.phpAdmin/crm/auto_send.phpAdmin/crm/crm_recipe_send.phpAdmin/crm/layer_message_template_approval_history.phpAdmin/crm/layer_mobile_message_performance_detail.phpAdmin/crm/layer_mobile_message_performance_summary.phpAdmin/crm/layer_save_as_template.phpAdmin/crm/layer_update_kakao_template.phpAdmin/crm/layer_manage_sms_verification.phpAdmin/crm/message_config/message_config.phpAdmin/crm/message_template_result.phpAdmin/crm/message_template/message_template_search.phpAdmin/crm/message_template/message_template.phpAdmin/crm/basic_config.phpAdmin/crm/message_template_register/message_template_register.phpAdmin/crm/template_contents_sms.phpAdmin/crm/message_template_register/layer_kakao_friendtalk_content_setting.phpAdmin/crm/mobile_send/layer_mobile_send_save_sms_template.phpAdmin/crm/mobile_send/layer_mobile_send_sms_template.phpAdmin/crm/mobile_send/layer_recipient_setting.phpAdmin/crm/mobile_send/layer_send_method_setting.phpAdmin/crm/mobile_send/layer_sms_content_setting.phpAdmin/crm/mobile_send/mobile_send.phpAdmin/crm/sms_auto.phpAdmin/member/sms_send.phpAdmin/member/_manage_permission_menu_list.phpAdmin/policy/manage_permission.phpAdmin/policy/manage_register.phpAdmin/policy/manage_security.phpAdmin/policy/cart_remind_list.phpAdmin/promotion/comeback_coupon_list.phpAdmin/promotion/layer_message_representative_register_guide.phpAdmin/share/layer_message_representative_send_restrict.phpAdmin/share/layer_occupancy_auth.phpAdmin/share/layer_sms_contents.phpAdmin/share/DBTableField.phpComponent/Database/Manager.phpComponent/Member/SmsAdmin.phpComponent/Sms/SmsAuto.phpComponent/Sms/AutoSendConfigController.phpController/Admin/Crm/LayerAutoSendModifyKakaoTemplateController.phpController/Admin/Crm/AutoSendConfig/LayerAutoSendSaveTemplateController.phpController/Admin/Crm/AutoSendConfig/LayerAutoSendTemplateContentController.phpController/Admin/Crm/AutoSendConfig/LayerAutoSendTemplateController.phpController/Admin/Crm/AutoSendConfig/AutoSendConfigPsController.phpController/Admin/Crm/AutoSendController.phpController/Admin/Crm/LayerListController.phpController/Admin/Crm/AutoSend/LayerListRecommendController.phpController/Admin/Crm/AutoSend/LayerListSearchController.phpController/Admin/Crm/AutoSend/AutoSendPsController.phpController/Admin/Crm/CrmRecipeClickLogPsController.phpController/Admin/Crm/CrmRecipeSendController.phpController/Admin/Crm/LayerKakaoTemplateCommentController.phpController/Admin/Crm/LayerMobileMessagePerformanceDetailController.phpController/Admin/Crm/LayerMobileMessagePerformanceSummaryController.phpController/Admin/Crm/LayerMyAppSendGuideController.phpController/Admin/Crm/LayerPreviewAutoSendController.phpController/Admin/Crm/LayerPreviewMobileSendController.phpController/Admin/Crm/LayerRejectRecipientMemberInfoController.phpController/Admin/Crm/LayerSaveAsTemplateController.phpController/Admin/Crm/LayerSelectCouponController.phpController/Admin/Crm/LayerSelectCouponResultController.phpController/Admin/Crm/LayerSelectMemberGroupsController.phpController/Admin/Crm/LayerSelectMemberGroupsResultController.phpController/Admin/Crm/LayerTemplateContentsController.phpController/Admin/Crm/LayerUpdateKaKaoTemplateController.phpController/Admin/Crm/LayerViewMessageLinkTypeController.phpController/Admin/Crm/MessageConfigController.phpController/Admin/Crm/LayerBizmKakaoTalkChannelController.phpController/Admin/Crm/MessageConfig/LayerBizmKakaoTalkChannelPsController.phpController/Admin/Crm/MessageConfig/LayerChargeMessagePointsHistoryController.phpController/Admin/Crm/MessageConfig/LayerChargeMessagePointsHistoryResultController.phpController/Admin/Crm/MessageConfig/LayerCloudKakaoTalkChannelController.phpController/Admin/Crm/MessageConfig/LayerCloudKakaoTalkChannelPsController.phpController/Admin/Crm/MessageConfig/LayerLunaKakaoTalkChannelController.phpController/Admin/Crm/MessageConfig/LayerLunaKakaoTalkChannelPsController.phpController/Admin/Crm/MessageConfig/LayerManageSmsVerificationController.phpController/Admin/Crm/MessageConfig/LayerManageSmsVerificationPsController.phpController/Admin/Crm/MessageConfig/LayerMessagePointController.phpController/Admin/Crm/MessageConfig/LayerRegisterKakaoTalkChannelController.phpController/Admin/Crm/MessageConfig/LayerRegisterKakaoTalkChannelPsController.phpController/Admin/Crm/MessageConfig/LayerRejectNumberListController.phpController/Admin/Crm/MessageConfig/LayerRejectNumberListPsController.phpController/Admin/Crm/MessageConfig/LayerRejectNumberListResultController.phpController/Admin/Crm/MessageConfig/LayerSmsVerificationController.phpController/Admin/Crm/MessageConfig/MessageConfigPsController.phpController/Admin/Crm/MessageTemplateController.phpController/Admin/Crm/MessageTemplatePsController.phpController/Admin/Crm/MessageTemplateRegisterController.phpController/Admin/Crm/MessageTemplateRegisterPsController.phpController/Admin/Crm/MobileHistoryController.phpController/Admin/Crm/MobileHistoryListController.phpController/Admin/Crm/LayerBizmKakaoAlimTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerCloudKakaoAlimTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerKakaoAlimTalkContentsController.phpController/Admin/Crm/MobileMessageHistory/LayerKakaoAlimTalkHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkContentsController.phpController/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerLunaKakaoAlimTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberBizmKakaoAlimTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberCloudKakaoAlimTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberKakaoAlimTalkHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberKakaoFriendTalkHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberKakaoFriendTalkHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberSmsHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMemberSmsHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryDetailController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryDetailResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelDownloadController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestPsController.phpController/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMyappPushContentsController.phpController/Admin/Crm/MobileMessageHistory/LayerMyappPushHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerMyappPushHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/LayerSmsContentsController.phpController/Admin/Crm/MobileMessageHistory/LayerSmsHistoryResultController.phpController/Admin/Crm/MobileMessageHistory/LayerSmsHistoryResultPsController.phpController/Admin/Crm/MobileMessageHistory/LayerSmsHistorySearchController.phpController/Admin/Crm/MobileMessageHistory/MobileSendController.phpController/Admin/Crm/LayerKakaoAlimtalkContentSettingController.phpController/Admin/Crm/MobileSend/LayerKakaoFriendtalkContentSettingController.phpController/Admin/Crm/MobileSend/LayerMessagePointController.phpController/Admin/Crm/MobileSend/LayerMobileSendMyappPushTemplateController.phpController/Admin/Crm/MobileSend/LayerMobileSendMyappPushTemplateResultController.phpController/Admin/Crm/MobileSend/LayerMobileSendSaveMyappPushTemplateController.phpController/Admin/Crm/MobileSend/LayerMobileSendSaveSmsTemplateController.phpController/Admin/Crm/MobileSend/LayerMobileSendSmsTemplateController.phpController/Admin/Crm/MobileSend/LayerMobileSendSmsTemplateResultController.phpController/Admin/Crm/MobileSend/LayerMyappPushContentSettingController.phpController/Admin/Crm/MobileSend/LayerPreviewMessageController.phpController/Admin/Crm/MobileSend/LayerRecipeSendTimeSettingController.phpController/Admin/Crm/MobileSend/LayerRecipientSettingController.phpController/Admin/Crm/MobileSend/LayerRecipientSettingPsController.phpController/Admin/Crm/MobileSend/LayerSendMethodSettingController.phpController/Admin/Crm/MobileSend/LayerSendTimeSettingController.phpController/Admin/Crm/MobileSend/LayerSmsContentSettingController.phpController/Admin/Crm/MobileSend/MobileSendPsController.phpController/Admin/Crm/MyappPushTemplatePsController.phpController/Admin/Crm/Myapp/MyappPushTemplatePsController.phpController/Admin/Crm/MyappPush/PopupChargeMessagePointsController.phpController/Admin/Crm/PopupSelectMembersController.phpController/Admin/Crm/LayerSelectedMembersResultController.phpController/Admin/Crm/PopupSelectMembers/LayerSelectMembersResultController.phpController/Admin/Crm/PopupSelectMembers/SmsTemplatePsController.phpController/Admin/Crm/Sms/SmsAutoController.phpController/Admin/Member/SmsSendController.phpController/Admin/Member/SmsSendPsController.phpController/Admin/Member/ManagePsController.phpController/Admin/Policy/ManageRegisterController.phpController/Admin/Policy/CartRemindPsController.phpController/Admin/Promotion/CouponPsController.phpController/Admin/Promotion/LayerManagePermissionController.phpController/Admin/Share/LayerSmsContentsController.phpController/Admin/Share/LayerSmsPasswordPsController.phpController/Admin/Share/
변경 파일 코드
추가삭제
2026-10-06 · 문자 발신번호 관리 프로세스 개선.
Admin/crm/layer_reject_recipient_member_info.php+1-1
변경 전layer_reject_recipient_member_info.php
<div class="modal-dialog__content">
<article class="ncua-content reject-recipient-member-info">
<p>
발송대상 전체 <?= $totalCount ?>명 수신거부회원 (<?= $rejectedCount ?>명) 입니다.<br />
수신거부회원에게 메시지 발송하시겠습니까?<br />
수신거부한 회원에게 광고성 정보를 발송하는 경우 과태료가 부과될 수 있습니다.
</p>
 
변경 후layer_reject_recipient_member_info.php
<div class="modal-dialog__content">
<article class="ncua-content reject-recipient-member-info">
<p>
발송대상 <?= $totalCount ?> 명 전체가 수신거부회원 (<?= $rejectedCount ?>명) 입니다.<br />
수신거부회원에게 메시지 발송하시겠습니까?<br />
수신거부한 회원에게 광고성 정보를 발송하는 경우 과태료가 부과될 수 있습니다.
</p>
 
Admin/crm/message_config/layer_caller_connector.php-110
변경 전layer_caller_connector.php
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/layer-caller-connector.css') ?>">
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/ncds-jquery-validator.js') ?>"></script>
<article class="ncua-content layer-caller-connector">
<form id="registerSendManagerForm">
<div class="layer-caller-connector__content">
<details class="ncua-accordion ncua-accordion--blue" open>
<summary>발신번호 관리 책임자를 먼저 연동해주세요.</summary>
<div class="ncua-accordion__content">
<p>발신번호 위조를 통한 스팸·금융사기 방지를 위해 「전기통신사업법」 제84조의2에 따라</p>
<p>발신번호의 실제 소유자 또는 관리책임자를 확인 후에만 SMS 발송이 가능합니다.</p>
<p>안전한 메시지 발송을 위해 관리책임자를 먼저 설정하시기 바랍니다.</p>
</div>
</details>
 
<div class="ncua-table ncua-table--vertical">
<table>
<colgroup>
<col width="144px">
<col>
</colgroup>
<tr>
<th><div >관리책임자</div></th>
<td>
<?php if (count($smsCallerList) > 1): ?>
<!-- 관리책임자 복수 -->
<div class="ncua-select ncua-select--xs" data-manager-select-wrapper>
<span class="ncua-select__content">
<select name="caller" id="caller" class="ncua-select__tag" data-caller-select>
(caller.isAgent ? ' (대리인)' : ' (대표자 본인)')
<option value="">관리책임자를 선택하세요</option>
<?php foreach ($smsCallerList as $smsCaller): ?>
<option value="<?= $smsCaller->getCallerNo() ?>"><?= $smsCaller->getCallerName() ?> (<?= $smsCaller->isAgent() ? '대리인' : '대표자 본인' ?>)</option>
<?php endforeach; ?>
</select>
</span>
</div>
<?php else: ?>
<div class="ncua-flex">
<span class="ncua-flex">
<input type="hidden" name="caller" id="caller" value="<?= $smsCallerList[0]->getCallerNo() ?>" />
<div>
<?= $smsCallerList[0]->getCallerName() ?> (<?= $smsCallerList[0]->isAgent() ? '대리인' : '대표자 본인' ?>)
</div>
</span>
</div>
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
</form>
<div class="ncua-content-footer">
<button type="button" id="registerCallerCancel" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray">
<span class="ncua-btn__label">취소</span>
</button>
<button type="button" id="registerCallerConfirm" class="ncua-btn ncua-btn--sm ncua-btn--primary">
<span class="ncua-btn__label">연동</span>
</button>
</div>
</article>
 
<script>
 
const onReady = () => {
try {
// 취소 버튼
document.getElementById('registerCallerCancel')?.addEventListener('click', () => {
layer_close();
});
// 연동 버튼
document.getElementById('registerCallerConfirm')?.addEventListener('click', () => {
const callerNo = document.getElementById('caller')?.value;
 
if (!callerNo) {
NCDSValidator.highlight(document.querySelector('[data-caller-select]'));
return;
}
 
connectCaller(callerNo);
});
} catch (e) {
NCDSAlert({ message: e.message, iconType: 'error' });
}
};
 
function connectCaller(callerNo) {
$.post('./message_config_ps.php', {mode: 'connectCaller', callerNo: callerNo}, function (response) {
if (response.error || !response.success) {
NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', 'iconType': 'error' });
return;
}
 
if (response.success) {
layer_close();
$('#manageCallNumber').click();
}
});
}
 
const DOM_READY_STATES = ['complete', 'interactive'];
 
if (DOM_READY_STATES.includes(document.readyState)) {
onReady();
} else {
document.addEventListener('DOMContentLoaded', onReady);
}
</script>
 
변경 후layer_caller_connector.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/crm/message_config/layer_call_number.php+33-46
변경 전layer_call_number.php
<?php if ($hasCaller && $smsPreRegister && !empty($smsAutoData['smsCallNum'])): ?>
 
 
<div class="ncua-flex ncua-gap-4 ncua-align-items-center">
<!-- 발신번호 등록 상태 -->
<div class="ncua-input ncua-input--xs ncua-input-width-320 is-disabled" id="registeredCallNumberCard">
$(document).ready(function () {
// 발신번호 설정
$('#manageCallNumber').click(function () {
$.post('./message_config_ps.php', {'mode': 'checkCaller'}, function (response) {
if (response.error || !response.success) {
NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', 'iconType': 'error' });
 
return;
}
 
switch (response.data.type) {
case 'no_caller':
NCDSConfirm({
message: '발신번호 관리책임자를 먼저 등록해 주세요.',
subMessage: `발신번호 위조를 통한 스팸·금융사기 방지를 위해 관리책임자를 필수로 등록해야 합니다.<br/>
관리책임자는 <a href="https://www.nhn-commerce.com/mygodo/sms/dashboard.gd" class="send-manager-link" target="_blank">[NHN 커머스 > 마이페이지 > SMS 발신번호 관리]</a>에서 등록할 수 있습니다.`,
btnText: {
confirmLabel: '관리책임자 등록',
cancelLabel: '취소'
},
callback: (result) => {
if (!result) {
return;
}
window.open('https://www.nhn-commerce.com/mygodo/sms/dashboard.gd', '_blank');
}
});
break;
case 'connected_caller':
show_popup_message_config({
url: './popup_manage_call_number.php',
width: 760,
height: 700,
onClose: function () {
loadCallNumberLayer();
 
}
});
break;
case 'single_caller':
case 'multiple_callers':
$.post('./message_config/layer_caller_connector.php', null, function (data) {
ncds_layer_popup({message: data, title: '관리책임자 연동'});
});
break;
case 'unverified_account':
NCDSAlert({
message: '',
subMessage: '발신번호 등록 전 본인확인(최초 1회)이 필요합니다.<br>확인을 누르면 통합회원 본인인증 페이지로 이동합니다.',
iconType: 'warning',
callback: () => {
show_popup('https://accounts.nhn-commerce.com/mypage/certify?redirect=mypage/acc');
}
});
break;
}
}, 'json');
 
 
});
});
</script>
 
변경 후layer_call_number.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<?php if ($isUsableCallNumber): ?>
<div class="ncua-flex ncua-gap-4 ncua-align-items-center">
<!-- 발신번호 등록 상태 -->
<div class="ncua-input ncua-input--xs ncua-input-width-320 is-disabled" id="registeredCallNumberCard">
$(document).ready(function () {
// 발신번호 설정
$('#manageCallNumber').click(function () {
$.post('./message_config/layer_manage_call_number.php', null, function (data) {
// 200 으로 내려오는 오류(연동 등)는 JSON(Content-Type 기반 자동 파싱된 객체), 정상은 레이어 HTML 문자열로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
ncds_layer_popup({
message: data,
title: '발신번호 관리',
callback: function () {
loadCallNumberLayer();
}
});
}).fail(function (xhr) {
var response = xhr.responseJSON;
if (response && response.data && response.data.canRegisterRepresentative) {
SmsCallerHubLayer.openRepresentativeRegisterGuide();
return;
}
if (response && response.data && response.data.isMigrationRequired) {
if (response.data.isRepresentative) {
SmsCallerHubLayer.openMigration({
// 이관을 마치면 막혀 있던 발신번호 관리 레이어를 다시 연다
onCompleted: function () {
 
 
 
 
 
 
loadCallNumberLayer();
$('#manageCallNumber').click();
}
});
return;
}
NCDSAlert({ message: response.message, subMessage: response.data.subMessage, iconType: 'error' });
return;
 
 
 
 
 
 
 
 
 
 
 
 
 
}
// 권한 실패는 403 이라 success 가 아니라 여기로 들어오므로 응답 본문의 안내 문구를 그대로 노출한다
NCDSAlert({ message: xhr.responseJSON?.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
});
});
</script>
 
Admin/crm/message_config/layer_call_number_result.php+165
변경 전layer_call_number_result.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_call_number_result.php
<!-- 내역 -->
<div class="ncua-search-result">
<div class="ncua-table ncua-table--horizontal">
<table class="popup-manage-call-number__product-table">
<colgroup>
<col width="79px">
<col>
<col>
<col width="100px">
<col>
<col width="90px">
</colgroup>
<thead>
<tr>
<th><div>선택</div></th>
<th><div>발신번호</div></th>
<th><div>관리 명칭</div></th>
<th><div>상태</div></th>
<th><div>승인일</div></th>
<th><div>관리</div></th>
</tr>
</thead>
<tbody>
<?php if (!empty($smsCallNumberList)): ?>
<?php foreach ($smsCallNumberList as $smsCallNumber): ?>
<tr>
<td>
<div>
<?php if ($currentCallNumber !== $smsCallNumber->getCallNumber()): ?>
<label class="ncua-radio-field ncua-radio-field--xs ncua-flex">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input <?= $smsCallNumber->getStatus()->name !== 'APPROVED' ? 'is-disabled' : '' ?>">
<input type="radio" name="callNumber" value="<?= $smsCallNumber->getCallNumber() ?>" data-call-number-text="<?= gd_number_to_phone($smsCallNumber->getCallNumber()) ?>" <?= $smsCallNumber->getStatus()->name !== 'APPROVED' ? 'disabled' : '' ?>/>
</span>
</label>
<?php else: ?>
<div>사용 중</div>
<?php endif; ?>
</div>
</td>
<td><div class="js-call-number" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>"><?= $smsCallNumber->getCallNumber() ?></div></td>
<td><div><?= gd_htmlspecialchars($smsCallNumber->getAliasName()) ?></div></td>
<td><div><?= $smsCallNumber->getStatus()->value ?></div></td>
<td><div><?= $smsCallNumber->getApprovedAt() ?? '-' ?></div></td>
<td>
<?php if (in_array($smsCallNumber->getStatus()->name, ['WAITING', 'APPROVED'])): ?>
<div>
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>" data-call-number="<?= $smsCallNumber->getCallNumber() ?>" data-manage-status="<?= $smsCallNumber->getStatus()->name ?>">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
<?php elseif (in_array($smsCallNumber->getStatus()->name, ['REJECTED', 'BLOCKED'])): ?>
<div>
<input type="hidden" id="rejectReason<?= $smsCallNumber->getCallNumberNo() ?>" value="<?= gd_htmlspecialchars($smsCallNumber->getReason() ?? '') ?>" />
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>" data-manage-status="<?= $smsCallNumber->getStatus()->name ?>">
<span class="ncua-btn__label">사유</span>
</button>
</div>
<?php else: ?>
<div>-</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6">
<div class="no-data">
<span>등록된 발신번호가 없습니다.</span>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="ncua-table-bottom"></div>
<!-- 페이지네이션 -->
<div class="ncua-pagination"><?= $page->getPage('loadContentsByPage(\'PAGELINK\')') ?>
</div>
 
<script type="text/javascript">
// 이 fragment 는 모달에서 페이지 이동마다 재주입되므로 top-level const/let 선언 시 재선언 SyntaxError 가 발생한다
$(document).ready(function () {
// 발신번호 상태 클릭 이벤트
document.querySelectorAll('[data-manage-status]')?.forEach( element => {
element.addEventListener('click', function (e) {
var manageStatus = this.getAttribute('data-manage-status');
 
switch (manageStatus) {
case 'WAITING':
case 'APPROVED':
confirmDeleteCallNumber(element);
break;
case 'REJECTED':
case 'BLOCKED':
var callNumberNo = element.getAttribute('data-call-number-no');
var rejectReason = document.getElementById(`rejectReason${callNumberNo}`)?.value ?? '';
var reasonTitle = manageStatus === 'BLOCKED' ? '발신번호 차단 사유' : '발신번호 반려 사유';
NCDSAlert({ message: reasonTitle, subMessage: rejectReason, iconType: 'warning' });
break;
default:
break;
}
});
});
});
 
function confirmDeleteCallNumber(element) {
var isRepresentative = <?= !empty($isRepresentative) ? 'true' : 'false' ?>;
 
if (!isRepresentative) {
NCDSAlert({
message: '발신관리책임자만 이용할 수 있습니다.',
subMessage: '발신관리책임자(최고운영자)에게 문의하시기 바랍니다.',
iconType: 'error'
});
return;
}
 
var callNumberNo = element.getAttribute('data-call-number-no');
var callNumber = element.getAttribute('data-call-number');
// 목록의 '사용 중' 표기와 같은 기준이어야 하므로 이 몰의 현재 발신번호와 비교한다
var inUse = callNumber === <?= json_encode($currentCallNumber) ?>;
 
NCDSConfirm({
message: inUse ? '사용 중인 발신번호입니다. 삭제하시겠습니까?' : '발신번호를 삭제하시겠습니까?',
subMessage: '발신번호 삭제 시, 다시 복구되지 않습니다.<br/>삭제 후 자동알림을 포함한 SMS가 발송되지 않습니다.',
btnText: {
confirmLabel: '확인',
cancelLabel: '취소'
},
callback: (type) => {
if (type) {
deleteCallNumber(callNumberNo);
}
}
});
}
 
function deleteCallNumber(callNumberNo) {
var callNumber = document.querySelector(`.js-call-number[data-call-number-no='${callNumberNo}']`).innerText;
$.post('/crm/message_config/layer_manage_call_number_ps.php', {mode: 'deleteCallNumber', callNumberNo: callNumberNo, callNumber: callNumber}, function (response) {
if (response.error || !response.success) {
NCDSAlert({ message: response.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
return;
}
 
NCDSAlert({
message: '발신번호 삭제가 완료되었습니다.',
iconType: 'success',
callback: function () {
loadContents();
}
});
}).fail(function (xhr) {
// 권한 실패는 403 이라 success 가 아니라 여기로 들어오므로 응답 본문의 안내 문구를 그대로 노출한다
NCDSAlert({ message: xhr.responseJSON?.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
}
 
function loadContentsByPage(page) {
var pageNumber = parseInt(page.split('=')[1], 10);
loadContents(pageNumber);
}
</script>
 
Admin/crm/message_config/layer_manage_call_number.php+130
변경 전layer_manage_call_number.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_manage_call_number.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/popup-manage-call-number.css') ?>">
<article class="ncua-content popup-manage-call-number">
<article class="ncua-content popup-manage-call-number__content">
<!-- 안내사항 -->
<div class="ncua-accordion ncua-accordion--blue">
<div class="ncua-accordion__content has-dot">
<li>발신번호 등록·삭제는 발신관리책임자(최고운영자)만 가능합니다.</li>
<li>발신번호 회선은 최대 유선 발신번호 5회선, 무선 발신번호 3회선 까지만 등록 가능합니다.</li>
<li>등록한 발신번호는 통합회원이 보유한 다른 쇼핑몰 어드민에도 함께 등록됩니다.</li>
</div>
</div>
<div class="ncua-table ncua-table--vertical ncua-info-table">
<table>
<colgroup>
<col width="144px">
<col >
</colgroup>
<tr>
<th><div>발신관리책임자</div></th>
<td>
<div>
<span><?= gd_htmlspecialchars($representativeName ?? '') ?></span>
</div>
</td>
</tr>
</table>
</div>
 
<!-- 발신번호 리스트 섹션 -->
<section class="send-number-section">
<header>
<h4 class="ncua-flex">발신번호 리스트</h4>
</header>
<div id="layerManageCallNumberResult"></div>
</section>
</article>
 
<!-- 하단 버튼 -->
<div class="ncua-content-footer">
<button type="button" id="registerSendNumber" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray">
<span class="ncua-btn__label">추가 등록</span>
</button>
<button type="button" id="saveSendNumber" class="ncua-btn ncua-btn--sm ncua-btn--primary">
<span class="ncua-btn__label">발신번호 설정</span>
</button>
</div>
</article>
<script type="text/javascript">
$(document).ready(function () {
loadContents();
 
// 추가 등록 버튼
document.querySelector('#registerSendNumber')?.addEventListener('click', function (e) {
e.preventDefault();
 
var isRepresentative = <?= !empty($isRepresentative) ? 'true' : 'false' ?>;
if (!isRepresentative) {
NCDSAlert({
message: '발신관리책임자만 이용할 수 있습니다.',
subMessage: '발신관리책임자(최고운영자)에게 문의하시기 바랍니다.',
iconType: 'error'
});
return;
}
 
SmsCallerHubLayer.openRegistration({
onCompleted: function () {
loadContents();
}
});
});
 
// 발신 번호 설정 버튼
document.querySelector('#saveSendNumber').addEventListener('click', function (e) {
e.preventDefault();
const selectedCallNumber = document.querySelector('input[name="callNumber"]:checked');
if (!selectedCallNumber) {
NCDSAlert({ message: '발신번호를 선택해 주세요.', iconType: 'error' });
return;
}
saveCallNumber(selectedCallNumber.value, selectedCallNumber.dataset.callNumberText);
});
});
 
function saveCallNumber(callNumber, callNumberText) {
// 이 레이어는 crm 외 화면(promotion 등)에서도 열리므로 상대경로를 쓰면 base 가 달라져 404 가 된다
$.post('/crm/message_config/layer_manage_call_number_ps.php', {mode: 'setCallNumber', callNumber: callNumber}, function (response) {
if (response.error || !response.success) {
NCDSAlert({ message: (response.error ? response.error.message : response.message) || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
return;
}
 
// 부모 폼이 smsCallNum 을 그대로 재저장하는 화면(자동알림 설정)이 있어 저장 직후 입력값까지 맞춰야 한다
$('input[name="smsCallNum"]').val(callNumber);
$('.smsCallNumText').html(callNumberText || callNumber).addClass('number text-darkblue bold');
 
if (window.NCDSToast) {
window.NCDSToast({message: '발신번호 설정이 완료되었습니다.', color: 'success'});
}
 
layer_close();
}).fail(function (xhr) {
// 권한 실패는 403 이라 success 가 아니라 여기로 들어오므로 응답 본문의 안내 문구를 그대로 노출한다
NCDSAlert({ message: xhr.responseJSON?.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
}
 
function loadContents(page = 1) {
$.ajax({
url: '/crm/message_config/layer_call_number_result.php',
type: 'POST',
data: { page: page },
success: function (data) {
// 200 으로 내려오는 오류는 JSON, 정상은 목록 HTML 로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
$('#layerManageCallNumberResult').html(data);
},
error: function (xhr) {
// 권한 실패는 403 이라 success 가 아니라 여기로 들어오므로 응답 본문의 안내 문구를 그대로 노출한다
NCDSAlert({ message: xhr.responseJSON?.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
}
});
}
</script>
 
Admin/crm/message_config/layer_manage_call_number_result.php-150
변경 전layer_manage_call_number_result.php
<!-- 내역 -->
<div class="ncua-search-result">
<div class="ncua-table ncua-table--horizontal">
<table class="popup-manage-call-number__product-table">
<colgroup>
<col width="79px">
<col>
<col>
<col width="100px">
<col>
<col width="90px">
</colgroup>
<thead>
<tr>
<th><div>선택</div></th>
<th><div>발신번호</div></th>
<th><div>관리 명칭</div></th>
<th><div>상태</div></th>
<th><div>승인일</div></th>
<th><div>관리</div></th>
</tr>
</thead>
<tbody>
<?php if (!empty($smsCallNumberList)): ?>
<?php foreach ($smsCallNumberList as $smsCallNumber): ?>
<tr>
<td>
<div>
<?php if ($currentCallNumber !== $smsCallNumber->getCallNumber()): ?>
<label class="ncua-radio-field ncua-radio-field--xs ncua-flex">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input <?= $smsCallNumber->getStatus()->name !== 'AUTHENTICATED' ? 'is-disabled' : '' ?>">
<input type="radio" name="callNumber" value="<?= $smsCallNumber->getCallNumber() ?>" <?= $smsCallNumber->getStatus()->name !== 'AUTHENTICATED' ? 'disabled' : '' ?>/>
</span>
</label>
<?php else: ?>
<div>사용 중</div>
<?php endif; ?>
</div>
</td>
<td><div class="js-call-number" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>"><?= $smsCallNumber->getCallNumber() ?></div></td>
<td><div><?= $smsCallNumber->getTitle() ?></div></td>
<td><div><?= $smsCallNumber->getStatus()->value ?></div></td>
<td><div><?= $smsCallNumber->getApprovedAt() ?? '-' ?></div></td>
<td>
<?php if (in_array($smsCallNumber->getStatus()->name, ['REGISTER', 'AUTHENTICATED'])): ?>
<div>
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>" data-manage-status="<?= $smsCallNumber->getStatus()->name ?>">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
<?php elseif ($smsCallNumber->getStatus()->name === 'CANCEL'): ?>
<div>
<input type="hidden" id="deniedReason<?= $smsCallNumber->getCallNumberNo() ?>" value="<?= $smsCallNumber->getDeniedReason() ?>" />
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" data-call-number-no="<?= $smsCallNumber->getCallNumberNo() ?>" data-manage-status="<?= $smsCallNumber->getStatus()->name ?>">
<span class="ncua-btn__label">반려사유</span>
</button>
</div>
<?php else: ?>
<div>-</div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6">
<div class="no-data">
<span>등록된 발신번호가 없습니다.</span>
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="ncua-table-bottom"></div>
<!-- 페이지네이션 -->
<div class="ncua-pagination"><?= $page->getPage('loadContentsByPage(\'PAGELINK\')') ?>
</div>
 
<script type="text/javascript">
const callerNo = <?= $caller->getCallerNo() ?>;
$(document).ready(function () {
// 발신번호 상태 클릭 이벤트
document.querySelectorAll('[data-manage-status]')?.forEach( element => {
element.addEventListener('click', function (e) {
const manageStatus = this.getAttribute('data-manage-status');
 
switch (manageStatus) {
case 'REGISTER':
case 'AUTHENTICATED':
NCDSConfirm({
message: '발신번호를 삭제하시겠습니까?',
subMessage: '발신번호 삭제 시, 다시 복구되지 않습니다.<br/>쇼핑몰에 연동된 발신번호가 없을 경우 자동알림을 포함한 모바일 메시지가 발송되지 않습니다.',
btnText: {
confirmLabel: '확인',
cancelLabel: '취소'
},
callback: (type) => {
if (type) {
const callNumberNo = element.getAttribute('data-call-number-no');
deleteCallNumber(callNumberNo);
}
}
});
break;
case 'CANCEL':
const callNumberNo = element.getAttribute('data-call-number-no');
const deniedReason = document.getElementById(`deniedReason${callNumberNo}`)?.value ?? '';
NCDSAlert({ message: '발신번호 반려 사유', subMessage: deniedReason, iconType: 'warning' });
break;
default:
break;
}
});
});
});
 
function deleteCallNumber(callNumberNo) {
var callNumber = document.querySelector(`.js-call-number[data-call-number-no='${callNumberNo}']`).innerText;
$.post('./popup_manage_call_number_ps.php', {mode: 'deleteCallNumber', callerNo: callerNo, callNumberNo: callNumberNo, callNumber: callNumber}, function (response) {
if (response.error || (!response.success && !response.data)) {
NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
return;
}
 
if (!response.success && response.data.type === 'conflict') {
NCDSAlert({
message: '다른 쇼핑몰 어드민에서 사용되고 있어 삭제할 수 없습니다.',
subMessage: 'NHN 커머스 마이페이지 > SMS 발신번호 관리에서 삭제하시기 바랍니다.',
iconType: 'error',
});
return;
}
 
NCDSAlert({
message: '발신번호 삭제가 완료되었습니다.',
iconType: 'success',
callback: function () {
window.location.reload();
}
});
});
}
 
function loadContentsByPage(page) {
const pageNumber = parseInt(page.split('=')[1], 10);
loadContents(pageNumber);
}
</script>
 
변경 후layer_manage_call_number_result.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/crm/popup_manage_call_number.php-130
변경 전popup_manage_call_number.php
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/popup-manage-call-number.css') ?>">
<article class="ncua-content popup-manage-call-number">
<header class="page-header ncua-page-header">
<h3>
<?= end($naviMenu->location) ?>
</h3>
</header>
<article class="ncua-content popup-manage-call-number__content">
<!-- 안내사항 아코디언 -->
<details class="ncua-accordion ncua-accordion--blue" open>
<summary>안내사항</summary>
<div class="ncua-accordion__content">
<li class="ncua-notice-info">발신번호 등록은 <a href="https://www.nhn-commerce.com/mygodo/sms/dashboard.gd" target="_blank">NHN커머스 > 마이페이지 > SMS발신번호 관리</a>에서 가능합니다.</li>
<li class="ncua-notice-info">발신번호 등록 단계: 등록 → 심사중 → 인증 또는 반려</li>
<li class="ncua-notice-info">반려가 된 발신번호는 ‘반려 사유’를 클릭해 반려된 사유를 확인하시기 바랍니다.</li>
<li class="ncua-notice-info">발신번호 관리 담당자가 변경된 경우, <a href="https://www.nhn-commerce.com/mygodo/sms/dashboard.gd" target="_blank">NHN커머스 > 마이페이지 > SMS발신번호 관리</a>에서 관리책임자 해제 후 발신번호를 다시 등록해주시기 바랍니다.</li>
</div>
</details>
<div class="ncua-table ncua-table--vertical ncua-info-table">
<table>
<colgroup>
<col width="144px">
<col >
<col width="144px">
<col >
</colgroup>
<tr>
<th><div>관리책임자</div></th>
<td>
<div>
<span><?= $caller->getCallerName() ?></span>
</div>
</td>
<th><div>인증 구분</div></th>
<td>
<div>
<span><?= $caller->isAgent() ? '대리인' : '대표자 본인' ?></span>
</div>
</td>
</tr>
</table>
</div>
 
<!-- 발신번호 리스트 섹션 -->
<section class="send-number-section">
<header>
<h4 class="ncua-flex">발신번호 리스트</h4>
</header>
<div id="layerManageCallNumberResult"></div>
</section>
</article>
 
<!-- 하단 버튼 -->
<div class="ncua-content-footer">
<button type="button" id="registerSendNumber" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray">
<span class="ncua-btn__label">추가 등록</span>
</button>
<button type="button" id="saveSendNumber" class="ncua-btn ncua-btn--sm ncua-btn--primary">
<span class="ncua-btn__label">발신 번호 설정</span>
</button>
</div>
</article>
<script>
 
function saveCallNumber(callNumber) {
$.post('./popup_manage_call_number_ps.php', {mode: 'setCallNumber', callNumber: callNumber}, function (response) {
if (response.error) {
NCDSAlert({ message: response.error.message, iconType: 'error' });
return;
}
 
if (response.success) {
if (window.opener && window.opener.NCDSToast) {
window.opener.NCDSToast({message: '발신번호 설정이 완료되었습니다.', color: 'success'});
}
 
window.close();
}
});
}
 
function initPopupManageCallNumber() {
loadContents();
 
// 추가 등록 버튼
document.querySelector('#registerSendNumber')?.addEventListener('click', function (e) {
e.preventDefault();
window.open('https://www.nhn-commerce.com/mygodo/sms/dashboard.gd', '_blank');
});
 
// 발신 번호 설정 버튼
document.querySelector('#saveSendNumber').addEventListener('click', function (e) {
e.preventDefault();
const selectedCallNumber = document.querySelector('input[name="callNumber"]:checked');
if(!selectedCallNumber) {
NCDSAlert({ message: '발신번호를 선택해 주세요.', iconType: 'error'});
return;
}
saveCallNumber(selectedCallNumber.value);
});
}
 
function loadContents(page = 1) {
$.ajax({
url: './message_config/layer_manage_call_number_result.php',
type: 'POST',
data: { page: page },
success: function (data) {
$('#layerManageCallNumberResult').html(data);
}
});
}
 
 
const DOM_READY_STATES = ['complete', 'interactive'];
const onReady = () => {
try {
initPopupManageCallNumber();
} catch (error) {
NCDSAlert({ message: e.message, iconType: 'error' });
}
};
if (DOM_READY_STATES.includes(document.readyState)) {
onReady();
} else {
document.addEventListener('DOMContentLoaded', onReady);
}
</script>
 
변경 후popup_manage_call_number.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/member/sms_auto.php+41-29
변경 전sms_auto.php
<!-- //@formatter:off -->
 
 
<input type="hidden" id="kakaoUse" name="kakaoUse" value="<?php echo $kakaoSetting['useFlag']; ?>"/>
<input type="hidden" id="kakaoOrderUse" name="kakaoOrderUse" value="<?php echo $kakaoSetting['orderUseFlag']; ?>"/>
<input type="hidden" id="kakaoMemberUse" name="kakaoMemberUse" value="<?php echo $kakaoSetting['memberUseFlag']; ?>"/>
<tr>
<th>SMS 발신번호</th>
<td class="form-inline">
<?php if ($smsPreRegister === false) { ?>
 
 
 
 
 
 
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<a href="<?= $commerceSmsIntroUrl; ?>" target="_blank" class="btn btn-gray btn-sm">발신번호 등록하기</a>
<?php } ?>
</div>
<div class="notice-info">
발신번호 사전등록제 : (전기통신사업법 제 84조의 2) 거짓으로 표시된 전화번호로 인한 이용자 피해 예방을 위해 사전 등록한 발신번호로만 SMS를 발송하실 수 있습니다. <a href="https://www.nhn-commerce.com/customer/board-view.gd?type=notice&idx=1247" target="_blank" class="snote bold">자세히보기 ></a>
</div>
<?php } elseif ($smsPreRegister === 'reset') { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">기 설정된 SMS 발신번호는 사전 등록된 번호가 아닙니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
<?php } ?>
</div>
<?php } elseif ($smsPreRegister === 'empty') { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
</span>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
</div>
<?php } elseif ($smsPreRegister === true) { ?>
<input type="hidden" name="smsCallNum" value="<?php echo gd_isset($smsAutoData['smsCallNum']) ?>"/>
<span class="smsCallNumText number text-blue bold"><?php echo gd_number_to_phone($smsAutoData['smsCallNum']) ?></span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 변경하기</button>
<?php } ?>
<?php } ?>
<?php if ($smsPreRegister !== true) { ?>
<div class="notice-danger">[SMS 설정]에 설정된 인증번호와 <a href="<?= $commerceShopMainUrl; ?>" class="btn-link text-red"><b>[마이페이지 > 쇼핑몰 관리]</b></a>에 설정된 인증번호가 일치해야 발신번호 정보가 정상 출력됩니다.</div>
<?php } ?>
</td>
</tr>
<tr>
 
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
var params = {
returnInput: 'smsCallNum'
};
$.get('../member/layer_sms_call_number_select.php', params, function (data) {
BootstrapDialog.show({
title: 'SMS 발신번호 목록',
message: $(data),
closable: true
 
 
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
});
});
 
 
변경 후sms_auto.php
<!-- //@formatter:off -->
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<input type="hidden" id="kakaoUse" name="kakaoUse" value="<?php echo $kakaoSetting['useFlag']; ?>"/>
<input type="hidden" id="kakaoOrderUse" name="kakaoOrderUse" value="<?php echo $kakaoSetting['orderUseFlag']; ?>"/>
<input type="hidden" id="kakaoMemberUse" name="kakaoMemberUse" value="<?php echo $kakaoSetting['memberUseFlag']; ?>"/>
<tr>
<th>SMS 발신번호</th>
<td class="form-inline">
<?php if (!empty($smsAutoData['smsCallNum'])) { ?>
<input type="hidden" name="smsCallNum" value="<?= gd_isset($smsAutoData['smsCallNum']) ?>"/>
<span class="smsCallNumText number text-blue bold"><?= gd_number_to_phone($smsAutoData['smsCallNum']) ?></span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 변경하기</button>
<?php } ?>
<?php } elseif (!$hasRegisteredCallNumber) { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 등록하기</button>
<?php } ?>
</div>
<div class="notice-info">
발신번호 사전등록제 : (전기통신사업법 제 84조의 2) 거짓으로 표시된 전화번호로 인한 이용자 피해 예방을 위해 사전 등록한 발신번호로만 SMS를 발송하실 수 있습니다. <a href="https://www.nhn-commerce.com/customer/board-view.gd?type=notice&idx=1247" target="_blank" class="snote bold">자세히보기 ></a>
</div>
<?php } else { ?>
 
 
 
 
 
 
 
 
 
 
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
</span>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
</div>
 
 
 
 
 
 
<?php } ?>
 
<div class="notice-danger">[SMS 설정]에 설정된 인증번호와 <a href="<?= $commerceShopMainUrl; ?>" class="btn-link text-red"><b>[마이페이지 > 쇼핑몰 관리]</b></a>에 설정된 인증번호가 일치해야 발신번호 정보가 정상 출력됩니다.</div>
 
</td>
</tr>
<tr>
 
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
$.post('/crm/message_config/layer_manage_call_number.php', null, function (data) {
// 권한·연동 오류는 JSON(Content-Type 기반 자동 파싱된 객체), 정상은 레이어 HTML 문자열로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
ncds_layer_popup({
message: data,
title: '발신번호 관리'
});
}).fail(function (xhr) {
var response = xhr.responseJSON;
if (response && response.data && response.data.canRegisterRepresentative) {
SmsCallerHubLayer.openRepresentativeRegisterGuide();
return;
}
if (response && response.data && response.data.isMigrationRequired) {
if (response.data.isRepresentative) {
SmsCallerHubLayer.openMigration({
// 이관을 마치면 막혀 있던 발신번호 관리 레이어를 다시 연다
onCompleted: function () {
$('.js-sms-call-number').click();
}
});
return;
}
NCDSAlert({ message: response.message, subMessage: response.data.subMessage, iconType: 'error' });
return;
}
NCDSAlert({ message: (response && response.message) || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
});
 
 
Admin/member/sms_send.php+41-27
변경 전sms_send.php
 
 
<script type="text/javascript">
<!--
/**
 
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
var params = {returnInput: 'smsCallNum'};
$.get('../member/layer_sms_call_number_select.php', params, function (data) {
BootstrapDialog.show({
title: 'SMS 발신번호 목록',
message: $(data),
closable: true
 
 
 
 
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
});
});
 
<tr>
<th class="require">SMS 발신번호</th>
<td class="form-inline">
<?php if ($smsPreRegister === false) { ?>
 
 
 
 
 
 
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<a href="<?= $commerceSmsIntroUrl; ?>" target="_blank" class="btn btn-gray btn-sm">발신번호 등록하기</a>
<?php } ?>
</div>
<div class="notice-info">
발신번호 사전등록제 : (전기통신사업법 제 84조의 2) 거짓으로 표시된 전화번호로 인한 이용자 피해 예방을 위해 사전 등록한 발신번호로만 SMS를 발송하실 수 있습니다. <a href="https://www.nhn-commerce.com/customer/board-view.gd?type=notice&idx=1247" target="_blank"
class="snote bold btn-link">자세히보기 ></a>
</div>
<?php } elseif ($smsPreRegister === 'reset') { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">기 설정된 SMS 발신번호는 사전 등록된 번호가 아닙니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
<?php } ?>
</div>
<?php } elseif ($smsPreRegister === 'empty') { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
</span>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
</div>
<?php } elseif ($smsPreRegister === true) { ?>
<input type="hidden" name="smsCallNum" value="<?php echo gd_isset($smsCallNum) ?>"/>
<span class="smsCallNumText number text-darkblue bold"><?php echo gd_number_to_phone($smsCallNum) ?></span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 변경하기</button>
<?php } ?>
<?php } ?>
<?php if ($smsPreRegister !== true) { ?>
<div class="notice-danger">[SMS 설정]에 설정된 인증번호와 <a href="<?= $commerceShopMainUrl; ?>" class="btn-link text-red"><b>[마이페이지 > 쇼핑몰 관리]</b></a>에 설정된 인증번호가 일치해야 발신번호 정보가 정상 출력됩니다.</div>
<?php } ?>
</td>
</tr>
 
 
변경 후sms_send.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script type="text/javascript">
<!--
/**
 
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
$.post('/crm/message_config/layer_manage_call_number.php', null, function (data) {
// 권한·연동 오류는 JSON(Content-Type 기반 자동 파싱된 객체), 정상은 레이어 HTML 문자열로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
ncds_layer_popup({
message: data,
title: '발신번호 관리'
});
}).fail(function (xhr) {
var response = xhr.responseJSON;
if (response && response.data && response.data.canRegisterRepresentative) {
SmsCallerHubLayer.openRepresentativeRegisterGuide();
return;
}
if (response && response.data && response.data.isMigrationRequired) {
if (response.data.isRepresentative) {
SmsCallerHubLayer.openMigration({
// 이관을 마치면 막혀 있던 발신번호 관리 레이어를 다시 연다
onCompleted: function () {
$('.js-sms-call-number').click();
}
});
return;
}
NCDSAlert({ message: response.message, subMessage: response.data.subMessage, iconType: 'error' });
return;
}
NCDSAlert({ message: (response && response.message) || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
});
 
<tr>
<th class="require">SMS 발신번호</th>
<td class="form-inline">
<?php if (!empty($smsCallNum)) { ?>
<input type="hidden" name="smsCallNum" value="<?= gd_isset($smsCallNum) ?>"/>
<span class="smsCallNumText number text-darkblue bold"><?= gd_number_to_phone($smsCallNum) ?></span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 변경하기</button>
<?php } ?>
<?php } elseif (!$hasRegisteredCallNumber) { ?>
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
<span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span>
</span>
<?php if (gd_is_provider() === false) { ?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 등록하기</button>
<?php } ?>
</div>
<div class="notice-info">
발신번호 사전등록제 : (전기통신사업법 제 84조의 2) 거짓으로 표시된 전화번호로 인한 이용자 피해 예방을 위해 사전 등록한 발신번호로만 SMS를 발송하실 수 있습니다. <a href="https://www.nhn-commerce.com/customer/board-view.gd?type=notice&idx=1247" target="_blank"
class="snote bold btn-link">자세히보기 ></a>
</div>
<?php } else { ?>
 
 
 
 
 
 
 
 
 
 
<div>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText">
</span>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 선택하기</button>
</div>
 
 
 
 
 
 
<?php } ?>
 
<div class="notice-danger">[SMS 설정]에 설정된 인증번호와 <a href="<?= $commerceShopMainUrl; ?>" class="btn-link text-red"><b>[마이페이지 > 쇼핑몰 관리]</b></a>에 설정된 인증번호가 일치해야 발신번호 정보가 정상 출력됩니다.</div>
 
</td>
</tr>
 
 
Admin/policy/admin_log_list.php+37-23
변경 전admin_log_list.php
 
<form name="frmSearch" id="frmSearch" action="admin_log_list.php" method="get" class="js-form-enter-submit">
<input type="hidden" name="view" value="<?=$view;?>">
 
 
<div class="search-detail-box">
<table class="table table-cols mgb15">
<tr>
<th class="width-md">운영자 아이디</th>
<td>
<div class="form-inline">
<?= gd_select_box('searchKind', 'searchKind', $searchKindASelectBox, null, gd_isset($req['searchKind']), null, null, 'form-control '); ?>
<input type="text" class="form-control width300" name="managerId" value="<?=$req['managerId']?>"/>
</div>
</td>
</tr>
<li role="presentation" <?=$view == 'adminAccess' ? 'class="active"' : ''?>>
<a href="../policy/admin_log_list.php?view=adminAccess<?=$queryString;?>">로그인, 인증기록</a>
</li>
 
 
 
</ul>
 
<div class="table-header">
</div>
</form>
 
 
 
 
 
<table class="table table-rows table-fixed">
<colgroup>
<col class="width15p">
<col class="width15p">
<col class="width15p">
<?php if ($view == 'default') { ?><col class="width15p"><?php } ?>
<?php if ($view == 'default' || $view == 'adminList') { ?><col ><?php } ?>
<col class="width15p">
</colgroup>
<thead>
<tr>
<th>접속일시</th>
<th>접속IP</th>
<th>운영자 아이디</th>
<?php if ($view == 'default') { ?><th>메뉴구분</th><?php } ?>
<?php if ($view == 'default' || $view == 'adminList') { ?><th>접속페이지<br>(개인정보관련)</th><?php } ?>
<th>수행업무</th>
</tr>
</thead>
<tbody>
<?php foreach($data['list'] as $row){
if(!$row['page']) {
$row['page'] = $row['baseUri'];
}
<td><?=$row['regDt']?></td>
<td><?=$row['ip']?></td>
<td><?=$row['managerId']?> <?=$row['deleteText']?></td>
<?php if ($view == 'default') { ?><td><?=$row['menu']?></td><?php } ?>
<?php if ($view == 'default' || $view == 'adminList') { ?><td><?=$row['page']?></td><?php } ?>
<td>
<?=$row['action']?>
<?php if ($row['displayDetailLogFl'] == 'y') { ?>
 
 
 
<button class="btn btn-white btn-sm js-detail-log" data-sno="<?=$row['sno']?>">상세</button>
<?php } ?>
 
</td>
</tr>
<?php }?>
</tbody>
</table>
<?php if ($view == 'default') { ?>
<div class="col-xs-12">
<div class="pull-right">
<button type="button" class="btn btn-white js-admin-log-excel">엑셀 다운로드 내역</button>
</button>
</div>
</div>
<?php } ?>
<div align="center"><?=$data['page']; ?></div>
 
<script type="text/javascript">
});
});
 
 
 
 
 
 
 
 
 
 
 
$(document).on('click', '.js-admin-log-excel', function (e) {
var addParam = {
"view": 'layer'
};
layer_add_info('admin_log_excel', addParam);
});
 
//검색어 변경 될 때 placeHolder 교체 및 검색 종류 변환 및 검색 종류 변환
var searchKeyword = $('#frmSearch input[name=managerId]');
var searchKind = $('#frmSearch #searchKind');
setKeywordPlaceholder(searchKeyword, searchKind);
searchKind.change(function (e) {
setKeywordPlaceholder(searchKeyword, searchKind);
});
});
</script>
 
변경 후admin_log_list.php
 
<form name="frmSearch" id="frmSearch" action="admin_log_list.php" method="get" class="js-form-enter-submit">
<input type="hidden" name="view" value="<?=$view;?>">
<?php // 튜닝 컨트롤러는 searchKind 를 강제하지 않아 화면 기본 검색을 전체일치로 유지하려 싣는다 — 쿼리스트링으로 덮을 수 있어 통제 수단은 아니다 ?>
<input type="hidden" name="searchKind" value="equalSearch">
<div class="search-detail-box">
<table class="table table-cols mgb15">
<tr>
<th class="width-md">운영자 아이디</th>
<td>
<div class="form-inline">
<input type="text" class="form-control width300" name="managerId" value="<?= gd_htmlspecialchars($req['managerId'] ?? '') ?>" placeholder="검색어 전체를 정확히 입력하세요."/>
 
</div>
</td>
</tr>
<li role="presentation" <?=$view == 'adminAccess' ? 'class="active"' : ''?>>
<a href="../policy/admin_log_list.php?view=adminAccess<?=$queryString;?>">로그인, 인증기록</a>
</li>
<li role="presentation" <?=$view == 'smsList' ? 'class="active"' : ''?>>
<a href="../policy/admin_log_list.php?view=smsList<?=$queryString;?>">SMS 관련 기록</a>
</li>
</ul>
 
<div class="table-header">
</div>
</form>
 
<?php if ($view == 'smsList' && !empty($data['apiErrorFl'])): ?>
<div class="notice-danger mgb0 mgl15 mgb15">SMS 관련 기록을 조회할 수 없습니다. 잠시 후 다시 시도해 주세요.</div>
<?php endif; ?>
 
<table class="table table-rows table-fixed">
<colgroup>
<col class="width15p">
<col class="width15p">
<col class="width15p">
<?php if ($displayMenuColumnFl): ?><col class="width15p"><?php endif; ?>
<?php if ($displayPageColumnFl): ?><col ><?php endif; ?>
<col class="width15p">
</colgroup>
<thead>
<tr>
<th><?= $view == 'smsList' ? '처리일시' : '접속일시' ?></th>
<th>접속IP</th>
<th>운영자 아이디</th>
<?php if ($displayMenuColumnFl): ?><th>메뉴구분</th><?php endif; ?>
<?php if ($displayPageColumnFl): ?><th>접속페이지<?= $displayPersonalInfoNoteFl ? '<br>(개인정보관련)' : '' ?></th><?php endif; ?>
<th>수행업무</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['list'] as $row):
if(!$row['page']) {
$row['page'] = $row['baseUri'];
}
<td><?=$row['regDt']?></td>
<td><?=$row['ip']?></td>
<td><?=$row['managerId']?> <?=$row['deleteText']?></td>
<?php if ($displayMenuColumnFl): ?><td><?=$row['menu']?></td><?php endif; ?>
<?php if ($displayPageColumnFl): ?><td><?=$row['page']?></td><?php endif; ?>
<td>
<?=$row['action']?>
<?php if ($row['displayDetailLogFl'] == 'y'): ?>
<?php if ($view == 'smsList'): ?>
<button class="btn btn-white btn-sm js-sms-detail-log" data-log-id="<?=$row['logId']?>">상세</button>
<?php else: ?>
<button class="btn btn-white btn-sm js-detail-log" data-sno="<?=$row['sno']?>">상세</button>
<?php endif; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($view == 'default'): ?>
<div class="col-xs-12">
<div class="pull-right">
<button type="button" class="btn btn-white js-admin-log-excel">엑셀 다운로드 내역</button>
</button>
</div>
</div>
<?php endif; ?>
<div align="center"><?=$data['page']; ?></div>
 
<script type="text/javascript">
});
});
 
// SMS 관련 기록 상세
$('.js-sms-detail-log').on('click', function () {
var addParam = {
'logId': $(this).data('log-id'),
'layerTitle': '개인정보접속기록'
};
 
layer_add_info('sms_personal_info_access_log', addParam);
});
 
$(document).on('click', '.js-admin-log-excel', function (e) {
var addParam = {
"view": 'layer'
};
layer_add_info('admin_log_excel', addParam);
});
 
 
 
 
 
 
 
 
});
</script>
 
Admin/policy/manage_list.php+14
변경 전manage_list.php
<input type="text" name="keyword" value="<?= $search['keyword']; ?>" class="form-control width-xl"/>
</td>
</tr>
 
 
 
 
 
 
 
 
 
 
<tbody class="js-search-detail">
<tr>
<th>장기 미로그인</th>
<th class="width10p">공급사 구분</th>
<th class="width30p">아이디/닉네임</th>
<th class="width7p">이름</th>
 
<th class="width7p">직원여부</th>
<th class="width10p">직원/부서/직급/직책</th>
<th class="width10p">연락처</th>
<td>
<div><?= $val['managerNm']; ?></div>
</td>
 
 
 
<td>
<?= $employeeList[$val['employeeFl']] ?>
</td>
 
변경 후manage_list.php
<input type="text" name="keyword" value="<?= $search['keyword']; ?>" class="form-control width-xl"/>
</td>
</tr>
<tr>
<th>SMS 권한</th>
<td colspan="3" class="form-inline">
<?php foreach ($smsRoleList as $smsRoleValue => $smsRoleName) { ?>
<label class="radio-inline">
<input type="radio" name="smsRole" value="<?= $smsRoleValue; ?>" <?= gd_isset($checked['smsRole'][$smsRoleValue]); ?> /> <?= $smsRoleName; ?>
</label>
<?php } ?>
</td>
</tr>
<tbody class="js-search-detail">
<tr>
<th>장기 미로그인</th>
<th class="width10p">공급사 구분</th>
<th class="width30p">아이디/닉네임</th>
<th class="width7p">이름</th>
<th class="width7p">SMS 권한</th>
<th class="width7p">직원여부</th>
<th class="width10p">직원/부서/직급/직책</th>
<th class="width10p">연락처</th>
<td>
<div><?= $val['managerNm']; ?></div>
</td>
<td>
<div><?= gd_isset($val['smsRoleName']); ?></div>
</td>
<td>
<?= $employeeList[$val['employeeFl']] ?>
</td>
 
Admin/policy/_manage_permission_manager_list.php+11-8
변경 전_manage_permission_manager_list.php
<table class="table table-rows table-fixed mgb0">
<thead>
<tr>
<th class="width10p center"><input type="checkbox" id="chk_all" class="js-checkall" data-target-name="manage_sno"/></th>
<th class="width10p center">번호</th>
<th class="width20p center">공급사 구분</th>
<th class="center">아이디/닉네임</th>
<th class="width20p center">이름</th>
<th class="width18p center">개별수정</th>
 
</tr>
</thead>
</table>
<div class="manager-list-body mgb15">
<table class="table table-rows table-fixed mgb0">
<colgroup>
<col class="width10p"/>
<col class="width10p"/>
<col class="width20p"/>
<col/>
<col class="width20p"/>
<col class="width18p"/>
 
</colgroup>
<tbody>
<?php
$superText = ($val['sno'] == 1) ? '<br/><span class="text-blue">(최고운영자)</span>' : '<br/><span class="text-blue">(대표운영자)</span>';
}
?>
<tr data-sno="<?= $val['sno']; ?>" data-issuper="<?= $val['isSuper']; ?>">
<td class="center">
<input type="checkbox" name="manage_sno[]" value="<?= $val['sno']; ?>" />
</td>
<?= $superText ?>
</td>
<td class="center"><div><?= $val['managerNm']; ?></div></td>
 
<td class="center"><input type="button" value="권한보기" class="btn btn-white btn-view-unit-permission"></td>
</tr>
<?php
} else {
?>
<tr>
<td class="center" colspan="6">검색된 정보가 없습니다.</td>
</tr>
<?php
}
 
변경 후_manage_permission_manager_list.php
<table class="table table-rows table-fixed mgb0">
<thead>
<tr>
<th class="width5p center"><input type="checkbox" id="chk_all" class="js-checkall" data-target-name="manage_sno"/></th>
<th class="width7p center">번호</th>
<th class="width20p center">공급사 구분</th>
<th class="center">아이디/닉네임</th>
<th class="width20p center">이름</th>
<th class="width15p center">SMS 권한</th>
<th class="width11p center">개별수정</th>
</tr>
</thead>
</table>
<div class="manager-list-body mgb15">
<table class="table table-rows table-fixed mgb0">
<colgroup>
<col class="width5p"/>
<col class="width7p"/>
<col class="width20p"/>
<col/>
<col class="width20p"/>
<col class="width15p"/>
<col class="width11p"/>
</colgroup>
<tbody>
<?php
$superText = ($val['sno'] == 1) ? '<br/><span class="text-blue">(최고운영자)</span>' : '<br/><span class="text-blue">(대표운영자)</span>';
}
?>
<tr data-sno="<?= $val['sno']; ?>" data-issuper="<?= $val['isSuper']; ?>" data-sms-role="<?= gd_isset($val['smsRole']); ?>" data-sms-permission-locked="<?= $val['smsPermissionLocked']; ?>">
<td class="center">
<input type="checkbox" name="manage_sno[]" value="<?= $val['sno']; ?>" />
</td>
<?= $superText ?>
</td>
<td class="center"><div><?= $val['managerNm']; ?></div></td>
<td class="center"><div><?= gd_isset($val['smsRoleName']); ?></div></td>
<td class="center"><input type="button" value="권한보기" class="btn btn-white btn-view-unit-permission"></td>
</tr>
<?php
} else {
?>
<tr>
<td class="center" colspan="7">검색된 정보가 없습니다.</td>
</tr>
<?php
}
 
Admin/policy/_manage_permission_menu_list.php+16-1
변경 전_manage_permission_menu_list.php
}
break;
}
 
 
 
 
 
 
 
 
 
 
 
 
default:
if ($adminMenuType == 'd' && $isSuper == 'y') { // 본사 최고운영자 수정 일 경우
$disabled['functionAuth'][$code] = 'disabled="disabled"';
break;
}
?>
<li class="dropdown-item"><label><input type="checkbox" name="functionAuth[<?= $code; ?>]" data-value="<?= $code; ?>" value="y" <?= gd_isset($disabled['functionAuth'][$code]); ?> <?= gd_isset($checked['functionAuth'][$code]['y']); ?> /> <?= $name; ?></label></li>
<?php
// 속도 높이기 위해 불필요해진 변수 unset
unset($disabled['functionAuth'][$code]);
?>
</table>
</div>
 
<div class="access-control-reason <?=(!empty($sno) || $changeType == 'init') ? '' : 'display-none'?>" style="clear: both;">
<div class="table-action-dropdown">
<div class="table-action mgt0 mgb15">
$('input:checkbox[name="manage_sno[]"]:checked').length > 0 ? $('.manage-btn').prop('disabled', false) : $('.manage-btn').prop('disabled', true);
}
});
 
 
}
});
 
 
변경 후_manage_permission_menu_list.php
}
break;
}
case 'messageSendSms':
if ($adminMenuType == 'd') {
if (!empty($isMessageApprover)) {
$disabled['functionAuth'][$code] = 'data-disabled-lock="lock" disabled="disabled"';
$checked['functionAuth'][$code]['y'] = 'checked="checked"';
} else {
// lock 만 부여 = 전체권한 프리셋 일괄 변경(managePermission.js) 대상에서만 제외, 체크박스는 조작 가능
$disabled['functionAuth'][$code] = 'data-disabled-lock="lock"';
}
break;
}
// 그 외에는 일반 기능권한과 동일 동작 (default 로 진행)
default:
if ($adminMenuType == 'd' && $isSuper == 'y') { // 본사 최고운영자 수정 일 경우
$disabled['functionAuth'][$code] = 'disabled="disabled"';
break;
}
?>
<li class="dropdown-item"><label><input type="checkbox" name="functionAuth[<?= $code; ?>]" data-value="<?= $code; ?>" value="y" data-origin-checked="<?= !empty($checked['functionAuth'][$code]['y']) ? 'y' : 'n'; ?>" <?= gd_isset($disabled['functionAuth'][$code]); ?> <?= gd_isset($checked['functionAuth'][$code]['y']); ?> /> <?= $name; ?></label></li>
<?php
// 속도 높이기 위해 불필요해진 변수 unset
unset($disabled['functionAuth'][$code]);
?>
</table>
</div>
<input type="hidden" id="messageSenderMeta" data-editor-approver="<?= !empty($isEditorMessageApprover) ? 'y' : 'n'; ?>" data-editor-representative="<?= !empty($isEditorMessageRepresentative) ? 'y' : 'n'; ?>" data-caller-blocked="<?= !empty($isCallerBlocked) ? 'y' : 'n'; ?>" data-sender-count="<?= (int)(gd_isset($messageSenderCount, 0)); ?>" data-sender-limit="<?= (int)(gd_isset($messageSenderLimit, 0)); ?>"/>
<div class="access-control-reason <?=(!empty($sno) || $changeType == 'init') ? '' : 'display-none'?>" style="clear: both;">
<div class="table-action-dropdown">
<div class="table-action mgt0 mgb15">
$('input:checkbox[name="manage_sno[]"]:checked').length > 0 ? $('.manage-btn').prop('disabled', false) : $('.manage-btn').prop('disabled', true);
}
});
} else if ($('#menuList input:checkbox[data-value="messageSendSms"]').length > 0) {
$('.access-control-reason').removeClass('display-none');
}
});
 
 
Admin/policy/manage_permission.php+61
변경 전manage_permission.php
</tr>
<?php } ?>
<?php } ?>
 
 
 
 
 
 
 
 
 
 
<tr>
<th>검색어</th>
<td class="form-inline">
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
form.target = 'ifrmProcess';
form.submit();
},
 
변경 후manage_permission.php
</tr>
<?php } ?>
<?php } ?>
<tr>
<th>SMS 권한</th>
<td colspan="3" class="form-inline">
<?php foreach ($smsRoleList as $smsRoleValue => $smsRoleName) { ?>
<label class="radio-inline">
<input type="radio" name="smsRole" value="<?= $smsRoleValue; ?>" <?= $smsRoleValue === 'all' ? 'checked="checked"' : ''; ?> /> <?= $smsRoleName; ?>
</label>
<?php } ?>
</td>
</tr>
<tr>
<th>검색어</th>
<td class="form-inline">
}
}
 
// 최고운영자와 부운영자 동시 선택 차단
var $smsCheckedRows = $('input:checkbox[name="manage_sno[]"]:checked').closest('tr');
if ($smsCheckedRows.length > 1 && $smsCheckedRows.filter('[data-issuper="y"]').length > 0) {
dialog_alert('최고운영자와 부운영자를 동시에 수정할 수 없습니다.', '정보');
return false;
}
 
// 발송책임자/위임자 대상은 disabled(강제 부여)라 저장 직렬화·서버 모두 미반영
var $smsSetBox = $('#menuList input:checkbox[data-value="messageSendSms"]:not(:disabled)');
if ($smsSetBox.length > 0) {
var smsSetGranted = $smsSetBox.is(':checked');
var smsChanged = false, smsNewSenderCount = 0;
$smsCheckedRows.each(function () {
var smsRole = $(this).data('smsRole');
// 잠금 판정은 서버가 행마다 내려준다 — 미등록 최고운영자는 smsRole 이 비어 있어 역할값만으로는 걸러지지 않는다
if ($(this).data('smsPermissionLocked') === 'y') {
return; // 최고운영자/위임자는 messageSendSms 강제라 대상 아님
}
var nowSender = (smsRole === 'sender');
if (nowSender !== smsSetGranted) {
smsChanged = true;
}
if (smsSetGranted && !nowSender) {
smsNewSenderCount++;
}
});
if (smsChanged) {
var $smsMeta = $('#messageSenderMeta');
if ($smsMeta.data('editorApprover') !== 'y') {
dialog_alert('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.', '경고');
return false;
}
if ($smsMeta.data('editorRepresentative') === 'y' && $smsMeta.data('callerBlocked') === 'y') {
dialog_alert('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.', '경고');
return false;
}
if (smsSetGranted) {
var smsSenderCount = parseInt($smsMeta.data('senderCount'), 10) || 0;
var smsSenderLimit = parseInt($smsMeta.data('senderLimit'), 10) || 0;
if (smsSenderCount + smsNewSenderCount > smsSenderLimit) {
dialog_alert('발송담당자는 최대 ' + smsSenderLimit + '명까지 지정 가능합니다.', '경고');
return false;
}
}
if ($.trim($('#accessControlReason').val()) === '') {
dialog_alert('발송담당자 권한 변경 사유를 작성해주세요.', '경고');
return false;
}
}
}
 
form.target = 'ifrmProcess';
form.submit();
},
 
Admin/policy/manage_register.php+210-9
변경 전manage_register.php
기본정보
</div>
 
 
 
 
 
 
 
 
 
 
<table class="table table-cols">
<colgroup>
<col class="width-md"/>
<tr>
<th class="require">이름</th>
<td class="form-inline">
<input type="text" name="managerNm" value="<?= gd_isset($data['managerNm']); ?>" class="form-control js-maxlength" maxlength="20"/>
 
 
 
</td>
<th>닉네임</th>
<td class="form-inline">
<tr>
<th>휴대폰번호</th>
<td class="form-inline" colspan="3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<label title="휴대폰 번호를 입력해 주세요!">
<input type="text" name="cellPhone" value="<?= $data['cellPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</label>
<div class="notice-info">
발신번호 및 메시지 인증번호 관리는 <a href="/crm/message_config.php" target="_blank" class="btn-link">[CRM > 메시지 설정 > 모바일 메시지 설정]</a> 에서 확인할 수 있습니다.
</div>
 
</td>
</tr>
 
<th>이메일</th>
<td class="form-inline" colspan="3">
<label title="이메일을 입력해 주세요!">
<input type="text" name="email[]" value="<?= gd_isset($data['email'][0]); ?>" class="form-control width-md"/>
@
<input type="text" id="email" name="email[]" value="<?= gd_isset($data['email'][1]); ?>" class="form-control width-md"/>
<?= gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1]); ?>
</label>
 
 
 
 
<button type="button" class="btn btn-gray btn-sm js-request-authemail">인증번호받기</button>
<span id="emailAuthSendBox" class="<?php if (gd_isset($data['isEmailAuth']) === 'y') { ?>display-none<?php } ?>">
인증번호 :
<a href="/policy/manage_security.php" target="_blank" class="btn-link">운영보안설정 바로가기 ></a>
<?php } ?>
</div>
 
</td>
</tr>
 
<col/>
</colgroup>
<tbody>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<tr>
<th>운영자 IP 접속제한</th>
<td>
<script type="text/javascript">
<!--
var isUseProvider = '<?=gd_use_provider()?>';
 
 
 
 
 
 
 
 
 
 
var manage_register = {
regex: {
auth_no: /\d{8}/
$(document).ready(function () {
$('.sms-point').text(manage_register.sms_point);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$.validator.addMethod('smsAuto', function(value){
if (value == 'n') {
return true;
onkeyup: false,
ignore: ':hidden',
submitHandler: function (form) {
 
 
 
 
 
if ($('input[name="permissionSetupMode"]').length == 1) { // 운영자 권한 설정 방식 경우
if ($('input[name="permissionFl"]').length == 0) {
alert("메뉴 권한은 최소 1개 이상 '읽기' 또는 '읽기+쓰기'로 설정하셔야 합니다.");
});
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
form.target = 'ifrmProcess';
form.submit();
},
});
 
$('.js-request-authno').bind('click', function () {
if (manage_register.sms_point < 1) {
alert('메시지 포인트가 부족합니다. 인증번호 발송을 위해 메시지 포인트를 충전해주세요');
return false;
}
 
var cellPhoneFlag = true;
var $sms_auth_send_box = $('#smsAuthSendBox');
});
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$('.js-request-authemail').bind('click', function () {
var emailFlag = true;
var $email_auth_send_box = $('#emailAuthSendBox');
$('input[name*=\'cellPhone\']').number_only();
 
if ($('input[name="permissionSetupMode"]').length == 1) { // 운영자 권한 설정 방식 경우
 
 
 
 
 
 
 
$('input[name=scmFl][value=n]').bind('click', function () {
if ($('input[name="scmNo"]').length > 0) {
$('.btn-icon-delete').trigger('click');
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
function layer_register(typeStr, mode) {
var addParam = {
"mode": mode,
 
if (typeStr == 'scm') {
addParam['callFunc'] = 'setScmSelect';
$('input:radio[name=scmFl]:input[value=y]').prop("checked", true);
 
 
$('select[name="add_must_info_sel"]').html("<option>= 상품 필수 정보 선택 =</option>");
$('select[name="add_goods_info_sel"]').html("<option>= 추가 상품 그룹 정보 선택 =</option>");
}
}
 
function setScmSelect(data) {
 
 
 
//공급사 값 세팅
displayTemplate(data);
 
if ($('input[name="permissionSetupMode"]').length != 1) { // 운영자 권한 설정 방식이 아닌 경우(기존)
developmentPermissionToggle(data);
changePermissionLayout(data);
 
변경 후manage_register.php
기본정보
</div>
 
<?php if (!empty($superManagerCommerceGuide)): ?>
<div class="mgb10">
<div class="notice-info"><strong>최고운영자 정보(이름/휴대폰번호/이메일)는 통합회원 마이페이지에서 수정할 수 있습니다.</strong></div>
<div class="notice-info">보안 강화를 위해 계정당 1인 1계정 사용 환경으로 개선되어, 최고운영자 정보는 통합회원 정보와 항상 동일하게 유지됩니다.</div>
<div class="notice-info">통합회원 마이페이지에서 이름/휴대폰번호/이메일을 수정하시면, 최고운영자 정보에도 동일하게 반영됩니다. <a href="https://accounts.godo.co.kr/mypage/acc" target="_blank" class="btn-link">통합회원 정보 수정하러 가기 ></a></div>
<div class="notice-info">통합회원이 아닌 담당자가 최고운영자 계정을 이용하고 계셨다면, 부운영자 계정을 별도로 등록해 이용해 주세요.</div>
</div>
<?php endif; ?>
 
<table class="table table-cols">
<colgroup>
<col class="width-md"/>
<tr>
<th class="require">이름</th>
<td class="form-inline">
<input type="text" name="managerNm" value="<?= gd_isset($data['managerNm']); ?>" class="form-control js-maxlength" maxlength="20"<?= !empty($senderNameLocked) ? ' disabled="disabled"' : ''; ?>/>
<?php if (!empty($superManagerCommerceLinked)): ?>
<div class="notice-info"><a href="https://accounts.godo.co.kr/mypage/acc" target="_blank" class="btn-link">통합 마이페이지</a>에서 수정할 수 있습니다.</div>
<?php endif; ?>
</td>
<th>닉네임</th>
<td class="form-inline">
<tr>
<th>휴대폰번호</th>
<td class="form-inline" colspan="3">
<?php if (!empty($superManagerCommerceLinked)): ?>
<label title="휴대폰 번호를 입력해 주세요!">
<input type="text" name="cellPhone" value="<?= $data['cellPhone']; ?>" maxlength="12" class="form-control js-number-only width-md" disabled="disabled"/>
</label>
<?php if ($selfCertification['enabled']): ?>
<button type="button" class="btn btn-gray btn-sm js-request-self-certification">본인인증</button>
<span class="my-sms-auth js-self-certification-state<?= $selfCertification['certified'] ? '' : ' text-red'; ?>">
<?= $selfCertification['certified'] ? '(인증완료)' : '(미인증)'; ?>
</span>
<?php endif; ?>
<div class="notice-info"><a href="https://accounts.godo.co.kr/mypage/acc" target="_blank" class="btn-link">통합 마이페이지</a>에서 수정할 수 있습니다.</div>
<?php if ($selfCertification['enabled']): ?>
<div class="notice-info">
인증이 완료된 휴대폰번호는 관리자페이지 보안로그인 및 화면보안접속 시 인증정보로 사용할 수 있습니다.
<?php if (!$isProvider): ?>
<a href="/policy/manage_security.php" target="_blank" class="btn-link">운영보안설정 바로가기 ></a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php elseif ($selfCertification['enabled']): ?>
<label title="휴대폰 번호를 입력해 주세요!">
<input type="text" name="cellPhone" value="<?= $data['cellPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"<?= !empty($senderCellPhoneLocked) ? ' disabled="disabled"' : ''; ?>/>
</label>
<button type="button" class="btn btn-gray btn-sm js-request-self-certification">본인인증</button>
<span class="my-sms-auth js-self-certification-state<?= $selfCertification['certified'] ? '' : ' text-red'; ?>">
<?= $selfCertification['certified'] ? '(인증완료)' : '(미인증)'; ?>
</span>
<div class="notice-info">
인증이 완료된 휴대폰번호는 관리자페이지 보안로그인 및 화면보안접속 시 인증정보로 사용할 수 있습니다.
<?php if (!$isProvider): ?>
<a href="/policy/manage_security.php" target="_blank" class="btn-link">운영보안설정 바로가기 ></a>
<?php endif; ?>
</div>
<?php else: ?>
<label title="휴대폰 번호를 입력해 주세요!">
<input type="text" name="cellPhone" value="<?= $data['cellPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</label>
<div class="notice-info">
발신번호 및 메시지 인증번호 관리는 <a href="/crm/message_config.php" target="_blank" class="btn-link">[CRM > 메시지 설정 > 모바일 메시지 설정]</a> 에서 확인할 수 있습니다.
</div>
<?php endif; ?>
</td>
</tr>
 
<th>이메일</th>
<td class="form-inline" colspan="3">
<label title="이메일을 입력해 주세요!">
<input type="text" name="email[]" value="<?= gd_isset($data['email'][0]); ?>" class="form-control width-md"<?= !empty($superManagerCommerceLinked) ? ' disabled="disabled"' : ''; ?>/>
@
<input type="text" id="email" name="email[]" value="<?= gd_isset($data['email'][1]); ?>" class="form-control width-md"<?= !empty($superManagerCommerceLinked) ? ' disabled="disabled"' : ''; ?>/>
<?= gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, !empty($superManagerCommerceLinked) ? 'disabled="disabled"' : null); ?>
</label>
 
<?php if (!empty($superManagerCommerceLinked)): ?>
<div class="notice-info"><a href="https://accounts.godo.co.kr/mypage/acc" target="_blank" class="btn-link">통합 마이페이지</a>에서 수정할 수 있습니다.</div>
<?php else: ?>
<button type="button" class="btn btn-gray btn-sm js-request-authemail">인증번호받기</button>
<span id="emailAuthSendBox" class="<?php if (gd_isset($data['isEmailAuth']) === 'y') { ?>display-none<?php } ?>">
인증번호 :
<a href="/policy/manage_security.php" target="_blank" class="btn-link">운영보안설정 바로가기 ></a>
<?php } ?>
</div>
<?php endif; ?>
</td>
</tr>
 
<col/>
</colgroup>
<tbody>
<?php if (!empty($messageDelegate['visible'])): // SMS 발신 위임자 권한 (권한 설정 테이블 row, 지정/회수는 최고운영자만) ?>
<tr class="js-message-delegate-area">
<th>SMS 발신 위임자 권한 설정</th>
<td class="form-inline">
<input type="hidden" name="messageDelegateAction" id="messageDelegateAction" value="">
<?php if ($messageDelegate['assigned'] === true): ?>
<span class="js-message-delegate-done">권한 설정 완료</span>
<?php if ($messageDelegate['editable'] === true): ?>
<button type="button" class="btn btn-sm btn-gray js-message-delegate-release">위임자 권한 해제</button>
<?php endif; ?>
<?php elseif ($messageDelegate['editable'] === true): ?>
<button type="button" class="btn btn-sm btn-gray js-message-delegate-assign">위임자 권한 설정</button>
<?php else: ?>
<span class="notice-info">미지정</span>
<?php endif; ?>
<br>
<span class="notice-info">문자를 발송할 '발송담당자' 지정은 위임과 다르므로 <a href="/policy/manage_permission.php" target="_blank" class="btn-link">[설정 > 관리 정책 > 운영자 관리]</a> 페이지의 CRM 추가설정 중 'SMS 발송' 권한을 설정해 주세요.</span>
</td>
</tr>
<?php if ($messageDelegate['assigned'] === false && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-set-reason-row" style="display:none;">
<th>SMS 발신 위임자 권한 설정 사유</th>
<td>
<input type="text" id="messageDelegateSetReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 설정하기 위한 명확한 사유를 입력해 주세요.</span>
</td>
</tr>
<?php endif; ?>
<?php if ($messageDelegate['assigned'] === true && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-release-reason-row" style="display:none;">
<th>SMS 발신 위임자 권한 해제 사유</th>
<td>
<input type="text" id="messageDelegateReleaseReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 해제하기 위한 명확한 사유를 입력해 주세요.</span>
</td>
</tr>
<?php endif; ?>
<?php endif; ?>
<tr>
<th>운영자 IP 접속제한</th>
<td>
<script type="text/javascript">
<!--
var isUseProvider = '<?=gd_use_provider()?>';
// 저장 게이트는 서버(ManagerEditPolicy::canEditAccount)와 같은 판정이라 휴대폰 인증 방식 분기와 분리한다
// 통합계정 연동된 최고운영자 화면은 3개 필드만 잠그고 나머지 항목 저장은 열어 둔다 (잠긴 필드는 저장 시 서버가 기존 값을 유지)
var isManagerInfoModifiable = <?= !empty($managerInfoModifiable) ? 'true' : 'false' ?>;
var isSelfCertificationTarget = <?= !empty($selfCertification['isSelf']) ? 'true' : 'false' ?>;
const delegateActionAssign = 'assign';
const delegateActionRelease = 'release';
const isDelegateCallerBlocked = <?= !empty($messageDelegate['callerBlocked']) ? 'true' : 'false' ?>;
const isDelegateBlockedByExisting = <?= !empty($messageDelegate['blockedByExisting']) ? 'true' : 'false' ?>;
const delegateCallerBlockedMessage = 'SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.';
const delegateBlockedByExistingMessage = '상점에는 최대 1명의 위임자만 설정할 수 있습니다.<br>이미 권한을 갖고 있는 운영자가 있어 권한을 설정할 수 없습니다.';
var manage_register = {
regex: {
auth_no: /\d{8}/
$(document).ready(function () {
$('.sms-point').text(manage_register.sms_point);
 
// SMS 발신 위임자 권한 — 버튼 클릭 시 사유 입력행 노출 + 처리 의도(action) 세팅
$('.js-message-delegate-assign').click(function () {
if (isDelegateCallerBlocked) {
dialog_alert(delegateCallerBlockedMessage, '경고');
return;
}
if (isDelegateBlockedByExisting) {
dialog_alert(delegateBlockedByExistingMessage, '안내');
return;
}
$('#messageDelegateAction').val(delegateActionAssign);
$('.js-message-delegate-set-reason-row').show();
$('#messageDelegateSetReason').focus();
});
$('.js-message-delegate-release').click(function () {
$('#messageDelegateAction').val(delegateActionRelease);
$('.js-message-delegate-release-reason-row').show();
$('#messageDelegateReleaseReason').focus();
});
 
$.validator.addMethod('smsAuto', function(value){
if (value == 'n') {
return true;
onkeyup: false,
ignore: ':hidden',
submitHandler: function (form) {
if (!isManagerInfoModifiable) {
// 1차(UI) 차단 — 서버 예외로 흐르면 1회성 CSRF 토큰만 소비되어 재시도 시 "잘못된 경로" 가 된다
alert('해당 계정 정보는 책임자 및 본인만 수정할 수 있습니다.');
return false;
}
if ($('input[name="permissionSetupMode"]').length == 1) { // 운영자 권한 설정 방식 경우
if ($('input[name="permissionFl"]').length == 0) {
alert("메뉴 권한은 최소 1개 이상 '읽기' 또는 '읽기+쓰기'로 설정하셔야 합니다.");
});
}
 
// SMS 발신 위임자 권한 — 설정/해제 사유 필수 검증 + 전송값 세팅
var $mdAction = $('#messageDelegateAction');
if ($mdAction.length && $mdAction.val() !== '') {
if (isDelegateCallerBlocked) {
dialog_alert(delegateCallerBlockedMessage, '경고');
return false;
}
var mdReason = $mdAction.val() === delegateActionRelease
? $.trim($('#messageDelegateReleaseReason').val())
: $.trim($('#messageDelegateSetReason').val());
if (mdReason === '') {
dialog_alert($mdAction.val() === delegateActionRelease
? '발신관리위임자 권한 해제 사유를 작성해주세요.'
: '발신관리위임자 권한 설정 사유를 작성해주세요.', '경고');
return false;
}
// 위임자 사유를 권한 변경 사유(accessControlReason)로 제출 → 운영자 관리/수정 개인정보접속기록에 기록
var $mdReasonField = $('#frmManager input[name="accessControlReason"]');
if ($mdReasonField.length === 0) {
$mdReasonField = $('<input>', {type: 'hidden', name: 'accessControlReason'}).appendTo('#frmManager');
}
$mdReasonField.val(mdReason);
}
 
form.target = 'ifrmProcess';
form.submit();
},
});
 
$('.js-request-authno').bind('click', function () {
 
 
 
 
 
var cellPhoneFlag = true;
var $sms_auth_send_box = $('#smsAuthSendBox');
});
});
 
<?php if ($selfCertification['enabled']): ?>
$('.js-request-self-certification').on('click', function () {
// 본인인증은 등록된 계정 정보의 본인확인 수단이라 본인 계정에서만 진행한다
if (!isSelfCertificationTarget) {
alert('본인 계정의 정보만 수정할 수 있습니다.');
return;
}
if ($.trim($('input[name="cellPhone"]').val()) === '') {
alert('책임자에게 휴대폰 번호 등록을 먼저 요청해 주세요.');
return;
}
managerCertification.certify({
onComplete: function (success, reason) {
if (success) {
$('.js-self-certification-state').removeClass('text-red').text('(인증완료)');
alert('본인인증이 완료되었습니다.');
return;
}
if (reason === 'mismatch') {
dialog_alert('입력하신 정보가 등록된 계정 정보와 일치하지 않습니다.<br>다시 확인해 주세요.', '경고');
return;
}
if (reason === 'duplicateDi') {
dialog_alert('이미 등록된 SMS 관리자입니다.<br>SMS 관리자 권한은 한 사람이 중복으로 보유할 수 없습니다.', '경고');
return;
}
alert('본인인증에 실패했습니다. 인증 정보를 확인 후 다시 시도해 주세요.');
}
});
});
<?php endif; ?>
 
$('.js-request-authemail').bind('click', function () {
var emailFlag = true;
var $email_auth_send_box = $('#emailAuthSendBox');
$('input[name*=\'cellPhone\']').number_only();
 
if ($('input[name="permissionSetupMode"]').length == 1) { // 운영자 권한 설정 방식 경우
$('input[name="scmFl"]').on('click', function () {
if ($(this).val() === 'n') {
scmFlConfirmed = 'n'; // 본사 직접 선택은 즉시 확정
}
toggleMessageDelegateAreaByScm();
});
toggleMessageDelegateAreaByScm(); // 초기 노출 상태
$('input[name=scmFl][value=n]').bind('click', function () {
if ($('input[name="scmNo"]').length > 0) {
$('.btn-icon-delete').trigger('click');
}
}
 
var scmFlConfirmed = $('input[name="scmFl"]:checked').val() || 'n'; // 확정된 공급사 구분 — 모달에서 공급사를 실제 선택(확인)해야만 'y'
 
// 공급사 구분에 따라 발신관리위임자 권한 설정 영역 노출/숨김 — 공급사 운영자에겐 미노출(서버 assertAssignableTarget 와 이중 방어)
function toggleMessageDelegateAreaByScm() {
var isHeadOffice = $('input[name="scmFl"]:checked').val() !== 'y';
$('.js-message-delegate-area').toggle(isHeadOffice);
if (isHeadOffice === false) {
$('#messageDelegateAction').val('');
$('.js-message-delegate-set-reason-row, .js-message-delegate-release-reason-row').hide();
}
}
 
function layer_register(typeStr, mode) {
var addParam = {
"mode": mode,
 
if (typeStr == 'scm') {
addParam['callFunc'] = 'setScmSelect';
// 라디오는 모달에서 공급사를 실제 선택(확인)해야 공급사로 변경 — 열기 시점엔 확정값 유지(미선택 닫기 시 자연히 본사 유지)
$('input[name="scmFl"][value="' + scmFlConfirmed + '"]').prop('checked', true);
toggleMessageDelegateAreaByScm();
$('select[name="add_must_info_sel"]').html("<option>= 상품 필수 정보 선택 =</option>");
$('select[name="add_goods_info_sel"]').html("<option>= 추가 상품 그룹 정보 선택 =</option>");
}
}
 
function setScmSelect(data) {
// 모달에서 공급사 확정 선택 시에만 라디오를 공급사로 변경
$('input[name="scmFl"][value="y"]').prop('checked', true);
scmFlConfirmed = 'y';
//공급사 값 세팅
displayTemplate(data);
toggleMessageDelegateAreaByScm();
if ($('input[name="permissionSetupMode"]').length != 1) { // 운영자 권한 설정 방식이 아닌 경우(기존)
developmentPermissionToggle(data);
changePermissionLayout(data);
 
Admin/policy/manage_security.php-7
변경 전manage_security.php
<p class="notice-info">
로그인한 운영자 정보에 인증정보(SMS, 이메일)가 없는 경우, 보안인증화면이 노출되지 않습니다.<br/>관리자 보안인증 설정을 사용 시, 운영자 정보에 인증정보를 등록해주세요. <a href="../policy/manage_list.php" target="_blank" class="btn-link">운영자 정보 수정하기 ></a>
</p>
<p class="notice-info">
인증번호 SMS는 잔여포인트가 있어야 발송됩니다. (잔여포인트 : <?php echo number_format($smsPoint, 1) ?>) <a href="../crm/message_config.php" target="_blank" class="btn-link">메시지 포인트 충전하기 ></a>
</p>
<p class="notice-info">
잔여 메시지 포인트가 없는 경우 자동으로 이메일이 인증수단으로 노출되며, 포인트 충전 이후 설정한 인증수단으로 노출됩니다.
</p>
<p class="notice-info">
이메일인증은 주문처리 및 프로모션 등으로 인해 이메일이 대량으로 발송될 경우 인증번호 전송이 지연될 수 있으므로 SMS인증을 사용하시길 권장합니다.
</p>
<td>
<input type="hidden" name="excel[smsAuth]" id="excelSmsAuthManager" value="manager" />
운영자 정보에 등록된 SMS로 관리자 보안 인증번호가 발송됩니다.
<p class="notice-info">운영자 인증번호 SMS는 잔여포인트가 있어야 발송됩니다. (잔여포인트 : <?= number_format($smsPoint) ?>) <a href="../crm/message_config.php" target="_blank" class="btn-link">메시지 포인트 충전하기 ></a></p>
<p class="notice-info">로그인한 운영자 정보에 SMS 인증정보(휴대폰번호)가 없는 경우, 엑셀 다운로드가 불가합니다.</p>
</td>
</tr>
 
변경 후manage_security.php
<p class="notice-info">
로그인한 운영자 정보에 인증정보(SMS, 이메일)가 없는 경우, 보안인증화면이 노출되지 않습니다.<br/>관리자 보안인증 설정을 사용 시, 운영자 정보에 인증정보를 등록해주세요. <a href="../policy/manage_list.php" target="_blank" class="btn-link">운영자 정보 수정하기 ></a>
</p>
 
 
 
 
 
 
<p class="notice-info">
이메일인증은 주문처리 및 프로모션 등으로 인해 이메일이 대량으로 발송될 경우 인증번호 전송이 지연될 수 있으므로 SMS인증을 사용하시길 권장합니다.
</p>
<td>
<input type="hidden" name="excel[smsAuth]" id="excelSmsAuthManager" value="manager" />
운영자 정보에 등록된 SMS로 관리자 보안 인증번호가 발송됩니다.
 
<p class="notice-info">로그인한 운영자 정보에 SMS 인증정보(휴대폰번호)가 없는 경우, 엑셀 다운로드가 불가합니다.</p>
</td>
</tr>
 
Admin/promotion/cart_remind_list.php+35-2
변경 전cart_remind_list.php
 
 
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?>
</h3>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText"><span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span></span>
<?php if (gd_is_provider() === false) {?>
<a href="<?= $commerceSmsIntroUrl; ?>" target="_blank" class="btn btn-gray btn-sm">발신번호 등록하기</a>
<?php }?>
</div>
<div class="notice-info">
$(document).ready(function () {
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
show_popup('/crm/popup_manage_call_number.php');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
});
 
$('#frmCartRemind').validate({
 
변경 후cart_remind_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?>
</h3>
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText"><span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span></span>
<?php if (gd_is_provider() === false) {?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 등록하기</button>
<?php }?>
</div>
<div class="notice-info">
$(document).ready(function () {
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
$.post('/crm/message_config/layer_manage_call_number.php', null, function (data) {
// 권한·연동 오류는 JSON(Content-Type 기반 자동 파싱된 객체), 정상은 레이어 HTML 문자열로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
ncds_layer_popup({
message: data,
title: '발신번호 관리'
});
}).fail(function (xhr) {
var response = xhr.responseJSON;
if (response && response.data && response.data.canRegisterRepresentative) {
SmsCallerHubLayer.openRepresentativeRegisterGuide();
return;
}
if (response && response.data && response.data.isMigrationRequired) {
if (response.data.isRepresentative) {
SmsCallerHubLayer.openMigration({
// 이관을 마치면 막혀 있던 발신번호 관리 레이어를 다시 연다
onCompleted: function () {
$('.js-sms-call-number').click();
}
});
return;
}
NCDSAlert({ message: response.message, subMessage: response.data.subMessage, iconType: 'error' });
return;
}
NCDSAlert({ message: (response && response.message) || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
});
 
$('#frmCartRemind').validate({
 
Admin/promotion/comeback_coupon_list.php+35-2
변경 전comeback_coupon_list.php
 
 
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
<div class="btn-group">
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText"><span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span></span>
<?php if (gd_is_provider() === false) {?>
<a href="<?= $commerceSmsIntroUrl; ?>" target="_blank" class="btn btn-gray btn-sm">발신번호 등록하기</a>
<?php }?>
</div>
<div class="notice-info">
$(document).ready(function () {
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
show_popup('/crm/popup_manage_call_number.php');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
});
 
$('#frmCouponList').validate({
 
변경 후comeback_coupon_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
<div class="btn-group">
<input type="hidden" name="smsCallNum" value=""/>
<span class="smsCallNumText"><span class="text-darkred">등록된 SMS 발신번호가 없습니다.</span></span>
<?php if (gd_is_provider() === false) {?>
<button type="button" class="btn btn-white btn-sm js-sms-call-number">발신번호 등록하기</button>
<?php }?>
</div>
<div class="notice-info">
$(document).ready(function () {
// SMS 사전 등록 발신번호 선택하기
$('.js-sms-call-number').click(function (e) {
$.post('/crm/message_config/layer_manage_call_number.php', null, function (data) {
// 권한·연동 오류는 JSON(Content-Type 기반 자동 파싱된 객체), 정상은 레이어 HTML 문자열로 응답된다
if (typeof data === 'object') {
NCDSAlert({ message: data.message, iconType: 'error' });
return;
}
 
ncds_layer_popup({
message: data,
title: '발신번호 관리'
});
}).fail(function (xhr) {
var response = xhr.responseJSON;
if (response && response.data && response.data.canRegisterRepresentative) {
SmsCallerHubLayer.openRepresentativeRegisterGuide();
return;
}
if (response && response.data && response.data.isMigrationRequired) {
if (response.data.isRepresentative) {
SmsCallerHubLayer.openMigration({
// 이관을 마치면 막혀 있던 발신번호 관리 레이어를 다시 연다
onCompleted: function () {
$('.js-sms-call-number').click();
}
});
return;
}
NCDSAlert({ message: response.message, subMessage: response.data.subMessage, iconType: 'error' });
return;
}
NCDSAlert({ message: (response && response.message) || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' });
});
});
 
$('#frmCouponList').validate({
 
Admin/share/layer_message_representative_register_guide.php+149
변경 전layer_message_representative_register_guide.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_message_representative_register_guide.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/manager-certification.js') ?>"></script>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-message-representative-register-guide.css') ?>">
<div class="modal-dialog__content ncua-sms-representative-guide">
<div class="ncua-sms-representative-guide-hero">
<p class="ncua-sms-representative-guide-hero-title"><b>우리 쇼핑몰 번호, 도용되지 않게 지켜드려요</b></p>
<p class="ncua-sms-representative-guide-hero-desc">발신관리책임자와 담당자 정보로 발신번호 사용을 관리해<br/>다른 사람이 SMS를 보내지 못하게 막아줘요.</p>
</div>
<ul class="ncua-sms-representative-guide-list">
<li class="ncua-flex">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>
<span class="ncua-sms-representative-guide-data">
최고운영자만 등록 가능
<span>통합회원과 최고운영자 정보가 동일해야 등록이 가능합니다.</span>
</span>
</li>
<li class="ncua-flex"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>등록은 한 번, 1분이면 완료</li>
<li class="ncua-flex">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>
<span class="ncua-sms-representative-guide-data">
26년 10월 25일까지 등록 필수
<span>미등록시 SMS 관련 기능 이용이 제한됩니다.</span>
</span>
</li>
</ul>
 
<ul class="ncua-sms-representative-guide-caution">
<li>※ <?= !empty($noticeUri) ? '<a href="' . $noticeUri . '" target="_blank">공지사항</a>' : '공지사항' ?>에서 등록 방법, 미등록 시 제한 사항에 대해 자세하게 확인하실 수 있습니다.</li>
<li>※ 급증하는 불법스팸·스미싱 피해 예방을 위한 전기통신사업법 제22조의11 및 전송자격인증 기준 등에 관한 고시(방통위 고시 제2026-1호) 개정에 따른 조치입니다.</li>
</ul>
</div>
<div class="modal-dialog__footer">
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"><input type="checkbox" class="js-hide-representative-guide" /></span>
<span class="ncua-checkbox-field__text">7일동안 보지 않기</span>
</label>
<div>
<button type="button" class="ncua-btn ncua-btn--md ncua-btn--secondary-gray" onclick="layer_close();"><span class="ncua-btn__label">나중에 하기</span></button>
<button type="button" class="ncua-btn ncua-btn--md ncua-btn--primary js-representative-register">등록하기</button>
</div>
</div>
<script>
(function () {
// 안내 레이어 쿠키 규약 — 'false' 저장이 노출 억제를 뜻한다 (layer_legalRequirements.php 와 동일)
var displayCookie = <?= json_encode($displayCookie) ?>;
var expireDay = <?= json_encode($displayCookieExpireDay) ?>;
var isCommerceConnected = <?= json_encode($isCommerceConnected) ?>;
var isSelfCertified = <?= json_encode($isSelfCertified) ?>;
 
$('.js-hide-representative-guide').on('change', function () {
if (this.checked) {
$.cookie(displayCookie, 'false', {expires: expireDay, path: '/'});
return;
}
$.removeCookie(displayCookie, {path: '/'});
});
 
$('.js-representative-register').on('click', function () {
layer_close();
 
// 미연동 최고운영자는 기존 통합회원 연동 프로세스로 보낸다 (복귀 주소는 컨트롤러가 URL 에 담아둔다)
if (!isCommerceConnected) {
NCDSAlert({
message: '발신관리책임자 등록을 위해 통합회원 연동을 진행합니다.',
subMessage: '통합회원 연동을 한 최고운영자만 발신관리책임자 등록이 가능합니다.',
iconType: 'warning',
callback: function () {
location.href = <?= json_encode($commerceLoginUri) ?>;
}
});
return;
}
 
// 본인인증 이력이 없으면 인증부터 진행한다
if (!isSelfCertified) {
managerCertification.certifyWithGuide({onSuccess: checkBlockAndRegister});
return;
}
 
// 이미 본인인증(솔루션 diValue 존재)한 운영자는 곧바로 등록 흐름을 탄다
checkBlockAndRegister();
});
 
// 인증을 마쳐도 차단된 책임자면 등록을 진행시키지 않는다
function checkBlockAndRegister() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkBlock'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.blocked) {
NCDSAlert({message: response.data.message, subMessage: response.data.subMessage, iconType: 'error'});
return;
}
 
checkMigrationCaller();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 통합회원 전환 전에 등록해 둔 기존 발신관리책임자가 있으면 이관 여부를 묻는다
function checkMigrationCaller() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkMigration'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.exists) {
NCDSConfirm({
message: '발신관리책임자 등록이 완료됐습니다.<br>이전에 사용하던 발신번호를 가져오겠습니까?',
subMessage: '가져오기를 완료해야 새로운 발신번호를 등록할 수 있습니다.',
callback: function (confirmed) {
if (!confirmed) {
showRegisterCompleted();
return;
}
 
SmsCallerHubLayer.openMigration({onCompleted: showRegisterCompleted});
}
});
 
return;
}
 
showRegisterCompleted();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 응답이 비어 있거나 파싱에 실패하면 백단 문구가 없으므로 기본 문구로 안내한다
function showRequestFailed(response) {
NCDSAlert({message: (response && response.message) || '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', iconType: 'error'});
}
 
// 이관하지 않고 끝나는 경로는 모두 등록 완료를 알린다
function showRegisterCompleted() {
NCDSAlert({
message: '발신관리책임자 등록이 완료되었습니다.',
subMessage: "부운영자와 함께 SMS 업무를 하려면<br>'발신관리위임자'로 설정하거나 'SMS 발송'권한을 부여해 주세요.",
iconType: 'success'
});
}
})();
</script>
 
Admin/share/layer_sms_personal_info_access_log.php+36
변경 전layer_sms_personal_info_access_log.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_sms_personal_info_access_log.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
?>
<style>
.sms-access-log-layer{overflow-y:auto;max-width:600px;max-height:400px}
.sms-access-log-detail{margin:0;padding:0;list-style:none}
.sms-access-log-detail > li{padding:10px 0}
.sms-access-log-detail > li + li{border-top:1px solid #ddd}
.sms-access-log-detail__items{margin:5px 0 0;padding-left:14px;list-style:none}
.sms-access-log-detail__items > li{padding:2px 0}
</style>
<div class="sms-access-log-layer">
<?php if (!empty($errorMessage)): ?>
<p><?= gd_htmlspecialchars($errorMessage) ?></p>
<?php elseif (!empty($detailRows)): ?>
<ul class="sms-access-log-detail">
<?php foreach ($detailRows as $row): ?>
<li>
* <?= gd_htmlspecialchars($row['label']) ?><?php if (!empty($row['value'])): ?> : <?= gd_htmlspecialchars($row['value']) ?><?php endif; ?>
<?php if (!empty($row['items'])): ?>
<ul class="sms-access-log-detail__items">
<?php foreach ($row['items'] as $item): ?>
<li>&bull; <?= gd_htmlspecialchars($item['label']) ?> : <?= gd_htmlspecialchars($item['value']) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
 
Admin/share/manager_certification_result.php+11
변경 전manager_certification_result.php
 
 
 
 
 
 
 
 
 
 
 
 
변경 후manager_certification_result.php
<script type="text/javascript">
(function () {
var isCertificated = <?= $isCertificated ? 'true' : 'false' ?>;
var certKey = <?= json_encode($certKey) ?>;
 
if (window.opener && window.opener.managerCertification) {
window.opener.managerCertification.result(isCertificated, certKey);
}
window.close();
})();
</script>
 
Component/Admin/AdminLogDAO.php+21-1
변경 전AdminLogDAO.php
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Component\Member\MemberMasking;
 
 
/**
* Class AdminLogDAO
case 'register':
case 'modify':
$result['searchConditionMsg'] = '권한 변경 사유 : '. $data['data']['POST']['accessControlReason'] . '<br>';
 
 
 
 
 
 
 
 
$result['searchConditionMsg'] .= '처리 대상 : '. $data['data']['POST']['managerId'];
break;
case 'delete':
}
$data['searchData'] = gd_implode(', ', $managerId);
}
 
 
 
 
 
 
 
 
 
 
// 암호화 적용 필드
$encryptTarget = [
'email',
$arrBind = [];
 
if ($request['managerId']) {
$where[] = "a.managerId LIKE concat('%',?,'%') ";
 
$this->_db->bind_param_push($arrBind, 's', $request['managerId']);
}
 
 
변경 후AdminLogDAO.php
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Component\Member\MemberMasking;
use Origin\Service\Member\ManagerEditPolicy;
 
/**
* Class AdminLogDAO
case 'register':
case 'modify':
$result['searchConditionMsg'] = '권한 변경 사유 : '. $data['data']['POST']['accessControlReason'] . '<br>';
// manage_ps 스냅샷의 휴대폰번호는 적재 시 암호화돼 있어 그대로 출력하면 암호문이 노출된다
// 적재값은 검증 전 요청 원문이고 상세 화면이 이 문자열을 DOM 으로 파싱하므로 출력 시 이스케이프한다
$changedCellPhone = '';
if (!empty($data['data']['isCellPhoneChanged'])) {
$changedCellPhone = gd_htmlspecialchars((string)\Encryptor::decryptJson($data['data']['POST']['cellPhone']));
}
// 항목을 통째로 빼면 기록을 읽는 쪽이 번호 미변경과 기록 누락을 구분할 수 없다
$result['searchConditionMsg'] .= '휴대폰번호 : ' . $changedCellPhone . '<br>';
$result['searchConditionMsg'] .= '처리 대상 : '. $data['data']['POST']['managerId'];
break;
case 'delete':
}
$data['searchData'] = gd_implode(', ', $managerId);
}
$cellPhoneChangeMode = $data['POST']['mode'] ?? null;
$inputCellPhone = (string)($data['POST']['cellPhone'] ?? '');
// 뒤따르는 암호화 루프가 입력값을 암호문으로 덮으므로 판정은 원문이 남아 있는 이 시점에 끝내야 한다
if (in_array($cellPhoneChangeMode, ['register', 'modify'], true) && !empty($inputCellPhone)) {
/** @var ManagerEditPolicy $managerEditPolicy */
$managerEditPolicy = \App::getInstance(ManagerEditPolicy::class);
if ($managerEditPolicy->isCellPhoneChangeApplied((int)($data['POST']['sno'] ?? 0), $inputCellPhone)) {
$data['isCellPhoneChanged'] = true;
}
}
// 암호화 적용 필드
$encryptTarget = [
'email',
$arrBind = [];
 
if ($request['managerId']) {
// 개인정보 과다 노출 방지를 위해 부분포함이 아닌 전체일치로만 조회한다
$where[] = "a.managerId = ? ";
$this->_db->bind_param_push($arrBind, 's', $request['managerId']);
}
 
 
Component/Admin/AdminMenu.php+30-3
변경 전AdminMenu.php
'godo02700' => [
'messageExcelDown' => '엑셀다운로드',
'messageMaskingUseFl' => '개인정보조회 제한',
 
],
];
 
$this->db->bind_param_push($this->arrBind, 's', $subMenu);
$this->db->bind_param_push($this->arrBind, 's', $lastMenu);
 
$this->db->strField = 't.adminMenuNo';
$join[] = 'LEFT JOIN ' . DB_ADMIN_MENU . ' as s ON s.adminMenuDepth = 2 AND f.adminMenuNo = s.adminMenuParentNo';
$join[] = 'LEFT JOIN ' . DB_ADMIN_MENU . ' as t ON t.adminMenuDepth = 3 AND s.adminMenuNo = t.adminMenuParentNo';
$this->db->strJoin = implode(' ', $join);
$this->db->strWhere = implode(' AND ', $this->arrWhere);
$query = $this->db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_ADMIN_MENU . ' as f ' . gd_implode(' ', $query);
$menuData = $this->db->query_fetch($strSQL, $this->arrBind, false);
unset($this->arrBind);
unset($this->arrWhere);
 
return $menuData['adminMenuNo'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
/**
 
변경 후AdminMenu.php
'godo02700' => [
'messageExcelDown' => '엑셀다운로드',
'messageMaskingUseFl' => '개인정보조회 제한',
'messageSendSms' => 'SMS 발송',
],
];
 
$this->db->bind_param_push($this->arrBind, 's', $subMenu);
$this->db->bind_param_push($this->arrBind, 's', $lastMenu);
 
$this->db->strField = 't.adminMenuNo, t.adminMenuType';
$join[] = 'LEFT JOIN ' . DB_ADMIN_MENU . ' as s ON s.adminMenuDepth = 2 AND f.adminMenuNo = s.adminMenuParentNo';
$join[] = 'LEFT JOIN ' . DB_ADMIN_MENU . ' as t ON t.adminMenuDepth = 3 AND s.adminMenuNo = t.adminMenuParentNo';
$this->db->strJoin = implode(' ', $join);
$this->db->strWhere = implode(' AND ', $this->arrWhere);
$query = $this->db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_ADMIN_MENU . ' as f ' . gd_implode(' ', $query);
$menuList = $this->db->query_fetch($strSQL, $this->arrBind);
unset($this->arrBind);
unset($this->arrWhere);
 
$adminMenuType = gd_is_provider() && self::isProviderDirectory() ? 's' : 'd';
 
return $this->pickAdminMenuNoByAdminMenuType($menuList, $adminMenuType);
}
 
/**
* 본사(d)·공급사(s) 에 같은 3단 코드가 존재하는 메뉴는 조회 결과가 여러 행이 된다.
* 한쪽 유형만 등록된 메뉴가 0행으로 사라지지 않게 유형 조건은 SQL 에 넣지 않고 여기서 고른다.
*
* @param array $menuList [['adminMenuNo' => string, 'adminMenuType' => string], ...]
* @param string $adminMenuType 본사(d)/공급사(s)
* @return string
*/
protected function pickAdminMenuNoByAdminMenuType(array $menuList, string $adminMenuType): string
{
// 메뉴 행이 없으면 빈 menuCode 로 진행해 TypeError 500 대신 admin API 401 안내 경로를 타게 한다
if (empty($menuList)) {
return '';
}
 
foreach ($menuList as $menuData) {
if ($menuData['adminMenuType'] === $adminMenuType) {
return $menuData['adminMenuNo'];
}
}
 
return $menuList[0]['adminMenuNo'];
}
 
/**
 
Component/CartRemind/CartRemind.php+21
변경 전CartRemind.php
use Framework\Utility\ArrayUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
 
use Session;
 
class CartRemind
// 전송실패 수
$result['fail'] = 0;
 
 
 
 
 
 
 
 
 
 
foreach ($cartRemindSendData['member'] as $memKey => $memVal) {
// 장바구니 알림에 쿠폰 지급이 있다면 지급
if ($cartRemindSendData['coupon'] > 0) {
return $result;
}
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 장바구니 알림 삭제 (이미 발송 등록된 SMS 는 삭제 되지 않음)
*
 
변경 후CartRemind.php
use Framework\Utility\ArrayUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Origin\Enum\ApiClient\Notification\SmsPersonalInfoAccessLogEventType;
use Session;
 
class CartRemind
// 전송실패 수
$result['fail'] = 0;
 
// 개인정보접속기록 발행은 발송 지점(SmsAdmin::sendSms)에서 한다
// 튜닝 상점의 구 버전 복제본에는 없는 신규 메서드라 존재 확인 후 호출한다
if (method_exists($smsAdmin, 'setPublishPersonalInfoAccessLog') && method_exists($smsAdmin, 'setPersonalInfoAccessLogEventType')) {
$smsAdmin->setPublishPersonalInfoAccessLog(true);
$smsAdmin->setPersonalInfoAccessLogEventType($this->resolveAccessLogEventType($cartRemindSendData['type']));
} else {
\Logger::channel('sms')->warning('튜닝으로 인한 개인정보접속기록 발행 설정 미적용', ['missingMethod' => 'SmsAdmin::setPublishPersonalInfoAccessLog, SmsAdmin::setPersonalInfoAccessLogEventType']);
}
 
foreach ($cartRemindSendData['member'] as $memKey => $memVal) {
// 장바구니 알림에 쿠폰 지급이 있다면 지급
if ($cartRemindSendData['coupon'] > 0) {
return $result;
}
 
/**
* @param string $sendType 'auto' 면 크론 자동 발송, 그 외는 관리자 즉시발송
* @return SmsPersonalInfoAccessLogEventType
*/
protected function resolveAccessLogEventType($sendType)
{
return $sendType === 'auto'
? SmsPersonalInfoAccessLogEventType::SMS_SEND_AUTO
: SmsPersonalInfoAccessLogEventType::SMS_SEND_MANUAL;
}
 
/**
* 장바구니 알림 삭제 (이미 발송 등록된 SMS 는 삭제 되지 않음)
*
 
Component/Coupon/CouponAdmin.php+9
변경 전CouponAdmin.php
use Framework\Utility\ArrayUtils;
use Framework\Utility\ProducerUtils;
use Globals;
 
use Origin\Service\Coupon\CopyCouponService;
use Origin\Service\Coupon\UpdateCouponService;
use Origin\Service\Member\ManagerLoginService;
if ($getData['smsFl'] == 'y') {
$smsSendData['password'] = $request->post()->get('password');
$smsSendData['passwordCheckFl'] = $request->post()->get('passwordCheckFl');
 
 
 
 
 
 
 
 
$cnt = $smsAdmin->sendSms($smsSendData);
$logger->info(sprintf(__('SMS 발송이 완료되었습니다. (%1$s건 성공 / %2$s건 실패)'), $cnt['success'], $cnt['fail']));
}
 
변경 후CouponAdmin.php
use Framework\Utility\ArrayUtils;
use Framework\Utility\ProducerUtils;
use Globals;
use Origin\Enum\ApiClient\Notification\SmsPersonalInfoAccessLogEventType;
use Origin\Service\Coupon\CopyCouponService;
use Origin\Service\Coupon\UpdateCouponService;
use Origin\Service\Member\ManagerLoginService;
if ($getData['smsFl'] == 'y') {
$smsSendData['password'] = $request->post()->get('password');
$smsSendData['passwordCheckFl'] = $request->post()->get('passwordCheckFl');
// 개인정보접속기록 발행은 발송 지점(SmsAdmin::sendSms)에서 한다
// 튜닝 상점의 구 버전 복제본에는 없는 신규 메서드라 존재 확인 후 호출한다
if (method_exists($smsAdmin, 'setPublishPersonalInfoAccessLog') && method_exists($smsAdmin, 'setPersonalInfoAccessLogEventType')) {
$smsAdmin->setPublishPersonalInfoAccessLog(true);
$smsAdmin->setPersonalInfoAccessLogEventType(SmsPersonalInfoAccessLogEventType::SMS_SEND_MANUAL);
} else {
\Logger::channel('sms')->warning('튜닝으로 인한 개인정보접속기록 발행 설정 미적용', ['missingMethod' => 'SmsAdmin::setPublishPersonalInfoAccessLog, SmsAdmin::setPersonalInfoAccessLogEventType']);
}
$cnt = $smsAdmin->sendSms($smsSendData);
$logger->info(sprintf(__('SMS 발송이 완료되었습니다. (%1$s건 성공 / %2$s건 실패)'), $cnt['success'], $cnt['fail']));
}
 
Component/Database/DBTableField.php+2
변경 전DBTableField.php
['val' => 'isOrderSearchMultiGrid', 'typ' => 's', 'def' => 'n'], //주문 멀티검색여부
['val' => 'ipManagerSecurityFl', 'typ' => 's', 'def' => 'n'], //관리자 ip접속제한 사용여부
['val' => 'commerceId', 'typ' => 's', 'def' => null], // 커머스 아이디
 
 
];
// @formatter:on
 
 
변경 후DBTableField.php
['val' => 'isOrderSearchMultiGrid', 'typ' => 's', 'def' => 'n'], //주문 멀티검색여부
['val' => 'ipManagerSecurityFl', 'typ' => 's', 'def' => 'n'], //관리자 ip접속제한 사용여부
['val' => 'commerceId', 'typ' => 's', 'def' => null], // 커머스 아이디
['val' => 'diValue', 'typ' => 's', 'def' => null], // 중복가입확인정보(DI)
['val' => 'messageRoleType', 'typ' => 's', 'def' => null], // 발신관리위임자 역할(위임자: delegate, 미지정: null)
];
// @formatter:on
 
 
Component/Member/ManagerCs.php+2
변경 전ManagerCs.php
}
unset($funcs);
 
 
 
if (!$isSuper) {
unset($result['goodsStockExceptView']); // 상품 상세 재고 수정 제외
unset($result['orderExcelDown']); // 주문/배송 엑셀 다운로드
 
변경 후ManagerCs.php
}
unset($funcs);
 
unset($result['messageSendSms']); // 발송담당자는 일반 운영자만 대상 (CS 계정 제외)
 
if (!$isSuper) {
unset($result['goodsStockExceptView']); // 상품 상세 재고 수정 제외
unset($result['orderExcelDown']); // 주문/배송 엑셀 다운로드
 
Component/Member/Manager.php+113-33
변경 전Manager.php
use Component\Member\Util\MemberUtil;
use Component\Policy\ManageSecurityPolicy;
use Component\Sms\Sms;
use Component\Sms\SmsMessage;
use Component\Sms\SmsSender;
use Component\Storage\Storage;
use Component\Validator\Validator;
use Encryptor;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Admin\Menu\AdminMenuFlagService;
 
 
 
 
use Origin\Service\Member\ManagerLogoutService;
 
 
use Origin\Service\Storage\ManagerDuplicateLoginService;
use Password;
use Request;
$this->search['isSuper'] = $req['isSuper'];
$this->search['noVisitFl'] = gd_isset($req['noVisitFl'], 'n');
$this->search['searchKind'] = gd_isset($req['searchKind']);
 
 
$this->checked['employeeFl'][$this->search['employeeFl']] =
$this->checked['smsAutoReceive'][$this->search['smsAutoReceive']] =
$this->checked['scmFl'][$this->search['scmFl']] = 'checked = "checked"';
 
 
// 키워드 검색
if ($this->search['keyword']) {
$this->checked['smsAutoReceive']['all'] = 'checked';
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 부서 검색
if ($this->search['departmentCd']) {
$this->arrWhere[] = 'departmentCd = ?';
$validator = new Validator();
 
$arrData['memo'] = strip_tags($arrData['memo']);//관리자 메모 XSS방지
 
 
 
if ($checkMode === 'modify') {
$managerData = $this->getManagerInfo($arrData['sno']);
if (empty($arrData['functionAuth']['goodsStockExceptView']) === false) {
$functionAuth['functionAuth']['goodsStockExceptView'] = $arrData['functionAuth']['goodsStockExceptView'];
}
 
 
 
 
 
 
 
 
 
 
}
$arrData['permissionMenu'] = json_encode($permission, JSON_UNESCAPED_UNICODE); // 운영자 접근 권한 설정
$arrData['functionAuth'] = json_encode($functionAuth, JSON_UNESCAPED_UNICODE); // 운영자 기능 권한 설정
$validator->add('permissionMenu', ''); // 운영자 권한 - 일부 권한 일 경우 허용된 메뉴 접근 권한
$validator->add('functionAuth', ''); // 운영자 권한 - 일부 권한 일 경우 허용된 기능 권한
$validator->add('isSmsAuth', 'yn'); // 휴대폰인증여부
 
 
$validator->add('isEmailAuth', 'yn'); // 이메일인증여부
$validator->add('memo', ''); // 메모
$validator->add('ipManagerSecurityFl', 'yn', true); // 운영자 ip 접속제한 설정여부
if (empty($arrData['functionAuth']['boardMaskingUseFl']) === false) {
$functionAuth['functionAuth']['boardMaskingUseFl'] = $arrData['functionAuth']['boardMaskingUseFl'];
}
 
 
 
 
 
 
 
 
 
 
}
$arrData['permissionMenu'] = json_encode($permission, JSON_UNESCAPED_UNICODE); // 운영자 접근 권한 설정
$arrData['writeEnabledMenu'] = json_encode($writeEnabledMenu, JSON_UNESCAPED_UNICODE); // 운영자 쓰기 권한 설정
$managersData[] = $data;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 운영자 권한 저장
foreach ($managersData as $data) {
$arrManager = [];
foreach ($functionList as $lists) {
foreach ($lists as $code => $name) {
if ($code == 'goodsStockExceptView') continue;
 
$data['functionAuth'][$code] = 'y';
}
}
throw new ManagerAuthException(ManagerAuthException::CODE_NOT_EQUALS_CELLPHONE);
}
$arrData['isSmsAuth'] = 'y';
 
 
 
 
 
 
 
 
} elseif ($arrData['isSmsAuth'] === 'y') {
unset($arrData['cellPhone']);
} else {
}
}
 
$strSql = "UPDATE " . DB_MANAGER . " SET isDelete = 'y' WHERE sno = ?";
 
 
$this->db->bind_query(
$strSql,
[
*/
public function sendSmsAuthNumber($managerInfo)
{
$smsPoint = Sms::getPoint();
if ($smsPoint >= 1) {
if (!$managerInfo['cellPhone']) {
throw new \Exception(__('관리자 보안을 위해 휴대폰 인증 로그인을 사용중입니다. 로그인하신 관리 아이디는 인증된 휴대폰번호가 없어 로그인할 수 없으니, 대표운영자에게 문의하시기 바랍니다.'));
}
 
// 인증번호 생성
$adminSecuritySmsAuthNumber = Otp::getOtp(8);
$contents = sprintf(__('관리자님 인증번호는 [%s] 입니다.'), $adminSecuritySmsAuthNumber);
$receiver[]['cellPhone'] = $managerInfo['cellPhone'];
$smsSender = \App::load(SmsSender::class);
$smsSender->validPassword(\App::load(\Component\Sms\SmsUtil::class)->getPassword());
$smsSender->setSmsPoint($smsPoint);
$smsSender->setMessage(new SmsMessage($contents));
$smsSender->setSmsType('user');
$smsSender->setMsgType('auth'); //인증용
$smsSender->setReceiver($receiver);
$smsSender->setLogData(['disableResend' => true]);
$smsSender->setContentsMask([$adminSecuritySmsAuthNumber]);
$smsResult = $smsSender->send();
$smsAuth['smsAuthNumber'] = $adminSecuritySmsAuthNumber;
if ($smsResult['success'] === 1) {
$smsAuth['message'] = 'OK';
} else {
$smsAuth['message'] = 'SMS Send Fail';
}
} else {
$smsAuth['message'] = 'SMS Point Fail';
}
 
 
 
 
 
 
 
 
return $smsAuth;
}
 
 
변경 후Manager.php
use Component\Member\Util\MemberUtil;
use Component\Policy\ManageSecurityPolicy;
use Component\Sms\Sms;
 
 
use Component\Storage\Storage;
use Component\Validator\Validator;
use Encryptor;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Admin\Menu\AdminMenuFlagService;
use Origin\Enum\Crm\Message\MessageOperatorRoleType;
use Origin\Service\Crm\Message\MessageOperatorRoleGuard;
use Origin\Service\Crm\Message\MessageOperatorRoleService;
use Origin\Service\Member\ManagerEditPolicy;
use Origin\Service\Member\ManagerLogoutService;
use Origin\Service\Member\ManagerService;
use Origin\Service\Sms\OccupancyAuth\OccupancyOtpSmsService;
use Origin\Service\Storage\ManagerDuplicateLoginService;
use Password;
use Request;
$this->search['isSuper'] = $req['isSuper'];
$this->search['noVisitFl'] = gd_isset($req['noVisitFl'], 'n');
$this->search['searchKind'] = gd_isset($req['searchKind']);
$this->search['smsRole'] = gd_isset($req['smsRole'], 'all');
 
$this->checked['employeeFl'][$this->search['employeeFl']] = 'checked = "checked"';
$this->checked['smsAutoReceive'][$this->search['smsAutoReceive']] = 'checked = "checked"';
$this->checked['scmFl'][$this->search['scmFl']] = 'checked = "checked"';
$this->checked['smsRole'][$this->search['smsRole']] = 'checked="checked"';
 
// 키워드 검색
if ($this->search['keyword']) {
$this->checked['smsAutoReceive']['all'] = 'checked';
}
 
// SMS 권한 검색 (발신관리책임자=등록 완료 최고운영자 sno=1 / 발신관리위임자=messageRole / 발송담당자=functionAuth.messageSendSms 보유)
switch ($this->search['smsRole']) {
case MessageOperatorRoleType::REPRESENTATIVE->value:
// 통합회원 연동(commerceId)·본인인증(diValue) 미완료 최고운영자는 역할이 없어 검색 결과에서 빠진다
$this->arrWhere[] = "m.sno = ? AND IFNULL(m.commerceId, '') != '' AND IFNULL(m.diValue, '') != ''";
$this->db->bind_param_push($this->arrBind, 'i', ManagerService::SUPER_MANAGER_SNO);
break;
case MessageOperatorRoleType::DELEGATE->value:
$this->arrWhere[] = 'm.messageRoleType = ?';
$this->db->bind_param_push($this->arrBind, 's', MessageOperatorRoleType::DELEGATE->value);
break;
case MessageOperatorRoleType::SENDER->value:
// functionAuth JSON 저장 시 공백 유무가 경로마다 달라 JSON 경로로 판정
$this->arrWhere[] = "JSON_VALID(m.functionAuth) AND JSON_UNQUOTE(JSON_EXTRACT(m.functionAuth, '$.functionAuth.messageSendSms')) = ? AND m.sno != ? AND IFNULL(m.messageRoleType, '') != ?";
$this->db->bind_param_push($this->arrBind, 's', 'y');
$this->db->bind_param_push($this->arrBind, 'i', ManagerService::SUPER_MANAGER_SNO);
$this->db->bind_param_push($this->arrBind, 's', MessageOperatorRoleType::DELEGATE->value);
break;
}
 
// 부서 검색
if ($this->search['departmentCd']) {
$this->arrWhere[] = 'departmentCd = ?';
$validator = new Validator();
 
$arrData['memo'] = strip_tags($arrData['memo']);//관리자 메모 XSS방지
// 본인인증 결과(DI)는 인증 경로만 기록·파기한다 — 요청값이 그대로 실려 저장되지 않도록 여기서 걷어낸다
unset($arrData['diValue']);
 
if ($checkMode === 'modify') {
$managerData = $this->getManagerInfo($arrData['sno']);
if (empty($arrData['functionAuth']['goodsStockExceptView']) === false) {
$functionAuth['functionAuth']['goodsStockExceptView'] = $arrData['functionAuth']['goodsStockExceptView'];
}
// 발송담당자-SMS 는 전체권한과 무관한 개별 지정 항목이라 보존
if (!empty($arrData['functionAuth']['messageSendSms'])) {
$functionAuth['functionAuth']['messageSendSms'] = $arrData['functionAuth']['messageSendSms'];
}
}
// 최고운영자·위임자는 발송담당자-SMS 권한이 강제(체크박스 disabled 라 폼 미전송)되므로 저장 시 보존
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
if (!empty($arrData['sno']) && $messageOperatorRoleService->isSenderPermissionLocked((int)$arrData['sno'])) {
$functionAuth['functionAuth']['messageSendSms'] = 'y';
}
$arrData['permissionMenu'] = json_encode($permission, JSON_UNESCAPED_UNICODE); // 운영자 접근 권한 설정
$arrData['functionAuth'] = json_encode($functionAuth, JSON_UNESCAPED_UNICODE); // 운영자 기능 권한 설정
$validator->add('permissionMenu', ''); // 운영자 권한 - 일부 권한 일 경우 허용된 메뉴 접근 권한
$validator->add('functionAuth', ''); // 운영자 권한 - 일부 권한 일 경우 허용된 기능 권한
$validator->add('isSmsAuth', 'yn'); // 휴대폰인증여부
// 미등록 항목은 act() 의 garbage delete 로 사라져 파기가 저장에 반영되지 않는다
$validator->add('diValue', ''); // 중복가입확인정보(DI)
$validator->add('isEmailAuth', 'yn'); // 이메일인증여부
$validator->add('memo', ''); // 메모
$validator->add('ipManagerSecurityFl', 'yn', true); // 운영자 ip 접속제한 설정여부
if (empty($arrData['functionAuth']['boardMaskingUseFl']) === false) {
$functionAuth['functionAuth']['boardMaskingUseFl'] = $arrData['functionAuth']['boardMaskingUseFl'];
}
// 발송담당자-SMS 는 전체권한과 무관한 개별 지정 항목이라 보존
if (!empty($arrData['functionAuth']['messageSendSms'])) {
$functionAuth['functionAuth']['messageSendSms'] = $arrData['functionAuth']['messageSendSms'];
}
}
// 최고운영자·위임자는 발송담당자-SMS 권한이 강제(체크박스 disabled 라 폼 미전송)되므로 저장 시 보존
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
if (!empty($arrData['sno']) && $messageOperatorRoleService->isSenderPermissionLocked((int)$arrData['sno'])) {
$functionAuth['functionAuth']['messageSendSms'] = 'y';
}
$arrData['permissionMenu'] = json_encode($permission, JSON_UNESCAPED_UNICODE); // 운영자 접근 권한 설정
$arrData['writeEnabledMenu'] = json_encode($writeEnabledMenu, JSON_UNESCAPED_UNICODE); // 운영자 쓰기 권한 설정
$managersData[] = $data;
}
 
// SMS 발송담당자(messageSendSms) 권한 변경 검증 — 벌크 대상 합산 (클라이언트 submitHandler 와 이중 방어)
$smsRoleService = \App::getInstance(MessageOperatorRoleService::class);
$smsRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
$smsGranted = is_array($arrData['functionAuth']) && (($arrData['functionAuth'][MessageOperatorRoleService::SEND_SMS_FUNCTION_AUTH] ?? '') === 'y');
$smsChanged = false;
$smsNewSenderCount = 0;
foreach ($managersData as $smsTarget) {
// 최고운영자(sno=1)/위임자는 messageSendSms 강제(잠금)라 변경 대상 아님
if ($smsRoleService->isSenderPermissionLocked((int)$smsTarget['sno'])) {
continue;
}
$smsNowGranted = $smsRoleService->hasSenderPermission((int)$smsTarget['sno']);
// CS 계정은 발송담당자로 지정할 수 없음 (신규 부여 차단)
if ($smsGranted && $smsNowGranted === false && ($smsTarget['isSuper'] ?? 'n') === 'cs') {
throw new Exception(__('CS 계정은 발송담당자로 지정할 수 없습니다.'));
}
if ($smsNowGranted !== $smsGranted) {
$smsChanged = true;
}
if ($smsGranted && $smsNowGranted === false) {
$smsNewSenderCount++; // 신규로 발송담당자가 되는 대상
}
}
if ($smsChanged) {
if ($smsRoleGuard->isDelegateOrHigher() === false) {
throw new Exception(__('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.'));
}
if ($smsRoleGuard->isRepresentative() && $smsRoleService->isBlockedRepresentative()) {
throw new Exception(__('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.'));
}
if ($smsGranted && ($smsRoleService->countSenders() + $smsNewSenderCount) > MessageOperatorRoleService::MAX_SENDER_COUNT) {
throw new Exception(__('발송담당자는 최대 %d명까지 지정 가능합니다.', MessageOperatorRoleService::MAX_SENDER_COUNT));
}
if (trim((string)($arrData['accessControlReason'] ?? '')) === '') {
throw new Exception(__('발송담당자 권한 변경 사유를 작성해주세요.'));
}
}
 
// 운영자 권한 저장
foreach ($managersData as $data) {
$arrManager = [];
foreach ($functionList as $lists) {
foreach ($lists as $code => $name) {
if ($code == 'goodsStockExceptView') continue;
if ($code == MessageOperatorRoleService::SEND_SMS_FUNCTION_AUTH) continue; // SMS 발송담당자는 통제 권한이라 신규 기본 부여 제외
$data['functionAuth'][$code] = 'y';
}
}
throw new ManagerAuthException(ManagerAuthException::CODE_NOT_EQUALS_CELLPHONE);
}
$arrData['isSmsAuth'] = 'y';
/** @var ManagerEditPolicy $managerEditPolicy */
$managerEditPolicy = \App::getInstance(ManagerEditPolicy::class);
$targetSno = (int)($arrData['sno'] ?? 0);
$cellPhone = (string)($arrData['cellPhone'] ?? '');
if ($managerEditPolicy->isCertifiedCellPhoneReplaced($targetSno, $cellPhone)) {
// 점유인증으로 번호를 교체하면 기존 DI 는 이전 번호로 받은 인증 결과라 남겨 둘 수 없다
$arrData['diValue'] = null;
}
} elseif ($arrData['isSmsAuth'] === 'y') {
unset($arrData['cellPhone']);
} else {
}
}
 
// 삭제된 계정에 본인확인 식별자를 남겨 두지 않는다 — 점유인증 여부(isSmsAuth)는 번호 보유 사실이라 유지한다
// NULL 은 값이 아니라 키워드라 바인드로 표현할 수 없다 — bind_param 이 값마다 escape 를 걸어 빈 문자열이 된다
$strSql = "UPDATE " . DB_MANAGER . " SET isDelete = 'y', diValue = NULL WHERE sno = ?";
$this->db->bind_query(
$strSql,
[
*/
public function sendSmsAuthNumber($managerInfo)
{
if (!$managerInfo['cellPhone']) {
throw new \Exception(__('관리자 보안을 위해 휴대폰 인증 로그인을 사용중입니다. 로그인하신 관리 아이디는 인증된 휴대폰번호가 없어 로그인할 수 없으니, 대표운영자에게 문의하시기 바랍니다.'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
$adminSecuritySmsAuthNumber = Otp::getOtp(8);
$contents = sprintf(__('관리자님 인증번호는 [%s] 입니다.'), $adminSecuritySmsAuthNumber);
$isSent = \App::getInstance(OccupancyOtpSmsService::class)->send($managerInfo['cellPhone'], $contents, $adminSecuritySmsAuthNumber);
 
$smsAuth['smsAuthNumber'] = $adminSecuritySmsAuthNumber;
$smsAuth['message'] = $isSent ? 'OK' : 'SMS Send Fail';
 
return $smsAuth;
}
 
 
Component/Sms/SmsAdmin.php+48-8
변경 전SmsAdmin.php
use Framework\Utility\GodoUtils;
use Framework\Utility\NumberUtils;
use Framework\Utility\StringUtils;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateListSearchDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateResponseDTO;
use Origin\Enum\ManagerTutorialType;
use Origin\Service\Admin\Member\ManagerTutorial\ManagerTutorialTopicService;
 
use Respect\Validation\Validator;
use Origin\Service\Member\CrmGroupService;
use Origin\Enum\Crm\Message\SmsTemplateCategory;
 
/**
* SMS 관리자 관련 처리
class SmsAdmin
{
/** @var \Framework\Database\DBTool $db */
private $db;
/** @var SmsAutoCode $smsAutoCode */
protected $smsAutoCode;
protected $recall;
protected $fieldTypes;
 
 
 
 
 
 
public function __construct($config = [])
{
}
}
 
// SMS 기본 정보 저장
$config['smsCallNum'] = $request['smsCallNum'];
$this->setConfig($config);
 
// 기 저장된 항목 삭제
unset($request['mode'], $request['smsCallNum']);
 
return $setData;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 관리자 SMS 개별/전체 발송
*
 
// 예약 일자 (발송시간)
$smsSendDate = date('Y-m-d H:i:s', $smsSendReserveDate);
 
 
// 발송 타입
$tranType = 'res_send';
} else {
// 발송시간
$smsSendDate = date('Y-m-d H:i:s', time());
 
 
// 발송 타입
$tranType = 'send';
 
// SMS 발송
$cnt = $transport->send();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// excel전송일경우 DB_SMS_EXCEL_LOG 테이블정보 삭제
if ((int) $cnt['success'] > 0 && $receiverData['receiverType'] === 'excel') {
 
변경 후SmsAdmin.php
use Framework\Utility\GodoUtils;
use Framework\Utility\NumberUtils;
use Framework\Utility\StringUtils;
use Origin\Enum\ApiClient\Notification\SmsPersonalInfoAccessLogEventType;
 
use Origin\Enum\ManagerTutorialType;
use Origin\Service\Admin\Member\ManagerTutorial\ManagerTutorialTopicService;
use Origin\Service\Crm\Message\SmsPersonalInfoAccessLogService;
use Respect\Validation\Validator;
use Origin\Service\Member\CrmGroupService;
 
 
/**
* SMS 관리자 관련 처리
class SmsAdmin
{
/** @var \Framework\Database\DBTool $db */
protected $db;
/** @var SmsAutoCode $smsAutoCode */
protected $smsAutoCode;
protected $recall;
protected $fieldTypes;
// sendSms() 는 CartRemind·CouponAdmin 등 발행 대상이 아닌 경로에서도 호출되므로 수동발송 경로만 opt-in 으로 발행한다
protected $publishPersonalInfoAccessLog = false;
protected $personalInfoAccessLogEventType = SmsPersonalInfoAccessLogEventType::SMS_SEND_MANUAL;
// CLI(스케줄러) 세션은 SessionManager 가 아니라 발행 서비스 생성이 매번 실패하고 수신대상 루프 안에서 호출돼, 첫 실패 후에는 같은 프로세스에서 재시도·재로그하지 않는다
protected static $personalInfoAccessLogPublishFailed = false;
 
public function __construct($config = [])
{
}
}
 
 
 
 
 
// 기 저장된 항목 삭제
unset($request['mode'], $request['smsCallNum']);
 
return $setData;
}
 
/**
* @param bool $publishPersonalInfoAccessLog 수동 발송 개인정보접속기록 발행 여부
*/
public function setPublishPersonalInfoAccessLog(bool $publishPersonalInfoAccessLog)
{
$this->publishPersonalInfoAccessLog = $publishPersonalInfoAccessLog;
}
 
/**
* @param SmsPersonalInfoAccessLogEventType $personalInfoAccessLogEventType 발행할 개인정보접속기록 이벤트 유형
*/
public function setPersonalInfoAccessLogEventType(SmsPersonalInfoAccessLogEventType $personalInfoAccessLogEventType)
{
$this->personalInfoAccessLogEventType = $personalInfoAccessLogEventType;
}
 
/**
* 관리자 SMS 개별/전체 발송
*
 
// 예약 일자 (발송시간)
$smsSendDate = date('Y-m-d H:i:s', $smsSendReserveDate);
$reservedAt = $smsSendDate;
 
// 발송 타입
$tranType = 'res_send';
} else {
// 발송시간
$smsSendDate = date('Y-m-d H:i:s', time());
$reservedAt = null;
 
// 발송 타입
$tranType = 'send';
 
// SMS 발송
$cnt = $transport->send();
if ($this->publishPersonalInfoAccessLog && !self::$personalInfoAccessLogPublishFailed) {
// CLI(스케줄러) 세션은 SessionManager 가 아니라 서비스 생성부터 실패할 수 있어, 이미 끝난 발송 흐름을 끊지 않도록 잡는다
try {
/** @var SmsPersonalInfoAccessLogService $publishService */
$publishService = \App::getInstance(SmsPersonalInfoAccessLogService::class);
// 예약발송도 등록 API 호출이 이 요청 안에서 일어나므로 발행 payload 의 accessIp 는 예약 생성 시점 관리자 IP다
$publishService->publishSmsSendAccessLog(
$this->personalInfoAccessLogEventType,
array_column($receivers, 'cellPhone'),
$countReceivers,
$senderPhoneNumber,
$cnt,
$reservedAt
);
} catch (\Throwable $e) {
self::$personalInfoAccessLogPublishFailed = true;
\Logger::channel(SmsPersonalInfoAccessLogService::PRIVACY_LOG_CHANNEL)->error('[SMS 발송] 개인정보접속기록 발행 실패', [
'exception' => $e,
'eventType' => $this->personalInfoAccessLogEventType->name,
]);
}
}
 
// excel전송일경우 DB_SMS_EXCEL_LOG 테이블정보 삭제
if ((int) $cnt['success'] > 0 && $receiverData['receiverType'] === 'excel') {
 
Component/Sms/SmsAuto.php+42-1
변경 전SmsAuto.php
use Framework\Utility\ComponentUtils;
use Framework\Utility\StringUtils;
use Origin\DTO\AutoSend\AutoSendOptionCodeConfigDto;
 
use Origin\Enum\AutoSend\AutoSendChannel;
use Origin\Enum\AutoSend\AutoSendRecipientPolicy;
use Origin\Enum\AutoSend\AutoSendKakaoVendorSupport;
use Origin\Service\AutoSend\AutoSendMyappService;
use Origin\Service\AutoSend\AutoSendTemplateVariableReplacer;
use Origin\Service\AutoSend\FindAutoSendConfigService;
 
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSendService;
use Origin\Util\EmojiRemover;
use SplObserver;
const MYAPP = 'myapp';
const SMS = 'sms';
 
 
 
 
/** @var \Framework\Database\DBTool $db */
protected $db;
/** @var \Bundle\Component\Sms\Sms $sms */
return $this->smsAutoCodeType !== Code::COUPON_MANUAL;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 루나 알림톡은 기존 치환자 처리 방식 유지
*
if ($this->smsType == SmsAutoCode::MEMBER && $this->smsAutoCodeType == Code::PASS_AUTH) {
$transport->setMsgType('auth');
}
return $transport->send();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
/**
 
변경 후SmsAuto.php
use Framework\Utility\ComponentUtils;
use Framework\Utility\StringUtils;
use Origin\DTO\AutoSend\AutoSendOptionCodeConfigDto;
use Origin\Enum\ApiClient\Notification\SmsPersonalInfoAccessLogEventType;
use Origin\Enum\AutoSend\AutoSendChannel;
use Origin\Enum\AutoSend\AutoSendRecipientPolicy;
use Origin\Enum\AutoSend\AutoSendKakaoVendorSupport;
use Origin\Service\AutoSend\AutoSendMyappService;
use Origin\Service\AutoSend\AutoSendTemplateVariableReplacer;
use Origin\Service\AutoSend\FindAutoSendConfigService;
use Origin\Service\Crm\Message\SmsPersonalInfoAccessLogService;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSendService;
use Origin\Util\EmojiRemover;
use SplObserver;
const MYAPP = 'myapp';
const SMS = 'sms';
 
// CLI(스케줄러) 세션은 SessionManager 가 아니라 발행 서비스 생성이 매번 실패하고 수신대상 루프 안에서 호출돼, 첫 실패 후에는 같은 프로세스에서 재시도·재로그하지 않는다
protected static $personalInfoAccessLogPublishFailed = false;
 
/** @var \Framework\Database\DBTool $db */
protected $db;
/** @var \Bundle\Component\Sms\Sms $sms */
return $this->smsAutoCodeType !== Code::COUPON_MANUAL;
}
 
/**
* 수동 쿠폰 발급 안내만 관리자 조작 발송이라 수동 기록으로 남는다
*
* @return SmsPersonalInfoAccessLogEventType
*/
protected function resolveAccessLogEventType()
{
return $this->isAutoSend()
? SmsPersonalInfoAccessLogEventType::SMS_SEND_AUTO
: SmsPersonalInfoAccessLogEventType::SMS_SEND_MANUAL;
}
 
/**
* 루나 알림톡은 기존 치환자 처리 방식 유지
*
if ($this->smsType == SmsAutoCode::MEMBER && $this->smsAutoCodeType == Code::PASS_AUTH) {
$transport->setMsgType('auth');
}
$sendResult = $transport->send();
$smsConfig = gd_policy('sms.config');
// CLI(스케줄러) 세션은 SessionManager 가 아니라 서비스 생성부터 실패할 수 있어, 이미 끝난 발송 흐름을 끊지 않도록 잡는다
if (!self::$personalInfoAccessLogPublishFailed) {
$eventType = $this->resolveAccessLogEventType();
try {
/** @var SmsPersonalInfoAccessLogService $publishService */
$publishService = \App::getInstance(SmsPersonalInfoAccessLogService::class);
$publishService->publishSmsSendAccessLog(
$eventType,
array_column($receiverData, 'cellPhone'),
count($receiverData),
$smsConfig['smsCallNum'] ?? '',
$sendResult
);
} catch (\Throwable $e) {
self::$personalInfoAccessLogPublishFailed = true;
\Logger::channel(SmsPersonalInfoAccessLogService::PRIVACY_LOG_CHANNEL)->error('[SMS 자동발송] 개인정보접속기록 발행 실패', [
'exception' => $e,
'eventType' => $eventType->name,
]);
}
}
 
return $sendResult;
}
 
/**
 
Component/Sms/SmsSender.php+30-9
변경 전SmsSender.php
protected $logData;
/** @var int sms 포인트 */
protected $smsPoint;
 
 
/** @var int sms 전송내역 일련번호 */
protected $smsLogSno;
/** @var array sms api 호출 시 사용될 정보 */
*/
protected function validatePoint()
{
 
 
 
if ($this->smsPoint < $this->getSendPoint() * $this->message->count()) {
$logger = \App::getInstance('logger');
$logger->info(sprintf('current sms point[%d], send count[%d], message count[%d]', $this->smsPoint, $this->getSendPoint(), $this->message->count()));
$defaultReplaceCode = array_fill_keys($this->message->getReplaceCodes(), '');
$replaceCode = gd_array_merge($defaultReplaceCode, $replaceCode);
$replaceContents = $replace->appendBracesAndReplace($this->message->getContents(), $replaceCode);
$hasPoint = $this->smsPoint > 1;
if ($this->message->exceedSmsLength($replaceContents)) { // 치환 후 sms 길이를 초과하는 경우 lms 로 변환
$this->message = $this->message->toLmsMessage();
$replaceContents = $replace->appendBracesAndReplace($this->message->getContents(), $replaceCode);
$hasPoint = $this->smsPoint > Sms::LMS_POINT;
if ($this->message->exceedLmsLength($replaceContents)) {
$logger->error('Message [' . $replaceContents . '] is exceed lms length');
$this->saveSendResultByFail($this->sendSms['hp'], '1002');
$this->sendSms['msg'] = $message;
$this->sendSms['memberNo'] = $item['memNo'];
$res = $this->callApi();
$this->smsPoint -= ($this->_isLms() ? Sms::LMS_POINT : 1);
 
 
$this->callApiPostProcess($res, 1);
} else {
$this->saveSendResultByFail($this->sendSms['hp'], '1001');
}
}
$logger->info(__METHOD__ . ', end point[' . $this->smsPoint . ']');
Sms::saveSmsPoint($this->smsPoint);
 
 
}
/**
if ($this->getReceiverGroup() > 1) {
$receiverCount = gd_count(explode(',', $this->sendSms['hp']));
}
$this->smsPoint -= ($this->getSendPoint($receiverCount) * $this->message->count());
Sms::saveSmsPoint($this->smsPoint);
 
 
}
/**
$this->callApiByLargePostProcess($res, $hpCount);
// 그룹을 2개 이상 발송할 경우 sms포인트가 중복 차감 처리 되어 해당 부분 수신인으로 카운팅 처리
$this->smsPoint -= ($this->getSendPoint() * $this->message->count());
Sms::saveSmsPoint($this->smsPoint);
 
 
}
/**
$this->smsPoint = $smsPoint;
}
 
 
 
 
 
 
 
 
/**
* @param array $contentsMask
*/
return '[제목없음]';
}
return $subject;
}
/**
 
변경 후SmsSender.php
protected $logData;
/** @var int sms 포인트 */
protected $smsPoint;
protected $usePoint = true;
/** @var int sms 전송내역 일련번호 */
protected $smsLogSno;
/** @var array sms api 호출 시 사용될 정보 */
*/
protected function validatePoint()
{
if ($this->usePoint === false) {
return;
}
if ($this->smsPoint < $this->getSendPoint() * $this->message->count()) {
$logger = \App::getInstance('logger');
$logger->info(sprintf('current sms point[%d], send count[%d], message count[%d]', $this->smsPoint, $this->getSendPoint(), $this->message->count()));
$defaultReplaceCode = array_fill_keys($this->message->getReplaceCodes(), '');
$replaceCode = gd_array_merge($defaultReplaceCode, $replaceCode);
$replaceContents = $replace->appendBracesAndReplace($this->message->getContents(), $replaceCode);
$hasPoint = $this->usePoint === false || $this->smsPoint > 1;
if ($this->message->exceedSmsLength($replaceContents)) { // 치환 후 sms 길이를 초과하는 경우 lms 로 변환
$this->message = $this->message->toLmsMessage();
$replaceContents = $replace->appendBracesAndReplace($this->message->getContents(), $replaceCode);
$hasPoint = $this->usePoint === false || $this->smsPoint > Sms::LMS_POINT;
if ($this->message->exceedLmsLength($replaceContents)) {
$logger->error('Message [' . $replaceContents . '] is exceed lms length');
$this->saveSendResultByFail($this->sendSms['hp'], '1002');
$this->sendSms['msg'] = $message;
$this->sendSms['memberNo'] = $item['memNo'];
$res = $this->callApi();
if ($this->usePoint) {
$this->smsPoint -= ($this->_isLms() ? Sms::LMS_POINT : 1);
}
$this->callApiPostProcess($res, 1);
} else {
$this->saveSendResultByFail($this->sendSms['hp'], '1001');
}
}
$logger->info(__METHOD__ . ', end point[' . $this->smsPoint . ']');
if ($this->usePoint) {
Sms::saveSmsPoint($this->smsPoint);
}
}
/**
if ($this->getReceiverGroup() > 1) {
$receiverCount = gd_count(explode(',', $this->sendSms['hp']));
}
if ($this->usePoint) {
$this->smsPoint -= ($this->getSendPoint($receiverCount) * $this->message->count());
Sms::saveSmsPoint($this->smsPoint);
}
}
/**
$this->callApiByLargePostProcess($res, $hpCount);
// 그룹을 2개 이상 발송할 경우 sms포인트가 중복 차감 처리 되어 해당 부분 수신인으로 카운팅 처리
if ($this->usePoint) {
$this->smsPoint -= ($this->getSendPoint() * $this->message->count());
Sms::saveSmsPoint($this->smsPoint);
}
}
/**
$this->smsPoint = $smsPoint;
}
/**
* @param bool $usePoint
*/
public function setUsePoint($usePoint)
{
$this->usePoint = (bool)$usePoint;
}
/**
* @param array $contentsMask
*/
return '[제목없음]';
}
return SmsUtil::cutSubject($subject);
}
/**
 
Controller/Admin/Base/LayerSmsAuthController.php+4-41
변경 전LayerSmsAuthController.php
 
use Component\Member\Manager;
use Component\Policy\ManageSecurityPolicy;
use Component\Sms\Exception\PasswordException;
use Component\Sms\Sms;
use Component\Sms\SmsMessage;
use Component\Sms\SmsSender;
use Component\Validator\Validator;
use Framework\Debug\Exception\AlertReloadException;
 
$isEmailAuth = $manager['isEmailAuth'] === 'y' && Validator::required($manager['email']);
$isCsAuth = gd_array_key_exists('csSno', $manager) && $manager['csSno'] > 0;
$isInvalidPassword = false;
$smsPoint = Sms::getPoint(true);
$hasSmsPoint = ($smsPoint >= 1);
 
$email = $cellPhone = '';
 
$securitySelect = \App::load(ManageSecurityPolicy::class)->getLoginSecuritySelect();
 
if (gd_array_key_exists('smsReSend', $securitySelect) && !$hasSmsPoint) {
try {
$sender = \App::load(SmsSender::class);
$sender->setIsThrowPasswordException(true);
$sender->setMessage(new SmsMessage('SMS 인증번호 검증'));
$receiver = [
[
'memNo' => '0',
'cellPhone' => '00000000000',
],
];
$sender->setReceiver($receiver);
$sender->setSmsType('guest');
$sender->setLogData(['disableResend' => true]);
$sender->validPassword(\App::load(\Component\Sms\SmsUtil::class)->getPassword());
$sender->setSmsPoint(1);
$sender->send();
} catch (PasswordException $e) {
if (gd_array_key_exists('emailSend', $securitySelect)) {
unset($securitySelect['smsReSend']);
} else {
$isInvalidPassword = true;
}
}
}
 
$countSelect = \gd_count($securitySelect);
 
if ($isCsAuth) {
$emptyAuthMethod = true;
$securitySelect = ['emailSend' => '이메일'];
} elseif ($countSelect === 2) {
if ($isSmsAuth && !$isEmailAuth && !$hasSmsPoint) {
unset($securitySelect['smsReSend']);
$emptySmsPoint = true;
} elseif (!$isSmsAuth && !$isEmailAuth) {
$securitySelect = [];
} elseif ($isSmsAuth && !$isEmailAuth) {
unset($securitySelect['emailSend']);
} elseif ($isEmailAuth && (!$isSmsAuth || !$hasSmsPoint)) {
unset($securitySelect['smsReSend']);
}
} elseif (gd_array_key_exists('smsReSend', $securitySelect) && $countSelect === 1) {
if (!$hasSmsPoint) {
$emptySmsPoint = true;
} elseif (!$isSmsAuth && $hasSmsPoint) {
unset($securitySelect['smsReSend']);
}
} elseif (gd_array_key_exists('emailSend', $securitySelect) && $countSelect === 1 && !$isEmailAuth) {
unset($securitySelect['emailSend']);
}
 
if ($isSmsAuth && $hasSmsPoint) {
$phoneArr = explode('-', $manager['cellPhone']);
$phoneLen = \strlen($phoneArr[1]);
$s = '';
 
변경 후LayerSmsAuthController.php
 
use Component\Member\Manager;
use Component\Policy\ManageSecurityPolicy;
 
 
 
 
use Component\Validator\Validator;
use Framework\Debug\Exception\AlertReloadException;
 
$isEmailAuth = $manager['isEmailAuth'] === 'y' && Validator::required($manager['email']);
$isCsAuth = gd_array_key_exists('csSno', $manager) && $manager['csSno'] > 0;
$isInvalidPassword = false;
 
 
 
$email = $cellPhone = '';
 
$securitySelect = \App::load(ManageSecurityPolicy::class)->getLoginSecuritySelect();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$countSelect = \gd_count($securitySelect);
 
if ($isCsAuth) {
$emptyAuthMethod = true;
$securitySelect = ['emailSend' => '이메일'];
} elseif ($countSelect === 2) {
if (!$isSmsAuth && !$isEmailAuth) {
 
 
 
$securitySelect = [];
} elseif ($isSmsAuth && !$isEmailAuth) {
unset($securitySelect['emailSend']);
} elseif ($isEmailAuth && !$isSmsAuth) {
unset($securitySelect['smsReSend']);
}
} elseif (gd_array_key_exists('smsReSend', $securitySelect) && $countSelect === 1) {
if (!$isSmsAuth) {
 
 
unset($securitySelect['smsReSend']);
}
} elseif (gd_array_key_exists('emailSend', $securitySelect) && $countSelect === 1 && !$isEmailAuth) {
unset($securitySelect['emailSend']);
}
 
if ($isSmsAuth) {
$phoneArr = explode('-', $manager['cellPhone']);
$phoneLen = \strlen($phoneArr[1]);
$s = '';
 
Controller/Admin/Base/LoginPsController.php-11
변경 전LoginPsController.php
use Component\Member\Manager;
use Component\Member\ManagerCs;
use Component\Policy\ManageSecurityPolicy;
use Component\Sms\Exception\PasswordException;
use Component\Sms\SmsSender;
use Component\Validator\Validator;
use Exception;
use Framework\Debug\Exception\AlertRedirectException;
// 임시 세션 정보
$tmpManager = $session->get(Manager::SESSION_TEMP_MANAGER);
$manager = \App::load(Manager::class);
$smsSender = \App::load(SmsSender::class);
$smsSender->setIsThrowPasswordException(true);
$smsAuth = $manager->sendSmsAuthNumber($tmpManager);
$tmpManager['smsAuthNumber'] = $smsAuth['smsAuthNumber'];
$tmpManager['isSmsLogin'] = $smsAuth['message'];
throw $e;
} catch (LayerNotReloadException $e) {
throw $e;
} catch (PasswordException $e) {
$this->json(
[
'error' => 5,
'message' => $e->getMessage(),
]
);
} catch (Exception $e) {
$adminLog->setAdminLog();
$logger->warning($e->getMessage() . ', ' . $e->getFile() . ', ' . $e->getLine(), $e->getTrace());
 
변경 후LoginPsController.php
use Component\Member\Manager;
use Component\Member\ManagerCs;
use Component\Policy\ManageSecurityPolicy;
 
 
use Component\Validator\Validator;
use Exception;
use Framework\Debug\Exception\AlertRedirectException;
// 임시 세션 정보
$tmpManager = $session->get(Manager::SESSION_TEMP_MANAGER);
$manager = \App::load(Manager::class);
 
 
$smsAuth = $manager->sendSmsAuthNumber($tmpManager);
$tmpManager['smsAuthNumber'] = $smsAuth['smsAuthNumber'];
$tmpManager['isSmsLogin'] = $smsAuth['message'];
throw $e;
} catch (LayerNotReloadException $e) {
throw $e;
 
 
 
 
 
 
 
} catch (Exception $e) {
$adminLog->setAdminLog();
$logger->warning($e->getMessage() . ', ' . $e->getFile() . ', ' . $e->getLine(), $e->getTrace());
 
Controller/Admin/Crm/CrmRecipeSendController.php+4-11
변경 전CrmRecipeSendController.php
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Godo\GodoSmsServerApi;
use Bundle\Component\Sms\SmsAdmin;
use Component\Sms\Sms;
use Component\Validator\Validator;
use Framework\Debug\Exception\AlertBackException;
use Origin\Enum\ApiClient\Crm\RecipeType;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
use Origin\Service\Crm\Message\MessageConfigService;
 
 
class CrmRecipeSendController extends \Controller\Admin\Controller
{
 
Sms::saveSmsPoint();
 
/** @var SmsAdmin $smsAdmin */
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
/** @var GodoSmsServerApi $godoSms */
$godoSms = \App::load('Component\\Godo\\GodoSmsServerApi');
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
$hasCallNumber = $smsPreRegister && !empty($smsAutoData['smsCallNum']);
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
 
변경 후CrmRecipeSendController.php
 
namespace Bundle\Controller\Admin\Crm;
 
 
 
use Component\Sms\Sms;
use Component\Validator\Validator;
use Framework\Debug\Exception\AlertBackException;
use Origin\Enum\ApiClient\Crm\RecipeType;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
class CrmRecipeSendController extends \Controller\Admin\Controller
{
 
Sms::saveSmsPoint();
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$hasCallNumber = $smsManageCallNumberService->hasUsableCallNumber();
 
 
 
 
 
 
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
 
Controller/Admin/Crm/MessageConfig/LayerCallerConnectorController.php-30
변경 전LayerCallerConnectorController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Origin\Service\Crm\Message\SmsCallerService;
 
class LayerCallerConnectorController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$this->setMenuCode('crm', 'messageConfig', 'messageConfig');
 
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
 
// 발신 관리책임자 목록 조회
$smsCallerList = $smsCallerService->getSmsCallerList()->getSmsCallerList();
 
$this->setData('smsCallerList', $smsCallerList);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerCallerConnectorController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerCallNumberController.php-42
변경 전LayerCallNumberController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Godo\GodoSmsServerApi;
use Bundle\Component\Sms\SmsAdmin;
use Origin\Service\Crm\Message\SmsCallerService;
 
class LayerCallNumberController extends \Controller\Admin\Controller
{
public function index()
{
/** @var SmsAdmin $smsAdmin */
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
/** @var GodoSmsServerApi $godoSms */
$godoSms = \App::load('Component\\Godo\\GodoSmsServerApi');
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
 
// 발신 관리책임자 목록 조회
$smsCallerList = $smsCallerService->getSmsCallerList()->getSmsCallerList();
 
// 해당 검증은 발신 관리책임자가 연동되어 있지 않을 경우에만 발동되므로, 방어코드 추가
$hasCaller = !empty(array_values(array_filter($smsCallerList, fn($item) => $item->isSelected())));
 
$this->setData('hasCaller', $hasCaller);
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('smsAutoData', gd_htmlspecialchars($smsAutoData));
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerCallNumberController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerManageCallNumberResultController.php-56
변경 전LayerManageCallNumberResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Sms\SmsAdmin;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Origin\Service\Crm\Message\SmsCallerService;
 
class LayerManageCallNumberResultController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'manageCallNumber');
$this->setMenuCode('crm', 'messageConfig', 'manageCallNumber');
 
/** @var Request $request */
$request = \App::getInstance('request');
$page = $request->post()->get('page', 1);
 
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
 
$smsCallerList = $smsCallerService->getSmsCallerList()->getSmsCallerList();
// 해당 검증은 발신 관리책임자가 연동되어 있지 않을 경우에만 발동되므로, 방어코드 추가
$selectedCaller = array_values(array_filter($smsCallerList, fn($item) => $item->isSelected()));
 
if (empty($selectedCaller)) {
throw new AlertCloseException('발신 관리책임자가 연동 되어 있지 않습니다.');
}
 
$caller = $selectedCaller[0];
$smsCallNumberListResponseDTO = $smsCallerService->getSmsNumberListByCallerNo($caller->getCallerNo(), $page);
 
$smsCallNumberList = $smsCallNumberListResponseDTO->getSmsCallNumberList();
$page = $smsCallNumberListResponseDTO->getPage();
 
/** @var SmsAdmin $smsAdmin */
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
$currentCallNumber = $smsAutoData['smsCallNum'] ?? '';
 
$this->setData('caller', $selectedCaller[0]);
$this->setData('currentCallNumber', $currentCallNumber);
$this->setData('smsCallNumberList', $smsCallNumberList);
$this->setData('page', $page);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerManageCallNumberResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfigPsController.php-49
변경 전MessageConfigPsController.php
use Origin\DTO\Crm\Message\SmsAutoConfigDTO;
use Origin\DTO\Crm\Message\SmsRejectConfigDTO;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\SmsCallerService;
 
class MessageConfigPsController extends \Controller\Admin\Controller
{
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
case 'checkCaller':
try {
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
 
// 발신 관리책임자 목록 조회
$smsCallerList = $smsCallerService->getSmsCallerList()->getSmsCallerList();
$smsCallerCount = count($smsCallerList);
 
// 해당 검증은 발신 관리책임자가 연동되어 있지 않을 경우에만 발동되므로, 방어코드 추가
$isSelectedExists = !empty(array_values(array_filter($smsCallerList, fn($item) => $item->isSelected())));
 
if ($isSelectedExists) {
$this->json(['success' => true, 'message' => '발신 관리책임자가 단일로 등록되어 있습니다.', 'data' => ['type' => 'connected_caller', 'caller' => $smsCallerList[0]->toArray()]]);
}
 
switch ($smsCallerCount) {
case 0: // 발신 관리책임자 없음
$this->json(['success' => true, 'message' => '등록된 발신관리 책임자가 없습니다.', 'data' => ['type' => 'no_caller']]);
break;
case 1:
$this->json(['success' => true, 'message' => '발신 관리책임자가 단일로 등록되어 있습니다.', 'data' => ['type' => 'single_caller', 'caller' => $smsCallerList[0]->toArray()]]);
break;
default:
$this->json(['success' => true, 'message' => '발신 관리책임자가 복수로 등록되어 있습니다.', 'data' => ['type' => 'multiple_callers']]);
}
} catch (\Exception $e) {
\Logger::channel('sms')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
case 'connectCaller':
try {
$callerNo = $request->post()->get('callerNo');
 
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
$result = $smsCallerService->connectSmsCaller($callerNo);
if ($result) {
$this->json(['success' => true, 'message' => '발신 관리책임자가 정상적으로 연동되었습니다.']);
} else {
$this->json(['success' => false, 'message' => '발신 관리책임자 연동에 실패하였습니다.']);
}
} catch (\Exception $e) {
\Logger::channel('sms')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
default:
throw new \InvalidArgumentException('잘못된 접근입니다.');
}
 
변경 후MessageConfigPsController.php
use Origin\DTO\Crm\Message\SmsAutoConfigDTO;
use Origin\DTO\Crm\Message\SmsRejectConfigDTO;
use Origin\Service\Crm\Message\MessageConfigService;
 
 
class MessageConfigPsController extends \Controller\Admin\Controller
{
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
default:
throw new \InvalidArgumentException('잘못된 접근입니다.');
}
 
Controller/Admin/Crm/MobileSendController.php+8-45
변경 전MobileSendController.php
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Godo\GodoSmsServerApi;
use Bundle\Component\Sms\SmsAdmin;
use Component\Sms\Sms;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MyappPushSettingService;
 
class MobileSendController extends \Controller\Admin\Controller
{
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
/** @var MyappPushSettingService $myappPushSettingService */
$myappPushSettingService = \App::getInstance(MyappPushSettingService::class);
$myappPushConfig = $myappPushSettingService->getMyappPushConfig();
 
/** @var SmsAdmin $smsAdmin */
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
/** @var GodoSmsServerApi $godoSms */
$godoSms = \App::load('Component\\Godo\\GodoSmsServerApi');
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
$hasCallNumber = $smsPreRegister && !empty($smsAutoData['smsCallNum']);
 
$messageConfig = [
"hasCallNumber" => $hasCallNumber,
"kakaoAlimTalk" => [
'sender' => $kakaoAlimTalkConfig->getSender()->value,
'useFlag' => $kakaoAlimTalkConfig->getUseFlag(),
'useAlternative' => $kakaoAlimTalkConfig->getManualSmsAlternativeSendFlag(),
],
"kakaoFriendTalk" => [
'sender' => $kakaoFriendTalkConfig->getSender()->value,
'useFlag' => $kakaoFriendTalkConfig->getUseFlag(),
'linkPlatformType' => $kakaoFriendTalkConfig->getLinkPlatformType()->name,
'useAlternative' => $kakaoFriendTalkConfig->getSmsAlternativeSendFlag(),
],
"myappPush" => [
'useFlag' => $myappPushConfig->getUseFlag(),
'useAlternative' => $myappPushConfig->getManualSmsAlternativeSendFlag(),
],
];
 
$this->setData('messageConfig', $messageConfig);
$this->setData('smsPointEach', Sms::SMS_POINT);
$this->setData('lmsPointEach', Sms::LMS_POINT);
$this->setData('kakaoAlrimTalkPointEach', Sms::KAKAO_POINT);
 
변경 후MobileSendController.php
 
namespace Bundle\Controller\Admin\Crm;
 
 
 
use Component\Sms\Sms;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
 
class MobileSendController extends \Controller\Admin\Controller
{
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$hasCallNumber = $smsManageCallNumberService->hasUsableCallNumber();
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
 
 
$this->setData('messageConfig', $messageConfigService->buildSendableMessageConfig($hasCallNumber));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$this->setData('smsPointEach', Sms::SMS_POINT);
$this->setData('lmsPointEach', Sms::LMS_POINT);
$this->setData('kakaoAlrimTalkPointEach', Sms::KAKAO_POINT);
 
Controller/Admin/Crm/PopupManageCallNumberController.php-37
변경 전PopupManageCallNumberController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Debug\Exception\AlertCloseException;
use Origin\Service\Crm\Message\SmsCallerService;
 
class PopupManageCallNumberController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageConfig', 'manageCallNumber');
$this->setMenuCode('crm', 'messageConfig', 'manageCallNumber');
 
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
 
$smsCallerList = $smsCallerService->getSmsCallerList()->getSmsCallerList();
// 해당 검증은 발신 관리책임자가 연동되어 있지 않을 경우에만 발동되므로, 방어코드 추가
$selectedCaller = array_values(array_filter($smsCallerList, fn($item) => $item->isSelected()));
 
if (empty($selectedCaller)) {
throw new AlertCloseException('발신 관리책임자가 연동 되어 있지 않습니다.');
}
 
$this->setData('caller', $selectedCaller[0]);
 
$this->getView()->setDefine('layout', 'layout_blank.php');
}
}
 
변경 후PopupManageCallNumberController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/PopupManageCallNumberPsController.php-79
변경 전PopupManageCallNumberPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Sms\SmsAdmin;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Origin\Exception\Internal\ApiException;
use Origin\Service\Crm\Message\SmsCallerService;
 
class PopupManageCallNumberPsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'manageCallNumber');
$this->setMenuCode('crm', 'messageConfig', 'manageCallNumber');
 
/** @var Request $request */
$request = \App::getInstance('request');
$mode = $request->request()->get('mode');
 
switch ($mode) {
case 'deleteCallNumber':
$callerNo = $request->request()->get('callerNo');
$callNumberNo = $request->request()->get('callNumberNo');
$callNumber = $request->request()->get('callNumber');
 
/** @var \Bundle\Component\Sms\SmsAdmin $smsAdmin */
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
$mallCallNumber = $smsAutoData['smsCallNum'] ?? '';
$isCurrentCallNumber = $mallCallNumber === $callNumber;
 
try {
/** @var SmsCallerService $smsCallerService */
$smsCallerService = \App::getInstance(SmsCallerService::class);
$result = $smsCallerService->deleteSmsCallNumber($callerNo, $callNumberNo);
 
if ($result && $isCurrentCallNumber) {
gd_set_policy('sms.config', ['smsCallNum' => '']);
}
 
$this->json(['success' => $result, 'message' => $result ? '발신번호가 삭제되었습니다.' : '발신번호 삭제에 실패하였습니다.']);
} catch (ApiException $e) {
$data = ($e->getCode() == ApiException::CONFLICT) ? ['type' => 'conflict'] : null;
$this->json(['success' => false, 'message' => $e->getMessage(), 'data' => $data]);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], ['mallCallNumber' => $mallCallNumber, 'callNumber' => $callNumber]);
}
break;
case 'setCallNumber':
try {
$callNumber = $request->request()->get('callNumber');
 
if (empty($callNumber)) {
throw new AlertCloseException('발신번호를 선택해 주세요.');
}
 
$smsAdmin = \App::load(SmsAdmin::class);
$result = $smsAdmin->saveCallNum($callNumber);
if ($result) {
$this->json(['success' => true, 'message' => '발신번호 설정에 성공하였습니다.']);
} else {
$this->json(['success' => false, 'message' => '발신번호 설정에 실패하였습니다.']);
}
} catch (\Throwable $e) {
throw new AlertCloseException($e->getMessage());
}
break;
default:
throw new AlertCloseException('잘못된 접근입니다.');
}
}
}
 
변경 후PopupManageCallNumberPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Member/SmsAutoController.php+8-4
변경 전SmsAutoController.php
use Component\Sms\Sms;
use Framework\Utility\ComponentUtils;
use Framework\Utility\ConfigUrlUtils;
 
 
/**
* SMS 자동 발송 설정
$request = \App::getInstance('request');
// --- 메뉴 설정
$this->callMenu('member', 'sms', 'auto');
 
 
 
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
// SMS 발신번호 사전 등록 번호 정보
$godoSms = \App::load('Component\\Godo\\GodoSmsServerApi');
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
 
$checked = $smsAutoData['checked'];
 
$this->setData('smsAutoReservationTime', $smsAdmin->getSmsReservationTime());
$this->setData('smsAutoData', gd_htmlspecialchars($smsAutoData));
$this->setData('checked', gd_isset($checked));
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('policy', ComponentUtils::getPolicy('sms.sms080'));
$this->setData('memberJoin', ComponentUtils::getPolicy('member.join'));
$this->setData('useApprovalFlag', ($memberJoin['appUseFl'] != 'n' || $memberJoin['under14Fl'] == 'y'));
 
변경 후SmsAutoController.php
use Component\Sms\Sms;
use Framework\Utility\ComponentUtils;
use Framework\Utility\ConfigUrlUtils;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
/**
* SMS 자동 발송 설정
$request = \App::getInstance('request');
// --- 메뉴 설정
$this->callMenu('member', 'sms', 'auto');
// 인터셉터의 menuCode 보완은 postHandle 이라 index() 안의 admin API 호출보다 늦다
$this->setMenuCode('member', 'sms', 'auto');
 
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
// 스킨이 === 로 4분기하고 else 가 없어, 키를 빼면 발신번호 영역이 통째로 렌더되지 않는다
$hasRegisteredCallNumber = $smsManageCallNumberService->hasRegisteredCallNumber();
 
$checked = $smsAutoData['checked'];
 
$this->setData('smsAutoReservationTime', $smsAdmin->getSmsReservationTime());
$this->setData('smsAutoData', gd_htmlspecialchars($smsAutoData));
$this->setData('checked', gd_isset($checked));
$this->setData('hasRegisteredCallNumber', $hasRegisteredCallNumber);
$this->setData('policy', ComponentUtils::getPolicy('sms.sms080'));
$this->setData('memberJoin', ComponentUtils::getPolicy('member.join'));
$this->setData('useApprovalFlag', ($memberJoin['appUseFl'] != 'n' || $memberJoin['under14Fl'] == 'y'));
 
Controller/Admin/Member/SmsPsController.php-205
변경 전SmsPsController.php
<?php
 
/**
* This is commercial software, only users who have purchased a valid license
* and accept to the terms of the License Agreement can install and use this
* program.
*
* Do not edit or add to this file if you wish to upgrade Godomall5 to newer
* versions in the future.
*
* @copyright ⓒ 2016, NHN godo: Corp.
* @link http://www.godo.co.kr
*/
 
namespace Bundle\Controller\Admin\Member;
 
use Component\Godo\GodoSmsServerApi;
use Component\Sms\Exception\PasswordException;
use Component\Sms\SmsAdmin;
use Framework\Debug\Exception\LayerException;
use Framework\Debug\Exception\LayerNotReloadException;
use Origin\Service\Admin\Member\ManagerTutorial\HelperPopupExposureAvailabilityService;
use Origin\Enum\ManagerTutorialType;
 
/**
* SMS 관련 처리
*
* @author Shin Donggyu <artherot@godo.co.kr>
*/
class SmsPsController extends \Controller\Admin\Controller
{
 
/**
* index
*
* @throws LayerException
*/
public function index()
{
// --- 모듈 호출
$request = \App::getInstance('request');
$logger = \App::getInstance('logger');
$postData = $request->post()->toArray();
$mode = $request->post()->get('mode');
 
switch (gd_isset($mode)) {
case 'smsAuto': // SMS 자동발송 설정
try {
$smsAdmin = \App::load(SmsAdmin::class);
$smsAdmin->saveSmsAuto($postData);
 
// 운영 도우미 팝업 노출
$helperPopupExposureAvailabilityService = \App::getInstance(HelperPopupExposureAvailabilityService::class);
$getTutorialUrl = $helperPopupExposureAvailabilityService->getTutorialUrl(ManagerTutorialType::SMS_REGISTER->value);
 
if (!empty($getTutorialUrl)) {
$this->layer(__('저장이 완료되었습니다.'), 'parent.tutorial_layer("' . $getTutorialUrl . '");');
}
$this->layer(__('저장이 완료되었습니다.'));
} catch (\Exception $e) {
$item = ($e->getMessage() ? ' - ' . str_replace("\n", ' - ', $e->getMessage()) : '');
throw new LayerException(__('처리중에 오류가 발생하여 실패되었습니다.') . $item);
}
break;
 
case 'smsCallNumSave': // SMS 발신번호 저장
try {
$smsAdmin = \App::load(SmsAdmin::class);
$result = $smsAdmin->saveCallNum($postData['smsCallNum']);
if ($result === true) {
echo 'OK';
} else {
echo 'FAIL';
}
} catch (\Exception $e) {
echo 'FAIL';
}
break;
 
case 'contentsRegister': // SMS 내용 등록
case 'contentsModify': // SMS 내용 수정
try {
$smsAdmin = \App::load(SmsAdmin::class);
$smsAdmin->saveContentsData($postData);
$this->layer(__('저장이 완료되었습니다.'));
} catch (\Exception $e) {
$item = ($e->getMessage() ? ' - ' . str_replace("\n", ' - ', $e->getMessage()) : '');
throw new LayerException(__('처리중에 오류가 발생하여 실패되었습니다.') . $item);
}
break;
 
case 'contentsDelete': // SMS 내용 수정
try {
$smsAdmin = \App::load(SmsAdmin::class);
$smsAdmin->deleteContentsData($request->post()->get('sno'));
$this->layer(__('저장이 완료되었습니다.'));
} catch (\Exception $e) {
$item = ($e->getMessage() ? ' - ' . str_replace("\n", ' - ', $e->getMessage()) : '');
throw new LayerException(__('처리중에 오류가 발생하여 실패되었습니다.') . $item);
}
break;
 
case 'smsSend': // 발송
try {
$smsAdmin = \App::load(SmsAdmin::class);
$cnt = $smsAdmin->sendSms($postData);
$info = ' (' . $cnt['success'] . __('건 성공') . ' / ' . $cnt['fail'] . __('건 실패') . ')';
$logger->info($info, $postData);
if ((int) $cnt['success'] > 0) {
echo "<script> parent.BootstrapDialog.closeAll(); </script>";
$this->layer(__('SMS 발송 요청이 완료되었습니다. 자세한 내용은 SMS 발송 내역 보기 확인 바랍니다.'), 'parent.location.href="' . $request->getReferer() . '"');
} else {
throw new \Exception('SEND_FAIL');
}
} catch (PasswordException $e) {
echo "<script> parent.BootstrapDialog.closeAll(); </script>";
throw new LayerNotReloadException($e->getMessage(), null, null, $e->getScript(), 4000);
} catch (\Exception $e) {
echo "<script> parent.BootstrapDialog.closeAll(); </script>";
if ($e->getMessage() == 'SEND_FAIL') {
throw new LayerNotReloadException(__('SMS 발송 요청이 실패했습니다.'), null, null, null, 4000);
} else {
$item = ($e->getMessage() ? ' - ' . str_replace("\n", ' - ', $e->getMessage()) : '');
throw new LayerNotReloadException(__('처리중에 오류가 발생하여 실패되었습니다.') . $item, null, null, null, 4000);
}
}
break;
case 'cancel_all_member'://예약문자 전체회원 취소
try {
// --- SMS 모듈
$godoSms = new GodoSmsServerApi();
$componentSmsLog = \App::load('Component\\Sms\\SmsLog');
$smsLog = $componentSmsLog->getSmsLog('*', $postData['smsLogSno']);
if ($smsLog['replaceCodeType'] == 'none') {
// SMS 결과 수신 처리
$godoSms->smsReserveChange($postData['smsLogSno'], 'cancelAll', [], true);
$js = 'parent.hide_process();';
$js .= 'top.dialog_alert(\'전체 예약취소가 완료되었습니다.\', \'SMS 발송 내역 상세보기\', {callback: function(){parent.layer_list_search(\''.$postData['page'].'\')}});';
$this->js($js);
} else {
// SMS 결과 수신 처리
if ($godoSms->smsSendListReserveChange($postData['smsLogSno'], 'cancelAll', [], true)) {
//SMS 발송 내역 상세보기
$js = 'parent.hide_process();';
$js .= 'top.dialog_alert(\'전체 예약취소가 완료되었습니다.\', \'SMS 발송 내역 상세보기\', {callback: function(){parent.layer_list_search(\''.$postData['page'].'\')}});';
$this->js($js);
} else {
throw new LayerException('통신이 정상적이지 않습니다.\r\n잠시 후 다시 시도해 주세요.', null, null, null, 3000);
}
}
} catch (\Exception $e) {
throw new LayerException($e->getMessage(), null, null, null, 3000);
}
break;
case 'cancel_select_member'://예약문자 특정회원 취소
case 'cancel_search_member'://예약문자 검색회원 취소
try {
// --- SMS 모듈
$godoSms = new GodoSmsServerApi();
$componentSmsLog = \App::load('Component\\Sms\\SmsLog');
$smsLog = $componentSmsLog->getSmsLog('*', $postData['smsLogSno']);
 
$smeModeText = '선택';
if($mode == 'cancel_search_member'){
if($postData['smsKey'] == 'receiverCellPhone') {
$postData['smsKeyword'] = str_replace('-', '', $postData['smsKeyword']);
}
$postData['noPaging'] = 'y';
unset($postData['sno']);
$getData = $componentSmsLog->getSmsSendList($postData);
$postData['sno'] = array();
foreach($getData['data'] as $smsSendItem){
if($smsSendItem['sendCheckFl'] != 'c'){
$postData['sno'][] = $smsSendItem['sno'];
}
}
$smeModeText = '검색';
}
 
if ($smsLog['replaceCodeType'] == 'none') {
// SMS 결과 수신 처리
$godoSms->smsReserveChange($postData['smsLogSno'], 'cancel', $postData['sno'], true);
$js = 'parent.hide_process();';
$js .= 'top.dialog_alert(\''.$smeModeText.'회원 예약취소가 완료되었습니다.\', \'SMS 발송 내역 상세보기\', {callback: function(){parent.layer_list_search(\''.$postData['page'].'\')}});';
$this->js($js);
} else {
// SMS 결과 수신 처리
if ($godoSms->smsSendListReserveChange($postData['smsLogSno'], 'cancel', $postData['sno'], true)) {
$js = 'parent.hide_process();';
$js .= 'top.dialog_alert(\''.$smeModeText.'회원 예약취소가 완료되었습니다.\', \'SMS 발송 내역 상세보기\', {callback: function(){parent.layer_list_search(\''.$postData['page'].'\')}});';
$this->js($js);
} else {
echo "<script> parent.hide_process(); </script>";
throw new LayerException('통신이 정상적이지 않습니다.\r\n잠시 후 다시 시도해 주세요.', null, null, null, 3000);
}
}
} catch (LayerException $e) {
throw new LayerException($e->getMessage(), null, null, null, 3000);
}
break;
}
 
exit();
}
}
 
변경 후SmsPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Member/SmsSendController.php+15-10
변경 전SmsSendController.php
namespace Bundle\Controller\Admin\Member;
 
use Component\Code\Code;
use Component\Godo\GodoSmsServerApi;
use Component\Member\Group\Util;
use Component\Member\MemberDAO;
use Component\Sms\Sms;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
 
use Origin\Service\Member\CrmGroupService;
 
/**
*/
if (gd_is_provider()) { // 공급사관리
$this->callMenu('order', 'order', 'smsSend');
 
 
} else {
$this->callMenu('member', 'sms', 'send');
 
}
$logger = \App::getInstance('logger');
$request = \App::getInstance('request');
$popupMode = false;
$receiverMode = 'select';
 
// SMS 발신번호 사전 등록 번호 정보
$smsCallNum = $smsAdmin->getSmsCallNum();
$godoSms = new GodoSmsServerApi();
$smsPreRegister = $godoSms->checkSmsCallNumber($smsCallNum);
 
$memberGroupNames = Util::getGroupName();
 
if ($request->get()->has('opener')) {
//상품 재입고 알림
$this->setData('receiverCount', gd_count($request->get()->get('receiverKeys', [])));
$getSmsContentsByGoodsRestock = function () {
$returnData = '';
$db = \App::getInstance('DB');
$db->strField = '*';
$db->strWhere = 'smsType = \'user\' AND smsAutoCode LIKE CONCAT(\'01007501\', \'%\')';
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
list($resultData) = $db->query_fetch($strSQL);
$returnData = StringUtils::htmlSpecialCharsStripSlashes($resultData['contents']);
 
 
 
return $returnData;
};
 
$smsContents = $getSmsContentsByGoodsRestock();
$this->setData('smsForbidTime', Sms::SMS_FORBID_TIME);
$this->setData('lmsPoint', Sms::LMS_POINT);
$this->setData('smsCallNum', $smsCallNum);
$this->setData('smsPreRegister', $smsPreRegister);
if ($this->getData('receiverCount') < 1) {
$this->setData('receiverCount', gd_count($receiverData));
}
 
변경 후SmsSendController.php
namespace Bundle\Controller\Admin\Member;
 
use Component\Code\Code;
 
use Component\Member\Group\Util;
use Component\Member\MemberDAO;
use Component\Sms\Sms;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Origin\Service\Member\CrmGroupService;
 
/**
*/
if (gd_is_provider()) { // 공급사관리
$this->callMenu('order', 'order', 'smsSend');
// 인터셉터의 menuCode 보완은 postHandle 이라 index() 안의 admin API 호출보다 늦다
$this->setMenuCode('order', 'order', 'smsSend');
} else {
$this->callMenu('member', 'sms', 'send');
$this->setMenuCode('member', 'sms', 'send');
}
$logger = \App::getInstance('logger');
$request = \App::getInstance('request');
$popupMode = false;
$receiverMode = 'select';
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$smsCallNum = $smsManageCallNumberService->getCallNumber();
// 스킨이 === 로 4분기하고 else 가 없어, 키를 빼면 발신번호 영역이 통째로 렌더되지 않는다
$hasRegisteredCallNumber = $smsManageCallNumberService->hasRegisteredCallNumber();
$memberGroupNames = Util::getGroupName();
 
if ($request->get()->has('opener')) {
//상품 재입고 알림
$this->setData('receiverCount', gd_count($request->get()->get('receiverKeys', [])));
$getSmsContentsByGoodsRestock = function () {
 
$db = \App::getInstance('DB');
$db->strField = '*';
$db->strWhere = 'smsType = \'user\' AND smsAutoCode LIKE CONCAT(\'01007501\', \'%\')';
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
$resultData = $db->query_fetch($strSQL);
if (empty($resultData)) {
return '';
}
 
return StringUtils::htmlSpecialCharsStripSlashes($resultData[0]['contents']);
};
 
$smsContents = $getSmsContentsByGoodsRestock();
$this->setData('smsForbidTime', Sms::SMS_FORBID_TIME);
$this->setData('lmsPoint', Sms::LMS_POINT);
$this->setData('smsCallNum', $smsCallNum);
$this->setData('hasRegisteredCallNumber', $hasRegisteredCallNumber);
if ($this->getData('receiverCount') < 1) {
$this->setData('receiverCount', gd_count($receiverData));
}
 
Controller/Admin/Policy/AdminLogListController.php+44-11
변경 전AdminLogListController.php
namespace Bundle\Controller\Admin\Policy;
 
 
use Bundle\Component\Member\Member;
use Component\Admin\AdminLogDAO;
 
 
class AdminLogListController extends \Controller\Admin\Controller
{
 
 
 
 
 
 
public function index()
{
$this->callMenu('policy', 'management', 'adminLogList');
$adminLog = new AdminLogDAO();
$request = \Request::get()->toArray();
gd_isset($request['view'], 'default');
 
 
 
 
$request['searchDate'][0] = $request['searchDate'][0] ? $request['searchDate'][0] : date('Y-m-d', strtotime('-6 day'));
$request['searchDate'][1] = $request['searchDate'][1] ? $request['searchDate'][1] : date('Y-m-d');
 
// 페이지 구분
$request['adminAccessPage'] = 'adminInfo';
if ($request['view'] == 'adminAccess') {
$request['adminAccessFl'] = true;
}
 
 
 
 
 
 
 
 
$fields = ['sno','regDt', 'ip','managerId','menu','page','action','baseUri','data'];
$data = $adminLog->getList($request,$fields,\Request::get()->get('pageNum',20));
 
 
// 정규식 패턴 view 파라미터 제거
$pattern = '/view=[^&]+$|view=[^&]+&/';
$this->setData('data',$data);
$this->setData('view', $request['view']);
$this->setData('queryString', $queryString);
$this->setData('searchKindASelectBox', Member::getSearchKindASelectBox());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
}
 
변경 후AdminLogListController.php
namespace Bundle\Controller\Admin\Policy;
 
 
 
use Component\Admin\AdminLogDAO;
use Origin\Service\Crm\Message\SmsPersonalInfoAccessLogListDataService;
 
class AdminLogListController extends \Controller\Admin\Controller
{
const VIEW_DEFAULT = 'default';
const VIEW_ADMIN_LIST = 'adminList';
const VIEW_ADMIN_ACCESS = 'adminAccess';
const VIEW_SMS_LIST = 'smsList';
const SEARCH_KIND_EQUAL_SEARCH = 'equalSearch';
 
public function index()
{
$this->callMenu('policy', 'management', 'adminLogList');
 
$request = \Request::get()->toArray();
gd_isset($request['view'], self::VIEW_DEFAULT);
 
// 개인정보 과다 조회를 막기 위해 이 화면은 부분포함 검색을 제공하지 않는다
$request['searchKind'] = self::SEARCH_KIND_EQUAL_SEARCH;
 
$request['searchDate'][0] = $request['searchDate'][0] ? $request['searchDate'][0] : date('Y-m-d', strtotime('-6 day'));
$request['searchDate'][1] = $request['searchDate'][1] ? $request['searchDate'][1] : date('Y-m-d');
 
$pageNum = \Request::get()->get('pageNum', 20);
 
if ($request['view'] == self::VIEW_SMS_LIST) {
$data = $this->getSmsPersonalInfoAccessLogData($request, (int)$pageNum);
} else {
$adminLog = new AdminLogDAO();
 
// 페이지 구분
$request['adminAccessPage'] = 'adminInfo';
if ($request['view'] == self::VIEW_ADMIN_ACCESS) {
$request['adminAccessFl'] = true;
}
 
$fields = ['sno', 'regDt', 'ip', 'managerId', 'menu', 'page', 'action', 'baseUri', 'data'];
$data = $adminLog->getList($request, $fields, $pageNum);
}
 
// 정규식 패턴 view 파라미터 제거
$pattern = '/view=[^&]+$|view=[^&]+&/';
$this->setData('data',$data);
$this->setData('view', $request['view']);
$this->setData('queryString', $queryString);
$this->setData('displayMenuColumnFl', $request['view'] == self::VIEW_DEFAULT);
$this->setData('displayPageColumnFl', in_array($request['view'], [self::VIEW_DEFAULT, self::VIEW_ADMIN_LIST, self::VIEW_SMS_LIST], true));
$this->setData('displayPersonalInfoNoteFl', $request['view'] != self::VIEW_SMS_LIST);
}
 
/**
* SMS 관련 개인정보 접속기록 목록 조회
*
* @param array $request
* @param int $pageNum
* @return array list, count, page, request, apiErrorFl
*/
protected function getSmsPersonalInfoAccessLogData(array $request, int $pageNum): array
{
/** @var SmsPersonalInfoAccessLogListDataService $smsPersonalInfoAccessLogListDataService */
$smsPersonalInfoAccessLogListDataService = \App::getInstance(SmsPersonalInfoAccessLogListDataService::class);
 
return $smsPersonalInfoAccessLogListDataService->getSmsPersonalInfoAccessLogData($request, $pageNum);
}
}
 
Controller/Admin/Policy/ManageListController.php+14
변경 전ManageListController.php
use Component\Member\ManagerCs;
use Component\Page\Page;
use Component\Scm\Scm;
 
 
use Framework\Debug\Exception\LayerException;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
$component = \App::load(Manager::class);
$getValue = $request->get()->toArray();
$getData = $component->getManagerList($getValue);
 
 
 
 
 
 
 
 
$page = \App::load(Page::class); // 페이지 재설정
$department = gd_code('02001'); // 부서
$position = gd_code('02002'); // 직급
$smsAutoReceiveKind = $component->smsAutoReceiveKind;
$smsAutoReceiveKind = gd_array_merge(['all' => __('전체')], ['n' => __('SMS 수신안함')], $smsAutoReceiveKind);
 
 
 
 
// 장기 미로그인 운영자 안내
$dataSecurity = gd_policy('manage.security');
gd_isset($dataSecurity['noVisitAlarmFl'], 'n');
$this->setData('position', $position);
$this->setData('duty', $duty);
$this->setData('smsAutoReceiveKind', $smsAutoReceiveKind);
 
$this->setData('scmList', $scmList);
$this->addCss(['layer.css']);
}
 
변경 후ManageListController.php
use Component\Member\ManagerCs;
use Component\Page\Page;
use Component\Scm\Scm;
use Origin\Enum\Crm\Message\MessageOperatorRoleType;
use Origin\Service\Crm\Message\MessageOperatorRoleService;
use Framework\Debug\Exception\LayerException;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
$component = \App::load(Manager::class);
$getValue = $request->get()->toArray();
$getData = $component->getManagerList($getValue);
// SMS 권한 컬럼 라벨 (조회된 행 데이터만으로 판정 — 추가 쿼리 없음)
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
if (is_array($getData['data'])) {
foreach ($getData['data'] as $messageRoleRowKey => $messageRoleRow) {
$getData['data'][$messageRoleRowKey]['smsRoleName'] = $messageOperatorRoleService->extractRole($messageRoleRow)?->getName() ?? '';
}
}
$page = \App::load(Page::class); // 페이지 재설정
$department = gd_code('02001'); // 부서
$position = gd_code('02002'); // 직급
$smsAutoReceiveKind = $component->smsAutoReceiveKind;
$smsAutoReceiveKind = gd_array_merge(['all' => __('전체')], ['n' => __('SMS 수신안함')], $smsAutoReceiveKind);
 
// SMS 권한 검색 라디오 목록 ('all'=전체 는 필터 미적용 UI 옵션이라 역할 목록 앞에 붙임)
$smsRoleList = ['all' => __('전체')] + MessageOperatorRoleType::availableForSearch();
 
// 장기 미로그인 운영자 안내
$dataSecurity = gd_policy('manage.security');
gd_isset($dataSecurity['noVisitAlarmFl'], 'n');
$this->setData('position', $position);
$this->setData('duty', $duty);
$this->setData('smsAutoReceiveKind', $smsAutoReceiveKind);
$this->setData('smsRoleList', $smsRoleList);
$this->setData('scmList', $scmList);
$this->addCss(['layer.css']);
}
 
Controller/Admin/Policy/ManagePermissionController.php+5
변경 전ManagePermissionController.php
 
namespace Bundle\Controller\Admin\Policy;
 
 
 
//use Globals;
//use Request;
//use Session;
$this->addScript([
'managePermission.js?'.time(), // 운영자 권한 설정 JS
]);
 
 
 
}
 
public function post()
 
변경 후ManagePermissionController.php
 
namespace Bundle\Controller\Admin\Policy;
 
use Origin\Enum\Crm\Message\MessageOperatorRoleType;
 
//use Globals;
//use Request;
//use Session;
$this->addScript([
'managePermission.js?'.time(), // 운영자 권한 설정 JS
]);
 
// SMS 권한 검색 필터 목록 ('all'=전체 + 역할)
$this->setData('smsRoleList', ['all' => __('전체')] + MessageOperatorRoleType::availableForSearch());
}
 
public function post()
 
Controller/Admin/Policy/ManagePsController.php+137-37
변경 전ManagePsController.php
use Component\Policy\ManageSecurityPolicy;
use Component\Policy\Policy;
use Component\Scm\ScmAdmin;
use Component\Sms\Sms;
use Component\Sms\SmsMessage;
use Component\Sms\SmsSender;
use Exception;
use Framework\Debug\Exception\LayerNotReloadException;
use Framework\Security\Otp;
use Framework\Utility\ComponentUtils;
use Framework\Utility\StringUtils;
use Framework\Security\Token;
 
 
 
 
 
/**
* 운영자 등록/수정 처리 페이지
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$_managerClass = \App::load(Manager::class);
 
// 각 모드에 따른 처리
switch ($request->request()->get('mode')) {
case 'modify': // --- 관리자 등록 / 수정
if (gd_is_provider()) {
$managerData = $_managerClass->getManagerData($request->post()->get('sno'));
$this->layer(__('잘못된 경로로 접근하셨습니다.'), 'parent.location.reload()');
}
case 'register':
 
try {
if ((DEFAULT_CODE_SCMNO === (int)$request->post()->get('sno')) && (DEFAULT_CODE_SCMNO !== (int)$session->get('manager.sno'))) {
$this->layer(__('잘못된 경로로 접근하셨습니다.'), 'parent.location.reload()');
}
// 데모인 경우 패스워드 수정 금지
$demo = \App::load(GodoDemoApi::class);
if ($demo->checkDomain() === true && $request->request()->get('mode') === 'modify') {
$request->post()->del('isModManagerPw');
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
if ($request->post()->get('permissionSetupMode') == 'managePermission' && $this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
$postData = $request->post()->toArray();
if (method_exists($_managerClass, 'setManagerWorkDebugPermissionFl')) {
$_managerClass->saveManagerWritePermissionData($postData, $managerSno);
$_managerClass->saveManagerIp($postData,$managerSno);
} else {
$_managerClass->saveManagerData($request->post()->toArray(), $request->files()->toArray());
}
 
 
 
 
 
 
 
 
$this->layer(__('저장이 완료되었습니다.') . '<br/><b class="notice-danger notice-info">'.__('개인정보보호법에 따라 개인정보처리시스템에 접속하는') . '<br>'.__('개인정보취급자(운영자)인 경우에는 접속보안(접속가능 IP)을 설정하시기 바랍니다.'). '</b>', 'parent.location.replace("manage_list.php")');
} catch (Exception $e) {
throw new LayerNotReloadException($e->getMessage(), $e->getCode(), $e, null, 3000);
} else { // 기존 운영자 권한 불러오기
$mode = 'modify';
}
 
 
// 운영자 정보
if ($request->get()->get('sno') == '') {
$this->setData('disabled', $disabled);
$this->setData('changeType', $request->get()->get('type'));
$this->setData('isAccessControlReason', $request->get()->get('isAccessControlReason'));
 
 
$this->setData('sno', $request->get()->get('sno'));
 
 
 
 
 
 
 
 
 
 
 
break;
 
// --- 운영자 권한 설정> 운영자 검색
gd_isset($getValue['pageNum'], 40);
 
$getData = $_managerClass->getManagerList($getValue);
 
 
 
 
 
 
 
 
 
 
 
$page = \App::load('\\Component\\Page\\Page', $getValue['page']); // 페이지 재설정
 
// --- 관리자 디자인 템플릿
 
// --- 운영자 권한 설정 저장
case 'setManagePermission':
 
 
 
try {
if ($this->getData('managerPermissionMethodExist') !== true || $this->getData('adminMenuPermissionMethodExist') !== true) {
throw new \RuntimeException(__('정상적으로 서비스가 제공되지 않습니다. 잠시 후 다시 시도해주세요.<br>문제가 지속될 경우 1:1문의 게시판에 문의해주세요.'));
}
$_managerClass->saveManagersPermission($request->post()->toArray());
 
 
 
 
 
 
 
 
 
 
 
 
$this->layerNotReload(__('저장이 완료되었습니다.'), 'parent.save_after_manger();');
} catch (Exception $e) {
throw new LayerNotReloadException($e->getMessage(), null, null, null, 5000);
 
case 'authSms':
$session->del(Member::SESSION_USER_CERTIFICATION);
$smsPoint = Sms::getPoint();
if ($smsPoint < 1) {
$this->json(
[
'error' => 100,
'message' => __('SMS 포인트가 부족합니다. 인증번호 발송을 위해 SMS 포인트를 충전해주세요'),
]
);
}
// 인증번호 생성
$userCertificationSession['certificationCode'] = Otp::getOtp(8);
$cellPhone = $request->post()->get('cellPhone');
$receiver[] = ['cellPhone' => $cellPhone];
$logData['reserve']['dbTable'] = $userCertificationSession;
 
$msg = sprintf(__('인증번호는 [%s]입니다. 정확히 입력해주세요.'), $userCertificationSession['certificationCode']);
$smsSender = \App::load(SmsSender::class);
$smsSender->validPassword(\App::load(\Component\Sms\SmsUtil::class)->getPassword());
$smsSender->setSmsPoint($smsPoint);
$smsSender->setMessage(new SmsMessage($msg));
$smsSender->setSmsType('user');
$smsSender->setMsgType('auth'); //인증용
$smsSender->setReceiver($receiver);
$smsSender->setLogData(['disableResend' => true]);
$smsSender->setContentsMask([$userCertificationSession['certificationCode']]);
$smsResult[] = $smsSender->send();
if ($smsResult[0]['success'] === 1) {
$userCertificationSession['authType'] = 'authSms';
$certData = [
'certNo' => $userCertificationSession['certificationCode'],
]
);
}
$exitMessage = '등록된 SMS 발신번호가 없어 인증번호를 SMS로 발송할 수 없습니다.';
$exitMessage .= ' 발신번호 사전등록제에 따라 SMS를 발송하려면 발신번호가 먼저 등록되어 있어야 합니다.';
$exitMessage .= '<br />고도몰 홈페이지의 마이페이지>SMS발신번호 등록/관리에서 발신번호 등록 후';
$exitMessage .= ' 관리자의 회원>SMS 관리>자동 SMS 설정에서 발신번호를 설정할 수 있습니다.';
$this->json(
[
'error' => 200,
'message' => __($exitMessage),
]
);
break;
break;
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후ManagePsController.php
use Component\Policy\ManageSecurityPolicy;
use Component\Policy\Policy;
use Component\Scm\ScmAdmin;
 
 
 
use Exception;
use Framework\Debug\Exception\LayerNotReloadException;
use Framework\Security\Otp;
use Framework\Utility\ComponentUtils;
use Framework\Utility\StringUtils;
use Framework\Security\Token;
use Origin\Service\Crm\Message\MessageOperatorRoleGuard;
use Origin\Service\Crm\Message\MessageOperatorRoleService;
use Origin\Service\Member\ManagerEditPolicy;
use Origin\Service\Sms\OccupancyAuth\OccupancyOtpSmsService;
 
/**
* 운영자 등록/수정 처리 페이지
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$_managerClass = \App::load(Manager::class);
$mode = $request->request()->get('mode');
// 각 모드에 따른 처리
switch ($mode) {
case 'modify': // --- 관리자 등록 / 수정
if (gd_is_provider()) {
$managerData = $_managerClass->getManagerData($request->post()->get('sno'));
$this->layer(__('잘못된 경로로 접근하셨습니다.'), 'parent.location.reload()');
}
case 'register':
$this->setManageMenuCode($mode);
try {
if ((DEFAULT_CODE_SCMNO === (int)$request->post()->get('sno')) && (DEFAULT_CODE_SCMNO !== (int)$session->get('manager.sno'))) {
$this->layer(__('잘못된 경로로 접근하셨습니다.'), 'parent.location.reload()');
}
// 데모인 경우 패스워드 수정 금지
$demo = \App::load(GodoDemoApi::class);
if ($demo->checkDomain() === true && $mode === 'modify') {
$request->post()->del('isModManagerPw');
}
// SMS 발신관리위임자 지정/회수 — 버튼 선택(설정/해제) + 사유 입력 (책임자만). 저장 전엔 검증만, 실제 지정/회수(functionAuth messageSendSms 반영 포함)는 저장 후 Service 로 일원화.
/** @var MessageOperatorRoleService $messageRoleService */
$messageRoleService = \App::getInstance(MessageOperatorRoleService::class);
 
// 위임자/발송담당자 계정 정보는 책임자(최고운영자)와 본인만 수정 가능 — 메뉴 쓰기 권한보다 우선
if ($mode === 'modify') {
$editTargetSno = (int)$request->post()->get('sno');
/** @var ManagerEditPolicy $managerEditPolicy */
$managerEditPolicy = \App::getInstance(ManagerEditPolicy::class);
if (!$managerEditPolicy->canEditAccount($editTargetSno)) {
throw new Exception(__('해당 계정 정보는 책임자 및 본인만 수정할 수 있습니다.'));
}
// 화면 잠금으로 값이 빠진 요청과 잠금 우회로 값이 실린 요청을 같은 방식으로 처리한다
$isSenderNameLocked = $managerEditPolicy->isSenderNameLocked($editTargetSno);
$isSenderCellPhoneLocked = $managerEditPolicy->isSenderCellPhoneLocked($editTargetSno);
if ($isSenderNameLocked || $isSenderCellPhoneLocked) {
$senderRoleInfo = $_managerClass->getManagerInfo($editTargetSno);
if ($isSenderNameLocked) {
$request->post()->set('managerNm', $senderRoleInfo['managerNm']);
}
if ($isSenderCellPhoneLocked) {
$request->post()->set('cellPhone', $senderRoleInfo['cellPhone']);
}
}
if ($managerEditPolicy->isRepresentativeProfileLocked($editTargetSno)) {
$superManagerInfo = $_managerClass->getManagerInfo($editTargetSno);
$request->post()->set('managerNm', $superManagerInfo['managerNm']);
$request->post()->set('cellPhone', $superManagerInfo['cellPhone']);
$request->post()->set('email', $superManagerInfo['email']);
}
}
 
$targetSno = (int)$request->post()->get('sno');
$messageDelegateAction = (string)$request->post()->get('messageDelegateAction');
$accessControlReason = trim((string)$request->post()->get('accessControlReason')); // 사유는 accessControlReason 으로 제출 → 운영자 관리/수정 개인정보접속기록에 남음
$postFunctionAuth = $request->post()->get('functionAuth');
// 권한 검증 실패는 [확인] 버튼으로 닫는 얼럿이어야 해 자동 닫힘 레이어를 쓰는 공통 catch 를 타지 않고 여기서 직접 출력한다
try {
$messageRoleService->assertRoleChangeAllowed(
$targetSno,
$messageDelegateAction,
$accessControlReason,
$request->post()->get('permissionSetupMode') === 'managePermission',
is_array($postFunctionAuth) ? $postFunctionAuth : []
);
if ($mode === 'register') {
$messageRoleService->assertAssignableNewAccount(
(string)$request->post()->get('scmNo'),
(string)$request->post()->get('isSuper'),
$messageDelegateAction
);
}
} catch (Exception $e) {
$this->js('parent.dialog_alert("' . addslashes(__($e->getMessage())) . '","' . __('경고') . '" ,{isReload:false});');
}
 
// 발송담당자 권한 전이 발행용 스냅샷 — 저장 후 상태와 비교해 실제로 바뀐 대상만 발행한다
$senderPermissionSnapshot = $messageRoleService->collectSenderPermissions([$targetSno]);
 
if ($request->post()->get('permissionSetupMode') == 'managePermission' && $this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
$postData = $request->post()->toArray();
if (method_exists($_managerClass, 'setManagerWorkDebugPermissionFl')) {
$_managerClass->saveManagerWritePermissionData($postData, $managerSno);
$_managerClass->saveManagerIp($postData,$managerSno);
} else {
$managerSno = $_managerClass->saveManagerData($request->post()->toArray(), $request->files()->toArray());
}
 
// 저장 후 반영은 Service 로 일원화 — 전이 발행과 위임자 지정/회수 순서가 여기에 고정돼 있다. 등록은 저장 후 확정된 sno.
$messageRoleService->applyRoleChange(
$targetSno > 0 ? $targetSno : (int)$managerSno,
$messageDelegateAction,
$accessControlReason,
$senderPermissionSnapshot
);
$this->layer(__('저장이 완료되었습니다.') . '<br/><b class="notice-danger notice-info">'.__('개인정보보호법에 따라 개인정보처리시스템에 접속하는') . '<br>'.__('개인정보취급자(운영자)인 경우에는 접속보안(접속가능 IP)을 설정하시기 바랍니다.'). '</b>', 'parent.location.replace("manage_list.php")');
} catch (Exception $e) {
throw new LayerNotReloadException($e->getMessage(), $e->getCode(), $e, null, 3000);
} else { // 기존 운영자 권한 불러오기
$mode = 'modify';
}
$this->setManageMenuCode($mode);
 
// 운영자 정보
if ($request->get()->get('sno') == '') {
$this->setData('disabled', $disabled);
$this->setData('changeType', $request->get()->get('type'));
$this->setData('isAccessControlReason', $request->get()->get('isAccessControlReason'));
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
$this->setData('sno', $request->get()->get('sno'));
// 최고운영자/위임자면 발송담당자-SMS(messageSendSms) 체크 고정+잠금 (뷰에서 사용)
$this->setData('isMessageApprover', $messageOperatorRoleService->isSenderPermissionLocked((int)$request->get()->get('sno')));
$this->setData('isEditorMessageApprover', $messageOperatorRoleService->isApprover((int)$session->get('manager.sno')));
/** @var MessageOperatorRoleGuard $messageOperatorRoleGuard */
$messageOperatorRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
$isEditorMessageRepresentative = $messageOperatorRoleGuard->isRepresentative();
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
break;
 
// --- 운영자 권한 설정> 운영자 검색
gd_isset($getValue['pageNum'], 40);
 
$getData = $_managerClass->getManagerList($getValue);
// SMS 권한 컬럼 라벨 (조회된 행 데이터만으로 판정 — 추가 쿼리 없음)
/** @var MessageOperatorRoleService $messageSmsRoleService */
$messageSmsRoleService = \App::getInstance(MessageOperatorRoleService::class);
if (is_array($getData['data'])) {
foreach ($getData['data'] as $smsRoleRowKey => $smsRoleRow) {
$smsRoleEnum = $messageSmsRoleService->extractRole($smsRoleRow);
$getData['data'][$smsRoleRowKey]['smsRoleName'] = $smsRoleEnum?->getName() ?? '';
$getData['data'][$smsRoleRowKey]['smsRole'] = $smsRoleEnum?->value ?? '';
$getData['data'][$smsRoleRowKey]['smsPermissionLocked'] = $messageSmsRoleService->isSenderPermissionLockedByRow($smsRoleRow) ? 'y' : 'n';
}
}
$page = \App::load('\\Component\\Page\\Page', $getValue['page']); // 페이지 재설정
 
// --- 관리자 디자인 템플릿
 
// --- 운영자 권한 설정 저장
case 'setManagePermission':
// 차단 조회 등 admin API 는 request 의 menuCode 를 헤더로 싣는다
// — PS 요청은 인터셉터가 채워주지 않으므로 실제 화면 메뉴로 직접 지정한다
$this->setMenuCode('policy', 'management', 'managePermission');
try {
if ($this->getData('managerPermissionMethodExist') !== true || $this->getData('adminMenuPermissionMethodExist') !== true) {
throw new \RuntimeException(__('정상적으로 서비스가 제공되지 않습니다. 잠시 후 다시 시도해주세요.<br>문제가 지속될 경우 1:1문의 게시판에 문의해주세요.'));
}
$permissionPostData = $request->post()->toArray();
/** @var MessageOperatorRoleService $senderRoleService */
$senderRoleService = \App::getInstance(MessageOperatorRoleService::class);
// 벌크 저장은 여러 운영자의 발송담당자 권한을 한 번에 바꾸므로 저장 전 상태를 모아 전이 대상만 1건으로 발행한다
$senderPermissionTargetSnos = is_array($permissionPostData['manage_sno'] ?? null) ? $permissionPostData['manage_sno'] : [];
$senderPermissionSnapshot = $senderRoleService->collectSenderPermissions($senderPermissionTargetSnos);
$_managerClass->saveManagersPermission($permissionPostData);
$revokedSenderSnos = $senderRoleService->publishSenderPermissionTransition(
$senderPermissionTargetSnos,
$senderPermissionSnapshot,
trim((string)($permissionPostData['accessControlReason'] ?? ''))
);
$senderRoleService->destroyCertificationIdentifiers($revokedSenderSnos);
$this->layerNotReload(__('저장이 완료되었습니다.'), 'parent.save_after_manger();');
} catch (Exception $e) {
throw new LayerNotReloadException($e->getMessage(), null, null, null, 5000);
 
case 'authSms':
$session->del(Member::SESSION_USER_CERTIFICATION);
 
 
 
 
 
 
 
 
 
 
$userCertificationSession['certificationCode'] = Otp::getOtp(8);
$cellPhone = $request->post()->get('cellPhone');
$isSent = \App::getInstance(OccupancyOtpSmsService::class)
->sendCertificationCode($cellPhone, $userCertificationSession['certificationCode']);
if ($isSent) {
 
 
 
 
 
 
 
 
 
 
 
 
$userCertificationSession['authType'] = 'authSms';
$certData = [
'certNo' => $userCertificationSession['certificationCode'],
]
);
}
 
 
 
 
$this->json(
[
'error' => 200,
'message' => __('인증번호 SMS 발송에 실패했습니다. 잠시 후 다시 시도해주세요.'),
]
);
break;
break;
}
}
 
/**
* 차단 조회 등 admin API 는 request 의 menuCode 를 헤더로 싣는데
* PS 요청은 인터셉터가 채워주지 않으므로 운영자 등록·수정 화면 메뉴로 직접 지정한다
*/
protected function setManageMenuCode(string $mode)
{
switch ($mode) {
case 'modify':
$this->setMenuCode('policy', 'management', 'modify');
break;
default:
$this->setMenuCode('policy', 'management', 'register');
break;
}
}
}
 
Controller/Admin/Policy/ManageRegisterController.php+81
변경 전ManageRegisterController.php
use Component\Member\Member;
use Framework\Debug\Exception\AlertBackException;
use Framework\Security\Token;
 
 
 
 
use Origin\Util\Oauth\Commerce\CommerceLoginUtils;
use Globals;
use Request;
 
if ($mode === 'register') {
$this->callMenu('policy', 'management', 'register');
 
 
 
} elseif ($isModify) {
$this->callMenu('policy', 'management', 'modify');
 
}
 
// 메일도메인
// 최고운영자 커머스 통합회원 로그인 연동 패치일 이후 설치 상점인지
$isMallInstalledAfterPatch = CommerceLoginUtils::isMallInstalledAfterPatch();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// --- 관리자 디자인 템플릿
$this->setData('isPasswordChangeRestricted', $isCompanySuperManager && $isMallInstalledAfterPatch);
$this->setData('isgPlusGift', $isgPlusGift);
 
변경 후ManageRegisterController.php
use Component\Member\Member;
use Framework\Debug\Exception\AlertBackException;
use Framework\Security\Token;
use Origin\Service\Crm\Message\MessageOperatorRoleGuard;
use Origin\Service\Crm\Message\MessageOperatorRoleService;
use Origin\Service\Member\ManagerEditPolicy;
use Origin\Service\Member\ManagerService;
use Origin\Util\Oauth\Commerce\CommerceLoginUtils;
use Globals;
use Request;
 
if ($mode === 'register') {
$this->callMenu('policy', 'management', 'register');
// 차단 조회 등 admin API 는 request 의 menuCode 를 헤더로 싣는데
// 인터셉터는 발송 게이트 메뉴만 채워주므로 여기서 직접 지정한다
$this->setMenuCode('policy', 'management', 'register');
} elseif ($isModify) {
$this->callMenu('policy', 'management', 'modify');
$this->setMenuCode('policy', 'management', 'modify');
}
 
// 메일도메인
// 최고운영자 커머스 통합회원 로그인 연동 패치일 이후 설치 상점인지
$isMallInstalledAfterPatch = CommerceLoginUtils::isMallInstalledAfterPatch();
 
// SMS 발신 위임자 권한 (권한 설정 섹션 row + [위임자 권한 설정]/[위임자 권한 해제] 버튼 + 사유)
// 규칙(뷰어 공급사·대상 적격성·역할)은 Origin(Guard/Service)에 위임 — 튜닝돼도 규칙 유지. 컨트롤러는 뷰데이터 조립만.
/** @var MessageOperatorRoleGuard $messageOperatorRoleGuard */
$messageOperatorRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
/** @var ManagerEditPolicy $managerEditPolicy */
$managerEditPolicy = \App::getInstance(ManagerEditPolicy::class);
 
$messageDelegate = ['visible' => false, 'editable' => false, 'assigned' => false];
$managerNo = (int)$data['data']['sno'];
$scmNo = (int)$data['data']['scmNo'];
// 등록(통합회원 연동 + 본인인증)을 마치지 않은 최고운영자는 지정/회수 주체가 아니므로 UI 자체를 내린다 — 저장측 차단만으로는 안내 없는 실패가 된다
$isRegisteredRepresentative = $messageOperatorRoleGuard->isRepresentative()
&& $messageOperatorRoleGuard->isRepresentativeConnected();
// 노출 대상: 발신관리책임자만 + 뷰어가 공급사 아님 + (등록이거나, 수정 대상이 본사 일반운영자)
if ($isRegisteredRepresentative
&& $messageOperatorRoleGuard->isProviderContext() === false
&& (!$isModify || $messageOperatorRoleService->isEligibleDelegateTarget($managerNo, $scmNo))) {
$delegate = $messageOperatorRoleService->getDelegate();
$assigned = $isModify && $delegate !== null && $delegate->getSno() === $managerNo;
$messageDelegate = [
'visible' => true,
'editable' => $isRegisteredRepresentative, // 지정/회수는 발신관리책임자만
'assigned' => $assigned,
'blockedByExisting' => $delegate !== null && $delegate->getSno() !== $managerNo, // 다른 운영자가 이미 위임자 → 설정 클릭 시 안내 모달
// 노출 대상일 때만 조회 — 차단 여부는 외부 API 호출이라 비노출 화면에서 불필요한 호출을 만들지 않는다
'callerBlocked' => $messageOperatorRoleService->isBlockedRepresentative(),
];
}
$this->setData('messageDelegate', $messageDelegate);
 
// 발송 역할 계정(책임자·위임자·발송담당자)의 휴대폰번호는 본인인증(DI 주입)으로 처리한다
// 본인인증은 본인 계정에서만 성립하므로 책임자·제3자가 여는 화면은 기존 점유인증(SMS) UI 를 그대로 쓴다
$isSelfTarget = $isModify && (int)$session->get('manager.sno') === $managerNo;
// 최고운영자·발송 역할 계정은 정보 수정 주체를 책임자와 본인으로 제한한다
$isRestrictedEditTarget = $isModify
&& ($managerNo === ManagerService::SUPER_MANAGER_SNO
|| $messageOperatorRoleService->hasSenderPermission($managerNo));
// 최고운영자의 이름·휴대폰번호·이메일은 통합회원 정보 동기화 또는 발신관리책임자 등록 이후 어드민 입력이 기준이 아니다 — 그때부터 화면 입력을 잠그고 통합 마이페이지 안내를 노출한다
$isRepresentativeSelf = $isModify && $managerEditPolicy->isRepresentativeProfileLocked($managerNo);
$this->setData('superManagerCommerceLinked', $isRepresentativeSelf);
 
$selfCertification = ['enabled' => false];
// 최고운영자의 본인인증 제공 기준은 잠금과 달라(동기화 후에는 미제공) 정책 판정을 따로 받는다 — 등록 전에는 점유인증으로 정보를 맞춘다
$isSelfCertificationProvided = $managerNo === ManagerService::SUPER_MANAGER_SNO
? $managerEditPolicy->isRepresentativeSelfCertificationProvided($managerNo)
: $isRestrictedEditTarget;
if ($isSelfTarget && $isSelfCertificationProvided) {
$selfCertification = [
'enabled' => true,
'certified' => $messageOperatorRoleGuard->isConnected($managerNo),
'isSelf' => $isSelfTarget,
];
$this->addScript([
'manager-certification.js?' . time(),
]);
}
$this->setData('selfCertification', $selfCertification);
// 인증 방식 분기와 분리해 둔다 — 본인인증 화면이 본인 한정이 되어도 책임자·제3자 화면의 1차 차단은 남아야 한다
// 저장측 throw 는 발송 역할 계정만 막으므로 최고운영자 대상은 이 화면 판정이 유일한 차단이다
$this->setData(
'managerInfoModifiable',
!$isRestrictedEditTarget || $messageOperatorRoleGuard->isRepresentative() || $isSelfTarget
);
// 동기화 전에만 통합회원 연동 안내를 노출한다 — 동기화 후에는 잠긴 필드 하단 안내가 대신한다
$this->setData('superManagerCommerceGuide', $isModify && $managerEditPolicy->isRepresentativeProfileUnsynced($managerNo));
 
// 이름·휴대폰번호는 본인인증 명의라 직접 수정할 수 없다 — 발송 역할 계정 본인 화면과 책임자 본인 화면에서 잠근다
// 이름은 발송 역할 계정이면 책임자에게도 잠기고, 휴대폰번호는 책임자에게만 열린다
$this->setData('senderNameLocked', ($isModify && $managerEditPolicy->isSenderNameLocked($managerNo)) || $isRepresentativeSelf);
$this->setData('senderCellPhoneLocked', ($isModify && $managerEditPolicy->isSenderCellPhoneLocked($managerNo)) || $isRepresentativeSelf);
 
// --- 관리자 디자인 템플릿
$this->setData('isPasswordChangeRestricted', $isCompanySuperManager && $isMallInstalledAfterPatch);
$this->setData('isgPlusGift', $isgPlusGift);
 
Controller/Admin/Policy/ManageSecurityController.php+1-2
변경 전ManageSecurityController.php
 
namespace Bundle\Controller\Admin\Policy;
 
use Component\Sms\Sms;
use Exception;
use Framework\Debug\Exception\LayerException;
use Framework\Utility\StringUtils;
}
gd_isset($dataSecurity['countryAccessBlocking'], []);
 
if ($superMangerInfo['data']['isSmsAuth'] == 'y' && Sms::getPoint() > 0) {
$dataSecurity['superCellPhoneFl'] = true;
} else {
$dataSecurity['superCellPhoneFl'] = false;
 
변경 후ManageSecurityController.php
 
namespace Bundle\Controller\Admin\Policy;
 
 
use Exception;
use Framework\Debug\Exception\LayerException;
use Framework\Utility\StringUtils;
}
gd_isset($dataSecurity['countryAccessBlocking'], []);
 
if ($superMangerInfo['data']['isSmsAuth'] == 'y') {
$dataSecurity['superCellPhoneFl'] = true;
} else {
$dataSecurity['superCellPhoneFl'] = false;
 
Controller/Admin/Promotion/CartRemindListController.php+9-9
변경 전CartRemindListController.php
namespace Bundle\Controller\Admin\Promotion;
 
use Component\CartRemind\CartRemind;
use Component\Godo\GodoSmsServerApi;
use Component\Sms\Sms;
use Component\Sms\SmsAdmin;
use Framework\Utility\ConfigUrlUtils;
 
 
use Request;
 
/**
// --- 메뉴 설정
$this->callMenu('promotion', 'cartRemind', 'cartRemindList');
 
$smsAdmin = new SmsAdmin();
$smsAutoData = $smsAdmin->getSmsAutoData();
 
 
// SMS 발신번호 사전 등록 번호 정보
$godoSms = new GodoSmsServerApi();
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
$searchData = Request::get()->all();
$cartRemind = new CartRemind();
 
$this->setData('smsAutoList', Sms::SMS_AUTO_RECEIVE_LIST);
$this->setData('smsAutoOrderPeriod', Sms::SMS_AUTO_ORDER_PERIOD);
$this->setData('smsAutoData', gd_htmlspecialchars($smsAutoData));
 
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('cartRemindData', gd_isset($getData['data']));
$this->setData('convertArrData', gd_isset($convertArrData));
$this->setData('page', $page);
$this->setData('commerceSmsIntroUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('sms-intro'));
$this->setData('commerceShopMainUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('shop-main'));
}
}
 
변경 후CartRemindListController.php
namespace Bundle\Controller\Admin\Promotion;
 
use Component\CartRemind\CartRemind;
 
use Component\Sms\Sms;
 
use Framework\Utility\ConfigUrlUtils;
use Origin\Enum\Crm\Message\LegacyCallNumberStatus;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Request;
 
/**
// --- 메뉴 설정
$this->callMenu('promotion', 'cartRemind', 'cartRemindList');
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$smsCallNum = $smsManageCallNumberService->getCallNumber();
 
// 스킨이 === 로 4분기하고 else 가 없어, 키를 빼면 발신번호 영역이 통째로 렌더되지 않는다
$smsPreRegister = LegacyCallNumberStatus::toLegacyValue($smsManageCallNumberService->getLegacyCallNumberStatus());
 
 
$searchData = Request::get()->all();
$cartRemind = new CartRemind();
 
$this->setData('smsAutoList', Sms::SMS_AUTO_RECEIVE_LIST);
$this->setData('smsAutoOrderPeriod', Sms::SMS_AUTO_ORDER_PERIOD);
// 스킨은 smsAutoData['smsCallNum'] 만 참조하므로 배열 계약만 유지하고 자동발송 전건 조회는 하지 않는다
$this->setData('smsAutoData', gd_htmlspecialchars(['smsCallNum' => $smsCallNum]));
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('cartRemindData', gd_isset($getData['data']));
$this->setData('convertArrData', gd_isset($convertArrData));
$this->setData('page', $page);
 
$this->setData('commerceShopMainUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('shop-main'));
}
}
 
Controller/Admin/Promotion/ComebackCouponListController.php+9-9
변경 전ComebackCouponListController.php
namespace Bundle\Controller\Admin\Promotion;
 
use Exception;
use Component\Godo\GodoSmsServerApi;
use Component\Sms\SmsAdmin;
use Framework\Debug\Exception\LayerException;
use Framework\Utility\ConfigUrlUtils;
 
 
use Request;
 
class ComebackCouponListController extends \Controller\Admin\Controller
 
// --- 모듈 호출
try {
$smsAdmin = new SmsAdmin();
$smsAutoData = $smsAdmin->getSmsAutoData();
 
 
// SMS 발신번호 사전 등록 번호 정보
$godoSms = new GodoSmsServerApi();
$smsPreRegister = $godoSms->checkSmsCallNumber($smsAutoData['smsCallNum']);
 
// 리스트 정보가져오기
$couponAdmin = \App::load('\\Component\\Coupon\\CouponAdmin');
}
 
// --- 관리자 디자인 템플릿
$this->setData('smsAutoData', gd_htmlspecialchars($smsAutoData));
 
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('data', gd_isset($getData['data']));
$this->setData('list', gd_isset($getData['list']));
$this->setData('page', $page);
$this->setData('commerceSmsIntroUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('sms-intro'));
$this->setData('commerceShopMainUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('shop-main'));
}
}
 
변경 후ComebackCouponListController.php
namespace Bundle\Controller\Admin\Promotion;
 
use Exception;
 
 
use Framework\Debug\Exception\LayerException;
use Framework\Utility\ConfigUrlUtils;
use Origin\Enum\Crm\Message\LegacyCallNumberStatus;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Request;
 
class ComebackCouponListController extends \Controller\Admin\Controller
 
// --- 모듈 호출
try {
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$smsCallNum = $smsManageCallNumberService->getCallNumber();
 
// 스킨이 === 로 4분기하고 else 가 없어, 키를 빼면 발신번호 영역이 통째로 렌더되지 않는다
$smsPreRegister = LegacyCallNumberStatus::toLegacyValue($smsManageCallNumberService->getLegacyCallNumberStatus());
 
 
// 리스트 정보가져오기
$couponAdmin = \App::load('\\Component\\Coupon\\CouponAdmin');
}
 
// --- 관리자 디자인 템플릿
// 스킨은 smsAutoData['smsCallNum'] 만 참조하므로 배열 계약만 유지하고 자동발송 전건 조회는 하지 않는다
$this->setData('smsAutoData', gd_htmlspecialchars(['smsCallNum' => $smsCallNum]));
$this->setData('smsPreRegister', $smsPreRegister);
$this->setData('data', gd_isset($getData['data']));
$this->setData('list', gd_isset($getData['list']));
$this->setData('page', $page);
 
$this->setData('commerceShopMainUrl', ConfigUrlUtils::getNhnCommerceMyGodoUrl('shop-main'));
}
}
 
Controller/Admin/Share/LayerExcelAuthController.php+9-19
변경 전LayerExcelAuthController.php
namespace Bundle\Controller\Admin\Share;
use Component\Sms\Sms;
use Framework\Utility\StringUtils;
/**
];
$isExcelAuthSms = ($auth['requireExcelAuthSms']) && $manager['isSmsAuth'] == 'y' && $manager['cellPhone'];
$isExcelAuthEmail = $auth['requireExcelAuthEmail'] && $manager['isEmailAuth'] == 'y' && $manager['email'];
$hasSmsPoint = (Sms::getPoint() >= 1);
$getValue = \Request::get()->all();
if ($isExcelAuthSms) {
if ($hasSmsPoint) {
$securitySelect['authSms'] = '휴대폰';
$phoneArr = explode('-', $manager['cellPhone']);
$phoneLen = strlen($phoneArr[1]);
$s = '';
for ($i = 1; $i <= $phoneLen; $i++) {
$s .= '*';
}
$phoneArr[1] = $s;
$phoneArr[2] = '**' . substr($phoneArr[2], 2, 2);
$authInformation['cellPhone'] = gd_implode('-', $phoneArr);
} else {
if ($isExcelAuthEmail) {
$this->setData('message', __('잔여 SMS포인트가 부족하여 이메일로 인증을 진행합니다.'));
} else {
$this->setData('superAlertMessage', __('SMS 잔여 포인트가 부족하여 SMS 인증을 할 수 없습니다. 포인트를 충전해주시기 바랍니다.'));
}
}
 
 
 
}
if ($auth['requireExcelAuthSmsGodo']) {
$securitySelect['authSmsGodo'] = '고도회원 휴대폰번호';
 
변경 후LayerExcelAuthController.php
namespace Bundle\Controller\Admin\Share;
 
use Framework\Utility\StringUtils;
/**
];
$isExcelAuthSms = ($auth['requireExcelAuthSms']) && $manager['isSmsAuth'] == 'y' && $manager['cellPhone'];
$isExcelAuthEmail = $auth['requireExcelAuthEmail'] && $manager['isEmailAuth'] == 'y' && $manager['email'];
 
$getValue = \Request::get()->all();
if ($isExcelAuthSms) {
$securitySelect['authSms'] = '휴대폰';
$phoneArr = explode('-', $manager['cellPhone']);
$phoneLen = strlen($phoneArr[1]);
$s = '';
for ($i = 1; $i <= $phoneLen; $i++) {
$s .= '*';
 
 
 
 
 
 
 
 
 
 
 
}
$phoneArr[1] = $s;
$phoneArr[2] = '**' . substr($phoneArr[2], 2, 2);
$authInformation['cellPhone'] = gd_implode('-', $phoneArr);
}
if ($auth['requireExcelAuthSmsGodo']) {
$securitySelect['authSmsGodo'] = '고도회원 휴대폰번호';
 
Controller/Admin/Share/LayerManagePermissionController.php+21
변경 전LayerManagePermissionController.php
use Framework\Debug\Exception\LayerException;
use Component\Admin\AdminMenu;
use Component\Member\Manager;
 
 
use Globals;
 
/**
$request = \App::getInstance('request');
$session = \App::getInstance('session');
 
 
 
 
 
$_managerClass = new Manager();
 
// 실행 조건 검증
 
$checked['permissionFl'][$existingPermission['permissionFl']] = 'checked="checked"';
 
 
 
 
 
 
 
 
 
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setPageName('share/layer_manage_permission.php');
 
$this->setData('checked', $checked);
$this->setData('disabled', $disabled);
$this->setData('sno', $request->post()->get('sno'));
 
 
 
 
 
 
 
$this->setData('scmFl', $scmFl);
$this->setData('scmNo', $scmNo);
$this->setData('viewMemuDepth', $viewMemuDepth);
 
변경 후LayerManagePermissionController.php
use Framework\Debug\Exception\LayerException;
use Component\Admin\AdminMenu;
use Component\Member\Manager;
use Origin\Service\Crm\Message\MessageOperatorRoleGuard;
use Origin\Service\Crm\Message\MessageOperatorRoleService;
use Globals;
 
/**
$request = \App::getInstance('request');
$session = \App::getInstance('session');
 
// 차단 조회 등 admin API 는 request 의 menuCode 를 헤더로 싣는다
// — 레이어 요청은 인터셉터가 채워주지 않으므로 실제 화면 메뉴로 직접 지정한다
$this->setMenuCode('policy', 'management', $request->post()->get('mode') === 'register' ? 'register' : 'modify');
 
$_managerClass = new Manager();
 
// 실행 조건 검증
 
$checked['permissionFl'][$existingPermission['permissionFl']] = 'checked="checked"';
 
// 최고운영자/위임자면 발송담당자-SMS(messageSendSms) 체크 고정+잠금 (레이어 최초 렌더에도 반영)
/** @var MessageOperatorRoleService $messageOperatorRoleService */
$messageOperatorRoleService = \App::getInstance(MessageOperatorRoleService::class);
$isMessageApprover = $messageOperatorRoleService->isSenderPermissionLocked((int)$request->post()->get('sno'));
/** @var MessageOperatorRoleGuard $messageOperatorRoleGuard */
$messageOperatorRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
$isEditorMessageRepresentative = $messageOperatorRoleGuard->isRepresentative();
 
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setPageName('share/layer_manage_permission.php');
 
$this->setData('checked', $checked);
$this->setData('disabled', $disabled);
$this->setData('sno', $request->post()->get('sno'));
$this->setData('isMessageApprover', $isMessageApprover);
$this->setData('isEditorMessageApprover', $messageOperatorRoleService->isApprover((int)$session->get('manager.sno')));
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
$this->setData('scmFl', $scmFl);
$this->setData('scmNo', $scmNo);
$this->setData('viewMemuDepth', $viewMemuDepth);
 
2026-10-26 · SMS 발송 보안 강화
Admin/crm/auto_send_config/auto_send_config_contents.php+2
변경 전auto_send_config_contents.php
<?php include __DIR__ . '/auto_send_config_recipient_checkboxes.php'; ?>
 
 
 
<?php foreach ($autoSendConfigsResponse as $recipient => $autoSendConfig): ?>
<?php include __DIR__ . '/auto_send_config_recipient_section.php'; ?>
<?php endforeach; ?>
 
변경 후auto_send_config_contents.php
<?php include __DIR__ . '/auto_send_config_recipient_checkboxes.php'; ?>
 
<script type="application/json" class="sms-templates-data"><?= json_encode($smsTemplates, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?></script>
 
<?php foreach ($autoSendConfigsResponse as $recipient => $autoSendConfig): ?>
<?php include __DIR__ . '/auto_send_config_recipient_section.php'; ?>
<?php endforeach; ?>
 
Admin/crm/auto_send_config/auto_send_config_preview.php+3-3
변경 전auto_send_config_preview.php
<div class="swiper swiper-initialized swiper-horizontal">
<div class="swiper-wrapper">
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button is-active" data-preview-tab="sms">SMS</button>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button" data-preview-tab="kakao">카카오 알림톡</button>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button" data-preview-tab="app">마이앱(앱푸시)</button>
</div>
</div>
</div>
 
변경 후auto_send_config_preview.php
<div class="swiper swiper-initialized swiper-horizontal">
<div class="swiper-wrapper">
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button is-active" data-preview-tab="kakao">카카오 알림톡</button>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button" data-preview-tab="app">마이앱(앱푸시)</button>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button" data-preview-tab="sms">SMS</button>
</div>
</div>
</div>
 
Admin/crm/auto_send_config/auto_send_config_recipient_section.php+77-29
변경 전auto_send_config_recipient_section.php
// MYAPP_PUSH는 member인 경우에만 표시
$filteredChannels = array_filter($channels, fn($channel) => $channel->name !== 'MYAPP_PUSH' || in_array($recipient, $myappSupportRecipients));
$isSingleChannel = count($filteredChannels) === 1;
foreach ($filteredChannels as $channel) {
if ($isSingleChannel) {
$isChecked = true;
$isDisabled = true;
</span>
<span class="ncua-checkbox-field__text"><?= $channel->getTitle() ?></span>
</label>
<?php } ?>
</div>
<div class="channel-hint" hidden>
<span class="ncua-hint-text destructive ncua-input__hint-text">발송수단을 선택해 주세요.</span>
<div class="ncua-horizontal-tab ncua-horizontal-tab--panel">
<div class="swiper swiper-initialized swiper-horizontal">
<div class="swiper-wrapper">
<?php foreach ($filteredChannels as $channel) { ?>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button is-active" data-tab-target="<?= $channel->name ?>"><?= $channel->getTitle() ?></button>
</div>
<?php } ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
 
<div class="ncua-card__body-block-section-wrap">
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs tooltip-align" data-tooltip-seq="009">내용 입력</div>
<?php /* 제목은 본문이 LMS(90bytes 초과)일 때만 활성 — 토글·카운트는 JS(initMessageModules)가 담당 */ ?>
<div class="message-title-field">
<div class="ncua-input ncua-input--xs is-disabled">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input name="recipients[<?= $recipient ?>][SMS][templateTitle]" type="text"
value="<?= htmlspecialchars($autoSendConfig['SMS']['templateTitle'] ?? '', ENT_QUOTES) ?>"
placeholder="제목을 입력해 주세요." data-sms-title-input disabled />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
</div>
</div>
</div>
<span class="message-title-count">
<span data-sms-title-current>0</span>/40
</span>
</div>
<div id="sms-message-input-container-<?= $recipient ?>"
data-initial-value="<?= htmlspecialchars($autoSendConfig['SMS']['templateContents'] ?? '', ENT_QUOTES) ?>">
</div>
</section>
<section class="ncua-card__body-block-section">
<div id="sms-chip-selector-container-<?= $recipient ?>"></div>
</section>
 
</div>
<div class="ncua-card__body-block-section-actions">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary" onclick="load_as_template('<?= $recipient ?>', 'SMS')">템플릿 불러오기</button>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray file-btn" onclick="save_as_template('<?= $recipient ?>', 'SMS')">템플릿으로 저장하기</button>
 
 
 
 
 
 
 
 
</div>
</div>
<?php endif; ?>
<?php if (!empty($autoSendConfig['KAKAO_ALRIM_TALK'])): ?>
<!-- 카카오 알림톡 -->
<div class="tab-content-item" data-tab="KAKAO_ALRIM_TALK" style="display: none;">
<script type="application/json" class="kakao-templates-data"><?= json_encode($kakaoTemplates, JSON_UNESCAPED_UNICODE) ?></script>
<?php if (!$kakaoCrmUseFlag): ?>
<div class="no-ready-send-type">
<p class="ncua-caution-text">
 
변경 후auto_send_config_recipient_section.php
// MYAPP_PUSH는 member인 경우에만 표시
$filteredChannels = array_filter($channels, fn($channel) => $channel->name !== 'MYAPP_PUSH' || in_array($recipient, $myappSupportRecipients));
$isSingleChannel = count($filteredChannels) === 1;
foreach ($filteredChannels as $channel):
if ($isSingleChannel) {
$isChecked = true;
$isDisabled = true;
</span>
<span class="ncua-checkbox-field__text"><?= $channel->getTitle() ?></span>
</label>
<?php endforeach; ?>
</div>
<div class="channel-hint" hidden>
<span class="ncua-hint-text destructive ncua-input__hint-text">발송수단을 선택해 주세요.</span>
<div class="ncua-horizontal-tab ncua-horizontal-tab--panel">
<div class="swiper swiper-initialized swiper-horizontal">
<div class="swiper-wrapper">
<?php foreach ($filteredChannels as $channel): ?>
<div class="swiper-slide ncua-horizontal-tab__item">
<button type="button" class="ncua-tab-button is-active" data-tab-target="<?= $channel->name ?>"><?= $channel->getTitle() ?></button>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
 
<?php
$smsSelection = $smsTemplateSelections[$recipient];
$smsCurrentCategory = $smsSelection['category'];
$smsCurrentType = $smsSelection['templateType'];
$smsSelectedSno = $smsSelection['selectedSno'];
$smsVisibleTemplates = $smsSelection['visibleTemplates'];
?>
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs tooltip-align" data-tooltip-seq="009">내용 입력</div>
<div class="ncua-border-content-layout" <?= $isSingleChannel ? 'data-sms-template-restrict-target' : '' ?>>
<div class="ncua-border-content">
<div class="ncua-border-content-title required">카테고리</div>
<div class="ncua-border-content-form ncua-flex ncua-flex-wrap ncua-flex-gap">
<?php foreach ($smsTemplateCategories as $smsCategory): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" class="sms-template-category-radio" name="recipients[<?= $recipient ?>][SMS][category]" value="<?= $smsCategory['value'] ?>" data-recipient="<?= $recipient ?>" <?= $smsCurrentCategory === $smsCategory['value'] ? 'checked' : '' ?> />
</span>
<span class="ncua-radio-field__text"><?= $smsCategory['name'] ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="ncua-border-content">
<div class="ncua-border-content-title required">템플릿 명</div>
<div class="ncua-flex ncua-flex-column ncua-gap-8">
<div class="ncua-border-content-form ncua-flex ncua-flex-gap">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" class="sms-template-type-radio" name="recipients[<?= $recipient ?>][SMS][templateType]" value="godomall" data-recipient="<?= $recipient ?>" <?= $smsCurrentType === 'godomall' ? 'checked' : '' ?> />
</span>
<span class="ncua-radio-field__text">고도몰 템플릿</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" class="sms-template-type-radio" name="recipients[<?= $recipient ?>][SMS][templateType]" value="user" data-recipient="<?= $recipient ?>" <?= $smsCurrentType === 'user' ? 'checked' : '' ?> />
</span>
<span class="ncua-radio-field__text">사용자 템플릿</span>
</label>
</div>
<div class="ncua-border-content-form ncua-flex ncua-gap-4">
<input type="hidden" class="sms-selected-template-godomall" data-recipient="<?= $recipient ?>" value="<?= $smsCurrentType === 'godomall' && $smsSelectedSno > 0 ? $smsSelectedSno : '' ?>" />
<input type="hidden" class="sms-selected-template-user" data-recipient="<?= $recipient ?>" value="<?= $smsCurrentType === 'user' && $smsSelectedSno > 0 ? $smsSelectedSno : '' ?>" />
<input type="hidden" class="sms-template-cleared" name="recipients[<?= $recipient ?>][SMS][templateCleared]" data-recipient="<?= $recipient ?>" value="n" />
<div class="ncua-select ncua-select--xs sms-template-select">
<div class="ncua-select__content">
<select class="ncua-select__tag sms-template-dropdown" name="recipients[<?= $recipient ?>][SMS][sourceTemplateSno]" data-recipient="<?= $recipient ?>">
<option value="">템플릿을 선택하세요</option>
<?php foreach ($smsVisibleTemplates as $smsTemplate): ?>
<option value="<?= $smsTemplate['sno'] ?>" <?= $smsTemplate['sno'] === $smsSelectedSno ? 'selected' : '' ?>><?= htmlspecialchars($smsTemplate['name'], ENT_QUOTES) ?></option>
<?php endforeach; ?>
<?php if ($smsVisibleTemplates === []): ?>
<option value="" disabled>사용할 수 있는 템플릿이 없습니다.</option>
<?php endif; ?>
</select>
</div>
</div>
</div>
</div>
 
 
 
 
 
 
</div>
</div>
</section>
 
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs sms-template-title">
<textarea class="ncua-input__textarea" rows="1" readonly><?= htmlspecialchars($smsSelection['selectedLmsTitle'], ENT_QUOTES) ?></textarea>
</div>
 
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs sms-template-preview">
<textarea class="ncua-input__textarea" readonly><?= htmlspecialchars($smsSelection['selectedContents'], ENT_QUOTES) ?></textarea>
</div>
 
<div class="ncua-accordion ncua-accordion--gray notice-accordion">
<div class="ncua-accordion__summary">SMS/LMS 발송 안내</div>
<ul class="ncua-accordion__content">
<li class="ncua-notice-info">승인된 템플릿만 자동 알림으로 발송할 수 있습니다.</li>
<li class="ncua-notice-info">SMS/LMS 템플릿 등록·관리·승인은 <a class="ncua-link" href="./message_template.php?sendMethod=sms" target="_blank">템플릿 관리</a>에서 가능합니다.</li>
</ul>
</div>
</div>
<?php endif; ?>
<?php if (!empty($autoSendConfig['KAKAO_ALRIM_TALK'])): ?>
<!-- 카카오 알림톡 -->
<div class="tab-content-item" data-tab="KAKAO_ALRIM_TALK" style="display: none;">
<script type="application/json" class="kakao-templates-data"><?= json_encode($kakaoTemplates, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?></script>
<?php if (!$kakaoCrmUseFlag): ?>
<div class="no-ready-send-type">
<p class="ncua-caution-text">
 
Admin/crm/auto_send_config/layer_auto_send_modify_kakao_template.php-384
변경 전layer_auto_send_modify_kakao_template.php
<div class="modal-dialog__content layer_update_kakao_template">
<div class="ncua-table ncua-table--vertical">
<table>
<tbody>
<tr>
<th class="ncua-required"><div>구분</div></th>
<td>
<div class="ncua-flex-gap">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">주문/배송</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">정기결제(배송)</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">선물하기</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">회원</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">게시판</span>
</label>
</div>
</td>
</tr>
<tr>
<th class="ncua-required"><div>템플릿 카테고리</div></th>
<td>
<div class="ncua-flex-gap">
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class="ncua-select__tag">
<option value="">대분류</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
</div>
</div>
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class="ncua-select__tag" disabled>
<option value="">중분류</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
</div>
</div>
</div>
</td>
</tr>
<tr>
<th class="ncua-required"><div>템플릿 제목</div></th>
<td>
<div>
<div class="ncua-input ncua-input--xs ">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs ncua-input-width-520">
<input maxlength="10" type="text" placeholder="제목을 입력하세요" class="kakao-template-title" data-charcount-key="kakaoTemplateTitle" /></div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoTemplateTitle">
<output class="ncua-input__field-text-count-current">0</output>
<span>/10</span>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">템플릿 제목을 입력해 주세요.</span>
</div>
</div>
</td>
</tr>
<tr>
<th><div>보안 템플릿 설정</div></th>
<td>
<div>
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" checked="" name="demo-switch" />
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="demo-switch" />
<span class="ncua-switch__label">사용안함</span>
</label>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
 
<div class="content-border-box">
<div class="template_type">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">메시지형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">강조표기형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">이미지형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">아이템리스트형</span>
</label>
</div>
 
<div class="template_content ncua-card__body-block-section-wrap">
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs">본문 입력</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea kakao-template-textarea-content" placeholder="템플릿 본문을 작성해 주세요." maxlength="1000"></textarea>
<div class="ncua-input__text-count-wrap">
<div class="ncua-hint-text">본문을 입력해 주세요.</div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/1000
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs" data-tooltip-seq="008">사용 가능한 변수</div>
<div class="chip-selector">
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class=" ncua-select__tag">
<option value="1">회원</option>
<option value="2">게시판</option>
<option value="3">통계</option>
</select>
</div>
</div>
<div class="chip-button-wrap">
<button class="chip-button"><span class="chip-button-text">{rc_mallNm} 쇼핑몰 명/상점명</span></button>
<button class="chip-button"><span class="chip-button-text">{memId} 회원 아이디</span></button>
<button class="chip-button"><span class="chip-button-text">{memNm} 회원명</span></button>
<button class="chip-button"><span class="chip-button-text">{sleepScheduleDt} 휴면회원 전환 예정 일</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_scheduleDt} 일반 회원 전환 일</span></button>
<button class="chip-button"><span class="chip-button-text">{smsAgreementDt} SMS 수신동의일</span></button>
<button class="chip-button"><span class="chip-button-text">{mailAgreementDt}메일 수신동의일</span></button>
<button class="chip-button"><span class="chip-button-text">{mileage} 보유한 마일리지</span></button>
<button class="chip-button"><span class="chip-button-text">{deposit}보유한 예치금</span></button>
<button class="chip-button"><span class="chip-button-text">{groupNm} 회원등급</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_mileage} 보유한 마일리지</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_deposit}보유한 예치금</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_certificationCode} 인증코드</span></button>
<button class="chip-button"><span class="chip-button-text">{wriNm} 작성자</span></button>
<button class="chip-button"><span class="chip-button-text">{orderNo} 주문번호</span></button>
<button class="chip-button"><span class="chip-button-text">{orderName} 주문명</span></button>
<button class="chip-button"><span class="chip-button-text">{settlePrice} 결제금액</span></button>
<button class="chip-button"><span class="chip-button-text">{bankAccount} 입금계좌번호</span></button>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title-wrap has-checkbox">
<span class="ncua-card__body-title--xs">부가 정보 문구</span>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" />
</span>
<span class="ncua-checkbox-field__text">추가</span>
</label>
</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea kakao-template-textarea-add-info" placeholder="템플릿 본문을 작성해 주세요." maxlength="200"></textarea>
<div class="ncua-input__text-count-wrap">
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<div class="ncua-hint-text destructive">본문을 입력해 주세요.</div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/200
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section channel_add_message">
<div class="ncua-card__body-title-wrap has-checkbox">
<span class="ncua-card__body-title--xs">채널 추가 문구(고정 내용)</span>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" />
</span>
<span class="ncua-checkbox-field__text">추가</span>
</label>
</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea" maxlength="36" readonly>채널 추가하고 이 채널의 광고와 마케팅을 카카오톡으로 받기</textarea>
<div class="ncua-input__text-count-wrap">
<div class="ncua-hint-text"></div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/36
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title-wrap">
<span class="ncua-card__body-title--xs">버튼</span>
<div>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray auto-send-icon auto-send-icon--plus">
<span class="ncua-btn__label">페이지 링크 추가</span>
</button>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray auto-send-icon auto-send-icon--plus">
<span class="ncua-btn__label">배송 조회 추가</span>
</button>
</div>
</div>
<div class="ncua-table ncua-table--horizontal">
<table>
<colgroup>
<col width="79px">
<col width="auto">
<col width="auto">
<col width="88px">
</colgroup>
<thead>
<tr>
<th><div class="ncua-align-center">타입</div></th>
<th class="ncua-required"><div>버튼명</div></th>
<th class="ncua-required"><div>URL</div></th>
<th><div class="ncua-align-center">관리</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="ncua-align-center">페이지 링크</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs ncua-input--full-width-with-count">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="20" type="text" placeholder="버튼명을 입력해주세요" class="kakao-button-page-link" data-charcount-key="kakaoButtonPageLink" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoButtonPageLink">
<output class="ncua-input__field-text-count-current">0</output>
<span>/20</span>
</div>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">버튼명을 입력해 주세요.</span>
</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input type="text" placeholder="버튼명을 입력해주세요" />
</div>
</div>
</div>
<span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span>
</div>
</div>
</td>
<td>
<div class="ncua-align-center">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray auto-send-icon auto-send-icon--minus">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="ncua-table ncua-table--horizontal">
<table>
<colgroup>
<col width="79px">
<col width="auto">
<col width="auto">
<col width="88px">
</colgroup>
<thead>
<tr>
<th><div class="ncua-align-center">타입</div></th>
<th class="ncua-required"><div>버튼명</div></th>
<th class="ncua-required"><div>URL</div></th>
<th><div class="ncua-align-center">관리</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="ncua-align-center">배송 조회</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs ncua-input--full-width-with-count">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="20" type="text" placeholder="버튼명을 입력해주세요" class="kakao-button-delivery-search" data-charcount-key="kakaoButtonDeliverySearch" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoButtonDeliverySearch">
<output class="ncua-input__field-text-count-current">0</output>
<span>/20</span>
</div>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">버튼명을 입력해 주세요.</span>
</div>
</td>
<td>
<div>
<p>본문에 택배사명과 송장번호가 포함되면 버튼이 카카오 배송조회 페이지로 연결됩니다. 단, 미지원 택배사는 조회되지 않습니다.</p>
</div>
</td>
<td>
<div class="ncua-align-center">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray minus-icon">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
<div class="modal-dialog__footer">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-layer-close">
<span class="ncua-btn__label">취소</span>
</button>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-layer-close">
<span class="ncua-btn__label">저장</span>
</button>
</div>
 
변경 후layer_auto_send_modify_kakao_template.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/crm/auto_send_config.php+222-123
변경 전auto_send_config.php
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/ncds-jquery-validator.js')?>"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/unsaved-changes-guard.js')?>"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/scroll-sticky.js')?>"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<!-- CRM Preview -->
<script src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/crm-preview/crm-preview-loader.js')?>" data-exclude="friendtalk"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/chip-selector/chip-selector.js')?>"></script>
<script type="text/javascript">
const AUTO_SEND_CODE = '<?= $code ?>';
const USE_JOIN_POLICY = <?= $useJoinPolicy ? 'true' : 'false' ?>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 뒤로가기 버튼 핸들러
// 카카오 알림톡 구분(카테고리) 변경 시 템플릿 드롭다운 업데이트
initKakaoCategoryChange();
 
 
 
// 스크롤 시 미리보기 섹션 전환 초기화
initPreviewScrollSync();
 
const recipient = section.dataset.targetType;
 
if (tabInfo.channel === 'SMS') {
const input = messageInputInstances[recipient]?.SMS;
const smsContent = input ? normalizeNewlines(input.getValue()) : '';
const smsTitleInput = section.querySelector('input[data-sms-title-input]');
const smsTitle = smsTitleInput && !smsTitleInput.disabled ? smsTitleInput.value.replace(/\{[^}]*\}/g, '') : '';
this.setPreview(section, { title: smsTitle, content: smsContent || '메시지 내용을 입력하세요.' });
 
 
} else if (tabInfo.channel === 'KAKAO_ALRIM_TALK') {
const tabContent = section.querySelector('[data-tab="KAKAO_ALRIM_TALK"]');
if (!tabContent) return;
messageInputInstances[recipient] = {};
chipSelectorInstances[recipient] = {};
 
// SMS MessageInput
const smsContainer = document.getElementById(`sms-message-input-container-${recipient}`);
if (smsContainer) {
const smsInitialValue = normalizeNewlines(smsContainer.dataset.initialValue || '');
const smsInput = GodoUIModule.render({
type: 'MessageInput',
target: `#sms-message-input-container-${recipient}`,
name: `recipients[${recipient}][SMS][templateContent]`,
maxLength: 2000,
useBytes: true,
hintText: 'SMS 건당 1포인트 차감',
initialValue: smsInitialValue,
allowEmoji: false,
onInput: (input) => {
const normalized = normalizeNewlines(input);
if (normalized !== input && smsInput.textarea) {
const pos = Math.max(0, smsInput.textarea.selectionStart - (input.length - normalized.length));
smsInput.textarea.value = normalized;
smsInput.textarea.setSelectionRange(pos, pos);
smsInput.updateCount();
}
PreviewFacade.setPreview(smsContainer.closest('[data-target-type]'), { content: normalized });
// SMS/LMS 포인트 힌트 업데이트
const byteLength = smsInput.calculateLength(normalized);
smsInput.setHint(byteLength > 90 ? 'LMS 건당 3포인트 차감' : 'SMS 건당 1포인트 차감');
// 본문 기준 SMS/LMS 판단 — LMS 일 때만 제목 입력 활성화
setSmsTitleEnabled(byteLength > 90);
// 빈 내용일 때 안내문구 숨김
if (normalized === '') {
const caution = document.querySelector(`#sms-chip-selector-container-${recipient} [data-caution-template]`);
if (caution) caution.style.display = 'none';
crmPreview.setContent('메시지 내용을 입력하세요.');
}
}
});
messageInputInstances[recipient].SMS = smsInput;
 
// ===== SMS/LMS 제목 — 본문이 LMS(90bytes 초과)일 때만 활성 (모바일 발송과 동일 규칙) =====
const SMS_TITLE_MAX_BYTES = 40;
const sectionEl = smsContainer.closest('[data-target-type]');
const smsTitleInput = sectionEl.querySelector('input[data-sms-title-input]');
const smsTitleCounter = sectionEl.querySelector('[data-sms-title-current]');
 
// 미리보기 — LMS 상태에서 값이 있을 때만 노출, 제목은 변수 미지원이라 치환자는 공란으로 표기
const updateSmsTitlePreview = () => {
if (!smsTitleInput) return;
const value = !smsTitleInput.disabled ? smsTitleInput.value.replace(/\{[^}]*\}/g, '') : '';
PreviewFacade.setPreview(sectionEl, { title: value });
};
 
// 제목 입력 — 이모지 제거, 40bytes 초과분 자동 삭제 (byte 기준은 본문 MessageInput 과 동일)
const handleSmsTitleInput = () => {
if (!smsTitleInput) return;
let value = smsInput.removeEmoji(smsTitleInput.value).value;
value = smsInput.truncateBytes(value, SMS_TITLE_MAX_BYTES);
if (smsTitleInput.value !== value) {
smsTitleInput.value = value;
}
if (smsTitleCounter) smsTitleCounter.textContent = smsInput.getByteLength(value);
updateSmsTitlePreview();
};
 
// SMS 로 돌아오면 값은 유지한 채 비활성
const setSmsTitleEnabled = (enabled) => {
if (!smsTitleInput) return;
smsTitleInput.disabled = !enabled;
smsTitleInput.closest('.ncua-input').classList.toggle('is-disabled', !enabled);
updateSmsTitlePreview();
};
 
if (smsTitleInput) {
smsTitleInput.addEventListener('input', handleSmsTitleInput);
// 초기 상태: 저장된 본문 byte 기준 활성 여부 + 저장된 제목 카운트 반영
setSmsTitleEnabled(smsInput.calculateLength(smsInitialValue) > 90);
handleSmsTitleInput();
}
 
// 초기 힌트 상태 설정
const hintEl = smsContainer.querySelector('[data-message-hint]');
if (hintEl) hintEl.classList.add('destructive');
if (smsInitialValue) {
const byteLength = smsInput.calculateLength(smsInitialValue);
smsInput.setHint(byteLength > 90 ? 'LMS 건당 3포인트 차감' : 'SMS 건당 1포인트 차감');
}
}
 
// SMS ChipSelector
const smsChipContainer = document.getElementById(`sms-chip-selector-container-${recipient}`);
if (smsChipContainer) {
chipSelectorInstances[recipient].SMS = GodoUIModule.render({
type: 'ChipSelector',
target: `#sms-chip-selector-container-${recipient}`,
title: '사용 가능한 변수',
tooltipSeq: '008',
cautionHTML: cautionHTML,
categories: channelChipSelectorCategories['SMS'] || [],
onSelect: (variable) => {
messageInputInstances[recipient]?.SMS?.insertText(variable);
}
});
}
 
// MYAPP MessageInput
const myappContainer = document.getElementById(`myapp-message-input-container-${recipient}`);
if (myappContainer) {
 
let isSubmitting = false;
 
const submitForm = (form) => {
if (isSubmitting) return;
 
isSubmitting = true;
 
const formData = new FormData(form);
if (!section) continue;
const smsCheckbox = section.querySelector('input[type="checkbox"][name$="[SMS][isEnabled]"]:checked');
if (!smsCheckbox) continue;
const input = messageInputInstances[recipient]?.SMS;
if (input && input.getValue().trim() === '') return section;
}
return null;
};
if (section.dataset[dataAttr] !== 'true') continue;
const channelCheckbox = section.querySelector(`input[type="checkbox"][name$="[${channel}][isEnabled]"]:checked`);
if (!channelCheckbox) continue;
const input = messageInputInstances[recipient]?.SMS;
if (input && input.getValue().trim() === '') return true;
}
}
return false;
if (!section) return;
 
const channel = tabButton.dataset.tabTarget;
 
 
// 모든 탭 숨기고 버튼 비활성화
section.querySelectorAll('.tab-content-item').forEach(item => item.style.display = 'none');
tabButtons.forEach((button) => {
button.addEventListener('click', (event) => {
const targetTab = event.currentTarget.getAttribute('data-preview-tab');
 
 
tabButtons.forEach(btn => btn.classList.remove('is-active'));
event.currentTarget.classList.add('is-active');
};
 
// 카카오 템플릿 드롭다운 업데이트 공통 함수
const updateKakaoTemplateDropdown = (tabContent, recipient, kakaoTemplates, selectedCategory, selectedType, shouldResetSelection = false) => {
const dropdown = tabContent.querySelector('.kakao-template-dropdown');
if (!dropdown) return;
 
return selectedType === 'godomall' ? isGodomall : !isGodomall;
});
 
const currentHiddenInput = tabContent.querySelector(`.kakao-selected-template-${selectedType}[data-recipient="${recipient}"]`);
const savedTemplateCode = shouldResetSelection ? '' : (currentHiddenInput ? currentHiddenInput.value : '');
 
filteredTemplates.forEach((template) => {
const selectedCategory = event.target.value;
const section = event.target.closest('[data-target-type]');
if (!section) return;
const recipient = section.dataset.targetType;
 
const tabContent = section.querySelector('[data-tab="KAKAO_ALRIM_TALK"]');
if (!tabContent) return;
}
 
// 구분 변경 시 템플릿 선택 초기화 (shouldResetSelection = true)
updateKakaoTemplateDropdown(tabContent, recipient, kakaoTemplates, selectedCategory, selectedType, true);
});
});
 
if (!dropdown) return;
 
// 직전 선택 템플릿 저장용 hidden input
const previousHiddenInput = tabContent.querySelector(`.kakao-selected-template-${previousType}[data-recipient="${recipient}"]`);
 
// 현재 선택된 템플릿을 이전 유형의 hidden input에 저장
if (previousHiddenInput && dropdown.value) {
}
 
// 템플릿 타입 변경 시에는 저장된 값 유지 시도
updateKakaoTemplateDropdown(tabContent, recipient, kakaoTemplates, selectedCategory, selectedType, false);
});
});
 
const currentType = checkedRadio ? checkedRadio.value : 'godomall';
 
// 현재 유형의 hidden input에 선택값 저장
const currentHiddenInput = tabContent.querySelector(`.kakao-selected-template-${currentType}[data-recipient="${recipient}"]`);
if (currentHiddenInput) {
currentHiddenInput.value = selectedCode;
}
});
};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
let isLoadingTemplate = false;
 
const load_as_template = (recipient, channel) => {
}
 
// 템플릿 불러오기 후 안내문구 표시
const containerSelector = channel === 'SMS'
? `#sms-chip-selector-container-${recipient}`
: `#myapp-chip-selector-container-${recipient}`;
const cautionEl = document.querySelector(`${containerSelector} [data-caution-template]`);
if (cautionEl) cautionEl.style.display = '';
};
 
 
변경 후auto_send_config.php
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/ncds-jquery-validator.js')?>"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/unsaved-changes-guard.js')?>"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/scroll-sticky.js')?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<script type="text/javascript">
$(function () {
SmsSendGate.useNcdsDialog();
SmsSendGate.applyRestrictOnAjax('<?= gd_isset($smsSendRestrictType, '') ?>', {
tooltip: '<?= gd_isset($smsSendRestrictTooltip, '') ?>',
selectors: ['input[type="checkbox"][name$="[SMS][isEnabled]"]']
});
SmsSendGate.bindClickGate({
message: '<?= gd_isset($smsSendRestrictAlertMessage, '') ?>',
subMessage: '<?= gd_isset($smsSendRestrictAlertSubMessage, '') ?>',
selectors: ['[data-sms-template-restrict-target]']
});
});
</script>
<!-- CRM Preview -->
<script src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/crm-preview/crm-preview-loader.js')?>" data-exclude="friendtalk"></script>
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/chip-selector/chip-selector.js')?>"></script>
<script type="text/javascript">
const AUTO_SEND_CODE = '<?= $code ?>';
const USE_JOIN_POLICY = <?= $useJoinPolicy ? 'true' : 'false' ?>;
const IS_SMS_TAB_ACCESSIBLE = <?= json_encode($isSmsTabAccessible) ?>;
const SMS_NO_ROLE_MESSAGE = '<?= gd_htmlspecialchars_stripslashes($smsNoRoleMessage) ?>';
const SMS_NO_ROLE_SUB_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsNoRoleSubMessage, '')) ?>';
 
const SMS_RESTRICT_ALERT_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertMessage, '')) ?>';
const SMS_RESTRICT_ALERT_SUB_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertSubMessage, '')) ?>';
 
// 탭은 노출 유지가 스펙이라 렌더 차단 대신 클릭 진입점에서 막는다
const blockSmsTabWithoutRole = () => {
if (SMS_RESTRICT_ALERT_MESSAGE) {
NCDSAlert({message: SMS_RESTRICT_ALERT_MESSAGE, subMessage: SMS_RESTRICT_ALERT_SUB_MESSAGE, iconType: 'error'});
return true;
}
if (IS_SMS_TAB_ACCESSIBLE) return false;
NCDSAlert({message: SMS_NO_ROLE_MESSAGE, subMessage: SMS_NO_ROLE_SUB_MESSAGE, iconType: 'error'});
return true;
};
 
/**
* 뒤로가기 버튼 핸들러
// 카카오 알림톡 구분(카테고리) 변경 시 템플릿 드롭다운 업데이트
initKakaoCategoryChange();
 
initSmsCategoryChange();
 
// 스크롤 시 미리보기 섹션 전환 초기화
initPreviewScrollSync();
 
const recipient = section.dataset.targetType;
 
if (tabInfo.channel === 'SMS') {
const tabContent = section.querySelector('[data-tab="SMS"]');
if (!tabContent) return;
 
const dropdown = tabContent.querySelector('.sms-template-dropdown');
const template = findSmsTemplateBySno(tabContent, dropdown?.value);
const smsContent = template ? normalizeNewlines(template.contents) : '';
this.setPreview(section, { title: getSmsLmsTitle(template), content: smsContent || '메시지 내용을 입력하세요.' });
} else if (tabInfo.channel === 'KAKAO_ALRIM_TALK') {
const tabContent = section.querySelector('[data-tab="KAKAO_ALRIM_TALK"]');
if (!tabContent) return;
messageInputInstances[recipient] = {};
chipSelectorInstances[recipient] = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// MYAPP MessageInput
const myappContainer = document.getElementById(`myapp-message-input-container-${recipient}`);
if (myappContainer) {
 
let isSubmitting = false;
 
const submitForm = async (form) => {
if (isSubmitting) return;
 
isSubmitting = true;
 
const formData = new FormData(form);
if (!section) continue;
const smsCheckbox = section.querySelector('input[type="checkbox"][name$="[SMS][isEnabled]"]:checked');
if (!smsCheckbox) continue;
const dropdown = section.querySelector('.sms-template-dropdown');
if (dropdown && dropdown.value === '') return section;
}
return null;
};
if (section.dataset[dataAttr] !== 'true') continue;
const channelCheckbox = section.querySelector(`input[type="checkbox"][name$="[${channel}][isEnabled]"]:checked`);
if (!channelCheckbox) continue;
const dropdown = section.querySelector('.sms-template-dropdown');
if (dropdown && dropdown.value === '') return true;
}
}
return false;
if (!section) return;
 
const channel = tabButton.dataset.tabTarget;
if (channel === 'SMS' && blockSmsTabWithoutRole()) return;
 
// 모든 탭 숨기고 버튼 비활성화
section.querySelectorAll('.tab-content-item').forEach(item => item.style.display = 'none');
tabButtons.forEach((button) => {
button.addEventListener('click', (event) => {
const targetTab = event.currentTarget.getAttribute('data-preview-tab');
if (targetTab === 'sms' && blockSmsTabWithoutRole()) return;
 
tabButtons.forEach(btn => btn.classList.remove('is-active'));
event.currentTarget.classList.add('is-active');
};
 
// 카카오 템플릿 드롭다운 업데이트 공통 함수
const updateKakaoTemplateDropdown = (tabContent, kakaoTemplates, selectedCategory, selectedType, shouldResetSelection = false) => {
const dropdown = tabContent.querySelector('.kakao-template-dropdown');
if (!dropdown) return;
 
return selectedType === 'godomall' ? isGodomall : !isGodomall;
});
 
const currentHiddenInput = tabContent.querySelector(`.kakao-selected-template-${selectedType}`);
const savedTemplateCode = shouldResetSelection ? '' : (currentHiddenInput ? currentHiddenInput.value : '');
 
filteredTemplates.forEach((template) => {
const selectedCategory = event.target.value;
const section = event.target.closest('[data-target-type]');
if (!section) return;
 
 
const tabContent = section.querySelector('[data-tab="KAKAO_ALRIM_TALK"]');
if (!tabContent) return;
}
 
// 구분 변경 시 템플릿 선택 초기화 (shouldResetSelection = true)
updateKakaoTemplateDropdown(tabContent, kakaoTemplates, selectedCategory, selectedType, true);
});
});
 
if (!dropdown) return;
 
// 직전 선택 템플릿 저장용 hidden input
const previousHiddenInput = tabContent.querySelector(`.kakao-selected-template-${previousType}`);
 
// 현재 선택된 템플릿을 이전 유형의 hidden input에 저장
if (previousHiddenInput && dropdown.value) {
}
 
// 템플릿 타입 변경 시에는 저장된 값 유지 시도
updateKakaoTemplateDropdown(tabContent, kakaoTemplates, selectedCategory, selectedType, false);
});
});
 
const currentType = checkedRadio ? checkedRadio.value : 'godomall';
 
// 현재 유형의 hidden input에 선택값 저장
const currentHiddenInput = tabContent.querySelector(`.kakao-selected-template-${currentType}`);
if (currentHiddenInput) {
currentHiddenInput.value = selectedCode;
}
});
};
 
let smsTemplatesCache;
const parseSmsTemplates = () => {
if (smsTemplatesCache !== undefined) return smsTemplatesCache;
 
const dataScript = document.querySelector('.sms-templates-data');
if (!dataScript) {
smsTemplatesCache = null;
return smsTemplatesCache;
}
 
try {
smsTemplatesCache = JSON.parse(dataScript.textContent);
} catch (e) {
console.error('SMS 템플릿 데이터 파싱 오류:', e);
smsTemplatesCache = null;
}
 
return smsTemplatesCache;
};
 
const getSmsLmsTitle = (template) => template?.lmsTitle || '';
 
const setSmsTemplatePreview = (tabContent, template) => {
const lmsTitle = getSmsLmsTitle(template);
const titleTextarea = tabContent.querySelector('.sms-template-title textarea');
if (titleTextarea) titleTextarea.value = lmsTitle;
 
const textarea = tabContent.querySelector('.sms-template-preview textarea');
const contents = template ? normalizeNewlines(template.contents) : '';
if (textarea) textarea.value = contents;
 
PreviewFacade.setPreview(tabContent.closest('[data-target-type]'), {
title: lmsTitle,
content: contents || '메시지 내용을 입력하세요.'
});
};
 
const getSmsSelectedType = (tabContent) => {
const checked = tabContent.querySelector('.sms-template-type-radio:checked');
return checked ? checked.value : 'godomall';
};
 
const getSmsSelectedCategory = (tabContent) => {
const checked = tabContent.querySelector('.sms-template-category-radio:checked');
return checked ? checked.value : '';
};
 
const findSmsTemplateBySno = (tabContent, sno) => {
const smsTemplates = parseSmsTemplates();
if (!smsTemplates || !sno) return null;
 
for (const templates of Object.values(smsTemplates)) {
const found = templates.find(t => String(t.sno) === String(sno));
if (found) return found;
}
return null;
};
 
const setSmsTemplateCleared = (tabContent, value) => {
const clearedInput = tabContent.querySelector('.sms-template-cleared');
if (clearedInput) {
clearedInput.value = value;
}
};
 
const updateSmsTemplateDropdown = (tabContent, selectedCategory, selectedType, shouldResetSelection = false) => {
const dropdown = tabContent.querySelector('.sms-template-dropdown');
const smsTemplates = parseSmsTemplates();
if (!dropdown || !smsTemplates) return;
 
dropdown.innerHTML = '<option value="">템플릿을 선택하세요</option>';
 
const filteredTemplates = (smsTemplates[selectedCategory] || [])
.filter(template => (selectedType === 'godomall') === Boolean(template.isProvided));
 
const hiddenInput = tabContent.querySelector(`.sms-selected-template-${selectedType}`);
const initialSno = hiddenInput?.dataset.initialSno || '';
const restoredSno = shouldResetSelection && initialSno && filteredTemplates.some(template => String(template.sno) === initialSno)
? initialSno
: '';
const savedSno = shouldResetSelection ? restoredSno : (hiddenInput ? hiddenInput.value : '');
 
filteredTemplates.forEach(template => {
const option = document.createElement('option');
option.value = template.sno;
option.textContent = template.name;
if (savedSno && String(template.sno) === String(savedSno)) {
option.selected = true;
}
dropdown.appendChild(option);
});
 
if (filteredTemplates.length === 0) {
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.disabled = true;
emptyOption.textContent = '사용할 수 있는 템플릿이 없습니다.';
dropdown.appendChild(emptyOption);
}
 
if (shouldResetSelection) {
if (hiddenInput) {
hiddenInput.value = savedSno;
}
setSmsTemplateCleared(tabContent, savedSno ? 'n' : 'y');
}
 
const selectedTemplate = savedSno
? filteredTemplates.find(template => String(template.sno) === String(savedSno))
: null;
setSmsTemplatePreview(tabContent, selectedTemplate || null);
};
 
const initSmsCategoryChange = () => {
document.querySelectorAll('.sms-selected-template-godomall, .sms-selected-template-user').forEach(hiddenInput => {
hiddenInput.dataset.initialSno = hiddenInput.value;
});
 
document.querySelectorAll('.sms-template-category-radio').forEach(radio => {
radio.addEventListener('change', event => {
const recipient = event.target.dataset.recipient;
const tabContent = document.querySelector(`[data-target-type="${recipient}"] [data-tab="SMS"]`);
if (!tabContent) return;
 
updateSmsTemplateDropdown(tabContent, event.target.value, getSmsSelectedType(tabContent), true);
});
});
 
document.querySelectorAll('.sms-template-type-radio').forEach(radio => {
radio.addEventListener('change', event => {
const selectedType = event.target.value;
const recipient = event.target.dataset.recipient;
const tabContent = document.querySelector(`[data-target-type="${recipient}"] [data-tab="SMS"]`);
if (!tabContent) return;
 
const dropdown = tabContent.querySelector('.sms-template-dropdown');
const previousType = selectedType === 'godomall' ? 'user' : 'godomall';
const previousHiddenInput = tabContent.querySelector(`.sms-selected-template-${previousType}`);
if (previousHiddenInput && dropdown?.value) {
previousHiddenInput.value = dropdown.value;
}
 
updateSmsTemplateDropdown(tabContent, getSmsSelectedCategory(tabContent), selectedType, false);
setSmsTemplateCleared(tabContent, dropdown?.value ? 'n' : 'y');
});
});
 
document.querySelectorAll('.sms-template-dropdown').forEach(dropdown => {
dropdown.addEventListener('change', event => {
const recipient = event.target.dataset.recipient;
const tabContent = document.querySelector(`[data-target-type="${recipient}"] [data-tab="SMS"]`);
if (!tabContent) return;
 
const selectedType = getSmsSelectedType(tabContent);
const hiddenInput = tabContent.querySelector(`.sms-selected-template-${selectedType}`);
if (hiddenInput) {
hiddenInput.value = event.target.value;
hiddenInput.dataset.initialSno = event.target.value;
}
 
setSmsTemplateCleared(tabContent, event.target.value ? 'n' : 'y');
setSmsTemplatePreview(tabContent, findSmsTemplateBySno(tabContent, event.target.value));
});
});
};
 
let isLoadingTemplate = false;
 
const load_as_template = (recipient, channel) => {
}
 
// 템플릿 불러오기 후 안내문구 표시
const cautionEl = document.querySelector(`#myapp-chip-selector-container-${recipient} [data-caution-template]`);
 
 
 
if (cautionEl) cautionEl.style.display = '';
};
 
 
Admin/crm/auto_send.php+20
변경 전auto_send.php
const useJoinPolicy = <?= $useJoinPolicy ? 'true' : 'false' ?>;
const MEMBER_JOIN_APPROVE_CODE = 'APPROVAL';
const FORCE_SEND_CODE = 'PRESENT';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
document.addEventListener('DOMContentLoaded', () => {
onClickAutoSendSearch();
// 저장 버튼 클릭
document.querySelector('.auto-send-save-button').addEventListener('click', function (e) {
e.preventDefault();
 
const changedCodes = getChangedAutoSendCodes();
const loadingModal = window.spinnerModal({ message: '저장 중...' });
 
updateInitialValues(changedCodes);
window.unsavedGuard?.reset();
NCDSToast({message: '저장되었습니다.', color: 'success'});
 
 
} else {
NCDSToast({message: '일시적인 오류가 발생하였습니다. </br>다시 시도해 주세요.', color: 'error'});
}
 
변경 후auto_send.php
const useJoinPolicy = <?= $useJoinPolicy ? 'true' : 'false' ?>;
const MEMBER_JOIN_APPROVE_CODE = 'APPROVAL';
const FORCE_SEND_CODE = 'PRESENT';
const IS_SMS_SEND_ACCESSIBLE = <?= json_encode($isSmsSendAccessible) ?>;
const SMS_NO_ROLE_MESSAGE = '<?= gd_htmlspecialchars_stripslashes($smsNoRoleMessage) ?>';
const SMS_NO_ROLE_SUB_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsNoRoleSubMessage, '')) ?>';
 
const SMS_RESTRICT_ALERT_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertMessage, '')) ?>';
const SMS_RESTRICT_ALERT_SUB_MESSAGE = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertSubMessage, '')) ?>';
 
// 저장 버튼은 노출 유지가 스펙이라 렌더 차단 대신 클릭 진입점에서 막는다
const blockSaveWithoutSmsRole = () => {
if (SMS_RESTRICT_ALERT_MESSAGE) {
NCDSAlert({message: SMS_RESTRICT_ALERT_MESSAGE, subMessage: SMS_RESTRICT_ALERT_SUB_MESSAGE, iconType: 'error'});
return true;
}
if (IS_SMS_SEND_ACCESSIBLE) return false;
NCDSAlert({message: SMS_NO_ROLE_MESSAGE, subMessage: SMS_NO_ROLE_SUB_MESSAGE, iconType: 'error'});
return true;
};
 
document.addEventListener('DOMContentLoaded', () => {
onClickAutoSendSearch();
// 저장 버튼 클릭
document.querySelector('.auto-send-save-button').addEventListener('click', function (e) {
e.preventDefault();
if (blockSaveWithoutSmsRole()) return;
const changedCodes = getChangedAutoSendCodes();
const loadingModal = window.spinnerModal({ message: '저장 중...' });
 
updateInitialValues(changedCodes);
window.unsavedGuard?.reset();
NCDSToast({message: '저장되었습니다.', color: 'success'});
} else if (data.restricted) {
NCDSAlert({message: data.message, subMessage: data.subMessage || '', iconType: 'error'});
} else {
NCDSToast({message: '일시적인 오류가 발생하였습니다. </br>다시 시도해 주세요.', color: 'error'});
}
 
Admin/crm/crm_recipe_send.php+25-34
변경 전crm_recipe_send.php
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/jquery/jquery.multi_select_box.js') ?>"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/mobile_message_replace_code.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/message_send_common.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_status.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-checkbox-group/ncds-checkbox-group.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-datepicker-factory/ncds-datepicker-factory.js') ?>"></script>
};
}
 
function validateHasMessagePoint() {
return messagePoint > 0;
}
 
function validateMessagePoint() {
const sendMethod = document.querySelector('input[name="sendMethod"]:checked')?.value ?? 'SMS';
const expectCount = getExpectTargetCount();
let requiredPoint;
switch (sendMethod) {
case 'SMS':
const messageLength = window.messageInput?.calculateLength(window.messageInput.getValue()) ?? 0;
requiredPoint = messageLength > 90 ? lmsPointEach * expectCount : smsPointEach * expectCount;
break;
case 'ALIMTALK':
requiredPoint = kakaoAlrimtalkPointEach * expectCount;
break;
case 'FRIENDTALK':
requiredPoint = kakaoFriendtalkPointEach * expectCount;
break;
default:
requiredPoint = 1;
}
return messagePoint >= requiredPoint;
}
 
function generateSendSchedulePayload() {
const sendCycle = document.querySelector('#sendCycle').value;
const [repeatStartDate, repeatEndDate] = [...document.querySelectorAll('input[name="repeatDate[]"]')].map(el => el.value);
$button.prop('disabled', false);
};
 
if (!messageConfig.hasCallNumber) {
return fail('발신번호가 없어 발송할 수 없습니다.', '설정에서 발신번호 등록 후 발송 시도를 해주세요.');
}
if (!validateHasMessagePoint()) {
return fail('메시지 포인트를 충전해 주세요');
}
if (!validateMessagePoint()) {
return fail('포인트가 부족하여 발송할 수 없습니다.', '발송 대상 인원보다 포인트가 적어 발송이 불가합니다. <br /> 충전 후 다시 시도해주세요.');
 
 
 
}
const result = validateSendMethodContent();
if (!result.success) {
}
 
// 대체 메시지 미작성 시 본문 치환코드 변환해 자동 세팅
const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value;
const mainContents = window.messageInput?.getValue() ?? '';
if (enableAlternative(sendMethod) && (window.alternativeMessageInput?.getValue() ?? '') === '') {
const allKeys = [
 
변경 후crm_recipe_send.php
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/jquery/jquery.multi_select_box.js') ?>"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/mobile_message_replace_code.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/message_send_common.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<script type="text/javascript">
$(function () {
SmsSendGate.useNcdsDialog();
SmsSendGate.applyRestrictOnAjax('<?= gd_isset($smsSendRestrictType, '') ?>', {
tooltip: '<?= gd_isset($smsSendRestrictTooltip, '') ?>'
});
SmsSendGate.bindClickGate({
message: '<?= gd_isset($smsSendRestrictAlertMessage, '') ?>',
subMessage: '<?= gd_isset($smsSendRestrictAlertSubMessage, '') ?>',
selectors: ['[data-send-tab-type="ALTERNATIVE"]', '[data-preview-message-type="ALTERNATIVE"]']
});
});
</script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_status.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-checkbox-group/ncds-checkbox-group.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-datepicker-factory/ncds-datepicker-factory.js') ?>"></script>
};
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
function generateSendSchedulePayload() {
const sendCycle = document.querySelector('#sendCycle').value;
const [repeatStartDate, repeatEndDate] = [...document.querySelectorAll('input[name="repeatDate[]"]')].map(el => el.value);
$button.prop('disabled', false);
};
 
const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value;
const smsIncluded = sendMethod === 'SMS' || enableAlternative(sendMethod);
 
try {
await SmsSendGate.ensure({
requiredPoint: getRequiredPoint(),
smsIncluded
});
} catch (e) {
$button.prop('disabled', false);
return;
}
const result = validateSendMethodContent();
if (!result.success) {
}
 
// 대체 메시지 미작성 시 본문 치환코드 변환해 자동 세팅
 
const mainContents = window.messageInput?.getValue() ?? '';
if (enableAlternative(sendMethod) && (window.alternativeMessageInput?.getValue() ?? '') === '') {
const allKeys = [
 
Admin/crm/layer_message_template_approval_history.php+36
변경 전layer_message_template_approval_history.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_message_template_approval_history.php
<div class="modal-dialog__content">
<div class="ncua-table ncua-table--horizontal">
<table>
<colgroup>
<col width="60px" />
<col width="180px" />
<col />
<col />
</colgroup>
<thead>
<tr>
<th><div class="ncua-align-center">번호</div></th>
<th><div class="ncua-align-center">처리 일시</div></th>
<th><div class="ncua-align-center">처리 내용</div></th>
<th><div class="ncua-align-center">처리자</div></th>
</tr>
</thead>
<tbody>
<?php if (empty($histories)): ?>
<tr>
<td colspan="4" height="50" class="no-data"><div>조회된 승인 이력이 없습니다.</div></td>
</tr>
<?php else: ?>
<?php foreach ($histories as $history): ?>
<tr>
<td><div class="ncua-align-center"><?= gd_htmlspecialchars($history['number']) ?></div></td>
<td><div class="ncua-align-center"><?= gd_htmlspecialchars($history['processedAt']) ?></div></td>
<td><div><?= gd_htmlspecialchars($history['eventName']) ?></div></td>
<td><div><?= gd_htmlspecialchars($history['processedBy']) ?></div></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
 
Admin/crm/layer_mobile_message_performance_detail.php-6
변경 전layer_mobile_message_performance_detail.php
<div>
발송 성과 상세 contents
</div>
 
<script type="text/javascript">
</script>
 
변경 후layer_mobile_message_performance_detail.php
 
 
 
 
 
 
 
Admin/crm/layer_mobile_message_performance_summary.php-6
변경 전layer_mobile_message_performance_summary.php
<div>
항목별 집계 기준 contents
</div>
 
<script type="text/javascript">
</script>
 
변경 후layer_mobile_message_performance_summary.php
 
 
 
 
 
 
 
Admin/crm/layer_save_as_template.php-60
변경 전layer_save_as_template.php
<script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/charcount.js')?>"></script>
<link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/layer-save-as-template.css')?>">
 
<div class="modal-dialog__content layer-save-as-template">
<div class="ncua-table ncua-table--vertical">
<table>
<tbody>
<tr>
<th><div>카테고리</div></th>
<td>
<div>
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class="ncua-select__tag">
<option value="">카테고리 선택</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
</div>
</div>
</div>
</td>
</tr>
<tr>
<th><div>제목</div></th>
<td>
<div>
<div class="ncua-input ncua-input--xs ncua-input--full-width-with-count">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="10" type="text" placeholder="제목을 입력하세요" class="layer-template-title" data-charcount-key="layerTemplateTitle" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="layerTemplateTitle">
<output class="ncua-input__field-text-count-current">0</output>
<span>/10</span>
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-dialog__footer">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-layer-close">
<span class="ncua-btn__label">저장</span>
</button>
</div>
 
<script type="text/javascript">
const charCountManager = createCharCountManager({
targetClasses: ['ncua-title'],
});
charCountManager.init();
</script>
 
변경 후layer_save_as_template.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/crm/layer_update_kakao_template.php-384
변경 전layer_update_kakao_template.php
<div class="modal-dialog__content layer_update_kakao_template">
<div class="ncua-table ncua-table--vertical">
<table>
<tbody>
<tr>
<th class="ncua-required"><div>구분</div></th>
<td>
<div class="ncua-flex-gap">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">주문/배송</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">정기결제(배송)</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">선물하기</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">회원</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoType" />
</span>
<span class="ncua-radio-field__text">게시판</span>
</label>
</div>
</td>
</tr>
<tr>
<th class="ncua-required"><div>템플릿 카테고리</div></th>
<td>
<div class="ncua-flex-gap">
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class="ncua-select__tag">
<option value="">대분류</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
</div>
</div>
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class="ncua-select__tag" disabled>
<option value="">중분류</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
</div>
</div>
</div>
</td>
</tr>
<tr>
<th class="ncua-required"><div>템플릿 제목</div></th>
<td>
<div>
<div class="ncua-input ncua-input--xs ">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs ncua-input-width-520">
<input maxlength="10" type="text" placeholder="제목을 입력하세요" class="kakao-template-title" data-charcount-key="kakaoTemplateTitle" /></div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoTemplateTitle">
<output class="ncua-input__field-text-count-current">0</output>
<span>/10</span>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">템플릿 제목을 입력해 주세요.</span>
</div>
</div>
</td>
</tr>
<tr>
<th><div>보안 템플릿 설정</div></th>
<td>
<div>
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" checked="" name="demo-switch" />
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="demo-switch" />
<span class="ncua-switch__label">사용안함</span>
</label>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
 
<div class="content-border-box">
<div class="template_type">
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">메시지형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">강조표기형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">이미지형</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="templateType" />
</span>
<span class="ncua-radio-field__text">아이템리스트형</span>
</label>
</div>
 
<div class="template_content ncua-card__body-block-section-wrap">
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs">본문 입력</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea kakao-template-textarea-content" placeholder="템플릿 본문을 작성해 주세요." maxlength="1000"></textarea>
<div class="ncua-input__text-count-wrap">
<div class="ncua-hint-text">본문을 입력해 주세요.</div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/1000
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title--xs" data-tooltip-seq="008">사용 가능한 변수</div>
<div class="chip-selector">
<div class="ncua-select ncua-select--xs">
<div class="ncua-select__content">
<select class=" ncua-select__tag">
<option value="1">회원</option>
<option value="2">게시판</option>
<option value="3">통계</option>
</select>
</div>
</div>
<div class="chip-button-wrap">
<button class="chip-button"><span class="chip-button-text">{rc_mallNm} 쇼핑몰 명/상점명</span></button>
<button class="chip-button"><span class="chip-button-text">{memId} 회원 아이디</span></button>
<button class="chip-button"><span class="chip-button-text">{memNm} 회원명</span></button>
<button class="chip-button"><span class="chip-button-text">{sleepScheduleDt} 휴면회원 전환 예정 일</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_scheduleDt} 일반 회원 전환 일</span></button>
<button class="chip-button"><span class="chip-button-text">{smsAgreementDt} SMS 수신동의일</span></button>
<button class="chip-button"><span class="chip-button-text">{mailAgreementDt}메일 수신동의일</span></button>
<button class="chip-button"><span class="chip-button-text">{mileage} 보유한 마일리지</span></button>
<button class="chip-button"><span class="chip-button-text">{deposit}보유한 예치금</span></button>
<button class="chip-button"><span class="chip-button-text">{groupNm} 회원등급</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_mileage} 보유한 마일리지</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_deposit}보유한 예치금</span></button>
<button class="chip-button"><span class="chip-button-text">{rc_certificationCode} 인증코드</span></button>
<button class="chip-button"><span class="chip-button-text">{wriNm} 작성자</span></button>
<button class="chip-button"><span class="chip-button-text">{orderNo} 주문번호</span></button>
<button class="chip-button"><span class="chip-button-text">{orderName} 주문명</span></button>
<button class="chip-button"><span class="chip-button-text">{settlePrice} 결제금액</span></button>
<button class="chip-button"><span class="chip-button-text">{bankAccount} 입금계좌번호</span></button>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title-wrap has-checkbox">
<span class="ncua-card__body-title--xs">부가 정보 문구</span>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" />
</span>
<span class="ncua-checkbox-field__text">추가</span>
</label>
</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea kakao-template-textarea-add-info" placeholder="템플릿 본문을 작성해 주세요." maxlength="200"></textarea>
<div class="ncua-input__text-count-wrap">
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<div class="ncua-hint-text destructive">본문을 입력해 주세요.</div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/200
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section channel_add_message">
<div class="ncua-card__body-title-wrap has-checkbox">
<span class="ncua-card__body-title--xs">채널 추가 문구(고정 내용)</span>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" />
</span>
<span class="ncua-checkbox-field__text">추가</span>
</label>
</div>
<div class="ncua-input ncua-input--textarea ncua-input--textarea--xs">
<textarea class="ncua-input__textarea" maxlength="36" readonly>채널 추가하고 이 채널의 광고와 마케팅을 카카오톡으로 받기</textarea>
<div class="ncua-input__text-count-wrap">
<div class="ncua-hint-text"></div>
<div class="ncua-input__text-count">
<span class="ncua-input__text-count-text">
<span class="ncua-input__text-count-text-count">0</span>
/36
</span>
</div>
</div>
</div>
</section>
 
<section class="ncua-card__body-block-section">
<div class="ncua-card__body-title-wrap">
<span class="ncua-card__body-title--xs">버튼</span>
<div>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray auto-send-icon auto-send-icon--plus">
<span class="ncua-btn__label">페이지 링크 추가</span>
</button>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray auto-send-icon auto-send-icon--plus">
<span class="ncua-btn__label">배송 조회 추가</span>
</button>
</div>
</div>
<div class="ncua-table ncua-table--horizontal">
<table>
<colgroup>
<col width="79px">
<col width="auto">
<col width="auto">
<col width="88px">
</colgroup>
<thead>
<tr>
<th><div class="ncua-align-center">타입</div></th>
<th class="ncua-required"><div>버튼명</div></th>
<th class="ncua-required"><div>URL</div></th>
<th><div class="ncua-align-center">관리</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="ncua-align-center">페이지 링크</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs ncua-input--full-width-with-count">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="20" type="text" placeholder="버튼명을 입력해주세요" class="kakao-button-page-link" data-charcount-key="kakaoButtonPageLink" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoButtonPageLink">
<output class="ncua-input__field-text-count-current">0</output>
<span>/20</span>
</div>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">버튼명을 입력해 주세요.</span>
</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input type="text" placeholder="버튼명을 입력해주세요" />
</div>
</div>
</div>
<span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span>
</div>
</div>
</td>
<td>
<div class="ncua-align-center">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray auto-send-icon auto-send-icon--minus">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="ncua-table ncua-table--horizontal">
<table>
<colgroup>
<col width="79px">
<col width="auto">
<col width="auto">
<col width="88px">
</colgroup>
<thead>
<tr>
<th><div class="ncua-align-center">타입</div></th>
<th class="ncua-required"><div>버튼명</div></th>
<th class="ncua-required"><div>URL</div></th>
<th><div class="ncua-align-center">관리</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="ncua-align-center">배송 조회</div>
</td>
<td>
<div>
<div class="ncua-input ncua-input--xs ncua-input--full-width-with-count">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="20" type="text" placeholder="버튼명을 입력해주세요" class="kakao-button-delivery-search" data-charcount-key="kakaoButtonDeliverySearch" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="kakaoButtonDeliverySearch">
<output class="ncua-input__field-text-count-current">0</output>
<span>/20</span>
</div>
</div>
</div>
<!-- NOTE [저장] 유효성 검사 : 내용을 입력하지 않은 경우 -->
<span class="ncua-hint-text destructive ncua-input__hint-text">버튼명을 입력해 주세요.</span>
</div>
</td>
<td>
<div>
<p>본문에 택배사명과 송장번호가 포함되면 버튼이 카카오 배송조회 페이지로 연결됩니다. 단, 미지원 택배사는 조회되지 않습니다.</p>
</div>
</td>
<td>
<div class="ncua-align-center">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray minus-icon">
<span class="ncua-btn__label">삭제</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
<div class="modal-dialog__footer">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-layer-close">
<span class="ncua-btn__label">취소</span>
</button>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-layer-close">
<span class="ncua-btn__label">저장</span>
</button>
</div>
 
변경 후layer_update_kakao_template.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/crm/message_config/layer_manage_sms_verification.php+2
변경 전layer_manage_sms_verification.php
if (validatorInstance) validatorInstance.showErrors({ smsPassword: '10~16자의 영대/소문자, 숫자, 특수문자 중 2가지 이상의 조합을 사용해 주세요.' });
}
}
 
 
});
}
 
 
변경 후layer_manage_sms_verification.php
if (validatorInstance) validatorInstance.showErrors({ smsPassword: '10~16자의 영대/소문자, 숫자, 특수문자 중 2가지 이상의 조합을 사용해 주세요.' });
}
}
}).fail(function (xhr) {
NCDSAlert({ message: xhr.responseJSON?.message || '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', subMessage: xhr.responseJSON?.subMessage || '', iconType: 'error' });
});
}
 
 
Admin/crm/message_config.php+28-15
변경 전message_config.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/switch.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/unsaved-changes-guard.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/ncds-jquery-validator.js') ?>"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
<article class="ncua-content message-config-wrapper">
<form id="frmMessageConfig" action="message_config_ps.php" method="post">
<input type="hidden" name="mode" value="save"/>
<td>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($smsAutoSendOverTypes as $smsAutoSendOverType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="smsAutoConfig[smsAutoSendOver]" value="<?= $smsAutoSendOverType->value ?>" <?= $smsAutoConfig->getSmsAutoSendOver() === $smsAutoSendOverType ? 'checked' : '' ?>/>
</span>
<td>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoFriendTalkConfig[smsAlternativeSendFlag]" <?= $kakaoFriendTalkConfig->getSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoFriendTalkConfig[smsAlternativeSendFlag]" <?= $kakaoFriendTalkConfig->getSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center kakao-alim-talk-manual-sms-alternative-send-setting <?= $kakaoAlimTalkConfig->getSender()->value === 'kakaoAlrim' ? 'display-none' : '' ?>">
모바일 메시지 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoAlimTalkConfig[manualSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getManualSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoAlimTalkConfig[manualSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getManualSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
자동 알림 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoAlimTalkConfig[autoSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoAlimTalkConfig[autoSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="017">대체발송 처리 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($autoSmsAlternativeSendTypes as $autoSmsAlternativeSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoAlimTalkConfig[autoSmsAlternativeSendType]" value="<?= $autoSmsAlternativeSendType->name ?>" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendType() === $autoSmsAlternativeSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="014">90 byte 초과 시 메시지 전송 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($failedMessageSendTypes as $failedMessageSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoAlimTalkConfig[failedMessageSendType]" value="<?= $failedMessageSendType->name ?>" <?= $kakaoAlimTalkConfig->getFailedMessageSendType() === $failedMessageSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
모바일 메시지 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="myappPushConfig[manualSmsAlternativeSendFlag]" <?= $myappPushConfig->getManualSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="myappPushConfig[manualSmsAlternativeSendFlag]" <?= $myappPushConfig->getManualSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
자동 알림 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active">
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="myappPushConfig[autoSmsAlternativeSendFlag]" <?= $myappPushConfig->getAutoSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive">
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="myappPushConfig[autoSmsAlternativeSendFlag]" <?= $myappPushConfig->getAutoSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="013">대체발송 처리 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($autoSmsAlternativeSendTypes as $autoSmsAlternativeSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="myappPushConfig[autoSmsAlternativeSendType]" value="<?= $autoSmsAlternativeSendType->name ?>" <?= $myappPushConfig->getAutoSmsAlternativeSendType() === $autoSmsAlternativeSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="014">90 byte 초과 시 메시지 전송 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($failedMessageSendTypes as $failedMessageSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="myappPushConfig[failedMessageSendType]" value="<?= $failedMessageSendType->name ?>" <?= $myappPushConfig->getFailedMessageSendType() == $failedMessageSendType ? 'checked' : '' ?>/>
</span>
 
변경 후message_config.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/switch.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/unsaved-changes-guard.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/ncds-jquery-validator.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<script type="text/javascript">
$(function () {
SmsSendGate.bindClickGate({
message: '<?= gd_isset($smsSendRestrictAlertMessage, '') ?>',
subMessage: '<?= gd_isset($smsSendRestrictAlertSubMessage, '') ?>',
selectors: <?= json_encode(array_values(array_filter([
'[data-sms-click-gate]',
gd_isset($smsCallNumberManageRestricted, false) ? '#manageCallNumber' : null,
]))) ?>
});
});
</script>
<article class="ncua-content message-config-wrapper">
<form id="frmMessageConfig" action="message_config_ps.php" method="post">
<input type="hidden" name="mode" value="save"/>
<td>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($smsAutoSendOverTypes as $smsAutoSendOverType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-sms-click-gate>
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="smsAutoConfig[smsAutoSendOver]" value="<?= $smsAutoSendOverType->value ?>" <?= $smsAutoConfig->getSmsAutoSendOver() === $smsAutoSendOverType ? 'checked' : '' ?>/>
</span>
<td>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoFriendTalkConfig[smsAlternativeSendFlag]" <?= $kakaoFriendTalkConfig->getSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoFriendTalkConfig[smsAlternativeSendFlag]" <?= $kakaoFriendTalkConfig->getSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center kakao-alim-talk-manual-sms-alternative-send-setting <?= $kakaoAlimTalkConfig->getSender()->value === 'kakaoAlrim' ? 'display-none' : '' ?>">
모바일 메시지 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoAlimTalkConfig[manualSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getManualSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoAlimTalkConfig[manualSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getManualSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
자동 알림 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="kakaoAlimTalkConfig[autoSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="kakaoAlimTalkConfig[autoSmsAlternativeSendFlag]" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="017">대체발송 처리 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($autoSmsAlternativeSendTypes as $autoSmsAlternativeSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-sms-click-gate>
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoAlimTalkConfig[autoSmsAlternativeSendType]" value="<?= $autoSmsAlternativeSendType->name ?>" <?= $kakaoAlimTalkConfig->getAutoSmsAlternativeSendType() === $autoSmsAlternativeSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="014">90 byte 초과 시 메시지 전송 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($failedMessageSendTypes as $failedMessageSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-sms-click-gate>
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="kakaoAlimTalkConfig[failedMessageSendType]" value="<?= $failedMessageSendType->name ?>" <?= $kakaoAlimTalkConfig->getFailedMessageSendType() === $failedMessageSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
모바일 메시지 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="myappPushConfig[manualSmsAlternativeSendFlag]" <?= $myappPushConfig->getManualSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="myappPushConfig[manualSmsAlternativeSendFlag]" <?= $myappPushConfig->getManualSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-gap-8 ncua-align-items-center">
자동 알림 발송
<div class="ncua-switch ncua-switch--xs">
<label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--active" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--left" type="radio" value="y" name="myappPushConfig[autoSmsAlternativeSendFlag]" <?= $myappPushConfig->getAutoSmsAlternativeSendFlag() === 'y' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--inactive" data-sms-click-gate>
<input class="ncua-switch__radio ncua-switch__radio--right" type="radio" value="n" name="myappPushConfig[autoSmsAlternativeSendFlag]" <?= $myappPushConfig->getAutoSmsAlternativeSendFlag() === 'n' ? 'checked' : '' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="013">대체발송 처리 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($autoSmsAlternativeSendTypes as $autoSmsAlternativeSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-sms-click-gate>
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="myappPushConfig[autoSmsAlternativeSendType]" value="<?= $autoSmsAlternativeSendType->name ?>" <?= $myappPushConfig->getAutoSmsAlternativeSendType() === $autoSmsAlternativeSendType ? 'checked' : '' ?>/>
</span>
<div class="ncua-flex ncua-align-items-center" data-tooltip-seq="014">90 byte 초과 시 메시지 전송 방법</div>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
<?php foreach ($failedMessageSendTypes as $failedMessageSendType): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-sms-click-gate>
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="myappPushConfig[failedMessageSendType]" value="<?= $failedMessageSendType->name ?>" <?= $myappPushConfig->getFailedMessageSendType() == $failedMessageSendType ? 'checked' : '' ?>/>
</span>
 
Admin/crm/message_template/message_template_result.php+55-44
변경 전message_template_result.php
<td><div>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" name="templateCodeList[]" value="<?= $result->getTemplateCode() ?>" <?= ( $result->getBasicFl() === 'y') ? 'disabled' : '' ?> />
</span>
</label>
</div></td>
<div class="ncua-search-result__actions">
<div class="ncua-search-result__actions-button">
<div class="ncua-search-result__actions-text">
 
 
 
 
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--destructive ncua-select-delete js-btn-delete">선택 삭제</button>
</div>
</div>
<col>
<col width="120px">
<col>
<col>
<?php if ($sendMethod === 'kakao') :?>
<col>
<col>
<col>
<?php endif; ?>
 
 
 
 
 
<col width="180px">
 
 
 
</colgroup>
<thead>
<tr>
</div>
</th>
<th rowspan="2"><div data-tooltip-seq="007">템플릿코드</div></th>
<th rowspan="2"><div data-tooltip-seq="008"><?= $sendMethod === 'kakao' ? '구분' : '카테고리' ?></div></th>
<th rowspan="2"><div data-tooltip-seq="009">템플릿명</div></th>
<?php if ($sendMethod === 'kakao') :?>
<th colspan="2"><div data-tooltip-seq="010">알림톡 검수</div></th>
<?php endif ?>
<th rowspan="2"><div data-tooltip-seq="011">등록일</div></th>
<?php if ($sendMethod === 'kakao') :?>
<th rowspan="2"><div data-tooltip-seq="012">사용 중인 자동 알림</div></th>
 
<?php endif ?>
<th rowspan="2"><div data-tooltip-seq="013">관리</div></th>
 
 
 
</tr>
<?php if ($sendMethod === 'kakao') :?>
<tr>
<th><div data-tooltip-seq="014">검수 상태</div></th>
<th><div data-tooltip-seq="015">검수 답변</div></th>
<?php endif ?>
</thead>
<tbody>
<?php if (
($sendMethod === 'kakao' && (
($search->getProvider() === 'bizm' && $config->getBizmFl() === 'n') ||
($search->getProvider() === 'cloud' && $config->getCloudFl() === 'n') ||
($config->getKakaoUseFl() === 'n')
)) ||
($sendMethod === 'myapp' && $config->getMyappUseFl() === 'n')
) :?>
<tr>
<td colspan="<?= ($sendMethod === 'kakao') ? '9' : '6' ?>" height="50" class="no-data">
<div class="message-config-info">이 발송수단은 현재 '사용 안함'으로 설정되어 있습니다.<br>템플릿 등록 및 메시지를 발송하려면 <a href="./message_config.php" class="ncua-link">메시지 설정</a>에서 사용을 활성화해 주세요.</div>
</td>
</tr>
<?php elseif (empty($results)):?>
<tr>
<td colspan="<?= ($sendMethod === 'kakao') ? '9' : '6' ?>" height="50" class="no-data"><div>검색된 정보가 없습니다.</div></td>
</tr>
<?php else: ?>
<?php foreach ($results as $result) : ?>
<tr>
<td><div>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<button type="button" class="ncua-left-align message-list-content-btn" onclick="check_template_contents('<?= $result->getTemplateCode() ?>', '<?= $sendMethod ?>', '<?= $search->getProvider() ?>')"><?= $result->getTemplateName() ?></button>
</div>
</td>
<?php if ($sendMethod === 'kakao') :?>
<td><div><?= $result->getTemplateStatus() ?></div></td>
<?php if ($result->getTemplateStatus() === '반려') :?>
<td><div><button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" onclick="check_kakao_template_comment('<?= $search->getProvider() ?>','<?= $result->getTemplateCode() ?>')">답변 확인</button></div></td>
<?php else: ?>
<td><div>-</div></td>
<?php endif; ?>
<?php endif; ?>
<td><div><?= date('Y. m. d', strtotime($result->getRegDt())) . ' ' . ((date('A', strtotime($result->getRegDt())) === 'AM' ? '오전' : '오후') . ' ' . date('h:i:s', strtotime($result->getRegDt()))) ?></div></td>
<?php if ($sendMethod === 'kakao') :?>
<?php if ($result->getAutoFl() === 'y') :?>
<td>
<div class="ncua-custom-tooltip">
<div data-tooltip-type="auto-notification-count">
<div class="ncua-left-align message-list-content-btn" ><?= $result->getAutoName() ?></div>
<div class="ncua-custom-tooltip__content">
<?php foreach ($result->getAutoList() as $auto): ?>
<p><?= $auto ?></p>
<?php endforeach; ?>
</div>
</div>
</div>
</td>
<?php else : ?>
<td><div><?= $result->getAutoName() ?></div></td>
<?php endif; ?>
<?php endif; ?>
<td><div class="ncua-flex-gap ncua-gap-4">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-btn-copy" value='<?= $result->getTemplateCode() ?>'>복제</button>
<?php
$canModify = $sendMethod !== 'kakao' || in_array($result->getTemplateStatus(), ['등록', '반려']);
?>
<?php if ($canModify): ?>
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-btn-modify" value='<?= $result->getTemplateCode() ?>'>수정</button>
<?php endif; ?>
</div></td>
 
 
 
 
 
 
 
</tr>
<?php endforeach ?>
<?php endif; ?>
 
변경 후message_template_result.php
<td><div>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
<input type="checkbox" name="templateCodeList[]" value="<?= $result->getTemplateCode() ?>" <?= $result->isSelectable() ? '' : 'disabled' ?> />
</span>
</label>
</div></td>
<div class="ncua-search-result__actions">
<div class="ncua-search-result__actions-button">
<div class="ncua-search-result__actions-text">
<?php if ($isSms): ?>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-btn-approve">선택 승인</button>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-btn-reject">선택 반려</button>
<?php endif; ?>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--destructive ncua-select-delete js-btn-delete">선택 삭제</button>
</div>
</div>
<col>
<col width="120px">
<col>
<?php if ($isKakao): ?>
 
 
<col>
<col>
<?php endif; ?>
<col>
<col>
<?php if ($isSms): ?>
<col width="100px">
<?php endif; ?>
<col width="180px">
<?php if ($isSms): ?>
<col width="120px">
<?php endif; ?>
</colgroup>
<thead>
<tr>
</div>
</th>
<th rowspan="2"><div data-tooltip-seq="007">템플릿코드</div></th>
<th rowspan="2"><div data-tooltip-seq="008"><?= $isKakao ? '구분' : '카테고리' ?></div></th>
<th rowspan="2"><div data-tooltip-seq="009">템플릿명</div></th>
<?php if ($isKakao): ?>
<th colspan="2"><div data-tooltip-seq="010">알림톡 검수</div></th>
<?php endif ?>
<th rowspan="2"><div data-tooltip-seq="011">등록일</div></th>
<th rowspan="2"><div data-tooltip-seq="012">사용 중인 자동 알림</div></th>
<?php if ($isSms): ?>
<th rowspan="2"><div data-tooltip-seq="014">검수 상태</div></th>
<?php endif ?>
<th rowspan="2"><div data-tooltip-seq="013">관리</div></th>
<?php if ($isSms): ?>
<th rowspan="2"><div data-tooltip-seq="016">승인 이력</div></th>
<?php endif ?>
</tr>
<?php if ($isKakao): ?>
<tr>
<th><div data-tooltip-seq="014">검수 상태</div></th>
<th><div data-tooltip-seq="015">검수 답변</div></th>
<?php endif ?>
</thead>
<tbody>
<?php $columnCount = 7 + ($isKakao ? 2 : 0) + ($isSms ? 2 : 0); ?>
<?php if ($sendMethodDisabled): ?>
 
 
 
 
 
 
<tr>
<td colspan="<?= $columnCount ?>" height="50" class="no-data">
<div class="message-config-info">이 발송수단은 현재 '사용 안함'으로 설정되어 있습니다.<br>템플릿 등록 및 메시지를 발송하려면 <a href="./message_config.php" class="ncua-link">메시지 설정</a>에서 사용을 활성화해 주세요.</div>
</td>
</tr>
<?php elseif (empty($results)): ?>
<tr>
<td colspan="<?= $columnCount ?>" height="50" class="no-data"><div>검색된 정보가 없습니다.</div></td>
</tr>
<?php else: ?>
<?php foreach ($results as $result): ?>
<tr>
<td><div>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<button type="button" class="ncua-left-align message-list-content-btn" onclick="check_template_contents('<?= $result->getTemplateCode() ?>', '<?= $sendMethod ?>', '<?= $search->getProvider() ?>')"><?= $result->getTemplateName() ?></button>
</div>
</td>
<?php if ($isKakao): ?>
<td><div><?= $result->getTemplateStatus() ?></div></td>
<?php if ($result->isRejected()): ?>
<td><div><button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" onclick="check_kakao_template_comment('<?= $search->getProvider() ?>','<?= $result->getTemplateCode() ?>')">답변 확인</button></div></td>
<?php else: ?>
<td><div>-</div></td>
<?php endif; ?>
<?php endif; ?>
<td><div><?= date('Y. m. d', strtotime($result->getRegDt())) . ' ' . ((date('A', strtotime($result->getRegDt())) === 'AM' ? '오전' : '오후') . ' ' . date('h:i:s', strtotime($result->getRegDt()))) ?></div></td>
<?php if ($result->getAutoFl() === 'y'): ?>
<td><div>
<button type="button" class="ncua-btn ncua-btn--xs has-underline ncua-btn--text-gray ncua-message-auto-alam">
<span class="ncua-btn__label"><?= $result->getAutoName() ?></span>
<span class="ncua-tooltip-panel ncua-tooltip__bg ncua-tooltip__bg--white ncua-tooltip__bg--bottom">
<?php foreach ($result->getAutoList() as $auto): ?>
<span class="ncua-tooltip__content"><?= $auto ?></span>
<?php endforeach; ?>
</span>
</button>
</div></td>
<?php else : ?>
<td><div><?= $result->getAutoName() ?></div></td>
<?php endif; ?>
<?php if ($isSms): ?>
<td><div><?= $result->getTemplateStatus() ?></div></td>
 
<?php endif; ?>
<td><div class="ncua-flex-gap ncua-gap-4">
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-btn-copy" value='<?= $result->getTemplateCode() ?>'>복제</button>
<?php if ($result->isModifiable()): ?>
 
 
 
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-btn-modify" value='<?= $result->getTemplateCode() ?>'>수정</button>
<?php endif; ?>
</div></td>
<?php if ($isSms): ?>
<td>
<div>
<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-btn-detail" value="<?= $result->getTemplateCode() ?>">상세</button>
</div>
</td>
<?php endif; ?>
</tr>
<?php endforeach ?>
<?php endif; ?>
 
Admin/crm/message_template/message_template_search.php+7-31
변경 전message_template_search.php
<col>
</colgroup>
<tbody>
<?php if ($sendMethod === 'kakao') : ?>
<tr>
<th>
<div data-tooltip-seq="001">제공사</div>
</span>
<span class="ncua-radio-field__text">고도몰</span>
</label>
<?php if ($isNewMall === false) : ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="provider"
</div>
</td>
</tr>
<?php
$categories = [
['value' => 'all', 'name' => '전체'],
];
if ($sendMethod === 'sms') {
$categories = array_merge($categories, $searchCategory->getSmsCategory());
} elseif ($sendMethod === 'myapp') {
$categories = array_merge($categories, $searchCategory->getMyappCategory());
} elseif ($search->getProvider() === 'cloud') {
$categories = array_merge($categories, $searchCategory->getCloudCategory());
} elseif ($search->getProvider() === 'bizm') {
$categories = array_merge($categories, $searchCategory->getBizmCategory());
}
?>
<tr>
<th>
<div data-tooltip-seq="003"><?= $sendMethod === 'kakao' ? '구분' : '카테고리' ?></div>
</th>
<td colspan="3">
<div class="ncua-flex-gap js-trigger-type">
<?php foreach ($categories as $cat): ?>
<?php
$checked = in_array($cat['value'], $search->getCategory()) ? 'checked' : '';
?>
</div>
</td>
</tr>
<?php if ($sendMethod === 'kakao') : ?>
<?php
$statusOptions = [
['value' => 'all', 'label' => '전체'],
];
if ($search->getProvider() === 'cloud') {
$statusOptions = array_merge($statusOptions, $searchStatus->getCloudStatus());
} elseif ($search->getProvider() === 'bizm') {
$statusOptions = array_merge($statusOptions, $searchStatus->getBizmStatus());
}
?>
<tr>
<th>
<div data-tooltip-seq="004">검수 상태</div>
</th>
<td>
<div class="ncua-flex-gap js-trigger-status">
<?php foreach ($statusOptions as $option): ?>
<?php if (!isset($option['provider']) || $search->getProvider() === $option['provider']): ?>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
</table>
</div>
<div class="ncua-flex ncua-justify-between ncua-align-items-start search-footer-container">
<?php if ($sendMethod === 'kakao') : ?>
<p class="ncua-notice-info">블룸에이아이의 템플릿은 <a class="ncua-link" href="https://bizmsg.blumn.ai/template"
target="_blank">블룸에이아이 파트너스 센터</a>에서 확인 가능합니다.</p>
<?php else : ?>
 
변경 후message_template_search.php
<col>
</colgroup>
<tbody>
<?php if ($isKakao): ?>
<tr>
<th>
<div data-tooltip-seq="001">제공사</div>
</span>
<span class="ncua-radio-field__text">고도몰</span>
</label>
<?php if ($isNewMall === false): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="provider"
</div>
</td>
</tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<tr>
<th>
<div data-tooltip-seq="003"><?= $isKakao ? '구분' : '카테고리' ?></div>
</th>
<td colspan="3">
<div class="ncua-flex-gap js-trigger-type">
<?php foreach ($searchCategories as $cat): ?>
<?php
$checked = in_array($cat['value'], $search->getCategory()) ? 'checked' : '';
?>
</div>
</td>
</tr>
<?php if ($isKakao || $isSms): ?>
 
 
 
 
 
 
 
 
 
 
<tr>
<th>
<div data-tooltip-seq="004">검수 상태</div>
</th>
<td>
<div class="ncua-flex-gap js-trigger-status">
<?php foreach ($searchStatusOptions as $option): ?>
<?php if (!isset($option['provider']) || $search->getProvider() === $option['provider']): ?>
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">
</table>
</div>
<div class="ncua-flex ncua-justify-between ncua-align-items-start search-footer-container">
<?php if ($isKakao): ?>
<p class="ncua-notice-info">블룸에이아이의 템플릿은 <a class="ncua-link" href="https://bizmsg.blumn.ai/template"
target="_blank">블룸에이아이 파트너스 센터</a>에서 확인 가능합니다.</p>
<?php else : ?>
 
Admin/crm/message_template.php+179-31
변경 전message_template.php
 
if (hasBasic) {
NCDSAlert({
message: '고도몰에서 제공하는 기본 템플릿은 수정할 수 없습니다.<br>복제 후 내용을 수정해 신규 템플릿으로 등록해 주세요.',
iconType: 'error'
});
return false;
 
<div class="swiper ncua-horizontal-tab ncua-horizontal-tab--md ncua-horizontal-tab--underline-fill">
<div class="swiper-wrapper ncua-gap-8">
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="redirectWithParams('sendMethod','sms',false)"
class="ncua-tab-button <?= ($sendMethod === 'sms') ? ' is-active' : '' ?>" data-html="true"
data-content="SMS" data-placement="top">SMS</a>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="redirectWithParams('sendMethod','kakao',false)"
class="ncua-tab-button <?= ($sendMethod === 'kakao') ? ' is-active' : '' ?>" data-html="true"
data-content="카카오 알림톡" data-placement="top">카카오 알림톡</a>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="redirectWithParams('sendMethod','myapp',false)"
class="ncua-tab-button <?= ($sendMethod === 'myapp') ? ' is-active' : '' ?>" data-html="true"
data-content="마이앱(앱푸시)" data-placement="top">마이앱(앱푸시)</a>
</div>
 
 
 
 
 
</div>
</div>
 
</section>
</section>
</article>
 
<script type="text/javascript">
const start = new Date('1970-01-01');
const end = new Date(); // 오늘 날짜
NCDSAlert({ message, iconType: 'error' });
});
 
// 템플릿 코드별 autoFl 매핑
const templateAutoMap = {
<?php foreach ($results as $item): ?>
'<?= $item->getTemplateCode() ?>': '<?= $item->getAutoFl() ?>',
<?php endforeach; ?>
};
 
const templateBasicMap = {
<?php foreach ($results as $item): ?>
'<?= $item->getTemplateCode() ?>': '<?= $item->getBasicFl() ?>',
<?php endforeach; ?>
};
 
const searchData = JSON.parse('<?= json_encode($search->toArray(), JSON_UNESCAPED_UNICODE) ?>');
 
}
 
 
function redirectWithParams(key, value, isAll = true) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
const form = document.createElement('form');
form.method = 'POST';
form.action = '';
 
form.appendChild(createHiddenInput('page', 1));
form.appendChild(createHiddenInput(key, value));
 
document.body.appendChild(form);
form.submit();
}
form.submit();
}
 
 
 
 
 
 
// 페이지 이동
function loadByPage(page) {
const [key, value] = page.split('=');
// 수정 버튼 (기본 템플릿 수정 불가 안내)
if (target.classList.contains('js-btn-modify')) {
const templateCode = target.value;
const hasBasic = templateBasicMap[templateCode] === 'y';
 
if (hasBasic) {
NCDSAlert({
return false;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
const data = {
mode: 'modify',
sendMethod: '<?= $sendMethod ?>',
}
});
 
 
 
 
 
 
 
 
 
 
// 선택삭제 유효성 검사
$('#frmList').validate({
ignore: ':hidden', dialog: false, submitHandler: function (form) {
const selectedCodes = Array.from(form.querySelectorAll('input[name="templateCodeList[]"]:checked'))
.map(input => input.value);
 
// 자동알림 사용 중인 템플릿 체크
const hasAutoAlrim = selectedCodes.some(code => {
return templateAutoMap[code] === 'y';
});
 
if (hasAutoAlrim) {
NCDSAlert({
message: '자동알림에 사용 중인 템플릿은 삭제할 수 없습니다.',
iconType: 'error'
});
return false;
const data = {
mode: mode,
sendMethod: '<?= $sendMethod ?>',
templateCodeList: selectedCodes,
provider: '<?= $search->getProvider() ?>'
}
 
 
$.post('./message_template_ps.php', data, function (response) {
if (response.success) {
NCDSToast({message: '템플릿이 삭제되었습니다.', color: 'success'});
 
 
 
 
 
 
 
 
 
 
 
setTimeout(() => {
redirectWithParams('page', 1, true);
},
messages: {
'templateCode[]': {
required: '선택하신 글이 없습니다.'
},
 
},
const btnDelete = document.querySelector('.js-btn-delete');
if (btnDelete) {
btnDelete.addEventListener('click', handleDeleteClick);
 
 
 
 
 
 
 
 
 
 
 
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
const code = '251219001';
// COS 가이드 초기화
if (window.GodoCosGuide) {
 
변경 후message_template.php
 
if (hasBasic) {
NCDSAlert({
message: '<?= $basicNotModifiableMessage ?>',
iconType: 'error'
});
return false;
 
<div class="swiper ncua-horizontal-tab ncua-horizontal-tab--md ncua-horizontal-tab--underline-fill">
<div class="swiper-wrapper ncua-gap-8">
 
 
 
 
 
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="redirectWithParams('sendMethod','kakao',false)"
class="ncua-tab-button <?= $isKakao ? ' is-active' : '' ?>" data-html="true"
data-content="카카오 알림톡" data-placement="top">카카오 알림톡</a>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="redirectWithParams('sendMethod','myapp',false)"
class="ncua-tab-button <?= $isMyapp ? ' is-active' : '' ?>" data-html="true"
data-content="마이앱(앱푸시)" data-placement="top">마이앱(앱푸시)</a>
</div>
<div class="swiper-slide ncua-horizontal-tab__item">
<a href="javascript:" onclick="selectSmsTab()"
class="ncua-tab-button <?= $isSms ? ' is-active' : '' ?>" data-html="true"
data-content="SMS" data-placement="top">SMS</a>
</div>
</div>
</div>
 
</section>
</section>
</article>
 
<script type="text/javascript">
const start = new Date('1970-01-01');
const end = new Date(); // 오늘 날짜
NCDSAlert({ message, iconType: 'error' });
});
 
const templateMap = <?= json_encode($templateMap) ?>;
 
 
 
 
 
 
const templateApprovalStatus = <?= json_encode($approvalStatusValues) ?>;
 
const hasSmsApprovalPermission = <?= $hasSmsApprovalPermission ? 'true' : 'false' ?>;
 
 
 
const searchData = JSON.parse('<?= json_encode($search->toArray(), JSON_UNESCAPED_UNICODE) ?>');
 
}
 
 
function selectSmsTab() {
const restrictAlertMessage = '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertMessage, '')) ?>';
if (restrictAlertMessage) {
NCDSAlert({
message: restrictAlertMessage,
subMessage: '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsSendRestrictAlertSubMessage, '')) ?>',
iconType: 'error'
});
return;
}
if (<?= json_encode($isSmsTabVisible) ?>) {
redirectWithParams('sendMethod', 'sms', false, {smsTabReturnMethod: '<?= $sendMethod ?>'});
return;
}
NCDSAlert({
message: '<?= gd_htmlspecialchars_stripslashes($smsNoRoleMessage) ?>',
subMessage: '<?= gd_htmlspecialchars_stripslashes(gd_isset($smsNoRoleSubMessage, '')) ?>',
iconType: 'error'
});
}
 
function redirectWithParams(key, value, isAll = true, extraParams = {}) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '';
 
form.appendChild(createHiddenInput('page', 1));
form.appendChild(createHiddenInput(key, value));
Object.entries(extraParams).forEach(([k, v]) => form.appendChild(createHiddenInput(k, v)));
document.body.appendChild(form);
form.submit();
}
form.submit();
}
 
// 점유인증 완료 후 복귀 — POST 로만 유지되는 현재 탭·검색조건을 재요청한다
window.occupancyAuthReload = function () {
redirectWithParams('sendMethod', '<?= gd_isset($requestedSendMethod, $sendMethod) ?>', true);
};
 
// 페이지 이동
function loadByPage(page) {
const [key, value] = page.split('=');
// 수정 버튼 (기본 템플릿 수정 불가 안내)
if (target.classList.contains('js-btn-modify')) {
const templateCode = target.value;
const hasBasic = templateMap[templateCode]?.basicFl === 'y';
 
if (hasBasic) {
NCDSAlert({
return false;
}
 
if (templateMap[templateCode]?.approvalStatus === templateApprovalStatus.PENDING) {
NCDSAlert({
message: '<?= $notModifiableMessage ?>',
iconType: 'info'
});
return false;
}
 
if ('<?= $sendMethod ?>' === 'sms' && templateMap[templateCode]?.usedFl === 'y') {
NCDSAlert({
message: '<?= $linkedNotModifiableMessage ?>',
iconType: 'info'
});
return false;
}
 
const data = {
mode: 'modify',
sendMethod: '<?= $sendMethod ?>',
}
});
 
$('.js-btn-detail').on('click', function () {
$.post('./layer_message_template_approval_history.php', {templateCode: this.value}, function (data) {
ncds_layer_popup({
message: data,
title: 'SMS 발송 템플릿 승인 이력',
});
});
});
 
// 선택삭제 유효성 검사
$('#frmList').validate({
ignore: ':hidden', dialog: false, submitHandler: function (form) {
const selectedCodes = Array.from(form.querySelectorAll('input[name="templateCodeList[]"]:checked'))
.map(input => input.value);
 
const deletableCodes = selectedCodes.filter(code => templateMap[code]?.usedFl !== 'y');
const usedCount = selectedCodes.length - deletableCodes.length;
 
 
 
if (deletableCodes.length === 0) {
NCDSAlert({
message: '<?= $usedNotDeletableMessage ?>',
iconType: 'error'
});
return false;
const data = {
mode: mode,
sendMethod: '<?= $sendMethod ?>',
templateCodeList: deletableCodes,
provider: '<?= $search->getProvider() ?>'
}
 
 
$.post('./message_template_ps.php', data, function (response) {
if (response.success) {
const excludedCount = usedCount + (response.mappedCount || 0);
if (excludedCount > 0) {
NCDSAlert({
message: `자동 알림 발송 목록 ${excludedCount}건 제외 ${response.deletedCount}건의 템플릿이 삭제되었습니다.`,
iconType: 'warning',
onClose: () => redirectWithParams('page', 1, true)
});
return;
}
 
NCDSToast({message: response.message, color: 'success'});
 
setTimeout(() => {
redirectWithParams('page', 1, true);
},
messages: {
'templateCode[]': {
required: '선택하신 템플릿이 없습니다.'
},
 
},
const btnDelete = document.querySelector('.js-btn-delete');
if (btnDelete) {
btnDelete.addEventListener('click', handleDeleteClick);
 
const syncDeleteButton = () => {
btnDelete.disabled = document.querySelectorAll('input[name="templateCodeList[]"]:checked').length === 0;
};
// 전체선택 체크박스는 공용 스크립트가 각 행을 갱신한 뒤 change 를 올리므로 위임으로 함께 받는다
document.addEventListener('change', (e) => {
if (e.target.matches('input[name="templateCodeList[]"], .ncua-checkall')) {
syncDeleteButton();
}
});
syncDeleteButton();
}
 
const handleApprovalClick = (mode) => {
if (!hasSmsApprovalPermission) {
NCDSAlert({message: '<?= $smsNoApprovalPermissionMessage ?>', iconType: 'error'});
return;
}
 
const actionName = mode === 'approve' ? '승인' : '반려';
const selectedCodes = Array.from(document.querySelectorAll('input[name="templateCodeList[]"]:checked'))
.map(input => input.value);
 
if (selectedCodes.length === 0) {
NCDSAlert({message: '선택하신 템플릿이 없습니다.', iconType: 'error'});
return;
}
 
const pendingCodes = selectedCodes.filter(code => templateMap[code]?.approvalStatus === templateApprovalStatus.PENDING);
if (pendingCodes.length === 0) {
if (selectedCodes.length === 1) {
NCDSAlert({
message: '이미 승인 또는 반려 처리가 된 신청 건입니다.<br>페이지 새로고침 후 확인해 주세요.',
iconType: 'error',
callback: () => location.reload()
});
return;
}
NCDSAlert({
message: `선택한 템플릿 중 ${actionName} 가능한 템플릿이 없습니다. 검수중 상태의 템플릿만 처리할 수 있습니다.`,
iconType: 'error'
});
return;
}
 
const approvedCount = selectedCodes.filter(code => templateMap[code]?.approvalStatus === templateApprovalStatus.APPROVED).length;
const rejectedCount = selectedCodes.filter(code => templateMap[code]?.approvalStatus === templateApprovalStatus.REJECTED).length;
const tailMessage = mode === 'approve'
? '승인한 템플릿은 승인 취소가 불가합니다.'
: '반려한 템플릿은 수정 후 재검수가 가능합니다.';
 
NCDSConfirm({
message: `선택한 템플릿을 ${actionName}하시겠습니까?`,
subMessage: `선택한 ${selectedCodes.length}건 중 검수중 ${pendingCodes.length}건을 ${actionName}합니다. (승인 ${approvedCount}건·반려 ${rejectedCount}건 제외). ${tailMessage}`,
callback: function (result) {
if (!result) return false;
 
const data = {
mode: mode,
sendMethod: '<?= $sendMethod ?>',
templateCodeList: pendingCodes,
provider: '<?= $search->getProvider() ?>'
};
 
$.post('./message_template_ps.php', data, function (response) {
if (response.noPermission) {
NCDSAlert({
message: response.message,
iconType: 'error',
callback: () => location.reload()
});
return;
}
 
if (!response.success) {
NCDSAlert({
message: response.message || `${actionName} 처리에 실패했습니다.`,
iconType: 'error'
});
return;
}
 
NCDSToast({
message: response.message,
color: 'success'
});
 
setTimeout(() => {
redirectWithParams('page', 1, true);
}, 3000);
}, 'json');
},
});
};
 
document.querySelector('.js-btn-approve')?.addEventListener('click', () => handleApprovalClick('approve'));
document.querySelector('.js-btn-reject')?.addEventListener('click', () => handleApprovalClick('reject'));
 
const code = '251219001';
// COS 가이드 초기화
if (window.GodoCosGuide) {
 
Admin/crm/message_template_register/basic_config.php+9-6
변경 전basic_config.php
<th><div>발송수단</div></th>
<td>
<div class="ncua-flex ncua-flex-gap ncua-custom-tooltip <?= in_array($request->getMode(), ['copy', 'modify']) ? 'is-edit-mode' : '' ?>">
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-tooltip-type="disabled-send-method">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="sms" <?= ($request->getSendMethod() === 'sms') ? 'checked="checked"' : '' ?> <?= in_array($request->getMode(), ['copy', 'modify']) ? 'disabled' : '' ?> />
</span>
<span class="ncua-radio-field__text">SMS/LMS</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-tooltip-type="disabled-send-method">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="kakao" <?= ($request->getSendMethod() === 'kakao') ? 'checked="checked"' : '' ?> <?= (in_array($request->getMode(), ['copy', 'modify']) || $config->getKakaoUseFl() === 'n' || ($config->getCloudFl() === 'n' && $config->getBizmFl() === 'n')) ? 'disabled' : '' ?> />
</span>
<span class="ncua-radio-field__text">마이앱 (앱푸시)</span>
</label>
 
 
 
 
 
 
</div>
</td>
</tr>
$templateTitleMaxLengthAttr = '';
} elseif ($request->getSendMethod() === 'kakao' && $request->getProvider() === 'bizm') {
$templateTitleMaxLengthAttr = 'maxlength="30"';
 
 
} else {
$templateTitleMaxLengthAttr = 'maxlength="10"';
}
<output class="ncua-input__field-text-count-current"><?= mb_strlen($templateTitleValue) ?></output><span class="js-template-title-max-length"><?= $templateTitleMaxLength ? '/' . $templateTitleMaxLength : '' ?></span>
</div>
</div>
 
</div>
</div>
</td>
 
변경 후basic_config.php
<th><div>발송수단</div></th>
<td>
<div class="ncua-flex ncua-flex-gap ncua-custom-tooltip <?= in_array($request->getMode(), ['copy', 'modify']) ? 'is-edit-mode' : '' ?>">
 
 
 
 
 
 
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-tooltip-type="disabled-send-method">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="kakao" <?= ($request->getSendMethod() === 'kakao') ? 'checked="checked"' : '' ?> <?= (in_array($request->getMode(), ['copy', 'modify']) || $config->getKakaoUseFl() === 'n' || ($config->getCloudFl() === 'n' && $config->getBizmFl() === 'n')) ? 'disabled' : '' ?> />
</span>
<span class="ncua-radio-field__text">마이앱 (앱푸시)</span>
</label>
<label class="ncua-radio-field ncua-radio-field--xs has-text" data-tooltip-type="disabled-send-method" data-tooltip-text="<?= gd_htmlspecialchars(gd_isset($smsSendMethodRestrictTooltip, '')) ?>">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="sms" <?= ($request->getSendMethod() === 'sms') ? 'checked="checked"' : '' ?> <?= (in_array($request->getMode(), ['copy', 'modify']) || gd_isset($smsSendMethodRestricted, false)) ? 'disabled' : '' ?> />
</span>
<span class="ncua-radio-field__text">SMS/LMS</span>
</label>
</div>
</td>
</tr>
$templateTitleMaxLengthAttr = '';
} elseif ($request->getSendMethod() === 'kakao' && $request->getProvider() === 'bizm') {
$templateTitleMaxLengthAttr = 'maxlength="30"';
} elseif (mb_strlen($templateTitleValue) > 10) {
$templateTitleMaxLengthAttr = '';
} else {
$templateTitleMaxLengthAttr = 'maxlength="10"';
}
<output class="ncua-input__field-text-count-current"><?= mb_strlen($templateTitleValue) ?></output><span class="js-template-title-max-length"><?= $templateTitleMaxLength ? '/' . $templateTitleMaxLength : '' ?></span>
</div>
</div>
<span class="ncua-hint-text ncua-input__hint-text">템플릿 제목은 템플릿을 구분하기 위한 항목이며, 실제 발송되는 내용에는 포함되지 않습니다.</span>
</div>
</div>
</td>
 
Admin/crm/message_template_register.php+38-5
변경 전message_template_register.php
const sendMethodRadios = document.querySelectorAll('input[name="sendMethod"]');
sendMethodRadios.forEach((radio) => {
radio.addEventListener('change', (event) => {
const selectedValue = event.target.value;
 
 
 
 
// 이전 발송 수단의 필드 값 저장
if (previousSendMethod) {
saveFieldValues(previousSendMethod);
if (selectedValue === 'sms') {
updateSmsMessageHint(smsMessageInput.getValue());
}
});
});
 
/**
* 제공사 라디오 버튼 변경 시 조건부 표시 업데이트
// 제목 입력 — 이모지 제거, 40bytes 초과분 자동 삭제 (byte 기준은 본문 MessageInput 과 동일)
const handleSmsTitleInput = () => {
const input = getSmsTitleInput();
let value = smsMessageInput.removeEmoji(input.value).value;
value = smsMessageInput.truncateBytes(value, SMS_TITLE_MAX_BYTES);
if (input.value !== value) {
 
input.value = value;
 
}
document.querySelector('[data-sms-title-current]').textContent = smsMessageInput.getByteLength(value);
updateSmsTitlePreview();
getSmsTitleInput().addEventListener('input', handleSmsTitleInput);
handleSmsTitleInput();
 
 
 
 
 
 
 
 
 
 
 
 
 
const smsVariableSelector = GodoUIModule.render({
type: 'ChipSelector',
target: '#sms-variable-selector-container',
return true;
};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 검증 에러 표시 (공통)
*/
},
},
invalidHandler: function (event, validator) {
 
 
const templateTypeRadio = document.querySelector('input[name="templateType"]:checked');
 
// IMAGE 타입: 상단 이미지 미등록 시 highlight + alert (ITEM_LIST는 submitHandler에서 별도 검증)
 
변경 후message_template_register.php
const sendMethodRadios = document.querySelectorAll('input[name="sendMethod"]');
sendMethodRadios.forEach((radio) => {
radio.addEventListener('change', (event) => {
switchSendMethod(event.target.value);
});
});
 
const switchSendMethod = (selectedValue) => {
// 이전 발송 수단의 필드 값 저장
if (previousSendMethod) {
saveFieldValues(previousSendMethod);
if (selectedValue === 'sms') {
updateSmsMessageHint(smsMessageInput.getValue());
}
};
 
 
/**
* 제공사 라디오 버튼 변경 시 조건부 표시 업데이트
// 제목 입력 — 이모지 제거, 40bytes 초과분 자동 삭제 (byte 기준은 본문 MessageInput 과 동일)
const handleSmsTitleInput = () => {
const input = getSmsTitleInput();
const stripped = smsMessageInput.removeEmoji(input.value, input.selectionStart);
const value = smsMessageInput.truncateBytes(stripped.value, SMS_TITLE_MAX_BYTES);
if (input.value !== value) {
const caret = Math.min(stripped.cursorPos, value.length);
input.value = value;
input.setSelectionRange(caret, caret);
}
document.querySelector('[data-sms-title-current]').textContent = smsMessageInput.getByteLength(value);
updateSmsTitlePreview();
getSmsTitleInput().addEventListener('input', handleSmsTitleInput);
handleSmsTitleInput();
 
const templateTitleInput = document.querySelector('.js-template-title');
if (templateTitleInput) {
templateTitleInput.addEventListener('input', () => {
const stripped = smsMessageInput.removeEmoji(templateTitleInput.value, templateTitleInput.selectionStart);
if (templateTitleInput.value === stripped.value) return;
 
templateTitleInput.value = stripped.value;
templateTitleInput.setSelectionRange(stripped.cursorPos, stripped.cursorPos);
templateTitleInput.dispatchEvent(new Event('keyup', {bubbles: true}));
});
}
 
const smsVariableSelector = GodoUIModule.render({
type: 'ChipSelector',
target: '#sms-variable-selector-container',
return true;
};
 
const scrollToTopmostInvalidField = (validator) => {
const fields = (validator.errorList || [])
.map(error => error.element)
.filter(element => element && element.offsetParent !== null);
 
if (fields.length < 2) {
return;
}
 
const topmost = fields.reduce((top, element) => (
element.getBoundingClientRect().top < top.getBoundingClientRect().top ? element : top
));
topmost.scrollIntoView({behavior: 'smooth', block: 'center'});
};
 
/**
* 검증 에러 표시 (공통)
*/
},
},
invalidHandler: function (event, validator) {
scrollToTopmostInvalidField(validator);
 
const templateTypeRadio = document.querySelector('input[name="templateType"]:checked');
 
// IMAGE 타입: 상단 이미지 미등록 시 highlight + alert (ITEM_LIST는 submitHandler에서 별도 검증)
 
Admin/crm/message_template_register/template_contents_sms.php+1
변경 전template_contents_sms.php
<ul>
<li>URL 삽입 시 [단축 URL] 앱을 이용하시면 짧고 간단하게 URL을 사용하실 수 있습니다. <a href="https://apps.godo.co.kr/apps/1519" target="_blank" class="ncua-link-underline">바로가기</a></li>
<li>제목은 LMS 발송 시에만 적용되는 선택 입력 항목으로, 미입력 시에는 ‘제목 없음’으로 발송되며 SMS 발송 시에는 포함되지 않습니다.</li>
 
</ul>
</div>
</details>
 
변경 후template_contents_sms.php
<ul>
<li>URL 삽입 시 [단축 URL] 앱을 이용하시면 짧고 간단하게 URL을 사용하실 수 있습니다. <a href="https://apps.godo.co.kr/apps/1519" target="_blank" class="ncua-link-underline">바로가기</a></li>
<li>제목은 LMS 발송 시에만 적용되는 선택 입력 항목으로, 미입력 시에는 ‘제목 없음’으로 발송되며 SMS 발송 시에는 포함되지 않습니다.</li>
<li>등록/수정한 템플릿은 승인 후 발송 가능합니다.</li>
</ul>
</div>
</details>
 
Admin/crm/mobile_send/layer_kakao_friendtalk_content_setting.php+7-3
변경 전layer_kakao_friendtalk_content_setting.php
kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignImageLink"]').value = carousel.imageLink;
 
// 메시지 (message)
window.messageInput.setValue(carousel.message);
<?php if ($isRecipeContext): ?>
// 레시피: 슬라이드 전환은 setValue 라 onInput 미발생 → 포인트 힌트·안내문구 직접 반영
window.messageInput.setHint(carousel.message ? friendtalkUnitPointHint(getCampaignMessageType()) : null);
MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeContainer', carousel.message);
<?php endif; ?>
 
}
 
function renderMessageInput(campaignMessageType) {
 
 
window.messageInput.clear();
window.messageInput.setHint(null);
window.preview.reset();
kakaoFriendtalkButtonSection.querySelectorAll('.ncua-table').forEach(t => t.remove());
kakaoFriendtalkButtonSection.querySelector('.empty-coupon-data').classList.remove('display-none');
 
window.preview.reset();
checkTable();
 
// 아이템 리스트 테이블 row 수 초기화
}
 
function updateDefaultPreview(campaignType) {
 
 
switch (campaignType) {
case 'TEXT':
setDefaultPreview(['TEXT']);
 
변경 후layer_kakao_friendtalk_content_setting.php
kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignImageLink"]').value = carousel.imageLink;
 
// 메시지 (message)
window.messageInput?.setValue(carousel.message);
<?php if ($isRecipeContext): ?>
// 레시피: 슬라이드 전환은 setValue 라 onInput 미발생 → 포인트 힌트·안내문구 직접 반영
window.messageInput?.setHint(carousel.message ? friendtalkUnitPointHint(getCampaignMessageType()) : null);
MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeContainer', carousel.message);
<?php endif; ?>
 
}
 
function renderMessageInput(campaignMessageType) {
if (!window.messageInput || !window.preview) return;
 
window.messageInput.clear();
window.messageInput.setHint(null);
window.preview.reset();
kakaoFriendtalkButtonSection.querySelectorAll('.ncua-table').forEach(t => t.remove());
kakaoFriendtalkButtonSection.querySelector('.empty-coupon-data').classList.remove('display-none');
 
window.preview?.reset();
checkTable();
 
// 아이템 리스트 테이블 row 수 초기화
}
 
function updateDefaultPreview(campaignType) {
if (!window.preview) return;
 
switch (campaignType) {
case 'TEXT':
setDefaultPreview(['TEXT']);
 
Admin/crm/mobile_send/layer_mobile_send_save_sms_template.php+5-1
변경 전layer_mobile_send_save_sms_template.php
<link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/layer-save-as-template.css')?>">
 
<div class="modal-dialog__content layer-save-as-template">
 
 
 
 
<input type="hidden" id="templateContents" value="<?= htmlspecialchars($contents, ENT_QUOTES) ?>" />
<input type="hidden" id="templateMessageTitle" value="<?= htmlspecialchars($messageTitle ?? '', ENT_QUOTES) ?>" />
<div class="ncua-table ncua-table--vertical">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="10" type="text" id="templateSubject" placeholder="제목을 입력하세요" class="layer-template-title" data-charcount-key="layerTemplateTitle" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="layerTemplateTitle">
 
변경 후layer_mobile_send_save_sms_template.php
<link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/layer-save-as-template.css')?>">
 
<div class="modal-dialog__content layer-save-as-template">
<ul>
<li class="ncua-notice-info">작성한 내용을 저장할 카테고리와 템플릿명을 입력하여 주시기 바랍니다.</li>
<li class="ncua-notice-info">저장한 템플릿은 검수중 상태로 등록되며, 승인 후 발송 가능합니다.</li>
</ul>
<input type="hidden" id="templateContents" value="<?= htmlspecialchars($contents, ENT_QUOTES) ?>" />
<input type="hidden" id="templateMessageTitle" value="<?= htmlspecialchars($messageTitle ?? '', ENT_QUOTES) ?>" />
<div class="ncua-table ncua-table--vertical">
<div class="ncua-input__content-wrap">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input maxlength="10" type="text" id="templateSubject" placeholder="템플릿명을 입력하여 주세요." class="layer-template-title" data-charcount-key="layerTemplateTitle" />
</div>
</div>
<div class="ncua-input__field-text-count" data-charcount-text="layerTemplateTitle">
 
Admin/crm/mobile_send/layer_mobile_send_sms_template.php+1
변경 전layer_mobile_send_sms_template.php
 
<div class="modal-dialog__content layer-load-template js-sms-template-section">
<ul>
 
<li class="ncua-caution-text">치환코드는 지정된 페이지에서만 사용하실 수 있습니다.</li>
<li class="ncua-notice-info">예) 주문 관련 치환코드는 주문리스트에서 SMS 발송 시에만 사용하실 수 있습니다.</li>
<li class="ncua-notice-info">중복 선택은 불가합니다.</li>
 
변경 후layer_mobile_send_sms_template.php
 
<div class="modal-dialog__content layer-load-template js-sms-template-section">
<ul>
<li class="ncua-notice-info">템플릿 작성 및 수정은 템플릿 관리에서 가능합니다. <a href="./message_template.php?sendMethod=sms" target="_blank" class="ncua-link">바로가기</a></li>
<li class="ncua-caution-text">치환코드는 지정된 페이지에서만 사용하실 수 있습니다.</li>
<li class="ncua-notice-info">예) 주문 관련 치환코드는 주문리스트에서 SMS 발송 시에만 사용하실 수 있습니다.</li>
<li class="ncua-notice-info">중복 선택은 불가합니다.</li>
 
Admin/crm/mobile_send/layer_recipient_setting.php+2
변경 전layer_recipient_setting.php
 
function updateFriendtalkCampainUnitPoint() {
if (document.querySelector('input[name="sendMethod"]:checked')?.value === 'FRIENDTALK') {
 
 
const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value;
updateCampaignUnitPoint(campaignMessageType);
}
 
변경 후layer_recipient_setting.php
 
function updateFriendtalkCampainUnitPoint() {
if (document.querySelector('input[name="sendMethod"]:checked')?.value === 'FRIENDTALK') {
if (typeof updateCampaignUnitPoint !== 'function') return;
 
const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value;
updateCampaignUnitPoint(campaignMessageType);
}
 
Admin/crm/mobile_send/layer_send_method_setting.php+47-81
변경 전layer_send_method_setting.php
<?php foreach ($sendMethods as $sendMethod): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="<?= $sendMethod->name ?>" <?= $sendMethod->name === 'SMS' ? 'checked="checked"' : ''?> />
</span>
<span>
<span class="ncua-radio-field__text"><?= $sendMethod->value ?></span>
}
}
 
const validateResult = validateSendMethodUseFlag(this.value);
if (!validateResult.result) {
e.preventDefault();
NCDSAlert({
message: validateResult.message,
subMessage: validateResult.subMessage,
iconType: 'error'
});
return;
});
}
 
function validateSendMethodUseFlag(sendMethod) {
switch (sendMethod) {
case 'FRIENDTALK':
if (messageConfig.kakaoFriendTalk.useFlag === 'y') {
return { result: true };
} else {
return {
result: false,
message: '발송 수단이 준비되지 않았습니다.',
subMessage: '메시지를 발송하려면 발송 수단 인증 및 설치가 필요합니다.<br />통합 설정 페이지에서 먼저 완료해 주세요.'
};
}
case 'ALIMTALK':
if (messageConfig.kakaoAlimTalk.useFlag === 'y') {
if (messageConfig.kakaoAlimTalk.sender !== 'kakaoAlrimCloud') {
return {
result: false,
message: '비즈엠/블룸에이아이 발송 불가 안내',
subMessage: '비즈엠, 블룸에이아이 채널은 알림톡으로 메시지를 발송할 수 없습니다.<br />CRM > 메시지 > 메시지 설정에서 제공사를 고도몰로 변경해 주세요.'
};
}
 
return {result: true};
} else {
return {
result: false,
message: '발송 수단이 준비되지 않았습니다.',
subMessage: '메시지를 발송하려면 발송 수단 인증 및 설치가 필요합니다.<br />통합 설정 페이지에서 먼저 완료해 주세요.'
};
}
case 'MYAPP':
if (messageConfig.myappPush.useFlag === 'y') {
return { result: true };
} else {
return {
result: false,
message: '발송 수단이 준비되지 않았습니다.',
subMessage: '메시지를 발송하려면 발송 수단 인증 및 설치가 필요합니다.<br />통합 설정 페이지에서 먼저 완료해 주세요.'
};
}
default:
return { result: true };
}
}
 
function initializeData() {
}
 
function loadSendMethodContentSettingLayer(sendMethod) {
 
 
 
 
 
 
const urlMap = {
SMS: './mobile_send/layer_sms_content_setting.php',
FRIENDTALK: './mobile_send/layer_kakao_friendtalk_content_setting.php',
});
}
 
 
 
 
 
 
 
 
 
 
 
 
function getActivatedSendTabType() {
return document.querySelector('.ncua-tab-button[data-send-tab-type].is-active')?.dataset.sendTabType ?? null;
}
 
// 제목 byte 규칙 적용(이모지 제거 포함) 후 최종 값 반환
function applySmsTitleByteRule(titleInput, messageInputModule) {
let value = messageInputModule.removeEmoji(titleInput.value).value;
 
const isOver = messageInputModule.getByteLength(value) > SMS_TITLE_MAX_BYTES;
// addAdText 가 (광고) 삽입 시에만 세우는 1회성 플래그 (사용자 타이핑과 구분)
const adInserted = titleInput.dataset.titleAdInsert === '1';
const base = titleInput.dataset.titleOverBase;
let caret = null;
 
delete titleInput.dataset.titleAdInsert;
 
// C4-2 — 절삭하지 않는다. 초과 상태에서는 늘리는 입력만 되돌리고 삭제는 허용
if (!adInserted && base !== undefined && messageInputModule.getByteLength(value) > messageInputModule.getByteLength(base)) {
// 되돌린 만큼 커서를 입력 직전 위치로 복원
caret = Math.max(0, (titleInput.selectionStart ?? value.length) - (value.length - base.length));
value = base;
}
titleInput.dataset.titleOverBase = value;
if (titleInput.value !== value) {
titleInput.value = value;
if (caret !== null) {
titleInput.setSelectionRange(caret, caret);
 
}
}
return value;
* 대체메시지 사용 가능여부 검증
*/
function enableAlternative(activatedSendMethod) {
switch (activatedSendMethod) {
case 'SMS':
return false;
case 'FRIENDTALK':
if (messageConfig.kakaoFriendTalk.useAlternative !== 'y') {
return false;
}
 
const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value;
if (['CAROUSEL_FEED', 'WIDE_ITEM_LIST'].includes(campaignMessageType)) {
return false;
}
break;
case 'ALIMTALK':
if (messageConfig.kakaoAlimTalk.useAlternative !== 'y') {
return false;
}
break;
case 'MYAPP':
if (messageConfig.myappPush.useAlternative !== 'y') {
return false;
}
 
const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value ?? 'ALL'
const isOnlyAgreed = document.querySelector('input[name="isOnlyAgreed"]')?.checked;
const myappSendCondition = document.querySelector('input[name="myappSendCondition"]:checked').value;
 
 
if (recipientType === 'ALL' && !isOnlyAgreed && myappSendCondition === 'APP_INSTALLED') {
return false;
}
break;
 
 
}
 
return true;
}
 
function initializeSendMethodSettingSectionElement() {
document.querySelector('input[name="sendMethod"]:checked')?.dispatchEvent(new Event('change'));
 
}
 
function isEditing() {
 
변경 후layer_send_method_setting.php
<?php foreach ($sendMethods as $sendMethod): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input name="sendMethod" type="radio" value="<?= $sendMethod->name ?>" <?= $sendMethod->name === 'FRIENDTALK' ? 'checked="checked"' : ''?> />
</span>
<span>
<span class="ncua-radio-field__text"><?= $sendMethod->value ?></span>
}
}
 
const restriction = getSendMethodRestriction(this.value);
if (restriction?.reason === 'ALIMTALK_SENDER') {
e.preventDefault();
NCDSAlert({
...buildSendMethodRestrictionText(restriction),
 
iconType: 'error'
});
return;
});
}
 
function getSendMethodRestriction(sendMethod) {
return messageConfig.sendMethods?.[sendMethod]?.restriction ?? null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
function initializeData() {
}
 
function loadSendMethodContentSettingLayer(sendMethod) {
const restriction = getSendMethodRestriction(sendMethod);
if (restriction) {
renderSendMethodRestrictionNotice(restriction);
return;
}
 
const urlMap = {
SMS: './mobile_send/layer_sms_content_setting.php',
FRIENDTALK: './mobile_send/layer_kakao_friendtalk_content_setting.php',
});
}
 
function renderSendMethodRestrictionNotice(restriction) {
window.preview?.reset();
window.alternativePreview?.reset();
const { message, subMessage } = buildSendMethodRestrictionText(restriction);
$('#layerSendMethodContent').html(`
<div class="no-ready-send-type">
<p class="ncua-caution-text">${message} ${subMessage}</p>
</div>
`);
}
 
function getActivatedSendTabType() {
return document.querySelector('.ncua-tab-button[data-send-tab-type].is-active')?.dataset.sendTabType ?? null;
}
 
// 제목 byte 규칙 적용(이모지 제거 포함) 후 최종 값 반환
function applySmsTitleByteRule(titleInput, messageInputModule) {
const stripped = messageInputModule.removeEmoji(titleInput.value, titleInput.selectionStart ?? titleInput.value.length);
let value = stripped.value;
const isOver = messageInputModule.getByteLength(value) > SMS_TITLE_MAX_BYTES;
// addAdText 가 (광고) 삽입 시에만 세우는 1회성 플래그 (사용자 타이핑과 구분)
const adInserted = titleInput.dataset.titleAdInsert === '1';
const base = titleInput.dataset.titleOverBase;
let caret = value === titleInput.value ? null : stripped.cursorPos;
 
delete titleInput.dataset.titleAdInsert;
 
// C4-2 — 절삭하지 않는다. 초과 상태에서는 늘리는 입력만 되돌리고 삭제는 허용
if (!adInserted && base !== undefined && messageInputModule.getByteLength(value) > messageInputModule.getByteLength(base)) {
// 되돌린 만큼 커서를 입력 직전 위치로 복원
caret = Math.max(0, stripped.cursorPos - (value.length - base.length));
value = base;
}
titleInput.dataset.titleOverBase = value;
if (titleInput.value !== value) {
titleInput.value = value;
if (caret !== null) {
const caretPos = Math.min(caret, value.length);
titleInput.setSelectionRange(caretPos, caretPos);
}
}
return value;
* 대체메시지 사용 가능여부 검증
*/
function enableAlternative(activatedSendMethod) {
if (messageConfig.isSmsManager !== true) {
return false;
}
 
 
 
 
 
if (messageConfig.sendMethods?.[activatedSendMethod]?.useAlternative !== 'y') {
return false;
}
 
 
 
 
 
 
 
 
 
 
 
 
if (activatedSendMethod === 'FRIENDTALK') {
const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value;
return !['CAROUSEL_FEED', 'WIDE_ITEM_LIST'].includes(campaignMessageType);
}
 
if (activatedSendMethod === 'MYAPP') {
const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value ?? 'ALL'
const isOnlyAgreed = document.querySelector('input[name="isOnlyAgreed"]')?.checked;
const myappSendCondition = document.querySelector('input[name="myappSendCondition"]:checked').value;
 
return !(recipientType === 'ALL' && !isOnlyAgreed && myappSendCondition === 'APP_INSTALLED');
}
 
return true;
}
 
function initializeSendMethodSettingSectionElement() {
sendMethodSettingSection.querySelector('input[name="sendMethod"]:checked')
?.dispatchEvent(new Event('change'));
}
 
function isEditing() {
 
Admin/crm/mobile_send/layer_sms_content_setting.php+2
변경 전layer_sms_content_setting.php
<li>SMS 작성 시 90byte를 초과하면 LMS로 자동 전환되어 발송됩니다.</li>
<li>변수에 실제 데이터가 적용된 최종 메시지가 90byte를 초과하는 경우에도 LMS로 전환될 수 있습니다.</li>
<li>본문 내용이 LMS인 경우에만 제목 입력이 가능하며, 제목 영역에는 직접 (광고) 문구를 입력하지 않도록 하고 광고 삽입이 필요한 경우 광고성 문구 추가 기능을 사용해야 합니다.</li>
 
 
</ul>
</div>
</details>
 
변경 후layer_sms_content_setting.php
<li>SMS 작성 시 90byte를 초과하면 LMS로 자동 전환되어 발송됩니다.</li>
<li>변수에 실제 데이터가 적용된 최종 메시지가 90byte를 초과하는 경우에도 LMS로 전환될 수 있습니다.</li>
<li>본문 내용이 LMS인 경우에만 제목 입력이 가능하며, 제목 영역에는 직접 (광고) 문구를 입력하지 않도록 하고 광고 삽입이 필요한 경우 광고성 문구 추가 기능을 사용해야 합니다.</li>
<li>SMS/LMS 템플릿 등록·관리·승인은 <a class="ncua-link" href="./message_template.php?sendMethod=sms" target="_blank">템플릿 관리</a>에서 가능합니다.</li>
<li>SMS/LMS 템플릿은 승인 상태에서만 발송이 가능합니다. (검수중·반려 상태의 템플릿은 발송이 제한됩니다)</li>
</ul>
</div>
</details>
 
Admin/crm/mobile_send.php+22-53
변경 전mobile_send.php
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_description.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/mobile_message_replace_code.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/message_send_common.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_status.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-checkbox-group/ncds-checkbox-group.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-datepicker-factory/ncds-datepicker-factory.js') ?>"></script>
const lmsPointEach = Number(<?= $lmsPointEach ?>);
const kakaoAlrimtalkPointEach = Number(<?= $kakaoAlrimTalkPointEach ?>);
const kakaoFriendtalkPointEach = Number(<?= $kakaoFriendTalkPointEach ?>);
const messageConfig = JSON.parse('<?= json_encode($messageConfig, JSON_UNESCAPED_UNICODE) ?>');
 
$(document).ready(function () {
commonLogic();
refreshOverSmsTitleState();
}
 
// 발신번호 검증
if (!messageConfig.hasCallNumber) {
NCDSAlert({message: '발신번호가 없어 발송할 수 없습니다.', subMessage: '설정에서 발신번호 등록 후 발송 시도를 해주세요.', iconType: 'error'});
$button.prop('disabled', false);
return;
}
 
// 포인트 0인 경우
if (!validateHasMessagePoint()) {
NCDSAlert({
message: '메시지 포인트를 충전해 주세요',
iconType: 'error'
});
$button.prop('disabled', false);
return;
}
 
// 발송인원 X 수단별 포인트 검증
if (!validateMessagePoint()) {
NCDSAlert({
message: '포인트가 부족하여 발송할 수 없습니다.',
subMessage: '발송 대상 인원보다 포인트가 적어 발송이 불가합니다. <br /> 충전 후 다시 시도해주세요.',
iconType: 'error'
});
 
$button.prop('disabled', false);
return;
}
break;
}
 
const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value;
const mainContents = sendMethod === 'ALIMTALK'
? (document.querySelector('textarea[name="messageContent"]')?.value ?? '')
: (window.messageInput?.getValue() ?? '');
});
}
 
function validateHasMessagePoint() {
return messagePoint > 0;
}
 
function validateMessagePoint() {
const sendMethod = document.querySelector('input[name="sendMethod"]:checked')?.value ?? "SMS";
const expectCount = getExpectTargetCount();
 
let requiredPoint;
 
switch (sendMethod) {
case 'SMS':
const messageLength = window.messageInput.calculateLength(window.messageInput.getValue());
requiredPoint = messageLength > 90 ? lmsPointEach * expectCount : smsPointEach * expectCount;
break;
case 'ALIMTALK':
requiredPoint = kakaoAlrimtalkPointEach * expectCount;
break;
case 'FRIENDTALK':
requiredPoint = kakaoFriendtalkPointEach * expectCount;
break;
default:
requiredPoint = 1;
}
 
return messagePoint >= requiredPoint;
}
 
 
function getExpectTargetCount() {
const recipientType = document.querySelector('[name="recipientType"]:checked')?.value
if (recipientType == null || recipientType === '') {
 
변경 후mobile_send.php
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_description.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/mobile_message_replace_code.js"></script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/message_send_common.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<script type="text/javascript">
$(function () {
SmsSendGate.useNcdsDialog();
SmsSendGate.applyRestrictOnAjax('<?= gd_isset($smsSendRestrictType, '') ?>', {
tooltip: '<?= gd_isset($smsSendRestrictTooltip, '') ?>'
});
SmsSendGate.bindClickGate({
message: '<?= gd_isset($smsSendRestrictAlertMessage, '') ?>',
subMessage: '<?= gd_isset($smsSendRestrictAlertSubMessage, '') ?>',
selectors: ['[data-send-tab-type="ALTERNATIVE"]', '[data-preview-message-type="ALTERNATIVE"]']
});
});
</script>
<script type="text/javascript" src="<?= PATH_ADMIN_GD_SHARE ?>script/crm/crm_group_status.js"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-checkbox-group/ncds-checkbox-group.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-datepicker-factory/ncds-datepicker-factory.js') ?>"></script>
const lmsPointEach = Number(<?= $lmsPointEach ?>);
const kakaoAlrimtalkPointEach = Number(<?= $kakaoAlrimTalkPointEach ?>);
const kakaoFriendtalkPointEach = Number(<?= $kakaoFriendTalkPointEach ?>);
const messageConfig = <?= json_encode($messageConfig, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
 
$(document).ready(function () {
commonLogic();
refreshOverSmsTitleState();
}
 
const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value;
const smsIncluded = sendMethod === 'SMS' || enableAlternative(sendMethod);
 
 
 
 
 
try {
await SmsSendGate.ensure({
requiredPoint: getRequiredPoint(),
smsIncluded
 
 
 
 
 
 
 
 
 
 
 
 
});
} catch (e) {
$button.prop('disabled', false);
return;
}
break;
}
 
 
const mainContents = sendMethod === 'ALIMTALK'
? (document.querySelector('textarea[name="messageContent"]')?.value ?? '')
: (window.messageInput?.getValue() ?? '');
});
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
function getExpectTargetCount() {
const recipientType = document.querySelector('[name="recipientType"]:checked')?.value
if (recipientType == null || recipientType === '') {
 
Admin/member/sms_auto.php+56-17
변경 전sms_auto.php
<!-- //@formatter:off -->
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
 
<input type="hidden" id="kakaoUse" name="kakaoUse" value="<?php echo $kakaoSetting['useFlag']; ?>"/>
<input type="hidden" id="kakaoOrderUse" name="kakaoOrderUse" value="<?php echo $kakaoSetting['orderUseFlag']; ?>"/>
<input type="hidden" id="kakaoMemberUse" name="kakaoMemberUse" value="<?php echo $kakaoSetting['memberUseFlag']; ?>"/>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][recipientMode]" value="<?php echo $typeData['recipientMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][recipientSno]" value="<?php echo $typeData['recipientSno']; ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['recipientMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['recipientContents'] ?></textarea>
 
 
 
 
 
 
 
 
 
</td>
<?php } ?>
<td class="form-inline">
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][memberMode]" value="<?php echo $typeData['memberMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][memberSno]" value="<?php echo $typeData['memberSno']; ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['memberMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['memberContents'] ?></textarea>
 
 
 
 
 
 
 
 
 
<?php } else { ?>
<div class="text-center text-blue">운영자 전용</div>
<?php } ?>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminMode]" value="<?php echo $typeData['adminMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminSno]" value="<?php echo $typeData['adminSno']; ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][adminMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['adminMessageTitle']) ?>"/>
</div>
<textarea name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminContents]" rows="4"
class="form-control js-sms-auto-contents <?php if ($sKey == 'admin') { ?> width100p <?php } ?>"><?= $typeData['adminContents'] ?></textarea>
 
 
 
 
 
 
 
 
 
<?php } else { ?>
<div class="text-center text-orange-red">회원 전용</div>
<?php } ?>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][providerMode]" value="<?php echo $typeData['providerMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][providerSno]" value="<?php echo $typeData['providerSno']; ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['providerMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['providerContents'] ?></textarea>
 
 
 
 
 
 
 
 
 
<?php } else { ?>
<?php if ($sKey == 'admin') { ?>
<div class="text-center text-orange-red">본사 전용</div>
while (smsAutoStringToByte(text) > <?= $lmsSubjectLimit; ?>) { text = text.substring(0, text.length - 1); }
input.val(text);
});
 
 
$('.btn-change-password').click(function () {
$.get('../share/layer_sms_password.php?mode=change', function () {
BootstrapDialog.closeAll();
 
변경 후sms_auto.php
<!-- //@formatter:off -->
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-emoji.js') ?>"></script>
<input type="hidden" id="kakaoUse" name="kakaoUse" value="<?php echo $kakaoSetting['useFlag']; ?>"/>
<input type="hidden" id="kakaoOrderUse" name="kakaoOrderUse" value="<?php echo $kakaoSetting['orderUseFlag']; ?>"/>
<input type="hidden" id="kakaoMemberUse" name="kakaoMemberUse" value="<?php echo $kakaoSetting['memberUseFlag']; ?>"/>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][recipientMode]" value="<?php echo $typeData['recipientMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][recipientSno]" value="<?php echo $typeData['recipientSno']; ?>"/>
<?php if (isset($typeData['recipientLinkedContents'])) { ?>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientMessageTitle]" value="<?= gd_isset($typeData['recipientMessageTitle']) ?>"/>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientContents]" value="<?= $typeData['recipientContents'] ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" class="form-control js-sms-auto-subject" value="<?= $typeData['recipientLinkedMessageTitle'] ?>" readonly/>
</div>
<textarea rows="4" class="form-control js-sms-auto-contents" readonly><?= $typeData['recipientLinkedContents'] ?></textarea>
<?php } else { ?>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['recipientMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][recipientContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['recipientContents'] ?></textarea>
<?php } ?>
</td>
<?php } ?>
<td class="form-inline">
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][memberMode]" value="<?php echo $typeData['memberMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][memberSno]" value="<?php echo $typeData['memberSno']; ?>"/>
<?php if (isset($typeData['memberLinkedContents'])) { ?>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberMessageTitle]" value="<?= gd_isset($typeData['memberMessageTitle']) ?>"/>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberContents]" value="<?= $typeData['memberContents'] ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" class="form-control js-sms-auto-subject" value="<?= $typeData['memberLinkedMessageTitle'] ?>" readonly/>
</div>
<textarea rows="4" class="form-control js-sms-auto-contents" readonly><?= $typeData['memberLinkedContents'] ?></textarea>
<?php } else { ?>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['memberMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][memberContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['memberContents'] ?></textarea>
<?php } ?>
<?php } else { ?>
<div class="text-center text-blue">운영자 전용</div>
<?php } ?>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminMode]" value="<?php echo $typeData['adminMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminSno]" value="<?php echo $typeData['adminSno']; ?>"/>
<?php if (isset($typeData['adminLinkedContents'])) { ?>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][adminMessageTitle]" value="<?= gd_isset($typeData['adminMessageTitle']) ?>"/>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][adminContents]" value="<?= $typeData['adminContents'] ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" class="form-control js-sms-auto-subject" value="<?= $typeData['adminLinkedMessageTitle'] ?>" readonly/>
</div>
<textarea rows="4" class="form-control js-sms-auto-contents" readonly><?= $typeData['adminLinkedContents'] ?></textarea>
<?php } else { ?>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][adminMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['adminMessageTitle']) ?>"/>
</div>
<textarea name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][adminContents]" rows="4"
class="form-control js-sms-auto-contents <?php if ($sKey == 'admin') { ?> width100p <?php } ?>"><?= $typeData['adminContents'] ?></textarea>
<?php } ?>
<?php } else { ?>
<div class="text-center text-orange-red">회원 전용</div>
<?php } ?>
</label>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][providerMode]" value="<?php echo $typeData['providerMode']; ?>"/>
<input type="hidden" name="<?php echo $sKey; ?>[<?php echo $typeData['code']; ?>][providerSno]" value="<?php echo $typeData['providerSno']; ?>"/>
<?php if (isset($typeData['providerLinkedContents'])) { ?>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerMessageTitle]" value="<?= gd_isset($typeData['providerMessageTitle']) ?>"/>
<input type="hidden" name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerContents]" value="<?= $typeData['providerContents'] ?>"/>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" class="form-control js-sms-auto-subject" value="<?= $typeData['providerLinkedMessageTitle'] ?>" readonly/>
</div>
<textarea rows="4" class="form-control js-sms-auto-contents" readonly><?= $typeData['providerLinkedContents'] ?></textarea>
<?php } else { ?>
<div class="js-sms-auto-subject-wrap mgb5">
<input type="text" name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerMessageTitle]" class="form-control js-sms-auto-subject" placeholder="제목 입력" value="<?= gd_isset($typeData['providerMessageTitle']) ?>"/>
</div>
<textarea name="<?= $sKey ?>[<?= $typeData['code'] ?>][providerContents]" rows="4" class="form-control js-sms-auto-contents"><?= $typeData['providerContents'] ?></textarea>
<?php } ?>
<?php } else { ?>
<?php if ($sKey == 'admin') { ?>
<div class="text-center text-orange-red">본사 전용</div>
while (smsAutoStringToByte(text) > <?= $lmsSubjectLimit; ?>) { text = text.substring(0, text.length - 1); }
input.val(text);
});
smsEmoji.bind('#frmSmsAuto textarea');
 
$('.btn-change-password').click(function () {
$.get('../share/layer_sms_password.php?mode=change', function () {
BootstrapDialog.closeAll();
 
Admin/member/sms_send.php+23
변경 전sms_send.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
 
 
 
 
 
 
 
 
 
<script type="text/javascript">
<!--
/**
$('input[name=receiverCnt]').val(receiverCount * pointCnt);
$('input[name=agreeCnt]').val(agreeCount * pointCnt);
$('input[name=rejectCnt]').val(rejectCount * pointCnt);
 
}
 
$(document).ready(function () {
 
var $formSendSms = gd_sms_send.get_form();
 
 
 
 
 
 
 
 
 
 
 
 
 
// 폼 체크
$formSendSms.validate({
dialog: false,
<input type="hidden" name="receiverCnt" value="" />
<input type="hidden" name="agreeCnt" value="" />
<input type="hidden" name="rejectCnt" value="<?=$countSmsFlN?>" />
 
<input type="hidden" name="sendFl" value="sms" />
<input type="hidden" name="rejectSend" value="n" />
<input type="hidden" name="receiverList" value="" />
 
변경 후sms_send.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<script type="text/javascript">
$(function () {
SmsSendGate.applyRestrict('<?= gd_isset($smsSendRestrictType, '') ?>', {
tooltip: '<?= gd_isset($smsSendRestrictTooltip, '') ?>',
selectors: ['#formSendSms input[type="submit"]', '#formSendSms textarea[name="smsContents"]']
});
});
</script>
<script type="text/javascript">
<!--
/**
$('input[name=receiverCnt]').val(receiverCount * pointCnt);
$('input[name=agreeCnt]').val(agreeCount * pointCnt);
$('input[name=rejectCnt]').val(rejectCount * pointCnt);
$('input[name=sendTargetCnt]').val(receiverCount);
}
 
$(document).ready(function () {
 
var $formSendSms = gd_sms_send.get_form();
 
// 점유인증이 입력값 유효성보다 앞이라 submitHandler 로 옮기면 안 된다
$formSendSms.on('click', 'input[type="submit"]', function (event) {
event.preventDefault();
// 발송 건수는 유효성 검증 단계에서 채워지는데 게이트가 그보다 앞이라 여기서 먼저 세운다
setReceiverCount();
SmsSendGate.ensure({
targetCount: parseInt($('input[name=sendTargetCnt]').val()) || 0
}).then(function () {
$formSendSms.submit();
}).catch(function () {});
});
 
// 폼 체크
$formSendSms.validate({
dialog: false,
<input type="hidden" name="receiverCnt" value="" />
<input type="hidden" name="agreeCnt" value="" />
<input type="hidden" name="rejectCnt" value="<?=$countSmsFlN?>" />
<input type="hidden" name="sendTargetCnt" value="" />
<input type="hidden" name="sendFl" value="sms" />
<input type="hidden" name="rejectSend" value="n" />
<input type="hidden" name="receiverList" value="" />
 
Admin/policy/_manage_permission_menu_list.php+1-1
변경 전_manage_permission_menu_list.php
?>
</table>
</div>
<input type="hidden" id="messageSenderMeta" data-editor-approver="<?= !empty($isEditorMessageApprover) ? 'y' : 'n'; ?>" data-editor-representative="<?= !empty($isEditorMessageRepresentative) ? 'y' : 'n'; ?>" data-caller-blocked="<?= !empty($isCallerBlocked) ? 'y' : 'n'; ?>" data-sender-count="<?= (int)(gd_isset($messageSenderCount, 0)); ?>" data-sender-limit="<?= (int)(gd_isset($messageSenderLimit, 0)); ?>"/>
<div class="access-control-reason <?=(!empty($sno) || $changeType == 'init') ? '' : 'display-none'?>" style="clear: both;">
<div class="table-action-dropdown">
<div class="table-action mgt0 mgb15">
 
변경 후_manage_permission_menu_list.php
?>
</table>
</div>
<input type="hidden" id="messageSenderMeta" data-editor-approver="<?= !empty($isEditorMessageApprover) ? 'y' : 'n'; ?>" data-editor-representative="<?= !empty($isEditorMessageRepresentative) ? 'y' : 'n'; ?>" data-caller-blocked="<?= !empty($isCallerBlocked) ? 'y' : 'n'; ?>" data-caller-uncertified-delegate="<?= !empty($isCallerUncertifiedDelegate) ? 'y' : 'n'; ?>" data-sender-count="<?= (int)(gd_isset($messageSenderCount, 0)); ?>" data-sender-limit="<?= (int)(gd_isset($messageSenderLimit, 0)); ?>"/>
<div class="access-control-reason <?=(!empty($sno) || $changeType == 'init') ? '' : 'display-none'?>" style="clear: both;">
<div class="table-action-dropdown">
<div class="table-action mgt0 mgb15">
 
Admin/policy/manage_permission.php+4
변경 전manage_permission.php
dialog_alert('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.', '경고');
return false;
}
 
 
 
 
if ($smsMeta.data('editorRepresentative') === 'y' && $smsMeta.data('callerBlocked') === 'y') {
dialog_alert('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.', '경고');
return false;
 
변경 후manage_permission.php
dialog_alert('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.', '경고');
return false;
}
if ($smsMeta.data('callerUncertifiedDelegate') === 'y') {
dialog_alert('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>본인인증을 완료해야 발신관리위임자 역할을 수행할 수 있습니다.<br>운영자 수정 페이지에서 본인인증을 먼저 완료하시기 바랍니다.', '경고');
return false;
}
if ($smsMeta.data('editorRepresentative') === 'y' && $smsMeta.data('callerBlocked') === 'y') {
dialog_alert('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.', '경고');
return false;
 
Admin/policy/manage_register.php+8-4
변경 전manage_register.php
<tbody>
<?php if (!empty($messageDelegate['visible'])): // SMS 발신 위임자 권한 (권한 설정 테이블 row, 지정/회수는 최고운영자만) ?>
<tr class="js-message-delegate-area">
<th>SMS 발신 위임자 권한 설정</th>
<td class="form-inline">
<input type="hidden" name="messageDelegateAction" id="messageDelegateAction" value="">
<?php if ($messageDelegate['assigned'] === true): ?>
<span class="notice-info">미지정</span>
<?php endif; ?>
<br>
<span class="notice-info">문자를 발송할 '발송담당자' 지정은 위임과 다르므로 <a href="/policy/manage_permission.php" target="_blank" class="btn-link">[설정 > 관리 정책 > 운영자 관리]</a> 페이지의 CRM 추가설정 중 'SMS 발송' 권한을 설정해 주세요.</span>
</td>
</tr>
<?php if ($messageDelegate['assigned'] === false && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-set-reason-row" style="display:none;">
<th>SMS 발신 위임자 권한 설정 사유</th>
<td>
<input type="text" id="messageDelegateSetReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 설정하기 위한 명확한 사유를 입력해 주세요.</span>
<?php endif; ?>
<?php if ($messageDelegate['assigned'] === true && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-release-reason-row" style="display:none;">
<th>SMS 발신 위임자 권한 해제 사유</th>
<td>
<input type="text" id="messageDelegateReleaseReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 해제하기 위한 명확한 사유를 입력해 주세요.</span>
});
});
 
 
 
 
 
$('.js-check-authno').click(function () {
var cell_phone_auth_no = $('input[name=cellPhoneAuthNo]').val();
if (!manage_register.regex.auth_no.test(cell_phone_auth_no)) {
 
변경 후manage_register.php
<tbody>
<?php if (!empty($messageDelegate['visible'])): // SMS 발신 위임자 권한 (권한 설정 테이블 row, 지정/회수는 최고운영자만) ?>
<tr class="js-message-delegate-area">
<th>발신관리위임자 권한 설정</th>
<td class="form-inline">
<input type="hidden" name="messageDelegateAction" id="messageDelegateAction" value="">
<?php if ($messageDelegate['assigned'] === true): ?>
<span class="notice-info">미지정</span>
<?php endif; ?>
<br>
<span class="notice-info">문자를 발송할 '발송담당자' 지정은 위임과 다르므로 <a href="/policy/manage_permission.php" target="_blank" class="btn-link">[설정 > 관리 정책 > 운영자 권한 설정]</a> 페이지의 CRM 추가설정 중 'SMS 발송' 권한을 설정해 주세요.</span>
</td>
</tr>
<?php if ($messageDelegate['assigned'] === false && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-set-reason-row" style="display:none;">
<th>발신관리위임자 권한 설정 사유</th>
<td>
<input type="text" id="messageDelegateSetReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 설정하기 위한 명확한 사유를 입력해 주세요.</span>
<?php endif; ?>
<?php if ($messageDelegate['assigned'] === true && $messageDelegate['editable'] === true): ?>
<tr class="js-message-delegate-release-reason-row" style="display:none;">
<th>발신관리위임자 권한 해제 사유</th>
<td>
<input type="text" id="messageDelegateReleaseReason" class="form-control width-2xl" maxlength="50">
<span class="notice-info">SMS 발송 위임 권한을 해제하기 위한 명확한 사유를 입력해 주세요.</span>
});
});
 
$('input[name="cellPhone"]').on('input', function () {
$('.my-sms-auth').text('(미인증)');
});
 
$('.js-check-authno').click(function () {
var cell_phone_auth_no = $('input[name=cellPhoneAuthNo]').val();
if (!manage_register.regex.auth_no.test(cell_phone_auth_no)) {
 
Admin/policy/manage_security.php+1-2
변경 전manage_security.php
화면보안접속 사용 시 보안접속 인증화면이 노출되는 화면<br>
- 설정 > 관리정책 > 운영자관리/운영자등록<br>
- 설정 > 관리정책 > 운영보안설정<br>
- 설정 > 결제정책 > 무통장입금은행관리<br>
- CRM > 메시지 발송 > 모바일 메시지 발송
</p>
</div>
</td>
 
변경 후manage_security.php
화면보안접속 사용 시 보안접속 인증화면이 노출되는 화면<br>
- 설정 > 관리정책 > 운영자관리/운영자등록<br>
- 설정 > 관리정책 > 운영보안설정<br>
- 설정 > 결제정책 > 무통장입금은행관리
 
</p>
</div>
</td>
 
Admin/promotion/cart_remind_list.php+10
변경 전cart_remind_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
 
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?>
</h3>
menual_send();
}
function menual_send() {
 
 
 
 
 
 
 
 
 
var params = {
mode: 'sendCartRemindManual',
cartRemindNo: cartRemindNo,
 
변경 후cart_remind_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?>
</h3>
menual_send();
}
function menual_send() {
if (window.__cartRemindGatePassed !== true) {
SmsSendGate.ensure().then(function () {
window.__cartRemindGatePassed = true;
menual_send();
window.__cartRemindGatePassed = false;
}).catch(function () {});
return;
}
 
var params = {
mode: 'sendCartRemindManual',
cartRemindNo: cartRemindNo,
 
Admin/promotion/comeback_coupon_list.php+13
변경 전comeback_coupon_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
 
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
<div class="btn-group">
}
});
 
 
 
function sendAction(sno, couponNo) {
var params = {
mode : 'ajaxComebackCouponActionCount',
var smsCount = $('#sms-point').val();
var smsFl = $('#dataInfo' + sno).attr('data-smsFl');
 
 
 
 
 
 
 
 
 
 
 
$.ajax({
method: "POST",
cache: false,
 
변경 후comeback_coupon_list.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/crm/sms-send-gate.js') ?>"></script>
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
<div class="btn-group">
}
});
 
var comebackCouponGatePassed = false;
 
function sendAction(sno, couponNo) {
var params = {
mode : 'ajaxComebackCouponActionCount',
var smsCount = $('#sms-point').val();
var smsFl = $('#dataInfo' + sno).attr('data-smsFl');
 
if (smsFl === 'y' && comebackCouponGatePassed !== true) {
SmsSendGate.ensure().then(function () {
comebackCouponGatePassed = true;
sendAction(sno, couponNo);
comebackCouponGatePassed = false;
}).catch(function () {});
 
return false;
}
 
$.ajax({
method: "POST",
cache: false,
 
Admin/share/layer_message_representative_register_guide.php-155
변경 전layer_message_representative_register_guide.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/manager-certification.js') ?>"></script>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-message-representative-register-guide.css') ?>">
<div class="modal-dialog__content ncua-sms-representative-guide">
<div class="ncua-sms-representative-guide-hero">
<p class="ncua-sms-representative-guide-hero-title"><b>우리 쇼핑몰 번호, 도용되지 않게 지켜드려요</b></p>
<p class="ncua-sms-representative-guide-hero-desc">발신관리책임자와 담당자 정보로 발신번호 사용을 관리해<br/>다른 사람이 SMS를 보내지 못하게 막아줘요.</p>
</div>
<ul class="ncua-sms-representative-guide-list">
<li class="ncua-flex"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>등록은 한 번, 1분이면 완료</li>
<li class="ncua-flex">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>
<span class="ncua-sms-representative-guide-data">
등록 시, 최고운영자 정보가 <em class="ncua-sms-representative-guide-emphasis">통합회원 정보로 자동 변경</em>
<span>등록 이후 이름·휴대폰번호·이메일은 <a href="https://accounts.godo.co.kr/mypage/acc" target="_blank" rel="noopener noreferrer">통합회원 마이페이지</a>에서만 수정할 수 있어요.</span>
</span>
</li>
<li class="ncua-flex">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"></path></svg>
<span class="ncua-sms-representative-guide-data">
26년 10월 25일까지 등록 필수
<span>미등록 시 SMS 관련 기능 이용이 제한됩니다.</span>
</span>
</li>
</ul>
 
<ul class="ncua-sms-representative-guide-caution">
<li>※ <?= !empty($noticeUri) ? '<a href="' . $noticeUri . '" target="_blank">공지사항</a>' : '공지사항' ?>에서 등록 방법, 미등록 시 제한 사항에 대해 자세하게 확인하실 수 있습니다.</li>
<li>※ 급증하는 불법스팸·스미싱 피해 예방을 위한 전기통신사업법 제22조의11 및 전송자격인증 기준 등에 관한 고시(방통위 고시 제2026-1호) 개정에 따른 조치입니다.</li>
</ul>
</div>
<div class="modal-dialog__footer">
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"><input type="checkbox" class="js-hide-representative-guide" /></span>
<span class="ncua-checkbox-field__text">7일동안 보지 않기</span>
</label>
<div>
<button type="button" class="ncua-btn ncua-btn--md ncua-btn--secondary-gray" onclick="layer_close();"><span class="ncua-btn__label">나중에 하기</span></button>
<button type="button" class="ncua-btn ncua-btn--md ncua-btn--primary js-representative-register">등록하기</button>
</div>
</div>
<script>
(function () {
// 안내 레이어 쿠키 규약 — 'false' 저장이 노출 억제를 뜻한다 (layer_legalRequirements.php 와 동일)
var displayCookie = <?= json_encode($displayCookie) ?>;
var expireDay = <?= json_encode($displayCookieExpireDay) ?>;
var isCommerceConnected = <?= json_encode($isCommerceConnected) ?>;
var isSelfCertified = <?= json_encode($isSelfCertified) ?>;
var isCommerceLoginRequired = <?= json_encode($isCommerceLoginRequired) ?>;
$('.js-hide-representative-guide').on('change', function () {
if (this.checked) {
$.cookie(displayCookie, 'false', {expires: expireDay, path: '/'});
return;
}
$.removeCookie(displayCookie, {path: '/'});
});
 
$('.js-representative-register').on('click', function () {
layer_close();
 
// 미연동 최고운영자는 기존 통합회원 연동 프로세스로 보낸다 (복귀 주소는 컨트롤러가 URL 에 담아둔다)
if (!isCommerceConnected) {
NCDSAlert({
message: '발신관리책임자 등록을 위해 통합회원 연동을 진행합니다.',
subMessage: '통합회원 연동을 한 최고운영자만 발신관리책임자 등록이 가능합니다.',
iconType: 'warning',
callback: function () {
location.href = <?= json_encode($commerceLoginUri) ?>;
}
});
return;
}
 
// 이미 본인인증한 운영자도 일반 로그인이면 등록을 진행시키지 않는다 — 로그인 방식은 인증 이력과 무관하게 판정한다
if (isCommerceLoginRequired) {
managerCertification.alertCommerceLoginRequired();
return;
}
 
// 본인인증 이력이 없으면 인증부터 진행한다
if (!isSelfCertified) {
managerCertification.certifyWithGuide({onSuccess: checkBlockAndRegister});
return;
}
 
// 이미 본인인증(솔루션 diValue 존재)한 운영자는 곧바로 등록 흐름을 탄다
checkBlockAndRegister();
});
 
// 인증을 마쳐도 차단된 책임자면 등록을 진행시키지 않는다
function checkBlockAndRegister() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkBlock'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.blocked) {
NCDSAlert({message: response.data.message, subMessage: response.data.subMessage, iconType: 'error'});
return;
}
 
checkMigrationCaller();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 통합회원 전환 전에 등록해 둔 기존 발신관리책임자가 있으면 이관 여부를 묻는다
function checkMigrationCaller() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkMigration'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.exists) {
NCDSConfirm({
message: '발신관리책임자 등록이 완료됐습니다.<br>이전에 사용하던 발신번호를 가져오겠습니까?',
subMessage: '가져오기를 완료해야 새로운 발신번호를 등록할 수 있습니다.',
callback: function (confirmed) {
if (!confirmed) {
showRegisterCompleted();
return;
}
 
SmsCallerHubLayer.openMigration({onCompleted: showRegisterCompleted});
}
});
 
return;
}
 
showRegisterCompleted();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 응답이 비어 있거나 파싱에 실패하면 백단 문구가 없으므로 기본 문구로 안내한다
function showRequestFailed(response) {
NCDSAlert({message: (response && response.message) || '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', iconType: 'error'});
}
 
// 이관하지 않고 끝나는 경로는 모두 등록 완료를 알린다
function showRegisterCompleted() {
NCDSAlert({
message: '발신관리책임자 등록이 완료되었습니다.',
subMessage: "최고운영자 정보(이름·휴대폰번호·이메일)는<br>이제 통합회원 마이페이지에서만 수정할 수 있습니다.<br><br>부운영자와 함께 SMS 업무를 하려면<br>'발신관리위임자'로 설정하거나 'SMS 발송'권한을 부여해 주세요.",
iconType: 'success'
});
}
})();
</script>
 
변경 후layer_message_representative_register_guide.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Admin/share/layer_message_representative_send_restrict.php+135
변경 전layer_message_representative_send_restrict.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_message_representative_send_restrict.php
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-caller-hub-layer.js') ?>"></script>
<script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/manager-certification.js') ?>"></script>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-message-representative-send-restrict.css') ?>">
<div class="modal-dialog__content ncua-sms-period-expire">
<p class="ncua-sms-period-expire-caution"><em>SMS 메뉴 접근과 발송 기능 이용이 제한된 상태입니다.</em></p>
<ul>
<li class="ncua-notice-info">발신관리책임자 등록을 완료하면 즉시 다시 이용할 수 있습니다.</li>
<li class="ncua-notice-info">발신관리책임자 등록은 최고운영자만 할 수 있으며,<br>등록되면 최고운영자 정보가 <a class="ncua-btn--tertiary has-underline" href="https://accounts.godo.co.kr/mypage/acc" target="_blank" rel="noopener noreferrer"><strong>통합회원 정보</strong></a><strong>로 자동 변경</strong>됩니다.</li>
<li class="ncua-notice-info"><?= !empty($guideUri) ? '<a class="ncua-btn--tertiary has-underline" href="' . gd_htmlspecialchars($guideUri) . '" target="_blank">관련 가이드</a>' : '관련 가이드' ?>에서 등록 방법, 미등록 시 제한 사항에 대해 자세하게 확인하실 수 있습니다.</li>
</ul>
<p class="ncua-sms-period-expire-info">※ 「전기통신사업법」 제22조의11 및 「전송자격인증 기준 등에 관한 고시」(방통위 고시 제2026-1호) 2.1 계정관리 기준에 따른 안내입니다.</p>
</div>
<div class="modal-dialog__footer ncua-justify-between">
<label class="ncua-checkbox-field ncua-checkbox-field--xs has-text">
<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"><input type="checkbox" class="js-hide-representative-guide" /></span>
<span class="ncua-checkbox-field__text">7일동안 보지 않기</span>
</label>
<div>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray" onclick="layer_close();"><span class="ncua-btn__label">다음에 등록하기</span></button>
<?php if (!empty($isRegisterAvailable)) { ?>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-representative-register"><span class="ncua-btn__label">발신관리책임자 등록</span></button>
<?php } ?>
</div>
</div>
<script>
(function () {
// 안내 레이어 쿠키 규약 — 'false' 저장이 노출 억제를 뜻한다 (layer_legalRequirements.php 와 동일)
var displayCookie = <?= json_encode($displayCookie) ?>;
var expireDay = <?= json_encode($displayCookieExpireDay) ?>;
var isCommerceConnected = <?= json_encode($isCommerceConnected) ?>;
var isSelfCertified = <?= json_encode($isSelfCertified) ?>;
var isCommerceLoginRequired = <?= json_encode($isCommerceLoginRequired) ?>;
 
$('.js-hide-representative-guide').on('change', function () {
if (this.checked) {
$.cookie(displayCookie, 'false', {expires: expireDay, path: '/'});
return;
}
$.removeCookie(displayCookie, {path: '/'});
});
 
$('.js-representative-register').on('click', function () {
layer_close();
 
// 미연동 최고운영자는 기존 통합회원 연동 프로세스로 보낸다 (복귀 주소는 컨트롤러가 URL 에 담아둔다)
if (!isCommerceConnected) {
NCDSAlert({
message: '발신관리책임자 등록을 위해 통합회원 연동을 진행합니다.',
subMessage: '통합회원 연동을 한 최고운영자만 발신관리책임자 등록이 가능합니다.',
iconType: 'warning',
callback: function () {
location.href = <?= json_encode($commerceLoginUri) ?>;
}
});
return;
}
 
// 이미 본인인증한 운영자도 일반 로그인이면 등록을 진행시키지 않는다 — 로그인 방식은 인증 이력과 무관하게 판정한다
if (isCommerceLoginRequired) {
managerCertification.alertCommerceLoginRequired();
return;
}
 
// 본인인증 이력이 없으면 인증부터 진행한다
if (!isSelfCertified) {
managerCertification.certifyWithGuide({onSuccess: checkBlockAndRegister});
return;
}
 
// 이미 본인인증(솔루션 diValue 존재)한 운영자는 곧바로 등록 흐름을 탄다
checkBlockAndRegister();
});
 
// 인증을 마쳐도 차단된 책임자면 등록을 진행시키지 않는다
function checkBlockAndRegister() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkBlock'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.blocked) {
NCDSAlert({message: response.data.message, subMessage: response.data.subMessage, iconType: 'error'});
return;
}
 
checkMigrationCaller();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 통합회원 전환 전에 등록해 둔 기존 발신관리책임자가 있으면 이관 여부를 묻는다
function checkMigrationCaller() {
$.post('/share/message_representative_register_ps.php', {mode: 'checkMigration'}, function (response) {
if (!response || response.success !== true) {
showRequestFailed(response);
return;
}
 
if (response.data.exists) {
NCDSConfirm({
message: '발신관리책임자 등록이 완료됐습니다.<br>이전에 사용하던 발신번호를 가져오겠습니까?',
subMessage: '가져오기를 완료해야 새로운 발신번호를 등록할 수 있습니다.',
callback: function (confirmed) {
if (!confirmed) {
showRegisterCompleted();
return;
}
 
SmsCallerHubLayer.openMigration({onCompleted: showRegisterCompleted});
}
});
 
return;
}
 
showRegisterCompleted();
}, 'json').fail(function () {
showRequestFailed();
});
}
 
// 응답이 비어 있거나 파싱에 실패하면 백단 문구가 없으므로 기본 문구로 안내한다
function showRequestFailed(response) {
NCDSAlert({message: (response && response.message) || '일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', iconType: 'error'});
}
 
// 이관하지 않고 끝나는 경로는 모두 등록 완료를 알린다
function showRegisterCompleted() {
managerCertification.registerCompletedAlert(function () { window.location.reload(); });
}
})();
</script>
 
Admin/share/layer_occupancy_auth.php+409
변경 전layer_occupancy_auth.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후layer_occupancy_auth.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
?>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'css/layer-occupancy-auth.css') ?>">
<script type="text/javascript">
(function () {
if (BootstrapDialog.occupancyAuthGuarded) {
return;
}
BootstrapDialog.occupancyAuthGuarded = true;
 
BootstrapDialog.closeAll = function () {
$.each(BootstrapDialog.dialogs, function (id, instance) {
var cssClass = instance.getCssClass ? String(instance.getCssClass() || '') : '';
if (cssClass.indexOf('occupancy-auth-dialog') === -1) {
instance.close();
}
});
};
}());
</script>
<?php if (!empty($isLocked)): ?>
<div class="ncua-sms-auth-modal">
<p class="ncua-sms-auth-caution js-occupancy-auth-caution"><strong><?= $lockedMessage ?></strong><br>잠금 해제를 위해서는 본인확인 절차를 진행해야 합니다.</p>
<div class="text-center mgt20">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-layer-close"><span class="ncua-btn__label">닫기</span></button>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-unlock-occupancy-auth"><span class="ncua-btn__label">본인 인증</span></button>
</div>
</div>
 
<script type="text/javascript">
(function () {
var AUTH_CONTEXT = <?= json_encode($authContext, JSON_UNESCAPED_UNICODE) ?>;
 
$('.js-unlock-occupancy-auth').on('click', function () {
$.getScript('<?= PATH_ADMIN_GD_SHARE ?>script/manager-certification.js').done(function () {
managerCertification.certify({
onComplete: function (success, reason) {
if (success === false) {
if (reason === 'mismatch') {
// 불일치는 잠금 모달을 유지한 채 안내 문구만 바꿔 재인증을 유도한다
$('.js-occupancy-auth-caution').html('<strong>인증하신 정보가 등록된 운영자의 정보와 일치하지 않습니다. 다시 인증해 주세요.</strong>');
return;
}
NCDSAlert({message: '본인인증에 실패했습니다. 인증 정보를 확인 후 다시 시도해 주세요.', iconType: 'error'});
return;
}
 
// 발송 화면에서는 리로드하면 작성 중인 발송 내용이 사라지므로 게이트만 다시 태운다
var reloadsAfterAuth = (AUTH_CONTEXT !== 'send' || typeof SmsSendGate === 'undefined');
if (reloadsAfterAuth && typeof window.occupancyAuthCompleted === 'function') {
window.occupancyAuthCompleted();
$('.js-layer-close').trigger('click');
return;
}
$('.js-layer-close').trigger('click');
if (reloadsAfterAuth) {
location.reload();
return;
}
SmsSendGate.ensure({}).catch(function () {});
}
});
});
});
})();
</script>
<?php else: ?>
<div class="ncua-sms-auth-modal">
<details class="ncua-accordion ncua-accordion--blue" open>
<summary>안내사항</summary>
<ul>
<li class="ncua-notice-info">「전기통신사업법」 제22조의11 및 전송자격인증 기준(방송통신위원회 고시)에 따라 계정 탈취 및 무단 사용을 방지하기 위해 <?= gd_htmlspecialchars($authNotice) ?> 적용됩니다. 등록된 인증수단으로 본인 확인을 완료한 후 이용해 주세요.</li>
</ul>
</details>
<div class="ncua-table ncua-table--vertical mgt20">
<table>
<tbody>
<?php if (!empty($hasAuthTarget)): ?>
<tr>
<th><div>인증수단</div></th>
<td>
<div class="ncua-flex-gap">
<?php foreach ($authTypeStates as $authType => $selectable): ?>
<label class="ncua-radio-field ncua-radio-field--xs has-text">
<span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input">
<input type="radio" name="occupancyAuthType" value="<?= gd_htmlspecialchars($authType) ?>" <?= gd_isset($checked['occupancyAuthType'][$authType]); ?> <?= gd_isset($disabled['occupancyAuthType'][$authType]); ?> />
</span>
<span><span class="ncua-radio-field__text"><?= gd_htmlspecialchars(gd_isset($authTypeLabels[$authType], $authType)) ?></span></span>
</label>
<?php endforeach; ?>
</div>
<p class="ncua-radio-field__support-text mgl5">운영자 관리 페이지에서 설정한 인증수단 정보로 인증번호가 발송됩니다.</p>
</td>
</tr>
<tr>
<th><div>인증정보</div></th>
<td>
<div class="ncua-flex ncua-flex-column ncua-align-items-start">
<div class="js-otp-info-line">
<span class="js-auth-target-display"></span>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary js-request-occupancy-otp"><span class="ncua-btn__label">인증번호 발송</span></button>
<span class="ncua-caution-text js-otp-status"></span>
<span class="ncua-caution-text js-resend-notice"></span>
</div>
<div class="js-otp-code-row mgt10" style="display:none;">
<div class="ncua-input ncua-input--xs ncua-input-width-320 js-otp-input">
<div class="ncua-input__content">
<div class="ncua-input__field ncua-input__field--xs">
<input type="text" name="occupancyAuthCode" id="occupancyAuthCode" maxlength="6" placeholder="인증번호를 입력해주세요." autocomplete="off">
<div class="ncua-input__icon-wrap ncua-input__right-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none" class="ncua-input__destructive-icon"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>
</div>
</div>
</div>
</div>
</div>
<p class="ncua-caution-text js-otp-error mgt5" style="display:none;"><em></em></p>
</div>
</td>
</tr>
<?php else: ?>
<tr>
<th><div>인증수단</div></th>
<td><span class="ncua-caution-text js-no-target"><em><?= nl2br(gd_htmlspecialchars($noAuthTargetMessage)) ?></em></span></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="text-center mgt20">
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-layer-close"><span class="ncua-btn__label">닫기</span></button>
<?php if (!empty($hasAuthTarget)): ?>
<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-complete-occupancy-auth"><span class="ncua-btn__label">인증 완료</span></button>
<?php endif; ?>
</div>
</div>
 
<script type="text/javascript">
(function () {
var PS_URL = '../share/occupancy_auth_ps.php';
var LAYER_URL = '../share/layer_occupancy_auth.php';
var CODE_INPUT = '#occupancyAuthCode';
var CODE_PATTERN = /^[0-9]{1,6}$/;
 
var AUTH_CONTEXT = <?= json_encode($authContext, JSON_UNESCAPED_UNICODE) ?>;
 
var popupSession = <?= (int)$popupSessionSeconds ?>;
var otpTtl = <?= (int)$otpTtlSeconds ?>;
var resendInterval = <?= (int)$resendIntervalSeconds ?>;
var authTargets = <?= json_encode($authTypeTargets, JSON_UNESCAPED_UNICODE) ?>;
 
var otpTimer = null;
var restRemain = 0;
var sessionHandle = null;
var resendHandle = null;
var resendRemain = 0;
var lastSentAt = 0;
 
function selectedType() {
return $('input[name="occupancyAuthType"]:checked').val() || 'SMS';
}
 
function updateAuthTargetDisplay() {
$('.js-auth-target-display').text(authTargets[selectedType()] || '');
}
 
function nowSec() {
return Math.floor(Date.now() / 1000);
}
 
function pad2(value) {
return value < 10 ? '0' + value : '' + value;
}
 
function renderStatus() {
$('.js-otp-status').text(pad2(Math.floor(restRemain / 60)) + ':' + pad2(restRemain % 60));
}
 
function renderResendCoolDown() {
$('.js-resend-notice').text(resendInterval + '초 후에 다시 시도해주세요.');
}
 
function clearResendCoolDown() {
resendRemain = 0;
if (resendHandle) {
clearInterval(resendHandle);
resendHandle = null;
}
$('.js-resend-notice').text('');
}
 
function startResendCoolDown() {
clearResendCoolDown();
resendRemain = resendInterval - (nowSec() - lastSentAt);
if (resendRemain <= 0) {
return;
}
renderResendCoolDown();
resendHandle = setInterval(tickResendCoolDown, 1000);
}
 
function tickResendCoolDown() {
resendRemain -= 1;
if (resendRemain <= 0) {
clearResendCoolDown();
return;
}
renderResendCoolDown();
}
 
function tickCountDown() {
restRemain -= 1;
if (restRemain <= 0) {
restRemain = 0;
stopOtpTimer();
$('.js-otp-status').text('00:00');
return;
}
renderStatus();
}
 
function startOtpTimer() {
stopOtpTimer();
restRemain = otpTtl;
renderStatus();
otpTimer = setInterval(tickCountDown, 1000);
}
 
function stopOtpTimer() {
if (otpTimer) {
clearInterval(otpTimer);
otpTimer = null;
}
}
 
function stopTimers() {
stopOtpTimer();
clearResendCoolDown();
if (sessionHandle) {
clearTimeout(sessionHandle);
sessionHandle = null;
}
}
 
function startPopupSession() {
if (sessionHandle) {
clearTimeout(sessionHandle);
}
sessionHandle = setTimeout(function () {
stopTimers();
if (typeof window.occupancyAuthMarkExpired === 'function') {
window.occupancyAuthMarkExpired();
}
// callback 은 [확인] 에서만 실행돼 백드롭 클릭으로 닫으면 만료된 인증 모달이 남는다
NCDSAlert({message: '인증시간이 만료되었습니다. 다시 시도해주세요.', iconType: 'error', onClose: closeLayer});
}, popupSession * 1000);
}
 
function clearError() {
$('.js-otp-input').removeClass('destructive');
$('.js-otp-error').hide().find('em').text('');
}
 
function showError(message) {
$('.js-otp-input').addClass('destructive');
$('.js-otp-error').find('em').text(message);
$('.js-otp-error').show();
}
 
function showSendFailure(message) {
$('.js-otp-error').find('em').text(message);
$('.js-otp-error').show();
}
 
function showResendRequired(message) {
stopOtpTimer();
restRemain = 0;
$('.js-otp-status').text('00:00');
showError(message);
}
 
function showLockedLayer() {
stopTimers();
var $layerBody = $('.js-otp-error').closest('.bootstrap-dialog-message');
if ($layerBody.length === 0) {
closeLayer();
return;
}
$.ajax({
url: LAYER_URL,
data: {authContext: AUTH_CONTEXT},
cache: false,
dataType: 'html'
}).done(function (lockedHtml) {
$layerBody.html(lockedHtml);
}).fail(closeLayer);
}
 
function resetToInitial() {
stopOtpTimer();
restRemain = 0;
lastSentAt = 0;
clearResendCoolDown();
$('.js-otp-code-row').hide();
clearError();
$(CODE_INPUT).val('');
$('.js-otp-status').text('');
$('.js-request-occupancy-otp').removeClass('ncua-btn--secondary-gray').addClass('ncua-btn--secondary');
$('.js-request-occupancy-otp .ncua-btn__label').text('인증번호 발송');
}
 
function requestOtp() {
if (lastSentAt > 0 && nowSec() - lastSentAt < resendInterval) {
startResendCoolDown();
return;
}
clearError();
$.post(PS_URL, {mode: 'sendOtp', authType: selectedType(), authContext: AUTH_CONTEXT}, function (data) {
if (data.success === false) {
if ((data.data || {}).locked) {
showLockedLayer();
return;
}
if ((data.data || {}).sendBlocked) {
NCDSAlert({message: data.message, iconType: 'error'});
return;
}
showSendFailure(data.message);
return;
}
lastSentAt = nowSec();
clearError();
$('.js-otp-code-row').show();
$(CODE_INPUT).val('').focus();
$('.js-request-occupancy-otp').removeClass('ncua-btn--secondary').addClass('ncua-btn--secondary-gray');
$('.js-request-occupancy-otp .ncua-btn__label').text('재전송');
startOtpTimer();
}, 'json');
}
 
function completeAuth() {
var code = $(CODE_INPUT).val();
if (code === '') {
showError('인증번호를 입력해 주세요.');
$(CODE_INPUT).focus();
return;
}
if (CODE_PATTERN.test(code) === false) {
showError('인증번호가 일치하지 않습니다.');
$(CODE_INPUT).focus();
return;
}
 
$.post(PS_URL, {mode: 'verifyOtp', code: code, authContext: AUTH_CONTEXT}, function (data) {
if (data.success) {
if (typeof window.occupancyAuthCompleted === 'function') {
window.occupancyAuthCompleted();
}
closeLayer();
return;
}
 
var result = data.data || {};
if (result.expired) {
stopTimers();
NCDSAlert({message: data.message, iconType: 'error', onClose: closeLayer});
return;
}
if (result.locked) {
showLockedLayer();
return;
}
if (result.resendRequired) {
showResendRequired(data.message);
return;
}
showError(data.message);
$(CODE_INPUT).focus();
}, 'json');
}
 
function closeLayer() {
stopTimers();
$('.js-layer-close').trigger('click');
}
 
$('.js-layer-close').on('click', stopTimers);
$('.js-request-occupancy-otp').on('click', requestOtp);
$('.js-complete-occupancy-auth').on('click', completeAuth);
$(CODE_INPUT).on('input', clearError);
$('input[name="occupancyAuthType"]').on('change', function () {
updateAuthTargetDisplay();
resetToInitial();
});
updateAuthTargetDisplay();
if (window.occupancyAuthCleanup) {
window.occupancyAuthCleanup();
}
startPopupSession();
window.occupancyAuthCleanup = stopTimers;
})();
</script>
<?php endif; ?>
 
Admin/share/layer_sms_contents.php+16-5
변경 전layer_sms_contents.php
 
 
<div>
<div class="table-title gd-help-manual">SMS / LMS 문구 등록</div>
<table class="table table-cols">
<tr class="text-center">
<td><input name="sno[]" type="checkbox" value="<?php echo $val['sno']; ?>" /></td>
<td class="font-num"><?php echo number_format($page->idx--); ?></td>
<td><?php echo $arrGroup[$val['smsAutoCode']]; ?></td>
<td class="smsContents-<?php echo $val['sno']; ?>"><?php echo $val['contents']; ?></td>
<td><input type="button" value="선택" class="btn btn-sm btn-black js-sms-contents-select" data-sno="<?php echo $val['sno']; ?>" /></span></td>
</tr>
// 글자수 체크
$('textarea[name=smsContentsText]').keyup(getSmsContentsLength).change(getSmsContentsLength);
 
 
 
// SMS 문구 등록
$('.js-sms-contents-register').click(function (e) {
var smsContentsGroup = $('select[name=\'smsContentsGroup\']').val();
var smsContentsText = $('textarea[name=\'smsContentsText\']').val();
 
if (smsContentsGroup == '' || smsContentsGroup == '0') {
alert('그룹을 선택해 주세요.');
return;
}
 
if (smsContentsText == '') {
alert('내용을 작성해 주세요.');
return;
}
 
if (data == 'OK') {
layer_list_search('page=<?php echo $search['page'];?>');
} else {
alert('등록시 오류가 발생하였습니다.');
}
});
});
if (data == 'OK') {
layer_list_search('page=<?php echo $search['page'];?>');
} else {
alert('삭제시 오류가 발생하였습니다.');
 
 
 
 
 
 
 
}
});
}
 
변경 후layer_sms_contents.php
<script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/sms/sms-emoji.js') ?>"></script>
 
<div>
<div class="table-title gd-help-manual">SMS / LMS 문구 등록</div>
<table class="table table-cols">
<tr class="text-center">
<td><input name="sno[]" type="checkbox" value="<?php echo $val['sno']; ?>" /></td>
<td class="font-num"><?php echo number_format($page->idx--); ?></td>
<td><?= $arrGroup[$val['smsAutoCode']] ?? '' ?></td>
<td class="smsContents-<?php echo $val['sno']; ?>"><?php echo $val['contents']; ?></td>
<td><input type="button" value="선택" class="btn btn-sm btn-black js-sms-contents-select" data-sno="<?php echo $val['sno']; ?>" /></span></td>
</tr>
// 글자수 체크
$('textarea[name=smsContentsText]').keyup(getSmsContentsLength).change(getSmsContentsLength);
 
smsEmoji.bind('textarea[name=smsContentsText]', getSmsContentsLength);
 
// SMS 문구 등록
$('.js-sms-contents-register').click(function (e) {
var smsContentsGroup = $('select[name=\'smsContentsGroup\']').val();
var smsContentsText = $('textarea[name=\'smsContentsText\']').val();
 
if (smsContentsGroup == '' || smsContentsGroup == '0') {
NCDSAlert({message: '그룹을 선택해 주세요.'});
return;
}
 
if (smsContentsText == '') {
NCDSAlert({message: '내용을 작성해 주세요.'});
return;
}
 
if (data == 'OK') {
layer_list_search('page=<?php echo $search['page'];?>');
} else {
NCDSAlert({message: data && data != 'ERROR' ? data : '등록시 오류가 발생하였습니다.', iconType: 'error'});
}
});
});
if (data == 'OK') {
layer_list_search('page=<?php echo $search['page'];?>');
} else {
// 자동알림 사용 건만 제외하고 삭제된 경우에도 문구가 오므로 닫을 때 목록을 다시 읽는다
NCDSAlert({
message: data && data != 'ERROR' ? data : '삭제시 오류가 발생하였습니다.',
iconType: 'error',
onClose: function () {
layer_list_search('page=<?php echo $search['page'];?>');
}
});
}
});
}
 
Component/Database/DBTableField.php+5
변경 전DBTableField.php
// @formatter:off
$arrField = [
['val' => 'sno', 'typ' => 'i', 'def' => null], // 일련번호
 
 
 
 
 
['val' => 'smsType', 'typ' => 's', 'def' => 'user'], // SMS 전송타입 (order, member, promotion, user)
['val' => 'smsAutoCode', 'typ' => 's', 'def' => null], // SMS 자동발송 코드
['val' => 'smsAutoType', 'typ' => 's', 'def' => null], // SMS 자동발송 수신 종류 (member, admin, provider)
 
변경 후DBTableField.php
// @formatter:off
$arrField = [
['val' => 'sno', 'typ' => 'i', 'def' => null], // 일련번호
['val' => 'templateCode', 'typ' => 's', 'def' => null],
['val' => 'templateCategoryCode', 'typ' => 's', 'def' => null],
['val' => 'referenceSmsContentNo', 'typ' => 'i', 'def' => null],
['val' => 'approvalStatus', 'typ' => 's', 'def' => 'PENDING'],
['val' => 'autoConversionFl', 'typ' => 's', 'def' => 'n'],
['val' => 'smsType', 'typ' => 's', 'def' => 'user'], // SMS 전송타입 (order, member, promotion, user)
['val' => 'smsAutoCode', 'typ' => 's', 'def' => null], // SMS 자동발송 코드
['val' => 'smsAutoType', 'typ' => 's', 'def' => null], // SMS 자동발송 수신 종류 (member, admin, provider)
 
Component/Member/Manager.php+17-4
변경 전Manager.php
if ($sessionManager['managerId'] === $arrData['managerId']) {
$sessionManager['isSmsAuth'] = $arrData['isSmsAuth'];
$sessionManager['isEmailAuth'] = $arrData['isEmailAuth'];
$sessionManager['email'] = $arrData['email'];
$sessionManager['cellPhone'] = $arrData['cellPhone'];
 
 
 
 
$session->set(self::SESSION_MANAGER_LOGIN, $sessionManager);
}
 
if ($smsRoleGuard->isDelegateOrHigher() === false) {
throw new Exception(__('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.'));
}
 
 
 
if ($smsRoleGuard->isRepresentative() && $smsRoleService->isBlockedRepresentative()) {
throw new Exception(__('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.'));
}
*/
protected function setAuthenticationInfo(&$arrData, $smsAuth, $emailAuth)
{
 
 
 
 
 
 
if ($smsAuth['isAdminSmsAuth'] === true) {
if (str_replace('-', '', $smsAuth['authCellPhone']) !== str_replace('-', '', $arrData['cellPhone'])) {
throw new ManagerAuthException(ManagerAuthException::CODE_NOT_EQUALS_CELLPHONE);
}
$arrData['isSmsAuth'] = 'y';
 
$adminSecuritySmsAuthNumber = Otp::getOtp(8);
$contents = sprintf(__('관리자님 인증번호는 [%s] 입니다.'), $adminSecuritySmsAuthNumber);
$isSent = \App::getInstance(OccupancyOtpSmsService::class)->send($managerInfo['cellPhone'], $contents, $adminSecuritySmsAuthNumber);
 
$smsAuth['smsAuthNumber'] = $adminSecuritySmsAuthNumber;
$smsAuth['message'] = $isSent ? 'OK' : 'SMS Send Fail';
 
변경 후Manager.php
if ($sessionManager['managerId'] === $arrData['managerId']) {
$sessionManager['isSmsAuth'] = $arrData['isSmsAuth'];
$sessionManager['isEmailAuth'] = $arrData['isEmailAuth'];
if (array_key_exists('email', $arrData)) {
$sessionManager['email'] = $arrData['email'];
}
if (array_key_exists('cellPhone', $arrData)) {
$sessionManager['cellPhone'] = $arrData['cellPhone'];
}
$session->set(self::SESSION_MANAGER_LOGIN, $sessionManager);
}
 
if ($smsRoleGuard->isDelegateOrHigher() === false) {
throw new Exception(__('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>해당 권한을 해제한 후 다시 저장해 주세요.'));
}
if ($smsRoleService->isUncertifiedDelegateActor()) {
throw new Exception(__('SMS 발송 권한은 발신관리책임자 또는 발신관리위임자만 부여할 수 있습니다.<br>본인인증을 완료해야 발신관리위임자 역할을 수행할 수 있습니다.<br>운영자 수정 페이지에서 본인인증을 먼저 완료하시기 바랍니다.'));
}
if ($smsRoleGuard->isRepresentative() && $smsRoleService->isBlockedRepresentative()) {
throw new Exception(__('SMS 발송 메뉴 접근이 제한되었습니다.<br>스팸 또는 부정 사용으로 차단되어 접근할 수 없습니다.<br>자세한 사항은 1:1문의로 문의하시기 바랍니다.'));
}
*/
protected function setAuthenticationInfo(&$arrData, $smsAuth, $emailAuth)
{
$isAuthCellPhoneReplaced = $smsAuth['isAdminSmsAuth'] === true
&& str_replace('-', '', $smsAuth['authCellPhone']) !== str_replace('-', '', $arrData['cellPhone']);
// 수정 저장에서 점유인증 뒤 번호를 다시 고치면 미인증으로 보고 기존 번호를 유지한다
if ($isAuthCellPhoneReplaced && ($arrData['mode'] ?? '') === 'modify') {
$smsAuth['isAdminSmsAuth'] = false;
}
if ($smsAuth['isAdminSmsAuth'] === true) {
if ($isAuthCellPhoneReplaced) {
throw new ManagerAuthException(ManagerAuthException::CODE_NOT_EQUALS_CELLPHONE);
}
$arrData['isSmsAuth'] = 'y';
 
$adminSecuritySmsAuthNumber = Otp::getOtp(8);
$contents = sprintf(__('관리자님 인증번호는 [%s] 입니다.'), $adminSecuritySmsAuthNumber);
$isSent = \App::getInstance(OccupancyOtpSmsService::class)->send($managerInfo['cellPhone'], $contents);
 
$smsAuth['smsAuthNumber'] = $adminSecuritySmsAuthNumber;
$smsAuth['message'] = $isSent ? 'OK' : 'SMS Send Fail';
 
Component/Sms/SmsAdmin.php+61-53
변경 전SmsAdmin.php
use Origin\Service\Crm\Message\SmsPersonalInfoAccessLogService;
use Respect\Validation\Validator;
use Origin\Service\Member\CrmGroupService;
 
 
 
 
 
/**
* SMS 관리자 관련 처리
// CLI(스케줄러) 세션은 SessionManager 가 아니라 발행 서비스 생성이 매번 실패하고 수신대상 루프 안에서 호출돼, 첫 실패 후에는 같은 프로세스에서 재시도·재로그하지 않는다
protected static $personalInfoAccessLogPublishFailed = false;
 
 
 
 
 
 
 
 
 
public function __construct($config = [])
{
// 기존 DBTool 파라미터를 보장하기 위한 로직
*/
public function saveSmsAuto($request)
{
// 디비에 저장될 항목을 정리 (insert , update)
$smsAutoReceiveKind = Sms::SMS_AUTO_RECEIVE_LIST;
$smsAutoType = [
'member',
'admin',
'provider',
'board',
'recipient',
];
foreach ($smsAutoReceiveKind as $smsType => $val) {
foreach ($request[$smsType] as $autoCode => $autoData) {
foreach ($smsAutoType as $autoType) {
if (isset($autoData[$autoType . 'Mode'])) {
$dbData[$autoData[$autoType . 'Mode']][$smsType][$autoCode][$autoType]['sno'] = $autoData[$autoType . 'Sno'];
$dbData[$autoData[$autoType . 'Mode']][$smsType][$autoCode][$autoType]['subject'] = $autoData['subject'];
$dbData[$autoData[$autoType . 'Mode']][$smsType][$autoCode][$autoType]['contents'] = $autoData[$autoType . 'Contents'];
$dbData[$autoData[$autoType . 'Mode']][$smsType][$autoCode][$autoType]['messageTitle'] = gd_isset($autoData[$autoType . 'MessageTitle'], '');
// $dbData[$autoData[$autoType . 'Mode']][$smsType][$autoCode][$autoType]['smsType'] = $smsType;
unset($request[$smsType][$autoCode][$autoType . 'Mode']);
unset($request[$smsType][$autoCode][$autoType . 'Sno']);
unset($request[$smsType][$autoCode]['subject']);
unset($request[$smsType][$autoCode][$autoType . 'Contents']);
unset($request[$smsType][$autoCode][$autoType . 'MessageTitle']);
}
}
}
}
// 디비 정보 저장
foreach ($dbData as $saveMode => $tmpData1) {
foreach ($tmpData1 as $smsType => $tmpData2) {
foreach ($tmpData2 as $smsAutoCode => $tmpData3) {
foreach ($tmpData3 as $smsAutoType => $saveData) {
$arrBind = [];
// 저장 방법
if ($saveMode === 'insert') {
$saveData['groupNm'] = __('자동발송');
$saveData['smsAutoCode'] = $smsAutoCode;
$saveData['smsAutoType'] = $smsAutoType;
$saveData['smsType'] = $smsType;
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $saveData, 'insert');
$this->db->set_insert_db(DB_SMS_CONTENTS, $arrBind['param'], $arrBind['bind'], 'y');
} elseif ($saveMode === 'update') {
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $saveData, 'update', gd_array_keys($saveData));
$this->db->bind_param_push($arrBind['bind'], 'i', $saveData['sno']);
$this->db->set_update_db(DB_SMS_CONTENTS, $arrBind['param'], 'sno=?', $arrBind['bind']);
}
unset($arrBind);
}
}
}
}
 
// 기 저장된 항목 삭제
unset($request['mode'], $request['smsCallNum']);
} else {
$this->db->strWhere = 'smsType IN (\'order\', \'member\', \'promotion\', \'board\', \'regular\', \'present\') AND smsAutoCode IS NOT NULL AND smsAutoType IS NOT NULL';
}
$this->db->strOrder = 'smsType ASC';
 
$query = $this->db->query_complete();
 
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . array_shift($query) . ' ' . gd_implode(' ', $query);
$getData = gd_htmlspecialchars_stripslashes($this->db->query_fetch($strSQL, null, false));
foreach ($getData as $data) {
 
 
 
 
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['contents'] = $data['contents'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['messageTitle'] = $data['messageTitle'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['sno'] = $data['sno'];
 
 
}
unset($getData);
// SMS 자동발송 설정값 불러오기
$smsAutoType[$keyType][$key][$sendType . 'Title'] = $messageTitle; // CRM 뷰 호환(messageTitle 일원화)
$smsAutoType[$keyType][$key][$sendType . 'Mode'] = 'update';
$smsAutoType[$keyType][$key][$sendType . 'Sno'] = $setData[$keyType][$val['code']][$sendType]['sno'];
 
 
} else {
$smsAutoType[$keyType][$key][$sendType . 'Contents'] = $val[$sendType . 'Contents'];
$smsAutoType[$keyType][$key][$sendType . 'MessageTitle'] = '';
$smsAutoType[$keyType][$key][$sendType . 'Title'] = ''; // CRM 뷰 호환
$smsAutoType[$keyType][$key][$sendType . 'Mode'] = 'insert';
$smsAutoType[$keyType][$key][$sendType . 'Sno'] = '';
 
 
}
}
 
// 기본 검색
$arrWhere[] = 'smsType = ?';
$this->db->bind_param_push($arrBind, 's', $smsType);
 
 
 
// 키워드 검색
if ($search['key'] && $search['keyword']) {
}
$postValue['smsType'] = 'user';
StringUtils::strIsSet($postValue['subject'], '');
 
 
 
 
 
 
 
if (gd_array_key_exists('sno', $postValue)) {
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $postValue, 'update');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$this->db->bind_param_push($arrBind['bind'], 'i', $postValue['sno']);
$this->db->set_update_db(DB_SMS_CONTENTS, $arrBind['param'], 'sno = ?', $arrBind['bind']);
 
 
 
} else {
 
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $postValue, 'insert');
$this->db->set_insert_db(DB_SMS_CONTENTS, $arrBind['param'], $arrBind['bind'], 'y');
 
}
 
return true;
 
변경 후SmsAdmin.php
use Origin\Service\Crm\Message\SmsPersonalInfoAccessLogService;
use Respect\Validation\Validator;
use Origin\Service\Member\CrmGroupService;
use Origin\Enum\Crm\Message\SmsTemplateApprovalStatus;
use Origin\Repository\Member\Sms\SmsContentsRepository;
use Origin\Service\Crm\Message\SmsTemplateService;
use Origin\Service\Sms\AutoSend\SaveAutoSendContentsService;
 
/**
* SMS 관리자 관련 처리
// CLI(스케줄러) 세션은 SessionManager 가 아니라 발행 서비스 생성이 매번 실패하고 수신대상 루프 안에서 호출돼, 첫 실패 후에는 같은 프로세스에서 재시도·재로그하지 않는다
protected static $personalInfoAccessLogPublishFailed = false;
 
const SMS_AUTO_TYPES = [
'member',
'admin',
'provider',
'board',
'recipient',
];
 
public function __construct($config = [])
{
// 기존 DBTool 파라미터를 보장하기 위한 로직
*/
public function saveSmsAuto($request)
{
/** @var SaveAutoSendContentsService $saveAutoSendContentsService */
$saveAutoSendContentsService = \App::getInstance(SaveAutoSendContentsService::class);
$request = $saveAutoSendContentsService->saveAutoSendContents(
$request,
array_keys(Sms::SMS_AUTO_RECEIVE_LIST),
self::SMS_AUTO_TYPES
);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 기 저장된 항목 삭제
unset($request['mode'], $request['smsCallNum']);
} else {
$this->db->strWhere = 'smsType IN (\'order\', \'member\', \'promotion\', \'board\', \'regular\', \'present\') AND smsAutoCode IS NOT NULL AND smsAutoType IS NOT NULL';
}
$this->db->strOrder = 'smsType ASC, sno ASC';
 
$query = $this->db->query_complete();
 
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . array_shift($query) . ' ' . gd_implode(' ', $query);
$getData = gd_htmlspecialchars_stripslashes($this->db->query_fetch($strSQL, null, false));
foreach ($getData as $data) {
if ((int)($data['referenceSmsContentNo'] ?? 0) === SmsContentsRepository::CLEARED_REFERENCE_SMS_CONTENT_NO) {
$data['contents'] = '';
$data['messageTitle'] = '';
}
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['contents'] = $data['contents'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['messageTitle'] = $data['messageTitle'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['sno'] = $data['sno'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['templateCategoryCode'] = $data['templateCategoryCode'];
$setData[$data['smsType']][$data['smsAutoCode']][$data['smsAutoType']]['referenceSmsContentNo'] = $data['referenceSmsContentNo'] ?? null;
}
unset($getData);
// SMS 자동발송 설정값 불러오기
$smsAutoType[$keyType][$key][$sendType . 'Title'] = $messageTitle; // CRM 뷰 호환(messageTitle 일원화)
$smsAutoType[$keyType][$key][$sendType . 'Mode'] = 'update';
$smsAutoType[$keyType][$key][$sendType . 'Sno'] = $setData[$keyType][$val['code']][$sendType]['sno'];
$smsAutoType[$keyType][$key][$sendType . 'TemplateCategoryCode'] = gd_isset($setData[$keyType][$val['code']][$sendType]['templateCategoryCode'], '');
$smsAutoType[$keyType][$key][$sendType . 'ReferenceSmsContentNo'] = gd_isset($setData[$keyType][$val['code']][$sendType]['referenceSmsContentNo'], '');
} else {
$smsAutoType[$keyType][$key][$sendType . 'Contents'] = $val[$sendType . 'Contents'];
$smsAutoType[$keyType][$key][$sendType . 'MessageTitle'] = '';
$smsAutoType[$keyType][$key][$sendType . 'Title'] = ''; // CRM 뷰 호환
$smsAutoType[$keyType][$key][$sendType . 'Mode'] = 'insert';
$smsAutoType[$keyType][$key][$sendType . 'Sno'] = '';
$smsAutoType[$keyType][$key][$sendType . 'TemplateCategoryCode'] = '';
$smsAutoType[$keyType][$key][$sendType . 'ReferenceSmsContentNo'] = '';
}
}
 
// 기본 검색
$arrWhere[] = 'smsType = ?';
$this->db->bind_param_push($arrBind, 's', $smsType);
$arrWhere[] = 'approvalStatus = ?';
$this->db->bind_param_push($arrBind, 's', SmsTemplateApprovalStatus::APPROVED->value);
 
// 키워드 검색
if ($search['key'] && $search['keyword']) {
}
$postValue['smsType'] = 'user';
StringUtils::strIsSet($postValue['subject'], '');
/** @var SmsTemplateService $smsTemplateService */
$smsTemplateService = \App::getInstance(SmsTemplateService::class);
$postValue = $smsTemplateService->normalizeSendText(
$postValue,
gd_array_key_exists('sno', $postValue) ? (int)$postValue['sno'] : null
);
$contents = $postValue['contents'];
if (gd_array_key_exists('sno', $postValue)) {
$smsTemplateService->assertSavable((int)$postValue['sno']);
$modifyResult = $smsTemplateService->resolveModifyResult(
(int)$postValue['sno'],
$contents,
isset($postValue['subject']) ? (string)$postValue['subject'] : null,
isset($postValue['messageTitle']) ? (string)$postValue['messageTitle'] : null
);
if ($modifyResult['status'] !== null) {
$postValue['approvalStatus'] = $modifyResult['status'];
}
if ($modifyResult['contentsChanged'] === false) {
unset($postValue['contents']);
}
if (!$modifyResult['messageTitleChanged']) {
unset($postValue['messageTitle']);
}
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $postValue, 'update', gd_array_keys($postValue));
$this->db->bind_param_push($arrBind['bind'], 'i', $postValue['sno']);
$this->db->set_update_db(DB_SMS_CONTENTS, $arrBind['param'], 'sno = ?', $arrBind['bind']);
if ($modifyResult['modified']) {
$smsTemplateService->writeModifiedHistory((int)$postValue['sno']);
}
} else {
$postValue['approvalStatus'] = SmsTemplateApprovalStatus::PENDING->value;
$arrBind = $this->db->get_binding(DBTableField::tableSmsContents(), $postValue, 'insert');
$this->db->set_insert_db(DB_SMS_CONTENTS, $arrBind['param'], $arrBind['bind'], 'y');
$smsTemplateService->issueTemplateCodeWithHistory((int)$this->db->insert_id());
}
 
return true;
 
Component/Sms/SmsAuto.php+43-4
변경 전SmsAuto.php
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Enum\Crm\Message\FailedMessageSendType;
use Origin\Enum\Crm\Message\SmsAutoSendOverType;
 
use Origin\Service\AutoSend\AutoSendMyappService;
use Origin\Service\AutoSend\AutoSendTemplateVariableReplacer;
use Origin\Service\AutoSend\FindAutoSendConfigService;
{
$logger = $this->logger;
$binds = [];
$this->db->strField = 'smsType, smsAutoType, contents, subject, messageTitle';
$this->db->strJoin = DB_SMS_CONTENTS;
$this->db->strWhere = 'smsType= ? AND smsAutoCode = ? AND smsAutoType IN (\'' . gd_implode('\', \'', $wheres) . '\') AND contents != \'\' AND contents IS NOT NULL';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$this->db->bind_param_push($binds, 's', $this->smsType);
$this->db->bind_param_push($binds, 's', $this->smsAutoCodeType);
 
 
$query = $this->db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . array_shift($query) . ' ' . gd_implode(' ', $query);
$getData = $this->db->query_fetch($strSQL, $binds);
throw new \Exception(__('자동발송 내용을 찾을 수 없습니다.'));
}
$getData = StringUtils::htmlSpecialCharsStripSlashes($getData);
 
 
 
 
 
return $getData;
}
 
/**
 
변경 후SmsAuto.php
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Enum\Crm\Message\FailedMessageSendType;
use Origin\Enum\Crm\Message\SmsAutoSendOverType;
use Origin\Enum\Crm\Message\SmsTemplateApprovalStatus;
use Origin\Service\AutoSend\AutoSendMyappService;
use Origin\Service\AutoSend\AutoSendTemplateVariableReplacer;
use Origin\Service\AutoSend\FindAutoSendConfigService;
{
$logger = $this->logger;
$binds = [];
$approvedStatus = SmsTemplateApprovalStatus::APPROVED->value;
$latestApprovedOf = static fn(string $column): string => 'SELECT stah.' . $column
. ' FROM ' . DB_SMS_TEMPLATE_APPROVAL_HISTORY . ' stah'
. ' WHERE stah.smsContentNo = rsc.sno'
. ' AND stah.approvalStatus = ?'
. ' ORDER BY stah.historyNo DESC LIMIT 1';
$latestApproved = $latestApprovedOf('templateContent');
$latestApprovedTitle = $latestApprovedOf('messageTitle');
$isApproved = 'rsc.approvalStatus = ?';
 
$field = [];
$field[] = 'sc.smsType';
$field[] = 'sc.smsAutoType';
$field[] = 'IF(' . $isApproved . ', rsc.contents, (' . $latestApproved . ')) AS contents';
$field[] = 'sc.subject';
$field[] = 'IF(' . $isApproved . ', rsc.messageTitle, COALESCE((' . $latestApprovedTitle . '), rsc.messageTitle)) AS messageTitle';
 
$where = [];
$where[] = 'sc.smsType = ?';
$where[] = 'sc.smsAutoCode = ?';
$where[] = 'sc.smsAutoType IN (\'' . gd_implode('\', \'', $wheres) . '\')';
$where[] = 'rsc.contents != \'\'';
$where[] = 'rsc.contents IS NOT NULL';
$where[] = '(' . $isApproved . ' OR (' . $latestApproved . ') IS NOT NULL)';
 
$this->db->strField = gd_implode(', ', $field);
$this->db->strJoin = DB_SMS_CONTENTS . ' sc INNER JOIN ' . DB_SMS_CONTENTS . ' rsc ON rsc.sno = COALESCE(sc.referenceSmsContentNo, sc.sno)';
$this->db->strWhere = gd_implode(' AND ', $where);
$this->db->strOrder = 'sc.sno ASC';
 
// query_complete() 가 SELECT → FROM → WHERE 순으로 조립하므로 바인드도 그 순서로 밀어 넣는다
$this->db->bind_param_push($binds, 's', $approvedStatus);
$this->db->bind_param_push($binds, 's', $approvedStatus);
$this->db->bind_param_push($binds, 's', $approvedStatus);
$this->db->bind_param_push($binds, 's', $approvedStatus);
$this->db->bind_param_push($binds, 's', $this->smsType);
$this->db->bind_param_push($binds, 's', $this->smsAutoCodeType);
$this->db->bind_param_push($binds, 's', $approvedStatus);
$this->db->bind_param_push($binds, 's', $approvedStatus);
$query = $this->db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . array_shift($query) . ' ' . gd_implode(' ', $query);
$getData = $this->db->query_fetch($strSQL, $binds);
throw new \Exception(__('자동발송 내용을 찾을 수 없습니다.'));
}
$getData = StringUtils::htmlSpecialCharsStripSlashes($getData);
$latestByAutoType = [];
foreach ($getData as $row) {
$latestByAutoType[$row['smsAutoType']] = $row;
}
 
return array_values($latestByAutoType);
}
 
/**
 
Controller/Admin/Crm/AutoSendConfigController.php-192
변경 전AutoSendConfigController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use App;
use Bundle\Component\Sms\Code;
use Bundle\Component\Sms\SmsAutoCode;
use Bundle\Controller\Admin\Controller;
use Framework\Debug\Exception\AlertRedirectException;
use GuzzleHttp\Exception\ConnectException;
use Origin\Enum\AutoSend\AutoSendChannelPolicy;
use Origin\Enum\AutoSend\AutoSendRecipient;
use Origin\Enum\AutoSend\AutoSendRecipientPolicy;
use Origin\Enum\AutoSend\AutoSendOptionVisibilityPolicy;
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Enum\AutoSend\AutoSendChannel;
use Origin\Enum\AutoSend\AutoSendTemplateVariableCatalog;
use Origin\Service\AutoSend\FindAutoSendCodeConfigService;
use Origin\Service\AutoSend\FindAutoSendConfigService;
use Origin\Service\Crm\Message\MyappPushTemplateService;
use Origin\Service\Crm\Message\SmsPasswordService;
use Request;
 
class AutoSendConfigController extends Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$code = Request::get()->get('code');
$isBlock = false;
$blockMode = null;
$validAutoSendSms = false;
 
if (!AutoSendSupport::canSendIfClaim($code)) {
$isBlock = true;
$blockMode = 'claim';
}
 
try {
$validAutoSendSms = $this->validAutoSendSms();
} catch (ConnectException) {
$isBlock = true;
$blockMode = 'smsConnect';
}
 
$this->applyBlockPage();
 
if ($isBlock) {
$this->setData('blockMode', $blockMode);
} else {
/** @var SmsAutoCode $smsAutoCode */
$smsAutoCode = \App::load('\\Component\\Sms\\SmsAutoCode');
$groupCode = $smsAutoCode->getByCode($code);
 
$recipients = AutoSendRecipientPolicy::getCodesWithRecipientsWithBoard()[$code] ?? [];
$channels = AutoSendChannelPolicy::getCodesWithChannelsWithBoard()[$code] ?? [];
 
/** @var FindAutoSendCodeConfigService $codeConfigService */
$codeConfigService = App::getInstance(FindAutoSendCodeConfigService::class);
[$options, $autoSendConfigsResponse, $channelCodeConfigDto] = $codeConfigService->createRecipientConfigByCode($code);
 
/** @var FindAutoSendConfigService $configService */
$configService = App::getInstance(FindAutoSendConfigService::class);
$crmMyappConfig = $configService->getCrmMyappConfig();
$kakaoCrmConfig = $configService->getCrmKakaoConfig();
$autoSendOptionVisibility = AutoSendOptionVisibilityPolicy::getOptionVisibilityByCode($code)
?? AutoSendOptionVisibilityPolicy::getDefaultBoardVisibility($code);
$checkedRecipients = $this->collectGroupsHavingAutoSendEnabled($autoSendConfigsResponse);
$shouldChangeKakaoTemplate = in_array($kakaoCrmConfig['sender'] ?? '', ['kakaoAlrim', 'kakaoAlrimCloud'], true);
$kakaoVendorLabel = match ($kakaoCrmConfig['sender'] ?? '') {
'kakaoAlrimCloud' => '클라우드',
'kakaoAlrim' => '비즈엠',
'kakaoAlrimLuna' => '블룸에이아이',
default => '',
};
 
/** @var MyappPushTemplateService $myappPushTemplateService */
$myappPushTemplateService = App::getInstance(MyappPushTemplateService::class);
 
$this->callMenu('crm', 'messageSend', 'autoSendConfig');
$this->setData('mode', 'save');
$this->setData('code', $code);
$this->setData('groupCode', $groupCode);
$this->setData('displayOptions', $autoSendOptionVisibility->bindToDisplay());
$this->setData('options', $options);
$this->setData('defaultRecipient', AutoSendRecipientPolicy::getDefaultRecipient($code));
$this->setData('recipientTitles', $this->buildRecipientTitles($recipients));
$this->setData('forceCheckRecipients', $this->getForceCheckRecipients($code));
$this->setData('recipients', $recipients);
$this->setData('channels', $channels);
$this->setData('useJoinPolicy', AutoSendSupport::useJoinPolicy());
$this->setData('myappSupportRecipients', ['member']);
$this->setData('kakaoCrmUseFlag', ($kakaoCrmConfig['useFlag'] ?? '') === 'y');
$this->setData('myappCrmUseFlag', ($crmMyappConfig['useFlag'] ?? '') === 'y');
$this->setData('isKakaoAlternativeBySms', ($kakaoCrmConfig['autoSmsAlternativeSendFlag'] ?? '') === 'y' && ($kakaoCrmConfig['autoSmsAlternativeSendType'] ?? '') === 'SMS');
$this->setData('isMyappAlternativeBySms', ($crmMyappConfig['autoSmsAlternativeSendFlag'] ?? '') === 'y' && ($crmMyappConfig['autoSmsAlternativeSendType'] ?? '') === 'SMS');
$this->setData('validAutoSendSms', $validAutoSendSms);
$this->setData('checkedRecipients', $checkedRecipients);
$this->setData('autoSendConfigsResponse', $autoSendConfigsResponse);
$this->setData('channelTemplateVariables', [
AutoSendChannel::SMS->name => AutoSendTemplateVariableCatalog::groupCodeVariablesByCategoryAndChannel($code, AutoSendChannel::SMS, true),
AutoSendChannel::KAKAO_ALRIM_TALK->name => AutoSendTemplateVariableCatalog::groupCodeVariablesByCategoryAndChannel($code, AutoSendChannel::KAKAO_ALRIM_TALK, true),
AutoSendChannel::MYAPP_PUSH->name => AutoSendTemplateVariableCatalog::groupCodeVariablesByCategoryAndChannel($code, AutoSendChannel::MYAPP_PUSH, true),
]);
$this->setData('groupedTemplateAllVariables', AutoSendTemplateVariableCatalog::groupVariablesByCategory(true));
$this->setData('shouldChangeKakaoTemplate', $shouldChangeKakaoTemplate);
$this->setData('kakaoTemplates', $channelCodeConfigDto->kakaoTemplates);
$this->setData('kakaoVendorLabel', $kakaoVendorLabel);
$this->setData('myappDomain', $myappPushTemplateService->getMyappDomainUrl());
$this->getView()->setDefine('autoSendConfigOptions', 'crm/auto_send_config/auto_send_config_options.php');
$this->getView()->setDefine('autoSendConfigContents', 'crm/auto_send_config/auto_send_config_contents.php');
$this->getView()->setDefine('autoSendConfigPreview', 'crm/auto_send_config/auto_send_config_preview.php');
}
}
 
private function buildRecipientTitles(array $recipients): array
{
$titles = [];
foreach ($recipients as $recipient) {
$titles[strtolower($recipient->name)] = $recipient->getTitle();
}
return $titles;
}
 
/**
* @throws ConnectException
*/
private function validAutoSendSms(): bool
{
try {
/** @var SmsPasswordService $smsPasswordService */
$smsPasswordService = App::getInstance(SmsPasswordService::class);
$smsPassword = $smsPasswordService->getSmsPassword(true);
if (empty($smsPassword)) {
return false;
}
 
if (empty(gd_policy('sms.config')['smsCallNum'])) {
return false;
}
 
return $smsPasswordService->verifySmsPassword($smsPassword);
} catch (ConnectException $e) {
throw $e;
} catch (\Exception) {
return false;
}
}
 
private function collectGroupsHavingAutoSendEnabled(array $autoSendConfigsResponse): array
{
$result = [];
foreach ($autoSendConfigsResponse as $groupKey => $channels) {
foreach ($channels as $channel) {
if (($channel['isAutoSend'] ?? null) === 'y') {
$result[] = $groupKey;
break;
}
}
}
return $result;
}
 
/**
* 정책상 무조건 수신 대상 설정에 체크 상태로 있어야 하는 수신자
* @param string $code
* @return array
*/
public function getForceCheckRecipients(mixed $code): array
{
$forceCheckedRecipients = [];
if ($code == Code::PRESENT) {
$forceCheckedRecipients[] = AutoSendRecipient::RECIPIENT;
}
 
return $forceCheckedRecipients;
}
 
/**
* @return void
*/
public function applyBlockPage(): void
{
$this->getView()->setPageName('crm/auto_send_config.php');
$this->getView()->setDefine('autoSendConfigBlock', 'crm/auto_send_config/auto_send_config_block.php');
}
}
 
변경 후AutoSendConfigController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendConfig/LayerAutoSendModifyKakaoTemplateController.php-28
변경 전LayerAutoSendModifyKakaoTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSendConfig;
 
use App;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudMessageTemplateService;
use Request;
 
class LayerAutoSendModifyKakaoTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$templateCode = $request['templateCode'] ?? '';
 
// todo test
$templateCode = 'custom_order_0011';
 
$this->setData('template', $template);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerAutoSendModifyKakaoTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendConfig/LayerAutoSendSaveTemplateController.php-49
변경 전LayerAutoSendSaveTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSendConfig;
 
use Exception;
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Origin\Enum\Crm\Message\SmsTemplateCategory;
use Request;
 
class LayerAutoSendSaveTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$contents = $request['contents'] ?? '';
$title = $request['title'] ?? '';
$channel = $request['channel'];
 
switch ($channel) {
case 'MYAPP_PUSH':
$categories = MyappPushTemplateCategory::getFindOptions();
break;
case 'SMS':
$categories = SmsTemplateCategory::getFindOptions();
break;
default:
throw new Exception("지원하지 않는 채널");
}
 
$url = $request['url'] ?? '';
$image = $request['image'] ?? '';
 
$this->setData('channel', $channel);
$this->setData('categories', $categories);
$this->setData('contents', $contents);
$this->setData('title', $title);
$this->setData('url', $url);
$this->setData('image', $image);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setPageName('crm/auto_send_config/layer_auto_send_save_template.php');
}
}
 
변경 후LayerAutoSendSaveTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendConfig/LayerAutoSendTemplateContentController.php-75
변경 전LayerAutoSendTemplateContentController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSendConfig;
 
use Bundle\Component\Page\Page;
use Exception;
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Origin\Enum\Crm\Message\SmsTemplateCategory;
use Origin\Repository\Member\Myapp\MyappPushRepository;
use Origin\Repository\Member\Sms\SmsContentsRepository;
use Request;
 
class LayerAutoSendTemplateContentController extends \Controller\Admin\Controller
{
private const PER_PAGE = 10;
 
public function index()
{
$request = Request::post()->toArray();
$channel = $request['channel'];
$categoryValue = $request['category'] ?? '';
$keyword = $request['keyword'] ?? '';
$page = max(1, (int)($request['page'] ?? 1));
$offset = ($page - 1) * self::PER_PAGE;
 
switch ($channel) {
case 'MYAPP_PUSH':
$category = MyappPushTemplateCategory::tryFrom($categoryValue);
$repository = new MyappPushRepository();
$totalCount = $repository->searchCount($category, $keyword);
$templates = array_map(function ($template) {
return [
'sno' => $template['pushSno'],
'subject' => $template['templateName'] ?? '',
'contents' => $template['pushContent'],
'title' => $template['pushSubject'] ?? '',
'myappImage' => $template['pushImage'],
'url' => $template['pushUrl'] ?? '',
];
}, $repository->searchPaginated($category, $keyword, self::PER_PAGE, $offset));
$categories = MyappPushTemplateCategory::getFindOptions();
break;
case 'SMS':
$category = SmsTemplateCategory::tryFrom($categoryValue);
$repository = new SmsContentsRepository();
$totalCount = $repository->searchCount($category, $keyword);
$templates = $repository->searchPaginated($category, $keyword, self::PER_PAGE, $offset);
$categories = SmsTemplateCategory::getFindOptions();
break;
default:
throw new Exception();
}
 
$page = new Page($page, $totalCount, $totalCount, self::PER_PAGE);
 
$templates = array_map(function ($template) {
$template['subject'] = nl2br(gd_htmlspecialchars_stripslashes($template['subject']));
$template['contents'] = gd_htmlspecialchars_stripslashes($template['contents']);
return $template;
}, $templates);
 
$this->setData('channel', $channel);
$this->setData('templates', $templates);
$this->setData('categories', $categories);
$this->setData('page', $page);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setPageName('crm/auto_send_config/layer_auto_send_template_content.php');
}
}
 
변경 후LayerAutoSendTemplateContentController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendConfig/LayerAutoSendTemplateController.php-24
변경 전LayerAutoSendTemplateController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSendConfig;
 
use Request;
 
class LayerAutoSendTemplateController extends \Controller\Admin\Controller
{
 
public function index()
{
$request = Request::post()->toArray();
$channel = $request['channel'] ?? 'SMS';
 
$this->setData('channel', $channel);
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setPageName('crm/auto_send_config/layer_auto_send_template.php');
}
}
 
변경 후LayerAutoSendTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendConfigPsController.php-125
변경 전AutoSendConfigPsController.php
<?php
 
namespace Bundle\Controller\Admin\Crm;
 
use App;
use Bundle\Component\Sms\SmsAutoCode;
use Exception;
use Origin\DTO\AutoSend\AutoSendOptionCodeConfigDto;
use Origin\Enum\AutoSend\AutoSendChannel;
use Origin\Enum\AutoSend\AutoSendChannelPolicy;
use Origin\Enum\AutoSend\AutoSendRecipient;
use Origin\Enum\AutoSend\AutoSendRecipientPolicy;
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Service\AutoSend\FindAutoSendConfigService;
use Origin\Service\AutoSend\Request\UpdateAutoSendConfigRequest;
use Origin\Service\AutoSend\SaveAutoSendConfigService;
use Origin\Service\AutoSend\SaveAutoSendTemplateService;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSyncService;
use Request;
 
class AutoSendConfigPsController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$files = Request::files()->toArray();
$mode = $request['mode'] ?? ''; // todo throw
$code = $request['code'];
$options = $request['options'] ?? [];
 
try {
switch ($mode) {
case 'save':
$configOptions = AutoSendOptionCodeConfigDto::createCodeConfigByOption($code, $options);
/** @var SmsAutoCode $smsAutoCode */
$smsAutoCode = App::load('Component\\Sms\\SmsAutoCode');
$smsAutoCode->getCodes();
/** @var SaveAutoSendConfigService $saveAutoSendConfigService */
$saveAutoSendConfigService = App::getInstance(SaveAutoSendConfigService::class);
$channels = AutoSendChannelPolicy::getCodesWithChannelsWithBoard()[$code] ?? [];
/** @var SaveAutoSendTemplateService $saveAutoSendTemplateService */
$saveAutoSendTemplateService = App::getInstance(SaveAutoSendTemplateService::class);
$recipientFiles = $files['recipients'] ?? [];
$flagMap = [];
$templateItems = [];
 
foreach ($request['recipients'] as $requestRecipientName => $requestRecipientConfig) {
$autoSendRecipient = AutoSendRecipient::fromName(strtoupper($requestRecipientName));
foreach ($channels as $channel) {
$channelConfig = $requestRecipientConfig[$channel->name] ?? [];
if (!empty($channelConfig)) {
$imageMode = $channelConfig['imageMode'] ?? null;
if ($imageMode === 'FILE') {
$uploadedFile = $this->extractUploadedFile($recipientFiles, $requestRecipientName, $channel->name);
if ($uploadedFile !== null) {
$channelConfig['uploadedImage'] = $uploadedFile;
}
}
 
$flagMap[] = [$channel, $requestRecipientName, $channelConfig['isEnabled'] ?? 'n'];
$templateItems[] = [
'channel' => $channel,
'recipient' => $autoSendRecipient,
'config' => $channelConfig,
];
}
}
}
 
 
$insertedSnoMap = \DB::transaction(function () use ($saveAutoSendConfigService, $code, $flagMap, $configOptions, $saveAutoSendTemplateService, $templateItems) {
$saveAutoSendConfigService->updateAutoSendConfigBatch($code, $flagMap, $configOptions);
$insertedSnoMap = $saveAutoSendTemplateService->updateTemplatesBatch($code, $templateItems);
 
/** @var FindAutoSendConfigService $findAutoSendConfigService */
$findAutoSendConfigService = App::getInstance(FindAutoSendConfigService::class);
 
if ($findAutoSendConfigService->getActiveCrmKakaoAlrimSender() == 'kakaoAlrimCloud') {
/** @var KakaoAlrimCloudSyncService $kakaoAlrimCloudSyncService */
$kakaoAlrimCloudSyncService = App::getInstance(KakaoAlrimCloudSyncService::class);
$kakaoAlrimCloudSyncService->syncMessageTemplate();
}
 
return $insertedSnoMap;
});
 
$this->json(['result'=> true, 'message' => '', 'insertedSnoMap' => $insertedSnoMap]);
 
break;
 
default:
break;
}
} catch (Exception $exception) {
$this->json(['result'=> false, 'message' => $exception->getMessage()]);
}
}
 
/**
* $_FILES 중첩 배열에서 수신자/채널별 업로드 파일 정보 추출
*
* @param array $recipientFiles $_FILES['recipients']
* @param string $recipientName 수신자 (member, admin, provider)
* @param string $channelName 채널 (SMS, KAKAO_ALRIM_TALK, MYAPP_PUSH)
* @return array|null ['name', 'type', 'tmp_name', 'error', 'size'] 또는 null
*/
private function extractUploadedFile(array $recipientFiles, string $recipientName, string $channelName): ?array
{
$tmpName = $recipientFiles['tmp_name'][$recipientName][$channelName]['image'] ?? null;
$error = $recipientFiles['error'][$recipientName][$channelName]['image'] ?? UPLOAD_ERR_NO_FILE;
$size = $recipientFiles['size'][$recipientName][$channelName]['image'] ?? 0;
 
if (empty($tmpName) || (int)$error === UPLOAD_ERR_NO_FILE || (int)$size === 0) {
return null;
}
 
return [
'name' => $recipientFiles['name'][$recipientName][$channelName]['image'] ?? '',
'type' => $recipientFiles['type'][$recipientName][$channelName]['image'] ?? '',
'tmp_name' => $tmpName,
'error' => $error,
'size' => $recipientFiles['size'][$recipientName][$channelName]['image'] ?? 0,
];
}
}
 
변경 후AutoSendConfigPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendController.php-59
변경 전AutoSendController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Sms\Code;
use Bundle\Component\Sms\Sms;
use Bundle\Component\Sms\SmsUtil;
use Component\Admin\AdminMenu;
use Origin\Enum\AutoSend\AutoSendRecipient;
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Service\AutoSend\SearchAutoSendListService;
use Origin\Service\AutoSend\SearchAutoSendsRequest;
use Request;
 
class AutoSendController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$request = Request::get()->toArray();
 
$this->callMenu('crm', 'messageSend', 'autoSend');
 
$smsPoint = (float) Sms::getPoint();
$sendableMethodCountByPoint = [
'sms' => SmsUtil::getAvailableCount('sms', $smsPoint),
'lms' => SmsUtil::getAvailableCount('lms', $smsPoint),
'kakaoAlrimTalk' => SmsUtil::getAvailableCount('kakaoAlrimTalk', $smsPoint),
'kakaoFriendTalk' => SmsUtil::getAvailableCount('kakaoFriendTalk', $smsPoint),
];
Sms::saveSmsPoint();
 
/** @var SearchAutoSendListService $findAutoSendService */
$findAutoSendService = \App::getInstance(SearchAutoSendListService::class);
$response = $findAutoSendService->search(SearchAutoSendsRequest::fromArray($request));
 
$this->setData('shouldExpandRecommend', AutoSendSupport::shouldExpandRecommend());
$this->setData('newIconCodes', AutoSendSupport::getNewIconCodes());
$this->setData('useJoinPolicy', AutoSendSupport::useJoinPolicy());
$this->setData('autoSends', $response['autoSends']);
$this->setData('recommendAutoSends', $response['recommendAutoSends']);
$this->setData('pageCounts', $response['pageCounts']);
$this->setData('smsPoint', $smsPoint);
$this->setData('sendableMethodCountByPoint', $sendableMethodCountByPoint);
$this->setData('recipients', AutoSendRecipient::cases());
$this->setData('request', $request);
$this->getView()->setPageName('crm/auto_send.php');
$this->getView()->setDefine('messagePoint', 'crm/auto_send/layer_list_message_point.php');
$this->getView()->setDefine('autoRecommend', 'crm/auto_send/layer_list_recommend.php');
$this->getView()->setDefine('autoListSearch', 'crm/auto_send/layer_list_search.php');
$this->getView()->setDefine('autoList', 'crm/auto_send/layer_list.php');
}
}
 
변경 후AutoSendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSend/LayerListController.php-30
변경 전LayerListController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSend;
 
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Service\AutoSend\SearchAutoSendListService;
use Origin\Service\AutoSend\SearchAutoSendsRequest;
use Request;
 
class LayerListController extends \Controller\Admin\Controller
{
public function index()
{
$request = SearchAutoSendsRequest::fromArray(Request::post()->toArray());
 
/** @var SearchAutoSendListService $findAutoSendService */
$findAutoSendService = \App::getInstance(SearchAutoSendListService::class);
$response = $findAutoSendService->search($request);
$this->setData('newIconCodes', AutoSendSupport::getNewIconCodes());
$this->setData('autoSends', $response['autoSends']);
$this->setData('pageCounts', $response['pageCounts']);
$this->getView()->setPageName('crm/auto_send/layer_list.php');
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerListController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSend/LayerListRecommendController.php-20
변경 전LayerListRecommendController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSend;
 
use Origin\Service\AutoSend\SearchAutoSendListService;
 
class LayerListRecommendController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setPageName('crm/auto_send/layer_list_recommend.php');
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerListRecommendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSend/LayerListSearchController.php-19
변경 전LayerListSearchController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\AutoSend;
 
 
class LayerListSearchController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setPageName('crm/auto_send/layer_list_search.php');
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerListSearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/AutoSendPsController.php-51
변경 전AutoSendPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Exception;
use Framework\Http\Response;
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Service\AutoSend\Request\UpdateAutoSendConfigRequest;
use Origin\Service\AutoSend\SaveAutoSendConfigService;
use Request;
 
class AutoSendPsController extends \Controller\Admin\Controller
{
public function index() {
$request = Request::post()->toArray();
try {
switch ($request['mode']) {
case 'updateEnabledFlags':
if (!empty($request['codes'])) {
/** @var SaveAutoSendConfigService $saveAutoSendConfigService */
$saveAutoSendConfigService = \App::getInstance(SaveAutoSendConfigService::class);
$saveAutoSendConfigService->updateAutoSendConfig(
new UpdateAutoSendConfigRequest(
codesWithFlag: $request['codes'],
)
);
}
$this->json(['result'=> true, 'message' => '']);
break;
case 'saveRecommendCollapseState':
$collapsed = ($request['collapsed'] ?? '') === 'true';
AutoSendSupport::saveRecommendCollapseState($collapsed);
$this->json(['result' => true, 'message' => '']);
break;
default:
break;
}
} catch (Exception $exception) {
// todo logging
$this->json(['result'=> false, 'message' => $exception->getMessage()]);
}
}
 
 
}
 
변경 후AutoSendPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/CrmRecipeClickLogPsController.php-46
변경 전CrmRecipeClickLogPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Http\Request;
use Origin\Service\Crm\CrmRecipeClickLogCollectorService;
 
class CrmRecipeClickLogPsController extends \Controller\Admin\Controller
{
private const ALLOWED_ACTIONS = [
CrmRecipeClickLogCollectorService::ACTION_SEND,
CrmRecipeClickLogCollectorService::ACTION_CLOSE,
];
 
public function index()
{
$this->callMenu('crm', 'crm', 'crmRecipe');
$this->setMenuCode('crm', 'crm', 'crmRecipe');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$action = $postData['action'] ?? '';
if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
$this->json(['success' => false]);
return;
}
 
/** @var CrmRecipeClickLogCollectorService $clickLogCollector */
$clickLogCollector = \App::getInstance(CrmRecipeClickLogCollectorService::class);
$clickLogCollector->collect([
'action' => $action,
'recipeType' => $postData['recipeType'] ?? '',
'currentPage' => $postData['currentPage'] ?? '',
]);
 
$this->json(['success' => true]);
}
}
 
변경 후CrmRecipeClickLogPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/CrmRecipeSendController.php-148
변경 전CrmRecipeSendController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Sms\Sms;
use Component\Validator\Validator;
use Framework\Debug\Exception\AlertBackException;
use Origin\DTO\ApiClient\Commerce\Crm\TargetsRepeatRuleStatusRequestDTO;
use Origin\Enum\ApiClient\Crm\RecipeType;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
class CrmRecipeSendController extends \Controller\Admin\Controller
{
protected $messageLogger;
 
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'crm', 'crmRecipe');
$this->setMenuCode('crm', 'crm', 'crmRecipe');
 
$this->messageLogger = \Logger::channel('mobileMessage');
 
$recipeType = (string) \Request::get()->get('recipeType', '');
 
if (!empty($recipeType) && RecipeType::tryFromSlug($recipeType) === null) {
throw new AlertBackException(__('잘못된 접근입니다.'));
}
 
$crmGroupNoRaw = \Request::get()->get('no', '');
 
if (Validator::number($crmGroupNoRaw, 1) === false) {
throw new AlertBackException(__('잘못된 접근입니다.'));
}
 
$crmGroupNo = (int) $crmGroupNoRaw;
 
/** @var CrmAdminApiClientService $crmAdminApiClientService */
$crmAdminApiClientService = \App::getInstance(CrmAdminApiClientService::class);
 
$crmGroup = $crmGroupNo > 0
? $this->fetchSelectedCrmGroup($crmAdminApiClientService, $crmGroupNo)
: [];
 
$recipeDefault = [];
if (!empty($recipeType)) {
try {
$recipeDefault = $crmAdminApiClientService->getRecipeDefault($recipeType)->toRecursiveArray();
} catch (\Throwable $e) {
$this->messageLogger->error('CRM 레시피 기본값 처리 실패', [$e->getMessage(), $recipeType, __METHOD__]);
$recipeDefault = [];
}
}
 
$recipeName = $recipeDefault['recipeTitle'] ?? '레시피';
$conditionGuideView = $this->buildConditionGuideView($recipeDefault['conditionGuide'] ?? []);
 
Sms::saveSmsPoint();
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$hasCallNumber = $smsManageCallNumberService->hasUsableCallNumber();
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
$messageConfig = $messageConfigService->buildSendableMessageConfig($hasCallNumber);
 
$this->setData('messageConfig', $messageConfig);
$this->setData('smsPointEach', Sms::SMS_POINT);
$this->setData('lmsPointEach', Sms::LMS_POINT);
$this->setData('kakaoAlrimTalkPointEach', Sms::KAKAO_POINT);
$this->setData('kakaoFriendTalkPointEach', Sms::KAKAO_FRIEND_TALK_POINT);
$this->setData('recipeType', $recipeType);
$this->setData('crmGroup', $crmGroup);
$this->setData('recipeName', $recipeName);
$this->setData('recipeDefault', $recipeDefault);
$this->setData('conditionGuideView', $conditionGuideView);
 
// 어드민 헤더·네비 없는 blank 레이아웃 (드로어용)
$this->getView()->setDefine('layout', 'layout_blank_noiframe.php');
$this->getView()->setDefine('layoutHeader', 'header_no_breadcrumb.php');
$this->getView()->setPageName('crm/crm_recipe_send.php');
 
$this->setData('adminBodyClass', trim($this->getData('adminBodyClass') . ' ncds'));
}
 
protected function fetchSelectedCrmGroup(CrmAdminApiClientService $crmAdminApiClientService, int $crmGroupNo): array
{
try {
$crmGroup = $crmAdminApiClientService
->getNotificationTargetsRepeatRuleStatus(new TargetsRepeatRuleStatusRequestDTO(), $crmGroupNo)
->toRecursiveArray();
} catch (\Throwable $e) {
$this->messageLogger->error('CRM 그룹 상태 조회 실패', [$e->getMessage(), $crmGroupNo, __METHOD__]);
return [];
}
 
if (empty($crmGroup)) {
return [];
}
 
// status API 응답의 no 는 내부 레코드 PK라 crmGroupNo 로 덮어씀
$crmGroup['no'] = $crmGroupNo;
 
// status 응답엔 title 이 없어 규칙 단건 조회로 보강
try {
$crmGroup['title'] = $crmAdminApiClientService
->getNotificationTargetsRepeatRule($crmGroupNo)
->getTitle();
} catch (\Throwable $e) {
$this->messageLogger->error('CRM 그룹명 조회 실패', [$e->getMessage(), $crmGroupNo, __METHOD__]);
}
 
return $crmGroup;
}
 
protected function buildConditionGuideView(array $conditionGuide): array
{
$hasConditionGuide = !empty($conditionGuide['selectionGuide'])
&& !empty($conditionGuide['selectableValues'])
&& is_array($conditionGuide['selectableValues']);
 
if (!$hasConditionGuide) {
return ['hasConditionGuide' => false];
}
 
$selectableValues = $conditionGuide['selectableValues'];
$currentValue = $conditionGuide['value'] ?? $selectableValues[0];
$guideParts = explode('{no}', $conditionGuide['selectionGuide'], 2);
 
// 미리보기 문구는 JS(updateConditionPreview)가 단독 계산·렌더 — 날짜 규칙 단일화
return [
'hasConditionGuide' => true,
'selectableValues' => $selectableValues,
'currentValue' => $currentValue,
'guideParts' => $guideParts,
'productSelectableCount' => (int) ($conditionGuide['productSelectableCount'] ?? 0),
];
}
}
 
변경 후CrmRecipeSendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerKakaoTemplateCommentController.php-40
변경 전LayerKakaoTemplateCommentController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\StaticProxy\Proxy\Logger;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudMessageTemplateService;
use Bundle\Component\Member\KakaoAlrim;
 
class LayerKakaoTemplateCommentController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
 
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
$provider = $requestData['provider'] ?? "";
$templateCode = $requestData['templateCode'] ?? "";
 
if ($provider === 'bizm'){
$kakao = new KakaoAlrim;
$messageTemplateComments = $kakao->getTemplateComment($templateCode);
$comments = $messageTemplateComments['comments'] ?? [];
$comment = !empty($comments) ? end($comments)->content ?? '' : '';
$this->setData('comment', $comment);
} else{
/** @var KakaoAlrimCloudMessageTemplateService $service */
$service = \App::getInstance(KakaoAlrimCloudMessageTemplateService::class);
$messageTemplateComments = $service->getMessageTemplateComments($templateCode);
$this->setData('comment', $messageTemplateComments);
}
 
}
}
 
변경 후LayerKakaoTemplateCommentController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerMobileMessagePerformanceDetailController.php-17
변경 전LayerMobileMessagePerformanceDetailController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerMobileMessagePerformanceDetailController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileMessagePerformanceDetailController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerMobileMessagePerformanceSummaryController.php-17
변경 전LayerMobileMessagePerformanceSummaryController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerMobileMessagePerformanceSummaryController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileMessagePerformanceSummaryController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerMyAppSendGuideController.php-17
변경 전LayerMyAppSendGuideController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerMyAppSendGuideController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMyAppSendGuideController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerPreviewAutoSendController.php-98
변경 전LayerPreviewAutoSendController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use App;
use Bundle\Component\Sms\SmsAutoCode;
use Origin\Enum\AutoSend\AutoSendChannel;
use Origin\Enum\AutoSend\AutoSendChannelPolicy;
use Origin\Enum\AutoSend\AutoSendRecipient;
use Origin\Enum\AutoSend\AutoSendRecipientPolicy;
use Origin\Enum\AutoSend\AutoSendSupport;
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Origin\Enum\Crm\Message\SmsTemplateCategory;
use Origin\Repository\Member\Myapp\MyappPushRepository;
use Origin\Repository\Member\Sms\SmsContentsRepository;
use Origin\Service\AutoSend\FindAutoSendCodeConfigService;
use Origin\Service\AutoSend\FindAutoSendConfigService;
use Origin\Service\AutoSend\FindAutoSendTemplateService;
use Request;
 
class LayerPreviewAutoSendController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::get()->toArray();
$code = $request['code'];
$supportChannels = AutoSendChannelPolicy::getCodesWithChannelsWithBoard()[$code] ?? [];
 
// 수신대상 라벨 (key => label)
$recipientLabels = [];
foreach (AutoSendRecipient::cases() as $recipient) {
$recipientLabels[strtolower($recipient->name)] = $recipient->getTitle();
}
 
// 발송 수단 라벨 (key => label)
$channelLabels = [];
foreach ($supportChannels as $channel) {
$channelLabels[$channel->name] = $channel->getShortTitle();
}
 
/** @var FindAutoSendCodeConfigService $codeConfigService */
$codeConfigService = App::getInstance(FindAutoSendCodeConfigService::class);
[$_, $autoSendConfigsResponse, $channelCodeConfigDto] = $codeConfigService->createRecipientConfigByCode($code, false);
 
// kakaoTemplates를 templateCode 기준 flat map으로 변환
$kakaoTemplateMap = [];
foreach ($channelCodeConfigDto->kakaoTemplates as $categoryTemplates) {
foreach ($categoryTemplates as $template) {
if (!empty($template['templateCode'])) {
$kakaoTemplateMap[$template['templateCode']] = $template;
}
}
}
 
 
$this->setData('allRecipients', $this->getSortedAllRecipients());
$this->setData('recipientLabels', $recipientLabels);
$this->setData('channelLabels', $channelLabels);
$this->setData('templates', $autoSendConfigsResponse);
$this->setData('kakaoTemplateMap', $kakaoTemplateMap);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
private function getAutoSendMeta($code)
{
/** @var SmsAutoCode $smsAutoCode */
$smsAutoCode = App::load('Component\\Sms\\SmsAutoCode');
$groupKey = $smsAutoCode->getGroupByCode($code);
$recipients = AutoSendRecipientPolicy::getCodesWithRecipientsWithBoard()[$code] ?? [];
$channels = AutoSendChannelPolicy::getCodesWithChannelsWithBoard()[$code] ?? [];
return [$groupKey, $recipients, $channels];
}
 
/**
* 전체 수신 대상들 반환 (탭 순서용)
* 순서 정책: 회원 - 수령자 - 본사 운영자 - 공급사
*
* @return array
*/
public function getSortedAllRecipients(): array
{
return array_map(
fn(AutoSendRecipient $r) => strtolower($r->name),
[
AutoSendRecipient::MEMBER,
AutoSendRecipient::RECIPIENT,
AutoSendRecipient::ADMIN,
AutoSendRecipient::PROVIDER,
]
);
}
}
 
변경 후LayerPreviewAutoSendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerPreviewMobileSendController.php-144
변경 전LayerPreviewMobileSendController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\StaticProxy\Proxy\UserFilePath;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MyappPushSettingService;
use Request;
use Logger;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Validator;
 
class LayerPreviewMobileSendController extends \Controller\Admin\Controller
{
public function index()
{
try {
$postValue = Request::post()->toArray();
 
// 요청 검증
Validator::key('sendMethod', Validator::in(['SMS', 'FRIENDTALK', 'ALIMTALK', 'MYAPP']))
->setTemplate('유효하지 않은 요청입니다.')
->assert($postValue);
Validator::key('viewType', Validator::in(['layer', 'side']))
->setTemplate('유효하지 않은 요청입니다.')
->assert($postValue);
Validator::key('payload', Validator::stringType()->notEmpty())
->setTemplate('유효하지 않은 요청입니다.')
->assert($postValue);
 
$payload = json_decode($postValue['payload'], true);
$sendMethod = $postValue['sendMethod'];
 
// 공통 데이터
$this->setData('sendMethod', $sendMethod);
$this->setData('payloadJson', $postValue['payload']);
$useAlternative = !empty($payload['enableAlternative']) ? 'y' : 'n';
$this->setData('useAlternative', $useAlternative);
 
$previewContents = "";
$alternativePreviewContents = 'crm/preview_mobile_send/layer_preview_mobile_send_alternative.php';
switch ($sendMethod) {
case 'SMS':
$smsData = $payload['smsLmsRequest'] ?? [];
$this->setData('mainTitle', $smsData['title'] ?? '');
$this->setData('mainContents', $smsData['content'] ?? '');
$this->setData('showAdWarningText', 'n');
$previewContents = 'crm/preview_mobile_send/layer_preview_mobile_send_sms.php';
break;
case 'FRIENDTALK':
$friendTalkData = $payload['kakaoFriendtalkRequest'] ?? [];
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
 
$showAdWarningText = 'n';
if ($kakaoFriendTalkConfig->getSmsAlternativeSendFlag() === 'y' && !$friendTalkData['alternativeInfo']['isAdText'] && $friendTalkData['type'] !== 'WIDE_ITEM_LIST' && $friendTalkData['type'] !== 'CAROUSEL_FEED') {
$showAdWarningText = 'y';
}
 
$this->setData('friendtalk', $friendTalkData);
$this->setData('alternativeContents', $friendTalkData['alternativeInfo']['content'] ?? '');
// 대체 제목 미입력 시 메인 메시지 타이틀(친구톡 헤더)이 있으면 카피하여 표기 (미리보기 전용)
$alternativeTitle = $friendTalkData['alternativeInfo']['title'] ?? '';
if ($alternativeTitle === '' || $alternativeTitle === null) {
$alternativeTitle = $friendTalkData['header'] ?? '';
}
$this->setData('alternativeTitle', $alternativeTitle);
$this->setData('showAdWarningText', $showAdWarningText);
$previewContents = 'crm/preview_mobile_send/layer_preview_mobile_send_kakao_friendtalk.php';
break;
case 'ALIMTALK':
$alimTalkData = $payload['kakaoAlimtalkRequest'] ?? [];
$previewMeta = json_decode($postValue['previewMeta'] ?? '{}', true);
$hasAddChannelButton = !empty(array_filter(
$previewMeta['templateButtons'] ?? [],
fn($button) => ($button['type'] ?? '') === 'ADD_CHANNEL'
));
$this->setData('template', $previewMeta);
$this->setData('hasAddChannelButton', $hasAddChannelButton);
$this->setData('alternativeContents', $alimTalkData['alternativeInfo']['content'] ?? '');
$this->setData('alternativeTitle', $alimTalkData['alternativeInfo']['title'] ?? '');
$this->setData('showAdWarningText', 'n');
$previewContents = 'crm/preview_mobile_send/layer_preview_mobile_send_kakao_alimtalk.php';
break;
case 'MYAPP':
$myappPushData = $payload['myappRequest'] ?? [];
 
/** @var MyappPushSettingService $myappPushSettingService */
$myappPushSettingService = \App::getInstance(MyappPushSettingService::class);
$myappPushConfig = $myappPushSettingService->getMyappPushConfig();
$showAdWarningText = 'n';
if ($myappPushData['notificationType'] === 'AD' && $myappPushConfig->getManualSmsAlternativeSendFlag() === 'y' && !$myappPushData['alternativeInfo']['isAdText']) {
$showAdWarningText = 'y';
}
$this->setData('myappPayload', $myappPushData);
$this->setData('showAdWarningText', $showAdWarningText);
$this->setData('alternativeContents', $payload['myappRequest']['alternativeInfo']['content'] ?? '');
// 대체 제목 미입력 시 메인 메시지 타이틀(푸시 제목)이 있으면 카피하여 표기 (미리보기 전용)
$alternativeTitle = $payload['myappRequest']['alternativeInfo']['title'] ?? '';
if ($alternativeTitle === '' || $alternativeTitle === null) {
$alternativeTitle = $myappPushData['pushSubject'] ?? '';
}
$this->setData('alternativeTitle', $alternativeTitle);
$previewContents = 'crm/preview_mobile_send/layer_preview_mobile_send_myapp_push.php';
break;
}
 
if($postValue['viewType'] === 'side') {
ob_start();
$paths = explode('/', $previewContents);
include UserFilePath::adminSkin(...$paths);
if($out = ob_get_clean()) {
echo $out;
}
exit;
} else {
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setDefine('previewContents', $previewContents);
$this->getView()->setDefine('alternativePreviewContents', $alternativePreviewContents);
}
} catch (ValidationException $e) {
Logger::channel('mobileMessage')->warning(__CLASS__ . ' 모바일 메시지 전송 처리 검증 실패 : ', [
'error' => $e->getMainMessage(),
'postValue' => $postValue ?? []
]);
 
$this->alert(__('잘못된 요청입니다 발송전 미리보기 로드에 실패하였습니다. 잠시후 다시 시도해주세요.'));
} catch (\Throwable $e) {
Logger::channel('mobileMessage')->warning(__CLASS__ . ' 모바일 메시지 전송 처리 중 에러 발생 : ', [
'error' => $e->getMessage(),
'postValue' => $postValue ?? []
]);
$this->alert(__('발송전 미리보기 로드에 실패하였습니다. 잠시후 다시 시도해주세요.'));
}
}
}
 
변경 후LayerPreviewMobileSendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerRejectRecipientMemberInfoController.php-22
변경 전LayerRejectRecipientMemberInfoController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Request;
 
class LayerRejectRecipientMemberInfoController extends \Controller\Admin\Controller
{
public function index()
{
$postValue = Request::post()->toArray();
$this->setData('totalCount', $postValue['totalCount']);
$this->setData('rejectedCount', $postValue['rejectedCount']);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerRejectRecipientMemberInfoController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerSaveAsTemplateController.php-17
변경 전LayerSaveAsTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerSaveAsTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSaveAsTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerSelectCouponController.php-17
변경 전LayerSelectCouponController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerSelectCouponController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSelectCouponController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerSelectCouponResultController.php-49
변경 전LayerSelectCouponResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Coupon\CouponAdmin;
 
class LayerSelectCouponResultController extends \Controller\Admin\Controller
{
public function index()
{
$request = \App::getInstance('request');
$getParams = $request->get()->all();
 
/** @var \Bundle\Component\Coupon\CouponAdmin $couponAdmin */
$couponAdmin = \App::load('\\Component\\Coupon\\CouponAdmin');
$useTypeArray = [];
if (!empty($getParams['couponUseTypeList'])) {
$useTypeArray = explode(',', $getParams['couponUseTypeList']);
$useTypeArray = array_filter($useTypeArray); // 빈 문자열 제거
}
$deviceTypeArray = [];
if (!empty($getParams['couponDeviceTypeList'])) {
$deviceTypeArray = explode(',', $getParams['couponDeviceTypeList']);
$deviceTypeArray = array_filter($deviceTypeArray); // 빈 문자열 제거
$deviceTypeArray = array_map('strtolower', $deviceTypeArray); // 소문자로 변환
}
$couponAdminList = $couponAdmin->getCouponAdminList(addUseTypeWhereIn: $useTypeArray, addDeviceTypeWhereIn: $deviceTypeArray);
 
$couponData = [];
foreach ($couponAdminList['data'] as $index => $item) {
$couponData[$item['couponNo']] = $item;
}
 
$page = \App::load('\\Component\\Page\\Page');
$searchCount = $page->recode['total'];
 
$this->setData('page', $page);
$this->setData('couponData', $couponData);
$this->setData('couponSearchCount', $searchCount);
$this->setData('requestData', $getParams);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSelectCouponResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerSelectMemberGroupsController.php-22
변경 전LayerSelectMemberGroupsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Page\Page;
use Component\Member\MemberGroup;
use Framework\StaticProxy\Proxy\UserFilePath;
use Framework\Utility\StringUtils;
 
class LayerSelectMemberGroupsController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSelectMemberGroupsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerSelectMemberGroupsResultController.php-42
변경 전LayerSelectMemberGroupsResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Page\Page;
use Component\Member\MemberGroup;
use Framework\StaticProxy\Proxy\UserFilePath;
use Framework\Utility\StringUtils;
 
class LayerSelectMemberGroupsResultController extends \Controller\Admin\Controller
{
public function index()
{
$request = \App::getInstance('request');
$getParams = $request->get()->all();
 
/* @var \Bundle\Component\Member\MemberGroup $memberGroupComponent */
$memberGroupComponent = \App::load(MemberGroup::class);
$groupData = $memberGroupComponent->getGroupListSearch();
$groups = $groupData['data'] ?? [];
 
// getGroupListSearch 내부 페이징 처리 포함
$page = \App::load('\\Component\\Page\\Page');
$searchCount = $page->recode['total'];
$totalCount = $memberGroupComponent->getAllGroupCount()[0]['cnt'];
 
$this->setData('search', StringUtils::strIsSet($groupData['search']));
$this->setData('groups', $groups);
$this->setData('groupSearchCount', $searchCount);
$this->setData('groupTotalCount', $totalCount);
$this->setData('page', $page);
$this->setData('requestData', $getParams);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSelectMemberGroupsResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerTemplateContentsController.php-33
변경 전LayerTemplateContentsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateResultDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateSearchDTO;
use Origin\Service\Crm\Message\MessageTemplateFindService;
 
class LayerTemplateContentsController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
 
$request = \App::getInstance('request');
$searchDto = new MessageTemplateSearchDTO($request->request()->toArray());
 
/** @var MessageTemplateFindService $service */
$service = \App::getInstance(MessageTemplateFindService::class);
$responseDto = $service->getMessageTemplate($searchDto);
 
$this->getView()->setDefine('layerTemplateContentsSms', 'crm/layer_template_contents_sms.php');
$this->getView()->setDefine('layerTemplateContentsKakao', 'crm/layer_template_contents_kakao.php');
$this->getView()->setDefine('layerTemplateContentsMyapp', 'crm/layer_template_contents_myapp.php');
$this->setData('request', $searchDto);
$this->setData('response', $responseDto);
}
}
 
변경 후LayerTemplateContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerUpdateKaKaoTemplateController.php-17
변경 전LayerUpdateKaKaoTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerUpdateKaKaoTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerUpdateKaKaoTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/LayerViewMessageLinkTypeController.php-18
변경 전LayerViewMessageLinkTypeController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
class LayerViewMessageLinkTypeController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
 
변경 후LayerViewMessageLinkTypeController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfigController.php-95
변경 전MessageConfigController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Utility\GodoUtils;
use Origin\DTO\ApiClient\Commerce\Apps\MyappStatusResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessagePointConfigResponseDTO;
use Origin\Enum\Crm\Message\AutoSmsAlternativeSendType;
use Origin\Enum\Crm\Message\FailedMessageSendType;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Enum\Crm\Message\LinkPlatformType;
use Origin\Enum\Crm\Message\SmsAutoSendOverType;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MessagePointConfigService;
use Origin\Service\Crm\Message\MyappPushSettingService;
use Origin\Service\Crm\Message\SmsSettingService;
use Origin\Service\MyApp\MyappSettingService;
use Origin\Traits\Member\KakaoAlrim\KakaoAlrimCloudAvailable;
 
class MessageConfigController extends \Controller\Admin\Controller
{
use KakaoAlrimCloudAvailable;
 
public function index()
{
$this->setNcdsSubBodyClass();
// 메뉴 설정
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$this->setMenuCode('crm', 'messageConfig', 'messageConfig');
 
try {
/** @var MessagePointConfigService $messagePointConfigService */
$messagePointConfigService = \App::getInstance(MessagePointConfigService::class);
$messagePointConfig = $messagePointConfigService->getMessagePointConfig();
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$messagePointConfig = new MessagePointConfigResponseDTO(null);
}
 
/** @var SmsSettingService $smsSettingService */
$smsSettingService = \App::getInstance(SmsSettingService::class);
$smsAutoConfig = $smsSettingService->getSmsAutoConfig();
$smsRejectPolicy = $smsSettingService->getSmsRejectConfig();
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
/** @var MyappPushSettingService $myappPushSettingService */
$myappPushSettingService = \App::getInstance(MyappPushSettingService::class);
$myappPushConfig = $myappPushSettingService->getMyappPushConfig();
 
try {
/** @var MyappSettingService $myappSettingService */
$myappSettingService = \App::getInstance(MyappSettingService::class);
$myappStatus = $myappSettingService->getMyappStatus();
} catch (\Throwable $e) {
\Logger::channel('myapp')->error($e->getMessage());
$myappStatus = new MyappStatusResponseDTO(['isInstalled' => false, 'isReleased' => false]);
}
 
$isKakaoAlrimAvailable = !$this->isNewMall();
$isKakaoAlrimLunaInstalled = GodoUtils::isPlusShop(PLUSSHOP_CODE_KAKAOALRIMLUNA);
 
$this->setData('messagePointConfig', $messagePointConfig);
$this->setData('smsRejectPolicy', $smsRejectPolicy);
$this->setData('smsAutoConfig', $smsAutoConfig);
$this->setData('kakaoFriendTalkConfig', $kakaoFriendTalkConfig);
$this->setData('isKakaoAlrimAvailable', $isKakaoAlrimAvailable);
$this->setData('isKakaoAlrimLunaInstalled', $isKakaoAlrimLunaInstalled);
$this->setData('kakaoAlimTalkConfig', $kakaoAlimTalkConfig);
$this->setData('isMyappInstalled', $myappStatus->isInstalled());
$this->setData('isMyappReleased', $myappStatus->isReleased());
$this->setData('myappPushConfig', $myappPushConfig);
 
$this->setData('linkPlatformTypes', LinkPlatformType::cases());
$this->setData('kakaoAlimTalkSenders', KakaoAlimTalkSender::cases());
$this->setData('autoSmsAlternativeSendTypes', AutoSmsAlternativeSendType::cases());
$this->setData('failedMessageSendTypes', FailedMessageSendType::cases());
$this->setData('smsAutoSendOverTypes', SmsAutoSendOverType::cases());
 
$this->getView()->setPageName('crm/message_config.php');
}
}
 
변경 후MessageConfigController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerBizmKakaoTalkChannelController.php-37
변경 전LayerBizmKakaoTalkChannelController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Member\KakaoAlrim;
 
class LayerBizmKakaoTalkChannelController extends \Controller\Admin\Controller
{
public function index()
{
$kakaoAlrimBizmConfig = gd_policy('kakaoAlrim.config');
 
gd_isset($kakaoAlrimBizmConfig['plusId'], '');
gd_isset($kakaoAlrimBizmConfig['kakaoKey'], '');
gd_isset($kakaoAlrimBizmConfig['phoneNumber'], '');
gd_isset($kakaoAlrimBizmConfig['useFlag'], 'n');
gd_isset($kakaoAlrimBizmConfig['approvalFl'], 'n');
 
$oKakao = new KakaoAlrim;
$senderProfileLookup = $oKakao->senderProfileLookup();
$kakaoAlrimBizmConfig['status'] = $senderProfileLookup->senderProfileStatus;
 
// PG 설정 불러오기
$pgConfig = gd_pgs();
$isConnectedPg = ($pgConfig['pgAutoSetting'] === 'y' || $pgConfig['pgApprovalSetting'] === 'y') ? 'y' : 'n';
 
$this->setData('isConnectedPg', $isConnectedPg);
$this->setData('kakaoAlrimBizmConfig', $kakaoAlrimBizmConfig);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerBizmKakaoTalkChannelController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerBizmKakaoTalkChannelPsController.php-74
변경 전LayerBizmKakaoTalkChannelPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Member\KakaoAlrim;
use Bundle\Component\Policy\Policy;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\MyappPushSettingService;
 
class LayerBizmKakaoTalkChannelPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'delete':
try {
$oKakao = new KakaoAlrim;
$result = $oKakao->deleteKakaoKey();
if ($result['result'] !== 'success') {
$this->json(['success' => false, 'message' => '프로필키 삭제에 실패하였습니다.', 'data' => ['type' => 'fail_delete_kakao_key']]);
}
 
/** @var Policy $policy */
$policy = \App::load('\\Component\\Policy\\Policy');
$result = $policy->setValue('kakaoAlrim.config', []);
 
if (!$result) {
$this->json(['success' => false, 'message' => '설정 업데이트에 실패하였습니다.', 'data' => ['type' => 'fail_update_policy']]);
break;
}
 
$oKakao->deleteAllTemplate();
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
// 비즈엠 알림톡 사용안함 처리
if ($kakaoAlimTalkConfig->getSender() === KakaoAlimTalkSender::BIZM && $kakaoAlimTalkConfig->getUseFlag() === 'y') {
$kakaoAlimTalkConfig->setUseFlag('n');
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
$messageConfigService->save(null, null, null, null, $kakaoAlimTalkConfig, null);
}
 
$this->json(['success' => true, 'message' => '프로필키 삭제에 성공하였습니다.']);
break;
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerBizmKakaoTalkChannelPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerChargeMessagePointsHistoryController.php-16
변경 전LayerChargeMessagePointsHistoryController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
class LayerChargeMessagePointsHistoryController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerChargeMessagePointsHistoryController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerChargeMessagePointsHistoryResultController.php-47
변경 전LayerChargeMessagePointsHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Framework\Http\Request;
use Origin\DTO\Member\Sms\SmsPointChargeDTO;
use Origin\Service\Member\Sms\SmsPointChargeService;
 
class LayerChargeMessagePointsHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'chargeMessagePoints');
$naviMenu = $this->getData('naviMenu');
 
/** @var Request $request */
$request = \App::getInstance('request');
$requestData = $request->post()->toArray();
 
/** @var SmsPointChargeService $smsPointChargeService */
$smsPointChargeService = \App::getInstance(SmsPointChargeService::class);
 
$smsPointChargeListResponseDTO = $smsPointChargeService->getSmsPointChargeList(
$naviMenu->lno['2'],
$requestData['page'] ?? SmsPointChargeService::DEFAULT_PAGE,
$requestData['pageSize'] ?? SmsPointChargeService::DEFAULT_CRM_PAGE_SIZE
);
 
$smsPointChargeList = array_map(function(SmsPointChargeDTO $dto) {
$item = $dto->toArray();
$item['payTypeName'] = $dto->getPayTypeName(); // 원하는 값으로 변경
return $item;
}, $smsPointChargeListResponseDTO->getSmsPointChargeList());
 
$page = $smsPointChargeListResponseDTO->getPage();
 
$this->setData('smsPointChargeList', $smsPointChargeList);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerChargeMessagePointsHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerCloudKakaoTalkChannelController.php-30
변경 전LayerCloudKakaoTalkChannelController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSettingService;
 
class LayerCloudKakaoTalkChannelController extends \Controller\Admin\Controller
{
public function index()
{
/** @var KakaoAlrimCloudSettingService $kakaoAlrimCloudSettingService */
$kakaoAlrimCloudSettingService = \App::getInstance(KakaoAlrimCloudSettingService::class);
$kakaoAlrimCloudConfig = $kakaoAlrimCloudSettingService->getKakaoAlrimCloudData();
 
// PG 설정 불러오기
$pgConfig = gd_pgs();
$isConnectedPg = ($pgConfig['pgAutoSetting'] === 'y' || $pgConfig['pgApprovalSetting'] === 'y') ? 'y' : 'n';
 
$this->setData('isConnectedPg', $isConnectedPg);
$this->setData('kakaoAlrimCloudConfig', $kakaoAlrimCloudConfig);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerCloudKakaoTalkChannelController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerCloudKakaoTalkChannelPsController.php-74
변경 전LayerCloudKakaoTalkChannelPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\MyappPushSettingService;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudPsService;
 
class LayerCloudKakaoTalkChannelPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'delete':
try {
/** @var KakaoAlrimCloudPsService $service */
$service = \App::getInstance(KakaoAlrimCloudPsService::class);
$result = $service->deletePlusId($postData);
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
$needConfigUpdate = false;
// 친구톡 사용안함 처리
if ($kakaoFriendTalkConfig->getUseFlag() === 'y') {
$kakaoFriendTalkConfig->setUseFlag('n');
$needConfigUpdate = true;
}
 
// 클라우드 알림톡 사용안함 처리
if ($kakaoAlimTalkConfig->getSender() === KakaoAlimTalkSender::NHN_CLOUD && $kakaoAlimTalkConfig->getUseFlag() === 'y') {
$kakaoAlimTalkConfig->setUseFlag('n');
$needConfigUpdate = true;
}
 
if ($needConfigUpdate) {
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
$messageConfigService->save(null, null, null, $kakaoFriendTalkConfig, $kakaoAlimTalkConfig, null);
}
 
$this->json(['success' => $result]);
break;
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerCloudKakaoTalkChannelPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerLunaKakaoTalkChannelController.php-29
변경 전LayerLunaKakaoTalkChannelController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
 
use Framework\Enum\ExternalUrl;
 
class LayerLunaKakaoTalkChannelController extends \Controller\Admin\Controller
{
public function index()
{
$kakaoAlrimLunaConfig = gd_policy('kakaoAlrimLuna.config');
 
gd_isset($kakaoAlrimLunaConfig['useFlag'], 'n');
gd_isset($kakaoAlrimLunaConfig['lunaCliendId'], '');
gd_isset($kakaoAlrimLunaConfig['lunaClientKey'], '');
 
$this->setData('kakaoAlrimLunaConfig', $kakaoAlrimLunaConfig);
$this->setData('lunaKakaoRequestUrl', ExternalUrl::GODO_KAKAO_API->getUrl('/lunaKakaoRequest.php'));
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerLunaKakaoTalkChannelController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerLunaKakaoTalkChannelPsController.php-88
변경 전LayerLunaKakaoTalkChannelPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Member\KakaoAlrimLuna;
use Bundle\Component\Policy\Policy;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\MyappPushSettingService;
 
class LayerLunaKakaoTalkChannelPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'generateSendData':
try {
unset($postData['mode']);
 
$oKakao = new KakaoAlrimLuna;
$encodedContent = $oKakao->sendLunaId($postData);
 
$this->json(['success' => true, 'data' => $encodedContent]);
} catch (\Throwable $e) {
\Logger::channel('kakao')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
case 'logout':
try {
$oKakao = new KakaoAlrimLuna;
$deleteResult = $oKakao->deleteLunaKey();
 
if ($deleteResult['result'] !== 'success') {
$this->json(['success' => false, 'message' => '처리중에 오류가 발생하여 실패되었습니다.']);
}
 
$updateData = ['lunaKeyDel' => 'y'];
 
/** @var Policy $policy */
$policy = \App::load('\\Component\\Policy\\Policy');
$result = $policy->saveKakaoAlrimLunaConfig($updateData);
if ($result) {
$tmpkakaoAutoSet = gd_policy('kakaoAlrimLuna.kakaoAuto');
$tmpLunaSet = gd_array_merge($tmpkakaoAutoSet, array('useFlag' => 'n'));
gd_set_policy('kakaoAlrimLuna.kakaoAuto', $tmpLunaSet);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
// 루나 알림톡 사용안함 처리
if ($kakaoAlimTalkConfig->getSender() === KakaoAlimTalkSender::BLUMN_AI && $kakaoAlimTalkConfig->getUseFlag() === 'y') {
$kakaoAlimTalkConfig->setUseFlag('n');
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
$messageConfigService->save(null, null, null, null, $kakaoAlimTalkConfig, null);
}
}
 
$this->json(['success' => $result]);
break;
} catch (\Throwable $e) {
\Logger::channel('kakao')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerLunaKakaoTalkChannelPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerManageSmsVerificationController.php-37
변경 전LayerManageSmsVerificationController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Framework\Http\Request;
use Origin\Service\Crm\Message\SmsPasswordService;
 
class LayerManageSmsVerificationController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$displayInfo = [
'title' => 'SMS 인증번호',
'useCaptcha' => false,
'retry' => $session->get('captchaRetry', 1),
];
 
if ($request->request()->get('mode', 'input') === 'change') {
$displayInfo['useCaptcha'] = true;
}
 
$restrictedKeywords = SmsPasswordService::RESTRICTED_KEYWORDS;
 
$this->setData('displayInfo', $displayInfo);
$this->setData('restrictedKeywords', $restrictedKeywords);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerManageSmsVerificationController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerManageSmsVerificationPsController.php-69
변경 전LayerManageSmsVerificationPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Captcha\Captcha;
use Bundle\Component\Sms\Exception\PasswordException;
use Component\Sms\SmsSender;
use Framework\Http\Request;
use Framework\Security\Encryptor;
use Framework\Utility\ComponentUtils;
 
class LayerManageSmsVerificationPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
 
$isChangePassword = $request->post()->get('mode', 'changePassword') === 'changePassword';
if ($isChangePassword) {
$captchaNumber = $request->post()->get('captcha');
$captcha = new Captcha();
$result = $captcha->verify($captchaNumber, 1);
if ($result['code'] !== '0000') {
$this->json(['success' => false, 'message' => '자동등록 방지문자가 맞지 않습니다.', 'data' => ['type' => 'invalid_captcha']]);
}
}
 
$smsPassword = $request->post()->get('password');
 
/** @var \Bundle\Component\Sms\SmsSender $smsSender */
$smsSender = \App::load(SmsSender::class);
try {
$smsSender->validPassword($smsPassword, !$isChangePassword);
 
/** @var Encryptor $encryptor */
$encryptor = \App::getInstance('encryptor');
$policy = ComponentUtils::getPolicy('sms.config');
if ($isChangePassword) {
$policy['authentication'] = [
'failLog' => [],
'failCnt' => 0,
'password' => $encryptor->encrypt($smsPassword),
];
} else {
$policy['authentication']['failLog'] = [];
$policy['authentication']['failCnt'] = 0;
}
 
ComponentUtils::setPolicy('sms.config', $policy);
 
$this->json(['success' => true, 'message' => '메시지 인증번호 등록이 완료되었습니다.']);
} catch (PasswordException $e) {
$message = $e->getMessage();
if ($isChangePassword) {
$message = '메시지 인증번호는 영문대문자/영문소문자/숫자/특수문자 중 2가지 이상 조합, 10~16자리로만 설정할 수 있습니다.';
$message .= '<br/>[마이페이지 > 쇼핑몰 관리]에서 메시지 인증번호를 수정 후 다시 시도해주세요.';
}
 
$this->json(['success' => false, 'message' => $message, 'data' => ['type' => 'invalid_format']]);
}
}
}
 
변경 후LayerManageSmsVerificationPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerMessagePointController.php-50
변경 전LayerMessagePointController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Component\Sms\SmsUtil;
use Component\Sms\Sms;
use Origin\Service\Member\Sms\SmsPointChargeService;
 
class LayerMessagePointController extends \Controller\Admin\Controller
{
public function index()
{
// 메뉴 설정
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$naviMenu = $this->getData('naviMenu');
 
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
// 포인트 충전 내역
/** @var SmsPointChargeService $smsPointChargeService */
$smsPointChargeService = \App::getInstance(SmsPointChargeService::class);
$existsChargeHistory = $smsPointChargeService->existsSmsPointChargeHistory($naviMenu->lno['2']);
 
if ($existsChargeHistory) {
// 현재 메시지 포인트
$nowSmsPoint = (float) Sms::getPoint();
 
$availableCount = [
'sms' => SmsUtil::getAvailableCount('sms', $nowSmsPoint),
'lms' => SmsUtil::getAvailableCount('lms', $nowSmsPoint),
'kakaoAlrimTalk' => SmsUtil::getAvailableCount('kakaoAlrimTalk', $nowSmsPoint),
'kakaoFriendTalk' => SmsUtil::getAvailableCount('kakaoFriendTalk', $nowSmsPoint),
];
 
$this->setData('availableCount', $availableCount);
}
 
// 포인트 충전내역 존재 여부 set
$this->setData('existsChargeHistory', $existsChargeHistory);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMessagePointController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerRegisterKakaoTalkChannelController.php-38
변경 전LayerRegisterKakaoTalkChannelController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudRegistService;
 
class LayerRegisterKakaoTalkChannelController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$sender = $request->post()->get('sender');
 
try {
/** @var KakaoAlrimCloudRegistService $service */
$service = \App::getInstance(KakaoAlrimCloudRegistService::class);
[$firstLevelCategories, $secondLevelCategories, $thirdLevelCategories] = $service->getCategoryInfo();
} catch (\Throwable $e) {
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
 
$this->setData('sender', $sender);
$this->setData('firstLevelCategories', gd_isset($firstLevelCategories));
$this->setData('secondLevelCategories', gd_isset($secondLevelCategories));
$this->setData('thirdLevelCategories', gd_isset($thirdLevelCategories));
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerRegisterKakaoTalkChannelController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerRegisterKakaoTalkChannelPsController.php-154
변경 전LayerRegisterKakaoTalkChannelPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Member\KakaoAlrim;
use Bundle\Component\Policy\Policy;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Framework\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use Origin\DTO\Member\KakaoAlrim\KakaoAlirmCloudRegisterDto;
use Origin\DTO\Member\KakaoAlrim\KakaoAlirmGetTokenDto;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudRegistService;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSyncService;
use Origin\Util\RequestValidation;
 
class LayerRegisterKakaoTalkChannelPsController extends \Controller\Admin\Controller
{
/**
* @throws AlertCloseException
* @throws GuzzleException
*/
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
$mode = $requestData['mode'];
 
switch ($mode) {
case 'getToken':
try {
switch ($requestData['sender']) {
case 'kakaoAlrimCloud':
RequestValidation::validate($requestData, ['plusId', 'phoneNumber', 'category']);
 
$dto = new KakaoAlirmGetTokenDto($requestData);
 
/** @var KakaoAlrimCloudRegistService $service */
$service = \App::getInstance(KakaoAlrimCloudRegistService::class);
 
$result = $service->getToken($dto);
$this->json(['success' => $result, 'message' => $result ? null : '인증 번호 발송 실패']);
break;
case 'kakaoAlrim':
$bizmRequestData = [
'yellowId' => $requestData['plusId'],
'phoneNumber' => $requestData['phoneNumber']
];
 
$kakaoAlrim = new KakaoAlrim();
$result = $kakaoAlrim->getToken($bizmRequestData);
 
if (!empty($result['code']) && $result['code'] == 'success') {
$this->json(['success' => true, 'message' => '인증 번호 발송 성공']);
} else {
$this->json(['success' => false, 'message' => $result['message'] ?? '인증 번호 발송 실패']);
}
break;
}
} catch (\Throwable $e) {
\Logger::channel('kakao')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'register':
try {
RequestValidation::validate($requestData, ['sender', 'plusId', 'token', 'phoneNumber']);
$requestData['approvalFl'] = 'y';
 
switch ($requestData['sender']) {
case 'kakaoAlrimCloud':
$registerDto = new KakaoAlirmCloudRegisterDto($requestData);
/** @var KakaoAlrimCloudRegistService $registerService */
$registerService = \App::getInstance(KakaoAlrimCloudRegistService::class);
$result = $registerService->register($registerDto);
/** @var KakaoAlrimCloudSyncService $kakaoAlrimCloudSyncService */
$kakaoAlrimCloudSyncService = \App::getInstance(KakaoAlrimCloudSyncService::class);
$kakaoAlrimCloudSyncService->syncMessageTemplate();
 
if ($result) {
$this->json([
'success' => true,
'message' => '카카오톡 채널이 등록되었습니다.',
'data' => [
'alertMessage' => [
'message' => '카카오톡 채널이 등록되었습니다.<br>메시지 설정 화면에서 등록된 채널 정보를 확인할 수 있습니다.'
],
'requestData' => $requestData
]
]);
} else {
\Logger::channel('kakao')->warning('카카오톡 채널 등록에 실패했습니다.', [$requestData['sender']]);
throw new \Exception("카카오톡 채널 등록에 실패했습니다.");
}
break;
case 'kakaoAlrim':
$kakaoAlrim = new KakaoAlrim();
$profile = $kakaoAlrim->getKakaoKey($requestData);
 
if ($profile['result'] == 'success') {
$requestData['approvalFl'] = 'y';
$requestData['approvalDt'] = date('Y-m-d');
$requestData['approvalId'] = \Session::get('manager.managerId');
$requestData['kakaoKey'] = $profile['data'];
 
/** @var Policy $policy */
$policy = \App::load('\\Component\\Policy\\Policy');
$result = $policy->saveKakaoAlrimConfig($requestData);
 
if($result) {
$kakaoAlrim->saveKakaoAuto($requestData);
$this->json([
'success' => true,
'message' => '카카오톡 채널이 등록되었습니다.',
'data' => [
'alertMessage' => [
'message' => '카카오톡 채널이 등록되었습니다.<br>메시지 설정 화면에서 등록된 채널 정보를 확인할 수 있습니다.'
],
'requestData' => $requestData
]
]);
} else {
\Logger::channel('kakao')->warning('카카오톡 채널 등록에 실패하였습니다.', [$requestData['sender']]);
throw new \Exception("카카오톡 채널 등록에 실패했습니다.");
}
}
break;
}
} catch (\Throwable $e) {
\Logger::channel('kakao')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json([
'success' => false,
'message' => $e->getMessage(),
'data' => [
'alertMessage' => [
'message' => '카카오톡 채널 등록에 실패했습니다.',
'subMessage' => '카카오 채널의 관리자 설정 또는 비즈니스 인증 상태를 확인 후 다시 시도해 주세요.'
]
]
]);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerRegisterKakaoTalkChannelPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerRejectNumberListController.php-17
변경 전LayerRejectNumberListController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
class LayerRejectNumberListController extends \Controller\Admin\Controller
{
public function index()
{
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerRejectNumberListController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerRejectNumberListPsController.php-49
변경 전LayerRejectNumberListPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Sms\Sms080;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Framework\Utility\ComponentUtils;
use Framework\Utility\StringUtils;
 
class LayerRejectNumberListPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$requestData = $request->request();
$mode = $requestData->get('mode');
 
switch ($mode) {
case 'checkActivation':
$policy = ComponentUtils::getPolicy('sms.sms080');
StringUtils::strIsSet($policy['status'], '');
$isOpened = $policy['status'] == 'O';
 
$this->json(['success' => $isOpened]);
break;
case 'manualSync':
try {
/** @var Sms080 $sms080 */
$sms080 = \App::load('Component\\Sms\\Sms080');
$sms080->syncListByManual();
$this->json(['success' => true, 'message' => "동기화가 완료되었습니다."]);
} catch (\Throwable $e) {
\Logger::channel('sms')->warning($e->getMessage(), [__METHOD__, $e->getTraceAsString()]);
$this->json(['success' => false, 'message' => "회원 정보와 동기화 중 오류가 발생했습니다."]);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerRejectNumberListPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerRejectNumberListResultController.php-44
변경 전LayerRejectNumberListResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Bundle\Component\Page\Page;
use Bundle\Component\Sms\Sms080DAO;
use Framework\Http\Request;
 
class LayerRejectNumberListResultController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$naviMenu = $this->getData('naviMenu');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post();
$page = $postData->get('page', 1);
$pageSize = $postData->get('pageSize', 10);
 
$keyword = str_replace('-', '', $postData->get('keyword', ''));
 
/** @var Sms080DAO $dao */
$dao = \App::load('Component\\Sms\\Sms080DAO');
$sms080RejectList = $dao->selectList(['offset' => $page, 'limit' => $pageSize, 'keyword' => $keyword]);
 
$totalCount = $dao->countList(['keyword' => $keyword]);
$amountCount = $dao->countList([]);
$page = new Page($page, $totalCount, $amountCount, $pageSize);
 
$this->setData('sms080RejectList', $sms080RejectList);
$this->setData('keyword', $keyword);
$this->setData('page', $page);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerRejectNumberListResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfig/LayerSmsVerificationController.php-34
변경 전LayerSmsVerificationController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MessageConfig;
 
use Origin\Service\Crm\Message\SmsPasswordService;
 
class LayerSmsVerificationController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$this->setMenuCode('crm', 'messageConfig', 'messageConfig');
 
try {
/** @var SmsPasswordService $smsPasswordService */
$smsPasswordService = \App::getInstance(SmsPasswordService::class);
$smsPassword = $smsPasswordService->getSmsPassword(true);
$isVerified = $smsPasswordService->verifySmsPassword($smsPassword);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$isVerified = false;
}
 
$this->setData('smsPassword', $smsPassword);
$this->setData('isVerified', $isVerified);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSmsVerificationController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageConfigPsController.php-65
변경 전MessageConfigPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Http\Request;
use Origin\DTO\Crm\Message\InsufficMessagePointAlertConfigDTO;
use Origin\DTO\Crm\Message\KakaoAlimTalkConfigDTO;
use Origin\DTO\Crm\Message\KakaoFriendTalkConfigDTO;
use Origin\DTO\Crm\Message\MessagePointConfigDTO;
use Origin\DTO\Crm\Message\MyappPushConfigDTO;
use Origin\DTO\Crm\Message\SmsAutoConfigDTO;
use Origin\DTO\Crm\Message\SmsRejectConfigDTO;
use Origin\Service\Crm\Message\MessageConfigService;
 
class MessageConfigPsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageConfig', 'messageConfig');
$this->setMenuCode('crm', 'messageConfig', 'messageConfig');
 
/** @var Request $request */
$request = \App::getInstance('request');
$mode = $request->request()->get('mode');
 
switch ($mode) {
case 'save':
$postValue = $request->post()->toArray();
 
try {
$messagePointConfig = new MessagePointConfigDTO($postValue['messagePointConfig']);
$smsAutoConfig = new SmsAutoConfigDTO($postValue['smsAutoConfig']);
$smsRejectConfig = $postValue['smsRejectUse'] ? new SmsRejectConfigDTO(['use' => $postValue['smsRejectUse']]) : null;
$kakaoFriendTalkConfig = new KakaoFriendTalkConfigDTO($postValue['kakaoFriendTalkConfig']);
$kakaoAlimTalkConfig = new KakaoAlimTalkConfigDTO($postValue['kakaoAlimTalkConfig']);
$myappPushConfig = new MyappPushConfigDTO($postValue['myappPushConfig']);
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
$messageConfigService->save(
$messagePointConfig,
$smsAutoConfig,
$smsRejectConfig,
$kakaoFriendTalkConfig,
$kakaoAlimTalkConfig,
$myappPushConfig
);
 
$this->json(['success' => true, 'message' => '메시지 설정이 저장되었습니다.']);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->warning($e->getMessage(), ['requestData' => $postValue]);
$this->json(['success' => false, 'message' => $e->getMessage()]);
}
break;
default:
throw new \InvalidArgumentException('잘못된 접근입니다.');
}
}
}
 
변경 후MessageConfigPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageTemplateController.php-52
변경 전MessageTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Page\Page;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateCategoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateKakaoStatusDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateListSearchDTO;
use Origin\Service\Crm\Message\MessageTemplateFindService;
use Origin\Traits\Member\KakaoAlrim\KakaoAlrimCloudAvailable;
 
 
class MessageTemplateController extends \Controller\Admin\Controller
{
use KakaoAlrimCloudAvailable;
 
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageSend', 'messageTemplate');
$this->getView()->setPageName('crm/message_template.php');
$this->getView()->setDefine('messageTemplateSearch', 'crm/message_template/message_template_search.php');
$this->getView()->setDefine('messageTemplateResult', 'crm/message_template/message_template_result.php');
 
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
 
/** @var MessageTemplateFindService $messageHistoryService */
$service = \App::getInstance(MessageTemplateFindService::class);
$configDto = $service->getUseFlagDto();
 
$searchDto = new MessageTemplateListSearchDTO($requestData);
$results = $service->getMessageTemplateList($searchDto);
 
$isNewMall = $this->isNewMall();
 
$this->setData('sendMethod', $searchDto->getSendMethod());
$this->setData('search', $searchDto);
$this->setData('config', $configDto);
$this->setData('results', $results['result']);
$this->setData('page', $results['page']);
$this->setData('searchCategory', new MessageTemplateCategoryDTO());
$this->setData('searchStatus', new MessageTemplateKakaoStatusDTO());
$this->setData('isNewMall', $isNewMall);
}
}
 
변경 후MessageTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageTemplatePsController.php-47
변경 전MessageTemplatePsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Http\Request;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateCopyRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateMyappDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateRemoveRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateSmsDTO;
use Origin\Service\Crm\Message\MessageTemplateFindService;
use Origin\Service\Crm\Message\MessageTemplateRegisterService;
use Origin\Service\Crm\Message\MessageTemplateRemoveService;
 
class MessageTemplatePsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
$mode = $requestData['mode'] ?? '';
 
switch ($mode) {
case 'delete':
$requestDto = new MessageTemplateRemoveRequestDTO($requestData);
/** @var MessageTemplateRemoveService $service */
$service = \App::getInstance(MessageTemplateRemoveService::class);
$result = $service->deleteTemplate($requestDto);
break;
case 'copy':
$requestDto = new MessageTemplateCopyRequestDTO($requestData);
/** @var MessageTemplateRegisterService $service */
$service = \App::getInstance(MessageTemplateRegisterService::class);
$result = $service->copyTemplate($requestDto);
break;
default:
$result = ['success' => false, 'message' => '잘못된 요청입니다.'];
break;
}
$this->json($result);
}
}
 
변경 후MessageTemplatePsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageTemplateRegisterController.php-75
변경 전MessageTemplateRegisterController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Http\Request;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateCategoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateKakaoStatusDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateRegisterRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateResultDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateSearchDTO;
use Origin\Service\Crm\Message\MessageTemplateFindService;
use Origin\Service\Crm\Message\MyappPushTemplateService;
use Origin\Traits\Member\KakaoAlrim\KakaoAlrimCloudAvailable;
use function JmesPath\search;
 
class MessageTemplateRegisterController extends \Controller\Admin\Controller
{
use KakaoAlrimCloudAvailable;
 
public function index()
{
$this->setNcdsSubBodyClass();
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
 
 
/** @var MessageTemplateFindService $messageHistoryService */
$service = \App::getInstance(MessageTemplateFindService::class);
$configDto = $service->getUseFlagDto();
 
/** @var MessageTemplateRegisterRequestDTO $messageTemplateRegisterRequestDTO */
$requestDto = new MessageTemplateRegisterRequestDTO($requestData);
$searchDto = new MessageTemplateSearchDTO($requestData);
 
$this->getView()->setPageName('crm/message_template_register.php');
$this->getView()->setDefine('basicConfig', 'crm/message_template_register/basic_config.php');
$this->getView()->setDefine('templateContentsSms', 'crm/message_template_register/template_contents_sms.php');
$this->getView()->setDefine('templateContentsKakao', 'crm/message_template_register/template_contents_kakao.php');
$this->getView()->setDefine('templateContentsMyapp', 'crm/message_template_register/template_contents_myapp.php');
$this->getView()->setDefine('previewMessage', 'crm/message_template_register/preview_message.php');
 
switch ($requestDto->getMode()) {
case 'register':
$requestDto = $service->changeRegisterPormat($requestDto,$configDto);
$this->callMenu('crm', 'messageSend', 'messageTemplateRegister');
$responseDto = new MessageTemplateResultDTO();
break;
case 'copy':
$this->callMenu('crm', 'messageSend', 'messageTemplateRegister');
$responseDto = $service->getMessageTemplate($searchDto);
break;
case 'modify':
$this->callMenu('crm', 'messageSend', 'messageTemplateModify');
$responseDto = $service->getMessageTemplate($searchDto);
break;
default:
$responseDto = new MessageTemplateResultDTO();
}
$isNewMall = $this->isNewMall();
 
$myappService = \App::getInstance(MyappPushTemplateService::class);
$this->getView()->setData('myappUrl', $myappService->getMyappDomainUrl());
$this->getView()->setData('config', $configDto);
$this->getView()->setData('request', $requestDto);
$this->getView()->setData('response', $responseDto);
$this->setData('category', new MessageTemplateCategoryDTO());
$this->setData('status', new MessageTemplateKakaoStatusDTO());
$this->setData('isNewMall', $isNewMall);
}
}
 
변경 후MessageTemplateRegisterController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MessageTemplateRegisterPsController.php-50
변경 전MessageTemplateRegisterPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateKakaoBizmDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateKakaoCloudDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateMyappDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateRegisterRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateSmsDTO;
use Origin\Service\Crm\Message\MessageTemplateModifyService;
use Origin\Service\Crm\Message\MessageTemplateRegisterService;
 
class MessageTemplateRegisterPsController extends \Controller\Admin\Controller
{
public function index()
{
try {
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
$filesData = $request->files()->toArray();
 
$requestData['mode'] = ($requestData['mode'] === 'copy') ? 'register' : $requestData['mode'];
$requestDto = new MessageTemplateRegisterRequestDTO($requestData);
 
switch ($requestDto->getMode()){
case 'register':
/** @var MessageTemplateRegisterService $service */
$service = \App::getInstance(MessageTemplateRegisterService::class);
$result = $service->saveTemplate($requestDto, $requestData, $filesData);
break;
case 'modify':
/** @var MessageTemplateModifyService $service */
$service = \App::getInstance(MessageTemplateModifyService::class);
$result = $service->updateTemplate($requestDto, $requestData, $filesData);
break;
default:
$result = ['success' => false, 'message' => '잘못된 모드입니다.'];
break;
}
$this->json($result);
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => '등록, 수정에 실패하였습니다']);
}
}
}
 
변경 후MessageTemplateRegisterPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileHistoryController.php-109
변경 전MobileHistoryController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use App;
use Component\Sms\Sms;
use Component\Sms\SmsUtil;
use DateTime;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistorySummaryRequestDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistorySummaryResponseDTO;
use Origin\DTO\ApiClient\Commerce\Crm\CampaignCountsRequestDTO;
use Origin\DTO\ApiClient\Commerce\Crm\CampaignCountsResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageHistorySummaryResponseDTO;
use Origin\Enum\ApiClient\Crm\CampaignSendType;
use Origin\Enum\ApiClient\Crm\CampaignStatusType;
use Origin\Service\AutoSend\ActiveAutoSendService;
use Origin\Service\Crm\Message\CrmNotificationCampaignService;
use Origin\Service\Crm\Message\MessageHistoryService;
use Origin\Service\Crm\Message\MyappPushHistoryService;
 
class MobileHistoryController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageHistory', 'mobileHistory');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistory');
 
try {
/** @var MessageHistoryService $messageHistoryService */
$messageHistoryService = \App::getInstance(MessageHistoryService::class);
$messageHistorySummaryResponseDTO = $messageHistoryService->getMessageHistorySummary();
 
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$messageHistorySummaryResponseDTO = new MessageHistorySummaryResponseDTO(null);
}
 
try {
$requestDTO = (new MyappPushHistorySummaryRequestDTO())
->setStartDate(new DateTime('-7 days'))
->setEndDate(new DateTime('-1 days'));
 
/** @var MyappPushHistoryService $myappPushHistoryService */
$myappPushHistoryService = \App::getInstance(MyappPushHistoryService::class);
$myappPushHistorySummaryResponseDTO = $myappPushHistoryService->getMyappPushHistorySummary($requestDTO);
 
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$myappPushHistorySummaryResponseDTO = new MyappPushHistorySummaryResponseDTO(null);
}
 
try {
$requestDTO = (new CampaignCountsRequestDTO())
->setSendType([CampaignSendType::SCHEDULED, CampaignSendType::REPEAT])
->setStatusType([CampaignStatusType::WAITING, CampaignStatusType::PROCESSING]);
 
 
/** @var CrmNotificationCampaignService $crmNotificationCampaignService */
$crmNotificationCampaignService = \App::getInstance(CrmNotificationCampaignService::class);
$crmNotificationCampaignCountsResponseDTO = $crmNotificationCampaignService->getNotificationCampaignCounts($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$crmNotificationCampaignCountsResponseDTO = new CampaignCountsResponseDTO(null);
}
 
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
// 현재 메시지 포인트
$nowSmsPoint = (float) Sms::getPoint();
 
$availableCount = [
'sms' => SmsUtil::getAvailableCount('sms', $nowSmsPoint),
'lms' => SmsUtil::getAvailableCount('lms', $nowSmsPoint),
'kakaoAlrimTalk' => SmsUtil::getAvailableCount('kakaoAlrimTalk', $nowSmsPoint),
'kakaoFriendTalk' => SmsUtil::getAvailableCount('kakaoFriendTalk', $nowSmsPoint),
];
 
/** @var ActiveAutoSendService $activeAutoSendService */
$activeAutoSendService = \App::getInstance(ActiveAutoSendService::class);
$autoSendCounts = [
'active' => $activeAutoSendService->count(),
];
 
$this->setData('availableCount', $availableCount);
 
$this->setData('messageCountLast7Days', $messageHistorySummaryResponseDTO->getMessageCountLast7Days());
$this->setData('friendtalkCountLast7Days', $messageHistorySummaryResponseDTO->getFriendtalkCountLast7Days());
$this->setData('alimtalkCountLast7Days', $messageHistorySummaryResponseDTO->getAlimtalkCountLast7Days());
$this->setData('myappPushCountLast7Days', $myappPushHistorySummaryResponseDTO->getTotalPushMessageCount());
$this->setData('pointUsageLast7Days', $messageHistorySummaryResponseDTO->getPointUsageLast7Days());
$this->setData('pointUsageLast28Days', $messageHistorySummaryResponseDTO->getPointUsageLast28Days());
$this->setData('crmNotificationCampaignCounts', $crmNotificationCampaignCountsResponseDTO->getStatusCount());
$this->setData('autoSendCounts', $autoSendCounts);
 
$this->setData('scheduledSendWaitingUrl', './scheduled_send.php?statusType=WAITING');
$this->setData('scheduledSendRepeatUrl', './scheduled_send.php?formType=REPEAT&statusType=PROCESSING&periodType=1YEAR&createdAtFrom=' . date('Y-m-d', strtotime('-1 year')) . '&createdAtTo=' . date('Y-m-d'));
$this->setData('autoSendUrl', './auto_send.php?shouldAutoSend=y');
 
$this->getView()->setPageName('crm/mobile_history.php');
}
}
 
변경 후MobileHistoryController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileHistoryListController.php-27
변경 전MobileHistoryListController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Http\Request;
 
class MobileHistoryListController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$getData = $request->get()->toArray();
 
$this->setData('sendMethod', $getData['sendMethod']);
$this->getView()->setPageName('crm/mobile_history_list.php');
}
}
 
변경 후MobileHistoryListController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerBizmKakaoAlimTalkHistoryResultController.php-114
변경 전LayerBizmKakaoAlimTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\BizmKakaoAlimTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\BizmKakaoAlimTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\BizmKakaoAlimTalkHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\BizmKakaoAlimTalkHistorySearchFormDTO;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Service\Crm\Message\KakaoAlimTalkHistoryService;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
 
class LayerBizmKakaoAlimTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new BizmKakaoAlimTalkHistorySearchFormDTO($postData);
$searchFromArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFromArray);
$requestDTO = new BizmKakaoAlimTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
$isEnabled = $kakaoAlimTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoAlimTalkHistoryService $kakaoAlimTalkHistoryService */
$kakaoAlimTalkHistoryService = \App::getInstance(KakaoAlimTalkHistoryService::class);
$bizmKakaoAlimTalkHistoryListResponseDTO = $kakaoAlimTalkHistoryService->getBizmKakaoAlimTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$bizmKakaoAlimTalkHistoryListResponseDTO = new BizmKakaoAlimTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $bizmKakaoAlimTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormDTO->toRecursiveArray());
$this->setData('excelSearchQuery', $requestDTO->toRecursiveArray());
$this->setData('excelMenu', ExcelGenerateMenu::ADMIN_BIZM_ALIMTALK_SEND_RESULT);
$this->setData('sort', KakaoAlimTalkHistoryService::SORT_LIST);
$this->setData('bizmKakaoAlimTalkHistoryList', $this->sanitizeHistoryList($bizmKakaoAlimTalkHistoryListResponseDTO->getBizmKakaoAlimTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sort = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sort[1] === 'desc') ? 'true' : 'false';
 
unset($preparedData['triggerTypes']); // bizm 에서는 사용하지 않음
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoAlimTalkSendStatus::availableForSearch());
if (!empty($preparedData['sendStatuses']) && !array_diff($preparedData['sendStatuses'], $allSendStatuses) && !array_diff($allSendStatuses, $preparedData['sendStatuses'])) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
return $preparedData;
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param BizmKakaoAlimTalkHistoryDTO[] $historyList
* @return BizmKakaoAlimTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
}
 
변경 후LayerBizmKakaoAlimTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerCloudKakaoAlimTalkHistoryResultController.php-118
변경 전LayerCloudKakaoAlimTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\CloudKakaoAlimTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\CloudKakaoAlimTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\CloudKakaoAlimTalkHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\CloudKakaoAlimTalkHistorySearchFormDTO;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkTriggerType;
use Origin\Service\Crm\Message\KakaoAlimTalkHistoryService;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
 
class LayerCloudKakaoAlimTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new CloudKakaoAlimTalkHistorySearchFormDTO($postData);
$searchFromArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFromArray);
$requestDTO = new CloudKakaoAlimTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
$isEnabled = $kakaoAlimTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoAlimTalkHistoryService $kakaoAlimTalkHistoryService */
$kakaoAlimTalkHistoryService = \App::getInstance(KakaoAlimTalkHistoryService::class);
$cloudKakaoAlimTalkHistoryListResponseDTO = $kakaoAlimTalkHistoryService->getCloudKakaoAlimTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$cloudKakaoAlimTalkHistoryListResponseDTO = new CloudKakaoAlimTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $cloudKakaoAlimTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFromArray);
$this->setData('excelSearchQuery', $requestDTO->toRecursiveArray());
$this->setData('excelMenu', ExcelGenerateMenu::ADMIN_ALIMTALK_SEND_RESULT);
$this->setData('sort', KakaoAlimTalkHistoryService::SORT_LIST);
$this->setData('cloudKakaoAlimTalkHistoryList', $this->sanitizeHistoryList($cloudKakaoAlimTalkHistoryListResponseDTO->getCloudKakaoAlimTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param CloudKakaoAlimTalkHistoryDTO[] $historyList
* @return CloudKakaoAlimTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sort = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sort[1] === 'desc') ? 'true' : 'false';
 
$allTriggerTypes = array_map(fn($e) => $e->name, KakaoAlimTalkTriggerType::availableForSearch());
if (!empty($preparedData['triggerTypes']) && !array_diff($preparedData['triggerTypes'], $allTriggerTypes) && !array_diff($allTriggerTypes, $preparedData['triggerTypes'])) {
unset($preparedData['triggerTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoAlimTalkSendStatus::availableForSearch());
if (!empty($preparedData['sendStatuses']) && !array_diff($preparedData['sendStatuses'], $allSendStatuses) && !array_diff($allSendStatuses, $preparedData['sendStatuses'])) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
return $preparedData;
}
}
 
변경 후LayerCloudKakaoAlimTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerKakaoAlimTalkContentsController.php-84
변경 전LayerKakaoAlimTalkContentsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\KakaoAlimTalkHistoryContentRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\KakaoAlimTalkTemplateButtonDTO;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkTemplateButtonType;
use Origin\Enum\ApiClient\Notification\NotificationSendType;
use Origin\Service\Crm\Message\KakaoAlimTalkHistoryService;
 
class LayerKakaoAlimTalkContentsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$sendGroupKey = $request->post()->get('sendGroupKey');
 
try {
$requestDTO = (new KakaoAlimTalkHistoryContentRequestDTO(['type' => NotificationSendType::ALIMTALK->name]));
/** @var KakaoAlimTalkHistoryService $kakaoAlimTalkHistoryService */
$kakaoAlimTalkHistoryService = \App::getInstance(KakaoAlimTalkHistoryService::class);
$kakaoAlimTalkTalkHistoryContentDTO = $kakaoAlimTalkHistoryService->getKakaoAlimTalkHistoryContent($sendGroupKey, $requestDTO);
} catch (\Throwable $e) {
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
 
$templateButtons = $kakaoAlimTalkTalkHistoryContentDTO->getTemplateButtons();
$hasAddChannelButton = !empty(array_filter($templateButtons ?? [], fn($button) => $button->getType() === KakaoAlimTalkTemplateButtonType::ADD_CHANNEL));
$this->sortButtons($templateButtons);
 
$previewMessage = null;
if ($kakaoAlimTalkTalkHistoryContentDTO->isPendingOrRequestFailed()) {
$previewMessage = '발송 중이거나 발송 요청 단계에서 실패한 메시지는 본문 내용만 제공됩니다.';
} elseif (!$kakaoAlimTalkTalkHistoryContentDTO->isTemplateExists() || !empty($kakaoAlimTalkTalkHistoryContentDTO->getTemplateImageUrl())) {
$previewMessage = '상단 이미지는 현재 템플릿 기준으로 제공되며, 템플릿이 삭제된 경우 이미지를 표시할 수 없습니다.';
}
 
$this->setData('kakaoAlimTalkTalkHistoryContentDTO', $kakaoAlimTalkTalkHistoryContentDTO);
$this->setData('templateContent', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateContent());
$this->setData('templateExtra', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateExtra());
$this->setData('hasAddChannelButton', $hasAddChannelButton);
$this->setData('templateButtons', $templateButtons);
$this->setData('templateTitle', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateTitle());
$this->setData('templateSubTitle', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateSubTitle());
$this->setData('templateItemHighlight', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateItemHighlight());
$this->setData('templateItems', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateItems());
$this->setData('templateImageUrl', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateImageUrl());
$this->setData('isTemplateExists', $kakaoAlimTalkTalkHistoryContentDTO->isTemplateExists());
$this->setData('previewMessage', $previewMessage);
$this->setData('isPendingOrRequestFailed', $kakaoAlimTalkTalkHistoryContentDTO->isPendingOrRequestFailed());
$this->setData('templateHeader', $kakaoAlimTalkTalkHistoryContentDTO->getTemplateHeader());
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
/**
* 버튼 유형에 따라 분리
*
* @param KakaoAlimTalkTemplateButtonDTO[] $templateButtons
* @return void
*/
protected function sortButtons(?array &$templateButtons): void
{
if (empty($templateButtons)) {
return;
}
 
usort($templateButtons, function ($a, $b) {
return ($a->getOrder() ?? 0) <=> ($b->getOrder() ?? 0);
});
}
}
 
변경 후LayerKakaoAlimTalkContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerKakaoAlimTalkHistorySearchController.php-49
변경 전LayerKakaoAlimTalkHistorySearchController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Enum\ExternalUrl;
use Framework\Utility\GodoUtils;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkResultSearchType;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkTriggerType;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Traits\Member\KakaoAlrim\KakaoAlrimCloudAvailable;
 
class LayerKakaoAlimTalkHistorySearchController extends \Controller\Admin\Controller
{
use KakaoAlrimCloudAvailable;
 
public function index()
{
$triggerTypes = KakaoAlimTalkTriggerType::availableForSearch();
$sendStatuses = KakaoAlimTalkSendStatus::availableForSearch();
$searchType = KakaoAlimTalkResultSearchType::generateSelectBoxData();
 
$isKakaoAlrimAvailable = !$this->isNewMall();
$isKakaoAlrimLunaInstalled = GodoUtils::isPlusShop(PLUSSHOP_CODE_KAKAOALRIMLUNA);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
$isSelectedBizm = $kakaoAlimTalkConfig->getSender() === KakaoAlimTalkSender::BIZM;
 
$this->setData('searchType', $searchType);
$this->setData('isSelectedBizm', $isSelectedBizm);
$this->setData('triggerTypes', $triggerTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->setData('isKakaoAlrimAvailable', $isKakaoAlrimAvailable);
$this->setData('isKakaoAlrimLunaInstalled', $isKakaoAlrimLunaInstalled);
$this->setData('blumnAiStatisticsUrl', ExternalUrl::BIZMSG_BLUMN_AI->getUrl('/statistics-alimtalk'));
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerKakaoAlimTalkHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkContentsController.php-56
변경 전LayerKakaoFriendTalkContentsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkType;
use Origin\Service\Crm\Message\KakaoFriendTalkHistoryService;
 
class LayerKakaoFriendTalkContentsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$sendHistoryNo = $request->post()->get('sendHistoryNo');
 
try {
/** @var KakaoFriendTalkHistoryService $kakaoFriendTalkHistoryService */
$kakaoFriendTalkHistoryService = \App::getInstance(KakaoFriendTalkHistoryService::class);
$kakaoFriendTalkHistoryContentDTO = $kakaoFriendTalkHistoryService->getKakaoFriendTalkHistoryContent($sendHistoryNo);
} catch (\Throwable $e) {
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
 
\Logger::channel('mobileMessage')->info('content', $kakaoFriendTalkHistoryContentDTO->getContent());
 
$this->setData('type', $kakaoFriendTalkHistoryContentDTO->getType());
$this->setData('isText', $kakaoFriendTalkHistoryContentDTO->getType() === KakaoFriendTalkType::TEXT);
$this->setData('isCarousel', $kakaoFriendTalkHistoryContentDTO->getType() === KakaoFriendTalkType::CAROUSEL);
$this->setData('isWideItemList', $kakaoFriendTalkHistoryContentDTO->getType() === KakaoFriendTalkType::WIDE_ITEM_LIST);
$this->setData('wideTypes', KakaoFriendTalkType::wideTypes());
$this->setData('availableImageTypes', KakaoFriendTalkType::availableImageTypes());
$this->setData('singleButtonTypes', KakaoFriendTalkType::singleButtonTypes());
$this->setData('doubleButtonTypes', KakaoFriendTalkType::doubleButtonTypes());
$this->setData('header', $kakaoFriendTalkHistoryContentDTO->getHeader());
$this->setData('imageUrl', $kakaoFriendTalkHistoryContentDTO->getImageUrl());
$this->setData('imageLink', $kakaoFriendTalkHistoryContentDTO->getImageLink());
$this->setData('items', $kakaoFriendTalkHistoryContentDTO->getItems());
$this->setData('content', $kakaoFriendTalkHistoryContentDTO->getContent());
$this->setData('buttons', $kakaoFriendTalkHistoryContentDTO->getButtons());
$this->setData('coupon', $kakaoFriendTalkHistoryContentDTO->getCoupon());
$this->setData('carousels', $kakaoFriendTalkHistoryContentDTO->getCarousels());
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerKakaoFriendTalkContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkHistoryResultController.php-121
변경 전LayerKakaoFriendTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Origin\DTO\ApiClient\Commerce\Apps\KakaoFriendTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Apps\KakaoFriendTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Apps\KakaoFriendTalkHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Apps\KakaoFriendTalkHistorySearchFormDTO;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultDirection;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultSortType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkScheduleType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkSendStatus;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Origin\Service\Crm\Message\KakaoFriendTalkHistoryService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
 
class LayerKakaoFriendTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new KakaoFriendTalkHistorySearchFormDTO($postData);
$searchFormArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFormArray);
$requestDTO = new KakaoFriendTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
$isEnabled = $kakaoFriendTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoFriendTalkHistoryService $kakaoFriendTalkHistoryService */
$kakaoFriendTalkHistoryService = \App::getInstance(KakaoFriendTalkHistoryService::class);
$kakaoFriendTalkHistoryListResponseDTO = $kakaoFriendTalkHistoryService->getKakaoFriendTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$kakaoFriendTalkHistoryListResponseDTO = new KakaoFriendTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $kakaoFriendTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormArray);
$this->setData('excelSearchQuery', $requestDTO->toRecursiveArray());
$this->setData('excelMenu', ExcelGenerateMenu::ADMIN_FRIENDTALK_SEND_RESULT);
$this->setData('sort', KakaoFriendTalkHistoryService::generateSortList());
$this->setData('kakaoFriendTalkHistoryList', $this->sanitizeHistoryList($kakaoFriendTalkHistoryListResponseDTO->getKakaoFriendTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param KakaoFriendTalkHistoryDTO[] $historyList
* @return KakaoFriendTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendGroupKey = $dto->getSendGroupKey();
if (!empty($sendGroupKey)) {
$dto->setSendGroupKey(htmlspecialchars($sendGroupKey, ENT_QUOTES, 'UTF-8'));
}
 
$campaignName = $dto->getCampaignName();
if (!empty($campaignName)) {
$dto->setCampaignName(htmlspecialchars($campaignName, ENT_QUOTES, 'UTF-8'));
}
 
$snapshot = $dto->getSnapshot();
if (!empty($snapshot)) {
$dto->setSnapshot(htmlspecialchars($snapshot, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
 
$sort = explode(' ', $preparedData['sort'] ?? (KakaoFriendTalkResultSortType::CREATED_DATE_TIME->name . ' ' . KakaoFriendTalkResultDirection::DESC->name));
$preparedData['sortType'] = (KakaoFriendTalkResultSortType::tryFromName($sort[0]) ?? KakaoFriendTalkResultSortType::CREATED_DATE_TIME)->name;
$preparedData['direction'] = (KakaoFriendTalkResultDirection::tryFromName(strtoupper($sort[1])) ?? KakaoFriendTalkResultDirection::DESC)->name;
 
$allScheduleTypes = array_map(fn($e) => $e->name, KakaoFriendTalkScheduleType::cases());
if (!empty($preparedData['scheduleTypes']) && !array_diff($preparedData['scheduleTypes'], $allScheduleTypes) && !array_diff($allScheduleTypes, $preparedData['scheduleTypes'])) {
unset($preparedData['scheduleTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoFriendTalkSendStatus::availableForSearch());
if (!empty($preparedData['sendResultStatuses']) && !array_diff($preparedData['sendResultStatuses'], $allSendStatuses) && !array_diff($allSendStatuses, $preparedData['sendResultStatuses'])) {
unset($preparedData['sendResultStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
return $preparedData;
}
}
 
변경 후LayerKakaoFriendTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerKakaoFriendTalkHistorySearchController.php-28
변경 전LayerKakaoFriendTalkHistorySearchController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultSearchType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkScheduleType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkSendStatus;
 
class LayerKakaoFriendTalkHistorySearchController extends \Controller\Admin\Controller
{
public function index()
{
$scheduleTypes = KakaoFriendTalkScheduleType::cases();
$sendResultStatuses = KakaoFriendTalkSendStatus::availableForSearch();
$searchType = KakaoFriendTalkResultSearchType::generateSelectBoxData();
 
$this->setData('searchType', $searchType);
$this->setData('scheduleTypes', $scheduleTypes);
$this->setData('sendResultStatuses', $sendResultStatuses);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerKakaoFriendTalkHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerLunaKakaoAlimTalkHistoryResultController.php-26
변경 전LayerLunaKakaoAlimTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
 
class LayerLunaKakaoAlimTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$naviMenu = $this->getData('naviMenu');
 
/** @var Request $request */
$request = \App::getInstance('request');
$requestData = $request->request()->toArray();
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerLunaKakaoAlimTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberBizmKakaoAlimTalkHistoryResultController.php-115
변경 전LayerMemberBizmKakaoAlimTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\MemberBizmKakaoAlimTalkHistorySearchFormDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberBizmKakaoAlimTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberBizmKakaoAlimTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberBizmKakaoAlimTalkHistoryListResponseDTO;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Service\Crm\Message\KakaoAlimTalkHistoryService;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
 
class LayerMemberBizmKakaoAlimTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new MemberBizmKakaoAlimTalkHistorySearchFormDTO($postData);
$searchFromArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFromArray);
$requestDTO = new MemberBizmKakaoAlimTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
$isEnabled = $kakaoAlimTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoAlimTalkHistoryService $kakaoAlimTalkHistoryService */
$kakaoAlimTalkHistoryService = \App::getInstance(KakaoAlimTalkHistoryService::class);
$bizmKakaoAlimTalkHistoryListResponseDTO = $kakaoAlimTalkHistoryService->getMemberBizmKakaoAlimTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$bizmKakaoAlimTalkHistoryListResponseDTO = new MemberBizmKakaoAlimTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $bizmKakaoAlimTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormDTO->toRecursiveArray());
$this->setData('sort', KakaoAlimTalkHistoryService::SORT_LIST);
$this->setData('bizmKakaoAlimTalkHistoryList', $this->sanitizeHistoryList($bizmKakaoAlimTalkHistoryListResponseDTO->getBizmKakaoAlimTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $throwable) {
$this->json(['success' => false, 'message' => $throwable->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sort = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sort[1] === 'desc') ? 'true' : 'false';
 
unset($preparedData['triggerTypes']); // bizm 에서는 사용하지 않음
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoAlimTalkSendStatus::availableForMemberHistorySearch());
if (
!empty($preparedData['sendStatuses']) &&
!array_diff($preparedData['sendStatuses'], $allSendStatuses) &&
!array_diff($allSendStatuses, $preparedData['sendStatuses'])
) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
return $preparedData;
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param MemberBizmKakaoAlimTalkHistoryDTO[] $historyList
* @return MemberBizmKakaoAlimTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
}
 
변경 후LayerMemberBizmKakaoAlimTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberCloudKakaoAlimTalkHistoryResultController.php-119
변경 전LayerMemberCloudKakaoAlimTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\MemberCloudKakaoAlimTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberCloudKakaoAlimTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberCloudKakaoAlimTalkHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberCloudKakaoAlimTalkHistorySearchFormDTO;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkTriggerType;
use Origin\Service\Crm\Message\KakaoAlimTalkHistoryService;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
 
class LayerMemberCloudKakaoAlimTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new MemberCloudKakaoAlimTalkHistorySearchFormDTO($postData);
$searchFromArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFromArray);
$requestDTO = new MemberCloudKakaoAlimTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
$isEnabled = $kakaoAlimTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoAlimTalkHistoryService $kakaoAlimTalkHistoryService */
$kakaoAlimTalkHistoryService = \App::getInstance(KakaoAlimTalkHistoryService::class);
$cloudKakaoAlimTalkHistoryListResponseDTO = $kakaoAlimTalkHistoryService->getMemberCloudKakaoAlimTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$cloudKakaoAlimTalkHistoryListResponseDTO = new MemberCloudKakaoAlimTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $cloudKakaoAlimTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFromArray);
$this->setData('sort', KakaoAlimTalkHistoryService::SORT_LIST);
$this->setData('cloudKakaoAlimTalkHistoryList', $this->sanitizeHistoryList($cloudKakaoAlimTalkHistoryListResponseDTO->getMemberCloudKakaoAlimTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $throwable) {
$this->json(['success' => false, 'message' => $throwable->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param MemberCloudKakaoAlimTalkHistoryDTO[] $historyList
* @return MemberCloudKakaoAlimTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sort = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sort[1] === 'desc') ? 'true' : 'false';
 
$allTriggerTypes = array_map(fn($e) => $e->name, KakaoAlimTalkTriggerType::availableForSearch());
if (!empty($preparedData['triggerTypes']) && !array_diff($preparedData['triggerTypes'], $allTriggerTypes) && !array_diff($allTriggerTypes, $preparedData['triggerTypes'])) {
unset($preparedData['triggerTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoAlimTalkSendStatus::availableForMemberHistorySearch());
if (
!empty($preparedData['sendStatuses']) &&
!array_diff($preparedData['sendStatuses'], $allSendStatuses) &&
!array_diff($allSendStatuses, $preparedData['sendStatuses'])
) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
return $preparedData;
}
}
 
변경 후LayerMemberCloudKakaoAlimTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberKakaoAlimTalkHistorySearchController.php-56
변경 전LayerMemberKakaoAlimTalkHistorySearchController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Enum\ExternalUrl;
use Framework\Http\Request;
use Framework\Utility\GodoUtils;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkResultSearchType;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkSendStatus;
use Origin\Enum\ApiClient\Notification\KakaoAlimTalkTriggerType;
use Origin\Enum\Crm\Message\KakaoAlimTalkSender;
use Origin\Service\Crm\Message\KakaoAlimTalkSettingService;
use Origin\Traits\Member\KakaoAlrim\KakaoAlrimCloudAvailable;
 
class LayerMemberKakaoAlimTalkHistorySearchController extends \Controller\Admin\Controller
{
use KakaoAlrimCloudAvailable;
 
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$memberNo = filter_var($postData['memberNo'] ?? 0, FILTER_VALIDATE_INT) ?: 0;
 
$triggerTypes = KakaoAlimTalkTriggerType::availableForSearch();
$sendStatuses = KakaoAlimTalkSendStatus::availableForMemberHistorySearch();
$searchType = KakaoAlimTalkResultSearchType::generateSelectBoxData();
 
$isKakaoAlrimAvailable = !$this->isNewMall();
$isKakaoAlrimLunaInstalled = GodoUtils::isPlusShop(PLUSSHOP_CODE_KAKAOALRIMLUNA);
 
/** @var KakaoAlimTalkSettingService $kakaoAlimTalkSettingService */
$kakaoAlimTalkSettingService = \App::getInstance(KakaoAlimTalkSettingService::class);
$kakaoAlimTalkConfig = $kakaoAlimTalkSettingService->getKakaoAlimTalkConfig();
 
$isSelectedBizm = $kakaoAlimTalkConfig->getSender() === KakaoAlimTalkSender::BIZM;
 
$this->setData('memberNo', $memberNo);
$this->setData('searchType', $searchType);
$this->setData('isSelectedBizm', $isSelectedBizm);
$this->setData('triggerTypes', $triggerTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->setData('isKakaoAlrimAvailable', $isKakaoAlrimAvailable);
$this->setData('isKakaoAlrimLunaInstalled', $isKakaoAlrimLunaInstalled);
$this->setData('blumnAiStatisticsUrl', ExternalUrl::BIZMSG_BLUMN_AI->getUrl('/statistics-alimtalk'));
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMemberKakaoAlimTalkHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberKakaoFriendTalkHistoryResultController.php-126
변경 전LayerMemberKakaoFriendTalkHistoryResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Apps\MemberKakaoFriendTalkHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MemberKakaoFriendTalkHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MemberKakaoFriendTalkHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MemberKakaoFriendTalkHistorySearchFormDTO;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultDirection;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultSortType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkScheduleType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkSendStatus;
use Origin\Service\Crm\Message\KakaoFriendTalkHistoryService;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
 
class LayerMemberKakaoFriendTalkHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new MemberKakaoFriendTalkHistorySearchFormDTO($postData);
$searchFormArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFormArray);
$requestDTO = new MemberKakaoFriendTalkHistoryListRequestDTO($preparedData);
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
$isEnabled = $kakaoFriendTalkConfig->getUseFlag() === 'y';
 
try {
/** @var KakaoFriendTalkHistoryService $kakaoFriendTalkHistoryService */
$kakaoFriendTalkHistoryService = \App::getInstance(KakaoFriendTalkHistoryService::class);
$kakaoFriendTalkHistoryListResponseDTO = $kakaoFriendTalkHistoryService->getMemberKakaoFriendTalkHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$kakaoFriendTalkHistoryListResponseDTO = new MemberKakaoFriendTalkHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $kakaoFriendTalkHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormArray);
$this->setData('sort', KakaoFriendTalkHistoryService::generateSortList());
$this->setData('kakaoFriendTalkHistoryList', $this->sanitizeHistoryList($kakaoFriendTalkHistoryListResponseDTO->getKakaoFriendTalkHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $throwable) {
$this->json(['success' => false, 'message' => $throwable->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param MemberKakaoFriendTalkHistoryDTO[] $historyList
* @return MemberKakaoFriendTalkHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendGroupKey = $dto->getSendGroupKey();
if (!empty($sendGroupKey)) {
$dto->setSendGroupKey(htmlspecialchars($sendGroupKey, ENT_QUOTES, 'UTF-8'));
}
 
$campaignName = $dto->getCampaignName();
if (!empty($campaignName)) {
$dto->setCampaignName(htmlspecialchars($campaignName, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
 
// 정렬 기준 추가를 대비하여 분리
$sortType = explode(' ', $preparedData['sort'] ?? (KakaoFriendTalkResultSortType::CREATED_DATE_TIME->name . ' ' . KakaoFriendTalkResultDirection::DESC->name));
$preparedData['sortDescending'] = ($sortType[1] === KakaoFriendTalkResultDirection::DESC->name) ? 'true' : 'false';
 
$allScheduleTypes = array_map(fn($e) => $e->name, KakaoFriendTalkScheduleType::cases());
if (
!empty($preparedData['triggerTypes']) &&
!array_diff($preparedData['triggerTypes'], $allScheduleTypes) &&
!array_diff($allScheduleTypes, $preparedData['triggerTypes'])
) {
unset($preparedData['triggerTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, KakaoFriendTalkSendStatus::availableForMemberHistorySearch());
if (
!empty($preparedData['sendStatuses']) &&
!array_diff($preparedData['sendStatuses'], $allSendStatuses) &&
!array_diff($allSendStatuses, $preparedData['sendStatuses'])
) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
return $preparedData;
}
}
 
변경 후LayerMemberKakaoFriendTalkHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberKakaoFriendTalkHistorySearchController.php-35
변경 전LayerMemberKakaoFriendTalkHistorySearchController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkResultSearchType;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkSendStatus;
use Origin\Enum\ApiClient\Apps\KakaoFriendTalkTriggerType;
 
class LayerMemberKakaoFriendTalkHistorySearchController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$memberNo = filter_var($postData['memberNo'] ?? 0, FILTER_VALIDATE_INT) ?: 0;
 
$triggerTypes = KakaoFriendTalkTriggerType::cases();
$sendStatuses = KakaoFriendTalkSendStatus::availableForMemberHistorySearch();
$searchType = KakaoFriendTalkResultSearchType::generateSelectBoxData();
 
$this->setData('memberNo', $memberNo);
$this->setData('searchType', $searchType);
$this->setData('triggerTypes', $triggerTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMemberKakaoFriendTalkHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberSmsHistoryResultController.php-122
변경 전LayerMemberSmsHistoryResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\MemberSmsHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberSmsHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberSmsHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MemberSmsHistorySearchFormDTO;
use Origin\Enum\ApiClient\Notification\SmsSendStatus;
use Origin\Enum\ApiClient\Notification\SmsTriggerType;
use Origin\Service\Crm\Message\SmsHistoryService;
 
class LayerMemberSmsHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new MemberSmsHistorySearchFormDTO($postData);
$searchFormArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFormArray);
$requestDTO = new MemberSmsHistoryListRequestDTO($preparedData);
 
try {
/** @var SmsHistoryService $smsHistoryService */
$smsHistoryService = \App::getInstance(SmsHistoryService::class);
$smsHistoryListResponseDTO = $smsHistoryService->getMemberSmsHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO]);
$smsHistoryListResponseDTO = new MemberSmsHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $smsHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormArray);
$this->setData('sortType', SmsHistoryService::SORT_LIST);
$this->setData('smsHistoryList', $this->sanitizeHistoryList($smsHistoryListResponseDTO->getMemberSmsHistoryList()));
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $throwable) {
$this->json(['success' => false, 'message' => $throwable->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param MemberSmsHistoryDTO[] $historyList
* @return MemberSmsHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$senderName = $dto->getSenderName();
if (!empty($senderName)) {
$dto->setSenderName(htmlspecialchars($senderName, ENT_QUOTES, 'UTF-8'));
}
 
$from = $dto->getFrom();
if (!empty($from)) {
$dto->setFrom(htmlspecialchars($from, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sortType = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sortType[1] === 'desc') ? 'true' : 'false';
 
$allTriggerTypes = array_map(fn($e) => $e->name, SmsTriggerType::cases());
if (!empty($preparedData['triggerTypes']) && !array_diff($preparedData['triggerTypes'], $allTriggerTypes) && !array_diff($allTriggerTypes, $preparedData['triggerTypes'])) {
unset($preparedData['triggerTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, SmsSendStatus::availableForMemberHistorySearch());
if (
!empty($preparedData['sendStatuses']) &&
!array_diff($preparedData['sendStatuses'], $allSendStatuses) &&
!array_diff($allSendStatuses, $preparedData['sendStatuses'])
) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['smsType'])) {
unset($preparedData['smsType']);
}
 
return $preparedData;
}
}
 
변경 후LayerMemberSmsHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMemberSmsHistorySearchController.php-38
변경 전LayerMemberSmsHistorySearchController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Origin\Enum\ApiClient\Notification\SmsResultSearchType;
use Origin\Enum\ApiClient\Notification\SmsSendStatus;
use Origin\Enum\ApiClient\Notification\SmsTriggerType;
use Origin\Enum\ApiClient\Notification\SmsType;
 
class LayerMemberSmsHistorySearchController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$triggerTypes = SmsTriggerType::cases();
$sendStatuses = SmsSendStatus::availableForMemberHistorySearch();
$smsTypes = SmsType::cases();
$memberNo = filter_var($postData['memberNo'] ?? 0, FILTER_VALIDATE_INT) ?: 0;
$searchType = SmsResultSearchType::generateSelectBoxData((bool) $memberNo);
 
$this->setData('memberNo', $memberNo);
$this->setData('triggerTypes', $triggerTypes);
$this->setData('smsTypes', $smsTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->setData('searchType', $searchType);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMemberSmsHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryDetailController.php-36
변경 전LayerMessageHistoryDetailController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Origin\Enum\ApiClient\Notification\MessageResultType;
use Origin\Enum\Crm\Message\SendMethod;
 
class LayerMessageHistoryDetailController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$sendGroupKey = $postData['sendGroupKey'];
$type = $postData['type'];
 
$this->setData('sendGroupKey', $sendGroupKey);
$this->setData('type', $type);
if ($type === SendMethod::SMS->name) {
$this->setData('messageResultTypes', MessageResultType::cases());
} else {
$this->setData('messageResultTypes', MessageResultType::availableForSearch());
}
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMessageHistoryDetailController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryDetailResultController.php-52
변경 전LayerMessageHistoryDetailResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\MessageHistoryDetailRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\MessageHistoryDetailResponseDTO;
use Origin\Enum\ApiClient\Crm\WorkingType;
use Origin\Enum\ApiClient\Notification\MessageAccessLocationType;
use Origin\Service\Crm\Message\MessageHistoryService;
 
class LayerMessageHistoryDetailResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$requestDTO = (new MessageHistoryDetailRequestDTO($postData))
->setAccessLocation(MessageAccessLocationType::HISTORY)
->setWorkingType(WorkingType::tryFromName($postData['workingType'] ?? null) ?? WorkingType::VIEW);
try {
/** @var MessageHistoryService $messageHistoryService */
$messageHistoryService = \App::getInstance(MessageHistoryService::class);
$messageHistoryDetailResponseDTO = $messageHistoryService->getMessageHistoryDetail($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO]);
$messageHistoryDetailResponseDTO = new MessageHistoryDetailResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $messageHistoryDetailResponseDTO->getPage();
 
$this->setData('postData', $postData);
$this->setData('messageHistoryDetailList', $messageHistoryDetailResponseDTO->getMessageHistoryDetailList());
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
 
변경 후LayerMessageHistoryDetailResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelDownloadController.php-34
변경 전LayerMessageHistoryExcelDownloadController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Framework\Http\Request;
use Framework\Http\Response;
use Session;
 
class LayerMessageHistoryExcelDownloadController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
if (Session::get('manager.functionAuthState') == 'check' && Session::get('manager.functionAuth.messageExcelDown') != 'y') {
$this->json(['success' => false, 'message' => '권한이 없습니다.<br>권한은 대표운영자에게 문의하시기 바랍니다.'], Response::HTTP_FORBIDDEN);
}
// 대응되는 es_code itemCd를 이용해 사유들 가져오기
$excelDownloadReasonList = ExcelGenerateMenu::fromName($request->post()->get('menu'))->getCode();
 
$this->setData('menu', $excelDownloadReasonList);
$this->setData('excelNo', $postData['excelNo']);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMessageHistoryExcelDownloadController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestController.php-38
변경 전LayerMessageHistoryExcelRequestController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\Enum\ApiClient\Excel\ExcelDownloadRange;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Session;
 
class LayerMessageHistoryExcelRequestController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post();
$menu = ExcelGenerateMenu::fromName($postData->get('type'));
$selectedSendKeys = $postData->get('selectedSendKeys', []);
$searchQuery = $postData->get('searchQuery', []);
 
if (Session::get('manager.functionAuthState') == 'check' && Session::get('manager.functionAuth.messageExcelDown') != 'y') {
$this->json(['success' => false, 'message' => '권한이 없습니다.<br>권한은 대표운영자에게 문의하시기 바랍니다.'], Response::HTTP_FORBIDDEN);
}
 
$this->setData('menu', $menu);
$this->setData('searchQuery', $searchQuery);
$this->setData('selectedSendKeys', $selectedSendKeys);
$this->setData('excelRequestDownloadRanges', ExcelDownloadRange::cases());
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMessageHistoryExcelRequestController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestPsController.php-79
변경 전LayerMessageHistoryExcelRequestPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\Enum\ApiClient\Excel\ExcelDownloadRange;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Origin\Service\Admin\Excel\CommerceExcelService;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Validator;
 
class LayerMessageHistoryExcelRequestPsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'generate':
try {
Validator::arrayVal()
->key('menu', Validator::stringType()->notEmpty()->in(array_map(fn($case) => $case->name, ExcelGenerateMenu::cases())))
->key('downloadRange', Validator::stringType()->notEmpty()->in(array_map(fn($case) => $case->name, ExcelDownloadRange::cases())))
->key('excelPassword', Validator::stringType()->notEmpty())
->assert($postData);
 
/** @var CommerceExcelService $commerceExcelService */
$commerceExcelService = \App::getInstance(CommerceExcelService::class);
$requestDTO = $commerceExcelService->prepareCommerceExcelGenerateRequestDTO($postData);
$commerceExcelService->requestExcelGenerate($requestDTO);
 
$this->json(['success' => true, 'message' => '다운로드할 엑셀 파일 생성이 완료되었습니다.<br />파일을 선택하여 다운로드하시기 바랍니다.'], Response::HTTP_OK);
} catch (ValidationException $e) {
\Logger::channel('excel')->error("엑셀 생성 요청 파라미터가 잘못 되었습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
} catch (\Throwable $e) {
\Logger::channel('excel')->error("엑셀 생성 요청에 실패 하였습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'download':
try {
Validator::arrayVal()
->key('reason', Validator::stringType()->notEmpty())
->key('excelNo', Validator::stringType()->notEmpty())
->assert($postData);
 
$naviMenu = $this->getData('naviMenu');
 
/** @var CommerceExcelService $commerceExcelService */
$commerceExcelService = \App::getInstance(CommerceExcelService::class);
$requestDTO = $commerceExcelService->prepareCommerceExcelDownloadRequestDTO($postData['reason'], $naviMenu->location[0], $naviMenu->location[2]);
$commerceExcelService->downloadExcel($requestDTO, $postData['excelNo']);
} catch (ValidationException $e) {
\Logger::channel('excel')->error("엑셀 다운로드 파라미터가 잘못 되었습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
} catch (\Throwable $e) {
\Logger::channel('excel')->error("엑셀 다운로드에 실패 하였습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
default:
throw new \InvalidArgumentException('잘못된 접근입니다.');
}
}
}
 
변경 후LayerMessageHistoryExcelRequestPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMessageHistoryExcelRequestResultController.php-58
변경 전LayerMessageHistoryExcelRequestResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\Excel\CommerceExcelGenerateHistoryListResponseDTO;
use Origin\DTO\Excel\CommerceExcelGenerateHistoryRequestDTO;
use Origin\Enum\ApiClient\Excel\ExcelGenerateStatus;
use Origin\Service\Admin\Excel\CommerceExcelService;
 
class LayerMessageHistoryExcelRequestResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$requestData = [
'excelMenu' => $postData['menu'],
'page' => $postData['page'] ?? CommerceExcelGenerateHistoryRequestDTO::DEFAULT_PAGE,
'pageSize' => CommerceExcelGenerateHistoryRequestDTO::DEFAULT_PAGE_SIZE
];
$requestDTO = new CommerceExcelGenerateHistoryRequestDTO($requestData);
 
try {
/** @var CommerceExcelService $commerceExcelService */
$commerceExcelService = \App::getInstance(CommerceExcelService::class);
$excelGenerateHistoryListResponseDTO = $commerceExcelService->getExcelGenerateHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('excel')->error($e->getMessage(), ['requestDTO' => $requestDTO]);
$excelGenerateHistoryListResponseDTO = new CommerceExcelGenerateHistoryListResponseDTO(
['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize()
);
}
$page = $excelGenerateHistoryListResponseDTO->getPage();
 
$this->setData('postData', $postData);
$this->setData('excelGenerateHistoryList', $excelGenerateHistoryListResponseDTO->getExcelGenerateHistoryList());
$this->setData('downloadableStatuses', ExcelGenerateStatus::getDownloadableStatuses());
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
 
변경 후LayerMessageHistoryExcelRequestResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMyappPushContentsController.php-43
변경 전LayerMyappPushContentsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Origin\Enum\ApiClient\Apps\MyappPushNotificationType;
use Origin\Service\Crm\Message\MyappPushHistoryService;
 
class LayerMyappPushContentsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$pushNo = $request->post()->get('pushNo');
 
try {
/** @var MyappPushHistoryService $myappPushHistoryService */
$myappPushHistoryService = \App::getInstance(MyappPushHistoryService::class);
$myappPushHistoryContentDTO = $myappPushHistoryService->getMyappPushHistoryContent($pushNo);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['pushNo' => $pushNo]);
exit;
}
 
$this->setData('title', $myappPushHistoryContentDTO->getTitle());
$this->setData('content', $myappPushHistoryContentDTO->getContent());
$this->setData('unsubscribeGuide', $myappPushHistoryContentDTO->getUnsubscribeGuide());
$this->setData('imageUrl', $myappPushHistoryContentDTO->getImageUrl());
$this->setData('pushUrl', $myappPushHistoryContentDTO->getPushUrl());
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMyappPushContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMyappPushHistoryResultController.php-129
변경 전LayerMyappPushHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Apps\MyappPushHistorySearchFormDTO;
use Origin\Enum\ApiClient\Apps\MyappPushResultDirection;
use Origin\Enum\ApiClient\Apps\MyappPushResultSortType;
use Origin\Enum\ApiClient\Apps\MyappPushScheduleType;
use Origin\Enum\ApiClient\Apps\MyappPushSendStatus;
use Origin\Service\Crm\Message\MyappPushHistoryService;
use Origin\Service\Crm\Message\MyappPushSettingService;
 
class LayerMyappPushHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new MyappPushHistorySearchFormDTO($postData);
$searchFormArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFormArray);
$requestDTO = new MyappPushHistoryListRequestDTO($preparedData);
 
/** @var MyappPushSettingService $myappPushSettingService */
$myappPushSettingService = \App::getInstance(MyappPushSettingService::class);
$myappPushConfig = $myappPushSettingService->getMyappPushConfig();
$isEnabled = $myappPushConfig->getUseFlag() === 'y';
 
try {
/** @var MyappPushHistoryService $myappPushHistoryService */
$myappPushHistoryService = \App::getInstance(MyappPushHistoryService::class);
$myappPushHistoryListResponseDTO = $myappPushHistoryService->getMyappPushHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$myappPushHistoryListResponseDTO = new MyappPushHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $myappPushHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormArray);
$this->setData('sort', MyappPushHistoryService::generateSortList());
$this->setData('myappPushHistoryList', $this->sanitizeHistoryList($myappPushHistoryListResponseDTO->getMyappPushHistoryList()));
$this->setData('isEnabled', $isEnabled);
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param MyappPushHistoryDTO[] $historyList
* @return MyappPushHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$title = $dto->getTitle();
if (!empty($title)) {
$dto->setTitle(htmlspecialchars($title, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
 
$failReason = $dto->getFailReason();
if (!empty($failReason)) {
$dto->setFailReason(htmlspecialchars($failReason, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
/**
* DTO 생성을 위한 요청 데이터 전처리
*
* @param array $postData
* @return array
*/
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
 
$sort = explode(' ', $preparedData['sort'] ?? (MyappPushResultSortType::SEND_DATE_TIME->name . ' ' . MyappPushResultDirection::DESC->name));
$preparedData['sortType'] = (MyappPushResultSortType::tryFromName($sort[0]) ?? MyappPushResultSortType::SEND_DATE_TIME)->name;
$preparedData['direction'] = (MyappPushResultDirection::tryFromName(strtoupper($sort[1])) ?? MyappPushResultDirection::DESC)->name;
 
$allScheduleTypes = array_map(fn($e) => $e->name, MyappPushScheduleType::cases());
if (!empty($preparedData['scheduleTypes']) && !array_diff($preparedData['scheduleTypes'], $allScheduleTypes) && !array_diff($allScheduleTypes, $preparedData['scheduleTypes'])) {
unset($preparedData['scheduleTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, MyappPushSendStatus::availableForSearch());
if (!empty($preparedData['sendStatuses']) && !array_diff($preparedData['sendStatuses'], $allSendStatuses) && !array_diff($allSendStatuses, $preparedData['sendStatuses'])) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['searchType'])) {
unset($preparedData['searchType']);
}
 
return $preparedData;
}
}
 
변경 후LayerMyappPushHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerMyappPushHistorySearchController.php-28
변경 전LayerMyappPushHistorySearchController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Origin\Enum\ApiClient\Apps\MyappPushScheduleType;
use Origin\Enum\ApiClient\Apps\MyappPushSendStatus;
use Origin\Enum\ApiClient\Notification\MyappPushResultSearchType;
 
class LayerMyappPushHistorySearchController extends \Controller\Admin\Controller
{
public function index()
{
$scheduleTypes = MyappPushScheduleType::cases();
$sendStatuses = MyappPushSendStatus::availableForSearch();
$searchType = MyappPushResultSearchType::generateSelectBoxData();
 
$this->setData('searchType', $searchType);
$this->setData('scheduleTypes', $scheduleTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMyappPushHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerSmsContentsController.php-42
변경 전LayerSmsContentsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\SmsHistoryContentRequestDTO;
use Origin\Enum\ApiClient\Notification\NotificationSendType;
use Origin\Service\Crm\Message\SmsHistoryService;
 
class LayerSmsContentsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$sendGroupKey = $request->post()->get('sendGroupKey');
 
try {
$requestDTO = (new SmsHistoryContentRequestDTO(['type' => NotificationSendType::SMS->name]));
/** @var SmsHistoryService $smsHistoryService */
$smsHistoryService = \App::getInstance(SmsHistoryService::class);
$smsHistoryContentDTO = $smsHistoryService->getSmsHistoryContent($sendGroupKey, $requestDTO);
} catch (\Throwable $e) {
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
 
$this->setData('title', $smsHistoryContentDTO->getTitle());
$this->setData('content', $smsHistoryContentDTO->getContent());
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSmsContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerSmsHistoryResultController.php-121
변경 전LayerSmsHistoryResultController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\SmsHistoryDTO;
use Origin\DTO\ApiClient\Commerce\Notification\SmsHistoryListRequestDTO;
use Origin\DTO\ApiClient\Commerce\Notification\SmsHistoryListResponseDTO;
use Origin\DTO\ApiClient\Commerce\Notification\SmsHistorySearchFormDTO;
use Origin\Enum\ApiClient\Excel\ExcelGenerateMenu;
use Origin\Enum\ApiClient\Notification\SmsSendStatus;
use Origin\Enum\ApiClient\Notification\SmsTriggerType;
use Origin\Service\Crm\Message\SmsHistoryService;
 
class LayerSmsHistoryResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
 
$searchFormDTO = new SmsHistorySearchFormDTO($postData);
$searchFormArray = $searchFormDTO->toRecursiveArray();
$preparedData = $this->prepareRequestDataForDto($searchFormArray);
$requestDTO = new SmsHistoryListRequestDTO($preparedData);
 
try {
/** @var SmsHistoryService $smsHistoryService */
$smsHistoryService = \App::getInstance(SmsHistoryService::class);
$smsHistoryListResponseDTO = $smsHistoryService->getSmsHistoryList($requestDTO);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO]);
$smsHistoryListResponseDTO = new SmsHistoryListResponseDTO(['totalCount' => 0, 'contents' => []], $requestDTO->getPage(), $requestDTO->getPageSize());
}
$page = $smsHistoryListResponseDTO->getPage();
 
$this->setData('searchFormData', $searchFormArray);
$this->setData('excelSearchQuery', $requestDTO->toRecursiveArray());
$this->setData('excelMenu', ExcelGenerateMenu::ADMIN_SMS_SEND_RESULT);
$this->setData('sortType', SmsHistoryService::SORT_LIST);
$this->setData('smsHistoryList', $this->sanitizeHistoryList($smsHistoryListResponseDTO->getSmsHistoryList()));
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
 
/**
* DTO 리스트의 문자열 필드에 htmlspecialchars 처리
*
* @param SmsHistoryDTO[] $historyList
* @return SmsHistoryDTO[]
*/
private function sanitizeHistoryList(array $historyList): array
{
foreach ($historyList as $dto) {
$sendKey = $dto->getSendKey();
if (!empty($sendKey)) {
$dto->setSendKey(htmlspecialchars($sendKey, ENT_QUOTES, 'UTF-8'));
}
 
$senderName = $dto->getSenderName();
if (!empty($senderName)) {
$dto->setSenderName(htmlspecialchars($senderName, ENT_QUOTES, 'UTF-8'));
}
 
$from = $dto->getFrom();
if (!empty($from)) {
$dto->setFrom(htmlspecialchars($from, ENT_QUOTES, 'UTF-8'));
}
 
$content = $dto->getContent();
if (!empty($content)) {
$dto->setContent(htmlspecialchars($content, ENT_QUOTES, 'UTF-8'));
}
}
 
return $historyList;
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sortType = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sortType[1] === 'desc') ? 'true' : 'false';
 
$allTriggerTypes = array_map(fn($e) => $e->name, SmsTriggerType::cases());
if (!empty($preparedData['triggerTypes']) && !array_diff($preparedData['triggerTypes'], $allTriggerTypes) && !array_diff($allTriggerTypes, $preparedData['triggerTypes'])) {
unset($preparedData['triggerTypes']);
}
 
$allSendStatuses = array_map(fn($e) => $e->name, SmsSendStatus::availableForSearch());
if (!empty($preparedData['sendStatuses']) && !array_diff($preparedData['sendStatuses'], $allSendStatuses) && !array_diff($allSendStatuses, $preparedData['sendStatuses'])) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['smsType'])) {
unset($preparedData['smsType']);
}
 
return $preparedData;
}
}
 
변경 후LayerSmsHistoryResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerSmsHistoryResultPsController.php-63
변경 전LayerSmsHistoryResultPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Notification\RemoveReservedSmsSendRequestDTO;
use Origin\Service\Crm\Message\SmsHistoryService;
use Respect\Validation\Exceptions\ValidationException;
use Respect\Validation\Validator;
 
class LayerSmsHistoryResultPsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageHistory', 'mobileHistoryList');
$this->setMenuCode('crm', 'messageHistory', 'mobileHistoryList');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'cancel':
try {
Validator::arrayVal()
->key('sendGroupKey', Validator::stringType()->notEmpty())
->key('type', Validator::stringType()->notEmpty())
->key('cancelMode', Validator::stringType()->notEmpty())
->when(
Validator::key('cancelMode', Validator::equals('SELECT')),
Validator::key('ids', Validator::stringType()->notEmpty()),
Validator::alwaysValid()
)
->assert($postData);
 
$requestDTO = new RemoveReservedSmsSendRequestDTO($postData);
 
/** @var SmsHistoryService $smsHistoryService */
$smsHistoryService = \App::getInstance(SmsHistoryService::class);
$smsHistoryService->removeReservedSmsSend($postData['sendGroupKey'], $requestDTO);
 
$this->json(['success' => true, 'message' => '예약 발송이 취소되었습니다.'], Response::HTTP_OK);
} catch (ValidationException $e) {
\Logger::channel('mobileMessage')->error("예약 취소 요청 파라미터가 잘못 되었습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage(), 'data' => $postData], Response::HTTP_BAD_REQUEST);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error("예약 취소 요청에 실패 하였습니다.", [$e->getMessage(), __METHOD__]);
$this->json(['success' => false, 'message' => $e->getMessage(), 'data' => $postData], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
default:
$this->json(['success' => false, 'message' => '잘못된 접근입니다.'], Response::HTTP_BAD_REQUEST);
}
}
}
 
변경 후LayerSmsHistoryResultPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileMessageHistory/LayerSmsHistorySearchController.php-31
변경 전LayerSmsHistorySearchController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileMessageHistory;
 
use Origin\Enum\ApiClient\Notification\SmsResultSearchType;
use Origin\Enum\ApiClient\Notification\SmsSendStatus;
use Origin\Enum\ApiClient\Notification\SmsTriggerType;
use Origin\Enum\ApiClient\Notification\SmsType;
 
class LayerSmsHistorySearchController extends \Controller\Admin\Controller
{
public function index()
{
$triggerTypes = SmsTriggerType::cases();
$sendStatuses = SmsSendStatus::availableForSearch();
$smsTypes = SmsType::cases();
$searchType = SmsResultSearchType::generateSelectBoxData();
 
$this->setData('triggerTypes', $triggerTypes);
$this->setData('smsTypes', $smsTypes);
$this->setData('sendStatuses', $sendStatuses);
$this->setData('searchType', $searchType);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSmsHistorySearchController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSendController.php-39
변경 전MobileSendController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Sms\Sms;
use Origin\Service\Crm\Message\MessageConfigService;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
class MobileSendController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
$hasCallNumber = $smsManageCallNumberService->hasUsableCallNumber();
 
/** @var MessageConfigService $messageConfigService */
$messageConfigService = \App::getInstance(MessageConfigService::class);
 
$this->setData('messageConfig', $messageConfigService->buildSendableMessageConfig($hasCallNumber));
$this->setData('smsPointEach', Sms::SMS_POINT);
$this->setData('lmsPointEach', Sms::LMS_POINT);
$this->setData('kakaoAlrimTalkPointEach', Sms::KAKAO_POINT);
$this->setData('kakaoFriendTalkPointEach', Sms::KAKAO_FRIEND_TALK_POINT);
$this->getView()->setPageName('crm/mobile_send.php');
}
}
 
변경 후MobileSendController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerKakaoAlimtalkContentSettingController.php-41
변경 전LayerKakaoAlimtalkContentSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Framework\Utility\StringUtils;
use Origin\Enum\Crm\Message\KakaoAlimTalkTemplateType;
use Origin\Service\Crm\Message\Sms080RejectService;
use Origin\Service\Member\KakaoAlrim\KakaoAlrimCloudSettingService;
 
class LayerKakaoAlimtalkContentSettingController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/* @var KakaoAlrimCloudSettingService $kakaoAlimCloudsSettingService*/
$kakaoAlimCloudsSettingService = \App::getInstance(KakaoAlrimCloudSettingService::class);
 
$kakaoAlimTalkTemplateTypes = KakaoAlimTalkTemplateType::cases();
$allowedKeys = array_map(fn($type) => strtolower($type->name), $kakaoAlimTalkTemplateTypes);
$kakaoAlimtalkTemplates = array_filter(
$kakaoAlimCloudsSettingService->getTemplates(),
fn($key) => in_array($key, $allowedKeys),
ARRAY_FILTER_USE_KEY
);
 
/** @var Sms080RejectService $sms080RejectService */
$sms080RejectService = \App::getInstance(Sms080RejectService::class);
$is080RejectAvailable = $sms080RejectService->isAvailable();
 
$this->setData('is080RejectAvailable', $is080RejectAvailable);
$this->setData('kakaoAlimTalkTemplateTypes', $kakaoAlimTalkTemplateTypes);
$this->setData('kakaoAlimtalkTemplates', $kakaoAlimtalkTemplates);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerKakaoAlimtalkContentSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerKakaoFriendtalkContentSettingController.php-59
변경 전LayerKakaoFriendtalkContentSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Bundle\Util\Crm\RecipeContextResolverTrait;
use Origin\Enum\Crm\Message\CampaignMessageType;
use Origin\Service\Crm\Message\KakaoFriendTalkSettingService;
use Origin\Service\Crm\Message\Sms080RejectService;
 
class LayerKakaoFriendtalkContentSettingController extends \Controller\Admin\Controller
{
use RecipeContextResolverTrait;
 
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var KakaoFriendTalkSettingService $kakaoFriendTalkSettingService */
$kakaoFriendTalkSettingService = \App::getInstance(KakaoFriendTalkSettingService::class);
$kakaoFriendTalkConfig = $kakaoFriendTalkSettingService->getKakaoFriendTalkConfig();
 
/** @var Sms080RejectService $sms080RejectService */
$sms080RejectService = \App::getInstance(Sms080RejectService::class);
$is080RejectAvailable = $sms080RejectService->isAvailable();
 
$campaignMessageTypeUnitPoint = [];
foreach (CampaignMessageType::cases() as $type) {
$campaignMessageTypeUnitPoint[$type->name] = $type->unitPoint();
}
 
$recipeContext = $this->resolveRecipeContext();
$isRecipeContext = $this->hasRecipeContext();
$defaultCampaignRequest = $recipeContext['defaultCampaignRequest'] ?? [];
$campaignName = $recipeContext['recipeTitle'] ?? '';
$friendtalkRequest = $defaultCampaignRequest['kakaoFriendtalkRequest'] ?? [];
$initialTextContent = $friendtalkRequest['text']['content'] ?? '';
$initialAlternativeMessage = $defaultCampaignRequest['smsLmsRequest']['content'] ?? '';
$initialAlternativeTitle = $defaultCampaignRequest['smsLmsRequest']['title'] ?? '';
 
$this->setData('is080RejectAvailable', $is080RejectAvailable);
$this->setData('campaignMessageTypes', CampaignMessageType::cases());
$this->setData('campaignMessageTypeUnitPoints', $campaignMessageTypeUnitPoint);
$this->setData('useSmsAlternativeSendFlag', $kakaoFriendTalkConfig->getSmsAlternativeSendFlag());
$this->setData('linkPlatformType', $kakaoFriendTalkConfig->getLinkPlatformType()->name);
$this->setData('isRecipeContext', $isRecipeContext);
$this->setData('campaignName', $campaignName);
$this->setData('friendtalkRequest', $friendtalkRequest);
$this->setData('initialTextContent', $initialTextContent);
$this->setData('initialAlternativeMessage', $initialAlternativeMessage);
$this->setData('initialAlternativeTitle', $initialAlternativeTitle);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerKakaoFriendtalkContentSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMessagePointController.php-36
변경 전LayerMessagePointController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Component\Sms\SmsUtil;
use Component\Sms\Sms;
 
class LayerMessagePointController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
// 현재 SMS 포인트 싱크
Sms::saveSmsPoint();
 
// 현재 메시지 포인트
$nowSmsPoint = (float) Sms::getPoint();
 
$availableCount = [
'sms' => SmsUtil::getAvailableCount('sms', $nowSmsPoint),
'lms' => SmsUtil::getAvailableCount('lms', $nowSmsPoint),
'kakaoAlrimTalk' => SmsUtil::getAvailableCount('kakaoAlrimTalk', $nowSmsPoint),
'kakaoFriendTalk' => SmsUtil::getAvailableCount('kakaoFriendTalk', $nowSmsPoint),
];
 
$this->setData('availableCount', $availableCount);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMessagePointController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendMyappPushTemplateController.php-21
변경 전LayerMobileSendMyappPushTemplateController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
 
class LayerMobileSendMyappPushTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$categories = MyappPushTemplateCategory::getFindOptions();
 
$this->setData('categories', $categories);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileSendMyappPushTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendMyappPushTemplateResultController.php-48
변경 전LayerMobileSendMyappPushTemplateResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Component\Page\Page;
use Framework\StaticProxy\Proxy\UserFilePath;
use Origin\Enum\Sms\SmsContentsType;
use Origin\Repository\Member\Myapp\MyappPushRepository;
use Origin\Repository\Member\Sms\SmsContentsRepository;
use Request;
 
class LayerMobileSendMyappPushTemplateResultController extends \Controller\Admin\Controller
{
public function index()
{
$request = \App::getInstance('request');
$getParams = $request->get()->all();
 
$pageNum = 10;
$page = $getParams['page'] ?? 1;
$pushType = $getParams['pushType'] ?? '';
$keyword = $getParams['keyword'] ?? '';
$searchParams = ['keyword' => $keyword, 'templateFl'=> 'y', 'pushType' => $pushType];
 
$repository = \App::getInstance(MyappPushRepository::class);
$templates = $repository->findAllWithPagination($searchParams, $pageNum, ($page - 1) * $pageNum);
$totalCount = $repository->countAll($searchParams);
 
/** @var \Bundle\Component\Page\Page $pageComponent */
$pageComponent = \App::load('\\Component\\Page\\Page', $page);
$pageComponent->page['list'] = $pageNum;
$pageComponent->recode['amount'] = $totalCount;
$pageComponent->recode['total'] = $totalCount;
$pageComponent->setPage();
$pageComponent->setUrl(\Request::getQueryString());
 
$this->setData('templates', $templates);
$this->setData('page', $pageComponent);
$this->setData('requestData', $getParams);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileSendMyappPushTemplateResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendSaveMyappPushTemplateController.php-37
변경 전LayerMobileSendSaveMyappPushTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
 
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Request;
 
class LayerMobileSendSaveMyappPushTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$pushSubject = $request['pushSubject'] ?? '';
$pushContent = $request['pushContent'] ?? '';
$pushImage = $request['pushImage'] ?? '';
$pushUrl = $request['pushUrl'] ?? '';
$pushWithdraw = $request['pushWithdraw'] ?? '';
 
$categories = MyappPushTemplateCategory::getFindOptions();
 
$this->setData('categories', $categories);
$this->setData('pushSubject', $pushSubject);
$this->setData('pushContent', $pushContent);
$this->setData('pushImage', $pushImage);
$this->setData('pushUrl', $pushUrl);
$this->setData('pushWithdraw', $pushWithdraw);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileSendSaveMyappPushTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendSaveSmsTemplateController.php-30
변경 전LayerMobileSendSaveSmsTemplateController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Origin\Enum\Crm\Message\SmsTemplateCategory;
use Request;
 
class LayerMobileSendSaveSmsTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$contents = $request['contents'] ?? '';
$messageTitle = $request['messageTitle'] ?? '';
 
$categories = SmsTemplateCategory::getFindOptions();
 
$this->setData('categories', $categories);
$this->setData('contents', $contents);
$this->setData('messageTitle', $messageTitle);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMobileSendSaveSmsTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendSmsTemplateController.php-22
변경 전LayerMobileSendSmsTemplateController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Origin\Enum\Crm\Message\SmsTemplateCategory;
 
class LayerMobileSendSmsTemplateController extends \Controller\Admin\Controller
{
public function index()
{
$categories = SmsTemplateCategory::getFindOptions();
 
$this->setData('categories', $categories);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
}
 
변경 후LayerMobileSendSmsTemplateController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMobileSendSmsTemplateResultController.php-50
변경 전LayerMobileSendSmsTemplateResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Component\Page\Page;
use Framework\Http\Response;
use Origin\DTO\Crm\Message\SmsTemplateSearchDTO;
use Origin\Service\Crm\Message\SmsTemplateService;
 
class LayerMobileSendSmsTemplateResultController extends \Controller\Admin\Controller
{
public function index()
{
try {
$request = \App::getInstance('request');
$getParams = $request->get()->all();
 
/** @var SmsTemplateService $smsTemplateService */
$smsTemplateService = \App::getInstance(SmsTemplateService::class);
$dto = new SmsTemplateSearchDTO($getParams);
$result = $smsTemplateService->getSmsTemplateList($dto);
$templates = $result['templates'];
$totalCount = $result['totalCount'];
$pageNum = $result['pageNum'];
$page = $result['page'];
 
/** @var \Bundle\Component\Page\Page $pageComponent */
$pageComponent = \App::load('\\Component\\Page\\Page', $page);
$pageComponent->page['list'] = $pageNum;
$pageComponent->recode['amount'] = $totalCount;
$pageComponent->recode['total'] = $totalCount;
$pageComponent->setPage();
$pageComponent->setUrl(\Request::getQueryString());
 
$this->setData('templates', $templates);
$this->setData('page', $pageComponent);
$this->setData('requestData', $getParams);
 
$this->getView()->setDefine('layout', 'layout_layer.php');
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
 
변경 후LayerMobileSendSmsTemplateResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerMyappPushContentSettingController.php-55
변경 전LayerMyappPushContentSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Bundle\Util\Crm\RecipeContextResolverTrait;
use Framework\Utility\StringUtils;
use Origin\Enum\Crm\Message\MyappPushNotificationType;
use Origin\Enum\Crm\Message\MyappPushSendCondition;
use Origin\Enum\Crm\Message\MyappPushSendPlatform;
use Origin\Service\Crm\Message\MyappPushTemplateService;
use Origin\Service\Crm\Message\Sms080RejectService;
 
class LayerMyappPushContentSettingController extends \Controller\Admin\Controller
{
use RecipeContextResolverTrait;
 
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var Sms080RejectService $sms080RejectService */
$sms080RejectService = \App::getInstance(Sms080RejectService::class);
$is080RejectAvailable = $sms080RejectService->isAvailable();
 
/** @var MyappPushTemplateService $myappPushTemplateService */
$myappPushTemplateService = \App::getInstance(MyappPushTemplateService::class);
$myappPushUrl = $myappPushTemplateService->getMyappDomainUrl();
 
$recipeContext = $this->resolveRecipeContext();
$isRecipeContext = $this->hasRecipeContext();
$myappRequest = $recipeContext['defaultCampaignRequest']['myappRequest'] ?? [];
$initialTitle = $myappRequest['title'] ?? '';
$initialMessage = $myappRequest['content'] ?? '';
$initialAlternativeMessage = $recipeContext['defaultCampaignRequest']['smsLmsRequest']['content'] ?? '';
$initialAlternativeTitle = $recipeContext['defaultCampaignRequest']['smsLmsRequest']['title'] ?? '';
 
$this->setData('is080RejectAvailable', $is080RejectAvailable);
$this->setData('myappPushSendConditions', MyappPushSendCondition::cases());
$this->setData('myappPushNotificationTypes', MyappPushNotificationType::cases());
$this->setData('myappPushSendPlatforms', MyappPushSendPlatform::cases());
$this->setData('myappPushUrl', $myappPushUrl);
$this->setData('isRecipeContext', $isRecipeContext);
$this->setData('initialTitle', $initialTitle);
$this->setData('initialMessage', $initialMessage);
$this->setData('initialAlternativeMessage', $initialAlternativeMessage);
$this->setData('initialAlternativeTitle', $initialAlternativeTitle);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerMyappPushContentSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerPreviewMessageController.php-19
변경 전LayerPreviewMessageController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
class LayerPreviewMessageController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerPreviewMessageController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerRecipeSendTimeSettingController.php-45
변경 전LayerRecipeSendTimeSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Bundle\Util\Crm\RecipeContextResolverTrait;
use Origin\Enum\Common\WeekDay;
use Origin\Enum\Crm\Message\MessageRepeatCycle;
 
class LayerRecipeSendTimeSettingController extends \Controller\Admin\Controller
{
use RecipeContextResolverTrait;
 
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
$sendSchedule = $this->resolveRecipeContext()['sendSchedule'] ?? [];
$sendTimeView = $this->buildSendTimeView($sendSchedule);
 
$this->setData('messageRepeatCycles', MessageRepeatCycle::cases());
$this->setData('sendTimeView', $sendTimeView);
$this->setData('weekDays', WeekDay::cases());
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
protected function buildSendTimeView(array $sendSchedule): array
{
$selectedRepeatType = $sendSchedule['repeatType'] ?? 'WEEKLY';
$repeatConfig = $sendSchedule['repeatConfig'] ?? [];
$sendTime = $repeatConfig['sendTime'] ?? '';
 
return [
'selectedRepeatType' => $selectedRepeatType,
'selectedRepeatHour' => $sendTime !== '' ? substr($sendTime, 0, 2) : '',
'selectedRepeatMinutes' => $sendTime !== '' ? substr($sendTime, 3, 2) : '',
'selectedRepeatDays' => $repeatConfig['repeatDays'] ?? [],
];
}
}
 
변경 후LayerRecipeSendTimeSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerRecipientSettingController.php-35
변경 전LayerRecipientSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Component\Sms\SmsUtil;
use Component\Sms\Sms;
use Framework\Http\Request;
use Origin\Enum\Crm\Message\RecipientType;
 
class LayerRecipientSettingController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var Request $request */
$request = \App::getInstance('request');
$selectedCrmGroup = $request->post()->get('selectedCrmGroup');
 
// PG 설정 불러오기
$pgConfig = gd_pgs();
$isConnectedPg = ($pgConfig['pgAutoSetting'] === 'y' || $pgConfig['pgApprovalSetting'] === 'y') ? 'y' : 'n';
 
$this->setData('recipientTypes', RecipientType::cases());
$this->setData('isConnectedPg', $isConnectedPg);
$this->setData('selectedCrmGroup', $selectedCrmGroup);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerRecipientSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerRecipientSettingPsController.php-112
변경 전LayerRecipientSettingPsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Component\Crm\MobileMessage;
use Bundle\Component\Excel\Enum\ExcelSampleType;
use Bundle\Component\Excel\ExcelSample;
use Bundle\Component\Excel\ExcelSmsConvert;
use Bundle\Component\Sms\SmsExcelLog;
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use Framework\Http\Response;
use Origin\DTO\ApiClient\Commerce\Crm\TargetsRepeatRulesRequestDTO;
use Origin\DTO\ApiClient\Commerce\Crm\TargetsRepeatRuleStatusRequestDTO;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
 
class LayerRecipientSettingPsController extends \Controller\Admin\Controller
{
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$mode = $request->request()->get('mode');
 
switch ($mode) {
case 'getCountAll':
case 'getCountMembers':
try {
$getData = $request->get()->toArray();
$memberNos = json_decode($getData['selectedMemberNos']);
 
/* @var MobileMessage $mobileMessage*/
$mobileMessage = \App::getInstance(MobileMessage::class);
 
$counts = $mobileMessage->getReceiverCounts($memberNos, null);
$this->json(['success' => true, 'data' => $counts], Response::HTTP_OK);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'getCountGroupMembers':
try {
$getData = $request->get()->toArray();
$groupSnos = json_decode($getData['selectedMemberGroups']);
 
/* @var MobileMessage $mobileMessage*/
$mobileMessage = \App::getInstance(MobileMessage::class);
 
$counts = $mobileMessage->getReceiverCounts(null, $groupSnos);
$this->json(['success' => true, 'data' => $counts], Response::HTTP_OK);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage());
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'getCrmGroupStatus':
try {
$getData = $request->get()->toArray();
$requestDTO = new TargetsRepeatRuleStatusRequestDTO($getData);
 
/** @var $crmAdminApiClient CrmAdminApiClientService */
$crmAdminApiClient = \App::getInstance(CrmAdminApiClientService::class);
$targetsRepeatRuleStatusResponseDTO = $crmAdminApiClient->getNotificationTargetsRepeatRuleStatus($requestDTO, $getData['crmGroupNo']);
 
$this->json([
'success' => true,
'message' => 'CRN 그룹 개수 조회 성공',
'data' => $targetsRepeatRuleStatusResponseDTO->toRecursiveArray()
]);
} catch (\Throwable $e) {
\Logger::channel('mobileMessage')->error($e->getMessage(), ['requestDTO' => $requestDTO->toArray()]);
$this->json(['success' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'downloadExcelSample':
(new ExcelSample(ExcelSampleType::SMS))->outputSample();
break;
case 'uploadExcelTarget':
$requestFiles = $request->files()->toArray();
$excelFile = $requestFiles['excel'] ?? null;
if (!$excelFile) {
$this->json(['success' => false, 'message' => '엑셀 파일이 없습니다.'], Response::HTTP_BAD_REQUEST);
break;
}
 
set_time_limit(RUN_TIME_LIMIT);
/* @var ExcelSmsConvert $excelSmsConvert */
$excelSmsConvert = \App::load('Component\\Excel\\ExcelSmsConvert');
/* @var SmsExcelLog $smsExcelLog */
$smsExcelLog = \App::load('Component\\Sms\\SmsExcelLog');
try {
$excelSmsConvert->upload();
$uploadKey = $excelSmsConvert->getUploadKey();
$counts = $smsExcelLog->countValidationLogByUploadKey($uploadKey);
$this->json(['success' => true, 'data' => ['uploadedExcelKey' => $uploadKey, 'successTargetCount' => $counts]], Response::HTTP_OK);
} catch (\Throwable $e) {
Logger::channel('mobileMessage')->error(__CLASS__ . ' 엑셀 업로드 처리 중 에러 발생 : ', ['error' => $e->getMessage()]);
$this->json(['success' => false, 'message' => '엑셀 파일 처리에 실패했습니다.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후LayerRecipientSettingPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerSendMethodSettingController.php-38
변경 전LayerSendMethodSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Bundle\Util\Crm\RecipeContextResolverTrait;
use Origin\Enum\Crm\Message\SendMethod;
use Origin\Service\Crm\Message\Sms080RejectService;
 
class LayerSendMethodSettingController extends \Controller\Admin\Controller
{
use RecipeContextResolverTrait;
 
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var Sms080RejectService $sms080RejectService */
$sms080RejectService = \App::getInstance(Sms080RejectService::class);
$smsRejectPolicy = $sms080RejectService->getConfig();
$smsRejectText = ($smsRejectPolicy['rejectNumber'] != '' && $smsRejectPolicy['status'] == 'O' && $smsRejectPolicy['use'] == 'y') ? "수신거부 {$smsRejectPolicy['rejectNumber']}" : '';
 
$recipeContext = $this->resolveRecipeContext();
$isRecipeContext = $this->hasRecipeContext();
$messageSendGuide = $recipeContext['guideMessages']['messageSendGuide'] ?? '';
 
$this->setData('sendMethods', SendMethod::cases());
$this->setData('smsRejectText', $smsRejectText);
$this->setData('isRecipeContext', $isRecipeContext);
$this->setData('messageSendGuide', $messageSendGuide);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSendMethodSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerSendTimeSettingController.php-26
변경 전LayerSendTimeSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Origin\Enum\Common\WeekDay;
use Origin\Enum\Crm\Message\MessageRepeatCycle;
use Origin\Enum\Crm\Message\MessageSendType;
 
class LayerSendTimeSettingController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
$this->setData('messageSendTypes', MessageSendType::cases());
$this->setData('messageRepeatCycles', MessageRepeatCycle::cases());
$this->setData('weekDays', WeekDay::cases());
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSendTimeSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSend/LayerSmsContentSettingController.php-37
변경 전LayerSmsContentSettingController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MobileSend;
 
use Bundle\Util\Crm\RecipeContextResolverTrait;
use Origin\Service\Crm\Message\Sms080RejectService;
 
class LayerSmsContentSettingController extends \Controller\Admin\Controller
{
use RecipeContextResolverTrait;
 
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var Sms080RejectService $sms080RejectService */
$sms080RejectService = \App::getInstance(Sms080RejectService::class);
$is080RejectAvailable = $sms080RejectService->isAvailable();
 
$recipeContext = $this->resolveRecipeContext();
$isRecipeContext = $this->hasRecipeContext();
$initialMessage = $recipeContext['defaultCampaignRequest']['smsLmsRequest']['content'] ?? '';
$initialTitle = $recipeContext['defaultCampaignRequest']['smsLmsRequest']['title'] ?? '';
 
$this->setData('is080RejectAvailable', $is080RejectAvailable);
$this->setData('isRecipeContext', $isRecipeContext);
$this->setData('initialMessage', $initialMessage);
$this->setData('initialTitle', $initialTitle);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSmsContentSettingController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MobileSendPsController.php-89
변경 전MobileSendPsController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Framework\Debug\Exception\AlertCloseException;
use Framework\Http\Request;
use GuzzleHttp\Exception\RequestException;
use Origin\Enum\ApiClient\Crm\RecipeType;
use Origin\Service\Crm\Message\MessageSendService;
use Origin\Service\Crm\Message\SmsPasswordService;
use Component\Crm\MobileMessage;
use Framework\Http\Response;
 
class MobileSendPsController extends \Controller\Admin\Controller
{
public function index()
{
$this->callMenu('crm', 'messageSend', 'mobileSend');
$this->setMenuCode('crm', 'messageSend', 'mobileSend');
 
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$mode = $postData['mode'];
 
switch ($mode) {
case 'send':
try {
$requestData = json_decode($postData['payload'], true);
 
// CRM 레시피 발송은 레시피 메뉴 권한으로 인가
if (!empty($requestData['recipeType'])) {
if (RecipeType::tryFromSlug($requestData['recipeType']) === null) {
throw new AlertCloseException(__('잘못된 접근입니다.'));
}
$this->setMenuCode('crm', 'crm', 'crmRecipe');
}
 
// 수신대상 - 엑셀업로드, 업로드된 정보로 수신자 설정
if (($requestData['targetInfo']['targetType'] ?? null) === 'EXCEL') {
/* @var \Bundle\Component\Crm\MobileMessage $mobileMessage*/
$mobileMessage = \App::getInstance(MobileMessage::class);
 
$requestData = $mobileMessage->setExcelTargets($requestData);
}
 
/* @var MessageSendService $messageSendService*/
$messageSendService = \App::getInstance(MessageSendService::class);
$messageSendService->send($requestData);
$this->json(['success' => true]);
} catch (RequestException $e) {
if ($e->hasResponse()) {
$response = $e->getResponse();
if ($response->getStatusCode() === Response::HTTP_BAD_REQUEST) {
$contents = json_decode($response->getBody()->getContents(), true);
$this->json(['success' => false, 'message' => $contents['message']], Response::HTTP_BAD_REQUEST);
}
}
throw $e;
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
case 'validateSendPassword':
try {
/** @var SmsPasswordService $smsPasswordService */
$smsPasswordService = \App::getInstance(SmsPasswordService::class);
$smsPassword = $smsPasswordService->getSmsPassword(true);
 
if ($postData['sendPassword'] === $smsPassword) {
$this->json(['success' => true]);
} else {
$this->json(['success' => false, 'message' => '설정 메뉴에 저장된 메시지 인증번호와 일치하지 않습니다.', 'data' => ['subMessage' => '설정 페이지에서 메시지 인증번호를 먼저 확인해주세요.']]);
}
} catch (\Throwable $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
break;
default:
throw new AlertCloseException(__("잘못된 접근입니다."));
}
}
}
 
변경 후MobileSendPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/Myapp/MyappPushTemplatePsController.php-76
변경 전MyappPushTemplatePsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\Myapp;
 
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateMyappDTO;
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Origin\Repository\Member\Myapp\MyappPushRepository;
use Origin\Service\Crm\Message\MyappPushTemplateService;
use Request;
 
class MyappPushTemplatePsController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$mode = $request['mode'] ?? '';
 
if ($mode === 'save') {
$this->save($request);
} else {
$this->json(['result' => true, 'message' => '잘못된 요청입니다.']);
}
}
 
private function save(array $request): void
{
$categoryValue = $request['category'] ?? '';
$subject = $request['subject'] ?? '';
$contents = $request['contents'] ?? '';
$title = $request['title'] ?? '';
$url = $request['url'] ?? '';
$image = $request['image'] ?? '';
 
$category = MyappPushTemplateCategory::tryFrom($categoryValue);
 
if ($category === null) {
$this->json(['success' => false, 'message' => '카테고리를 선택해주세요.']);
}
 
if (empty($subject)) {
$this->json(['success' => false, 'message' => '제목을 입력해주세요.']);
}
 
if (empty($contents)) {
$this->json(['success' => false, 'message' => '내용을 입력해주세요.']);
}
 
try {
$repository = \App::getInstance(MyappPushRepository::class);
$sno = $repository->saveByGodomallTemplate($category, $subject, $contents, $title, $url, $image);
 
// 파일 업로드된 이미지가 있으면 서버에 저장
$files = Request::files()->toArray();
if (!empty($files['imageFile']['tmp_name']) && (int)$files['imageFile']['error'] === UPLOAD_ERR_OK) {
/** @var MyappPushTemplateService $templateService */
$templateService = \App::getInstance(MyappPushTemplateService::class);
$files['imageFile']['pushImage'] = $files['imageFile']['tmp_name'];
$templateService->processAndSaveImage(
$sno,
new MessageTemplateMyappDTO(['pushSno' => $sno]),
$files['imageFile']
);
}
 
$this->json(['success' => true, 'message' => '템플릿이 저장되었습니다.']);
} catch (\Exception $e) {
$this->json(['success' => false, 'message' => '저장에 실패했습니다.']);
}
}
}
 
변경 후MyappPushTemplatePsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/MyappPush/MyappPushTemplatePsController.php-72
변경 전MyappPushTemplatePsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\MyappPush;
 
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateMyappDTO;
use Origin\Enum\Crm\Message\MyappPushTemplateCategory;
use Origin\Service\Crm\Message\MyappPushTemplateService;
use Request;
 
class MyappPushTemplatePsController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$mode = $request['mode'] ?? '';
 
$service = \App::getInstance(MyappPushTemplateService::class);
 
switch ($mode) {
case 'save':
$categoryValue = $request['category'] ?? '';
$category = MyappPushTemplateCategory::tryFrom($categoryValue);
if ($category === null) {
$this->json(['success' => false, 'message' => '카테고리를 선택해주세요.']);
return;
}
 
$templateName = trim($request['templateName'] ?? '');
if ($templateName === '') {
$this->json(['success' => false, 'message' => '템플릿명을 입력해주세요.']);
return;
}
 
$pushContent = trim($request['pushContent'] ?? '');
if ($pushContent === '') {
$this->json(['success' => false, 'message' => '푸시 내용을 입력해주세요.']);
return;
}
 
$pushSubject = trim($request['pushSubject'] ?? '');
 
$dto = new MessageTemplateMyappDTO([
'pushType' => $category->value,
'templateName' => $templateName,
'pushSubject' => $pushSubject,
'pushContent' => $pushContent,
'pushImage' => $request['pushImage'],
'pushUrl' => $request['pushUrl'],
'pushWithdraw' => $request['pushWithdraw'],
]);
 
$pushSno = $service->saveMyappTemplate($dto);
 
if ($pushSno == 0) {
$this->json(['success' => false, 'message' => '템플릿 저장에 실패하였습니다.']);
} else {
$this->json(['success' => true, 'data' => ['sno' => $pushSno], 'message' => '템플릿이 저장되었습니다.']);
}
 
return;
 
default:
$this->json(['error' => 'Invalid mode', 'data' => []]);
return;
}
}
}
 
변경 후MyappPushTemplatePsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/PopupChargeMessagePointsController.php-72
변경 전PopupChargeMessagePointsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Bundle\Component\Member\Manager;
use Bundle\Component\Sms\SmsUtil;
use Bundle\Component\Sms\SmsAdmin;
use Bundle\Component\Sms\Sms;
use Core\Base\Interceptor\AdminCertification;
use Framework\Debug\Exception\AlertRedirectException;
 
class PopupChargeMessagePointsController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$globals = \App::getInstance('globals');
 
$this->callMenu('crm', 'messageConfig', 'chargeMessagePoints');
 
if ($request->get()->get('popupMode', '') === 'yes') {
$this->getView()->setDefine('layout', 'layout_blank.php');
} elseif ($session->has(Manager::SESSION_TEMP_MANAGER)
&& $session->has(AdminCertification::SESSION_TEMP_CERTIFICATION)) {
if (strpos($request->getReferer(), '/base/login') === false) {
$session->del(Manager::SESSION_TEMP_MANAGER);
$session->del(AdminCertification::SESSION_TEMP_CERTIFICATION);
throw new AlertRedirectException('잘못된 경로로 접근되었습니다.', 403, null, '/base/login.php', 'top');
}
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->setData('guestCharge', true);
}
 
$shopNo = $globals->get('gLicense.godosno');
 
// SMS 포인트 Sync
Sms::saveSmsPoint();
 
// 현재 메시지 포인트
$nowSmsPoint = (float) Sms::getPoint();
$availableCount = [
'sms' => SmsUtil::getAvailableCount('sms', $nowSmsPoint),
'lms' => SmsUtil::getAvailableCount('lms', $nowSmsPoint),
'kakaoAlrimTalk' => SmsUtil::getAvailableCount('kakaoAlrimTalk', $nowSmsPoint),
'kakaoFriendTalk' => SmsUtil::getAvailableCount('kakaoFriendTalk', $nowSmsPoint),
];
 
$smsAdmin = \App::load(SmsAdmin::class);
 
$webConfig = \App::getSystemConfig('internal')['commerce-web'];
$messagePointPayUrl = $webConfig['url'] . '/' . $webConfig['path']['message-point-pay-popup'];
 
$this->setData('lmsPoint', Sms::LMS_POINT);
$this->setData('kakaoPoint', Sms::KAKAO_POINT);
$this->setData('kakaoFriendTalkPoint', Sms::KAKAO_FRIEND_TALK_POINT);
$this->setData('kakaoFriendTalkImagePoint', Sms::KAKAO_FRIEND_TALK_IMAGE_POINT ?? 2.2);
$this->setData('kakaoFriendTalkWideItemPoint', Sms::KAKAO_FRIEND_TALK_WIDE_ITEM_POINT ?? 2.7);
$this->setData('availableCount', $availableCount);
$this->setData('smsPriceList', $smsAdmin->getSmsPriceList());
$this->setData('shopSno', $shopNo);
$this->setData('messagePointPayUrl', $messagePointPayUrl);
 
$this->getView()->setDefine('layout', 'layout_blank.php');
}
}
 
변경 후PopupChargeMessagePointsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/PopupSelectMembersController.php-71
변경 전PopupSelectMembersController.php
<?php
/*
* Copyright (C) 2025 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm;
 
use Component\Member\Group\Util;
use Component\Member\Member;
use Component\Member\Util\MemberUtil;
use Component\Page\Page;
use Framework\Http\Request;
 
class PopupSelectMembersController extends \Controller\Admin\Controller
{
public function index()
{
$this->setNcdsSubBodyClass();
$this->callMenu('crm', 'messageSend', 'mobileSend');
 
/** @var Request $request */
$request = \App::getInstance('request');
$this->setDefaultRequestParams($request);
 
$postData = $request->post()->toArray();
 
/* @var \Bundle\Component\Member\Member $memberComponent */
$memberComponent = \App::load(Member::class);
 
$groups = Util::getGroupName();
$checked = MemberUtil::checkedByMemberListSearch($postData);
$selected = MemberUtil::selectedByMemberListSearch($postData);
 
$this->setData('combineSearch', $memberComponent->getCombineSearchSelectBox());
$this->setData('searchKind', $memberComponent->getSearchKindASelectBox());
$this->setData('groups', $groups);
$this->setData('search', $postData);
$this->setData('checked', $checked);
$this->setData('selected', $selected);
 
// 필요
$this->setData('sendMode', $getParams['sendMode']);
 
$this->getView()->setDefine('layout', 'layout_blank.php');
$this->getView()->setPageName('crm/popup_select_members.php');
}
 
private function setDefaultRequestParams($request): void
{
$post = $request->post();
 
if (!$post->has('mallSno')) {
$post->set('mallSno', '');
}
if (!$post->get('sendMode', '') != 'mail') {
$post->set('mallSno', DEFAULT_MALL_NUMBER);
}
 
if (!$post->has('maillingFl')) {
$post->set('maillingFl', 'y');
}
if (!$post->has('smsFl')) {
$post->set('smsFl', 'y');
}
 
$post->set('sendMode', $request->get()->get('sendMode'));
}
}
 
변경 후PopupSelectMembersController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/PopupSelectMembers/LayerSelectedMembersResultController.php-114
변경 전LayerSelectedMembersResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\PopupSelectMembers;
 
use Bundle\Component\Page\Page;
use Component\Member\Group\Util;
use Component\Member\Member;
use Framework\Http\Request;
use Origin\Enum\ApiClient\Notification\SmsSendStatus;
use Origin\Enum\ApiClient\Notification\SmsTriggerType;
 
class LayerSelectedMembersResultController extends \Controller\Admin\Controller
{
const SORT_LIST = [
'entryDt desc' => '회원가입일 내림차순',
'entryDt asc' => '회원가입일 오름차순',
'lastLoginDt desc' => '최종로그인 내림차순',
'lastLoginDt asc' => '최종로그인 오름차순',
'loginCnt desc' => '방문횟수 내림차순',
'loginCnt asc' => '방문횟수 오름차순',
'memNm desc' => '이름 내림차순',
'memNm asc' => '이름 오름차순',
'memId desc' => '아이디 내림차순',
'memId asc' => '아이디 오름차순',
'mileage desc' => '마일리지 내림차순',
'mileage asc' => '마일리지 오름차순',
'saleAmt desc' => '주문금액 내림차순',
'saleAmt asc' => '주문금액 오름차순',
];
 
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 10;
 
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$postData['sendMode'] = $request->request()->get('sendMode');
 
/* @var \Bundle\Component\Member\Member $memberComponent */
$memberComponent = \App::load(Member::class);
 
$members = [];
$searchCount = 0;
 
if (!empty($postData['selectedMemberNos'])) {
$getParams = [
'sendMode' => $postData['sendMode'],
'chk' => $postData['selectedMemberNos'],
'sort' => $postData['sort'],
'mallSno' => $postData['mallSno'] ?? '',
];
 
$page = $postData['page'] ?? 1;
$pageSize = $postData['pageSize'] ?? self::DEFAULT_PER_PAGE;
 
$members = $memberComponent->listsWithCoupon($getParams, $page, $pageSize);
$searchCount = $memberComponent->foundRowsByListsWithCoupon($getParams);
}
 
$page = new Page($postData['page'] ?? self::DEFAULT_PAGE, $searchCount, $searchCount, $postData['pageSize'] ?? self::DEFAULT_PER_PAGE);
 
$sysIcons = [
'payco' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/payco_logo.png', 'name' => '페이코'],
'facebook' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/facebook_logo.png', 'name' => '페이스북'],
'naver' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/naver_logo.png', 'name' => '네이버'],
'kakao' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/kakao_logo.png', 'name' => '카카오'],
'wonder' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/wemakeprice_logo.png', 'name' => '위메프'],
'apple' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/apple_logo.png', 'name' => '애플'],
'google' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/google_logo.png', 'name' => '구글'],
];
 
$this->setData('selectedFormData', $postData);
$this->setData('snsIcons', $sysIcons);
$this->setData('sortType', self::SORT_LIST);
$this->setData('members', $members);
$this->setData('groups', Util::getGroupName());
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
 
private function prepareRequestDataForDto(array $postData): array
{
$preparedData = $postData;
// TODO 정렬 기준 추가를 대비하여 분리
$sortType = explode(' ', $preparedData['sort'] ?? 'reservedAt desc');
$preparedData['sortDescending'] = ($sortType[1] === 'desc') ? 'true' : 'false';
 
if (!empty($preparedData['triggerTypes']) && empty(array_diff(array_map(fn($e) => $e->name, SmsTriggerType::cases()), $preparedData['triggerTypes']))) {
unset($preparedData['triggerTypes']);
}
 
if (!empty($preparedData['sendStatuses']) && empty(array_diff(array_map(fn($e) => $e->name, SmsSendStatus::availableForSearch()), $preparedData['sendStatuses']))) {
unset($preparedData['sendStatuses']);
}
 
if (empty($preparedData['keyword'])) {
unset($preparedData['keyword']);
}
 
if (empty($preparedData['smsType'])) {
unset($preparedData['smsType']);
}
 
return $preparedData;
}
}
 
변경 후LayerSelectedMembersResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/PopupSelectMembers/LayerSelectMembersResultController.php-73
변경 전LayerSelectMembersResultController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\PopupSelectMembers;
 
use Bundle\Component\Page\Page;
use Component\Member\Group\Util;
use Component\Member\Member;
use Framework\Http\Request;
 
class LayerSelectMembersResultController extends \Controller\Admin\Controller
{
const SORT_LIST = [
'entryDt desc' => '회원가입일 내림차순',
'entryDt asc' => '회원가입일 오름차순',
'lastLoginDt desc' => '최종로그인 내림차순',
'lastLoginDt asc' => '최종로그인 오름차순',
'loginCnt desc' => '방문횟수 내림차순',
'loginCnt asc' => '방문횟수 오름차순',
'memNm desc' => '이름 내림차순',
'memNm asc' => '이름 오름차순',
'memId desc' => '아이디 내림차순',
'memId asc' => '아이디 오름차순',
'mileage desc' => '마일리지 내림차순',
'mileage asc' => '마일리지 오름차순',
'saleAmt desc' => '주문금액 내림차순',
'saleAmt asc' => '주문금액 오름차순',
];
 
const DEFAULT_PAGE = 1;
const DEFAULT_PER_PAGE = 10;
 
public function index()
{
/** @var Request $request */
$request = \App::getInstance('request');
$postData = $request->post()->toArray();
$postData['sendMode'] = $request->request()->get('sendMode');
if (in_array($postData['key'], ['company', 'busiNo'])) $postData['searchKind'] = 'fullLikeSearch';
 
/* @var \Bundle\Component\Member\Member $memberComponent */
$memberComponent = \App::load(Member::class);
 
$members = $memberComponent->listsWithCoupon($postData, $postData['page'] ?? self::DEFAULT_PAGE, $postData['pageSize'] ?? self::DEFAULT_PER_PAGE);
$searchCount = $memberComponent->foundRowsByListsWithCoupon($postData);
$totalCount = $memberComponent->foundRowsByListsWithCoupon(['sendMode' => $postData['sendMode'], 'mallSno' => $postData['mallSno']]); // sendMode만 포함하여 전체 회원수 조회
 
$page = new Page($postData['page'] ?? self::DEFAULT_PAGE, $searchCount, $totalCount, $postData['pageSize'] ?? self::DEFAULT_PER_PAGE);
 
$sysIcons = [
'payco' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/payco_logo.png', 'name' => '페이코'],
'facebook' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/facebook_logo.png', 'name' => '페이스북'],
'naver' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/naver_logo.png', 'name' => '네이버'],
'kakao' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/kakao_logo.png', 'name' => '카카오'],
'wonder' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/wemakeprice_logo.png', 'name' => '위메프'],
'apple' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/apple_logo.png', 'name' => '애플'],
'google' => ['icon' => PATH_ADMIN_GD_SHARE . 'ncds/image/google_logo.png', 'name' => '구글'],
];
 
$this->setData('searchFormData', $postData);
$this->setData('snsIcons', $sysIcons);
$this->setData('sortType', self::SORT_LIST);
$this->setData('members', $members);
$this->setData('groups', Util::getGroupName());
$this->setData('page', $page);
$this->getView()->setDefine('layout', 'layout_layer.php');
}
}
 
변경 후LayerSelectMembersResultController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Crm/Sms/SmsTemplatePsController.php-69
변경 전SmsTemplatePsController.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Controller\Admin\Crm\Sms;
 
use Origin\DTO\ApiClient\Commerce\Notification\MessageTemplateSmsDTO;
use Origin\Enum\Crm\Message\SmsTemplateCategory;
use Origin\Repository\Member\Sms\SmsContentsRepository;
use Origin\Service\Crm\Message\SmsTemplateService;
use Request;
 
class SmsTemplatePsController extends \Controller\Admin\Controller
{
public function index()
{
$request = Request::post()->toArray();
$mode = $request['mode'] ?? '';
 
$service = \App::getInstance(SmsTemplateService::class);
 
switch ($mode) {
case 'save':
$categoryValue = $request['category'] ?? '';
$category = SmsTemplateCategory::tryFrom($categoryValue);
if ($category === null) {
$this->json(['success' => false, 'message' => '카테고리를 선택해주세요.']);
return;
}
 
$subject = trim($request['subject'] ?? '');
if ($subject === '') {
$this->json(['success' => false, 'message' => '제목을 입력해주세요.']);
return;
}
 
$contents = trim($request['contents'] ?? '');
if ($contents === '') {
$this->json(['success' => false, 'message' => '내용을 입력해주세요.']);
return;
}
 
$templateDto = MessageTemplateSmsDTO::fromRegisterRequest([
'templateCategory' => $category->value,
'templateTitle' => $subject,
'templateMessageContent' => $contents,
'templateMessageTitle' => trim($request['messageTitle'] ?? ''),
]);
 
$sno = $service->setSmsContents($templateDto);
 
if ($sno == 0) {
$this->json(['success' => false, 'message' => '템플릿 저장에 실패하였습니다.']);
} else {
$this->json(['success' => true, 'data' => ['sno' => $sno], 'message' => '템플릿이 저장되었습니다.']);
}
 
return;
 
default:
$this->json(['error' => 'Invalid mode', 'data' => []]);
return;
}
}
}
 
변경 후SmsTemplatePsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Member/SmsAutoController.php+4
변경 전SmsAutoController.php
use Framework\Utility\ComponentUtils;
use Framework\Utility\ConfigUrlUtils;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
 
 
/**
* SMS 자동 발송 설정
 
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
 
 
 
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
 
변경 후SmsAutoController.php
use Framework\Utility\ComponentUtils;
use Framework\Utility\ConfigUrlUtils;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Origin\Service\Crm\Message\SmsTemplateService;
 
/**
* SMS 자동 발송 설정
 
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsAutoData = $smsAdmin->getSmsAutoData();
/** @var SmsTemplateService $smsTemplateService */
$smsTemplateService = \App::getInstance(SmsTemplateService::class);
$smsAutoData = $smsTemplateService->attachLinkedAutoSendContents($smsAutoData, array_keys(Sms::SMS_AUTO_RECEIVE_LIST), \Bundle\Component\Sms\SmsAdmin::SMS_AUTO_TYPES);
 
/** @var SmsManageCallNumberService $smsManageCallNumberService */
$smsManageCallNumberService = \App::getInstance(SmsManageCallNumberService::class);
 
Controller/Admin/Member/SmsSendController.php+4-1
변경 전SmsSendController.php
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
 
 
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Origin\Service\Member\CrmGroupService;
 
$getSmsContentsByGoodsRestock = function () {
$db = \App::getInstance('DB');
$db->strField = '*';
$db->strWhere = 'smsType = \'user\' AND smsAutoCode LIKE CONCAT(\'01007501\', \'%\')';
 
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
$resultData = $db->query_fetch($strSQL);
 
변경 후SmsSendController.php
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Enum\Crm\Message\SmsTemplateApprovalStatus;
use Origin\Enum\Crm\Message\LegacyCallNumberStatus;
use Origin\Service\Crm\Message\SmsManageCallNumberService;
use Origin\Service\Member\CrmGroupService;
 
$getSmsContentsByGoodsRestock = function () {
$db = \App::getInstance('DB');
$db->strField = '*';
$db->strWhere = 'smsType = \'user\' AND smsAutoCode LIKE CONCAT(\'01007501\', \'%\')'
. ' AND approvalStatus = \'' . SmsTemplateApprovalStatus::APPROVED->value . '\'';
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
$resultData = $db->query_fetch($strSQL);
 
Controller/Admin/Member/SmsSendPsController.php+31-2
변경 전SmsSendPsController.php
use Bundle\Component\Excel\Enum\ExcelSampleType;
use Bundle\Component\Excel\ExcelSample;
use Framework\Utility\StringUtils;
 
 
 
/**
* Class SmsSendPsController
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
 
 
if ($request->post()->get('searchWord', '') != '') {
$db->strWhere .= ' AND (subject LIKE CONCAT(\'%\', ?, \'%\') OR contents LIKE CONCAT(\'%\', ?, \'%\'))';
$db->bind_param_push($binds, $fieldTypes['subject'], $request->post()->get('searchWord'));
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
 
 
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
$data = $db->query_fetch($strSQL, $binds, false);
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
 
 
if ($request->post()->get('searchWord', '') != '') {
$db->strWhere .= ' AND (subject LIKE CONCAT(\'%\', ?, \'%\') OR contents LIKE CONCAT(\'%\', ? , \'%\'))';
$db->bind_param_push($binds, $fieldTypes['subject'], $request->post()->get('searchWord'));
$request = \App::getInstance('request');
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
try {
 
 
 
 
 
$isUpdate = $request->post()->has('sno');
$smsAdmin->validateSmsContents($request->post(), $isUpdate);
if ($smsAdmin->saveSmsContentsData($request->post()->all())) {
case 'deleteSmsContents': // 선택 발송내역 삭제
$request = \App::getInstance('request');
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
if ($smsAdmin->deleteSmsContentsData($request->post()->get('sno', []))) {
 
 
 
 
 
 
 
 
 
 
 
 
 
$this->json(
[
'result' => 'OK',
'message' => '삭제되었습니다.',
 
 
 
]
);
} else {
 
변경 후SmsSendPsController.php
use Bundle\Component\Excel\Enum\ExcelSampleType;
use Bundle\Component\Excel\ExcelSample;
use Framework\Utility\StringUtils;
use Origin\Enum\Crm\Message\SmsTemplateApprovalStatus;
use Origin\Service\Crm\Message\SmsTemplateService;
 
/**
* Class SmsSendPsController
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
$db->strWhere .= ' AND approvalStatus = ?';
$db->bind_param_push($binds, 's', SmsTemplateApprovalStatus::APPROVED->value);
if ($request->post()->get('searchWord', '') != '') {
$db->strWhere .= ' AND (subject LIKE CONCAT(\'%\', ?, \'%\') OR contents LIKE CONCAT(\'%\', ?, \'%\'))';
$db->bind_param_push($binds, $fieldTypes['subject'], $request->post()->get('searchWord'));
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
$db->strWhere .= ' AND approvalStatus = ?';
$db->bind_param_push($binds, 's', SmsTemplateApprovalStatus::APPROVED->value);
$query = $db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_SMS_CONTENTS . ' ' . gd_implode(' ', $query);
$data = $db->query_fetch($strSQL, $binds, false);
$db->strWhere = 'smsType = \'user\' AND smsAutoCode = ?';
$db->bind_param_push($binds, $fieldTypes['smsAutoCode'], $request->post()->get('smsAutoCode'));
}
$db->strWhere .= ' AND approvalStatus = ?';
$db->bind_param_push($binds, 's', SmsTemplateApprovalStatus::APPROVED->value);
if ($request->post()->get('searchWord', '') != '') {
$db->strWhere .= ' AND (subject LIKE CONCAT(\'%\', ?, \'%\') OR contents LIKE CONCAT(\'%\', ? , \'%\'))';
$db->bind_param_push($binds, $fieldTypes['subject'], $request->post()->get('searchWord'));
$request = \App::getInstance('request');
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
try {
$restrictMessage = \App::getInstance(SmsTemplateService::class)->getWriteRestrictMessage();
if ($restrictMessage !== null) {
$this->json(['result' => 'FAIL', 'message' => $restrictMessage,]);
break;
}
$isUpdate = $request->post()->has('sno');
$smsAdmin->validateSmsContents($request->post(), $isUpdate);
if ($smsAdmin->saveSmsContentsData($request->post()->all())) {
case 'deleteSmsContents': // 선택 발송내역 삭제
$request = \App::getInstance('request');
$smsAdmin = \App::load('Component\\Sms\\SmsAdmin');
$smsTemplateService = \App::getInstance(SmsTemplateService::class);
$restrictMessage = $smsTemplateService->getWriteRestrictMessage();
if ($restrictMessage !== null) {
$this->json(['result' => 'FAIL', 'message' => $restrictMessage,]);
break;
}
$snoList = $request->post()->get('sno', []);
$excluded = $smsTemplateService->excludeUsedByAutoSend(is_array($snoList) ? $snoList : []);
$deletableSnoList = $smsTemplateService->filterLegacyDeletableSnoList($excluded['deletableSnoList']);
if (empty($deletableSnoList) && $excluded['mappedCount'] > 0) {
$this->json(['result' => 'FAIL', 'message' => SmsTemplateService::SMS_USED_NOT_DELETABLE_MESSAGE,]);
break;
}
if ($smsAdmin->deleteSmsContentsData($deletableSnoList)) {
$this->json(
[
'result' => 'OK',
'message' => $excluded['mappedCount'] > 0
? sprintf(SmsTemplateService::SMS_PARTIAL_DELETED_MESSAGE, $excluded['mappedCount'], count($deletableSnoList))
: '삭제되었습니다.',
'mappedCount' => $excluded['mappedCount'],
]
);
} else {
 
Controller/Admin/Policy/ManagePsController.php+3-1
변경 전ManagePsController.php
$this->setData('sno', $request->get()->get('sno'));
// 최고운영자/위임자면 발송담당자-SMS(messageSendSms) 체크 고정+잠금 (뷰에서 사용)
$this->setData('isMessageApprover', $messageOperatorRoleService->isSenderPermissionLocked((int)$request->get()->get('sno')));
$this->setData('isEditorMessageApprover', $messageOperatorRoleService->isApprover((int)$session->get('manager.sno')));
/** @var MessageOperatorRoleGuard $messageOperatorRoleGuard */
$messageOperatorRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
$isEditorMessageRepresentative = $messageOperatorRoleGuard->isRepresentative();
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
 
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
 
 
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
break;
 
변경 후ManagePsController.php
$this->setData('sno', $request->get()->get('sno'));
// 최고운영자/위임자면 발송담당자-SMS(messageSendSms) 체크 고정+잠금 (뷰에서 사용)
$this->setData('isMessageApprover', $messageOperatorRoleService->isSenderPermissionLocked((int)$request->get()->get('sno')));
 
/** @var MessageOperatorRoleGuard $messageOperatorRoleGuard */
$messageOperatorRoleGuard = \App::getInstance(MessageOperatorRoleGuard::class);
$isEditorMessageRepresentative = $messageOperatorRoleGuard->isRepresentative();
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
$this->setData('isEditorMessageApprover', $messageOperatorRoleGuard->isDelegateOrHigher());
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
// 위임자 아님 안내가 우선이라 위임자 이상일 때만 판정한다 — isUncertifiedDelegateActor 는 위임자가 아닌 운영자도 DI 가 없으면 true 다
$this->setData('isCallerUncertifiedDelegate', $messageOperatorRoleGuard->isDelegateOrHigher() && $messageOperatorRoleService->isUncertifiedDelegateActor());
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
break;
 
Controller/Admin/Policy/ManageRegisterController.php+19-2
변경 전ManageRegisterController.php
$this->setData('messageDelegate', $messageDelegate);
 
// 발송 역할 계정(책임자·위임자·발송담당자)의 휴대폰번호는 본인인증(DI 주입)으로 처리한다
// 본인인증은 본인 계정에서만 성립하므로 책임자·제3자가 여는 화면은 기존 점유인증(SMS) UI 를 그대로 쓴다
$isSelfTarget = $isModify && (int)$session->get('manager.sno') === $managerNo;
// 최고운영자·발송 역할 계정은 정보 수정 주체를 책임자와 본인으로 제한한다
$isRestrictedEditTarget = $isModify
$isSelfCertificationProvided = $managerNo === ManagerService::SUPER_MANAGER_SNO
? $managerEditPolicy->isRepresentativeSelfCertificationProvided($managerNo)
: $isRestrictedEditTarget;
 
 
 
 
 
 
if ($isSelfTarget && $isSelfCertificationProvided) {
$selfCertification = [
'enabled' => true,
$this->addScript([
'manager-certification.js?' . time(),
]);
 
 
 
 
 
 
}
$this->setData('selfCertification', $selfCertification);
// 인증 방식 분기와 분리해 둔다 — 본인인증 화면이 본인 한정이 되어도 책임자·제3자 화면의 1차 차단은 남아야 한다
 
// 이름·휴대폰번호는 본인인증 명의라 직접 수정할 수 없다 — 발송 역할 계정 본인 화면과 책임자 본인 화면에서 잠근다
// 이름은 발송 역할 계정이면 책임자에게도 잠기고, 휴대폰번호는 책임자에게만 열린다
 
$this->setData('senderNameLocked', ($isModify && $managerEditPolicy->isSenderNameLocked($managerNo)) || $isRepresentativeSelf);
$this->setData('senderCellPhoneLocked', ($isModify && $managerEditPolicy->isSenderCellPhoneLocked($managerNo)) || $isRepresentativeSelf);
 
 
 
 
 
// --- 관리자 디자인 템플릿
$this->setData('isPasswordChangeRestricted', $isCompanySuperManager && $isMallInstalledAfterPatch);
 
변경 후ManageRegisterController.php
$this->setData('messageDelegate', $messageDelegate);
 
// 발송 역할 계정(책임자·위임자·발송담당자)의 휴대폰번호는 본인인증(DI 주입)으로 처리한다
// 본인인증은 본인 계정에서만 성립하므로 책임자가 여는 화면은 기존 점유인증(SMS) UI 를 그대로 쓴다
$isSelfTarget = $isModify && (int)$session->get('manager.sno') === $managerNo;
// 최고운영자·발송 역할 계정은 정보 수정 주체를 책임자와 본인으로 제한한다
$isRestrictedEditTarget = $isModify
$isSelfCertificationProvided = $managerNo === ManagerService::SUPER_MANAGER_SNO
? $managerEditPolicy->isRepresentativeSelfCertificationProvided($managerNo)
: $isRestrictedEditTarget;
// 책임자도 본인도 아닌 운영자에게도 발송 역할 계정의 인증 방식은 본인인증으로 노출한다 — 인증 스크립트는 싣지 않고 클릭은 본인 아님 안내로 끝난다
$isOtherEditorOfSenderRole = $isModify
&& $managerNo !== ManagerService::SUPER_MANAGER_SNO
&& $isRestrictedEditTarget
&& !$isSelfTarget
&& !$messageOperatorRoleGuard->isRepresentative();
if ($isSelfTarget && $isSelfCertificationProvided) {
$selfCertification = [
'enabled' => true,
$this->addScript([
'manager-certification.js?' . time(),
]);
} elseif ($isOtherEditorOfSenderRole) {
$selfCertification = [
'enabled' => true,
'certified' => $messageOperatorRoleGuard->isConnected($managerNo),
'isSelf' => false,
];
}
$this->setData('selfCertification', $selfCertification);
// 인증 방식 분기와 분리해 둔다 — 본인인증 화면이 본인 한정이 되어도 책임자·제3자 화면의 1차 차단은 남아야 한다
 
// 이름·휴대폰번호는 본인인증 명의라 직접 수정할 수 없다 — 발송 역할 계정 본인 화면과 책임자 본인 화면에서 잠근다
// 이름은 발송 역할 계정이면 책임자에게도 잠기고, 휴대폰번호는 책임자에게만 열린다
// 그 외 운영자 화면의 휴대폰 입력은 기획상 활성으로 노출한다 — 저장은 화면 1차 차단과 저장측 되돌림이 막는다
$this->setData('senderNameLocked', ($isModify && $managerEditPolicy->isSenderNameLocked($managerNo)) || $isRepresentativeSelf);
$this->setData(
'senderCellPhoneLocked',
!$isOtherEditorOfSenderRole
&& (($isModify && $managerEditPolicy->isSenderCellPhoneLocked($managerNo)) || $isRepresentativeSelf)
);
 
// --- 관리자 디자인 템플릿
$this->setData('isPasswordChangeRestricted', $isCompanySuperManager && $isMallInstalledAfterPatch);
 
Controller/Admin/Promotion/CartRemindPsController.php-77
변경 전CartRemindPsController.php
<?php
 
/**
* This is commercial software, only users who have purchased a valid license
* and accept to the terms of the License Agreement can install and use this
* program.
*
* Do not edit or add to this file if you wish to upgrade Godomall5 to newer
* versions in the future.
*
* @copyright ⓒ 2016, NHN godo: Corp.
* @link http://www.godo.co.kr
*/
namespace Bundle\Controller\Admin\Promotion;
 
use Component\CartRemind\CartRemind;
use Component\Sms\Exception\PasswordException;
use Framework\Debug\Exception;
use Framework\Debug\Exception\LayerNotReloadException;
use Message;
use Request;
 
class CartRemindPsController extends \Controller\Admin\Controller
{
 
/**
* 장바구니 알림 처리
* [관리자 모드] 장바구니 알림 처리
*
* @author su
*/
public function index()
{
try {
$cartRemind = new CartRemind();
switch (Request::post()->get('mode')) {
case 'insertCartRemind':
case 'modifyCartRemind':
// 모듈 호출
$postValue = Request::post()->toArray();
$cartRemind->setCartRemind($postValue);
$this->layer(__('저장이 완료되었습니다.'), 'parent.location.href="cart_remind_list.php";');
break;
case 'modifyCartRemindMessage':
$postValue = Request::post()->toArray();
$cartRemind->setCartRemindSendMessage($postValue);
$this->layer(__('수정되었습니다.'), 'parent.location.href="cart_remind_list.php";');
break;
case 'sendCartRemindManual':
$postValue = Request::post()->toArray();
try {
$result = $cartRemind->sendCartRemind($postValue);
} catch (PasswordException $e) {
$result['msg'] = $e->getMessage();
}
echo json_encode($result);
exit;
break;
case 'setCartRemindAutoState':
$postValue = Request::post()->toArray();
$cartRemind->setCartRemindAutoState($postValue);
$result['code'] = 99;
$result['msg'] = __('변경되었습니다.');
echo json_encode($result);
exit;
break;
case 'deleteCartRemind':
$postValue = Request::post()->toArray();
$cartRemind->deleteCartRemind($postValue);
$this->layer(__('삭제되었습니다.'), 'parent.location.href="cart_remind_list.php";');
break;
}
} catch (\Exception $e) {
throw new LayerNotReloadException($e->getMessage()); //새로고침안됨
}
}
}
 
변경 후CartRemindPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Promotion/CouponPsController.php+8
변경 전CouponPsController.php
use Framework\File\FileValidator;
use Origin\DTO\ApiClient\Commerce\Crm\TargetsRepeatRuleLatestResultsRequestDTO;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
 
use Request;
 
class CouponPsController extends \Controller\Admin\Controller
break;
case 'sendComebackCoupon':
$aSno = Request::post()->get('chkCoupon');
 
 
 
 
 
 
 
$couponAdmin->sendComebackCoupon($aSno[0]);
$this->layer(__('컴백쿠폰 발송이 완료되었습니다.'), 'top.location.href="comeback_coupon_list.php";');
break;
 
변경 후CouponPsController.php
use Framework\File\FileValidator;
use Origin\DTO\ApiClient\Commerce\Crm\TargetsRepeatRuleLatestResultsRequestDTO;
use Origin\Service\ApiClient\Commerce\Crm\CrmAdminApiClientService;
use Origin\Service\Sms\OccupancyAuth\SendGateService;
use Request;
 
class CouponPsController extends \Controller\Admin\Controller
break;
case 'sendComebackCoupon':
$aSno = Request::post()->get('chkCoupon');
$comebackCouponInfo = $couponAdmin->getComebackCouponInfo($aSno[0], 'smsFl');
if (($comebackCouponInfo['smsFl'] ?? '') === 'y') {
/** @var SendGateService $sendGateService */
$sendGateService = \App::getInstance(SendGateService::class);
$sendGateService->assertSendable();
$sendGateService->assertCallNumberUsable();
}
$couponAdmin->sendComebackCoupon($aSno[0]);
$this->layer(__('컴백쿠폰 발송이 완료되었습니다.'), 'top.location.href="comeback_coupon_list.php";');
break;
 
Controller/Admin/Share/LayerManagePermissionController.php+3-1
변경 전LayerManagePermissionController.php
$this->setData('disabled', $disabled);
$this->setData('sno', $request->post()->get('sno'));
$this->setData('isMessageApprover', $isMessageApprover);
$this->setData('isEditorMessageApprover', $messageOperatorRoleService->isApprover((int)$session->get('manager.sno')));
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
 
 
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
$this->setData('scmFl', $scmFl);
 
변경 후LayerManagePermissionController.php
$this->setData('disabled', $disabled);
$this->setData('sno', $request->post()->get('sno'));
$this->setData('isMessageApprover', $isMessageApprover);
$this->setData('isEditorMessageApprover', $messageOperatorRoleGuard->isDelegateOrHigher());
$this->setData('isEditorMessageRepresentative', $isEditorMessageRepresentative);
// 차단 조회는 외부 API 호출이라 책임자 화면에서만 수행한다
$this->setData('isCallerBlocked', $isEditorMessageRepresentative && $messageOperatorRoleService->isBlockedRepresentative());
// 위임자 아님 안내가 우선이라 위임자 이상일 때만 판정한다 — isUncertifiedDelegateActor 는 위임자가 아닌 운영자도 DI 가 없으면 true 다
$this->setData('isCallerUncertifiedDelegate', $messageOperatorRoleGuard->isDelegateOrHigher() && $messageOperatorRoleService->isUncertifiedDelegateActor());
$this->setData('messageSenderCount', $messageOperatorRoleService->countSenders());
$this->setData('messageSenderLimit', MessageOperatorRoleService::MAX_SENDER_COUNT);
$this->setData('scmFl', $scmFl);
 
Controller/Admin/Share/LayerSmsContentsController.php-95
변경 전LayerSmsContentsController.php
<?php
 
/**
* This is commercial software, only users who have purchased a valid license
* and accept to the terms of the License Agreement can install and use this
* program.
*
* Do not edit or add to this file if you wish to upgrade Godomall5 to newer
* versions in the future.
*
* @copyright ⓒ 2016, NHN godo: Corp.
* @link http://www.godo.co.kr
*/
namespace Bundle\Controller\Admin\Share;
 
use Component\Sms\SmsAdmin;
use Request;
 
/**
* SMS 문구 선택하기
*
* @author Shin Donggyu <artherot@godo.co.kr>
*/
class LayerSmsContentsController extends \Controller\Admin\Controller
{
 
/**
* index
*
*/
public function index()
{
// --- SMS 모듈
$smsAdmin = new SmsAdmin();
 
// mode 가 있는 경우
if (Request::post()->has('mode')) {
// _POST 데이터
$postValue = Request::post()->toArray();
 
switch (Request::post()->get('mode')) {
// 등록 / 수정
case 'registerSmsContents':
case 'modifySmsContents':
$result = $smsAdmin->saveSmsContentsData($postValue);
if ($result === true) {
echo 'OK';
} else {
echo 'ERROR';
}
break;
 
// 선택 삭제
case 'deleteSmsContents':
$result = $smsAdmin->deleteSmsContentsData(Request::post()->get('delSno'));
if ($result === true) {
echo 'OK';
} else {
echo 'ERROR';
}
break;
}
exit();
}
 
// SMS 문구 그룹
$smsContentsGroup = gd_code('01007');
$smsContentsGroup = gd_array_merge(['0' => '문구 선택'], $smsContentsGroup);
 
// SMS 문구 리스트
$smsContentsList = $smsAdmin->getSmsContentsList('user');
 
// _GET Data
$getValue = Request::get()->toArray();
 
// page Url
$pageUrl = '../' . Request::getDirectoryUri() . '/' . Request::getFileUri();
 
//--- 관리자 디자인 템플릿
$this->getView()->setDefine('layout', 'layout_layer.php');
$this->getView()->setDefine('layoutContent', Request::getDirectoryUri() . '/' . Request::getFileUri());
 
$this->setData('layerFormID', $getValue['layerFormID']);
$this->setData('parentFormID', $getValue['parentFormID']);
$this->setData('dataFormID', $getValue['dataFormID']);
$this->setData('dataInputNm', $getValue['dataInputNm']);
$this->setData('mode', gd_isset($getValue['mode'], 'search'));
$this->setData('disabled', gd_isset($getValue['disabled'], ''));
$this->setData('callFunc', gd_isset($getValue['callFunc'], ''));
 
$this->setData('pageUrl', $pageUrl);
$this->setData('smsContentsGroup', $smsContentsGroup);
$this->setData($smsContentsList);
}
}
 
변경 후LayerSmsContentsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Controller/Admin/Share/LayerSmsPasswordPsController.php+35
변경 전LayerSmsPasswordPsController.php
use Component\Sms\Exception\PasswordException;
use Component\Sms\SmsSender;
use Framework\Utility\ComponentUtils;
 
 
 
 
class LayerSmsPasswordPsController extends \Controller\Admin\Controller
{
$request = \App::getInstance('request');
$isChangePassword = $request->post()->get('mode', 'changePassword') === 'changePassword';
if ($isChangePassword) {
 
 
 
 
 
$captchaNumber = $request->post()->get('captcha');
$captcha = new Captcha();
$result = $captcha->verify($captchaNumber, 1);
);
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후LayerSmsPasswordPsController.php
use Component\Sms\Exception\PasswordException;
use Component\Sms\SmsSender;
use Framework\Utility\ComponentUtils;
use Origin\Enum\Crm\Message\MessageSendMenuAccessVerdict;
use Origin\Enum\Crm\Message\SmsSendRestrictType;
use Origin\Service\Crm\Message\MessageSendMenuAccessService;
 
class LayerSmsPasswordPsController extends \Controller\Admin\Controller
{
$request = \App::getInstance('request');
$isChangePassword = $request->post()->get('mode', 'changePassword') === 'changePassword';
if ($isChangePassword) {
$restrictMessage = $this->getSmsRestrictMessage();
if ($restrictMessage !== null) {
$this->json(['error' => 1, 'message' => $restrictMessage]);
}
 
$captchaNumber = $request->post()->get('captcha');
$captcha = new Captcha();
$result = $captcha->verify($captchaNumber, 1);
);
}
}
 
 
/**
* SMS 인증번호 변경은 SMS 관리자만 가능하다.
* ps 는 인터셉터 게이트를 타지 않아 화면 제한을 우회한 직접 호출이 가능하므로 조작 시점에 다시 본다.
* 판정축은 진입 게이트와 동일(역할·차단·IP·진입 점유인증). 책임자 미연동은 등록 안내 플로우라 차단하지 않는다.
*/
protected function getSmsRestrictMessage(): ?string
{
/** @var MessageSendMenuAccessService $menuAccessService */
$menuAccessService = \App::getInstance(MessageSendMenuAccessService::class);
$verdict = $menuAccessService->evaluateEntry((int)\App::getInstance('session')->get('manager.sno'));
 
$writableVerdicts = [
null,
MessageSendMenuAccessVerdict::NOT_CONNECTED_REPRESENTATIVE,
MessageSendMenuAccessVerdict::NOT_CONNECTED_MANAGER,
];
 
if (in_array($verdict, $writableVerdicts, true)) {
return null;
}
 
$restrictType = SmsSendRestrictType::fromVerdict($verdict);
 
return $restrictType->getClickAlertMessage() . ' ' . $restrictType->getClickAlertSubMessage();
}
}