본문으로 건너뛰기

소스 변경 전체 보기

PHP 8 · GODO26
변경 파일 수: 59개
변경 파일 목록
총 59개
우체국 반품 수거 연동
epost_return_request_result.phpAdmin/order/epost_return_request/epost_return_request_search.phpAdmin/order/epost_return_request/epost_return_request.phpAdmin/order/inc_claim_view_exchange.phpAdmin/order/inc_claim_view.phpAdmin/order/inc_order_view.phpAdmin/order/layout_order_claim_list.phpAdmin/order/layout_order_goods_list.phpAdmin/order/layout_order_view_status_back.phpAdmin/order/popup_epost_apply_frame.phpAdmin/order/post_request.phpAdmin/order/DBTableField.phpComponent/Database/Delivery.phpComponent/Delivery/EpostReturnAdmin.phpComponent/Order/EpostReturnRequestValidator.phpComponent/Order/OrderAdminNew.phpComponent/Order/OrderAdmin.phpComponent/Order/OrderNew.phpComponent/Order/Order.phpComponent/Order/ReOrderCalculation.phpComponent/Order/ReturnInvoice.phpComponent/Order/EpostReturnRequestController.phpController/Admin/Order/EpostReturnRequestPsController.phpController/Admin/Order/OrderChangePsController.phpController/Admin/Order/OrderPsController.phpController/Admin/Order/PostIssueController.phpController/Admin/Order/PostPsController.phpController/Admin/Order/PostRequestController.phpController/Admin/Order/PostReserveController.phpController/Admin/Order/CancelListController.phpController/Front/Mypage/IndexController.phpController/Front/Mypage/OrderListController.phpController/Front/Mypage/OrderViewController.phpController/Front/Mypage/RefundListController.phpController/Front/Mypage/OrderListController.phpController/Mobile/Mypage/OrderViewController.phpController/Mobile/Mypage/EpostReturnSearchDTO.phpDTO/Order/OrderGiftRepository.phpRepository/Order/OrderGoodsRepository.phpRepository/Order/_order_goods_list.htmldata/skin/front/moment/mypage/_reorder_goods_list.htmldata/skin/front/moment/mypage/_order_goods_list.htmldata/skin/front/mygreek/mypage/_reorder_goods_list.htmldata/skin/front/mygreek/mypage/_order_goods_list.htmldata/skin/front/story_g/mypage/_reorder_goods_list.htmldata/skin/front/story_g/mypage/_order_goods_list.htmldata/skin/mobile/moment/mypage/_reorder_goods_list.htmldata/skin/mobile/moment/mypage/_order_goods_list.htmldata/skin/mobile/mygreek/mypage/_reorder_goods_list.htmldata/skin/mobile/mygreek/mypage/_order_goods_list.htmldata/skin/mobile/story_g/mypage/_reorder_goods_list.htmldata/skin/mobile/story_g/mypage/
통계 > 주문 분석 도움말에 5년 이전 주문 구매자수 안내 문구 추가
order_age.phpAdmin/statistics/order_area.phpAdmin/statistics/order_gender.phpAdmin/statistics/order.phpAdmin/statistics/
변경 파일 코드
추가삭제
우체국 반품 수거 연동
Admin/order/epost_return_request/epost_return_request_result.php+114
변경 전epost_return_request_result.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후epost_return_request_result.php
<div class="ncua-data-grid ncua-data-grid--search-result">
<div class="ncua-data-grid__summary"><span id="epr-summary-text">검색 <strong class="ncua-data-grid__summary-count"><?= number_format($total) ?></strong>개 / 전체 <strong class="ncua-data-grid__summary-count"><?= number_format($grandTotal) ?></strong>개</span></div>
<div class="ncua-data-grid__table-wrapper">
<div class="ncua-data-grid__action-bar ncua-data-grid__action-bar--top ncua-data-grid__action-bar--space-between">
<div class="ncua-data-grid__action-bar-group">
<span class="epr-leadin"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.667" d="M10 3 4.5 8.5 2 6"></path></svg>선택한 주문을</span>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-epr-request"><span class="ncua-btn__label">수거 신청</span></button>
</div>
<div class="ncua-data-grid__action-bar-group ncua-data-grid__action-bar-group--right">
<div id="ncua-select-box-sort"></div>
<div id="ncua-select-box-pagesize"></div>
</div>
</div>
<div class="ncua-data-grid__table">
<div class="ncua-table-wrapper" style="--ncua-table-header-height:40px;--ncua-table-default-min-width:1140px">
<div class="ncua-table ncua-table--in-data-grid ncua-table--horizontal ncua-table--hoverable ncua-table--selectable ncua-border-radius-none">
<div class="ncua-table__h-scroll-container">
<div class="ncua-table__h-scroll-inner" style="--ncua-table-min-width:1500px">
<table class="ncua-table__table" role="table">
<colgroup>
<col style="width:44px"/>
<col style="width:56px"/>
<col style="width:110px"/>
<col style="width:170px"/>
<col style="width:110px"/>
<col style="width:80px"/><col/>
<col style="width:150px"/>
<col style="width:130px"/>
<col style="width:80px"/>
<col style="width:130px"/>
<col style="width:110px"/>
<col style="width:90px"/>
</colgroup>
<thead class="ncua-table__header">
<tr class="ncua-table__row">
<th class="ncua-table__header-cell"><label class="ncua-checkbox-field ncua-checkbox-field--sm"><span class="ncua-checkbox-input ncua-checkbox-input--sm ncua-checkbox-field__input"><input type="checkbox"<?= empty($rows) ? ' disabled' : '' ?>/></span></label></th>
<th class="ncua-table__header-cell">번호</th>
<th class="ncua-table__header-cell">주문일시</th>
<th class="ncua-table__header-cell">주문번호</th>
<th class="ncua-table__header-cell">주문자</th>
<th class="ncua-table__header-cell">상품주문번호</th>
<th class="ncua-table__header-cell">주문상품</th>
<th class="ncua-table__header-cell">사은품</th>
<th class="ncua-table__header-cell">처리상태</th>
<th class="ncua-table__header-cell">배송번호</th>
<th class="ncua-table__header-cell">송장번호</th>
<th class="ncua-table__header-cell">공급사</th>
<th class="ncua-table__header-cell">수령자</th>
</tr>
</thead>
<tbody class="ncua-table__body">
<?php if (empty($rows)) : ?>
<tr class="epr-empty-row"><td class="ncua-table__empty" colspan="13" role="status" aria-live="polite"><div class="ncua-empty-state"><span class="ncua-empty-state__description">검색된 주문이 없습니다.</span></div></td></tr>
<?php else : ?>
<?php foreach ($groups as $gKey => $group) : ?>
<?php foreach ($group['rows'] as $row) : ?>
<tr class="ncua-table__row" data-group="g<?= $gKey ?>" data-sno="<?= (int)$row['sno'] ?>">
<td class="ncua-table__cell"><label class="ncua-checkbox-field ncua-checkbox-field--sm"><span class="ncua-checkbox-input ncua-checkbox-input--sm ncua-checkbox-field__input"><input type="checkbox" value="<?= (int)$row['sno'] ?>"/></span></label></td>
<td class="ncua-table__cell"><?= $row['rowNo'] ?></td>
<?php if ($row['isFirst']) : ?>
<td class="ncua-table__cell"<?= $row['groupRowCount'] > 1 ? ' rowspan="' . $row['groupRowCount'] . '"' : '' ?>><?= gd_date_format('Y-m-d', $row['regDt']) ?><br><?= gd_date_format('H:i', $row['regDt']) ?></td>
<td class="ncua-table__cell"<?= $row['groupRowCount'] > 1 ? ' rowspan="' . $row['groupRowCount'] . '"' : '' ?>><span class="epr-ordno"><?php if ($row['firstSaleFl'] === 'y') : ?><span class="ncua-badge ncua-badge--pill-outline ncua-badge--blue ncua-badge--xs"><span class="ncua-badge__label">첫주문</span></span><?php endif; ?><a class="epr-link" href="./order_view.php?orderNo=<?= urlencode($row['orderNo']) ?>" target="_blank"><?= htmlspecialchars($row['orderNo']) ?></a></span></td>
<td class="ncua-table__cell"<?= $row['groupRowCount'] > 1 ? ' rowspan="' . $row['groupRowCount'] . '"' : '' ?>><?= $row['orderName'] ?><br>(<?php if ($row['memNo']) : ?><a class="epr-link epr-link--plain js-layer-crm" href="#" data-member-no="<?= (int)$row['memNo'] ?>" title="회원 관리 팝업"><?= $row['memId'] ?></a><?php else : ?>비회원<?php endif; ?>)</td>
<?php endif; ?>
<td class="ncua-table__cell"><a class="epr-link" href="./order_view.php?orderNo=<?= urlencode($row['orderNo']) ?>" target="_blank"><?= (int)$row['sno'] ?></a></td>
<td class="ncua-table__cell epr-cell--goods">
<div class="epr-goods">
<a class="epr-goods__thumb" href="<?= URI_HOME ?>goods/goods_view.php?goodsNo=<?= (int)$row['goodsNo'] ?>" target="_blank" title="쇼핑몰 상품 상세 새 탭" aria-label="상품 이미지"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="none" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M8 6a3 3 0 1 0 0 6 3 3 0 0 0 0-6" clip-rule="evenodd"></path><path fill="currentColor" fill-rule="evenodd" d="M17.241 2H6.76c-.805 0-1.47 0-2.01.044-.563.046-1.08.145-1.565.392a4 4 0 0 0-1.748 1.748c-.247.485-.346 1.002-.392 1.564C1 6.29 1 6.954 1 7.758v8.483c0 .805 0 1.47.044 2.01.046.563.145 1.08.392 1.565a4 4 0 0 0 1.748 1.748c.485.247 1.002.346 1.564.392q.178.015.374.023l.022.002c.214.019.48.019.75.019h12.212c.27 0 .536 0 .75-.019l.022-.002q.195-.01.374-.023c.562-.046 1.079-.145 1.564-.392a4 4 0 0 0 1.748-1.748c.247-.485.346-1.002.392-1.564.044-.541.044-1.206.044-2.01V7.758c0-.805 0-1.47-.044-2.01-.046-.563-.145-1.08-.392-1.565a4 4 0 0 0-1.748-1.748c-.485-.247-1.002-.346-1.564-.392C18.71 2 18.046 2 17.242 2"></path></svg><?php if (!empty($row['goodsImageSrc'])) : ?><img src="<?= $row['goodsImageSrc'] ?>" alt="<?= htmlspecialchars($row['goodsNm']) ?>"/><?php endif; ?></a>
<span class="epr-goods__info">
<a class="epr-goods__name epr-goods__name--link js-goods-modify" href="#" data-goods-no="<?= (int)$row['goodsNo'] ?>" title="상품 수정 팝업"><?= htmlspecialchars($row['goodsNm']) ?></a>
<?php if ($row['optionText'] !== '') : ?><span class="epr-goods__option"><?= htmlspecialchars($row['optionText']) ?></span><?php endif; ?>
</span>
</div>
</td>
<?php if ($row['isFirst']) : ?>
<td class="ncua-table__cell"<?= $row['groupRowCount'] > 1 ? ' rowspan="' . $row['groupRowCount'] . '"' : '' ?>><?= empty($row['giftLines']) ? '-' : implode('<br>', array_map('htmlspecialchars', $row['giftLines'])) ?></td>
<?php endif; ?>
<td class="ncua-table__cell">반품접수<?php if ($row['progressLabel'] !== null) : ?><br>(<?= $row['progressLabel'] ?>)<?php endif; ?></td>
<td class="ncua-table__cell"><?= $row['deliverySno'] ? (int)$row['deliverySno'] : '' ?></td>
<td class="ncua-table__cell"><?= htmlspecialchars($row['invoiceNo']) ?></td>
<?php if ($row['scmRowspan'] > 0) : ?>
<td class="ncua-table__cell"<?= $row['scmRowspan'] > 1 ? ' rowspan="' . $row['scmRowspan'] . '"' : '' ?>><?= htmlspecialchars($row['companyNm']) ?></td>
<?php endif; ?>
<?php if ($row['isFirst']) : ?>
<td class="ncua-table__cell"<?= $row['groupRowCount'] > 1 ? ' rowspan="' . $row['groupRowCount'] . '"' : '' ?>><?= $row['receiverName'] ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="ncua-table__h-scrollbar" aria-hidden="true"><div class="ncua-table__h-scrollbar-thumb"></div></div>
</div>
</div>
</div>
<div class="ncua-data-grid__action-bar ncua-data-grid__action-bar--bottom ncua-data-grid__action-bar--space-between">
<?php if (!empty($rows)) : ?>
<div class="ncua-data-grid__action-bar-group">
<span class="epr-leadin"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.667" d="M10 3 4.5 8.5 2 6"></path></svg>선택한 주문을</span>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray js-epr-request"><span class="ncua-btn__label">수거 신청</span></button>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="ncua-data-grid__pagination">
<div class="ncua-pagination ncua-pagination--pc" role="navigation" aria-label="pagination">
<div class="pagination"><?= $pagination ?></div>
<p class="ncua-pagination__page-info"><em class="ncua-pagination__current-num"><?= $page ?></em> / <?= $totalPages ?></p>
</div>
</div>
</div>
 
Admin/order/epost_return_request/epost_return_request_search.php+80
변경 전epost_return_request_search.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후epost_return_request_search.php
<div class="epr-filter">
<div class="ncua-table-wrapper" style="--ncua-table-header-height:40px;--ncua-table-default-min-width:1140px">
<div class="ncua-table ncua-table--vertical">
<table class="ncua-table__table" role="table">
<tbody class="ncua-table__body">
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell"><div>공급사 구분</div></th>
<td class="ncua-table__cell">
<div class="epr-search__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="epr-supplier" value="all"/></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="epr-supplier" value="0"/></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="epr-supplier" value="1"/></span>
<span><span class="ncua-radio-field__text">공급사</span></span>
</label>
<div class="epr-search__supplier" id="epr-supplier-combo-wrap" hidden><div id="ncua-combo-box-supplier"></div></div>
</div>
<div id="epr-supplier-tags" class="ncua-combobox__tags"></div>
</td>
</tr>
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell"><div>검색어</div></th>
<td class="ncua-table__cell">
<div class="epr-search__row">
<div id="ncua-select-box-searchkey"></div>
<div id="epr-match-wrap" style="display:none"><div id="ncua-select-box-match"></div></div>
<div class="epr-search__input">
<div class="ncua-input ncua-input--xs full-width">
<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="<?= $searchValue('keyword') ?>" placeholder="검색어 전체를 정확히 입력하세요."/></div>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell"><div>기간검색</div></th>
<td class="ncua-table__cell">
<div class="epr-search__row">
<div id="ncua-select-box-datekey"></div>
<div id="ncua-range-date-picker"></div>
</div>
</td>
</tr>
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell"><div>주문상태</div></th>
<td class="ncua-table__cell">
<div class="epr-search__radio-group" id="epr-ordstatus">
<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"<?= empty($collectionStatuses) ? ' checked' : '' ?>/></span>
<span><span class="ncua-checkbox-field__text">전체</span></span>
</label>
<?php foreach ($collectionStatusFields as $statusValue => $statusLabel) : ?>
<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="<?= $statusValue ?>"<?= in_array($statusValue, $collectionStatuses, true) ? ' checked' : '' ?>/></span>
<span><span class="ncua-checkbox-field__text"><?= $statusLabel ?></span></span>
</label>
<?php endforeach; ?>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
 
<div class="epr-searchbar">
<button type="button" class="ncua-btn ncua-btn--xs has-underline ncua-btn--text" id="epr-btn-reset"><span class="ncua-btn__label">초기화</span></button>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary" id="epr-btn-search"><span class="ncua-btn__label">검색</span></button>
</div>
</div>
 
Admin/order/epost_return_request.php+449
변경 전epost_return_request.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후epost_return_request.php
<?php
$searchValue = static fn (string $key, string $default = '') => htmlspecialchars((string)($search[$key] ?? $default), ENT_QUOTES);
?>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/order/epost-return-request.css') ?>">
 
<header class="page-header ncua-page-header js-affix">
<h3><?= end($naviMenu->location); ?></h3>
</header>
 
<article class="ncua-content epr-page">
<form id="frmEprSearch" method="get" action="./epost_return_request.php">
<input type="hidden" name="searchKey" value="<?= $searchValue('searchKey', 'o.orderNo') ?>">
<input type="hidden" name="matchType" value="<?= $searchValue('matchType', 'equalSearch') ?>">
<input type="hidden" name="scmFl" value="<?= $searchValue('scmFl', 'all') ?>">
<input type="hidden" name="treatDateFl" value="<?= $searchValue('treatDateFl', 'oh.regDt.b') ?>">
<input type="hidden" name="treatDateStart" value="<?= $searchValue('treatDateStart') ?>">
<input type="hidden" name="treatDateEnd" value="<?= $searchValue('treatDateEnd') ?>">
<input type="hidden" name="sort" value="<?= $searchValue('sort', 'o.regDt desc') ?>">
<input type="hidden" name="pageSize" value="<?= (int)$pageSize ?>">
<input type="hidden" name="page" value="1">
<span id="epr-scmnos-slot"></span>
<span id="epr-status-slot"></span>
 
<section class="ncua-block-container">
<header class="ncua-block-header">
<div class="ncua-block-header__row">
<div class="ncua-block-header__title-area">
<span class="ncua-block-header__title">우체국택배 수거신청</span>
</div>
<div class="ncua-block-header__action-area"></div>
</div>
</header>
<div class="ncua-block-container__body">
 
<?php include $epostReturnSearch; ?>
 
<div id="epr-result">
<?php include $epostReturnResult; ?>
</div>
 
</div>
</section>
</form>
 
</article>
 
