변경 파일 목록
총 119개
간편상품등록 기능 추가
layer_goods_option_list.phpAdmin/goods/ncds/layer_goods_option_register.phpAdmin/goods/ncds/simple_goods_register.phpAdmin/goods/layer_goods_option_grid_config.phpAdmin/share/ncds/layer_goods_option_stock.phpAdmin/share/ncds/layer_goods.phpAdmin/share/ncds/CategoryAdmin.phpComponent/Category/DBTableField.phpComponent/Database/GoodsAdmin.phpComponent/Goods/SimpleGoodsDAO.phpComponent/Goods/SimpleGoods.phpComponent/Goods/GoodsPsController.phpController/Admin/Goods/LayerGoodsOptionListController.phpController/Admin/Goods/Ncds/LayerGoodsOptionRegisterController.phpController/Admin/Goods/Ncds/SimpleGoodsRegisterController.phpController/Admin/Goods/LayerGoodsOptionListController.phpController/Admin/Provider/Goods/Ncds/LayerGoodsOptionRegisterController.phpController/Admin/Provider/Goods/Ncds/SimpleGoodsRegisterController.phpController/Admin/Provider/Goods/LayerBrandController.phpController/Admin/Provider/Share/Ncds/LayerCategoryController.phpController/Admin/Provider/Share/Ncds/LayerGoodsController.phpController/Admin/Provider/Share/Ncds/LayerGoodsOptionGridConfigController.phpController/Admin/Provider/Share/Ncds/LayerGoodsOptionStockController.phpController/Admin/Provider/Share/Ncds/CategorySelectJsonController.phpController/Admin/Share/LayerGoodsOptionGridConfigController.phpController/Admin/Share/Ncds/LayerGoodsOptionStockController.phpController/Admin/Share/Ncds/관리자 동시 접속 보안 강화.
duplicate_login.phpAdmin/base/AdminLogDAO.phpComponent/Admin/ManagerCs.phpComponent/Member/Manager.phpComponent/Member/IndexController.phpController/Admin/Base/상품 리스트 및 상품 가격 관리 정렬 기능 개선.
DBTableField.phpComponent/Database/GoodsAdmin.phpComponent/Goods/GoodsBatchPriceController.phpController/Admin/Goods/GoodsPsController.phpController/Admin/Goods/고도몰 쇼핑몰 보안 강화
Board.phpComponent/Board/PollDetailViewController.phpController/Admin/Promotion/PollResultController.phpController/Front/Service/report.htmldata/skin/mobile/moment/board/report.htmldata/skin/mobile/mygreek/board/report.htmldata/skin/mobile/story_g/board/게시판 이미지 업로드 지원 확대.
Board.phpComponent/Board/EditorUpload.phpComponent/File/ArticlePsController.phpController/Admin/Board/BoardPsController.phpController/Front/Board/BoardPsController.phpController/Mobile/Board/CRM 관리 메뉴에 CRM 레시피 기능 추가
crm_recipe_frame.phpAdmin/crm/crm_recipe_send.phpAdmin/crm/layer_preview_mobile_send.phpAdmin/crm/layer_kakao_alimtalk_content_setting.phpAdmin/crm/mobile_send/layer_kakao_friendtalk_content_setting.phpAdmin/crm/mobile_send/layer_myapp_push_content_setting.phpAdmin/crm/mobile_send/layer_preview_message.phpAdmin/crm/mobile_send/layer_recipe_send_time_setting.phpAdmin/crm/mobile_send/layer_recipient_setting.phpAdmin/crm/mobile_send/layer_select_crm_groups.phpAdmin/crm/mobile_send/layer_send_method_setting.phpAdmin/crm/mobile_send/layer_send_time_setting.phpAdmin/crm/mobile_send/layer_sms_content_setting.phpAdmin/crm/mobile_send/mobile_send.phpAdmin/crm/popup_create_crm_group_frame.phpAdmin/crm/MemberAdmin.phpComponent/Member/MemberDAO.phpComponent/Member/CrmRecipeClickLogPsController.phpController/Admin/Crm/CrmRecipeSendController.phpController/Admin/Crm/LayerKakaoFriendtalkContentSettingController.phpController/Admin/Crm/MobileSend/LayerMyappPushContentSettingController.phpController/Admin/Crm/MobileSend/LayerRecipeSendTimeSettingController.phpController/Admin/Crm/MobileSend/LayerSendMethodSettingController.phpController/Admin/Crm/MobileSend/LayerSendTimeSettingController.phpController/Admin/Crm/MobileSend/LayerSmsContentSettingController.phpController/Admin/Crm/MobileSend/MobileSendPsController.phpController/Admin/Crm/RecipeContextResolverTrait.phpUtil/Crm/해외구매대행 전자상거래 업체부호 설정 기능 추가
base_info.phpAdmin/policy/BuyerInform.phpComponent/Agreement/DBTableField.phpComponent/Database/Mall.phpComponent/Mall/Policy.phpComponent/Policy/수기주문에서 회원 장바구니 상품 2개 이상 추가 안정성 개선
OrderPsController.phpController/Admin/Order/반응형 스킨 디자인에디터 팝업 노출 개선
PopupRegisterController.phpController/Admin/Design/DB 데드락 자동 복구 및 발송/장바구니 교착 완화
Cart.phpComponent/Cart/MailLog.phpComponent/Mail/CartPsController.phpController/Front/Order/비회원 주문 취소 복원 처리 개선
ReOrderCalculation.phpComponent/Order/관리자 주문 상세보기 성능 개선
order_view.phpAdmin/order/OrderAdminNew.phpComponent/Order/OrderAdmin.phpComponent/Order/OrderDeliveryOriginalRepository.phpRepository/Order/OrderHandleRepository.phpRepository/Order/사은품 주문 보안 강화.
Gift.phpComponent/Gift/GiftSelectCntValidator.phpComponent/Gift/OrderNew.phpComponent/Order/Order.phpComponent/Order/RegularGiftSelectCntValidator.phpComponent/RegularDelivery/RegularGoods/RegularGoodsChange.phpComponent/RegularDelivery/RegularGoods/RegularOrderApplicationCreate.phpComponent/RegularDelivery/RegularOrder/RegularOrderPsController.phpController/Front/Order/RegularOrderPsController.phpController/Mobile/Order/RegularGiftPresentInfoRepository.phpRepository/RegularDelivery/RegularGoods/카카오 아이디 로그인 설정 페이지 헤더 표시 개선.
kakao_login_config.phpAdmin/member/네이버 아이디 로그인 보안 강화
AdultController.phpController/Front/Intro/MemberController.phpController/Front/Intro/LoginController.phpController/Front/Member/NaverLoginController.phpController/Front/Member/Naver/AdultController.phpController/Mobile/Intro/MemberController.phpController/Mobile/Intro/디자인에디터 화면의 렌더링 일관성 및 유지보수성 개선
base_info.phpAdmin/policy/BuyerInform.phpComponent/Agreement/DBTableField.phpComponent/Database/Mall.phpComponent/Mall/Policy.phpComponent/Policy/회원등급 평가방법 설정의 '주문금액' 표기를 '실결제금액'으로 변경
member_group_appraisal_config.phpAdmin/member/고도몰 관리자 팝업 표시 방식이 개선되었어요.
AdminPanelApiController.phpController/Admin/Share/변경 파일 코드
추가삭제
변경 전layer_goods_option_list.php
변경 후layer_goods_option_list.php
| <link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-goods-option-list.css')?>"> | |
| <div class="modal-dialog__content"> | |
| <article class="ncua-content"> | |
| <div class="ncua-table ncua-table--vertical"> | |
| <table> | |
| <tbody> | |
| <tr> | |
| <th><div>검색어</div></th> | |
| <td> | |
| <div class="ncua-gap-4"> | |
| <span class="ncua-select ncua-select--xs" style="width:140px;flex:0 0 140px;"> | |
| <span class="ncua-select__content"> | |
| <?php echo gd_select_box('key','key',array('all'=>'=통합검색=','optionManageNm'=>'옵션 관리명','optionName'=>'옵션명'),null,$search['key'], null, null, 'ncua-select__tag');?> | |
| </span> | |
| </span> | |
| <div class="ncua-input ncua-input--xs" style="flex:1;"> | |
| <div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"> | |
| <input type="text" name="keyword" value="<?=$search['keyword'];?>" placeholder="검색어를 입력하세요" /> | |
| </div></div></div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th><div>옵션 노출 방식</div></th> | |
| <td> | |
| <div class="ncua-radio-group 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" name="optionDisplayFl" value="" <?php if (empty($search['optionDisplayFl'])) { echo 'checked'; } ?> /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">전체</span></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="optionDisplayFl" value="s" <?=gd_isset($checked['optionDisplayFl']['s']);?> /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">일체형</span></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="optionDisplayFl" value="d" <?=gd_isset($checked['optionDisplayFl']['d']);?> /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">분리형</span></span> | |
| </label> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <p class="ncua-btn-group ncua-align-right" style="gap:16px;"> | |
| <button type="button" class="ncua-btn ncua-btn--xs has-underline ncua-btn--text" onclick="layer_list_reset();"><span class="ncua-btn__label">초기화</span></button> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary" onclick="layer_list_search();"><span class="ncua-btn__label">검색</span></button> | |
| </p> | |
| <div class="ncua-search-result"> | |
| <div class="ncua-search-result__summary"> | |
| <p class="ncua-search-result__summary-count">검색 <strong><?= number_format(gd_isset($page->recode['total'], 0)); ?></strong>개 / 전체 <strong class="text-danger"><?= number_format(gd_isset($page->recode['amount'], 0)); ?></strong>개</p> | |
| </div> | |
| </div> | |
| <div class="ncua-freq-list__table-wrap"> | |
| <form id="frm_layer_goods_option"> | |
| <input type="hidden" name="mode" value="apply_goods_option"> | |
| <div class="ncua-table ncua-table--horizontal ncua-table--rounded"> | |
| <table> | |
| <colgroup> | |
| <col width="56px"/> | |
| <col width="80px"/> | |
| <col width="200px"/> | |
| <col width="100px"/> | |
| <col width="120px"/> | |
| <col width="*"/> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th> | |
| <div style="justify-content:center;"> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="checkbox" id="optLoadFreqCheckAll"/> | |
| </span> | |
| </label> | |
| </div> | |
| </th> | |
| <th><div style="justify-content:center;">번호</div></th> | |
| <th><div style="justify-content:center;">옵션 관리명</div></th> | |
| <th><div style="justify-content:center;">옵션표시</div></th> | |
| <th><div style="justify-content:center;">옵션갯수</div></th> | |
| <th><div style="justify-content:center;">옵션명</div></th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <?php | |
| $hasData = is_array($data) && count($data) > 0; | |
| if ($hasData) { | |
| $arrOptionDisplay = array('s' => '일체형', 'd' => '분리형', 'm' => '멀티형'); | |
| foreach ($data as $key => $val) { | |
| $arrOptionName = explode(STR_DIVISION, $val['optionName']); | |
| ?> | |
| <tr> | |
| <td> | |
| <div style="justify-content:center;"> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="checkbox" value="<?=$val['sno'];?>" name="sno[]" data-option-display="<?=$val['optionDisplayFl']?>"/> | |
| </span> | |
| </label> | |
| </div> | |
| </td> | |
| <td><div style="justify-content:center;"><?=number_format($page->recode['start'] + $key + 1);?></div></td> | |
| <td><div><?=gd_htmlspecialchars($val['optionManageNm']);?></div></td> | |
| <td><div style="justify-content:center;"><?=$arrOptionDisplay[$val['optionDisplayFl']];?></div></td> | |
| <td><div style="justify-content:center;"><?=gd_count($arrOptionName);?></div></td> | |
| <td><div style="justify-content:center;"><?=gd_htmlspecialchars(str_replace(STR_DIVISION, ', ', $val['optionName']));?></div></td> | |
| </tr> | |
| <?php | |
| } | |
| } else { | |
| ?> | |
| <tr> | |
| <td colspan="6"><div style="justify-content:center;"><?= empty($search['keyword']) && empty($search['optionDisplayFl']) ? '등록된 자주쓰는 옵션이 없습니다.' : '검색 결과가 없습니다.' ?></div></td> | |
| </tr> | |
| <?php } ?> | |
| </tbody> | |
| </table> | |
| <?php if ($hasData) { ?> | |
| <div class="ncua-freq-list__footer-bar"></div> | |
| <?php } ?> | |
| </div> | |
| </form> | |
| </div> | |
| <?php if ($hasData) { ?> | |
| <div class="ncua-search-result-pagination"> | |
| <div class="ncua-pagination"> | |
| <?php echo $page->getPage('layer_list_search(\'PAGELINK\')'); ?> | |
| </div> | |
| </div> | |
| <?php } ?> | |
| <div class="ncua-notice-info" style="margin-top:8px;"> | |
| "일체형/분리형" 옵션을 함께 선택한 경우 등록이 불가능합니다.<br/> | |
| 선택된 옵션의 "옵션갯수"의 총 합계가 5개를 초과한 경우 등록이 불가능합니다. | |
| </div> | |
| </article> | |
| </div> | |
| <div class="board-template-write-footer modal-dialog__footer"> | |
| <button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-cancel"> | |
| <span class="ncua-btn__label">취소</span> | |
| </button> | |
| <button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-close"> | |
| <span class="ncua-btn__label">선택 완료</span> | |
| </button> | |
| </div> | |
| <script type="text/javascript"> | |
| <!-- | |
| $(document).ready(function(){ | |
| var callFunc = '<?= gd_isset($callFunc, '') ?>'; | |
| $('#optLoadFreqCheckAll').click(function(){ | |
| $('input[name*="sno[]"]').prop('checked', $(this).prop('checked')); | |
| }); | |
| $('input[name*="sno[]"]').click(function(){ | |
| var total = $('input[name*="sno[]"]').length; | |
| var checked = $('input[name*="sno[]"]:checked').length; | |
| $('#optLoadFreqCheckAll').prop('checked', total === checked); | |
| }); | |
| $('.js-cancel').click(function(){ | |
| $('div.bootstrap-dialog-close-button').click(); | |
| }); | |
| $('.js-close').click(function(e){ | |
| if ($('input[name*="sno[]"]:checked').length == 0) { | |
| NCDSAlert({ message: '선택된 옵션이 없습니다.', iconType: 'error' }); | |
| return false; | |
| } | |
| optionValueFill = false; | |
| var parameters = $("#frm_layer_goods_option").serialize(); | |
| $.post('./goods_ps.php', parameters, function (data) { | |
| var getData = $.parseJSON(data); | |
| var msg = []; | |
| if(getData.displayFl =='') { | |
| msg.push("옵션표시 유형이 다른 경우 등록이 불가능합니다."); | |
| } | |
| if(getData.optionName.length > 5) { | |
| msg.push("선택된 옵션의 옵션개수가 5개를 초과하여 등록이 불가능합니다."); | |
| } | |
| if(msg.length > 0) { | |
| NCDSAlert({ message: msg.join('<br>'), iconType: 'error' }); | |
| return false; | |
| } | |
| if (callFunc && typeof window[callFunc] === 'function') { | |
| // 'deferClose' 반환 콜백은 적용 성공 시점에 스스로 모달을 닫는다 (실패·취소 시 모달 유지) | |
| if (window[callFunc](getData) !== 'deferClose') { | |
| $('div.bootstrap-dialog-close-button').click(); | |
| } | |
| return; | |
| } | |
| var optionCnt = getData.optionName.length; | |
| if(typeof(option_reset_layer) == "undefined"){ | |
| option_reset(); | |
| $('#optionY_optionCnt').val(optionCnt); | |
| $("input[name='optionY[optionDisplayFl]'][value='"+getData.displayFl+"']").prop('checked',true); | |
| option_setting(optionCnt); | |
| for(var i = 0; i < optionCnt; i++) { | |
| $('#option_optionName_'+i).val(getData.optionName[i]); | |
| if (typeof getData.optionValue != 'undefined' && getData.optionValue != null) { | |
| if (typeof getData.optionValue[i] != 'undefined') { | |
| var valueCnt = getData.optionValue[i].length; | |
| $('#option_optionCnt_' + i).val(valueCnt); | |
| option_value_conf(i, valueCnt, true); | |
| for (var j = 0; j < valueCnt; j++) { | |
| $('#option_optionValue_' + i + '_' + j).val(getData.optionValue[i][j]); | |
| } | |
| } | |
| } | |
| } | |
| option_grid(); | |
| }else{ | |
| option_reset_layer(); | |
| $('#optionY_optionCnt').val(optionCnt); | |
| $("input[name='optionY[optionDisplayFl]'][value='"+getData.displayFl+"']").prop('checked',true); | |
| option_setting_layer(optionCnt); | |
| for(var i = 0; i < optionCnt; i++) { | |
| $('#option_optionName_layer_'+i).val(getData.optionName[i]); | |
| if (typeof getData.optionValue != 'undefined' && getData.optionValue != null) { | |
| if (typeof getData.optionValue[i] != 'undefined') { | |
| var valueCnt = getData.optionValue[i].length; | |
| $('#option_optionCnt_' + i).val(valueCnt); | |
| option_value_conf_layer(i, valueCnt, true); | |
| for (var j = 0; j < valueCnt; j++) { | |
| $('#option_optionValue_' + i + '_' + j).val(getData.optionValue[i][j]); | |
| } | |
| } | |
| } | |
| } | |
| option_grid_layer('y'); | |
| } | |
| $('div.bootstrap-dialog-close-button').click(); | |
| }); | |
| }); | |
| }); | |
| function layer_list_search(pagelink) | |
| { | |
| var keyStr = $('#key').val(); | |
| var keyword = $('input[name=\'keyword\']').val(); | |
| var optionDisplayFl = $('input[name=\'optionDisplayFl\']:checked').val(); | |
| if (typeof optionDisplayFl == 'undefined') { | |
| optionDisplayFl = ''; | |
| } | |
| var parameters = { | |
| 'key' : keyStr, | |
| 'keyword' : keyword, | |
| 'optionDisplayFl' : optionDisplayFl, | |
| 'scmNo' : '<?=$scmNo?>', | |
| 'callFunc' : '<?= gd_isset($callFunc, '') ?>', | |
| 'pagelink' : pagelink | |
| }; | |
| $.get('ncds/layer_goods_option_list.php', parameters, function(data){ | |
| $('#layerOptionListForm').html(data); | |
| }); | |
| } | |
| function layer_list_reset() | |
| { | |
| var parameters = { | |
| 'scmNo' : '<?=$scmNo?>', | |
| 'callFunc' : '<?= gd_isset($callFunc, '') ?>' | |
| }; | |
| $.get('ncds/layer_goods_option_list.php', parameters, function(data){ | |
| $('#layerOptionListForm').html(data); | |
| }); | |
| } | |
| //--> | |
| </script> | |
변경 전layer_goods_option_register.php
변경 후layer_goods_option_register.php
| <div class="modal-dialog__content"> | |
| <article class="ncua-content"> | |
| <div class="ncua-table ncua-table--vertical"> | |
| <table> | |
| <tbody> | |
| <tr> | |
| <th class="ncua-required"><div>옵션 관리명</div></th> | |
| <td> | |
| <div class="ncua-input ncua-input--xs ncua-input-full-width"> | |
| <div class="ncua-input__content-wrap" style="width:100%;"><div class="ncua-input__content" style="width:100%;"><div class="ncua-input__field ncua-input__field--xs"><input type="text" id="freqSaveGroupName" name="groupName" placeholder="옵션관리명을 입력하세요." maxlength="50" aria-required="true" /></div></div></div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </article> | |
| </div> | |
| <div class="modal-dialog__footer"> | |
| <button type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary js-opt-save-freq-submit"><span class="ncua-btn__label">등록</span></button> | |
| </div> | |
| <script type="text/javascript"> | |
| <!-- | |
| $(function () { | |
| setTimeout(function () { $('#freqSaveGroupName').focus(); }, 50); | |
| $('#freqSaveGroupName').on('keydown', function (e) { | |
| if (e.key === 'Enter') { e.preventDefault(); $('.js-opt-save-freq-submit').trigger('click'); } | |
| }); | |
| $('.js-opt-save-freq-submit').on('click', function () { | |
| var managNm = $.trim($('#freqSaveGroupName').val()); | |
| if (!managNm) { | |
| NCDSAlert({ message: '옵션 관리명을 입력하세요.', iconType: 'error', callback: function () { $('#freqSaveGroupName').focus(); } }); | |
| return; | |
| } | |
| const DIV = '<?= STR_DIVISION ?>'; | |
| var names = []; | |
| var values = []; | |
| $('.js-opt-name-input').each(function (i) { | |
| var nm = $.trim($(this).val()); | |
| if (!nm) return; | |
| names.push(nm); | |
| var vals = []; | |
| $(this).closest('.js-opt-name-row').find('.js-opt-val-input').each(function () { | |
| vals.push($.trim($(this).val())); | |
| }); | |
| values.push(vals.join(DIV)); | |
| }); | |
| $.ajax({ | |
| url: './goods_ps.php', type: 'POST', | |
| data: { | |
| mode: 'option_direct_register', | |
| optionManageNm: managNm, | |
| optionDisplayFl: $('[name="optionY[optionDisplayFl]"]:checked').val() || 's', | |
| optionName: names.join(DIV), | |
| optionValue: values | |
| }, | |
| success: function () { | |
| layer_close(); | |
| NCDSAlert({ message: '자주쓰는 옵션이 등록되었습니다.', iconType: 'success' }); | |
| }, | |
| error: function () { | |
| NCDSAlert({ message: '자주쓰는 옵션 등록에 실패했습니다.', iconType: 'error' }); | |
| } | |
| }); | |
| }); | |
| }); | |
| //--> | |
| </script> | |
변경 전simple_goods_register.php
변경 후simple_goods_register.php
| <link rel="stylesheet" href="<?= PATH_ADMIN_GD_SHARE ?>css/goods/simple_goods.css"> | |
| <link type="text/css" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/ncds-editor/' . NCDS_EDITOR_VERSION . '/ncds-editor.css')?>" rel="stylesheet"/> | |
| <script type="text/javascript" src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-editor/' . NCDS_EDITOR_VERSION . '/ncds-editor.js')?>"></script> | |
| <script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/ncds-multi-select/ncds-multi-select.js')?>"></script> | |
| <script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/supply-combo-box.js')?>"></script> | |
| <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/number-only.js')?>"></script> | |
| <script defer src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/charcount.js')?>"></script> | |
| <script type="text/javascript" src="/admin/gd_share/script/jquery/jquery.multi_select_box.js"></script> | |
| <article class="ncua-content simple-goods-register"> | |
| <form id="frmSimpleGoods" name="frmSimpleGoods" action="./goods_ps.php" method="post" enctype="multipart/form-data"> | |
| <input type="hidden" name="mode" value="simple_register" /> | |
| <input type="hidden" name="simpleGoodsToken" value="<?=$simpleGoodsToken?>" /> | |
| <input type="hidden" name="imageStorage" value="<?=$defaultImageStorage?>" /> | |
| <input type="hidden" name="deliverySno" value="" /> | |
| <input type="hidden" name="optionReged" value="y" /> | |
| <?php if (gd_is_provider()): ?> | |
| <input type="hidden" name="scmFl" value="y" /> | |
| <input type="hidden" name="scmNo" value="<?= gd_htmlspecialchars($scmNo) ?>" /> | |
| <?php else: ?> | |
| <input type="hidden" name="scmNo" id="scmNoInput" value="<?= DEFAULT_CODE_SCMNO ?>" /> | |
| <?php endif; ?> | |
| <header class="page-header js-affix ncua-page-header"> | |
| <h3 class="ncua-help-manual"><?= end($naviMenu->location); ?></h3> | |
| <div class="ncua-page-header__actions"> | |
| <button type="button" class="ncua-btn ncua-btn--md ncua-btn--secondary-gray js-sgr-list-btn" onclick="location.href='./goods_list.php'"><span class="ncua-btn__label">목록</span></button> | |
| <button type="submit" class="ncua-btn ncua-btn--md ncua-btn--primary js-sgr-save-btn" id="saveBtn"><span class="ncua-btn__label">저장</span></button> | |
| </div> | |
| </header> | |
| <div class="sgr-notification-banner"> | |
| <div class="ncua-full-width-notification ncua-full-width-notification--info" role="alert" style="min-height: 52px; display: flex; align-items: center;"> | |
| <div class="ncua-full-width-notification__container" style="padding: 15px 40px; max-width: none; width: 100%;"> | |
| <div class="ncua-full-width-notification__content"> | |
| <div class="ncua-full-width-notification__content-wrapper"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="none" color="#5720B7" class="ncua-full-width-notification__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 class="ncua-full-width-notification__text-container"> | |
| <span class="ncua-full-width-notification__title">상품 간편 등록에서는 상품 판매에 필요한 핵심 정보만 입력이 가능하며, 세부적인 설정이 필요한 경우 <a href="./goods_register.php" class="sgr-inline-link">상품등록</a>을 이용해주세요.</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Content Area --> | |
| <!-- Block 1: 기본정보 --> | |
| <section class="ncua-card" id="sgr-block-basic"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">기본정보</h3> | |
| <span class="ncua-card__sub-info"><strong>*</strong> 는 필수 입력 항목입니다.</span> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <?php if (!gd_is_provider()): ?> | |
| <tr> | |
| <th><div>공급사구분</div></th> | |
| <td> | |
| <div class="sgr-scm-area"> | |
| <div class="sgr-scm-area__radio sgr-scm-radio-group"> | |
| <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="scmFl" value="n" checked/> | |
| </span> | |
| <span><span class="ncua-radio-field__text">본사</span></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="scmFl" value="y"/> | |
| </span> | |
| <span><span class="ncua-radio-field__text">공급사</span></span> | |
| </label> | |
| </div> | |
| <div class="sgr-scm-area__main" id="scmSelectBox" style="display: none;"> | |
| <div id="scmComboBox"></div> | |
| </div> | |
| </div> | |
| <div id="scmTagArea" class="sgr-scm-area__tags" style="display: none;" aria-live="polite"></div> | |
| </td> | |
| </tr> | |
| <?php endif; ?> | |
| <tr> | |
| <th><div>상품코드</div></th> | |
| <td><div><span class="sgr-readonly-text">저장 시 자동 생성</span></div></td> | |
| </tr> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="001"> | |
| 자체상품코드 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-input-field" style="width: 400px;"> | |
| <div class="ncua-input ncua-input--xs ncua-input--full-width-with-count js-sgr-goodsCd-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="goodsCd" id="goodsCd" class="js-sgr-charcount" data-charcount-key="goodsCd" maxlength="30" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="goodsCd"> | |
| <output class="ncua-input__field-text-count-current">0</output><span>/30</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th class="ncua-required"> | |
| <div data-tooltip-seq="002"> | |
| 상품명 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-input-field" style="width: 520px;"> | |
| <div class="ncua-input ncua-input--xs ncua-input--full-width-with-count js-sgr-goodsNm-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="goodsNm" id="goodsNm" class="js-sgr-charcount" data-charcount-key="goodsNm" placeholder="상품명을 입력하세요." maxlength="250" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="goodsNm"> | |
| <output class="ncua-input__field-text-count-current">0</output><span>/250</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr class="is-tall-row is-visual-last" id="cateInputRow"> | |
| <th> | |
| <div data-tooltip-seq="003"> | |
| 카테고리 선택 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-cate-wrap"> | |
| <div class="sgr-cate-toolbar"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-cate-bulk-btn"><span class="ncua-btn__label">카테고리 일괄선택</span></button> | |
| <?php if (!$isProvider): ?> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray" onclick="window.open('/goods/category_tree.php', '_blank')"><span class="ncua-btn__label">새 카테고리 등록</span></button> | |
| <?php endif; ?> | |
| </div> | |
| <div class="sgr-cate-container"> | |
| <div class="sgr-cate-breadcrumb" id="cateBreadcrumb"> | |
| <span style="color:#A4A5A8">카테고리를 선택하세요</span> | |
| </div> | |
| <div class="sgr-cate-browser" id="categoryBrowser"> | |
| <div class="sgr-cate-column" id="cateCol0"></div> | |
| <div class="sgr-cate-column" id="cateCol1"></div> | |
| <div class="sgr-cate-column" id="cateCol2"></div> | |
| <div class="sgr-cate-column" id="cateCol3"></div> | |
| <div class="sgr-cate-select-btn"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary js-sgr-cate-select-btn"><span class="ncua-btn__label">선택</span></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr class="is-tall-row" id="selectedCateRow" style="display: none;"> | |
| <th> | |
| <div data-tooltip-seq="012"> | |
| 선택된 카테고리 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-cate-wrap"> | |
| <div class="ncua-accordion ncua-accordion--blue"> | |
| <div class="ncua-accordion__summary-style">카테고리 등록 안내</div> | |
| <div class="ncua-accordion__content"> | |
| <div class="ncua-notice-info">카테고리 등록 시 상위카테고리는 자동 등록되며, 등록된 카테고리에 상품이 노출됩니다.</div> | |
| <div class="ncua-notice-info">상품 노출을 원하지 않는 카테고리는 '삭제'버튼을 이용하여 삭제할 수 있습니다.</div> | |
| <div class="ncua-notice-info">등록하신 카테고리들 중 체크된 카테고리가 대표 카테고리로 설정됩니다.</div> | |
| </div> | |
| </div> | |
| <div class="ncua-table ncua-table--horizontal"> | |
| <table> | |
| <colgroup> | |
| <col style="width: 80px;"> | |
| <col style="width: 120px;"> | |
| <col> | |
| <col style="width: 160px;"> | |
| <col style="width: 100px;"> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th><div>대표설정</div></th> | |
| <th><div>노출상점</div></th> | |
| <th><div>카테고리</div></th> | |
| <th><div>카테고리 코드</div></th> | |
| <th><div>연결해제</div></th> | |
| </tr> | |
| </thead> | |
| <tbody id="selectedCateBody"> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr id="cateTopFixRow" style="display: none;"> | |
| <th> | |
| <div data-tooltip-seq="004"> | |
| 카테고리 페이지 상단 고정 | |
| </div> | |
| </th> | |
| <td> | |
| <div> | |
| <div class="ncua-switch ncua-switch--xs"> | |
| <label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--inactive"> | |
| <input class="ncua-switch__radio ncua-switch__radio--left" type="radio" name="cateTopFix" value="y" /> | |
| <span class="ncua-switch__label">사용함</span> | |
| </label> | |
| <label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--active"> | |
| <input class="ncua-switch__radio ncua-switch__radio--right" type="radio" name="cateTopFix" value="n" checked="checked" /> | |
| <span class="ncua-switch__label">사용안함</span> | |
| </label> | |
| </div> | |
| <input type="hidden" name="goodsSortTop" value="n" /> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| <!-- Block 2: 노출/판매 설정 --> | |
| <section class="ncua-card" id="sgr-block-display-sale"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">노출/판매 설정</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="005"> | |
| 노출 상태 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-display-sale-switches"> | |
| <div class="sgr-display-sale-switches__item"> | |
| <span class="sgr-display-sale-switches__label">PC 쇼핑몰</span> | |
| <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" name="goodsDisplayFl" value="y" checked="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" name="goodsDisplayFl" value="n" /> | |
| <span class="ncua-switch__label">노출안함</span> | |
| </label> | |
| </div> | |
| </div> | |
| <?php if ($mobileShopFl === 'y'): ?> | |
| <div class="sgr-display-sale-switches__item"> | |
| <span class="sgr-display-sale-switches__label">모바일 쇼핑몰</span> | |
| <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" name="goodsDisplayMobileFl" value="y" checked="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" name="goodsDisplayMobileFl" value="n" /> | |
| <span class="ncua-switch__label">노출안함</span> | |
| </label> | |
| </div> | |
| </div> | |
| <?php else: ?> | |
| <input type="hidden" name="goodsDisplayMobileFl" value="y" /> | |
| <?php endif; ?> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="006"> | |
| 판매 상태 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-display-sale-switches"> | |
| <div class="sgr-display-sale-switches__item"> | |
| <span class="sgr-display-sale-switches__label">PC 쇼핑몰</span> | |
| <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" name="goodsSellFl" value="y" checked="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" name="goodsSellFl" value="n" /> | |
| <span class="ncua-switch__label">판매안함</span> | |
| </label> | |
| </div> | |
| </div> | |
| <?php if ($mobileShopFl === 'y'): ?> | |
| <div class="sgr-display-sale-switches__item"> | |
| <span class="sgr-display-sale-switches__label">모바일 쇼핑몰</span> | |
| <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" name="goodsSellMobileFl" value="y" checked="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" name="goodsSellMobileFl" value="n" /> | |
| <span class="ncua-switch__label">판매안함</span> | |
| </label> | |
| </div> | |
| </div> | |
| <?php else: ?> | |
| <input type="hidden" name="goodsSellMobileFl" value="y" /> | |
| <?php endif; ?> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| <!-- Block 3: 추가정보 --> | |
| <section class="ncua-card" id="sgr-block-additional"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">추가정보</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <tr> | |
| <th><div>제조사</div></th> | |
| <td> | |
| <div class="sgr-input-field" style="width: 400px;"> | |
| <div class="ncua-input ncua-input--xs ncua-input--full-width-with-count js-sgr-makerNm-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="makerNm" id="makerNm" class="js-sgr-charcount" data-charcount-key="makerNm" placeholder="제조사를 입력하세요." maxlength="30" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="makerNm"> | |
| <output class="ncua-input__field-text-count-current">0</output><span>/30</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th><div>원산지</div></th> | |
| <td> | |
| <div class="sgr-input-field" style="width: 400px;"> | |
| <div class="ncua-input ncua-input--xs ncua-input--full-width-with-count js-sgr-originNm-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="originNm" id="originNm" class="js-sgr-charcount" data-charcount-key="originNm" placeholder="원산지를 입력하세요." maxlength="30" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="originNm"> | |
| <output class="ncua-input__field-text-count-current">0</output><span>/30</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th><div>브랜드</div></th> | |
| <td> | |
| <div class="sgr-brand-field"> | |
| <div class="sgr-input-field"> | |
| <div class="ncua-input ncua-input--xs is-disabled js-sgr-brandCdNm-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="brandCdNm" id="brandCdNm" placeholder="브랜드를 선택해주세요." readonly style="width: 100%;" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-brand-select-btn"> | |
| <span class="ncua-btn__label">선택</span> | |
| </button> | |
| <div id="brandLayer" class="sgr-brand-layer"> | |
| <input type="hidden" name="brandCd" value="" /> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| <!-- Block 4: 판매정보 --> | |
| <section class="ncua-card" id="sgr-block-sale-info"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">판매정보</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <tr> | |
| <th><div>판매가</div></th> | |
| <td> | |
| <div class="sgr-price-field"> | |
| <div class="ncua-input ncua-input--xs" style="width: 120px;"> | |
| <div class="ncua-input__content-wrap"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input type="text" name="goodsPrice" | |
| inputmode="numeric" placeholder="0" | |
| value="" maxlength="9" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="sgr-unit-text">원</span> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th><div>정가</div></th> | |
| <td> | |
| <div class="sgr-price-field"> | |
| <div class="ncua-input ncua-input--xs" style="width: 120px;"> | |
| <div class="ncua-input__content-wrap"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input type="text" name="fixedPrice" | |
| inputmode="numeric" placeholder="0" | |
| value="" maxlength="9" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="sgr-unit-text">원</span> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th><div>판매 재고</div></th> | |
| <td> | |
| <div class="sgr-stock-row"> | |
| <div class="sgr-stock-radio-group"> | |
| <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="stockFl" value="n" checked/> | |
| </span> | |
| <span><span class="ncua-radio-field__text">무한정판매</span></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="stockFl" value="y"/> | |
| </span> | |
| <span><span class="ncua-radio-field__text">재고량에 따름</span></span> | |
| </label> | |
| </div> | |
| <div id="sgr-stock-input-area" class="sgr-stock-input-area" style="display: none;"> | |
| <span class="sgr-stock-detail__label">재고수</span> | |
| <div class="ncua-input ncua-input--xs" style="width: 120px;"> | |
| <div class="ncua-input__content-wrap"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input type="text" name="stockCnt" class="js-number" | |
| inputmode="numeric" pattern="[0-9]*" placeholder="0" | |
| value="" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="sgr-unit-text">개</span> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| <!-- Block 5: 옵션/재고 --> | |
| <section class="ncua-card" id="sgr-block-option-stock"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title" data-tooltip-seq="007">옵션/재고</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <!-- Row 1: 옵션 사용여부 Switch + 보조 버튼 --> | |
| <tr> | |
| <th><div>옵션 사용여부</div></th> | |
| <td> | |
| <div class="sgr-option-switch-row"> | |
| <div class="ncua-switch ncua-switch--xs"> | |
| <label class="ncua-switch__option ncua-switch__option--left ncua-switch__option--inactive"> | |
| <input class="ncua-switch__radio ncua-switch__radio--left" type="radio" name="optionFl" value="y" /> | |
| <span class="ncua-switch__label">사용함</span> | |
| </label> | |
| <label class="ncua-switch__option ncua-switch__option--right ncua-switch__option--active"> | |
| <input class="ncua-switch__radio ncua-switch__radio--right" type="radio" name="optionFl" value="n" checked="checked" /> | |
| <span class="ncua-switch__label">사용안함</span> | |
| </label> | |
| </div> | |
| <div class="sgr-option-helper-buttons" style="display: none;"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-option-load-existing"><span class="ncua-btn__label">기존상품 옵션 불러오기</span></button> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-option-load-frequent"><span class="ncua-btn__label">자주쓰는 옵션 불러오기</span></button> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <!-- Row 2: 옵션 노출 방식 --> | |
| <tr class="sgr-option-sub-row" style="display: none;"> | |
| <th><div>옵션 노출 방식</div></th> | |
| <td> | |
| <div class="sgr-option-radio-group"> | |
| <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="optionY[optionDisplayFl]" value="s" checked /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">일체형</span></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="optionY[optionDisplayFl]" value="d" /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">분리형</span></span> | |
| </label> | |
| </div> | |
| </td> | |
| </tr> | |
| <!-- Row 3: 옵션 이미지 노출 설정 --> | |
| <tr class="sgr-option-sub-row" style="display: none;"> | |
| <th> | |
| <div data-tooltip-seq="008"> | |
| 옵션 이미지 노출 설정 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-option-checkbox-group"> | |
| <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="optionImagePreviewFl" value="y" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">미리보기 사용</span> | |
| </label> | |
| <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="optionImageDisplayFl" value="y" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">상세 이미지에 추가</span> | |
| </label> | |
| </div> | |
| </td> | |
| </tr> | |
| <!-- Row 4: 옵션 등록 --> | |
| <tr class="sgr-option-sub-row" style="display: none;"> | |
| <th><div>옵션 등록</div></th> | |
| <td> | |
| <div class="ncua-accordion ncua-accordion--blue" style="margin-top:8px; margin-bottom:12px;"> | |
| <div class="ncua-accordion__summary-style">옵션 등록 안내</div> | |
| <div class="ncua-accordion__content"> | |
| <div class="ncua-notice-info">옵션명, 옵션값은 한글, 영문 대/소문자, 숫자, 특수문자 등록 가능합니다. (단, - ` ' ' " " 입력 불가)</div> | |
| <div class="ncua-notice-info ncua-notice-info--red">옵션값에 & 특수문자 사용 시, 옵션 이미지가 출력되지 않습니다.</div> | |
| <div class="ncua-notice-info">"직접 업로드와 URL 직접입력" 방식 모두 사용하여 이미지를 등록한 경우 "직접 업로드"된 이미지만 적용됩니다.</div> | |
| <div class="ncua-notice-info">옵션명/옵션값 입력 후에 [옵션 적용] 버튼 클릭 시 옵션의 가격/재고/상태 설정부분이 출력 됩니다.</div> | |
| <div class="ncua-notice-info">옵션 정보 적용 후 옵션을 수정한 경우 [옵션 적용] 버튼을 다시 클릭해야 수정된 정보가 옵션 정보 항목에 적용됩니다.</div> | |
| </div> | |
| </div> | |
| <div id="sgr-option-register-area" class="sgr-option-register-area"> | |
| <div class="sgr-opt-register"> | |
| <div class="sgr-opt-register__header"> | |
| <div class="sgr-opt-register__col-name">옵션명</div> | |
| <div class="sgr-opt-register__col-val"> | |
| <span>옵션값</span> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs has-text" style="margin-left: 8px;"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"><input type="checkbox" id="optImageUrlCheck" /></span> | |
| <span class="ncua-checkbox-field__text">옵션 이미지 URL 직접입력</span> | |
| </label> | |
| </div> | |
| <div class="sgr-opt-register__col-del">삭제</div> | |
| </div> | |
| <div class="sgr-opt-register__row js-opt-name-row"> | |
| <div class="sgr-opt-register__col-name"> | |
| <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 type="text" placeholder="예시) 사이즈" maxlength="30" class="sgr-text-count-input js-opt-name-input" data-charcount-key="sgr-opt-0" /></div></div><div class="ncua-input__field-text-count" data-charcount-text="sgr-opt-0"><output class="ncua-input__field-text-count-current">0</output><span>/30</span></div></div></div> | |
| </div> | |
| <div class="sgr-opt-register__col-val"> | |
| <div class="sgr-opt-vname-card"> | |
| <div class="sgr-opt-vname"> | |
| <div class="sgr-opt-vname-header"> | |
| <div class="sgr-opt-vname-header__col--fill">옵션값</div> | |
| <div class="sgr-opt-vname-header__col--price">옵션가</div> | |
| <div class="sgr-opt-vname-header__col--image">옵션 이미지</div> | |
| <div class="sgr-opt-vname-header__col--url">옵션 이미지 URL</div> | |
| </div> | |
| <div class="sgr-opt-vname-row"> | |
| <div class="sgr-opt-vname-row__val"><div class="ncua-input ncua-input--xs ncua-input--full-width-with-count" style="width:100%;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" placeholder="Enter키로 연속 입력할 수 있습니다. 예시) XL" maxlength="255" class="sgr-text-count-input js-opt-val-input" data-charcount-key="sgr-opt-1" /></div></div><div class="ncua-input__field-text-count" data-charcount-text="sgr-opt-1"><output class="ncua-input__field-text-count-current">0</output><span>/255</span></div></div></div></div> | |
| <div class="sgr-opt-vname-row__price"><div style="display:flex;align-items:center;gap:8px;"><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" inputmode="numeric" placeholder="0" class="js-opt-price-input" /></div></div></div></div><span class="sgr-unit-text">원</span></div></div> | |
| <div class="sgr-opt-vname-row__image"><button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-opt-image-btn"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 15v1.2c0 1.68 0 2.52-.327 3.162a3 3 0 0 1-1.311 1.311C18.72 21 17.88 21 16.2 21H7.8c-1.68 0-2.52 0-3.162-.327a3 3 0 0 1-1.311-1.311C3 18.72 3 17.88 3 16.2V15m14-7-5-5m0 0L7 8m5-5v12"/></svg><span class="ncua-btn__label">파일 찾기</span></button><input type="file" class="no-filestyle js-opt-image-file" accept="image/png,image/jpeg,image/gif,image/bmp,image/svg+xml,image/tiff,image/x-icon,application/postscript" style="display:none" /></div> | |
| <div class="sgr-opt-vname-row__url"><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="이미지 URL을 입력해주세요." class="js-opt-url-input" /></div></div></div></div></div> | |
| <div class="sgr-opt-vname-row__del"><button type="button" class="ncua-btn ncua-btn--xxs only-icon ncua-btn--tertiary-gray js-opt-val-del" disabled><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3h6M3 6h18m-2 0-.701 10.52c-.105 1.578-.158 2.367-.499 2.965a3 3 0 0 1-1.298 1.215c-.62.3-1.41.3-2.993.3h-3.018c-1.582 0-2.373 0-2.993-.3A3 3 0 0 1 6.2 19.485c-.34-.598-.394-1.387-.499-2.966L5 6m5 4.5v5m4-5v5"/></svg></button></div> | |
| </div> | |
| </div> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-opt-val-add"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg><span class="ncua-btn__label">옵션값 추가</span></button> | |
| </div> | |
| </div> | |
| <div class="sgr-opt-register__col-del"> | |
| <button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-opt-name-del" disabled><div class="ncua-minus-icon"></div><span class="ncua-btn__label">삭제</span></button> | |
| </div> | |
| </div> | |
| <div class="sgr-opt-register__footer"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray" id="optNameAddBtn"><span class="ncua-btn__label">추가</span></button> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary" id="optionApplyBtn"><span class="ncua-btn__label">옵션 적용</span></button> | |
| </div> | |
| </div> | |
| <div style="display:flex;justify-content:flex-start;margin-top:8px;"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray" data-modal-open="optSaveFreqModal"><span class="ncua-btn__label">자주쓰는 옵션으로 등록하기</span></button> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div id="sgr-option-info-row" style="display:none;"> | |
| <div class="sgr-option-info-section"> | |
| <div class="sgr-option-info-section__label">옵션 정보</div> | |
| <div class="sgr-option-info-section__content"> | |
| <div class="ncua-accordion ncua-accordion--blue" style="margin-bottom:12px;"> | |
| <div class="ncua-accordion__summary-style">옵션 정보 안내</div> | |
| <div class="ncua-accordion__content"> | |
| <div class="ncua-notice-info">옵션 매입가는 상품 매입가 기준, 옵션가는 상품의 판매가 기준 추가 또는 차감될 옵션별 금액이 있는 경우에만 입력합니다.</div> | |
| <div class="ncua-notice-info ncua-notice-info--red">상품 매입가 및 판매가에 추가될 금액은 양수, 차감될 금액은 음수(마이너스)로 입력 합니다.</div> | |
| <div class="ncua-notice-info">순서조정 버튼을 이용하여 옵션의 순서를 변경할 수 있으며, 설정된 순서대로 쇼핑몰에 노출됩니다.</div> | |
| <div class="ncua-notice-info">옵션추가 버튼을 이용하여 옵션 정보를 초기화하지 않고 추가 생성할 수 있습니다.</div> | |
| <div class="ncua-notice-info">옵션 정보에 출력되는 항목을 [조회항목설정] 버튼을 이용하여 설정할 수 있습니다.</div> | |
| <div class="ncua-notice-info">옵션품절상태의 "정상/품절" 제외 상태와 옵션배송상태는 쇼핑몰에만 적용되며, 네이버 ep/네이버 쇼핑/다음 쇼핑하우 ep 등의 외부연동 시 적용되지 않습니다.</div> | |
| </div> | |
| </div> | |
| <div class="ncua-data-grid sgr-option-grid"> | |
| <div class="ncua-data-grid__action-bar ncua-data-grid__action-bar--top ncua-data-grid__action-bar--space-between sgr-option-action-bar--top"> | |
| <div class="sgr-option-action-bar__left"> | |
| <div class="ncua-button-group ncua-button-group--xs has-border sgr-option-sort-btns" id="sgr-sort-btn-group"> | |
| <button type="button" class="ncua-button-group__item js-sgr-sort-btn" data-direction="top" title="맨 위로" disabled><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.5286 8.19518C7.78894 7.93483 8.21106 7.93483 8.4714 8.19518L11.8047 11.5285C12.0651 11.7889 12.0651 12.211 11.8047 12.4713C11.5444 12.7317 11.1223 12.7317 10.8619 12.4713L8 9.60939L5.13807 12.4713C4.87772 12.7317 4.45561 12.7317 4.19526 12.4713C3.93491 12.211 3.93491 11.7889 4.19526 11.5285L7.5286 8.19518Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7.5286 3.52851C7.78894 3.26816 8.21106 3.26816 8.4714 3.52851L11.8047 6.86185C12.0651 7.1222 12.0651 7.54431 11.8047 7.80466C11.5444 8.06501 11.1223 8.06501 10.8619 7.80466L8 4.94273L5.13807 7.80466C4.87772 8.06501 4.45561 8.06501 4.19526 7.80466C3.93491 7.54431 3.93491 7.1222 4.19526 6.86185L7.5286 3.52851Z" fill="currentColor"/></svg></button> | |
| <button type="button" class="ncua-button-group__item js-sgr-sort-btn" data-direction="up" title="위로" disabled><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M12 10L8 6L4 10" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg></button> | |
| <button type="button" class="ncua-button-group__item js-sgr-sort-btn" data-direction="down" title="아래로" disabled><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg></button> | |
| <button type="button" class="ncua-button-group__item js-sgr-sort-btn" data-direction="bottom" title="맨 아래로" disabled><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.66699 8.66667L8.00033 12L11.3337 8.66667M4.66699 4L8.00033 7.33333L11.3337 4" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg></button> | |
| </div> | |
| </div> | |
| <div class="sgr-option-action-bar__right"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-grid-config-btn"><span class="ncua-btn__label">조회항목설정</span></button> | |
| </div> | |
| </div> | |
| <div class="ncua-data-grid__table"> | |
| <div class="ncua-table ncua-table--in-data-grid ncua-table--horizontal ncua-table--draggable sgr-option-table-container"> | |
| <table class="ncua-table__table sgr-option-table"> | |
| <thead class="ncua-table__header"> | |
| <tr class="ncua-table__row"> | |
| <th class="ncua-table__header-cell ncua-table__checkbox-cell ncua-table__checkbox-cell--header sgr-opt-col-dragcheck"><div><label class="ncua-checkbox-field ncua-checkbox-field--xs" style="margin:0;"><span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"><input type="checkbox" id="sgr-option-select-all" /></span></label></div></th> | |
| <th class="ncua-table__header-cell sgr-opt-col-name js-sgr-opt-col-name1"><div>옵션명1</div></th> | |
| <th class="ncua-table__header-cell sgr-opt-col-name js-sgr-opt-col-name2" style="display:none;"><div>옵션명2</div></th> | |
| <th class="ncua-table__header-cell sgr-opt-col-name js-sgr-opt-col-name3" style="display:none;"><div>옵션명3</div></th> | |
| <th class="ncua-table__header-cell sgr-opt-col-name js-sgr-opt-col-name4" style="display:none;"><div>옵션명4</div></th> | |
| <th class="ncua-table__header-cell sgr-opt-col-name js-sgr-opt-col-name5" style="display:none;"><div>옵션명5</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionCostPrice"><div>옵션매입가</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionPrice"><div>옵션가</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="stockCnt"><div>재고량</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionViewFl"><div>옵션노출상태</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionSellFl"><div>옵션품절상태</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionDeliveryFl"><div>옵션배송상태</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionCode"><div>자체 옵션코드</div></th> | |
| <th class="ncua-table__header-cell" data-grid-key="optionMemo"><div>메모</div></th> | |
| </tr> | |
| <tr class="ncua-table__row sgr-option-table__bulk-row"> | |
| <td colspan="2" class="ncua-table__cell js-sgr-bulk-first-cell"><div class="sgr-option-table__bulk-left"><span class="sgr-option-table__bulk-label">옵션 정보 일괄 적용하기</span><button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-bulk-apply-btn"><span class="ncua-btn__label">적용</span></button></div></td> | |
| <td class="ncua-table__cell js-sgr-bulk-col-name2" style="display:none;"></td> | |
| <td class="ncua-table__cell js-sgr-bulk-col-name3" style="display:none;"></td> | |
| <td class="ncua-table__cell js-sgr-bulk-col-name4" style="display:none;"></td> | |
| <td class="ncua-table__cell js-sgr-bulk-col-name5" style="display:none;"></td> | |
| <td class="ncua-table__cell" data-grid-key="optionCostPrice"><div style="display:flex;align-items:center;gap:8px;"><div class="ncua-input ncua-input--xs" style="width:68px;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" inputmode="numeric" data-field="optionBuyPrice" class="js-sgr-bulk-field" placeholder="0" /></div></div></div></div><span class="sgr-unit-text">원</span></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionPrice"><div style="display:flex;align-items:center;gap:8px;"><div class="ncua-input ncua-input--xs" style="width:68px;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" inputmode="numeric" data-field="optionPrice" class="js-sgr-bulk-field" placeholder="0" /></div></div></div></div><span class="sgr-unit-text">원</span></div></td> | |
| <td class="ncua-table__cell" data-grid-key="stockCnt"><div style="display:flex;align-items:center;gap:8px;"><div class="ncua-input ncua-input--xs" style="width:60px;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" inputmode="numeric" data-field="stockCnt" class="js-number js-sgr-bulk-field" placeholder="0" /></div></div></div></div><span class="sgr-unit-text">개</span></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionViewFl"><div><select class="js-sgr-bulk-field" data-field="optionDisplayFl"><option value="">선택</option><option value="y">노출함</option><option value="n">노출안함</option></select></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionSellFl"><div><select class="js-sgr-bulk-field" data-field="optionSoldoutFl"><option value="">선택</option><?php foreach($stockReason as $k => $v) { ?><option value="<?=$k?>"><?=$v?></option><?php } ?></select></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionDeliveryFl"><div><select class="js-sgr-bulk-field" data-field="optionDeliveryFl"><option value="">선택</option><?php foreach($deliveryReason as $k => $v) { ?><option value="<?=$k?>"><?=$v?></option><?php } ?></select></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionCode"><div style="display:flex;align-items:center;gap:8px;"><div class="ncua-input ncua-input--xs" style="width:85px;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" data-field="optionCode" class="js-sgr-bulk-field" /></div></div></div></div></div></td> | |
| <td class="ncua-table__cell" data-grid-key="optionMemo"><div style="display:flex;align-items:center;gap:8px;"><div class="ncua-input ncua-input--xs" style="width:85px;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" data-field="optionMemo" class="js-sgr-bulk-field" /></div></div></div></div></div></td> | |
| </tr> | |
| </thead> | |
| <tbody class="ncua-table__body" id="sgr-option-table-body"> | |
| <tr class="ncua-table__row sgr-option-table__empty" id="sgr-option-table-empty"><td class="ncua-table__cell" colspan="14"><div>옵션 등록 시 자동 생성됩니다.</div></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="ncua-data-grid__action-bar ncua-data-grid__action-bar--bottom sgr-option-action-bar--bottom"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-sgr-add-option-btn"><span class="ncua-btn__label">옵션 추가</span></button> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--destructive js-sgr-delete-selected-btn" disabled><span class="ncua-btn__label">선택 삭제</span></button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </section> | |
| <!-- Block 6: 상품이미지 --> | |
| <section class="ncua-card" id="sgr-block-image"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">상품이미지</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="009"> | |
| 원본이미지 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-image-upload-area"> | |
| <div id="sgr-image-file-input"></div> | |
| <input type="file" name="image[imageOriginal][]" id="sgrOriginalImageInput" class="no-filestyle" tabindex="-1" aria-hidden="true" /> | |
| </div> | |
| <div class="sgr-auto-resize-wrap"> | |
| <span class="ncua-checkbox-field__support-text">원본이미지 등록 시 상품이미지 사이즈 설정값으로 개별이미지(상세/썸네일/리스트/확대)가 자동 생성됩니다</span> | |
| <span class="ncua-checkbox-field__support-text">최초 상품 이미지 등록 시 <?php if (!$isProvider): ?><a href="../policy/goods_images.php" target="_blank" class="sgr-inline-link">상품 이미지 사이즈 설정</a><?php else: ?>상품 이미지 사이즈 설정<?php endif; ?>에서 이미지 사이즈를 먼저 설정해야 합니다</span> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| <!-- Block 7: 상세정보 --> | |
| <section class="ncua-card" id="sgr-block-detail"> | |
| <header class="ncua-card__header"> | |
| <h3 class="ncua-card__title">상세정보</h3> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="ncua-table ncua-table--vertical"><table> | |
| <tbody> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="010"> | |
| 짧은설명 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="ncua-input ncua-input--xs" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="shortDescription" placeholder="짧은설명을 입력하세요." maxlength="250" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th> | |
| <div>상세설명</div> | |
| </th> | |
| <td> | |
| <div class="sgr-detail-desc"> | |
| <?php if ($mobileShopFl === 'y'): ?> | |
| <div class="sgr-detail-desc__header"> | |
| <div class="ncua-horizontal-tab ncua-horizontal-tab--button-white ncua-horizontal-tab--sm"> | |
| <div class="swiper"> | |
| <div class="swiper-wrapper"> | |
| <div class="swiper-slide ncua-horizontal-tab__item"> | |
| <button type="button" class="ncua-tab-button is-active js-sgr-desc-tab" data-target="pc">PC 쇼핑몰 상세설명</button> | |
| </div> | |
| <div class="swiper-slide ncua-horizontal-tab__item"> | |
| <button type="button" class="ncua-tab-button js-sgr-desc-tab" data-target="mobile">모바일 쇼핑몰 상세설명</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs has-text" style="margin-left: 12px; white-space: nowrap;"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="checkbox" name="goodsDescriptionSameFl" value="y" class="js-sgr-desc-same-checkbox" checked /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">PC/모바일 상세설명 동일사용</span> | |
| </label> | |
| </div> | |
| <?php endif; ?> | |
| <div class="sgr-editor-wrap js-sgr-editor-pc"> | |
| <textarea name="goodsDescription" id="editor" data-godo-editor="basic-editor" data-height-min="300" imageUploadCallback="handleGodoImageUpload(images, editor)"></textarea> | |
| </div> | |
| <?php if ($mobileShopFl === 'y'): ?> | |
| <div class="sgr-editor-wrap js-sgr-editor-mobile" style="display: none;"> | |
| <div class="sgr-editor-overlay js-sgr-editor-overlay" style="display: none;"></div> | |
| <div class="sgr-editor-same-notice js-sgr-editor-same-notice" style="display: none;">저장 시 PC 상세설명이 모바일에 동일 적용됩니다</div> | |
| <textarea name="goodsDescriptionMobile" id="editor2" data-height-min="300" imageUploadCallback="handleGodoImageUpload(images, editor2)"></textarea> | |
| </div> | |
| <?php endif; ?> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th> | |
| <div data-tooltip-seq="011"> | |
| 검색 키워드 | |
| </div> | |
| </th> | |
| <td> | |
| <div class="sgr-search-keyword-row"> | |
| <div class="sgr-search-keyword-input"> | |
| <div class="ncua-input ncua-input--xs" style="width: 100%;"> | |
| <div class="ncua-input__content-wrap" style="width: 100%;"> | |
| <div class="ncua-input__content" style="width: 100%;"> | |
| <div class="ncua-input__field ncua-input__field--xs" style="width: 100%;"> | |
| <input type="text" name="goodsSearchWord" placeholder="콤마(,)로 구분하여 입력" maxlength="250" style="width: 100%;" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs has-text" style="white-space: nowrap;"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="checkbox" name="goodsSearchWordAddGoodsNmFl" value="y" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">상품명을 검색 키워드에 추가</span> | |
| </label> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table></div> | |
| </section> | |
| </section> | |
| </form> | |
| </article> | |
| <script> | |
| $(document).ready(function () { | |
| // 판매가/정가: 일반상품등록과 동일하게 마이너스 입력 허용 (number_only) | |
| $('input[name="goodsPrice"], input[name="fixedPrice"]').number_only(); | |
| <?php if (!gd_is_provider()): ?> | |
| var sgrScmManager = null; | |
| (function initSgrScmComboBox() { | |
| if (typeof initSupplyComboBox === 'undefined') { | |
| setTimeout(initSgrScmComboBox, 100); | |
| return; | |
| } | |
| sgrScmManager = initSupplyComboBox({ | |
| scmLayerSelector: 'scmTagArea', | |
| dataInputNm: 'scmNo', | |
| comboBoxLayerId: 'scmComboBox', | |
| radioGroupClass: 'sgr-scm-radio-group', | |
| apiUrl: '/share/ncds/layer_scm.php', | |
| multiple: false, | |
| tagIdPrefix: 'scm', | |
| tagHiddenName: 'scmCompanyNm', | |
| tagCloseButton: true, | |
| autoHideTagArea: true, | |
| supplyButtonNamesTypes: ['y'] | |
| }); | |
| sgrScmManager.scmDatas.subscribe(function (data) { | |
| if (data.length > 0) { | |
| sgrLoadDefaultDelivery(data[0].id); | |
| } | |
| }); | |
| })(); | |
| $('input[name="scmFl"]').on('change', function () { | |
| var $combo = $('#scmSelectBox'); | |
| var isSupplier = this.value === 'y'; | |
| if (isSupplier) { | |
| $combo.show(); | |
| } else { | |
| $combo.hide(); | |
| if (sgrScmManager) { | |
| sgrScmManager.scmDatas([]); | |
| sgrScmManager.comboBox.setValues([]); | |
| } | |
| $('#scmNoInput').val('<?= DEFAULT_CODE_SCMNO ?>'); | |
| sgrLoadDefaultDelivery('<?= DEFAULT_CODE_SCMNO ?>'); | |
| } | |
| }); | |
| <?php endif; ?> | |
| var sgrDeliveryConfigUrl = '<?= gd_is_provider() === true ? "/provider" : "" ?>/policy/delivery_config.php'; | |
| // 기본 배송비조건이 없을 때 경고 (확인 클릭 시 배송비조건 관리 새 탭 이동) | |
| function sgrShowNoDeliveryWarning() { | |
| NCDSAlert({ | |
| message: '기본 배송비조건이 없어 상품을 등록할 수 없습니다.', | |
| subMessage: '배송비조건 관리에서 먼저 설정해 주세요.', | |
| iconType: 'warning', | |
| callback: function () { | |
| window.open(sgrDeliveryConfigUrl, '_blank'); | |
| } | |
| }); | |
| } | |
| function sgrLoadDefaultDelivery(scmNo) { | |
| if (!scmNo) { | |
| $('input[name="deliverySno"]').val(''); | |
| return; | |
| } | |
| $.post('../policy/delivery_ps.php', {mode: 'search_scm', scmNo: scmNo}, function (data) { | |
| try { | |
| var deliveryData = $.parseJSON(data); | |
| if (deliveryData && deliveryData.sno) { | |
| $('input[name="deliverySno"]').val(deliveryData.sno); | |
| } else { | |
| $('input[name="deliverySno"]').val(''); | |
| } | |
| } catch (e) { | |
| console.error('배송비 정보 파싱 오류:', e); | |
| $('input[name="deliverySno"]').val(''); | |
| } | |
| }).fail(function() { | |
| console.error('배송비 정보를 가져오는데 실패했습니다.'); | |
| $('input[name="deliverySno"]').val(''); | |
| }); | |
| } | |
| <?php if (gd_is_provider()): ?> | |
| sgrLoadDefaultDelivery('<?= gd_htmlspecialchars($scmNo) ?>'); | |
| <?php else: ?> | |
| sgrLoadDefaultDelivery('<?= DEFAULT_CODE_SCMNO ?>'); | |
| <?php endif; ?> | |
| var sgrCharCountManager = null; | |
| (function initSgrCharCount() { | |
| if (typeof createCharCountManager === 'undefined') { | |
| setTimeout(initSgrCharCount, 100); | |
| return; | |
| } | |
| sgrCharCountManager = createCharCountManager({ | |
| targetClasses: ['js-sgr-charcount', 'sgr-text-count-input'], | |
| }); | |
| sgrCharCountManager.init(); | |
| })(); | |
| // 카테고리 Column Browser | |
| (function () { | |
| var CHEVRON_SVG = '<span class="sgr-cate-node__arrow"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"/></svg></span>'; | |
| var SEP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"/></svg>'; | |
| var API_URL = '/share/category_select_json.php'; | |
| var MAX_DEPTH = <?= DEFAULT_DEPTH_CATE ?>; | |
| var MAX_CATE_LEN = <?= DEFAULT_DEPTH_CATE * DEFAULT_LENGTH_CATE ?>; | |
| var SPINNER_DELAY_MS = 300; | |
| var FADE_DURATION_MS = 200; | |
| var ROOT_CATE_DATA = <?= $rootCateList ?>; | |
| var SGR_MALL_DATA = <?= $mallData ?>; | |
| var columns = []; | |
| for (var ci = 0; ci < MAX_DEPTH; ci++) { | |
| columns.push(document.getElementById('cateCol' + ci)); | |
| } | |
| var $breadcrumb = $('#cateBreadcrumb'); | |
| var selectedCateBody = document.getElementById('selectedCateBody'); | |
| var $selectedCateRow = $('#selectedCateRow'); | |
| var $cateInputRow = $('#cateInputRow'); | |
| var selectedNodes = []; | |
| var selectedCategories = []; | |
| var isCurrentLeaf = false; | |
| var leafCache = {}; | |
| function escapeHtml(str) { | |
| var div = document.createElement('div'); | |
| div.appendChild(document.createTextNode(str)); | |
| return div.innerHTML; | |
| } | |
| function isMaxDepth(cateCd) { | |
| return cateCd && cateCd.length >= MAX_CATE_LEN; | |
| } | |
| function hasPotentialChildren(cateCd) { | |
| return !isMaxDepth(cateCd) && !leafCache[cateCd]; | |
| } | |
| function fetchCategories(parentCateCd, callback) { | |
| $.post(API_URL, { mode: 'next_select', value: parentCateCd || '' }, function (data) { | |
| try { | |
| var parsed = data ? (typeof data === 'string' ? JSON.parse(data) : data) : []; | |
| callback(null, parsed); | |
| } catch (e) { | |
| console.error('[category] response parse failed', e); | |
| callback(e, null); | |
| } | |
| }).fail(function (xhr, textStatus, errorThrown) { | |
| console.error('[category] api request failed', textStatus, errorThrown); | |
| callback(errorThrown || textStatus || 'network_error', null); | |
| }); | |
| } | |
| function renderColumn(depth, items) { | |
| var col = columns[depth]; | |
| if (!col) return; | |
| col.innerHTML = ''; | |
| if (!items || items.length === 0) return; | |
| items.forEach(function (item) { | |
| if (item.hasChildren === false) { | |
| leafCache[String(item.optionValue)] = true; | |
| } | |
| }); | |
| items.forEach(function (item) { | |
| var cateCd = String(item.optionValue); | |
| var name = item.optionText; | |
| var flag = item.flag || ''; | |
| var mallName = item.mallName || ''; | |
| var hasChildren = hasPotentialChildren(cateCd); | |
| var isSelected = selectedNodes[depth] && selectedNodes[depth].cateCd === cateCd; | |
| var btn = document.createElement('button'); | |
| btn.type = 'button'; | |
| btn.className = 'sgr-cate-node' + (isSelected ? ' is-selected' : ''); | |
| btn.innerHTML = '<span class="sgr-cate-node__text">' + escapeHtml(name) + '</span>' + (hasChildren ? CHEVRON_SVG : ''); | |
| btn.setAttribute('data-cate-cd', cateCd); | |
| btn.setAttribute('data-flag', flag); | |
| btn.setAttribute('data-mall-name', mallName); | |
| btn.addEventListener('click', function () { | |
| selectNode(depth, cateCd, name, flag, mallName, hasPotentialChildren(cateCd)); | |
| }); | |
| col.appendChild(btn); | |
| }); | |
| } | |
| function markParentAsLeaf(parentDepth, parentCateCd) { | |
| if (!parentCateCd || parentDepth < 0) return; | |
| leafCache[parentCateCd] = true; | |
| var $parentBtn = $(columns[parentDepth]).find('[data-cate-cd="' + parentCateCd + '"]'); | |
| $parentBtn.find('.sgr-cate-node__arrow').remove(); | |
| } | |
| function selectNode(depth, cateCd, name, flag, mallName, hasChildren) { | |
| selectedNodes[depth] = { cateCd: cateCd, name: name, flag: flag, mallName: mallName }; | |
| for (var j = depth + 1; j < MAX_DEPTH; j++) { | |
| selectedNodes[j] = null; | |
| columns[j].innerHTML = ''; | |
| } | |
| $(columns[depth]).find('.sgr-cate-node').removeClass('is-selected'); | |
| $(columns[depth]).find('[data-cate-cd="' + cateCd + '"]').addClass('is-selected'); | |
| updateBreadcrumb(); | |
| if (hasChildren) { | |
| isCurrentLeaf = false; | |
| loadColumn(depth + 1, cateCd); | |
| } else { | |
| isCurrentLeaf = true; | |
| } | |
| } | |
| function loadColumn(depth, parentCateCd) { | |
| if (depth >= MAX_DEPTH) { | |
| isCurrentLeaf = true; | |
| return; | |
| } | |
| var col = columns[depth]; | |
| var spinnerTimer = setTimeout(function () { | |
| col.innerHTML = '<div class="sgr-cate-spinner"><span class="ncua-spinner ncua-spinner--xs" aria-hidden="true"><span class="ncua-spinner__content"></span></span></div>'; | |
| }, SPINNER_DELAY_MS); | |
| fetchCategories(parentCateCd, function (err, items) { | |
| clearTimeout(spinnerTimer); | |
| if (err) { | |
| col.innerHTML = '<div class="sgr-cate-error">' + | |
| '<span>카테고리를 불러올 수 없습니다</span>' + | |
| '<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--tertiary-gray js-sgr-cate-retry" data-depth="' + depth + '" data-parent="' + escapeHtml(parentCateCd) + '"><span class="ncua-btn__label">재시도</span></button>' + | |
| '</div>'; | |
| return; | |
| } | |
| if (items.length === 0) { | |
| isCurrentLeaf = true; | |
| col.innerHTML = ''; | |
| markParentAsLeaf(depth - 1, parentCateCd); | |
| return; | |
| } | |
| renderColumn(depth, items); | |
| }); | |
| } | |
| function updateBreadcrumb() { | |
| var html = ''; | |
| for (var bi = 0; bi < MAX_DEPTH; bi++) { | |
| if (!selectedNodes[bi]) break; | |
| if (bi > 0) html += '<span class="sgr-cate-breadcrumb__sep">' + SEP_SVG + '</span>'; | |
| html += '<span>' + escapeHtml(selectedNodes[bi].name) + '</span>'; | |
| } | |
| $breadcrumb.html(html || '<span style="color:#A4A5A8">카테고리를 선택하세요</span>'); | |
| } | |
| function getLastSelectedNode() { | |
| for (var li = MAX_DEPTH - 1; li >= 0; li--) { | |
| if (selectedNodes[li]) return selectedNodes[li]; | |
| } | |
| return null; | |
| } | |
| $('.js-sgr-cate-select-btn').on('click', function () { | |
| var lastNode = getLastSelectedNode(); | |
| if (!lastNode) return; | |
| var cateCd = lastNode.cateCd; | |
| for (var di = 0; di < selectedCategories.length; di++) { | |
| if (selectedCategories[di].cateCd === cateCd) { | |
| NCDSAlert({ message: '이미 선택된 카테고리입니다.', iconType: 'warning' }); | |
| return; | |
| } | |
| } | |
| var wasEmpty = selectedCategories.length === 0; | |
| for (var pi = 0; pi < selectedNodes.length; pi++) { | |
| if (!selectedNodes[pi]) continue; | |
| var nodeCd = selectedNodes[pi].cateCd; | |
| var alreadyExists = false; | |
| for (var ei = 0; ei < selectedCategories.length; ei++) { | |
| if (selectedCategories[ei].cateCd === nodeCd) { alreadyExists = true; break; } | |
| } | |
| if (alreadyExists) continue; | |
| var pathParts = []; | |
| for (var pp = 0; pp <= pi; pp++) { | |
| if (selectedNodes[pp]) pathParts.push(selectedNodes[pp].name); | |
| } | |
| selectedCategories.push({ | |
| cateCd: nodeCd, | |
| pathText: pathParts.join(' > '), | |
| isRep: wasEmpty && selectedCategories.length === 0, | |
| flag: selectedNodes[pi].flag || '', | |
| mallName: selectedNodes[pi].mallName || '', | |
| parentCateCds: [] | |
| }); | |
| } | |
| renderSelectedTable(); | |
| if (wasEmpty) { | |
| $cateInputRow.removeClass('is-visual-last'); | |
| $selectedCateRow.hide().fadeIn(FADE_DURATION_MS); | |
| $('#cateTopFixRow').hide().fadeIn(FADE_DURATION_MS); | |
| } | |
| }); | |
| function parseCateMalls(cat) { | |
| var flags = cat.flag ? cat.flag.split(',') : []; | |
| var names = cat.mallName ? cat.mallName.split(',') : []; | |
| var malls = []; | |
| for (var mi = 0; mi < flags.length; mi++) { | |
| if (flags[mi]) malls.push({ domainFl: flags[mi], mallName: names[mi] || '' }); | |
| } | |
| return malls.length > 0 ? malls : SGR_MALL_DATA; | |
| } | |
| function renderSelectedTable() { | |
| var $body = $(selectedCateBody); | |
| $body.empty(); | |
| if (selectedCategories.length === 0) return; | |
| selectedCategories.forEach(function (cat) { | |
| var safeCateCd = escapeHtml(cat.cateCd); | |
| var malls = parseCateMalls(cat); | |
| var flagsHtml = ''; | |
| malls.forEach(function (mall) { | |
| flagsHtml += '<span class="js-popover flag flag-16 flag-' + escapeHtml(mall.domainFl) + '" data-content="' + escapeHtml(mall.mallName) + '"></span>'; | |
| }); | |
| var hiddenHtml = | |
| '<input type="hidden" name="link[cateCd][]" value="' + safeCateCd + '" />' + | |
| '<input type="hidden" name="link[cateLinkFl][]" value="y" />'; | |
| var tr = document.createElement('tr'); | |
| tr.id = 'info_category_' + cat.cateCd; | |
| tr.setAttribute('data-cate-cd', cat.cateCd); | |
| tr.innerHTML = | |
| '<td><div><label class="ncua-radio-field ncua-radio-field--xs"><span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input"><input type="radio" name="cateCd" value="' + safeCateCd + '"' + (cat.isRep ? ' checked' : '') + '/></span><span></span></label></div></td>' + | |
| '<td><div>' + flagsHtml + '</div></td>' + | |
| '<td><div class="sgr-cate-path"><span>' + escapeHtml(cat.pathText) + '</span></div></td>' + | |
| '<td><div>' + safeCateCd + '</div></td>' + | |
| '<td><div><button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-sgr-cate-delete"><div class="ncua-minus-icon"></div><span class="ncua-btn__label">삭제</span></button></div>' + | |
| hiddenHtml + '</td>'; | |
| $body.append(tr); | |
| }); | |
| $body.find('.js-popover').popover({trigger: 'hover', container: '#content'}); | |
| } | |
| $(selectedCateBody).on('click', '.js-sgr-cate-delete', function () { | |
| var cateCd = $(this).closest('tr').attr('data-cate-cd'); | |
| var idx = -1; | |
| for (var si = 0; si < selectedCategories.length; si++) { | |
| if (selectedCategories[si].cateCd === cateCd) { idx = si; break; } | |
| } | |
| if (idx === -1) return; | |
| var wasRep = selectedCategories[idx].isRep; | |
| selectedCategories.splice(idx, 1); | |
| if (wasRep && selectedCategories.length > 0) { | |
| selectedCategories[0].isRep = true; | |
| } | |
| var $rows = $(selectedCateBody).find('tr[data-cate-cd="' + cateCd + '"]'); | |
| var rowCount = $rows.length; | |
| var removed = 0; | |
| $rows.fadeOut(FADE_DURATION_MS, function () { | |
| $(this).remove(); | |
| removed++; | |
| if (removed < rowCount) return; | |
| if (wasRep && selectedCategories.length > 0) { | |
| $(selectedCateBody).find('input[name="cateCd"]').first().prop('checked', true); | |
| } | |
| if (selectedCategories.length === 0) { | |
| $selectedCateRow.fadeOut(FADE_DURATION_MS, function () { | |
| $cateInputRow.addClass('is-visual-last'); | |
| }); | |
| $('#cateTopFixRow').fadeOut(FADE_DURATION_MS); | |
| var $nRadio = $('input[name="cateTopFix"][value="n"]'); | |
| if (!$nRadio.is(':checked')) { | |
| $nRadio.prop('checked', true).trigger('change'); | |
| } | |
| } | |
| }); | |
| }); | |
| $(selectedCateBody).on('change', 'input[name="cateCd"]', function () { | |
| var val = $(this).val(); | |
| selectedCategories.forEach(function (cat) { | |
| cat.isRep = (cat.cateCd === val); | |
| }); | |
| }); | |
| $('input[name="cateTopFix"]').on('change', function () { | |
| var $switch = $(this).closest('.ncua-switch'); | |
| $('input[name="goodsSortTop"]').val($(this).val()); | |
| $switch.find('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--active') | |
| .addClass('ncua-switch__option--inactive'); | |
| $switch.find('.ncua-switch__radio:checked').closest('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--inactive') | |
| .addClass('ncua-switch__option--active'); | |
| }); | |
| $('#sgr-block-display-sale .ncua-switch__radio').on('change', function () { | |
| var $switch = $(this).closest('.ncua-switch'); | |
| $switch.find('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--active') | |
| .addClass('ncua-switch__option--inactive'); | |
| $switch.find('.ncua-switch__radio:checked').closest('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--inactive') | |
| .addClass('ncua-switch__option--active'); | |
| }); | |
| window.sgrCategoryBulkCallback = function (resultJson) { | |
| var addedCount = 0; | |
| var dupCount = 0; | |
| $.each(resultJson.info, function (idx, item) { | |
| for (var i = 0; i < selectedCategories.length; i++) { | |
| if (selectedCategories[i].cateCd === item.cateCd) { | |
| dupCount++; | |
| return true; | |
| } | |
| } | |
| var isFirst = (selectedCategories.length === 0); | |
| var pathText = item.cateNm.replace(/>/g, '>'); | |
| selectedCategories.push({ | |
| cateCd: item.cateCd, | |
| pathText: pathText, | |
| isRep: isFirst | |
| }); | |
| addedCount++; | |
| if (isFirst) { | |
| $cateInputRow.removeClass('is-visual-last'); | |
| $selectedCateRow.hide().fadeIn(FADE_DURATION_MS); | |
| $('#cateTopFixRow').hide().fadeIn(FADE_DURATION_MS); | |
| } | |
| }); | |
| renderSelectedTable(); | |
| if (dupCount > 0 && addedCount > 0) { | |
| NCDSAlert({ message: addedCount + '개 카테고리가 추가되었습니다. (중복 ' + dupCount + '개 제외)', iconType: 'warning' }); | |
| } else if (dupCount > 0 && addedCount === 0) { | |
| NCDSAlert({ message: '이미 선택된 카테고리입니다.', iconType: 'warning' }); | |
| } | |
| }; | |
| $('.js-sgr-cate-bulk-btn').on('click', function () { | |
| layer_add_info('category', { | |
| mode: 'search', | |
| layerTitle: '카테고리 일괄선택', | |
| callFunc: 'sgrCategoryBulkCallback' | |
| }); | |
| }); | |
| $('#categoryBrowser').on('click', '.js-sgr-cate-retry', function () { | |
| var depth = parseInt($(this).attr('data-depth')); | |
| var parentCateCd = $(this).attr('data-parent'); | |
| loadColumn(depth, parentCateCd); | |
| }); | |
| if (ROOT_CATE_DATA && ROOT_CATE_DATA.length > 0) { | |
| renderColumn(0, ROOT_CATE_DATA); | |
| } else { | |
| loadColumn(0, ''); | |
| } | |
| })(); | |
| $('#frmSimpleGoods').on('input', '.js-sgr-charcount, .sgr-text-count-input', function () { | |
| var max = parseInt($(this).attr('maxlength')); | |
| $(this).closest('.ncua-input').toggleClass('sgr-count-over', this.value.length >= max); | |
| }); | |
| // 상품명: onBlur 유효성 (highlight만, 얼럿은 submit에서) | |
| $('#goodsNm').on('blur', function () { | |
| var val = this.value.trim(); | |
| if (val.length === 0) { | |
| NCDSValidator.highlight(this); | |
| } else { | |
| clearGoodsNmError(); | |
| } | |
| }); | |
| // 상품명: onInput 시 에러 해제 | |
| $('#goodsNm').on('input', function () { | |
| if (this.value.trim().length > 0) { | |
| clearGoodsNmError(); | |
| } | |
| }); | |
| function validateGoodsNm() { | |
| var val = $('#goodsNm').val().trim(); | |
| if (val.length === 0) { | |
| NCDSValidator.highlight($('#goodsNm')[0]); | |
| NCDSAlert({ message: '상품명을 입력해주세요', iconType: 'error', callback: function () { $('#goodsNm').focus(); } }); | |
| return false; | |
| } | |
| clearGoodsNmError(); | |
| return true; | |
| } | |
| function clearGoodsNmError() { | |
| NCDSValidator.unhighlight($('#goodsNm')[0]); | |
| } | |
| // 상품명 검색 키워드 추가 | |
| $('input[name="goodsSearchWordAddGoodsNmFl"]').on('click', function () { | |
| var goodsNm = $.trim($('input[name="goodsNm"]').val()); | |
| var target = $('input[name="goodsSearchWord"]'); | |
| if (goodsNm.length > 0 && $(this).prop('checked')) { | |
| var maxLength = parseInt(target.attr('maxlength')); | |
| var oldKeyword = $.trim(target.val()); | |
| var newKeyword = oldKeyword ? goodsNm + ',' + oldKeyword : goodsNm; | |
| if (newKeyword.length > maxLength) { | |
| newKeyword = newKeyword.substr(0, maxLength); | |
| } | |
| target.val(newKeyword); | |
| target.trigger('input'); | |
| } | |
| }); | |
| $('.js-sgr-brand-select-btn').on('click', function () { | |
| layer_add_info('brand', {mode: 'radio'}); | |
| }); | |
| // Block 6: 상품이미지 | |
| var SGR_IMAGE_ACCEPT_TYPES = [ | |
| 'image/png', 'image/jpeg', 'image/gif', 'image/bmp', | |
| 'image/svg+xml', 'image/tiff', 'image/x-icon', 'application/postscript' | |
| ]; | |
| var SGR_IMAGE_MAX_SIZE = 10 * 1024 * 1024; | |
| function sgrValidateImageFile(file) { | |
| if (!file) { | |
| return { valid: false, message: '' }; | |
| } | |
| if (SGR_IMAGE_ACCEPT_TYPES.indexOf(file.type) === -1) { | |
| return { valid: false, message: '지원하지 않는 파일 형식입니다' }; | |
| } | |
| if (file.size > SGR_IMAGE_MAX_SIZE) { | |
| return { valid: false, message: '파일 크기는 10MB 이하만 가능합니다' }; | |
| } | |
| return { valid: true, message: '' }; | |
| } | |
| function sgrSetOriginalImageFile(files) { | |
| var fileInput = document.getElementById('sgrOriginalImageInput'); | |
| if (!fileInput) { | |
| return false; | |
| } | |
| try { | |
| var dataTransfer = new DataTransfer(); | |
| if (files && files.length > 0) { | |
| dataTransfer.items.add(files[0]); | |
| } | |
| fileInput.files = dataTransfer.files; | |
| return true; | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| function sgrShowImageHint(message) { | |
| if (message) { | |
| NCDSAlert({ message: message, iconType: 'error' }); | |
| } | |
| } | |
| var sgrImageOnChangeInProgress = false; | |
| if (typeof ncua !== 'undefined' && ncua.ImageFileInput) { | |
| window.sgrImageFileInput = new ncua.ImageFileInput({ | |
| container: 'sgr-image-file-input', | |
| size: 'sm', | |
| buttonLabel: '파일 찾기', | |
| maxFileCount: 1, | |
| accept: SGR_IMAGE_ACCEPT_TYPES.join(', '), | |
| hintItems: ['용량 : 최대 10MB', '권장 파일 형식 : png, jpg, jpeg, gif, bmp, svg'], | |
| disabled: false, | |
| onChange: function (newFiles) { | |
| if (sgrImageOnChangeInProgress) { | |
| return; | |
| } | |
| sgrImageOnChangeInProgress = true; | |
| try { | |
| sgrShowImageHint(''); | |
| if (!newFiles || newFiles.length === 0) { | |
| sgrSetOriginalImageFile([]); | |
| return; | |
| } | |
| var file = newFiles[0]; | |
| var check = sgrValidateImageFile(file); | |
| if (!check.valid) { | |
| sgrShowImageHint(check.message); | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.clearFiles === 'function') { | |
| window.sgrImageFileInput.clearFiles(); | |
| } | |
| sgrSetOriginalImageFile([]); | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.renderImagePreviews === 'function') { | |
| window.sgrImageFileInput.renderImagePreviews(); | |
| } | |
| return; | |
| } | |
| var ok = sgrSetOriginalImageFile([file]); | |
| if (!ok) { | |
| sgrShowImageHint('이미지 업로드에 실패했습니다. 다시 시도해주세요'); | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.clearFiles === 'function') { | |
| window.sgrImageFileInput.clearFiles(); | |
| } | |
| } | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.renderImagePreviews === 'function') { | |
| window.sgrImageFileInput.renderImagePreviews(); | |
| } | |
| } catch (e) { | |
| sgrShowImageHint('이미지 업로드에 실패했습니다. 다시 시도해주세요'); | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.clearFiles === 'function') { | |
| window.sgrImageFileInput.clearFiles(); | |
| } | |
| sgrSetOriginalImageFile([]); | |
| if (window.sgrImageFileInput && typeof window.sgrImageFileInput.renderImagePreviews === 'function') { | |
| window.sgrImageFileInput.renderImagePreviews(); | |
| } | |
| } finally { | |
| sgrImageOnChangeInProgress = false; | |
| } | |
| } | |
| }); | |
| var $protectedFiles = $('#sgr-image-file-input :file, #sgrOriginalImageInput').addClass('no-filestyle'); | |
| if ($protectedFiles.filestyle) { | |
| try { $protectedFiles.filestyle('destroy'); } catch (e) { /* filestyle 미적용 상태면 무시 */ } | |
| } | |
| } | |
| // ─── 상세설명 에디터 (PC/모바일 탭 + Froala) ───────────────── | |
| (function () { | |
| var swiperEl = document.querySelector('.ncua-horizontal-tab--button-white .swiper'); | |
| if (swiperEl && typeof ncua !== 'undefined' && ncua.Tab) { | |
| new ncua.Tab(swiperEl); | |
| } | |
| var mobileEditorInitialized = false; | |
| var $pcWrap = $('.js-sgr-editor-pc'); | |
| var $mobileWrap = $('.js-sgr-editor-mobile'); | |
| var $overlay = $('.js-sgr-editor-overlay'); | |
| var $sameNotice = $('.js-sgr-editor-same-notice'); | |
| var $sameCheckbox = $('.js-sgr-desc-same-checkbox'); | |
| var mobileEditor2 = document.getElementById('editor2'); | |
| // 탭 전환 | |
| $(document).on('click', '.js-sgr-desc-tab', function () { | |
| var target = $(this).data('target'); | |
| $('.js-sgr-desc-tab').removeClass('is-active'); | |
| $(this).addClass('is-active'); | |
| if (target === 'pc') { | |
| $pcWrap.show(); | |
| $mobileWrap.hide(); | |
| } else { | |
| $mobileWrap.show(); | |
| $pcWrap.hide(); | |
| initMobileEditor(); | |
| } | |
| }); | |
| // 모바일 에디터 lazy 초기화 | |
| function initMobileEditor() { | |
| if (mobileEditorInitialized || !mobileEditor2) return; | |
| if (typeof NcdsEditor !== 'undefined') { | |
| var editor = new NcdsEditor(mobileEditor2); | |
| mobileEditor2.ncdsEditor = editor; | |
| mobileEditor2.froalaInstance = editor.froalaInstance || undefined; | |
| mobileEditorInitialized = true; | |
| } | |
| } | |
| // 동일사용 체크박스 | |
| $sameCheckbox.on('change', function () { | |
| if ($(this).is(':checked')) { | |
| $overlay.show(); | |
| $sameNotice.show(); | |
| } else { | |
| $overlay.hide(); | |
| $sameNotice.hide(); | |
| } | |
| }); | |
| // 초기 로드 시 체크 상태 반영 | |
| if ($sameCheckbox.is(':checked')) { | |
| $overlay.show(); | |
| $sameNotice.show(); | |
| } | |
| window.sgrSyncEditors = function () { | |
| var pcEditor = document.getElementById('editor'); | |
| if (pcEditor && pcEditor.ncdsEditor) { | |
| pcEditor.value = pcEditor.ncdsEditor.getHTML(); | |
| } | |
| if (mobileEditorInitialized && mobileEditor2 && mobileEditor2.ncdsEditor) { | |
| mobileEditor2.value = mobileEditor2.ncdsEditor.getHTML(); | |
| } | |
| }; | |
| })(); | |
| var $saveBtn = $('#saveBtn'); | |
| function setSaveBtnLoading() { | |
| if ($saveBtn.find('.ncua-btn__spinner').length === 0) { | |
| $saveBtn.addClass('is-loading is-disable').attr({'disabled': '', 'aria-busy': 'true'}); | |
| $saveBtn.find('.ncua-btn__label').before('<span class="ncua-btn__spinner"></span>'); | |
| } | |
| } | |
| // ─── 옵션/재고 블록 JS ─── | |
| // 옵션 데이터 유무 판단 | |
| function hasOptionData() { | |
| return window.sgrOption && window.sgrOption.hasData(); | |
| } | |
| function showOptionSubRows() { | |
| $('#sgr-block-option-stock .sgr-option-sub-row').show(); | |
| $('#sgr-block-option-stock .sgr-option-helper-buttons').show(); | |
| } | |
| function hideOptionSubRows() { | |
| $('#sgr-block-option-stock .sgr-option-sub-row').hide(); | |
| $('#sgr-block-option-stock .sgr-option-helper-buttons').hide(); | |
| } | |
| function setOptionFlSwitchActive(value) { | |
| var $switch = $('#sgr-block-option-stock input[name="optionFl"]').first().closest('.ncua-switch'); | |
| $switch.find('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--active') | |
| .addClass('ncua-switch__option--inactive'); | |
| var $target = (value === 'y') | |
| ? $switch.find('.ncua-switch__option--left') | |
| : $switch.find('.ncua-switch__option--right'); | |
| $target.removeClass('ncua-switch__option--inactive') | |
| .addClass('ncua-switch__option--active'); | |
| $switch.find('.ncua-switch__radio[value="' + value + '"]').prop('checked', true); | |
| } | |
| $('#sgr-block-option-stock .ncua-switch__radio').on('change', function () { | |
| var value = $(this).val(); | |
| var $switch = $(this).closest('.ncua-switch'); | |
| $switch.find('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--active') | |
| .addClass('ncua-switch__option--inactive'); | |
| $switch.find('.ncua-switch__radio:checked').closest('.ncua-switch__option') | |
| .removeClass('ncua-switch__option--inactive') | |
| .addClass('ncua-switch__option--active'); | |
| if (value === 'y') { | |
| showOptionSubRows(); | |
| } else { | |
| if (hasOptionData()) { | |
| NCDSConfirm({ | |
| message: '옵션 설정 변경', | |
| subMessage: '옵션 설정을 사용안함으로 변경하면 입력한 옵션 정보가 초기화됩니다. 변경하시겠습니까?', | |
| btnText: { confirmLabel: '변경', cancelLabel: '취소' }, | |
| callback: function (result) { | |
| if (result) { | |
| hideOptionSubRows(); | |
| if (window.sgrOption) window.sgrOption.reset(); | |
| $('input[name="stockCnt"]').prop('disabled', false).closest('.ncua-input').removeClass('is-disabled'); | |
| } else { | |
| setOptionFlSwitchActive('y'); | |
| } | |
| } | |
| }); | |
| } else { | |
| hideOptionSubRows(); | |
| $('input[name="stockCnt"]').prop('disabled', false).closest('.ncua-input').removeClass('is-disabled'); | |
| } | |
| } | |
| }); | |
| $('#sgr-block-sale-info input[name="stockFl"]').on('change', function () { | |
| if ($(this).val() === 'y') { | |
| $('#sgr-stock-input-area').show(); | |
| } else { | |
| $('#sgr-stock-input-area').hide(); | |
| } | |
| }); | |
| // ─── Block 5: 옵션 등록 ────────────────────────────────────────────── | |
| (function () { | |
| var MAX_OPT_NAMES = 5; | |
| var $optRegister = $('.sgr-opt-register'); | |
| window.sgrOption = { | |
| combinations: [], | |
| loadedStockMap: {}, | |
| hasData: function () { | |
| var found = false; | |
| $optRegister.find('.js-opt-name-row').each(function () { | |
| if ($(this).find('.js-opt-name-input').val().trim()) { found = true; return false; } | |
| $(this).find('.js-opt-val-input').each(function () { | |
| if ($(this).val().trim()) { found = true; return false; } | |
| }); | |
| if (found) return false; | |
| }); | |
| return found; | |
| }, | |
| reset: function () { | |
| var $rows = $optRegister.find('.js-opt-name-row'); | |
| $rows.slice(1).remove(); | |
| var $first = $rows.first(); | |
| $first.find('.js-opt-name-input').val(''); | |
| var $vname = $first.find('.sgr-opt-vname'); | |
| $vname.find('.sgr-opt-vname-row').slice(1).remove(); | |
| $vname.find('.js-opt-val-input').val(''); | |
| $vname.find('.js-opt-price-input').val(''); | |
| $vname.find('.js-opt-url-input').val(''); | |
| $vname.find('.js-opt-image-file').val(''); | |
| $vname.find('.js-opt-image-tag-wrap').remove(); | |
| $first.find('.js-opt-name-del').prop('disabled', true); | |
| updateTextCounters($first); | |
| updateOptNameButtons(); | |
| window.sgrOption.combinations = []; | |
| window.sgrOption.loadedStockMap = {}; | |
| $('#sgr-option-info-row').hide(); | |
| $('.sgr-option-table__bulk-row .js-sgr-bulk-field').val(''); | |
| } | |
| }; | |
| function updateTextCounters($scope) { | |
| ($scope || $optRegister).find('.sgr-text-count-input').each(function () { | |
| if (sgrCharCountManager) sgrCharCountManager.update(this); | |
| }); | |
| } | |
| $optRegister.find('.js-opt-price-input').number_only(); | |
| $('.sgr-option-table__bulk-row [data-field="optionBuyPrice"], .sgr-option-table__bulk-row [data-field="optionPrice"]').number_only(); | |
| var sgrCharcountSeq = 2; | |
| // 옵션값 row HTML 생성 | |
| function createValRowHtml(isFirst) { | |
| var seq = ++sgrCharcountSeq; | |
| var urlActive = $('#optImageUrlCheck').is(':checked'); | |
| return '<div class="sgr-opt-vname-row">' + | |
| '<div class="sgr-opt-vname-row__val"><div class="ncua-input ncua-input--xs ncua-input--full-width-with-count" style="width:100%;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" placeholder="Enter키로 연속 입력할 수 있습니다. 예시) XL" maxlength="255" class="sgr-text-count-input js-opt-val-input" data-charcount-key="sgr-opt-' + seq + '" /></div></div><div class="ncua-input__field-text-count" data-charcount-text="sgr-opt-' + seq + '"><output class="ncua-input__field-text-count-current">0</output><span>/255</span></div></div></div></div>' + | |
| '<div class="sgr-opt-vname-row__price"><div style="display:flex;align-items:center;gap:8px;"><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" inputmode="numeric" placeholder="0" class="js-opt-price-input" /></div></div></div></div><span class="sgr-unit-text">원</span></div></div>' + | |
| '<div class="sgr-opt-vname-row__image"><button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-opt-image-btn"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 15v1.2c0 1.68 0 2.52-.327 3.162a3 3 0 0 1-1.311 1.311C18.72 21 17.88 21 16.2 21H7.8c-1.68 0-2.52 0-3.162-.327a3 3 0 0 1-1.311-1.311C3 18.72 3 17.88 3 16.2V15m14-7-5-5m0 0L7 8m5-5v12"/></svg><span class="ncua-btn__label">파일 찾기</span></button><input type="file" class="no-filestyle js-opt-image-file" accept="image/png,image/jpeg,image/gif,image/bmp,image/svg+xml,image/tiff,image/x-icon,application/postscript" style="display:none" /></div>' + | |
| '<div class="sgr-opt-vname-row__url"><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="이미지 URL을 입력해주세요." class="js-opt-url-input" /></div></div></div></div></div>' + | |
| '<div class="sgr-opt-vname-row__del"><button type="button" class="ncua-btn ncua-btn--xxs only-icon ncua-btn--tertiary-gray js-opt-val-del"' + (isFirst ? ' disabled' : '') + '><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3h6M3 6h18m-2 0-.701 10.52c-.105 1.578-.158 2.367-.499 2.965a3 3 0 0 1-1.298 1.215c-.62.3-1.41.3-2.993.3h-3.018c-1.582 0-2.373 0-2.993-.3A3 3 0 0 1 6.2 19.485c-.34-.598-.394-1.387-.499-2.966L5 6m5 4.5v5m4-5v5"/></svg></button></div>' + | |
| '</div>'; | |
| } | |
| // 옵션명 카드 row HTML 생성 | |
| function createNameRowHtml() { | |
| var nameSeq = ++sgrCharcountSeq; | |
| var urlActive = $('#optImageUrlCheck').is(':checked'); | |
| return '<div class="sgr-opt-register__row js-opt-name-row">' + | |
| '<div class="sgr-opt-register__col-name">' + | |
| '<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 type="text" placeholder="예시) 사이즈" maxlength="30" class="sgr-text-count-input js-opt-name-input" data-charcount-key="sgr-opt-' + nameSeq + '" /></div></div><div class="ncua-input__field-text-count" data-charcount-text="sgr-opt-' + nameSeq + '"><output class="ncua-input__field-text-count-current">0</output><span>/30</span></div></div></div>' + | |
| '</div>' + | |
| '<div class="sgr-opt-register__col-val">' + | |
| '<div class="sgr-opt-vname-card' + (urlActive ? ' is-url-active' : '') + '">' + | |
| '<div class="sgr-opt-vname">' + | |
| '<div class="sgr-opt-vname-header">' + | |
| '<div class="sgr-opt-vname-header__col--fill">옵션값</div>' + | |
| '<div class="sgr-opt-vname-header__col--price">옵션가</div>' + | |
| '<div class="sgr-opt-vname-header__col--image">옵션 이미지</div>' + | |
| '<div class="sgr-opt-vname-header__col--url">옵션 이미지 URL</div>' + | |
| '</div>' + | |
| createValRowHtml(true) + | |
| '</div>' + | |
| '<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-opt-val-add"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg><span class="ncua-btn__label">옵션값 추가</span></button>' + | |
| '</div>' + | |
| '</div>' + | |
| '<div class="sgr-opt-register__col-del">' + | |
| '<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray js-opt-name-del"><div class="ncua-minus-icon"></div><span class="ncua-btn__label">삭제</span></button>' + | |
| '</div>' + | |
| '</div>'; | |
| } | |
| function updateOptNameButtons() { | |
| var $rows = $optRegister.find('.js-opt-name-row'); | |
| var count = $rows.length; | |
| $('#optNameAddBtn').prop('disabled', count >= MAX_OPT_NAMES); | |
| $rows.find('.js-opt-name-del').prop('disabled', count <= 1); | |
| } | |
| function updateOptImageVisibility() { | |
| $optRegister.find('.js-opt-name-row').each(function (idx) { | |
| $(this).find('.sgr-opt-vname-card').toggleClass('is-image-hidden', idx > 0); | |
| }); | |
| } | |
| // 옵션값/옵션명 Enter | |
| var lastOptEnter = 0; | |
| $optRegister.on('keydown', '.js-opt-val-input, .js-opt-name-input', function (e) { | |
| if (e.key === 'Enter' || e.keyCode === 13) e.preventDefault(); | |
| }); | |
| $optRegister.on('keyup', '.js-opt-val-input', function (e) { | |
| if (e.key !== 'Enter') return; | |
| var now = Date.now(); | |
| if (now - lastOptEnter < 100) return; | |
| lastOptEnter = now; | |
| var $card = $(this).closest('.sgr-opt-vname-card'); | |
| var $vname = $card.find('.sgr-opt-vname'); | |
| var $newRow = $(createValRowHtml(false)); | |
| $vname.append($newRow); | |
| $newRow.find('.js-opt-price-input').number_only(); | |
| $newRow.find('.js-opt-val-input').focus(); | |
| }); | |
| // 옵션명 Enter → 옵션값 첫 행 포커스 | |
| $optRegister.on('keyup', '.js-opt-name-input', function (e) { | |
| if (e.key !== 'Enter') return; | |
| var now = Date.now(); | |
| if (now - lastOptEnter < 100) return; | |
| lastOptEnter = now; | |
| $(this).closest('.js-opt-name-row').find('.js-opt-val-input').first().focus(); | |
| }); | |
| // 정규 상품등록(option_value_check)과 동일하게 동일 옵션명 내 옵션값 중복 차단 | |
| $optRegister.on('blur', '.js-opt-val-input', function () { | |
| var $input = $(this); | |
| var val = ($input.val() || '').trim(); | |
| if (val === '') return; | |
| var isDup = false; | |
| $input.closest('.js-opt-name-row').find('.js-opt-val-input').each(function () { | |
| if (this === $input[0]) return true; | |
| if (($(this).val() || '').trim() === val) { | |
| isDup = true; | |
| return false; | |
| } | |
| }); | |
| if (isDup) { | |
| NCDSAlert({ message: '현재 입력한 옵션값과 동일한 옵션값이 존재합니다.', iconType: 'error', callback: function () { $input.val('').focus(); } }); | |
| } | |
| }); | |
| // + 옵션값 추가 버튼 | |
| $optRegister.on('click', '.js-opt-val-add', function () { | |
| var $vname = $(this).closest('.sgr-opt-vname-card').find('.sgr-opt-vname'); | |
| var $newRow = $(createValRowHtml(false)); | |
| $vname.append($newRow); | |
| $newRow.find('.js-opt-price-input').number_only(); | |
| $newRow.find('.js-opt-val-input').focus(); | |
| }); | |
| // 옵션값 행 삭제 | |
| $optRegister.on('click', '.js-opt-val-del', function () { | |
| $(this).closest('.sgr-opt-vname-row').remove(); | |
| }); | |
| // Footer 추가 → 옵션명 카드 추가 | |
| $('#optNameAddBtn').on('click', function () { | |
| var $newRow = $(createNameRowHtml()); | |
| $optRegister.find('.sgr-opt-register__footer').before($newRow); | |
| $newRow.find('.js-opt-price-input').number_only(); | |
| updateOptNameButtons(); | |
| updateOptImageVisibility(); | |
| $newRow.find('.js-opt-name-input').focus(); | |
| }); | |
| // 옵션명 카드 삭제 | |
| $optRegister.on('click', '.js-opt-name-del', function () { | |
| $(this).closest('.js-opt-name-row').remove(); | |
| updateOptNameButtons(); | |
| updateOptImageVisibility(); | |
| }); | |
| // 옵션 이미지 URL 체크박스 토글 | |
| $('#optImageUrlCheck').on('change', function () { | |
| var active = this.checked; | |
| $optRegister.find('.sgr-opt-vname-card').toggleClass('is-url-active', active); | |
| }); | |
| // 옵션 이미지 파일 찾기 버튼 → hidden file input 트리거 | |
| $optRegister.on('click', '.js-opt-image-btn', function () { | |
| $(this).closest('.sgr-opt-vname-row__image').find('.js-opt-image-file').trigger('click'); | |
| }); | |
| // 옵션 이미지 파일 선택 시 → 버튼 유지 + 버튼 아래에 파일명 Tag 노출 | |
| $optRegister.on('change', '.js-opt-image-file', function () { | |
| var $imageDiv = $(this).closest('.sgr-opt-vname-row__image'); | |
| if (this.files && this.files[0]) { | |
| var file = this.files[0]; | |
| var check = sgrValidateImageFile(file); | |
| if (!check.valid) { | |
| NCDSAlert({ message: check.message, iconType: 'error' }); | |
| $(this).val(''); | |
| return; | |
| } | |
| var fileName = file.name; | |
| // 기존 Tag 제거 후 버튼 아래에 새 Tag 추가 (버튼은 유지) | |
| $imageDiv.find('.js-opt-image-tag-wrap').remove(); | |
| $imageDiv.append( | |
| '<div class="js-opt-image-tag-wrap" style="margin-top:4px;">' + | |
| '<span class="ncua-tag ncua-tag--sm" style="max-width:100%;display:inline-flex;align-items:center;">' + | |
| '<span class="ncua-tag__text" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escHtml(fileName) + '</span>' + | |
| '<button type="button" class="ncua-tag__close js-opt-image-remove"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6 6 18M6 6l12 12"/></svg></button>' + | |
| '</span></div>' | |
| ); | |
| } | |
| }); | |
| // 옵션 이미지 Tag × 버튼 → 이미지 제거 | |
| $optRegister.on('click', '.js-opt-image-remove', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| var $imageDiv = $(this).closest('.sgr-opt-vname-row__image'); | |
| $imageDiv.find('.js-opt-image-file').val(''); | |
| $imageDiv.find('.js-opt-image-tag-wrap').remove(); | |
| }); | |
| // 옵션 적용 버튼 | |
| function collectOptionGroups() { | |
| var groups = []; | |
| $optRegister.find('.js-opt-name-row').each(function () { | |
| var name = $(this).find('.js-opt-name-input').val().trim(); | |
| var values = []; | |
| $(this).find('.sgr-opt-vname-row').each(function () { | |
| var val = $(this).find('.js-opt-val-input').val().trim(); | |
| var price = $(this).find('.js-opt-price-input').val().trim(); | |
| var url = $(this).find('.js-opt-url-input').val().trim(); | |
| if (val) { | |
| var fileInput = $(this).find('.js-opt-image-file')[0]; | |
| var hasImage = fileInput && fileInput.files && fileInput.files.length > 0; | |
| var existingImage = $(this).find('.js-opt-existing-image').val() || ''; | |
| values.push({ val: val, price: price || '0', image: hasImage ? fileInput.files[0].name : existingImage, url: url }); | |
| } | |
| }); | |
| if (values.length > 0) { | |
| groups.push({ name: name, values: values }); | |
| } | |
| }); | |
| return groups; | |
| } | |
| function applyOptionCombinations(groups, fresh) { | |
| var newCombinations = computeCombinations(groups); | |
| window.sgrOption.combinations = newCombinations; | |
| $('#sgr-option-info-row').show(); | |
| document.dispatchEvent(new CustomEvent('sgr:option-combinations-updated', { detail: { fresh: !!fresh } })); | |
| NCDSAlert({ message: '옵션 조합이 갱신되었습니다.', iconType: 'success' }); | |
| } | |
| $('#optionApplyBtn').on('click', function () { | |
| var forbiddenOptChars = ['`', '‘', '’', '“', '”', "'", '"']; | |
| var hasForbiddenOptChar = false; | |
| $optRegister.find('.js-opt-name-input, .js-opt-val-input').each(function () { | |
| var val = $(this).val() || ''; | |
| for (var ci = 0; ci < forbiddenOptChars.length; ci++) { | |
| if (val.indexOf(forbiddenOptChars[ci]) !== -1) { | |
| hasForbiddenOptChar = true; | |
| return false; | |
| } | |
| } | |
| }); | |
| if (hasForbiddenOptChar) { | |
| NCDSAlert({ message: '옵션명/옵션값에 사용할 수 없는 문자가 있습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| var groups = collectOptionGroups(); | |
| // 빈 옵션값 | |
| if (groups.length === 0) { | |
| NCDSAlert({ message: '옵션값을 1개 이상 입력해주세요.', iconType: 'error' }); | |
| return; | |
| } | |
| // 옵션값은 입력됐으나 옵션명이 비면 옵션명 공란 옵션이 저장되므로 차단 | |
| if (groups.some(function (g) { return !g.name; })) { | |
| NCDSAlert({ message: '옵션명을 입력해 주세요!', iconType: 'error' }); | |
| return; | |
| } | |
| var comboCount = countOptionCombinations(groups); | |
| if (comboCount > <?=DEFAULT_LIMIT_OPTION_COMBINATION;?>) { | |
| NCDSAlert({ message: '옵션의 조합은 <?=DEFAULT_LIMIT_OPTION_COMBINATION;?>개 이하로 가능합니다.', iconType: 'error' }); | |
| return; | |
| } | |
| function proceedApply() { | |
| // 기존 조합이 있으면 Confirm — 확인 시 기존 옵션 정보는 전부 초기화(fresh) | |
| if (window.sgrOption.combinations.length > 0) { | |
| NCDSConfirm({ | |
| message: '옵션 재적용', | |
| subMessage: '옵션 정보 재적용 시 기존 옵션 정보는 삭제됩니다. 현재 등록된 옵션 정보로 조합하여 재적용 하시겠습니까?', | |
| callback: function (result) { | |
| if (result) { | |
| applyOptionCombinations(groups, true); | |
| } | |
| } | |
| }); | |
| return; | |
| } | |
| applyOptionCombinations(groups, true); | |
| } | |
| // 옵션 조합 개수 경고 후 계속 진행 (일반등록 동일 정책) | |
| if (comboCount > <?=DEFAULT_LIMIT_OPTION_VALUE;?>) { | |
| NCDSConfirm({ | |
| message: '옵션 조합 확인', | |
| subMessage: '옵션값 개수가 ' + comboCount + '개 입니다. 옵션이 <?=DEFAULT_LIMIT_OPTION_VALUE;?>개 이상이 되면 너무 많아 작성이 힘들어 지거나 느려질 수 있습니다. 계속 옵션 작성 하시겠습니까?', | |
| callback: function (result) { | |
| if (result) { | |
| proceedApply(); | |
| } | |
| } | |
| }); | |
| return; | |
| } | |
| proceedApply(); | |
| }); | |
| function countOptionCombinations(groups) { | |
| var nonEmpty = groups.map(function (g) { | |
| return g.values.filter(function (v) { return v.val && v.val.trim().length > 0; }); | |
| }).filter(function (vals) { return vals.length > 0; }); | |
| if (nonEmpty.length === 0) return 0; | |
| return nonEmpty.reduce(function (acc, vals) { return acc * vals.length; }, 1); | |
| } | |
| function computeCombinations(groups) { | |
| var nonEmpty = groups.map(function (g) { | |
| return g.values.filter(function (v) { return v.val && v.val.trim().length > 0; }); | |
| }).filter(function (vals) { return vals.length > 0; }); | |
| if (nonEmpty.length === 0) return []; | |
| return nonEmpty.reduce(function (acc, vals) { | |
| var next = []; | |
| acc.forEach(function (prefix) { | |
| vals.forEach(function (v) { | |
| next.push(prefix.concat([{ val: v.val, price: parseInt(v.price, 10) || 0 }])); | |
| }); | |
| }); | |
| return next; | |
| }, [[]]).map(function (combo) { | |
| var totalPrice = 0; | |
| var values = []; | |
| combo.forEach(function (item) { | |
| values.push(item.val); | |
| totalPrice += item.price; | |
| }); | |
| return { | |
| key: values.join('|'), | |
| values: values, | |
| optionPurchasePrice: 0, | |
| optionPrice: totalPrice, | |
| stockCnt: 0, | |
| displayFl: 'y', | |
| soldOutFl: 'y', | |
| deliveryFl: 'normal', | |
| selfOptionCd: '', | |
| memo: '' | |
| }; | |
| }); | |
| } | |
| updateOptNameButtons(); | |
| var SGR_STOCK_REASON = <?= json_encode($stockReason, JSON_UNESCAPED_UNICODE) ?>; | |
| var SGR_DELIVERY_REASON = <?= json_encode($deliveryReason, JSON_UNESCAPED_UNICODE) ?>; | |
| var $optTableBody = $('#sgr-option-table-body'); | |
| var $optTableEmpty = $('#sgr-option-table-empty'); | |
| function escHtml(s) { | |
| return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
| } | |
| function getDragHandleSvg() { | |
| return '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" r="1" fill="currentColor"/><circle cx="9.5" cy="2.5" r="1" fill="currentColor"/><circle cx="4.5" cy="7" r="1" fill="currentColor"/><circle cx="9.5" cy="7" r="1" fill="currentColor"/><circle cx="4.5" cy="11.5" r="1" fill="currentColor"/><circle cx="9.5" cy="11.5" r="1" fill="currentColor"/></svg>'; | |
| } | |
| function normalizeDisplayFl(val) { | |
| if (val === 'y' || val === 'n') return val; | |
| return val === '노출안함' ? 'n' : 'y'; | |
| } | |
| function normalizeSoldoutFl(val) { | |
| if (SGR_STOCK_REASON[val] !== undefined) return val; | |
| return 'y'; | |
| } | |
| function normalizeDeliveryFl(val) { | |
| if (SGR_DELIVERY_REASON[val] !== undefined) return val; | |
| return 'normal'; | |
| } | |
| function buildSelectHtml(field, value, options) { | |
| var html = '<select data-field="' + field + '">'; | |
| options.forEach(function(opt) { | |
| html += '<option value="' + opt.val + '"' + (value === opt.val ? ' selected' : '') + '>' + opt.label + '</option>'; | |
| }); | |
| html += '</select>'; | |
| return html; | |
| } | |
| function buildReasonSelectHtml(field, value, reasonMap) { | |
| var html = '<select data-field="' + field + '">'; | |
| Object.keys(reasonMap).forEach(function(key) { | |
| html += '<option value="' + escHtml(key) + '"' + (value === key ? ' selected' : '') + '>' + escHtml(reasonMap[key]) + '</option>'; | |
| }); | |
| html += '</select>'; | |
| return html; | |
| } | |
| function buildInputHtml(field, value) { | |
| return '<div class="ncua-input ncua-input--xs" style="width:100%;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" data-field="' + field + '" value="' + escHtml(String(value)) + '" /></div></div></div></div>'; | |
| } | |
| function buildNumericInputHtml(field, value, width, extraClass) { | |
| var classAttr = extraClass ? ' class="' + extraClass + '"' : ''; | |
| return '<div class="ncua-input ncua-input--xs" style="width:' + width + ';"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" inputmode="numeric" data-field="' + field + '" value="' + escHtml(String(value)) + '"' + classAttr + ' /></div></div></div></div>'; | |
| } | |
| function getActiveGroups() { | |
| var groups = []; | |
| $optRegister.find('.js-opt-name-row').each(function() { | |
| var name = $(this).find('.js-opt-name-input').val().trim(); | |
| var hasVals = false; | |
| $(this).find('.js-opt-val-input').each(function() { | |
| if ($(this).val().trim()) { hasVals = true; return false; } | |
| }); | |
| if (hasVals) groups.push({ name: name }); | |
| }); | |
| return groups; | |
| } | |
| function updateTableHeader() { | |
| var groups = getActiveGroups(); | |
| var count = groups.length; | |
| for (var i = 1; i <= 5; i++) { | |
| if (i <= count) { | |
| $('th.js-sgr-opt-col-name' + i).find('div').text(groups[i - 1].name || ('옵션명' + i)); | |
| $('th.js-sgr-opt-col-name' + i + ', td.js-sgr-data-col-name' + i).show(); | |
| } else { | |
| $('th.js-sgr-opt-col-name' + i + ', td.js-sgr-data-col-name' + i).hide(); | |
| } | |
| } | |
| $('.js-sgr-bulk-first-cell').attr('colspan', 1 + count); | |
| } | |
| function buildOptionNameCells(values, groupCount) { | |
| var cells = ''; | |
| for (var i = 0; i < 5; i++) { | |
| var isVisible = i < groupCount; | |
| cells += '<td class="ncua-table__cell sgr-opt-col-name js-sgr-data-col-name' + (i + 1) + '"' + | |
| (!isVisible ? ' style="display:none;"' : '') + | |
| '><div>' + escHtml(values[i] || '') + '</div></td>'; | |
| } | |
| return cells; | |
| } | |
| function buildCombinationRow(combo, saved) { | |
| var s = saved || {}; | |
| var displayFlVal = normalizeDisplayFl(s.optionDisplayFl !== undefined ? s.optionDisplayFl : combo.displayFl); | |
| var soldoutFlVal = normalizeSoldoutFl(s.optionSoldoutFl !== undefined ? s.optionSoldoutFl : combo.soldOutFl); | |
| var deliveryFlVal = normalizeDeliveryFl(s.optionDeliveryFl !== undefined ? s.optionDeliveryFl : combo.deliveryFl); | |
| var groupCount = getActiveGroups().length; | |
| var html = '<tr class="ncua-table__row sgr-option-table__row" data-combination-key="' + escHtml(combo.key) + '">' + | |
| '<td class="ncua-table__cell ncua-table__checkbox-cell sgr-opt-col-dragcheck"><div><span class="ncua-table__drag-handle sgr-option-drag-handle js-sgr-drag-handle" aria-hidden="true">' + getDragHandleSvg() + '</span>' + | |
| '<label class="ncua-checkbox-field ncua-checkbox-field--xs" style="margin:0;">' + | |
| '<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">' + | |
| '<input type="checkbox" class="js-sgr-row-check" />' + | |
| '</span></label></div></td>' + | |
| buildOptionNameCells(combo.values, groupCount) + | |
| '<td class="ncua-table__cell" data-grid-key="optionCostPrice"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('optionBuyPrice', s.optionBuyPrice !== undefined ? s.optionBuyPrice : (combo.optionPurchasePrice || ''), '68px', '') + | |
| '<span class="sgr-unit-text">원</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionPrice"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('optionPrice', s.optionPrice !== undefined ? s.optionPrice : (combo.optionPrice || ''), '68px', '') + | |
| '<span class="sgr-unit-text">원</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="stockCnt"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('stockCnt', s.stockCnt !== undefined ? s.stockCnt : (combo.stockCnt || ''), '60px', 'js-number js-sgr-stock-input') + | |
| '<span class="sgr-unit-text">개</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionViewFl"><div>' + | |
| buildSelectHtml('optionDisplayFl', displayFlVal, [{ val: 'y', label: '노출함' }, { val: 'n', label: '노출안함' }]) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionSellFl"><div>' + | |
| buildReasonSelectHtml('optionSoldoutFl', soldoutFlVal, SGR_STOCK_REASON) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionDeliveryFl"><div>' + | |
| buildReasonSelectHtml('optionDeliveryFl', deliveryFlVal, SGR_DELIVERY_REASON) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionCode"><div>' + | |
| buildInputHtml('optionCode', s.optionCode !== undefined ? s.optionCode : (combo.selfOptionCd || '')) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionMemo"><div>' + | |
| buildInputHtml('optionMemo', s.optionMemo !== undefined ? s.optionMemo : (combo.memo || '')) + | |
| '</div></td>' + | |
| '</tr>'; | |
| return html; | |
| } | |
| function renderOptionTable(combinations, fresh) { | |
| // 재렌더링 전 기존 입력값 보존 (조합 키 기준) — fresh 모드면 보존하지 않음 | |
| var existing = {}; | |
| if (!fresh) { | |
| $optTableBody.find('.sgr-option-table__row').each(function() { | |
| var key = $(this).data('combination-key'); | |
| if (key) { | |
| existing[key] = { | |
| optionBuyPrice: $(this).find('[data-field="optionBuyPrice"]').val(), | |
| optionPrice: $(this).find('[data-field="optionPrice"]').val(), | |
| stockCnt: $(this).find('[data-field="stockCnt"]').val(), | |
| optionDisplayFl: $(this).find('[data-field="optionDisplayFl"]').val(), | |
| optionSoldoutFl: $(this).find('[data-field="optionSoldoutFl"]').val(), | |
| optionDeliveryFl: $(this).find('[data-field="optionDeliveryFl"]').val(), | |
| optionCode: $(this).find('[data-field="optionCode"]').val(), | |
| optionMemo: $(this).find('[data-field="optionMemo"]').val() | |
| }; | |
| } | |
| }); | |
| } | |
| $optTableBody.find('.sgr-option-table__row').remove(); | |
| if (!combinations || combinations.length === 0) { | |
| $optTableEmpty.show(); | |
| $('#sgr-option-select-all').prop('checked', false).prop('indeterminate', false).closest('.ncua-checkbox-input').removeClass('has-indeterminate'); | |
| return; | |
| } | |
| $optTableEmpty.hide(); | |
| var loadedMap = (window.sgrOption && window.sgrOption.loadedStockMap) || {}; | |
| combinations.forEach(function(combo) { | |
| var saved = existing[combo.key]; | |
| // 옵션가: 신규 조합·옵션등록 옵션가 수정분은 설정값 반영, 불러온 조합 미수정분(합산 0)만 기존 값 유지 | |
| if (saved) { | |
| var keepLoadedPrice = loadedMap.hasOwnProperty(combo.key) && (parseInt(combo.optionPrice, 10) || 0) === 0; | |
| if (!keepLoadedPrice) { | |
| saved = $.extend({}, saved); | |
| delete saved.optionPrice; | |
| } | |
| } | |
| $optTableBody.append(buildCombinationRow(combo, saved)); | |
| }); | |
| $optTableBody.find('[data-field="optionBuyPrice"], [data-field="optionPrice"]').number_only(); | |
| updateSelectAllState(); | |
| sgrApplyGridColumns(); | |
| } | |
| // sgr:option-combinations-updated 이벤트 구독 | |
| document.addEventListener('sgr:option-combinations-updated', function(e) { | |
| var fresh = e.detail && e.detail.fresh; | |
| updateTableHeader(); | |
| renderOptionTable(window.sgrOption.combinations || [], fresh); | |
| // 옵션 (재)적용 시 조합 테이블이 재생성되므로 일괄 적용 행의 잔여 입력값 초기화 | |
| $('.sgr-option-table__bulk-row .js-sgr-bulk-field').val(''); | |
| }); | |
| // 전체 선택 Checkbox | |
| $(document).on('change', '#sgr-option-select-all', function() { | |
| var checked = $(this).prop('checked'); | |
| $optTableBody.find('.js-sgr-row-check').prop('checked', checked); | |
| $optTableBody.find('.sgr-option-table__row').toggleClass('is-selected', checked); | |
| $(this).prop('indeterminate', false).closest('.ncua-checkbox-input').removeClass('has-indeterminate'); | |
| $('.js-sgr-delete-selected-btn').prop('disabled', !checked || $optTableBody.find('.js-sgr-row-check').length === 0); | |
| }); | |
| $(document).on('change', '#sgr-option-table-body .js-sgr-row-check', function() { | |
| $(this).closest('.sgr-option-table__row').toggleClass('is-selected', $(this).prop('checked')); | |
| updateSelectAllState(); | |
| }); | |
| function updateSelectAllState() { | |
| var total = $optTableBody.find('.js-sgr-row-check').length; | |
| var checked = $optTableBody.find('.js-sgr-row-check:checked').length; | |
| var allChecked = total > 0 && total === checked; | |
| var indeterminate = checked > 0 && !allChecked; | |
| $('#sgr-option-select-all').prop('checked', allChecked).prop('indeterminate', indeterminate) | |
| .closest('.ncua-checkbox-input').toggleClass('has-indeterminate', indeterminate); | |
| $('.js-sgr-delete-selected-btn').prop('disabled', checked === 0); | |
| } | |
| // 일괄 적용 버튼 | |
| $(document).on('click', '.js-sgr-bulk-apply-btn', function() { | |
| var $checked = $optTableBody.find('.js-sgr-row-check:checked'); | |
| if ($checked.length === 0) { | |
| NCDSAlert({ message: '선택된 옵션이 없습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| var bulkValues = {}; | |
| var hasAnyValue = false; | |
| $('.js-sgr-bulk-field').each(function() { | |
| var field = $(this).data('field'); | |
| var val = $(this).val(); | |
| if (val !== '') { | |
| bulkValues[field] = val; | |
| hasAnyValue = true; | |
| } | |
| }); | |
| if (!hasAnyValue) { | |
| NCDSAlert({ message: '적용할 값을 입력해주세요.', iconType: 'error' }); | |
| return; | |
| } | |
| $checked.each(function() { | |
| applyBulkValuesToRow($(this).closest('.sgr-option-table__row'), bulkValues); | |
| }); | |
| syncCombinationsFromTable(); | |
| if (bulkValues.stockCnt !== undefined) { | |
| document.dispatchEvent(new CustomEvent('sgr:option-stock-changed')); | |
| } | |
| }); | |
| function applyBulkValuesToRow($row, bulkValues) { | |
| Object.keys(bulkValues).forEach(function(field) { | |
| $row.find('[data-field="' + field + '"]').val(bulkValues[field]); | |
| }); | |
| } | |
| function syncCombinationsFromTable() { | |
| $optTableBody.find('.sgr-option-table__row').each(function() { | |
| var key = $(this).data('combination-key'); | |
| var combo = (window.sgrOption.combinations || []).find(function(c) { return c.key === key; }); | |
| if (combo) { | |
| combo.optionPurchasePrice = $(this).find('[data-field="optionBuyPrice"]').val(); | |
| combo.optionPrice = $(this).find('[data-field="optionPrice"]').val(); | |
| combo.stockCnt = $(this).find('[data-field="stockCnt"]').val(); | |
| combo.displayFl = $(this).find('[data-field="optionDisplayFl"]').val(); | |
| combo.soldOutFl = $(this).find('[data-field="optionSoldoutFl"]').val(); | |
| combo.deliveryFl = $(this).find('[data-field="optionDeliveryFl"]').val(); | |
| combo.selfOptionCd = $(this).find('[data-field="optionCode"]').val(); | |
| combo.memo = $(this).find('[data-field="optionMemo"]').val(); | |
| } | |
| }); | |
| } | |
| // 재고량 직접 변경 시 | |
| var stockDebounceTimer = null; | |
| $(document).on('input', '#sgr-option-table-body .js-sgr-stock-input', function() { | |
| var key = $(this).closest('.sgr-option-table__row').data('combination-key'); | |
| var combo = (window.sgrOption.combinations || []).find(function(c) { return c.key === key; }); | |
| if (combo) { combo.stockCnt = $(this).val(); } | |
| clearTimeout(stockDebounceTimer); | |
| stockDebounceTimer = setTimeout(function() { | |
| document.dispatchEvent(new CustomEvent('sgr:option-stock-changed')); | |
| }, 200); | |
| }); | |
| // 정렬 버튼 enabled/disabled 토글 | |
| function updateSortBtnsState() { | |
| var hasChecked = $optTableBody.find('.js-sgr-row-check:checked').length > 0; | |
| $('#sgr-sort-btn-group .js-sgr-sort-btn').prop('disabled', !hasChecked); | |
| } | |
| // DOM 행 순서 기반 combinations 배열 동기화 | |
| function syncCombinationsOrderFromDOM() { | |
| var ordered = []; | |
| $optTableBody.find('.sgr-option-table__row').each(function() { | |
| var key = $(this).data('combination-key'); | |
| var combo = (window.sgrOption.combinations || []).find(function(c) { return c.key === key; }); | |
| if (combo) ordered.push(combo); | |
| }); | |
| window.sgrOption.combinations = ordered; | |
| } | |
| // 체크 변경 시 정렬 버튼 상태 갱신 (sgr-option-select-all 변경도 포함) | |
| $(document).on('change', '#sgr-option-table-body .js-sgr-row-check', function() { | |
| updateSortBtnsState(); | |
| }); | |
| $(document).on('change', '#sgr-option-select-all', function() { | |
| setTimeout(updateSortBtnsState, 0); | |
| }); | |
| // 정렬 버튼 클릭 | |
| $(document).on('click', '#sgr-sort-btn-group .js-sgr-sort-btn', function() { | |
| if ($(this).prop('disabled')) return; | |
| var direction = $(this).data('direction'); | |
| var $rows = $optTableBody.find('.sgr-option-table__row'); | |
| var $checked = $rows.filter(function() { | |
| return $(this).find('.js-sgr-row-check:checked').length > 0; | |
| }); | |
| if ($checked.length === 0) return; | |
| if (direction === 'top') { | |
| $checked.get().reverse().forEach(function(row) { $optTableBody.prepend(row); }); | |
| } else if (direction === 'up') { | |
| $checked.each(function() { | |
| var $prev = $(this).prev('.sgr-option-table__row'); | |
| if ($prev.length) { $prev.before(this); } | |
| }); | |
| } else if (direction === 'down') { | |
| $checked.get().reverse().forEach(function(row) { | |
| var $next = $(row).next('.sgr-option-table__row'); | |
| if ($next.length) { $next.after(row); } | |
| }); | |
| } else if (direction === 'bottom') { | |
| $checked.each(function() { $optTableBody.append(this); }); | |
| } | |
| syncCombinationsOrderFromDOM(); | |
| }); | |
| // HTML5 Drag & Drop | |
| var $sgrDragSrc = null; | |
| // mousedown on drag handle → draggable 활성화, 그 외에서는 비활성 | |
| $(document).on('mousedown', '#sgr-option-table-body .js-sgr-drag-handle', function() { | |
| $(this).closest('.sgr-option-table__row').attr('draggable', 'true'); | |
| }); | |
| $(document).on('mouseup', '#sgr-option-table-body .sgr-option-table__row', function() { | |
| $(this).removeAttr('draggable'); | |
| }); | |
| $(document).on('dragstart', '#sgr-option-table-body .sgr-option-table__row', function(e) { | |
| $sgrDragSrc = $(this); | |
| $(this).addClass('is-dragging'); | |
| e.originalEvent.dataTransfer.effectAllowed = 'move'; | |
| e.originalEvent.dataTransfer.setData('text/plain', ''); | |
| }); | |
| $(document).on('dragover', '#sgr-option-table-body .sgr-option-table__row', function(e) { | |
| if (!$sgrDragSrc || $sgrDragSrc[0] === this) return; | |
| e.preventDefault(); | |
| e.originalEvent.dataTransfer.dropEffect = 'move'; | |
| $optTableBody.find('.sgr-option-table__row').not(this).removeClass('ncua-table__row--drag-over-top'); | |
| $(this).addClass('ncua-table__row--drag-over-top'); | |
| }); | |
| $(document).on('dragleave', '#sgr-option-table-body .sgr-option-table__row', function(e) { | |
| if (!$(e.relatedTarget).closest('.sgr-option-table__row').is(this)) { | |
| $(this).removeClass('ncua-table__row--drag-over-top'); | |
| } | |
| }); | |
| $(document).on('drop', '#sgr-option-table-body .sgr-option-table__row', function(e) { | |
| e.preventDefault(); | |
| if (!$sgrDragSrc || $sgrDragSrc[0] === this) return; | |
| $(this).before($sgrDragSrc); | |
| syncCombinationsOrderFromDOM(); | |
| }); | |
| $(document).on('dragend', '#sgr-option-table-body .sgr-option-table__row', function() { | |
| $optTableBody.find('.sgr-option-table__row').removeClass('is-dragging ncua-table__row--drag-over-top').removeAttr('draggable'); | |
| $sgrDragSrc = null; | |
| }); | |
| window.sgrGridActiveKeys = null; | |
| window.sgrApplyGridColumns = function(activeKeys) { | |
| if (activeKeys) { | |
| window.sgrGridActiveKeys = activeKeys; | |
| } | |
| var keys = window.sgrGridActiveKeys; | |
| if (!keys) return; | |
| var $table = $('.sgr-option-table'); | |
| $table.find('tr').each(function() { | |
| var $row = $(this); | |
| var $gridCells = $row.children('[data-grid-key]'); | |
| if ($gridCells.length === 0) return; | |
| var $anchor = $gridCells.first().prev(); | |
| var cellMap = {}; | |
| $gridCells.each(function() { | |
| cellMap[$(this).data('grid-key')] = $(this); | |
| }); | |
| $gridCells.detach(); | |
| var $insertAfter = $anchor; | |
| for (var i = 0; i < keys.length; i++) { | |
| var $cell = cellMap[keys[i]]; | |
| if ($cell) { | |
| $cell.show(); | |
| $cell.insertAfter($insertAfter); | |
| $insertAfter = $cell; | |
| delete cellMap[keys[i]]; | |
| } | |
| } | |
| $.each(cellMap, function(k, $cell) { | |
| $cell.hide(); | |
| $cell.insertAfter($insertAfter); | |
| $insertAfter = $cell; | |
| }); | |
| }); | |
| }; | |
| $.post('/goods/goods_ps.php', {mode: 'get_goods_option_admin_grid_list', goodsOptionGridMode: 'goods_option_list'}, function(data) { | |
| if (data) { | |
| var result = $.parseJSON(data); | |
| if (result && result.select) { | |
| var keys = []; | |
| $.each(result.select, function(key) { keys.push(key); }); | |
| sgrApplyGridColumns(keys); | |
| } | |
| } | |
| }); | |
| // 조회항목설정 Confirm Alert | |
| $(document).on('click', '.js-sgr-grid-config-btn', function() { | |
| NCDSConfirm({ | |
| message: '조회항목 설정을 계속 진행하시겠습니까?', | |
| subMessage: '조회항목 설정 시 화면이 새로고침되어 입력된 내용은 저장되지 않습니다.', | |
| callback: function(result) { | |
| if (!result) return; | |
| window.sgrOption.combinations = []; | |
| $optTableBody.find('.sgr-option-table__row').remove(); | |
| $optTableEmpty.show(); | |
| $('.sgr-option-table__bulk-row .js-sgr-bulk-field').val(''); | |
| $('#sgr-option-select-all').prop('checked', false).prop('indeterminate', false).closest('.ncua-checkbox-input').removeClass('has-indeterminate'); | |
| updateSortBtnsState(); | |
| $('#sgr-option-info-row').hide(); | |
| $('#sgr-stock-input-area input[name="stockCnt"]').prop('disabled', false).val('0').closest('.ncua-input').removeClass('is-disabled'); | |
| var colSettingModal = new window.ncua.Modal({ | |
| size: 'xl', | |
| closeOnEsc: true, | |
| onClose: function() { $(document).off('keydown.gridConfig'); } | |
| }); | |
| window._colSettingModal = colSettingModal; | |
| var modalEl = colSettingModal.getModalElement(); | |
| var colSettingHeader = new window.ncua.Modal.Header({ | |
| title: '조회항목 설정', | |
| showDivider: true | |
| }); | |
| colSettingHeader.setCloseHandler(function() { colSettingModal.close(); }); | |
| colSettingHeader.appendTo(modalEl); | |
| var headerEl = modalEl.querySelector('.ncua-modal__header'); | |
| if (headerEl) headerEl.style.borderBottom = '1px solid var(--gray-100, #e4e5e7)'; | |
| var colSettingActions = new window.ncua.Modal.Actions('', { | |
| layout: 'horizontal', | |
| align: 'right' | |
| }); | |
| var cancelBtn = document.createElement('button'); | |
| cancelBtn.className = 'ncua-btn ncua-btn--sm ncua-btn--secondary-gray'; | |
| cancelBtn.innerHTML = '<span class="ncua-btn__label">취소</span>'; | |
| cancelBtn.addEventListener('click', function() { colSettingModal.close(); }); | |
| var saveBtn = document.createElement('button'); | |
| saveBtn.className = 'ncua-btn ncua-btn--sm ncua-btn--primary'; | |
| saveBtn.innerHTML = '<span class="ncua-btn__label">설정</span>'; | |
| saveBtn.addEventListener('click', function() { | |
| var activeKeys = $('.js-list-active .ncua-col-setting__item').map(function() { | |
| return $(this).data('field-key'); | |
| }).get(); | |
| $('#goodsGridOptionForm input[name="mode"]').val('save_goods_option_admin_grid_list'); | |
| $.post('goods_ps.php', $('#goodsGridOptionForm').serialize(), function() { | |
| if (typeof window.sgrApplyGridColumns === 'function') { | |
| window.sgrApplyGridColumns(activeKeys); | |
| } | |
| colSettingModal.close(); | |
| }); | |
| }); | |
| colSettingActions.setContent([cancelBtn, saveBtn]); | |
| $.get('/share/ncds/layer_goods_option_grid_config.php', {goodsOptionGridMode: 'goods_option_list'}, function(html) { | |
| var scripts = []; | |
| var cleanHtml = html.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(m, code) { | |
| scripts.push(code.replace(/^<!--/, '').replace(/\/\/-->$/, '')); | |
| return ''; | |
| }); | |
| colSettingModal.open(); | |
| var $wrapper = $('<div/>').appendTo(modalEl); | |
| $wrapper[0].innerHTML = cleanHtml; | |
| colSettingActions.appendTo(modalEl); | |
| var actionsEl = modalEl.querySelector('.ncua-modal__actions-wrapper'); | |
| if (actionsEl) actionsEl.style.borderTop = '1px solid var(--gray-100, #e4e5e7)'; | |
| scripts.forEach(function(s) { $.globalEval(s); }); | |
| }); | |
| } | |
| }); | |
| }); | |
| // 옵션 추가 (인라인 빈 행) | |
| function buildManualOptionNameCells(groupCount) { | |
| var cells = ''; | |
| for (var i = 0; i < 5; i++) { | |
| var isVisible = i < groupCount; | |
| var placeholder = i === 0 ? '옵션명 입력' : ('옵션명' + (i + 1) + ' 입력'); | |
| cells += '<td class="ncua-table__cell sgr-opt-col-name js-sgr-data-col-name' + (i + 1) + '"' + | |
| (!isVisible ? ' style="display:none;"' : '') + | |
| '><div><div class="ncua-input ncua-input--xs" style="width:100%;"><div class="ncua-input__content-wrap"><div class="ncua-input__content"><div class="ncua-input__field ncua-input__field--xs"><input type="text" placeholder="' + placeholder + '" /></div></div></div></div></div></td>'; | |
| } | |
| return cells; | |
| } | |
| function buildManualEntryRow(combo) { | |
| var groupCount = getActiveGroups().length; | |
| var html = '<tr class="ncua-table__row sgr-option-table__row" data-combination-key="' + escHtml(combo.key) + '">' + | |
| '<td class="ncua-table__cell ncua-table__checkbox-cell sgr-opt-col-dragcheck"><div><span class="ncua-table__drag-handle sgr-option-drag-handle js-sgr-drag-handle" aria-hidden="true">' + getDragHandleSvg() + '</span>' + | |
| '<label class="ncua-checkbox-field ncua-checkbox-field--xs" style="margin:0;">' + | |
| '<span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input">' + | |
| '<input type="checkbox" class="js-sgr-row-check" />' + | |
| '</span></label></div></td>' + | |
| buildManualOptionNameCells(groupCount) + | |
| '<td class="ncua-table__cell" data-grid-key="optionCostPrice"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('optionBuyPrice', '', '68px', '') + | |
| '<span class="sgr-unit-text">원</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionPrice"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('optionPrice', '', '68px', '') + | |
| '<span class="sgr-unit-text">원</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="stockCnt"><div style="display:flex;align-items:center;gap:8px;">' + | |
| buildNumericInputHtml('stockCnt', '', '60px', 'js-number js-sgr-stock-input') + | |
| '<span class="sgr-unit-text">개</span></div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionViewFl"><div>' + | |
| buildSelectHtml('optionDisplayFl', 'y', [{ val: 'y', label: '노출함' }, { val: 'n', label: '노출안함' }]) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionSellFl"><div>' + | |
| buildReasonSelectHtml('optionSoldoutFl', 'y', SGR_STOCK_REASON) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionDeliveryFl"><div>' + | |
| buildReasonSelectHtml('optionDeliveryFl', 'normal', SGR_DELIVERY_REASON) + | |
| '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionCode"><div>' + buildInputHtml('optionCode', '') + '</div></td>' + | |
| '<td class="ncua-table__cell" data-grid-key="optionMemo"><div>' + buildInputHtml('optionMemo', '') + '</div></td>' + | |
| '</tr>'; | |
| return html; | |
| } | |
| $(document).on('click', '.js-sgr-add-option-btn', function() { | |
| var newCombo = { | |
| key: 'manual_' + Date.now(), | |
| values: [''], | |
| manualEntry: true, | |
| optionPurchasePrice: '', | |
| optionPrice: '', | |
| stockCnt: '', | |
| displayFl: 'y', | |
| soldOutFl: 'y', | |
| deliveryFl: 'normal', | |
| selfOptionCd: '', | |
| memo: '' | |
| }; | |
| window.sgrOption.combinations = window.sgrOption.combinations || []; | |
| window.sgrOption.combinations.push(newCombo); | |
| $optTableEmpty.hide(); | |
| var $newRow = $(buildManualEntryRow(newCombo)); | |
| $optTableBody.append($newRow); | |
| $newRow.find('[data-field="optionBuyPrice"], [data-field="optionPrice"]').number_only(); | |
| updateSelectAllState(); | |
| sgrApplyGridColumns(); | |
| }); | |
| // 선택 삭제 | |
| $(document).on('click', '.js-sgr-delete-selected-btn', function() { | |
| var $checked = $optTableBody.find('.js-sgr-row-check:checked'); | |
| if ($checked.length === 0) return; | |
| $checked.each(function() { | |
| var key = $(this).closest('.sgr-option-table__row').data('combination-key'); | |
| window.sgrOption.combinations = (window.sgrOption.combinations || []).filter(function(c) { return c.key !== key; }); | |
| $(this).closest('.sgr-option-table__row').remove(); | |
| }); | |
| $('#sgr-option-select-all').prop('checked', false); | |
| updateSelectAllState(); | |
| updateSortBtnsState(); | |
| document.dispatchEvent(new CustomEvent('sgr:option-stock-changed')); | |
| if ($optTableBody.find('.sgr-option-table__row').length === 0) { | |
| $optTableEmpty.show(); | |
| $('#sgr-option-info-row').hide(); | |
| $('#sgr-stock-input-area input[name="stockCnt"]').prop('disabled', false).val('0').closest('.ncua-input').removeClass('is-disabled'); | |
| } | |
| }); | |
| /** | |
| * 옵션 카드 영역을 groups 배열로 채운다. | |
| * @param {Array} groups [{name, values:[{val,price,image,url}]}] | |
| * @param {string} displayFl 's'|'d' | |
| */ | |
| function fillOptCards(groups, displayFl, serverCombinations) { | |
| window.sgrOption.reset(); | |
| $('[name="optionY[optionDisplayFl]"]').filter('[value="' + (displayFl === 'd' ? 'd' : 's') + '"]').prop('checked', true).trigger('change'); | |
| $optRegister.find('.js-opt-name-row').remove(); | |
| var $footer = $optRegister.find('.sgr-opt-register__footer'); | |
| $.each(groups, function(i, grp) { | |
| var $row = $(createNameRowHtml()); | |
| $row.find('.js-opt-name-input').val(grp.name); | |
| $footer.before($row); | |
| $.each(grp.values, function(j, v) { | |
| var $valRow; | |
| if (j === 0) { | |
| $valRow = $row.find('.sgr-opt-vname-row').first(); | |
| } else { | |
| $valRow = $(createValRowHtml(false)); | |
| $row.find('.sgr-opt-vname').append($valRow); | |
| } | |
| $valRow.find('.js-opt-val-input').val(v.val); | |
| $valRow.find('.js-opt-price-input').val(v.price || 0); | |
| if (v.url) { $valRow.find('.js-opt-url-input').val(v.url); } | |
| if (v.image) { | |
| var $imageDiv = $valRow.find('.sgr-opt-vname-row__image'); | |
| $imageDiv.find('.js-opt-image-tag-wrap').remove(); | |
| $imageDiv.append( | |
| '<div class="js-opt-image-tag-wrap" style="margin-top:4px;">' + | |
| '<span class="ncua-tag ncua-tag--sm" style="max-width:100%;display:inline-flex;align-items:center;">' + | |
| '<span class="ncua-tag__text" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escHtml(v.image) + '</span>' + | |
| '<button type="button" class="ncua-tag__close js-opt-image-remove"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6 6 18M6 6l12 12"/></svg></button>' + | |
| '</span>' + | |
| '<input type="hidden" class="js-opt-existing-image" value="' + escHtml(v.image) + '" />' + | |
| '</div>' | |
| ); | |
| } | |
| }); | |
| }); | |
| var hasUrl = groups.some(function(g) { return g.values.some(function(v) { return !!v.url; }); }); | |
| if (hasUrl && !$('#optImageUrlCheck').is(':checked')) { | |
| $('#optImageUrlCheck').prop('checked', true).trigger('change'); | |
| } | |
| $optRegister.find('.js-opt-price-input').number_only(); | |
| $('[name="optionY[optionDisplayFl]"]:checked').trigger('change'); | |
| updateOptNameButtons(); | |
| updateOptImageVisibility(); | |
| updateSaveFreqBtnState(); | |
| // 조합별 옵션 정보(옵션가·재고량 등)가 있으면 자동 옵션 적용 + stock 데이터 반영 | |
| if (serverCombinations && serverCombinations.length > 0) { | |
| var newGroups = collectOptionGroups(); | |
| var newCombinations = computeCombinations(newGroups); | |
| // 서버 조합 데이터를 key 기준으로 lookup | |
| var stockMap = {}; | |
| serverCombinations.forEach(function(s) { | |
| stockMap[s.key] = s; | |
| }); | |
| // 생성된 조합에 서버 stock 데이터 병합 | |
| newCombinations.forEach(function(combo) { | |
| var stock = stockMap[combo.key]; | |
| if (stock) { | |
| combo.optionPurchasePrice = parseInt(stock.optionCostPrice, 10) || 0; | |
| combo.optionPrice = parseInt(stock.optionPrice, 10) || 0; | |
| combo.stockCnt = parseInt(stock.stockCnt, 10) || 0; | |
| combo.displayFl = stock.optionViewFl || 'y'; | |
| combo.soldOutFl = stock.optionSellFl || 'y'; | |
| combo.deliveryFl = stock.optionDeliveryFl || 'normal'; | |
| combo.selfOptionCd = stock.optionCode || ''; | |
| combo.memo = stock.optionMemo || ''; | |
| } | |
| }); | |
| window.sgrOption.combinations = newCombinations; | |
| window.sgrOption.loadedStockMap = stockMap; | |
| $('#sgr-option-info-row').show(); | |
| document.dispatchEvent(new CustomEvent('sgr:option-combinations-updated', { detail: { fresh: true } })); | |
| } | |
| } | |
| function updateSaveFreqBtnState() { | |
| var hasName = false; | |
| $('.js-opt-name-input').each(function() { if ($.trim($(this).val()) !== '') { hasName = true; return false; } }); | |
| $('[data-modal-open="optSaveFreqModal"]').prop('disabled', !hasName); | |
| } | |
| // ── Task 5: 기존상품 옵션 불러오기 ────────────────────────────────────── | |
| // 옵션 대체 성공 시에만 옵션 모달을 닫고, Confirm 취소·조회 실패 시에는 모달을 유지한다. | |
| // 'deferClose' 반환 시 레이어 공통 로직이 모달을 닫지 않고 콜백에 닫기를 위임한다. | |
| function sgrCloseOptionLayer() { | |
| $('div.bootstrap-dialog-close-button').click(); | |
| } | |
| window.sgrApplyGoodsOption = function(resultJson) { | |
| if (!resultJson.info || !resultJson.info.length) return true; | |
| var goodsNo = resultJson.info[0].goodsNo; | |
| $.ajax({ | |
| url: './goods_ps.php', type: 'POST', | |
| data: { mode: 'sgr_get_goods_option', goodsNo: goodsNo }, | |
| dataType: 'json', | |
| success: function(res) { | |
| if (res.error) { | |
| NCDSAlert({ message: res.message, iconType: 'error' }); | |
| return; | |
| } | |
| var groups = res.groups || []; | |
| var displayFl = res.optionDisplayFl || 's'; | |
| var combinations = res.combinations || []; | |
| if (window.sgrOption.hasData()) { | |
| NCDSConfirm({ | |
| message: '현재 입력된 옵션 정보가 불러온 옵션으로 대체됩니다. 진행하시겠습니까?', | |
| callback: function(result) { | |
| if (result) { | |
| fillOptCards(groups, displayFl, combinations); | |
| sgrCloseOptionLayer(); | |
| } | |
| } | |
| }); | |
| } else { | |
| fillOptCards(groups, displayFl, combinations); | |
| sgrCloseOptionLayer(); | |
| } | |
| } | |
| }); | |
| return 'deferClose'; | |
| }; | |
| $(document).on('click', '.js-sgr-option-load-existing', function() { | |
| layer_add_info('goods', {optionRegister: 'y', callFunc: 'sgrApplyGoodsOption', layerTitle: '상품 선택'}); | |
| }); | |
| // ── Task 6: 자주쓰는 옵션 불러오기 (ncds 레이어 팝업) ──────────────── | |
| window.sgrApplyFreqOption = function(getData) { | |
| if (!getData || !getData.optionName) return; | |
| var names = getData.optionName || []; | |
| var displayFl = getData.displayFl || 's'; | |
| var groups = []; | |
| $.each(names, function(i, nm) { | |
| var rawVals = (getData.optionValue && getData.optionValue[i]) ? getData.optionValue[i] : []; | |
| var vals = []; | |
| $.each(rawVals, function(j, v) { | |
| var parts = String(v).split('|'); | |
| vals.push({ val: parts[0] || '', price: parseInt(parts[1], 10) || 0, image: parts[2] || '', url: parts[3] || '' }); | |
| }); | |
| groups.push({ name: nm, values: vals }); | |
| }); | |
| if (window.sgrOption.hasData()) { | |
| NCDSConfirm({ | |
| message: '현재 입력된 옵션 정보가 선택한 자주쓰는 옵션으로 대체됩니다. 진행하시겠습니까?', | |
| callback: function(result) { | |
| if (result) { | |
| fillOptCards(groups, displayFl); | |
| sgrCloseOptionLayer(); | |
| } | |
| } | |
| }); | |
| } else { | |
| fillOptCards(groups, displayFl); | |
| sgrCloseOptionLayer(); | |
| } | |
| return 'deferClose'; | |
| }; | |
| $(document).on('click', '.js-sgr-option-load-frequent', function() { | |
| var loadChk = $('#layerOptionListForm').length; | |
| $.get('ncds/layer_goods_option_list.php', { callFunc: 'sgrApplyFreqOption' }, function(data) { | |
| if (loadChk == 0) { | |
| data = '<div id="layerOptionListForm">' + data + '</div>'; | |
| } | |
| ncds_layer_popup({ message: data, title: '자주쓰는 옵션 선택', size: 'wide' }); | |
| }); | |
| }); | |
| // ── Task 7: 자주쓰는 옵션 등록 (ncds 레이어 팝업) ───────────────────── | |
| $(document).on('click', '[data-modal-open="optSaveFreqModal"]', function() { | |
| if ($(this).prop('disabled')) return; | |
| var loadChk = $('#layerOptionRegisterForm').length; | |
| $.get('ncds/layer_goods_option_register.php', function(data) { | |
| if (loadChk == 0) { | |
| data = '<div id="layerOptionRegisterForm">' + data + '</div>'; | |
| } | |
| ncds_layer_popup({ message: data, title: '자주쓰는 옵션 등록', onshown: function(dialog) { dialog.getModalDialog().css('width', '560px'); } }); | |
| }); | |
| }); | |
| // 초기 상태 | |
| updateSaveFreqBtnState(); | |
| $(document).on('input change', '.js-opt-name-input', updateSaveFreqBtnState); | |
| // 옵션 combinations 합계를 판매정보 재고수 필드에 반영하고 disabled 상태를 제어한다. | |
| // stockFl='n'(무한정판매) 상태는 재고수 영역 자체가 숨겨지므로 처리하지 않는다. | |
| function updateStockFromOptions() { | |
| var combinations = window.sgrOption ? window.sgrOption.combinations : []; | |
| var $stockCnt = $('input[name="stockCnt"]'); | |
| var stockFl = $('input[name="stockFl"]:checked').val(); | |
| var $stockCntWrap = $stockCnt.closest('.ncua-input'); | |
| if (combinations.length > 0 && stockFl === 'y') { | |
| var sum = combinations.reduce(function(acc, c) { | |
| return acc + (parseInt(c.stockCnt) || 0); | |
| }, 0); | |
| if (sum > 9999999999) sum = 9999999999; | |
| $stockCnt.val(sum).prop('disabled', true); | |
| $stockCntWrap.addClass('is-disabled'); | |
| } else if (combinations.length === 0) { | |
| $stockCnt.prop('disabled', false).val(0); | |
| $stockCntWrap.removeClass('is-disabled'); | |
| } | |
| } | |
| document.addEventListener('sgr:option-stock-changed', function() { | |
| updateStockFromOptions(); | |
| }); | |
| // "옵션 적용" 버튼은 sgr:option-combinations-updated 를 발행하므로 추가 구독 | |
| document.addEventListener('sgr:option-combinations-updated', function() { | |
| updateStockFromOptions(); | |
| }); | |
| // stockFl "재고량에 따름"(y) 전환 시 옵션 있으면 즉시 합계 재계산 | |
| $('input[name="stockFl"]').on('change.sgrStockSync', function() { | |
| updateStockFromOptions(); | |
| }); | |
| })(); | |
| $('#frmSimpleGoods').on('submit', function (e) { | |
| var nmValid = validateGoodsNm(); | |
| if (!nmValid) { | |
| e.preventDefault(); | |
| $('#goodsNm').focus(); | |
| return false; | |
| } | |
| // 기본 배송비조건이 없으면 등록 불가 | |
| if (!$('input[name="deliverySno"]').val()) { | |
| e.preventDefault(); | |
| sgrShowNoDeliveryWarning(); | |
| return false; | |
| } | |
| if (window.sgrSyncEditors) window.sgrSyncEditors(); | |
| // ── 옵션 이미지 name 할당 + 옵션 조합 직렬화 (간편상품등록 옵션/재고) ── | |
| // 옵션 이미지 file input에 name 속성 할당 (옵션명 카드 인덱스 기준) | |
| $('#frmSimpleGoods .js-opt-name-row').each(function (nameIdx) { | |
| $(this).find('.js-opt-image-file').attr('name', 'optionYIcon[goodsImage][' + nameIdx + '][]'); | |
| $(this).find('.js-opt-existing-image').attr('name', 'sgrExistingImage[' + nameIdx + '][]'); | |
| $(this).find('.js-opt-url-input').attr('name', 'optionYIcon[goodsImageText][' + nameIdx + '][]'); | |
| }); | |
| $('#frmSimpleGoods .js-sgr-opt-addurl-hidden').remove(); | |
| if ($('#optImageUrlCheck').is(':checked')) { | |
| $('#frmSimpleGoods').append('<input type="hidden" class="js-sgr-opt-addurl-hidden" name="optionImageAddUrl" value="y" />'); | |
| } | |
| // 옵션 사용 시 조합 데이터를 optionY 히든 필드로 직렬화 | |
| $('#frmSimpleGoods .js-sgr-option-hidden').remove(); | |
| // 옵션 사용함인데 적용된 옵션 조합이 없으면 사용안함으로 전환 후 저장 (일반등록 동일 동작) | |
| if ($('input[name="optionFl"]:checked').val() === 'y' | |
| && !(window.sgrOption && window.sgrOption.combinations && window.sgrOption.combinations.length > 0)) { | |
| $('input[name="optionFl"][value="n"]').prop('checked', true); | |
| $('input[name="stockCnt"]').prop('disabled', false); | |
| } | |
| var optFl = $('input[name="optionFl"]:checked').val(); | |
| var combinations = window.sgrOption ? window.sgrOption.combinations : []; | |
| if (optFl === 'y' && combinations.length > 0) { | |
| var STR_DIV = '<?=STR_DIVISION?>'; | |
| var $form = $('#frmSimpleGoods'); | |
| var groups = []; | |
| $('.sgr-opt-register .js-opt-name-row').each(function () { | |
| var name = $(this).find('.js-opt-name-input').val().trim(); | |
| var vals = []; | |
| $(this).find('.js-opt-val-input').each(function () { | |
| var v = $(this).val().trim(); | |
| if (v) vals.push(v); | |
| }); | |
| if (vals.length > 0) groups.push({ name: name, values: vals }); | |
| }); | |
| var h = function(name, value) { | |
| $form.append($('<input type="hidden" class="js-sgr-option-hidden">').attr('name', name).val(value)); | |
| }; | |
| groups.forEach(function (g, gi) { | |
| h('optionY[optionName][]', g.name); | |
| h('optionY[optionCnt][]', g.values.length); | |
| g.values.forEach(function (v) { | |
| h('optionY[optionValue][' + gi + '][]', v); | |
| }); | |
| }); | |
| var currentValues = {}; | |
| $('#sgr-option-table-body .sgr-option-table__row').each(function () { | |
| var key = $(this).data('combination-key'); | |
| currentValues[key] = { | |
| optionBuyPrice: $(this).find('[data-field="optionBuyPrice"]').val() || '0', | |
| optionPrice: $(this).find('[data-field="optionPrice"]').val() || '0', | |
| stockCnt: $(this).find('[data-field="stockCnt"]').val() || '0', | |
| optionDisplayFl: $(this).find('[data-field="optionDisplayFl"]').val() || 'y', | |
| optionSoldoutFl: $(this).find('[data-field="optionSoldoutFl"]').val() || 'y', | |
| optionDeliveryFl: $(this).find('[data-field="optionDeliveryFl"]').val() || 'normal', | |
| optionCode: $(this).find('[data-field="optionCode"]').val() || '', | |
| optionMemo: $(this).find('[data-field="optionMemo"]').val() || '' | |
| }; | |
| }); | |
| combinations.forEach(function (combo) { | |
| var cv = currentValues[combo.key] || {}; | |
| var soldoutFl = cv.optionSoldoutFl || 'y'; | |
| var deliveryFl = cv.optionDeliveryFl || 'normal'; | |
| h('optionY[sno][]', ''); | |
| h('optionY[optionValueText][]', combo.values.join(STR_DIV)); | |
| h('optionY[optionCostPrice][]', cv.optionBuyPrice || '0'); | |
| h('optionY[optionPrice][]', cv.optionPrice || combo.optionPrice || '0'); | |
| h('optionY[stockCnt][]', cv.stockCnt || combo.stockCnt || '0'); | |
| h('optionY[optionViewFl][]', cv.optionDisplayFl === 'n' ? 'n' : 'y'); | |
| h('optionY[optionSellFl][]', soldoutFl); | |
| h('optionY[optionDeliveryFl][]', deliveryFl); | |
| h('optionY[optionCode][]', cv.optionCode || ''); | |
| h('optionY[optionMemo][]', cv.optionMemo || ''); | |
| }); | |
| } | |
| // ── 판매재고/옵션재고 불일치 확인 ── | |
| var form = this; | |
| var stockFl = $('input[name="stockFl"]:checked').val(); | |
| var hasOptionStock = optFl === 'y' && combinations.length > 0 && combinations.some(function (combo) { | |
| var cv = currentValues ? currentValues[combo.key] : null; | |
| return parseInt((cv && cv.stockCnt) || combo.stockCnt || '0') > 0; | |
| }); | |
| if (stockFl === 'n' && hasOptionStock) { | |
| e.preventDefault(); | |
| NCDSConfirm({ | |
| message: '판매재고 변경 확인', | |
| subMessage: '상품재고가 등록되었습니다.\n판매재고를 "재고량에 따름"으로 변경 후 상품 정보를 저장하시겠습니까?', | |
| btnText: { confirmLabel: '예', cancelLabel: '아니오' }, | |
| callback: function (result) { | |
| if (result) { | |
| $('input[name="stockFl"][value="y"]').prop('checked', true); | |
| var $stockCnt = $('input[name="stockCnt"]'); | |
| var sum = combinations.reduce(function (acc, c) { | |
| var cv = currentValues ? currentValues[c.key] : null; | |
| return acc + (parseInt((cv && cv.stockCnt) || c.stockCnt || '0') || 0); | |
| }, 0); | |
| $stockCnt.val(sum); | |
| } | |
| setSaveBtnLoading(); | |
| form.target = 'ifrmProcess'; | |
| form.submit(); | |
| } | |
| }); | |
| return false; | |
| } | |
| setSaveBtnLoading(); | |
| this.target = 'ifrmProcess'; | |
| }); | |
| }); | |
| </script> | |
| <script type="text/javascript"> | |
| if (window.GodoCosGuide) { | |
| window.GodoCosGuide.init({ | |
| apiUrl: <?= json_encode($cosGuideApiUrl) ?>, | |
| guideCode: '260512001', | |
| }); | |
| } | |
| </script> | |
변경 전layer_goods_option_grid_config.php
변경 후layer_goods_option_grid_config.php
| <link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-goods-option-grid-config.css')?>"> | |
| <article class="ncua-content" style="padding:20px;"> | |
| <form name="goodsGridOptionForm" id="goodsGridOptionForm" action="./goods_ps.php" method="post" target="ifrmProcess"> | |
| <input type="hidden" name="mode" value="get_goods_option_admin_grid_list" /> | |
| <input type="hidden" name="goodsOptionGridMode" value="<?=$goodsGridOptionMode?>" /> | |
| <input type="hidden" name="gridSort" value="" class="js-grid-sort-value" /> | |
| <div class="ncua-col-setting"> | |
| <!-- 좌측 패널: 전체 조회항목 --> | |
| <div class="ncua-col-setting__panel"> | |
| <div class="ncua-col-setting__panel-title-row"> | |
| <span class="ncua-col-setting__panel-title">전체 조회항목</span> | |
| </div> | |
| <div class="ncua-col-setting__panel-body"> | |
| <div class="ncua-col-setting__panel-head-bar"> | |
| <div class="ncua-col-setting__head-left"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-select-all-left"><span class="ncua-btn__label">전체선택</span></button> | |
| <span class="ncua-col-setting__count js-count-left">선택 <span class="ncua-col-setting__count-num">0</span>개 / 전체 <span class="js-count-left-total">0</span></span> | |
| </div> | |
| <div class="ncua-selectbox ncua-selectbox--xs js-goods-grid-sort-box"></div> | |
| </div> | |
| <ul class="ncua-col-setting__list js-list-all"> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- 중앙 이동 버튼 --> | |
| <div class="ncua-col-setting__actions"> | |
| <button type="button" class="ncua-btn ncua-btn--sm only-icon ncua-btn--secondary-gray js-add" title="선택 항목을 노출 조회항목에 추가" aria-label="선택 항목을 노출 조회항목에 추가" disabled> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"></path></svg> | |
| </button> | |
| <button type="button" class="ncua-btn ncua-btn--sm only-icon ncua-btn--secondary-gray js-remove" title="노출 조회항목에서 제거" aria-label="노출 조회항목에서 제거" disabled> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 18-6-6 6-6"></path></svg> | |
| </button> | |
| </div> | |
| <!-- 우측 패널: 노출 조회항목 --> | |
| <div class="ncua-col-setting__panel"> | |
| <div class="ncua-col-setting__panel-title-row"> | |
| <span class="ncua-col-setting__panel-title">노출 조회항목</span> | |
| </div> | |
| <div class="ncua-col-setting__panel-body"> | |
| <div class="ncua-col-setting__panel-head-bar"> | |
| <div class="ncua-col-setting__head-left"> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-select-all-right"><span class="ncua-btn__label">전체선택</span></button> | |
| <span class="ncua-col-setting__count js-count-right">선택 <span class="ncua-col-setting__count-num">0</span>개 / 전체 <span class="js-count-right-total">0</span></span> | |
| </div> | |
| <div class="ncua-button-group ncua-button-group--xs has-border"> | |
| <button type="button" class="ncua-button-group__item is-only-icon js-reorder" data-direction="bottom" title="맨 아래로"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 13 6 6 6-6M6 7l6 6 6-6"/></svg> | |
| </button> | |
| <button type="button" class="ncua-button-group__item is-only-icon js-reorder" data-direction="down" title="아래로"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 9 6 6 6-6"/></svg> | |
| </button> | |
| <button type="button" class="ncua-button-group__item is-only-icon js-reorder" data-direction="up" title="위로"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m18 15-6-6-6 6"/></svg> | |
| </button> | |
| <button type="button" class="ncua-button-group__item is-only-icon js-reorder" data-direction="top" title="맨 위로"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m18 17-6-6-6 6M18 11l-6-6-6 6"/></svg> | |
| </button> | |
| </div> | |
| </div> | |
| <ul class="ncua-col-setting__list js-list-active"> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="ncua-col-setting__note"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="none"><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"/></svg> | |
| <span>Shift 버튼을 누른 상태에서 선택하면 여러 항목을 동시에 선택할 수 있습니다.</span> | |
| </div> | |
| </form> | |
| <script type="text/javascript"> | |
| <!-- | |
| var CHECK_SVG = '<svg class="ncua-col-setting__item-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6 9 17l-5-5"/></svg>'; | |
| function buildItem(key, label) { | |
| return '<li class="ncua-col-setting__item" data-field-key="' + key + '">' + | |
| '<div class="ncua-col-setting__item-inner">' + | |
| '<span class="ncua-col-setting__item-text">' + label + '</span>' + | |
| CHECK_SVG + | |
| '</div></li>'; | |
| } | |
| function buildActiveItem(key, label) { | |
| return '<li class="ncua-col-setting__item" data-field-key="' + key + '">' + | |
| '<div class="ncua-col-setting__item-inner">' + | |
| '<span class="ncua-col-setting__item-text">' + label + '</span>' + | |
| CHECK_SVG + | |
| '</div>' + | |
| '<input type="hidden" name="goodsOptionGridList[]" value="' + key + '" />' + | |
| '</li>'; | |
| } | |
| function updateCountLeft() { | |
| var selected = $('.js-list-all .ncua-col-setting__item.is-selected:not(.is-in-active)').length; | |
| var total = $('.js-list-all .ncua-col-setting__item:not(.is-in-active)').length; | |
| $('.js-count-left .ncua-col-setting__count-num').text(selected); | |
| $('.js-count-left .js-count-left-total').text(total); | |
| $('.js-add').prop('disabled', selected === 0); | |
| } | |
| function updateCountRight() { | |
| var selected = $('.js-list-active .ncua-col-setting__item.is-selected').length; | |
| var total = $('.js-list-active .ncua-col-setting__item').length; | |
| $('.js-count-right .ncua-col-setting__count-num').text(selected); | |
| $('.js-count-right .js-count-right-total').text(total); | |
| $('.js-remove').prop('disabled', selected === 0); | |
| } | |
| function syncLeftDimmed() { | |
| var activeKeys = $('.js-list-active .ncua-col-setting__item').map(function() { | |
| return $(this).data('field-key'); | |
| }).get(); | |
| $('.js-list-all .ncua-col-setting__item').each(function() { | |
| var key = $(this).data('field-key'); | |
| if ($.inArray(key, activeKeys) !== -1) { | |
| $(this).addClass('is-in-active'); | |
| } else { | |
| $(this).removeClass('is-in-active'); | |
| } | |
| }); | |
| } | |
| $(document).ready(function () { | |
| $(document).off('click', '.js-list-all .ncua-col-setting__item'); | |
| $(document).off('click', '.js-list-active .ncua-col-setting__item'); | |
| var lastLeftRow = null; | |
| $(document).on('click', '.js-list-all .ncua-col-setting__item', function (e) { | |
| $('.js-list-active .ncua-col-setting__item').removeClass('is-selected'); | |
| if (e.shiftKey && lastLeftRow) { | |
| var items = $('.js-list-all .ncua-col-setting__item'); | |
| var ia = items.index(lastLeftRow); | |
| var ib = items.index(this); | |
| var bot = Math.min(ia, ib); | |
| var top = Math.max(ia, ib); | |
| for (var i = bot; i <= top; i++) { | |
| items.eq(i).addClass('is-selected'); | |
| } | |
| } else { | |
| $(this).toggleClass('is-selected'); | |
| } | |
| lastLeftRow = $(this); | |
| updateCountLeft(); | |
| updateCountRight(); | |
| }); | |
| var lastRightRow = null; | |
| $(document).on('click', '.js-list-active .ncua-col-setting__item', function (e) { | |
| $('.js-list-all .ncua-col-setting__item').removeClass('is-selected'); | |
| if (e.shiftKey && lastRightRow) { | |
| var items = $('.js-list-active .ncua-col-setting__item'); | |
| var ia = items.index(lastRightRow); | |
| var ib = items.index(this); | |
| var bot = Math.min(ia, ib); | |
| var top = Math.max(ia, ib); | |
| for (var i = bot; i <= top; i++) { | |
| items.eq(i).addClass('is-selected'); | |
| } | |
| } else { | |
| $(this).toggleClass('is-selected'); | |
| } | |
| lastRightRow = $(this); | |
| updateCountLeft(); | |
| updateCountRight(); | |
| }); | |
| $('.js-select-all-left').click(function () { | |
| var items = $('.js-list-all .ncua-col-setting__item:not(.is-in-active)'); | |
| var allSelected = items.filter('.is-selected').length === items.length; | |
| items.toggleClass('is-selected', !allSelected); | |
| updateCountLeft(); | |
| }); | |
| $('.js-select-all-right').click(function () { | |
| var items = $('.js-list-active .ncua-col-setting__item'); | |
| var allSelected = items.filter('.is-selected').length === items.length; | |
| items.toggleClass('is-selected', !allSelected); | |
| updateCountRight(); | |
| }); | |
| $('.js-add').click(function () { | |
| var selectedItems = $('.js-list-all .ncua-col-setting__item.is-selected'); | |
| if (selectedItems.length === 0) { NCDSAlert({message: '이동할 항목을 선택해주세요.', iconType: 'error'}); return; } | |
| var dupCount = 0; | |
| selectedItems.each(function () { | |
| var key = $(this).data('field-key'); | |
| var label = $(this).find('.ncua-col-setting__item-text').text(); | |
| var exists = false; | |
| $('.js-list-active .ncua-col-setting__item').each(function () { | |
| if ($(this).data('field-key') === key) { exists = true; dupCount++; return false; } | |
| }); | |
| if (!exists) { $('.js-list-active').append(buildActiveItem(key, label)); } | |
| $(this).removeClass('is-selected'); | |
| }); | |
| if (dupCount > 0) { NCDSAlert({message: '중복된 항목은 추가 되지 않습니다.', iconType: 'error'}); } | |
| syncLeftDimmed(); updateCountLeft(); updateCountRight(); | |
| }); | |
| $('.js-remove').click(function () { | |
| var selectedItems = $('.js-list-active .ncua-col-setting__item.is-selected'); | |
| if (selectedItems.length === 0) { NCDSAlert({message: '삭제할 항목을 선택해주세요.', iconType: 'error'}); return; } | |
| selectedItems.remove(); | |
| syncLeftDimmed(); updateCountLeft(); updateCountRight(); | |
| }); | |
| $('.js-reorder').click(function () { | |
| var direction = $(this).data('direction'); | |
| var selected = $('.js-list-active .ncua-col-setting__item.is-selected'); | |
| if (selected.length === 0) { NCDSAlert({message: '순서 변경을 원하시는 항목을 선택해주세요.', iconType: 'error'}); return; } | |
| var indices = selected.map(function () { return $('.js-list-active .ncua-col-setting__item').index(this); }).get(); | |
| for (var i = 1; i < indices.length; i++) { | |
| if (indices[i] !== indices[i-1] + 1) { NCDSAlert({message: '비연속 선택된 항목은 순서 변경이 불가합니다.', iconType: 'error'}); return; } | |
| } | |
| var list = $('.js-list-active'); | |
| var items = list.children('.ncua-col-setting__item'); | |
| switch (direction) { | |
| case 'up': if (indices[0] > 0) selected.first().prev().before(selected); break; | |
| case 'down': if (indices[indices.length - 1] < items.length - 1) selected.last().next().after(selected); break; | |
| case 'top': list.prepend(selected); break; | |
| case 'bottom': list.append(selected); break; | |
| } | |
| }); | |
| $(document).on('keydown.gridConfig', function (e) { | |
| var selected = $('.js-list-active .ncua-col-setting__item.is-selected'); | |
| if (selected.length === 0) return; | |
| if (e.keyCode === 38) { selected.first().prev().before(selected); e.preventDefault(); } | |
| else if (e.keyCode === 40) { selected.last().next().after(selected); e.preventDefault(); } | |
| }); | |
| var sortBoxEl = document.querySelector('.js-goods-grid-sort-box'); | |
| if (sortBoxEl && window.ncua && window.ncua.SelectBox) { | |
| new window.ncua.SelectBox(sortBoxEl, { | |
| options: [ | |
| { id: '', text: '기본순서' }, | |
| { id: 'desc', text: '이름순' }, | |
| { id: 'asc', text: '이름역순' } | |
| ], | |
| value: '', | |
| placeholder: '기본순서', | |
| size: 'xs', | |
| onChange: function(value) { | |
| $('.js-grid-sort-value').val(value); | |
| $('#goodsGridOptionForm input[name="mode"]').val('get_grid_option_list_sort'); | |
| $.post('goods_ps.php', $('#goodsGridOptionForm').serialize(), function (data) { | |
| if (data) { | |
| var result = $.parseJSON(data); | |
| if (result) { | |
| var html = ''; | |
| $.each(result, function (key, val) { html += buildItem(key, val); }); | |
| $('.js-list-all').html(html); | |
| syncLeftDimmed(); updateCountLeft(); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| $.post('goods_ps.php', $('#goodsGridOptionForm').serialize(), function (data) { | |
| if (data) { | |
| var result = $.parseJSON(data); | |
| if (result.all) { | |
| var html = ''; | |
| $.each(result.all, function (key, val) { html += buildItem(key, val); }); | |
| $('.js-list-all').html(html); | |
| } | |
| if (result.select) { | |
| var html = ''; | |
| $.each(result.select, function (key, val) { html += buildActiveItem(key, val); }); | |
| $('.js-list-active').html(html); | |
| } | |
| syncLeftDimmed(); updateCountLeft(); updateCountRight(); | |
| } | |
| }); | |
| }); | |
| //--> | |
| </script> | |
| </article> | |
변경 전layer_goods_option_stock.php
변경 후layer_goods_option_stock.php
| <style> | |
| .bootstrap-dialog.ncds-modal:has(#layerShowGoodsOptionStock) .modal-dialog { | |
| top: 50% !important; | |
| transform: translateY(-50%) !important; | |
| } | |
| </style> | |
| <div class="modal-dialog__content"> | |
| <div class="ncua-table ncua-table--horizontal ncua-table--rounded"> | |
| <table> | |
| <colgroup> | |
| <col width="60px"> | |
| <?php | |
| foreach($optionName as $v){ | |
| ?><col width="*"><?php | |
| }; | |
| ?> | |
| <col width="80px"> | |
| <col width="80px"> | |
| <col width="80px"> | |
| <col width="80px"> | |
| <col width="120px"> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th><div>번호</div></th> | |
| <?php | |
| foreach($optionName as $v){ | |
| ?><th><div><?=gd_htmlspecialchars($v)?></div></th><?php | |
| }; | |
| ?> | |
| <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 (gd_isset($data) && is_array($data)) { | |
| foreach ($data as $key => $val) { | |
| $optionDisplay = '노출함'; | |
| if ($val['optionViewFl'] != 'y') $optionDisplay = '노출안함'; | |
| if($val['optionSellFl'] == 't'){ | |
| $optionSell = $stockReason[$val['optionSellCode']]; | |
| }else{ | |
| $optionSell = $stockReason[$val['optionSellFl']]; | |
| } | |
| ?> | |
| <tr> | |
| <td><div><?php echo number_format($index + 1);?></div></td> | |
| <?php | |
| for($i=1;$i<=5;$i++){ | |
| if(empty($val['optionValue'.$i])) continue; | |
| $tmpOption[] = $val['optionValue'.$i]; | |
| } | |
| foreach($tmpOption as $v){ | |
| ?><td><div><?=gd_htmlspecialchars($v)?></div></td><?php | |
| } | |
| unset ($tmpOption); | |
| ?> | |
| <td><div><?php echo number_format($val['optionPrice']);?> 원</div></td> | |
| <td><div><?php echo number_format($val['stockCnt']);?></div></td> | |
| <td><div><?=$optionDisplay?></div></td> | |
| <td><div><?=$optionSell?></div></td> | |
| <td><div><?php echo gd_htmlspecialchars($val['optionCode']); ?></div></td> | |
| </tr> | |
| <?php | |
| $index++; | |
| } | |
| } else { | |
| ?> | |
| <tr> | |
| <td colspan="8"><div>검색을 이용해 주세요.</div></td> | |
| </tr> | |
| <?php | |
| } | |
| ?> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
변경 전layer_goods.php
| <div class="modal-dialog__content layer_goods"> | |
| <article class="ncua-content"> | |
| <form id="layer_search_goods_frm"> | |
| <col width="100px"> | |
| <?php | |
| if($optionRegister === 'y'){ | |
| ?><col width="70px"><?php | |
| } | |
| ?> | |
| </colgroup> | |
| $checkboxType = 'checkbox'; | |
| if($optionRegister === 'y' || $mode == 'multiSearch') $checkboxType = 'radio'; | |
| $optionViewButton = '<p class="center"><input type="button" value="옵션보기" class="btn btn-hf btn-gray" onclick="show_goods_option_stock('.$val['goodsNo'].');"></p>'; | |
| if($val['optionFl'] === 'n') $optionViewButton = '<p class="center">사용안함</p>'; | |
| ?> | |
| <tr id="tbl_goods_<?= $val['goodsNo'] ?>"> | |
| <td> | |
| <div> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="<?=$checkboxType?>" id="layer_goods_<?= $val['goodsNo'] ?>" name="layer_goods_<?php if($checkboxType !== 'radio'){ echo $i; }?>" value="<?= $val['goodsNo'] ?>" /> | |
| </span> | |
| </label> | |
| </div> | |
| </td> | |
| <td><div><?= number_format($page->idx--) ?></div></td> | |
| <?php | |
| if($optionRegister === 'y') { | |
| ?> | |
| <td><?=$optionViewButton?></td> | |
| <?php | |
| } | |
| ?> | |
| ?> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| }; | |
| $('#<?= $layerFormID ?> input[id*=\'layer_goods_\']:checked').each(function() { | |
| if ('<?=$optionRegister?>' == 'y') { | |
| NCDSConfirm({message: '선택된 상품의 옵션정보를 적용하시겠습니까? 적용 시 기존 등록된 정보는 삭제되며 상품에 적용중인 옵션이 아닌 경우 복구가 불가능합니다.', callback: (result)=>{ | |
| if (result) { | |
| window.location.href='goods_register_option.php?copy=1&goodsNo=' + $(this).val(); | |
| displayTemplate(resultJson); | |
| <?php } ?> | |
| if (callbackResult === true) { | |
| shouldDisplayDuplicateAlert = false; | |
| } | |
| } else { | |
| $('#' + resultJson.parentFormID).removeClass('active'); | |
| } | |
| $('div.bootstrap-dialog-close-button').click(); | |
| } else { | |
| NCDSAlert({message: '동일한 상품이 이미 존재합니다.', iconType: 'error'}); | |
| } | |
| 'layerFormID' : 'layerShowGoodsOptionStock', | |
| 'goodsNo' : goodsNo | |
| }; | |
| $.get('../share/layer_goods_option_stock.php', parameters, function (data) { | |
| if (loadChk == 0) { | |
| data = '<div id="layerShowGoodsOptionStock">' + data + '</div>'; | |
| } | |
| var layerForm = data; | |
| ncds_layer_popup({message: layerForm, title: '옵션 재고 보기', size: 'wide'}); | |
| }); | |
| } | |
변경 후layer_goods.php
| <link rel="stylesheet" href="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/share/layer-goods.css')?>"> | |
| <div class="modal-dialog__content layer_goods"> | |
| <article class="ncua-content"> | |
| <form id="layer_search_goods_frm"> | |
| <col width="100px"> | |
| <?php | |
| if($optionRegister === 'y'){ | |
| ?><col width="90px"><?php | |
| } | |
| ?> | |
| </colgroup> | |
| $checkboxType = 'checkbox'; | |
| if($optionRegister === 'y' || $mode == 'multiSearch') $checkboxType = 'radio'; | |
| $optionViewButton = '<button type="button" class="ncua-btn ncua-btn--xxs ncua-btn--secondary-gray" onclick="show_goods_option_stock('.$val['goodsNo'].');"><span class="ncua-btn__label">옵션보기</span></button>'; | |
| if($val['optionFl'] === 'n') $optionViewButton = '사용안함'; | |
| ?> | |
| <tr id="tbl_goods_<?= $val['goodsNo'] ?>"> | |
| <td> | |
| <div> | |
| <?php if ($checkboxType === 'radio') { ?> | |
| <label class="ncua-radio-field ncua-radio-field--xs"> | |
| <span class="ncua-radio-input ncua-radio-input--xs ncua-radio-field__input"> | |
| <input type="radio" id="layer_goods_<?= $val['goodsNo'] ?>" name="layer_goods" value="<?= $val['goodsNo'] ?>" /> | |
| </span> | |
| </label> | |
| <?php } else { ?> | |
| <label class="ncua-checkbox-field ncua-checkbox-field--xs"> | |
| <span class="ncua-checkbox-input ncua-checkbox-input--xs ncua-checkbox-field__input"> | |
| <input type="checkbox" id="layer_goods_<?= $val['goodsNo'] ?>" name="layer_goods_<?= $i ?>" value="<?= $val['goodsNo'] ?>" /> | |
| </span> | |
| </label> | |
| <?php } ?> | |
| </div> | |
| </td> | |
| <td><div><?= number_format($page->idx--) ?></div></td> | |
| <?php | |
| if($optionRegister === 'y') { | |
| ?> | |
| <td><div><?=$optionViewButton?></div></td> | |
| <?php | |
| } | |
| ?> | |
| ?> | |
| </tbody> | |
| </table> | |
| <div class="ncua-layer-goods__footer-bar"></div> | |
| </div> | |
| </div> | |
| }; | |
| $('#<?= $layerFormID ?> input[id*=\'layer_goods_\']:checked').each(function() { | |
| if ('<?=$optionRegister?>' == 'y' && '<?=$callFunc?>' === '') { | |
| NCDSConfirm({message: '선택된 상품의 옵션정보를 적용하시겠습니까? 적용 시 기존 등록된 정보는 삭제되며 상품에 적용중인 옵션이 아닌 경우 복구가 불가능합니다.', callback: (result)=>{ | |
| if (result) { | |
| window.location.href='goods_register_option.php?copy=1&goodsNo=' + $(this).val(); | |
| displayTemplate(resultJson); | |
| <?php } ?> | |
| if (callbackResult === true || callbackResult === 'deferClose') { | |
| shouldDisplayDuplicateAlert = false; | |
| } | |
| } else { | |
| $('#' + resultJson.parentFormID).removeClass('active'); | |
| } | |
| // 'deferClose' 반환 콜백은 적용 성공 시점에 스스로 모달을 닫는다 (실패·취소 시 모달 유지) | |
| if (callbackResult !== 'deferClose') { | |
| $('div.bootstrap-dialog-close-button').click(); | |
| } | |
| } else { | |
| NCDSAlert({message: '동일한 상품이 이미 존재합니다.', iconType: 'error'}); | |
| } | |
| 'layerFormID' : 'layerShowGoodsOptionStock', | |
| 'goodsNo' : goodsNo | |
| }; | |
| $.get('../share/ncds/layer_goods_option_stock.php', parameters, function (data) { | |
| if (loadChk == 0) { | |
| data = '<div id="layerShowGoodsOptionStock">' + data + '</div>'; | |
| } | |
| var layerForm = data; | |
| ncds_layer_popup({message: layerForm, title: '옵션 상세보기', size: 'wide'}); | |
| }); | |
| } | |
변경 전CategoryAdmin.php
| return $realSetData; | |
| } | |
| } | |
변경 후CategoryAdmin.php
| return $realSetData; | |
| } | |
| public function getRootCateListJson(): string | |
| { | |
| $gGlobal = \Globals::get('gGlobal'); | |
| $useMallList = []; | |
| if (isset($gGlobal['useMallList'])) { | |
| $useMallList = array_combine(gd_array_column($gGlobal['useMallList'], 'sno'), $gGlobal['useMallList']); | |
| } | |
| $rootCateList = []; | |
| $rootCateData = $this->getCategoryData(null, null, 'cateCd,cateNm,mallDisplay', "length(cateCd) = '" . DEFAULT_LENGTH_CATE . "' AND divisionFl = 'n'", 'cateSort asc'); | |
| $hasChildrenSet = []; | |
| $nextLength = DEFAULT_LENGTH_CATE * 2; | |
| if ($nextLength <= DEFAULT_DEPTH_CATE * DEFAULT_LENGTH_CATE && is_array($rootCateData) && !empty($rootCateData)) { | |
| $childData = $this->getCategoryData(null, null, 'cateCd', "length(cateCd) = '" . $nextLength . "' AND divisionFl = 'n'", null); | |
| if (is_array($childData)) { | |
| foreach ($childData as $child) { | |
| $hasChildrenSet[substr($child['cateCd'], 0, DEFAULT_LENGTH_CATE)] = true; | |
| } | |
| } | |
| } | |
| if (is_array($rootCateData)) { | |
| foreach ($rootCateData as $val) { | |
| $mallSno = []; | |
| $mallName = []; | |
| foreach (explode(',', $val['mallDisplay'] ?? '') as $v) { | |
| if (!empty($useMallList[$v])) { | |
| $mallSno[] = $useMallList[$v]['domainFl']; | |
| $mallName[] = $useMallList[$v]['mallName']; | |
| } | |
| } | |
| $rootCateList[] = [ | |
| 'optionValue' => $val['cateCd'], | |
| 'optionText' => $val['cateNm'], | |
| 'flag' => implode(',', $mallSno), | |
| 'mallName' => implode(',', $mallName), | |
| 'hasChildren' => isset($hasChildrenSet[$val['cateCd']]), | |
| ]; | |
| } | |
| } | |
| return json_encode($rootCateList, JSON_UNESCAPED_UNICODE); | |
| } | |
| } | |
변경 전DBTableField.php
| return $arrField; | |
| } | |
| } | |
변경 후DBTableField.php
| return $arrField; | |
| } | |
| public static function tableSimpleGoods() | |
| { | |
| // @formatter:off | |
| $arrField = [ | |
| ['val' => 'goodsNo', 'typ' => 'i', 'def' => null, 'name' => '상품 번호'], | |
| ['val' => 'simpleGoodsFl', 'typ' => 's', 'def' => 'y', 'name' => '간편 상품 여부'], | |
| ['val' => 'regDt', 'typ' => 's', 'def' => null, 'name' => '등록일시'], | |
| ['val' => 'modDt', 'typ' => 's', 'def' => null, 'name' => '수정일시'], | |
| ]; | |
| // @formatter:on | |
| return $arrField; | |
| } | |
| } | |
변경 전GoodsAdmin.php
| return $getData; | |
| } | |
| /** | |
| * 상품의 등록 및 수정에 관련된 정보 (관리자 사용, 임시테이블의 옵션 정보만 불러옴) | |
| * | |
| $arrGoodsTable[] = DB_CART; // 장바구니 | |
| $arrGoodsTable[] = DB_GOODS_DELIVERY_SCHEDULE; // 상품 배송일 | |
| $arrGoodsTable[] = DB_FACEBOOK_GOODS_FEED; // 페이스북 상품 피드 설정 | |
| if($this->goodsDivisionFl) $arrGoodsTable[] = DB_GOODS_SEARCH; // 상품 검색 정보 | |
| if (gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
변경 후GoodsAdmin.php
| return $getData; | |
| } | |
| /** | |
| * 간편등록용 옵션 구성 반환 | |
| * | |
| * 기존 상품의 옵션 데이터를 간편등록 폼에서 사용할 수 있는 그룹 구조로 변환한다. | |
| * getDataGoodsOption()이 반환하는 원본 옵션은 모든 옵션값 조합(행 단위)으로 저장되어 있으므로, | |
| * 이를 옵션명(열) 기준으로 피벗하여 그룹별 고유값 목록으로 재구성한다. | |
| * | |
| * @param int $goodsNo 상품 번호 | |
| * @return array{ | |
| * optionDisplayFl: string, | |
| * groups: array<int, array{ | |
| * name: string, | |
| * values: array<int, array{val: string, price: string, image: string, url: string}> | |
| * }>, | |
| * combinations: array<int, array{ | |
| * key: string, | |
| * optionPrice: string, | |
| * optionCostPrice: string, | |
| * stockCnt: string, | |
| * optionViewFl: string, | |
| * optionSellFl: string, | |
| * optionDeliveryFl: string, | |
| * optionCode: string, | |
| * optionMemo: string | |
| * }> | |
| * }|null 옵션 미사용(optionFl != 'y') 또는 옵션 데이터 없으면 null | |
| */ | |
| public function getSimpleGoodsOptionGroups(int $goodsNo): ?array | |
| { | |
| // 상품 기본정보 + 옵션 전체 조합 조회 (getGoodsOption으로 DB에서 가져온 행 단위 데이터) | |
| $optionData = $this->getDataGoodsOption($goodsNo); | |
| $goodsData = $optionData['data']; | |
| // 옵션 미사용이거나 옵션 데이터가 없으면 간편등록에서 옵션 영역 비표시 | |
| if ($goodsData['optionFl'] !== 'y' || empty($goodsData['option']) || empty($goodsData['optionName'])) { | |
| return null; | |
| } | |
| // optionName: "색상|사이즈" 같은 구분자 문자열 → 배열로 변환 | |
| // (getDataGoodsOption 내부에서 이미 explode 했을 수 있으므로 배열 여부 체크) | |
| $optionNames = is_array($goodsData['optionName']) ? $goodsData['optionName'] : explode(STR_DIVISION, $goodsData['optionName']); | |
| $groups = []; | |
| $optionNameCount = count($optionNames); | |
| // optionIcon lookup: "optionNo|optionValue" → goodsImage | |
| $iconLookup = []; | |
| if (!empty($goodsData['optionIcon'])) { | |
| foreach ($goodsData['optionIcon'] as $icon) { | |
| $iconKey = ($icon['optionNo'] ?? '') . '|' . ($icon['optionValue'] ?? ''); | |
| $iconLookup[$iconKey] = $icon['goodsImage'] ?? ''; | |
| } | |
| } | |
| // 옵션명별로 그룹 생성 — idx=0 → optionValue1, idx=1 → optionValue2, ... | |
| // DB의 option 행(조합)에서 해당 컬럼의 고유값만 추출 | |
| foreach ($optionNames as $idx => $name) { | |
| $col = 'optionValue' . ($idx + 1); | |
| $seen = []; | |
| $values = []; | |
| // 모든 옵션 조합 행을 순회하며 해당 컬럼의 중복 없는 값 목록 구성 | |
| foreach ($goodsData['option'] as $combo) { | |
| $val = $combo[$col] ?? ''; | |
| if ($val !== '' && !isset($seen[$val])) { | |
| $seen[$val] = true; | |
| $image = ''; | |
| $url = ''; | |
| $iconKey = $idx . '|' . $val; | |
| if (isset($iconLookup[$iconKey]) && $iconLookup[$iconKey] !== '') { | |
| if (strtolower(substr($iconLookup[$iconKey], 0, 4)) === 'http') { | |
| $url = $iconLookup[$iconKey]; | |
| } else { | |
| $image = $iconLookup[$iconKey]; | |
| } | |
| } | |
| $values[] = ['val' => $val, 'price' => '0', 'image' => $image, 'url' => $url]; | |
| } | |
| } | |
| $groups[] = ['name' => trim($name), 'values' => $values]; | |
| } | |
| // 조합별 옵션 정보(옵션가·재고량·노출상태 등) — 일반등록의 fill_value_layer() 대응 | |
| $combinations = []; | |
| foreach ($goodsData['option'] as $combo) { | |
| $keyParts = []; | |
| for ($i = 1; $i <= $optionNameCount; $i++) { | |
| $keyParts[] = $combo['optionValue' . $i] ?? ''; | |
| } | |
| // optionSellFl/optionDeliveryFl 가 't'(사유코드)이면 코드값 사용 | |
| $sellFl = ($combo['optionSellFl'] ?? '') === 't' ? ($combo['optionSellCode'] ?? 'y') : ($combo['optionSellFl'] ?? 'y'); | |
| $deliveryFl = ($combo['optionDeliveryFl'] ?? '') === 't' ? ($combo['optionDeliveryCode'] ?? 'normal') : ($combo['optionDeliveryFl'] ?? 'normal'); | |
| $combinations[] = [ | |
| 'key' => implode('|', $keyParts), | |
| 'optionPrice' => $combo['optionPrice'] ?? '0', | |
| 'optionCostPrice' => $combo['optionCostPrice'] ?? '0', | |
| 'stockCnt' => $combo['stockCnt'] ?? '0', | |
| 'optionViewFl' => $combo['optionViewFl'] ?? 'y', | |
| 'optionSellFl' => $sellFl, | |
| 'optionDeliveryFl'=> $deliveryFl, | |
| 'optionCode' => $combo['optionCode'] ?? '', | |
| 'optionMemo' => $combo['optionMemo'] ?? '', | |
| ]; | |
| } | |
| // optionDisplayFl: 옵션 표시 방식 (s=select, r=radio 등), 기본값 's' | |
| return [ | |
| 'optionDisplayFl' => $goodsData['optionDisplayFl'] ?? 's', | |
| 'groups' => $groups, | |
| 'combinations' => $combinations, | |
| 'imageStorage' => $goodsData['imageStorage'] ?? '', | |
| 'imagePath' => $goodsData['imagePath'] ?? (DIR_GOODS_IMAGE . $goodsNo . '/'), | |
| ]; | |
| } | |
| /** | |
| * 상품의 등록 및 수정에 관련된 정보 (관리자 사용, 임시테이블의 옵션 정보만 불러옴) | |
| * | |
| $arrGoodsTable[] = DB_CART; // 장바구니 | |
| $arrGoodsTable[] = DB_GOODS_DELIVERY_SCHEDULE; // 상품 배송일 | |
| $arrGoodsTable[] = DB_FACEBOOK_GOODS_FEED; // 페이스북 상품 피드 설정 | |
| $arrGoodsTable[] = DB_SIMPLE_GOODS; // 간편 상품 | |
| if($this->goodsDivisionFl) $arrGoodsTable[] = DB_GOODS_SEARCH; // 상품 검색 정보 | |
| if (gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
변경 전SimpleGoodsDAO.php
변경 후SimpleGoodsDAO.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\Component\Goods; | |
| use Bundle\Component\Database\DBTableField; | |
| use Framework\Database\DBManager; | |
| class SimpleGoodsDAO | |
| { | |
| public function __construct( | |
| private readonly DBManager $db | |
| ) { | |
| } | |
| public function insertSimpleGoods(int $goodsNo): void | |
| { | |
| $arrData = [ | |
| 'goodsNo' => $goodsNo, | |
| 'simpleGoodsFl' => 'y' | |
| ]; | |
| $arrBind = $this->db->get_binding(DBTableField::tableSimpleGoods(), $arrData, 'insert'); | |
| $this->db->set_insert_db(DB_SIMPLE_GOODS, $arrBind['param'], $arrBind['bind'], 'y'); | |
| } | |
| /** | |
| * 옵션 이미지 임시 세션 문자열 생성 (32자리, DB_GOODS_OPTION_ICON_TEMP 기준 중복 회피) | |
| * | |
| * 일반상품등록 옵션 팝업의 세션 생성(GoodsAdmin::getSessionString)과 동일한 규칙으로 생성. | |
| */ | |
| public function generateOptionIconTempSession(): string | |
| { | |
| do { | |
| $session = ''; | |
| for ($i = 0; $i < 32; $i++) { | |
| switch (random_int(0, 2)) { | |
| case 0: | |
| $session .= chr(random_int(48, 57)); // 0-9 | |
| break; | |
| case 1: | |
| $session .= chr(random_int(65, 90)); // A-Z | |
| break; | |
| default: | |
| $session .= chr(random_int(97, 122)); // a-z | |
| break; | |
| } | |
| } | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind['bind'], 's', $session); | |
| $strSQL = 'SELECT COUNT(*) cnt FROM ' . DB_GOODS_OPTION_ICON_TEMP . ' WHERE session = ?'; | |
| $row = $this->db->query_fetch($strSQL, $arrBind['bind'], false); | |
| } while (($row['cnt'] ?? 0) != 0); | |
| return $session; | |
| } | |
| /** | |
| * 옵션 이미지 임시테이블(DB_GOODS_OPTION_ICON_TEMP) 다건 INSERT | |
| * | |
| * @param array $rows [['session','optionNo','optionValue','goodsImage','isUpdated'], ...] | |
| */ | |
| public function insertOptionIconTempRows(array $rows): void | |
| { | |
| foreach ($rows as $row) { | |
| $arrBind = $this->db->get_binding(DBTableField::tableGoodsOptionIconTemp(), $row, 'insert'); | |
| $this->db->set_insert_db(DB_GOODS_OPTION_ICON_TEMP, $arrBind['param'], $arrBind['bind'], 'y'); | |
| } | |
| } | |
| } | |
변경 전SimpleGoods.php
변경 후SimpleGoods.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\Component\Goods; | |
| use Framework\Log\Logger; | |
| use Framework\Utility\StringUtils; | |
| use Request; | |
| use Session; | |
| class SimpleGoods | |
| { | |
| /** 과세/면세 폴백 기본값 (DBTableField::tableGoods 의 폴백과 동일하게 유지) */ | |
| private const DEFAULT_TAX_FREE_FL = 't'; | |
| private const DEFAULT_TAX_PERCENT = 10; | |
| public function __construct( | |
| private readonly SimpleGoodsDAO $dao, | |
| private readonly Logger $logger | |
| ) { | |
| } | |
| public function registerSimpleGoods(int $goodsNo): void | |
| { | |
| try { | |
| $this->dao->insertSimpleGoods($goodsNo); | |
| } catch (\Throwable $e) { | |
| $this->logger->channel('simpleGoods')->warning(__METHOD__ . ' 간편등록 테이블 INSERT 실패 goodsNo: ' . $goodsNo, ['exception' => $e->getMessage()]); | |
| } | |
| } | |
| /** | |
| * 간편상품등록 전용 기본값 세팅 | |
| * | |
| * 간편등록 폼은 UI에 없는 필드를 미전송하므로, 일반상품등록 신규 폼과 동일한 | |
| * 기본값을 POST 데이터에 채워 저장 로직(saveInfoGoodsAndUpdateRegularGoods)에 넘긴다. | |
| * | |
| * @param array $postValue 간편등록 폼 POST 데이터 | |
| * @return array 기본값이 병합된 POST 데이터 | |
| */ | |
| public function prepareRegisterPostValue(array $postValue): array | |
| { | |
| $goods = \App::load('\\Component\\Goods\\GoodsAdmin'); | |
| // 상품이미지 사이즈 | |
| $simpleImageConf = gd_policy('goods.image'); | |
| $simpleImageType = gd_isset($simpleImageConf['imageType']); | |
| $postValue['imageSize'] = []; | |
| foreach ($simpleImageConf as $imgKey => $imgVal) { | |
| if (is_array($imgVal) === false || isset($imgVal['size1']) === false || $imgVal['size1'] === '') { | |
| continue; | |
| } | |
| if ($simpleImageType === 'fixed') { | |
| $postValue['imageSize'][$imgKey] = $imgVal['size1'] . INT_DIVISION . gd_isset($imgVal['hsize1']); | |
| } else { | |
| $postValue['imageSize'][$imgKey] = $imgVal['size1']; | |
| } | |
| } | |
| // 이미지 리사이즈: 간편상품등록은 항상 자동 리사이즈로 저장 (original + 각 이미지 종류로 fan-out) | |
| $postValue['imageResize'] = ['original' => 'y']; | |
| foreach ($simpleImageConf as $imgKey => $imgVal) { | |
| if (is_array($imgVal) === false) { | |
| continue; | |
| } | |
| $postValue['imageResize'][$imgKey] = 'y'; | |
| } | |
| // 간편상품등록 기본값 세팅 | |
| if (empty($postValue['goodsNmFl'])) { | |
| $postValue['goodsNmFl'] = 'd'; | |
| } | |
| if (empty($postValue['commission'])) { | |
| $postValue['commission'] = '0'; | |
| } | |
| // 본사가 공급사 선택시 수수료 처리 | |
| if (!empty($postValue['scmNo']) && $postValue['scmNo'] != DEFAULT_CODE_SCMNO) { | |
| $scmNo = (int)$postValue['scmNo']; | |
| if ($scmNo > 0) { | |
| $scm = \App::load('\\Component\\Scm\\ScmAdmin'); | |
| $scmInfo = $scm->getScmInfo($scmNo, 'scmCommission'); | |
| if (!empty($scmInfo)) { | |
| $postValue['commission'] = $scmInfo['scmCommission']; | |
| } | |
| } | |
| } | |
| // 메인상품 진열 / 인기상품 포함 기본값 (본사만 처리 — 일반상품등록과 동일) | |
| if (!Session::get('manager.isProvider')) { | |
| $mainDisplayTheme = $goods->getAdminMainDisplayTheme(); | |
| if (is_array($mainDisplayTheme)) { | |
| foreach ($mainDisplayTheme as $theme) { | |
| if ($theme['sortAutoFl'] == 'y') { | |
| $postValue['displayThemeSno'][] = $theme['sno']; | |
| } | |
| } | |
| } | |
| $populate = \App::load('\\Component\\Goods\\Populate'); | |
| $populateInfo = $populate->getPopulateData(); | |
| if (is_array($populateInfo)) { | |
| foreach ($populateInfo as $pInfo) { | |
| if ($pInfo['range'] == 'all') { | |
| $postValue['populateListSno'][] = $pInfo['sno']; | |
| } | |
| } | |
| } | |
| } | |
| // 성인인증/접근권한 노출 기본값 | |
| if (empty($postValue['onlyAdultDisplayFl'])) { | |
| $postValue['onlyAdultDisplayFl'] = 'y'; | |
| } | |
| if (empty($postValue['goodsAccessDisplayFl'])) { | |
| $postValue['goodsAccessDisplayFl'] = 'y'; | |
| } | |
| // 네이버페이 사용/적립 기본값 | |
| if (empty($postValue['naverNpayAble'])) { | |
| $postValue['naverNpayAble'] = ['pc', 'mobile']; | |
| } | |
| if (empty($postValue['naverNpayAcumAble'])) { | |
| $postValue['naverNpayAcumAble'] = ['pc', 'mobile']; | |
| } | |
| // 네이버 연령대 기본값 (간편등록은 UI가 없어 미전송 → 일반등록 신규 폼과 동일: 전체 'a') | |
| if (empty($postValue['naverAgeGroup'])) { | |
| $postValue['naverAgeGroup'] = 'a'; | |
| } | |
| // 페이스북 제품 피드 사용 기본값 (일반등록 신규 폼과 동일: 설정안함 'n') | |
| if (empty($postValue['fbUseFl'])) { | |
| $postValue['fbUseFl'] = 'n'; | |
| } | |
| // 구글 쇼핑 상품 피드 사용 기본값 (일반등록 신규 폼과 동일: 설정함 'y') | |
| if (empty($postValue['googleUseFl'])) { | |
| $postValue['googleUseFl'] = 'y'; | |
| } | |
| // SEO 태그 기본값 (일반등록 신규 폼과 동일: 사용안함 'n'. | |
| // seoTagFl 세팅 시 저장 로직이 SEO 레코드를 생성하므로 seoTag는 빈 배열로 전달) | |
| if (empty($postValue['seoTagFl'])) { | |
| $postValue['seoTagFl'] = 'n'; | |
| } | |
| if (empty($postValue['seoTag']) || !is_array($postValue['seoTag'])) { | |
| $postValue['seoTag'] = []; | |
| } | |
| // HS코드 기본값 (간편등록은 UI가 없어 미전송 → 저장 로직의 array_map(trim, hscode)가 | |
| // PHP8에서 TypeError 발생. 일반등록 신규와 동일하게 빈 배열로 세팅 → 최종 저장값 "") | |
| if (empty($postValue['hscode']) || !is_array($postValue['hscode'])) { | |
| $postValue['hscode'] = []; | |
| } | |
| if (empty($postValue['hscodeNation']) || !is_array($postValue['hscodeNation'])) { | |
| $postValue['hscodeNation'] = []; | |
| } | |
| // 옵션 미사용 시 optionN 기본값 (간편등록은 UI가 없어 미전송 → 일반등록 신규 폼의 hidden과 동일 | |
| if (empty($postValue['optionN']) || !is_array($postValue['optionN'])) { | |
| $postValue['optionN'] = ['sno' => [''], 'optionNo' => ['']]; | |
| } | |
| // KC인증 기본값 (간편등록은 UI가 없어 미전송 → saveInfoGoods의 kcmarkInfo foreach가 | |
| // null 순회로 Warning 발생 및 "null" 저장. 일반등록 미사용('n') 제출과 동일하게 세팅) | |
| if (empty($postValue['kcmarkInfo']['kcmarkFl'])) { | |
| $postValue['kcmarkInfo']['kcmarkFl'] = 'n'; | |
| } | |
| if (empty($postValue['kcmarkInfo']['kcmarkNo']) || !is_array($postValue['kcmarkInfo']['kcmarkNo'])) { | |
| $postValue['kcmarkInfo']['kcmarkNo'] = ['']; | |
| } | |
| if (empty($postValue['kcmarkInfo']['kcmarkDivFl']) || !is_array($postValue['kcmarkInfo']['kcmarkDivFl'])) { | |
| $postValue['kcmarkInfo']['kcmarkDivFl'] = ['']; | |
| } | |
| if (empty($postValue['kcmarkDt']) || !is_array($postValue['kcmarkDt'])) { | |
| $postValue['kcmarkDt'] = ['']; | |
| } | |
| // 배송/AS/반품/교환 이용안내 기본값 (간편등록은 UI가 없어 미전송 → | |
| // 일반등록 신규 폼과 동일하게 몰에 설정된 기본 이용안내를 자동 적용. | |
| $buyerInform = \App::load('\\Component\\Agreement\\BuyerInform'); | |
| $goodsInfoDetail = $buyerInform->getGoodsInfoCode('register', $postValue['scmNo'] ?? null); | |
| foreach (['detailInfoDelivery', 'detailInfoAS', 'detailInfoRefund', 'detailInfoExchange'] as $detailInfoKey) { | |
| if (!empty($postValue[$detailInfoKey . 'Fl'])) { | |
| continue; | |
| } | |
| if (!empty($goodsInfoDetail['default'][$detailInfoKey])) { | |
| $postValue[$detailInfoKey] = $goodsInfoDetail['default'][$detailInfoKey]; | |
| $postValue[$detailInfoKey . 'Fl'] = 'selection'; | |
| } else { | |
| $postValue[$detailInfoKey . 'Fl'] = 'no'; | |
| } | |
| } | |
| // 과세/면세 정책 기본값 반영 | |
| $taxConf = gd_policy('goods.tax'); | |
| if (empty($postValue['taxFreeFl'])) { | |
| $postValue['taxFreeFl'] = empty($taxConf['taxFreeFl']) ? self::DEFAULT_TAX_FREE_FL : $taxConf['taxFreeFl']; | |
| } | |
| if (empty($postValue['taxPercent'])) { | |
| $postValue['taxPercent'] = empty($taxConf['taxPercent']) ? self::DEFAULT_TAX_PERCENT : $taxConf['taxPercent']; | |
| } | |
| // 간편상품등록: optionYIcon POST 데이터 초기화 (기존 상품등록은 옵션 팝업에서 설정) | |
| $postValue['optionYIcon']['optionImageAddUrl'] = ''; | |
| $postValue['optionYIcon']['goodsImage'][0] = $postValue['optionYIcon']['goodsImage'][0] ?? []; | |
| // 옵션 이미지를 일반상품등록과 동일한 임시세션(temp) 경로로 브릿지 | |
| $postValue = $this->prepareOptionImageTempSession($postValue); | |
| // 상세설명 XSS 대응 | |
| foreach (['goodsDescription', 'goodsDescriptionMobile'] as $descKey) { | |
| if (!empty($postValue[$descKey]) && is_string($postValue[$descKey])) { | |
| $postValue[$descKey] = StringUtils::xssClean($postValue[$descKey]); | |
| } | |
| } | |
| return $postValue; | |
| } | |
| /** | |
| * 간편상품등록 옵션 이미지 → 일반상품등록(temp세션) 경로로 브릿지 | |
| * | |
| * 간편등록 폼은 옵션 팝업을 쓰지 않고 옵션 이미지를 인라인으로 전송한다. | |
| * - optionYIcon[goodsImage][0][] : 신규 업로드 파일 ($_FILES) | |
| * - optionYIcon[goodsImageText][0][] : URL 직접입력 | |
| * - sgrExistingImage[0][] : 기존상품에서 불러온 이미지 파일명 (이미 option_temp/에 복사됨) | |
| * | |
| * 이를 일반상품등록의 옵션 팝업(GoodsAdmin::goodsOptionTempRegister)과 동일하게 option_temp/ 폴더 + | |
| * DB_GOODS_OPTION_ICON_TEMP 임시테이블에 세션 단위로 적재하고 optionTempSession 을 세팅한다. | |
| * 이후 GoodsAdmin 은 팝업 등록과 동일한 정상 경로(임시테이블 조회)로 옵션 이미지를 처리하므로 | |
| * GoodsAdmin 에 간편등록 전용 분기가 필요없다. | |
| * | |
| * 옵션 이미지는 대표(첫번째) 옵션 그룹에만 부여되므로 그룹 인덱스 0 만 처리한다. | |
| * | |
| * @param array $postValue 간편등록 폼 POST 데이터 | |
| * @return array optionTempSession 이 세팅된 POST 데이터 | |
| */ | |
| public function prepareOptionImageTempSession(array $postValue): array | |
| { | |
| // 옵션 미사용이면 옵션 이미지 처리 불필요 | |
| if (($postValue['optionFl'] ?? '') !== 'y') { | |
| return $postValue; | |
| } | |
| $optionFiles = Request::files()->get('optionYIcon') ?? []; // $_FILES[optionYIcon] | |
| $uploadNames = $optionFiles['name']['goodsImage'][0] ?? []; // [vKey => 업로드 파일명] | |
| $urlImages = $postValue['optionYIcon']['goodsImageText'][0] ?? []; | |
| $existingImages = $postValue['sgrExistingImage'][0] ?? []; | |
| $optionValues = $postValue['optionY']['optionValue'][0] ?? []; | |
| $isUrlMode = ($postValue['optionImageAddUrl'] ?? '') === 'y'; | |
| // 대표 옵션 그룹(0)에서 처리할 옵션값 인덱스(vKey) 집합 | |
| $valueKeys = array_unique(array_merge( | |
| array_keys($uploadNames), | |
| array_keys($urlImages), | |
| array_keys($existingImages) | |
| )); | |
| if (empty($valueKeys)) { | |
| return $postValue; | |
| } | |
| $optionTempPath = \App::getUserBasePath() . '/data/goods/option_temp'; | |
| $session = $this->dao->generateOptionIconTempSession(); | |
| $iconTempRows = []; | |
| foreach ($valueKeys as $vKey) { | |
| $goodsImage = null; | |
| if ($isUrlMode && !empty($urlImages[$vKey])) { | |
| // URL 직접입력 | |
| $goodsImage = $urlImages[$vKey]; | |
| } elseif (gd_file_uploadable($optionFiles, 'image', 'goodsImage', 0, $vKey)) { | |
| // 신규 업로드 파일 → option_temp/ 로 이동 (팝업 등록과 동일한 세션 기반 파일명) | |
| if (!file_exists($optionTempPath)) { | |
| mkdir($optionTempPath); | |
| } | |
| $fileExt = pathinfo($uploadNames[$vKey], PATHINFO_EXTENSION); | |
| $fileName = $session . '_' . $vKey . '.' . $fileExt; | |
| if (move_uploaded_file($optionFiles['tmp_name']['goodsImage'][0][$vKey], $optionTempPath . DIRECTORY_SEPARATOR . $fileName)) { | |
| $goodsImage = $fileName; | |
| } | |
| } | |
| // 신규 업로드/URL 이 없으면 기존상품에서 불러온 이미지 사용 (이미 option_temp/에 복사되어 있음) | |
| if ($goodsImage === null && !empty($existingImages[$vKey])) { | |
| $goodsImage = $existingImages[$vKey]; | |
| } | |
| if ($goodsImage === null) { | |
| continue; | |
| } | |
| $iconTempRows[] = [ | |
| 'session' => $session, | |
| 'optionNo' => $vKey, | |
| 'optionValue' => (string) ($optionValues[$vKey] ?? ''), | |
| 'goodsImage' => $goodsImage, | |
| 'isUpdated' => 'y', | |
| ]; | |
| } | |
| if (!empty($iconTempRows)) { | |
| $this->dao->insertOptionIconTempRows($iconTempRows); | |
| $postValue['optionTempSession'] = $session; | |
| } | |
| return $postValue; | |
| } | |
| } | |
변경 전GoodsPsController.php
| use Framework\Debug\Exception\LayerNotReloadException; | |
| use Component\RegularDelivery\RegularGoods\RegularGoodsHandler; | |
| use Exception; | |
| use Message; | |
| use Request; | |
| use Session; | |
| try { | |
| switch ($postValue['mode']) { | |
| // 상품 등록 / 수정 | |
| case 'register': | |
| case 'modify': | |
| exit; | |
| break; | |
| case 'get_category_flag' : | |
| $cate = \App::load('\\Component\\Category\\CategoryAdmin'); | |
| $result = $cate->getCategoryFlag($postValue['cateCd']); | |
변경 후GoodsPsController.php
| use Framework\Debug\Exception\LayerNotReloadException; | |
| use Component\RegularDelivery\RegularGoods\RegularGoodsHandler; | |
| use Exception; | |
| use Framework\Security\Token; | |
| use Message; | |
| use Request; | |
| use Session; | |
| try { | |
| switch ($postValue['mode']) { | |
| // 간편 상품 등록 | |
| case 'simple_register': | |
| $httpOrigin = Request::server()->get('HTTP_ORIGIN'); | |
| if (!empty($httpOrigin) && stripos($httpOrigin, $this->getDefalutDomain()) === false) { | |
| $this->layer(__('허용되지 않은 도메인에서의 접근입니다.')); | |
| } | |
| if (!Token::check('simpleGoodsToken', Request::post()->toArray(), false, null, true)) { | |
| $this->layer(__('잘못된 경로로 접근하셨습니다.')); | |
| } | |
| $postValue['mode'] = 'register'; | |
| $simpleGoods = \App::getInstance(\Component\Goods\SimpleGoods::class); | |
| // 간편상품등록 전용 기본값 세팅 | |
| $postValue = $simpleGoods->prepareRegisterPostValue($postValue); | |
| $applyFl = $goods->saveInfoGoodsAndUpdateRegularGoods($postValue); | |
| $simpleGoods->registerSimpleGoods($goods->goodsNo); | |
| if ($applyFl == 'a') { | |
| $this->layer(__('승인 대기 상태로 저장되었습니다.')); | |
| } else { | |
| $this->layer(__('저장되었습니다.')); | |
| } | |
| break; | |
| // 상품 등록 / 수정 | |
| case 'register': | |
| case 'modify': | |
| exit; | |
| break; | |
| // 간편등록 - 기존상품 옵션 불러오기: 선택 상품의 옵션 구성 반환 | |
| case 'sgr_get_goods_option': | |
| if (empty($postValue['goodsNo'])) { | |
| $this->json(['error' => true, 'message' => __('상품번호가 없습니다.')]); | |
| exit; | |
| } | |
| $result = $goods->getSimpleGoodsOptionGroups($postValue['goodsNo']); | |
| if ($result === null) { | |
| $this->json(['error' => true, 'message' => __('옵션이 없는 상품입니다.')]); | |
| exit; | |
| } | |
| // 로컬 옵션 이미지를 option_temp/에 복사 (저장 시 imageUploadIcon이 처리) | |
| if (!empty($result['imageStorage']) && $result['imageStorage'] !== 'url') { | |
| $optionTempPath = \App::getUserBasePath() . '/data/goods/option_temp'; | |
| if (!is_dir($optionTempPath)) { | |
| @mkdir($optionTempPath, 0777, true); | |
| } | |
| $disk = Storage::disk(Storage::PATH_CODE_GOODS, $result['imageStorage']); | |
| foreach ($result['groups'] as $group) { | |
| foreach ($group['values'] as $v) { | |
| if (!empty($v['image'])) { | |
| $srcFile = $disk->getRealPath($result['imagePath'] . $v['image']); | |
| if ($srcFile && file_exists($srcFile)) { | |
| @copy($srcFile, $optionTempPath . '/' . $v['image']); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| unset($result['imageStorage'], $result['imagePath']); | |
| $this->json($result); | |
| exit; | |
| case 'get_category_flag' : | |
| $cate = \App::load('\\Component\\Category\\CategoryAdmin'); | |
| $result = $cate->getCategoryFlag($postValue['cateCd']); | |
변경 전LayerGoodsOptionListController.php
변경 후LayerGoodsOptionListController.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\Goods\Ncds; | |
| class LayerGoodsOptionListController extends \Controller\Admin\Goods\LayerGoodsOptionListController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| $callFunc = \Request::get()->get('callFunc', ''); | |
| $this->setData('callFunc', $callFunc); | |
| // getAdminListOption('layer') 모드는 SQL LIMIT을 적용하지 않으므로 직접 페이지네이션 처리 | |
| $data = $this->getData('data'); | |
| $page = $this->getData('page'); | |
| if (is_array($data) && $page) { | |
| $start = max(0, (int)$page->recode['start']); | |
| $limit = max(1, min(100, (int)$page->page['list'])); // 최대 100개로 제한 | |
| $this->setData('data', array_slice($data, $start, $limit)); | |
| } | |
| $this->getView()->setPageName('goods/ncds/layer_goods_option_list.php'); | |
| } | |
| } | |
변경 전LayerGoodsOptionRegisterController.php
변경 후LayerGoodsOptionRegisterController.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\Goods\Ncds; | |
| class LayerGoodsOptionRegisterController extends \Controller\Admin\Goods\LayerGoodsOptionRegisterController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| $this->getView()->setPageName('goods/ncds/layer_goods_option_register.php'); | |
| } | |
| } | |
변경 전SimpleGoodsRegisterController.php
변경 후SimpleGoodsRegisterController.php
| <?php | |
| namespace Bundle\Controller\Admin\Goods; | |
| use Framework\Security\Token; | |
| use Framework\Utility\ImageUtils; | |
| use Globals; | |
| use Session; | |
| /** | |
| * 간편상품등록 컨트롤러 | |
| */ | |
| class SimpleGoodsRegisterController extends \Controller\Admin\Controller | |
| { | |
| public function index() | |
| { | |
| $this->callMenu('goods', 'goods', 'simpleGoodsRegister'); | |
| // 이미지 정책/저장소 설정 | |
| $conf['image'] = gd_policy('goods.image'); | |
| foreach ($conf['image'] as $k => $v) { | |
| foreach ($v as $key => $value) { | |
| if (stripos($key, 'size') === 0) { | |
| if ($conf['image']['imageType'] === 'fixed') { | |
| $conf['image'][$k]['fixed' . $key] = [$value, $conf['image'][$k]['h' . $key]]; | |
| unset($conf['image'][$k]['h' . $key]); | |
| } | |
| } | |
| } | |
| if (stripos($k, 'imageType') === 0) { | |
| $imageType = $conf['image']['imageType']; | |
| unset($conf['image']['imageType']); | |
| } | |
| unset($conf['image'][$k]['sequenceNumber']); | |
| } | |
| $tmpStorageConf = gd_policy('basic.storage'); | |
| $defaultImageStorage = ''; | |
| // 기본 정보 | |
| if (gd_is_provider() === true) { | |
| $scmAdmin = \App::load(\Component\Scm\ScmAdmin::class); | |
| $scmNo = Session::get('manager.scmNo'); | |
| $scmData = $scmAdmin->getScm($scmNo); | |
| } | |
| if (empty($tmpStorageConf['storageDefault'])) { | |
| $tmpStorageConf['storageDefault'] = ['imageStorage0' => ['goods']]; | |
| } | |
| foreach ($tmpStorageConf['storageDefault'] as $index => $item) { | |
| if (in_array('goods', $item)) { | |
| if (gd_isset($scmData['imageStorage'])) { | |
| $defaultImageStorage = $scmData['imageStorage']; | |
| } else { | |
| $defaultImageStorage = $tmpStorageConf['httpUrl'][$index]; | |
| } | |
| } | |
| } | |
| unset($tmpStorageConf); | |
| ImageUtils::sortImageConf($conf['image']); | |
| // 카테고리 데이터 | |
| $cate = \App::load('\\Component\\Category\\CategoryAdmin'); | |
| $rootCateList = $cate->getRootCateListJson(); | |
| $gGlobal = Globals::get('gGlobal'); | |
| $mallData = []; | |
| foreach ($gGlobal['useMallList'] as $mall) { | |
| $mallData[] = [ | |
| 'sno' => $mall['sno'], | |
| 'mallName' => $mall['mallName'], | |
| 'domainFl' => $mall['domainFl'], | |
| 'standardFl' => $mall['standardFl'], | |
| ]; | |
| } | |
| // 품절/배송 상태 코드 | |
| $request = \App::getInstance('request'); | |
| $mallSno = $request->get()->get('mallSno', 1); | |
| $codeComponent = \App::load('\\Component\\Code\\Code', $mallSno); | |
| $stockReason = $codeComponent->getGroupItems('05002'); | |
| $stockReasonNew['y'] = $stockReason['05002001']; //정상은 코드 변경 | |
| $stockReasonNew['n'] = $stockReason['05002002']; //품절은 코드 변경 | |
| unset($stockReason['05002001']); | |
| unset($stockReason['05002002']); | |
| $stockReason = gd_array_merge($stockReasonNew, $stockReason); | |
| $deliveryReason = $codeComponent->getGroupItems('05003'); | |
| $deliveryReasonNew['normal'] = $deliveryReason['05003001']; //정상은 코드 변경 | |
| unset($deliveryReason['05003001']); | |
| $deliveryReason = gd_array_merge($deliveryReasonNew, $deliveryReason); | |
| $isProvider = gd_is_provider(); | |
| $mobileConfig = gd_policy('mobile.config'); | |
| $mobileShopFl = gd_isset($mobileConfig['mobileShopFl'], 'n'); | |
| $this->setData('scmNo', gd_isset($scmNo, '')); | |
| $this->setData('isProvider', $isProvider); | |
| $this->setData('mobileShopFl', $mobileShopFl); | |
| $this->setData('cate', $cate); | |
| $this->setData('rootCateList', $rootCateList); | |
| $this->setData('mallData', json_encode($mallData, JSON_UNESCAPED_UNICODE)); | |
| $this->setData('conf', $conf); | |
| $this->setData('imageType', gd_isset($imageType)); | |
| $this->setData('defaultImageStorage', $defaultImageStorage); | |
| $this->setData('stockReason', $stockReason); | |
| $this->setData('deliveryReason', $deliveryReason); | |
| $this->setData('simpleGoodsToken', Token::generate('simpleGoodsToken')); | |
| // 공급사와 동일한 페이지 사용 | |
| $this->getView()->setPageName('goods/simple_goods_register.php'); | |
| $this->setData('adminBodySubClass', 'ncds'); | |
| } | |
| } | |
변경 전LayerGoodsOptionListController.php
변경 후LayerGoodsOptionListController.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\Provider\Goods\Ncds; | |
| class LayerGoodsOptionListController extends \Controller\Admin\Goods\Ncds\LayerGoodsOptionListController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerGoodsOptionRegisterController.php
변경 후LayerGoodsOptionRegisterController.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\Provider\Goods\Ncds; | |
| class LayerGoodsOptionRegisterController extends \Controller\Admin\Goods\Ncds\LayerGoodsOptionRegisterController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전SimpleGoodsRegisterController.php
변경 후SimpleGoodsRegisterController.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 ⓒ 2024, NHN COMMERCE Corp. | |
| * @link http://www.godo.co.kr | |
| */ | |
| namespace Bundle\Controller\Admin\Provider\Goods; | |
| /** | |
| * 공급사 간편상품등록 컨트롤러 | |
| */ | |
| class SimpleGoodsRegisterController extends \Controller\Admin\Goods\SimpleGoodsRegisterController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerBrandController.php
변경 후LayerBrandController.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\Provider\Share\Ncds; | |
| class LayerBrandController extends \Controller\Admin\Share\Ncds\LayerBrandController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerCategoryController.php
변경 후LayerCategoryController.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\Provider\Share\Ncds; | |
| class LayerCategoryController extends \Controller\Admin\Share\Ncds\LayerCategoryController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerGoodsController.php
변경 후LayerGoodsController.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\Provider\Share\Ncds; | |
| class LayerGoodsController extends \Controller\Admin\Share\Ncds\LayerGoodsController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerGoodsOptionGridConfigController.php
변경 후LayerGoodsOptionGridConfigController.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\Provider\Share\Ncds; | |
| class LayerGoodsOptionGridConfigController extends \Controller\Admin\Share\Ncds\LayerGoodsOptionGridConfigController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전LayerGoodsOptionStockController.php
변경 후LayerGoodsOptionStockController.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\Provider\Share\Ncds; | |
| class LayerGoodsOptionStockController extends \Controller\Admin\Share\Ncds\LayerGoodsOptionStockController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| } | |
| } | |
변경 전CategorySelectJsonController.php
| } | |
| //--- 카테고리 정보 | |
| $data = $cate->getCategoryData(null,$postValue['value'],'cateCd,cateNm,mallDisplay','divisionFl = "n" AND length(cateCd) = \''.(strlen($postValue['value']) + $cateLength).'\''.gd_isset($userWhere),'cateSort asc'); | |
| $i = 0; | |
| $tmp = array(); | |
| foreach ($data as $key => $val) { | |
| $tmp[$i]['optionText'] = $val['cateNm']; | |
| $tmp[$i]['mallName'] = gd_implode(",",$mallName); | |
| $tmp[$i]['flag'] = gd_implode(",",$mallSno); | |
| $i++; | |
| } | |
| if (!empty($tmp)) { | |
변경 후CategorySelectJsonController.php
| } | |
| //--- 카테고리 정보 | |
| $currentLength = strlen($postValue['value']) + $cateLength; | |
| $data = $cate->getCategoryData(null,$postValue['value'],'cateCd,cateNm,mallDisplay','divisionFl = "n" AND length(cateCd) = \''.$currentLength.'\''.gd_isset($userWhere),'cateSort asc'); | |
| $nextLength = $currentLength + $cateLength; | |
| $maxCateLen = DEFAULT_DEPTH_CATE * $cateLength; | |
| $hasChildrenSet = []; | |
| if ($nextLength <= $maxCateLen && !empty($data)) { | |
| $grandChildData = $cate->getCategoryData(null, $postValue['value'], 'cateCd', 'divisionFl = "n" AND length(cateCd) = \'' . $nextLength . '\'' . gd_isset($userWhere), null); | |
| if (is_array($grandChildData)) { | |
| foreach ($grandChildData as $gc) { | |
| $hasChildrenSet[substr($gc['cateCd'], 0, $currentLength)] = true; | |
| } | |
| } | |
| } | |
| $i = 0; | |
| $tmp = array(); | |
| foreach ($data as $key => $val) { | |
| $tmp[$i]['optionText'] = $val['cateNm']; | |
| $tmp[$i]['mallName'] = gd_implode(",",$mallName); | |
| $tmp[$i]['flag'] = gd_implode(",",$mallSno); | |
| $tmp[$i]['hasChildren'] = isset($hasChildrenSet[$val['cateCd']]); | |
| $i++; | |
| } | |
| if (!empty($tmp)) { | |
변경 전LayerGoodsOptionGridConfigController.php
변경 후LayerGoodsOptionGridConfigController.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\Share\Ncds; | |
| class LayerGoodsOptionGridConfigController extends \Controller\Admin\Share\LayerGoodsOptionGridConfigController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| $this->getView()->setPageName('share/ncds/layer_goods_option_grid_config.php'); | |
| } | |
| } | |
변경 전LayerGoodsOptionStockController.php
변경 후LayerGoodsOptionStockController.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\Share\Ncds; | |
| class LayerGoodsOptionStockController extends \Bundle\Controller\Admin\Share\LayerGoodsOptionStockController | |
| { | |
| public function index() | |
| { | |
| parent::index(); | |
| $this->getView()->setPageName('share/ncds/layer_goods_option_stock.php'); | |
| } | |
| } | |
변경 전duplicate_login.php
변경 후duplicate_login.php
| <link rel="stylesheet" href="<?= PATH_ADMIN_GD_SHARE ?>ncds/css/duplicate-login-ncds.css"/> | |
| <form id="frmDuplicateLogin" action="login_ps.php" method="post"> | |
| <input type="hidden" name="mode" value="concurrentLoginConfirm"/> | |
| <input type="hidden" name="returnUrl" value=""/> | |
| <input type="hidden" name="keepExisting" id="keepExistingInput" value="n"/> | |
| </form> | |
| <script> | |
| (() => { | |
| const init = () => { | |
| if (typeof window.ncua === 'undefined' || typeof window.ncua.Modal === 'undefined') { | |
| setTimeout(init, 50); | |
| return; | |
| } | |
| const makeBtn = (label, hierarchy, onClick) => { | |
| if (window.createButton) { return window.createButton(label, hierarchy, onClick, 'sm'); } | |
| const tpl = document.createElement('template'); | |
| tpl.innerHTML = `<button type="button" class="ncua-btn ncua-btn--sm ncua-btn--${hierarchy}"><span class="ncua-btn__label">${label}</span></button>`; | |
| const btn = tpl.content.firstElementChild; | |
| btn.addEventListener('click', onClick); | |
| return btn; | |
| }; | |
| const cancel = () => { | |
| location.href = '/base/login_ps.php?mode=logout'; | |
| }; | |
| const modal = new window.ncua.Modal({ | |
| size: 'lg', | |
| className: 'duplicate-login-ncds-modal', | |
| closeOnBackdropClick: false, | |
| onClose: cancel, | |
| }); | |
| const header = new window.ncua.Modal.Header({ | |
| title: '고객님의 계정이 이미 사용중입니다.', | |
| }); | |
| header.setCloseHandler(cancel); | |
| const contentHtml = ` | |
| <div class="duplicate-login-ncds-content"> | |
| <p class="duplicate-login-ncds-desc"> | |
| • 고객님의 계정이 다른 IP에서 로그인되었습니다. 아래 버튼을 선택하여 로그인을 진행해주세요.<br> | |
| • 안전하게 이용하려면 이전 로그인을 종료하고 새로 로그인해주세요. | |
| </p> | |
| <div class="ncua-table ncua-table--vertical"> | |
| <table> | |
| <colgroup><col width="50%"><col width="50%"></colgroup> | |
| <tbody> | |
| <tr> | |
| <th><div>현재 로그인한 IP</div></th> | |
| <td><div><?= gd_htmlspecialchars($currentLoginIp) ?></div></td> | |
| </tr> | |
| <tr> | |
| <th><div>이전 로그인한 IP</div></th> | |
| <td><div><?= gd_htmlspecialchars($lastLoginIp) ?></div></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="duplicate-login-ncds-radio-group"> | |
| <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="keepExistingModal" value="y" checked /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">이전 로그인을 그대로 유지한 상태에서 로그인 (동시 로그인 허용)</span></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="keepExistingModal" value="n" /> | |
| </span> | |
| <span><span class="ncua-radio-field__text">이전 로그인은 종료하고, 새로 로그인</span></span> | |
| </label> | |
| </div> | |
| </div> | |
| `; | |
| const content = new window.ncua.Modal.Content(contentHtml); | |
| const actions = new window.ncua.Modal.Actions('', { layout: 'horizontal', align: 'right' }); | |
| const confirmBtn = makeBtn('확인', 'primary', () => { | |
| const checked = modal.getModalElement().querySelector('input[name="keepExistingModal"]:checked'); | |
| const keepExisting = checked?.value ?? 'n'; | |
| const standalone = keepExisting !== 'y'; | |
| fetch('/share/duplicate_login_ps.php', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'application/json', | |
| 'X-Requested-With': 'XMLHttpRequest', | |
| }, | |
| body: JSON.stringify({ standalone: standalone }), | |
| }).then((response) => { | |
| if (!response.ok) { | |
| throw new Error('request failed'); | |
| } | |
| location.reload(); | |
| }).catch(() => { | |
| alert('요청하신 작업을 완료하지 못했습니다.\n잠시 후 다시 시도해 주세요.'); | |
| }); | |
| }); | |
| const cancelBtn = makeBtn('취소', 'secondary-gray', cancel); | |
| actions.setContent([cancelBtn, confirmBtn]); | |
| const modalEl = modal.getModalElement(); | |
| header.appendTo(modalEl); | |
| content.appendTo(modalEl); | |
| actions.appendTo(modalEl); | |
| modal.open(); | |
| }; | |
| init(); | |
| })(); | |
| </script> | |
변경 전AdminLogDAO.php
| const LOGIN = 'login'; | |
| const LOGOUT = 'logout'; | |
| const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout']; | |
| $page = '주문 내역 삭제'; | |
| } | |
| $logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y; | |
| // 개인정보접속기록 다운로드로그 5년보관 | |
| if ($page == 'adminLogExcel') { | |
변경 후AdminLogDAO.php
| const LOGIN = 'login'; | |
| const LOGOUT = 'logout'; | |
| const LOGOUT_SESSION_END = 'logoutSessionEnd'; | |
| const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout']; | |
| $page = '주문 내역 삭제'; | |
| } | |
| if ($mode === self::LOGOUT_SESSION_END) { | |
| $page = 'adminInfo'; | |
| $action = '로그아웃(세션 종료)'; | |
| } | |
| $logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y; | |
| // 개인정보접속기록 다운로드로그 5년보관 | |
| if ($page == 'adminLogExcel') { | |
변경 전ManagerCs.php
| use Framework\Debug\Exception\DatabaseException; | |
| use Framework\Security\Digester; | |
| use Framework\Security\Otp; | |
| use Framework\Utility\ComponentUtils; | |
| use Framework\Utility\DateTimeUtils; | |
| use Framework\Utility\StringUtils; | |
| use Framework\Utility\GodoUtils; | |
| /** | |
| * Class ManagerCs | |
| * @package Bundle\Component\Member | |
| $request = \App::getInstance('request'); | |
| $session = \App::getInstance('session'); | |
| $logger = \App::getInstance('logger'); | |
| $this->db->query_reset(); | |
| $data['lastLoginIp'] = $request->getRemoteAddress(); | |
| $data['sessionLimitTime'] = $sessionLimitTime; | |
| $session->del(self::SESSION_TEMP_MANAGER); | |
| $session->set(self::SESSION_MANAGER_LOGIN, $data); | |
| } | |
| /** | |
변경 후ManagerCs.php
| use Framework\Debug\Exception\DatabaseException; | |
| use Framework\Security\Digester; | |
| use Framework\Security\Otp; | |
| use Framework\SimpleCache\SimpleCache; | |
| use Framework\Utility\ComponentUtils; | |
| use Framework\Utility\DateTimeUtils; | |
| use Framework\Utility\StringUtils; | |
| use Framework\Utility\GodoUtils; | |
| use Origin\Service\Storage\ManagerDuplicateLoginService; | |
| /** | |
| * Class ManagerCs | |
| * @package Bundle\Component\Member | |
| $request = \App::getInstance('request'); | |
| $session = \App::getInstance('session'); | |
| $logger = \App::getInstance('logger'); | |
| $duplicateLoginService = new ManagerDuplicateLoginService( | |
| \App::getInstance('application'), | |
| \App::getInstance('session'), | |
| SimpleCache::init(SimpleCache::REDIS) | |
| ); | |
| $this->db->query_reset(); | |
| $data['lastLoginIp'] = $request->getRemoteAddress(); | |
| $data['sessionLimitTime'] = $sessionLimitTime; | |
| $session->del(self::SESSION_TEMP_MANAGER); | |
| $session->set(self::SESSION_MANAGER_LOGIN, $data); | |
| $duplicateLoginService->addCurrentLoginToList($data['sno']); | |
| } | |
| /** | |
변경 전Manager.php
| namespace Bundle\Component\Member; | |
| use Component\Admin\AdminMenu; | |
| use Component\Database\DBTableField; | |
| use Component\Member\Exception\LoginException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Framework\Security\Digester; | |
| use Framework\Security\Otp; | |
| use Framework\Utility\ComponentUtils; | |
| use Framework\Utility\DateTimeUtils; | |
| use Framework\Utility\GodoUtils; | |
| use Framework\Utility\StringUtils; | |
| use Origin\Service\Admin\Menu\AdminMenuFlagService; | |
| use Origin\Service\Member\ManagerLogoutService; | |
| use Password; | |
| use Request; | |
| use Session; | |
| */ | |
| private $_storage; | |
| /** | |
| * 생성자 | |
| */ | |
| $this->smsAutoReceiveKind[$newKey] = $val; | |
| } | |
| unset($tmp, $newKey); | |
| } | |
| /** | |
| $this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션 | |
| $this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션 | |
| $this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션 | |
| // 관리자 로그인 성공 로그 | |
| $request->request()->set('adminLoginFl', true); | |
| $adminMenuFlagService->updateAdminMenuFlagsInSession(); | |
| } | |
| /** | |
| * setManagerOldMenuAccessChange | |
| * 운영자 세부 접속 권한 마이그레이션 | |
| } | |
| } | |
| /** | |
| * 관리자 로그아웃 | |
| */ | |
| public function managerLogout() | |
| { | |
| $cookie = \App::getInstance('cookie'); | |
| // 관리자 로그아웃 로그 작성 | |
| $adminLog = \App::load('Component\\Admin\\AdminLogDAO'); | |
| $adminLog->setAdminLog($adminLog::LOGOUT); | |
| // 세션을 없애기전에 세션 쿠키를 지웁니다. | |
| if ($cookie->has($session->getName())) { | |
| // 관리자 통합회원 로그아웃 | |
| /** @var ManagerLogoutService $managerLogoutService */ | |
| $managerLogoutService = \App::getInstance(ManagerLogoutService::class); | |
| $managerLogoutService->logout(); | |
| // 관리자만 로그아웃하는 경우 세션 클리어 | |
| if (MemberUtil::isLogin()) { | |
| } | |
| if ($result > 0 && $type != 'reset') { | |
| Manager::managerLogout(); | |
| } | |
| } | |
| unset($arrBind); | |
| return gd_htmlspecialchars_stripslashes($data); | |
| } | |
| } | |
변경 후Manager.php
| namespace Bundle\Component\Member; | |
| use Component\Admin\AdminLogDAO; | |
| use Component\Admin\AdminMenu; | |
| use Component\Database\DBTableField; | |
| use Component\Member\Exception\LoginException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Framework\Security\Digester; | |
| use Framework\Security\Otp; | |
| use Framework\SimpleCache\SimpleCache; | |
| use Framework\Utility\ComponentUtils; | |
| use Framework\Utility\DateTimeUtils; | |
| 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; | |
| use Session; | |
| */ | |
| private $_storage; | |
| private ManagerDuplicateLoginService $managerLoginStorage; | |
| /** @var bool 중복 로그인으로 인한 (강제) 로그아웃 여부 */ | |
| private bool $shouldDuplicateLogout = false; | |
| /** | |
| * 생성자 | |
| */ | |
| $this->smsAutoReceiveKind[$newKey] = $val; | |
| } | |
| unset($tmp, $newKey); | |
| $this->managerLoginStorage = new ManagerDuplicateLoginService( | |
| \App::getInstance('application'), | |
| \App::getInstance('session'), | |
| SimpleCache::init(SimpleCache::REDIS) | |
| ); | |
| } | |
| /** | |
| $this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션 | |
| $this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션 | |
| $this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션 | |
| $this->setDuplicateLoginList($data['sno']); // 중복 로그인 감지용 로그인 세션 목록에 현재 세션 등록 | |
| // 관리자 로그인 성공 로그 | |
| $request->request()->set('adminLoginFl', true); | |
| $adminMenuFlagService->updateAdminMenuFlagsInSession(); | |
| } | |
| public function setDuplicateLoginList(int $sno): void | |
| { | |
| $this->managerLoginStorage->addCurrentLoginToList($sno); | |
| } | |
| /** | |
| * setManagerOldMenuAccessChange | |
| * 운영자 세부 접속 권한 마이그레이션 | |
| } | |
| } | |
| /** | |
| * 중복 로그인으로 인한 (강제) 로그아웃 여부 주입. | |
| * managerLogout() 을 무인자로 유지하기 위해 파라미터 대신 setter 로 받는다. | |
| * | |
| * @param bool $shouldDuplicateLogout true 면 es_adminLog 에 세션 종료 action 으로 기록 | |
| */ | |
| public function setShouldDuplicateLogout(bool $shouldDuplicateLogout): void | |
| { | |
| $this->shouldDuplicateLogout = $shouldDuplicateLogout; | |
| } | |
| /** | |
| * 관리자 로그아웃 | |
| * 중복 로그인 여부는 setShouldDuplicateLogout() 로 주입한다. | |
| */ | |
| public function managerLogout() | |
| { | |
| $cookie = \App::getInstance('cookie'); | |
| // 관리자 로그아웃 로그 작성 | |
| /** @var AdminLogDAO $adminLog */ | |
| $adminLog = \App::load('Component\\Admin\\AdminLogDAO'); | |
| $adminLog->setAdminLog($this->shouldDuplicateLogout ? $adminLog::LOGOUT_SESSION_END : $adminLog::LOGOUT); | |
| // 세션을 없애기전에 세션 쿠키를 지웁니다. | |
| if ($cookie->has($session->getName())) { | |
| // 관리자 통합회원 로그아웃 | |
| /** @var ManagerLogoutService $managerLogoutService */ | |
| $managerLogoutService = \App::getInstance(ManagerLogoutService::class); | |
| $managerLogoutService->logout($this->shouldDuplicateLogout); | |
| $this->managerLoginStorage->deleteCurrentLoginFromList(); | |
| // 관리자만 로그아웃하는 경우 세션 클리어 | |
| if (MemberUtil::isLogin()) { | |
| } | |
| if ($result > 0 && $type != 'reset') { | |
| $this->managerLogout(); | |
| } | |
| } | |
| unset($arrBind); | |
| return gd_htmlspecialchars_stripslashes($data); | |
| } | |
| public function isCurrentLoginRegistered(): bool | |
| { | |
| return $this->managerLoginStorage->isCurrentLoginRegistered(); | |
| } | |
| public function isDuplicateLogin(): bool | |
| { | |
| return $this->managerLoginStorage->isDuplicateLatestLogin(); | |
| } | |
| public function getLastLoginIp(): ?string | |
| { | |
| return $this->managerLoginStorage->getPreviousLoginIp(); | |
| } | |
| public function allLogoutWithoutMe(): void | |
| { | |
| $this->managerLoginStorage->deleteLoginAllWithoutMe(); | |
| } | |
| } | |
변경 전IndexController.php
| use Component\Member\Manager; | |
| use Core\Base\View\Alert; | |
| use Framework\Enum\ExternalUrl; | |
| use Framework\Utility\StringUtils; | |
| use Origin\Service\License\LicenseRenewService; | |
| $mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy'); | |
| $sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN); | |
| // 메인 통계의 권한 체크 | |
| $adminMenu = \App::load('Component\\Admin\\AdminMenu'); | |
| if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) { | |
| $this->getView()->setPageName('base/index.php'); | |
| } | |
| } | |
변경 후IndexController.php
| use Component\Member\Manager; | |
| use Core\Base\View\Alert; | |
| use Framework\Enum\ExternalUrl; | |
| use Framework\Http\Session\SessionManager; | |
| use Framework\Utility\StringUtils; | |
| use Origin\Service\License\LicenseRenewService; | |
| $mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy'); | |
| $sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN); | |
| $this->checkDuplicateLogin($session); | |
| // 메인 통계의 권한 체크 | |
| $adminMenu = \App::load('Component\\Admin\\AdminMenu'); | |
| if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) { | |
| $this->getView()->setPageName('base/index.php'); | |
| } | |
| public function checkDuplicateLogin(SessionManager $session) | |
| { | |
| $manager = \App::getInstance(\Manager::class); | |
| $request = \App::getInstance('request'); | |
| if ( | |
| $manager->isDuplicateLogin() && | |
| !$session->get('manager.allowDuplicateLogin') | |
| ) { | |
| $this->getView()->setDefine('layout', 'layout_blank_noiframe.php'); | |
| $this->getView()->setPageName('base/duplicate_login.php'); | |
| $this->getView()->setData('lastLoginIp',$manager->getLastLoginIp()); | |
| $this->getView()->setData('currentLoginIp', $request->getRemoteAddress()); | |
| echo $this->getView()->render(); | |
| exit; | |
| } | |
| } | |
| } | |
변경 전DBTableField.php
| ['val' => 'naverFl', 'typ' => 's', 'def' => 'y', 'name' => '네이버쇼핑여부','batch'=>'y'], // 네이버수입및제작여부 | |
| ['val' => 'daumFl', 'typ' => 's', 'def' => 'y', 'name' => '다음쇼핑하우연동여부', 'batch'=>'y'], // 다음쇼핑하우연동여부 | |
| ['val' => 'paycoFl', 'typ' => 's', 'def' => 'y', 'name' => '페이코쇼핑여부','batch'=>'y'], // 페이코수입및제작여부 | |
| ['val' => 'modDt', 'typ' => 's', 'def' => null], // 수정일 | |
| ['val' => 'regDt', 'typ' => 's', 'def' => null], // 등록일 | |
| ]; | |
| // @formatter:on | |
변경 후DBTableField.php
| ['val' => 'naverFl', 'typ' => 's', 'def' => 'y', 'name' => '네이버쇼핑여부','batch'=>'y'], // 네이버수입및제작여부 | |
| ['val' => 'daumFl', 'typ' => 's', 'def' => 'y', 'name' => '다음쇼핑하우연동여부', 'batch'=>'y'], // 다음쇼핑하우연동여부 | |
| ['val' => 'paycoFl', 'typ' => 's', 'def' => 'y', 'name' => '페이코쇼핑여부','batch'=>'y'], // 페이코수입및제작여부 | |
| ['val' => 'modDt', 'typ' => 's', 'def' => null, 'batch'=>'y'], // 수정일 | |
| ['val' => 'regDt', 'typ' => 's', 'def' => null], // 등록일 | |
| ]; | |
| // @formatter:on | |
변경 전GoodsAdmin.php
| const EXCEL_FL = 'R'; | |
| /** | |
| * 생성자 | |
| */ | |
| $this->search['sortList'] = array( | |
| 'g.goodsNo desc' => __('등록일 ↓'), | |
| 'g.goodsNo asc' => __('등록일 ↑'), | |
| 'g.goodsNm asc' => __('상품명 ↓'), | |
| 'g.goodsNm desc' => __('상품명 ↑'), | |
| 'g.goodsPrice asc' => __('판매가 ↓'), | |
| $getValue = Request::get()->toArray(); | |
| gd_isset($getValue['delFl'], 'n'); | |
| // --- 정렬 설정 | |
| if ($getValue['delFl'] == 'y') { | |
| $sort = gd_isset($getValue['sort'], 'delDt desc , g.goodsNo desc'); | |
| if ($this->goodsDivisionFl) { | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo '; | |
| } | |
| } elseif ($getValue['parentFormID'] === 'regular_goods') { | |
| $sort = gd_isset($getValue['sort'], 'g.goodsNo desc'); | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo '; | |
| } elseif ($getValue['parentFormID'] === 'present') { | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' AS goods ON g.goodsNo = goods.goodsNo '; | |
| $join[] = ' LEFT JOIN ' . DB_SCM_DELIVERY_BASIC . ' AS delivery ON delivery.sno = goods.deliverySno '; | |
| } else { | |
| $sort = gd_isset($getValue['sort'], 'g.goodsNo desc'); | |
| } | |
| $this->setSearchGoods($getValue); | |
| //상품 헤택 모듈 | |
| $getValue = Request::get()->toArray(); | |
| gd_isset($getValue['delFl'], 'n'); | |
| $sort = gd_isset($getValue['sort'], 'g.goodsNo desc'); | |
| $changeGoodsTableFl = false; | |
| if ($mode == 'restock') { | |
| if(empty($arrGoodsNo)) return true; | |
| $updateField = []; | |
| foreach(DBTableField::tableGoodsSearch() as $k => $v) { | |
| if($v['batch'] =='y') $updateField[] = 'gs.'.$v['val'].'='.'g.'.$v['val']; | |
| $this->setSearchGoods($getValue); | |
| if($getValue['goodsNo'] && is_array($getValue['goodsNo'])) { | |
| $this->arrWhere[] = 'goodsNo IN (' . gd_implode(',', $getValue['goodsNo']) . ')'; | |
| } | |
| $strSQL = ' SELECT COUNT(g.goodsNo) AS cnt FROM ' . $this->goodsTable . ' as g WHERE ' . gd_implode(' AND ', $this->arrWhere); | |
| $this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo); | |
| $this->db->setModDtUse(true); | |
| $this->db->set_update_db(DB_GOODS, 'cateCd = ?', 'goodsNo = ?', $arrBind['bind']); | |
| unset($getLink); | |
| } | |
| } | |
| $this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo); | |
| $this->db->setModDtUse(true); | |
| $return = $this->db->set_update_db(DB_GOODS, $arrBind['param'], 'goodsNo = ?', $arrBind['bind']); | |
| unset($arrBind, $goodsData); | |
| return $return; | |
| } | |
| unset($arrBind); | |
| } | |
| return $arrData['applyFl']; | |
| } | |
변경 후GoodsAdmin.php
| const EXCEL_FL = 'R'; | |
| // 정렬 SQL 인젝션 방지용 화이트리스트 (관리자 상품 리스트 정렬 옵션) | |
| protected const SORT_ALLOW_LIST = [ | |
| 'g.goodsNo desc', 'g.goodsNo asc', | |
| 'g.modDt desc', 'g.modDt asc', | |
| 'g.goodsNm asc', 'g.goodsNm desc', | |
| 'g.goodsPrice asc', 'g.goodsPrice desc', | |
| 'companyNm asc', 'companyNm desc', | |
| 'g.makerNm asc', 'g.makerNm desc', | |
| 'g.orderGoodsCnt desc', | |
| 'g.hitCnt desc', | |
| 'orderRate desc', | |
| 'g.cartCnt desc', | |
| 'g.wishCnt desc', | |
| 'g.reviewCnt desc', | |
| 'delDt desc, g.goodsNo desc', | |
| 'delDt asc', | |
| ]; | |
| // 정렬 SQL 인젝션 방지용 화이트리스트 (관리자 상품 일괄처리 리스트 - getAdminListBatch) | |
| protected const SORT_ALLOW_LIST_BATCH = [ | |
| 'g.goodsNo desc', 'g.goodsNo asc', | |
| 'g.modDt desc', 'g.modDt asc', | |
| 'goodsNm asc', 'goodsNm desc', | |
| 'companyNm asc', 'companyNm desc', | |
| 'goodsPrice asc', 'goodsPrice desc', | |
| 'fixedPrice asc', 'fixedPrice desc', | |
| 'costPrice asc', 'costPrice desc', | |
| ]; | |
| /** | |
| * 생성자 | |
| */ | |
| $this->search['sortList'] = array( | |
| 'g.goodsNo desc' => __('등록일 ↓'), | |
| 'g.goodsNo asc' => __('등록일 ↑'), | |
| 'g.modDt desc' => __('수정일 ↓'), | |
| 'g.modDt asc' => __('수정일 ↑'), | |
| 'g.goodsNm asc' => __('상품명 ↓'), | |
| 'g.goodsNm desc' => __('상품명 ↑'), | |
| 'g.goodsPrice asc' => __('판매가 ↓'), | |
| $getValue = Request::get()->toArray(); | |
| gd_isset($getValue['delFl'], 'n'); | |
| // --- 검색 설정 보조 JOIN | |
| if ($getValue['delFl'] == 'y') { | |
| if ($this->goodsDivisionFl) { | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo '; | |
| } | |
| } elseif ($getValue['parentFormID'] === 'regular_goods') { | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo '; | |
| } elseif ($getValue['parentFormID'] === 'present') { | |
| $join[] = ' LEFT JOIN ' . DB_GOODS . ' AS goods ON g.goodsNo = goods.goodsNo '; | |
| $join[] = ' LEFT JOIN ' . DB_SCM_DELIVERY_BASIC . ' AS delivery ON delivery.sno = goods.deliverySno '; | |
| } | |
| // --- 정렬 설정 (SQL 인젝션 방지 - 사용자 입력은 화이트리스트만 허용) | |
| if (isset($getValue['sort']) && in_array($getValue['sort'], self::SORT_ALLOW_LIST, true)) { | |
| $sort = $getValue['sort']; | |
| } else { | |
| $sort = ($getValue['delFl'] == 'y') ? 'delDt desc, g.goodsNo desc' : 'g.goodsNo desc'; | |
| } | |
| // 수정일 정렬 시 NULL 처리 - 등록일로 대체, 페이징 안정화 보조정렬 | |
| if (preg_match('/^g\.modDt\s+(asc|desc)$/i', $sort, $m)) { | |
| $sort = "COALESCE(g.modDt, g.regDt) {$m[1]}, g.goodsNo desc"; | |
| } | |
| $this->setSearchGoods($getValue); | |
| //상품 헤택 모듈 | |
| $getValue = Request::get()->toArray(); | |
| gd_isset($getValue['delFl'], 'n'); | |
| // 정렬 설정 (SQL 인젝션 방지 - 사용자 입력은 화이트리스트만 허용) | |
| if (isset($getValue['sort']) && in_array($getValue['sort'], self::SORT_ALLOW_LIST_BATCH, true)) { | |
| $sort = $getValue['sort']; | |
| } else { | |
| $sort = 'g.goodsNo desc'; | |
| } | |
| $changeGoodsTableFl = false; | |
| if ($mode == 'restock') { | |
| if(empty($arrGoodsNo)) return true; | |
| // goodsNo 는 정수 전용 식별자 → IN 절 SQL 인젝션 방어 (sink 지점에서 일괄 정수화) | |
| $arrGoodsNo = array_map('intval', $arrGoodsNo); | |
| $updateField = []; | |
| foreach(DBTableField::tableGoodsSearch() as $k => $v) { | |
| if($v['batch'] =='y') $updateField[] = 'gs.'.$v['val'].'='.'g.'.$v['val']; | |
| $this->setSearchGoods($getValue); | |
| if($getValue['goodsNo'] && is_array($getValue['goodsNo'])) { | |
| $this->arrWhere[] = 'goodsNo IN (' . gd_implode(',', array_map('intval', $getValue['goodsNo'])) . ')'; | |
| } | |
| $strSQL = ' SELECT COUNT(g.goodsNo) AS cnt FROM ' . $this->goodsTable . ' as g WHERE ' . gd_implode(' AND ', $this->arrWhere); | |
| $this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo); | |
| $this->db->setModDtUse(true); | |
| $this->db->set_update_db(DB_GOODS, 'cateCd = ?', 'goodsNo = ?', $arrBind['bind']); | |
| // es_goodsSearch 동기화 | |
| if ($this->goodsDivisionFl) { | |
| $this->updateGoodsSearch([$goodsNo]); | |
| } | |
| unset($getLink); | |
| } | |
| } | |
| $this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo); | |
| $this->db->setModDtUse(true); | |
| $return = $this->db->set_update_db(DB_GOODS, $arrBind['param'], 'goodsNo = ?', $arrBind['bind']); | |
| // es_goodsSearch 동기화 | |
| if ($this->goodsDivisionFl) { | |
| $this->updateGoodsSearch([$goodsNo]); | |
| } | |
| unset($arrBind, $goodsData); | |
| return $return; | |
| } | |
| unset($arrBind); | |
| } | |
| // es_goodsSearch 동기화 | |
| if ($this->goodsDivisionFl) { | |
| $this->updateGoodsSearch($arrGoodsNo, $getData['modDtUse']); | |
| } | |
| return $arrData['applyFl']; | |
| } | |
변경 전GoodsBatchPriceController.php
| //정렬 재정의 | |
| $getData['search']['sortList'] = array( | |
| 'g.goodsNo desc' => sprintf(__('등록일 %1$s'), '↓'), | |
| 'g.goodsNo asc' => sprintf(__('등록일 %1$s'), '↑'), | |
| 'goodsNm asc' => sprintf(__('상품명 %1$s'), '↓'), | |
| 'goodsNm desc' => sprintf(__('상품명 %1$s'), '↑'), | |
| 'companyNm asc' => sprintf(__('공급사 %1$s'), '↓'), | |
| 'companyNm desc' => sprintf(__('공급사 %1$s'), '↑'), | |
| 'fixedPrice asc' => sprintf(__('정가 %1$s'), '↓'), | |
| 'fixedPrice desc' => sprintf(__('정가 %1$s'), '↑'), | |
| 'costPrice asc' => sprintf(__('매입가 %1$s'), '↓'), | |
| 'costPrice desc' => sprintf(__('매입가 %1$s'), '↑'), | |
| 'goodsPrice asc' => sprintf(__('판매가 %1$s'), '↓'), | |
| 'goodsPrice desc' => sprintf(__('판매가 %1$s'), '↑'), | |
| ); | |
| $this->setData('goods', $goods); | |
변경 후GoodsBatchPriceController.php
| //정렬 재정의 | |
| $getData['search']['sortList'] = array( | |
| 'g.goodsNo desc' => __('등록일 ↓'), | |
| 'g.goodsNo asc' => __('등록일 ↑'), | |
| 'g.modDt desc' => __('수정일 ↓'), | |
| 'g.modDt asc' => __('수정일 ↑'), | |
| 'goodsNm asc' => __('상품명 ↓'), | |
| 'goodsNm desc' => __('상품명 ↑'), | |
| 'companyNm asc' => __('공급사 ↓'), | |
| 'companyNm desc' => __('공급사 ↑'), | |
| 'fixedPrice asc' => __('정가 ↓'), | |
| 'fixedPrice desc' => __('정가 ↑'), | |
| 'costPrice asc' => __('매입가 ↓'), | |
| 'costPrice desc' => __('매입가 ↑'), | |
| 'goodsPrice asc' => __('판매가 ↓'), | |
| 'goodsPrice desc' => __('판매가 ↑'), | |
| ); | |
| $this->setData('goods', $goods); | |
변경 전GoodsPsController.php
| // 상품리스트 메모 저장하기(그리드) | |
| case 'goods_list_memo' : | |
| try { | |
| if ($goods->getGoodsListAdminMemo($postValue['goodsNo'], $postValue['adminMemo'], 'update')) { | |
| $this->layer(__('정상적으로 저장 되었습니다.')); | |
| } | |
| } catch (Exception $e) { | |
| throw new LayerNotReloadException($e->getMessage()); | |
| } | |
| break; | |
| // 상품리스트 선택 조회항목 삭제하기 | |
| case 'goods_list_add_grid_del' : | |
| try { | |
| // 조회항목 | |
| if ($goods->goodsListDisplayEachDelete($postValue)) { | |
| echo true; | |
| } | |
| } catch (Exception $e) { | |
| throw new LayerNotReloadException($e->getMessage()); | |
| } | |
| break; | |
변경 후GoodsPsController.php
| // 상품리스트 메모 저장하기(그리드) | |
| case 'goods_list_memo' : | |
| try { | |
| \DB::begin_tran(); | |
| $result = $goods->getGoodsListAdminMemo($postValue['goodsNo'], $postValue['adminMemo'], 'update'); | |
| \DB::commit(); | |
| if ($result) { | |
| $this->layer(__('정상적으로 저장 되었습니다.')); | |
| } | |
| } catch (Exception $e) { | |
| \DB::rollback(); | |
| throw new LayerNotReloadException($e->getMessage()); | |
| } | |
| break; | |
| // 상품리스트 선택 조회항목 삭제하기 | |
| case 'goods_list_add_grid_del' : | |
| try { | |
| \DB::begin_tran(); | |
| // 조회항목 | |
| $result = $goods->goodsListDisplayEachDelete($postValue); | |
| \DB::commit(); | |
| if ($result) { | |
| echo true; | |
| } | |
| } catch (Exception $e) { | |
| \DB::rollback(); | |
| throw new LayerNotReloadException($e->getMessage()); | |
| } | |
| break; | |
변경 전Board.php
| protected function xssClean($data) | |
| { | |
| $onevents = 'onabort|onactivate|onafterprint|onafterupdate|onbeforeactivate|onbeforecopy|onbeforecut|onbeforedeactivate|onbeforeeditfocus|onbeforepaste|onbeforeprint|onbeforeunload|onbeforeupdate|onblur|onbounce|oncellchange|onchange|onclick|oncontextmenu|oncontrolselect|oncopy|oncut|ondataavailable|ondatasetchanged|ondatasetcomplete|ondblclick|ondeactivate|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|onerror|onerrorupdate|onfilterchange|onfinish|onfocus|onfocusin|onfocusout|onhelp|onkeydown|onkeypress|onkeyup|onlayoutcomplete|onload|onlosecapture|onmousedown|onmouseenter|onmouseleave|onmousemove|onmouseout|onmouseover|onmouseup|onmousewheel|onmove|onmoveend|onmovestart|onpaste|onpropertychange|onreadystatechange|onreset|onresize|onresizeend|onresizestart|onrowenter|onrowexit|onrowsdelete|onrowsinserted|onscroll|onselect|onselectionchange|onselectstart|onstart|onstop|onsubmit|onunload|ontoggle|onpageshow|onrepeat|onprogress|onbegin|onpointerdown|onauxclick|onpointerenter|onend|onsearch'; | |
| $data = preg_replace('#([\x00-\x20"\'/])(?:'.$onevents.'|xmlns)[^>]*+>#iu', '$1>', $data); | |
| // Remove javascript: and vbscript: protocols | |
변경 후Board.php
| protected function xssClean($data) | |
| { | |
| // 이벤트 핸들러 목록은 StringUtils 단일소스 사용 (목록 드리프트 방지) | |
| $onevents = StringUtils::getXssEventHandlers(); | |
| $data = preg_replace('#([\x00-\x20"\'/])(?:'.$onevents.'|xmlns)[^>]*+>#iu', '$1>', $data); | |
| // Remove javascript: and vbscript: protocols | |
변경 전PollDetailViewController.php
| use Component\Promotion\Poll; | |
| use Component\Page\Page; | |
| use Exception; | |
| use Request; | |
| /** | |
| if (is_array($decData) === true) { | |
| foreach ($decData as $k => $v) { | |
| if ($k == $getValue['detail']) { | |
| $resData[] = $v; | |
| } | |
| } | |
| } | |
변경 후PollDetailViewController.php
| use Component\Promotion\Poll; | |
| use Component\Page\Page; | |
| use Exception; | |
| use Framework\Utility\StringUtils; | |
| use Request; | |
| /** | |
| if (is_array($decData) === true) { | |
| foreach ($decData as $k => $v) { | |
| if ($k == $getValue['detail']) { | |
| $resData[] = StringUtils::htmlSpecialChars($v); | |
| } | |
| } | |
| } | |
변경 전PollResultController.php
| use Component\Promotion\Poll; | |
| use Framework\Debug\Exception\AlertBackException; | |
| use Request; | |
| class PollResultController extends \Controller\Front\Controller | |
| foreach ($item['itemAnswerType'] as $key => $val) { | |
| if (empty(isset($result[$key])) === false) { | |
| if ($val == 'sub') { | |
| if (gd_count($sortData[$key]) < 20 && $result[$key]) $sortData[$key][] = $result[$key]; | |
| } else { | |
| foreach ($item['itemAnswer'][$key] as $_key => $_value) { | |
| if ($item['itemResponseType'][$key] == 'radio') { | |
변경 후PollResultController.php
| use Component\Promotion\Poll; | |
| use Framework\Debug\Exception\AlertBackException; | |
| use Framework\Utility\StringUtils; | |
| use Request; | |
| class PollResultController extends \Controller\Front\Controller | |
| foreach ($item['itemAnswerType'] as $key => $val) { | |
| if (empty(isset($result[$key])) === false) { | |
| if ($val == 'sub') { | |
| if (gd_count($sortData[$key]) < 20 && $result[$key]) { | |
| $sortData[$key][] = StringUtils::htmlSpecialChars($result[$key]); | |
| } | |
| } else { | |
| foreach ($item['itemAnswer'][$key] as $_key => $_value) { | |
| if ($item['itemResponseType'][$key] == 'radio') { | |
변경 전report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterId" value="{=memo.writerId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val(); | |
| var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val(); | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| writerId = memoWriterId; | |
| } | |
| if (!writerId) { // 피신고자가 비회원인 경우 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 후report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val(); | |
| var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val(); | |
| var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호 | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호 | |
| } | |
| if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 전report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterId" value="{=memo.writerId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val(); | |
| var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val(); | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| writerId = memoWriterId; | |
| } | |
| if (!writerId) { // 피신고자가 비회원인 경우 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 후report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val(); | |
| var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val(); | |
| var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호 | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호 | |
| } | |
| if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 전report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterId" value="{=memo.writerId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val(); | |
| var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val(); | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| writerId = memoWriterId; | |
| } | |
| if (!writerId) { // 피신고자가 비회원인 경우 회원 신고 및 차단 불가 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 후report.html
| <input type="hidden" name="bdId" value="{=req.bdId}"/> | |
| <input type="hidden" name="bdSno" value="{=req.bdSno}"/> | |
| <input type="hidden" name="memId" value="{=memId}"/> | |
| <input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/> | |
| <input type="hidden" name="writerMemNo" value="{=writerMemNo}"/> | |
| <input type="hidden" name="writerId" value="{=writerId}"/> | |
| // 회원 신고차단탭 노출 | |
| function memberReportOn() { | |
| var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val() | |
| var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val(); | |
| var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val(); | |
| var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호 | |
| if (memoSno > 0) { // 댓글을 작성한 경우 | |
| targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호 | |
| } | |
| if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우 회원 신고 및 차단 불가 | |
| alert("회원 신고 및 차단이 불가한 회원입니다."); | |
| } else { | |
| if ($('#frmWrite').find('#reportMemo').val()) { | |
변경 전Board.php
| } | |
| foreach ($saveFileNmArr as $index => $saveFileNm) { | |
| $newSaveFileNm = substr(md5(microtime()), 0, 16) . rand(100, 999); | |
| $binaryData = file_get_contents($saveFileNm); | |
| $result = $imageUploadService->uploadBinaryImageToRealPath($binaryData, $movePath, $newSaveFileNm); | |
| $dbTable = self::getBoardTableName($this->req['moveBdId']); | |
| } | |
| $uploadFileNm[$i] = $file_array[$i]['name']; | |
| $saveFileNm[$i] = substr(md5(microtime()), 0, 16); | |
| $listImageWidth = $this->cfg['bdListImgWidth'] ?? 500; | |
| //이미지 아니면 섬네일 생성안됨; | |
| } | |
| $uploadFileNm = $fileData['name']; | |
| $saveFileNm = 'tmp_' . substr(md5(microtime()), 0, 16); | |
| if ($this->imageResize === true) { | |
| $result = $this->storage->upload($fileData['tmp_name'], $this->cfg['bdUploadPath'] . $saveFileNm, ['width' => $reSizeImgWidth, 'height' => $reSizeImgHeight]); | |
변경 후Board.php
| } | |
| foreach ($saveFileNmArr as $index => $saveFileNm) { | |
| $newSaveFileNm = ImageUploadService::generateSaveFileName($uploadFileNmArr[$index] ?? '', 16, true); | |
| $binaryData = file_get_contents($saveFileNm); | |
| $result = $imageUploadService->uploadBinaryImageToRealPath($binaryData, $movePath, $newSaveFileNm); | |
| $dbTable = self::getBoardTableName($this->req['moveBdId']); | |
| } | |
| $uploadFileNm[$i] = $file_array[$i]['name']; | |
| $saveFileNm[$i] = ImageUploadService::generateSaveFileName($uploadFileNm[$i]); | |
| $listImageWidth = $this->cfg['bdListImgWidth'] ?? 500; | |
| //이미지 아니면 섬네일 생성안됨; | |
| } | |
| $uploadFileNm = $fileData['name']; | |
| $saveFileNm = 'tmp_' . ImageUploadService::generateSaveFileName($uploadFileNm); | |
| if ($this->imageResize === true) { | |
| $result = $this->storage->upload($fileData['tmp_name'], $this->cfg['bdUploadPath'] . $saveFileNm, ['width' => $reSizeImgWidth, 'height' => $reSizeImgHeight]); | |
변경 전EditorUpload.php
| $result['result'] = false; | |
| try { | |
| if (file_exists($filePath)) { | |
| $saveFileName = substr(md5(microtime()), 0, 16) . rand(100, 999); | |
| $file = fopen($filePath, 'rb'); | |
| $size = filesize($filePath); | |
| $binary_data = fread($file, $size); | |
변경 후EditorUpload.php
| $result['result'] = false; | |
| try { | |
| if (file_exists($filePath)) { | |
| $saveFileName = ImageUploadService::generateSaveFileName($filePath, 16, true); | |
| $file = fopen($filePath, 'rb'); | |
| $size = filesize($filePath); | |
| $binary_data = fread($file, $size); | |
변경 전ArticlePsController.php
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| $boardConfig = new BoardConfig($req['bdId']); | |
| $result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| $boardAct = new ArticleActAdmin($req); | |
| $result = $boardAct->uploadAjax($fileData); | |
변경 후ArticlePsController.php
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| $boardConfig = new BoardConfig($req['bdId']); | |
| $imageUploadService = new ImageUploadService(); | |
| // 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단) | |
| if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) { | |
| throw new AlertBackException('업로드 할 수 없는 파일입니다.'); | |
| } | |
| $result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| $boardAct = new ArticleActAdmin($req); | |
| $result = $boardAct->uploadAjax($fileData); | |
변경 전BoardPsController.php
| // 기본 저장소 인지 확인 | |
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| // 기본 저장소일 경우 OBS Upload 진행 | |
| $result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| // 저장소 변경일 경우 해당 저장소에 Upload 진행 | |
| $boardAct = new BoardAct($req); | |
변경 후BoardPsController.php
| // 기본 저장소 인지 확인 | |
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| // 기본 저장소일 경우 OBS Upload 진행 | |
| $imageUploadService = new ImageUploadService(); | |
| // 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단) | |
| if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) { | |
| throw new AlertBackException('업로드 할 수 없는 파일입니다.'); | |
| } | |
| $result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| // 저장소 변경일 경우 해당 저장소에 Upload 진행 | |
| $boardAct = new BoardAct($req); | |
변경 전BoardPsController.php
| // 기본 저장소 인지 확인 | |
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| // 기본 저장소일 경우 OBS Upload 진행 | |
| $result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| // 저장소 변경일 경우 해당 저장소에 Upload 진행 | |
| $boardAct = new BoardAct($req); | |
변경 후BoardPsController.php
| // 기본 저장소 인지 확인 | |
| if (Board::isDefaultUploadStorage($req['bdId'])) { | |
| // 기본 저장소일 경우 OBS Upload 진행 | |
| $imageUploadService = new ImageUploadService(); | |
| // 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단) | |
| if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) { | |
| throw new AlertBackException('업로드 할 수 없는 파일입니다.'); | |
| } | |
| $result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']); | |
| } else { | |
| // 저장소 변경일 경우 해당 저장소에 Upload 진행 | |
| $boardAct = new BoardAct($req); | |
변경 전crm_recipe_frame.php
변경 후crm_recipe_frame.php
| <link type="text/css" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/crm-recipe.css') ?>" rel="stylesheet"/> | |
| <script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script> | |
| <iframe name="crmRecipe" src="<?= gd_htmlspecialchars($pageUrl) ?>" frameborder="0" marginwidth="0" marginheight="0" width="100%" style="height: 100vh;"></iframe> | |
| <script> | |
| (() => { | |
| 'use strict'; | |
| const DRAWER_ID = 'crm-recipe-drawer'; | |
| const PENDING_TOAST_KEY = 'crmRecipePendingToast'; | |
| (() => { | |
| try { | |
| const raw = sessionStorage.getItem(PENDING_TOAST_KEY); | |
| if (!raw) return; | |
| sessionStorage.removeItem(PENDING_TOAST_KEY); | |
| const saved = JSON.parse(raw); | |
| if (saved?.toast && window.NCDSToast && Date.now() - saved.at < 10000) { | |
| window.NCDSToast(saved.toast); | |
| } | |
| } catch (e) {/* sessionStorage 접근 불가 환경 무시 */} | |
| })(); | |
| const onKeydown = (event) => { | |
| if (event.key === 'Escape') closeDrawer(); | |
| }; | |
| const closeDrawer = () => { | |
| document.getElementById(DRAWER_ID)?.remove(); | |
| // 어드민은 window 스크롤이라 html/body 둘 다 해제 | |
| document.documentElement.classList.remove('crm-recipe-drawer-open'); | |
| document.body.classList.remove('crm-recipe-drawer-open'); | |
| document.removeEventListener('keydown', onKeydown); | |
| }; | |
| const openDrawer = (url) => { | |
| closeDrawer(); // 중복 오픈 방지 | |
| const iframe = Object.assign(document.createElement('iframe'), { | |
| className: 'crm-recipe-drawer__iframe', | |
| src: url, | |
| }); | |
| const panel = document.createElement('div'); | |
| panel.className = 'crm-recipe-drawer__panel'; | |
| panel.append(iframe); | |
| const backdrop = document.createElement('div'); | |
| backdrop.id = DRAWER_ID; | |
| backdrop.className = 'crm-recipe-drawer'; | |
| backdrop.append(panel); | |
| document.body.append(backdrop); | |
| // 부모 스크롤 잠금 + 스크롤바 제거 | |
| document.documentElement.classList.add('crm-recipe-drawer-open'); | |
| document.body.classList.add('crm-recipe-drawer-open'); | |
| document.addEventListener('keydown', onKeydown); | |
| }; | |
| window.addEventListener('message', ({ data }) => { | |
| if (data?.type !== 'CLOSE_CRM_RECIPE_DRAWER') return; | |
| closeDrawer(); | |
| if (data.toast) { | |
| try { | |
| sessionStorage.setItem(PENDING_TOAST_KEY, JSON.stringify({ toast: data.toast, at: Date.now() })); | |
| } catch (e) {/* sessionStorage 접근 불가 환경 무시 */} | |
| } | |
| if (data.reloadList) { | |
| const listFrame = document.querySelector('iframe[name="crmRecipe"]'); | |
| if (listFrame) listFrame.src = listFrame.src; | |
| } | |
| }); | |
| // 공통 상품선택 팝업의 콜백을 드로어 iframe 으로 전달 | |
| window.setAddGoods = function (resultJson) { | |
| document.querySelector('.crm-recipe-drawer__iframe')?.contentWindow?.setAddGoods?.(resultJson); | |
| }; | |
| // PERMISSION_CHECK 응답용 action 별 거부 사유 (null = 허용) | |
| const PERMISSION_DENIAL_REASONS = <?= json_encode($permissionDenialReasons, JSON_HEX_TAG) ?>; | |
| IframeMessageRouter.init({ | |
| onLayerOpen: openDrawer, | |
| onLayerClose: closeDrawer, | |
| onPermissionCheck: (event) => { | |
| if (event.data?.category !== 'CRM_RECIPE') return; | |
| const { action, requestId } = event.data.payload || {}; | |
| const known = Object.prototype.hasOwnProperty.call(PERMISSION_DENIAL_REASONS, action); | |
| const reason = known ? PERMISSION_DENIAL_REASONS[action] : null; | |
| const payload = { requestId, granted: known && !reason }; | |
| if (reason) payload.reason = reason; | |
| event.source?.postMessage({ category: 'CRM_RECIPE', type: 'PERMISSION_CHECK', payload }, event.origin || '*'); | |
| }, | |
| }); | |
| })(); | |
| </script> | |
변경 전crm_recipe_send.php
| }); | |
| } | |
| function initializeEvents() { | |
| document.querySelector('#closeCrmRecipe').addEventListener('click', function () { | |
| NCDSConfirm({ | |
| message: '레시피 등록을 중단하시겠습니까?', | |
| subMessage: '저장하지 않은 내용이 있습니다. 현재 설정한 내용이 모두 사라집니다.', | |
| callback: function (result) { | |
| if (result && window.parent && window.parent !== window) { | |
| window.parent.postMessage({ type: 'CLOSE_CRM_RECIPE_DRAWER' }, '*'); | |
| } | |
| }, | |
| }); | |
| }); | |
| window.alternativePreview?.setContent(alternativeContents); | |
| } | |
| // 인원 0명이어도 컨펌 없이 바로 발송전 미리보기 | |
| openPreviewLayerPopup($button); | |
| } | |
변경 후crm_recipe_send.php
| }); | |
| } | |
| function sendClickLog(action) { | |
| return $.post('./crm_recipe_click_log_ps.php', { | |
| action: action, | |
| recipeType: document.querySelector('input[name="recipeType"]')?.value ?? '', | |
| currentPage: location.href.split('?')[0], | |
| }); | |
| } | |
| function initializeEvents() { | |
| document.querySelector('#closeCrmRecipe').addEventListener('click', function () { | |
| NCDSConfirm({ | |
| message: '레시피 등록을 중단하시겠습니까?', | |
| subMessage: '저장하지 않은 내용이 있습니다. 현재 설정한 내용이 모두 사라집니다.', | |
| callback: function (result) { | |
| if (!result) { | |
| return; | |
| } | |
| // 로그 요청 완료 후 드로어를 닫아야 요청이 취소되지 않음 | |
| sendClickLog('close').always(function () { | |
| if (window.parent && window.parent !== window) { | |
| window.parent.postMessage({ type: 'CLOSE_CRM_RECIPE_DRAWER' }, '*'); | |
| } | |
| }); | |
| }, | |
| }); | |
| }); | |
| window.alternativePreview?.setContent(alternativeContents); | |
| } | |
| sendClickLog('send'); | |
| // 인원 0명이어도 컨펌 없이 바로 발송전 미리보기 | |
| openPreviewLayerPopup($button); | |
| } | |
| <link type="text/css" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/mobile-send.css') ?>" rel="stylesheet"/> | |
| <link type="text/css" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/crm-common.css') ?>" rel="stylesheet"/> | |
| <link type="text/css" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/crm/crm-recipe-send.css') ?>" rel="stylesheet"/> | |
| <link type="text/css" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/slider/slick/slick.css') ?>" rel="stylesheet"/> | |
| <script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/slider/slick/slick.min.js') ?>"></script> | |
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/charcount.js') ?>"></script> | |
| <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> | |
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/scroll-sticky.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 . 'ncds/script/common/godo-ui-module/chip-selector/chip-selector.js') ?>"></script> | |
| <script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/message-input/message-input.js') ?>"></script> | |
| <script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/crm-preview/crm-preview-loader.js') ?>"></script> | |
| <script type="text/javascript" src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/godo-ui-module/godo-ui-module.js') ?>"></script> | |
| <script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script> | |
| <article class="ncua-content content-wrap-initial mobile-send"> | |
| <form id="frmCrmRecipe"> | |
| <input type="hidden" name="recipeType" value="<?= gd_htmlspecialchars($recipeType ?? '') ?>"/> | |
| <input type="hidden" name="recipeName" value="<?= gd_htmlspecialchars($recipeName ?? '레시피') ?>"/> | |
| <header class="ncua-page-header page-header js-affix"> | |
| <h3 class="ncua-help-manual"><?= gd_htmlspecialchars($recipeName ?? '레시피') ?></h3> | |
| <div class="ncua-page-header__actions"> | |
| <button id="closeCrmRecipe" type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray">닫기</button> | |
| <button id="previewCrmRecipe" type="button" class="ncua-btn ncua-btn--sm ncua-btn--primary">발송</button> | |
| </div> | |
| </header> | |
| <div class="ncua-split-layout"> | |
| <div class="section-container"> | |
| <div id="layerRecipientSetting" class="ncua-card ncua-card--no-border"> | |
| <section class="js-recipe-recipient-section js-recipient-setting-section"> | |
| <header class="ncua-card__header"> | |
| <div> | |
| <h4 class="ncua-card__title">수신 대상</h4> | |
| <?php if (!empty($recipeDefault['guideMessages']['targetConditionGuide'])): ?> | |
| <p class="ncua-notice-info js-recipient-guide"><?= gd_htmlspecialchars($recipeDefault['guideMessages']['targetConditionGuide']) ?></p> | |
| <?php endif; ?> | |
| </div> | |
| <button type="button" id="openRecipientDetail" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray">상세 설정</button> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <input type="hidden" name="selectedCrmGroup" value="" /> | |
| <div class="ncua-border-group-box recipient-condition-group"> | |
| <?php if (!empty($conditionGuideView['hasConditionGuide'])): ?> | |
| <div class="condition-guide-box js-condition-guide-box"> | |
| <div class="condition-guide-row js-condition-selection-guide"> | |
| <?= gd_htmlspecialchars($conditionGuideView['guideParts'][0] ?? '') ?> | |
| <span class="ncua-select ncua-select--xs"> | |
| <span class="ncua-select__content"> | |
| <select name="conditionValue" class="ncua-select__tag js-condition-value-select"> | |
| <?php foreach ($conditionGuideView['selectableValues'] as $v): ?> | |
| <option value="<?= gd_htmlspecialchars((string) $v) ?>" <?= $v === $conditionGuideView['currentValue'] ? 'selected' : '' ?>><?= gd_htmlspecialchars((string) $v) ?></option> | |
| <?php endforeach; ?> | |
| </select> | |
| </span> | |
| </span> | |
| <?= gd_htmlspecialchars($conditionGuideView['guideParts'][1] ?? '') ?> | |
| </div> | |
| <p class="ncua-notice-info js-condition-selected-guide"></p> | |
| <?php if (!empty($conditionGuideView['productSelectableCount'])): ?> | |
| <div class="ncua-border-group-box condition-product-select-box js-condition-product-select"> | |
| <button type="button" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray js-open-product-select">상품 선택</button> | |
| <input type="hidden" name="selectedProductNos" class="js-selected-product-nos" value="" data-max="<?= (int) $conditionGuideView['productSelectableCount'] ?>" /> | |
| <div class="condition-product-select__list js-selected-product-list"></div> | |
| <p class="ncua-notice-info">특정 상품은 최대 <?= (int) $conditionGuideView['productSelectableCount'] ?>개까지 선택 가능합니다.</p> | |
| </div> | |
| <?php endif; ?> | |
| </div> | |
| <?php endif; ?> | |
| <div class="crm-group-selected-row js-crm-group-selected-row display-none"> | |
| <span id="crmGroupTagContainer" class="crm-group-tag-container"></span> | |
| <span class="crm-group-extraction js-crm-group-extraction display-none"> | |
| <span class="crm-group-extraction-text">고객 추출 중 입니다.</span> | |
| <button type="button" class="ncua-btn ncua-btn--xs ncua-btn--text-gray has-underline refresh-crm-group"> | |
| <img src="<?= PATH_ADMIN_GD_SHARE ?>ncds/image/refresh-cw-02.svg" alt="새로고침" /> | |
| 새로고침 | |
| </button> | |
| </span> | |
| <span class="recipient-count-text js-recipient-count-text display-none">발송 인원 총 <strong class="recipient-count-total">0</strong> 명 <span class="point-text">(수신거부대상자 <strong class="recipient-count-reject">0</strong> 명 포함)</span></span> | |
| </div> | |
| </div> | |
| <div class="recipient-info"> | |
| <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="isOnlyAgreed" checked /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">SMS 수신동의한 회원에게 발송</span> | |
| </label> | |
| <span class="ncua-flex" data-tooltip-code="260604001" data-tooltip-seq="001"></span> | |
| </div> | |
| </section> | |
| </section> | |
| </div> | |
| <div id="layerSendMethodSetting" class="ncua-card ncua-card--no-border"></div> | |
| <div id="layerSendTimeSetting" class="ncua-card ncua-card--no-border"></div> | |
| </div> | |
| <aside class="ncua-panel"> | |
| <div id="layerPreviewMessage"></div> | |
| </aside> | |
| </div> | |
| </form> | |
| </article> | |
| <div id="crmRecipeData" | |
| data-sms-point-each="<?= gd_htmlspecialchars((string) $smsPointEach) ?>" | |
| data-lms-point-each="<?= gd_htmlspecialchars((string) $lmsPointEach) ?>" | |
| data-kakao-alrimtalk-point-each="<?= gd_htmlspecialchars((string) $kakaoAlrimTalkPointEach) ?>" | |
| data-kakao-friendtalk-point-each="<?= gd_htmlspecialchars((string) $kakaoFriendTalkPointEach) ?>" | |
| data-message-config="<?= gd_htmlspecialchars(json_encode($messageConfig, JSON_UNESCAPED_UNICODE)) ?>" | |
| data-recipe-default="<?= gd_htmlspecialchars(json_encode($recipeDefault ?? [], JSON_UNESCAPED_UNICODE)) ?>" | |
| <?php if (!empty($crmGroup)): ?>data-crm-group="<?= gd_htmlspecialchars(json_encode($crmGroup, JSON_UNESCAPED_UNICODE)) ?>"<?php endif; ?> | |
| hidden></div> | |
| <script type="text/javascript"> | |
| const crmRecipeDataEl = document.getElementById('crmRecipeData'); | |
| const smsPointEach = Number(crmRecipeDataEl.dataset.smsPointEach); | |
| const lmsPointEach = Number(crmRecipeDataEl.dataset.lmsPointEach); | |
| const kakaoAlrimtalkPointEach = Number(crmRecipeDataEl.dataset.kakaoAlrimtalkPointEach); | |
| const kakaoFriendtalkPointEach = Number(crmRecipeDataEl.dataset.kakaoFriendtalkPointEach); | |
| const messagePoint = Number(<?= (float) ($smsPoint ?? 0) ?>); | |
| const messageConfig = JSON.parse(crmRecipeDataEl.dataset.messageConfig); | |
| const recipeDefault = JSON.parse(crmRecipeDataEl.dataset.recipeDefault); | |
| const crmGroup = JSON.parse(crmRecipeDataEl.dataset.crmGroup || '{}'); | |
| $(document).ready(function () { | |
| // 툴팁 init — layer init 보다 먼저 끝내 seq 충돌 회피 | |
| const tooltipCodeEl = document.querySelector('[data-tooltip-code]'); | |
| const cosGuideReady = (window.GodoCosGuide && tooltipCodeEl) | |
| ? window.GodoCosGuide.init({ | |
| apiUrl: <?= json_encode($cosGuideApiUrl) ?>, | |
| guideCode: Number(tooltipCodeEl.getAttribute('data-tooltip-code')), | |
| }) | |
| : Promise.resolve(); | |
| cosGuideReady.finally(() => { | |
| loadPreviewMessageLayer().then(() => { | |
| loadSendMethodSettingLayer(); | |
| }); | |
| loadSendTimeSettingLayer(); | |
| }); | |
| initializeEvents(); | |
| bindConditionGuideSelectChange(); | |
| initRecipientCrmGroup(); | |
| initProductSelect(); | |
| }); | |
| function bindConditionGuideSelectChange() { | |
| const select = document.querySelector('select.js-condition-value-select'); | |
| if (!select) return; | |
| select.addEventListener('change', updateConditionPreview); | |
| updateConditionPreview(); // 초기 미리보기도 JS가 렌더 (서버 initialPreview 제거) | |
| } | |
| function computeRelativeDateStr(offset, unit) { | |
| const date = new Date(); | |
| if (unit === 'MONTH') { | |
| const targetDay = date.getDate(); | |
| date.setDate(1); | |
| date.setMonth(date.getMonth() + offset); | |
| const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); | |
| date.setDate(Math.min(targetDay, lastDay)); | |
| } else { | |
| date.setDate(date.getDate() + offset); | |
| } | |
| // toISOString(UTC)은 KST에서 하루 어긋날 수 있어 로컬 일자로 포맷 | |
| const pad = n => String(n).padStart(2, '0'); | |
| return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; | |
| } | |
| function updateConditionPreview() { | |
| const cg = recipeDefault.conditionGuide || {}; | |
| const previewEl = document.querySelector('.js-condition-selected-guide'); | |
| const select = document.querySelector('select.js-condition-value-select'); | |
| if (!previewEl || !select || !cg.selectedValueGuide) return; | |
| const value = parseInt(select.value, 10) || 0; | |
| const unit = cg.valueUnit || 'DAY'; | |
| const recipeType = MobileMessageReplaceCode.normalizeRecipeType(document.querySelector('input[name="recipeType"]')?.value); | |
| const isPeriod = (cg.dateDisplayType || 'DAY') === 'PERIOD'; | |
| let startDay, endDay; | |
| if (recipeType === 'repurchase-reminder') { | |
| startDay = computeRelativeDateStr(-(value + 1), unit); | |
| endDay = computeRelativeDateStr(-value, unit); | |
| } else if (['dormant-coupon-wakeup', 'dormant-mileage-wakeup'].includes(recipeType)) { | |
| startDay = endDay = computeRelativeDateStr(value, unit); | |
| } else { | |
| startDay = computeRelativeDateStr(-value, unit); | |
| endDay = isPeriod ? computeRelativeDateStr(0, unit) : startDay; | |
| } | |
| previewEl.textContent = cg.selectedValueGuide | |
| .replaceAll('{no}', String(value)) | |
| .replaceAll('{selected_day}', startDay) | |
| .replaceAll('{start_day}', startDay) | |
| .replaceAll('{end_day}', endDay); | |
| } | |
| function setRecipientTypeAvailability(enable) { | |
| document.querySelectorAll('input[name="recipientType"]').forEach(input => { | |
| if (['EXCEL_UPLOAD', 'DIRECT'].includes(input.value)) { | |
| input.disabled = !enable; | |
| } | |
| }); | |
| } | |
| function setSendMethodAvailability(enable) { | |
| document.querySelectorAll('input[name="sendMethod"]').forEach(input => { | |
| if (['ALIMTALK', 'MYAPP'].includes(input.value)) { | |
| input.disabled = !enable; | |
| } | |
| }); | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked')?.value; | |
| if (sendMethod === 'FRIENDTALK') { | |
| const activeSendTabType = document.querySelector('button[data-send-tab-type].is-active')?.dataset.sendTabType; | |
| const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value; | |
| if (!enable) clearComponent?.(campaignMessageType); | |
| updateTabComponent?.(document, activeSendTabType); | |
| if (activeSendTabType === 'MAIN') updateCampaignTypeComponent?.(campaignMessageType); | |
| } else if (sendMethod === 'SMS') { | |
| document.querySelector('.js-replace-code')?.classList.toggle('display-none', !enable); | |
| if (!enable) clearSmsComponent?.(); | |
| } | |
| } | |
| function setMyappSendConditionAvailability(isRecipientAllType, isOnlyAgreed) { | |
| // 레시피는 항상 CRM 그룹 대상 → 발송조건 행 미노출, 회원 대상 고정 | |
| isRecipientAllType = false; | |
| const myappPushSendConditionRow = document.querySelector('.js-myapp-push-send-condition-row'); | |
| const appInstalledRadio = document.querySelector('input[name="myappSendCondition"][value="APP_INSTALLED"]'); | |
| const appInstalledField = appInstalledRadio?.closest('.ncua-radio-field'); | |
| myappPushSendConditionRow?.classList.toggle('display-none', !isRecipientAllType); | |
| appInstalledField?.classList.toggle('display-none', !isRecipientAllType || isOnlyAgreed); | |
| if (!isRecipientAllType || isOnlyAgreed) { | |
| const memberOnlyRadio = document.querySelector('input[name="myappSendCondition"][value="MEMBER_ONLY"]'); | |
| if (memberOnlyRadio) memberOnlyRadio.checked = true; | |
| } | |
| } | |
| function readCrmGroupCounts() { | |
| const total = parseInt((document.querySelector('.recipient-count-total')?.textContent ?? '0').replace(/[^0-9]/g, ''), 10) || 0; | |
| const reject = parseInt((document.querySelector('.recipient-count-reject')?.textContent ?? '0').replace(/[^0-9]/g, ''), 10) || 0; | |
| return { total, reject }; | |
| } | |
| function getExpectTargetCount() { | |
| if (!document.querySelector('input[name="selectedCrmGroup"]')?.value) return 0; | |
| const { total, reject } = readCrmGroupCounts(); | |
| const isOnlyAgreed = document.querySelector('input[name="isOnlyAgreed"]')?.checked; | |
| return isOnlyAgreed ? total - reject : total; | |
| } | |
| function getExpectTargetCountSeparated() { | |
| if (!document.querySelector('input[name="selectedCrmGroup"]')?.value) { | |
| return { agreedCount: 0, rejectedCount: 0, totalCount: 0 }; | |
| } | |
| const { total, reject } = readCrmGroupCounts(); | |
| return { agreedCount: total - reject, rejectedCount: reject, totalCount: total }; | |
| } | |
| function loadSendMethodSettingLayer() { | |
| $.post('./mobile_send/layer_send_method_setting.php', { recipeContext: JSON.stringify(recipeDefault) }, function (data) { | |
| $('#layerSendMethodSetting').html(data); | |
| filterSendMethodForRecipe(); | |
| }); | |
| } | |
| function loadSendTimeSettingLayer() { | |
| $.post('./mobile_send/layer_recipe_send_time_setting.php', { recipeContext: JSON.stringify(recipeDefault) }, function (data) { | |
| $('#layerSendTimeSetting').html(data); | |
| }); | |
| } | |
| function filterSendMethodForRecipe() { | |
| const input = document.querySelector('input[name="sendMethod"][value="ALIMTALK"]'); | |
| if (!input) return; | |
| input.disabled = true; | |
| input.closest('.ncua-radio-field')?.classList.add('display-none'); | |
| } | |
| function loadPreviewMessageLayer() { | |
| return $.post('./mobile_send/layer_preview_message.php', function (data) { | |
| $('#layerPreviewMessage').html(data); | |
| }); | |
| } | |
| function initializeEvents() { | |
| document.querySelector('#closeCrmRecipe').addEventListener('click', function () { | |
| NCDSConfirm({ | |
| message: '레시피 등록을 중단하시겠습니까?', | |
| subMessage: '저장하지 않은 내용이 있습니다. 현재 설정한 내용이 모두 사라집니다.', | |
| callback: function (result) { | |
| if (result && window.parent && window.parent !== window) { | |
| window.parent.postMessage({ type: 'CLOSE_CRM_RECIPE_DRAWER' }, '*'); | |
| } | |
| }, | |
| }); | |
| }); | |
| document.querySelector('#previewCrmRecipe').addEventListener('click', handleRecipeSendClick); | |
| } | |
| const recipeRecipientSection = document.querySelector('.js-recipe-recipient-section'); | |
| function initProductSelect() { | |
| const wrap = recipeRecipientSection?.querySelector('.js-condition-product-select'); | |
| if (!wrap) return; // 상품선택 미사용 레시피면 미렌더 | |
| const hidden = wrap.querySelector('.js-selected-product-nos'); | |
| const listEl = wrap.querySelector('.js-selected-product-list'); | |
| const max = parseInt(hidden?.dataset.max ?? '0', 10) || 0; | |
| let selectedProducts = []; // [{ no, name }] | |
| function escapeHtml(str) { | |
| return String(str).replace(/[&<>"']/g, function (c) { | |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; | |
| }); | |
| } | |
| function renderProducts() { | |
| hidden.value = selectedProducts.map(function (p) { return p.no; }).join(','); | |
| if (selectedProducts.length === 0) { | |
| listEl.innerHTML = ''; | |
| return; | |
| } | |
| const chips = selectedProducts.map(function (p) { | |
| return '<span class="ncua-tag ncua-tag--md condition-product-select__chip">' | |
| + '<span class="ncua-tag__text">' + escapeHtml(p.name) + '</span>' | |
| + '<button type="button" class="ncua-tag__close js-remove-product" data-no="' + escapeHtml(p.no) + '" aria-label="삭제"></button>' | |
| + '</span>'; | |
| }).join(''); | |
| listEl.innerHTML = chips | |
| + '<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--text-gray js-remove-all-product">전체 삭제</button>'; | |
| } | |
| wrap.querySelector('.js-open-product-select')?.addEventListener('click', function () { | |
| layer_add_info('goods', { layerTitle: '상품 선택', mode: 'simple', callFunc: 'setAddGoods' }); | |
| }); | |
| listEl.addEventListener('click', function (e) { | |
| const removeBtn = e.target.closest('.js-remove-product'); | |
| if (removeBtn) { | |
| selectedProducts = selectedProducts.filter(function (p) { return p.no !== removeBtn.dataset.no; }); | |
| renderProducts(); | |
| return; | |
| } | |
| if (e.target.closest('.js-remove-all-product')) { | |
| selectedProducts = []; | |
| renderProducts(); | |
| } | |
| }); | |
| // NCDS 상품선택 모달(layer_goods) 콜백 — 모달은 매번 빈 상태로 열리므로 기존 선택에 병합한다(no 기준 중복 제거). | |
| window.setAddGoods = function (resultJson) { | |
| const info = (resultJson && resultJson.info) ? resultJson.info : []; | |
| const incoming = info.map(function (v) { | |
| return { no: String(v.goodsNo), name: (v.goodsNm || v.goodsName || '') }; | |
| }); | |
| const merged = selectedProducts.slice(); | |
| incoming.forEach(function (p) { | |
| if (!merged.some(function (e) { return e.no === p.no; })) { | |
| merged.push(p); | |
| } | |
| }); | |
| if (max > 0 && merged.length > max) { | |
| NCDSAlert({ message: max + '개까지 선택 가능합니다.', iconType: 'error' }); | |
| selectedProducts = merged.slice(0, max); | |
| } else { | |
| selectedProducts = merged; | |
| } | |
| renderProducts(); | |
| }; | |
| } | |
| function initRecipientCrmGroup() { | |
| // self 로 MODAL_OPEN 을 보내 이 iframe 이 팝업의 opener 가 되게 한다 | |
| document.getElementById('openRecipientDetail')?.addEventListener('click', async function () { | |
| try { | |
| if (await CrmGroupStatus.getCount() >= CrmGroupStatus.MAX_COUNT) { | |
| NCDSAlert({ message: 'CRM 그룹은 최대 ' + CrmGroupStatus.MAX_COUNT + '개까지 등록할 수 있습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| window.postMessage({ | |
| type: 'MODAL_OPEN', | |
| payload: { | |
| target: 'CRM_GROUP_CREATE_POPUP', | |
| query: { | |
| recipeType: document.querySelector('input[name="recipeType"]')?.value ?? '', | |
| }, | |
| width: 1000, | |
| height: 570, | |
| }, | |
| }, '*'); | |
| } catch (e) { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }); | |
| recipeRecipientSection.querySelector('button.refresh-crm-group')?.addEventListener('click', async function () { | |
| try { | |
| const crmGroupNo = recipeRecipientSection.querySelector('input[name="selectedCrmGroup"]').value; | |
| const response = await CrmGroupStatus.getInfo(crmGroupNo); | |
| setSelectedMemberCount(response.data.count, response.data.smsOptOutCount); | |
| updateCrmGroupData(response.data.status); | |
| NCDSToast(CrmGroupStatus.toast(response.data.status)); | |
| } catch (e) { | |
| NCDSToast(CrmGroupStatus.errorToast()); | |
| } | |
| }); | |
| if (crmGroup?.no) { | |
| renderSelectedCrmGroup(crmGroup); | |
| } | |
| } | |
| function renderSelectedCrmGroup({ no, title, count, smsOptOutCount, status }) { | |
| const tagContainer = recipeRecipientSection.querySelector('#crmGroupTagContainer'); | |
| const selectedCrmGroupInput = recipeRecipientSection.querySelector('input[name="selectedCrmGroup"]'); | |
| markCrmGroupSelected(); | |
| tagContainer.innerHTML = ''; | |
| tagContainer.appendChild(generateCrmGroupTag(title)); | |
| selectedCrmGroupInput.value = no; | |
| selectedCrmGroupInput.dataset.crmGroupName = title; | |
| setSelectedMemberCount(count, smsOptOutCount); | |
| updateCrmGroupData(status); | |
| } | |
| window.selectCreatedCrmGroup = function (crmNo, groupName) { | |
| setSelectedCrmGroup(crmNo, groupName); | |
| }; | |
| window.getSelectedCrmGroup = function () { | |
| const input = recipeRecipientSection.querySelector('input[name="selectedCrmGroup"]'); | |
| return { crmNo: input.value, crmGroupName: input.dataset.crmGroupName }; | |
| }; | |
| window.setSelectedCrmGroup = async function (crmNo, groupName) { | |
| try { | |
| const { data } = await CrmGroupStatus.getInfo(crmNo); | |
| // status 응답 title 로 보강, 없으면 전달받은 그룹명 | |
| renderSelectedCrmGroup({ | |
| no: crmNo, | |
| title: data.title || groupName, | |
| count: data.count, | |
| smsOptOutCount: data.smsOptOutCount, | |
| status: data.status, | |
| }); | |
| } catch (e) { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }; | |
| function updateCrmGroupData(status) { | |
| CrmGroupStatus.toggleView(recipeRecipientSection, status); | |
| } | |
| function generateCrmGroupTag(groupName) { | |
| const tag = document.createElement('span'); | |
| tag.className = 'ncua-tag ncua-tag--md'; | |
| tag.innerHTML = ` | |
| <span class="ncua-tag__text"></span> | |
| <button type="button" class="ncua-tag__close"></button> | |
| `; | |
| tag.querySelector('.ncua-tag__text').textContent = groupName; | |
| tag.querySelector('.ncua-tag__close').addEventListener('click', function () { | |
| NCDSConfirm({ | |
| message: '이미 생성된 CRM 그룹입니다.', | |
| subMessage: '그룹 선택 해제하면 레시피에서 다시 선택하기 어렵습니다.<br>선택 해제하시겠습니까?<br>해당 그룹은 "CRM 그룹 관리"에서 조회할 수 있습니다.', | |
| callback: function (result) { | |
| if (!result) return; | |
| resetCrmGroupSelection(); | |
| tag.remove(); | |
| }, | |
| }); | |
| }); | |
| return tag; | |
| } | |
| function setSelectedMemberCount(totalCount, rejectCount) { | |
| recipeRecipientSection.querySelector('.recipient-count-total').innerText = totalCount; | |
| recipeRecipientSection.querySelector('.recipient-count-reject').innerText = rejectCount; | |
| } | |
| // 그룹 선택 상태 | |
| function markCrmGroupSelected() { | |
| recipeRecipientSection.querySelector('.js-condition-guide-box')?.classList.add('display-none'); | |
| recipeRecipientSection.querySelector('.js-crm-group-selected-row')?.classList.remove('display-none'); | |
| } | |
| // 그룹 미선택(초기화) 상태 | |
| function resetCrmGroupSelection() { | |
| const input = recipeRecipientSection.querySelector('input[name="selectedCrmGroup"]'); | |
| input.value = ''; | |
| input.dataset.crmGroupName = ''; | |
| setSelectedMemberCount(0, 0); | |
| recipeRecipientSection.querySelector('.js-condition-guide-box')?.classList.remove('display-none'); | |
| recipeRecipientSection.querySelector('.js-crm-group-selected-row')?.classList.add('display-none'); | |
| recipeRecipientSection.querySelector('.js-crm-group-extraction')?.classList.add('display-none'); | |
| recipeRecipientSection.querySelector('.js-recipient-count-text')?.classList.add('display-none'); | |
| } | |
| function generateTargetInfoPayload() { | |
| const input = document.querySelector('input[name="selectedCrmGroup"]'); | |
| return { | |
| targetType: 'CRM', | |
| targetCondition: { | |
| crm: { | |
| no: input?.value ? input.value : null, | |
| name: input?.dataset.crmGroupName || '' | |
| } | |
| } | |
| }; | |
| } | |
| 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); | |
| const repeatHour = document.querySelector('select[name="repeatHour"]').value; | |
| const repeatMinutes = document.querySelector('select[name="repeatMinutes"]').value; | |
| const sendTime = `${repeatHour}:${repeatMinutes}:00`; | |
| const hasNoEndDate = document.querySelector('input[name="hasNoEndDate"]').checked; | |
| const repeatConfig = { | |
| startDate: repeatStartDate, | |
| endDate: hasNoEndDate ? null : repeatEndDate, | |
| sendTime: sendTime, | |
| }; | |
| if (sendCycle === 'WEEKLY') { | |
| repeatConfig.repeatDays = [...document.querySelectorAll('input[name="repeatDays[]"]:checked')].map(el => parseInt(el.value)); | |
| } else if (sendCycle === 'MONTHLY') { | |
| const repeatDatesContainer = document.querySelector('#repeatDatesContainer'); | |
| repeatConfig.repeatDays = [...repeatDatesContainer.querySelectorAll('.ncua-tag')].map(el => parseInt(el.dataset.day)); | |
| } | |
| return { repeatType: sendCycle, repeatConfig: repeatConfig }; | |
| } | |
| function validateRepeatTime() { | |
| const [repeatStartDate, repeatEndDate] = [...document.querySelectorAll('input[name="repeatDate[]"]')].map(el => el.value); | |
| const repeatHour = document.querySelector('select[name="repeatHour"]').value; | |
| const repeatMinutes = document.querySelector('select[name="repeatMinutes"]').value; | |
| const hasNoEndDate = document.querySelector('input[name="hasNoEndDate"]').checked; | |
| if (!repeatStartDate || !repeatHour || !repeatMinutes || (!hasNoEndDate && !repeatEndDate)) { | |
| return { result: false, message: '처리 중에 오류가 발생하여 실패되었습니다.', subMessage: '반복 발송 주기 입력을 확인해주세요.' }; | |
| } | |
| const now = new Date(); | |
| const startDateTime = new Date(`${repeatStartDate} ${repeatHour}:${repeatMinutes}:00`); | |
| const diffMinutes = (startDateTime - now) / (1000 * 60); | |
| if (diffMinutes < 10) { | |
| return { result: false, message: '처리 중에 오류가 발생하여 실패되었습니다.', subMessage: '반복 발송은 현재 시간으로부터 10분 이후로만 설정이 가능합니다.' }; | |
| } | |
| const firstSchedule = getFirstScheduleDate(repeatStartDate, repeatHour, repeatMinutes); | |
| const endDate = hasNoEndDate ? null : new Date(`${repeatEndDate} ${repeatHour}:${repeatMinutes}:00`); | |
| if (firstSchedule === null || (endDate && firstSchedule > endDate)) { | |
| return { result: false, message: '선택한 기간 내 발송 일정이 없습니다.<br>발송 설정을 변경해주세요.' }; | |
| } | |
| return { result: true }; | |
| } | |
| function getFirstScheduleDate(startDateStr, hour, minutes) { | |
| const sendCycle = document.querySelector('#sendCycle').value; | |
| const baseDate = new Date(`${startDateStr} ${hour}:${minutes}:00`); | |
| if (sendCycle === 'WEEKLY') { | |
| const repeatDays = [...document.querySelectorAll('input[name="repeatDays[]"]:checked')].map(el => parseInt(el.value)); | |
| if (repeatDays.length === 0) return null; | |
| for (let i = 0; i < 7; i++) { | |
| const candidate = new Date(baseDate); | |
| candidate.setDate(candidate.getDate() + i); | |
| const dayOfWeek = candidate.getDay() === 0 ? 7 : candidate.getDay(); | |
| if (repeatDays.includes(dayOfWeek)) { | |
| return candidate; | |
| } | |
| } | |
| } else if (sendCycle === 'MONTHLY') { | |
| const repeatDatesContainer = document.querySelector('#repeatDatesContainer'); | |
| const repeatDays = [...repeatDatesContainer.querySelectorAll('.ncua-tag')].map(el => parseInt(el.dataset.day)).sort((a, b) => a - b); | |
| if (repeatDays.length === 0) return null; | |
| const startDay = baseDate.getDate(); | |
| for (const day of repeatDays) { | |
| if (day >= startDay) { | |
| const candidate = new Date(baseDate); | |
| candidate.setDate(day); | |
| if (candidate.getMonth() === baseDate.getMonth()) { | |
| return candidate; | |
| } | |
| } | |
| } | |
| const nextMonth = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 1, parseInt(hour), parseInt(minutes), 0); | |
| for (const day of repeatDays) { | |
| const candidate = new Date(nextMonth); | |
| candidate.setDate(day); | |
| if (candidate.getMonth() === nextMonth.getMonth()) { | |
| return candidate; | |
| } | |
| } | |
| } | |
| return baseDate; | |
| } | |
| function getSendData() { | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| const selectedGroupNo = document.querySelector('input[name="selectedCrmGroup"]')?.value || ''; | |
| const recipeName = document.querySelector('input[name="recipeName"]').value; | |
| const sendData = { | |
| recipeType: document.querySelector('input[name="recipeType"]').value, | |
| campaignTitle: recipeName, // 필수, 기본값=레시피명 | |
| targetInfo: generateTargetInfoPayload(), | |
| notificationServiceType: sendMethod, | |
| ...generateSendMethodRequest(), | |
| sendType: 'REPEAT', | |
| sendSchedule: generateSendSchedulePayload(), | |
| enableAlternative: enableAlternative(sendMethod), | |
| isOnlyAgreeSMSMember: document.querySelector('input[name="isOnlyAgreed"]').checked, | |
| accessRouteType: 'MESSAGE', | |
| targetExpectCount: getExpectTargetCount(), | |
| }; | |
| // 친구톡은 캠페인명을 제목으로 사용 | |
| const friendtalkCampaignName = sendData.kakaoFriendtalkRequest?.campaignName?.trim(); | |
| if (friendtalkCampaignName) { | |
| sendData.campaignTitle = friendtalkCampaignName; | |
| } | |
| // 재사용: 그룹번호 참조 / 신규: 조건+그룹명 | |
| if (selectedGroupNo) { | |
| sendData.notificationTargetRepeatNo = parseInt(selectedGroupNo, 10); | |
| } else { | |
| sendData.repeatRuleTitle = recipeName; // 신규 모드 필수, 기본값=레시피명 | |
| const conditionSelect = document.querySelector('select.js-condition-value-select'); | |
| const productNos = (document.querySelector('.js-selected-product-nos')?.value ?? '') | |
| .split(',').map(no => no.trim()).filter(Boolean); | |
| sendData.recipeCondition = { | |
| value: parseInt(conditionSelect?.value ?? '0', 10) || 0, | |
| products: productNos.map(no => ({ no: parseInt(no, 10) })), // name 은 선택값(N) | |
| }; | |
| } | |
| return sendData; | |
| } | |
| function getPreviewData() { | |
| const sendData = getSendData(); | |
| return { | |
| sendMethod: sendData.notificationServiceType, | |
| viewType: 'layer', | |
| payload: JSON.stringify(sendData), | |
| }; | |
| } | |
| function openPreviewLayerPopup(openButton) { | |
| $.ajax({ | |
| url: './layer_preview_mobile_send.php', | |
| type: 'POST', | |
| data: getPreviewData(), | |
| success: function (data) { | |
| ncds_layer_popup({ message: data, title: '발송전 미리보기', size: 'wide' }); | |
| }, | |
| complete: function () { | |
| openButton.prop('disabled', false); | |
| } | |
| }); | |
| } | |
| async function handleRecipeSendClick() { | |
| const $button = $(this); | |
| if ($button.prop('disabled')) return; | |
| $button.prop('disabled', true); | |
| document.querySelectorAll('.destructive').forEach(el => { | |
| el.classList.remove('destructive'); | |
| el.querySelector('.ncua-input__destructive-icon-wrap')?.remove(); | |
| }); | |
| const fail = (message, subMessage) => { | |
| NCDSAlert({ message, subMessage, iconType: 'error' }); | |
| $button.prop('disabled', false); | |
| }; | |
| if (!messageConfig.hasCallNumber) { | |
| return fail('발신번호가 없어 발송할 수 없습니다.', '설정에서 발신번호 등록 후 발송 시도를 해주세요.'); | |
| } | |
| if (!validateHasMessagePoint()) { | |
| return fail('메시지 포인트를 충전해 주세요'); | |
| } | |
| if (!validateMessagePoint()) { | |
| return fail('포인트가 부족하여 발송할 수 없습니다.', '발송 대상 인원보다 포인트가 적어 발송이 불가합니다. <br /> 충전 후 다시 시도해주세요.'); | |
| } | |
| const result = validateSendMethodContent(); | |
| if (!result.success) { | |
| return fail(result.message, result.subMessage); | |
| } | |
| const repeatResult = validateRepeatTime(); | |
| if (!repeatResult.result) { | |
| document.querySelectorAll('input[name="repeatDate[]"]').forEach(item => NCDSValidator.highlight(item)); | |
| NCDSValidator.highlight(document.querySelector('select[name="repeatHour"]')); | |
| NCDSValidator.highlight(document.querySelector('select[name="repeatMinutes"]')); | |
| return fail(repeatResult.message, repeatResult.subMessage); | |
| } | |
| // 대체 메시지 미작성 시 본문 치환코드 변환해 자동 세팅 | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| const mainContents = window.messageInput?.getValue() ?? ''; | |
| if (enableAlternative(sendMethod) && (window.alternativeMessageInput?.getValue() ?? '') === '') { | |
| const allKeys = [ | |
| MobileMessageReplaceCode.CRM_RECIPE, | |
| MobileMessageReplaceCode.MEMBER, MobileMessageReplaceCode.GOODS, MobileMessageReplaceCode.ORDER, | |
| MobileMessageReplaceCode.PROMOTION, MobileMessageReplaceCode.BOARD, MobileMessageReplaceCode.REGULAR, | |
| MobileMessageReplaceCode.PRESENT, | |
| ].flatMap(category => Object.keys(category)); | |
| const alternativeContents = mainContents.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => | |
| allKeys.includes(MobileMessageReplaceCode.normalizeReplaceKey(key)) ? `{${key}}` : match); | |
| window.alternativeMessageInput.insertText(alternativeContents); | |
| window.alternativePreview?.setContent(alternativeContents); | |
| } | |
| // 인원 0명이어도 컨펌 없이 바로 발송전 미리보기 | |
| openPreviewLayerPopup($button); | |
| } | |
| </script> | |
변경 전layer_preview_mobile_send.php
| <script src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/slider/slick/slick.min.js')?>"></script> | |
| <script> | |
| function resolveReplaceCodeForPreview(content, isMain = true) { | |
| const forceDefaultKeys = ['shopUrl']; | |
| const replaceMap = {}; | |
| [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| MobileMessageReplaceCode.PROMOTION, | |
| MobileMessageReplaceCode.BOARD, | |
| MobileMessageReplaceCode.REGULAR, | |
| MobileMessageReplaceCode.PRESENT, | |
| ].forEach(category => { | |
| Object.entries(category).forEach(([key, obj]) => { | |
| if (replaceMap[key] !== undefined) return; | |
| replaceMap[key] = (obj.sendMethod !== MobileMessageReplaceCode.SendMethod.AUTO || forceDefaultKeys.includes(key)) ? obj.default : ''; | |
| }); | |
| }); | |
| const shortLinks = isMain ? window.shortLinkKeys : window.shortLinkAlternativeKeys; | |
| shortLinks.forEach((_, key) => { | |
| replaceMap[key] = `쇼핑몰도메인/_s/a1B2c3D`; | |
| }); | |
| return content.replace(/[#$]?\{([a-zA-Z0-9_]+)}/g, (match, key) => { | |
| return replaceMap[key] !== undefined ? replaceMap[key] : match; | |
| }); | |
| } | |
| </script> | |
| return; | |
| } | |
| const isOnlyAgreed = previewPayload.isOnlyAgreeSMSMember ?? true; | |
| const targetCounts = getExpectTargetCountSeparated() | |
| data: { mode: 'send', payload: JSON.stringify(previewPayload) }, | |
| dataType: 'json', | |
| success: () => { | |
| NCDSToast({ | |
| message: '메시지 발송이 정상적으로 완료되었습니다.', | |
| color: 'success', | |
| }); | |
| setTimeout(() => { | |
| window.location.href = window.location.pathname; | |
| }, 3000); | |
| }, | |
| error: (xhr) => { | |
| if (xhr.status === 400) { | |
변경 후layer_preview_mobile_send.php
| <script src="<?=gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'script/slider/slick/slick.min.js')?>"></script> | |
| <script> | |
| function resolveReplaceCodeForPreview(content, isMain = true) { | |
| return MobileMessageReplaceCode.resolvePreviewContent(content, { | |
| shortLinkKeys: isMain ? window.shortLinkKeys : window.shortLinkAlternativeKeys, | |
| }); | |
| } | |
| </script> | |
| return; | |
| } | |
| // 레시피 신규 모드(기존 CRM 그룹 미선택)는 발송 시 그룹이 새로 생성되므로 20개 초과 차단 | |
| if (previewPayload.recipeType && !previewPayload.notificationTargetRepeatNo) { | |
| try { | |
| if (await CrmGroupStatus.getCount() >= CrmGroupStatus.MAX_COUNT) { | |
| NCDSAlert({ message: 'CRM 그룹은 최대 ' + CrmGroupStatus.MAX_COUNT + '개까지 등록할 수 있습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| } catch (e) { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| return; | |
| } | |
| } | |
| const isOnlyAgreed = previewPayload.isOnlyAgreeSMSMember ?? true; | |
| const targetCounts = getExpectTargetCountSeparated() | |
| data: { mode: 'send', payload: JSON.stringify(previewPayload) }, | |
| dataType: 'json', | |
| success: () => { | |
| const recipeType = document.querySelector('input[name="recipeType"]')?.value || ''; | |
| if (recipeType) { | |
| window.parent.postMessage({ | |
| type: 'CLOSE_CRM_RECIPE_DRAWER', | |
| reloadList: true, | |
| toast: { message: '메시지 발송이 정상적으로 완료되었습니다.', color: 'success', autoClose: 2000 }, | |
| }, '*'); | |
| } else { | |
| NCDSAlert({ | |
| message: '메시지 발송이 정상적으로 완료되었습니다.', | |
| iconType: 'success', | |
| callback: () => { | |
| window.location.href = window.location.pathname; | |
| }, | |
| }); | |
| } | |
| }, | |
| error: (xhr) => { | |
| if (xhr.status === 400) { | |
변경 전layer_kakao_alimtalk_content_setting.php
| kakaoAlimtalkTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = kakaoAlimtalkTabSection.querySelector('textarea[name="messageContent"]').value; | |
| const allKeys = [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(key) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| allowEmoji: false, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
변경 후layer_kakao_alimtalk_content_setting.php
| kakaoAlimtalkTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = kakaoAlimtalkTabSection.querySelector('textarea[name="messageContent"]').value; | |
| const allKeys = [ | |
| MobileMessageReplaceCode.CRM_RECIPE, | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(MobileMessageReplaceCode.normalizeReplaceKey(key)) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| allowEmoji: false, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeAlternativeContainer', input); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
변경 전layer_kakao_friendtalk_content_setting.php
| <div class="ncua-input ncua-input--sm"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--sm"> | |
| <input name="friendtalkCampaignName" type="text" value="" maxlength="20" placeholder="캠페인명을 입력해주세요 (최대 20자)" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 캠페인 이미지 --> | |
| </ul> | |
| </div> | |
| <!-- 친구톡 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="MAIN" open> | |
| <summary>친구톡 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| <li>예약 및 반복 발송 시 선택한 CRM 그룹의 예상 발송 인원은 실제 발송 시점의 인원과 상이할 수 있습니다.</li> | |
| <li>수신 대상 중 카카오톡 채널 친구가 아닌 회원에게는 캠페인 발송에 실패하며 발송 실패 시 차감된 포인트는 반환됩니다.</li> | |
| <li>수신 번호가 중복된 경우 가장 먼저 조회된 회원에게만 발송됩니다. 중복 발송 실패 시 차감된 포인트는 반환됩니다.</li> | |
| <li>대체 발송의 경우 <a href="../crm/message_config.php">메시지 설정</a>에서 변경이 가능합니다.</li> | |
| </ul> | |
| </div> | |
| </details> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="ALTERNATIVE" open> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| const kakaoFriendtalkCarouselSection = document.querySelector('.js-kakao-friendtalk-carousel'); | |
| const chipContainer = document.querySelector('[data-role="chip-container"]'); | |
| const linkPlatformType = '<?= $linkPlatformType ?>'; | |
| const campaignTypeUnitPoint = <?= json_encode($campaignMessageTypeUnitPoints, JSON_UNESCAPED_UNICODE) ?>; | |
| const MAX_CAMPAIGN_IMAGE_FILE_SIZE = 5 * 1024 * 1024; // 5MB | |
| const MIN_CAMPAIGN_IMAGE_DIMENSION = {minWidth: 500, minHeight: 250}; | |
| const MIN_ITEM_FIRST_IMAGE_DIMENSION = {minWidth: 500, minHeight: 250}; // 첫번째 아이템 2:1 비율 | |
| if (!carousel) return; | |
| // 타이틀 (header) | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]').value = carousel.header; | |
| // 이미지 URL (hidden input) | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignImageUrl"]').value = carousel.imageUrl; | |
| // 메시지 (message) | |
| window.messageInput.setValue(carousel.message); | |
| // 버튼/쿠폰 영역 렌더링 | |
| kakaoFriendtalkButtonSection.querySelectorAll('.ncua-table').forEach(t => t.remove()); | |
| } | |
| }; | |
| }), | |
| replaceKeys: extractReplaceKeys(this.carousels.filter(c => !c.isDeleted).map(c => c.message).join(' '), true), | |
| trackingLink: this.carousels.filter(c => !c.isDeleted).flatMap(carousel => { | |
| const shortLinks = []; | |
| carousel.shortLinkKeys.forEach((url, key) => { | |
| kakaoFriendtalkTabSection.querySelectorAll('input[name="campaignMessageType"]').forEach(radio => { | |
| radio.addEventListener('change', function () { | |
| clearComponent(radio.value); | |
| updateTabComponent(kakaoFriendtalkTabSection, 'MAIN'); | |
| renderMessageInput(radio.value); | |
| updateCampaignUnitPoint(radio.value); | |
| updateCampaignTypeComponent(radio.value); | |
| updateDefaultPreview(radio.value); | |
| }); | |
| }); | |
| if (e.target.value === '') { | |
| setDefaultPreview(['FRIENDTALK_TITLE']); | |
| } | |
| }); | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]').addEventListener('input', function(e) { | |
| kakaoFriendtalkTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| const allKeys = [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(key) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| function renderMessageInput(campaignMessageType) { | |
| window.messageInput.clear(); | |
| window.preview.reset(); | |
| window.preview.setMessageType(campaignMessageType) | |
| } | |
| function clearComponent(campaignMessageType) { | |
| // 캠페인 명, 타이틀 초기화 | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignName"]').value = ''; | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignHeader"]').value = ''; | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]').value = ''; | |
| // 캠페인 이미지 초기화 | |
| // 아이템 리스트 초기화 | |
| kakaoFriendtalkItemListSection.querySelector('tbody').innerHTML = ''; | |
| // 성과분석 숏링크 초기화 | |
| kakaoFriendtalkTabSection.querySelector('input[name="shortLink"]').value = ''; | |
| const itemListTableBody = kakaoFriendtalkItemListSection.querySelector('tbody'); | |
| let itemListDatas = []; | |
| itemListTableBody.querySelectorAll('tr').forEach(row => { | |
| const title = row.querySelector('input[name="itemTitle"]').value; | |
| const imageUrl = row.querySelector('input[name="itemImageUrl"]')?.value ?? ''; | |
| itemListDatas.push({title: title, desc: '', image: imageUrl}); | |
| }); | |
| newRow.querySelector('[data-action="remove-friendtalk-item"]')?.addEventListener('click', function() { | |
| if (itemListTableBody.querySelectorAll('tr').length > 1) { | |
| newRow.remove(); | |
| updateItemListPreview(); | |
| } | |
| }); | |
| initItemImageFileInput(`campaign-item-image-file-input-container-${itemKey}`, itemKey, isFirst); | |
| } | |
| function appendCouponRow() { | |
| <div class="ncua-table ncua-table--horizontal add-coupon-button-table" > | |
| <table id="kakaoFriendTalkcouponTable"> | |
| <colgroup> | |
| <col width="80px"/> | |
| <col/> | |
| <col width="88px"/> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th><div class="ncua-align-center">타입</div></th> | |
| <th class="ncua-required"><div>쿠폰 선택</div></th> | |
| <th><div class="ncua-align-center">삭제</div></th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td> | |
| <div class="ncua-align-center">쿠폰</div> | |
| </td> | |
| <td> | |
| <div class="ncua-flex-column ncua-gap-8"> | |
| <button id="selectCoupon" type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray">쿠폰 선택</button> | |
| <div class="ncua-table ncua-table--horizontal add-link-button-table"> | |
| <table id="linkButtonTable"> | |
| <colgroup> | |
| <col width="80px"/> | |
| <col/> | |
| <col/> | |
| <col width="88px"/> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| <th><div class="ncua-align-center">타입</div></th> | |
| <th class="ncua-required"><div>버튼명</div></th> | |
| ${isAppLink() ? `<th class="ncua-required"><div>APP 링크</div></th>` : `<th class="ncua-required"><div data-tooltip-seq="008">WEB 링크</div></th>`} | |
| <th><div class="ncua-align-center">삭제</div></th> | |
| const uniqueKey = `link-button-name-${Date.now()}`; | |
| return ` | |
| <tr> | |
| <td><div class="ncua-align-center">기본</div></td> | |
| <td> | |
| <div> | |
| <div class="ncua-input ncua-input--xs ncua-input-full-width button-name-input"> | |
| </div> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input name="linkButtonIOSUrl" type="text" value="" maxlength="800" placeholder="https://commerce.com/page/event01" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input name="linkButtonAOSUrl" type="text" value="" maxlength="800" placeholder="https://commerce.com/page/event01" /> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span> | |
| </div> | |
| </div> | |
| </td>` | |
| <input name="linkButtonWebUrl" type="text" value="" maxlength="800" placeholder="" /> | |
| </div> | |
| </div> | |
| <span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span> | |
| </div> | |
| </div> | |
| </td>`; | |
| } | |
| function getItemRowTemplate(isFirst = true) { | |
| const uniqueKey = Date.now().toString(); | |
| return ` | |
| <tr data-item-key="${uniqueKey}"> | |
| <input name="itemWebLink" type="text" value="" placeholder="" /> | |
| </div> | |
| </div> | |
| <span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span> | |
| </div> | |
| </div> | |
| </td> | |
| // 캐러셀 피드 초기화 | |
| carouselState.init(); | |
| // 본문 메시지 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| title: '내용 입력', | |
| maxLength: 1300, | |
| useBytes: false, | |
| onInput: function(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| if (input === '') { | |
| setDefaultPreview(['TEXT']); | |
| } | |
| } | |
| }); | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| categories: buildReplaceCodeCategories('#'), | |
| onSelect: (variable) => { | |
| if (getCampaignMessageType() === 'WIDE_ITEM_LIST') { | |
| const headerInput = kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignHeader"]'); | |
| headerInput.value = headerInput.value + variable; | |
| headerInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } else { | |
| window.messageInput.insertText(variable); | |
| } | |
| }, | |
| onCategoryChange: (category) => { | |
| window.alternativeChipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeAlternativeContainer', | |
| title: '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| categories: buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| window.alternativeMessageInput.insertText(variable); | |
| }, | |
| onCategoryChange: (category) => { | |
| if (category === 'MEMBER') { | |
| }); | |
| setDefaultPreview(['ALTERNATIVE_TEXT', 'TEXT']); | |
| } | |
| // 친구톡 아이템 이미지 업로드 초기화 | |
| onChange: async (newFiles) => { | |
| const file = newFiles[0]; | |
| // 하이라이트 제거 | |
| container.querySelector('.ncua-image-file-input').classList.remove('destructive'); | |
변경 후layer_kakao_friendtalk_content_setting.php
| <div class="ncua-input ncua-input--sm"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--sm"> | |
| <input name="friendtalkCampaignName" type="text" value="<?= gd_htmlspecialchars($campaignName ?? '') ?>" maxlength="20" placeholder="캠페인명을 입력해주세요 (최대 20자)" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <?php if ($isRecipeContext): ?> | |
| <div class="ncua-hint-text js-wide-item-point-hint display-none"></div> | |
| <?php endif; ?> | |
| </div> | |
| <!-- 캠페인 이미지 --> | |
| </ul> | |
| </div> | |
| <!-- 친구톡 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="MAIN" <?= !$isRecipeContext ? 'open' : '' ?>> | |
| <summary>친구톡 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| <li>예약 및 반복 발송 시 선택한 CRM 그룹의 예상 발송 인원은 실제 발송 시점의 인원과 상이할 수 있습니다.</li> | |
| <li>수신 대상 중 카카오톡 채널 친구가 아닌 회원에게는 캠페인 발송에 실패하며 발송 실패 시 차감된 포인트는 반환됩니다.</li> | |
| <li>수신 번호가 중복된 경우 가장 먼저 조회된 회원에게만 발송됩니다. 중복 발송 실패 시 차감된 포인트는 반환됩니다.</li> | |
| <li>대체 발송의 경우 <a href="../crm/message_config.php" target="_top">메시지 설정</a>에서 변경이 가능합니다.</li> | |
| </ul> | |
| </div> | |
| </details> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="ALTERNATIVE" <?= !$isRecipeContext ? 'open' : '' ?>> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| const kakaoFriendtalkCarouselSection = document.querySelector('.js-kakao-friendtalk-carousel'); | |
| const chipContainer = document.querySelector('[data-role="chip-container"]'); | |
| // 아이템 리스트 행별 이미지 업로더 (itemKey → ImageFileInput) | |
| const itemImageFileInputs = new Map(); | |
| // 동기 루프에서 itemKey 충돌 방지용 시퀀스 | |
| let itemRowKeySeq = 0; | |
| const isRecipeContext = <?= $isRecipeContext ? 'true' : 'false' ?>; | |
| const linkPlatformType = '<?= $linkPlatformType ?>'; | |
| const campaignTypeUnitPoint = <?= json_encode($campaignMessageTypeUnitPoints, JSON_UNESCAPED_UNICODE) ?>; | |
| function friendtalkUnitPointHint(campaignMessageType) { | |
| return `친구톡 건당 ${parseFloat(campaignTypeUnitPoint[campaignMessageType]).toFixed(1)}포인트 차감`; | |
| } | |
| const friendtalkRequest = <?= json_encode($friendtalkRequest ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| const MAX_CAMPAIGN_IMAGE_FILE_SIZE = 5 * 1024 * 1024; // 5MB | |
| const MIN_CAMPAIGN_IMAGE_DIMENSION = {minWidth: 500, minHeight: 250}; | |
| const MIN_ITEM_FIRST_IMAGE_DIMENSION = {minWidth: 500, minHeight: 250}; // 첫번째 아이템 2:1 비율 | |
| if (!carousel) return; | |
| // 타이틀 (header) | |
| const campaignTitleInput = kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]'); | |
| campaignTitleInput.value = carousel.header; | |
| const titleCountEl = kakaoFriendtalkTabSection.querySelector('.ncua-input__field-text-count[data-charcount-text="title"] .ncua-input__field-text-count-current'); | |
| if (titleCountEl) titleCountEl.textContent = campaignTitleInput.value.length; | |
| // 이미지 URL (hidden input) | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignImageUrl"]').value = carousel.imageUrl; | |
| // 메시지 (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; ?> | |
| // 버튼/쿠폰 영역 렌더링 | |
| kakaoFriendtalkButtonSection.querySelectorAll('.ncua-table').forEach(t => t.remove()); | |
| } | |
| }; | |
| }), | |
| replaceKeys: extractReplaceKeys(this.carousels.filter(c => !c.isDeleted).map(c => `${c.header || ''} ${c.message || ''}`).join(' '), true), | |
| trackingLink: this.carousels.filter(c => !c.isDeleted).flatMap(carousel => { | |
| const shortLinks = []; | |
| carousel.shortLinkKeys.forEach((url, key) => { | |
| kakaoFriendtalkTabSection.querySelectorAll('input[name="campaignMessageType"]').forEach(radio => { | |
| radio.addEventListener('change', function () { | |
| clearComponent(radio.value); | |
| // 슬라이드 재구성 전에 레시피 캐러셀 기본값 선반영 | |
| preloadRecipeCarouselsIfNeeded(radio.value); | |
| updateTabComponent(kakaoFriendtalkTabSection, 'MAIN'); | |
| renderMessageInput(radio.value); | |
| updateCampaignUnitPoint(radio.value); | |
| updateCampaignTypeComponent(radio.value); | |
| updateDefaultPreview(radio.value); | |
| applyFriendtalkRecipeDefaults(radio.value); | |
| }); | |
| }); | |
| if (e.target.value === '') { | |
| setDefaultPreview(['FRIENDTALK_TITLE']); | |
| } | |
| <?php if ($isRecipeContext): ?> | |
| kakaoFriendtalkTabSection.querySelector('.js-wide-item-point-hint')?.classList.toggle('display-none', e.target.value === ''); | |
| <?php endif; ?> | |
| }); | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]').addEventListener('input', function(e) { | |
| kakaoFriendtalkTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| const allKeys = [ | |
| MobileMessageReplaceCode.CRM_RECIPE, | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(MobileMessageReplaceCode.normalizeReplaceKey(key)) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| function renderMessageInput(campaignMessageType) { | |
| window.messageInput.clear(); | |
| window.messageInput.setHint(null); | |
| window.preview.reset(); | |
| window.preview.setMessageType(campaignMessageType) | |
| } | |
| function clearComponent(campaignMessageType) { | |
| // 타이틀 초기화 (캠페인 명은 캠페인 레벨 값이라 타입 전환 시 유지) | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignHeader"]').value = ''; | |
| kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignTitle"]').value = ''; | |
| // 캠페인 이미지 초기화 | |
| // 아이템 리스트 초기화 | |
| kakaoFriendtalkItemListSection.querySelector('tbody').innerHTML = ''; | |
| itemImageFileInputs.clear(); | |
| // 성과분석 숏링크 초기화 | |
| kakaoFriendtalkTabSection.querySelector('input[name="shortLink"]').value = ''; | |
| const itemListTableBody = kakaoFriendtalkItemListSection.querySelector('tbody'); | |
| let itemListDatas = []; | |
| itemListTableBody.querySelectorAll('tr').forEach((row, idx) => { | |
| const title = row.querySelector('input[name="itemTitle"]').value; | |
| let imageUrl = row.querySelector('input[name="itemImageUrl"]')?.value ?? ''; | |
| // 첫 아이템은 이미지가 비면 기본 placeholder 표시 | |
| if (idx === 0 && !imageUrl) { | |
| imageUrl = '<?= PATH_ADMIN_GD_SHARE ?>ncds/image/temp-preview02.png'; | |
| } | |
| itemListDatas.push({title: title, desc: '', image: imageUrl}); | |
| }); | |
| newRow.querySelector('[data-action="remove-friendtalk-item"]')?.addEventListener('click', function() { | |
| if (itemListTableBody.querySelectorAll('tr').length > 1) { | |
| newRow.remove(); | |
| itemImageFileInputs.delete(itemKey); | |
| updateItemListPreview(); | |
| } | |
| }); | |
| itemImageFileInputs.set(itemKey, initItemImageFileInput(`campaign-item-image-file-input-container-${itemKey}`, itemKey, isFirst)); | |
| } | |
| function appendCouponRow() { | |
| <div class="ncua-table ncua-table--horizontal add-coupon-button-table" > | |
| <table id="kakaoFriendTalkcouponTable"> | |
| <colgroup> | |
| ${isRecipeContext ? '' : '<col width="80px"/>'} | |
| <col/> | |
| <col width="88px"/> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| ${isRecipeContext ? '' : '<th><div class="ncua-align-center">타입</div></th>'} | |
| <th class="ncua-required"><div>쿠폰 선택</div></th> | |
| <th><div class="ncua-align-center">삭제</div></th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| ${isRecipeContext ? '' : `<td> | |
| <div class="ncua-align-center">쿠폰</div> | |
| </td>`} | |
| <td> | |
| <div class="ncua-flex-column ncua-gap-8"> | |
| <button id="selectCoupon" type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray">쿠폰 선택</button> | |
| <div class="ncua-table ncua-table--horizontal add-link-button-table"> | |
| <table id="linkButtonTable"> | |
| <colgroup> | |
| ${isRecipeContext ? '' : '<col width="80px"/>'} | |
| <col/> | |
| <col/> | |
| <col width="88px"/> | |
| </colgroup> | |
| <thead> | |
| <tr> | |
| ${isRecipeContext ? '' : '<th><div class="ncua-align-center">타입</div></th>'} | |
| <th class="ncua-required"><div>버튼명</div></th> | |
| ${isAppLink() ? `<th class="ncua-required"><div>APP 링크</div></th>` : `<th class="ncua-required"><div data-tooltip-seq="008">WEB 링크</div></th>`} | |
| <th><div class="ncua-align-center">삭제</div></th> | |
| const uniqueKey = `link-button-name-${Date.now()}`; | |
| return ` | |
| <tr> | |
| ${isRecipeContext ? '' : '<td><div class="ncua-align-center">기본</div></td>'} | |
| <td> | |
| <div> | |
| <div class="ncua-input ncua-input--xs ncua-input-full-width button-name-input"> | |
| </div> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input name="linkButtonIOSUrl" type="text" value="" maxlength="800" placeholder="${isRecipeContext ? '' : 'https://commerce.com/page/event01'}" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input name="linkButtonAOSUrl" type="text" value="" maxlength="800" placeholder="${isRecipeContext ? '' : 'https://commerce.com/page/event01'}" /> | |
| </div> | |
| </div> | |
| </div> | |
| ${isRecipeContext ? '' : '<span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span>'} | |
| </div> | |
| </div> | |
| </td>` | |
| <input name="linkButtonWebUrl" type="text" value="" maxlength="800" placeholder="" /> | |
| </div> | |
| </div> | |
| ${isRecipeContext ? '' : '<span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span>'} | |
| </div> | |
| </div> | |
| </td>`; | |
| } | |
| function getItemRowTemplate(isFirst = true) { | |
| const uniqueKey = `${Date.now()}_${itemRowKeySeq++}`; | |
| return ` | |
| <tr data-item-key="${uniqueKey}"> | |
| <input name="itemWebLink" type="text" value="" placeholder="" /> | |
| </div> | |
| </div> | |
| ${isRecipeContext ? '' : '<span class="ncua-hint-text ncua-input__hint-text">외부 링크 입력 시 캠페인 통계가 집계되지 않습니다.</span>'} | |
| </div> | |
| </div> | |
| </td> | |
| // 캐러셀 피드 초기화 | |
| carouselState.init(); | |
| const initialTextContent = <?= json_encode($initialTextContent ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| const initialAlternativeMessage = <?= json_encode($initialAlternativeMessage ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| if (isRecipeContext) { | |
| const wideItemPointHint = kakaoFriendtalkTabSection.querySelector('.js-wide-item-point-hint'); | |
| if (wideItemPointHint) wideItemPointHint.textContent = friendtalkUnitPointHint('WIDE_ITEM_LIST'); | |
| } | |
| // 본문 메시지 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| title: '내용 입력', | |
| maxLength: 1300, | |
| useBytes: false, | |
| initialValue: initialTextContent, | |
| onInput: function(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeContainer', input); | |
| if (input === '') { | |
| window.messageInput.setHint(null); | |
| setDefaultPreview(['TEXT']); | |
| } else if (isRecipeContext) { | |
| window.messageInput.setHint(friendtalkUnitPointHint(getCampaignMessageType())); | |
| } | |
| } | |
| }); | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| initialValue: initialAlternativeMessage, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeAlternativeContainer', input); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: isRecipeContext ? '추천 변수' : '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| hideCategorySelector: isRecipeContext, | |
| categories: isRecipeContext ? buildRecipeReplaceCodeCategories('#') : buildReplaceCodeCategories('#'), | |
| onSelect: (variable) => { | |
| const resolved = MobileMessageReplaceCode.resolveRecipeExpireInsertKey(variable, isRecipeContext); | |
| if (getCampaignMessageType() === 'WIDE_ITEM_LIST') { | |
| const headerInput = kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignHeader"]'); | |
| headerInput.value = headerInput.value + resolved; | |
| headerInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } else { | |
| window.messageInput.insertText(resolved); | |
| } | |
| }, | |
| onCategoryChange: (category) => { | |
| window.alternativeChipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeAlternativeContainer', | |
| title: isRecipeContext ? '추천 변수' : '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| hideCategorySelector: isRecipeContext, | |
| categories: isRecipeContext ? buildRecipeReplaceCodeCategories() : buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| window.alternativeMessageInput.insertText(MobileMessageReplaceCode.resolveRecipeExpireInsertKey(variable, isRecipeContext)); | |
| }, | |
| onCategoryChange: (category) => { | |
| if (category === 'MEMBER') { | |
| }); | |
| setDefaultPreview(['ALTERNATIVE_TEXT', 'TEXT']); | |
| if (initialAlternativeMessage) { | |
| window.alternativePreview.setContent(resolveReplaceCode(initialAlternativeMessage, false)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeAlternativeContainer', initialAlternativeMessage); | |
| } | |
| applyFriendtalkRecipeDefaults(getCampaignMessageType()); | |
| } | |
| function applyFriendtalkRecipeDefaults(campaignMessageType) { | |
| if (!friendtalkRequest || Object.keys(friendtalkRequest).length === 0) return; | |
| if (!window.messageInput) return; | |
| const typeKeyMap = { | |
| TEXT: 'text', | |
| IMAGE: 'image', | |
| WIDE_IMAGE: 'wideImage', | |
| WIDE_ITEM_LIST: 'wideItemList', | |
| CAROUSEL_FEED: 'carousel', | |
| }; | |
| const data = friendtalkRequest[typeKeyMap[campaignMessageType]]; | |
| if (!data) return; | |
| if (data.content) { | |
| window.messageInput.setValue(''); | |
| window.messageInput.insertText(data.content); | |
| } | |
| if (data.header) { | |
| const headerInput = kakaoFriendtalkTabSection.querySelector('input[name="friendtalkCampaignHeader"]'); | |
| if (headerInput) { | |
| headerInput.value = data.header; | |
| headerInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| } | |
| if (Array.isArray(data.buttons) && data.buttons.length > 0) { | |
| applyFriendtalkRecipeButtons(data.buttons); | |
| } | |
| if (Array.isArray(data.items) && data.items.length > 0) { | |
| applyFriendtalkRecipeItems(data.items); | |
| } | |
| if (Array.isArray(data.carousels) && data.carousels.length > 0) { | |
| applyFriendtalkRecipeCarousels(data.carousels); | |
| } | |
| } | |
| // 레시피 기본 캐러셀 응답 → carouselState.carousels 형태로 변환 | |
| function mapRecipeCarousels(carousels) { | |
| if (!Array.isArray(carousels)) { | |
| carousels = []; | |
| } | |
| const mapped = carousels.map(function (carousel) { | |
| const base = carouselState.createEmptyCarousel(); | |
| const attachment = carousel.attachment || {}; | |
| const image = attachment.image || {}; | |
| const buttons = Array.isArray(attachment.buttons) && attachment.buttons.length > 0 | |
| ? attachment.buttons.map(function (btn) { | |
| // url 은 nullable 이라 객체로 보정 | |
| let url = btn?.url; | |
| if (!url || typeof url !== 'object') { | |
| url = { mobileUrl: '', iosUrl: '', aosUrl: '' }; | |
| } | |
| return { | |
| name: btn?.name ?? '', | |
| url: url | |
| }; | |
| }) | |
| : base.buttons; | |
| return { | |
| ...base, | |
| header: carousel.header ?? '', | |
| message: carousel.message ?? '', | |
| imageUrl: image.imageUrl ?? '', | |
| imageLink: image.imageLink ?? '', | |
| buttons: buttons, | |
| // 응답 쿠폰엔 표시 메타가 없어 운영자 선택으로 남김 | |
| coupon: null | |
| }; | |
| }); | |
| // 최소 캐러셀 수 보장 | |
| while (mapped.length < carouselState.MIN_CAROUSELS) { | |
| mapped.push(carouselState.createEmptyCarousel()); | |
| } | |
| return mapped; | |
| } | |
| // 슬라이드 재구성보다 먼저 carouselState 를 채워 빈 데이터로 덮어쓰는 경합 차단 | |
| function preloadRecipeCarouselsIfNeeded(campaignMessageType) { | |
| if (campaignMessageType !== 'CAROUSEL_FEED' || !window.carouselState) return; | |
| const carousels = friendtalkRequest && friendtalkRequest.carousel && friendtalkRequest.carousel.carousels; | |
| if (!Array.isArray(carousels) || carousels.length === 0) return; | |
| carouselState.carousels = mapRecipeCarousels(carousels); | |
| carouselState.activeIndex = 0; | |
| } | |
| function applyFriendtalkRecipeCarousels(carousels) { | |
| if (!window.carouselState) return; | |
| carouselState.carousels = mapRecipeCarousels(carousels); | |
| carouselState.activeIndex = 0; | |
| carouselState.renderTabs(); | |
| carouselState.renderCurrentCarouselForm(); | |
| // 미리보기는 input 이벤트로만 동기화되므로 레시피 기본값은 모든 슬라이드에 직접 push 한다 | |
| carouselState.carousels.forEach(function (carousel, index) { | |
| if (carousel.isDeleted) return; | |
| window.preview.setSlideIndex(index); | |
| // 제목·본문은 치환코드 치환 후 반영, 비면 기본 placeholder | |
| if (carousel.header) { | |
| window.preview.setContentTitle(resolveReplaceCode(carousel.header)); | |
| } else { | |
| setDefaultPreview(['FRIENDTALK_CAROUSEL_TITLE']); | |
| } | |
| if (carousel.message) { | |
| window.preview.setContent(resolveReplaceCode(carousel.message)); | |
| } else { | |
| setDefaultPreview(['TEXT']); | |
| } | |
| // 이미지가 없으면 기본 placeholder 표시 | |
| if (carousel.imageUrl) { | |
| window.preview.setImage(carousel.imageUrl); | |
| } else { | |
| setDefaultPreview(['FRIENDTALK_IMAGE']); | |
| } | |
| const previewButtons = (carousel.buttons || []) | |
| .filter(function (button) { return button && button.name; }) | |
| .map(function (button) { return { text: button.name }; }); | |
| if (previewButtons.length > 0) { | |
| window.preview.setButtons(previewButtons); | |
| } | |
| }); | |
| window.preview.setSlideIndex(carouselState.activeIndex); | |
| } | |
| function applyFriendtalkRecipeButtons(buttons) { | |
| const isMobileApp = linkPlatformType === 'MOBILE_APP'; | |
| buttons.forEach(function (btn) { | |
| appendLinkRow(); | |
| const row = kakaoFriendtalkButtonSection.querySelector('#linkButtonTableBody tr:last-child'); | |
| if (!row) return; | |
| const nameInput = row.querySelector('.ncua-link-button-name'); | |
| if (nameInput && btn?.name) { | |
| nameInput.value = btn.name; | |
| nameInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| const url = btn?.url; | |
| if (!url || typeof url !== 'object') return; | |
| const webUrlInput = row.querySelector('input[name="linkButtonWebUrl"]'); | |
| const iosUrlInput = row.querySelector('input[name="linkButtonIOSUrl"]'); | |
| const aosUrlInput = row.querySelector('input[name="linkButtonAOSUrl"]'); | |
| if (webUrlInput) webUrlInput.value = isMobileApp ? '' : (url.mobileUrl ?? ''); | |
| if (iosUrlInput) iosUrlInput.value = !isMobileApp ? '' : (url.iosUrl ?? ''); | |
| if (aosUrlInput) aosUrlInput.value = !isMobileApp ? '' : (url.aosUrl ?? ''); | |
| }); | |
| updateLinkPreview(); | |
| } | |
| // 레시피 기본 이미지를 해당 행 썸네일로 복원 | |
| function restoreItemImagePreview(itemKey, imageUrl) { | |
| if (!imageUrl) return; | |
| const instance = itemImageFileInputs.get(itemKey); | |
| if (!instance) return; | |
| instance.setFiles([{ fileName: imageUrl.split('/').pop(), fileImageUrl: imageUrl }]); | |
| instance.renderImagePreviews(); | |
| } | |
| function applyFriendtalkRecipeItems(items) { | |
| const itemListTableBody = kakaoFriendtalkItemListSection.querySelector('tbody'); | |
| // 아이템 수만큼 행 확보 (첫 행은 기존 생성, 최대 4개) | |
| items.forEach(function (_, idx) { | |
| if (idx === 0) return; | |
| if (itemListTableBody.querySelectorAll('tr').length >= 4) return; | |
| appendItemListRow(false); | |
| }); | |
| const rows = itemListTableBody.querySelectorAll('tr'); | |
| items.forEach(function (item, idx) { | |
| const row = rows[idx]; | |
| if (!row) return; | |
| const titleInput = row.querySelector('input[name="itemTitle"]'); | |
| if (titleInput && item?.title) { | |
| titleInput.value = item.title; | |
| titleInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| const imageUrlInput = row.querySelector('input[name="itemImageUrl"]'); | |
| if (imageUrlInput && item?.imageUrl) { | |
| imageUrlInput.value = item.imageUrl; | |
| restoreItemImagePreview(row.dataset.itemKey, item.imageUrl); | |
| } | |
| const webLinkInput = row.querySelector('input[name="itemWebLink"]'); | |
| if (webLinkInput && item?.url) { | |
| webLinkInput.value = item.url; | |
| webLinkInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| } | |
| }); | |
| // 타이틀 이벤트가 먼저 미리보기를 갱신하므로 이미지 반영 후 한 번 더 갱신 | |
| updateItemListPreview(); | |
| } | |
| // 친구톡 아이템 이미지 업로드 초기화 | |
| onChange: async (newFiles) => { | |
| const file = newFiles[0]; | |
| // setFiles() 복원 호출이면 건너뛰기 | |
| if (file && !file.size) return; | |
| // 하이라이트 제거 | |
| container.querySelector('.ncua-image-file-input').classList.remove('destructive'); | |
변경 전layer_myapp_push_content_setting.php
| <div class="myapp-condition-setting send-method-component" data-target="MAIN"> | |
| <p class="ncua-card__body-title--xs">조건 설정</p> | |
| <ul class="myapp-condition-setting-notice"> | |
| <li class="ncua-notice-info">앱 설치자로 발송 시 로그인 상태와 상관없이 앱을 설치한 모든 유저에게 푸시가 발송됩니다.</li> | |
| <li class="ncua-notice-info">가입 회원에게 발송 시 선택된 회원의 계정이 로그인 되어 있는 모든 앱에 푸시가 발송됩니다.<br />다만, 발송 OS, 앱 설치 여부, 앱 알림 수신 동의 여부에 따라 수신 대상에서 제외될 수 있습니다.</li> | |
| </ul> | |
| <div class="ncua-table ncua-table--vertical"> | |
| <table> | |
| </div> | |
| <div class="message-content send-method-component" data-target="MAIN"> | |
| <div class="myapp-message-title"> | |
| <div class="ncua-input ncua-input--xs ncua-input-full-width"> | |
| <div class="ncua-input__content-wrap"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input class="ncua-myapp-message-title" data-charcount-key="myapp-title" maxlength="40" name="myappTitle" type="text" value="" placeholder="맛집 BEST 3" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="myapp-title"> | |
| </div> | |
| </div> | |
| <!-- 템플릿 버튼 --> | |
| <div class="template-control-wrap send-method-component" data-target="MAIN"> | |
| <button data-click-target="loadTemplate" type='button' class="ncua-btn ncua-btn--sm ncua-btn--secondary">템플릿 불러오기</button> | |
| <button data-click-target="saveAsTemplate" type='button' class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray file-btn"> | |
| 템플릿으로 저장하기 | |
| </button> | |
| </div> | |
| <!-- 메시지 성과 추적 --> | |
| <div class="message-performance-analysis send-method-component" data-target="ALTERNATIVE"> | |
| <p class="ncua-card__body-title--xs tooltip-align" data-tooltip-seq="004">메시지 성과 추적</p> | |
| </div> | |
| </div> | |
| <!-- 마이앱 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="MAIN" open> | |
| <summary>마이앱 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| </div> | |
| </details> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="ALTERNATIVE" open> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| const myappPushTabSection = document.querySelector('.js-myapp-push-tab-section'); | |
| const chipContainer = document.querySelector('[data-role="chip-container"]'); | |
| const isMyappInstalled = <?= json_encode($isMyappInstalled) ?>; | |
| let myappImageFileInput; | |
| let isLoadingTemplate = false; | |
| }) | |
| }); | |
| myappPushTabSection.querySelector('button[data-click-target="loadTemplate"]').addEventListener('click', function() { | |
| $.post('./mobile_send/layer_mobile_send_myapp_push_template.php', null, function (data) { | |
| ncds_layer_popup({message: data, title: '템플릿 불러오기', size: 'wide-sm'}); | |
| }); | |
| }); | |
| myappPushTabSection.querySelector('button[data-click-target="saveAsTemplate"]').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| if (message === '') { | |
| NCDSAlert({ | |
| }); | |
| myappPushTabSection.querySelector('input[name="myappTitle"]').addEventListener('input', function(e) { | |
| window.preview.setTitle(e.target.value); | |
| if (this.value === '') { | |
| setDefaultPreview(['MYAPP_TITLE']); | |
| } | |
| }); | |
| myappPushTabSection.querySelector('input[name="myappUnsubscribeGuide"]').addEventListener('input', function(e) { | |
| myappPushTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| const allKeys = [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(key) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| myappPushTabSection.querySelector('button[data-send-tab-type="MAIN"]')?.dispatchEvent(new Event('click')); | |
| // 전체회원 + SMS 수신동의한 회원 발송 여부에 따른 layout 변경 | |
| const isRecipientAllType = document.querySelector('input[name="recipientType"]:checked').value === 'ALL'; | |
| const isOnlyAgreed = document.querySelector('input[name="isOnlyAgreed"]').checked; | |
| setMyappSendConditionAvailability(isRecipientAllType, isOnlyAgreed); | |
| const myappImageFileTagContainer = myappPushTabSection.querySelector('#myappImageFileTagContainer'); | |
| }); | |
| charCountManager.init(); | |
| // 본문 메시지 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| name: 'messageContent', | |
| maxLength: 300, | |
| useBytes: false, | |
| onInput: function(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| if (input === '') { | |
| myappPushTabSection.querySelector('#useTemplateCaution').hidden = true; | |
| setDefaultPreview(['MYAPP_TEXT']); | |
| } | |
| } | |
| }); | |
| // 대체 메시지 | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| cautionHTML: ` | |
| <ul class="replace-code-caution"> | |
| <li id="nonMemberReplaceCodeCaution" class="ncua-caution-text" hidden>회원 외 치환코드는 정상 적용되지 않으니 저장된 메시지 수정 시에만 이용바랍니다.</li> | |
| <li id="useTemplateCaution" class="ncua-caution-text" hidden>템플릿 사용시에 미지원 치환코드가 포함된 경우, 해당 값은 공란으로 발송되므로 미리보기를 확인해주시기 바랍니다.</li> | |
| </ul> | |
| `, | |
| categories: buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| messageInput.insertText(variable); | |
| }, | |
| onCategoryChange: (category) => { | |
| myappPushTabSection.querySelector('#nonMemberReplaceCodeCaution').hidden = category === 'MEMBER'; | |
| window.alternativeChipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeAlternativeContainer', | |
| title: '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| categories: buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| window.alternativeMessageInput.insertText(variable); | |
| }, | |
| onCategoryChange: (category) => { | |
| if (category === 'MEMBER') { | |
| const notificationType = myappPushTabSection.querySelector('input[type="radio"][name="myappNotificationType"]:checked').value ?? ''; | |
| updateAdMessage(notificationType); | |
| setDefaultPreview(['ALTERNATIVE_TEXT', 'MYAPP_TITLE', 'MYAPP_TEXT', 'MYAPP_UNSUBSCRIBE_GUIDE']); | |
| } | |
| function createHiddenFileInput(fileName, file, options) { | |
| if (container) container.innerHTML = ''; | |
| const myappImageFileTagContainer = myappPushTabSection.querySelector('#myappImageFileTagContainer'); | |
| myappImageFileInput = new ncua.ImageFileInput({ | |
| container: 'myappImageFileInput', | |
| fileInputName: 'myappImageFile', | |
| hintItems: [ | |
| } | |
| }); | |
| if (existingImageUrl) { | |
| myappImageFileInput.setFiles([{ fileName: existingImageUrl.split('/').pop(), fileImageUrl: existingImageUrl }]); | |
| myappImageFileInput.renderImagePreviews(); | |
| } | |
변경 후layer_myapp_push_content_setting.php
| <div class="myapp-condition-setting send-method-component" data-target="MAIN"> | |
| <p class="ncua-card__body-title--xs">조건 설정</p> | |
| <ul class="myapp-condition-setting-notice"> | |
| <?php if ($isRecipeContext): ?> | |
| <li class="ncua-notice-info">선택된 회원의 계정이 로그인 되어있는 모든 앱에 푸시가 발송됩니다.<br />다만, 발송 OS, 앱 설치, 알림 수신 동의 여부에 따라 수신 대상에서 제외될 수 있습니다.</li> | |
| <?php else: ?> | |
| <li class="ncua-notice-info">앱 설치자로 발송 시 로그인 상태와 상관없이 앱을 설치한 모든 유저에게 푸시가 발송됩니다.</li> | |
| <li class="ncua-notice-info">가입 회원에게 발송 시 선택된 회원의 계정이 로그인 되어 있는 모든 앱에 푸시가 발송됩니다.<br />다만, 발송 OS, 앱 설치 여부, 앱 알림 수신 동의 여부에 따라 수신 대상에서 제외될 수 있습니다.</li> | |
| <?php endif; ?> | |
| </ul> | |
| <div class="ncua-table ncua-table--vertical"> | |
| <table> | |
| </div> | |
| <div class="message-content send-method-component" data-target="MAIN"> | |
| <div class="ncua-card__body-title-wrap"> | |
| <p class="ncua-card__body-title--xs">내용 입력</p> | |
| </div> | |
| <div class="myapp-message-title"> | |
| <div class="ncua-input ncua-input--xs ncua-input-full-width"> | |
| <div class="ncua-input__content-wrap"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs"> | |
| <input class="ncua-myapp-message-title" data-charcount-key="myapp-title" maxlength="40" name="myappTitle" type="text" value="<?= gd_htmlspecialchars($initialTitle ?? '') ?>" placeholder="맛집 BEST 3" /> | |
| </div> | |
| </div> | |
| <div class="ncua-input__field-text-count" data-charcount-text="myapp-title"> | |
| </div> | |
| </div> | |
| <!-- 템플릿 버튼 --> | |
| <?php if (!$isRecipeContext): ?> | |
| <div class="template-control-wrap send-method-component" data-target="MAIN"> | |
| <button data-click-target="loadTemplate" type='button' class="ncua-btn ncua-btn--sm ncua-btn--secondary">템플릿 불러오기</button> | |
| <button data-click-target="saveAsTemplate" type='button' class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray file-btn"> | |
| 템플릿으로 저장하기 | |
| </button> | |
| </div> | |
| <?php endif; ?> | |
| <!-- 메시지 성과 추적 --> | |
| <div class="message-performance-analysis send-method-component" data-target="ALTERNATIVE"> | |
| <p class="ncua-card__body-title--xs tooltip-align" data-tooltip-seq="004">메시지 성과 추적</p> | |
| </div> | |
| </div> | |
| <!-- 마이앱 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="MAIN" <?= !$isRecipeContext ? 'open' : '' ?>> | |
| <summary>마이앱 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| </div> | |
| </details> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray send-method-component" data-target="ALTERNATIVE" <?= !$isRecipeContext ? 'open' : '' ?>> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| const myappPushTabSection = document.querySelector('.js-myapp-push-tab-section'); | |
| const chipContainer = document.querySelector('[data-role="chip-container"]'); | |
| const isMyappInstalled = <?= json_encode($isMyappInstalled) ?>; | |
| const isRecipeContext = <?= $isRecipeContext ? 'true' : 'false' ?>; | |
| let myappImageFileInput; | |
| let isLoadingTemplate = false; | |
| }) | |
| }); | |
| myappPushTabSection.querySelector('button[data-click-target="loadTemplate"]')?.addEventListener('click', function() { | |
| $.post('./mobile_send/layer_mobile_send_myapp_push_template.php', null, function (data) { | |
| ncds_layer_popup({message: data, title: '템플릿 불러오기', size: 'wide-sm'}); | |
| }); | |
| }); | |
| myappPushTabSection.querySelector('button[data-click-target="saveAsTemplate"]')?.addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| if (message === '') { | |
| NCDSAlert({ | |
| }); | |
| myappPushTabSection.querySelector('input[name="myappTitle"]').addEventListener('input', function(e) { | |
| syncMyappTitlePreview(e.target.value); | |
| }); | |
| myappPushTabSection.querySelector('input[name="myappUnsubscribeGuide"]').addEventListener('input', function(e) { | |
| myappPushTabSection.querySelector('button.load-main-contents').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| const allKeys = [ | |
| MobileMessageReplaceCode.CRM_RECIPE, | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| ].flatMap(category => Object.keys(category)); | |
| const convertedMessage = message.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(MobileMessageReplaceCode.normalizeReplaceKey(key)) ? `{${key}}` : ''; | |
| }); | |
| alternativeMessageInput.setValue(''); | |
| myappPushTabSection.querySelector('button[data-send-tab-type="MAIN"]')?.dispatchEvent(new Event('click')); | |
| // 전체회원 + SMS 수신동의한 회원 발송 여부에 따른 layout 변경 | |
| const isRecipientAllType = (document.querySelector('input[name="recipientType"]:checked')?.value ?? 'ALL') === 'ALL'; | |
| const isOnlyAgreed = document.querySelector('input[name="isOnlyAgreed"]')?.checked ?? false; | |
| setMyappSendConditionAvailability(isRecipientAllType, isOnlyAgreed); | |
| const myappImageFileTagContainer = myappPushTabSection.querySelector('#myappImageFileTagContainer'); | |
| }); | |
| charCountManager.init(); | |
| const initialMyappMessage = <?= json_encode($initialMessage ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| const initialMyappTitle = <?= json_encode($initialTitle ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| const initialAlternativeMessage = <?= json_encode($initialAlternativeMessage ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| // 본문 메시지 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| name: 'messageContent', | |
| maxLength: 300, | |
| useBytes: false, | |
| initialValue: initialMyappMessage, | |
| onInput: syncMyappMessagePreview, | |
| }); | |
| // 대체 메시지 | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| initialValue: initialAlternativeMessage, | |
| onInput: function(input) { | |
| window.alternativePreview.setContent(resolveReplaceCode(input, false)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeAlternativeContainer', input); | |
| if (input === '') { | |
| window.alternativeMessageInput.setHint(null); | |
| setDefaultPreview(['ALTERNATIVE_TEXT']); | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: isRecipeContext ? '추천 변수' : '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| hideCategorySelector: isRecipeContext, | |
| cautionHTML: ` | |
| <ul class="replace-code-caution"> | |
| <li id="nonMemberReplaceCodeCaution" class="ncua-caution-text" hidden>회원 외 치환코드는 정상 적용되지 않으니 저장된 메시지 수정 시에만 이용바랍니다.</li> | |
| <li id="useTemplateCaution" class="ncua-caution-text" hidden>템플릿 사용시에 미지원 치환코드가 포함된 경우, 해당 값은 공란으로 발송되므로 미리보기를 확인해주시기 바랍니다.</li> | |
| </ul> | |
| `, | |
| categories: isRecipeContext ? buildRecipeReplaceCodeCategories() : buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| messageInput.insertText(MobileMessageReplaceCode.resolveRecipeExpireInsertKey(variable, isRecipeContext)); | |
| }, | |
| onCategoryChange: (category) => { | |
| myappPushTabSection.querySelector('#nonMemberReplaceCodeCaution').hidden = category === 'MEMBER'; | |
| window.alternativeChipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeAlternativeContainer', | |
| title: isRecipeContext ? '추천 변수' : '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| hideCategorySelector: isRecipeContext, | |
| categories: isRecipeContext ? buildRecipeReplaceCodeCategories() : buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| window.alternativeMessageInput.insertText(MobileMessageReplaceCode.resolveRecipeExpireInsertKey(variable, isRecipeContext)); | |
| }, | |
| onCategoryChange: (category) => { | |
| if (category === 'MEMBER') { | |
| const notificationType = myappPushTabSection.querySelector('input[type="radio"][name="myappNotificationType"]:checked').value ?? ''; | |
| updateAdMessage(notificationType); | |
| setDefaultPreview(['ALTERNATIVE_TEXT', 'MYAPP_UNSUBSCRIBE_GUIDE']); | |
| if (initialAlternativeMessage) { | |
| window.alternativePreview.setContent(resolveReplaceCode(initialAlternativeMessage, false)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeAlternativeContainer', initialAlternativeMessage); | |
| } | |
| syncMyappTitlePreview(initialMyappTitle); | |
| syncMyappMessagePreview(initialMyappMessage); | |
| } | |
| function syncMyappTitlePreview(title) { | |
| window.preview.setTitle(title); | |
| if (title === '') { | |
| setDefaultPreview(['MYAPP_TITLE']); | |
| } | |
| } | |
| function syncMyappMessagePreview(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeContainer', input); | |
| if (input === '') { | |
| myappPushTabSection.querySelector('#useTemplateCaution').hidden = true; | |
| setDefaultPreview(['MYAPP_TEXT']); | |
| } | |
| } | |
| function createHiddenFileInput(fileName, file, options) { | |
| if (container) container.innerHTML = ''; | |
| const myappImageFileTagContainer = myappPushTabSection.querySelector('#myappImageFileTagContainer'); | |
| // 레시피 모달에서는 이미지 미리보기 슬롯(+) 없이 파일 찾기 버튼만 노출 (board/article_write와 동일한 FileInput 사용) | |
| const FileInputComponent = isRecipeContext ? ncua.FileInput : ncua.ImageFileInput; | |
| myappImageFileInput = new FileInputComponent({ | |
| container: 'myappImageFileInput', | |
| fileInputName: 'myappImageFile', | |
| hintItems: [ | |
| } | |
| }); | |
| // FileInput(레시피)에는 프리뷰 API가 없으므로 기존 이미지 썸네일 표시는 ImageFileInput에서만 | |
| if (existingImageUrl && !isRecipeContext) { | |
| myappImageFileInput.setFiles([{ fileName: existingImageUrl.split('/').pop(), fileImageUrl: existingImageUrl }]); | |
| myappImageFileInput.renderImagePreviews(); | |
| } | |
변경 전layer_preview_message.php
| } | |
| function updatePreviewActiveTab(tabType) { | |
| container.querySelectorAll('button[data-preview-message-type]').forEach(btn => { | |
| btn.classList.toggle('is-active', btn.dataset.previewMessageType === tabType); | |
| }); | |
| } | |
변경 후layer_preview_message.php
| } | |
| function updatePreviewActiveTab(tabType) { | |
| previewMessageSection.querySelectorAll('button[data-preview-message-type]').forEach(btn => { | |
| btn.classList.toggle('is-active', btn.dataset.previewMessageType === tabType); | |
| }); | |
| } | |
변경 전layer_recipe_send_time_setting.php
변경 후layer_recipe_send_time_setting.php
| <section class="js-send-time-setting-section js-recipe-send-time-setting"> | |
| <header class="ncua-card__header"> | |
| <h4 class="ncua-card__title"> | |
| 발송 설정 | |
| </h4> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <div class="send-time-setting-content"> | |
| <div class="send-setting-repeat js-send-time-component" data-target="REPEAT"> | |
| <div class="send-time-setting-content-container"> | |
| <p class="ncua-card__body-title--sm">반복 발송 기간</p> | |
| <div class="send-setting-repeat-date"> | |
| <div id="datepicker-repeat-send-container"></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 name="hasNoEndDate" type="checkbox" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">종료일 없음</span> | |
| </label> | |
| </div> | |
| <ul class="send-setting-repeat-notice"> | |
| <li class="ncua-notice-info">반복 발송은 현재 시간으로부터 10분 이후로만 설정이 가능합니다.</li> | |
| </ul> | |
| </div> | |
| <div class="send-time-setting-content-container"> | |
| <p class="ncua-card__body-title--sm tooltip-align" data-tooltip-seq="005">발송 주기</p> | |
| <div class="send-setting-repeat-cycle"> | |
| <div class="send-setting-repeat-cycle-select"> | |
| <span class="ncua-select ncua-select--xs"> | |
| <span class="ncua-select__content"> | |
| <select id="sendCycle" class="ncua-select__tag"> | |
| <?php foreach ($messageRepeatCycles as $messageRepeatCycle): ?> | |
| <option value="<?= $messageRepeatCycle->name ?>" <?= $messageRepeatCycle->name === $sendTimeView['selectedRepeatType'] ? 'selected' : '' ?>><?= $messageRepeatCycle->value ?></option> | |
| <?php endforeach; ?> | |
| </select> | |
| </span> | |
| </span> | |
| <span class="ncua-select ncua-select--xs"> | |
| <span class="ncua-select__content"> | |
| <select name="repeatHour" class="ncua-select__tag"> | |
| <?php for ($i = 8; $i <= 20; $i++): ?> | |
| <option value="<?= sprintf('%02d', $i) ?>" <?= sprintf('%02d', $i) === $sendTimeView['selectedRepeatHour'] ? 'selected' : '' ?>> | |
| <?= sprintf('%02d', $i) ?> | |
| </option> | |
| <?php endfor; ?> | |
| </select> | |
| </span> | |
| </span> | |
| <span>시</span> | |
| <span class="ncua-select ncua-select--xs"> | |
| <span class="ncua-select__content"> | |
| <select name="repeatMinutes" class="ncua-select__tag"> | |
| <?php for ($i = 0; $i <= 50; $i += 10): ?> | |
| <option value="<?= sprintf('%02d', $i) ?>" <?= sprintf('%02d', $i) === $sendTimeView['selectedRepeatMinutes'] ? 'selected' : '' ?>> | |
| <?= sprintf('%02d', $i) ?> | |
| </option> | |
| <?php endfor; ?> | |
| </select> | |
| </span> | |
| </span> | |
| <span>분에 반복 발송</span> | |
| </div> | |
| <!-- 매주 발송 설정 --> | |
| <div class="send-cycle-weekly js-repeat-cycle-component js-weekly-day <?= $sendTimeView['selectedRepeatType'] === 'WEEKLY' ? '' : 'display-none' ?>" data-target="WEEKLY"> | |
| <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" value="all" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text">전체</span> | |
| </label> | |
| <?php foreach ($weekDays as $weekDay): ?> | |
| <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" value="<?= $weekDay->value ?>" name="repeatDays[]" <?= in_array($weekDay->value, $sendTimeView['selectedRepeatDays'], true) ? 'checked' : '' ?> /> | |
| </span> | |
| <span class="ncua-checkbox-field__text"><?= $weekDay->label() ?></span> | |
| </label> | |
| <?php endforeach; ?> | |
| </div> | |
| <!-- 매월 발송 설정 --> | |
| <div class="send-cycle-monthly js-repeat-cycle-component <?= $sendTimeView['selectedRepeatType'] === 'MONTHLY' ? '' : 'display-none' ?>" data-target="MONTHLY"> | |
| <div class="ncua-input ncua-input--xs ncua-input-width-320"> | |
| <div class="ncua-input__content"> | |
| <div class="ncua-input__field ncua-input__field--xs "> | |
| <input name="monthly-repeat-date" type="text" inputmode="numeric" pattern="[0-9]*" value="" placeholder="일자(1~31) 숫자 입력" /> | |
| </div> | |
| </div> | |
| </div> | |
| <span class="send-cycle-monthly-notice">5개까지 생성 가능합니다 (Enter로 구분)</span> | |
| </div> | |
| </div> | |
| <div class="send-tag-wrap <?= $sendTimeView['selectedRepeatType'] === 'MONTHLY' ? '' : 'display-none' ?>" data-component="send-cycle-monthly"> | |
| <div id="repeatDatesContainer" class="send-tag-content" data-role="tag-container"> | |
| <?php if ($sendTimeView['selectedRepeatType'] === 'MONTHLY'): ?> | |
| <?php foreach ($sendTimeView['selectedRepeatDays'] as $day): ?> | |
| <span class="ncua-tag ncua-tag--sm" data-day="<?= (int) $day ?>"> | |
| <span class="ncua-tag__text"><?= (int) $day ?>일</span> | |
| <button type="button" class="ncua-tag__close" data-role="tag-close"></button> | |
| </span> | |
| <?php endforeach; ?> | |
| <?php endif; ?> | |
| </div> | |
| <button type="button" data-click-target="deleteMonthlyDate" class="ncua-btn ncua-btn--xxs ncua-btn--text-gray has-underline">전체삭제</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </section> | |
| <script type="text/javascript"> | |
| const sendTimeSettingSection = document.querySelector('.js-send-time-setting-section'); | |
| $(document).ready(function () { | |
| initializeSendTimeSettingSectionEvents(); | |
| initializeSendTimeSettingSectionElement(); | |
| }); | |
| function initializeSendTimeSettingSectionEvents() { | |
| const repeatHourSelect = document.querySelector('select[name="repeatHour"]'); | |
| const repeatMinutesSelect = document.querySelector('select[name="repeatMinutes"]'); | |
| const monthlyInput = sendTimeSettingSection.querySelector('input[name="monthly-repeat-date"]'); | |
| const tagContainer = sendTimeSettingSection.querySelector('#repeatDatesContainer'); | |
| repeatHourSelect.addEventListener('change', function() { | |
| updateMinutesOptions(repeatHourSelect, repeatMinutesSelect) | |
| }); | |
| sendTimeSettingSection.querySelector('#sendCycle').addEventListener('change', function() { | |
| toggleComponentBySendCycle(this.value); | |
| sendTimeSettingSection.querySelector('#repeatDatesContainer').innerHTML = ''; | |
| switch (this.value) { | |
| case 'WEEKLY': | |
| sendTimeSettingSection.querySelectorAll('.js-weekly-day input[type="checkbox"]').forEach(el => el.checked = false); | |
| break; | |
| case 'MONTHLY': | |
| monthlyInput.value = ''; | |
| break; | |
| } | |
| }); | |
| monthlyInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| const date = parseInt(this.value); | |
| if (!date || date < 1 || date > 31) { | |
| NCDSAlert({message: '1~31 사이의 숫자를 입력해주세요.', iconType: 'error'}); | |
| return; | |
| } | |
| const currentTags = tagContainer.querySelectorAll('.ncua-tag'); | |
| if (currentTags.length >= 5) { | |
| NCDSAlert({message: `최대 5개까지만 생성 가능합니다.`, iconType: 'error'}); | |
| return; | |
| } | |
| const existingTag = tagContainer.querySelector(`[data-day="${date}"]`); | |
| if (existingTag) { | |
| NCDSAlert({message: '이미 등록된 날짜입니다.', iconType: 'error'}); | |
| return; | |
| } | |
| const tag = generateMonthlytag(date); | |
| tagContainer.appendChild(tag); | |
| this.value = ''; | |
| } | |
| }); | |
| sendTimeSettingSection.querySelector('button[data-click-target="deleteMonthlyDate"]').addEventListener('click', function() { | |
| const tags = tagContainer.querySelectorAll('.ncua-tag'); | |
| tags.forEach(tag => tag.remove()); | |
| }); | |
| sendTimeSettingSection.querySelector('input[name="hasNoEndDate"]').addEventListener('change', function() { | |
| const endDateInput = document.querySelectorAll('input[name="repeatDate[]"]')[1]; | |
| if (this.checked) { | |
| if (endDateInput) { | |
| endDateInput.disabled = true; | |
| endDateInput.value = ''; | |
| } | |
| } else { | |
| if (endDateInput) { | |
| endDateInput.disabled = false; | |
| } | |
| } | |
| }); | |
| tagContainer.querySelectorAll('[data-role="tag-close"]').forEach(closeBtn => { | |
| closeBtn.addEventListener('click', function() { | |
| closeBtn.closest('.ncua-tag').remove(); | |
| }); | |
| }); | |
| } | |
| function generateMonthlytag(date) { | |
| const tag = document.createElement('span'); | |
| tag.className = 'ncua-tag ncua-tag--sm'; | |
| tag.setAttribute('data-day', date); | |
| tag.innerHTML = ` | |
| <span class="ncua-tag__text">${date}일</span> | |
| <button type="button" class="ncua-tag__close" data-role="tag-close"></button> | |
| `; | |
| const closeBtn = tag.querySelector('[data-role="tag-close"]'); | |
| closeBtn.addEventListener('click', function() { | |
| tag.remove(); | |
| }); | |
| return tag; | |
| } | |
| function toggleComponentBySendCycle(sendCycle) { | |
| sendTimeSettingSection.querySelectorAll('.js-repeat-cycle-component').forEach(el => { | |
| el.classList.add('display-none'); | |
| const targets = el.dataset.target?.split(',') ?? []; | |
| if (targets.includes(sendCycle)) { | |
| el.classList.remove('display-none'); | |
| } | |
| }); | |
| sendTimeSettingSection.querySelector('.send-tag-wrap') | |
| .classList.toggle('display-none', sendCycle !== 'MONTHLY'); | |
| } | |
| function updateMinutesOptions(hourSelect, minutesSelect) { | |
| const isHour20 = hourSelect.value === '20'; | |
| minutesSelect.querySelectorAll('option').forEach(option => { | |
| const value = parseInt(option.value, 10); | |
| option.hidden = isHour20 && value >= 31; | |
| }); | |
| if (isHour20 && parseInt(minutesSelect.value, 10) >= 31) { | |
| minutesSelect.value = '00'; | |
| } | |
| } | |
| function initializeSendTimeSettingSectionElement() { | |
| document.querySelector('input[name="sendMethod"]:checked')?.dispatchEvent(new Event('change')); | |
| initRepeatSendDatePicker(false); | |
| ['.js-weekly-day'].forEach(selector => new CheckboxGroup(selector)); | |
| } | |
| function initRepeatSendDatePicker(isRestricted) { | |
| const startDateOptions = { | |
| defaultDate: moment().add(1, 'days').format(), | |
| minDate: moment().format(), | |
| maxDate: isRestricted ? moment().add(364, 'days').format() : null, | |
| }; | |
| const endDateOptions = { | |
| defaultDate: moment().add(3, 'months').format(), | |
| minDate: moment().format(), | |
| maxDate: isRestricted ? moment().add(364, 'days').format() : null, | |
| }; | |
| const repeatSendDatePicker = createCRMDatePicker({ | |
| containerId: 'datepicker-repeat-send-container', | |
| attrName: 'repeatDate[]', | |
| buttons: [], | |
| startDateOptions, | |
| endDateOptions, | |
| }); | |
| attachDateValidation(repeatSendDatePicker, {maxMonthRange: null, containerId: 'datepicker-repeat-send-container'}); | |
| const noEndDateInput = sendTimeSettingSection.querySelector('input[name="hasNoEndDate"]'); | |
| if (isRestricted) noEndDateInput.checked = false; | |
| noEndDateInput.disabled = isRestricted; | |
| } | |
| </script> | |
| <!-- 툴팁 스크립트 --> | |
| <script defer type="text/javascript"> | |
| const code = 251219003; | |
| if (window.GodoCosGuide) { | |
| window.GodoCosGuide.init({ | |
| apiUrl: <?= json_encode($cosGuideApiUrl) ?>, | |
| guideCode: code, | |
| }).then((data) => { | |
| window.cosData = data; | |
| }); | |
| } | |
| </script> | |
변경 전layer_recipient_setting.php
| }); | |
| }); | |
| recipientSettingSection.querySelector('button[data-click-target="createCrmGroup"]').addEventListener('click', () => { | |
| $.ajax({ | |
| url: './mobile_send/layer_select_crm_groups_ps.php', | |
| type: 'GET', | |
| data: {mode: 'getCrmGroups'}, | |
| success: function (response) { | |
| if (response.data.totalCount >= 20) { | |
| NCDSAlert({ message: '등록 가능한 개수를 초과하여 그룹 생성이 불가합니다.', iconType: 'error' }); | |
| return; | |
| } | |
| parent.postMessage({ type: 'MODAL_OPEN', payload: { target: 'CRM_GROUP_CREATE_POPUP', width: 1000, height: 570 } }, '*'); | |
| }, | |
| error: function () { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }); | |
| }); | |
| recipientSettingSection.querySelector('button[data-click-target="addDirectPhoneBtn"]').addEventListener('click', function() { | |
| recipientSettingSection.querySelector('button.refresh-crm-group').addEventListener('click', async function(e) { | |
| try { | |
| const crmGroupNo = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]').value; | |
| const response = await getCrmGroupInfo(crmGroupNo); | |
| setSelectedMemberCount(response.data.count, response.data.smsOptOutCount); | |
| updateCrmGroupData(response.data.memberCount, response.data.status); | |
| if (response.data.status === 'DONE') { | |
| NCDSToast({message: '데이터 추출이 완료되었습니다.', color: 'success'}); | |
| } else { | |
| NCDSToast({message: '데이터 추출이 진행 중입니다. 잠시만 기다려주세요.', color: 'error'}); | |
| } | |
| } catch (e) { | |
| NCDSToast({message: '일시적인 오류가 발생하였습니다. 다시 시도해 주세요.', color: 'error'}); | |
| } | |
| }); | |
| } | |
| function getCrmGroupInfo(crmGroupNo) { | |
| return $.ajax({ | |
| url: './mobile_send/layer_recipient_setting_ps.php', | |
| type: 'GET', | |
| data: {mode: 'getCrmGroupStatus', crmGroupNo: crmGroupNo}, | |
| }); | |
| } | |
| function clearRecipients() { | |
| recipientSettingSection.querySelector('input[name="selectedMembers"]').value = ""; | |
| recipientSettingSection.querySelector('input[name="selectedMemberGroups"]').value = ""; | |
| window.getSelectedCrmGroup = function () { | |
| const input = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]'); | |
| return {crmNo: input.value, crmGroupName: input.dataset.crmGroupName, memberCount: input.dataset.memberCount}; | |
| } | |
| window.setSelectedCrmGroup = async function (crmNo, groupName) { | |
| selectedCrmGroupInput.dataset.crmGroupName = groupName; | |
| try { | |
| const response = await getCrmGroupInfo(crmNo); | |
| setSelectedMemberCount(response.data.count, response.data.smsOptOutCount); | |
| updateCrmGroupData(response.data.memberCount, response.data.status); | |
| } catch (e) { | |
| NCDSAlert({message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error'}); | |
| } | |
| } | |
| function updateCrmGroupData(memberCount, status) { | |
| recipientSettingSection.querySelector('input[name="selectedCrmGroup"]').dataset.memberCount = memberCount; | |
| recipientSettingSection.querySelector('.js-crm-group-extraction').classList.toggle('display-none', status === 'DONE'); | |
| recipientSettingSection.querySelector('.js-recipient-count-text').classList.toggle('display-none', status !== 'DONE'); | |
| } | |
| window.getExcelUploadData = function () { | |
| const selectedCrmGroupInput = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]'); | |
| selectedCrmGroupInput.value = ''; | |
| selectedCrmGroupInput.dataset.crmGroupName = ''; | |
| selectedCrmGroupInput.dataset.memberCount = ''; | |
| setSelectedMemberCount(0, 0); | |
변경 후layer_recipient_setting.php
| }); | |
| }); | |
| recipientSettingSection.querySelector('button[data-click-target="createCrmGroup"]').addEventListener('click', async () => { | |
| try { | |
| if (await CrmGroupStatus.getCount() >= CrmGroupStatus.MAX_COUNT) { | |
| NCDSAlert({ message: 'CRM 그룹은 최대 ' + CrmGroupStatus.MAX_COUNT + '개까지 등록할 수 있습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| parent.postMessage({ type: 'MODAL_OPEN', payload: { target: 'CRM_GROUP_CREATE_POPUP', width: 1000, height: 570 } }, '*'); | |
| } catch (e) { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }); | |
| recipientSettingSection.querySelector('button[data-click-target="addDirectPhoneBtn"]').addEventListener('click', function() { | |
| recipientSettingSection.querySelector('button.refresh-crm-group').addEventListener('click', async function(e) { | |
| try { | |
| const crmGroupNo = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]').value; | |
| const response = await CrmGroupStatus.getInfo(crmGroupNo); | |
| setSelectedMemberCount(response.data.count, response.data.smsOptOutCount); | |
| updateCrmGroupData(response.data.status); | |
| NCDSToast(CrmGroupStatus.toast(response.data.status)); | |
| } catch (e) { | |
| NCDSToast(CrmGroupStatus.errorToast()); | |
| } | |
| }); | |
| } | |
| function clearRecipients() { | |
| recipientSettingSection.querySelector('input[name="selectedMembers"]').value = ""; | |
| recipientSettingSection.querySelector('input[name="selectedMemberGroups"]').value = ""; | |
| window.getSelectedCrmGroup = function () { | |
| const input = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]'); | |
| return {crmNo: input.value, crmGroupName: input.dataset.crmGroupName}; | |
| } | |
| window.setSelectedCrmGroup = async function (crmNo, groupName) { | |
| selectedCrmGroupInput.dataset.crmGroupName = groupName; | |
| try { | |
| const response = await CrmGroupStatus.getInfo(crmNo); | |
| setSelectedMemberCount(response.data.count, response.data.smsOptOutCount); | |
| updateCrmGroupData(response.data.status); | |
| } catch (e) { | |
| NCDSAlert({message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error'}); | |
| } | |
| } | |
| function updateCrmGroupData(status) { | |
| CrmGroupStatus.toggleView(recipientSettingSection, status); | |
| } | |
| window.getExcelUploadData = function () { | |
| const selectedCrmGroupInput = recipientSettingSection.querySelector('input[name="selectedCrmGroup"]'); | |
| selectedCrmGroupInput.value = ''; | |
| selectedCrmGroupInput.dataset.crmGroupName = ''; | |
| setSelectedMemberCount(0, 0); | |
변경 전layer_select_crm_groups.php
| /** | |
| * CRM 그룹 생성 버튼 | |
| */ | |
| document.querySelector('#createCrmGroupPopup')?.addEventListener('click', function () { | |
| $.ajax({ | |
| url: '/crm/mobile_send/layer_select_crm_groups_ps.php', | |
| type: 'GET', | |
| data: {mode: 'getCrmGroups'}, | |
| success: function (response) { | |
| if (response.data.totalCount >= 20) { | |
| NCDSAlert({ message: '등록 가능한 개수를 초과하여 그룹 생성이 불가합니다.', iconType: 'error' }); | |
| return; | |
| } | |
| parent.postMessage({ type: 'MODAL_OPEN', payload: { target: 'CRM_GROUP_CREATE_POPUP', width: 1000, height: 570 } }, '*'); | |
| }, | |
| error: function () { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }); | |
| }); | |
| /** | |
변경 후layer_select_crm_groups.php
| /** | |
| * CRM 그룹 생성 버튼 | |
| */ | |
| document.querySelector('#createCrmGroupPopup')?.addEventListener('click', async function () { | |
| try { | |
| if (await CrmGroupStatus.getCount() >= CrmGroupStatus.MAX_COUNT) { | |
| NCDSAlert({ message: 'CRM 그룹은 최대 ' + CrmGroupStatus.MAX_COUNT + '개까지 등록할 수 있습니다.', iconType: 'error' }); | |
| return; | |
| } | |
| parent.postMessage({ type: 'MODAL_OPEN', payload: { target: 'CRM_GROUP_CREATE_POPUP', width: 1000, height: 570 } }, '*'); | |
| } catch (e) { | |
| NCDSAlert({ message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.', iconType: 'error' }); | |
| } | |
| }); | |
| /** | |
변경 전layer_send_method_setting.php
| <section class="js-send-method-setting-section"> | |
| <header class="ncua-card__header"> | |
| <h4 class="ncua-card__title"> | |
| 발송수단 설정 | |
| </h4> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <!-- 발송수단 라디오 버튼 --> | |
| MYAPP: './mobile_send/layer_myapp_push_content_setting.php', | |
| }; | |
| $.post(urlMap[sendMethod], function (data) { | |
| $('#layerSendMethodContent').html(data); | |
| if (cosData && window.GodoCosGuide) { | |
| window.GodoCosGuide.apply(cosData); | |
| if (myappTitle && myappTitle.value !== '') return true; | |
| } | |
| function buildReplaceCodeCategories(prefix = '') { | |
| return [ | |
| { value: 'MEMBER', label: '회원' }, | |
| { value: 'GOODS', label: '상품' }, | |
| { value: 'ORDER', label: '주문' }, | |
| { value: 'PROMOTION', label: '프로모션' }, | |
| { value: 'BOARD', label: '게시판' }, | |
| { value: 'REGULAR', label: '정기결제(배송)' }, | |
| { value: 'PRESENT', label: '선물하기' }, | |
| ].map(category => ({ | |
| ...category, | |
| variables: MobileMessageReplaceCode.getItemsByMethod( | |
| MobileMessageReplaceCode[category.value], | |
| MobileMessageReplaceCode.SendMethod.MANUAL | |
| ).map(item => ({ | |
| key: `${prefix}{${item.key}}`, | |
| label: `${item.name}` | |
| })) | |
| })); | |
| } | |
| function resolveReplaceCode(content, isMain = true) { | |
| if (content === null) return content; | |
| const previewObject = isMain ? window.preview : window.alternativePreview; | |
| if (previewObject.isCheckboxChecked()) return content; | |
| const forceDefaultKeys = ['shopUrl']; | |
| const replaceMap = {}; | |
| [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| MobileMessageReplaceCode.PROMOTION, | |
| MobileMessageReplaceCode.BOARD, | |
| MobileMessageReplaceCode.REGULAR, | |
| MobileMessageReplaceCode.PRESENT, | |
| ].forEach(category => { | |
| Object.entries(category).forEach(([key, obj]) => { | |
| if (replaceMap[key] !== undefined) return; | |
| replaceMap[key] = (obj.sendMethod !== MobileMessageReplaceCode.SendMethod.AUTO || forceDefaultKeys.includes(key)) ? obj.default : ''; | |
| }); | |
| }); | |
| const shortLinks = isMain ? window.shortLinkKeys : window.shortLinkAlternativeKeys; | |
| shortLinks.forEach((_, key) => { | |
| replaceMap[key] = `쇼핑몰도메인/_s/a1B2c3D`; | |
| }); | |
| return content.replace(/[#$]?\{([a-zA-Z0-9_]+)}/g, (match, key) => { | |
| return replaceMap[key] !== undefined ? replaceMap[key] : match; | |
| }); | |
| } | |
| </script> | |
변경 후layer_send_method_setting.php
| <section class="js-send-method-setting-section"> | |
| <header class="ncua-card__header"> | |
| <h4 class="ncua-card__title"> | |
| <?= $isRecipeContext ? '메시지 발송' : '발송수단 설정' ?> | |
| </h4> | |
| <?php if ($isRecipeContext && $messageSendGuide !== ''): ?> | |
| <p class="ncua-notice-info js-message-send-guide"><?= nl2br(gd_htmlspecialchars($messageSendGuide)) ?></p> | |
| <?php endif; ?> | |
| </header> | |
| <section class="ncua-card__body"> | |
| <!-- 발송수단 라디오 버튼 --> | |
| MYAPP: './mobile_send/layer_myapp_push_content_setting.php', | |
| }; | |
| const postData = (typeof recipeDefault !== 'undefined' && recipeDefault) | |
| ? { recipeContext: JSON.stringify(recipeDefault) } | |
| : null; | |
| $.post(urlMap[sendMethod], postData, function (data) { | |
| $('#layerSendMethodContent').html(data); | |
| if (cosData && window.GodoCosGuide) { | |
| window.GodoCosGuide.apply(cosData); | |
| if (myappTitle && myappTitle.value !== '') return true; | |
| } | |
| // 치환코드 카테고리 목록을 칩 드로워용 구조로 변환한다. (일반/레시피 공용) | |
| // insertKey 는 _Nd 평탄화(defaultDay)용 — 일반 코드는 defaultDay 가 없어 key 와 동일해지므로 항상 포함해도 안전. | |
| function buildReplaceCodeCategoryList(categories, prefix = '') { | |
| return categories.map(category => ({ | |
| ...category, | |
| variables: MobileMessageReplaceCode.getItemsByMethod( | |
| MobileMessageReplaceCode[category.value], | |
| MobileMessageReplaceCode.SendMethod.MANUAL | |
| ).map(item => ({ | |
| key: `${prefix}{${item.key}}`, | |
| insertKey: `${prefix}{${item.defaultDay ? item.key.replace(/_Nd$/, `_${item.defaultDay}d`) : item.key}}`, | |
| label: `${item.name}` | |
| })) | |
| })); | |
| } | |
| function buildReplaceCodeCategories(prefix = '') { | |
| return buildReplaceCodeCategoryList([ | |
| { value: 'MEMBER', label: '회원' }, | |
| { value: 'GOODS', label: '상품' }, | |
| { value: 'ORDER', label: '주문' }, | |
| { value: 'PROMOTION', label: '프로모션' }, | |
| { value: 'BOARD', label: '게시판' }, | |
| { value: 'REGULAR', label: '정기결제(배송)' }, | |
| { value: 'PRESENT', label: '선물하기' }, | |
| ], prefix); | |
| } | |
| function buildRecipeReplaceCodeCategories(prefix = '') { | |
| return buildReplaceCodeCategoryList([ | |
| { value: 'CRM_RECIPE', label: '레시피' }, | |
| ], prefix); | |
| } | |
| function resolveReplaceCode(content, isMain = true) { | |
| if (content === null) return content; | |
| const previewObject = isMain ? window.preview : window.alternativePreview; | |
| if (previewObject.isCheckboxChecked()) return content; | |
| return MobileMessageReplaceCode.resolvePreviewContent(content, { | |
| includeRecipe: true, | |
| shortLinkKeys: isMain ? window.shortLinkKeys : window.shortLinkAlternativeKeys, | |
| }); | |
| } | |
| </script> | |
변경 전layer_send_time_setting.php
| </span> | |
| <span class="ncua-checkbox-field__text">전체</span> | |
| </label> | |
| <?php | |
| $weekDays = [ | |
| ['value' => 1, 'label' => '월'], | |
| ['value' => 2, 'label' => '화'], | |
| ['value' => 3, 'label' => '수'], | |
| ['value' => 4, 'label' => '목'], | |
| ['value' => 5, 'label' => '금'], | |
| ['value' => 6, 'label' => '토'], | |
| ['value' => 7, 'label' => '일'], | |
| ]; | |
| ?> | |
| <?php foreach ($weekDays as $weekDay): ?> | |
| <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" value="<?= $weekDay['value'] ?>" name="repeatDays[]" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text"><?= $weekDay['label'] ?></span> | |
| </label> | |
| <?php endforeach; ?> | |
| </div> | |
변경 후layer_send_time_setting.php
| </span> | |
| <span class="ncua-checkbox-field__text">전체</span> | |
| </label> | |
| <?php foreach ($weekDays as $weekDay): ?> | |
| <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" value="<?= $weekDay->value ?>" name="repeatDays[]" /> | |
| </span> | |
| <span class="ncua-checkbox-field__text"><?= $weekDay->label() ?></span> | |
| </label> | |
| <?php endforeach; ?> | |
| </div> | |
변경 전layer_sms_content_setting.php
| </div> | |
| <!-- 템플릿 버튼 --> | |
| <div class="template-control-wrap"> | |
| <button type="button" data-click-target="loadTemplate" class="ncua-btn ncua-btn--sm ncua-btn--secondary">템플릿 불러오기</button> | |
| <button type="button" data-click-target="saveAsTemplate" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray file-btn"> | |
| 템플릿으로 저장하기 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray" open> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| addShortLinkBtn.disabled = isImpossibleAddShortLink; | |
| }); | |
| smsSettingSection.querySelector('button[data-click-target="loadTemplate"]').addEventListener('click', function() { | |
| $.post('./mobile_send/layer_mobile_send_sms_template.php', null, function (data) { | |
| ncds_layer_popup({message: data, title: '템플릿 불러오기', size: 'wide-sm'}); | |
| }); | |
| }); | |
| smsSettingSection.querySelector('button[data-click-target="saveAsTemplate"]').addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| if (message === '') { | |
| NCDSAlert({ | |
| } | |
| function initializeSmsContentSettingSectionElement() { | |
| // MessageInput 생성 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| onInput: function(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| if (input === '') { | |
| window.messageInput.setHint(null); | |
| smsSettingSection.querySelector('#useTemplateCaution').hidden = true; | |
| setDefaultPreview(['TEXT']); | |
| } else { | |
| window.messageInput.setHint(window.messageInput.calculateLength(input) > 90 ? 'LMS 건당 3포인트 차감' : 'SMS 건당 1포인트 차감'); | |
| } | |
| } | |
| }); | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| cautionHTML: ` | |
| <ul class="replace-code-caution"> | |
| <li id="nonMemberReplaceCodeCaution" class="ncua-caution-text" hidden>회원 외 치환코드는 정상 적용되지 않으니 저장된 메시지 수정 시에만 이용바랍니다.</li> | |
| <li id="useTemplateCaution" class="ncua-caution-text" hidden>템플릿 사용시에 미지원 치환코드가 포함된 경우, 해당 값은 공란으로 발송되므로 미리보기를 확인해주시기 바랍니다.</li> | |
| </ul> | |
| `, | |
| categories: buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| messageInput.insertText(variable); | |
| }, | |
| onCategoryChange: (category) => { | |
| smsSettingSection.querySelector('#nonMemberReplaceCodeCaution').hidden = category === 'MEMBER'; | |
| window.preview.setContent(resolveReplaceCode(window.messageInput.getValue())); | |
| }); | |
| setDefaultPreview(['TEXT']); | |
| const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value; | |
| if (['EXCEL_UPLOAD', 'DIRECT'].includes(recipientType)) { | |
| } | |
| } | |
| function setTemplate(message) { | |
| smsSettingSection.querySelector('input[name="use080Reject"]').checked = false; | |
변경 후layer_sms_content_setting.php
| </div> | |
| <!-- 템플릿 버튼 --> | |
| <?php if (!$isRecipeContext): ?> | |
| <div class="template-control-wrap"> | |
| <button type="button" data-click-target="loadTemplate" class="ncua-btn ncua-btn--sm ncua-btn--secondary">템플릿 불러오기</button> | |
| <button type="button" data-click-target="saveAsTemplate" class="ncua-btn ncua-btn--sm ncua-btn--secondary-gray file-btn"> | |
| 템플릿으로 저장하기 | |
| </button> | |
| </div> | |
| <?php endif; ?> | |
| </div> | |
| <!-- SMS/LMS 대체 발송 안내 --> | |
| <details class="ncua-accordion ncua-accordion--gray" <?= !$isRecipeContext ? 'open' : '' ?>> | |
| <summary>SMS/LMS 발송 안내</summary> | |
| <div class="ncua-accordion__content has-dot"> | |
| <ul> | |
| addShortLinkBtn.disabled = isImpossibleAddShortLink; | |
| }); | |
| smsSettingSection.querySelector('button[data-click-target="loadTemplate"]')?.addEventListener('click', function() { | |
| $.post('./mobile_send/layer_mobile_send_sms_template.php', null, function (data) { | |
| ncds_layer_popup({message: data, title: '템플릿 불러오기', size: 'wide-sm'}); | |
| }); | |
| }); | |
| smsSettingSection.querySelector('button[data-click-target="saveAsTemplate"]')?.addEventListener('click', function() { | |
| const message = messageInput.getValue(); | |
| if (message === '') { | |
| NCDSAlert({ | |
| } | |
| function initializeSmsContentSettingSectionElement() { | |
| const initialMessage = <?= json_encode($initialMessage ?? '', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) ?>; | |
| // MessageInput 생성 | |
| window.messageInput = GodoUIModule.render({ | |
| type: 'MessageInput', | |
| maxLength: 2000, | |
| useBytes: true, | |
| allowEmoji: false, | |
| initialValue: initialMessage, | |
| onInput: syncSmsPreviewAndHint, | |
| }); | |
| const isRecipeContext = <?= $isRecipeContext ? 'true' : 'false' ?>; | |
| window.chipSelector = GodoUIModule.render({ | |
| type: 'ChipSelector', | |
| target: '#replaceCodeContainer', | |
| title: isRecipeContext ? '추천 변수' : '사용 가능한 변수', | |
| tooltipSeq: '003', | |
| hideCategorySelector: isRecipeContext, | |
| cautionHTML: ` | |
| <ul class="replace-code-caution"> | |
| <li id="nonMemberReplaceCodeCaution" class="ncua-caution-text" hidden>회원 외 치환코드는 정상 적용되지 않으니 저장된 메시지 수정 시에만 이용바랍니다.</li> | |
| <li id="useTemplateCaution" class="ncua-caution-text" hidden>템플릿 사용시에 미지원 치환코드가 포함된 경우, 해당 값은 공란으로 발송되므로 미리보기를 확인해주시기 바랍니다.</li> | |
| </ul> | |
| `, | |
| categories: isRecipeContext ? buildRecipeReplaceCodeCategories() : buildReplaceCodeCategories(), | |
| onSelect: (variable) => { | |
| messageInput.insertText(MobileMessageReplaceCode.resolveRecipeExpireInsertKey(variable, isRecipeContext)); | |
| }, | |
| onCategoryChange: (category) => { | |
| smsSettingSection.querySelector('#nonMemberReplaceCodeCaution').hidden = category === 'MEMBER'; | |
| window.preview.setContent(resolveReplaceCode(window.messageInput.getValue())); | |
| }); | |
| syncSmsPreviewAndHint(initialMessage); | |
| const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value; | |
| if (['EXCEL_UPLOAD', 'DIRECT'].includes(recipientType)) { | |
| } | |
| } | |
| function syncSmsPreviewAndHint(input) { | |
| window.preview.setContent(resolveReplaceCode(input)); | |
| MobileMessageReplaceCode.updateExpireCodeGuide('#replaceCodeContainer', input); | |
| if (input === '') { | |
| window.messageInput.setHint(null); | |
| smsSettingSection.querySelector('#useTemplateCaution').hidden = true; | |
| setDefaultPreview(['TEXT']); | |
| } else { | |
| window.messageInput.setHint(window.messageInput.calculateLength(input) > 90 ? 'LMS 건당 3포인트 차감' : 'SMS 건당 1포인트 차감'); | |
| } | |
| } | |
| function setTemplate(message) { | |
| smsSettingSection.querySelector('input[name="use080Reject"]').checked = false; | |
변경 전mobile_send.php
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/charcount.js')?> "></script> | |
| <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="<?= 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> | |
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/scroll-sticky.js') ?>"></script> | |
| ].flatMap(category => Object.keys(category)); | |
| const alternativeContents = mainContents.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(key) ? `{${key}}` : match; | |
| }); | |
| window.alternativeMessageInput.insertText(alternativeContents); | |
| window.alternativePreview.setContent(alternativeContents); | |
| } | |
| } | |
| function generateSmsLmsRequest() { | |
| const use080Reject = document.querySelector('input[name="use080Reject"]').checked; | |
| const mainContents = window.messageInput.getValue() ?? ''; | |
| const trackingLink = extractShortLinks(mainContents); | |
| const replaceKeys = extractReplaceKeys(mainContents).filter(key => !trackingLink.some(link => link.key === key)); | |
| const convertedContents = convertReplaceKeysToSmsReplaceKeys(mainContents, [...replaceKeys, ...trackingLink.map(link => link.key)]); | |
| return { | |
| isAdText: use080Reject, | |
| trackingLink: trackingLink, | |
| replaceKeys: replaceKeys, | |
| title: null, | |
| content: convertedContents | |
| } | |
| } | |
| function generateKakaoFriendtalkRequest() { | |
| // 캐러셀 데이터 | |
| const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value ?? 'TEXT'; | |
| const isCarouselFeed = campaignMessageType === 'CAROUSEL_FEED'; | |
| const carouselData = isCarouselFeed ? carouselState.getCarouselsData() : null; | |
| // 본문 메시지 | |
| const mainContents = window.messageInput.getValue() ?? ''; | |
| let trackingLink = isCarouselFeed ? carouselData.trackingLink : extractShortLinks(mainContents); | |
| let replaceKeys = (isCarouselFeed ? carouselData.replaceKeys : extractReplaceKeys(mainContents, true)).filter(key => !trackingLink.some(link => link.key === key)); | |
| // 와이드 아이템 리스트일 경우 타이틀도 치환코드 추출 | |
| if (campaignMessageType === 'WIDE_ITEM_LIST') { | |
| const campaignHeader = document.querySelector('input[name="friendtalkCampaignHeader"]')?.value || null; | |
| const headerReplaceKeys = extractReplaceKeys(campaignHeader, true).filter(key => !trackingLink.some(link => link.key === key)); | |
| replaceKeys = [...replaceKeys, ...headerReplaceKeys]; | |
| } | |
| // 대체 메시지 | |
| const alternativeContents = window.alternativeMessageInput.getValue() ?? ''; | |
| const alternativeTrackingLink = extractShortLinks(alternativeContents, false); | |
| const alternativeReplaceKeys = extractReplaceKeys(alternativeContents).filter(key => !alternativeTrackingLink.some(link => link.key === key)); | |
| const convertedAlternativeContents = convertReplaceKeysToSmsReplaceKeys(alternativeContents, [...alternativeReplaceKeys, ...alternativeTrackingLink.map(link => link.key)]); | |
| const use080Reject = document.querySelector('input[name="use080Reject"]').checked; | |
| return { | |
| campaignName: document.querySelector('input[name="friendtalkCampaignName"]')?.value ?? '', | |
| type: campaignMessageType, | |
| content: isCarouselFeed ? null : mainContents, | |
| replaceKeys: replaceKeys, | |
| buttons: isCarouselFeed ? null : collectFriendtalkLinkButtons(), | |
| coupon: isCarouselFeed ? null : collectFriendtalkCouponButton(), | |
| carousels: carouselData?.carousels, | |
| header: isCarouselFeed ? null : (document.querySelector('input[name="friendtalkCampaignHeader"]')?.value || null), | |
| items: collectFriendtalkItems(), | |
| imageLink: isCarouselFeed ? null : (document.querySelector('input[name="friendtalkCampaignImageLink"]')?.value.replace(/^\s+|\s+$/g, '') || null), | |
| imageUrl: isCarouselFeed ? null : (document.querySelector('input[name="friendtalkCampaignImageUrl"]')?.value.replace(/^\s+|\s+$/g, '') || null), | |
| linkPlatformType: messageConfig.kakaoFriendTalk.linkPlatformType, | |
| alternativeInfo: isCarouselFeed ? null : { | |
| title: null, | |
| content: convertedAlternativeContents, | |
| replaceKeys: alternativeReplaceKeys, | |
| isAdText: use080Reject, | |
| trackingLink: alternativeTrackingLink | |
| }, | |
| trackingLink: trackingLink | |
| } | |
| } | |
| function collectFriendtalkLinkButtons() { | |
| const linkButtonTableBody = document.querySelector('#linkButtonTableBody'); | |
| const buttons = []; | |
| if (linkButtonTableBody) { | |
| linkButtonTableBody.querySelectorAll('tr').forEach(row => { | |
| const buttonName = row.querySelector('.ncua-link-button-name').value; | |
| const webUrlInput = row.querySelector('input[name="linkButtonWebUrl"]')?.value; | |
| const iosUrlInput = row.querySelector('input[name="linkButtonIOSUrl"]')?.value; | |
| const aosUrlInput = row.querySelector('input[name="linkButtonAOSUrl"]')?.value; | |
| const linkPlatformType = messageConfig.kakaoFriendTalk.linkPlatformType; | |
| const isMobileApp = linkPlatformType === 'MOBILE_APP'; | |
| buttons.push({ | |
| name: buttonName.replace(/^\s+|\s+$/g, ''), | |
| url: { | |
| mobileUrl: isMobileApp ? null : (webUrlInput?.replace(/^\s+|\s+$/g, '') || ''), | |
| iosUrl: !isMobileApp ? null : (iosUrlInput?.replace(/^\s+|\s+$/g, '') || ''), | |
| aosUrl: !isMobileApp ? null : (aosUrlInput?.replace(/^\s+|\s+$/g, '') || '') | |
| } | |
| }); | |
| }); | |
| } | |
| return buttons.length > 0 ? buttons : null; | |
| } | |
| function collectFriendtalkCouponButton() { | |
| const couponTable = document.querySelector('#kakaoFriendTalkcouponTable'); | |
| if (!couponTable) return null; | |
| const friendtalkCouponNo = document.querySelector('input[name="friendtalkCouponNo"]')?.value; | |
| if (!friendtalkCouponNo) return null; | |
| const titleInput = couponTable.querySelector('input[name="couponTitle"]'); | |
| const descriptionInput = couponTable.querySelector('input[name="couponDescription"]'); | |
| const webLinkInput = couponTable.querySelector('input[name="couponUrl"]'); | |
| return { | |
| couponNo: parseInt(friendtalkCouponNo) || null, | |
| title: titleInput?.value?.replace(/^\s+|\s+$/g, '') || '', | |
| description: descriptionInput?.value?.replace(/^\s+|\s+$/g, '') || '', | |
| url: webLinkInput?.value?.replace(/^\s+|\s+$/g, '') || '' | |
| }; | |
| } | |
| function collectFriendtalkItems() { | |
| const items = []; | |
| document.querySelectorAll('.js-kakao-friendtalk-itemlist-section tbody tr').forEach(row => { | |
| const itemKey = row.dataset.itemKey; | |
| const titleInput = row.querySelector('input[name="itemTitle"]'); | |
| const urlInput = row.querySelector('input[name="itemWebLink"]'); | |
| const imageUrlInput = row.querySelector('input[name="itemImageUrl"]'); | |
| if (titleInput && titleInput.value.replace(/^\s+|\s+$/g, '')) { | |
| items.push({ | |
| key: itemKey, // 서버에서 파일 매칭용 | |
| title: titleInput.value.replace(/^\s+|\s+$/g, ''), | |
| imageUrl: imageUrlInput?.value || null, // 업로드된 imageUrl 사용 | |
| url: urlInput?.value?.replace(/^\s+|\s+$/g, '') || '' | |
| }); | |
| } | |
| }); | |
| return items.length > 0 ? items : null; | |
| } | |
| function generateKakaoAlimtalkRequest() { | |
| // 본문 메시지 | |
| const mainContents = document.querySelector('textarea[name="messageContent"]').value ?? ''; | |
| // 대체 메시지 | |
| const alternativeContents = window.alternativeMessageInput.getValue() ?? ''; | |
| const alternativeTrackingLink = extractShortLinks(alternativeContents, false); | |
| const alternativeReplaceKeys = extractReplaceKeys(alternativeContents).filter(key => !alternativeTrackingLink.some(link => link.key === key)); | |
| const convertedAlternativeContents = convertReplaceKeysToSmsReplaceKeys(alternativeContents, [...alternativeReplaceKeys, ...alternativeTrackingLink.map(link => link.key)]); | |
| const use080Reject = document.querySelector('input[name="use080Reject"]').checked; | |
| return { | |
| templateCode: document.querySelector('select[name="selectKakaoAlimtalkTemplate"]').value, | |
| title: null, | |
| content: mainContents, | |
| replaceKeys: extractReplaceKeys(mainContents, true), | |
| alternativeInfo: { | |
| title: null, | |
| isAdText: use080Reject, | |
| content: convertedAlternativeContents, | |
| replaceKeys: alternativeReplaceKeys, | |
| trackingLink: alternativeTrackingLink | |
| } | |
| } | |
| } | |
| function generateMyappRequest() { | |
| // 본문 메시지 | |
| const mainContents = window.messageInput.getValue() ?? ''; | |
| // 대체 메시지 | |
| const alternativeContents = window.alternativeMessageInput.getValue() ?? ''; | |
| const alternativeTrackingLink = extractShortLinks(alternativeContents, false); | |
| const alternativeReplaceKeys = extractReplaceKeys(alternativeContents).filter(key => !alternativeTrackingLink.some(link => link.key === key)); | |
| const convertedAlternativeContents = convertReplaceKeysToSmsReplaceKeys(alternativeContents, [...alternativeReplaceKeys, ...alternativeTrackingLink.map(link => link.key)]); | |
| const use080Reject = document.querySelector('input[name="use080Reject"]').checked; | |
| const myappSendCondition = document.querySelector('input[type="radio"][name="myappSendCondition"]:checked').value ?? ''; | |
| const myappNotificationType = document.querySelector('input[type="radio"][name="myappNotificationType"]:checked').value ?? ''; | |
| const myappTitle = document.querySelector('input[name="myappTitle"]').value ?? ''; | |
| const myappUnsubscribeGuide = document.querySelector('input[name="myappUnsubscribeGuide"]').value ?? ''; | |
| const pushUrl = document.querySelector('#mobileUrl').innerText + document.querySelector('input[name="myappPushPath"]')?.value; | |
| const platforms = { ALL: ['AOS', 'IOS'], ANDROID: ['AOS'], IOS: ['IOS'] }[document.querySelector('input[name="myappPlatform"]:checked')?.value] ?? null; | |
| return { | |
| sendCondition: myappSendCondition, | |
| notificationType: myappNotificationType, | |
| platforms: platforms, | |
| title: myappTitle, | |
| content: mainContents, | |
| unsubscribeGuide: myappUnsubscribeGuide, | |
| pushUrl: pushUrl, | |
| imageUrl: document.querySelector('input[name="myappImageUrl"]')?.value || null, | |
| replaceKeys: extractReplaceKeys(mainContents), | |
| alternativeInfo: { | |
| isAdText: use080Reject, | |
| content: convertedAlternativeContents, | |
| replaceKeys: alternativeReplaceKeys, | |
| trackingLink: alternativeTrackingLink | |
| } | |
| } | |
| } | |
| function generateSendMethodRequest() { | |
| // 발송 수단별 데이터 (smsLmsRequest, kakaoAlimtalkRequest, kakaoFriendtalkRequest, myappRequest) | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| let sendMethodRequest = {}; | |
| switch (sendMethod) { | |
| case 'SMS': | |
| sendMethodRequest.smsLmsRequest = generateSmsLmsRequest(); | |
| break; | |
| case 'FRIENDTALK': | |
| sendMethodRequest.kakaoFriendtalkRequest = generateKakaoFriendtalkRequest(); | |
| break; | |
| case 'ALIMTALK': | |
| sendMethodRequest.kakaoAlimtalkRequest = generateKakaoAlimtalkRequest(); | |
| break; | |
| case 'MYAPP': | |
| sendMethodRequest.myappRequest = generateMyappRequest(); | |
| break; | |
| default: | |
| break; | |
| } | |
| return sendMethodRequest; | |
| } | |
| function getSendData() { | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| }; | |
| } | |
| function extractShortLinks(contents, isMain = true) { | |
| const shortLinkKeys = isMain ? window.shortLinkKeys : window.shortLinkAlternativeKeys; | |
| const shortLinks = []; | |
| shortLinkKeys.forEach((url, key) => { | |
| if (contents.includes(`{${key}}`)) { | |
| shortLinks.push({ key, url }); | |
| } | |
| }); | |
| return shortLinks; | |
| } | |
| function extractReplaceKeys(str, isKakao = false) { | |
| if (!str) return []; | |
| const regex = isKakao ? /#\{([^}]+)\}/g : /\{([^}]+)\}/g; | |
| const matchedKeys = [...str.matchAll(regex)].map(m => m[1]); | |
| const allReplaceCodeKeys = [ | |
| MobileMessageReplaceCode.MEMBER, | |
| MobileMessageReplaceCode.GOODS, | |
| MobileMessageReplaceCode.ORDER, | |
| MobileMessageReplaceCode.PROMOTION, | |
| MobileMessageReplaceCode.BOARD, | |
| MobileMessageReplaceCode.REGULAR, | |
| MobileMessageReplaceCode.PRESENT, | |
| ].flatMap(category => MobileMessageReplaceCode.getAllKeys(category)); | |
| return matchedKeys.filter(key => | |
| allReplaceCodeKeys.includes(key) || key.startsWith('short_link_') | |
| ); | |
| } | |
| function convertReplaceKeysToSmsReplaceKeys(content, replaceKeys) { | |
| if (!content || !replaceKeys.length) return content; | |
| return replaceKeys.reduce((result, key) => { | |
| const pattern = new RegExp('(?<!\\$)\\{' + key + '\\}', 'g'); | |
| return result.replace(pattern, '${' + key + '}'); | |
| }, content); | |
| } | |
| function getExpectTargetCountSeparated() { | |
| const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value; | |
| return defaultResult; | |
| } | |
| function validateSendPassword(sendPassword) { | |
| return new Promise((resolve) => { | |
| $.ajax({ | |
| url: './mobile_send_ps.php', | |
| type: 'POST', | |
| data: { mode: 'validateSendPassword', sendPassword }, | |
| dataType: 'json', | |
| success: (response) => { | |
| if (response.success) return resolve({ success: true }); | |
| resolve({ success: false, message: response.message, subMessage: response.data?.subMessage }); | |
| }, | |
| error: () => { | |
| resolve({ success: false, message: '일시적인 오류가 발생하였습니다.<br>다시 시도해 주세요.' }); | |
| } | |
| }); | |
| }); | |
| } | |
| function validateCallNumber() { | |
| return new Promise((resolve) => { | |
| $.ajax({ | |
| return messagePoint >= requiredPoint; | |
| } | |
| function validateSendMethodContent() { | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| switch (sendMethod) { | |
| case 'SMS': | |
| const r = validateMessageInput(); | |
| if (!r.success) return r; | |
| break; | |
| case 'FRIENDTALK': | |
| const campaignMessageType = document.querySelector('input[name="campaignMessageType"]:checked')?.value; | |
| switch (campaignMessageType) { | |
| case 'TEXT': | |
| for (const r of [validateMessageInput(), validateCampaignName(), validateLinkButtons(), validateCoupon()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| case 'IMAGE': | |
| for (const r of [validateMessageInput(), validateCampaignName(), validateCampaignImage(), validateCampaignImageLink(), validateLinkButtons(), validateCoupon()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| case 'WIDE_IMAGE': | |
| for (const r of [validateMessageInput(), validateMessageContainsLink(campaignMessageType), validateCampaignName(), validateCampaignImage(), validateCampaignImageLink(), validateMessageContainsUrl(), validateLinkButtons(), validateCoupon()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| case 'WIDE_ITEM_LIST': | |
| for (const r of [validateCampaignName(), validateLinkButtons(), validateCoupon(), validateItemList()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| case 'CAROUSEL_FEED': | |
| // 캐러셀인경우 현재 상태 저장 | |
| window.carouselState.saveCurrentCarouselData(); | |
| for (const r of [validateCampaignName(), validateCarousel()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| } | |
| break; | |
| case 'MYAPP': | |
| for (const r of [validateMessageInput(), validateMyappPushTitle(), validateUnsubscribeGuide(), validateMyappPushUrlCheck()]) { | |
| if (!r.success) return r; | |
| } | |
| break; | |
| } | |
| return {success: true}; | |
| } | |
| function validateCarousel() { | |
| let slideIndex = 1; | |
| for (let index = 0; index < window.carouselState.carousels.length; index++) { | |
| const carousel = window.carouselState.carousels[index]; | |
| if (carousel.isDeleted) continue; | |
| // 캐러셀 이미지 검증 | |
| if (!carousel.imageUrl) { | |
| window.carouselState.setActiveIndex(index); | |
| const response = validateCampaignImage(slideIndex); | |
| if (!response.success) return response; | |
| } | |
| // 캐러셀 이미지 링크 검증 | |
| if (!carousel.imageLink || !(/^https?:\/\//.test(carousel.imageLink))) { | |
| window.carouselState.setActiveIndex(index); | |
| const response = validateCampaignImageLink(slideIndex); | |
| if (!response.success) return response; | |
| } | |
| // 캐러셀 메시지 검증 | |
| if (!carousel.message.replace(/^\s+|\s+$/g, '')) { | |
| window.carouselState.setActiveIndex(index); | |
| const response = validateMessageInput(index, slideIndex); | |
| if (!response.success) return response; | |
| } else { | |
| const response = validateMessageContainsLink('CAROUSEL_FEED', index, slideIndex); | |
| if (!response.success) return response; | |
| } | |
| // 캐러셀 링크버튼 검증 | |
| if (carousel.buttons.length > 0) { | |
| const urlPattern = /^https?:\/\//; | |
| let shouldValidate = false; | |
| for (const button of carousel.buttons) { | |
| if (!button.name) shouldValidate = true; | |
| const { iosUrl, aosUrl, mobileUrl } = button.url; | |
| if (!shouldValidate) { | |
| if (iosUrl || aosUrl) { | |
| shouldValidate = !iosUrl || !aosUrl || !urlPattern.test(iosUrl) || !urlPattern.test(aosUrl); | |
| } else { | |
| shouldValidate = !mobileUrl || !urlPattern.test(mobileUrl); | |
| } | |
| } | |
| } | |
| if (shouldValidate) { | |
| window.carouselState.setActiveIndex(index); | |
| const response = validateLinkButtons(slideIndex); | |
| if (!response.success) return response; | |
| } | |
| } | |
| // 캐러셀 쿠폰 검증 | |
| if (carousel.coupon) { | |
| if (!carousel.coupon.title || !carousel.coupon.description || !carousel.coupon.url || !(/^https?:\/\//.test(carousel.coupon.url))) { | |
| const response = validateCoupon(slideIndex); | |
| if (!response.success) return response; | |
| } | |
| } | |
| slideIndex++; | |
| } | |
| return {success: true}; | |
| } | |
| function validateMessageInput(carouselIndex = null, carouselSlideIndex = null) { | |
| const messageContent = carouselIndex !== null ? window.carouselState.carousels[carouselIndex].message.replace(/^\s+|\s+$/g, '') : window.messageInput.getValue().replace(/^\s+|\s+$/g, ''); | |
| if (!messageContent) { | |
| if (carouselIndex !== null) { | |
| window.carouselState.setActiveIndex(carouselIndex); | |
| } | |
| NCDSValidator.highlight(window.messageInput.getTextareaContainer()); | |
| const message = carouselIndex ? `캐러셀 ${carouselSlideIndex}번 내용을 입력해주세요.` : '발송할 내역이 없습니다.'; | |
| return {success: false, message, subMessage: '메시지 내용을 입력 해주세요.'}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateMessageContainsLink(campaignMessageType, carouselIndex = null, carouselSlideIndex = null) { | |
| const urlPattern = /https?:\/\//; | |
| const content = carouselIndex !== null ? window.carouselState.carousels[carouselIndex].message.replace(/^\s+|\s+$/g, '') : window.messageInput.getValue().replace(/^\s+|\s+$/g, ''); | |
| if (urlPattern.test(content)) { | |
| if (carouselIndex !== null) { | |
| window.carouselState.setActiveIndex(carouselIndex); | |
| } | |
| NCDSValidator.highlight(window.messageInput.getTextareaContainer()); | |
| const label = campaignMessageType === 'CAROUSEL_FEED' ? '캐러셀' : '와이드 이미지'; | |
| const message = carouselIndex | |
| ? `${label} ${carouselSlideIndex}번 메시지 타입은 메시지 내용에 링크(URL)를 포함할 수 없습니다.` | |
| : `${label} 메시지 타입은 메시지 내용에 링크(URL)를 포함할 수 없습니다.`; | |
| return {success: false, message}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateCampaignName() { | |
| const campaignNameContainer = document.querySelector('input[name="friendtalkCampaignName"]'); | |
| const campaignNameText = campaignNameContainer?.value?.replace(/^\s+|\s+$/g, '') ?? ''; | |
| if (!campaignNameText) { | |
| NCDSValidator.highlight(campaignNameContainer); | |
| return {success: false, message: '캠페인명을 입력해주세요.'}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateCampaignImage(carouselIndex = null) { | |
| const campaignImageUrl = document.querySelector('input[name="friendtalkCampaignImageUrl"]'); | |
| if (!campaignImageUrl?.value) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 이미지를 등록해주세요.` : '이미지를 등록해주세요.'; | |
| const imageButton = document.querySelector('#campaign-image-file-input-container .ncua-image-file-input'); | |
| if (imageButton) { | |
| NCDSValidator.highlight(imageButton); | |
| } | |
| return {success: false, message}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateCampaignImageLink(carouselIndex = null) { | |
| const campaignImageLink = document.querySelector('input[name="friendtalkCampaignImageLink"]'); | |
| if (!campaignImageLink?.value) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 WEB 링크를 입력해주세요.` : 'WEB 링크를 입력해주세요.'; | |
| NCDSValidator.highlight(campaignImageLink); | |
| return {success: false, message}; | |
| } | |
| const urlPattern = /^https?:\/\//; | |
| const urlFormatMessage = carouselIndex | |
| ? `캐러셀 ${carouselIndex}번 http:// 또는 https:// 로 시작하는 URL을 입력해주세요.` | |
| : 'http:// 또는 https:// 로 시작하는 URL을 입력해주세요.'; | |
| if (!urlPattern.test(campaignImageLink.value)) { | |
| NCDSValidator.highlight(campaignImageLink); | |
| return {success: false, message: urlFormatMessage}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateMessageContainsUrl() { | |
| const messageContent = window.messageInput.getValue(); | |
| const isContainUrl = /https?:\/\/[^\s]+/.test(messageContent); | |
| if (isContainUrl) { | |
| NCDSValidator.highlight(window.messageInput.getTextareaContainer()); | |
| return {success: false, message: '와이드 이미지 메시지 타입은 메시지 내용에 링크(URL)을 포함할 수 없습니다.'}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateLinkButtons(carouselIndex = null) { | |
| const rows = document.querySelectorAll('#linkButtonTableBody tr'); | |
| for (const row of rows) { | |
| const buttonName = row.querySelector('.ncua-link-button-name'); | |
| if (!buttonName?.value) { | |
| NCDSValidator.highlight(buttonName); | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 버튼명을 입력해주세요.` : '버튼명을 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| const webUrl = row.querySelector('input[name="linkButtonWebUrl"]'); | |
| if (webUrl && !webUrl.value) { | |
| NCDSValidator.highlight(webUrl); | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 WEB 링크를 입력해주세요.` : 'WEB 링크를 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| const urlPattern = /^https?:\/\//; | |
| const urlFormatMessage = carouselIndex | |
| ? `캐러셀 ${carouselIndex}번 http:// 또는 https:// 로 시작하는 URL을 입력해주세요.` | |
| : 'http:// 또는 https:// 로 시작하는 URL을 입력해주세요.'; | |
| if (webUrl?.value && !urlPattern.test(webUrl.value)) { | |
| NCDSValidator.highlight(webUrl); | |
| return {success: false, message: urlFormatMessage}; | |
| } | |
| const iosUrl = row.querySelector('input[name="linkButtonIOSUrl"]'); | |
| const aosUrl = row.querySelector('input[name="linkButtonAOSUrl"]'); | |
| if (iosUrl && (!iosUrl.value || !aosUrl?.value)) { | |
| if (!iosUrl.value) NCDSValidator.highlight(iosUrl); | |
| if (!aosUrl?.value) NCDSValidator.highlight(aosUrl); | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 iOS, AOS 링크 모두 입력해주세요.` : 'iOS, AOS 링크 모두 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| if (iosUrl?.value && !urlPattern.test(iosUrl.value)) { | |
| NCDSValidator.highlight(iosUrl); | |
| return {success: false, message: urlFormatMessage}; | |
| } | |
| if (aosUrl?.value && !urlPattern.test(aosUrl.value)) { | |
| NCDSValidator.highlight(aosUrl); | |
| return {success: false, message: urlFormatMessage}; | |
| } | |
| } | |
| return {success: true}; | |
| } | |
| function validateCoupon(carouselIndex = null) { | |
| const couponTable = document.querySelector('#kakaoFriendTalkcouponTable'); | |
| if (couponTable) { | |
| const selectCoupon = couponTable.querySelector('#selectCoupon'); | |
| if (selectCoupon) { | |
| NCDSValidator.highlight(selectCoupon); | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 쿠폰을 선택해주세요.` : '쿠폰을 선택해주세요.'; | |
| return {success: false, message}; | |
| } | |
| const couponTitle = couponTable.querySelector('input[name="couponTitle"]'); | |
| const couponDescription = couponTable.querySelector('input[name="couponDescription"]'); | |
| const couponUrl = couponTable.querySelector('input[name="couponUrl"]'); | |
| if (couponTitle) { | |
| const validCouponUrl = /^https?:\/\//.test(couponUrl?.value ?? ''); | |
| if (!couponTitle.value || !couponDescription.value || !couponUrl.value || !validCouponUrl) { | |
| if (!couponTitle.value) NCDSValidator.highlight(couponTitle); | |
| if (!couponDescription.value) NCDSValidator.highlight(couponDescription); | |
| if (!couponUrl.value || !validCouponUrl) NCDSValidator.highlight(couponUrl); | |
| if (!couponTitle.value) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 쿠폰 타이틀을 입력해주세요.` : '쿠폰 타이틀을 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| if (!couponDescription.value) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 쿠폰 설명을 입력해주세요.` : '쿠폰 설명을 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| if (!couponUrl.value) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 WEB 링크를 입력해주세요.` : 'WEB 링크를 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| if (!validCouponUrl) { | |
| const message = carouselIndex ? `캐러셀 ${carouselIndex}번 http:// 또는 https:// 로 시작하는 URL을 입력해주세요.` : 'http:// 또는 https:// 로 시작하는 URL을 입력해주세요.'; | |
| return {success: false, message}; | |
| } | |
| } | |
| } | |
| } | |
| return {success: true}; | |
| } | |
| function validateItemList() { | |
| const kakaoFriendtalkItemListSection = document.querySelectorAll('.js-kakao-friendtalk-itemlist-section tbody tr'); | |
| if (kakaoFriendtalkItemListSection.length < 3 || kakaoFriendtalkItemListSection.length > 4) { | |
| return {success: false, message: '와이드 아이템 리스트는 최소 3개 , 최대 4개까지 가능합니다.'}; | |
| } | |
| for (const item of kakaoFriendtalkItemListSection) { | |
| const itemImageUrl = item.querySelector('input[name="itemImageUrl"]'); | |
| const itemTitleContainer = item.querySelector('input[name="itemTitle"]'); | |
| const itemLinkContainer = item.querySelector('input[name="itemWebLink"]'); | |
| if (!itemImageUrl?.value) { | |
| const imageButton = item.querySelector('.campaign-item-image-file-input-container .ncua-image-file-input'); | |
| if (imageButton) { | |
| NCDSValidator.highlight(imageButton); | |
| } | |
| return {success: false, message: '이미지를 등록해주세요.'}; | |
| } | |
| if (!itemTitleContainer?.value?.replace(/^\s+|\s+$/g, '')) { | |
| NCDSValidator.highlight(itemTitleContainer); | |
| return {success: false, message: '타이틀을 입력해주세요.'}; | |
| } | |
| if (!itemLinkContainer?.value?.replace(/^\s+|\s+$/g, '')) { | |
| NCDSValidator.highlight(itemLinkContainer); | |
| return {success: false, message: 'WEB 링크를 입력해주세요.'}; | |
| } | |
| const urlPattern = /^https?:\/\//; | |
| const message = 'http:// 또는 https:// 로 시작하는 URL을 입력해주세요.'; | |
| if (!urlPattern.test(itemLinkContainer.value)) { | |
| NCDSValidator.highlight(itemLinkContainer); | |
| return {success: false, message}; | |
| } | |
| } | |
| return {success: true}; | |
| } | |
| function validateMyappPushTitle() { | |
| const myappPushTitleContainer = document.querySelector('input[name="myappTitle"]'); | |
| if (!myappPushTitleContainer.value) { | |
| NCDSValidator.highlight(myappPushTitleContainer); | |
| return {success: false, message: '푸시 제목을 입력해주세요.'}; | |
| } | |
| return {success: true}; | |
| } | |
| function validateUnsubscribeGuide() { | |
| const myappNotificationType = document.querySelector('input[type="radio"][name="myappNotificationType"]:checked').value; | |
| if (myappNotificationType === 'AD') { | |
| const unsubscribeGuideContainer = document.querySelector('input[name="myappUnsubscribeGuide"]'); | |
| if (!unsubscribeGuideContainer.value) { | |
| NCDSValidator.highlight(unsubscribeGuideContainer); | |
| return {success: false, message: '수신동의 철회 방법을 입력해주세요.'}; | |
| } | |
| } | |
| return {success: true}; | |
| } | |
| function validateMyappPushUrlCheck(){ | |
| const pushUrlPathContainer = document.querySelector('input[name="myappPushPath"]'); | |
| // URL 경로가 비어있으면 검증 불필요 (메인페이지 이동) | |
| if (!pushUrlPathContainer.value) { | |
| return {success: true}; | |
| } | |
| if (document.querySelector('input[name="validatePushUrl"]').value !== 'y') { | |
| NCDSValidator.highlight(pushUrlPathContainer); | |
| return {success: false, message: '입력한 푸시 URL을 검증해주세요.'}; | |
| } | |
| return {success: true}; | |
| } | |
| function getExpectTargetCount() { | |
| const recipientType = document.querySelector('[name="recipientType"]:checked')?.value | |
변경 후mobile_send.php
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/charcount.js')?> "></script> | |
| <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> | |
| <script defer src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/scroll-sticky.js') ?>"></script> | |
| ].flatMap(category => Object.keys(category)); | |
| const alternativeContents = mainContents.replace(/#\{([a-zA-Z_0-9]+)}/g, (match, key) => { | |
| return allKeys.includes(MobileMessageReplaceCode.normalizeReplaceKey(key)) ? `{${key}}` : match; | |
| }); | |
| window.alternativeMessageInput.insertText(alternativeContents); | |
| window.alternativePreview.setContent(alternativeContents); | |
| } | |
| } | |
| function getSendData() { | |
| const sendMethod = document.querySelector('input[name="sendMethod"]:checked').value; | |
| }; | |
| } | |
| function getExpectTargetCountSeparated() { | |
| const recipientType = document.querySelector('input[name="recipientType"]:checked')?.value; | |
| return defaultResult; | |
| } | |
| function validateCallNumber() { | |
| return new Promise((resolve) => { | |
| $.ajax({ | |
| return messagePoint >= requiredPoint; | |
| } | |
| function getExpectTargetCount() { | |
| const recipientType = document.querySelector('[name="recipientType"]:checked')?.value | |
변경 전popup_create_crm_group_frame.php
| <script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script> | |
| <iframe name="popupCreateCrmGroup" src="<?= htmlspecialchars($pageUrl, ENT_QUOTES, 'UTF-8') ?>" frameborder="0" marginwidth="0" marginheight="0" width="100%" style="height: 100vh;"></iframe> | |
| <script> | |
| window.addEventListener('message', function (event) { | |
| if (event.data.type === 'MODAL_CLOSE') { | |
| const { no, title } = event.data?.payload ?? {}; | |
| opener.selectCreatedCrmGroup(no, title); | |
| } | |
| }); | |
| </script> | |
변경 후popup_create_crm_group_frame.php
| <script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script> | |
| <iframe name="popupCreateCrmGroup" src="<?= htmlspecialchars($pageUrl, ENT_QUOTES, 'UTF-8') ?>" frameborder="0" marginwidth="0" marginheight="0" width="100%" style="height: 100vh;"></iframe> | |
| <script> | |
| // aggregator가 그룹 생성 성공 시 MODAL_CLOSE 전송 — iframe은 창을 못 닫으므로 여기서 opener 반영 후 닫는다. | |
| IframeMessageRouter.init({ | |
| onModalClose: function (payload) { | |
| const { no, title } = payload ?? {}; | |
| if (no) { | |
| opener?.selectCreatedCrmGroup?.(no, title); | |
| } | |
| window.close(); | |
| }, | |
| }); | |
| </script> | |
변경 전MemberAdmin.php
| use Origin\Service\WebHook\MemberWebHookService; | |
| use Request; | |
| use Session; | |
| use Origin\Model\Member\Member as MemberModel; | |
| /** | |
| * Class 관리자에서 사용하는 회원 관리 | |
| /** | |
| * 회원 알림 치환코드 조회 | |
| * | |
| * @param array $memberNos | |
| * | |
| * @return array | |
| */ | |
| public function getNotificationReplaceCodes(array $memberNos): array | |
| { | |
| $this->db = \App::load('DB'); | |
| $members = MemberModel::from('es_member as m') | |
| ->leftJoin('es_memberHistory as mh', function ($join){ | |
| $join->on('mh.memNo', '=', 'm.memNo') | |
| ->where('mh.updateColumn', '=', '\'SMS수신동의\''); | |
| }) | |
| ->leftJoin('es_memberHistory as mh2', function ($join){ | |
| $join->on('mh2.memNo', '=', 'm.memNo') | |
| ->where('mh2.updateColumn', '=', '\'메일수신동의\''); | |
| }) | |
| ->leftJoin('es_memberGroup as mg', 'm.groupSno', '=', 'mg.sno') | |
| ->whereIn("m.memNo", $memberNos) | |
| ->selectRaw('m.memNo, m.memId, m.memNm, m.groupSno, m.mileage, m.deposit, m.smsFl, m.maillingFl, m.cellPhone, m.email, m.entryDt, m.sleepWakeDt, m.expirationFl, mg.groupNm') | |
| ->selectRaw('IF(m.smsFl = \'y\', IF(mh.updateColumn IS NULL, IF(m.entryDt < \'2014-11-29\', \'2014-11-28 00:00:00\', m.entryDt), MAX(mh.regDt)), null) AS smsAgreementDt') | |
| ->selectRaw('IF(m.maillingFl = \'y\', IF(mh2.updateColumn IS NULL, IF(m.entryDt < \'2014-11-29\', \'2014-11-28 00:00:00\', m.entryDt), MAX(mh2.regDt)), null) AS mailAgreementDt') | |
| ->groupBy('m.memNo') | |
| ->get(); | |
| $members = $members ? $members->toArray() : []; | |
| $basicInfoConfigs = gd_policy('basic.info'); | |
| $replaceCodes = []; | |
| $members = $sleepService->getSleepScheduleList($members, $memberSleepConfig['useFl']); | |
| foreach ($members as $member) { | |
| $replaceCodes[] = [ | |
| 'memNo' => (int) $member['memNo'], | |
| 'codes' => [ | |
| 'rc_mallNm' => $basicInfoConfigs['mallNm'], | |
| 'memId' => $member['memId'], | |
| 'memNm' => $member['memNm'], | |
| 'sleepScheduleDt' => $member['sleepScheduleDt'], | |
| 'rc_scheduleDt' => DateTimeUtils::dateFormat('Y년 m월 d일', $member['sleepWakeDt']), | |
| 'smsAgreementDt' => DateTimeUtils::dateFormat('Y-m-d', $member['smsAgreementDt']), | |
| 'mailAgreementDt' => DateTimeUtils::dateFormat('Y-m-d', $member['mailAgreementDt']), | |
| 'smsAgreementFl' => $member['smsFl'], | |
| 'mailAgreementFl' => $member['maillingFl'], | |
| 'groupNm' => $member['groupNm'], | |
| 'mileage' => number_format($member['mileage']), | |
| 'deposit' => number_format($member['deposit']), | |
| 'passJoin' => $this->calculatePassJoinDays($member['entryDt']), | |
| 'shopUrl' => URI_HOME | |
| ] | |
| ]; | |
| } | |
| return $replaceCodes; | |
| } | |
| protected function calculatePassJoinDays(?string $entryDt): string | |
| { | |
| $start = $entryDt && $entryDt != '0000-00-00 00:00:00' | |
변경 후MemberAdmin.php
| use Origin\Service\WebHook\MemberWebHookService; | |
| use Request; | |
| use Session; | |
| /** | |
| * Class 관리자에서 사용하는 회원 관리 | |
| /** | |
| * 회원 알림 치환코드 조회 | |
| * | |
| * @param array $memberNos | |
| * @param array|null $replaceKeys 평탄화된 _Nd 키 포함 가능 (예: ['expireCoupon_7d', 'memId']) | |
| * | |
| * @return array | |
| */ | |
| public function getNotificationReplaceCodes(array $memberNos, ?array $replaceKeys = null): array | |
| { | |
| $expireDays = $this->extractExpireDays($replaceKeys); | |
| $couponDays = $expireDays['expireCoupon']; | |
| $mileageDays = $expireDays['expireMileage']; | |
| $needCouponJoin = !empty($couponDays) || in_array('useCouponCnt', $replaceKeys ?? [], true); | |
| $members = $this->memberDAO->selectNotificationReplaceData($memberNos, $couponDays, $mileageDays, $needCouponJoin); | |
| $basicInfoConfigs = gd_policy('basic.info'); | |
| $replaceCodes = []; | |
| $members = $sleepService->getSleepScheduleList($members, $memberSleepConfig['useFl']); | |
| foreach ($members as $member) { | |
| $codes = [ | |
| 'rc_mallNm' => $basicInfoConfigs['mallNm'], | |
| 'memId' => $member['memId'], | |
| 'memNm' => $member['memNm'], | |
| 'sleepScheduleDt' => $member['sleepScheduleDt'], | |
| 'rc_scheduleDt' => DateTimeUtils::dateFormat('Y년 m월 d일', $member['sleepWakeDt']), | |
| 'smsAgreementDt' => DateTimeUtils::dateFormat('Y-m-d', $member['smsAgreementDt']), | |
| 'mailAgreementDt' => DateTimeUtils::dateFormat('Y-m-d', $member['mailAgreementDt']), | |
| 'smsAgreementFl' => $member['smsFl'], | |
| 'mailAgreementFl' => $member['maillingFl'], | |
| 'groupNm' => $member['groupNm'], | |
| 'mileage' => number_format($member['mileage']), | |
| 'deposit' => number_format($member['deposit']), | |
| 'passJoin' => $this->calculatePassJoinDays($member['entryDt']), | |
| 'shopUrl' => URI_HOME, | |
| ]; | |
| if ($needCouponJoin) { | |
| $codes['useCouponCnt'] = (string) $member['useCouponCnt']; | |
| foreach ($couponDays as $n) { | |
| $key = "expireCoupon_{$n}d"; | |
| $codes[$key] = (string) ($member[$key] ?? ''); | |
| } | |
| } | |
| foreach ($mileageDays as $n) { | |
| $key = "expireMileage_{$n}d"; | |
| $codes[$key] = number_format((int) ($member[$key] ?? 0)); | |
| } | |
| $filteredCodes = empty($replaceKeys) | |
| ? $codes | |
| : array_intersect_key($codes, array_flip($replaceKeys)); | |
| // 매칭되는 키가 하나도 없으면 기본 전체 코드로 폴백 (replaceKeys 미전달 시와 동일) | |
| $replaceCodes[] = [ | |
| 'memNo' => (int) $member['memNo'], | |
| 'codes' => empty($filteredCodes) ? $codes : $filteredCodes, | |
| ]; | |
| } | |
| return $replaceCodes; | |
| } | |
| /** | |
| * replaceKeys에서 _Nd 평탄화 키의 N값 목록 추출 → {expireCoupon: [int], expireMileage: [int]} | |
| */ | |
| protected function extractExpireDays(?array $replaceKeys): array | |
| { | |
| $result = ['expireCoupon' => [], 'expireMileage' => []]; | |
| if (empty($replaceKeys)) { | |
| return $result; | |
| } | |
| foreach ($replaceKeys as $key) { | |
| if (!is_string($key)) { | |
| continue; | |
| } | |
| if (preg_match('/^expire(Coupon|Mileage)_(\d+)d$/', $key, $m)) { | |
| $type = $m[1] === 'Coupon' ? 'expireCoupon' : 'expireMileage'; | |
| $result[$type][] = (int) $m[2]; | |
| } | |
| } | |
| $result['expireCoupon'] = array_values(array_unique($result['expireCoupon'])); | |
| $result['expireMileage'] = array_values(array_unique($result['expireMileage'])); | |
| return $result; | |
| } | |
| protected function calculatePassJoinDays(?string $entryDt): string | |
| { | |
| $start = $entryDt && $entryDt != '0000-00-00 00:00:00' | |
변경 전MemberDAO.php
| $query->selectRaw('COUNT(DISTINCT mc.memberCouponNo) AS useCouponCnt'); | |
| foreach ($couponDays as $n) { | |
| // couponNo 높은 1개만 노출 | |
| $query->selectRaw("SUBSTRING_INDEX(GROUP_CONCAT(IF(mc.memberCouponEndDate BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL {$n} DAY), cp.couponNm, NULL) ORDER BY mc.couponNo DESC SEPARATOR 0x1D), 0x1D, 1) AS expireCoupon_{$n}d"); | |
| } | |
| } | |
| foreach ($mileageDays as $n) { | |
| use Framework\Utility\StringUtils; | |
| use Framework\Security\Digester; | |
| use Framework\Utility\GodoUtils; | |
| use Exception; | |
| /** | |
| $this->db->strOrder = StringUtils::strIsSet($params['sort'], 'm.memNo desc'); | |
| $this->db->strJoin .= ' LEFT JOIN ' . DB_MEMBER_SNS . ' AS ms ON m.memNo = ms.memNo'; | |
| } | |
| } | |
변경 후MemberDAO.php
| $query->selectRaw('COUNT(DISTINCT mc.memberCouponNo) AS useCouponCnt'); | |
| foreach ($couponDays as $n) { | |
| // couponNo 높은 1개만 노출 | |
| $query->selectRaw("SUBSTRING_INDEX(GROUP_CONCAT(IF(DATE(mc.memberCouponEndDate) = DATE(DATE_ADD(NOW(), INTERVAL {$n} DAY)), cp.couponNm, NULL) ORDER BY mc.couponNo DESC SEPARATOR 0x1D), 0x1D, 1) AS expireCoupon_{$n}d"); | |
| } | |
| } | |
| foreach ($mileageDays as $n) { | |
| use Framework\Utility\StringUtils; | |
| use Framework\Security\Digester; | |
| use Framework\Utility\GodoUtils; | |
| use Origin\Model\Member\Member as MemberModel; | |
| use Exception; | |
| /** | |
| $this->db->strOrder = StringUtils::strIsSet($params['sort'], 'm.memNo desc'); | |
| $this->db->strJoin .= ' LEFT JOIN ' . DB_MEMBER_SNS . ' AS ms ON m.memNo = ms.memNo'; | |
| } | |
| /** | |
| * 알림 치환코드 산출에 필요한 회원 원본 데이터 조회 | |
| * | |
| * 회원 기본정보 + 수신동의 이력 + (선택)쿠폰/마일리지 만료 집계. | |
| * 비즈니스 조립/포맷팅은 호출측(MemberAdmin::getNotificationReplaceCodes)이 담당한다. | |
| * | |
| * @param int[] $memberNos 대상 회원 번호 목록 | |
| * @param int[] $couponDays 쿠폰 만료 집계 일수 목록 (expireCoupon_Nd 의 N) | |
| * @param int[] $mileageDays 마일리지 만료 집계 일수 목록 (expireMileage_Nd 의 N) | |
| * @param bool $needCouponJoin 쿠폰 조인 필요 여부 (만료 쿠폰 또는 useCouponCnt 요청 시) | |
| * @return array 회원별 원본 행 배열 | |
| */ | |
| public function selectNotificationReplaceData(array $memberNos, array $couponDays, array $mileageDays, bool $needCouponJoin): array | |
| { | |
| $couponDays = array_map('intval', $couponDays); | |
| $mileageDays = array_map('intval', $mileageDays); | |
| $query = MemberModel::from('es_member as m') | |
| ->leftJoin('es_memberHistory as mh', function ($join) { | |
| $join->on('mh.memNo', '=', 'm.memNo') | |
| ->where('mh.updateColumn', 'SMS수신동의'); | |
| }) | |
| ->leftJoin('es_memberHistory as mh2', function ($join) { | |
| $join->on('mh2.memNo', '=', 'm.memNo') | |
| ->where('mh2.updateColumn', '메일수신동의'); | |
| }) | |
| ->leftJoin('es_memberGroup as mg', 'm.groupSno', '=', 'mg.sno'); | |
| if ($needCouponJoin) { | |
| $query->leftJoin('es_memberCoupon as mc', function ($join) { | |
| $join->on('mc.memNo', '=', 'm.memNo') | |
| ->where(function ($join) { | |
| $join->where('mc.memberCouponState', 'y') | |
| ->orWhere(function ($join) { | |
| $join->where('mc.memberCouponState', 'cart') | |
| ->where('mc.memberCouponCartDate', '>', '0000-00-00 00:00:00'); | |
| }); | |
| }) | |
| ->whereRaw('mc.memberCouponStartDate <= NOW()') | |
| ->whereRaw('mc.memberCouponEndDate >= NOW()'); | |
| }); | |
| if (!empty($couponDays)) { | |
| $query->leftJoin('es_coupon as cp', 'cp.couponNo', '=', 'mc.couponNo'); | |
| } | |
| } | |
| if (!empty($mileageDays)) { | |
| // 마일리지 합계는 mh/mh2·쿠폰 조인의 행 곱셈에 오염되지 않도록 회원별로 미리 집계한 서브쿼리로 분리한다. | |
| $mileageSubQuery = $this->buildExpiringMileageSubQuery($mileageDays, $memberNos); | |
| $query->leftJoinSub($mileageSubQuery, 'mm', 'mm.memNo', '=', 'm.memNo'); | |
| } | |
| $query->whereIn('m.memNo', $memberNos) | |
| ->selectRaw('m.memNo, m.memId, m.memNm, m.groupSno, m.mileage, m.deposit, m.smsFl, m.maillingFl, m.cellPhone, m.email, m.entryDt, m.sleepWakeDt, m.expirationFl, mg.groupNm') | |
| ->selectRaw('IF(m.smsFl = \'y\', IF(mh.updateColumn IS NULL, IF(m.entryDt < \'2014-11-29\', \'2014-11-28 00:00:00\', m.entryDt), MAX(mh.regDt)), null) AS smsAgreementDt') | |
| ->selectRaw('IF(m.maillingFl = \'y\', IF(mh2.updateColumn IS NULL, IF(m.entryDt < \'2014-11-29\', \'2014-11-28 00:00:00\', m.entryDt), MAX(mh2.regDt)), null) AS mailAgreementDt'); | |
| if ($needCouponJoin) { | |
| $query->selectRaw('COUNT(DISTINCT mc.memberCouponNo) AS useCouponCnt'); | |
| foreach ($couponDays as $n) { | |
| // couponNo 높은 1개만 노출 | |
| $query->selectRaw("SUBSTRING_INDEX(GROUP_CONCAT(IF(mc.memberCouponEndDate BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL {$n} DAY), cp.couponNm, NULL) ORDER BY mc.couponNo DESC SEPARATOR 0x1D), 0x1D, 1) AS expireCoupon_{$n}d"); | |
| } | |
| } | |
| foreach ($mileageDays as $n) { | |
| // 서브쿼리에서 회원별로 이미 합산된 값을 그대로 가져온다 (조인 곱셈 영향 없음). | |
| $query->selectRaw("mm.expireMileage_{$n}d"); | |
| } | |
| $members = $query->groupBy('m.memNo')->get(); | |
| return $members ? $members->toArray() : []; | |
| } | |
| /** | |
| * 회원별 만료 예정 마일리지 합계 서브쿼리를 만든다. | |
| * | |
| * 메인 쿼리에서 직접 SUM 하면 mh/mh2(수신동의 이력)·쿠폰 LEFT JOIN 의 행 곱셈만큼 합계가 부풀려진다. | |
| * 이를 막기 위해 es_memberMileage 를 회원별(memNo)로 GROUP BY 하여 1행으로 미리 집계한 뒤 조인한다. | |
| * | |
| * @param int[] $mileageDays 만료 임박 기준 일수 목록 (검증된 정수) | |
| * @param array $memberNos | |
| * | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| protected function buildExpiringMileageSubQuery(array $mileageDays, array $memberNos) | |
| { | |
| $subQuery = MemberModel::from('es_memberMileage') | |
| ->whereIn('deleteFl', ['n', 'use']) | |
| ->where('mileage', '>', 0) | |
| ->whereIn('memNo', $memberNos) | |
| ->groupBy('memNo') | |
| ->select('memNo'); | |
| foreach ($mileageDays as $n) { | |
| $subQuery->selectRaw("COALESCE(SUM(IF(DATE(deleteScheduleDt) = DATE(DATE_ADD(NOW(), INTERVAL {$n} DAY)), mileage, 0)), 0) AS expireMileage_{$n}d"); | |
| } | |
| return $subQuery; | |
| } | |
| } | |
변경 전CrmRecipeClickLogPsController.php
변경 후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]); | |
| } | |
| } | |
변경 전CrmRecipeSendController.php
변경 후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 Bundle\Component\Godo\GodoSmsServerApi; | |
| use Bundle\Component\Sms\SmsAdmin; | |
| 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; | |
| class CrmRecipeSendController extends \Controller\Admin\Controller | |
| { | |
| protected $messageLogger; | |
| public function index() | |
| { | |
| $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 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); | |
| $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), | |
| ]; | |
| } | |
| } | |
변경 전LayerKakaoFriendtalkContentSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Framework\Utility\StringUtils; | |
| use Origin\Enum\Crm\Message\CampaignMessageType; | |
| use Origin\Service\Crm\Message\KakaoFriendTalkSettingService; | |
| use Origin\Service\Crm\Message\Sms080RejectService; | |
| class LayerKakaoFriendtalkContentSettingController extends \Controller\Admin\Controller | |
| { | |
| public function index() | |
| { | |
| $this->callMenu('crm', 'messageSend', 'mobileSend'); | |
| $campaignMessageTypeUnitPoint[$type->name] = $type->unitPoint(); | |
| } | |
| $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->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 후LayerKakaoFriendtalkContentSettingController.php
| 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'); | |
| $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'] ?? ''; | |
| $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->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 전LayerMyappPushContentSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Framework\Utility\StringUtils; | |
| use Origin\Enum\Crm\Message\MyappPushNotificationType; | |
| use Origin\Enum\Crm\Message\MyappPushSendCondition; | |
| class LayerMyappPushContentSettingController extends \Controller\Admin\Controller | |
| { | |
| public function index() | |
| { | |
| $this->callMenu('crm', 'messageSend', 'mobileSend'); | |
| $myappPushTemplateService = \App::getInstance(MyappPushTemplateService::class); | |
| $myappPushUrl = $myappPushTemplateService->getMyappDomainUrl(); | |
| $this->setData('is080RejectAvailable', $is080RejectAvailable); | |
| $this->setData('myappPushSendConditions', MyappPushSendCondition::cases()); | |
| $this->setData('myappPushNotificationTypes', MyappPushNotificationType::cases()); | |
| $this->setData('myappPushSendPlatforms', MyappPushSendPlatform::cases()); | |
| $this->setData('myappPushUrl', $myappPushUrl); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 후LayerMyappPushContentSettingController.php
| 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; | |
| class LayerMyappPushContentSettingController extends \Controller\Admin\Controller | |
| { | |
| use RecipeContextResolverTrait; | |
| public function index() | |
| { | |
| $this->callMenu('crm', 'messageSend', 'mobileSend'); | |
| $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'] ?? ''; | |
| $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->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 전LayerRecipeSendTimeSettingController.php
변경 후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'] ?? [], | |
| ]; | |
| } | |
| } | |
변경 전LayerSendMethodSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Origin\Enum\Crm\Message\SendMethod; | |
| use Origin\Service\Crm\Message\Sms080RejectService; | |
| class LayerSendMethodSettingController extends \Controller\Admin\Controller | |
| { | |
| public function index() | |
| { | |
| $this->callMenu('crm', 'messageSend', 'mobileSend'); | |
| $smsRejectPolicy = $sms080RejectService->getConfig(); | |
| $smsRejectText = ($smsRejectPolicy['rejectNumber'] != '' && $smsRejectPolicy['status'] == 'O' && $smsRejectPolicy['use'] == 'y') ? "수신거부 {$smsRejectPolicy['rejectNumber']}" : ''; | |
| $this->setData('sendMethods', SendMethod::cases()); | |
| $this->setData('smsRejectText', $smsRejectText); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 후LayerSendMethodSettingController.php
| 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'); | |
| $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'); | |
| } | |
| } | |
변경 전LayerSendTimeSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Origin\Enum\Crm\Message\MessageRepeatCycle; | |
| use Origin\Enum\Crm\Message\MessageSendType; | |
| $this->setData('messageSendTypes', MessageSendType::cases()); | |
| $this->setData('messageRepeatCycles', MessageRepeatCycle::cases()); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 후LayerSendTimeSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Origin\Enum\Common\WeekDay; | |
| use Origin\Enum\Crm\Message\MessageRepeatCycle; | |
| use Origin\Enum\Crm\Message\MessageSendType; | |
| $this->setData('messageSendTypes', MessageSendType::cases()); | |
| $this->setData('messageRepeatCycles', MessageRepeatCycle::cases()); | |
| $this->setData('weekDays', WeekDay::cases()); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 전LayerSmsContentSettingController.php
| namespace Bundle\Controller\Admin\Crm\MobileSend; | |
| use Origin\Service\Crm\Message\Sms080RejectService; | |
| class LayerSmsContentSettingController extends \Controller\Admin\Controller | |
| { | |
| public function index() | |
| { | |
| $this->callMenu('crm', 'messageSend', 'mobileSend'); | |
| $sms080RejectService = \App::getInstance(Sms080RejectService::class); | |
| $is080RejectAvailable = $sms080RejectService->isAvailable(); | |
| $this->setData('is080RejectAvailable', $is080RejectAvailable); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 후LayerSmsContentSettingController.php
| 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'); | |
| $sms080RejectService = \App::getInstance(Sms080RejectService::class); | |
| $is080RejectAvailable = $sms080RejectService->isAvailable(); | |
| $recipeContext = $this->resolveRecipeContext(); | |
| $isRecipeContext = $this->hasRecipeContext(); | |
| $initialMessage = $recipeContext['defaultCampaignRequest']['smsLmsRequest']['content'] ?? ''; | |
| $this->setData('is080RejectAvailable', $is080RejectAvailable); | |
| $this->setData('isRecipeContext', $isRecipeContext); | |
| $this->setData('initialMessage', $initialMessage); | |
| $this->getView()->setDefine('layout', 'layout_layer.php'); | |
| } | |
| } | |
변경 전MobileSendPsController.php
| use Framework\Debug\Exception\AlertCloseException; | |
| use Framework\Http\Request; | |
| use GuzzleHttp\Exception\RequestException; | |
| use Origin\Service\Crm\Message\MessageSendService; | |
| use Origin\Service\Crm\Message\SmsPasswordService; | |
| use Component\Crm\MobileMessage; | |
| try { | |
| $requestData = json_decode($postData['payload'], true); | |
| // 수신대상 - 엑셀업로드, 업로드된 정보로 수신자 설정 | |
| if ($requestData['targetInfo']['targetType'] === 'EXCEL') { | |
| /* @var \Bundle\Component\Crm\MobileMessage $mobileMessage*/ | |
| $mobileMessage = \App::getInstance(MobileMessage::class); | |
변경 후MobileSendPsController.php
| 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; | |
| 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); | |
변경 전RecipeContextResolverTrait.php
변경 후RecipeContextResolverTrait.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\Util\Crm; | |
| trait RecipeContextResolverTrait | |
| { | |
| protected function resolveRecipeContext(): array | |
| { | |
| $raw = (string) \Request::post()->get('recipeContext', ''); | |
| if ($raw === '') { | |
| return []; | |
| } | |
| $decoded = json_decode($raw, true); | |
| return is_array($decoded) ? $decoded : []; | |
| } | |
| /** | |
| * 레시피 컨텍스트에서 호출되었는지 여부 (데이터가 비어 있어도 파라미터가 전달되면 레시피 화면) | |
| */ | |
| protected function hasRecipeContext(): bool | |
| { | |
| return (string) \Request::post()->get('recipeContext', '') !== ''; | |
| } | |
| } | |
변경 전base_info.php
| <form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data"> | |
| <input type="hidden" name="mode" value="base_info"/> | |
| <input type="hidden" name="mallFaviconTmp" value="<?php echo $data['mallFavicon']; ?>"/> | |
| <input type="hidden" name="stampImageTmp" value="<?php echo $data['stampImage'];?>" /> | |
| <input type="hidden" name="receiptImageTmp" value="<?php echo $data['receiptImage'];?>" /> | |
| <input type="hidden" name="mallSno" value="<?php echo $mallSno;?>" /> | |
| <input type="hidden" name="mallFl" value="<?php echo $mallFl;?>" /> | |
| <div class="page-header js-affix"> | |
| <h3><?php echo end($naviMenu->location); ?> | |
| <small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small> | |
| </h3> | |
| <input type="submit" value="저장" class="btn btn-red"/> | |
| <?php if ($mallCnt > 1) { ?> | |
| <ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;"> | |
| <?php foreach ($mallList as $key => $mall) { ?> | |
| <li role="presentation" class="js-popover <?php echo $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?php echo $mall['mallName']; ?>" data-placement="top"> | |
| <a href="./base_info.php?mallSno=<?php echo $mall['sno']; ?>"> | |
| <span class="flag flag-16 flag-<?php echo $mall['domainFl']?>"></span> | |
| <span class="mall-name"><?php echo $mall['mallName']; ?></span> | |
| </a> | |
| </li> | |
| <?php } ?> | |
| <tr> | |
| <th>쇼핑몰명</th> | |
| <td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>> | |
| <input type="text" name="mallNm" value="<?php echo $data['mallNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th> | |
| <td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>"> | |
| <input type="text" name="mallNmEng" value="<?php echo $data['mallNmEng']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>상단타이틀</th> | |
| <td class="form-inline"> | |
| <input type="text" name="mallTitle" value="<?php echo $data['mallTitle']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th>파비콘</th> | |
| <td class="form-inline"> | |
| <input type="file" name="mallFavicon" <?php echo $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['mallFavicon']) === false) { | |
| <th class="require">쇼핑몰 도메인</th> | |
| <td colspan="3" class="form-inline"> | |
| <span class="font-eng">http://</span> | |
| <input type="text" name="mallDomain" value="<?php echo $data['mallDomain']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-2xl"/> | |
| <input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm"> | |
| <?php if ($mallFl == 'kr'): ?> | |
| <input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm"> | |
| <?php else: ?> | |
| <input type="button" value="도메인 연결/관리" onclick="window.location.href='<?php echo $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm"> | |
| <?php endif; ?> | |
| <div class="notice-info"> | |
| 입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br /> | |
| 실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?php echo $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th class="require">대표카테고리</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="hidden" name="mallCategory" value="<?php echo $data['mallCategory']; ?>" <?php echo $readonly . $disabled; ?>/> | |
| <span id="mallCategory"><?php echo $data['mallCategory']; ?></span> | |
| <input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?php echo $disabled; ?> class="btn btn-gray btn-sm"/> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>상호(회사명)</th> | |
| <td class="form-inline"> | |
| <input type="text" name="companyNm" value="<?php echo $data['companyNm']; ?>" class="form-control width-md"/> | |
| </td> | |
| <th>사업자등록번호</th> | |
| <td class="form-inline"> | |
| <div> | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][0]; ?>" <?php echo $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][1]; ?>" <?php echo $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][2]; ?>" <?php echo $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/> | |
| </div> | |
| <div class="notice-info"> | |
| 인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다. | |
| <tr> | |
| <th>대표자명</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ceoNm" value="<?php echo $data['ceoNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>업태</th> | |
| <td class="form-inline"> | |
| <input type="text" name="service" value="<?php echo $data['service']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| <th>종목</th> | |
| <td class="form-inline"> | |
| <input type="text" name="item" value="<?php echo $data['item']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?php echo gd_isset($checked['email']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?php echo gd_isset($checked['email']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <?php } ?> | |
| <input type="text" id="emailId" name="email[]" value="<?php echo $data['email'][0]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @ | |
| <input type="text" id="email" name="email[]" value="<?php echo $data['email'][1]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/> | |
| <div id="email_domain_y" class="display-inline-block"> | |
| <?php echo gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?> | |
| </div> | |
| <div id="email_domain_n"></div> | |
| <li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li> | |
| <li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li> | |
| <li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다. | |
| <a class="btn-link" href="<?php echo $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a> | |
| </li> | |
| </ul> | |
| </div> | |
| <th>사업장 주소</th> | |
| <td colspan="3"> | |
| <div class="form-inline mgb5"> | |
| <input type="text" name="zonecode" value="<?php echo $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/> | |
| <input type="hidden" name="zipcode" value="<?php echo $data['zipcode']; ?>"/> | |
| <span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?php echo $data['zipcode']; ?>)</span> | |
| <input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/> | |
| </div> | |
| <div class="form-inline"> | |
| <input type="text" name="address" value="<?php echo $data['address']; ?>" class="form-control width-2xl"/> | |
| <input type="text" name="addressSub" value="<?php echo $data['addressSub']; ?>" class="form-control width-2xl"/> | |
| </div> | |
| </td> | |
| </tr> | |
| <td colspan="3"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?php echo gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?php echo gd_isset($checked['unstoringFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/> | |
| <div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['unstoringInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'unstoring') { ?> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?php echo $data['unstoringInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?php echo $data['unstoringInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?php echo gd_isset($checked['returnFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/> | |
| <div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['returnInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'return') { ?> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?php echo $data['returnInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?php echo $data['returnInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?php echo $data['returnInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?php echo $data['returnInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?php echo $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?php echo $data['returnInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <tr> | |
| <th>대표전화</th> | |
| <td class="form-inline"> | |
| <input type="text" name="phone" value="<?php echo $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| <th>팩스번호</th> | |
| <td class="form-inline"> | |
| <input type="text" name="fax" value="<?php echo $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>통신판매신고번호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="onlineOrderSerial" value="<?php echo $data['onlineOrderSerial']; ?>" class="form-control width-sm"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>인감 이미지 등록</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="file" name="stampImage" <?php echo $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['stampImage']) === false) { | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?php echo gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?php echo gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?php echo gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?php echo gd_isset($checked['receiptFl']['n']); ?> />노출 안함 | |
| </label> | |
| </div> | |
| <div id="receiptFl_default" class="mgt10"> | |
| <img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점"> | |
| </div> | |
| <div id="receiptFl_upload" class="mgt10"> | |
| <input type="file" name="receiptImageFile" <?php echo $disabled; ?> class="form-control"/> | |
| <div> | |
| <?php | |
| if (empty($data['receiptImage']) === false) { | |
| echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerPhone" value="<?php echo $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </div> | |
| <div> | |
| <?php | |
| echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerSubPhone" value="<?php echo $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </div> | |
| </td> | |
| <th>팩스번호</th> | |
| echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerFax" value="<?php echo $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?php echo gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?php echo gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <input type="text" id="centerEmailId" name="centerEmail[]" value="<?php echo $data['centerEmail'][0]; ?>" class="form-control width-lg" <?php echo gd_isset($deactivated['centerEmail']); ?>/> @ | |
| <input type="text" id="centerEmail" name="centerEmail[]" value="<?php echo $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?php echo gd_isset($deactivated['centerEmail']); ?> /> | |
| <div id="centerEmail_domain_y" class="display-inline-block"> | |
| <?php echo gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?> | |
| </div> | |
| <div id="centerEmail_domain_n"></div> | |
| </td> | |
| <tr> | |
| <th>운영시간</th> | |
| <td colspan="3" class="form-inline"> | |
| <textarea name="centerHours" rows="4" class="form-control width-2xl"><?php echo $data['centerHours']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>회사소개 내용</th> | |
| <td class="form-inline"> | |
| <textarea name="company" id="editor" class="form-control width100p height400"><?php echo $data['company']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <script type="text/javascript"> | |
| <!-- | |
| var unstoringNo = <?php echo json_encode($data['unstoringInfo']) ?>; | |
| var returnNo = <?php echo json_encode($data['returnInfo']) ?>; | |
| var $mallName = '<?= $mallName ?>'; | |
| var $mallFl = '<?php echo $mallFl; ?>'; | |
| var unstoringFl = '<?= empty($data['unstoringInfo']) ?>'; | |
| var returnFl = '<?= empty($data['returnInfo']) ?>'; | |
| var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>; | |
| } | |
| }); | |
| display_toggle('unstoringFl','<?php echo gd_isset($data['unstoringFl']);?>'); | |
| display_toggle('returnFl','<?php echo gd_isset($data['returnFl']);?>'); | |
| display_toggle('robotsTxt','<?php echo gd_isset($data['robotsFl']);?>'); | |
| display_toggle('receiptFl','<?php echo gd_isset($data['receiptFl']);?>'); | |
| display_toggle_inline('email_domain', '<?php echo gd_isset($data['emailFl']);?>'); | |
| display_toggle_inline('centerEmail_domain', '<?php echo gd_isset($data['centerEmailFl']);?>'); | |
| $('.email_domain').change(function () { | |
| var val = $(this).val() == 'self' ? '' : $(this).val(); | |
| 'unstoringNo' : addressNo, | |
| 'mallName' : $mallName, | |
| 'mallFl' : $mallFl, | |
| 'unstoringFl' : '<?php echo gd_isset($data['unstoringFl']);?>', | |
| 'returnFl' : '<?php echo gd_isset($data['returnFl']);?>', | |
| 'mallSno' : '<?php echo $mallSno;?>' | |
| }; | |
| $.ajax({ | |
변경 후base_info.php
| <form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data"> | |
| <input type="hidden" name="mode" value="base_info"/> | |
| <input type="hidden" name="mallFaviconTmp" value="<?= $data['mallFavicon']; ?>"/> | |
| <input type="hidden" name="stampImageTmp" value="<?= $data['stampImage'];?>" /> | |
| <input type="hidden" name="receiptImageTmp" value="<?= $data['receiptImage'];?>" /> | |
| <input type="hidden" name="mallSno" value="<?= $mallSno;?>" /> | |
| <input type="hidden" name="mallFl" value="<?= $mallFl;?>" /> | |
| <div class="page-header js-affix"> | |
| <h3><?= end($naviMenu->location); ?> | |
| <small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small> | |
| </h3> | |
| <input type="submit" value="저장" class="btn btn-red"/> | |
| <?php if ($mallCnt > 1) { ?> | |
| <ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;"> | |
| <?php foreach ($mallList as $key => $mall) { ?> | |
| <li role="presentation" class="js-popover <?= $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?= $mall['mallName']; ?>" data-placement="top"> | |
| <a href="./base_info.php?mallSno=<?= $mall['sno']; ?>"> | |
| <span class="flag flag-16 flag-<?= $mall['domainFl']?>"></span> | |
| <span class="mall-name"><?= $mall['mallName']; ?></span> | |
| </a> | |
| </li> | |
| <?php } ?> | |
| <tr> | |
| <th>쇼핑몰명</th> | |
| <td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>> | |
| <input type="text" name="mallNm" value="<?= $data['mallNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th> | |
| <td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>"> | |
| <input type="text" name="mallNmEng" value="<?= $data['mallNmEng']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>상단타이틀</th> | |
| <td class="form-inline"> | |
| <input type="text" name="mallTitle" value="<?= $data['mallTitle']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th>파비콘</th> | |
| <td class="form-inline"> | |
| <input type="file" name="mallFavicon" <?= $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['mallFavicon']) === false) { | |
| <th class="require">쇼핑몰 도메인</th> | |
| <td colspan="3" class="form-inline"> | |
| <span class="font-eng">http://</span> | |
| <input type="text" name="mallDomain" value="<?= $data['mallDomain']; ?>" <?= $readonly . $disabled; ?> class="form-control width-2xl"/> | |
| <input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm"> | |
| <?php if ($mallFl == 'kr'): ?> | |
| <input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm"> | |
| <?php else: ?> | |
| <input type="button" value="도메인 연결/관리" onclick="window.location.href='<?= $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm"> | |
| <?php endif; ?> | |
| <div class="notice-info"> | |
| 입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br /> | |
| 실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?= $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th class="require">대표카테고리</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="hidden" name="mallCategory" value="<?= $data['mallCategory']; ?>" <?= $readonly . $disabled; ?>/> | |
| <span id="mallCategory"><?= $data['mallCategory']; ?></span> | |
| <input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?= $disabled; ?> class="btn btn-gray btn-sm"/> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>상호(회사명)</th> | |
| <td class="form-inline"> | |
| <input type="text" name="companyNm" value="<?= $data['companyNm']; ?>" class="form-control width-md"/> | |
| </td> | |
| <th>사업자등록번호</th> | |
| <td class="form-inline"> | |
| <div> | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][0]; ?>" <?= $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][1]; ?>" <?= $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][2]; ?>" <?= $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/> | |
| </div> | |
| <div class="notice-info"> | |
| 인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다. | |
| <tr> | |
| <th>대표자명</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ceoNm" value="<?= $data['ceoNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>업태</th> | |
| <td class="form-inline"> | |
| <input type="text" name="service" value="<?= $data['service']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| <th>종목</th> | |
| <td class="form-inline"> | |
| <input type="text" name="item" value="<?= $data['item']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?= gd_isset($checked['email']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?= gd_isset($checked['email']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <?php } ?> | |
| <input type="text" id="emailId" name="email[]" value="<?= $data['email'][0]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @ | |
| <input type="text" id="email" name="email[]" value="<?= $data['email'][1]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/> | |
| <div id="email_domain_y" class="display-inline-block"> | |
| <?= gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?> | |
| </div> | |
| <div id="email_domain_n"></div> | |
| <li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li> | |
| <li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li> | |
| <li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다. | |
| <a class="btn-link" href="<?= $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a> | |
| </li> | |
| </ul> | |
| </div> | |
| <th>사업장 주소</th> | |
| <td colspan="3"> | |
| <div class="form-inline mgb5"> | |
| <input type="text" name="zonecode" value="<?= $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/> | |
| <input type="hidden" name="zipcode" value="<?= $data['zipcode']; ?>"/> | |
| <span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?= $data['zipcode']; ?>)</span> | |
| <input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/> | |
| </div> | |
| <div class="form-inline"> | |
| <input type="text" name="address" value="<?= $data['address']; ?>" class="form-control width-2xl"/> | |
| <input type="text" name="addressSub" value="<?= $data['addressSub']; ?>" class="form-control width-2xl"/> | |
| </div> | |
| </td> | |
| </tr> | |
| <td colspan="3"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?= gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?= gd_isset($checked['unstoringFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/> | |
| <div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['unstoringInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'unstoring') { ?> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?= $data['unstoringInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?= $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?= $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?= $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?= $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?= $data['unstoringInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?= gd_isset($checked['returnFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/> | |
| <div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['returnInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'return') { ?> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?= $data['returnInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?= $data['returnInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?= $data['returnInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?= $data['returnInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?= $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?= $data['returnInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <tr> | |
| <th>대표전화</th> | |
| <td class="form-inline"> | |
| <input type="text" name="phone" value="<?= $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| <th>팩스번호</th> | |
| <td class="form-inline"> | |
| <input type="text" name="fax" value="<?= $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>통신판매신고번호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="onlineOrderSerial" value="<?= $data['onlineOrderSerial']; ?>" class="form-control width-sm"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>전자상거래업자 부호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ecommerceBusinessCode" value="<?= $data['ecommerceBusinessCode']; ?>" <?= $readonly . $disabled; ?> maxlength="30" class="form-control width-sm"/> | |
| <div class="notice-info"> | |
| 전자상거래업자 부호는 관세청 UNI-PASS 에서 신청/발급받을 수 있습니다. | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>인감 이미지 등록</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="file" name="stampImage" <?= $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['stampImage']) === false) { | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?= gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?= gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?= gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?= gd_isset($checked['receiptFl']['n']); ?> />노출 안함 | |
| </label> | |
| </div> | |
| <div id="receiptFl_default" class="mgt10"> | |
| <img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점"> | |
| </div> | |
| <div id="receiptFl_upload" class="mgt10"> | |
| <input type="file" name="receiptImageFile" <?= $disabled; ?> class="form-control"/> | |
| <div> | |
| <?php | |
| if (empty($data['receiptImage']) === false) { | |
| echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerPhone" value="<?= $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </div> | |
| <div> | |
| <?php | |
| echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerSubPhone" value="<?= $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </div> | |
| </td> | |
| <th>팩스번호</th> | |
| echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerFax" value="<?= $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?= gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?= gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <input type="text" id="centerEmailId" name="centerEmail[]" value="<?= $data['centerEmail'][0]; ?>" class="form-control width-lg" <?= gd_isset($deactivated['centerEmail']); ?>/> @ | |
| <input type="text" id="centerEmail" name="centerEmail[]" value="<?= $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?= gd_isset($deactivated['centerEmail']); ?> /> | |
| <div id="centerEmail_domain_y" class="display-inline-block"> | |
| <?= gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?> | |
| </div> | |
| <div id="centerEmail_domain_n"></div> | |
| </td> | |
| <tr> | |
| <th>운영시간</th> | |
| <td colspan="3" class="form-inline"> | |
| <textarea name="centerHours" rows="4" class="form-control width-2xl"><?= $data['centerHours']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>회사소개 내용</th> | |
| <td class="form-inline"> | |
| <textarea name="company" id="editor" class="form-control width100p height400"><?= $data['company']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <script type="text/javascript"> | |
| <!-- | |
| var unstoringNo = <?= json_encode($data['unstoringInfo']) ?>; | |
| var returnNo = <?= json_encode($data['returnInfo']) ?>; | |
| var $mallName = '<?= $mallName ?>'; | |
| var $mallFl = '<?= $mallFl; ?>'; | |
| var unstoringFl = '<?= empty($data['unstoringInfo']) ?>'; | |
| var returnFl = '<?= empty($data['returnInfo']) ?>'; | |
| var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>; | |
| } | |
| }); | |
| display_toggle('unstoringFl','<?= gd_isset($data['unstoringFl']);?>'); | |
| display_toggle('returnFl','<?= gd_isset($data['returnFl']);?>'); | |
| display_toggle('robotsTxt','<?= gd_isset($data['robotsFl']);?>'); | |
| display_toggle('receiptFl','<?= gd_isset($data['receiptFl']);?>'); | |
| display_toggle_inline('email_domain', '<?= gd_isset($data['emailFl']);?>'); | |
| display_toggle_inline('centerEmail_domain', '<?= gd_isset($data['centerEmailFl']);?>'); | |
| $('.email_domain').change(function () { | |
| var val = $(this).val() == 'self' ? '' : $(this).val(); | |
| 'unstoringNo' : addressNo, | |
| 'mallName' : $mallName, | |
| 'mallFl' : $mallFl, | |
| 'unstoringFl' : '<?= gd_isset($data['unstoringFl']);?>', | |
| 'returnFl' : '<?= gd_isset($data['returnFl']);?>', | |
| 'mallSno' : '<?= $mallSno;?>' | |
| }; | |
| $.ajax({ | |
변경 전BuyerInform.php
| // 저장할 필드 | |
| //@formatter:off | |
| $arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',]; | |
| //@formatter:on | |
| // 데이터 조합 | |
변경 후BuyerInform.php
| // 저장할 필드 | |
| //@formatter:off | |
| $arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'ecommerceBusinessCode', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',]; | |
| //@formatter:on | |
| // 데이터 조합 | |
변경 전DBTableField.php
| ['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명 | |
| ['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지 | |
| ['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호 | |
| ['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태 | |
| ['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목 | |
| ['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자 | |
변경 후DBTableField.php
| ['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명 | |
| ['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지 | |
| ['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호 | |
| ['val' => 'ecommerceBusinessCode', 'typ' => 's', 'def' => null, 'name' => '전자상거래업체부호'], // 전자상거래업체부호 | |
| ['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태 | |
| ['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목 | |
| ['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자 | |
변경 전Mall.php
| 'item', | |
| 'email', | |
| 'stampImage', | |
| ]; | |
| /** 설정 공통 사용 **/ | |
| const GLOBAL_MALL_SHARE = [ | |
변경 후Mall.php
| 'item', | |
| 'email', | |
| 'stampImage', | |
| 'ecommerceBusinessCode', | |
| ]; | |
| /** 설정 공통 사용 **/ | |
| const GLOBAL_MALL_SHARE = [ | |
변경 전Policy.php
| 'phone', | |
| 'fax', | |
| 'onlineOrderSerial', | |
| 'unstoringNo', | |
| 'unstoringZonecode', | |
| 'unstoringZipcode', | |
변경 후Policy.php
| 'phone', | |
| 'fax', | |
| 'onlineOrderSerial', | |
| 'ecommerceBusinessCode', | |
| 'unstoringNo', | |
| 'unstoringZonecode', | |
| 'unstoringZipcode', | |
변경 전GoodsAdmin.php
| // 상품이미지 > obs 카프카 컨슈머 처리 start (상품복사) | |
| if($arrData['applyGoodsCopy'] && gd_in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) { | |
| $copiedGoodsData = ['goodsNo' => $arrData['applyNo'], 'newGoodsNo' => $arrData['goodsNo'], 'convertedType' => 'copy']; | |
| $kafka = new ProducerUtils(); | |
| $result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($copiedGoodsData, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, $copiedGoodsData]); | |
| if ($result['result'] == 'success') { | |
| // originGoodsNo, newGoodsNo obsConvertFl = N 업데이트 필요 | |
| $goodsObsConvertStatus->updateObsConvertFl($arrData['applyNo']); | |
| $goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo']); | |
| } | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| if (!empty($goodsImageMigrationLog)) { | |
| $goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제 | |
| $kafka = new ProducerUtils(); | |
| $result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($goodsImageMigrationLog, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, $goodsImageMigrationLog]); | |
| if ($result['result'] == 'success') { | |
| // goodsNo obsConvertFl = N 업데이트 필요 | |
| $goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo'], $goodsImageMigrationLog); | |
| } | |
| } | |
| } | |
| // 상품아이콘 > obs 카프카 컨슈머 처리 end | |
| // URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함) | |
| if (gd_in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) { | |
| if ($arrData['applyGoodsCopy']) { | |
| $goodsObsConvertStatus->setObsConvertFlagForUrlImages($arrData['applyNo']); | |
| if ($extraArrData['callBy'] == OPEN_API) { | |
| $goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제 | |
| $kafka = new ProducerUtils(); | |
| $result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE_BY_OPEN_API, $kafka->makeData($goodsImageMigrationLog, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, 'openAPI', $goodsImageMigrationLog]); | |
| if ($result['result'] == 'success') { | |
| // goodsNo obsConvertFl = N 업데이트 필요 | |
| $goodsObsConvertStatus->updateObsConvertFl($this->goodsNo, $goodsImageMigrationLog); | |
| } | |
| } | |
| // URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함) | |
| $goodsObsConvertStatus->setObsConvertFlagForUrlImages($this->goodsNo); | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| return $fbImageInfo; | |
| } | |
| /** | |
| * 상품 복사 | |
| * | |
| $addLogData = $goodsNo . ' -> ' . $newGoodsNo . ' 상품 복사' . chr(10); | |
| LogHandler::wholeLog('goods', null, 'copy', $newGoodsNo, $data['goodsNm'], $addLogData); | |
| // 상품이미지 > obs 카프카 컨슈머 처리 start | |
| if(gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
| $copiedGoodsData = ['goodsNo' => $goodsNo, 'newGoodsNo' => $newGoodsNo, 'convertedType' => 'copy']; | |
| $kafka = new ProducerUtils(); | |
| $result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($copiedGoodsData, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, $copiedGoodsData]); | |
| if ($result['result'] == 'success') { | |
| // originGoodsNo, newGoodsNo obsConvertFl = N 업데이트 필요 | |
| $goodsObsConvertStatus = \App::load('\\Component\\Goods\\GoodsObsConvertStatus'); | |
| $goodsObsConvertStatus->updateObsConvertFl($goodsNo); | |
| $goodsObsConvertStatus->updateObsConvertFl($newGoodsNo); | |
| } | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| // URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함) | |
| if (gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
| $goodsObsConvertStatusForUrl = \App::load('\\Component\\Goods\\GoodsObsConvertStatus'); | |
| $goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($goodsNo); | |
| $goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($newGoodsNo); | |
| } | |
| // --- 이미지 복사 처리 | |
| if($data['imageStorage'] !='url') { | |
| if ($data['imageStorage'] == 'obs') { | |
| $this->db->commit(); | |
| // 에디터 이미지 복사 토픽 발행 | |
| $kafka = new ProducerUtils(); | |
| $contentsInfo = ['contentsKey' => $newGoodsNo, 'tableName' => DB_GOODS, 'orgContentsKey' => $goodsNo, 'orgTableName' => DB_GOODS]; | |
| $result = $kafka->send($kafka::TOPIC_COPIED_EDITOR_IMAGE, $kafka->makeData($contentsInfo, 'cpei'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, $contentsInfo]); | |
| return $newGoodsNo; | |
| } catch (\Exception $e) { | |
변경 후GoodsAdmin.php
| // 상품이미지 > obs 카프카 컨슈머 처리 start (상품복사) | |
| if($arrData['applyGoodsCopy'] && gd_in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) { | |
| // 신규 상품을 변환 대기(obsConvertFl='n')로 표시 | |
| $goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo']); | |
| $copiedGoodsData = ['goodsNo' => $arrData['applyNo'], 'newGoodsNo' => $arrData['goodsNo'], 'convertedType' => 'copy']; | |
| $this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $copiedGoodsData, $arrData['goodsNo'], '상품복사 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)'); | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| if (!empty($goodsImageMigrationLog)) { | |
| $goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제 | |
| // 상품 이미지를 변환 대기(obsConvertFl='n')로 표시 | |
| $goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo'], $goodsImageMigrationLog); | |
| $this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $goodsImageMigrationLog, $arrData['goodsNo'], '상품이미지 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)'); | |
| } | |
| } | |
| // 상품아이콘 > obs 카프카 컨슈머 처리 end | |
| // URL 이미지는 변환 대상이 아니므로 'y'로 고정 | |
| if (gd_in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) { | |
| if ($arrData['applyGoodsCopy']) { | |
| $goodsObsConvertStatus->setObsConvertFlagForUrlImages($arrData['applyNo']); | |
| if ($extraArrData['callBy'] == OPEN_API) { | |
| $goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제 | |
| // 상품 이미지를 변환 대기(obsConvertFl='n')로 표시 | |
| $goodsObsConvertStatus->updateObsConvertFl($this->goodsNo, $goodsImageMigrationLog); | |
| $this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE_BY_OPEN_API, $goodsImageMigrationLog, $this->goodsNo, '상품이미지 OBS 토픽 발행 실패(openAPI) - obsConvertFl 해제(fail-open)', ['openAPI']); | |
| } | |
| // URL 이미지는 변환 대상이 아니므로 'y'로 고정 | |
| $goodsObsConvertStatus->setObsConvertFlagForUrlImages($this->goodsNo); | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| return $fbImageInfo; | |
| } | |
| /** | |
| * OBS 이미지 변환 토픽 발행 + 발행 실패 시 fail-open 처리 | |
| * | |
| * 발행에 실패(예외 발생 또는 비성공 응답)하면 obsConvertFl을 'y'로 되돌려 | |
| * 이미지가 '변환 대기(편집 잠금)' 상태로 고착되는 것을 방지한다. | |
| * 잠금(updateObsConvertFl) 자체는 발행 대상/시점이 달라 호출부에서 수행한다. | |
| * | |
| * @param \Component\Goods\GoodsObsConvertStatus $goodsObsConvertStatus 상태 처리 컴포넌트 | |
| * @param string $topic 발행 토픽 | |
| * @param array $data 발행 데이터(makeData 입력) | |
| * @param mixed $unlockGoodsNo 발행 실패 시 잠금 해제할 상품번호 | |
| * @param string $failMessage 발행 실패 시 남길 로그 메시지 | |
| * @param array $infoLogContext info 로그에 추가로 남길 항목(예: ['openAPI']) | |
| * @return void | |
| */ | |
| private function publishObsConvertTopic($goodsObsConvertStatus, string $topic, array $data, $unlockGoodsNo, string $failMessage, array $infoLogContext = []): void | |
| { | |
| $obsPublishOk = false; | |
| $obsPublishError = ''; | |
| try { | |
| $kafka = new ProducerUtils(); | |
| $result = $kafka->send($topic, $kafka->makeData($data, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge([$result], $infoLogContext, [$data])); | |
| $obsPublishOk = (isset($result['result']) && $result['result'] === 'success'); | |
| } catch (\Throwable $obsPublishException) { | |
| $obsPublishError = $obsPublishException->getMessage(); | |
| } | |
| if (!$obsPublishOk) { | |
| // 발행 실패 → obsConvertFl 'y'로 재변환 | |
| // 미변환 시, 이미지 변경 불가능 상태로 고착화됨 | |
| $goodsObsConvertStatus->unlockObsConvertFl($unlockGoodsNo); | |
| \Logger::channel('kafka')->warning($failMessage, [$unlockGoodsNo, $obsPublishError]); | |
| } | |
| } | |
| /** | |
| * 상품 복사 | |
| * | |
| $addLogData = $goodsNo . ' -> ' . $newGoodsNo . ' 상품 복사' . chr(10); | |
| LogHandler::wholeLog('goods', null, 'copy', $newGoodsNo, $data['goodsNm'], $addLogData); | |
| // --- 이미지 복사 처리 | |
| if($data['imageStorage'] !='url') { | |
| if ($data['imageStorage'] == 'obs') { | |
| $this->db->commit(); | |
| try { | |
| if(gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
| // 신규 상품을 변환 대기(obsConvertFl='n')로 표시 | |
| $goodsObsConvertStatus = \App::load('\\Component\\Goods\\GoodsObsConvertStatus'); | |
| $goodsObsConvertStatus->updateObsConvertFl($newGoodsNo); | |
| $copiedGoodsData = ['goodsNo' => $goodsNo, 'newGoodsNo' => $newGoodsNo, 'convertedType' => 'copy']; | |
| $this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $copiedGoodsData, $newGoodsNo, '상품복사 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)'); | |
| } | |
| // 상품이미지 > obs 카프카 컨슈머 처리 end | |
| // URL 이미지는 변환 대상이 아니므로 'y'로 고정 | |
| if (gd_in_array($data['imageStorage'], self::DEFAULT_STORAGE)) { | |
| $goodsObsConvertStatusForUrl = \App::load('\\Component\\Goods\\GoodsObsConvertStatus'); | |
| $goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($newGoodsNo); | |
| } | |
| // 에디터 이미지 복사 토픽 발행 | |
| $kafka = new ProducerUtils(); | |
| $contentsInfo = ['contentsKey' => $newGoodsNo, 'tableName' => DB_GOODS, 'orgContentsKey' => $goodsNo, 'orgTableName' => DB_GOODS]; | |
| $result = $kafka->send($kafka::TOPIC_COPIED_EDITOR_IMAGE, $kafka->makeData($contentsInfo, 'cpei'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', [$result, $contentsInfo]); | |
| } catch (\Throwable $postCommitException) { | |
| // 이미지 로컬 복사는 성공. 토픽 처리 실패만 기록하고 넘어간다. | |
| \Logger::channel('kafka')->warning('상품복사 commit 이후 부가처리 실패', [$postCommitException->getMessage(), $newGoodsNo]); | |
| } | |
| return $newGoodsNo; | |
| } catch (\Exception $e) { | |
변경 전GoodsObsConvertStatus.php
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| $this->db->bind_query($strSQL, $arrBind); | |
| } | |
| } | |
변경 후GoodsObsConvertStatus.php
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| $this->db->bind_query($strSQL, $arrBind); | |
| } | |
| /** | |
| * 상품번호를 기준으로 상품 이미지의 변환 대기 표시(obsConvertFl='n')를 풀어줌('y'). | |
| */ | |
| public function unlockObsConvertFl(int $goodsNo): void | |
| { | |
| // 상품이미지 | |
| $arrBind = []; | |
| $strSQL = 'UPDATE ' . DB_GOODS_IMAGE . ' SET obsConvertFl = ? WHERE goodsNo = ? AND imageName NOT LIKE "http%" '; | |
| $this->db->bind_param_push($arrBind, 's', 'y'); | |
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| $this->db->bind_query($strSQL, $arrBind); | |
| // 상품옵션이미지 | |
| $arrBind = []; | |
| $strSQL = 'UPDATE ' . DB_GOODS_OPTION_ICON . ' SET obsConvertFl = ? WHERE goodsNo = ? AND goodsImage NOT LIKE "http%" '; | |
| $this->db->bind_param_push($arrBind, 's', 'y'); | |
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| $this->db->bind_query($strSQL, $arrBind); | |
| } | |
| } | |
변경 전OrderPsController.php
| $postValue = Request::post()->toArray(); | |
| try { | |
| //회원의 장바구니 로드 | |
| $cart = new CartAdmin($postValue['memNo'], true); | |
| $cartSaveInfo = array(); | |
| foreach($postValue['cartSno'] as $key => $cartIdx){ | |
| $cartInfo = array(); | |
| $cartInfo = $cart->getCartGoodsData($cartIdx); | |
| if(gd_count($cartInfo) > 0){ | |
| } | |
| } | |
| } | |
| } | |
| //order write cart 에 저장 | |
| $cart = null; | |
| unset($cart); | |
| if(gd_count($cartSaveInfo) > 0){ | |
| $cart = new CartAdmin($postValue['memNo']); | |
| $resultCartSno = $cart->saveInfoMemberCartAdmin($cartSaveInfo); | |
변경 후OrderPsController.php
| $postValue = Request::post()->toArray(); | |
| try { | |
| $cartSaveInfo = array(); | |
| foreach($postValue['cartSno'] as $key => $cartIdx){ | |
| $cart = new CartAdmin($postValue['memNo'], true); | |
| $cartInfo = array(); | |
| $cartInfo = $cart->getCartGoodsData($cartIdx); | |
| if(gd_count($cartInfo) > 0){ | |
| } | |
| } | |
| } | |
| $cart = null; | |
| unset($cart); | |
| } | |
| //order write cart 에 저장 | |
| if(gd_count($cartSaveInfo) > 0){ | |
| $cart = new CartAdmin($postValue['memNo']); | |
| $resultCartSno = $cart->saveInfoMemberCartAdmin($cartSaveInfo); | |
변경 전PopupRegisterController.php
| $skinDesign->setSkin(Globals::get('gSkin.frontSkinLive')); | |
| $getPopupSkin = $skinDesign->getDirDesignPageInfo('popup', true); | |
| //--- SkinDesign 정의 (모바일 사용 스킨 기준) | |
| $skinDesign = new SkinDesign('mobile'); | |
| $skinDesign->setSkin(Globals::get('gSkin.mobileSkinLive')); | |
| $getMobilePopupSkin = $skinDesign->getDirDesignPageInfo('popup', true); | |
| /*$skinDesign->setSkin(Globals::get('gSkin.mobileSkinLive')); | |
| $getMobilePopupSkin = $skinDesign->getDirDesignPageInfo('popup', true);*/ | |
| //--- DesignPopup 정의 | |
| $designPopup = new DesignPopup(); | |
변경 후PopupRegisterController.php
| $skinDesign->setSkin(Globals::get('gSkin.frontSkinLive')); | |
| $getPopupSkin = $skinDesign->getDirDesignPageInfo('popup', true); | |
| //--- SkinDesign 정의 (모바일샵 사용 시에만 — 반응형/PC전용은 mobileSkinLive 공백이라 스킵) | |
| $getMobilePopupSkin = []; | |
| if (!empty(Globals::get('gSkin.mobileSkinLive'))) { | |
| $skinDesign = new SkinDesign('mobile'); | |
| $skinDesign->setSkin(Globals::get('gSkin.mobileSkinLive')); | |
| $getMobilePopupSkin = $skinDesign->getDirDesignPageInfo('popup', true); | |
| } | |
| //--- DesignPopup 정의 | |
| $designPopup = new DesignPopup(); | |
변경 전Cart.php
| return false; | |
| } | |
| $arrBind = []; | |
| foreach ($getData as $cartSno) { | |
| // 장바구니 삭제 전 쿠폰 장바구니사용 상태 되돌리기 | |
변경 후Cart.php
| return false; | |
| } | |
| // [데드락 회피] sno IN 다건 삭제 시 PK 잠금 순서를 오름차순으로 통일 | |
| // 삭제 대상 집합은 동일하며(동작 불변), 동시 트랜잭션 간 PK 획득 순서만 일치시켜 교착 순환을 제거 | |
| sort($getData, SORT_NUMERIC); | |
| $arrBind = []; | |
| foreach ($getData as $cartSno) { | |
| // 장바구니 삭제 전 쿠폰 장바구니사용 상태 되돌리기 | |
변경 전MailLog.php
| protected $arrWhere; | |
| /** @var int es_mailLog sno */ | |
| private $_sno; | |
| /** @var SimpleStorage */ | |
| private $requestStorage; | |
| $arrBind = $this->db->get_binding(DBTableField::tableMailSendList(), $bindArray, 'insert'); | |
| $this->db->set_insert_db(DB_MAIL_SEND_LIST, $arrBind['param'], $arrBind['bind'], 'y'); | |
| } | |
| /** | |
| */ | |
| public function updateSendList($email) | |
| { | |
| $this->db->bind_param_push($arrBind, 's', $this->_sno); | |
| $this->db->bind_param_push($arrBind, 's', $email); | |
| $this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'mailLogSno = ? AND receiverEmail = ? AND acceptCheckFl = \'n\' LIMIT 1', $arrBind); | |
변경 후MailLog.php
| protected $arrWhere; | |
| /** @var int es_mailLog sno */ | |
| private $_sno; | |
| /** @var int|null 직전 insertMailSendList 로 INSERT 한 es_mailSendList 의 PK(sno). updateSendList PK 단건 갱신용 */ | |
| private $sendListSno = null; | |
| /** @var SimpleStorage */ | |
| private $requestStorage; | |
| $arrBind = $this->db->get_binding(DBTableField::tableMailSendList(), $bindArray, 'insert'); | |
| $this->db->set_insert_db(DB_MAIL_SEND_LIST, $arrBind['param'], $arrBind['bind'], 'y'); | |
| // 방금 INSERT 한 발송내역 PK 를 보관 → updateSendList 에서 PK 단건 갱신(데드락 회피)에 사용 | |
| $this->sendListSno = (int) $this->db->insert_id(); | |
| } | |
| /** | |
| */ | |
| public function updateSendList($email) | |
| { | |
| // [데드락 회피] 직전 insertMailSendList 로 확보한 PK(sno)가 있으면 PK 단건 UPDATE | |
| // 보조인덱스(ix_es_mailSendList_05) 스캔을 제거해 잠금 순서를 PRIMARY 기준으로 통일 | |
| // acceptCheckFl='n' 가드를 유지해 멱등성을 보장(기존 동작과 동일하게 미접수 건만 갱신) | |
| // 사용 후 즉시 초기화하여, INSERT 가 선행되지 않은 호출은 아래 기존 로직으로 처리 | |
| if (!empty($this->sendListSno)) { | |
| $sendListSno = $this->sendListSno; | |
| $this->sendListSno = null; | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 'i', $sendListSno); | |
| $this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'sno = ? AND acceptCheckFl = \'n\'', $arrBind); | |
| return; | |
| } | |
| // (fallback) PK 미확보 호출 경로 — 기존 동작 100% 유지 | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 's', $this->_sno); | |
| $this->db->bind_param_push($arrBind, 's', $email); | |
| $this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'mailLogSno = ? AND receiverEmail = ? AND acceptCheckFl = \'n\' LIMIT 1', $arrBind); | |
변경 전CartPsController.php
| } | |
| // 처리별 이동 경로 | |
| if (gd_isset($postValue['cartMode']) == 'd') { | |
| $goodsList = gd_implode(',', $addedGoods); | |
| $goodsListQuote = gd_implode(',', $addedGoodsQuote); | |
| //장바구니에서 보이지 않도록 조치 시작 | |
| $this->db = \App::load('DB'); | |
| $where = 'sno IN (\''.str_replace(',', '\',\'', $goodsList.'\')'); | |
| $arrUpdate['directCart'] = 'y'; | |
| $arrBind = $this->db->get_binding(DBTableField::tableCart(), $arrUpdate, 'update', gd_array_keys($arrUpdate)); | |
| $this->db->set_update_db(DB_CART, $arrBind['param'], $where, $arrBind['bind']); | |
| //장바구니에서 보이지 않도록 조치 종료 | |
| $goodsListQuote = '['.$goodsListQuote.']'; | |
변경 후CartPsController.php
| } | |
| // 처리별 이동 경로 | |
| if (gd_isset($postValue['cartMode']) == 'd') { | |
| $goodsListQuote = implode(',', $addedGoodsQuote); | |
| //장바구니에서 보이지 않도록 조치 시작 | |
| $this->db = \App::load('DB'); | |
| // [데드락 회피] UPDATE WHERE 의 sno IN 잠금 순서만 오름차순 통일(대상 행 동일) | |
| // URL(cartIdx) 노출 순서에는 영향을 주지 않도록 정렬 복사본을 별도 생성 | |
| $sortedAddedGoods = $addedGoods; | |
| sort($sortedAddedGoods, SORT_NUMERIC); | |
| if (!empty($sortedAddedGoods)) { | |
| $arrUpdate['directCart'] = 'y'; | |
| $arrBind = $this->db->get_binding(DBTableField::tableCart(), $arrUpdate, 'update', gd_array_keys($arrUpdate)); | |
| // sno IN 값은 바인딩으로 처리(SQL 인젝션 방지) | |
| $where = 'sno IN (' . implode(', ', array_fill(0, count($sortedAddedGoods), '?')) . ')'; | |
| foreach ($sortedAddedGoods as $sno) { | |
| $this->db->bind_param_push($arrBind['bind'], 'i', $sno); | |
| } | |
| $this->db->set_update_db(DB_CART, $arrBind['param'], $where, $arrBind['bind']); | |
| } | |
| //장바구니에서 보이지 않도록 조치 종료 | |
| $goodsListQuote = '['.$goodsListQuote.']'; | |
변경 전Board.php
| } else { | |
| $extraData['arrGoodsNo'][] = $extraData['goodsNoText']; | |
| } | |
| foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) { | |
| $extraData['arrGoodsData'][$key] = $goods->getGoodsInfo($goodsNo); | |
| } | |
| } | |
| if ($extraData['orderGoodsNoText']) { | |
| $extraData['arrOrderGoodsNo'][] = $extraData['orderGoodsNoText']; | |
| } | |
| $order = new Order(); | |
| foreach ($extraData['arrOrderGoodsNo'] as $key => $orderGoodsNo) { | |
| $isAddGoods = substr($orderGoodsNo, 0, 1) == 'A' ? true : false; | |
| if ($isAddGoods) { | |
| $orderGoodsNo = substr($orderGoodsNo, 1); | |
| } | |
| $orderGoodsData = $order->getOrderGoods(null, $orderGoodsNo, null, null, null)[0]; | |
| $extraData['arrOrderGoodsData'][$key] = $orderGoodsData; | |
| $_arrOrderGoodsData = $extraData['arrOrderGoodsData'][$key]; | |
| $goodsNo = $_arrOrderGoodsData['goodsNo']; | |
| $extraData['arrOrderGoodsData'][$key]['goodsNo'] = $orderGoodsData['parentGoodsNo']; | |
| $extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($addGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']); | |
| } else { | |
| $goodsImage = $goods->getGoodsImage($goodsNo, 'main'); | |
| $goodsInfo = $goods->getGoodsInfo($goodsNo); | |
| $goodsImageSrc = SkinUtils::imageViewStorageConfig($goodsImage[0]['goodsImageStorage'] == 'obs' ? $goodsImage[0]['imageUrl'] : $goodsImage[0]['imageName'], $goodsInfo['imagePath'], $goodsInfo['imageStorage'], 100, 'goods')[0]; | |
| $extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($_arrOrderGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']); | |
| } | |
변경 후Board.php
| } else { | |
| $extraData['arrGoodsNo'][] = $extraData['goodsNoText']; | |
| } | |
| // N+1 쿼리 패턴 개선: 연관상품 개별 getGoodsInfo() 반복 -> IN절 배치 1회 | |
| // (user/module Goods 오버라이드가 배치 메서드 미상속 시 fatal 방지 가드) | |
| if (method_exists($goods, 'getGoodsInfoByNos')) { | |
| $batchGoodsData = $goods->getGoodsInfoByNos($extraData['arrGoodsNo']); | |
| foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) { | |
| $extraData['arrGoodsData'][$key] = $batchGoodsData[$goodsNo] ?? []; | |
| } | |
| } else { | |
| foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) { | |
| $extraData['arrGoodsData'][$key] = $goods->getGoodsInfo($goodsNo); | |
| } | |
| } | |
| } | |
| if ($extraData['orderGoodsNoText']) { | |
| $extraData['arrOrderGoodsNo'][] = $extraData['orderGoodsNoText']; | |
| } | |
| $order = new Order(); | |
| // N+1 쿼리 패턴 개선: 주문상품/이미지/정보 개별 반복 조회 -> 루프 진입 전 IN절 배치 선조회 | |
| // add-goods 의 getDataAddGoods 만 per-item 유지. user/module Goods 미상속 시 기존 per-item 폴백 | |
| $useBatch = method_exists($goods, 'getGoodsInfoByNos'); | |
| $orderGoodsMap = $goodsInfoMap = $goodsImageMap = []; | |
| if ($useBatch) { | |
| $snoList = []; | |
| foreach ($extraData['arrOrderGoodsNo'] as $orderGoodsNo) { | |
| $snoList[] = (substr($orderGoodsNo, 0, 1) == 'A') ? substr($orderGoodsNo, 1) : $orderGoodsNo; | |
| } | |
| $orderGoodsList = $order->getOrderGoods(null, $snoList, null, null, null); | |
| if (is_array($orderGoodsList)) { | |
| foreach ($orderGoodsList as $orderGoodsRow) { | |
| $orderGoodsMap[$orderGoodsRow['sno']] = $orderGoodsRow; | |
| } | |
| } | |
| $normalGoodsNos = []; | |
| foreach ($extraData['arrOrderGoodsNo'] as $orderGoodsNo) { | |
| if (substr($orderGoodsNo, 0, 1) == 'A') { | |
| continue; | |
| } | |
| if (isset($orderGoodsMap[$orderGoodsNo]['goodsNo'])) { | |
| $normalGoodsNos[] = $orderGoodsMap[$orderGoodsNo]['goodsNo']; | |
| } | |
| } | |
| $goodsInfoMap = $goods->getGoodsInfoByNos($normalGoodsNos); | |
| $goodsImageMap = $goods->getGoodsImageByNos($normalGoodsNos, 'main'); | |
| } | |
| foreach ($extraData['arrOrderGoodsNo'] as $key => $orderGoodsNo) { | |
| $isAddGoods = substr($orderGoodsNo, 0, 1) == 'A' ? true : false; | |
| if ($isAddGoods) { | |
| $orderGoodsNo = substr($orderGoodsNo, 1); | |
| } | |
| $orderGoodsData = $useBatch | |
| ? ($orderGoodsMap[$orderGoodsNo] ?? []) | |
| : ($order->getOrderGoods(null, $orderGoodsNo, null, null, null)[0] ?? []); | |
| $extraData['arrOrderGoodsData'][$key] = $orderGoodsData; | |
| $_arrOrderGoodsData = $extraData['arrOrderGoodsData'][$key]; | |
| $goodsNo = $_arrOrderGoodsData['goodsNo']; | |
| $extraData['arrOrderGoodsData'][$key]['goodsNo'] = $orderGoodsData['parentGoodsNo']; | |
| $extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($addGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']); | |
| } else { | |
| $goodsImage = $useBatch ? ($goodsImageMap[$goodsNo] ?? false) : $goods->getGoodsImage($goodsNo, 'main'); | |
| $goodsInfo = $useBatch ? ($goodsInfoMap[$goodsNo] ?? []) : $goods->getGoodsInfo($goodsNo); | |
| $goodsImageSrc = SkinUtils::imageViewStorageConfig($goodsImage[0]['goodsImageStorage'] == 'obs' ? $goodsImage[0]['imageUrl'] : $goodsImage[0]['imageName'], $goodsInfo['imagePath'], $goodsInfo['imageStorage'], 100, 'goods')[0]; | |
| $extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($_arrOrderGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']); | |
| } | |
변경 전Goods.php
| $this->db->set_insert_db(DB_LOG_REGULAR_GOODS, $arrBind['param'], $arrBind['bind'], 'y'); | |
| } | |
| } | |
| } | |
변경 후Goods.php
| $this->db->set_insert_db(DB_LOG_REGULAR_GOODS, $arrBind['param'], $arrBind['bind'], 'y'); | |
| } | |
| } | |
| /** | |
| * 상품정보 배치 조회 — getGoodsInfo() 의 IN절 배치판 (게시판 상세보기 N+1 쿼리 패턴 개선) | |
| * | |
| * 단건 getGoodsInfo() 를 goodsNo 건수만큼 반복 호출하던 구간을 IN 절 1회 조회로 대체한다. | |
| * | |
| * 게시판 상세보기 배치 전용 내부 헬퍼. 호출부(Board::getExtraData)가 method_exists 가드로 미구현(튜닝 fork) 환경을 단건 경로로 폴백하므로 외부 튜닝 확장점이 아니다. 시그니처 안정 유지를 권장한다. | |
| * | |
| * @param array $arrGoodsNo 상품번호 배열 | |
| * @param string|null $goodsField 출력 필드 (기본 null = 단건 getGoodsInfo 와 동일 전체 조회). goodsNo 컬럼은 결과 맵의 키이므로 부분 필드 지정 시에도 반드시 포함할 것 | |
| * @return array goodsNo 를 키로 하는 상품정보 맵 [goodsNo => row] | |
| */ | |
| public function getGoodsInfoByNos(array $arrGoodsNo, $goodsField = null): array | |
| { | |
| $arrGoodsNo = array_values(array_unique(array_filter($arrGoodsNo))); | |
| if (empty($arrGoodsNo)) { | |
| return []; | |
| } | |
| $arrBind = null; | |
| $bindQuery = []; | |
| foreach ($arrGoodsNo as $goodsNo) { | |
| $bindQuery[] = '?'; | |
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| } | |
| $inClause = ' g.goodsNo IN (' . implode(',', $bindQuery) . ') '; | |
| if ($this->db->strWhere) { | |
| $this->db->strWhere = $inClause . ' AND ' . $this->db->strWhere; | |
| } else { | |
| $this->db->strWhere = $inClause; | |
| } | |
| if ($goodsField) { | |
| $this->db->strField = $this->db->strField ? $goodsField . ', ' . $this->db->strField : $goodsField; | |
| } | |
| $query = $this->db->query_complete(); | |
| $strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_GOODS . ' g ' . implode(' ', $query); | |
| $getData = $this->db->secondary()->query_fetch($strSQL, $arrBind); | |
| $goodsInfoMap = []; | |
| foreach ((array) $getData as $row) { | |
| $row = gd_htmlspecialchars_stripslashes($row); | |
| $goodsInfoMap[$row['goodsNo']] = $row; | |
| } | |
| return $goodsInfoMap; | |
| } | |
| /** | |
| * 상품이미지 배치 조회 — getGoodsImage() 의 IN절 배치판 (게시판 상세보기 N+1 쿼리 패턴 개선) | |
| * | |
| * 게시판 상세보기 배치 전용 내부 헬퍼. 호출부(Board::getExtraData)가 method_exists 가드로 미구현(튜닝 fork) 환경을 단건 경로로 폴백하므로 외부 튜닝 확장점이 아니다. 시그니처 안정 유지를 권장한다. | |
| * | |
| * @param array $arrGoodsNo 상품번호 배열 | |
| * @param string $imageKind 이미지 종류 (기본 'main') | |
| * @return array goodsNo 를 키로 하는 이미지행 맵 [goodsNo => [row, ...]] | |
| */ | |
| public function getGoodsImageByNos(array $arrGoodsNo, $imageKind = 'main'): array | |
| { | |
| $arrGoodsNo = array_values(array_unique(array_filter($arrGoodsNo))); | |
| if (empty($arrGoodsNo)) { | |
| return []; | |
| } | |
| $arrBind = null; | |
| $bindQuery = []; | |
| foreach ($arrGoodsNo as $goodsNo) { | |
| $bindQuery[] = '?'; | |
| $this->db->bind_param_push($arrBind, 'i', $goodsNo); | |
| } | |
| $this->db->bind_param_push($arrBind, 's', $imageKind); | |
| // 단건 getGoodsImage 는 goodsNo 를 SELECT 에서 제외하나, 배치는 매핑 키로 필요 → goodsNo 추가 | |
| $arrField = DBTableField::setTableField('tableGoodsImage', null, 'goodsNo'); | |
| $strSQL = "SELECT goodsNo, sno, " . implode(', ', $arrField) . " FROM " . DB_GOODS_IMAGE | |
| . " WHERE goodsNo IN (" . implode(',', $bindQuery) . ") AND imageKind = ? " | |
| . " ORDER BY imageKind ASC, imageNo ASC"; | |
| $getData = $this->db->secondary()->query_fetch($strSQL, $arrBind); | |
| $goodsImageMap = []; | |
| foreach ((array) $getData as $row) { | |
| $row = gd_htmlspecialchars_stripslashes($row); | |
| $goodsImageMap[$row['goodsNo']][] = $row; | |
| } | |
| return $goodsImageMap; | |
| } | |
| } | |
변경 전ReOrderCalculation.php
| private function precheckRestoreAddPayment(string $orderNo, string $claimStatus): array | |
| { | |
| $original = $this->getOrderOriginalDepositMileageData($orderNo, $claimStatus); | |
| $current = $this->getOrderDepositMileageData($orderNo); | |
| // precheck 는 setOrderRestore 진입 직후(member 초기화 이전)에 호출되므로 직접 로드한다. | |
| if (!is_object($this->member)) { | |
| $this->member = \App::load('\\Component\\Member\\Member'); | |
| } | |
| $memInfo = []; | |
| if (is_array($original) && !empty($original['memNo'])) { | |
| $memInfo = $this->member->getMemberInfo($original['memNo']); | |
| } | |
| $exceptMsg = []; | |
| $checks = [ | |
| ['field' => 'useDeposit', 'hold' => 'deposit', 'msg' => '고객이 보유한 예치금 부족'], | |
| ['field' => 'useMileage', 'hold' => 'mileage', 'msg' => '고객이 보유한 마일리지 부족'], | |
| ]; | |
| foreach ($checks as $check) { | |
| $diff = gd_isset($current[$check['field']], 0) - gd_isset($original[$check['field']], 0); | |
| if ($diff === 0) { | |
| continue; | |
| } | |
| if (empty($memInfo) || (gd_isset($memInfo[$check['hold']], 0) - abs($diff) < 0)) { | |
| $exceptMsg[] = $check['msg']; | |
| } | |
| } | |
변경 후ReOrderCalculation.php
| private function precheckRestoreAddPayment(string $orderNo, string $claimStatus): array | |
| { | |
| $original = $this->getOrderOriginalDepositMileageData($orderNo, $claimStatus); | |
| $exceptMsg = []; | |
| if (is_array($original) && !empty($original['memNo'])) { | |
| $current = $this->getOrderDepositMileageData($orderNo); | |
| // precheck 는 setOrderRestore 진입 직후(member 초기화 이전)에 호출되므로 직접 로드한다. | |
| if (!is_object($this->member)) { | |
| $this->member = \App::load('\\Component\\Member\\Member'); | |
| } | |
| $memInfo = $this->member->getMemberInfo($original['memNo']); | |
| $checks = [ | |
| ['field' => 'useDeposit', 'hold' => 'deposit', 'msg' => '고객이 보유한 예치금 부족'], | |
| ['field' => 'useMileage', 'hold' => 'mileage', 'msg' => '고객이 보유한 마일리지 부족'], | |
| ]; | |
| foreach ($checks as $check) { | |
| $diff = ($current[$check['field']] ?? 0) - ($original[$check['field']] ?? 0); | |
| if ($diff === 0) { | |
| continue; | |
| } | |
| if (empty($memInfo) || (($memInfo[$check['hold']] ?? 0) - abs($diff) < 0)) { | |
| $exceptMsg[] = $check['msg']; | |
| } | |
| } | |
| } | |
변경 전order_view.php
| if ($('#tabOrderStatus .nav-tabs li').hasClass('active')) { | |
| $('#tabOrderStatus .nav-tabs li.active a').trigger('show.bs.tab'); | |
| } | |
| if ($('#tabClaimStatus .nav-tabs li').hasClass('active')) { | |
| $('#tabClaimStatus .nav-tabs li.active a').trigger('show.bs.tab'); | |
| } | |
| // 일괄 처리 모드의 체크 | |
변경 후order_view.php
| if ($('#tabOrderStatus .nav-tabs li').hasClass('active')) { | |
| $('#tabOrderStatus .nav-tabs li.active a').trigger('show.bs.tab'); | |
| } | |
| // 클레임 내역은 데이터가 있을 때만 초기 로드 (불필요한 inc_claim_view → getOrderView 호출 방지) | |
| // 데이터가 없으면 초기 AJAX를 생략하고, 탭 클릭 시 기존 show.bs.tab 핸들러로 지연 로드된다. | |
| var hasClaimData = false; | |
| $('#tabClaimStatus .nav-tabs a strong').each(function () { | |
| if (parseInt($(this).text().replace(/[^0-9]/g, ''), 10) > 0) { | |
| hasClaimData = true; | |
| return false; | |
| } | |
| }); | |
| if ($('#tabClaimStatus .nav-tabs li').hasClass('active')) { | |
| var $activeClaimPane = $('#tabClaimStatus .tab-pane.active'); | |
| if (hasClaimData || $activeClaimPane.attr('id') !== 'tab-claim-cancel') { | |
| // 클레임 데이터가 있거나, 활성 탭이 취소탭이 아닌 경우(공급사/교환·반품·환불 등) → 기존대로 AJAX 로드 | |
| $('#tabClaimStatus .nav-tabs li.active a').trigger('show.bs.tab'); | |
| } else { | |
| // 클레임이 전혀 없고 활성 탭이 취소탭인 가장 흔한 케이스 → AJAX 없이 동일한 '취소정보가 없습니다' 빈 상태를 렌더 | |
| $('#tabClaimStatus .tab-content').removeClass('loading'); | |
| $activeClaimPane.html( | |
| '<table class="table table-rows">' | |
| + '<thead><tr>' | |
| + '<th class="width3p">번호</th>' | |
| + '<th class="width5p">처리상태</th>' | |
| + '<th class="width8p">공급사</th>' | |
| + '<th class="width-3xs">이미지</th>' | |
| + '<th>상품</th>' | |
| + '<th class="width5p">취소수량</th>' | |
| + '<th class="width8p">취소사유</th>' | |
| + '<th class="width10p">상세사유</th>' | |
| + '</tr></thead>' | |
| + '<tbody><tr><td class="no-data" colspan="25">취소정보가 없습니다.</td></tr></tbody>' | |
| + '</table>' | |
| ); | |
| } | |
| } | |
| // 일괄 처리 모드의 체크 | |
변경 전OrderAdminNew.php
| use Origin\Enum\Order\OrderStatusGroup; | |
| use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService; | |
| use Origin\Service\WebHook\OrderWebHookService; | |
| use Request; | |
| use Session; | |
| use Component\Page\Page; | |
| $deliveryUniqueKey = ''; | |
| $aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용 | |
| // 전체 정책별 배송비와 지역별 배송비의 총합 구하기 | |
| foreach ($getData['goods'] as $sKey => $sVal) { | |
| foreach ($sVal as $dKey => $dVal) { | |
| // $dVal = array_shift($dVal); | |
| foreach ($dVal as $gKey => $gVal) { | |
| $realDeliveryTaxSum = gd_array_sum([ | |
| $gVal['realTaxSupplyDeliveryCharge'], | |
| $gVal['realTaxVatDeliveryCharge'], | |
| $gVal['realTaxFreeDeliveryCharge'], | |
| ]); | |
| // 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다 | |
| $deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno']; | |
| if ($deliveryKeyCheck == $deliveryUniqueKey) { | |
| if (gd_in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (!gd_in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 | |
| $aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']); | |
| $aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']); | |
| $getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| } | |
| } else { | |
| // 주문상세내 배송비 금액 합산 | |
| if (!gd_in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) { | |
| $getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['o1'])) { | |
| $getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (!gd_in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 | |
| $aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']); | |
| $aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']); | |
| $getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['r1', 'r2'])) { | |
| $getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum; | |
| } | |
| $getData['totalDeliveryPolicyCharge'] += $gVal['deliveryPolicyCharge']; | |
| // 배송무게 계산 | |
| $getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true); | |
| } | |
| $deliveryUniqueKey = $deliveryKeyCheck; | |
| // 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보 | |
| } | |
| } | |
| if (Manager::isProvider()) { | |
| $getData = $this->getProviderTotalPrice($getData); | |
| } | |
변경 후OrderAdminNew.php
| use Origin\Enum\Order\OrderStatusGroup; | |
| use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService; | |
| use Origin\Service\WebHook\OrderWebHookService; | |
| use Repository\Order\OrderDeliveryOriginalRepository; | |
| use Repository\Order\OrderHandleRepository; | |
| use Request; | |
| use Session; | |
| use Component\Page\Page; | |
| $deliveryUniqueKey = ''; | |
| $aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용 | |
| // 전체 정책별 배송비와 지역별 배송비의 총합 구하기 (DB 불필요 합산은 단일 순회로 처리, 취소행/sno는 수집만) | |
| $cancelRows = []; | |
| $handleSnoList = $deliveryOriginalSnoList = []; | |
| foreach ($getData['goods'] as $sKey => $sVal) { | |
| foreach ($sVal as $dKey => $dVal) { | |
| foreach ($dVal as $gKey => $gVal) { | |
| $realDeliveryTaxSum = array_sum([ | |
| $gVal['realTaxSupplyDeliveryCharge'], | |
| $gVal['realTaxVatDeliveryCharge'], | |
| $gVal['realTaxFreeDeliveryCharge'], | |
| ]); | |
| // 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다 | |
| $deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno']; | |
| if ($deliveryKeyCheck != $deliveryUniqueKey) { | |
| // 주문상세내 배송비 금액 합산 | |
| if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) { | |
| $getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (in_array($gVal['orderStatus'], ['o1'])) { | |
| $getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (in_array($gVal['orderStatus'], ['r1', 'r2'])) { | |
| $getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum; | |
| } | |
| $getData['totalDeliveryPolicyCharge'] += $gVal['deliveryPolicyCharge']; | |
| // 배송무게 계산 | |
| $getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true); | |
| } | |
| // 취소행은 기존에 배송키 분기와 무관하게 동일 처리됐으므로 모아두고, N+1 제거 위해 일괄조회 후 후처리 | |
| if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $cancelRows[] = $gVal; | |
| if ($gVal['handleSno'] > 0) { | |
| $handleSnoList[] = $gVal['handleSno']; | |
| } | |
| if ($gVal['orderDeliverySno'] > 0) { | |
| $deliveryOriginalSnoList[] = $gVal['orderDeliverySno']; | |
| } | |
| } | |
| $deliveryUniqueKey = $deliveryKeyCheck; | |
| // 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보 | |
| } | |
| } | |
| // 취소상태 배송행의 handleSno/orderDeliverySno 를 일괄 1회씩 조회 (N+1 제거) | |
| $handleInfoMap = \App::getInstance(OrderHandleRepository::class) | |
| ->findMapBySnos(array_values(array_unique($handleSnoList))); | |
| $deliveryOriginalMap = \App::getInstance(OrderDeliveryOriginalRepository::class) | |
| ->findMapBySnos(array_values(array_unique($deliveryOriginalSnoList))); | |
| // 취소 배송비 합산 (동일 orderDeliverySno 키는 1회만 계산) | |
| foreach ($cancelRows as $gVal) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| continue; | |
| } | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 (일괄조회 맵에서 룩업) | |
| $refundDeliveryCharge = $handleInfoMap[$gVal['handleSno']]['refundDeliveryCharge'] ?? 0; | |
| $originalDcPrice = $deliveryOriginalMap[$gVal['orderDeliverySno']]['divisionMemberDeliveryDcPrice'] ?? 0; | |
| $getData['dashBoardPrice']['cancelPrice'] += $refundDeliveryCharge - ($originalDcPrice - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| if (Manager::isProvider()) { | |
| $getData = $this->getProviderTotalPrice($getData); | |
| } | |
변경 전OrderAdmin.php
| use Origin\Enum\Order\OrderStatusGroup; | |
| use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService; | |
| use Origin\Service\WebHook\OrderWebHookService; | |
| use Request; | |
| use Session; | |
| use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader; | |
| $deliveryUniqueKey = ''; | |
| $aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용 | |
| // 전체 정책별 배송비와 지역별 배송비의 총합 구하기 | |
| foreach ($getData['goods'] as $sKey => $sVal) { | |
| foreach ($sVal as $dKey => $dVal) { | |
| // $dVal = array_shift($dVal); | |
| foreach ($dVal as $gKey => $gVal) { | |
| $realDeliveryTaxSum = gd_array_sum([ | |
| $gVal['realTaxSupplyDeliveryCharge'], | |
| $gVal['realTaxVatDeliveryCharge'], | |
| $gVal['realTaxFreeDeliveryCharge'], | |
| ]); | |
| // 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다 | |
| $deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno']; | |
| if ($deliveryKeyCheck == $deliveryUniqueKey) { | |
| if (gd_in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (!gd_in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 | |
| $aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']); | |
| $aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']); | |
| $getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| } | |
| } else { | |
| // 주문상세내 배송비 금액 합산 | |
| if (!gd_in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) { | |
| $getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['o1'])) { | |
| $getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (!gd_in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 | |
| $aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']); | |
| $aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']); | |
| $getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| } | |
| if (gd_in_array($gVal['orderStatus'], ['r1', 'r2'])) { | |
| $getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum; | |
| } | |
| $getData['totalDeliveryPolicyCharge'] += $gVal['deliveryPolicyCharge']; | |
| // 배송무게 계산 | |
| $getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true); | |
| } | |
| $deliveryUniqueKey = $deliveryKeyCheck; | |
| // 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보 | |
| } | |
| } | |
| if (Manager::isProvider()) { | |
| $getData = $this->getProviderTotalPrice($getData); | |
| } | |
변경 후OrderAdmin.php
| use Origin\Enum\Order\OrderStatusGroup; | |
| use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService; | |
| use Origin\Service\WebHook\OrderWebHookService; | |
| use Repository\Order\OrderDeliveryOriginalRepository; | |
| use Repository\Order\OrderHandleRepository; | |
| use Request; | |
| use Session; | |
| use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader; | |
| $deliveryUniqueKey = ''; | |
| $aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용 | |
| // 전체 정책별 배송비와 지역별 배송비의 총합 구하기 (DB 불필요 합산은 단일 순회로 처리, 취소행/sno는 수집만) | |
| $cancelRows = []; | |
| $handleSnoList = $deliveryOriginalSnoList = []; | |
| foreach ($getData['goods'] as $sKey => $sVal) { | |
| foreach ($sVal as $dKey => $dVal) { | |
| foreach ($dVal as $gKey => $gVal) { | |
| $realDeliveryTaxSum = array_sum([ | |
| $gVal['realTaxSupplyDeliveryCharge'], | |
| $gVal['realTaxVatDeliveryCharge'], | |
| $gVal['realTaxFreeDeliveryCharge'], | |
| ]); | |
| // 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다 | |
| $deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno']; | |
| if ($deliveryKeyCheck != $deliveryUniqueKey) { | |
| // 주문상세내 배송비 금액 합산 | |
| if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) { | |
| $getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (in_array($gVal['orderStatus'], ['o1'])) { | |
| $getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum; | |
| } | |
| if (in_array($gVal['orderStatus'], ['r1', 'r2'])) { | |
| $getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum; | |
| } | |
| $getData['totalDeliveryPolicyCharge'] += $gVal['deliveryPolicyCharge']; | |
| // 배송무게 계산 | |
| $getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true); | |
| } | |
| // 취소행은 기존에 배송키 분기와 무관하게 동일 처리됐으므로 모아두고, N+1 제거 위해 일괄조회 후 후처리 | |
| if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) { | |
| $cancelRows[] = $gVal; | |
| if ($gVal['handleSno'] > 0) { | |
| $handleSnoList[] = $gVal['handleSno']; | |
| } | |
| if ($gVal['orderDeliverySno'] > 0) { | |
| $deliveryOriginalSnoList[] = $gVal['orderDeliverySno']; | |
| } | |
| } | |
| $deliveryUniqueKey = $deliveryKeyCheck; | |
| // 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보 | |
| } | |
| } | |
| // 취소상태 배송행의 handleSno/orderDeliverySno 를 일괄 1회씩 조회 (N+1 제거) | |
| $handleInfoMap = \App::getInstance(OrderHandleRepository::class) | |
| ->findMapBySnos(array_values(array_unique($handleSnoList))); | |
| $deliveryOriginalMap = \App::getInstance(OrderDeliveryOriginalRepository::class) | |
| ->findMapBySnos(array_values(array_unique($deliveryOriginalSnoList))); | |
| // 취소 배송비 합산 (동일 orderDeliverySno 키는 1회만 계산) | |
| foreach ($cancelRows as $gVal) { | |
| $tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno']; | |
| if (in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) { | |
| continue; | |
| } | |
| // $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 (일괄조회 맵에서 룩업) | |
| $refundDeliveryCharge = $handleInfoMap[$gVal['handleSno']]['refundDeliveryCharge'] ?? 0; | |
| $originalDcPrice = $deliveryOriginalMap[$gVal['orderDeliverySno']]['divisionMemberDeliveryDcPrice'] ?? 0; | |
| $getData['dashBoardPrice']['cancelPrice'] += $refundDeliveryCharge - ($originalDcPrice - $gVal['divisionMemberDeliveryDcPrice']); | |
| $aCancelDeliveryKey[] = $tempCancelDeliveryKey; | |
| } | |
| if (Manager::isProvider()) { | |
| $getData = $this->getProviderTotalPrice($getData); | |
| } | |
변경 전OrderDeliveryOriginalRepository.php
변경 후OrderDeliveryOriginalRepository.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\Repository\Order; | |
| use Origin\Model\Order\OrderDeliveryOriginal; | |
| class OrderDeliveryOriginalRepository | |
| { | |
| /** | |
| * sno 목록으로 orderDeliveryOriginal 정보를 일괄 조회해 [sno => row] 맵으로 반환 (주문상세 N+1 제거용) | |
| * | |
| * @param array $snos orderDeliveryOriginal.sno 목록 | |
| * @return array sno 를 키로 하는 orderDeliveryOriginal row 맵 | |
| */ | |
| public function findMapBySnos(array $snos): array | |
| { | |
| if (empty($snos)) { | |
| return []; | |
| } | |
| return OrderDeliveryOriginal::query() | |
| ->whereIn('sno', $snos) | |
| ->get() | |
| ->keyBy('sno') | |
| ->map(fn ($model) => $model->toArray()) | |
| ->all(); | |
| } | |
| } | |
변경 전OrderHandleRepository.php
변경 후OrderHandleRepository.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\Repository\Order; | |
| use Origin\Model\Order\OrderHandle; | |
| class OrderHandleRepository | |
| { | |
| /** | |
| * sno 목록으로 orderHandle 정보를 일괄 조회해 [sno => row] 맵으로 반환 (주문상세 N+1 제거용) | |
| * | |
| * @param array $snos orderHandle.sno 목록 | |
| * @return array sno 를 키로 하는 orderHandle row 맵 | |
| */ | |
| public function findMapBySnos(array $snos): array | |
| { | |
| if (empty($snos)) { | |
| return []; | |
| } | |
| return OrderHandle::query() | |
| ->whereIn('sno', $snos) | |
| ->get() | |
| ->keyBy('sno') | |
| ->map(fn ($model) => $model->toArray()) | |
| ->all(); | |
| } | |
| } | |
변경 전Gift.php
| } | |
| return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData; | |
| } | |
| } | |
변경 후Gift.php
| } | |
| return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData; | |
| } | |
| /** | |
| * 주문 사은품 데이터 검증 및 정규화 (변조 방지) | |
| * | |
| * 주문서에서 전달된 사은품 hidden 필드(giveCnt/selectCnt/giftNo)는 개발자도구 등으로 | |
| * 변조가 가능하므로, 서버에서 재조회한 유효 사은품 정보($giftInfo)를 기준으로 검증·정규화 | |
| * - giveCnt(지급수량) : 지급조건의 값으로 강제 정규화 (변조 무력화, 주문은 정상 생성) | |
| * - selectCnt(선택수량) : 실제 선택한 사은품 개수가 지급조건과 다르면 차단 (전체=0 은 제외) | |
| * - 지급조건에 포함되지 않은 사은품(giftNo)은 제거 | |
| * | |
| * @param array $submittedGift 주문서에서 전달된 사은품 데이터 [presentSno][index] => [...] | |
| * @param array $giftInfo getGiftPresentOrder() 로 조회한 유효 사은품 정보 | |
| * @return array 정규화된 사은품 데이터 | |
| * @throws \Exception | |
| */ | |
| public function validateAndNormalizeOrderGift(array $submittedGift, array $giftInfo): array | |
| { | |
| if (empty($submittedGift) || empty($giftInfo)) { | |
| return $submittedGift; | |
| } | |
| foreach ($submittedGift as $presentSno => $giftData) { | |
| // 유효하지 않은 사은품 정책은 기존 저장 로직에서 스킵되므로 건드리지 않음 | |
| if (!array_key_exists($presentSno, $giftInfo) || empty($giftInfo[$presentSno]['gift'])) { | |
| continue; | |
| } | |
| // 권위 데이터 집계 : giftNo => 지급조건(row), 지급조건별 giveCnt/selectCnt | |
| $authGiveCnt = []; // giftNo => giveCnt | |
| $giftNoToRow = []; // giftNo => rowKey | |
| $rowSelectCnt = []; // rowKey => selectCnt | |
| foreach ($giftInfo[$presentSno]['gift'] as $rowKey => $row) { | |
| $rowSelectCnt[$rowKey] = (int) ($row['selectCnt'] ?? 0); | |
| if (empty($row['multiGiftNo']) || !is_array($row['multiGiftNo'])) { | |
| continue; | |
| } | |
| foreach ($row['multiGiftNo'] as $item) { | |
| if (!isset($item['giftNo'])) { | |
| continue; | |
| } | |
| $authGiveCnt[$item['giftNo']] = (int) ($row['giveCnt'] ?? 0); | |
| $giftNoToRow[$item['giftNo']] = $rowKey; | |
| } | |
| } | |
| // 제출된 사은품 정규화 + 지급조건별 선택 개수/제출 선택수량 집계 | |
| $selectedCntByRow = []; | |
| $submittedSelectCntByRow = []; | |
| foreach ($giftData as $index => $gift) { | |
| // 선택되지 않은 항목(giftNo 없음)은 통과 (기존 로직이 저장하지 않음) | |
| if (!isset($gift['giftNo']) || $gift['giftNo'] === '') { | |
| continue; | |
| } | |
| // 지급조건에 포함되지 않은 사은품(변조)은 제거 | |
| if (!isset($authGiveCnt[$gift['giftNo']])) { | |
| \Logger::channel('order')->info(__METHOD__ . ' 지급조건에 없는 사은품 제거', [$presentSno, $gift['giftNo']]); | |
| unset($submittedGift[$presentSno][$index]); | |
| continue; | |
| } | |
| // giveCnt(지급수량) 정규화 - POST 변조 무력화 | |
| $submittedGift[$presentSno][$index]['giveCnt'] = $authGiveCnt[$gift['giftNo']]; | |
| $rowKey = $giftNoToRow[$gift['giftNo']]; | |
| $selectedCntByRow[$rowKey] = ($selectedCntByRow[$rowKey] ?? 0) + 1; | |
| // 제출된 선택수량(변조 탐지용) - 같은 지급조건 항목은 동일 값 | |
| if (isset($gift['selectCnt'])) { | |
| $submittedSelectCntByRow[$rowKey] = (int) $gift['selectCnt']; | |
| } | |
| } | |
| // 선택수량(selectCnt) 검증 | |
| foreach ($rowSelectCnt as $rowKey => $selectCnt) { | |
| $selectedCnt = (int) ($selectedCntByRow[$rowKey] ?? 0); | |
| $submittedSelectCnt = $submittedSelectCntByRow[$rowKey] ?? null; | |
| $message = GiftSelectCntValidator::resolveViolationMessage($selectCnt, $selectedCnt, $submittedSelectCnt); | |
| if ($message !== null) { | |
| \Logger::channel('order')->warning(__METHOD__ . ' 사은품 선택수량 검증 실패', [ | |
| 'presentSno' => $presentSno, | |
| 'rowKey' => $rowKey, | |
| 'authSelectCnt' => $selectCnt, | |
| 'selectedCnt' => $selectedCnt, | |
| 'submittedSelectCnt' => $submittedSelectCnt, | |
| ]); | |
| throw new \Exception($message); | |
| } | |
| } | |
| } | |
| return $submittedGift; | |
| } | |
| } | |
변경 전GiftSelectCntValidator.php
변경 후GiftSelectCntValidator.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\Component\Gift; | |
| /** | |
| * 사은품 선택수량(selectCnt) 검증 | |
| * | |
| * DB/세션/로거 등 부수효과 없이 정수 입력만으로 위반 여부를 판정 | |
| * 일반주문/정기결제 등 각 호출부는 이 메시지를 받아 흐름에 맞는 예외를 던진다 | |
| */ | |
| class GiftSelectCntValidator | |
| { | |
| /** | |
| * 선택수량 위반 메시지를 반환한다. 위반이 없으면 null. | |
| * | |
| * - 지급조건 선택수량(전체=0)은 검증 제외 | |
| * - 미선택(0개)은 검증 제외 (사은품 거절 허용) | |
| * - 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건과 다르면 위반 | |
| * | |
| * @param int $authSelectCnt 서버(지급조건)의 선택수량. 0이면 전체 지급 | |
| * @param int $selectedCnt 실제 선택(체크)된 사은품 개수 | |
| * @param int|null $submittedSelectCnt 주문서에서 제출된 선택수량 값(변조 탐지용). null이면 미사용 | |
| * @return string|null 위반 메시지 또는 null | |
| */ | |
| public static function resolveViolationMessage(int $authSelectCnt, int $selectedCnt, ?int $submittedSelectCnt = null): ?string | |
| { | |
| // 전체(0) 또는 미선택은 검증 제외 (전체 지급 / 사은품 거절 허용) | |
| if ($authSelectCnt <= 0 || $selectedCnt <= 0) { | |
| return null; | |
| } | |
| // 제출된 선택수량이 없으면 실제 선택 개수로 비교 | |
| $submittedSelectCnt ??= $selectedCnt; | |
| return match (true) { | |
| // 최대 초과 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 큰 경우 | |
| $selectedCnt > $authSelectCnt || $submittedSelectCnt > $authSelectCnt | |
| => sprintf(__('사은품은 최대 %s개만 선택하실 수 있습니다.'), $authSelectCnt), | |
| // 최소 미달 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 작은 경우 | |
| $selectedCnt < $authSelectCnt || $submittedSelectCnt < $authSelectCnt | |
| => sprintf(__('사은품은 최소 %s개 이상 선택하셔야 합니다.'), $authSelectCnt), | |
| default => null, | |
| }; | |
| } | |
| } | |
변경 전OrderNew.php
| $giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음 | |
| \Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo); | |
| if (gd_count($giftInfo) > 0) { | |
| foreach ($orderInfo['gift'] as $presentSno => $giftData) { | |
| // 주문시 사용된 사은품 정보가 유효하지 않으면 삭제 | |
변경 후OrderNew.php
| $giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음 | |
| \Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo); | |
| // 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단은 프론트 주문만, 관리자 수기주문 제외) | |
| // 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그 | |
| if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) { | |
| $orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo); | |
| } else { | |
| \Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]); | |
| } | |
| if (gd_count($giftInfo) > 0) { | |
| foreach ($orderInfo['gift'] as $presentSno => $giftData) { | |
| // 주문시 사용된 사은품 정보가 유효하지 않으면 삭제 | |
변경 전Order.php
| $giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false); | |
| \Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo); | |
| if (gd_count($giftInfo) > 0) { | |
| foreach ($orderInfo['gift'] as $presentSno => $giftData) { | |
| // 주문시 사용된 사은품 정보가 유효하지 않으면 삭제 | |
변경 후Order.php
| $giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false); | |
| \Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo); | |
| // 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단) | |
| // 프론트/회원 주문($isWrite === false)만 검증한다. 관리자 수기주문($isWrite === true)은 | |
| // selectCnt 강제(차단) 대상이 아니므로 제외한다. | |
| if ($isWrite === false) { | |
| // 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그 | |
| if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) { | |
| $orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo); | |
| } else { | |
| \Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]); | |
| } | |
| } | |
| if (gd_count($giftInfo) > 0) { | |
| foreach ($orderInfo['gift'] as $presentSno => $giftData) { | |
| // 주문시 사용된 사은품 정보가 유효하지 않으면 삭제 | |
변경 전RegularGiftSelectCntValidator.php
변경 후RegularGiftSelectCntValidator.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\Component\RegularDelivery\RegularGoods; | |
| use Component\Gift\GiftSelectCntValidator; | |
| use Framework\Debug\Exception\AlertBackException; | |
| use Repository\RegularDelivery\RegularGoods\RegularGiftPresentInfoRepository; | |
| /** | |
| * 정기결제(신청/변경) 사은품 선택수량 검증 서비스 | |
| * | |
| * 제출된 사은품을 지급조건(regularGiftPresentInfoSno)별로 묶어, 서버 권위 selectCnt 와 | |
| * 실제 선택 개수/제출 선택수량을 비교한다. 위반 시 AlertBackException 을 던진다 | |
| */ | |
| class RegularGiftSelectCntValidator | |
| { | |
| public function __construct( | |
| private readonly RegularGiftPresentInfoRepository $presentInfoRepository | |
| ) { | |
| } | |
| /** | |
| * 정기결제 사은품 선택수량 검증 (변조 방지) | |
| * | |
| * @param array $giftItems 제출된 사은품 데이터 [['giftNo','regularGiftPresentInfoSno','selectCnt'?...], ...] | |
| * @return void | |
| * @throws AlertBackException 선택수량 위반 시 | |
| */ | |
| public function assertValid(array $giftItems): void | |
| { | |
| if (empty($giftItems)) { | |
| return; | |
| } | |
| // 지급조건(sno)별 선택 개수 / 제출 선택수량 집계 | |
| $snoList = []; | |
| $selectedCntBySno = []; | |
| $submittedSelectCntBySno = []; | |
| foreach ($giftItems as $gift) { | |
| if (!isset($gift['regularGiftPresentInfoSno'])) { | |
| continue; | |
| } | |
| $sno = (int) $gift['regularGiftPresentInfoSno']; | |
| $snoList[$sno] = $sno; | |
| if (isset($gift['selectCnt'])) { | |
| $submittedSelectCntBySno[$sno] = (int) $gift['selectCnt']; | |
| } | |
| if (isset($gift['giftNo']) && $gift['giftNo'] !== '') { | |
| $selectedCntBySno[$sno] = ($selectedCntBySno[$sno] ?? 0) + 1; | |
| } | |
| } | |
| if (empty($snoList)) { | |
| return; | |
| } | |
| $authInfo = $this->presentInfoRepository->findSelectInfoBySnoList(array_values($snoList)); | |
| foreach ($snoList as $sno) { | |
| if (!isset($authInfo[$sno])) { | |
| continue; | |
| } | |
| $authSelectCnt = (int) ($authInfo[$sno]['selectCnt'] ?? 0); | |
| $selectedCnt = (int) ($selectedCntBySno[$sno] ?? 0); | |
| $submittedSelectCnt = $submittedSelectCntBySno[$sno] ?? null; | |
| $message = GiftSelectCntValidator::resolveViolationMessage($authSelectCnt, $selectedCnt, $submittedSelectCnt); | |
| if ($message !== null) { | |
| throw new AlertBackException($message); | |
| } | |
| } | |
| } | |
| } | |
변경 전RegularGoodsChange.php
| /** @var int 단일 신청서 내 상품 합계 금액 */ | |
| private $totalPrice; | |
| public function __construct( | |
| GoodsOptionRepository $goodsOptionRepository, | |
| GoodsOptionTextRepository $goodsOptionTextRepository, | |
| RegularOrderDeliveryRepository $regularOrderDeliveryRepository, | |
| RegularOrderLogRepository $regularOrderLogRepository, | |
| Logger $logger, | |
| Manager $dbManager | |
| ) | |
| { | |
| $this->goodsOptionRepository = $goodsOptionRepository; | |
| $this->logger = $logger; | |
| $this->dbManager = $dbManager; | |
| $this->totalPrice = 0; | |
| } | |
| /** | |
| // 유효성 검사 | |
| $this->validate($goodsChangeDto, $originGoodsInfo, true); | |
| $this->dbManager->getConnection()->beginTransaction(); | |
| try { | |
| // 상품 정보 업데이트 | |
변경 후RegularGoodsChange.php
| /** @var int 단일 신청서 내 상품 합계 금액 */ | |
| private $totalPrice; | |
| /** @var RegularGiftSelectCntValidator */ | |
| private $regularGiftSelectCntValidator; | |
| public function __construct( | |
| GoodsOptionRepository $goodsOptionRepository, | |
| GoodsOptionTextRepository $goodsOptionTextRepository, | |
| RegularOrderDeliveryRepository $regularOrderDeliveryRepository, | |
| RegularOrderLogRepository $regularOrderLogRepository, | |
| Logger $logger, | |
| Manager $dbManager, | |
| RegularGiftSelectCntValidator $regularGiftSelectCntValidator | |
| ) | |
| { | |
| $this->goodsOptionRepository = $goodsOptionRepository; | |
| $this->logger = $logger; | |
| $this->dbManager = $dbManager; | |
| $this->totalPrice = 0; | |
| $this->regularGiftSelectCntValidator = $regularGiftSelectCntValidator; | |
| } | |
| /** | |
| // 유효성 검사 | |
| $this->validate($goodsChangeDto, $originGoodsInfo, true); | |
| // 사은품 선택수량 검증 (변조 방지) - 회원(마이페이지) 변경만 차단, 관리자 수기 변경은 제외 | |
| $giftInfoDto = $goodsChangeDto->getGiftInfo(); | |
| if ($sessionType === 'user' && $giftInfoDto !== null) { | |
| $this->regularGiftSelectCntValidator->assertValid($giftInfoDto->getGiftData()); | |
| } | |
| $this->dbManager->getConnection()->beginTransaction(); | |
| try { | |
| // 상품 정보 업데이트 | |
변경 전RegularOrderApplicationCreate.php
| use Repository\Scm\ScmDeliveryBasicRepository; | |
| use Util\Order\RegularOrderUtil; | |
| use Component\RegularDelivery\Notification\RegularDeliveryNotificationSender; | |
| /** | |
| * 정기 결제 신청서 생성 | |
| */ | |
| private $notificationSender; | |
| public function __construct( | |
| RegularOrderRepository $regularOrderRepository, | |
| RegularOrderGoodsRepository $regularOrderGoodsRepository, | |
| AddGoodsRepository $addGoodsRepository, | |
| Manager $dbManager, | |
| Logger $logger, | |
| RegularDeliveryNotificationSender $notificationSender | |
| ) | |
| { | |
| $this->regularOrderRepository = $regularOrderRepository; | |
| $this->logger = $logger; | |
| $this->totalPrice = 0; // 신청서 총 금액 ((상품가 + 옵션가 + 텍스트옵션가) * 수량) * 주문 상품 | |
| $this->notificationSender = $notificationSender; | |
| } | |
| /** | |
| return; | |
| } | |
| $optionGiftInfo = $originGiftInfo[$cartSno]; | |
| $giftInfo = []; | |
| foreach ($optionGiftInfo as $gift) { | |
변경 후RegularOrderApplicationCreate.php
| use Repository\Scm\ScmDeliveryBasicRepository; | |
| use Util\Order\RegularOrderUtil; | |
| use Component\RegularDelivery\Notification\RegularDeliveryNotificationSender; | |
| use Component\RegularDelivery\RegularGoods\RegularGiftSelectCntValidator; | |
| /** | |
| * 정기 결제 신청서 생성 | |
| */ | |
| private $notificationSender; | |
| /** | |
| * @var RegularGiftSelectCntValidator | |
| */ | |
| private $regularGiftSelectCntValidator; | |
| public function __construct( | |
| RegularOrderRepository $regularOrderRepository, | |
| RegularOrderGoodsRepository $regularOrderGoodsRepository, | |
| AddGoodsRepository $addGoodsRepository, | |
| Manager $dbManager, | |
| Logger $logger, | |
| RegularDeliveryNotificationSender $notificationSender, | |
| RegularGiftSelectCntValidator $regularGiftSelectCntValidator | |
| ) | |
| { | |
| $this->regularOrderRepository = $regularOrderRepository; | |
| $this->logger = $logger; | |
| $this->totalPrice = 0; // 신청서 총 금액 ((상품가 + 옵션가 + 텍스트옵션가) * 수량) * 주문 상품 | |
| $this->notificationSender = $notificationSender; | |
| $this->regularGiftSelectCntValidator = $regularGiftSelectCntValidator; | |
| } | |
| /** | |
| return; | |
| } | |
| $optionGiftInfo = $originGiftInfo[$cartSno] ?? []; | |
| // 사은품 선택수량 검증 (변조 방지) - 위반 시 AlertBackException | |
| $this->regularGiftSelectCntValidator->assertValid($optionGiftInfo); | |
| $giftInfo = []; | |
| foreach ($optionGiftInfo as $gift) { | |
변경 전RegularOrderPsController.php
| use Controller\Front\Controller; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderApplierDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderGoodsDTO; | |
| use Framework\Debug\Exception\AlertOnlyException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Request; | |
| $url .= '?cartIdx=' . $encodeData; | |
| } | |
| throw new AlertRedirectException('결제할 금액이 일치하지 않습니다. 할인/적립 금액이 변경되었을 수 있습니다. 새로고침 후 다시 시도해 주세요.', null, null, $url, 'parent'); | |
| } catch (\Throwable $e) { | |
| \Logger::channel('regularOrder')->error('Regular Order create error : ', $e->getMessage()); | |
| throw new AlertOnlyException('일시적인 오류로 처리에 실패하였습니다. 잠시 후 다시 시도해주세요.'); | |
변경 후RegularOrderPsController.php
| use Controller\Front\Controller; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderApplierDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderGoodsDTO; | |
| use Framework\Debug\Exception\AlertBackException; | |
| use Framework\Debug\Exception\AlertOnlyException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Request; | |
| $url .= '?cartIdx=' . $encodeData; | |
| } | |
| throw new AlertRedirectException('결제할 금액이 일치하지 않습니다. 할인/적립 금액이 변경되었을 수 있습니다. 새로고침 후 다시 시도해 주세요.', null, null, $url, 'parent'); | |
| } catch (AlertBackException $e) { | |
| // 사은품 선택수량 검증 실패 사유 | |
| \Logger::channel('regularOrder')->warning('Regular Order gift validation : ', [$e->getMessage()]); | |
| throw new AlertOnlyException($e->getMessage()); | |
| } catch (\Throwable $e) { | |
| \Logger::channel('regularOrder')->error('Regular Order create error : ', $e->getMessage()); | |
| throw new AlertOnlyException('일시적인 오류로 처리에 실패하였습니다. 잠시 후 다시 시도해주세요.'); | |
변경 전RegularOrderPsController.php
| use DTO\RegularDelivery\RegularOrder\RegularOrderDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderApplierDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderGoodsDTO; | |
| use Framework\Debug\Exception\AlertOnlyException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Request; | |
| $url .= '?cartIdx=' . $encodeData; | |
| } | |
| throw new AlertRedirectException('결제할 금액이 일치하지 않습니다. 할인/적립 금액이 변경되었을 수 있습니다. 새로고침 후 다시 시도해 주세요.', null, null, $url, 'parent'); | |
| } catch (\Throwable $e) { | |
| \Logger::channel('regularOrder')->error('Regular Order create error : ', [$e->getMessage(), $e->getTrace()]); | |
| throw new AlertOnlyException('일시적인 오류로 처리에 실패하였습니다. 잠시 후 다시 시도해주세요.'); | |
변경 후RegularOrderPsController.php
| use DTO\RegularDelivery\RegularOrder\RegularOrderDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderApplierDTO; | |
| use DTO\RegularDelivery\RegularOrder\RegularOrderGoodsDTO; | |
| use Framework\Debug\Exception\AlertBackException; | |
| use Framework\Debug\Exception\AlertOnlyException; | |
| use Framework\Debug\Exception\AlertRedirectException; | |
| use Request; | |
| $url .= '?cartIdx=' . $encodeData; | |
| } | |
| throw new AlertRedirectException('결제할 금액이 일치하지 않습니다. 할인/적립 금액이 변경되었을 수 있습니다. 새로고침 후 다시 시도해 주세요.', null, null, $url, 'parent'); | |
| } catch (AlertBackException $e) { | |
| // 사은품 선택수량 검증 실패 사유 | |
| \Logger::channel('regularOrder')->warning('Regular Order gift validation : ', [$e->getMessage()]); | |
| throw new AlertOnlyException($e->getMessage()); | |
| } catch (\Throwable $e) { | |
| \Logger::channel('regularOrder')->error('Regular Order create error : ', [$e->getMessage(), $e->getTrace()]); | |
| throw new AlertOnlyException('일시적인 오류로 처리에 실패하였습니다. 잠시 후 다시 시도해주세요.'); | |
변경 전RegularGiftPresentInfoRepository.php
| ->toArray(); | |
| } | |
| /** | |
| * 지급 조건 번호로 받을 수 있는 사은품 관련 정보 조회 | |
| * SELECT * | |
변경 후RegularGiftPresentInfoRepository.php
| ->toArray(); | |
| } | |
| /** | |
| * sno 리스트로 선택수량(selectCnt)과 사은품 목록(multiGiftNo) 조회 (주문 검증용) | |
| * | |
| * SELECT sno, selectCnt, multiGiftNo | |
| * FROM es_regularGiftPresentInfo | |
| * WHERE sno IN (...) | |
| * | |
| * @param array $snoList 지급조건 sno 리스트 | |
| * @return array sno => ['sno','selectCnt','multiGiftNo'] 형태 맵 (없으면 빈 배열) | |
| */ | |
| public function findSelectInfoBySnoList(array $snoList): array | |
| { | |
| if (empty($snoList)) { | |
| return []; | |
| } | |
| $rows = RegularGiftPresentInfo::query() | |
| ->whereIn('sno', $snoList) | |
| ->select(['sno', 'selectCnt', 'multiGiftNo']) | |
| ->get() | |
| ->toArray(); | |
| // sno 를 키로 하는 맵으로 변환 | |
| return array_column($rows, null, 'sno'); | |
| } | |
| /** | |
| * 지급 조건 번호로 받을 수 있는 사은품 관련 정보 조회 | |
| * SELECT * | |
변경 전kakao_login_config.php
| <div class="notice-info"> | |
| 카카오 아이디 로그인에 사용한 REST API 키와 ADMIN 키는 <a href="https://developers.kakao.com/" target="_blank" class="notice-ref btn-link">카카오 디벨로퍼스 > 내 애플리케이션 > 앱 설정 > 앱 키</a>에서 확인하실 수 있습니다. | |
| </div> | |
| <?php elseif($isAvailableOnlyKakaoSync === false) : ?> | |
| <form id="form" name="form" action="kakao_login_config_ps.php" method="post"> | |
| <div class="page-header js-affix"> | |
| <h3><?php echo end($naviMenu->location); ?></h3> | |
| <input type="submit" value="저장" class="btn btn-red" /> | |
| </div> | |
| <div class="table-title gd-help-manual"> | |
변경 후kakao_login_config.php
| <div class="notice-info"> | |
| 카카오 아이디 로그인에 사용한 REST API 키와 ADMIN 키는 <a href="https://developers.kakao.com/" target="_blank" class="notice-ref btn-link">카카오 디벨로퍼스 > 내 애플리케이션 > 앱 설정 > 앱 키</a>에서 확인하실 수 있습니다. | |
| </div> | |
| <?php else : ?> | |
| <form id="form" name="form" action="kakao_login_config_ps.php" method="post"> | |
| <div class="page-header js-affix"> | |
| <h3><?php echo end($naviMenu->location); ?></h3> | |
| <?php if (!$isAvailableOnlyKakaoSync) : ?> | |
| <input type="submit" value="저장" class="btn btn-red" /> | |
| <?php endif; ?> | |
| </div> | |
| <div class="table-title gd-help-manual"> | |
변경 전OrderNew.php
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', $result); | |
| } | |
| unset($mqData); | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', $result); | |
| } | |
| unset($mqData); | |
변경 후OrderNew.php
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $request = \App::getInstance('request'); | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos'], 'reqUri' => $request->getRequestUri()]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $request = \App::getInstance('request'); | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos'], 'reqUri' => $request->getRequestUri()]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| unset($mqData); | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| unset($mqData); | |
변경 전Order.php
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', $result); | |
| } | |
| unset($mqData); | |
| unset($arrOrderGoodsSno['mq']); | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', $result); | |
| } | |
| unset($mqData); | |
변경 후Order.php
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $request = \App::getInstance('request'); | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos'], 'reqUri' => $request->getRequestUri()]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| $request = \App::getInstance('request'); | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos'], 'reqUri' => $request->getRequestUri()]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : [])); | |
| } | |
| unset($mqData); | |
| unset($arrOrderGoodsSno['mq']); | |
| // 주문카운트 대상이 있을 때만 발행 (없으면 orderGoodsNos null 메시지 방지) | |
| if (!empty($mqData['orderGoodsNos'])) { | |
| $mqData['mode'] = 'standardOrder'; | |
| \Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData['orderGoodsNos']]); | |
| $result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true); | |
| \Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : [])); | |
| } | |
| unset($mqData); | |
변경 전AdultController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| $this->setData('naverUrl', $naver->getLoginURL($returnUrl)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 후AdultController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| // redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관 | |
| $naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php'; | |
| $naverState = $naver->createState([ | |
| 'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH), | |
| 'returnUrl' => $returnUrl, | |
| 'naverType' => '', | |
| ]); | |
| $this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 전MemberController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| $this->setData('naverUrl', $naver->getLoginURL($returnUrl)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
| $siteLink = new SiteLink(); | |
변경 후MemberController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| // redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관 | |
| $naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php'; | |
| $naverState = $naver->createState([ | |
| 'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH), | |
| 'returnUrl' => $returnUrl, | |
| 'naverType' => '', | |
| ]); | |
| $this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
| $siteLink = new SiteLink(); | |
변경 전LoginController.php
| if ($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| $this->setData('naverUrl', $naver->getLoginURL($returnUrl)); | |
| } | |
| $kakaoPolicy = \App::getInstance(KakaoLoginPolicy::class); | |
변경 후LoginController.php
| if ($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| // redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관 | |
| $naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php'; | |
| $naverState = $naver->createState([ | |
| 'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH), | |
| 'returnUrl' => $returnUrl, | |
| 'naverType' => '', | |
| ]); | |
| $this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState)); | |
| } | |
| $kakaoPolicy = \App::getInstance(KakaoLoginPolicy::class); | |
변경 전NaverLoginController.php
| $loginReturnURL = MemberUtil::getLoginReturnURL(); | |
| } | |
| $js = " | |
| if (typeof(window.top.layerSearchArea) == 'object') { | |
| parent.location.href='" . $loginReturnURL . "'; | |
| } else if (window.opener === null) { | |
| location.href='" . $loginReturnURL . "'; | |
| } else { | |
| opener.location.href='" . $loginReturnURL . "'; | |
| self.close(); | |
| } | |
| "; | |
변경 후NaverLoginController.php
| $loginReturnURL = MemberUtil::getLoginReturnURL(); | |
| } | |
| // location.href JS 문자열 컨텍스트 인젝션 방지: URL을 JSON 리터럴로 인코딩 | |
| $loginReturnUrlJs = json_encode((string) $loginReturnURL, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| $js = " | |
| if (typeof(window.top.layerSearchArea) == 'object') { | |
| parent.location.href=" . $loginReturnUrlJs . "; | |
| } else if (window.opener === null) { | |
| location.href=" . $loginReturnUrlJs . "; | |
| } else { | |
| opener.location.href=" . $loginReturnUrlJs . "; | |
| self.close(); | |
| } | |
| "; | |
변경 전AdultController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| $this->setData('naverUrl', $naver->getLoginURL($returnUrl)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 후AdultController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| // redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관 | |
| $request = \App::getInstance('request'); | |
| $naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php'; | |
| $naverState = $naver->createState([ | |
| 'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH), | |
| 'returnUrl' => $returnUrl, | |
| 'naverType' => '', | |
| ]); | |
| $this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 전MemberController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| $this->setData('naverUrl', $naver->getLoginURL($returnUrl)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 후MemberController.php
| if($useNaver) { | |
| $naver = new GodoNaverServerApi(); | |
| $scripts[] = 'gd_naver.js'; | |
| // redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관 | |
| $naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php'; | |
| $naverState = $naver->createState([ | |
| 'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH), | |
| 'returnUrl' => $returnUrl, | |
| 'naverType' => '', | |
| ]); | |
| $this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState)); | |
| } | |
| $this->setData('useNaverLogin', $useNaver); | |
변경 전base_info.php
| <form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data"> | |
| <input type="hidden" name="mode" value="base_info"/> | |
| <input type="hidden" name="mallFaviconTmp" value="<?php echo $data['mallFavicon']; ?>"/> | |
| <input type="hidden" name="stampImageTmp" value="<?php echo $data['stampImage'];?>" /> | |
| <input type="hidden" name="receiptImageTmp" value="<?php echo $data['receiptImage'];?>" /> | |
| <input type="hidden" name="mallSno" value="<?php echo $mallSno;?>" /> | |
| <input type="hidden" name="mallFl" value="<?php echo $mallFl;?>" /> | |
| <div class="page-header js-affix"> | |
| <h3><?php echo end($naviMenu->location); ?> | |
| <small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small> | |
| </h3> | |
| <input type="submit" value="저장" class="btn btn-red"/> | |
| <?php if ($mallCnt > 1) { ?> | |
| <ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;"> | |
| <?php foreach ($mallList as $key => $mall) { ?> | |
| <li role="presentation" class="js-popover <?php echo $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?php echo $mall['mallName']; ?>" data-placement="top"> | |
| <a href="./base_info.php?mallSno=<?php echo $mall['sno']; ?>"> | |
| <span class="flag flag-16 flag-<?php echo $mall['domainFl']?>"></span> | |
| <span class="mall-name"><?php echo $mall['mallName']; ?></span> | |
| </a> | |
| </li> | |
| <?php } ?> | |
| <tr> | |
| <th>쇼핑몰명</th> | |
| <td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>> | |
| <input type="text" name="mallNm" value="<?php echo $data['mallNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th> | |
| <td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>"> | |
| <input type="text" name="mallNmEng" value="<?php echo $data['mallNmEng']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>상단타이틀</th> | |
| <td class="form-inline"> | |
| <input type="text" name="mallTitle" value="<?php echo $data['mallTitle']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th>파비콘</th> | |
| <td class="form-inline"> | |
| <input type="file" name="mallFavicon" <?php echo $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['mallFavicon']) === false) { | |
| <th class="require">쇼핑몰 도메인</th> | |
| <td colspan="3" class="form-inline"> | |
| <span class="font-eng">http://</span> | |
| <input type="text" name="mallDomain" value="<?php echo $data['mallDomain']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-2xl"/> | |
| <input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm"> | |
| <?php if ($mallFl == 'kr'): ?> | |
| <input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm"> | |
| <?php else: ?> | |
| <input type="button" value="도메인 연결/관리" onclick="window.location.href='<?php echo $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm"> | |
| <?php endif; ?> | |
| <div class="notice-info"> | |
| 입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br /> | |
| 실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?php echo $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th class="require">대표카테고리</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="hidden" name="mallCategory" value="<?php echo $data['mallCategory']; ?>" <?php echo $readonly . $disabled; ?>/> | |
| <span id="mallCategory"><?php echo $data['mallCategory']; ?></span> | |
| <input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?php echo $disabled; ?> class="btn btn-gray btn-sm"/> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>상호(회사명)</th> | |
| <td class="form-inline"> | |
| <input type="text" name="companyNm" value="<?php echo $data['companyNm']; ?>" class="form-control width-md"/> | |
| </td> | |
| <th>사업자등록번호</th> | |
| <td class="form-inline"> | |
| <div> | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][0]; ?>" <?php echo $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][1]; ?>" <?php echo $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][2]; ?>" <?php echo $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/> | |
| </div> | |
| <div class="notice-info"> | |
| 인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다. | |
| <tr> | |
| <th>대표자명</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ceoNm" value="<?php echo $data['ceoNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>업태</th> | |
| <td class="form-inline"> | |
| <input type="text" name="service" value="<?php echo $data['service']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| <th>종목</th> | |
| <td class="form-inline"> | |
| <input type="text" name="item" value="<?php echo $data['item']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?php echo gd_isset($checked['email']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?php echo gd_isset($checked['email']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <?php } ?> | |
| <input type="text" id="emailId" name="email[]" value="<?php echo $data['email'][0]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @ | |
| <input type="text" id="email" name="email[]" value="<?php echo $data['email'][1]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/> | |
| <div id="email_domain_y" class="display-inline-block"> | |
| <?php echo gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?> | |
| </div> | |
| <div id="email_domain_n"></div> | |
| <li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li> | |
| <li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li> | |
| <li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다. | |
| <a class="btn-link" href="<?php echo $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a> | |
| </li> | |
| </ul> | |
| </div> | |
| <th>사업장 주소</th> | |
| <td colspan="3"> | |
| <div class="form-inline mgb5"> | |
| <input type="text" name="zonecode" value="<?php echo $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/> | |
| <input type="hidden" name="zipcode" value="<?php echo $data['zipcode']; ?>"/> | |
| <span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?php echo $data['zipcode']; ?>)</span> | |
| <input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/> | |
| </div> | |
| <div class="form-inline"> | |
| <input type="text" name="address" value="<?php echo $data['address']; ?>" class="form-control width-2xl"/> | |
| <input type="text" name="addressSub" value="<?php echo $data['addressSub']; ?>" class="form-control width-2xl"/> | |
| </div> | |
| </td> | |
| </tr> | |
| <td colspan="3"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?php echo gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?php echo gd_isset($checked['unstoringFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/> | |
| <div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['unstoringInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'unstoring') { ?> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?php echo $data['unstoringInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?php echo $data['unstoringInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?php echo gd_isset($checked['returnFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/> | |
| <div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['returnInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'return') { ?> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?php echo $data['returnInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?php echo $data['returnInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?php echo $data['returnInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?php echo $data['returnInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?php echo $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?php echo $data['returnInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <tr> | |
| <th>대표전화</th> | |
| <td class="form-inline"> | |
| <input type="text" name="phone" value="<?php echo $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| <th>팩스번호</th> | |
| <td class="form-inline"> | |
| <input type="text" name="fax" value="<?php echo $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>통신판매신고번호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="onlineOrderSerial" value="<?php echo $data['onlineOrderSerial']; ?>" class="form-control width-sm"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>인감 이미지 등록</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="file" name="stampImage" <?php echo $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['stampImage']) === false) { | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?php echo gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?php echo gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?php echo gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?php echo gd_isset($checked['receiptFl']['n']); ?> />노출 안함 | |
| </label> | |
| </div> | |
| <div id="receiptFl_default" class="mgt10"> | |
| <img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점"> | |
| </div> | |
| <div id="receiptFl_upload" class="mgt10"> | |
| <input type="file" name="receiptImageFile" <?php echo $disabled; ?> class="form-control"/> | |
| <div> | |
| <?php | |
| if (empty($data['receiptImage']) === false) { | |
| echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerPhone" value="<?php echo $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </div> | |
| <div> | |
| <?php | |
| echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerSubPhone" value="<?php echo $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </div> | |
| </td> | |
| <th>팩스번호</th> | |
| echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerFax" value="<?php echo $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?php echo gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?php echo gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <input type="text" id="centerEmailId" name="centerEmail[]" value="<?php echo $data['centerEmail'][0]; ?>" class="form-control width-lg" <?php echo gd_isset($deactivated['centerEmail']); ?>/> @ | |
| <input type="text" id="centerEmail" name="centerEmail[]" value="<?php echo $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?php echo gd_isset($deactivated['centerEmail']); ?> /> | |
| <div id="centerEmail_domain_y" class="display-inline-block"> | |
| <?php echo gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?> | |
| </div> | |
| <div id="centerEmail_domain_n"></div> | |
| </td> | |
| <tr> | |
| <th>운영시간</th> | |
| <td colspan="3" class="form-inline"> | |
| <textarea name="centerHours" rows="4" class="form-control width-2xl"><?php echo $data['centerHours']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>회사소개 내용</th> | |
| <td class="form-inline"> | |
| <textarea name="company" id="editor" class="form-control width100p height400"><?php echo $data['company']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <script type="text/javascript"> | |
| <!-- | |
| var unstoringNo = <?php echo json_encode($data['unstoringInfo']) ?>; | |
| var returnNo = <?php echo json_encode($data['returnInfo']) ?>; | |
| var $mallName = '<?= $mallName ?>'; | |
| var $mallFl = '<?php echo $mallFl; ?>'; | |
| var unstoringFl = '<?= empty($data['unstoringInfo']) ?>'; | |
| var returnFl = '<?= empty($data['returnInfo']) ?>'; | |
| var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>; | |
| } | |
| }); | |
| display_toggle('unstoringFl','<?php echo gd_isset($data['unstoringFl']);?>'); | |
| display_toggle('returnFl','<?php echo gd_isset($data['returnFl']);?>'); | |
| display_toggle('robotsTxt','<?php echo gd_isset($data['robotsFl']);?>'); | |
| display_toggle('receiptFl','<?php echo gd_isset($data['receiptFl']);?>'); | |
| display_toggle_inline('email_domain', '<?php echo gd_isset($data['emailFl']);?>'); | |
| display_toggle_inline('centerEmail_domain', '<?php echo gd_isset($data['centerEmailFl']);?>'); | |
| $('.email_domain').change(function () { | |
| var val = $(this).val() == 'self' ? '' : $(this).val(); | |
| 'unstoringNo' : addressNo, | |
| 'mallName' : $mallName, | |
| 'mallFl' : $mallFl, | |
| 'unstoringFl' : '<?php echo gd_isset($data['unstoringFl']);?>', | |
| 'returnFl' : '<?php echo gd_isset($data['returnFl']);?>', | |
| 'mallSno' : '<?php echo $mallSno;?>' | |
| }; | |
| $.ajax({ | |
변경 후base_info.php
| <form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data"> | |
| <input type="hidden" name="mode" value="base_info"/> | |
| <input type="hidden" name="mallFaviconTmp" value="<?= $data['mallFavicon']; ?>"/> | |
| <input type="hidden" name="stampImageTmp" value="<?= $data['stampImage'];?>" /> | |
| <input type="hidden" name="receiptImageTmp" value="<?= $data['receiptImage'];?>" /> | |
| <input type="hidden" name="mallSno" value="<?= $mallSno;?>" /> | |
| <input type="hidden" name="mallFl" value="<?= $mallFl;?>" /> | |
| <div class="page-header js-affix"> | |
| <h3><?= end($naviMenu->location); ?> | |
| <small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small> | |
| </h3> | |
| <input type="submit" value="저장" class="btn btn-red"/> | |
| <?php if ($mallCnt > 1) { ?> | |
| <ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;"> | |
| <?php foreach ($mallList as $key => $mall) { ?> | |
| <li role="presentation" class="js-popover <?= $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?= $mall['mallName']; ?>" data-placement="top"> | |
| <a href="./base_info.php?mallSno=<?= $mall['sno']; ?>"> | |
| <span class="flag flag-16 flag-<?= $mall['domainFl']?>"></span> | |
| <span class="mall-name"><?= $mall['mallName']; ?></span> | |
| </a> | |
| </li> | |
| <?php } ?> | |
| <tr> | |
| <th>쇼핑몰명</th> | |
| <td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>> | |
| <input type="text" name="mallNm" value="<?= $data['mallNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th> | |
| <td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>"> | |
| <input type="text" name="mallNmEng" value="<?= $data['mallNmEng']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>상단타이틀</th> | |
| <td class="form-inline"> | |
| <input type="text" name="mallTitle" value="<?= $data['mallTitle']; ?>" class="form-control width-lg"/> | |
| </td> | |
| <th>파비콘</th> | |
| <td class="form-inline"> | |
| <input type="file" name="mallFavicon" <?= $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['mallFavicon']) === false) { | |
| <th class="require">쇼핑몰 도메인</th> | |
| <td colspan="3" class="form-inline"> | |
| <span class="font-eng">http://</span> | |
| <input type="text" name="mallDomain" value="<?= $data['mallDomain']; ?>" <?= $readonly . $disabled; ?> class="form-control width-2xl"/> | |
| <input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm"> | |
| <?php if ($mallFl == 'kr'): ?> | |
| <input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm"> | |
| <?php else: ?> | |
| <input type="button" value="도메인 연결/관리" onclick="window.location.href='<?= $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm"> | |
| <?php endif; ?> | |
| <div class="notice-info"> | |
| 입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br /> | |
| 실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?= $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th class="require">대표카테고리</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="hidden" name="mallCategory" value="<?= $data['mallCategory']; ?>" <?= $readonly . $disabled; ?>/> | |
| <span id="mallCategory"><?= $data['mallCategory']; ?></span> | |
| <input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?= $disabled; ?> class="btn btn-gray btn-sm"/> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>상호(회사명)</th> | |
| <td class="form-inline"> | |
| <input type="text" name="companyNm" value="<?= $data['companyNm']; ?>" class="form-control width-md"/> | |
| </td> | |
| <th>사업자등록번호</th> | |
| <td class="form-inline"> | |
| <div> | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][0]; ?>" <?= $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][1]; ?>" <?= $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> - | |
| <input type="text" name="businessNo[]" value="<?= $data['businessNo'][2]; ?>" <?= $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/> | |
| </div> | |
| <div class="notice-info"> | |
| 인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다. | |
| <tr> | |
| <th>대표자명</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ceoNm" value="<?= $data['ceoNm']; ?>" class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>업태</th> | |
| <td class="form-inline"> | |
| <input type="text" name="service" value="<?= $data['service']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| <th>종목</th> | |
| <td class="form-inline"> | |
| <input type="text" name="item" value="<?= $data['item']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?= gd_isset($checked['email']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?= gd_isset($checked['email']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <?php } ?> | |
| <input type="text" id="emailId" name="email[]" value="<?= $data['email'][0]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @ | |
| <input type="text" id="email" name="email[]" value="<?= $data['email'][1]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/> | |
| <div id="email_domain_y" class="display-inline-block"> | |
| <?= gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?> | |
| </div> | |
| <div id="email_domain_n"></div> | |
| <li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li> | |
| <li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li> | |
| <li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다. | |
| <a class="btn-link" href="<?= $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a> | |
| </li> | |
| </ul> | |
| </div> | |
| <th>사업장 주소</th> | |
| <td colspan="3"> | |
| <div class="form-inline mgb5"> | |
| <input type="text" name="zonecode" value="<?= $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/> | |
| <input type="hidden" name="zipcode" value="<?= $data['zipcode']; ?>"/> | |
| <span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?= $data['zipcode']; ?>)</span> | |
| <input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/> | |
| </div> | |
| <div class="form-inline"> | |
| <input type="text" name="address" value="<?= $data['address']; ?>" class="form-control width-2xl"/> | |
| <input type="text" name="addressSub" value="<?= $data['addressSub']; ?>" class="form-control width-2xl"/> | |
| </div> | |
| </td> | |
| </tr> | |
| <td colspan="3"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?= gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?= gd_isset($checked['unstoringFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/> | |
| <div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['unstoringInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'unstoring') { ?> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?= $data['unstoringInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?= $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?= $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?= $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?= $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?= $data['unstoringInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?= gd_isset($checked['returnFl']['new']); ?> />주소 등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/> | |
| <div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>"> | |
| <?php foreach ($data['returnInfo'] as $k => $v) { ?> | |
| <?php if ($v['addressFl'] == 'return') { ?> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?= $data['returnInfo'][$k]['sno'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?= $data['returnInfo'][$k]['unstoringZonecode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?= $data['returnInfo'][$k]['unstoringZipcode'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?= $data['returnInfo'][$k]['unstoringAddress'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?= $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/> | |
| <input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?= $data['returnInfo'][$k]['mainFl'] ?>"/> | |
| <table class="table table-cols" value="<?= $v['sno'] ?>"> | |
| <colgroup> | |
| <col class="width-md"/> | |
| <tr> | |
| <th>대표전화</th> | |
| <td class="form-inline"> | |
| <input type="text" name="phone" value="<?= $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| <th>팩스번호</th> | |
| <td class="form-inline"> | |
| <input type="text" name="fax" value="<?= $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>통신판매신고번호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="onlineOrderSerial" value="<?= $data['onlineOrderSerial']; ?>" class="form-control width-sm"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>전자상거래업자 부호</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="text" name="ecommerceBusinessCode" value="<?= $data['ecommerceBusinessCode']; ?>" <?= $readonly . $disabled; ?> maxlength="30" class="form-control width-sm"/> | |
| <div class="notice-info"> | |
| 전자상거래업자 부호는 관세청 UNI-PASS 에서 신청/발급받을 수 있습니다. | |
| </div> | |
| </td> | |
| </tr> | |
| <tr> | |
| <th>인감 이미지 등록</th> | |
| <td colspan="3" class="form-inline"> | |
| <input type="file" name="stampImage" <?= $disabled; ?> class="form-control"/> | |
| <span> | |
| <?php | |
| if (empty($data['stampImage']) === false) { | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?= gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?= gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?= gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?= gd_isset($checked['receiptFl']['n']); ?> />노출 안함 | |
| </label> | |
| </div> | |
| <div id="receiptFl_default" class="mgt10"> | |
| <img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점"> | |
| </div> | |
| <div id="receiptFl_upload" class="mgt10"> | |
| <input type="file" name="receiptImageFile" <?= $disabled; ?> class="form-control"/> | |
| <div> | |
| <?php | |
| if (empty($data['receiptImage']) === false) { | |
| echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerPhone" value="<?= $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/> | |
| </div> | |
| <div> | |
| <?php | |
| echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerSubPhone" value="<?= $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </div> | |
| </td> | |
| <th>팩스번호</th> | |
| echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control'); | |
| } | |
| ?> | |
| <input type="text" name="centerFax" value="<?= $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/> | |
| </td> | |
| </tr> | |
| <tr> | |
| <td colspan="3" class="form-inline"> | |
| <div class="form-inline"> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?= gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일 | |
| </label> | |
| <label class="radio-inline"> | |
| <input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?= gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력 | |
| </label> | |
| </div><br> | |
| <input type="text" id="centerEmailId" name="centerEmail[]" value="<?= $data['centerEmail'][0]; ?>" class="form-control width-lg" <?= gd_isset($deactivated['centerEmail']); ?>/> @ | |
| <input type="text" id="centerEmail" name="centerEmail[]" value="<?= $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?= gd_isset($deactivated['centerEmail']); ?> /> | |
| <div id="centerEmail_domain_y" class="display-inline-block"> | |
| <?= gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?> | |
| </div> | |
| <div id="centerEmail_domain_n"></div> | |
| </td> | |
| <tr> | |
| <th>운영시간</th> | |
| <td colspan="3" class="form-inline"> | |
| <textarea name="centerHours" rows="4" class="form-control width-2xl"><?= $data['centerHours']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <tr> | |
| <th>회사소개 내용</th> | |
| <td class="form-inline"> | |
| <textarea name="company" id="editor" class="form-control width100p height400"><?= $data['company']; ?></textarea> | |
| </td> | |
| </tr> | |
| </table> | |
| <script type="text/javascript"> | |
| <!-- | |
| var unstoringNo = <?= json_encode($data['unstoringInfo']) ?>; | |
| var returnNo = <?= json_encode($data['returnInfo']) ?>; | |
| var $mallName = '<?= $mallName ?>'; | |
| var $mallFl = '<?= $mallFl; ?>'; | |
| var unstoringFl = '<?= empty($data['unstoringInfo']) ?>'; | |
| var returnFl = '<?= empty($data['returnInfo']) ?>'; | |
| var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>; | |
| } | |
| }); | |
| display_toggle('unstoringFl','<?= gd_isset($data['unstoringFl']);?>'); | |
| display_toggle('returnFl','<?= gd_isset($data['returnFl']);?>'); | |
| display_toggle('robotsTxt','<?= gd_isset($data['robotsFl']);?>'); | |
| display_toggle('receiptFl','<?= gd_isset($data['receiptFl']);?>'); | |
| display_toggle_inline('email_domain', '<?= gd_isset($data['emailFl']);?>'); | |
| display_toggle_inline('centerEmail_domain', '<?= gd_isset($data['centerEmailFl']);?>'); | |
| $('.email_domain').change(function () { | |
| var val = $(this).val() == 'self' ? '' : $(this).val(); | |
| 'unstoringNo' : addressNo, | |
| 'mallName' : $mallName, | |
| 'mallFl' : $mallFl, | |
| 'unstoringFl' : '<?= gd_isset($data['unstoringFl']);?>', | |
| 'returnFl' : '<?= gd_isset($data['returnFl']);?>', | |
| 'mallSno' : '<?= $mallSno;?>' | |
| }; | |
| $.ajax({ | |
변경 전BuyerInform.php
| // 저장할 필드 | |
| //@formatter:off | |
| $arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',]; | |
| //@formatter:on | |
| // 데이터 조합 | |
변경 후BuyerInform.php
| // 저장할 필드 | |
| //@formatter:off | |
| $arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'ecommerceBusinessCode', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',]; | |
| //@formatter:on | |
| // 데이터 조합 | |
변경 전DBTableField.php
| ['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명 | |
| ['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지 | |
| ['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호 | |
| ['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태 | |
| ['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목 | |
| ['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자 | |
변경 후DBTableField.php
| ['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명 | |
| ['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지 | |
| ['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호 | |
| ['val' => 'ecommerceBusinessCode', 'typ' => 's', 'def' => null, 'name' => '전자상거래업체부호'], // 전자상거래업체부호 | |
| ['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태 | |
| ['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목 | |
| ['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자 | |
변경 전Mall.php
| 'item', | |
| 'email', | |
| 'stampImage', | |
| ]; | |
| /** 설정 공통 사용 **/ | |
| const GLOBAL_MALL_SHARE = [ | |
변경 후Mall.php
| 'item', | |
| 'email', | |
| 'stampImage', | |
| 'ecommerceBusinessCode', | |
| ]; | |
| /** 설정 공통 사용 **/ | |
| const GLOBAL_MALL_SHARE = [ | |
변경 전Policy.php
| 'phone', | |
| 'fax', | |
| 'onlineOrderSerial', | |
| 'unstoringNo', | |
| 'unstoringZonecode', | |
| 'unstoringZipcode', | |
변경 후Policy.php
| 'phone', | |
| 'fax', | |
| 'onlineOrderSerial', | |
| 'ecommerceBusinessCode', | |
| 'unstoringNo', | |
| 'unstoringZonecode', | |
| 'unstoringZipcode', | |
변경 전member_group_appraisal_config.php
| </label> | |
| </th> | |
| <td colspan="3"> | |
| 주문금액, 상품주문건수, 주문상품후기횟수를 종합하여 평가하는 방법입니다. 회원등급별 평가기준을 입력하세요. | |
| <div class="notice-info">수동 평가 선택 시 회원등급이 자동으로 평가되지 않고 <a href="#" class="js-btn-appraisal">[회원등급 수동평가]</a>를 눌러 회원등급을 평가합니다.</div> | |
| </td> | |
| </tr> | |
| <th> | |
| <label class="checkbox-inline"> | |
| <input type="checkbox" name="appraisalPointOrderPriceFl" value="y" <?= gd_isset($checked['appraisalPointOrderPriceFl']['y']); ?> data-target-class="terms_p01"/> | |
| 주문금액 | |
| </label> | |
| </th> | |
| <td> | |
| ?> | |
| <label class="checkbox-inline"> | |
| <input type="checkbox" name="<?= $chknm1 ?>" value="y" <?= $checked['apprFigureOrderPriceFl'][$val['sno']]['y'] ?> data-target-class="<?= $checkClass1 ?>"/> | |
| 주문금액 | |
| </label> | |
| <br/> | |
| <label class="checkbox-inline"> | |
| var gd_group_list = { | |
| layerRule: null, | |
| message: { | |
| figure: "현재 설정된 평가방법은 실적수치제이며, 회원등급별 평가기준에 따라 주문금액, 상품주문건수, 주문상품후기횟수를 종합하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?", | |
| point: "현재 설정된 평가방법은 실적점수제이며, 주문금액, 상품주문건수, 주문상품후기횟수, 로그인횟수를 점수로 환산하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?" | |
| } | |
| }; | |
변경 후member_group_appraisal_config.php
| </label> | |
| </th> | |
| <td colspan="3"> | |
| 실결제금액, 상품주문건수, 주문상품후기횟수를 종합하여 평가하는 방법입니다. 회원등급별 평가기준을 입력하세요. | |
| <div class="notice-info">수동 평가 선택 시 회원등급이 자동으로 평가되지 않고 <a href="#" class="js-btn-appraisal">[회원등급 수동평가]</a>를 눌러 회원등급을 평가합니다.</div> | |
| </td> | |
| </tr> | |
| <th> | |
| <label class="checkbox-inline"> | |
| <input type="checkbox" name="appraisalPointOrderPriceFl" value="y" <?= gd_isset($checked['appraisalPointOrderPriceFl']['y']); ?> data-target-class="terms_p01"/> | |
| 실결제금액 | |
| </label> | |
| </th> | |
| <td> | |
| ?> | |
| <label class="checkbox-inline"> | |
| <input type="checkbox" name="<?= $chknm1 ?>" value="y" <?= $checked['apprFigureOrderPriceFl'][$val['sno']]['y'] ?> data-target-class="<?= $checkClass1 ?>"/> | |
| 실결제금액 | |
| </label> | |
| <br/> | |
| <label class="checkbox-inline"> | |
| var gd_group_list = { | |
| layerRule: null, | |
| message: { | |
| figure: "현재 설정된 평가방법은 실적수치제이며, 회원등급별 평가기준에 따라 실결제금액, 상품주문건수, 주문상품후기횟수를 종합하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?", | |
| point: "현재 설정된 평가방법은 실적점수제이며, 실결제금액, 상품주문건수, 주문상품후기횟수, 로그인횟수를 점수로 환산하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?" | |
| } | |
| }; | |
변경 전AdminPanelApiController.php
| $this->addPopupPanelByCos($cosApi, 'memberList'); | |
| } | |
| // 디자인 스킨 리스트 팝업 | |
| if ($this->isDesignSkinListPanel($pageCode)) { | |
| $this->addPopupPanelByCos($cosApi, 'designSkinList'); | |
| 'main' => $cosSectionCodeList['modalPopup']['main'], | |
| 'kakaoAlrim' => $cosSectionCodeList['kakaoAlrim'], | |
| 'memberList' => $cosSectionCodeList['modalPopup']['memberList'], | |
| 'designSkinList' => $cosSectionCodeList['modalPopup']['designSkinList'], | |
| 'mobileDesignSkinList' => $cosSectionCodeList['modalPopup']['mobileDesignSkinList'], | |
| 'bankdaService' => $cosSectionCodeList['modalPopup']['bankdaService'], | |
| return $pageCode['menuCode'] === 'member' && $pageCode['menuFile'] === 'member_list'; | |
| } | |
| /** | |
| * @param array $pageCode | |
| * @return bool | |
변경 후AdminPanelApiController.php
| $this->addPopupPanelByCos($cosApi, 'memberList'); | |
| } | |
| // CRM 그룹 팝업 | |
| if ($this->isCrmGroupPanel($pageCode)) { | |
| $this->addPopupPanelByCos($cosApi, 'crmGroup'); | |
| } | |
| // 디자인 스킨 리스트 팝업 | |
| if ($this->isDesignSkinListPanel($pageCode)) { | |
| $this->addPopupPanelByCos($cosApi, 'designSkinList'); | |
| 'main' => $cosSectionCodeList['modalPopup']['main'], | |
| 'kakaoAlrim' => $cosSectionCodeList['kakaoAlrim'], | |
| 'memberList' => $cosSectionCodeList['modalPopup']['memberList'], | |
| 'crmGroup' => $cosSectionCodeList['modalPopup']['crmGroup'], | |
| 'designSkinList' => $cosSectionCodeList['modalPopup']['designSkinList'], | |
| 'mobileDesignSkinList' => $cosSectionCodeList['modalPopup']['mobileDesignSkinList'], | |
| 'bankdaService' => $cosSectionCodeList['modalPopup']['bankdaService'], | |
| return $pageCode['menuCode'] === 'member' && $pageCode['menuFile'] === 'member_list'; | |
| } | |
| /** | |
| * CRM 그룹 페이지 | |
| * - CRM > CRM 관리 > CRM 그룹 (crm/crm/crmGroup) | |
| * @param array $pageCode | |
| * @return bool | |
| */ | |
| private function isCrmGroupPanel(array $pageCode): bool | |
| { | |
| return $pageCode['menuCode'] === 'crm' && $pageCode['menuFile'] === 'crm_group'; | |
| } | |
| /** | |
| * @param array $pageCode | |
| * @return bool | |