<script>
document.addEventListener('DOMContentLoaded', function () {
var EPR = {
hasWriteAuth: <?= $hasWriteAuth ? 'true' : 'false' ?>,
search: <?= json_encode([
'searchKey' => (string)($search['searchKey'] ?? 'o.orderNo'),
'matchType' => (string)($search['matchType'] ?? 'equalSearch'),
'scmFl' => (string)($search['scmFl'] ?? 'all'),
'scmNos' => $scmNos,
'treatDateFl' => (string)($search['treatDateFl'] ?? 'oh.regDt.b'),
'treatDateStart' => (string)($search['treatDateStart'] ?? ''),
'treatDateEnd' => (string)($search['treatDateEnd'] ?? ''),
'sort' => (string)($search['sort'] ?? 'o.regDt desc'),
'pageSize' => (string)$pageSize,
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>,
suppliers: <?= json_encode(array_map(static fn ($scm) => ['id' => (string)$scm['scmNo'], 'label' => $scm['companyNm']], $suppliers), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
};
var form = document.querySelector('#frmEprSearch');
function setHidden(name, value) { form.querySelector('input[name="' + name + '"]').value = value; }
 
// ── 검색어 항목 + 매칭 (개인정보 항목만 매칭 select 노출)
var FIXED_EXACT = ['o.orderNo', 'og.goodsNo', 'og.goodsCd', 'og.goodsModelNo', 'og.makerNm', 'sm.companyNm'];
var FIXED_PARTIAL = ['og.invoiceNo', 'og.goodsNm'];
var PH_EXACT = '검색어 전체를 정확히 입력하세요.';
var PH_PARTIAL = '검색어에 포함된 내용을 입력하세요.';
var matchWrap = document.querySelector('#epr-match-wrap');
var keywordInput = form.querySelector('input[name="keyword"]');
function matchType() {
if (FIXED_EXACT.indexOf(EPR.search.searchKey) >= 0) return 'equalSearch';
if (FIXED_PARTIAL.indexOf(EPR.search.searchKey) >= 0) return 'fullLikeSearch';
return EPR.search.matchType;
}
function refreshMatchUi() {
var fixed = FIXED_EXACT.indexOf(EPR.search.searchKey) >= 0 || FIXED_PARTIAL.indexOf(EPR.search.searchKey) >= 0;
matchWrap.style.display = fixed ? 'none' : '';
keywordInput.placeholder = matchType() === 'fullLikeSearch' ? PH_PARTIAL : PH_EXACT;
}
new ncua.SelectBox(document.querySelector('#ncua-select-box-searchkey'), {
size: 'xs', value: EPR.search.searchKey,
onChange: function (v) { EPR.search.searchKey = v; refreshMatchUi(); },
options: <?= json_encode($searchKeyOptions, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
});
new ncua.SelectBox(document.querySelector('#ncua-select-box-match'), {
size: 'xs', value: EPR.search.matchType === 'fullLikeSearch' ? 'fullLikeSearch' : 'equalSearch',
options: <?= json_encode($matchTypeOptions, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>,
onChange: function (v) { EPR.search.matchType = v; refreshMatchUi(); }
});
refreshMatchUi();
 
// ── 공급사 라디오 + 콤보박스 + 칩 (칩이 선택 목록의 정본, 해제는 칩 X 로만)
var supplierSel = [];
EPR.search.scmNos.forEach(function (no) {
var found = EPR.suppliers.filter(function (s) { return s.id === String(no); })[0];
if (found) supplierSel.push({ id: found.id, label: found.label });
});
var radios = Array.prototype.slice.call(document.querySelectorAll('input[name="epr-supplier"]'));
radios.forEach(function (r) { r.checked = r.value === EPR.search.scmFl; });
if (!radios.some(function (r) { return r.checked; })) radios[0].checked = true;
var comboWrap = document.querySelector('#epr-supplier-combo-wrap');
var tagBox = document.querySelector('#epr-supplier-tags');
var supplierClear = function () {};
(function () {
var host = document.querySelector('#ncua-combo-box-supplier');
if (!host) return;
var combo = new ncua.ComboBox(host, {
size: 'xs', multiple: true, showFooterButtons: true, placeholder: '공급사 선택',
options: EPR.suppliers,
onComplete: function (ids) {
var list = (Array.isArray(ids) ? ids : (ids ? [ids] : [])).map(function (id) {
return EPR.suppliers.filter(function (s) { return s.id === id; })[0];
}).filter(Boolean);
if (!list.length) return;
var fresh = list.filter(function (v) { return !supplierSel.some(function (x) { return x.id === v.id; }); });
if (!fresh.length) { openAlert('동일한 공급사가 이미 존재합니다.', '', 'close'); return; }
fresh.forEach(function (v) { supplierSel.push({ id: v.id, label: v.label }); });
renderTags();
if (fresh.length < list.length) {
ncua.Notification.success('선택한 ' + list.length + '개의 공급사 중 ' + fresh.length + '개의 공급사가 추가되었습니다.', '', { autoClose: 5000 });
}
}
});
supplierClear = function () {
supplierSel = [];
if (combo.setValues) combo.setValues([]);
renderTags();
};
})();
function syncSupplierVisibility() {
var on = (radios.filter(function (r) { return r.checked; })[0] || {}).value === '1';
comboWrap.hidden = !on;
tagBox.hidden = !on;
}
function renderTags() {
tagBox.innerHTML = '';
supplierSel.forEach(function (v) {
var tag = new ncua.Tag({
text: v.label, size: 'sm', close: true,
onButtonClick: function () {
supplierSel = supplierSel.filter(function (x) { return x.id !== v.id; });
renderTags();
}
});
tagBox.appendChild(tag.getElement());
});
syncSupplierVisibility();
}
radios.forEach(function (r) { r.addEventListener('change', syncSupplierVisibility); });
renderTags();
 
// ── 기간검색
new ncua.SelectBox(document.querySelector('#ncua-select-box-datekey'), {
size: 'xs', value: EPR.search.treatDateFl,
onChange: function (v) { EPR.search.treatDateFl = v; },
options: <?= json_encode($treatDateOptions, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
});
var datePickerHost = document.querySelector('#ncua-range-date-picker');
// 재조회 때마다 다시 그려지므로, 지난 검색 범위와 실제로 일치하는 버튼을 매번 다시 찾아 표시함
// (버튼 하나에 isCurrent 고정하면 다른 기간을 선택해도 재조회 후 그 버튼으로 되돌아가 보임)
function fmtYmd(d) {
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
function isPeriodCurrent(period) {
// 첫 진입 기본 7일은 서버가 채워서 내려줌 — 여기는 실제 검색 범위와 일치하는 버튼만 표시함
if (!EPR.search.treatDateStart || !EPR.search.treatDateEnd) return false;
var today = new Date();
today.setHours(0, 0, 0, 0);
if (EPR.search.treatDateEnd !== fmtYmd(today)) return false;
var start = new Date(today);
start.setDate(start.getDate() - period);
return EPR.search.treatDateStart === fmtYmd(start);
}
var datePicker = new ncua.DatePicker(datePickerHost, {
size: 'xs',
// mode 'single' 명시 — 기본 'range'로 두면 flatpickr가 선택 진행 중 상태로 남아 커서를 따라 헛 프리뷰 밴드가 칠해짐
datePickerOptions: [
{ element: 'start-date', placeholder: 'YYYY-MM-DD', options: { mode: 'single', static: true, dateFormat: 'Y-m-d', clickOpens: true, allowInvalidPreload: true, locale: 'ko', defaultDate: EPR.search.treatDateStart || undefined } },
{ element: 'end-date', placeholder: 'YYYY-MM-DD', options: { mode: 'single', static: true, dateFormat: 'Y-m-d', clickOpens: true, allowInvalidPreload: true, locale: 'ko', defaultDate: EPR.search.treatDateEnd || undefined } }
],
buttons: [
{ text: '오늘', period: 0, unit: 'days', isCurrent: isPeriodCurrent(0) },
{ text: '7일', period: 6, unit: 'days', isCurrent: isPeriodCurrent(6) },
{ text: '15일', period: 14, unit: 'days', isCurrent: isPeriodCurrent(14) },
{ text: '1개월', period: 29, unit: 'days', isCurrent: isPeriodCurrent(29) },
{ text: '3개월', period: 89, unit: 'days', isCurrent: isPeriodCurrent(89) },
{ text: '1년', period: 364, unit: 'days', isCurrent: isPeriodCurrent(364) }
]
});
// 지난 검색 기간 복원 — 버튼과 무관한 수동 기간은 defaultDate 가 입력창에 반영되지 않아 직접 채움
(function () {
var dateInputs = datePickerHost.querySelectorAll('input[type="text"]');
if (dateInputs[0] && EPR.search.treatDateStart) dateInputs[0].value = EPR.search.treatDateStart;
if (dateInputs[1] && EPR.search.treatDateEnd) dateInputs[1].value = EPR.search.treatDateEnd;
})();
 
// ── 주문상태 전체 ↔ 개별 토글
(function () {
var box = document.querySelector('#epr-ordstatus');
var all = box.querySelector('input[value="all"]');
var items = Array.prototype.slice.call(box.querySelectorAll('input:not([value="all"])'));
all.addEventListener('change', function () {
if (all.checked) { items.forEach(function (c) { c.checked = false; }); }
else if (!items.some(function (c) { return c.checked; })) { all.checked = true; }
});
items.forEach(function (c) {
c.addEventListener('change', function () {
if (c.checked) { all.checked = false; }
if (!items.some(function (x) { return x.checked; })) { all.checked = true; }
});
});
})();
 
// ── 검색 실행 — 서버 재조회 (판정·필터는 전부 서버)
function readDates() {
// datePickerHost.querySelectorAll('input')는 static 캘린더 내부의 연도 이동 input(type=number)까지 잡힘 — type=text만 골라 읽음.
// datePicker.getDates()는 defaultDate로 채워진 값(재조회 시 서버가 되돌려준 이전 검색값)은 추적 못 해 재조회마다 비어버려서 쓰지 않음
var textInputs = datePickerHost.querySelectorAll('input[type="text"]');
EPR.search.treatDateStart = textInputs[0] ? textInputs[0].value : '';
EPR.search.treatDateEnd = textInputs[1] ? textInputs[1].value : '';
}
function submitSearch(page) {
readDates();
var checkedRadio = radios.filter(function (r) { return r.checked; })[0];
setHidden('searchKey', EPR.search.searchKey);
setHidden('matchType', matchType());
setHidden('scmFl', checkedRadio ? checkedRadio.value : 'all');
setHidden('treatDateFl', EPR.search.treatDateFl);
setHidden('treatDateStart', EPR.search.treatDateStart);
setHidden('treatDateEnd', EPR.search.treatDateEnd);
setHidden('sort', EPR.search.sort);
setHidden('pageSize', EPR.search.pageSize);
setHidden('page', page);
var scmSlot = document.querySelector('#epr-scmnos-slot');
scmSlot.innerHTML = '';
if ((checkedRadio || {}).value === '1') {
supplierSel.forEach(function (v) {
var input = document.createElement('input');
input.type = 'hidden'; input.name = 'scmNos[]'; input.value = v.id;
scmSlot.appendChild(input);
});
}
var statusSlot = document.querySelector('#epr-status-slot');
statusSlot.innerHTML = '';
Array.prototype.slice.call(document.querySelectorAll('#epr-ordstatus input:not([value="all"])')).forEach(function (c) {
if (!c.checked) return;
var input = document.createElement('input');
input.type = 'hidden'; input.name = 'collectionStatuses[]'; input.value = c.value;
statusSlot.appendChild(input);
});
form.submit();
}
// ── 수거 신청 처리 후 재조회 — 스크롤 위치를 유지해야 해서 검색과 달리 전체 이동 없이 결과 영역만 fetch
function refreshResult() {
var params = new URLSearchParams(new FormData(form));
fetch('./epost_return_request.php?' + params.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
})
.then(function (html) {
document.querySelector('#epr-result').innerHTML = html;
bindResultEvents();
})
.catch(function () {
openAlert('조회에 실패하였습니다.', '잠시 후 다시 시도해 주세요.', 'close');
});
}
document.querySelector('#epr-btn-search').addEventListener('click', function () { submitSearch(1); });
document.querySelector('#epr-btn-reset').addEventListener('click', function () {
supplierClear();
location.href = './epost_return_request.php';
});
// 공용 Page 컴포넌트가 부르는 페이지 이동 — PAGELINK 치환값(page=N&쿼리)에서 번호만 취해 폼 상태 유지 재조회
window.goEprPage = function (link) {
var matched = /(?:^|&)page=(\d+)/.exec(String(link));
submitSearch(matched ? parseInt(matched[1], 10) : 1);
};
 
// ── 그리드: hover 그룹 강조 · 선택 강조 · 전체선택 · [수거 신청] 버튼 — #epr-result 재조회 시마다 다시 바인딩
var tableBody, headCheck, rowChecks = [];
function syncChecks() {
var on = rowChecks.filter(function (c) { return c.checked; }).length;
if (headCheck) {
headCheck.checked = rowChecks.length > 0 && on === rowChecks.length;
headCheck.indeterminate = on > 0 && on < rowChecks.length;
}
var groupOn = {};
rowChecks.forEach(function (c) {
var tr = c.closest('tr');
tr.classList.toggle('ncua-table__row--selected', c.checked);
if (!(tr.dataset.group in groupOn)) groupOn[tr.dataset.group] = false;
if (c.checked) groupOn[tr.dataset.group] = true;
});
Object.keys(groupOn).forEach(function (g) {
tableBody.querySelectorAll('tr[data-group="' + g + '"] > td[rowspan]').forEach(function (td) {
td.classList.toggle('epr-cell--group-selected', groupOn[g]);
});
});
}
function selectedSnos() {
return rowChecks.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
}
function bindResultEvents() {
tableBody = document.querySelector('.ncua-data-grid__table .ncua-table__body');
if (tableBody) {
var clearAll = function () {
tableBody.querySelectorAll('td.epr-cell-hover').forEach(function (td) { td.classList.remove('epr-cell-hover'); });
};
var mark = function (tr) {
clearAll();
tr.querySelectorAll(':scope > td').forEach(function (td) { td.classList.add('epr-cell-hover'); });
tableBody.querySelectorAll('tr[data-group="' + tr.dataset.group + '"]').forEach(function (row) {
row.querySelectorAll(':scope > td[rowspan]').forEach(function (td) { td.classList.add('epr-cell-hover'); });
});
};
tableBody.addEventListener('mouseover', function (e) {
var tr = e.target.closest('tr[data-group]');
if (tr) mark(tr);
});
tableBody.addEventListener('mouseleave', clearAll);
}
headCheck = document.querySelector('.ncua-data-grid__table thead input[type="checkbox"]');
rowChecks = Array.prototype.slice.call(document.querySelectorAll('.ncua-data-grid__table tbody input[type="checkbox"]'));
if (headCheck) {
headCheck.addEventListener('change', function () {
rowChecks.forEach(function (c) { c.checked = headCheck.checked; });
syncChecks();
});
}
rowChecks.forEach(function (c) { c.addEventListener('change', syncChecks); });
Array.prototype.slice.call(document.querySelectorAll('.js-epr-request')).forEach(function (btn) {
btn.addEventListener('click', function () {
if (!EPR.hasWriteAuth) {
openAlert('우체국택배 수거신청의 쓰기 권한이 없습니다.', '권한은 대표운영자에게 문의하시기 바랍니다.', 'close');
return;
}
var snos = selectedSnos();
if (!snos.length) {
openAlert('수거 신청할 주문을 선택해 주세요.', '', 'close');
return;
}
NCDSConfirm({ message: '선택한 ' + snos.length + '건을 수거 신청하시겠습니까?' }).then(function (ok) {
if (ok) requestPickup();
});
});
});
// 정렬·N개 보기 — 결과 영역과 함께 매번 새로 그려지므로 재조회 때마다 다시 초기화
var sortBox = document.querySelector('#ncua-select-box-sort');
if (sortBox) {
new ncua.SelectBox(sortBox, {
size: 'xs', value: EPR.search.sort,
onChange: function (v) { EPR.search.sort = v; submitSearch(1); },
options: <?= json_encode($sortOptions, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
});
}
var pageSizeBox = document.querySelector('#ncua-select-box-pagesize');
if (pageSizeBox) {
new ncua.SelectBox(pageSizeBox, {
size: 'xs', value: EPR.search.pageSize,
onChange: function (v) { EPR.search.pageSize = v; submitSearch(1); },
options: <?= json_encode($pageSizeOptions, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
});
}
}
bindResultEvents();
 
// ── 알림 — 공용 NCDSAlert·NCDSConfirm(head.php 전역). [확인] 후 동작은 케이스별로 갈림
function openAlert(title, subtitle, after) {
NCDSAlert({
message: title,
subMessage: subtitle || '',
iconType: 'error',
callback: function () {
if (after === 'reload') refreshResult();
}
});
}
 
// ── [수거 신청] — 검사 순서: 쓰기 권한 → 선택 여부 → 확인 다이얼로그 → 서버 검증 (버튼 클릭 바인딩은 bindResultEvents 참고)
var requesting = false;
function requestPickup() {
if (requesting) {
openAlert('수거 신청을 처리하고 있습니다.', '처리가 완료될 때까지 잠시 기다려 주세요.', 'close');
return;
}
requesting = true;
var submittedSnos = selectedSnos();
var params = new URLSearchParams();
params.append('mode', 'pickupRequest');
submittedSnos.forEach(function (sno) { params.append('orderGoodsSnos[]', sno); });
fetch('./epost_return_request_ps.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
body: params.toString()
}).then(function (res) { return res.json(); }).then(function (data) {
requesting = false;
if (data.code === 200) {
// 선점 제외(다른 요청이 처리 중)는 실패가 아니라 별도 안내로 처리함 — 실패로 묶으면 진행 중인 건에 재신청을 유도하게 됨
var success = data.successCount || 0;
var failure = data.failureCount || 0;
var excluded = data.excludedCount || 0;
if (failure > 0 && success > 0) {
openAlert('성공 ' + success + '건 / 실패 ' + failure + '건 처리되었습니다.', '실패한 주문을 선택해 다시 [수거 신청]해 주세요.', 'reload');
} else if (failure > 0) {
openAlert('수거 신청에 실패하였습니다.', '잠시 후 다시 시도해 주세요.', 'reload');
} else if (excluded > 0) {
openAlert('수거 신청을 처리하고 있습니다.', '처리가 완료될 때까지 잠시 기다려 주세요.', 'reload');
} else if (success > 0) {
ncua.Notification.success('수거 신청되었습니다.', '반품 리스트에서 수거 신청된 주문을 확인해 주세요.', { autoClose: 5000 });
refreshResult();
} else {
// 선점도 실패도 성공도 없음 — 응답에 배정·실패가 모두 비어 돌아온 경우라 성공으로 안내하면 안 됨
openAlert('수거 신청에 실패하였습니다.', '잠시 후 다시 시도해 주세요.', 'reload');
}
return;
}
var after = (data.reason === 'NOT_ELIGIBLE' || data.reason === 'DUPLICATED' || data.reason === 'ERROR') ? 'reload' : 'close';
openAlert(data.message || '수거 신청에 실패하였습니다.', data.subMessage || '', after);
}).catch(function () {
requesting = false;
openAlert('수거 신청에 실패하였습니다.', '잠시 후 다시 시도해 주세요.', 'reload');
});
}
 
// ── 상품 수정 팝업 (기존 어드민 전례 창) · 회원 관리 팝업(.js-layer-crm)은 common.js 전역 바인딩을 그대로 씀
document.addEventListener('click', function (e) {
var goods = e.target.closest('.js-goods-modify');
if (goods) {
e.preventDefault();
window.open('../goods/goods_register.php?goodsNo=' + goods.dataset.goodsNo, 'goodsModify', 'width=1400,height=900,scrollbars=yes');
}
});
});
</script>
<script type="text/javascript">
const code = '260921002';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>
 
Admin/order/inc_claim_view_exchange.php+1-1
변경 전inc_claim_view_exchange.php
<?= $val['handleRegDt']; ?>
</td>
<?php } ?>
<td class="center"><?= $val['orderStatusStr']; ?></td>
<td class="border-left text-center"><?= $val['companyNm']; ?></td>
<td>
<?php if ($val['goodsType'] === 'addGoods') { ?>
 
변경 후inc_claim_view_exchange.php
<?= $val['handleRegDt']; ?>
</td>
<?php } ?>
<td class="center"><?= $val['orderStatusStr']; ?><?= $val['returnStatusStr']; ?></td>
<td class="border-left text-center"><?= $val['companyNm']; ?></td>
<td>
<?php if ($val['goodsType'] === 'addGoods') { ?>
 
Admin/order/inc_claim_view.php+1-1
변경 전inc_claim_view.php
<?php if ($val['beforeStatusStr'] && $statusMode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<p><?= $val['orderStatusStr']; ?></p>
</td>
<?php if ($rowScm == 0) { ?>
<td <?=$orderScmRowSpan?> class="border-left text-center"><?= $val['companyNm']; ?></td>
 
변경 후inc_claim_view.php
<?php if ($val['beforeStatusStr'] && $statusMode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<p><?= $val['orderStatusStr']; ?><?= $val['returnStatusStr']; ?></p>
</td>
<?php if ($rowScm == 0) { ?>
<td <?=$orderScmRowSpan?> class="border-left text-center"><?= $val['companyNm']; ?></td>
 
Admin/order/inc_order_view.php+7
변경 전inc_order_view.php
<p>
<?= $val['orderStatusStr']; ?>
<input type="button" data-sno="<?= $val['sno']; ?>" data-name="<?= $val['goodsNm']; ?>" value="로그" class="btn btn-sm btn-white js-order-log"/>
 
</p>
<?php if($val['naverpayStatus']['code'] == 'DelayProductOrder'){ //발송지연?>
<div style="padding-bottom:5px" data-sno="<?=$val['sno']?>" data-info="<?=$val['naverpayStatus']['text']?>" class="js-btn-naverpay-status-detail">
<input type="button" onclick="delivery_trace('<?= $val['invoiceCompanySno']; ?>', '<?= $val['invoiceNo']; ?>');" value="배송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php } ?>
 
 
 
 
 
 
</td>
<?php } // 처리상태 ?>
 
 
변경 후inc_order_view.php
<p>
<?= $val['orderStatusStr']; ?>
<input type="button" data-sno="<?= $val['sno']; ?>" data-name="<?= $val['goodsNm']; ?>" value="로그" class="btn btn-sm btn-white js-order-log"/>
<?= $val['returnStatusStr']; ?>
</p>
<?php if($val['naverpayStatus']['code'] == 'DelayProductOrder'){ //발송지연?>
<div style="padding-bottom:5px" data-sno="<?=$val['sno']?>" data-info="<?=$val['naverpayStatus']['text']?>" class="js-btn-naverpay-status-detail">
<input type="button" onclick="delivery_trace('<?= $val['invoiceCompanySno']; ?>', '<?= $val['invoiceNo']; ?>');" value="배송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php } ?>
 
<?php if (!empty($val['returnTraceUrl'])) { ?>
<div>
<input type="button" onclick="return_trace('<?= gd_htmlspecialchars_addslashes($val['returnTraceUrl']); ?>');" value="반송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php } ?>
</td>
<?php } // 처리상태 ?>
 
 
Admin/order/layout_order_claim_list.php+1-1
변경 전layout_order_claim_list.php
<?php } ?>
<td <?= $orderDeliveryRowSpan ?> class="text-right"><?= gd_currency_display($val['realDeliveryCharge']); ?></td>
<?php } ?>
<td <?= $orderAddGoodsRowSpan ?>><?= $val['orderStatusStr']; ?></td>
</tr>
<?php
if ($val['addGoodsCnt'] > 0) {
 
변경 후layout_order_claim_list.php
<?php } ?>
<td <?= $orderDeliveryRowSpan ?> class="text-right"><?= gd_currency_display($val['realDeliveryCharge']); ?></td>
<?php } ?>
<td <?= $orderAddGoodsRowSpan ?>><?= $val['orderStatusStr']; ?><?= $val['returnStatusStr']; ?></td>
</tr>
<?php
if ($val['addGoodsCnt'] > 0) {
 
Admin/order/layout_order_goods_list.php+7-1
변경 전layout_order_goods_list.php
<?php if ($currentStatusCode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<div title="주문 상품별 주문 상태"><?= $val['orderStatusStr']; ?></div>
</td>
<?php } ?>
<?php } // 처리상태 ?>
</div>
<?php } ?>
<?php } ?>
 
 
 
 
 
 
<?php } ?>
</td>
<?php } // 송장번호 ?>
 
변경 후layout_order_goods_list.php
<?php if ($currentStatusCode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<div title="주문 상품별 주문 상태"><?= $val['orderStatusStr']; ?><?= $val['returnStatusStr']; ?></div>
</td>
<?php } ?>
<?php } // 처리상태 ?>
</div>
<?php } ?>
<?php } ?>
 
<?php if (!empty($val['returnTraceUrl'])) : ?>
<div class="delivery-trace">
<input type="button" onclick="return_trace('<?= gd_htmlspecialchars_addslashes($val['returnTraceUrl']); ?>');" value="반송추적" class="btn btn-sm btn-black">
</div>
<?php endif; ?>
<?php } ?>
</td>
<?php } // 송장번호 ?>
 
Admin/order/layout_order_view_status_back.php+7-1
변경 전layout_order_view_status_back.php
<?php if ($val['beforeStatusStr'] && $statusMode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<p><?= $val['orderStatusStr']; ?></p>
<?php if ($val['naverpayStatus']['code'] == 'DelayProductOrder') { //발송지연?>
<div style="padding-bottom:5px" data-sno="<?= $val['sno'] ?>" data-info="<?= $val['naverpayStatus']['text'] ?>" class="js-btn-naverpay-status-detail">
(<?= $val['naverpayStatus']['notice'] ?>)
<input type="button" onclick="delivery_trace('<?= $val['invoiceCompanySno']; ?>', '<?= $val['invoiceNo']; ?>');" value="배송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php } ?>
 
 
 
 
 
 
</td>
<!-- 처리상태 -->
</tr>
 
변경 후layout_order_view_status_back.php
<?php if ($val['beforeStatusStr'] && $statusMode == 'r') { ?>
<div class="text-muted" title="이전 상품별 주문 상태"><?= $val['beforeStatusStr']; ?> &gt;</div>
<?php } ?>
<p><?= $val['orderStatusStr']; ?><?= $val['returnStatusStr']; ?></p>
<?php if ($val['naverpayStatus']['code'] == 'DelayProductOrder') { //발송지연?>
<div style="padding-bottom:5px" data-sno="<?= $val['sno'] ?>" data-info="<?= $val['naverpayStatus']['text'] ?>" class="js-btn-naverpay-status-detail">
(<?= $val['naverpayStatus']['notice'] ?>)
<input type="button" onclick="delivery_trace('<?= $val['invoiceCompanySno']; ?>', '<?= $val['invoiceNo']; ?>');" value="배송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php } ?>
 
<?php if (!empty($val['returnTraceUrl'])) : ?>
<div>
<input type="button" onclick="return_trace('<?= gd_htmlspecialchars_addslashes($val['returnTraceUrl']); ?>');" value="반송추적" class="btn btn-sm btn-gray mgt5"/>
</div>
<?php endif; ?>
</td>
<!-- 처리상태 -->
</tr>
 
Admin/order/popup_epost_apply_frame.php+14
변경 전popup_epost_apply_frame.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후popup_epost_apply_frame.php
<script src="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/script/common/aggregator-message-router.js') ?>"></script>
<style>
/* 팝업(720px)이 어드민 공통 body min-width(970px)보다 좁아 오른쪽이 잘리는 것 방지 */
body { min-width: 0; overflow: hidden; }
</style>
<iframe name="epostApply" src="<?= htmlspecialchars($pageUrl, ENT_QUOTES, 'UTF-8') ?>" frameborder="0" marginwidth="0" marginheight="0" width="100%" style="height: 100vh;"></iframe>
<script>
// 허브가 신청 완료 시 MODAL_CLOSE 전송 — iframe 은 창을 못 닫으므로 여기서 팝업 창을 닫는다
IframeMessageRouter.init({
onModalClose: function () {
window.close();
},
});
</script>
 
Admin/order/post_request.php+200-32
변경 전post_request.php
<div class="page-header js-affix">
<h3><?=end($naviMenu->location); ?></h3>
</div>
 
<div class="table-title">우체국택배 배송상태 자동 업데이트 </div>
<table class="table table-cols">
<tbody>
<tr>
<td style="padding: 15px 0;">
<div class="notice-info" style="margin-top: 0;margin-bottom: 0">
우체국택배 배송상태를 쇼핑몰에 2시간마다 자동으로 업데이트 합니다.<br/>
2시간마다 자동으로 배송상태를 확인하여 배송이 완료된 주문은 ‘배송완료’로 업데이트 됩니다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
</div>
</td>
</tr>
</tbody>
</table>
 
<div class="table-title">우체국택배 배송상태 수동 업데이트 </div>
<table class="table table-cols">
<tbody>
<tr>
<td style="padding: 15px 0 20px 0;">
<div class="notice-info" style="margin-top: 0;margin-bottom: 0">
우체국택배 배송상태를 쇼핑몰에 수동 업데이트하려면, 아래 버튼을 클릭해 주세요.<br/>
수동으로 배송상태를 확인하여 배송이 완료된 주문은 ‘배송완료’로 업데이트 됩니다.
</div>
<div style="padding-top: 15px;"><a href="./post_ps.php?mode=manual" class="btn btn-lg btn-black" target="ifrmProcess" style="color:#fff">배송상태 수동 업데이트</a></div>
</td>
</tr>
</tbody>
</table>
<div class="table-title ">우체국택배 신청 </div>
<iframe name="requestPostIfrm" src="https://www.nhn-commerce.com/service/godopost/regist.php?shopSno=<?=$shopSno?>&shopHost=<?=$domainUrl?>" frameborder="0" height="830px" width="100%"></iframe>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후post_request.php
<?php
?>
<link rel="stylesheet" href="<?= gd_set_browser_cache(PATH_ADMIN_GD_SHARE . 'ncds/css/order/epost-service-management.css') ?>">
 
<header class="page-header ncua-page-header js-affix">
<h3><?= end($naviMenu->location); ?></h3>
<div class="ncua-page-header__actions">
<button type="button" class="ncua-btn ncua-btn--md ncua-btn--primary" id="epr-btn-save">
<span class="ncua-btn__label">저장</span>
</button>
</div>
</header>
 
<?php if (!empty($usageBanner)) : ?>
<div class="ncua-full-width-notification ncua-full-width-notification--info" role="alert">
<div class="ncua-full-width-notification__container">
<div class="ncua-full-width-notification__content">
<div class="ncua-full-width-notification__content-wrapper">
<div class="ncua-info-circle-violet"></div>
<div class="ncua-full-width-notification__text-container">
<span class="ncua-full-width-notification__title"><?= htmlspecialchars($usageBanner['title']) ?></span>
<?php if ($usageBanner['showApplyLink']) : ?>
<a href="javascript:void(0)" id="epr-btn-apply" class="ncua-full-width-notification__link">
<span class="ncua-full-width-notification__supporting-text">서비스 신청하기</span>
</a>
<?php else : ?>
<span class="ncua-full-width-notification__supporting-text"><?= htmlspecialchars($usageBanner['body']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="ncua-full-width-notification__actions-container">
<button type="button" class="ncua-full-width-notification__close-button ncua-close-icon-violet" style="visibility:hidden" aria-label="알림 닫기"></button>
</div>
</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
</div>
</div>
<?php endif; ?>
 
<article class="ncua-content epr-page">
<section class="ncua-block-container">
<header class="ncua-block-header">
<div class="ncua-block-header__row">
<div class="ncua-block-header__title-area">
<span class="ncua-block-header__title">우체국택배 관리</span>
</div>
<div class="ncua-block-header__action-area"></div>
</div>
</header>
<div class="ncua-block-container__body">
<div class="ncua-table-wrapper" style="--ncua-table-header-height:40px;--ncua-table-default-min-width:1140px">
<div class="ncua-table ncua-table--vertical">
<table class="ncua-table__table" role="table">
<tbody class="ncua-table__body">
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell">
<div data-tooltip-seq="002">우체국택배 배송상태 수동 업데이트</div>
</th>
<td class="ncua-table__cell">
<div>
<button type="button" class="ncua-btn ncua-btn--xs ncua-btn--secondary-gray" id="epr-btn-manual">
<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="M2 14s.121.85 3.636 4.364A9 9 0 0 0 20.776 14M2 14v6m0-6h6m14-4s-.121-.85-3.636-4.364A9 9 0 0 0 3.224 10M22 10V4m0 6h-6"></path></svg>
<span class="ncua-btn__label">배송상태 수동 업데이트</span>
</button>
</div>
</td>
</tr>
<tr class="ncua-table__row">
<th scope="row" class="ncua-table__cell">
<div data-tooltip-seq="003">우체국택배 수거신청 자동 접수</div>
</th>
<td class="ncua-table__cell">
<div>
<div class="ncua-switch ncua-switch--xs" id="epr-auto-switch">
<label class="ncua-switch__option ncua-switch__option--left<?= $autoAcceptSavedEnabled ? ' ncua-switch__option--active' : ' ncua-switch__option--inactive' ?>">
<input type="radio" name="epr-auto-collect" class="ncua-switch__radio ncua-switch__radio--left" value="y"<?= $autoAcceptSavedEnabled ? ' checked' : '' ?>/>
<span class="ncua-switch__label">사용함</span>
</label>
<label class="ncua-switch__option ncua-switch__option--right<?= $autoAcceptSavedEnabled ? ' ncua-switch__option--inactive' : ' ncua-switch__option--active' ?>">
<input type="radio" name="epr-auto-collect" class="ncua-switch__radio ncua-switch__radio--right" value="n"<?= $autoAcceptSavedEnabled ? '' : ' checked' ?>/>
<span class="ncua-switch__label">사용안함</span>
</label>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</article>
 
<script>
document.addEventListener('DOMContentLoaded', function () {
var EPR = {
hasWriteAuth: <?= $hasWriteAuth ? 'true' : 'false' ?>,
connected: <?= $epostConnected ? 'true' : 'false' ?>,
savedEnabled: <?= $autoAcceptSavedEnabled ? 'true' : 'false' ?>
};
 
// 미신청 배너 [서비스 신청하기] — 허브 신청 페이지를 감싼 어드민 팝업. 신청 결과는 재진입·새로고침 시 진행 상태 API 로 갱신됨(기획 확정)
var applyBtn = document.querySelector('#epr-btn-apply');
if (applyBtn) {
applyBtn.addEventListener('click', function () {
window.open('./popup_epost_apply.php', 'epostApply', 'width=720,height=800,scrollbars=yes');
});
}
 
 
// 스위치 활성 시각 동기화 — 전환만으로 저장되지 않음([저장] 필요)
var switchBox = document.querySelector('#epr-auto-switch');
function currentEnabled() {
return switchBox.querySelector('input:checked').value === 'y';
}
switchBox.querySelectorAll('input').forEach(function (radio) {
radio.addEventListener('change', function () {
switchBox.querySelectorAll('.ncua-switch__option').forEach(function (opt) {
var on = opt.querySelector('input').checked;
opt.classList.toggle('ncua-switch__option--active', on);
opt.classList.toggle('ncua-switch__option--inactive', !on);
});
});
});
 
// ── 알림 — 공용 NCDSAlert·NCDSConfirm(head.php 전역)
function openAlert(title, subtitle) {
NCDSAlert({ message: title, subMessage: subtitle || '', iconType: 'error' });
}
 
// [배송상태 수동 업데이트] — 쓰기 권한 검증 유지(현행), 0건이어도 동일 문구 Toast
document.querySelector('#epr-btn-manual').addEventListener('click', function () {
if (!EPR.hasWriteAuth) {
openAlert('우체국택배 신청/관리의 쓰기 권한이 없습니다.', '권한은 대표운영자에게 문의하시기 바랍니다.');
return;
}
fetch('./post_ps.php?mode=manualUpdateJson', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (res) { return res.json(); })
.then(function (data) {
if (data.code !== 200) {
openAlert(data.message || '배송상태 수동 업데이트에 실패하였습니다.', data.subMessage || '');
return;
}
ncua.Notification.success('주문 ' + (data.updatedCount || 0) + '건의 우체국택배 배송상태를 수동 업데이트 하였습니다.', '', { autoClose: 5000 });
})
.catch(function () {
openAlert('배송상태 수동 업데이트에 실패하였습니다.', '잠시 후 다시 시도해 주세요.');
});
});
 
// [저장] — 검사 순서: 쓰기 권한 Alert → 미신청 Alert → (사용안함→사용함이면) Confirm → 저장 Toast
function saveAutoAccept() {
var params = new URLSearchParams();
params.append('mode', 'saveAutoAccept');
params.append('autoAcceptFl', currentEnabled() ? 'y' : 'n');
fetch('./post_ps.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
body: params.toString()
}).then(function (res) { return res.json(); }).then(function (data) {
if (data.code === 200) {
EPR.savedEnabled = currentEnabled();
ncua.Notification.success('저장이 완료되었습니다.', '', { autoClose: 5000 });
return;
}
openAlert(data.message, data.subMessage || '');
}).catch(function () {
openAlert('저장에 실패하였습니다.', '잠시 후 다시 시도해 주세요.');
});
}
document.querySelector('#epr-btn-save').addEventListener('click', function () {
if (!EPR.hasWriteAuth) {
openAlert('우체국택배 신청/관리의 쓰기 권한이 없습니다.', '권한은 대표운영자에게 문의하시기 바랍니다.');
return;
}
if (!EPR.connected) {
openAlert('우체국택배 서비스 신청 후 이용 가능합니다.', '');
return;
}
// Confirm 노출 = 직전 저장값 기준 사용안함 → 사용함 방향만
if (!EPR.savedEnabled && currentEnabled()) {
NCDSConfirm({
message: '우체국택배 수거신청 자동 접수를 사용하시겠습니까?',
subMessage: '반품접수와 동시에 수거가 접수되며, 접수된 수거 신청은 관리자에서 취소할 수 없습니다.'
}).then(function (ok) {
if (ok) saveAutoAccept(); // 취소 시 스위치는 사용함 그대로 유지, 저장하지 않음
});
return;
}
saveAutoAccept();
});
});
</script>
<script type="text/javascript">
const code = '260921001';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>
 
Component/Database/DBTableField.php+21
변경 전DBTableField.php
return $arrField;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* [주문] orderHandle 필드 기본값
*
 
변경 후DBTableField.php
return $arrField;
}
 
/**
* [주문] orderReturnInvoice 필드 기본값
*
* @return array orderReturnInvoice 테이블 필드 정보
*/
public static function tableOrderReturnInvoice()
{
// @formatter:off
$arrField = [
['val' => 'orderGoodsNo', 'typ' => 'i', 'def' => null], // 주문상품번호 (es_orderGoods.sno)
['val' => 'orderNo', 'typ' => 's', 'def' => null], // 주문번호
['val' => 'returnCarrierNo', 'typ' => 'i', 'def' => null], // 반품 수거 택배사 (es_manageDeliveryCompany.sno)
['val' => 'returnInvoiceNo', 'typ' => 's', 'def' => null], // 반품 수거 송장번호
['val' => 'returnInvoiceStatus', 'typ' => 's', 'def' => null], // 반품 수거 상태 (REQUESTED:발행, FAILED:신청실패, 그외:택배사 배송상태)
['val' => 'returnInvoiceStatusDt', 'typ' => 's', 'def' => null], // 택배사 배송상태 발생일시
];
// @formatter:on
 
return $arrField;
}
 
/**
* [주문] orderHandle 필드 기본값
*
 
Component/Delivery/Delivery.php+23-7
변경 전Delivery.php
throw new Exception(__('송장 번호는 필수 항목입니다.'));
}
 
// bind 데이타
$arrBind = [];
$this->db->bind_param_push($arrBind, 'i', $invoiceCompanySno);
 
$strSQL = 'SELECT companyName, traceUrl FROM ' . DB_MANAGE_DELIVERY_COMPANY . ' WHERE sno = ?';
$getData = $this->db->query_fetch($strSQL, $arrBind, false);
unset($arrBind);
 
$traceUrl = str_replace('__INVOICENO__', str_replace('-', '', $invoiceNo), $getData['traceUrl']);
 
return $traceUrl;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 배송추적시 json 처리 해야할 배송업체인지 체크
*
 
변경 후Delivery.php
throw new Exception(__('송장 번호는 필수 항목입니다.'));
}
 
$getData = $this->getDeliveryTraceUrlTemplate($invoiceCompanySno);
 
 
 
 
 
 
 
$traceUrl = str_replace('__INVOICENO__', str_replace('-', '', $invoiceNo), $getData['traceUrl']);
 
return $traceUrl;
}
 
/**
* 배송추적 URL 템플릿(치환 전) 조회 — 같은 배송 업체를 여러 건 조회하는 호출부가 반복 쿼리를 피하려고 씀
*
* @param integer $invoiceCompanySno 배송 업체 sno
*
* @return array companyName·traceUrl (치환 전)
*/
public function getDeliveryTraceUrlTemplate($invoiceCompanySno): array
{
$arrBind = [];
$this->db->bind_param_push($arrBind, 'i', $invoiceCompanySno);
 
$strSQL = 'SELECT companyName, traceUrl FROM ' . DB_MANAGE_DELIVERY_COMPANY . ' WHERE sno = ?';
$getData = $this->db->query_fetch($strSQL, $arrBind, false);
unset($arrBind);
 
return [
'companyName' => $getData['companyName'] ?? null,
'traceUrl' => $getData['traceUrl'] ?? null,
];
}
 
/**
* 배송추적시 json 처리 해야할 배송업체인지 체크
*
 
Component/Order/EpostReturnAdmin.php+430
변경 전EpostReturnAdmin.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후EpostReturnAdmin.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\Order;
 
use Component\Admin\AdminLogDAO;
use Component\Admin\AdminMenu;
use Component\Member\MemberMasking;
use Component\Policy\Policy;
use DTO\Order\EpostReturnSearchDTO;
use Framework\Http\Request;
use Framework\Http\Session\SessionManager;
use Framework\Log\Channel;
use Origin\Enum\ApiClient\Epost\EpostUsageRequestStatus;
use Origin\Service\ApiClient\Commerce\Epost\EpostUsageAdminApiClientService;
use Framework\Log\Logger;
use Framework\Registry\Globals;
use Framework\Utility\SkinUtils;
use Origin\DTO\Member\AdminLogDTO;
use Origin\DTO\Order\EpostReturnRequestResultDTO;
use Origin\Enum\Order\EpostDeliveryStatus;
use Origin\Enum\Order\EpostReturnRequestRejectReason;
use Origin\Exception\Order\EpostReturnRequestRejectedException;
use Origin\Repository\Member\AdminLogRepository;
use Origin\Repository\Order\OrderReturnInvoiceRepository;
use Origin\Service\Aggregator\GodomallAggregatorService;
use Origin\Service\Order\EpostReturnService;
use Repository\Order\OrderGiftRepository;
use Repository\Order\OrderGoodsRepository;
use Exception;
use Throwable;
 
/**
* 우체국택배 수거신청 어드민
*/
class EpostReturnAdmin
{
/**
* 우체국택배 정책 키
*/
public const POLICY_KEY = 'order.godopost';
 
/**
* 자동접수 사용 여부 값 (order.godopost.autoAcceptFl)
*/
public const AUTO_ACCEPT_ENABLED = 'y';
 
public const AUTO_ACCEPT_DISABLED = 'n';
 
/**
* @var Channel epost 채널로 고정한 로거
*/
protected Channel $logger;
 
public function __construct(
protected OrderGoodsRepository $repository,
protected Policy $policy,
protected AdminLogRepository $adminLogRepository,
protected EpostReturnService $service,
protected Globals $globals,
Logger $logger,
protected OrderReturnInvoiceRepository $returnInvoiceRepository,
protected OrderGiftRepository $giftRepository,
protected ReturnInvoice $returnInvoice,
protected MemberMasking $memberMasking,
protected Request $request,
protected SessionManager $session,
protected EpostUsageAdminApiClientService $usageAdminApiClientService
) {
$this->logger = $logger->channel('epost');
}
 
/**
* @param array $getValue 화면 검색 파라미터
* @return array data(행 목록) · total(검색 건수) · grandTotal(전체 건수) · page·pageSize(보정 적용값)
*/
public function getTargetList(array $getValue): array
{
$search = new EpostReturnSearchDTO($getValue);
$rows = $this->repository->findTargets($search);
$total = $this->repository->countTargets($search);
 
$giftsByOrderNo = [];
$orderNos = array_values(array_unique(array_column($rows, 'orderNo')));
if (!empty($orderNos)) {
foreach ($this->giftRepository->findOrderGiftLinesByOrderNos($orderNos) as $gift) {
$giftsByOrderNo[$gift['orderNo']][] = $gift;
}
}
 
foreach ($rows as $key => $row) {
// 옵션 파싱은 주문 화면 전례를 따름 — optionInfo 는 통째 stripslashes 후 decode, optionTextInfo 는 decode 후 항목별 stripslashes
$rows[$key]['optionInfo'] = json_decode(gd_htmlspecialchars_stripslashes((string) ($row['optionInfo'] ?? '')), true) ?: [];
$rows[$key]['gifts'] = $giftsByOrderNo[$row['orderNo']] ?? [];
$formatOption = static function (string $name, string $value): string {
return $name !== '' ? $name . ': ' . $value : $value;
};
$optionLines = [];
foreach ($rows[$key]['optionInfo'] as $option) {
$optionLines[] = $formatOption(trim((string) ($option[0] ?? '')), trim((string) ($option[1] ?? '')));
}
foreach ((array) (json_decode((string) ($row['optionTextInfo'] ?? ''), true) ?: []) as $textOption) {
$optionLines[] = $formatOption(
trim(gd_htmlspecialchars_stripslashes((string) ($textOption[0] ?? ''))),
trim(gd_htmlspecialchars_stripslashes((string) ($textOption[1] ?? '')))
);
}
$optionLines = array_filter($optionLines, static fn (string $line) => $line !== '');
$rows[$key]['optionText'] = implode(' / ', $optionLines);
$rows[$key]['giftLines'] = array_map(static fn (array $gift) => ($gift['presentTitle'] ?: '-') . ' | ' . $gift['giftNm'] . ' | ' . $gift['giveCnt'] . '개', $rows[$key]['gifts']);
$rows[$key]['progressLabel'] = $this->returnInvoice->getProgressLabel($row['returnInvoiceStatus']);
$rows[$key]['orderName'] = $this->memberMasking->masking('order', 'name', $row['orderName']);
$rows[$key]['memId'] = $this->memberMasking->masking('order', 'id', $row['memId']);
$rows[$key]['receiverName'] = $this->memberMasking->masking('order', 'name', $row['receiverName']);
$rows[$key]['goodsImageSrc'] = empty($row['imageName']) ? null : SkinUtils::imageViewStorageConfig(
$row['imageName'],
$row['imagePath'],
$row['imageStorage'],
40,
'goods',
true,
$row['goodsImageStorage'] === 'obs' ? $row['imageUrl'] : null
)[0];
}
 
return [
'data' => $rows,
'total' => $total,
'grandTotal' => $this->repository->countTargets(new EpostReturnSearchDTO([])),
'page' => $search->getPage(),
'pageSize' => $search->getPageSize(),
];
}
 
/**
* 서비스 신청 진행 상태 배너 표시 재료 — 조회 실패나 배너 없는 단계(서비스 시작)는 null
*
* 실패해도 화면은 정상 노출해야 해서 여기서 기록만 남기고 배너를 생략함
*
* @return array|null [title, body, showApplyLink]
*/
public function getUsageBanner(): ?array
{
try {
$usageRequestStatus = $this->usageAdminApiClientService
->findUsageRequestStatus()
->getStatus();
} catch (\Throwable $e) {
$this->logger->warning('우체국택배 신청 진행 상태 조회 실패 — 배너 생략', ['exception' => $e]);
 
return null;
}
 
if (!$usageRequestStatus->hasBanner()) {
return null;
}
 
return [
'title' => $usageRequestStatus->bannerTitle(),
'body' => $usageRequestStatus->bannerBody(),
'showApplyLink' => $usageRequestStatus === EpostUsageRequestStatus::NONE,
];
}
 
/**
* 서비스 미신청 상점 판정 — compdivcd·API 상태값(NONE) 둘 중 하나라도 미신청을 가리키면 true.
* compdivcd 는 있어도 API 상태가 NONE 이면(불일치) 신뢰하지 않고 차단함
*/
public function isUsageNotRequested(): bool
{
if (empty($this->policy->getValue(self::POLICY_KEY)['compdivcd'])) {
return true;
}
 
try {
return $this->usageAdminApiClientService
->findUsageRequestStatus()
->getStatus() === EpostUsageRequestStatus::NONE;
} catch (\Throwable $e) {
$this->logger->warning('우체국택배 신청 진행 상태 조회 실패', ['exception' => $e]);
 
return false;
}
}
 
/**
* 미신청 배너 [서비스 신청하기] 팝업 URL — 허브 신청 페이지
* 연동 파라미터는 CRM 애그리게이터 호출 규격과 동일 (solution·shopNo·code·menuCode·adminUrl)
*/
public function getUsageApplyUrl(): string
{
$hubConfig = \App::getSystemConfig('internal')['hub'];
 
$params = [
'solution' => 'GODOMALL',
'shopNo' => $this->globals->get('gLicense.godosno'),
'code' => \App::getInstance(GodomallAggregatorService::class)->registerDisposalKey(),
'menuCode' => \App::load(AdminMenu::class)->getAdminMenuNo('order', 'epostParcel', 'request'),
'adminUrl' => parse_url(URI_ADMIN)['host'],
];
 
return $hubConfig['url'] . $hubConfig['path']['epost-apply'] . '?' . http_build_query($params);
}
 
/**
* 자동접수 사용 여부. 우체국택배 미사용 상점(승인번호 부재)과 정책·필드 부재는 사용안함
*
* @return bool 자동접수 사용이면 true
*/
public function isAutoAcceptEnabled(): bool
{
$config = $this->policy->getValue(self::POLICY_KEY);
 
if (empty($config['compdivcd'])) {
return false;
}
 
return ($config['autoAcceptFl'] ?? '') === self::AUTO_ACCEPT_ENABLED;
}
 
/**
* 자동접수 사용 여부 저장. 저장 성공 건만 감사기록을 남김
*
* @param bool $enabled 사용 여부
*
* @throws Exception 정책 저장 실패
*/
public function setAutoAccept(bool $enabled): void
{
$config = $this->policy->getValue(self::POLICY_KEY) ?: [];
$config['autoAcceptFl'] = $enabled ? self::AUTO_ACCEPT_ENABLED : self::AUTO_ACCEPT_DISABLED;
 
$saved = $this->policy->setValue(self::POLICY_KEY, $config);
 
if (!$saved) {
throw new Exception('우체국택배 자동접수 설정 저장 실패');
}
 
$this->adminLogRepository->insert($this->buildAutoAcceptAdminLog($enabled));
}
 
/**
* 자동접수 설정 저장 감사기록 조립
*
* @param bool $enabled 사용 여부
*/
protected function buildAutoAcceptAdminLog(bool $enabled): AdminLogDTO
{
$requestUri = preg_replace('#^/{2,}#', '/', ($this->request->getRequestUri() ?? ''));
 
return (new AdminLogDTO())
->setBaseUri(strtok($requestUri, '?'))
->setUri($requestUri)
->setData(json_encode(['autoAcceptFl' => $enabled ? self::AUTO_ACCEPT_ENABLED : self::AUTO_ACCEPT_DISABLED]))
->setMenu(__('주문/배송>택배연동 서비스>우체국택배 신청/관리'))
->setReferer((string)$this->request->getReferer())
->setType(AdminLogDAO::LOG_TYPE_P2Y)
->setPage(__('우체국택배 신청/관리'))
->setAction(__('수거신청 자동 접수 설정 저장'))
->setManagerNo((string)$this->session->get('manager.sno'))
->setManagerId((string)$this->session->get('manager.managerId'))
->setScmNo((string)$this->session->get('manager.scmNo'))
->setIp((string)$this->request->getRemoteAddress())
->setAccessControlReason('');
}
 
/**
* 수거 진행 건 상태변경 잠금 여부. 수거 상태가 있는 상품이 하나라도 있으면 잠김
*
* DELIVERY_COMPLETE는 잠그면 반품회수완료(b4) 자동전이가 자기잠금에 걸려서 제외함.
* FAILED는 재신청([수거 신청] 버튼)만 허용하고 일반 상태변경은 계속 잠금 대상임
*
* @param string $orderNo 주문번호
* @param array $orderGoodsSnos 반품접수 상태인 상품주문번호 목록
* @return bool 잠금이면 true
*/
public function isStatusChangeLocked(string $orderNo, array $orderGoodsSnos): bool
{
if (empty($orderGoodsSnos)) {
return false;
}
 
$returnInvoices = $this->returnInvoiceRepository->findReturnInvoiceStatusRows([$orderNo], $orderGoodsSnos);
 
foreach ($returnInvoices as $returnInvoice) {
if ($returnInvoice['returnInvoiceStatus'] !== null
&& $returnInvoice['returnInvoiceStatus'] !== EpostDeliveryStatus::DELIVERY_COMPLETE->value) {
return true;
}
}
 
return false;
}
 
/**
* 반품접수 자동접수 — 운영자 직접 접수 경로. 실패는 전파하지 않고 epost 채널 로그로만 남김
*
* @param array $orderGoodsSnos 반품접수된 상품주문번호 목록
*/
public function applyAutoAccept(array $orderGoodsSnos): void
{
if (empty($orderGoodsSnos)) {
return;
}
 
try {
if (!$this->isAutoAcceptEnabled()) {
return;
}
 
$eligibleSnos = $this->repository->findTargetSnos($orderGoodsSnos);
 
$excludedSnos = array_values(array_diff($orderGoodsSnos, $eligibleSnos));
if (!empty($excludedSnos)) {
$this->logger->info('우체국택배 자동접수 대상 아님', ['orderGoodsSnos' => $excludedSnos]);
}
 
$this->requestPickup($eligibleSnos);
} catch (EpostReturnRequestRejectedException $e) {
$this->logger->warning('우체국택배 자동접수 거부', [
'reason' => $e->getReason()->name,
'orderGoodsSnos' => $orderGoodsSnos,
]);
} catch (Throwable $e) {
$this->logger->warning('우체국택배 자동접수 실행 실패', [
'orderGoodsSnos' => $orderGoodsSnos,
'exception' => $e,
]);
}
}
 
/**
* 반품 승인 자동접수 — 고객 신청 승인 경로. 실패는 전파하지 않고 epost 채널 로그로만 남김
*
* @param array $userHandleSnos 승인한 고객 신청 sno 목록
*/
public function applyAutoAcceptByUserHandles(array $userHandleSnos): void
{
// 신청 없는 행은 userHandleSno=0
$userHandleSnos = array_values(array_filter(array_map('intval', $userHandleSnos), fn (int $sno) => $sno > 0));
 
if (empty($userHandleSnos)) {
return;
}
 
try {
if (!$this->isAutoAcceptEnabled()) {
return;
}
 
$this->requestPickup($this->repository->findTargetSnosByUserHandleSnos($userHandleSnos));
} catch (EpostReturnRequestRejectedException $e) {
$this->logger->warning('우체국택배 자동접수 거부', [
'reason' => $e->getReason()->name,
'userHandleSnos' => $userHandleSnos,
]);
} catch (Throwable $e) {
$this->logger->warning('우체국택배 자동접수 실행 실패', [
'userHandleSnos' => $userHandleSnos,
'exception' => $e,
]);
}
}
 
/**
* 수동 수거신청 — 「우체국택배 수거신청」 화면 진입점. 권한·본사 여부는 어드민 세션에서 호출부가 평가해서 넘김
*
* @param bool $hasMenuWritePermission 메뉴 쓰기 권한 여부
* @param bool $isHeadquarters 본사 운영자 여부
* @param array $orderGoodsSnos 신청할 상품주문번호 목록
* @return EpostReturnRequestResultDTO 실행 결과 (성공·실패·선점 제외)
* @throws EpostReturnRequestRejectedException 거부 사유를 담아 던짐
*/
public function applyManualRequest(bool $hasMenuWritePermission, bool $isHeadquarters, array $orderGoodsSnos): EpostReturnRequestResultDTO
{
$epostApprovalNo = (string)($this->policy->getValue(self::POLICY_KEY)['compdivcd'] ?? '');
$eligibleSnos = $this->repository->findTargetSnos($orderGoodsSnos);
 
try {
EpostReturnRequestValidator::validateManualRequest(
$hasMenuWritePermission,
$isHeadquarters,
$epostApprovalNo !== '',
$orderGoodsSnos,
$eligibleSnos
);
} catch (EpostReturnRequestRejectedException $e) {
// 화면 얼럿이 갈리는 두 사유(상태 아님 · 이미 신청)를 부적격 건의 수거 진행 행 유무로 세분화함
$ineligibleSnos = array_values(array_diff($orderGoodsSnos, $eligibleSnos));
if ($e->getReason() === EpostReturnRequestRejectReason::NOT_ELIGIBLE
&& !empty($this->returnInvoiceRepository->findRequestedSnos($ineligibleSnos))) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::DUPLICATED);
}
 
throw $e;
}
 
return $this->service->apply((int)$this->globals->get('gLicense.godosno'), $epostApprovalNo, $orderGoodsSnos);
}
 
/**
* 대상 조건 통과분 수거 신청 공통부
*
* @param array $eligibleSnos 대상 조건을 통과한 상품주문번호 목록
*/
protected function requestPickup(array $eligibleSnos): void
{
if (empty($eligibleSnos)) {
return;
}
 
$epostApprovalNo = (string)($this->policy->getValue(self::POLICY_KEY)['compdivcd'] ?? '');
EpostReturnRequestValidator::validateAutoRequest(true, $epostApprovalNo !== '', $eligibleSnos, $eligibleSnos);
 
$result = $this->service->apply((int)$this->globals->get('gLicense.godosno'), $epostApprovalNo, $eligibleSnos);
 
if (!empty($result->getExcludedOrderGoodsSnos())) {
$this->logger->warning('우체국택배 자동접수 선점 제외', ['orderGoodsSnos' => $result->getExcludedOrderGoodsSnos()]);
}
 
if ($result->getFailureCount() > 0) {
$this->logger->warning('우체국택배 자동접수 실패 건', ['orderNos' => $result->getFailedOrderNos()]);
}
}
}
 
Component/Order/EpostReturnRequestValidator.php+89
변경 전EpostReturnRequestValidator.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후EpostReturnRequestValidator.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\Order;
 
use Origin\Enum\Order\EpostReturnRequestRejectReason;
use Origin\Exception\Order\EpostReturnRequestRejectedException;
 
/**
* 우체국택배 수거 신청 서버측 검증 — 판정 근거는 전부 파라미터로 받는 순수 판정
*/
class EpostReturnRequestValidator
{
/**
* 1회 신청 건수 상한 — 실행당 1회 배치 호출 전제의 값
*/
public const BATCH_LIMIT = 500;
 
/**
* 수동 신청 검증 — 권한·본사 여부는 어드민 세션에서 호출부가 평가해서 넘김
*
* @param array $orderGoodsSnos 신청할 상품주문번호 목록
* @param array $eligibleSnos 대상 술어를 통과한 상품주문번호 목록
* @throws EpostReturnRequestRejectedException 거부 사유를 담아 던짐
*/
public static function validateManualRequest(
bool $hasMenuWritePermission,
bool $isHeadquarters,
bool $epostConnected,
array $orderGoodsSnos,
array $eligibleSnos
): void {
if (!$hasMenuWritePermission) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::NO_PERMISSION);
}
if (!$isHeadquarters) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::NOT_HEADQUARTERS);
}
 
static::validateRequestTargets($epostConnected, $orderGoodsSnos, $eligibleSnos);
}
 
/**
* 자동 신청 검증 — 세션 대신 상점 자동접수 설정으로 평가함
*
* @param array $orderGoodsSnos 신청할 상품주문번호 목록
* @param array $eligibleSnos 대상 술어를 통과한 상품주문번호 목록
* @throws EpostReturnRequestRejectedException 거부 사유를 담아 던짐
*/
public static function validateAutoRequest(
bool $autoAcceptEnabled,
bool $epostConnected,
array $orderGoodsSnos,
array $eligibleSnos
): void {
if (!$autoAcceptEnabled) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::AUTO_DISABLED);
}
 
static::validateRequestTargets($epostConnected, $orderGoodsSnos, $eligibleSnos);
}
 
/**
* 화면 필터와 별개로 서버가 최종 거부를 판정함 — 접수는 취소 창구가 없어 되돌릴 수 없음
*
* @param array $orderGoodsSnos 신청할 상품주문번호 목록
* @param array $eligibleSnos 대상 술어를 통과한 상품주문번호 목록
* @throws EpostReturnRequestRejectedException 거부 사유를 담아 던짐
*/
protected static function validateRequestTargets(bool $epostConnected, array $orderGoodsSnos, array $eligibleSnos): void
{
if (!$epostConnected) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::NOT_CONNECTED);
}
 
if (count($orderGoodsSnos) > self::BATCH_LIMIT) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::OVER_LIMIT);
}
 
if (array_diff(array_map('intval', $orderGoodsSnos), array_map('intval', $eligibleSnos)) !== []) {
throw new EpostReturnRequestRejectedException(EpostReturnRequestRejectReason::NOT_ELIGIBLE);
}
}
}
 
Component/Order/OrderAdminNew.php+137-4
변경 전OrderAdminNew.php
use Origin\Enum\Order\OrderStatusCode;
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Enum\Statistics\Analytics\Order\ClaimType;
 
use Origin\Service\Statistics\Analytics\Order\ClaimCompleteAnalyticsCollectorService;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
*/
class OrderAdminNew extends \Component\Order\OrderNew
{
 
 
 
 
 
 
 
 
 
 
use CommonLogStorageTrait; // 공용 로그 저장
 
/**
if ($methodType != 'cancel') {
$beforeStatus = $bundleData['orderStatus'][0];
$handleData['handler'] = 'admin';
 
 
foreach ($bundleData['sno'] as $orderGoodsSno) {
$newOrderGoodsData = $this->setHandleAccept($arrData['orderNo'], [$orderGoodsSno], $handleMode, $handleData, $beforeStatus);
 
 
 
 
 
 
 
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($newOrderGoodsData) === true && empty($newOrderGoodsData['userHandleGoodsNo']) === false) {
$bundleData['userHandleGoodsNo'] = $newOrderGoodsData['userHandleGoodsNo'];
unset($newOrderGoodsData);
}
}
 
 
 
 
}
}
}
$addData['memberDcPrice'] = $memberDcPrice;
$addData['memberOverlapDcPrice'] = $memberOverlapDcPrice;
$addData['couponGoodsDcPrice'] = $couponGoodsDcPrice;
 
 
 
 
 
 
 
 
 
 
// 마이앱 사용에 따른 분기 처리
if ($this->useMyapp) {
// 수량이 0보다 작은경우 처리하지 않는다.
if (isset($bundleData['goodsCnt']) && $bundleData['goodsCnt'][$orderGoodsSno] > 0) {
// 주문상품 update용 데이터
 
$fieldData = $this->divideHandleGoodsCntData($orderNo, $orderGoodsSno, $bundleData['goodsCnt'][$orderGoodsSno]);
 
$tmpOrderGoodsData = '';
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 환불/반품/교환 단계 복구 처리 (환불상세내 상품리스트)
*
$changeStatus = $arrData['changeStatus'];
$isUpdate = false;
 
 
 
// 핸들로 오기 전의 주문상태로 돌릴 수 있는지에 대한 체크 (롤백용)
foreach ($arrData['bundle']['statusCheck'] as $orderGoodsSno) {
 
continue;
}
 
// 주문 상태 변경 처리
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), substr($orderStatus, 0, 1), $changeStatus, __('[이전 단계 복구]'));
 
 
 
 
 
 
 
 
 
 
 
 
 
// 반품/교환/환불과 동일한 주문상태로 변경하지 않는 경우 예외 처리 (ex. 환불->반품, 반품->교환 등)
if (gd_in_array($orderStatus, $this->statusHandleListExclude) && !gd_in_array($changeStatus, $this->statusHandleListExclude)) {
 
// 반품회수 완료시 자동으로 환불접수 단계 이동 처리
if ($isAutoRefund) {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), 'b', 'r1', __('반품회수완료로 인한 자동'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 기존 반품/교환/환불 레코드에서 데이터 추출
$getData = $this->getOrderGoodsData($orderNo, null, $handleSno, null, null, false);
*/
public function updateStatusPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason = false, $bundleFl = false, $mode = null, $useVisit = null, $autoProcess = false, $userHandleFl = null)
{
 
 
 
if (($result = $this->updateStatusUnconditionalPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason, $bundleFl, $mode, $useVisit, $autoProcess, $userHandleFl))!== true) {
if($result['errorMsg']){
$message = sprintf(__('"주문상태변경 실패하였습니다.(%s")'), $result['errorMsg']);
}
 
$bundleData['changeStatus'] = $changeStatus;
$bundleData['reason'] = $reason . ' ' . $this->getOrderStatusAdmin($changeStatus) . __(' 처리');
 
 
 
 
 
if($autoProcess == true){
if($bundleData['changeStatus'] == 'r1'){
$bundleData['reason'] = '고객 자동환불 신청';
 
return $refundAccountData;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후OrderAdminNew.php
use Origin\Enum\Order\OrderStatusCode;
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Enum\Statistics\Analytics\Order\ClaimType;
use Origin\Exception\Order\EpostReturnStatusLockedException;
use Origin\Service\Statistics\Analytics\Order\ClaimCompleteAnalyticsCollectorService;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
*/
class OrderAdminNew extends \Component\Order\OrderNew
{
/**
* @var array 반품접수(b1) 처리된 상품주문번호 — 우체국 자동접수용, 호출부가 커밋 후 읽음
*/
protected array $epostReturnAcceptedSnos = [];
 
/**
* @var string divideHandleGoodsCntData() 가 참조하는 처리모드('r'/'b'/'e'/'z') — 기존 메서드 시그니처를 건드리지 않기 위해 속성으로 전달
*/
protected string $divideHandleMode = '';
 
use CommonLogStorageTrait; // 공용 로그 저장
 
/**
if ($methodType != 'cancel') {
$beforeStatus = $bundleData['orderStatus'][0];
$handleData['handler'] = 'admin';
$epostReturnSnos = [];
 
foreach ($bundleData['sno'] as $orderGoodsSno) {
$newOrderGoodsData = $this->setHandleAccept($arrData['orderNo'], [$orderGoodsSno], $handleMode, $handleData, $beforeStatus);
 
// 부분수량 접수는 새로 생긴 상품주문번호가 접수 상태가 됨
if (is_array($newOrderGoodsData) && !empty($newOrderGoodsData['userHandleGoodsNo'])) {
$epostReturnSnos[] = (int)$newOrderGoodsData['userHandleGoodsNo'];
} else {
$epostReturnSnos[] = (int)$orderGoodsSno;
}
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($newOrderGoodsData) === true && empty($newOrderGoodsData['userHandleGoodsNo']) === false) {
$bundleData['userHandleGoodsNo'] = $newOrderGoodsData['userHandleGoodsNo'];
unset($newOrderGoodsData);
}
}
 
if ($changeStatus === OrderStatusCode::BACK_REQUEST->value) {
$this->epostReturnAcceptedSnos = $epostReturnSnos;
}
}
}
}
$addData['memberDcPrice'] = $memberDcPrice;
$addData['memberOverlapDcPrice'] = $memberOverlapDcPrice;
$addData['couponGoodsDcPrice'] = $couponGoodsDcPrice;
if ($this->divideHandleMode === 'b') {
// 반품은 이미 나간 배송을 그대로 수거하는 것이라 원 송장을 그대로 승계함(신규 출고가 아님)
$addData['invoiceCompanySno'] = $originData['invoiceCompanySno'];
$addData['invoiceNo'] = $originData['invoiceNo'];
} else {
// 교환/환불 등 신규 출고가 필요한 추가상품은 송장번호 초기화
$addData['invoiceCompanySno'] = '';
$addData['invoiceNo'] = '';
}
 
// 마이앱 사용에 따른 분기 처리
if ($this->useMyapp) {
// 수량이 0보다 작은경우 처리하지 않는다.
if (isset($bundleData['goodsCnt']) && $bundleData['goodsCnt'][$orderGoodsSno] > 0) {
// 주문상품 update용 데이터
$this->divideHandleMode = $handleMode;
$fieldData = $this->divideHandleGoodsCntData($orderNo, $orderGoodsSno, $bundleData['goodsCnt'][$orderGoodsSno]);
 
$tmpOrderGoodsData = '';
}
}
 
/**
* setHandleRollback 주문 로그 사유 지정값 — 자동 전이(예: 우체국 반품회수완료)가 기본 문구 대신 쓸 문구
*/
protected $handleRollbackReason = null;
 
/**
* setHandleRollback 의 b4 전이 로그 처리자 지정값
*/
protected $handleRollbackManagerId = null;
 
/**
* setAutoRefundAccept 의 r1 전이 로그 처리자 IP 지정값
*/
protected $handleRollbackManagerIp = null;
 
public function setHandleRollbackReason(?string $reason): void
{
$this->handleRollbackReason = $reason;
}
 
public function setHandleRollbackManagerId(?string $managerId): void
{
$this->handleRollbackManagerId = $managerId;
}
 
public function setHandleRollbackManagerIp(?string $managerIp): void
{
$this->handleRollbackManagerIp = $managerIp;
}
 
/**
* 환불/반품/교환 단계 복구 처리 (환불상세내 상품리스트)
*
$changeStatus = $arrData['changeStatus'];
$isUpdate = false;
 
$this->blockEpostPickupInProgress($orderNo, array_intersect_key($arrData['bundle']['orderStatus'] ?? [], array_flip($arrData['bundle']['statusCheck'] ?? [])), $changeStatus);
 
// 핸들로 오기 전의 주문상태로 돌릴 수 있는지에 대한 체크 (롤백용)
foreach ($arrData['bundle']['statusCheck'] as $orderGoodsSno) {
 
continue;
}
 
// 지정 처리자가 있으면 이 전이 로그에만 남김 — 뒤따르는 자동 환불접수(r1) 로그는 공란 유지
$canSetManagerId = method_exists($this, 'setLogManagerId');
if ($this->handleRollbackManagerId !== null && $canSetManagerId) {
$this->setLogManagerId($this->handleRollbackManagerId);
}
try {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), substr($orderStatus, 0, 1), $changeStatus, $this->handleRollbackReason ?? __('[이전 단계 복구]'));
} catch (\Throwable $e) {
throw $e;
} finally {
if ($this->handleRollbackManagerId !== null && $canSetManagerId) {
$this->setLogManagerId(null);
}
}
 
// 반품/교환/환불과 동일한 주문상태로 변경하지 않는 경우 예외 처리 (ex. 환불->반품, 반품->교환 등)
if (gd_in_array($orderStatus, $this->statusHandleListExclude) && !gd_in_array($changeStatus, $this->statusHandleListExclude)) {
 
// 반품회수 완료시 자동으로 환불접수 단계 이동 처리
if ($isAutoRefund) {
$autoRefundReason = __('반품회수완료로 인한 자동 환불접수 처리');
// 공유 인스턴스라 반드시 되돌림 — 안 그러면 바깥 루프의 다음 상품이 이 문구를 그대로 물려받음
$prevRollbackReason = $this->handleRollbackReason;
$canSetManagerIp = method_exists($this, 'setLogManagerIp');
if ($prevRollbackReason !== null) {
// 지정 사유 흐름이면 뒤따르는 이 로그도 전문 그대로 — 안 하면 기본 꼬리('환불접수 처리')가 붙어 문구가 중복됨
$this->handleRollbackReason = $autoRefundReason;
}
if ($this->handleRollbackManagerIp !== null && $canSetManagerIp) {
$this->setLogManagerIp($this->handleRollbackManagerIp);
}
try {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), 'b', 'r1', $autoRefundReason);
} catch (\Throwable $e) {
throw $e;
} finally {
if ($prevRollbackReason !== null) {
$this->handleRollbackReason = $prevRollbackReason;
}
if ($this->handleRollbackManagerIp !== null && $canSetManagerIp) {
$this->setLogManagerIp(null);
}
}
 
// 기존 반품/교환/환불 레코드에서 데이터 추출
$getData = $this->getOrderGoodsData($orderNo, null, $handleSno, null, null, false);
*/
public function updateStatusPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason = false, $bundleFl = false, $mode = null, $useVisit = null, $autoProcess = false, $userHandleFl = null)
{
// statusMode 는 호출부마다 실제 orderStatus 와 어긋날 수 있어 신뢰하지 않음 — b1 판정은 아래에서 실제 상태로 다시 거름
$this->blockEpostPickupInProgress($orderNo, array_column((array)$goodsData, 'orderStatus', 'sno'), $changeStatus);
 
if (($result = $this->updateStatusUnconditionalPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason, $bundleFl, $mode, $useVisit, $autoProcess, $userHandleFl))!== true) {
if($result['errorMsg']){
$message = sprintf(__('"주문상태변경 실패하였습니다.(%s")'), $result['errorMsg']);
}
 
$bundleData['changeStatus'] = $changeStatus;
if ($this->handleRollbackReason !== null && $reason === $this->handleRollbackReason) {
// 지정 사유는 전문 그대로 — 기본 꼬리('{상태} 처리')를 붙이면 문구가 중복됨
$bundleData['reason'] = $reason;
} else {
$bundleData['reason'] = $reason . ' ' . $this->getOrderStatusAdmin($changeStatus) . __(' 처리');
}
if($autoProcess == true){
if($bundleData['changeStatus'] == 'r1'){
$bundleData['reason'] = '고객 자동환불 신청';
 
return $refundAccountData;
}
 
/**
* 반품접수(b1) 처리된 상품주문번호 — 우체국 자동접수용
*/
public function getEpostReturnAcceptedSnos(): array
{
return $this->epostReturnAcceptedSnos;
}
 
/**
* 우체국택배 수거 진행 건이 있으면 상태변경을 거부함
*
* @param string $orderNo 주문번호
* @param array $orderStatusBySno 상품주문번호별 주문상태 맵
* @param string $changeStatus 변경할 주문상태 코드
*
* @throws EpostReturnStatusLockedException 수거 진행 건 포함
*/
protected function blockEpostPickupInProgress(string $orderNo, array $orderStatusBySno, string $changeStatus): void
{
$epostLockSnos = array_keys($orderStatusBySno, OrderStatusCode::BACK_REQUEST->value, true);
 
if (!empty($epostLockSnos) && \App::getInstance(\Component\Order\EpostReturnAdmin::class)->isStatusChangeLocked($orderNo, $epostLockSnos)) {
throw new EpostReturnStatusLockedException((string) $this->getOrderStatusAdmin($changeStatus));
}
}
}
 
Component/Order/OrderAdmin.php+156-7
변경 전OrderAdmin.php
use Origin\Enum\Order\OrderStatusCode;
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Enum\Statistics\Analytics\Order\ClaimType;
 
use Origin\Service\Statistics\Analytics\Order\ClaimCompleteAnalyticsCollectorService;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
*/
class OrderAdmin extends \Component\Order\Order
{
 
 
 
 
 
 
 
 
 
 
use CommonLogStorageTrait; // 공용 로그 저장
 
/**
// 택배사 sno에 매핑된 택배사 회사명 배열 가져오기
$invoiceCompanyNameData = $this->getInvoiceCompanyNames();
 
 
 
 
 
 
 
// 결제방법과 처리 상태 설정
foreach ($data as $key => &$val) {
$orderGoods = $val['goods'];
}
}
 
 
 
 
 
//총 할인금액
$totalDcPriceArray = [
$orderData[$oVal['orderNo']]['totalGoodsDcPrice'],
$totalAddGoodsPrice = 0; //추가상품
$_naverpayDelivery = [];
$delivery = new Delivery();
 
 
 
 
 
foreach ($orderGoods as $scmNo => $dataVal) {
// 공급사로 로그인한 경우 처리 해당 공급사 상품만 보여지도록 수정 처리
if($useMultiShippingKey !== true){
$val['userHandleInfo'] = $this->getUserHandleInfo($orderNo, $val['sno']);
}
 
 
 
 
 
if($useMultiShippingKey === true){
// 복수배송지 사용시 상품배열을 [orderInfoSno][orderDeliverySno][goods]의 3차 배열로 재구성
$getData['goods'][$val['orderInfoSno']][$val['orderDeliverySno']][] = $val;
if ($methodType != 'cancel') {
$beforeStatus = $bundleData['orderStatus'][0];
$handleData['handler'] = 'admin';
 
 
foreach ($bundleData['sno'] as $orderGoodsSno) {
$newOrderGoodsData = $this->setHandleAccept($arrData['orderNo'], [$orderGoodsSno], $handleMode, $handleData, $beforeStatus);
 
 
 
 
 
 
 
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($newOrderGoodsData) && !empty($newOrderGoodsData['userHandleGoodsNo']) && !is_array($bundleData['sno'])) {
$bundleData['userHandleGoodsNo'] = $newOrderGoodsData['userHandleGoodsNo'];
unset($newOrderGoodsData);
}
}
 
 
 
 
}
}
}
$addData['memberDcPrice'] = $memberDcPrice;
$addData['memberOverlapDcPrice'] = $memberOverlapDcPrice;
$addData['couponGoodsDcPrice'] = $couponGoodsDcPrice;
// 추가상품은 송장번호 초기화
$addData['invoiceCompanySno'] = '';
$addData['invoiceNo'] = '';
 
 
 
 
 
 
 
// 마이앱 사용에 따른 분기 처리
if ($this->useMyapp) {
// 수량이 0보다 작은경우 처리하지 않는다.
if (isset($bundleData['goodsCnt']) && $bundleData['goodsCnt'][$orderGoodsSno] > 0) {
// 주문상품 update용 데이터
 
$fieldData = $this->divideHandleGoodsCntData($orderNo, $orderGoodsSno, $bundleData['goodsCnt'][$orderGoodsSno]);
 
$tmpOrderGoodsData = [];
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 환불/반품/교환 단계 복구 처리 (환불상세내 상품리스트)
*
$changeStatus = $arrData['changeStatus'];
$isUpdate = false;
 
 
 
// 핸들로 오기 전의 주문상태로 돌릴 수 있는지에 대한 체크 (롤백용)
foreach ($arrData['bundle']['statusCheck'] as $orderGoodsSno) {
 
continue;
}
 
// 주문 상태 변경 처리
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), substr($orderStatus, 0, 1), $changeStatus, __('[이전 단계 복구]'));
 
 
 
 
 
 
 
 
 
 
 
 
 
// 반품/교환/환불과 동일한 주문상태로 변경하지 않는 경우 예외 처리 (ex. 환불->반품, 반품->교환 등)
if (gd_in_array($orderStatus, $this->statusHandleListExclude) && !gd_in_array($changeStatus, $this->statusHandleListExclude)) {
 
// 반품회수 완료시 자동으로 환불접수 단계 이동 처리
if ($isAutoRefund) {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), 'b', 'r1', __('반품회수완료로 인한 자동'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 기존 반품/교환/환불 레코드에서 데이터 추출
$getData = $this->getOrderGoodsData($orderNo, null, $handleSno, null, null, false);
*/
public function updateStatusPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason = false, $bundleFl = false, $mode = null, $useVisit = null, $autoProcess = false, $userHandleFl = null)
{
 
 
 
if (($result = $this->updateStatusUnconditionalPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason, $bundleFl, $mode, $useVisit, $autoProcess, $userHandleFl))!== true) {
if($userHandleFl == false){
if($result['errorMsg']){
}
 
$bundleData['changeStatus'] = $changeStatus;
$bundleData['reason'] = $reason . ' ' . $this->getOrderStatusAdmin($changeStatus) . __(' 처리');
 
 
 
 
 
if($autoProcess == true){
if($bundleData['changeStatus'] == 'r1'){
$bundleData['reason'] = '고객 자동환불 신청';
 
return $refundAccountData;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후OrderAdmin.php
use Origin\Enum\Order\OrderStatusCode;
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Enum\Statistics\Analytics\Order\ClaimType;
use Origin\Exception\Order\EpostReturnStatusLockedException;
use Origin\Service\Statistics\Analytics\Order\ClaimCompleteAnalyticsCollectorService;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
*/
class OrderAdmin extends \Component\Order\Order
{
/**
* @var array 반품접수(b1) 처리된 상품주문번호 — 우체국 자동접수용, 호출부가 커밋 후 읽음
*/
protected array $epostReturnAcceptedSnos = [];
 
/**
* @var string divideHandleGoodsCntData() 가 참조하는 처리모드('r'/'b'/'e'/'z') — 기존 메서드 시그니처를 건드리지 않기 위해 속성으로 전달
*/
protected string $divideHandleMode = '';
 
use CommonLogStorageTrait; // 공용 로그 저장
 
/**
// 택배사 sno에 매핑된 택배사 회사명 배열 가져오기
$invoiceCompanyNameData = $this->getInvoiceCompanyNames();
 
$returnDisplayMap = \App::getInstance(\Component\Order\ReturnInvoice::class)->getDisplayMap(
array_keys($data),
array_column(array_merge(...array_column($data, 'goods')), 'sno')
);
$epostIcon = sprintf("<img src='%s' > ", \UserFilePath::adminSkin('gd_share', 'img', 'channel_icon', 'epost.gif')->www());
 
// 결제방법과 처리 상태 설정
foreach ($data as $key => &$val) {
$orderGoods = $val['goods'];
}
}
 
$returnProgressLabel = $returnDisplayMap[$oVal['sno']]['progressLabel'] ?? null;
$oVal['returnStatusStr'] = $returnProgressLabel === null ? '' : '<br>(' . $epostIcon . $returnProgressLabel . ')';
$oVal['returnTraceUrl'] = $returnDisplayMap[$oVal['sno']]['traceUrl'] ?? null;
 
//총 할인금액
$totalDcPriceArray = [
$orderData[$oVal['orderNo']]['totalGoodsDcPrice'],
$totalAddGoodsPrice = 0; //추가상품
$_naverpayDelivery = [];
$delivery = new Delivery();
$returnDisplayMap = \App::getInstance(\Component\Order\ReturnInvoice::class)->getDisplayMap(
[$orderNo],
array_column(array_merge(...array_values($orderGoods)), 'sno')
);
$epostIcon = sprintf("<img src='%s' > ", \UserFilePath::adminSkin('gd_share', 'img', 'channel_icon', 'epost.gif')->www());
foreach ($orderGoods as $scmNo => $dataVal) {
// 공급사로 로그인한 경우 처리 해당 공급사 상품만 보여지도록 수정 처리
if($useMultiShippingKey !== true){
$val['userHandleInfo'] = $this->getUserHandleInfo($orderNo, $val['sno']);
}
 
$returnProgressLabel = $returnDisplayMap[$val['sno']]['progressLabel'] ?? null;
$val['returnStatusStr'] = $returnProgressLabel === null ? '' : '<br>(' . $epostIcon . $returnProgressLabel . ')';
$val['returnTraceUrl'] = $returnDisplayMap[$val['sno']]['traceUrl'] ?? null;
 
if($useMultiShippingKey === true){
// 복수배송지 사용시 상품배열을 [orderInfoSno][orderDeliverySno][goods]의 3차 배열로 재구성
$getData['goods'][$val['orderInfoSno']][$val['orderDeliverySno']][] = $val;
if ($methodType != 'cancel') {
$beforeStatus = $bundleData['orderStatus'][0];
$handleData['handler'] = 'admin';
$epostReturnSnos = [];
 
foreach ($bundleData['sno'] as $orderGoodsSno) {
$newOrderGoodsData = $this->setHandleAccept($arrData['orderNo'], [$orderGoodsSno], $handleMode, $handleData, $beforeStatus);
 
// 부분수량 접수는 새로 생긴 상품주문번호가 접수 상태가 됨
if (is_array($newOrderGoodsData) && !empty($newOrderGoodsData['userHandleGoodsNo'])) {
$epostReturnSnos[] = (int)$newOrderGoodsData['userHandleGoodsNo'];
} else {
$epostReturnSnos[] = (int)$orderGoodsSno;
}
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($newOrderGoodsData) && !empty($newOrderGoodsData['userHandleGoodsNo']) && !is_array($bundleData['sno'])) {
$bundleData['userHandleGoodsNo'] = $newOrderGoodsData['userHandleGoodsNo'];
unset($newOrderGoodsData);
}
}
 
if ($changeStatus === OrderStatusCode::BACK_REQUEST->value) {
$this->epostReturnAcceptedSnos = $epostReturnSnos;
}
}
}
}
$addData['memberDcPrice'] = $memberDcPrice;
$addData['memberOverlapDcPrice'] = $memberOverlapDcPrice;
$addData['couponGoodsDcPrice'] = $couponGoodsDcPrice;
if ($this->divideHandleMode === 'b') {
// 반품은 이미 나간 배송을 그대로 수거하는 것이라 원 송장을 그대로 승계함(신규 출고가 아님)
$addData['invoiceCompanySno'] = $originData['invoiceCompanySno'];
$addData['invoiceNo'] = $originData['invoiceNo'];
} else {
// 교환/환불 등 신규 출고가 필요한 추가상품은 송장번호 초기화
$addData['invoiceCompanySno'] = '';
$addData['invoiceNo'] = '';
}
 
// 마이앱 사용에 따른 분기 처리
if ($this->useMyapp) {
// 수량이 0보다 작은경우 처리하지 않는다.
if (isset($bundleData['goodsCnt']) && $bundleData['goodsCnt'][$orderGoodsSno] > 0) {
// 주문상품 update용 데이터
$this->divideHandleMode = $handleMode;
$fieldData = $this->divideHandleGoodsCntData($orderNo, $orderGoodsSno, $bundleData['goodsCnt'][$orderGoodsSno]);
 
$tmpOrderGoodsData = [];
}
}
 
/**
* setHandleRollback 주문 로그 사유 지정값 — 자동 전이(예: 우체국 반품회수완료)가 기본 문구 대신 쓸 문구
*/
protected $handleRollbackReason = null;
 
/**
* setHandleRollback 의 b4 전이 로그 처리자 지정값
*/
protected $handleRollbackManagerId = null;
 
/**
* setAutoRefundAccept 의 r1 전이 로그 처리자 IP 지정값
*/
protected $handleRollbackManagerIp = null;
 
public function setHandleRollbackReason(?string $reason): void
{
$this->handleRollbackReason = $reason;
}
 
public function setHandleRollbackManagerId(?string $managerId): void
{
$this->handleRollbackManagerId = $managerId;
}
 
public function setHandleRollbackManagerIp(?string $managerIp): void
{
$this->handleRollbackManagerIp = $managerIp;
}
 
/**
* 환불/반품/교환 단계 복구 처리 (환불상세내 상품리스트)
*
$changeStatus = $arrData['changeStatus'];
$isUpdate = false;
 
$this->blockEpostPickupInProgress($orderNo, array_intersect_key($arrData['bundle']['orderStatus'] ?? [], array_flip($arrData['bundle']['statusCheck'] ?? [])), $changeStatus);
 
// 핸들로 오기 전의 주문상태로 돌릴 수 있는지에 대한 체크 (롤백용)
foreach ($arrData['bundle']['statusCheck'] as $orderGoodsSno) {
 
continue;
}
 
// 지정 처리자가 있으면 이 전이 로그에만 남김 — 뒤따르는 자동 환불접수(r1) 로그는 공란 유지
$canSetManagerId = method_exists($this, 'setLogManagerId');
if ($this->handleRollbackManagerId !== null && $canSetManagerId) {
$this->setLogManagerId($this->handleRollbackManagerId);
}
try {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), substr($orderStatus, 0, 1), $changeStatus, $this->handleRollbackReason ?? __('[이전 단계 복구]'));
} catch (\Throwable $e) {
throw $e;
} finally {
if ($this->handleRollbackManagerId !== null && $canSetManagerId) {
$this->setLogManagerId(null);
}
}
 
// 반품/교환/환불과 동일한 주문상태로 변경하지 않는 경우 예외 처리 (ex. 환불->반품, 반품->교환 등)
if (gd_in_array($orderStatus, $this->statusHandleListExclude) && !gd_in_array($changeStatus, $this->statusHandleListExclude)) {
 
// 반품회수 완료시 자동으로 환불접수 단계 이동 처리
if ($isAutoRefund) {
$autoRefundReason = __('반품회수완료로 인한 자동 환불접수 처리');
// 공유 인스턴스라 반드시 되돌림 — 안 그러면 setHandleRollback() 바깥 루프의 다음 상품이 이 문구를 그대로 물려받음
$prevRollbackReason = $this->handleRollbackReason;
$canSetManagerIp = method_exists($this, 'setLogManagerIp');
if ($prevRollbackReason !== null) {
// 지정 사유 흐름이면 뒤따르는 이 로그도 전문 그대로 — 안 하면 기본 꼬리('환불접수 처리')가 붙어 문구가 중복됨
$this->handleRollbackReason = $autoRefundReason;
}
if ($this->handleRollbackManagerIp !== null && $canSetManagerIp) {
$this->setLogManagerIp($this->handleRollbackManagerIp);
}
try {
$this->updateStatusPreprocess($orderNo, $this->getOrderGoods($orderNo, null, $handleSno, ['orderStatus']), 'b', 'r1', $autoRefundReason);
} catch (\Throwable $e) {
throw $e;
} finally {
if ($prevRollbackReason !== null) {
$this->handleRollbackReason = $prevRollbackReason;
}
if ($this->handleRollbackManagerIp !== null && $canSetManagerIp) {
$this->setLogManagerIp(null);
}
}
 
// 기존 반품/교환/환불 레코드에서 데이터 추출
$getData = $this->getOrderGoodsData($orderNo, null, $handleSno, null, null, false);
*/
public function updateStatusPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason = false, $bundleFl = false, $mode = null, $useVisit = null, $autoProcess = false, $userHandleFl = null)
{
// statusMode 는 호출부마다 실제 orderStatus 와 어긋날 수 있어 신뢰하지 않음 — b1 판정은 아래에서 실제 상태로 다시 거름
$this->blockEpostPickupInProgress($orderNo, array_column((array)$goodsData, 'orderStatus', 'sno'), $changeStatus);
 
if (($result = $this->updateStatusUnconditionalPreprocess($orderNo, $goodsData, $statusMode, $changeStatus, $reason, $bundleFl, $mode, $useVisit, $autoProcess, $userHandleFl))!== true) {
if($userHandleFl == false){
if($result['errorMsg']){
}
 
$bundleData['changeStatus'] = $changeStatus;
if ($this->handleRollbackReason !== null && $reason === $this->handleRollbackReason) {
// 지정 사유는 전문 그대로 — 기본 꼬리('{상태} 처리')를 붙이면 문구가 중복됨
$bundleData['reason'] = $reason;
} else {
$bundleData['reason'] = $reason . ' ' . $this->getOrderStatusAdmin($changeStatus) . __(' 처리');
}
if($autoProcess == true){
if($bundleData['changeStatus'] == 'r1'){
$bundleData['reason'] = '고객 자동환불 신청';
 
return $refundAccountData;
}
 
/**
* 반품접수(b1) 처리된 상품주문번호 — 우체국 자동접수용
*/
public function getEpostReturnAcceptedSnos(): array
{
return $this->epostReturnAcceptedSnos;
}
 
/**
* 우체국택배 수거 진행 건이 있으면 상태변경을 거부함
*
* @param string $orderNo 주문번호
* @param array $orderStatusBySno 상품주문번호별 주문상태 맵
* @param string $changeStatus 변경할 주문상태 코드
*
* @throws EpostReturnStatusLockedException 수거 진행 건 포함
*/
protected function blockEpostPickupInProgress(string $orderNo, array $orderStatusBySno, string $changeStatus): void
{
$epostLockSnos = array_keys($orderStatusBySno, OrderStatusCode::BACK_REQUEST->value, true);
 
if (!empty($epostLockSnos) && \App::getInstance(\Component\Order\EpostReturnAdmin::class)->isStatusChangeLocked($orderNo, $epostLockSnos)) {
throw new EpostReturnStatusLockedException((string) $this->getOrderStatusAdmin($changeStatus));
}
}
}
 
Component/Order/OrderNew.php+31
변경 전OrderNew.php
 
protected $logManagerNo = '';
 
 
 
 
 
 
 
 
 
 
 
public $orderExchangeChangeDate;
 
/**
return $this->channel;
}
 
 
 
 
 
 
 
 
 
 
 
public function setLogManagerNo($logManagerNo){
$this->logManagerNo = $logManagerNo;
}
}
}
 
 
 
 
 
 
 
 
 
 
 
 
$arrBind = $this->db->get_binding(DBTableField::tableLogOrder(), $arrData, 'insert');
$this->db->set_insert_db(DB_LOG_ORDER, $arrBind['param'], $arrBind['bind'], 'y');
unset($arrBind);
 
변경 후OrderNew.php
 
protected $logManagerNo = '';
 
/**
* 주문 로그 처리자 표기 지정값 — 세션 관리자 대신 시스템 주체(예: 우체국)로 남길 때 세터로 지정함
*/
protected $logManagerId = null;
 
/**
* 주문 로그 처리자 IP 표기 지정값 — 세션 없는 자동 전이(예: 우체국 자동 환불접수)에서 요청 IP 대신 공란으로 남길 때 세터로 지정함
*/
protected $logManagerIp = null;
 
public $orderExchangeChangeDate;
 
/**
return $this->channel;
}
 
public function setLogManagerId(?string $logManagerId): void
{
$this->logManagerId = $logManagerId;
}
 
public function setLogManagerIp(?string $logManagerIp): void
{
$this->logManagerIp = $logManagerIp;
}
 
public function setLogManagerNo($logManagerNo){
$this->logManagerNo = $logManagerNo;
}
}
}
 
// 시스템 주체 처리자 표기(예: 우체국) — 지정돼 있으면 세션 관리자 대신 남김 (네이버페이센터 표기와 같은 계열)
if ($this->logManagerId !== null) {
$arrData['managerNo'] = 0;
$arrData['managerId'] = $this->logManagerId;
}
 
// 처리자 공란 지정된 자동 전이(예: 우체국 자동 환불접수)는 요청 IP도 함께 공란 — 세션 없는 배치·API 호출 IP가 노출되는 것을 막음
if ($this->logManagerIp !== null) {
$arrData['managerIp'] = $this->logManagerIp;
}
 
$arrBind = $this->db->get_binding(DBTableField::tableLogOrder(), $arrData, 'insert');
$this->db->set_insert_db(DB_LOG_ORDER, $arrBind['param'], $arrBind['bind'], 'y');
unset($arrBind);
 
Component/Order/Order.php+31
변경 전Order.php
 
protected $logManagerNo = '';
 
 
 
 
 
 
 
 
 
 
 
public $orderExchangeChangeDate;
 
/**
return $this->channel;
}
 
 
 
 
 
 
 
 
 
 
 
public function setLogManagerNo($logManagerNo){
$this->logManagerNo = $logManagerNo;
}
}
}
 
 
 
 
 
 
 
 
 
 
 
 
$arrBind = $this->db->get_binding(DBTableField::tableLogOrder(), $arrData, 'insert');
$this->db->set_insert_db(DB_LOG_ORDER, $arrBind['param'], $arrBind['bind'], 'y');
unset($arrBind);
 
변경 후Order.php
 
protected $logManagerNo = '';
 
/**
* 주문 로그 처리자 표기 지정값 — 세션 관리자 대신 시스템 주체(예: 우체국)로 남길 때 세터로 지정함
*/
protected $logManagerId = null;
 
/**
* 주문 로그 처리자 IP 표기 지정값 — 세션 없는 자동 전이(예: 우체국 자동 환불접수)에서 요청 IP 대신 공란으로 남길 때 세터로 지정함
*/
protected $logManagerIp = null;
 
public $orderExchangeChangeDate;
 
/**
return $this->channel;
}
 
public function setLogManagerId(?string $logManagerId): void
{
$this->logManagerId = $logManagerId;
}
 
public function setLogManagerIp(?string $logManagerIp): void
{
$this->logManagerIp = $logManagerIp;
}
 
public function setLogManagerNo($logManagerNo){
$this->logManagerNo = $logManagerNo;
}
}
}
 
// 시스템 주체 처리자 표기(예: 우체국) — 지정돼 있으면 세션 관리자 대신 남김 (네이버페이센터 표기와 같은 계열)
if ($this->logManagerId !== null) {
$arrData['managerNo'] = 0;
$arrData['managerId'] = $this->logManagerId;
}
 
// 처리자 공란 지정된 자동 전이(예: 우체국 자동 환불접수)는 요청 IP도 함께 공란 — 세션 없는 배치·API 호출 IP가 노출되는 것을 막음
if ($this->logManagerIp !== null) {
$arrData['managerIp'] = $this->logManagerIp;
}
 
$arrBind = $this->db->get_binding(DBTableField::tableLogOrder(), $arrData, 'insert');
$this->db->set_insert_db(DB_LOG_ORDER, $arrBind['param'], $arrBind['bind'], 'y');
unset($arrBind);
 
Component/Order/ReOrderCalculation.php+8
변경 전ReOrderCalculation.php
}
 
$webhookOrderGoodsSnos = [];
 
// 각 상태별 처리 및 취소 테이블 등록
if (!empty($bundleData)) {
// 상태 변경 처리
if (!empty($newOrderGoodsData['userHandleGoodsNo'])) {
$webhookOrderGoodsSnos[] = $newOrderGoodsData['userHandleGoodsNo'];
$webhookOrderGoodsSnos[] = $orderGoodsSno;
 
 
 
 
}
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($webhookOrderGoodsSnos)){
$this->orderWebHookService->sendOrderGoodsWebHook($webhookOrderGoodsSnos, 'restore');
}
 
 
 
}
 
/**
 
변경 후ReOrderCalculation.php
}
 
$webhookOrderGoodsSnos = [];
$epostReturnAcceptedSnos = [];
// 각 상태별 처리 및 취소 테이블 등록
if (!empty($bundleData)) {
// 상태 변경 처리
if (!empty($newOrderGoodsData['userHandleGoodsNo'])) {
$webhookOrderGoodsSnos[] = $newOrderGoodsData['userHandleGoodsNo'];
$webhookOrderGoodsSnos[] = $orderGoodsSno;
// 부분수량 접수는 새로 생긴 상품주문번호가 접수 상태가 됨
$epostReturnAcceptedSnos[] = (int) $newOrderGoodsData['userHandleGoodsNo'];
} else {
$epostReturnAcceptedSnos[] = (int) $orderGoodsSno;
}
 
// 주문상품이 새로 생성됐을시, 클레임신청 정보 수정
if (is_array($webhookOrderGoodsSnos)){
$this->orderWebHookService->sendOrderGoodsWebHook($webhookOrderGoodsSnos, 'restore');
}
 
// 반품접수(b1)만 우체국 자동접수 대상 — 컨트롤러가 트랜잭션 커밋 후 트리거를 부름
return $methodType === 'back' ? $epostReturnAcceptedSnos : null;
}
 
/**
 
Component/Order/ReturnInvoice.php+199
변경 전ReturnInvoice.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후ReturnInvoice.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\Order;
 
use Bundle\Component\Delivery\Delivery;
use Framework\Log\Channel;
use Framework\Log\Logger;
use Origin\Enum\Order\EpostDeliveryStatus;
use Origin\Enum\Order\EpostReturnInvoiceStatus;
use Origin\Repository\Order\OrderReturnInvoiceRepository;
 
/**
* 반품 수거 송장·상태의 화면 지원 — 병기 라벨·반송추적 노출 판정·조회 URL 조립의 공용 지점.
* 판정·조립 값은 화면 목록이 조인해 온 행에서 받으며, 마이페이지 목록만 보충 조회 1회로 주입함
*/
class ReturnInvoice
{
private const LOG_CHANNEL = 'epost';
 
/**
* @var Channel epost 채널로 고정한 로거
*/
protected readonly Channel $logger;
 
public function __construct(
protected Delivery $delivery,
protected OrderReturnInvoiceRepository $returnInvoiceRepository,
Logger $logger
) {
$this->logger = $logger->channel(self::LOG_CHANNEL);
}
 
/**
* 처리상태에 괄호로 병기할 우체국 진행 라벨
*
* 한 컬럼에 고도몰 자체 상태와 우체국 배송상태가 함께 실리므로 둘을 차례로 판정함
*
* @param string|null $returnInvoiceStatus 반품 수거 상태
* @return string|null 병기 대상이 아니면 null
*/
public function getProgressLabel(?string $returnInvoiceStatus): ?string
{
if ($returnInvoiceStatus === null) {
return null;
}
 
if ($returnInvoiceStatus === EpostReturnInvoiceStatus::FAILED->value) {
return __('수거신청 실패');
}
 
if ($returnInvoiceStatus === EpostReturnInvoiceStatus::REQUESTED->value) {
return __('수거 접수');
}
 
return match (EpostDeliveryStatus::tryFrom($returnInvoiceStatus)) {
EpostDeliveryStatus::PRINT_INVOICE => __('수거 접수'),
EpostDeliveryStatus::COLLECT => __('수거 완료'),
EpostDeliveryStatus::NOT_COLLECT => __('미수거'),
EpostDeliveryStatus::TAKE_OVER,
EpostDeliveryStatus::SEND,
EpostDeliveryStatus::RECEIVE,
EpostDeliveryStatus::DELIVERY_READY => __('반송중'),
EpostDeliveryStatus::NOT_DELIVERY => __('미배달'),
default => null,
};
}
 
/**
* 반송추적 노출 여부
*
* @param string|null $returnInvoiceNo 반품 수거 송장번호
* @param string|null $returnInvoiceStatus 반품 수거 상태
* @return bool 노출이면 true
*/
public function isTraceVisible(?string $returnInvoiceNo, ?string $returnInvoiceStatus): bool
{
return !empty($returnInvoiceNo)
&& $returnInvoiceStatus !== null
&& $returnInvoiceStatus !== EpostReturnInvoiceStatus::FAILED->value;
}
 
/**
* 반송추적 조회 URL. 서버가 조립해 화면에 내려주며 클라이언트 조립 진입점을 두지 않음
*
* @param string|null $returnInvoiceNo 반품 수거 송장번호
* @param string|null $returnInvoiceStatus 반품 수거 상태
* @param int|null $returnCarrierNo 반품 수거 택배사
* @return string|null 노출 대상이 아니면 null
*/
public function getTraceUrl(?string $returnInvoiceNo, ?string $returnInvoiceStatus, ?int $returnCarrierNo): ?string
{
if (!$this->isTraceVisible($returnInvoiceNo, $returnInvoiceStatus) || empty($returnCarrierNo)) {
return null;
}
 
return $this->delivery->getDeliveryTrace($returnCarrierNo, $returnInvoiceNo);
}
 
/**
* 조회 행 목록의 반송추적 URL 맵
*
* 한 페이지의 택배사는 사실상 우체국 하나로 고정되므로, 택배사별 URL 템플릿을 1회만 조회해
* 메모이제이션하고 행마다는 송장번호 치환만 함(행 수만큼 조회하던 N+1 제거)
*
* @param array $rows findReturnInvoiceStatusRows 행 목록
* @return array [orderGoodsNo => 서버 조립 URL] — 노출 대상 아닌 행은 뺌
*/
public function getTraceUrlMap(array $rows): array
{
$traceUrlMap = [];
$templatesByCarrier = [];
 
foreach ($rows as $row) {
if (!$this->isTraceVisible($row['returnInvoiceNo'], $row['returnInvoiceStatus']) || empty($row['returnCarrierNo'])) {
continue;
}
 
$carrierSno = (int) $row['returnCarrierNo'];
if (!array_key_exists($carrierSno, $templatesByCarrier)) {
$templatesByCarrier[$carrierSno] = $this->delivery->getDeliveryTraceUrlTemplate($carrierSno);
}
 
$template = $templatesByCarrier[$carrierSno];
$traceUrl = str_replace('__INVOICENO__', str_replace('-', '', $row['returnInvoiceNo']), (string) $template['traceUrl']);
 
if (!filter_var($traceUrl, FILTER_VALIDATE_URL)) {
$this->logger->warning('반송추적 URL 조립 실패 — 택배사 traceUrl 미설정', [
'orderGoodsNo' => $row['orderGoodsNo'],
'returnCarrierNo' => $carrierSno,
]);
continue;
}
 
$traceUrlMap[$row['orderGoodsNo']] = $traceUrl;
}
 
return $traceUrlMap;
}
 
/**
* 상품주문번호별 병기 라벨·반송추적 URL — 목록이 조인하지 않는 화면용 보충 조회 1회
*
* @param array $orderNos 주문번호 목록
* @param array $orderGoodsSnos 상품주문번호 목록
* @return array [orderGoodsNo => ['progressLabel' => 라벨, 'traceUrl' => URL]] — 대상 아닌 값은 null
*/
public function getDisplayMap(array $orderNos, array $orderGoodsSnos): array
{
if (empty($orderNos) || empty($orderGoodsSnos)) {
return [];
}
 
$rows = $this->returnInvoiceRepository->findReturnInvoiceStatusRows($orderNos, $orderGoodsSnos);
$traceUrlMap = $this->getTraceUrlMap($rows);
 
$displayMap = [];
foreach ($rows as $row) {
$displayMap[$row['orderGoodsNo']] = [
'progressLabel' => $this->getProgressLabel($row['returnInvoiceStatus']),
'traceUrl' => $traceUrlMap[$row['orderGoodsNo']] ?? null,
];
}
 
return $displayMap;
}
 
/**
* 마이페이지 주문 목록 행에 반송추적 URL 주입 — 보충 조회 1회, 노출 대상 아니면 null (템플릿은 null 여부로만 분기)
*
* @param array $orderData 주문 목록 (각 행에 orderNo·goods)
* @return array returnTraceUrl 이 주입된 주문 목록
*/
public function assignTraceUrls(array $orderData): array
{
$orderGoodsSnos = array_column(array_merge(...array_column($orderData, 'goods')), 'sno');
if (empty($orderGoodsSnos)) {
return $orderData;
}
 
$traceUrlMap = $this->getTraceUrlMap($this->returnInvoiceRepository->findReturnInvoiceStatusRows(
array_values(array_unique(array_column($orderData, 'orderNo'))),
$orderGoodsSnos
));
 
foreach ($orderData as &$order) {
foreach ($order['goods'] ?? [] as $goodsKey => $orderGoods) {
$order['goods'][$goodsKey]['returnTraceUrl'] = $traceUrlMap[$orderGoods['sno']] ?? null;
}
}
 
return $orderData;
}
}
 
Controller/Admin/Order/EpostReturnRequestController.php+220
변경 전EpostReturnRequestController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후EpostReturnRequestController.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\Order;
 
use Component\Order\EpostReturnAdmin;
use Component\Page\Page;
use DTO\Order\EpostReturnSearchDTO;
use Framework\Debug\Exception\AlertBackException;
use Origin\Service\Order\EpostReturnAccessLogService;
use Request;
 
/**
* 우체국택배 수거신청
*/
class EpostReturnRequestController extends \Controller\Admin\Controller
{
private const LOG_MENU = '주문/배송>택배연동 서비스>우체국택배 수거신청';
private const LOG_PAGE = '우체국택배 수거신청';
private const LOG_ACTION = '수거신청 대상 조회';
 
/**
* {@inheritdoc}
*/
public function index()
{
$this->callMenu('order', 'epostParcel', 'collectRequest');
$this->setNcdsSubBodyClass();
 
// 서비스 미신청 상점 진입 차단 — 앱 미설치 상점은 메뉴 자체가 없어 여기 오지 않음
if (\App::getInstance(EpostReturnAdmin::class)->isUsageNotRequested()) {
throw new AlertBackException(__('우체국택배 서비스 신청 후 이용 가능합니다.'));
}
 
$getValue = Request::get()->toArray();
 
// 첫 진입 기간 기본 7일 — 파라미터가 아예 없을 때만. 비워서 제출한 검색은 전체 기간 의도라 그대로 둠
if (!isset($getValue['treatDateStart']) && !isset($getValue['treatDateEnd'])) {
$getValue['treatDateStart'] = date('Y-m-d', strtotime('-6 days'));
$getValue['treatDateEnd'] = date('Y-m-d');
}
 
$targetList = \App::getInstance(EpostReturnAdmin::class)->getTargetList($getValue);
$this->writeAccessLog($targetList['data'], $targetList['total']);
 
// [수거 신청] 쓰기 권한 — 화면은 얼럿 판정 재료만 받고 최종 차단은 PS 서버 검증
$naviMenu = $this->getData('naviMenu');
$writable = $naviMenu->getAdminMenuWritableAuth($naviMenu->lno, $naviMenu->location, $naviMenu->accessMenu, $naviMenu->writeEnabledMenu);
 
// 공급사 콤보박스 목록 — 본사도 하나의 공급사로 목록 하단에 둠
$scmList = \App::load(\Component\Scm\Scm::class)->selectOperationScmList();
$suppliers = [];
$headquarters = null;
foreach ((array)$scmList as $scm) {
if ((int)$scm['scmNo'] === DEFAULT_CODE_SCMNO) {
$headquarters = $scm;
continue;
}
$suppliers[] = $scm;
}
if ($headquarters !== null) {
$suppliers[] = $headquarters;
}
 
// 페이지·페이지크기 보정은 EpostReturnSearchDTO 한 곳 — 보정 적용값을 그대로 사용
$page = $targetList['page'];
$pageSize = $targetList['pageSize'];
 
// 행 번호 — 뷰는 출력만 함
$rowNo = $targetList['total'] - (($page - 1) * $pageSize);
foreach (array_keys($targetList['data']) as $key) {
$targetList['data'][$key]['rowNo'] = $rowNo--;
}
 
$this->setData('groups', $this->buildRowGroups($targetList['data']));
$this->setData('rows', $targetList['data']);
$this->setData('total', $targetList['total']);
$this->setData('grandTotal', $targetList['grandTotal']);
$totalPages = max(1, (int)ceil($targetList['total'] / $pageSize));
$pagination = new Page($page, $targetList['total'], $targetList['grandTotal'], $pageSize);
 
$this->setData('page', $page);
$this->setData('totalPages', $totalPages);
$this->setData('pagination', $pagination->getPage("goEprPage('PAGELINK')"));
$this->setData('pageSize', $pageSize);
$this->setData('search', $getValue);
$this->setData('searchKeyOptions', $this->toSelectOptions(EpostReturnSearchDTO::SEARCH_FIELDS));
$this->setData('treatDateOptions', $this->toSelectOptions(EpostReturnSearchDTO::DATE_FIELDS));
$this->setData('matchTypeOptions', $this->toSelectOptions(EpostReturnSearchDTO::MATCH_TYPES));
$this->setData('sortOptions', $this->buildSortOptions());
$this->setData('pageSizeOptions', array_map(
static fn (int $size) => ['id' => (string)$size, 'text' => $size . '개 보기'],
EpostReturnSearchDTO::PAGE_SIZES
));
$this->setData('collectionStatusFields', EpostReturnSearchDTO::COLLECTION_STATUSES);
$this->setData('collectionStatuses', (array)($getValue['collectionStatuses'] ?? []));
$this->setData('scmNos', array_map('intval', (array)($getValue['scmNos'] ?? [])));
$this->setData('suppliers', $suppliers);
$this->setData('hasWriteAuth', (bool)$writable['check']);
 
$this->getView()->setDefine('epostReturnResult', 'order/epost_return_request/epost_return_request_result.php');
 
// 수거 신청 처리 후 재조회
if (Request::isAjax()) {
$this->getView()->setData($this->getData());
echo $this->getView()->render('epostReturnResult');
exit;
}
 
$this->getView()->setDefine('epostReturnSearch', 'order/epost_return_request/epost_return_request_search.php');
$this->getView()->setPageName('order/epost_return_request.php');
}
 
/**
* 정렬 선택지 — 정렬 컬럼마다 내림/오름차순 조합
*
* @return array [['id' => '컬럼 방향', 'text' => 라벨], ...]
*/
protected function buildSortOptions(): array
{
$options = [];
foreach (EpostReturnSearchDTO::SORT_FIELDS as $column => $label) {
$options[] = ['id' => $column . ' desc', 'text' => $label . ' 내림차순'];
$options[] = ['id' => $column . ' asc', 'text' => $label . ' 오름차순'];
}
 
return $options;
}
 
/**
* 라벨 맵을 NCUA SelectBox 옵션 형태로 변환
*
* @param array $fields 값 => 라벨 맵
* @return array [['id' => 값, 'text' => 라벨], ...]
*/
protected function toSelectOptions(array $fields): array
{
return array_map(
static fn (string $id, string $text) => ['id' => $id, 'text' => $text],
array_keys($fields),
$fields
);
}
 
/**
* 그리드 병합 구조 — 연속 동일 주문번호 = 병합 그룹, 그룹 안 연속 동일 공급사 = 공급사 병합 구간
*
* @param array $rows 조회 행
* @return array 그룹 목록 [orderNo, rows, scmRunStart(행 인덱스 => rowspan)]
*/
protected function buildRowGroups(array $rows): array
{
$groups = [];
foreach ($rows as $row) {
$last = count($groups) - 1;
if ($last >= 0 && $groups[$last]['orderNo'] === $row['orderNo']) {
$groups[$last]['rows'][] = $row;
} else {
$groups[] = ['orderNo' => $row['orderNo'], 'rows' => [$row], 'scmRunStart' => []];
}
}
 
foreach ($groups as $gKey => $group) {
$runs = [];
foreach ($group['rows'] as $idx => $row) {
$lastRun = count($runs) - 1;
if ($lastRun >= 0 && $runs[$lastRun]['scmNo'] === $row['scmNo']) {
$runs[$lastRun]['count']++;
} else {
$runs[] = ['scmNo' => $row['scmNo'], 'start' => $idx, 'count' => 1];
}
}
foreach ($runs as $run) {
$groups[$gKey]['scmRunStart'][$run['start']] = $run['count'];
}
}
 
// 행 병합 표시 재료 — 뷰는 출력만 함
foreach ($groups as $gKey => $group) {
$groupRowCount = count($group['rows']);
foreach (array_keys($group['rows']) as $idx) {
$groups[$gKey]['rows'][$idx]['isFirst'] = $idx === 0;
$groups[$gKey]['rows'][$idx]['groupRowCount'] = $groupRowCount;
$groups[$gKey]['rows'][$idx]['scmRowspan'] = $group['scmRunStart'][$idx] ?? 0;
}
}
 
return $groups;
}
 
/**
* 수거신청 대상 조회 접속기록
*
* @param array $rows 조회된 대상 행
* @param int $total 검색 건수
*/
protected function writeAccessLog(array $rows, int $total): void
{
$session = \App::getInstance('session');
$orderNos = array_values(array_unique(array_column($rows, 'orderNo')));
 
\App::getInstance(EpostReturnAccessLogService::class)->write(
__(self::LOG_MENU),
__(self::LOG_PAGE),
__(self::LOG_ACTION),
(string)$session->get('manager.sno'),
(string)$session->get('manager.managerId'),
(string)$session->get('manager.scmNo'),
preg_replace('#^/{2,}#', '/', (Request::getRequestUri() ?? '')),
(string)Request::getReferer(),
(string)Request::getRemoteAddress(),
['searchData' => gd_implode(', ', $orderNos), 'searchCnt' => $total]
);
}
}
 
Controller/Admin/Order/EpostReturnRequestPsController.php+106
변경 전EpostReturnRequestPsController.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후EpostReturnRequestPsController.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\Order;
 
use Component\Order\EpostReturnAdmin;
use Origin\DTO\Order\EpostReturnRequestResultDTO;
use Origin\Exception\Order\EpostReturnRequestRejectedException;
use Origin\Service\Order\EpostReturnAccessLogService;
use Request;
use Throwable;
 
/**
* 우체국택배 수거신청 실행
*/
class EpostReturnRequestPsController extends \Controller\Admin\Controller
{
private const LOG_MENU = '주문/배송>택배연동 서비스>우체국택배 수거신청';
private const LOG_PAGE = '우체국택배 수거신청';
private const LOG_ACTION = '우체국 수거 예약 전송(고객 정보 조회·전송 포함)';
 
/**
* {@inheritdoc}
*/
public function index()
{
// 레이아웃 미사용 Ajax — callMenu 는 쓰기 권한 판정 재료(naviMenu) 세팅용
$this->callMenu('order', 'epostParcel', 'collectRequest');
 
$post = Request::post()->toArray();
if (($post['mode'] ?? '') !== 'pickupRequest') {
$this->json(['code' => 0, 'message' => __('잘못된 요청입니다.')]);
 
return;
}
 
$orderGoodsSnos = array_values(array_unique(array_map('intval', (array)($post['orderGoodsSnos'] ?? []))));
if (empty($orderGoodsSnos)) {
$this->json(['code' => 0, 'reason' => 'NO_SELECTION', 'message' => __('수거 신청할 주문을 선택해 주세요.'), 'subMessage' => '']);
 
return;
}
 
$naviMenu = $this->getData('naviMenu');
$writable = $naviMenu->getAdminMenuWritableAuth($naviMenu->lno, $naviMenu->location, $naviMenu->accessMenu, $naviMenu->writeEnabledMenu);
 
try {
/** @var EpostReturnRequestResultDTO $result */
$result = \App::getInstance(EpostReturnAdmin::class)
->applyManualRequest((bool)$writable['check'], !gd_is_provider(), $orderGoodsSnos);
 
// 자격 검증을 통과해 전송 단계까지 갔을 때만 남김 — 거부는 예외로 빠져 여기 오지 않음
$this->writePickupRequestAccessLog($orderGoodsSnos, $result);
 
$this->json([
'code' => 200,
'requestedCount' => count($orderGoodsSnos),
'successCount' => $result->getSuccessCount(),
'failureCount' => $result->getFailureCount(),
'excludedCount' => count($result->getExcludedOrderGoodsSnos()),
]);
} catch (EpostReturnRequestRejectedException $e) {
$reason = $e->getReason();
$this->json(['code' => 0, 'reason' => $reason->value, 'message' => __($reason->alertTitle()), 'subMessage' => __($reason->alertSubtitle())]);
} catch (Throwable $e) {
// 전송 도중 실패라도 고객 정보 조회·전송이 이미 일어났을 수 있어 접속기록은 남김(신청 결과가 없으니 대상 목록만)
\App::getInstance('logger')->channel('epost')->error('우체국택배 수거 신청 실패', ['orderGoodsSnos' => $orderGoodsSnos, 'exception' => $e]);
$this->writePickupRequestAccessLog($orderGoodsSnos, null);
$this->json(['code' => 0, 'reason' => 'ERROR', 'message' => __('수거 신청에 실패하였습니다.'), 'subMessage' => __('잠시 후 다시 시도해 주세요.')]);
}
}
 
/**
* 수거 신청 접속기록 — 실행당 1건, 처리 대상은 data 에 담음
*
* @param array $orderGoodsSnos 신청 대상 주문상품번호
* @param ?EpostReturnRequestResultDTO $result 신청 결과 — 전송 도중 실패로 없으면 null
*/
protected function writePickupRequestAccessLog(array $orderGoodsSnos, ?EpostReturnRequestResultDTO $result): void
{
$session = \App::getInstance('session');
 
\App::getInstance(EpostReturnAccessLogService::class)->write(
__(self::LOG_MENU),
__(self::LOG_PAGE),
__(self::LOG_ACTION),
(string)$session->get('manager.sno'),
(string)$session->get('manager.managerId'),
(string)$session->get('manager.scmNo'),
preg_replace('#^/{2,}#', '/', (Request::getRequestUri() ?? '')),
(string)Request::getReferer(),
(string)Request::getRemoteAddress(),
$result !== null ? [
'orderGoodsSnos' => $orderGoodsSnos,
'successCount' => $result->getSuccessCount(),
'failureCount' => $result->getFailureCount(),
'excludedCount' => count($result->getExcludedOrderGoodsSnos()),
] : ['orderGoodsSnos' => $orderGoodsSnos, 'error' => true]
);
}
}
 
Controller/Admin/Order/OrderChangePsController.php+8-1
변경 전OrderChangePsController.php
 
use App;
use Bundle\Component\Godo\NaverPayAPI;
 
use Component\Order\UserHandleAutoProcess;
use Exception;
use Request;
case "back" : // 반품
try {
$autoResult = ['approve' => [], 'reject' => []];
\DB::transaction(function () use ($orderReorderCalculation, $autoProcess, $postValue, &$autoResult) {
// 고객 클레임 신청 건의 자동 승인/거절 처리 (관리자 반품 접수 = 'b')
if (!empty($postValue['back']['statusCheck'])) {
$orderGoodsSnoList = array_keys($postValue['back']['statusCheck']);
// 트랜잭션 커밋 후 자동 승인/거절 건의 SMS/알림톡 자동 발송
$this->sendAutoApproveRejectSms($postValue['orderNo'], $autoResult);
 
 
 
 
 
 
 
$script = "parent.opener.location.href='./order_view.php?orderNo=".$postValue['orderNo']."';parent.self.close();";
$this->layerNotReload(__('반품 접수가 되었습니다.'), $script);
} catch (Exception $e) {
 
변경 후OrderChangePsController.php
 
use App;
use Bundle\Component\Godo\NaverPayAPI;
use Component\Order\EpostReturnAdmin;
use Component\Order\UserHandleAutoProcess;
use Exception;
use Request;
case "back" : // 반품
try {
$autoResult = ['approve' => [], 'reject' => []];
$epostReturnAcceptedSnos = \DB::transaction(function () use ($orderReorderCalculation, $autoProcess, $postValue, &$autoResult) {
// 고객 클레임 신청 건의 자동 승인/거절 처리 (관리자 반품 접수 = 'b')
if (!empty($postValue['back']['statusCheck'])) {
$orderGoodsSnoList = array_keys($postValue['back']['statusCheck']);
// 트랜잭션 커밋 후 자동 승인/거절 건의 SMS/알림톡 자동 발송
$this->sendAutoApproveRejectSms($postValue['orderNo'], $autoResult);
 
// 우체국택배 수거 자동접수 — 클레임 행 INSERT 가 끝난 뒤(커밋 후)에만 호출, 미사용 상점은 정책을 먼저 직접 확인해 컴포넌트를 안 만듦
$godopostConfig = gd_policy(EpostReturnAdmin::POLICY_KEY);
if (!empty($epostReturnAcceptedSnos) && !empty($godopostConfig['compdivcd']) && ($godopostConfig['autoAcceptFl'] ?? '') === 'y') {
\App::getInstance(EpostReturnAdmin::class)->applyAutoAccept($epostReturnAcceptedSnos);
}
 
$script = "parent.opener.location.href='./order_view.php?orderNo=".$postValue['orderNo']."';parent.self.close();";
$this->layerNotReload(__('반품 접수가 되었습니다.'), $script);
} catch (Exception $e) {
 
Controller/Admin/Order/OrderPsController.php+18
변경 전OrderPsController.php
use Component\Godo\MyGodoSmsServerApi;
use Component\Godo\NaverPayAPI;
use Component\Member\Manager;
 
use Component\Order\Order;
use Component\Order\OrderAdmin;
use Component\Order\OrderDelete;
}
}
);
 
 
 
 
 
 
 
 
$this->layer(__('저장이 완료되었습니다.'), null, 2000);
} catch (Exception $e) {
if (Request::isAjax()) {
}
else {
$aOrderGoodsSno = $order->approveUserHandle(Request::post()->toArray(), 'y'); // 주문 부분환불시
 
 
 
 
 
 
 
 
 
}
$statusCheck = Request::post()->get('statusCheck', []);
$config = ['smsAutoCodeType' => Code::ADMIN_APPROVAL];
 
변경 후OrderPsController.php
use Component\Godo\MyGodoSmsServerApi;
use Component\Godo\NaverPayAPI;
use Component\Member\Manager;
use Component\Order\EpostReturnAdmin;
use Component\Order\Order;
use Component\Order\OrderAdmin;
use Component\Order\OrderDelete;
}
}
);
 
// 우체국택배 수거 자동접수 — 미사용 상점에서는 컴포넌트를 만들지 않도록 정책을 먼저 직접 확인함
$godopostConfig = gd_policy(EpostReturnAdmin::POLICY_KEY);
if (!empty($godopostConfig['compdivcd']) && ($godopostConfig['autoAcceptFl'] ?? '') === 'y'
&& method_exists($order, 'getEpostReturnAcceptedSnos') && !empty($order->getEpostReturnAcceptedSnos())) {
\App::getInstance(EpostReturnAdmin::class)->applyAutoAccept($order->getEpostReturnAcceptedSnos());
}
 
$this->layer(__('저장이 완료되었습니다.'), null, 2000);
} catch (Exception $e) {
if (Request::isAjax()) {
}
else {
$aOrderGoodsSno = $order->approveUserHandle(Request::post()->toArray(), 'y'); // 주문 부분환불시
 
// 우체국택배 수거 자동접수 — 미사용 상점에서는 컴포넌트를 만들지 않도록 정책을 먼저 직접 확인함
$godopostConfig = gd_policy(EpostReturnAdmin::POLICY_KEY);
if (!empty($godopostConfig['compdivcd']) && ($godopostConfig['autoAcceptFl'] ?? '') === 'y'
&& Request::post()->get('statusMode') === 'b') {
\App::getInstance(EpostReturnAdmin::class)->applyAutoAcceptByUserHandles(
array_map(fn (string $item) => (int)explode(INT_DIVISION, $item)[2], Request::post()->get('statusCheck', []))
);
}
}
$statusCheck = Request::post()->get('statusCheck', []);
$config = ['smsAutoCodeType' => Code::ADMIN_APPROVAL];
 
Controller/Admin/Order/PostIssueController.php+5-4
변경 전PostIssueController.php
*/
namespace Bundle\Controller\Admin\Order;
use Framework\Debug\Exception\AlertBackException;
use Exception;
 
use Globals;
use Request;
try {
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'issue');
 
$this->addScript(
[
'jquery/jquery.multi_select_box.js',
]
);
$godopost= gd_policy('order.godopost');
if(empty($godopost['compdivcd']) === true) {
throw new AlertBackException(__("우체국택배 서비스 신청 후 이용 가능합니다."));
}
// --- 모듈 호출
 
변경 후PostIssueController.php
*/
namespace Bundle\Controller\Admin\Order;
use Component\Order\EpostReturnAdmin;
use Exception;
use Framework\Debug\Exception\AlertBackException;
use Globals;
use Request;
try {
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'issue');
$this->setMenuCode('order', 'epostParcel', 'issue');
$this->addScript(
[
'jquery/jquery.multi_select_box.js',
]
);
if (\App::getInstance(EpostReturnAdmin::class)->isUsageNotRequested()) {
throw new AlertBackException(__('우체국택배 서비스 신청 후 이용 가능합니다.'));
 
}
// --- 모듈 호출
 
Controller/Admin/Order/PostPsController.php+169-76
변경 전PostPsController.php
<?php
/**
* This is commercial software, only users who have purchased a valid license
* and accept to the terms of the License Agreement can install and use this
* program.
*
* Do not edit or add to this file if you wish to upgrade Godomall5 to newer
* versions in the future.
*
* @copyright ⓒ 2016, NHN godo: Corp.
* @link http://www.godo.co.kr
*/
namespace Bundle\Controller\Admin\Order;
use Framework\Debug\Exception\LayerException;
use Framework\Debug\Exception\AlertRedirectException;
use Message;
use Request;
/**
* 우체국택배연동 처리 페이지
* @author Young Eun Jung <atomyang@godo.co.kr>
*/
class PostPsController extends \Controller\Admin\Controller
{
/**
* index
*
*/
public function index()
{
$postValue = Request::post()->toArray();
$orderAdmin = \App::load('\\Component\\Order\\OrderAdmin');
try {
switch (Request::request()->get('mode')) {
// 우체국택배 송장 발급
case 'issue':
$result = $orderAdmin->saveOrderInvoice($postValue);
if(gd_count($result) > 0 ) {
throw new LayerException(sprintf(__("필수 정보(수령자명,수령자 핸드폰번호,수령자 주소,수령자 우편번호,주문자명) 가 누락된 주문(%s) 외 우체국 택배 송장번호가 발급되었습니다."), gd_implode(",",$result)),null,null,null,6000);
} else {
throw new LayerException("우체국 택배 송장번호가 발급되었습니다.");
}
break;
// 우체국택배 예약
case 'reserve':
$orderAdmin->reserveOrderInvoice($postValue);
throw new LayerException(__("우체국택배로 예약되었습니다."));
break;
// 우체국택배 취소
case 'cancel':
$orderAdmin->cancelOrderInvoice($postValue);
throw new LayerException(__("송장발급이 취소 되었습니다."));
break;
// 우체국수동업데이트
case 'manual':
$godoPost = \App::load('\\Component\\Godo\\GodoPostServerApi');
$data = $godoPost->saveManualUpdate();
throw new LayerException(sprintf(__("주문 %s 건의 우체국 택배 배송상태를 수동 업데이트 하였습니다."), $data));
break;
}
} catch (\Exception $e) {
throw new LayerException($e->getMessage());
}
exit();
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후PostPsController.php
<?php
 
/**
* This is commercial software, only users who have purchased a valid license
* and accept to the terms of the License Agreement can install and use this
* program.
*
* Do not edit or add to this file if you wish to upgrade Godomall5 to newer
* versions in the future.
*
* @copyright ⓒ 2016, NHN godo: Corp.
* @link http://www.godo.co.kr
*/
namespace Bundle\Controller\Admin\Order;
 
use Component\Order\EpostReturnAdmin;
use Framework\Debug\Exception\LayerException;
use Framework\Debug\Exception\AlertRedirectException;
use Message;
use Origin\Service\Order\EpostReturnAccessLogService;
use Request;
 
/**
* 우체국택배연동 처리 페이지
* @author Young Eun Jung <atomyang@godo.co.kr>
*/
class PostPsController extends \Controller\Admin\Controller
{
private const LOG_MENU = '주문/배송>택배연동 서비스>우체국택배 신청/관리';
private const LOG_PAGE = '우체국택배 신청/관리';
private const LOG_ACTION = '배송상태 수동 업데이트';
 
/**
* index
*
*/
public function index()
{
$postValue = Request::post()->toArray();
 
$orderAdmin = \App::load('\\Component\\Order\\OrderAdmin');
 
try {
switch (Request::request()->get('mode')) {
// 우체국택배 송장 발급
case 'issue':
$this->setMenuCode('order', 'epostParcel', 'issue');
if (\App::getInstance(EpostReturnAdmin::class)->isUsageNotRequested()) {
throw new LayerException(__('우체국택배 서비스 신청 후 이용 가능합니다.'));
}
 
$result = $orderAdmin->saveOrderInvoice($postValue);
 
if(gd_count($result) > 0 ) {
throw new LayerException(sprintf(__("필수 정보(수령자명,수령자 핸드폰번호,수령자 주소,수령자 우편번호,주문자명) 가 누락된 주문(%s) 외 우체국 택배 송장번호가 발급되었습니다."), gd_implode(",",$result)),null,null,null,6000);
} else {
throw new LayerException("우체국 택배 송장번호가 발급되었습니다.");
}
 
 
break;
// 우체국택배 예약
case 'reserve':
$this->setMenuCode('order', 'epostParcel', 'reserve');
if (\App::getInstance(EpostReturnAdmin::class)->isUsageNotRequested()) {
throw new LayerException(__('우체국택배 서비스 신청 후 이용 가능합니다.'));
}
 
$orderAdmin->reserveOrderInvoice($postValue);
throw new LayerException(__("우체국택배로 예약되었습니다."));
break;
// 우체국택배 취소
case 'cancel':
$orderAdmin->cancelOrderInvoice($postValue);
throw new LayerException(__("송장발급이 취소 되었습니다."));
break;
// 우체국수동업데이트(Toast용 json 응답) — 검사 순서: 쓰기 권한 → 발송(01) → 반품(02)
case 'manualUpdateJson':
$this->callMenu('order', 'epostParcel', 'request');
$naviMenu = $this->getData('naviMenu');
$writable = $naviMenu->getAdminMenuWritableAuth($naviMenu->lno, $naviMenu->location, $naviMenu->accessMenu, $naviMenu->writeEnabledMenu);
if (!$writable['check']) {
$this->json(['code' => 0, 'reason' => 'NO_PERMISSION', 'message' => __('우체국택배 신청/관리의 쓰기 권한이 없습니다.'), 'subMessage' => __('권한은 대표운영자에게 문의하시기 바랍니다.')]);
 
return;
}
 
$updated = (int)\App::load('\\Component\\Godo\\GodoPostServerApi')->saveManualUpdate();
 
// 반품(02) 재동기화 — 연동 신청을 마친 상점에만 조회할 이력이 생김.
// 실패해도 이미 커밋된 발송(01) 갱신의 감사기록·응답은 이어가야 해 여기서 막음(원인은 resync() 자신이 로깅함)
$epostApprovalNo = (string)(gd_policy(EpostReturnAdmin::POLICY_KEY)['compdivcd'] ?? '');
if ($epostApprovalNo !== '') {
try {
\App::getInstance(\Origin\Service\Order\EpostReturnResyncService::class)
->resync((int)\Globals::get('gLicense.godosno'), $epostApprovalNo);
} catch (\Throwable $e) {
}
}
 
$this->writeManualUpdateAccessLog($updated, $epostApprovalNo !== '');
 
$this->json(['code' => 200, 'updatedCount' => $updated]);
 
return;
// 수거신청 자동 접수 저장 — 검사 순서: 쓰기 권한 → 미신청 → 값 검증 → 저장
case 'saveAutoAccept':
$this->callMenu('order', 'epostParcel', 'request');
$naviMenu = $this->getData('naviMenu');
$writable = $naviMenu->getAdminMenuWritableAuth($naviMenu->lno, $naviMenu->location, $naviMenu->accessMenu, $naviMenu->writeEnabledMenu);
if (!$writable['check']) {
$this->json(['code' => 0, 'reason' => 'NO_PERMISSION', 'message' => __('우체국택배 신청/관리의 쓰기 권한이 없습니다.'), 'subMessage' => __('권한은 대표운영자에게 문의하시기 바랍니다.')]);
 
return;
}
$epostReturnAdmin = \App::getInstance(EpostReturnAdmin::class);
if (empty(gd_policy(EpostReturnAdmin::POLICY_KEY)['compdivcd'])) {
$this->json(['code' => 0, 'reason' => 'NOT_CONNECTED', 'message' => __('우체국택배 서비스 신청 후 이용 가능합니다.'), 'subMessage' => '']);
 
return;
}
$autoAcceptFl = Request::post()->get('autoAcceptFl');
if (!in_array($autoAcceptFl, [EpostReturnAdmin::AUTO_ACCEPT_ENABLED, EpostReturnAdmin::AUTO_ACCEPT_DISABLED], true)) {
$this->json(['code' => 0, 'reason' => 'INVALID_PARAM', 'message' => __('잘못된 요청입니다.'), 'subMessage' => ''], 400);
}
$enabled = $autoAcceptFl === EpostReturnAdmin::AUTO_ACCEPT_ENABLED;
try {
$epostReturnAdmin->setAutoAccept($enabled);
} catch (\Throwable $e) {
\App::getInstance('logger')->channel('epost')->error('우체국택배 자동접수 설정 저장 실패', ['exception' => $e]);
$this->json(['code' => 0, 'reason' => 'ERROR', 'message' => __('저장에 실패하였습니다.'), 'subMessage' => __('잠시 후 다시 시도해 주세요.')]);
 
return;
}
$this->json(['code' => 200]);
 
return;
}
} catch (\Exception $e) {
throw new LayerException($e->getMessage());
}
 
exit();
}
 
/**
* 배송상태 수동 업데이트 접속기록 — 실행당 1건, 처리 대상은 data 에 담음
*
* @param int $updatedCount 발송(01) 갱신 건수
* @param bool $returnResync 반품(02) 재동기화 실행 여부
*/
protected function writeManualUpdateAccessLog(int $updatedCount, bool $returnResync): void
{
$session = \App::getInstance('session');
 
\App::getInstance(EpostReturnAccessLogService::class)->write(
__(self::LOG_MENU),
__(self::LOG_PAGE),
__(self::LOG_ACTION),
(string)$session->get('manager.sno'),
(string)$session->get('manager.managerId'),
(string)$session->get('manager.scmNo'),
preg_replace('#^/{2,}#', '/', (Request::getRequestUri() ?? '')),
(string)Request::getReferer(),
(string)Request::getRemoteAddress(),
['updatedCount' => $updatedCount, 'returnResync' => $returnResync ? 'y' : 'n']
);
}
}
 
Controller/Admin/Order/PostRequestController.php+14-6
변경 전PostRequestController.php
*/
namespace Bundle\Controller\Admin\Order;
use Globals;
use Request;
class PostRequestController extends \Controller\Admin\Controller
{
{
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'request');
// --- 관리자 디자인 템플릿
$gLicense = Globals::get('gLicense');
$this->setData('shopSno', $gLicense['godosno']);
$this->setData('domainUrl', Request::getHostNoPort());
 
 
 
 
 
 
 
 
 
 
}
}
 
변경 후PostRequestController.php
*/
namespace Bundle\Controller\Admin\Order;
 
 
class PostRequestController extends \Controller\Admin\Controller
{
{
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'request');
$this->setMenuCode('order', 'epostParcel', 'request');
$this->setNcdsSubBodyClass();
// 수거신청 자동 접수 — 직전 저장값·연동 여부·쓰기 권한 (배너 4종·신청 상태 표기는 상태 전달 API 확정 후)
$policyConfig = gd_policy(\Component\Order\EpostReturnAdmin::POLICY_KEY);
$naviMenu = $this->getData('naviMenu');
$writable = $naviMenu->getAdminMenuWritableAuth($naviMenu->lno, $naviMenu->location, $naviMenu->accessMenu, $naviMenu->writeEnabledMenu);
$this->setData('epostConnected', !empty($policyConfig['compdivcd']));
$this->setData('autoAcceptSavedEnabled', ($policyConfig['autoAcceptFl'] ?? '') === \Component\Order\EpostReturnAdmin::AUTO_ACCEPT_ENABLED);
$this->setData('hasWriteAuth', (bool)$writable['check']);
// 우체국택배 서비스 신청 진행 상태 배너 — 표시 재료 가공·실패 처리 모두 컴포넌트 몫, 뷰는 출력만 함
$epostReturnAdmin = \App::getInstance(\Component\Order\EpostReturnAdmin::class);
$this->setData('usageBanner', $epostReturnAdmin->getUsageBanner());
}
}
 
Controller/Admin/Order/PostReserveController.php+5-4
변경 전PostReserveController.php
*/
namespace Bundle\Controller\Admin\Order;
use Framework\Debug\Exception\AlertBackException;
use Exception;
 
use Globals;
use Request;
try {
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'reserve');
 
$godopost= gd_policy('order.godopost');
if(empty($godopost['compdivcd']) === true) {
throw new AlertBackException(__("우체국택배 서비스 신청 후 이용 가능합니다."));
}
// --- 모듈 호출
 
변경 후PostReserveController.php
*/
namespace Bundle\Controller\Admin\Order;
use Component\Order\EpostReturnAdmin;
use Exception;
use Framework\Debug\Exception\AlertBackException;
use Globals;
use Request;
try {
// --- 메뉴 설정
$this->callMenu('order', 'epostParcel', 'reserve');
$this->setMenuCode('order', 'epostParcel', 'reserve');
if (\App::getInstance(EpostReturnAdmin::class)->isUsageNotRequested()) {
throw new AlertBackException(__('우체국택배 서비스 신청 후 이용 가능합니다.'));
 
}
// --- 모듈 호출
 
Controller/Front/Mypage/CancelListController.php+4
변경 전CancelListController.php
use Exception;
use Request;
use Component\Validator\Validator;
 
 
/**
* 마이페이지 > 주문배송/조회
}
}
 
 
 
 
$this->setData('mode', $mode);
$this->setData('orderData', gd_isset($orderData));
 
 
변경 후CancelListController.php
use Exception;
use Request;
use Component\Validator\Validator;
use Component\Order\ReturnInvoice;
 
/**
* 마이페이지 > 주문배송/조회
}
}
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('mode', $mode);
$this->setData('orderData', gd_isset($orderData));
 
 
Controller/Front/Mypage/IndexController.php+4
변경 전IndexController.php
use Request;
use Session;
use Component\Cart\ReorderCart;
 
 
/**
* Class MypageQnaController
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
 
 
 
$this->setData('orderData', gd_isset($orderData));
// 사용자 반품/교환/환불 신청 사용여부
$orderBasic = gd_policy('order.basic');
 
변경 후IndexController.php
use Request;
use Session;
use Component\Cart\ReorderCart;
use Component\Order\ReturnInvoice;
 
/**
* Class MypageQnaController
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('orderData', gd_isset($orderData));
// 사용자 반품/교환/환불 신청 사용여부
$orderBasic = gd_policy('order.basic');
 
Controller/Front/Mypage/OrderListController.php+4
변경 전OrderListController.php
use Exception;
use Request;
use Component\Cart\ReorderCart;
 
 
/**
* 마이페이지 > 주문배송/조회
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
 
 
 
$this->setData('orderData', gd_isset($orderData));
 
// 주문셀 합치는 조건
 
변경 후OrderListController.php
use Exception;
use Request;
use Component\Cart\ReorderCart;
use Component\Order\ReturnInvoice;
 
/**
* 마이페이지 > 주문배송/조회
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('orderData', gd_isset($orderData));
 
// 주문셀 합치는 조건
 
Controller/Front/Mypage/OrderViewController.php+4
변경 전OrderViewController.php
use Request;
use App;
use Component\Cart\ReorderCart;
 
/**
* 주문 상세 보기 페이지
*
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
 
 
 
$this->setData('orderData', $orderData);
$orderData = $orderData[0];
// 회원할인에 회원 배송비 할인 추가
 
변경 후OrderViewController.php
use Request;
use App;
use Component\Cart\ReorderCart;
use Component\Order\ReturnInvoice;
/**
* 주문 상세 보기 페이지
*
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('orderData', $orderData);
$orderData = $orderData[0];
// 회원할인에 회원 배송비 할인 추가
 
Controller/Front/Mypage/RefundListController.php+4
변경 전RefundListController.php
use Request;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
 
 
/**
* 마이페이지 > 주문배송/조회
$mode = 'refund';
$orderData = $order->getOrderList(10, $wDate, $mode);
}
 
 
 
$this->setData('mode', $mode);
$this->setData('orderData', gd_isset($orderData));
 
 
변경 후RefundListController.php
use Request;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Component\Order\ReturnInvoice;
 
/**
* 마이페이지 > 주문배송/조회
$mode = 'refund';
$orderData = $order->getOrderList(10, $wDate, $mode);
}
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('mode', $mode);
$this->setData('orderData', gd_isset($orderData));
 
 
Controller/Mobile/Mypage/OrderListController.php+4
변경 전OrderListController.php
use Framework\Utility\StringUtils;
use Request;
use Globals;
 
 
/**
* 마이페이지 > 주문배송/조회
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
 
 
 
// 주문 리스트 정보
$this->setData('orderData', gd_isset($orderData));
$this->setData('pageName', 'list');
 
변경 후OrderListController.php
use Framework\Utility\StringUtils;
use Request;
use Globals;
use Component\Order\ReturnInvoice;
 
/**
* 마이페이지 > 주문배송/조회
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
// 주문 리스트 정보
$this->setData('orderData', gd_isset($orderData));
$this->setData('pageName', 'list');
 
Controller/Mobile/Mypage/OrderViewController.php+4
변경 전OrderViewController.php
use App;
use Session;
use Component\Cart\ReorderCart;
 
 
/**
* 주문 상세 보기 페이지
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
 
 
 
$this->setData('orderData', $orderData);
 
$ordersByRegisterDay = [];
 
변경 후OrderViewController.php
use App;
use Session;
use Component\Cart\ReorderCart;
use Component\Order\ReturnInvoice;
 
/**
* 주문 상세 보기 페이지
// 배송 중, 배송 완료된 상품 카운트해서 버튼 생성 여부
$orderData = $order->getOrderSettleButton($orderData);
 
// 우체국 반품 수거 반송추적 — 보충 조회 1회로 행에 서버 조립 URL 주입 (송장 원문 미노출)
$orderData = \App::getInstance(ReturnInvoice::class)->assignTraceUrls($orderData);
 
$this->setData('orderData', $orderData);
 
$ordersByRegisterDay = [];
 
DTO/Order/EpostReturnSearchDTO.php+253
변경 전EpostReturnSearchDTO.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후EpostReturnSearchDTO.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\DTO\Order;
 
use Origin\DTO\AbstractDTO;
 
/**
* 우체국택배 수거신청 대상 검색 조건
*/
class EpostReturnSearchDTO extends AbstractDTO
{
/**
* 기존 주문 목록 검색어 항목을 승계함
*/
public const SEARCH_FIELDS = [
'o.orderNo' => '주문번호',
'og.invoiceNo' => '송장번호',
'og.goodsNm' => '상품명',
'og.goodsNo' => '상품코드',
'og.goodsCd' => '자체 상품코드',
'og.goodsModelNo' => '상품모델명',
'og.makerNm' => '제조사',
'oi.orderName' => '주문자명',
'oi.orderPhone' => '주문자 전화번호',
'oi.orderCellPhone' => '주문자 휴대폰번호',
'oi.orderEmail' => '주문자 이메일',
'oi.receiverName' => '수령자명',
'oi.receiverPhone' => '수령자 전화번호',
'oi.receiverCellPhone' => '수령자 휴대폰번호',
'o.bankSender' => '입금자명',
'm.memId' => '아이디',
'm.nickNm' => '닉네임',
'sm.companyNm' => '공급사명',
];
 
/**
* 기존 처리일자 검색 토큰을 승계함 — 키가 토큰(.b/.e/.r 접미는 클레임 모드)이고 값은 화면 표기 라벨
*/
public const DATE_FIELDS = [
'o.regDt' => '주문일',
'og.paymentDt' => '결제확인일',
'og.invoiceDt' => '송장입력일',
'og.deliveryDt' => '배송일',
'og.deliveryCompleteDt' => '배송완료일',
'og.finishDt' => '구매확정일',
'oh.regDt.b' => '반품접수일',
];
 
/**
* 기존 정렬 컬럼을 승계함 — 키가 정렬 컬럼이고 값은 화면 표기 라벨
*/
public const SORT_FIELDS = [
'o.regDt' => '주문일',
'oh.regDt' => '반품접수일',
'og.orderNo' => '주문번호',
'og.goodsNm' => '상품명',
'oi.orderName' => '주문자',
'o.settlePrice' => '총 결제금액',
'oi.receiverName' => '수령자',
'sm.companyNm' => '공급사',
];
 
/**
* 검색어 매칭 방식 — 키가 matchType 값이고 값은 화면 표기 라벨
*/
public const MATCH_TYPES = [
'equalSearch' => '검색어 전체일치',
'fullLikeSearch' => '검색어 부분포함',
];
 
/**
* 페이지 크기 선택지 — 상한은 PAGE_SIZE_MAX 와 같음
*/
public const PAGE_SIZES = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 500];
 
/**
* 주문상태 필터 — 키가 collectionStatuses 값이고 값은 화면 표기 라벨
*/
public const COLLECTION_STATUSES = [
'pending' => '반품접수',
'failed' => '반품접수(수거신청 실패)',
];
 
private const PAGE_MIN = 1;
private const PAGE_SIZE_MIN = 1;
private const PAGE_SIZE_MAX = 500;
 
protected string $searchKey;
 
protected string $keyword;
 
protected string $matchType;
 
protected string $scmFl;
 
protected array $scmNos;
 
protected string $treatDateFl;
 
protected string $treatDateStart;
 
protected string $treatDateEnd;
 
protected array $collectionStatuses;
 
protected string $sort;
 
protected int $page;
 
protected int $pageSize;
 
/**
* @param array $getValue 화면 검색 파라미터
*/
public function __construct(array $getValue = [])
{
$this->searchKey = (string) ($getValue['searchKey'] ?? '');
$this->keyword = trim((string) ($getValue['keyword'] ?? ''));
$this->matchType = (string) ($getValue['matchType'] ?? 'equalSearch');
$this->scmFl = (string) ($getValue['scmFl'] ?? 'all');
$this->scmNos = array_map('intval', (array) ($getValue['scmNos'] ?? []));
$this->treatDateFl = (string) ($getValue['treatDateFl'] ?? 'oh.regDt.b');
$this->treatDateStart = (string) ($getValue['treatDateStart'] ?? '');
$this->treatDateEnd = (string) ($getValue['treatDateEnd'] ?? '');
$this->collectionStatuses = (array) ($getValue['collectionStatuses'] ?? []);
$this->sort = (string) ($getValue['sort'] ?? 'o.regDt desc');
// GET 파라미터는 문자열이고 0·음수 페이지는 OFFSET 오류를 만들므로 여기서 잘라냄
$this->page = max(self::PAGE_MIN, (int) ($getValue['page'] ?? self::PAGE_MIN));
$this->pageSize = min(self::PAGE_SIZE_MAX, max(self::PAGE_SIZE_MIN, (int) ($getValue['pageSize'] ?? 20)));
}
 
public function getSearchColumn(): ?string
{
if ($this->keyword === '') {
return null;
}
 
return array_key_exists($this->searchKey, self::SEARCH_FIELDS) ? $this->searchKey : null;
}
 
public function getKeyword(): string
{
return $this->keyword;
}
 
public function isLikeMatch(): bool
{
return $this->matchType === 'fullLikeSearch';
}
 
/**
* @return ?bool true 본사만 · false 본사 제외 · null 무조건
*/
public function getHeadquartersOnly(): ?bool
{
if ($this->scmFl === '0') {
return true;
}
 
if ($this->scmFl === '1' && empty($this->scmNos)) {
return false;
}
 
return null;
}
 
/**
* @return array 공급사 번호 목록 (scmFl 이 1 이고 선택이 있을 때만)
*/
public function getScmNos(): array
{
return $this->scmFl === '1' ? $this->scmNos : [];
}
 
public function getDateColumn(): ?string
{
if ($this->treatDateStart === '' || $this->treatDateEnd === ''
|| !array_key_exists($this->treatDateFl, self::DATE_FIELDS)) {
return null;
}
 
$suffix = substr($this->treatDateFl, -2);
 
return in_array($suffix, ['.b', '.e', '.r'], true)
? substr($this->treatDateFl, 0, -2)
: $this->treatDateFl;
}
 
public function getHandleMode(): ?string
{
if ($this->getDateColumn() === null) {
return null;
}
 
$suffix = substr($this->treatDateFl, -2);
 
return in_array($suffix, ['.b', '.e', '.r'], true) ? substr($suffix, -1) : null;
}
 
public function getDateStart(): string
{
return $this->treatDateStart;
}
 
public function getDateEnd(): string
{
return $this->treatDateEnd;
}
 
public function isPendingOnly(): bool
{
return in_array('pending', $this->collectionStatuses, true)
&& !in_array('failed', $this->collectionStatuses, true);
}
 
public function isFailedOnly(): bool
{
return in_array('failed', $this->collectionStatuses, true)
&& !in_array('pending', $this->collectionStatuses, true);
}
 
/**
* @return array [정렬 컬럼, 방향]
*/
public function getSort(): array
{
$parts = preg_split('/\\s+/', trim($this->sort));
$column = $parts[0] ?? '';
$direction = strtolower($parts[1] ?? 'desc');
 
if (!array_key_exists($column, self::SORT_FIELDS) || !in_array($direction, ['asc', 'desc'], true)) {
return ['o.regDt', 'desc'];
}
 
return [$column, $direction];
}
 
public function getPage(): int
{
return $this->page;
}
 
public function getPageSize(): int
{
return $this->pageSize;
}
}
 
Repository/Order/OrderGiftRepository.php+20-2
변경 전OrderGiftRepository.php
 
class OrderGiftRepository
{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 주문 사은품 정보 조회
*
* SELECT es_gift.giftNm, es_orderGift.giveCnt
* FROM es_orderGift
* JOIN es_gift ON es_orderGift.giftNo = es_gift.giftNo
* WHERE es_orderGift.orderNo = ?
* GROUP BY es_orderGift.orderNo
* LIMIT 1
*
* @param string $orderNo
* @return array
*/
 
변경 후OrderGiftRepository.php
 
class OrderGiftRepository
{
/**
* 주문 목록의 사은품 행 조회 — 지급조건명·사은품명·수량 (그리드 사은품 컬럼용)
*
* @param array $orderNos 주문번호 목록
* @return array 행 목록 [orderNo, presentTitle, giftNm, giveCnt]
*/
public function findOrderGiftLinesByOrderNos(array $orderNos): array
{
return OrderGift::query()
->select('es_orderGift.orderNo', 'gp.presentTitle', 'g.giftNm', 'es_orderGift.giveCnt')
->leftJoin(DB_GIFT_PRESENT . ' AS gp', 'es_orderGift.presentSno', '=', 'gp.sno')
->join(DB_GIFT . ' AS g', 'es_orderGift.giftNo', '=', 'g.giftNo')
->whereIn('es_orderGift.orderNo', array_map('strval', $orderNos))
->orderBy('es_orderGift.orderNo')
->get()
->toArray();
}
 
/**
* 주문 사은품 정보 조회
*
* SELECT es_gift.giftNm, es_orderGift.giveCnt
* FROM es_orderGift
* JOIN es_gift ON es_orderGift.giftNo = es_gift.giftNo
* WHERE es_orderGift.orderNo = ?
* GROUP BY es_orderGift.orderNo
* LIMIT 1
*
* @param string $orderNo
* @return array
*/
 
Repository/Order/OrderGoodsRepository.php+180
변경 전OrderGoodsRepository.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후OrderGoodsRepository.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 DTO\Order\EpostReturnSearchDTO;
use Illuminate\Database\Eloquent\Builder;
use Origin\Enum\Order\EpostReturnInvoiceStatus;
use Origin\Enum\Order\OrderChannel;
use Origin\Enum\Order\OrderStatusCode;
use Origin\Model\Order\OrderGoods;
 
/**
* 주문상품(es_orderGoods) 조회
*/
class OrderGoodsRepository
{
/**
* @return array 행 목록 [sno, orderNo, regDt, orderName, goodsNm, goodsCnt, orderStatus,
* returnInvoiceStatus, returnInvoiceNo, orderDeliverySno, deliverySno, invoiceNo, companyNm, receiverName, scmNo, handleSno]
*/
public function findTargets(EpostReturnSearchDTO $search): array
{
[$sortColumn, $sortDirection] = $search->getSort();
 
return $this->buildTargetQuery($search)
->groupBy('og.sno')
->orderBy($sortColumn, $sortDirection)
->orderBy('og.sno', 'desc')
->forPage($search->getPage(), $search->getPageSize())
->get([
'og.sno', 'og.orderNo', 'o.regDt', 'oi.orderName', 'og.goodsNm', 'og.goodsCnt',
'og.orderStatus', 'rt.returnInvoiceStatus', 'rt.returnInvoiceNo', 'og.orderDeliverySno', 'sdb.sno AS deliverySno',
'og.invoiceNo', 'sm.companyNm', 'oi.receiverName', 'og.scmNo', 'og.handleSno',
'og.goodsNo', 'og.optionInfo', 'og.optionTextInfo', 'o.firstSaleFl', 'o.memNo', 'm.memId',
'g.imagePath', 'g.imageStorage', 'gi.imageName', 'gi.imageUrl', 'gi.goodsImageStorage',
])
->toArray();
}
 
/**
* 요청 상품주문번호 중 대상 술어를 통과하는 것만 돌려줌 — 목록과 같은 술어를 지남
*
* @param array $orderGoodsSnos 상품주문번호 목록
* @return array 통과한 상품주문번호 목록
*/
public function findTargetSnos(array $orderGoodsSnos): array
{
return $this->buildTargetQuery(null)
->whereIn('og.sno', array_map('intval', $orderGoodsSnos))
->distinct()
->pluck('og.sno')
->all();
}
 
/**
* 신청 sno(og.userHandleSno) 기준으로 대상 조건을 통과하는 상품주문번호를 돌려줌 — 목록과 같은 대상 조건을 지남
*
* @param array $userHandleSnos 고객 신청 sno 목록
* @return array 통과한 상품주문번호 목록
*/
public function findTargetSnosByUserHandleSnos(array $userHandleSnos): array
{
return $this->buildTargetQuery(null)
->whereIn('og.userHandleSno', $userHandleSnos)
->distinct()
->pluck('og.sno')
->all();
}
 
public function countTargets(EpostReturnSearchDTO $search): int
{
// oi.orderInfoCd=1 행이 한 주문에 여럿일 수 있어 주문상품 기준으로 센다
return $this->buildTargetQuery($search)->distinct()->count('og.sno');
}
 
/**
* 수거신청 대상 술어 — 목록·건수·게이트 재검증이 전부 이 한 지점을 지남
*
* @param ?EpostReturnSearchDTO $search 화면 검색 조건 — null 이면 술어만 적용함
*/
protected function buildTargetQuery(?EpostReturnSearchDTO $search): Builder
{
$query = OrderGoods::query()
->from(DB_ORDER_GOODS . ' AS og')
->join(DB_ORDER . ' AS o', 'o.orderNo', '=', 'og.orderNo')
->join(DB_ORDER_INFO . ' AS oi', function ($join) {
$join->on('oi.orderNo', '=', 'og.orderNo')
->where('oi.orderInfoCd', 1);
})
->join(DB_ORDER_HANDLE . ' AS oh', function ($join) {
$join->on('oh.sno', '=', 'og.handleSno')
->on('oh.orderNo', '=', 'og.orderNo');
})
->leftJoin(DB_ORDER_RETURN_INVOICE . ' AS rt', 'rt.orderGoodsNo', '=', 'og.sno')
->leftJoin(DB_ORDER_DELIVERY . ' AS od', 'od.sno', '=', 'og.orderDeliverySno')
// 배송번호 = 상품별 배송비 코드(배송비조건 sno) — 삭제된 코드는 공란 처리라 존재 확인 조인을 거침
->leftJoin(DB_SCM_DELIVERY_BASIC . ' AS sdb', 'sdb.sno', '=', 'od.deliverySno')
->leftJoin(DB_SCM_MANAGE . ' AS sm', 'sm.scmNo', '=', 'og.scmNo')
->leftJoin(DB_MEMBER . ' AS m', 'm.memNo', '=', 'o.memNo')
->leftJoin(DB_GOODS . ' AS g', 'g.goodsNo', '=', 'og.goodsNo')
->leftJoin(DB_GOODS_IMAGE . ' AS gi', function ($join) {
$join->on('gi.goodsNo', '=', 'og.goodsNo')
->where('gi.imageKind', 'list');
})
->where('og.orderStatus', OrderStatusCode::BACK_REQUEST->value)
->where('og.invoiceCompanySno', DEFAULT_CODE_GODOPOST)
->whereRaw("TRIM(og.invoiceNo) != ''")
->where('o.orderChannelFl', '!=', OrderChannel::NAVERPAY->value)
->where(function ($where) {
$where->whereNull('rt.returnInvoiceStatus')
->orWhere('rt.returnInvoiceStatus', EpostReturnInvoiceStatus::FAILED->value);
});
 
if ($search !== null) {
$this->applyStatusFilter($query, $search);
$this->applyScmFilter($query, $search);
$this->applyDateFilter($query, $search);
$this->applyKeywordFilter($query, $search);
}
 
return $query;
}
 
protected function applyStatusFilter(Builder $query, EpostReturnSearchDTO $search): void
{
if ($search->isPendingOnly()) {
$query->whereNull('rt.returnInvoiceStatus');
} elseif ($search->isFailedOnly()) {
$query->where('rt.returnInvoiceStatus', EpostReturnInvoiceStatus::FAILED->value);
}
}
 
protected function applyScmFilter(Builder $query, EpostReturnSearchDTO $search): void
{
$headquartersOnly = $search->getHeadquartersOnly();
 
if ($headquartersOnly === true) {
$query->where('og.scmNo', DEFAULT_CODE_SCMNO);
} elseif ($headquartersOnly === false) {
$query->where('og.scmNo', '!=', DEFAULT_CODE_SCMNO);
} elseif (!empty($search->getScmNos())) {
$query->whereIn('og.scmNo', $search->getScmNos());
}
}
 
protected function applyDateFilter(Builder $query, EpostReturnSearchDTO $search): void
{
$column = $search->getDateColumn();
if ($column === null) {
return;
}
 
if ($search->getHandleMode() !== null) {
$query->where('oh.handleMode', $search->getHandleMode());
}
 
// 종료일이 YYYY-MM-DD만 오면 00:00:00으로 해석되어 종료일 당일 데이터가 전부 빠지므로 하루 끝까지 포함시킴
$query->whereBetween($column, [$search->getDateStart(), $search->getDateEnd() . ' 23:59:59']);
}
 
protected function applyKeywordFilter(Builder $query, EpostReturnSearchDTO $search): void
{
$column = $search->getSearchColumn();
if ($column === null) {
return;
}
 
if ($search->isLikeMatch()) {
$query->where($column, 'LIKE', '%' . addcslashes($search->getKeyword(), '%_\\') . '%');
} else {
$query->where($column, $search->getKeyword());
}
}
}
 
data/skin/front/moment/mypage/_order_goods_list.html+19
변경 전_order_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
변경 후_order_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <a href="{..returnTraceUrl}" class="btn_gray_small js_btn_return_trace"><span>{=__('반송추적')}</span></a>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.js_btn_return_trace').click(function (e) {
e.preventDefault();
win = gd_popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
data/skin/front/moment/mypage/_reorder_goods_list.html+19
변경 전_reorder_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
변경 후_reorder_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <a href="{..returnTraceUrl}" class="btn_gray_small js_btn_return_trace"><span>{=__('반송추적')}</span></a>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.js_btn_return_trace').click(function (e) {
e.preventDefault();
win = gd_popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
data/skin/front/mygreek/mypage/_order_goods_list.html+19
변경 전_order_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
변경 후_order_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <a href="{..returnTraceUrl}" class="btn_gray_small js_btn_return_trace"><span>{=__('반송추적')}</span></a>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.js_btn_return_trace').click(function (e) {
e.preventDefault();
win = gd_popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
data/skin/front/mygreek/mypage/_reorder_goods_list.html+19
변경 전_reorder_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
변경 후_reorder_goods_list.html
</div>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <a href="{..returnTraceUrl}" class="btn_gray_small js_btn_return_trace"><span>{=__('반송추적')}</span></a>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<a href="#" class="btn_gray_small js_btn_order_delivery"><span>{=__('수취확인')}</span></a>
<!--{ / }-->
}
});
 
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.js_btn_return_trace').click(function (e) {
e.preventDefault();
win = gd_popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
 
// 배송추적
$('.js_btn_delivery_trace').click(function (e) {
win = gd_popup({
 
data/skin/front/story_g/mypage/_order_goods_list.html+19
변경 전_order_goods_list.html
</span>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<div class="button"><a href="#" class="normal-btn small1 btn-order-delivery"><em>{=__('수취확인')}</em></a></div>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.btn-delivery-trace').click(function (e) {
win = popup({
 
변경 후_order_goods_list.html
</span>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <span class="button"><a href="{..returnTraceUrl}" class="normal-btn small1 btn-return-trace"><em>{=__('반송추적')}</em></a></span>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<div class="button"><a href="#" class="normal-btn small1 btn-order-delivery"><em>{=__('수취확인')}</em></a></div>
<!--{ / }-->
}
});
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.btn-return-trace').click(function (e) {
e.preventDefault();
win = popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
// 배송추적
$('.btn-delivery-trace').click(function (e) {
win = popup({
 
data/skin/front/story_g/mypage/_reorder_goods_list.html+19
변경 전_reorder_goods_list.html
</span>
<!--{ / }-->
<!--{ : }-->
 
 
 
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
 
<!--{ ? ..orderStatus == 'd1' }-->
<div class="button"><a href="#" class="normal-btn small1 btn-order-delivery"><em>{=__('수취확인')}</em></a></div>
<!--{ / }-->
}
});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// 배송추적
$('.btn-delivery-trace').click(function (e) {
win = popup({
 
변경 후_reorder_goods_list.html
</span>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ..returnTraceUrl }-->
({=__(..deliveryMethodFlText)}) - <span class="button"><a href="{..returnTraceUrl}" class="normal-btn small1 btn-return-trace"><em>{=__('반송추적')}</em></a></span>
<!--{ : }-->
<div>({=__(..deliveryMethodFlText)})</div>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ..orderStatus == 'd1' }-->
<div class="button"><a href="#" class="normal-btn small1 btn-order-delivery"><em>{=__('수취확인')}</em></a></div>
<!--{ / }-->
}
});
 
// 반송추적 — 서버 조립 URL 팝업 (송장 원문 미노출)
$('.btn-return-trace').click(function (e) {
e.preventDefault();
win = popup({
url: $(this).attr('href'),
target: 'returnTrace',
width: 650,
height: 660,
resizable: 'yes',
scrollbars: 'yes'
});
win.focus();
return win;
});
 
// 배송추적
$('.btn-delivery-trace').click(function (e) {
win = popup({
 
data/skin/mobile/moment/mypage/_order_goods_list.html+4
변경 전_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="delivery_trace_btn" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
data/skin/mobile/moment/mypage/_reorder_goods_list.html+4
변경 전_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="delivery_trace_btn" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
data/skin/mobile/mygreek/mypage/_order_goods_list.html+4
변경 전_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="delivery_trace_btn" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
data/skin/mobile/mygreek/mypage/_reorder_goods_list.html+4
변경 전_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="delivery_trace_btn" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="delivery_trace_btn" style="max-width:100px; overflow:hidden;">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="delivery_order_btn btn_order_delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
data/skin/mobile/story_g/mypage/_order_goods_list.html+4
변경 전_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="btn btn_w">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="btn btn_y btn-order-delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_order_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="btn btn_w" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="btn btn_w">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="btn btn_y btn-order-delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
data/skin/mobile/story_g/mypage/_reorder_goods_list.html+4
변경 전_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
 
 
 
<li>
<a class="btn btn_w">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
 
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="btn btn_y btn-order-delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
변경 후_reorder_goods_list.html
</li>
<!--{ / }-->
<!--{ : }-->
<!--{ ? ...returnTraceUrl }-->
<li><a href="{...returnTraceUrl}" class="btn btn_w" target="_blank">{=__('반송추적')}</a></li>
<!--{ : }-->
<li>
<a class="btn btn_w">{...deliveryMethodFlText}</a>
</li>
<!--{ / }-->
<!--{ / }-->
<!--{ ? ...orderStatus == 'd1' }-->
<li><a href="#" class="btn btn_y btn-order-delivery" order-no="{..orderNo}" order-goodsno="{...sno}">{=__('수취확인')}</a></li>
<!--{ / }-->
 
WebFTP 보안 강화
Controller/Admin/Share/PopupWebftpPsController.php+59
변경 전PopupWebftpPsController.php
]
);
}
 
 
 
 
 
 
 
 
 
 
 
FileHandler::delete(iconv('UTF-8', 'CP949', $file), true);
$this->json(
[
if (!Request::request()->has('target')) {
throw new \InvalidArgumentException(__('변경할 이름을 입력해주세요.'));
}
 
 
 
 
 
 
 
 
 
if (getenv('setting') == 'y') {
FileHandler::rename(iconv('UTF-8', 'CP949', $file), iconv('UTF-8', 'CP949', Request::request()->get('target')));
} else {
exit;
}
 
 
 
 
 
 
// 한글 파일명 업로드를 위해 인코딩 변경 처리
move_uploaded_file($tmpName[0], iconv('UTF-8', 'CP949', $file . '/' . $name[0]));
break;
}
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후PopupWebftpPsController.php
]
);
}
 
// 목록에 없는 경로를 직접 보내 상점 폴더 밖을 지우는 것을 막는다 — data/../../route.php
if ($this->isOutsideDataPath($file)) {
$this->json(
[
'error' => 1,
'message' => __('삭제할 파일이 없습니다.'),
]
);
}
 
FileHandler::delete(iconv('UTF-8', 'CP949', $file), true);
$this->json(
[
if (!Request::request()->has('target')) {
throw new \InvalidArgumentException(__('변경할 이름을 입력해주세요.'));
}
 
// 세팅 모드 target 은 앞단에서 USERPATH 로 치환돼 들어오므로 아래 호출부와 같은 방식으로 조립한다
$renameTarget = getenv('setting') == 'y' ? Request::request()->get('target') : USERPATH . Request::request()->get('target');
 
// 원본은 밖의 파일을 건드리는 것을(route.php → x.js), 대상은 밖으로 빼내는 것을(a.png → ../../evil.js) 막는다
if ($this->isOutsideDataPath($file) || $this->isOutsideDataPath($renameTarget)) {
throw new \InvalidArgumentException(__('잘못된 경로로 접근하셨습니다.'));
}
 
if (getenv('setting') == 'y') {
FileHandler::rename(iconv('UTF-8', 'CP949', $file), iconv('UTF-8', 'CP949', Request::request()->get('target')));
} else {
exit;
}
 
// 저장 위치와 파일명이 합쳐진 최종 경로로 판정한다 — 둘 중 어느 쪽에 상위 이동이 섞여도 걸린다
if ($this->isOutsideDataPath($file . '/' . $name[0])) {
throw new \InvalidArgumentException(__('잘못된 경로로 접근하셨습니다.'));
}
 
// 한글 파일명 업로드를 위해 인코딩 변경 처리
move_uploaded_file($tmpName[0], iconv('UTF-8', 'CP949', $file . '/' . $name[0]));
break;
}
}
}
 
/**
* 작업 대상이 상점 data 폴더 밖을 가리키는지 확인한다.
*
* @param string $path 파일 처리 함수에 넘어갈 경로
*
* @return bool data 폴더 밖이면 true
*/
protected function isOutsideDataPath(string $path): bool
{
// 세팅 모드 파일 경로는 앞에 슬래시가 하나 더 붙어 들어온다 — //shop/user/data/a.png
$targetPath = preg_replace('#/+#', '/', $path);
 
// 세팅 모드는 화면이 넘긴 현재 경로가 상대형으로 온다 — data/logo.png
if (strpos($targetPath, USERPATH) !== 0) {
$targetPath = USERPATH . ltrim($targetPath, '/');
}
 
// 상위 이동을 해석해 실제로 가리키는 경로를 구한다 — data/skin/../../route.php 는 상점 폴더 밖
$resolved = [];
foreach (explode('/', $targetPath) as $segment) {
if ($segment === '.') {
continue;
}
if ($segment === '..') {
array_pop($resolved);
continue;
}
$resolved[] = $segment;
}
$resolvedPath = implode('/', $resolved);
 
return strpos($resolvedPath, USERPATH . 'data/') !== 0;
}
}
 
선물하기 자동알림 LMS 발송 고정
Admin/crm/message_config.php+12-1
변경 전message_config.php
<div class="ncua-table ncua-table--vertical">
<table>
<tr>
<th><div data-tooltip-seq="018">자동알림 90byte 초과시 전송 방법</div>
</th>
<td>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
</details>
</section>
<?php endif; ?>
 
 
 
 
 
 
 
 
 
 
 
</section>
 
<!-- 카카오 친구톡 섹션 -->
 
변경 후message_config.php
<div class="ncua-table ncua-table--vertical">
<table>
<tr>
<th><div>자동알림 90byte 초과시 전송 방법</div>
</th>
<td>
<div class="ncua-radio-group ncua-flex ncua-flex-gap">
</details>
</section>
<?php endif; ?>
<section class="ncua-card__body accordion-section">
<details class="ncua-accordion ncua-accordion--gray" open>
<summary>자동알림 90byte 초과시 전송 방법 확인사항</summary>
<ul class="ncua-accordion__content">
<li class="ncua-notice-info">SMS는 최대 90byte까지만 발송 가능합니다.</li>
<li class="ncua-notice-info">내용 또는 변수 값에 따라 90byte를 넘을 경우, 선택한 전송 방법으로 발송됩니다.</li>
<li class="ncua-notice-info">해당 설정은 SMS 자동알림, 자동알림 대체발송(SMS 내용으로 발송)에만 적용됩니다.</li>
<li class="ncua-notice-info text-danger">선물하기 자동알림 발송 메시지 내용이 90byte를 초과할 경우, SMS 발송 설정과 무관하게 LMS로 자동 발송됩니다.</li>
</ul>
</details>
</section>
</section>
 
<!-- 카카오 친구톡 섹션 -->
 
Admin/goods/present_config/function_config.php+1
변경 전function_config.php
<h3 class="ncua-card__title ncua-flex ncua-align-items-center" data-tooltip-seq="001" data-tooltip-icon-type="fill">기능 설정</h3>
</header>
<section class="ncua-card__body js-track-change">
 
<div class="ncua-table ncua-table--vertical">
<table>
<tbody>
 
변경 후function_config.php
<h3 class="ncua-card__title ncua-flex ncua-align-items-center" data-tooltip-seq="001" data-tooltip-icon-type="fill">기능 설정</h3>
</header>
<section class="ncua-card__body js-track-change">
<div class="ncua-notice-info" style="margin-bottom:16px;">선물하기 자동알림 발송 메시지 내용이 90byte를 초과할 경우, SMS 발송 설정과 무관하게 LMS로 자동 발송됩니다.</div>
<div class="ncua-table ncua-table--vertical">
<table>
<tbody>
 
Component/Sms/SmsAuto.php+111-26
변경 전SmsAuto.php
$smsContentForRecipient['contents'], $originalReplaceArguments, $this->smsAutoCodeType, AutoSendChannel::SMS
);
}
$kakaoSmsOverFlag = SmsAutoSendOverType::LIMIT->value;
if ($this->findAutoSendConfigService->isOverSmsLength($replacedKakaoContents)) {
$kakaoSmsOverFlag = $this->findAutoSendConfigService->getSmsSendOverFlag($this->smsAutoConfig)->value;
}
} else {
$kakaoSmsOverFlag = SmsAutoSendOverType::LIMIT->value;
if ($this->findAutoSendConfigService->isOverSmsLength($replacedKakaoContents)) {
$kakaoSmsOverFlag = $this->findAutoSendConfigService->convertCrmToSendOverFlag($this->kakaoCrmConfig);;
}
}
} else {
$replacedKakaoContents = '';
$smsContentForRecipient['contents'], $originalReplaceArguments, $this->smsAutoCodeType, AutoSendChannel::SMS
);
}
$kakaoSmsOverFlag = SmsAutoSendOverType::LIMIT->value;
if ($this->findAutoSendConfigService->isOverSmsLength($replacedKakaoContents)) {
$kakaoSmsOverFlag = $this->findAutoSendConfigService->getSmsSendOverFlag($this->smsAutoConfig)->value;
}
} else {
$kakaoSmsOverFlag = SmsAutoSendOverType::LIMIT->value;
if ($this->findAutoSendConfigService->isOverSmsLength($replacedKakaoContents)) {
$kakaoSmsOverFlag = $this->findAutoSendConfigService->convertCrmToSendOverFlag($this->kakaoCrmConfig);;
}
}
} else {
$replacedKakaoContents = '';
$logData = $this->buildSendLogData($logData);
 
// 발송
$smsOverFlag = $this->findAutoSendConfigService->getSmsSendOverFlag($this->smsAutoConfig);
$message = new \Component\Sms\SmsMessage($contents);
if ($message->exceedSmsLength()) {
$message = match ($smsOverFlag) {
SmsAutoSendOverType::LMS => new \Component\Sms\LmsMessage($contents),
SmsAutoSendOverType::DIVISION => new \Component\Sms\DivisionSmsMessage($contents),
default => new \Component\Sms\SmsMessage($contents),
};
}
 
$transport = \App::load(SmsSender::class);
if ($this->isAutoSend() || !$this->passwordCheckFl) {
// LMS 제목 (설정된 자동발송 문구 제목 - LMS 발송 시에만 SmsSender 에서 사용, 미입력 시 [제목없음])
// 튜닝 상점 대비: 구버전 SmsSender(setSubject 미보유)에서 발송 중단 방지
if (method_exists($transport, 'setSubject')) {
$transport->setSubject($item['messageTitle'] ?? '');
}
$transport->setSmsType($item['smsType']);
$transport->setReceiver($receiverData);
return $transport->send();
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 카카오 비즈엠 버튼 데이터 생성
* 페이지링크에 송장번호 치환코드가 있는 경우 치환 처리
 
변경 후SmsAuto.php
$smsContentForRecipient['contents'], $originalReplaceArguments, $this->smsAutoCodeType, AutoSendChannel::SMS
);
}
$kakaoSmsOverFlag = $this->resolveKakaoAlternativeSmsOverFlag($replacedKakaoContents, bySmsContent: true);
 
 
 
} else {
$kakaoSmsOverFlag = $this->resolveKakaoAlternativeSmsOverFlag($replacedKakaoContents, bySmsContent: false);
 
 
 
}
} else {
$replacedKakaoContents = '';
$smsContentForRecipient['contents'], $originalReplaceArguments, $this->smsAutoCodeType, AutoSendChannel::SMS
);
}
$kakaoSmsOverFlag = $this->resolveKakaoAlternativeSmsOverFlag($replacedKakaoContents, bySmsContent: true);
 
 
 
} else {
$kakaoSmsOverFlag = $this->resolveKakaoAlternativeSmsOverFlag($replacedKakaoContents, bySmsContent: false);
 
 
 
}
} else {
$replacedKakaoContents = '';
$logData = $this->buildSendLogData($logData);
 
// 발송
$message = $this->createSendMessage($contents);
 
 
 
 
 
 
 
 
 
$transport = \App::load(SmsSender::class);
if ($this->isAutoSend() || !$this->passwordCheckFl) {
// LMS 제목 (설정된 자동발송 문구 제목 - LMS 발송 시에만 SmsSender 에서 사용, 미입력 시 [제목없음])
// 튜닝 상점 대비: 구버전 SmsSender(setSubject 미보유)에서 발송 중단 방지
if (method_exists($transport, 'setSubject')) {
$transport->setSubject($this->resolveSendSubject($item, $recipientKey));
}
$transport->setSmsType($item['smsType']);
$transport->setReceiver($receiverData);
return $transport->send();
}
 
/**
* 치환 완료된 내용으로 SMS 발송 메시지를 생성한다.
*
* @param string $contents 치환 완료된 발송 내용
* @return AbstractMessage 90byte 이하이면 SmsMessage, 초과 시 전송 방법에 따른 메시지
*/
protected function createSendMessage(string $contents): AbstractMessage
{
$smsOverFlag = $this->findAutoSendConfigService->getSmsSendOverFlag($this->smsAutoConfig);
if ($this->isPresentAutoSend()) {
$smsOverFlag = SmsAutoSendOverType::LMS;
}
 
$message = new \Component\Sms\SmsMessage($contents);
if ($message->exceedSmsLength()) {
$message = match ($smsOverFlag) {
SmsAutoSendOverType::LMS => new \Component\Sms\LmsMessage($contents),
SmsAutoSendOverType::DIVISION => new \Component\Sms\DivisionSmsMessage($contents),
default => new \Component\Sms\SmsMessage($contents),
};
}
 
return $message;
}
 
/**
* LMS 발송 시 사용할 제목을 결정한다.
*
* @param array $item 조회된 SMS 템플릿. 등록된 제목은 messageTitle key 에 담긴다
* @param string $recipientKey 수신대상 ('member', 'admin', 'provider', 'recipient')
* @return string 등록된 제목. 선물하기 자동알림이면서 치환코드를 제외한 제목이 비어 있으면 기본 제목
*/
protected function resolveSendSubject(array $item, string $recipientKey): string
{
$messageTitle = $item['messageTitle'] ?? '';
if ($this->isPresentAutoSend()) {
// 실발송 제목은 SmsSender 가 치환코드를 제거한 뒤 공란을 판정하므로 같은 기준으로 봐야
// 치환코드만 등록된 제목이 '[제목없음]' 으로 발송되는 것을 막을 수 있다
$titleWithoutReplaceCode = trim(preg_replace('/#?\{[^}]*\}/u', '', (string) $messageTitle));
if ($titleWithoutReplaceCode === '') {
return $this->getPresentDefaultSubject($recipientKey);
}
}
 
return $messageTitle;
}
 
/**
* 선물하기 자동알림의 LMS 기본 제목을 반환한다.
*
* 선물하기 템플릿에는 제목이 등록돼 있지 않아 LMS 로 발송하면 수신자에게 '[제목없음]' 으로 보인다.
*
* @param string $recipientKey 수신대상 ('member', 'admin', 'provider', 'recipient')
* @return string 기본 제목. 정의된 제목이 없으면 빈 문자열
*/
protected function getPresentDefaultSubject(string $recipientKey): string
{
$defaultSubjects = [
Code::PRESENT => ['common' => '선물 발송 완료', 'recipient' => '선물이 도착했어요'],
Code::PRESENT_REMAIN_THREE => ['common' => '선물 수락 기한 3일 전', 'recipient' => '선물 수락 기한 3일 전'],
Code::PRESENT_REMAIN_ONE => ['common' => '선물 수락 기한 1일 전', 'recipient' => '선물 수락 기한 1일 전'],
Code::PRESENT_ACCEPT => ['common' => '선물 수락 완료', 'recipient' => '선물 수락 완료'],
Code::PRESENT_REJECT => ['common' => '선물 거절 및 환불 접수', 'recipient' => '선물 거절 및 환불 접수'],
Code::PRESENT_CHANGE_DEST => ['common' => '선물 배송지 변경', 'recipient' => '배송지 변경 완료'],
Code::PRESENT_INVOICE_CODE => ['recipient' => '선물 배송 시작'],
Code::PRESENT_REJECT_FAIL => ['common' => '선물 수락기간 만료', 'recipient' => '선물 수락기간 만료'],
];
 
$subjectKey = $recipientKey === 'recipient' ? 'recipient' : 'common';
 
return $defaultSubjects[$this->smsAutoCodeType][$subjectKey] ?? '';
}
 
/**
* 알림톡 발송 실패 시 대체발송할 SMS 의 90byte 초과 전송 방법을 결정한다.
*
* @param string|null $contents 대체발송할 내용. 치환 결과가 비어 있으면 null 이 넘어온다
* @param bool $bySmsContent 대체발송 내용을 SMS 템플릿에서 가져온 경우 true, 알림톡 내용을 그대로 쓰면 false
* @return string SmsAutoSendOverType 값 또는 'none'
*/
protected function resolveKakaoAlternativeSmsOverFlag(?string $contents, bool $bySmsContent): string
{
if (!$this->findAutoSendConfigService->isOverSmsLength($contents)) {
return SmsAutoSendOverType::LIMIT->value;
}
 
if ($this->isPresentAutoSend()) {
return SmsAutoSendOverType::LMS->value;
}
 
if ($bySmsContent) {
return $this->findAutoSendConfigService->getSmsSendOverFlag($this->smsAutoConfig)->value;
}
 
return $this->findAutoSendConfigService->convertCrmToSendOverFlag($this->kakaoCrmConfig);
}
 
/**
* 선물확인 URL 이 절단되거나 분할되면 수신자가 선물을 확인할 수 없어, 선물하기는 상점의 90byte 초과 전송 방법 설정과 무관하게 LMS 로 발송한다.
*/
protected function isPresentAutoSend(): bool
{
return $this->smsType === SmsAutoCode::PRESENT;
}
 
/**
* 카카오 비즈엠 버튼 데이터 생성
* 페이지링크에 송장번호 치환코드가 있는 경우 치환 처리
 
통계 > 주문 분석 도움말에 5년 이전 주문 구매자수 안내 문구 추가
Admin/statistics/order_age.php+9
변경 전order_age.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
 
 
 
 
 
 
 
 
 
 
변경 후order_age.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
<script type="text/javascript">
const code = '251119007';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>
 
Admin/statistics/order_area.php+9
변경 전order_area.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
 
 
 
 
 
 
 
 
 
 
변경 후order_area.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
<script type="text/javascript">
const code = '251119008';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>
 
Admin/statistics/order_gender.php+9
변경 전order_gender.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
 
 
 
 
 
 
 
 
 
 
변경 후order_gender.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
<script type="text/javascript">
const code = '251119009';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>
 
Admin/statistics/order.php+9
변경 전order.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
 
 
 
 
 
 
 
 
 
 
변경 후order.php
});
</script>
<script type="text/javascript" src="<?=PATH_ADMIN_GD_SHARE?>script/statistics.js"></script>
<script type="text/javascript">
const code = '251119006';
if (window.GodoCosGuide) {
window.GodoCosGuide.init({
apiUrl: <?= json_encode($cosGuideApiUrl) ?>,
guideCode: code,
});
}
</script>