본문으로 건너뛰기

소스 변경 전체 보기

PHP 7 · GODO25
변경 파일 수: 53개
변경 파일 목록
53
상품 리스트 복사 시 이미지 보호 기능 개선.
GoodsAdmin.phpComponent/Goods/GoodsObsConvertStatus.phpComponent/Goods/
수기주문에서 회원 장바구니 상품 2개 이상 추가 안정성 개선
OrderPsController.phpController/Admin/Order/
DB 데드락 자동 복구 및 발송/장바구니 교착 완화
Cart.phpComponent/Cart/MailLog.phpComponent/Mail/CartPsController.phpController/Front/Order/
모바일 상품 게시판 상세보기 조회 속도 개선
Board.phpComponent/Board/Goods.phpComponent/Goods/
비회원 주문 취소 복원 처리 개선
ReOrderCalculation.phpComponent/Order/
카카오 아이디 로그인 설정 페이지 헤더 표시 개선.
kakao_login_config.phpAdmin/member/
상품 판매 통계 로그 정확도 개선
OrderNew.phpComponent/Order/Order.phpComponent/Order/
회원등급 평가방법 설정의 '주문금액' 표기를 '실결제금액'으로 변경
member_group_appraisal_config.phpAdmin/member/
변경 파일 코드
추가삭제
관리자 동시 접속 보안 강화.
Admin/base/duplicate_login.php+84
변경 전duplicate_login.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후duplicate_login.php
<link rel="stylesheet" href="<?= PATH_ADMIN_GD_SHARE ?>css/duplicate-login.css"/>
<form id="frmDuplicateLogin" action="login_ps.php" method="post">
<input type="hidden" name="mode" value="concurrentLoginConfirm"/>
<input type="hidden" name="returnUrl" value=""/>
 
<div class="duplicate-login-backdrop">
<div class="duplicate-login-dialog">
<a href="/base/login_ps.php?mode=logout" class="close duplicate-login-close" aria-label="닫기">&times;</a>
<h2 class="duplicate-login-title">고객님의 계정이 이미 사용중입니다.</h2>
 
<div class="duplicate-login-content">
<p class="duplicate-login-desc">
• 고객님의 계정이 다른 IP에서 로그인되었습니다. 아래 버튼을 선택하여 로그인을 진행해주세요.<br>
• 안전하게 이용하려면 이전 로그인을 종료하고 새로 로그인해주세요.
</p>
 
<table class="duplicate-login-ip-table">
<colgroup>
<col width="50%">
<col width="50%">
</colgroup>
<tbody>
<tr>
<th>현재 로그인한 IP</th>
<td><?= gd_htmlspecialchars($currentLoginIp) ?></td>
</tr>
<tr>
<th>이전 로그인한 IP</th>
<td><?= gd_htmlspecialchars($lastLoginIp) ?></td>
</tr>
</tbody>
</table>
 
<div class="duplicate-login-options">
<label class="duplicate-login-option">
<input type="radio" name="keepExisting" value="y" checked />
이전 로그인을 그대로 유지한 상태에서 로그인 (동시 로그인 허용)
</label>
<label class="duplicate-login-option">
<input type="radio" name="keepExisting" value="n" />
이전 로그인은 종료하고, 새로 로그인
</label>
</div>
</div>
 
<div class="duplicate-login-actions">
<a href="/base/login_ps.php?mode=logout" class="btn btn-white">취소</a>
<input type="submit" value="확인" class="btn btn-black"/>
</div>
</div>
</div>
</form>
 
<script>
(() => {
const form = document.getElementById('frmDuplicateLogin');
if (!form) { return; }
 
form.addEventListener('submit', (e) => {
e.preventDefault();
 
const checked = form.querySelector('input[name="keepExisting"]:checked');
const keepExisting = checked?.value ?? 'n';
const standalone = keepExisting !== 'y';
 
fetch('/share/duplicate_login_ps.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ standalone: standalone }),
}).then((response) => {
if (!response.ok) {
throw new Error('request failed');
}
location.reload();
}).catch(() => {
alert('요청하신 작업을 완료하지 못했습니다.\n잠시 후 다시 시도해 주세요.');
});
});
})();
</script>
 
Component/Admin/AdminLogDAO.php+6
변경 전AdminLogDAO.php
 
const LOGIN = 'login';
const LOGOUT = 'logout';
 
 
const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout'];
 
$page = '주문 내역 삭제';
}
 
 
 
 
 
 
$logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y;
// 개인정보접속기록 다운로드로그 5년보관
if ($page == 'adminLogExcel') {
 
변경 후AdminLogDAO.php
 
const LOGIN = 'login';
const LOGOUT = 'logout';
const LOGOUT_SESSION_END = 'logoutSessionEnd';
 
const ALLOWED_MODES = ['authEmail', 'authSms', 'register', 'modify', 'delete', 'setManagePermission', 'getManagerListLayout'];
 
$page = '주문 내역 삭제';
}
 
if ($mode === self::LOGOUT_SESSION_END) {
$page = 'adminInfo';
$action = '로그아웃(세션 종료)';
}
 
$logType = $pageData['type'] ?? AdminLogDAO::LOG_TYPE_P2Y;
// 개인정보접속기록 다운로드로그 5년보관
if ($page == 'adminLogExcel') {
 
Component/Member/ManagerCs.php+9
변경 전ManagerCs.php
use Framework\Debug\Exception\DatabaseException;
use Framework\Security\Digester;
use Framework\Security\Otp;
 
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Framework\Utility\GodoUtils;
 
 
/**
* Class ManagerCs
* @package Bundle\Component\Member
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$logger = \App::getInstance('logger');
 
 
 
 
 
 
$this->db->query_reset();
$data['lastLoginIp'] = $request->getRemoteAddress();
$data['sessionLimitTime'] = $sessionLimitTime;
$session->del(self::SESSION_TEMP_MANAGER);
$session->set(self::SESSION_MANAGER_LOGIN, $data);
 
}
 
/**
 
변경 후ManagerCs.php
use Framework\Debug\Exception\DatabaseException;
use Framework\Security\Digester;
use Framework\Security\Otp;
use Framework\SimpleCache\SimpleCache;
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\StringUtils;
use Framework\Utility\GodoUtils;
use Origin\Service\Storage\ManagerDuplicateLoginService;
 
/**
* Class ManagerCs
* @package Bundle\Component\Member
$request = \App::getInstance('request');
$session = \App::getInstance('session');
$logger = \App::getInstance('logger');
$duplicateLoginService = new ManagerDuplicateLoginService(
\App::getInstance('application'),
\App::getInstance('session'),
SimpleCache::init(SimpleCache::REDIS)
);
 
$this->db->query_reset();
$data['lastLoginIp'] = $request->getRemoteAddress();
$data['sessionLimitTime'] = $sessionLimitTime;
$session->del(self::SESSION_TEMP_MANAGER);
$session->set(self::SESSION_MANAGER_LOGIN, $data);
$duplicateLoginService->addCurrentLoginToList($data['sno']);
}
 
/**
 
Component/Member/Manager.php+74-3
변경 전Manager.php
 
namespace Bundle\Component\Member;
 
 
use Component\Admin\AdminMenu;
use Component\Database\DBTableField;
use Component\Member\Exception\LoginException;
use Framework\Debug\Exception\AlertRedirectException;
use Framework\Security\Digester;
use Framework\Security\Otp;
 
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Member\ManagerLogoutService;
 
use Password;
use Request;
use Session;
*/
private $_storage;
 
 
 
 
 
 
 
/**
* 생성자
*/
$this->smsAutoReceiveKind[$newKey] = $val;
}
unset($tmp, $newKey);
 
 
 
 
 
 
}
 
/**
$this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션
$this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션
$this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션
 
 
// 관리자 로그인 성공 로그
$request->request()->set('adminLoginFl', true);
 
$adminMenuFlagService->updateAdminMenuFlagsInSession();
}
 
 
 
 
 
 
 
 
 
 
 
/**
* setManagerOldMenuAccessChange
* 운영자 세부 접속 권한 마이그레이션
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 관리자 로그아웃
 
*/
public function managerLogout()
{
$cookie = \App::getInstance('cookie');
 
// 관리자 로그아웃 로그 작성
 
$adminLog = \App::load('Component\\Admin\\AdminLogDAO');
$adminLog->setAdminLog($adminLog::LOGOUT);
 
// 세션을 없애기전에 세션 쿠키를 지웁니다.
if ($cookie->has($session->getName())) {
// 관리자 통합회원 로그아웃
/** @var ManagerLogoutService $managerLogoutService */
$managerLogoutService = \App::getInstance(ManagerLogoutService::class);
$managerLogoutService->logout();
 
 
// 관리자만 로그아웃하는 경우 세션 클리어
if (MemberUtil::isLogin()) {
}
 
if ($result > 0 && $type != 'reset') {
Manager::managerLogout();
}
}
 
unset($arrBind);
return gd_htmlspecialchars_stripslashes($data);
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후Manager.php
 
namespace Bundle\Component\Member;
 
use Component\Admin\AdminLogDAO;
use Component\Admin\AdminMenu;
use Component\Database\DBTableField;
use Component\Member\Exception\LoginException;
use Framework\Debug\Exception\AlertRedirectException;
use Framework\Security\Digester;
use Framework\Security\Otp;
use Framework\SimpleCache\SimpleCache;
use Framework\Utility\ComponentUtils;
use Framework\Utility\DateTimeUtils;
use Framework\Utility\GodoUtils;
use Framework\Utility\StringUtils;
use Origin\Service\Member\ManagerLogoutService;
use Origin\Service\Storage\ManagerDuplicateLoginService;
use Password;
use Request;
use Session;
*/
private $_storage;
 
/** @var ManagerDuplicateLoginService */
private $managerLoginStorage;
 
/** @var bool 중복 로그인으로 인한 (강제) 로그아웃 여부 */
private $shouldDuplicateLogout = false;
 
/**
* 생성자
*/
$this->smsAutoReceiveKind[$newKey] = $val;
}
unset($tmp, $newKey);
 
$this->managerLoginStorage = new ManagerDuplicateLoginService(
\App::getInstance('application'),
\App::getInstance('session'),
SimpleCache::init(SimpleCache::REDIS)
);
}
 
/**
$this->setManagerOldMenuAccessChange(); // 운영자 접속 권한 - 세부 권한으로 마이그레이션
$this->setManagerFunctionAuthChange(); // 운영자 기능 권한 - 기능 권한으로 마이그레이션
$this->setManagerScmFunctionAuthChange(); // 공급사 기능 권한 - 기능 권한으로 마이그레이션
$this->setDuplicateLoginList($data['sno']); // 중복 로그인 감지용 로그인 세션 목록에 현재 세션 등록
 
// 관리자 로그인 성공 로그
$request->request()->set('adminLoginFl', true);
 
$adminMenuFlagService->updateAdminMenuFlagsInSession();
}
 
/**
* 동시 로그인 정보를 Redis 에 등록한다.
*
* @param int $sno 관리자 sno
*/
public function setDuplicateLoginList(int $sno)
{
$this->managerLoginStorage->addCurrentLoginToList($sno);
}
 
/**
* setManagerOldMenuAccessChange
* 운영자 세부 접속 권한 마이그레이션
}
}
 
/**
* 중복 로그인으로 인한 (강제) 로그아웃 여부 주입.
* managerLogout() 을 무인자로 유지하기 위해 파라미터 대신 setter 로 받는다.
*
* @param bool $shouldDuplicateLogout true 면 es_adminLog 에 세션 종료 action 으로 기록
* @return void
*/
public function setShouldDuplicateLogout(bool $shouldDuplicateLogout)
{
$this->shouldDuplicateLogout = $shouldDuplicateLogout;
}
 
/**
* 관리자 로그아웃
* 중복 로그인 여부는 setShouldDuplicateLogout() 로 주입한다.
*/
public function managerLogout()
{
$cookie = \App::getInstance('cookie');
 
// 관리자 로그아웃 로그 작성
/** @var AdminLogDAO $adminLog */
$adminLog = \App::load('Component\\Admin\\AdminLogDAO');
$adminLog->setAdminLog($this->shouldDuplicateLogout ? $adminLog::LOGOUT_SESSION_END : $adminLog::LOGOUT);
 
// 세션을 없애기전에 세션 쿠키를 지웁니다.
if ($cookie->has($session->getName())) {
// 관리자 통합회원 로그아웃
/** @var ManagerLogoutService $managerLogoutService */
$managerLogoutService = \App::getInstance(ManagerLogoutService::class);
$managerLogoutService->logout($this->shouldDuplicateLogout);
$this->managerLoginStorage->deleteCurrentLoginFromList();
 
// 관리자만 로그아웃하는 경우 세션 클리어
if (MemberUtil::isLogin()) {
}
 
if ($result > 0 && $type != 'reset') {
$this->managerLogout();
}
}
 
unset($arrBind);
return gd_htmlspecialchars_stripslashes($data);
}
 
/**
* @return bool
*/
public function isCurrentLoginRegistered(): bool
{
return $this->managerLoginStorage->isCurrentLoginRegistered();
}
 
/**
* @return bool
*/
public function isDuplicateLogin(): bool
{
return $this->managerLoginStorage->isDuplicateLatestLogin();
}
 
/**
* @return string|null
*/
public function getLastLoginIp()
{
return $this->managerLoginStorage->getPreviousLoginIp();
}
 
public function allLogoutWithoutMe()
{
$this->managerLoginStorage->deleteLoginAllWithoutMe();
}
}
 
Controller/Admin/Base/IndexController.php+21
변경 전IndexController.php
 
use Component\Member\Manager;
use Core\Base\View\Alert;
 
use Framework\Utility\StringUtils;
use Origin\Service\License\LicenseRenewService;
 
$mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy');
$sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN);
 
 
 
// 메인 통계의 권한 체크
$adminMenu = \App::load('Component\\Admin\\AdminMenu');
if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
 
$this->getView()->setPageName('base/index.php');
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후IndexController.php
 
use Component\Member\Manager;
use Core\Base\View\Alert;
use Framework\Http\Session\SessionManager;
use Framework\Utility\StringUtils;
use Origin\Service\License\LicenseRenewService;
 
$mainSettingPolicy = \App::load('Component\\Policy\\MainSettingPolicy');
$sessionByManager = $session->get(Manager::SESSION_MANAGER_LOGIN);
 
$this->checkDuplicateLogin($session);
 
// 메인 통계의 권한 체크
$adminMenu = \App::load('Component\\Admin\\AdminMenu');
if ($this->getData('managerPermissionMethodExist') === true && $this->getData('adminMenuPermissionMethodExist') === true) {
 
$this->getView()->setPageName('base/index.php');
}
 
public function checkDuplicateLogin(SessionManager $session)
{
$manager = \App::getInstance(\Manager::class);
$request = \App::getInstance('request');
 
if (
$manager->isDuplicateLogin() &&
!$session->get('manager.allowDuplicateLogin')
) {
$this->getView()->setDefine('layout', 'layout_blank_noiframe.php');
$this->getView()->setPageName('base/duplicate_login.php');
$this->getView()->setData('lastLoginIp',$manager->getLastLoginIp());
$this->getView()->setData('currentLoginIp', $request->getRemoteAddress());
echo $this->getView()->render();
exit;
}
}
}
 
상품 리스트 및 상품 가격 관리 정렬 기능 개선.
Component/Database/DBTableField.php+1-1
변경 전DBTableField.php
['val' => 'naverFl', 'typ' => 's', 'def' => 'y', 'name' => '네이버쇼핑여부','batch'=>'y'], // 네이버수입및제작여부
['val' => 'daumFl', 'typ' => 's', 'def' => 'y', 'name' => '다음쇼핑하우연동여부', 'batch'=>'y'], // 다음쇼핑하우연동여부
['val' => 'paycoFl', 'typ' => 's', 'def' => 'y', 'name' => '페이코쇼핑여부','batch'=>'y'], // 페이코수입및제작여부
['val' => 'modDt', 'typ' => 's', 'def' => null], // 수정일
['val' => 'regDt', 'typ' => 's', 'def' => null], // 등록일
];
// @formatter:on
 
변경 후DBTableField.php
['val' => 'naverFl', 'typ' => 's', 'def' => 'y', 'name' => '네이버쇼핑여부','batch'=>'y'], // 네이버수입및제작여부
['val' => 'daumFl', 'typ' => 's', 'def' => 'y', 'name' => '다음쇼핑하우연동여부', 'batch'=>'y'], // 다음쇼핑하우연동여부
['val' => 'paycoFl', 'typ' => 's', 'def' => 'y', 'name' => '페이코쇼핑여부','batch'=>'y'], // 페이코수입및제작여부
['val' => 'modDt', 'typ' => 's', 'def' => null, 'batch'=>'y'], // 수정일
['val' => 'regDt', 'typ' => 's', 'def' => null], // 등록일
];
// @formatter:on
 
Component/Goods/GoodsAdmin.php+67-5
변경 전GoodsAdmin.php
 
const DEFAULT_STORAGE = ['local', 'obs'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 생성자
*/
$this->search['sortList'] = array(
'g.goodsNo desc' => __('등록일 ↓'),
'g.goodsNo asc' => __('등록일 ↑'),
 
 
'g.goodsNm asc' => __('상품명 ↓'),
'g.goodsNm desc' => __('상품명 ↑'),
'g.goodsPrice asc' => __('판매가 ↓'),
$getValue = Request::get()->toArray();
 
gd_isset($getValue['delFl'], 'n');
// --- 정렬 설정
if ($getValue['delFl'] == 'y') {
$sort = gd_isset($getValue['sort'], 'delDt desc , g.goodsNo desc');
if ($this->goodsDivisionFl) {
$join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo ';
}
 
 
 
 
 
} else {
$sort = gd_isset($getValue['sort'], 'g.goodsNo desc');
}
 
 
 
 
 
 
$this->setSearchGoods($getValue);
 
//상품 헤택 모듈
$getValue = Request::get()->toArray();
 
gd_isset($getValue['delFl'], 'n');
$sort = gd_isset($getValue['sort'], 'g.goodsNo desc');
 
 
 
 
 
$changeGoodsTableFl = false;
 
if ($mode == 'restock') {
 
if(empty($arrGoodsNo)) return true;
 
 
 
 
$updateField = [];
foreach(DBTableField::tableGoodsSearch() as $k => $v) {
if($v['batch'] =='y') $updateField[] = 'gs.'.$v['val'].'='.'g.'.$v['val'];
 
$this->setSearchGoods($getValue);
if($getValue['goodsNo'] && is_array($getValue['goodsNo'])) {
$this->arrWhere[] = 'goodsNo IN (' . implode(',', $getValue['goodsNo']) . ')';
}
 
$strSQL = ' SELECT COUNT(g.goodsNo) AS cnt FROM ' . $this->goodsTable . ' as g WHERE ' . implode(' AND ', $this->arrWhere);
$this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo);
$this->db->setModDtUse(true);
$this->db->set_update_db(DB_GOODS, 'cateCd = ?', 'goodsNo = ?', $arrBind['bind']);
 
 
 
 
unset($getLink);
}
}
$this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo);
$this->db->setModDtUse(true);
$return = $this->db->set_update_db(DB_GOODS, $arrBind['param'], 'goodsNo = ?', $arrBind['bind']);
 
 
 
 
unset($arrBind, $goodsData);
return $return;
}
unset($arrBind);
}
 
 
 
 
 
 
return $arrData['applyFl'];
}
 
 
변경 후GoodsAdmin.php
 
const DEFAULT_STORAGE = ['local', 'obs'];
 
// 정렬 SQL 인젝션 방지용 화이트리스트 (관리자 상품 리스트 정렬 옵션)
const SORT_ALLOW_LIST = [
'g.goodsNo desc', 'g.goodsNo asc',
'g.modDt desc', 'g.modDt asc',
'g.goodsNm asc', 'g.goodsNm desc',
'g.goodsPrice asc', 'g.goodsPrice desc',
'companyNm asc', 'companyNm desc',
'g.makerNm asc', 'g.makerNm desc',
'g.orderGoodsCnt desc',
'g.hitCnt desc',
'orderRate desc',
'g.cartCnt desc',
'g.wishCnt desc',
'g.reviewCnt desc',
'delDt desc, g.goodsNo desc',
'delDt asc',
];
 
// 정렬 SQL 인젝션 방지용 화이트리스트 (관리자 상품 일괄처리 리스트 - getAdminListBatch)
const SORT_ALLOW_LIST_BATCH = [
'g.goodsNo desc', 'g.goodsNo asc',
'g.modDt desc', 'g.modDt asc',
'goodsNm asc', 'goodsNm desc',
'companyNm asc', 'companyNm desc',
'goodsPrice asc', 'goodsPrice desc',
'fixedPrice asc', 'fixedPrice desc',
'costPrice asc', 'costPrice desc',
];
 
/**
* 생성자
*/
$this->search['sortList'] = array(
'g.goodsNo desc' => __('등록일 ↓'),
'g.goodsNo asc' => __('등록일 ↑'),
'g.modDt desc' => __('수정일 ↓'),
'g.modDt asc' => __('수정일 ↑'),
'g.goodsNm asc' => __('상품명 ↓'),
'g.goodsNm desc' => __('상품명 ↑'),
'g.goodsPrice asc' => __('판매가 ↓'),
$getValue = Request::get()->toArray();
 
gd_isset($getValue['delFl'], 'n');
// --- 검색 설정 보조 JOIN
if ($getValue['delFl'] == 'y') {
 
if ($this->goodsDivisionFl) {
$join[] = ' LEFT JOIN ' . DB_GOODS . ' as goods ON g.goodsNo = goods.goodsNo ';
}
}
 
// --- 정렬 설정 (SQL 인젝션 방지 - 사용자 입력은 화이트리스트만 허용)
if (isset($getValue['sort']) && in_array($getValue['sort'], self::SORT_ALLOW_LIST, true)) {
$sort = $getValue['sort'];
} else {
$sort = ($getValue['delFl'] == 'y') ? 'delDt desc , g.goodsNo desc' : 'g.goodsNo desc';
}
 
// 수정일 정렬 시 NULL 처리 - 등록일로 대체, 페이징 안정화 보조정렬
if (preg_match('/^g\.modDt\s+(asc|desc)$/i', $sort, $m)) {
$sort = "COALESCE(g.modDt, g.regDt) {$m[1]}, g.goodsNo desc";
}
 
$this->setSearchGoods($getValue);
 
//상품 헤택 모듈
$getValue = Request::get()->toArray();
 
gd_isset($getValue['delFl'], 'n');
// 정렬 설정 (SQL 인젝션 방지 - 사용자 입력은 화이트리스트만 허용)
if (isset($getValue['sort']) && in_array($getValue['sort'], self::SORT_ALLOW_LIST_BATCH, true)) {
$sort = $getValue['sort'];
} else {
$sort = 'g.goodsNo desc';
}
$changeGoodsTableFl = false;
 
if ($mode == 'restock') {
 
if(empty($arrGoodsNo)) return true;
 
// goodsNo 는 정수 전용 식별자 → IN 절 SQL 인젝션 방어 (sink 지점에서 일괄 정수화)
$arrGoodsNo = array_map('intval', $arrGoodsNo);
 
$updateField = [];
foreach(DBTableField::tableGoodsSearch() as $k => $v) {
if($v['batch'] =='y') $updateField[] = 'gs.'.$v['val'].'='.'g.'.$v['val'];
 
$this->setSearchGoods($getValue);
if($getValue['goodsNo'] && is_array($getValue['goodsNo'])) {
$this->arrWhere[] = 'goodsNo IN (' . implode(',', array_map('intval', $getValue['goodsNo'])) . ')';
}
 
$strSQL = ' SELECT COUNT(g.goodsNo) AS cnt FROM ' . $this->goodsTable . ' as g WHERE ' . implode(' AND ', $this->arrWhere);
$this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo);
$this->db->setModDtUse(true);
$this->db->set_update_db(DB_GOODS, 'cateCd = ?', 'goodsNo = ?', $arrBind['bind']);
// es_goodsSearch 동기화
if ($this->goodsDivisionFl) {
$this->updateGoodsSearch([$goodsNo]);
}
unset($getLink);
}
}
$this->db->bind_param_push($arrBind['bind'], 'i', $goodsNo);
$this->db->setModDtUse(true);
$return = $this->db->set_update_db(DB_GOODS, $arrBind['param'], 'goodsNo = ?', $arrBind['bind']);
// es_goodsSearch 동기화
if ($this->goodsDivisionFl) {
$this->updateGoodsSearch([$goodsNo]);
}
unset($arrBind, $goodsData);
return $return;
}
unset($arrBind);
}
 
// es_goodsSearch 동기화
if ($this->goodsDivisionFl) {
$this->updateGoodsSearch($arrGoodsNo, $getData['modDtUse']);
}
 
return $arrData['applyFl'];
}
 
 
Controller/Admin/Goods/GoodsBatchPriceController.php+14-12
변경 전GoodsBatchPriceController.php
 
//정렬 재정의
$getData['search']['sortList'] = array(
'g.goodsNo desc' => sprintf(__('등록일 %1$s'), '↓'),
'g.goodsNo asc' => sprintf(__('등록일 %1$s'), '↑'),
'goodsNm asc' => sprintf(__('상품명 %1$s'), '↓'),
'goodsNm desc' => sprintf(__('상품명 %1$s'), '↑'),
'companyNm asc' => sprintf(__('공급사 %1$s'), '↓'),
'companyNm desc' => sprintf(__('공급사 %1$s'), '↑'),
'fixedPrice asc' => sprintf(__('정가 %1$s'), '↓'),
'fixedPrice desc' => sprintf(__('정가 %1$s'), '↑'),
'costPrice asc' => sprintf(__('매입가 %1$s'), '↓'),
'costPrice desc' => sprintf(__('매입가 %1$s'), '↑'),
'goodsPrice asc' => sprintf(__('판매가 %1$s'), '↓'),
'goodsPrice desc' => sprintf(__('판매가 %1$s'), '↑'),
 
 
);
 
$this->setData('goods', $goods);
 
변경 후GoodsBatchPriceController.php
 
//정렬 재정의
$getData['search']['sortList'] = array(
'g.goodsNo desc' => __('등록일 ↓'),
'g.goodsNo asc' => __('등록일 ↑'),
'g.modDt desc' => __('수정일 ↓'),
'g.modDt asc' => __('수정일 ↑'),
'goodsNm asc' => __('상품명 ↓'),
'goodsNm desc' => __('상품명 ↑'),
'companyNm asc' => __('공급사 ↓'),
'companyNm desc' => __('공급사 ↑'),
'fixedPrice asc' => __('정가 ↓'),
'fixedPrice desc' => __('정가 ↑'),
'costPrice asc' => __('매입가 ↓'),
'costPrice desc' => __('매입가 ↑'),
'goodsPrice asc' => __('판매가 ↓'),
'goodsPrice desc' => __('판매가 ↑'),
);
 
$this->setData('goods', $goods);
 
Controller/Admin/Goods/GoodsPsController.php+10-2
변경 전GoodsPsController.php
// 상품리스트 메모 저장하기(그리드)
case 'goods_list_memo' :
try {
if ($goods->getGoodsListAdminMemo($postValue['goodsNo'], $postValue['adminMemo'], 'update')) {
 
 
 
$this->layer(__('정상적으로 저장 되었습니다.'));
}
} catch (Exception $e) {
 
throw new LayerNotReloadException($e->getMessage());
}
break;
// 상품리스트 선택 조회항목 삭제하기
case 'goods_list_add_grid_del' :
try {
 
// 조회항목
if ($goods->goodsListDisplayEachDelete($postValue)) {
 
 
echo true;
}
} catch (Exception $e) {
 
throw new LayerNotReloadException($e->getMessage());
}
break;
 
변경 후GoodsPsController.php
// 상품리스트 메모 저장하기(그리드)
case 'goods_list_memo' :
try {
\DB::begin_tran();
$result = $goods->getGoodsListAdminMemo($postValue['goodsNo'], $postValue['adminMemo'], 'update');
\DB::commit();
if ($result) {
$this->layer(__('정상적으로 저장 되었습니다.'));
}
} catch (Exception $e) {
\DB::rollback();
throw new LayerNotReloadException($e->getMessage());
}
break;
// 상품리스트 선택 조회항목 삭제하기
case 'goods_list_add_grid_del' :
try {
\DB::begin_tran();
// 조회항목
$result = $goods->goodsListDisplayEachDelete($postValue);
\DB::commit();
if ($result) {
echo true;
}
} catch (Exception $e) {
\DB::rollback();
throw new LayerNotReloadException($e->getMessage());
}
break;
 
고도몰 쇼핑몰 보안 강화
Component/Board/Board.php+2-1
변경 전Board.php
protected function xssClean($data)
{
 
$onevents = 'onabort|onactivate|onafterprint|onafterupdate|onbeforeactivate|onbeforecopy|onbeforecut|onbeforedeactivate|onbeforeeditfocus|onbeforepaste|onbeforeprint|onbeforeunload|onbeforeupdate|onblur|onbounce|oncellchange|onchange|onclick|oncontextmenu|oncontrolselect|oncopy|oncut|ondataavailable|ondatasetchanged|ondatasetcomplete|ondblclick|ondeactivate|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|onerror|onerrorupdate|onfilterchange|onfinish|onfocus|onfocusin|onfocusout|onhelp|onkeydown|onkeypress|onkeyup|onlayoutcomplete|onload|onlosecapture|onmousedown|onmouseenter|onmouseleave|onmousemove|onmouseout|onmouseover|onmouseup|onmousewheel|onmove|onmoveend|onmovestart|onpaste|onpropertychange|onreadystatechange|onreset|onresize|onresizeend|onresizestart|onrowenter|onrowexit|onrowsdelete|onrowsinserted|onscroll|onselect|onselectionchange|onselectstart|onstart|onstop|onsubmit|onunload|ontoggle|onpageshow|onrepeat|onprogress|onbegin|onpointerdown|onauxclick|onpointerenter|onend|onsearch';
 
$data = preg_replace('#([\x00-\x20"\'/])(?:'.$onevents.'|xmlns)[^>]*+>#iu', '$1>', $data);
 
// Remove javascript: and vbscript: protocols
 
변경 후Board.php
protected function xssClean($data)
{
 
// 이벤트 핸들러 목록은 StringUtils 단일소스 사용 (목록 드리프트 방지)
$onevents = StringUtils::getXssEventHandlers();
$data = preg_replace('#([\x00-\x20"\'/])(?:'.$onevents.'|xmlns)[^>]*+>#iu', '$1>', $data);
 
// Remove javascript: and vbscript: protocols
 
Controller/Admin/Promotion/PollDetailViewController.php+2-5
변경 전PollDetailViewController.php
use Component\Promotion\Poll;
use Component\Page\Page;
use Exception;
 
use Request;
/**
$resData = [];
foreach ($resultData as $key => $value) {
if (in_array($key, $range) === false) continue;
$decData = json_decode(stripslashes($value[$getValue['type']]), true);
if (is_array($decData) === true) {
foreach ($decData as $k => $v) {
if ($k == $getValue['detail']) {
$resData[] = $v;
}
}
}
//array_filter 로 인하여 빈값,0,null 값 filter 처리 -> 빈값, 0으로 대답한 경우 삭제됨
$resData = array_values($resData);
$total = count($resData);
$max = ($total - ($pageNum*($currentPage - 1))) - 1;
$min = $max - $pageNum + 1;
$range = range($min,$max);
$page = \App::load('\\Component\\Page\\Page', $currentPage);
$page->page['list'] = $pageNum; // 페이지당 리스트 수
 
변경 후PollDetailViewController.php
use Component\Promotion\Poll;
use Component\Page\Page;
use Exception;
use Framework\Utility\StringUtils;
use Request;
/**
$resData = [];
foreach ($resultData as $key => $value) {
 
$decData = json_decode(stripslashes($value[$getValue['type']]), true);
if (is_array($decData) === true) {
foreach ($decData as $k => $v) {
if ($k == $getValue['detail']) {
$resData[] = StringUtils::htmlSpecialChars($v);
}
}
}
//array_filter 로 인하여 빈값,0,null 값 filter 처리 -> 빈값, 0으로 대답한 경우 삭제됨
$resData = array_values($resData);
$total = count($resData);
 
 
 
$page = \App::load('\\Component\\Page\\Page', $currentPage);
$page->page['list'] = $pageNum; // 페이지당 리스트 수
 
Controller/Front/Service/PollResultController.php+4-1
변경 전PollResultController.php
use Component\Promotion\Poll;
use Framework\Debug\Exception\AlertBackException;
 
use Request;
class PollResultController extends \Controller\Front\Controller
foreach ($item['itemAnswerType'] as $key => $val) {
if (empty(isset($result[$key])) === false) {
if ($val == 'sub') {
if (count($sortData[$key]) < 20 && $result[$key]) $sortData[$key][] = $result[$key];
 
 
} else {
foreach ($item['itemAnswer'][$key] as $_key => $_value) {
if ($item['itemResponseType'][$key] == 'radio') {
 
변경 후PollResultController.php
use Component\Promotion\Poll;
use Framework\Debug\Exception\AlertBackException;
use Framework\Utility\StringUtils;
use Request;
class PollResultController extends \Controller\Front\Controller
foreach ($item['itemAnswerType'] as $key => $val) {
if (empty(isset($result[$key])) === false) {
if ($val == 'sub') {
if (count($sortData[$key]) < 20 && $result[$key]) {
$sortData[$key][] = StringUtils::htmlSpecialChars($result[$key]);
}
} else {
foreach ($item['itemAnswer'][$key] as $_key => $_value) {
if ($item['itemResponseType'][$key] == 'radio') {
 
data/skin/mobile/moment/board/report.html+5-5
변경 전report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
<input type="hidden" name="memoWriterId" value="{=memo.writerId}"/>
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val();
var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val();
 
if (memoSno > 0) { // 댓글을 작성한 경우
writerId = memoWriterId;
}
if (!writerId) { // 피신고자가 비회원인 경우
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
변경 후report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
 
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val();
var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val();
var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호
if (memoSno > 0) { // 댓글을 작성한 경우
targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호
}
if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
data/skin/mobile/mygreek/board/report.html+5-5
변경 전report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
<input type="hidden" name="memoWriterId" value="{=memo.writerId}"/>
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val();
var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val();
 
if (memoSno > 0) { // 댓글을 작성한 경우
writerId = memoWriterId;
}
if (!writerId) { // 피신고자가 비회원인 경우
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
변경 후report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
 
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val();
var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val();
var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호
if (memoSno > 0) { // 댓글을 작성한 경우
targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호
}
if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
data/skin/mobile/story_g/board/report.html+5-5
변경 전report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
<input type="hidden" name="memoWriterId" value="{=memo.writerId}"/>
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerId = $('#frmMemberReportWrite').find('input[name=writerId]').val();
var memoWriterId = $('#frmMemberReportWrite').find('input[name=memoWriterId]').val();
 
if (memoSno > 0) { // 댓글을 작성한 경우
writerId = memoWriterId;
}
if (!writerId) { // 피신고자가 비회원인 경우 회원 신고 및 차단 불가
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
변경 후report.html
<input type="hidden" name="bdId" value="{=req.bdId}"/>
<input type="hidden" name="bdSno" value="{=req.bdSno}"/>
<input type="hidden" name="memId" value="{=memId}"/>
 
<input type="hidden" name="memoWriterMemNo" value="{=memo.memNo}"/>
<input type="hidden" name="writerMemNo" value="{=writerMemNo}"/>
<input type="hidden" name="writerId" value="{=writerId}"/>
// 회원 신고차단탭 노출
function memberReportOn() {
var memoSno = $('#frmMemberReportWrite').find('input[name=memoSno]').val()
var writerMemNo = $('#frmMemberReportWrite').find('input[name=writerMemNo]').val();
var memoWriterMemNo = $('#frmMemberReportWrite').find('input[name=memoWriterMemNo]').val();
var targetMemNo = writerMemNo; // 기본: 게시글 작성자 회원번호
if (memoSno > 0) { // 댓글을 작성한 경우
targetMemNo = memoWriterMemNo; // 댓글 작성자 회원번호
}
if (!targetMemNo || targetMemNo <= 0) { // 피신고자가 비회원인 경우
alert("회원 신고 및 차단이 불가한 회원입니다.");
} else {
if ($('#frmWrite').find('#reportMemo').val()) {
 
게시판 이미지 업로드 지원 확대.
Component/Board/Board.php+4-4
변경 전Board.php
}
 
foreach ($saveFileNmArr as $index => $saveFileNm) {
$newSaveFileNm = substr(md5(microtime()), 0, 16) . rand(100, 999);
$binaryData = file_get_contents($saveFileNm);
$result = $imageUploadService->uploadBinaryImageToRealPath($binaryData, $movePath, $newSaveFileNm);
$dbTable = self::getBoardTableName($this->req['moveBdId']);
$parseGetUrl = parse_url($getUrl);
$parseDomain = $parseGetUrl["host"];
if (preg_match('/' . Request::getHost() . '/i', $parseDomain) != 1) {
throw new\Exception(__('무단링크를 금지합니다.'));
}
return true;
}
}
 
$uploadFileNm[$i] = $file_array[$i]['name'];
$saveFileNm[$i] = substr(md5(microtime()), 0, 16);
 
$listImageWidth = $this->cfg['bdListImgWidth'] ?? 500;
//이미지 아니면 섬네일 생성안됨;
}
 
$uploadFileNm = $fileData['name'];
$saveFileNm = 'tmp_' . substr(md5(microtime()), 0, 16);
 
if ($this->imageResize === true) {
$result = $this->storage->upload($fileData['tmp_name'], $this->cfg['bdUploadPath'] . $saveFileNm, ['width' => $reSizeImgWidth, 'height' => $reSizeImgHeight]);
 
변경 후Board.php
}
 
foreach ($saveFileNmArr as $index => $saveFileNm) {
$newSaveFileNm = ImageUploadService::generateSaveFileName($uploadFileNmArr[$index] ?? '', 16, true);
$binaryData = file_get_contents($saveFileNm);
$result = $imageUploadService->uploadBinaryImageToRealPath($binaryData, $movePath, $newSaveFileNm);
$dbTable = self::getBoardTableName($this->req['moveBdId']);
$parseGetUrl = parse_url($getUrl);
$parseDomain = $parseGetUrl["host"];
if (preg_match('/' . Request::getHost() . '/i', $parseDomain) != 1) {
throw new \Exception(__('무단링크를 금지합니다.'));
}
return true;
}
}
 
$uploadFileNm[$i] = $file_array[$i]['name'];
$saveFileNm[$i] = ImageUploadService::generateSaveFileName($uploadFileNm[$i]);
 
$listImageWidth = $this->cfg['bdListImgWidth'] ?? 500;
//이미지 아니면 섬네일 생성안됨;
}
 
$uploadFileNm = $fileData['name'];
$saveFileNm = 'tmp_' . ImageUploadService::generateSaveFileName($uploadFileNm);
 
if ($this->imageResize === true) {
$result = $this->storage->upload($fileData['tmp_name'], $this->cfg['bdUploadPath'] . $saveFileNm, ['width' => $reSizeImgWidth, 'height' => $reSizeImgHeight]);
 
Component/File/EditorUpload.php+1-1
변경 전EditorUpload.php
$result['result'] = false;
try {
if (file_exists($filePath)) {
$saveFileName = substr(md5(microtime()), 0, 16) . rand(100, 999);
$file = fopen($filePath, 'rb');
$size = filesize($filePath);
$binary_data = fread($file, $size);
 
변경 후EditorUpload.php
$result['result'] = false;
try {
if (file_exists($filePath)) {
$saveFileName = ImageUploadService::generateSaveFileName($filePath, 16, true);
$file = fopen($filePath, 'rb');
$size = filesize($filePath);
$binary_data = fread($file, $size);
 
Controller/Admin/Board/ArticlePsController.php+8-1
변경 전ArticlePsController.php
 
if (Board::isDefaultUploadStorage($req['bdId'])) {
$boardConfig = new BoardConfig($req['bdId']);
$result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
 
 
 
 
 
 
 
} else {
$boardAct = new ArticleActAdmin($req);
$result = $boardAct->uploadAjax($fileData);
 
변경 후ArticlePsController.php
 
if (Board::isDefaultUploadStorage($req['bdId'])) {
$boardConfig = new BoardConfig($req['bdId']);
$imageUploadService = new ImageUploadService();
 
// 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단)
if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) {
throw new AlertBackException('업로드 할 수 없는 파일입니다.');
}
 
$result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
} else {
$boardAct = new ArticleActAdmin($req);
$result = $boardAct->uploadAjax($fileData);
 
Controller/Front/Board/BoardPsController.php+8-1
변경 전BoardPsController.php
// 기본 저장소 인지 확인
if (Board::isDefaultUploadStorage($req['bdId'])) {
// 기본 저장소일 경우 OBS Upload 진행
$result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
 
 
 
 
 
 
 
} else {
// 저장소 변경일 경우 해당 저장소에 Upload 진행
$boardAct = new BoardAct($req);
 
변경 후BoardPsController.php
// 기본 저장소 인지 확인
if (Board::isDefaultUploadStorage($req['bdId'])) {
// 기본 저장소일 경우 OBS Upload 진행
$imageUploadService = new ImageUploadService();
 
// 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단)
if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) {
throw new AlertBackException('업로드 할 수 없는 파일입니다.');
}
 
$result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
} else {
// 저장소 변경일 경우 해당 저장소에 Upload 진행
$boardAct = new BoardAct($req);
 
Controller/Mobile/Board/BoardPsController.php+8-1
변경 전BoardPsController.php
// 기본 저장소 인지 확인
if (Board::isDefaultUploadStorage($req['bdId'])) {
// 기본 저장소일 경우 OBS Upload 진행
$result = (new ImageUploadService())->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
 
 
 
 
 
 
 
} else {
// 저장소 변경일 경우 해당 저장소에 Upload 진행
$boardAct = new BoardAct($req);
 
변경 후BoardPsController.php
// 기본 저장소 인지 확인
if (Board::isDefaultUploadStorage($req['bdId'])) {
// 기본 저장소일 경우 OBS Upload 진행
$imageUploadService = new ImageUploadService();
 
// 확장자 검증 (저장소 변경 경로의 uploadAjax 와 동일하게 차단)
if ($imageUploadService->isAllowUploadExtention($fileData['name']) === false) {
throw new AlertBackException('업로드 할 수 없는 파일입니다.');
}
 
$result = $imageUploadService->uploadImage($fileData, '/temp', true, $boardConfig->cfg['bdUploadMaxSize']);
} else {
// 저장소 변경일 경우 해당 저장소에 Upload 진행
$boardAct = new BoardAct($req);
 
해외구매대행 전자상거래 업체부호 설정 기능 추가
Admin/policy/base_info.php+95-86
변경 전base_info.php
<form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="mode" value="base_info"/>
<input type="hidden" name="mallFaviconTmp" value="<?php echo $data['mallFavicon']; ?>"/>
<input type="hidden" name="stampImageTmp" value="<?php echo $data['stampImage'];?>" />
<input type="hidden" name="receiptImageTmp" value="<?php echo $data['receiptImage'];?>" />
<input type="hidden" name="mallSno" value="<?php echo $mallSno;?>" />
<input type="hidden" name="mallFl" value="<?php echo $mallFl;?>" />
 
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?>
<small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small>
</h3>
<input type="submit" value="저장" class="btn btn-red"/>
<?php if ($mallCnt > 1) { ?>
<ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;">
<?php foreach ($mallList as $key => $mall) { ?>
<li role="presentation" class="js-popover <?php echo $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?php echo $mall['mallName']; ?>" data-placement="top">
<a href="./base_info.php?mallSno=<?php echo $mall['sno']; ?>">
<span class="flag flag-16 flag-<?php echo $mall['domainFl']?>"></span>
<span class="mall-name"><?php echo $mall['mallName']; ?></span>
</a>
</li>
<?php } ?>
<tr>
<th>쇼핑몰명</th>
<td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>>
<input type="text" name="mallNm" value="<?php echo $data['mallNm']; ?>" class="form-control width-lg"/>
</td>
<th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th>
<td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>">
<input type="text" name="mallNmEng" value="<?php echo $data['mallNmEng']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
</tr>
<tr>
<th>상단타이틀</th>
<td class="form-inline">
<input type="text" name="mallTitle" value="<?php echo $data['mallTitle']; ?>" class="form-control width-lg"/>
</td>
<th>파비콘</th>
<td class="form-inline">
<input type="file" name="mallFavicon" <?php echo $disabled; ?> class="form-control"/>
<span>
<?php
if (empty($data['mallFavicon']) === false) {
<th class="require">쇼핑몰 도메인</th>
<td colspan="3" class="form-inline">
<span class="font-eng">http://</span>
<input type="text" name="mallDomain" value="<?php echo $data['mallDomain']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-2xl"/>
<input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm">
<?php if ($mallFl == 'kr'): ?>
<input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm">
<?php else: ?>
<input type="button" value="도메인 연결/관리" onclick="window.location.href='<?php echo $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm">
<?php endif; ?>
<div class="notice-info">
입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br />
실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?php echo $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a>
</div>
</td>
</tr>
<tr>
<th class="require">대표카테고리</th>
<td colspan="3" class="form-inline">
<input type="hidden" name="mallCategory" value="<?php echo $data['mallCategory']; ?>" <?php echo $readonly . $disabled; ?>/>
<span id="mallCategory"><?php echo $data['mallCategory']; ?></span>
<input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?php echo $disabled; ?> class="btn btn-gray btn-sm"/>
</td>
</tr>
</table>
<tr>
<th>상호(회사명)</th>
<td class="form-inline">
<input type="text" name="companyNm" value="<?php echo $data['companyNm']; ?>" class="form-control width-md"/>
</td>
<th>사업자등록번호</th>
<td class="form-inline">
<div>
<input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][0]; ?>" <?php echo $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> -
<input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][1]; ?>" <?php echo $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> -
<input type="text" name="businessNo[]" value="<?php echo $data['businessNo'][2]; ?>" <?php echo $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/>
</div>
<div class="notice-info">
인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다.
<tr>
<th>대표자명</th>
<td colspan="3" class="form-inline">
<input type="text" name="ceoNm" value="<?php echo $data['ceoNm']; ?>" class="form-control width-lg"/>
</td>
</tr>
<tr>
<th>업태</th>
<td class="form-inline">
<input type="text" name="service" value="<?php echo $data['service']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
<th>종목</th>
<td class="form-inline">
<input type="text" name="item" value="<?php echo $data['item']; ?>" <?php echo $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
</tr>
<tr>
<?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?>
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?php echo gd_isset($checked['email']['default']); ?> /> 기본 이메일
</label>
<label class="radio-inline">
<input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?php echo gd_isset($checked['email']['manual']); ?> /> 직접 입력
</label>
</div><br>
<?php } ?>
 
<input type="text" id="emailId" name="email[]" value="<?php echo $data['email'][0]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @
<input type="text" id="email" name="email[]" value="<?php echo $data['email'][1]; ?>" <?php echo gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/>
 
<div id="email_domain_y" class="display-inline-block">
<?php echo gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?>
</div>
<div id="email_domain_n"></div>
 
<li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li>
<li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li>
<li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다.
<a class="btn-link" href="<?php echo $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a>
</li>
</ul>
</div>
<th>사업장 주소</th>
<td colspan="3">
<div class="form-inline mgb5">
<input type="text" name="zonecode" value="<?php echo $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/>
<input type="hidden" name="zipcode" value="<?php echo $data['zipcode']; ?>"/>
<span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?php echo $data['zipcode']; ?>)</span>
<input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/>
</div>
<div class="form-inline">
<input type="text" name="address" value="<?php echo $data['address']; ?>" class="form-control width-2xl"/>
<input type="text" name="addressSub" value="<?php echo $data['addressSub']; ?>" class="form-control width-2xl"/>
</div>
</td>
</tr>
<td colspan="3">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?php echo gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?php echo gd_isset($checked['unstoringFl']['new']); ?> />주소 등록
</label>
<label class="radio-inline">
<input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/>
<div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>">
<?php foreach ($data['unstoringInfo'] as $k => $v) { ?>
<?php if ($v['addressFl'] == 'unstoring') { ?>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?php echo $data['unstoringInfo'][$k]['sno'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?php echo $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?php echo $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?php echo $data['unstoringInfo'][$k]['mainFl'] ?>"/>
<table class="table table-cols" value="<?= $v['sno'] ?>">
<colgroup>
<col class="width-md"/>
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?php echo gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?php echo gd_isset($checked['returnFl']['new']); ?> />주소 등록
</label>
<label class="radio-inline">
<input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/>
<div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>">
<?php foreach ($data['returnInfo'] as $k => $v) { ?>
<?php if ($v['addressFl'] == 'return') { ?>
<input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?php echo $data['returnInfo'][$k]['sno'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?php echo $data['returnInfo'][$k]['unstoringZonecode'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?php echo $data['returnInfo'][$k]['unstoringZipcode'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?php echo $data['returnInfo'][$k]['unstoringAddress'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?php echo $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?php echo $data['returnInfo'][$k]['mainFl'] ?>"/>
<table class="table table-cols" value="<?= $v['sno'] ?>">
<colgroup>
<col class="width-md"/>
<tr>
<th>대표전화</th>
<td class="form-inline">
<input type="text" name="phone" value="<?php echo $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</td>
<th>팩스번호</th>
<td class="form-inline">
<input type="text" name="fax" value="<?php echo $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</td>
</tr>
<tr>
<th>통신판매신고번호</th>
<td colspan="3" class="form-inline">
<input type="text" name="onlineOrderSerial" value="<?php echo $data['onlineOrderSerial']; ?>" class="form-control width-sm"/>
 
 
 
 
 
 
 
 
 
</td>
</tr>
<tr>
<th>인감 이미지 등록</th>
<td colspan="3" class="form-inline">
<input type="file" name="stampImage" <?php echo $disabled; ?> class="form-control"/>
<span>
<?php
if (empty($data['stampImage']) === false) {
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?php echo gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?php echo gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?php echo gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?php echo gd_isset($checked['receiptFl']['n']); ?> />노출 안함
</label>
</div>
<div id="receiptFl_default" class="mgt10">
<img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점">
</div>
<div id="receiptFl_upload" class="mgt10">
<input type="file" name="receiptImageFile" <?php echo $disabled; ?> class="form-control"/>
<div>
<?php
if (empty($data['receiptImage']) === false) {
echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerPhone" value="<?php echo $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</div>
<div>
<?php
echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerSubPhone" value="<?php echo $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/>
</div>
</td>
<th>팩스번호</th>
echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerFax" value="<?php echo $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/>
</td>
</tr>
<tr>
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?php echo gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일
</label>
<label class="radio-inline">
<input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?php echo gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력
</label>
</div><br>
 
<input type="text" id="centerEmailId" name="centerEmail[]" value="<?php echo $data['centerEmail'][0]; ?>" class="form-control width-lg" <?php echo gd_isset($deactivated['centerEmail']); ?>/> @
<input type="text" id="centerEmail" name="centerEmail[]" value="<?php echo $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?php echo gd_isset($deactivated['centerEmail']); ?> />
 
<div id="centerEmail_domain_y" class="display-inline-block">
<?php echo gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?>
</div>
<div id="centerEmail_domain_n"></div>
</td>
<tr>
<th>운영시간</th>
<td colspan="3" class="form-inline">
<textarea name="centerHours" rows="4" class="form-control width-2xl"><?php echo $data['centerHours']; ?></textarea>
</td>
</tr>
</table>
<tr>
<th>회사소개 내용</th>
<td class="form-inline">
<textarea name="company" id="editor" class="form-control width100p height400"><?php echo $data['company']; ?></textarea>
</td>
</tr>
</table>
<script type="text/javascript">
<!--
 
var unstoringNo = <?php echo json_encode($data['unstoringInfo']) ?>;
var returnNo = <?php echo json_encode($data['returnInfo']) ?>;
var $mallName = '<?= $mallName ?>';
var $mallFl = '<?php echo $mallFl; ?>';
var unstoringFl = '<?= empty($data['unstoringInfo']) ?>';
var returnFl = '<?= empty($data['returnInfo']) ?>';
var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>;
}
});
 
display_toggle('unstoringFl','<?php echo gd_isset($data['unstoringFl']);?>');
display_toggle('returnFl','<?php echo gd_isset($data['returnFl']);?>');
display_toggle('robotsTxt','<?php echo gd_isset($data['robotsFl']);?>');
display_toggle('receiptFl','<?php echo gd_isset($data['receiptFl']);?>');
display_toggle_inline('email_domain', '<?php echo gd_isset($data['emailFl']);?>');
display_toggle_inline('centerEmail_domain', '<?php echo gd_isset($data['centerEmailFl']);?>');
 
$('.email_domain').change(function () {
var val = $(this).val() == 'self' ? '' : $(this).val();
'unstoringNo' : addressNo,
'mallName' : $mallName,
'mallFl' : $mallFl,
'unstoringFl' : '<?php echo gd_isset($data['unstoringFl']);?>',
'returnFl' : '<?php echo gd_isset($data['returnFl']);?>',
'mallSno' : '<?php echo $mallSno;?>'
};
 
$.ajax({
 
변경 후base_info.php
<form id="frmBase" action="base_ps.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="mode" value="base_info"/>
<input type="hidden" name="mallFaviconTmp" value="<?= $data['mallFavicon']; ?>"/>
<input type="hidden" name="stampImageTmp" value="<?= $data['stampImage'];?>" />
<input type="hidden" name="receiptImageTmp" value="<?= $data['receiptImage'];?>" />
<input type="hidden" name="mallSno" value="<?= $mallSno;?>" />
<input type="hidden" name="mallFl" value="<?= $mallFl;?>" />
 
<div class="page-header js-affix">
<h3><?= end($naviMenu->location); ?>
<small>쇼핑몰의 기본적인 정보를 변경하실 수 있습니다.</small>
</h3>
<input type="submit" value="저장" class="btn btn-red"/>
<?php if ($mallCnt > 1) { ?>
<ul class="multi-skin-nav nav nav-tabs" style="margin-bottom:20px;">
<?php foreach ($mallList as $key => $mall) { ?>
<li role="presentation" class="js-popover <?= $mallSno == $mall['sno'] ? 'active' : 'passive'; ?>" data-html="true" data-content="<?= $mall['mallName']; ?>" data-placement="top">
<a href="./base_info.php?mallSno=<?= $mall['sno']; ?>">
<span class="flag flag-16 flag-<?= $mall['domainFl']?>"></span>
<span class="mall-name"><?= $mall['mallName']; ?></span>
</a>
</li>
<?php } ?>
<tr>
<th>쇼핑몰명</th>
<td class="form-inline" <?php if ($mallSno > 1) { ?>colspan="3"<?php } ?>>
<input type="text" name="mallNm" value="<?= $data['mallNm']; ?>" class="form-control width-lg"/>
</td>
<th class="<?php if ($mallSno > 1) { ?>display-none<?php } ?>">쇼핑몰영문명</th>
<td class="form-inline <?php if ($mallSno > 1) { ?>display-none<?php } ?>">
<input type="text" name="mallNmEng" value="<?= $data['mallNmEng']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
</tr>
<tr>
<th>상단타이틀</th>
<td class="form-inline">
<input type="text" name="mallTitle" value="<?= $data['mallTitle']; ?>" class="form-control width-lg"/>
</td>
<th>파비콘</th>
<td class="form-inline">
<input type="file" name="mallFavicon" <?= $disabled; ?> class="form-control"/>
<span>
<?php
if (empty($data['mallFavicon']) === false) {
<th class="require">쇼핑몰 도메인</th>
<td colspan="3" class="form-inline">
<span class="font-eng">http://</span>
<input type="text" name="mallDomain" value="<?= $data['mallDomain']; ?>" <?= $readonly . $disabled; ?> class="form-control width-2xl"/>
<input type="button" value="도메인 신청하기" onclick="moveToExternalDomainUrl('intro');" class="btn btn-gray btn-sm">
<?php if ($mallFl == 'kr'): ?>
<input type="button" value="도메인 연결/관리" onclick="moveToExternalDomainUrl('manage');" class="btn btn-gray btn-sm">
<?php else: ?>
<input type="button" value="도메인 연결/관리" onclick="window.location.href='<?= $domainUrlList['manage']; ?>'" class="btn btn-gray btn-sm">
<?php endif; ?>
<div class="notice-info">
입력 시 쇼핑몰 및 관리자 화면에 도메인 정보가 노출됩니다. <br />
실제 쇼핑몰 접속 도메인의 추가 및 변경은 [도메인 연결/관리] 버튼을 통해서 가능합니다. <a class="btn-link" href="<?= $domainUrlList['guide']; ?>" target="_blank">도움말 바로가기></a>
</div>
</td>
</tr>
<tr>
<th class="require">대표카테고리</th>
<td colspan="3" class="form-inline">
<input type="hidden" name="mallCategory" value="<?= $data['mallCategory']; ?>" <?= $readonly . $disabled; ?>/>
<span id="mallCategory"><?= $data['mallCategory']; ?></span>
<input type="button" onclick="mall_categoty();" value="대표카테고리 선택" <?= $disabled; ?> class="btn btn-gray btn-sm"/>
</td>
</tr>
</table>
<tr>
<th>상호(회사명)</th>
<td class="form-inline">
<input type="text" name="companyNm" value="<?= $data['companyNm']; ?>" class="form-control width-md"/>
</td>
<th>사업자등록번호</th>
<td class="form-inline">
<div>
<input type="text" name="businessNo[]" value="<?= $data['businessNo'][0]; ?>" <?= $readonly . $disabled; ?> maxlength="3" class="form-control js-number-only width-3xs"/> -
<input type="text" name="businessNo[]" value="<?= $data['businessNo'][1]; ?>" <?= $readonly . $disabled; ?> maxlength="2" class="form-control js-number-only width-3xs"/> -
<input type="text" name="businessNo[]" value="<?= $data['businessNo'][2]; ?>" <?= $readonly . $disabled; ?> maxlength="5" class="form-control js-number-only width-3xs"/>
</div>
<div class="notice-info">
인터넷 쇼핑몰 운영자는 전자상거래법에 의해 사업자정보 공개페이지를 쇼핑몰 홈페이지 초기 화면에 연결해야 합니다.
<tr>
<th>대표자명</th>
<td colspan="3" class="form-inline">
<input type="text" name="ceoNm" value="<?= $data['ceoNm']; ?>" class="form-control width-lg"/>
</td>
</tr>
<tr>
<th>업태</th>
<td class="form-inline">
<input type="text" name="service" value="<?= $data['service']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
<th>종목</th>
<td class="form-inline">
<input type="text" name="item" value="<?= $data['item']; ?>" <?= $readonly . $disabled; ?> class="form-control width-lg"/>
</td>
</tr>
<tr>
<?php if (empty($mallSno) || $mallSno == DEFAULT_MALL_NUMBER) { ?>
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="emailDefaultFl" value="default" onclick="default_toggle_email_info('email', true);" <?= gd_isset($checked['email']['default']); ?> /> 기본 이메일
</label>
<label class="radio-inline">
<input type="radio" name="emailDefaultFl" value="manual" onclick="default_toggle_email_info('email', false);" <?= gd_isset($checked['email']['manual']); ?> /> 직접 입력
</label>
</div><br>
<?php } ?>
 
<input type="text" id="emailId" name="email[]" value="<?= $data['email'][0]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-lg"/> @
<input type="text" id="email" name="email[]" value="<?= $data['email'][1]; ?>" <?= gd_isset($deactivated['email']) . $disabled; ?> class="form-control width-md js-email-domain"/>
 
<div id="email_domain_y" class="display-inline-block">
<?= gd_select_box('email_domain', null, $emailDomain, null, $data['email'][1], null, $disabled, 'email_domain'); ?>
</div>
<div id="email_domain_n"></div>
 
<li>• 몰 생성 시 기본 이메일 주소로 설정됩니다.</li>
<li>• 25년 2월부터 네이버(naver) 이메일을 사용할 경우 메일 발송이 불가하오니 기본 이메일 사용을 권장합니다.</li>
<li>• 포탈(gmail, hanmail 등) 도메인으로 사용할 경우, 고객의 메일 수신이 차단될 수 있습니다.
<a class="btn-link" href="<?= $domainUrlList['guide_mail_cert']; ?>" target="_blank">메일 레코드 등록 가이드 보기 ></a>
</li>
</ul>
</div>
<th>사업장 주소</th>
<td colspan="3">
<div class="form-inline mgb5">
<input type="text" name="zonecode" value="<?= $data['zonecode']; ?>" maxlength="5" class="form-control width-2xs"/>
<input type="hidden" name="zipcode" value="<?= $data['zipcode']; ?>"/>
<span id="zipcodeText" class="number <?php if (strlen($data['zipcode']) != 7) { echo 'display-none'; } ?>">(<?= $data['zipcode']; ?>)</span>
<input type="button" onclick="postcode_search('zonecode', 'address', 'zipcode');" value="우편번호찾기" class="btn btn-gray btn-sm"/>
</div>
<div class="form-inline">
<input type="text" name="address" value="<?= $data['address']; ?>" class="form-control width-2xl"/>
<input type="text" name="addressSub" value="<?= $data['addressSub']; ?>" class="form-control width-2xl"/>
</div>
</td>
</tr>
<td colspan="3">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="unstoringFl" value="same" onclick="display_toggle('unstoringFl','same');" <?= gd_isset($checked['unstoringFl']['same']); ?> />사업장 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="unstoringFl" value="new" onclick="display_toggle('unstoringFl','new');" <?= gd_isset($checked['unstoringFl']['new']); ?> />주소 등록
</label>
<label class="radio-inline">
<input type="button" value="출고지 관리" class="btn btn-black btn-sm btn-unstoring" onclick="address_register('출고지', 'unstoringFl', 'unstoringFl_new', 'unstoringTable');"/>
<div class="form-inline mgt10 mgb5 unstoringTable <?= empty($data['unstoringInfo']) === false ? 'active' : '' ?>">
<?php foreach ($data['unstoringInfo'] as $k => $v) { ?>
<?php if ($v['addressFl'] == 'unstoring') { ?>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringNo]" value="<?= $data['unstoringInfo'][$k]['sno'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZonecode]" value="<?= $data['unstoringInfo'][$k]['unstoringZonecode'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringZipcode]" value="<?= $data['unstoringInfo'][$k]['unstoringZipcode'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddress]" value="<?= $data['unstoringInfo'][$k]['unstoringAddress'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][unstoringAddressSub]" value="<?= $data['unstoringInfo'][$k]['unstoringAddressSub'] ?>"/>
<input type="hidden" name="unstoringInfo[<?= $k ?>][mainFl]" value="<?= $data['unstoringInfo'][$k]['mainFl'] ?>"/>
<table class="table table-cols" value="<?= $v['sno'] ?>">
<colgroup>
<col class="width-md"/>
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="returnFl" value="same" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['same']); ?> />사업장 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="returnFl" value="unstoring" onclick="display_toggle('returnFl','same');" <?= gd_isset($checked['returnFl']['unstoring']); ?> />출고지 주소와 동일
</label>
<label class="radio-inline">
<input type="radio" name="returnFl" value="new" onclick="display_toggle('returnFl','new');" <?= gd_isset($checked['returnFl']['new']); ?> />주소 등록
</label>
<label class="radio-inline">
<input type="button" value="반품/교환지 관리" class="btn btn-black btn-sm btn-return" onclick="address_register('반품/교환지', 'returnFl', 'returnFl_new', 'returnTable');"/>
<div class="form-inline mgt10 mgb5 returnTable <?= empty($data['returnInfo']) === false ? 'active' : '' ?>">
<?php foreach ($data['returnInfo'] as $k => $v) { ?>
<?php if ($v['addressFl'] == 'return') { ?>
<input type="hidden" name="returnInfo[<?= $k ?>][returnNo]" value="<?= $data['returnInfo'][$k]['sno'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnZonecode]" value="<?= $data['returnInfo'][$k]['unstoringZonecode'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnZipcode]" value="<?= $data['returnInfo'][$k]['unstoringZipcode'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnAddress]" value="<?= $data['returnInfo'][$k]['unstoringAddress'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][returnAddressSub]" value="<?= $data['returnInfo'][$k]['unstoringAddressSub'] ?>"/>
<input type="hidden" name="returnInfo[<?= $k ?>][mainFl]" value="<?= $data['returnInfo'][$k]['mainFl'] ?>"/>
<table class="table table-cols" value="<?= $v['sno'] ?>">
<colgroup>
<col class="width-md"/>
<tr>
<th>대표전화</th>
<td class="form-inline">
<input type="text" name="phone" value="<?= $data['phone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</td>
<th>팩스번호</th>
<td class="form-inline">
<input type="text" name="fax" value="<?= $data['fax']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</td>
</tr>
<tr>
<th>통신판매신고번호</th>
<td colspan="3" class="form-inline">
<input type="text" name="onlineOrderSerial" value="<?= $data['onlineOrderSerial']; ?>" class="form-control width-sm"/>
</td>
</tr>
<tr>
<th>전자상거래업자 부호</th>
<td colspan="3" class="form-inline">
<input type="text" name="ecommerceBusinessCode" value="<?= $data['ecommerceBusinessCode']; ?>" <?= $readonly . $disabled; ?> maxlength="30" class="form-control width-sm"/>
<div class="notice-info">
전자상거래업자 부호는 관세청 UNI-PASS 에서 신청/발급받을 수 있습니다.
</div>
</td>
</tr>
<tr>
<th>인감 이미지 등록</th>
<td colspan="3" class="form-inline">
<input type="file" name="stampImage" <?= $disabled; ?> class="form-control"/>
<span>
<?php
if (empty($data['stampImage']) === false) {
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="receiptFl" value="default" onclick="display_toggle('receiptFl','default');" <?= gd_isset($checked['receiptFl']['default']); ?> />현금영수증 가맹점
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="liability" onclick="display_toggle('receiptFl','liability');" <?= gd_isset($checked['receiptFl']['liability']); ?> />현금영수증 의무발행 가맹점
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="upload" onclick="display_toggle('receiptFl','upload');" <?= gd_isset($checked['receiptFl']['upload']); ?> />노출 이미지 직접등록
</label>
<label class="radio-inline">
<input type="radio" name="receiptFl" value="n" onclick="display_toggle('receiptFl','n');" <?= gd_isset($checked['receiptFl']['n']); ?> />노출 안함
</label>
</div>
<div id="receiptFl_default" class="mgt10">
<img src="/admin/gd_share/img/logo_cash_receipt_liability.png" alt="현금영수증 의무발행 가맹점">
</div>
<div id="receiptFl_upload" class="mgt10">
<input type="file" name="receiptImageFile" <?= $disabled; ?> class="form-control"/>
<div>
<?php
if (empty($data['receiptImage']) === false) {
echo gd_select_box('centerPhoneHead', 'centerPhoneHead', $countryAddress, null, $data['centerPhoneHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerPhone" value="<?= $data['centerPhone']; ?>" maxlength="12" class="form-control js-number-only width-md"/>
</div>
<div>
<?php
echo gd_select_box('centerSubPhoneHead', 'centerSubPhoneHead', $countryAddress, null, $data['centerSubPhoneHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerSubPhone" value="<?= $data['centerSubPhone']; ?>" maxlength="15" class="form-control js-number-only width-md"/>
</div>
</td>
<th>팩스번호</th>
echo gd_select_box('centerFaxHead', 'centerFaxHead', $countryAddress, null, $data['centerFaxHead'], '==선택==', null, 'form-control');
}
?>
<input type="text" name="centerFax" value="<?= $data['centerFax']; ?>" maxlength="15" class="form-control js-number-only width-md"/>
</td>
</tr>
<tr>
<td colspan="3" class="form-inline">
<div class="form-inline">
<label class="radio-inline">
<input type="radio" name="centerEmailDefaultFl" value="default" onclick="default_toggle_email_info('centerEmail', true);" <?= gd_isset($checked['centerEmail']['default']); ?> /> 기본 이메일
</label>
<label class="radio-inline">
<input type="radio" name="centerEmailDefaultFl" value="manual" onclick="default_toggle_email_info('centerEmail', false);" <?= gd_isset($checked['centerEmail']['manual']); ?> /> 직접 입력
</label>
</div><br>
 
<input type="text" id="centerEmailId" name="centerEmail[]" value="<?= $data['centerEmail'][0]; ?>" class="form-control width-lg" <?= gd_isset($deactivated['centerEmail']); ?>/> @
<input type="text" id="centerEmail" name="centerEmail[]" value="<?= $data['centerEmail'][1]; ?>" class="form-control width-md js-email-domain" <?= gd_isset($deactivated['centerEmail']); ?> />
 
<div id="centerEmail_domain_y" class="display-inline-block">
<?= gd_select_box('center_email_domain', null, $emailDomain, null, $data['centerEmail'][1], null, null, 'email_domain'); ?>
</div>
<div id="centerEmail_domain_n"></div>
</td>
<tr>
<th>운영시간</th>
<td colspan="3" class="form-inline">
<textarea name="centerHours" rows="4" class="form-control width-2xl"><?= $data['centerHours']; ?></textarea>
</td>
</tr>
</table>
<tr>
<th>회사소개 내용</th>
<td class="form-inline">
<textarea name="company" id="editor" class="form-control width100p height400"><?= $data['company']; ?></textarea>
</td>
</tr>
</table>
<script type="text/javascript">
<!--
 
var unstoringNo = <?= json_encode($data['unstoringInfo']) ?>;
var returnNo = <?= json_encode($data['returnInfo']) ?>;
var $mallName = '<?= $mallName ?>';
var $mallFl = '<?= $mallFl; ?>';
var unstoringFl = '<?= empty($data['unstoringInfo']) ?>';
var returnFl = '<?= empty($data['returnInfo']) ?>';
var isOauthSuperManagerLoggedIn = <?= $isOauthSuperManagerLoggedIn ? 'true' : 'false'; ?>;
}
});
 
display_toggle('unstoringFl','<?= gd_isset($data['unstoringFl']);?>');
display_toggle('returnFl','<?= gd_isset($data['returnFl']);?>');
display_toggle('robotsTxt','<?= gd_isset($data['robotsFl']);?>');
display_toggle('receiptFl','<?= gd_isset($data['receiptFl']);?>');
display_toggle_inline('email_domain', '<?= gd_isset($data['emailFl']);?>');
display_toggle_inline('centerEmail_domain', '<?= gd_isset($data['centerEmailFl']);?>');
 
$('.email_domain').change(function () {
var val = $(this).val() == 'self' ? '' : $(this).val();
'unstoringNo' : addressNo,
'mallName' : $mallName,
'mallFl' : $mallFl,
'unstoringFl' : '<?= gd_isset($data['unstoringFl']);?>',
'returnFl' : '<?= gd_isset($data['returnFl']);?>',
'mallSno' : '<?= $mallSno;?>'
};
 
$.ajax({
 
Component/Agreement/BuyerInform.php+1-1
변경 전BuyerInform.php
 
// 저장할 필드
//@formatter:off
$arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',];
//@formatter:on
 
// 데이터 조합
 
변경 후BuyerInform.php
 
// 저장할 필드
//@formatter:off
$arrSaveData = ['companyNm', 'businessNo', 'service', 'item', 'email', 'zonecode', 'zipcode', 'address', 'addressSub', 'phone', 'fax', 'onlineOrderSerial', 'ecommerceBusinessCode', 'unstoringZonecode', 'unstoringZipcode', 'unstoringAddress', 'unstoringAddressSub', 'returnZonecode', 'returnZipcode', 'returnAddress', 'returnAddressSub',];
//@formatter:on
 
// 데이터 조합
 
Component/Database/DBTableField.php+1
변경 전DBTableField.php
['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명
['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지
['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호
 
['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태
['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목
['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자
 
변경 후DBTableField.php
['val' => 'businessNm', 'typ' => 's', 'def' => null, 'name' => '상호명'], // 상호명
['val' => 'businessLicenseImage', 'typ' => 's', 'def' => null, 'name' => '사업자등록증이미지'], // 사업자등록증이미지
['val' => 'onlineOrderSerial', 'typ' => 's', 'def' => null, 'name' => '통신판매 신고번호'], // 통신 판매 신고 번호
['val' => 'ecommerceBusinessCode', 'typ' => 's', 'def' => null, 'name' => '전자상거래업체부호'], // 전자상거래업체부호
['val' => 'service', 'typ' => 's', 'def' => null, 'name' => '업태'], // 업태
['val' => 'item', 'typ' => 's', 'def' => null, 'name' => '종목'], // 종목
['val' => 'ceoNm', 'typ' => 's', 'def' => null, 'name' => '대표자'], // 대표자
 
Component/Mall/Mall.php+1
변경 전Mall.php
'item',
'email',
'stampImage',
 
];
/** 설정 공통 사용 **/
const GLOBAL_MALL_SHARE = [
 
변경 후Mall.php
'item',
'email',
'stampImage',
'ecommerceBusinessCode',
];
/** 설정 공통 사용 **/
const GLOBAL_MALL_SHARE = [
 
Component/Policy/Policy.php+1
변경 전Policy.php
'phone',
'fax',
'onlineOrderSerial',
 
'unstoringNo',
'unstoringZonecode',
'unstoringZipcode',
 
변경 후Policy.php
'phone',
'fax',
'onlineOrderSerial',
'ecommerceBusinessCode',
'unstoringNo',
'unstoringZonecode',
'unstoringZipcode',
 
상품 리스트 복사 시 이미지 보호 기능 개선.
Component/Goods/GoodsAdmin.php+67-46
변경 전GoodsAdmin.php
 
// 상품이미지 > obs 카프카 컨슈머 처리 start (상품복사)
if($arrData['applyGoodsCopy'] && in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) {
 
 
 
$copiedGoodsData = ['goodsNo' => $arrData['applyNo'], 'newGoodsNo' => $arrData['goodsNo'], 'convertedType' => 'copy'];
$kafka = new ProducerUtils();
$result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($copiedGoodsData, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', [$result, $copiedGoodsData]);
if ($result['result'] == 'success') {
// originGoodsNo, newGoodsNo obsConvertFl = N 업데이트 필요
$goodsObsConvertStatus->updateObsConvertFl($arrData['applyNo']);
$goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo']);
}
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
 
if (!empty($goodsImageMigrationLog)) {
$goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제
 
$kafka = new ProducerUtils();
$result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($goodsImageMigrationLog, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', [$result, $goodsImageMigrationLog]);
if ($result['result'] == 'success') {
// goodsNo obsConvertFl = N 업데이트 필요
$goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo'], $goodsImageMigrationLog);
}
}
}
// 상품아이콘 > obs 카프카 컨슈머 처리 end
 
// URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함)
if (in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) {
if ($arrData['applyGoodsCopy']) {
$goodsObsConvertStatus->setObsConvertFlagForUrlImages($arrData['applyNo']);
if ($extraArrData['callBy'] == OPEN_API) {
$goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제
 
$kafka = new ProducerUtils();
$result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE_BY_OPEN_API, $kafka->makeData($goodsImageMigrationLog, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', [$result, 'openAPI', $goodsImageMigrationLog]);
if ($result['result'] == 'success') {
// goodsNo obsConvertFl = N 업데이트 필요
$goodsObsConvertStatus->updateObsConvertFl($this->goodsNo, $goodsImageMigrationLog);
}
}
 
// URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함)
$goodsObsConvertStatus->setObsConvertFlagForUrlImages($this->goodsNo);
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
return $fbImageInfo;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 상품 복사
*
$addLogData = $goodsNo . ' -> ' . $newGoodsNo . ' 상품 복사' . chr(10);
LogHandler::wholeLog('goods', null, 'copy', $newGoodsNo, $data['goodsNm'], $addLogData);
 
// 상품이미지 > obs 카프카 컨슈머 처리 start
if(in_array($data['imageStorage'], self::DEFAULT_STORAGE)) {
$copiedGoodsData = ['goodsNo' => $goodsNo, 'newGoodsNo' => $newGoodsNo, 'convertedType' => 'copy'];
$kafka = new ProducerUtils();
$result = $kafka->send($kafka::TOPIC_CONVERTED_GOODS_IMAGE, $kafka->makeData($copiedGoodsData, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', [$result, $copiedGoodsData]);
if ($result['result'] == 'success') {
// originGoodsNo, newGoodsNo obsConvertFl = N 업데이트 필요
$goodsObsConvertStatus = \App::load('\\Component\\Goods\\GoodsObsConvertStatus');
$goodsObsConvertStatus->updateObsConvertFl($goodsNo);
$goodsObsConvertStatus->updateObsConvertFl($newGoodsNo);
}
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
 
// URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함)
if (in_array($data['imageStorage'], self::DEFAULT_STORAGE)) {
$goodsObsConvertStatusForUrl = \App::load('\\Component\\Goods\\GoodsObsConvertStatus');
$goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($goodsNo);
$goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($newGoodsNo);
}
 
// --- 이미지 복사 처리
if($data['imageStorage'] !='url') {
if ($data['imageStorage'] == 'obs') {
}
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
return $newGoodsNo;
}
 
 
변경 후GoodsAdmin.php
 
// 상품이미지 > obs 카프카 컨슈머 처리 start (상품복사)
if($arrData['applyGoodsCopy'] && in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) {
// 신규 상품을 변환 대기(obsConvertFl='n')로 표시
$goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo']);
 
$copiedGoodsData = ['goodsNo' => $arrData['applyNo'], 'newGoodsNo' => $arrData['goodsNo'], 'convertedType' => 'copy'];
$this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $copiedGoodsData, $arrData['goodsNo'], '상품복사 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)');
 
 
 
 
 
 
 
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
 
if (!empty($goodsImageMigrationLog)) {
$goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제
 
// 상품 이미지를 변환 대기(obsConvertFl='n')로 표시
$goodsObsConvertStatus->updateObsConvertFl($arrData['goodsNo'], $goodsImageMigrationLog);
 
$this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $goodsImageMigrationLog, $arrData['goodsNo'], '상품이미지 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)');
 
 
 
}
}
// 상품아이콘 > obs 카프카 컨슈머 처리 end
 
// URL 이미지는 변환 대상이 아니므로 'y'로 고정
if (in_array($arrData['imageStorage'], self::DEFAULT_STORAGE)) {
if ($arrData['applyGoodsCopy']) {
$goodsObsConvertStatus->setObsConvertFlagForUrlImages($arrData['applyNo']);
if ($extraArrData['callBy'] == OPEN_API) {
$goodsImageMigrationLog['convertedType'] = 'manage'; // 상품이미지 등록,수정,삭제
 
// 상품 이미지를 변환 대기(obsConvertFl='n')로 표시
$goodsObsConvertStatus->updateObsConvertFl($this->goodsNo, $goodsImageMigrationLog);
 
$this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE_BY_OPEN_API, $goodsImageMigrationLog, $this->goodsNo, '상품이미지 OBS 토픽 발행 실패(openAPI) - obsConvertFl 해제(fail-open)', ['openAPI']);
 
 
 
}
 
// URL 이미지는 변환 대상이 아니므로 'y'로 고정
$goodsObsConvertStatus->setObsConvertFlagForUrlImages($this->goodsNo);
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
return $fbImageInfo;
}
 
/**
* OBS 이미지 변환 토픽 발행 + 발행 실패 시 fail-open 처리
*
* 발행에 실패(예외 발생 또는 비성공 응답)하면 obsConvertFl을 'y'로 되돌려
* 이미지가 '변환 대기(편집 잠금)' 상태로 고착되는 것을 방지한다.
* 잠금(updateObsConvertFl) 자체는 발행 대상/시점이 달라 호출부에서 수행한다.
*
* @param \Component\Goods\GoodsObsConvertStatus $goodsObsConvertStatus 상태 처리 컴포넌트
* @param string $topic 발행 토픽
* @param array $data 발행 데이터(makeData 입력)
* @param mixed $unlockGoodsNo 발행 실패 시 잠금 해제할 상품번호
* @param string $failMessage 발행 실패 시 남길 로그 메시지
* @param array $infoLogContext info 로그에 추가로 남길 항목(예: ['openAPI'])
* @return void
*/
private function publishObsConvertTopic($goodsObsConvertStatus, $topic, array $data, $unlockGoodsNo, $failMessage, array $infoLogContext = [])
{
$obsPublishOk = false;
$obsPublishError = '';
try {
$kafka = new ProducerUtils();
$result = $kafka->send($topic, $kafka->makeData($data, 'cgi'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge([$result], $infoLogContext, [$data]));
$obsPublishOk = (isset($result['result']) && $result['result'] === 'success');
} catch (\Throwable $obsPublishException) {
$obsPublishError = $obsPublishException->getMessage();
}
 
if (!$obsPublishOk) {
// 발행 실패 → obsConvertFl 'y'로 재변환
// 미변환 시, 이미지 변경 불가능 상태로 고착화됨
$goodsObsConvertStatus->unlockObsConvertFl($unlockGoodsNo);
\Logger::channel('kafka')->warning($failMessage, [$unlockGoodsNo, $obsPublishError]);
}
}
 
/**
* 상품 복사
*
$addLogData = $goodsNo . ' -> ' . $newGoodsNo . ' 상품 복사' . chr(10);
LogHandler::wholeLog('goods', null, 'copy', $newGoodsNo, $data['goodsNm'], $addLogData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
// --- 이미지 복사 처리
if($data['imageStorage'] !='url') {
if ($data['imageStorage'] == 'obs') {
}
}
}
 
if (in_array($data['imageStorage'], self::DEFAULT_STORAGE)) {
// 신규 상품을 변환 대기(obsConvertFl='n')로 표시
$goodsObsConvertStatus = \App::load('\\Component\\Goods\\GoodsObsConvertStatus');
$goodsObsConvertStatus->updateObsConvertFl($newGoodsNo);
 
$copiedGoodsData = ['goodsNo' => $goodsNo, 'newGoodsNo' => $newGoodsNo, 'convertedType' => 'copy'];
$this->publishObsConvertTopic($goodsObsConvertStatus, ProducerUtils::TOPIC_CONVERTED_GOODS_IMAGE, $copiedGoodsData, $newGoodsNo, '상품복사 OBS 토픽 발행 실패 - obsConvertFl 해제(fail-open)');
}
// 상품이미지 > obs 카프카 컨슈머 처리 end
 
// URL 이미지는 OBS 변환 대상 아님 → 'y' 고정 (legacy 'n' 잠금 해제 포함)
if (in_array($data['imageStorage'], self::DEFAULT_STORAGE)) {
$goodsObsConvertStatusForUrl = \App::load('\\Component\\Goods\\GoodsObsConvertStatus');
$goodsObsConvertStatusForUrl->setObsConvertFlagForUrlImages($newGoodsNo);
}
 
return $newGoodsNo;
}
 
 
Component/Goods/GoodsObsConvertStatus.php+20
변경 전GoodsObsConvertStatus.php
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
$this->db->bind_query($strSQL, $arrBind);
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후GoodsObsConvertStatus.php
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
$this->db->bind_query($strSQL, $arrBind);
}
 
/**
* 상품번호를 기준으로 상품 이미지의 변환 대기 표시(obsConvertFl='n')를 풀어줌('y').
*/
public function unlockObsConvertFl(int $goodsNo)
{
// 상품이미지
$arrBind = [];
$strSQL = 'UPDATE ' . DB_GOODS_IMAGE . ' SET obsConvertFl = ? WHERE goodsNo = ? AND imageName NOT LIKE "http%" ';
$this->db->bind_param_push($arrBind, 's', 'y');
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
$this->db->bind_query($strSQL, $arrBind);
 
// 상품옵션이미지
$arrBind = [];
$strSQL = 'UPDATE ' . DB_GOODS_OPTION_ICON . ' SET obsConvertFl = ? WHERE goodsNo = ? AND goodsImage NOT LIKE "http%" ';
$this->db->bind_param_push($arrBind, 's', 'y');
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
$this->db->bind_query($strSQL, $arrBind);
}
}
 
수기주문에서 회원 장바구니 상품 2개 이상 추가 안정성 개선
Controller/Admin/Order/OrderPsController.php+3-4
변경 전OrderPsController.php
$postValue = Request::post()->toArray();
 
try {
//회원의 장바구니 로드
$cart = new CartAdmin($postValue['memNo'], true);
$cartSaveInfo = array();
foreach($postValue['cartSno'] as $key => $cartIdx){
 
$cartInfo = array();
$cartInfo = $cart->getCartGoodsData($cartIdx);
if(count($cartInfo) > 0){
}
}
}
 
 
}
 
//order write cart 에 저장
$cart = null;
unset($cart);
if(count($cartSaveInfo) > 0){
$cart = new CartAdmin($postValue['memNo']);
$resultCartSno = $cart->saveInfoMemberCartAdmin($cartSaveInfo);
 
변경 후OrderPsController.php
$postValue = Request::post()->toArray();
 
try {
 
 
$cartSaveInfo = array();
foreach($postValue['cartSno'] as $key => $cartIdx){
$cart = new CartAdmin($postValue['memNo'], true);
$cartInfo = array();
$cartInfo = $cart->getCartGoodsData($cartIdx);
if(count($cartInfo) > 0){
}
}
}
$cart = null;
unset($cart);
}
 
//order write cart 에 저장
 
 
if(count($cartSaveInfo) > 0){
$cart = new CartAdmin($postValue['memNo']);
$resultCartSno = $cart->saveInfoMemberCartAdmin($cartSaveInfo);
 
DB 데드락 자동 복구 및 발송/장바구니 교착 완화
Component/Cart/Cart.php+4
변경 전Cart.php
return false;
}
 
 
 
 
 
$arrBind = [];
foreach ($getData as $cartSno) {
// 장바구니 삭제 전 쿠폰 장바구니사용 상태 되돌리기
 
변경 후Cart.php
return false;
}
 
// [데드락 회피] sno IN 다건 삭제 시 PK 잠금 순서를 오름차순으로 통일
// 삭제 대상 집합은 동일하며(동작 불변), 동시 트랜잭션 간 PK 획득 순서만 일치시켜 교착 순환을 제거
sort($getData, SORT_NUMERIC);
 
$arrBind = [];
foreach ($getData as $cartSno) {
// 장바구니 삭제 전 쿠폰 장바구니사용 상태 되돌리기
 
Component/Mail/MailLog.php+21
변경 전MailLog.php
protected $arrWhere;
/** @var int es_mailLog sno */
private $_sno;
 
 
/** @var SimpleStorage */
private $requestStorage;
$arrBind = $this->db->get_binding(DBTableField::tableMailSendList(), $bindArray, 'insert');
$this->db->set_insert_db(DB_MAIL_SEND_LIST, $arrBind['param'], $arrBind['bind'], 'y');
 
 
unset($arrData);
}
*/
public function updateSendList($email)
{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
$this->db->bind_param_push($arrBind, 's', $this->_sno);
$this->db->bind_param_push($arrBind, 's', $email);
$this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'mailLogSno = ? AND receiverEmail = ? AND acceptCheckFl = \'n\' LIMIT 1', $arrBind);
 
변경 후MailLog.php
protected $arrWhere;
/** @var int es_mailLog sno */
private $_sno;
/** @var int|null 직전 insertMailSendList 로 INSERT 한 es_mailSendList 의 PK(sno). updateSendList PK 단건 갱신용 */
private $sendListSno = null;
/** @var SimpleStorage */
private $requestStorage;
$arrBind = $this->db->get_binding(DBTableField::tableMailSendList(), $bindArray, 'insert');
$this->db->set_insert_db(DB_MAIL_SEND_LIST, $arrBind['param'], $arrBind['bind'], 'y');
// 방금 INSERT 한 발송내역 PK 를 보관 → updateSendList 에서 PK 단건 갱신(데드락 회피)에 사용
$this->sendListSno = (int) $this->db->insert_id();
unset($arrData);
}
*/
public function updateSendList($email)
{
// [데드락 회피] 직전 insertMailSendList 로 확보한 PK(sno)가 있으면 PK 단건 UPDATE
// 보조인덱스(ix_es_mailSendList_05) 스캔을 제거해 잠금 순서를 PRIMARY 기준으로 통일
// acceptCheckFl='n' 가드를 유지해 멱등성을 보장(기존 동작과 동일하게 미접수 건만 갱신)
// 사용 후 즉시 초기화하여, INSERT 가 선행되지 않은 호출은 아래 기존 로직으로 처리
if (!empty($this->sendListSno)) {
$sendListSno = $this->sendListSno;
$this->sendListSno = null;
$arrBind = [];
$this->db->bind_param_push($arrBind, 'i', $sendListSno);
$this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'sno = ? AND acceptCheckFl = \'n\'', $arrBind);
return;
}
// (fallback) PK 미확보 호출 경로 — 기존 동작 100% 유지
$arrBind = [];
$this->db->bind_param_push($arrBind, 's', $this->_sno);
$this->db->bind_param_push($arrBind, 's', $email);
$this->db->set_update_db(DB_MAIL_SEND_LIST, 'acceptCheckFl = \'y\'', 'mailLogSno = ? AND receiverEmail = ? AND acceptCheckFl = \'n\' LIMIT 1', $arrBind);
 
Controller/Front/Order/CartPsController.php+15-5
변경 전CartPsController.php
}
// 처리별 이동 경로
if (gd_isset($postValue['cartMode']) == 'd') {
$goodsList = implode(',', $addedGoods);
$goodsListQuote = implode(',', $addedGoodsQuote);
//장바구니에서 보이지 않도록 조치 시작
$this->db = \App::load('DB');
$where = 'sno IN (\''.str_replace(',', '\',\'', $goodsList.'\')');
$arrUpdate['directCart'] = 'y';
$arrBind = $this->db->get_binding(DBTableField::tableCart(), $arrUpdate, 'update', array_keys($arrUpdate));
$this->db->set_update_db(DB_CART, $arrBind['param'], $where, $arrBind['bind']);
 
 
 
 
 
 
 
 
 
 
 
//장바구니에서 보이지 않도록 조치 종료
 
$goodsListQuote = '['.$goodsListQuote.']';
 
변경 후CartPsController.php
}
// 처리별 이동 경로
if (gd_isset($postValue['cartMode']) == 'd') {
 
$goodsListQuote = implode(',', $addedGoodsQuote);
//장바구니에서 보이지 않도록 조치 시작
$this->db = \App::load('DB');
// [데드락 회피] UPDATE WHERE 의 sno IN 잠금 순서만 오름차순 통일(대상 행 동일)
// URL(cartIdx) 노출 순서에는 영향을 주지 않도록 정렬 복사본을 별도 생성
$sortedAddedGoods = $addedGoods;
sort($sortedAddedGoods, SORT_NUMERIC);
 
if (!empty($sortedAddedGoods)) {
$arrUpdate['directCart'] = 'y';
$arrBind = $this->db->get_binding(DBTableField::tableCart(), $arrUpdate, 'update', array_keys($arrUpdate));
// sno IN 값은 바인딩으로 처리(SQL 인젝션 방지)
$where = 'sno IN (' . implode(', ', array_fill(0, count($sortedAddedGoods), '?')) . ')';
foreach ($sortedAddedGoods as $sno) {
$this->db->bind_param_push($arrBind['bind'], 'i', $sno);
}
$this->db->set_update_db(DB_CART, $arrBind['param'], $where, $arrBind['bind']);
}
//장바구니에서 보이지 않도록 조치 종료
 
$goodsListQuote = '['.$goodsListQuote.']';
 
모바일 상품 게시판 상세보기 조회 속도 개선
Component/Board/Board.php+45-5
변경 전Board.php
} else {
$extraData['arrGoodsNo'][] = $extraData['goodsNoText'];
}
foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) {
$extraData['arrGoodsData'][$key] = $goods->getGoodsInfo($goodsNo);
 
 
 
 
 
 
 
 
 
}
}
if ($extraData['orderGoodsNoText']) {
$extraData['arrOrderGoodsNo'][] = $extraData['orderGoodsNoText'];
}
$order = new Order();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
foreach ($extraData['arrOrderGoodsNo'] as $key => $orderGoodsNo) {
$isAddGoods = substr($orderGoodsNo, 0, 1) == 'A' ? true : false;
if ($isAddGoods) {
$orderGoodsNo = substr($orderGoodsNo, 1);
}
$orderGoodsData = $order->getOrderGoods(null, $orderGoodsNo, null, null, null)[0];
 
 
$extraData['arrOrderGoodsData'][$key] = $orderGoodsData;
$_arrOrderGoodsData = $extraData['arrOrderGoodsData'][$key];
$goodsNo = $_arrOrderGoodsData['goodsNo'];
$extraData['arrOrderGoodsData'][$key]['goodsNo'] = $orderGoodsData['parentGoodsNo'];
$extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($addGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']);
} else {
$goodsImage = $goods->getGoodsImage($goodsNo, 'main');
$goodsInfo = $goods->getGoodsInfo($goodsNo);
$goodsImageSrc = SkinUtils::imageViewStorageConfig($goodsImage[0]['goodsImageStorage'] == 'obs' ? $goodsImage[0]['imageUrl'] : $goodsImage[0]['imageName'], $goodsInfo['imagePath'], $goodsInfo['imageStorage'], 100, 'goods')[0];
$extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($_arrOrderGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']);
}
 
변경 후Board.php
} else {
$extraData['arrGoodsNo'][] = $extraData['goodsNoText'];
}
// N+1 쿼리 패턴 개선: 연관상품 개별 getGoodsInfo() 반복 -> IN절 배치 1회
// (user/module Goods 오버라이드가 배치 메서드 미상속 시 fatal 방지 가드)
if (method_exists($goods, 'getGoodsInfoByNos')) {
$batchGoodsData = $goods->getGoodsInfoByNos($extraData['arrGoodsNo']);
foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) {
$extraData['arrGoodsData'][$key] = $batchGoodsData[$goodsNo] ?? [];
}
} else {
foreach ($extraData['arrGoodsNo'] as $key => $goodsNo) {
$extraData['arrGoodsData'][$key] = $goods->getGoodsInfo($goodsNo);
}
}
}
if ($extraData['orderGoodsNoText']) {
$extraData['arrOrderGoodsNo'][] = $extraData['orderGoodsNoText'];
}
$order = new Order();
 
// N+1 쿼리 패턴 개선: 주문상품/이미지/정보 개별 반복 조회 -> 루프 진입 전 IN절 배치 선조회
// add-goods 의 getDataAddGoods 만 per-item 유지. user/module Goods 미상속 시 기존 per-item 폴백
$useBatch = method_exists($goods, 'getGoodsInfoByNos');
$orderGoodsMap = $goodsInfoMap = $goodsImageMap = [];
if ($useBatch) {
$snoList = [];
foreach ($extraData['arrOrderGoodsNo'] as $orderGoodsNo) {
$snoList[] = (substr($orderGoodsNo, 0, 1) == 'A') ? substr($orderGoodsNo, 1) : $orderGoodsNo;
}
$orderGoodsList = $order->getOrderGoods(null, $snoList, null, null, null);
if (is_array($orderGoodsList)) {
foreach ($orderGoodsList as $orderGoodsRow) {
$orderGoodsMap[$orderGoodsRow['sno']] = $orderGoodsRow;
}
}
$normalGoodsNos = [];
foreach ($extraData['arrOrderGoodsNo'] as $orderGoodsNo) {
if (substr($orderGoodsNo, 0, 1) == 'A') {
continue;
}
if (isset($orderGoodsMap[$orderGoodsNo]['goodsNo'])) {
$normalGoodsNos[] = $orderGoodsMap[$orderGoodsNo]['goodsNo'];
}
}
$goodsInfoMap = $goods->getGoodsInfoByNos($normalGoodsNos);
$goodsImageMap = $goods->getGoodsImageByNos($normalGoodsNos, 'main');
}
 
foreach ($extraData['arrOrderGoodsNo'] as $key => $orderGoodsNo) {
$isAddGoods = substr($orderGoodsNo, 0, 1) == 'A' ? true : false;
if ($isAddGoods) {
$orderGoodsNo = substr($orderGoodsNo, 1);
}
$orderGoodsData = $useBatch
? ($orderGoodsMap[$orderGoodsNo] ?? [])
: ($order->getOrderGoods(null, $orderGoodsNo, null, null, null)[0] ?? []);
$extraData['arrOrderGoodsData'][$key] = $orderGoodsData;
$_arrOrderGoodsData = $extraData['arrOrderGoodsData'][$key];
$goodsNo = $_arrOrderGoodsData['goodsNo'];
$extraData['arrOrderGoodsData'][$key]['goodsNo'] = $orderGoodsData['parentGoodsNo'];
$extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($addGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']);
} else {
$goodsImage = $useBatch ? ($goodsImageMap[$goodsNo] ?? false) : $goods->getGoodsImage($goodsNo, 'main');
$goodsInfo = $useBatch ? ($goodsInfoMap[$goodsNo] ?? []) : $goods->getGoodsInfo($goodsNo);
$goodsImageSrc = SkinUtils::imageViewStorageConfig($goodsImage[0]['goodsImageStorage'] == 'obs' ? $goodsImage[0]['imageUrl'] : $goodsImage[0]['imageName'], $goodsInfo['imagePath'], $goodsInfo['imageStorage'], 100, 'goods')[0];
$extraData['arrOrderGoodsData'][$key]['totalGoodsPrice'] = $_arrOrderGoodsData['goodsCnt'] * ($_arrOrderGoodsData['goodsPrice'] + $_arrOrderGoodsData['optionPrice'] + $_arrOrderGoodsData['optionTextPrice']);
}
 
Component/Goods/Goods.php+86
변경 전Goods.php
 
return $infoDataContent;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후Goods.php
 
return $infoDataContent;
}
 
/**
* 상품정보 배치 조회 — getGoodsInfo() 의 IN절 배치판 (게시판 상세보기 N+1 쿼리 패턴 개선)
*
* 단건 getGoodsInfo() 를 goodsNo 건수만큼 반복 호출하던 구간을 IN 절 1회 조회로 대체한다.
*
* 게시판 상세보기 배치 전용 내부 헬퍼. 호출부(Board::getExtraData)가 method_exists 가드로 미구현(튜닝 fork) 환경을 단건 경로로 폴백하므로 외부 튜닝 확장점이 아니다. 시그니처 안정 유지를 권장한다.
*
* @param array $arrGoodsNo 상품번호 배열
* @param string|null $goodsField 출력 필드 (기본 null = 단건 getGoodsInfo 와 동일 전체 조회). goodsNo 컬럼은 결과 맵의 키이므로 부분 필드 지정 시에도 반드시 포함할 것
* @return array goodsNo 를 키로 하는 상품정보 맵 [goodsNo => row]
*/
public function getGoodsInfoByNos(array $arrGoodsNo, $goodsField = null): array
{
$arrGoodsNo = array_values(array_unique(array_filter($arrGoodsNo)));
if (empty($arrGoodsNo)) {
return [];
}
 
$arrBind = null;
$bindQuery = [];
foreach ($arrGoodsNo as $goodsNo) {
$bindQuery[] = '?';
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
}
$inClause = ' g.goodsNo IN (' . implode(',', $bindQuery) . ') ';
if ($this->db->strWhere) {
$this->db->strWhere = $inClause . ' AND ' . $this->db->strWhere;
} else {
$this->db->strWhere = $inClause;
}
 
if ($goodsField) {
$this->db->strField = $this->db->strField ? $goodsField . ', ' . $this->db->strField : $goodsField;
}
 
$query = $this->db->query_complete();
$strSQL = 'SELECT ' . array_shift($query) . ' FROM ' . DB_GOODS . ' g ' . implode(' ', $query);
$getData = $this->db->secondary()->query_fetch($strSQL, $arrBind);
 
$goodsInfoMap = [];
foreach ((array) $getData as $row) {
$row = gd_htmlspecialchars_stripslashes($row);
$goodsInfoMap[$row['goodsNo']] = $row;
}
return $goodsInfoMap;
}
 
/**
* 상품이미지 배치 조회 — getGoodsImage() 의 IN절 배치판 (게시판 상세보기 N+1 쿼리 패턴 개선)
*
* 게시판 상세보기 배치 전용 내부 헬퍼. 호출부(Board::getExtraData)가 method_exists 가드로 미구현(튜닝 fork) 환경을 단건 경로로 폴백하므로 외부 튜닝 확장점이 아니다. 시그니처 안정 유지를 권장한다.
*
* @param array $arrGoodsNo 상품번호 배열
* @param string $imageKind 이미지 종류 (기본 'main')
* @return array goodsNo 를 키로 하는 이미지행 맵 [goodsNo => [row, ...]]
*/
public function getGoodsImageByNos(array $arrGoodsNo, $imageKind = 'main'): array
{
$arrGoodsNo = array_values(array_unique(array_filter($arrGoodsNo)));
if (empty($arrGoodsNo)) {
return [];
}
 
$arrBind = null;
$bindQuery = [];
foreach ($arrGoodsNo as $goodsNo) {
$bindQuery[] = '?';
$this->db->bind_param_push($arrBind, 'i', $goodsNo);
}
$this->db->bind_param_push($arrBind, 's', $imageKind);
 
// 단건 getGoodsImage 는 goodsNo 를 SELECT 에서 제외하나, 배치는 매핑 키로 필요 → goodsNo 추가
$arrField = DBTableField::setTableField('tableGoodsImage', null, 'goodsNo');
$strSQL = "SELECT goodsNo, sno, " . implode(', ', $arrField) . " FROM " . DB_GOODS_IMAGE
. " WHERE goodsNo IN (" . implode(',', $bindQuery) . ") AND imageKind = ? "
. " ORDER BY imageKind ASC, imageNo ASC";
$getData = $this->db->secondary()->query_fetch($strSQL, $arrBind);
 
$goodsImageMap = [];
foreach ((array) $getData as $row) {
$row = gd_htmlspecialchars_stripslashes($row);
$goodsImageMap[$row['goodsNo']][] = $row;
}
return $goodsImageMap;
}
}
 
비회원 주문 취소 복원 처리 개선
Component/Order/ReOrderCalculation.php+19-20
변경 전ReOrderCalculation.php
private function precheckRestoreAddPayment(string $orderNo, string $claimStatus): array
{
$original = $this->getOrderOriginalDepositMileageData($orderNo, $claimStatus);
$current = $this->getOrderDepositMileageData($orderNo);
 
// precheck 는 setOrderRestore 진입 직후(member 초기화 이전)에 호출되므로 직접 로드한다.
if (!is_object($this->member)) {
$this->member = \App::load('\\Component\\Member\\Member');
}
 
$memInfo = [];
if (is_array($original) && !empty($original['memNo'])) {
$memInfo = $this->member->getMemberInfo($original['memNo']);
}
 
$exceptMsg = [];
$checks = [
['field' => 'useDeposit', 'hold' => 'deposit', 'msg' => '고객이 보유한 예치금 부족'],
['field' => 'useMileage', 'hold' => 'mileage', 'msg' => '고객이 보유한 마일리지 부족'],
];
foreach ($checks as $check) {
$diff = gd_isset($current[$check['field']], 0) - gd_isset($original[$check['field']], 0);
if ($diff === 0) {
continue;
}
if (empty($memInfo) || (gd_isset($memInfo[$check['hold']], 0) - abs($diff) < 0)) {
$exceptMsg[] = $check['msg'];
 
 
 
 
 
 
 
 
 
 
 
 
}
}
 
 
변경 후ReOrderCalculation.php
private function precheckRestoreAddPayment(string $orderNo, string $claimStatus): array
{
$original = $this->getOrderOriginalDepositMileageData($orderNo, $claimStatus);
 
 
$exceptMsg = [];
 
 
 
 
 
if (is_array($original) && !empty($original['memNo'])) {
$current = $this->getOrderDepositMileageData($orderNo);
 
 
// precheck 는 setOrderRestore 진입 직후(member 초기화 이전)에 호출되므로 직접 로드한다.
if (!is_object($this->member)) {
$this->member = \App::load('\\Component\\Member\\Member');
 
 
 
 
 
 
}
 
$memInfo = $this->member->getMemberInfo($original['memNo']);
$checks = [
['field' => 'useDeposit', 'hold' => 'deposit', 'msg' => '고객이 보유한 예치금 부족'],
['field' => 'useMileage', 'hold' => 'mileage', 'msg' => '고객이 보유한 마일리지 부족'],
];
foreach ($checks as $check) {
$diff = ($current[$check['field']] ?? 0) - ($original[$check['field']] ?? 0);
if ($diff === 0) {
continue;
}
if (empty($memInfo) || (($memInfo[$check['hold']] ?? 0) - abs($diff) < 0)) {
$exceptMsg[] = $check['msg'];
}
}
}
 
 
관리자 주문 상세보기 성능 개선
Admin/order/order_view.php+32-1
변경 전order_view.php
if ($('#tabOrderStatus .nav-tabs li').hasClass('active')) {
$('#tabOrderStatus .nav-tabs li.active a').trigger('show.bs.tab');
}
 
 
 
 
 
 
 
 
 
if ($('#tabClaimStatus .nav-tabs li').hasClass('active')) {
$('#tabClaimStatus .nav-tabs li.active a').trigger('show.bs.tab');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
// 일괄 처리 모드의 체크
 
변경 후order_view.php
if ($('#tabOrderStatus .nav-tabs li').hasClass('active')) {
$('#tabOrderStatus .nav-tabs li.active a').trigger('show.bs.tab');
}
// 클레임 내역은 데이터가 있을 때만 초기 로드 (불필요한 inc_claim_view → getOrderView 호출 방지)
// 데이터가 없으면 초기 AJAX를 생략하고, 탭 클릭 시 기존 show.bs.tab 핸들러로 지연 로드된다.
var hasClaimData = false;
$('#tabClaimStatus .nav-tabs a strong').each(function () {
if (parseInt($(this).text().replace(/[^0-9]/g, ''), 10) > 0) {
hasClaimData = true;
return false;
}
});
if ($('#tabClaimStatus .nav-tabs li').hasClass('active')) {
var $activeClaimPane = $('#tabClaimStatus .tab-pane.active');
if (hasClaimData || $activeClaimPane.attr('id') !== 'tab-claim-cancel') {
// 클레임 데이터가 있거나, 활성 탭이 취소탭이 아닌 경우(공급사/교환·반품·환불 등) → 기존대로 AJAX 로드
$('#tabClaimStatus .nav-tabs li.active a').trigger('show.bs.tab');
} else {
// 클레임이 전혀 없고 활성 탭이 취소탭인 가장 흔한 케이스 → AJAX 없이 동일한 '취소정보가 없습니다' 빈 상태를 렌더
$('#tabClaimStatus .tab-content').removeClass('loading');
$activeClaimPane.html(
'<table class="table table-rows">'
+ '<thead><tr>'
+ '<th class="width3p">번호</th>'
+ '<th class="width5p">처리상태</th>'
+ '<th class="width8p">공급사</th>'
+ '<th class="width-3xs">이미지</th>'
+ '<th>상품</th>'
+ '<th class="width5p">취소수량</th>'
+ '<th class="width8p">취소사유</th>'
+ '<th class="width10p">상세사유</th>'
+ '</tr></thead>'
+ '<tbody><tr><td class="no-data" colspan="25">취소정보가 없습니다.</td></tr></tbody>'
+ '</table>'
);
}
}
 
// 일괄 처리 모드의 체크
 
Component/Order/OrderAdminNew.php+36-24
변경 전OrderAdminNew.php
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
 
 
use Request;
use Session;
use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader;
 
$deliveryUniqueKey = '';
$aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용
// 전체 정책별 배송비와 지역별 배송비의 총합 구하기
 
 
 
foreach ($getData['goods'] as $sKey => $sVal) {
foreach ($sVal as $dKey => $dVal) {
// $dVal = array_shift($dVal);
foreach ($dVal as $gKey => $gVal) {
$realDeliveryTaxSum = array_sum([
$gVal['realTaxSupplyDeliveryCharge'],
]);
// 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다
$deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno'];
if ($deliveryKeyCheck == $deliveryUniqueKey) {
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (!in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산
$aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']);
$aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']);
$getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
}
} else {
// 주문상세내 배송비 금액 합산
if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) {
$getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum;
if (in_array($gVal['orderStatus'], ['o1'])) {
$getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum;
}
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (!in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산
$aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']);
$aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']);
$getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
}
if (in_array($gVal['orderStatus'], ['r1', 'r2'])) {
$getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum;
}
// 배송무게 계산
$getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true);
}
 
 
 
 
 
 
 
 
 
 
$deliveryUniqueKey = $deliveryKeyCheck;
 
// 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
if (Manager::isProvider()) {
$getData = $this->getProviderTotalPrice($getData);
}
 
변경 후OrderAdminNew.php
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
use Repository\Order\OrderDeliveryOriginalRepository;
use Repository\Order\OrderHandleRepository;
use Request;
use Session;
use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader;
 
$deliveryUniqueKey = '';
$aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용
 
// 전체 정책별 배송비와 지역별 배송비의 총합 구하기 (DB 불필요 합산은 단일 순회로 처리, 취소행/sno는 수집만)
$cancelRows = [];
$handleSnoList = $deliveryOriginalSnoList = [];
foreach ($getData['goods'] as $sKey => $sVal) {
foreach ($sVal as $dKey => $dVal) {
 
foreach ($dVal as $gKey => $gVal) {
$realDeliveryTaxSum = array_sum([
$gVal['realTaxSupplyDeliveryCharge'],
]);
// 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다
$deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno'];
if ($deliveryKeyCheck != $deliveryUniqueKey) {
 
 
 
 
 
 
 
 
 
 
 
// 주문상세내 배송비 금액 합산
if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) {
$getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum;
if (in_array($gVal['orderStatus'], ['o1'])) {
$getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum;
}
 
 
 
 
 
 
 
 
 
 
if (in_array($gVal['orderStatus'], ['r1', 'r2'])) {
$getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum;
}
// 배송무게 계산
$getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true);
}
// 취소행은 기존에 배송키 분기와 무관하게 동일 처리됐으므로 모아두고, N+1 제거 위해 일괄조회 후 후처리
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$cancelRows[] = $gVal;
if ($gVal['handleSno'] > 0) {
$handleSnoList[] = $gVal['handleSno'];
}
if ($gVal['orderDeliverySno'] > 0) {
$deliveryOriginalSnoList[] = $gVal['orderDeliverySno'];
}
}
$deliveryUniqueKey = $deliveryKeyCheck;
 
// 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보
}
}
 
// 취소상태 배송행의 handleSno/orderDeliverySno 를 일괄 1회씩 조회 (N+1 제거)
$handleInfoMap = \App::getInstance(OrderHandleRepository::class)
->findMapBySnos(array_values(array_unique($handleSnoList)));
$deliveryOriginalMap = \App::getInstance(OrderDeliveryOriginalRepository::class)
->findMapBySnos(array_values(array_unique($deliveryOriginalSnoList)));
 
// 취소 배송비 합산 (동일 orderDeliverySno 키는 1회만 계산)
foreach ($cancelRows as $gVal) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
continue;
}
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 (일괄조회 맵에서 룩업)
$refundDeliveryCharge = $handleInfoMap[$gVal['handleSno']]['refundDeliveryCharge'] ?? 0;
$originalDcPrice = $deliveryOriginalMap[$gVal['orderDeliverySno']]['divisionMemberDeliveryDcPrice'] ?? 0;
$getData['dashBoardPrice']['cancelPrice'] += $refundDeliveryCharge - ($originalDcPrice - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
 
if (Manager::isProvider()) {
$getData = $this->getProviderTotalPrice($getData);
}
 
Component/Order/OrderAdmin.php+36-24
변경 전OrderAdmin.php
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
 
 
use Request;
use Session;
use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader;
 
$deliveryUniqueKey = '';
$aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용
// 전체 정책별 배송비와 지역별 배송비의 총합 구하기
 
 
 
foreach ($getData['goods'] as $sKey => $sVal) {
foreach ($sVal as $dKey => $dVal) {
// $dVal = array_shift($dVal);
foreach ($dVal as $gKey => $gVal) {
$realDeliveryTaxSum = array_sum([
$gVal['realTaxSupplyDeliveryCharge'],
]);
// 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다
$deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno'];
if ($deliveryKeyCheck == $deliveryUniqueKey) {
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (!in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산
$aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']);
$aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']);
$getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
}
} else {
// 주문상세내 배송비 금액 합산
if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) {
$getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum;
if (in_array($gVal['orderStatus'], ['o1'])) {
$getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum;
}
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (!in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산
$aHandleInfo = $this->getOrderHandleInfo($gVal['handleSno']);
$aDeliveryOriginalInfo = $this->getOrderDeliveryOriginal($gVal['orderDeliverySno']);
$getData['dashBoardPrice']['cancelPrice'] += $aHandleInfo['refundDeliveryCharge'] - ($aDeliveryOriginalInfo['divisionMemberDeliveryDcPrice'] - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
}
if (in_array($gVal['orderStatus'], ['r1', 'r2'])) {
$getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum;
}
// 배송무게 계산
$getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true);
}
 
 
 
 
 
 
 
 
 
 
$deliveryUniqueKey = $deliveryKeyCheck;
 
// 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
if (Manager::isProvider()) {
$getData = $this->getProviderTotalPrice($getData);
}
 
변경 후OrderAdmin.php
use Origin\Enum\Order\OrderStatusGroup;
use Origin\Service\Statistics\Analytics\Order\OrderCompleteAnalyticsCollectorService;
use Origin\Service\WebHook\OrderWebHookService;
use Repository\Order\OrderDeliveryOriginalRepository;
use Repository\Order\OrderHandleRepository;
use Request;
use Session;
use Vendor\Spreadsheet\Excel\Reader as SpreadsheetExcelReader;
 
$deliveryUniqueKey = '';
$aCancelDeliveryKey = array(); // 품목 하나가 부분으로 취소되서 품목수가 나뉘어진경우 취소배송비 중복계산이 안되도록 체크하기위해서 orderDeliverySno로 키값 저장&체크 todo 취소부분 환불과 맞추면 다 삭제될 내용
 
// 전체 정책별 배송비와 지역별 배송비의 총합 구하기 (DB 불필요 합산은 단일 순회로 처리, 취소행/sno는 수집만)
$cancelRows = [];
$handleSnoList = $deliveryOriginalSnoList = [];
foreach ($getData['goods'] as $sKey => $sVal) {
foreach ($sVal as $dKey => $dVal) {
 
foreach ($dVal as $gKey => $gVal) {
$realDeliveryTaxSum = array_sum([
$gVal['realTaxSupplyDeliveryCharge'],
]);
// 전체 배송비 계산 (주문상품추가/주문타상품교환으로 같은 배송비 조건(deliverySno)이더라도 주문배송비(orderDeliverySno)가 추가된다
$deliveryKeyCheck = $gVal['deliverySno'] . '-' . $gVal['orderDeliverySno'];
if ($deliveryKeyCheck != $deliveryUniqueKey) {
 
 
 
 
 
 
 
 
 
 
 
// 주문상세내 배송비 금액 합산
if (!in_array($gVal['orderStatus'], ['o1', 'c1', 'c2', 'c3', 'c4'])) {
$getData['dashBoardPrice']['settlePrice'] += $realDeliveryTaxSum;
if (in_array($gVal['orderStatus'], ['o1'])) {
$getData['dashBoardPrice']['dueSettlePrice'] += $realDeliveryTaxSum;
}
 
 
 
 
 
 
 
 
 
 
if (in_array($gVal['orderStatus'], ['r1', 'r2'])) {
$getData['dashBoardPrice']['dueRefundPrice'] += $realDeliveryTaxSum;
}
// 배송무게 계산
$getData['totalDeliveryWeights'] = json_decode($gVal['deliveryWeightInfo'], true);
}
// 취소행은 기존에 배송키 분기와 무관하게 동일 처리됐으므로 모아두고, N+1 제거 위해 일괄조회 후 후처리
if (in_array($gVal['orderStatus'], ['c1', 'c2', 'c3', 'c4'])) {
$cancelRows[] = $gVal;
if ($gVal['handleSno'] > 0) {
$handleSnoList[] = $gVal['handleSno'];
}
if ($gVal['orderDeliverySno'] > 0) {
$deliveryOriginalSnoList[] = $gVal['orderDeliverySno'];
}
}
$deliveryUniqueKey = $deliveryKeyCheck;
 
// 해당 주문의 배송비es_orderDelivery.sno에 대한 배송비 세율정보
}
}
 
// 취소상태 배송행의 handleSno/orderDeliverySno 를 일괄 1회씩 조회 (N+1 제거)
$handleInfoMap = \App::getInstance(OrderHandleRepository::class)
->findMapBySnos(array_values(array_unique($handleSnoList)));
$deliveryOriginalMap = \App::getInstance(OrderDeliveryOriginalRepository::class)
->findMapBySnos(array_values(array_unique($deliveryOriginalSnoList)));
 
// 취소 배송비 합산 (동일 orderDeliverySno 키는 1회만 계산)
foreach ($cancelRows as $gVal) {
$tempCancelDeliveryKey = substr($gVal['orderStatus'], 0, 1) . '-' . $gVal['orderDeliverySno'];
if (in_array($tempCancelDeliveryKey, $aCancelDeliveryKey)) {
continue;
}
// $gVal['handleSno'] 값으로 orderHandle에 기록된 취소 배송비 값 가져와서 계산 (일괄조회 맵에서 룩업)
$refundDeliveryCharge = $handleInfoMap[$gVal['handleSno']]['refundDeliveryCharge'] ?? 0;
$originalDcPrice = $deliveryOriginalMap[$gVal['orderDeliverySno']]['divisionMemberDeliveryDcPrice'] ?? 0;
$getData['dashBoardPrice']['cancelPrice'] += $refundDeliveryCharge - ($originalDcPrice - $gVal['divisionMemberDeliveryDcPrice']);
$aCancelDeliveryKey[] = $tempCancelDeliveryKey;
}
 
if (Manager::isProvider()) {
$getData = $this->getProviderTotalPrice($getData);
}
 
Repository/Order/OrderDeliveryOriginalRepository.php+33
변경 전OrderDeliveryOriginalRepository.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후OrderDeliveryOriginalRepository.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Repository\Order;
 
use Origin\Model\Order\OrderDeliveryOriginal;
 
class OrderDeliveryOriginalRepository
{
/**
* sno 목록으로 orderDeliveryOriginal 정보를 일괄 조회해 [sno => row] 맵으로 반환 (주문상세 N+1 제거용)
*
* @param array $snos orderDeliveryOriginal.sno 목록
* @return array sno 를 키로 하는 orderDeliveryOriginal row 맵
*/
public function findMapBySnos(array $snos): array
{
if (empty($snos)) {
return [];
}
 
return OrderDeliveryOriginal::query()
->whereIn('sno', $snos)
->get()
->keyBy('sno')
->toArray();
}
}
 
Repository/Order/OrderHandleRepository.php+33
변경 전OrderHandleRepository.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후OrderHandleRepository.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Repository\Order;
 
use Origin\Model\Order\OrderHandle;
 
class OrderHandleRepository
{
/**
* sno 목록으로 orderHandle 정보를 일괄 조회해 [sno => row] 맵으로 반환 (주문상세 N+1 제거용)
*
* @param array $snos orderHandle.sno 목록
* @return array sno 를 키로 하는 orderHandle row 맵
*/
public function findMapBySnos(array $snos): array
{
if (empty($snos)) {
return [];
}
 
return OrderHandle::query()
->whereIn('sno', $snos)
->get()
->keyBy('sno')
->toArray();
}
}
 
사은품 주문 보안 강화.
Component/Gift/Gift.php+90
변경 전Gift.php
}
return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후Gift.php
}
return $isStrip ? gd_htmlspecialchars_stripslashes($setData) : $setData;
}
 
/**
* 주문 사은품 데이터 검증 및 정규화 (변조 방지)
*
* 주문서에서 전달된 사은품 hidden 필드(giveCnt/selectCnt/giftNo)는 개발자도구 등으로
* 변조가 가능하므로, 서버에서 재조회한 유효 사은품 정보($giftInfo)를 기준으로 검증·정규화
* - giveCnt(지급수량) : 지급조건의 값으로 강제 정규화 (변조 무력화, 주문은 정상 생성)
* - selectCnt(선택수량) : 실제 선택한 사은품 개수가 지급조건과 다르면 차단 (전체=0 은 제외)
* - 지급조건에 포함되지 않은 사은품(giftNo)은 제거
*
* @param array $submittedGift 주문서에서 전달된 사은품 데이터 [presentSno][index] => [...]
* @param array $giftInfo getGiftPresentOrder() 로 조회한 유효 사은품 정보
* @return array 정규화된 사은품 데이터
* @throws \Exception
*/
public function validateAndNormalizeOrderGift(array $submittedGift, array $giftInfo): array
{
if (empty($submittedGift) || empty($giftInfo)) {
return $submittedGift;
}
 
foreach ($submittedGift as $presentSno => $giftData) {
// 유효하지 않은 사은품 정책은 기존 저장 로직에서 스킵되므로 건드리지 않음
if (!array_key_exists($presentSno, $giftInfo) || empty($giftInfo[$presentSno]['gift'])) {
continue;
}
 
// 권위 데이터 집계 : giftNo => 지급조건(row), 지급조건별 giveCnt/selectCnt
$authGiveCnt = []; // giftNo => giveCnt
$giftNoToRow = []; // giftNo => rowKey
$rowSelectCnt = []; // rowKey => selectCnt
foreach ($giftInfo[$presentSno]['gift'] as $rowKey => $row) {
$rowSelectCnt[$rowKey] = (int) ($row['selectCnt'] ?? 0);
if (empty($row['multiGiftNo']) || !is_array($row['multiGiftNo'])) {
continue;
}
foreach ($row['multiGiftNo'] as $item) {
if (!isset($item['giftNo'])) {
continue;
}
$authGiveCnt[$item['giftNo']] = (int) ($row['giveCnt'] ?? 0);
$giftNoToRow[$item['giftNo']] = $rowKey;
}
}
 
// 제출된 사은품 정규화 + 지급조건별 선택 개수/제출 선택수량 집계
$selectedCntByRow = [];
$submittedSelectCntByRow = [];
foreach ($giftData as $index => $gift) {
// 선택되지 않은 항목(giftNo 없음)은 통과 (기존 로직이 저장하지 않음)
if (!isset($gift['giftNo']) || $gift['giftNo'] === '') {
continue;
}
// 지급조건에 포함되지 않은 사은품(변조)은 제거
if (!isset($authGiveCnt[$gift['giftNo']])) {
\Logger::channel('order')->info(__METHOD__ . ' 지급조건에 없는 사은품 제거', [$presentSno, $gift['giftNo']]);
unset($submittedGift[$presentSno][$index]);
continue;
}
// giveCnt(지급수량) 정규화 - POST 변조 무력화
$submittedGift[$presentSno][$index]['giveCnt'] = $authGiveCnt[$gift['giftNo']];
 
$rowKey = $giftNoToRow[$gift['giftNo']];
$selectedCntByRow[$rowKey] = ($selectedCntByRow[$rowKey] ?? 0) + 1;
// 제출된 선택수량(변조 탐지용) - 같은 지급조건 항목은 동일 값
if (isset($gift['selectCnt'])) {
$submittedSelectCntByRow[$rowKey] = (int) $gift['selectCnt'];
}
}
 
// 선택수량(selectCnt) 검증
foreach ($rowSelectCnt as $rowKey => $selectCnt) {
$selectedCnt = (int) ($selectedCntByRow[$rowKey] ?? 0);
$submittedSelectCnt = $submittedSelectCntByRow[$rowKey] ?? null;
$message = GiftSelectCntValidator::resolveViolationMessage($selectCnt, $selectedCnt, $submittedSelectCnt);
if ($message !== null) {
\Logger::channel('order')->warning(__METHOD__ . ' 사은품 선택수량 검증 실패', [
'presentSno' => $presentSno,
'rowKey' => $rowKey,
'authSelectCnt' => $selectCnt,
'selectedCnt' => $selectedCnt,
'submittedSelectCnt' => $submittedSelectCnt,
]);
throw new \Exception($message);
}
}
}
 
return $submittedGift;
}
}
 
Component/Gift/GiftSelectCntValidator.php+55
변경 전GiftSelectCntValidator.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후GiftSelectCntValidator.php
<?php
/*
* Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved
*
* Unauthorized copying or redistribution of this file in source and binary forms via any medium
* is strictly prohibited.
*/
 
namespace Bundle\Component\Gift;
 
/**
* 사은품 선택수량(selectCnt) 검증
*
* DB/세션/로거 등 부수효과 없이 정수 입력만으로 위반 여부를 판정
* 일반주문 등 각 호출부는 이 메시지를 받아 흐름에 맞는 예외를 던진다
*/
class GiftSelectCntValidator
{
/**
* 선택수량 위반 메시지를 반환한다. 위반이 없으면 null
*
* - 지급조건 선택수량(전체=0)은 검증 제외
* - 미선택(0개)은 검증 제외 (사은품 거절 허용)
* - 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건과 다르면 위반
*
* @param int $authSelectCnt 서버(지급조건)의 선택수량. 0이면 전체 지급
* @param int $selectedCnt 실제 선택(체크)된 사은품 개수
* @param int|null $submittedSelectCnt 주문서에서 제출된 선택수량 값(변조 탐지용). null이면 미사용
* @return string|null 위반 메시지 또는 null
*/
public static function resolveViolationMessage(int $authSelectCnt, int $selectedCnt, int $submittedSelectCnt = null)
{
// 전체(0) 또는 미선택은 검증 제외 (전체 지급 / 사은품 거절 허용)
if ($authSelectCnt <= 0 || $selectedCnt <= 0) {
return null;
}
 
// 제출된 선택수량이 없으면 실제 선택 개수로 비교
if ($submittedSelectCnt === null) {
$submittedSelectCnt = $selectedCnt;
}
 
// 최대 초과 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 큰 경우
if ($selectedCnt > $authSelectCnt || $submittedSelectCnt > $authSelectCnt) {
return sprintf(__('사은품은 최대 %s개만 선택하실 수 있습니다.'), $authSelectCnt);
}
 
// 최소 미달 : 실제 선택 개수 또는 제출된 선택수량(변조)이 지급조건보다 작은 경우
if ($selectedCnt < $authSelectCnt || $submittedSelectCnt < $authSelectCnt) {
return sprintf(__('사은품은 최소 %s개 이상 선택하셔야 합니다.'), $authSelectCnt);
}
 
return null;
}
}
 
Component/Order/OrderNew.php+8
변경 전OrderNew.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
 
 
 
 
 
 
 
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
변경 후OrderNew.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], 0, false, false); // 수기 주문 케이스는 이 구간을 거치지 않음
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
// 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단은 프론트 주문만, 관리자 수기주문 제외)
// 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그
if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) {
$orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo);
} else {
\Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]);
}
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
Component/Order/Order.php+12
변경 전Order.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false);
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
 
 
 
 
 
 
 
 
 
 
 
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
변경 후Order.php
$giftInfo = $giftPresent->getGiftPresentOrder($orderInfo['giftForData'], $groupSno, $isWrite, false);
\Logger::channel('goods')->info(__METHOD__ . ' getGiftPresentOrder() result :', $giftInfo);
 
// 사은품 지급/선택 수량 검증 및 정규화 (giveCnt 변조 무력화 / selectCnt 위반 시 차단)
// 프론트/회원 주문($isWrite === false)만 검증한다. 관리자 수기주문($isWrite === true)은
// selectCnt 강제(차단) 대상이 아니므로 제외한다.
if ($isWrite === false) {
// 존재 여부 가드 - 미존재 시 fatal 대신 검증 미적용 + 경고 로그
if (method_exists($giftPresent, 'validateAndNormalizeOrderGift')) {
$orderInfo['gift'] = $giftPresent->validateAndNormalizeOrderGift($orderInfo['gift'], $giftInfo);
} else {
\Logger::channel('order')->warning(__METHOD__ . ' validateAndNormalizeOrderGift 미존재(Gift.php 튜닝본) - 사은품 수량 변조 검증 미적용', ['orderNo' => $this->orderNo]);
}
}
 
if (count($giftInfo) > 0) {
foreach ($orderInfo['gift'] as $presentSno => $giftData) {
// 주문시 사용된 사은품 정보가 유효하지 않으면 삭제
 
카카오 아이디 로그인 설정 페이지 헤더 표시 개선.
Admin/member/kakao_login_config.php+3-1
변경 전kakao_login_config.php
<div class="notice-info">
카카오 아이디 로그인에 사용한 REST API 키와 ADMIN 키는 <a href="https://developers.kakao.com/" target="_blank" class="notice-ref btn-link">카카오 디벨로퍼스 > 내 애플리케이션 > 앱 설정 > 앱 키</a>에서 확인하실 수 있습니다.
</div>
<?php elseif($isAvailableOnlyKakaoSync === false) : ?>
<form id="form" name="form" action="kakao_login_config_ps.php" method="post">
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
 
<input type="submit" value="저장" class="btn btn-red" />
 
</div>
 
<div class="table-title gd-help-manual">
 
변경 후kakao_login_config.php
<div class="notice-info">
카카오 아이디 로그인에 사용한 REST API 키와 ADMIN 키는 <a href="https://developers.kakao.com/" target="_blank" class="notice-ref btn-link">카카오 디벨로퍼스 > 내 애플리케이션 > 앱 설정 > 앱 키</a>에서 확인하실 수 있습니다.
</div>
<?php else : ?>
<form id="form" name="form" action="kakao_login_config_ps.php" method="post">
<div class="page-header js-affix">
<h3><?php echo end($naviMenu->location); ?></h3>
<?php if (!$isAvailableOnlyKakaoSync) : ?>
<input type="submit" value="저장" class="btn btn-red" />
<?php endif; ?>
</div>
 
<div class="table-title gd-help-manual">
 
상품 판매 통계 로그 정확도 개선
Component/Order/OrderNew.php+8-4
변경 전OrderNew.php
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq']]);
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData]);
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', $result);
}
 
unset($arrOrderGoodsSno['mq']);
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', $result);
}
unset($mqData);
 
 
변경 후OrderNew.php
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
$request = \App::getInstance('request');
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq'], 'reqUri' => $request->getRequestUri()]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
$request = \App::getInstance('request');
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData, 'reqUri' => $request->getRequestUri()]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq']]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
 
unset($arrOrderGoodsSno['mq']);
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
unset($mqData);
 
 
Component/Order/Order.php+8-4
변경 전Order.php
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq']]);
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData]);
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', $result);
}
unset($arrOrderGoodsSno['mq']);
 
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
 
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', $result);
}
unset($mqData);
 
 
변경 후Order.php
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
$request = \App::getInstance('request');
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq'], 'reqUri' => $request->getRequestUri()]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
$request = \App::getInstance('request');
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData, 'reqUri' => $request->getRequestUri()]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
 
$kafka = new ProducerUtils();
if (!empty($arrOrderGoodsSno['mq'])) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $this->orderNo, 'orderGoodsNos' => $arrOrderGoodsSno['mq']]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($arrOrderGoodsSno['mq'], 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $this->orderNo], is_array($result) ? $result : []));
}
unset($arrOrderGoodsSno['mq']);
 
 
$kafka = new ProducerUtils();
if (!empty($mqData)) {
\Logger::channel('kafka')->info('process sendMQ - payload :', ['topic' => $kafka::TOPIC_PRODUCT_ORDER_COUNT, 'orderNo' => $orderNo, 'orderGoodsNos' => $mqData]);
$result = $kafka->send($kafka::TOPIC_PRODUCT_ORDER_COUNT, $kafka->makeData($mqData, 'p'), $kafka::MODE_RESULT_CALLLBACK, true);
\Logger::channel('kafka')->info('process sendMQ - return :', array_merge(['orderNo' => $orderNo], is_array($result) ? $result : []));
}
unset($mqData);
 
 
네이버 아이디 로그인 보안 강화
Controller/Front/Intro/AdultController.php+8-1
변경 전AdultController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
$this->setData('naverUrl', $naver->getLoginURL());
 
 
 
 
 
 
 
}
$this->setData('useNaverLogin', $useNaver);
 
 
변경 후AdultController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
// redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관
$naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php';
$naverState = $naver->createState([
'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH),
'returnUrl' => $returnUrl,
'naverType' => '',
]);
$this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState));
}
$this->setData('useNaverLogin', $useNaver);
 
 
Controller/Front/Intro/MemberController.php+8-1
변경 전MemberController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
$this->setData('naverUrl', $naver->getLoginURL());
 
 
 
 
 
 
 
}
$this->setData('useNaverLogin', $useNaver);
$siteLink = new SiteLink();
 
변경 후MemberController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
// redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관
$naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php';
$naverState = $naver->createState([
'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH),
'returnUrl' => $returnUrl,
'naverType' => '',
]);
$this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState));
}
$this->setData('useNaverLogin', $useNaver);
$siteLink = new SiteLink();
 
Controller/Front/Member/LoginController.php+8-1
변경 전LoginController.php
if ($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
$this->setData('naverUrl', $naver->getLoginURL($returnUrl));
 
 
 
 
 
 
 
}
 
$kakaoPolicy = \App::getInstance(KakaoLoginPolicy::class);
 
변경 후LoginController.php
if ($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
// redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관
$naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php';
$naverState = $naver->createState([
'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH),
'returnUrl' => $returnUrl,
'naverType' => '',
]);
$this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState));
}
 
$kakaoPolicy = \App::getInstance(KakaoLoginPolicy::class);
 
Controller/Front/Member/Naver/NaverLoginController.php+6-3
변경 전NaverLoginController.php
$loginReturnURL = MemberUtil::getLoginReturnURL();
}
 
 
 
 
$js = "
if (typeof(window.top.layerSearchArea) == 'object') {
parent.location.href='" . $loginReturnURL . "';
} else if (window.opener === null) {
location.href='" . $loginReturnURL . "';
} else {
opener.location.href='" . $loginReturnURL . "';
self.close();
}
";
 
변경 후NaverLoginController.php
$loginReturnURL = MemberUtil::getLoginReturnURL();
}
 
// location.href JS 문자열 컨텍스트 인젝션 방지: URL을 JSON 리터럴로 인코딩
$loginReturnUrlJs = json_encode((string) $loginReturnURL, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
 
$js = "
if (typeof(window.top.layerSearchArea) == 'object') {
parent.location.href=" . $loginReturnUrlJs . ";
} else if (window.opener === null) {
location.href=" . $loginReturnUrlJs . ";
} else {
opener.location.href=" . $loginReturnUrlJs . ";
self.close();
}
";
 
Controller/Mobile/Intro/AdultController.php+8-1
변경 전AdultController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
$this->setData('naverUrl', $naver->getLoginURL());
 
 
 
 
 
 
 
}
$this->setData('useNaverLogin', $useNaver);
 
 
변경 후AdultController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
// redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관
$naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php';
$naverState = $naver->createState([
'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH),
'returnUrl' => $returnUrl,
'naverType' => '',
]);
$this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState));
}
$this->setData('useNaverLogin', $useNaver);
 
 
Controller/Mobile/Intro/MemberController.php+8-1
변경 전MemberController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
$this->setData('naverUrl', $naver->getLoginURL());
 
 
 
 
 
 
 
}
$this->setData('useNaverLogin', $useNaver);
 
 
변경 후MemberController.php
if($useNaver) {
$naver = new GodoNaverServerApi();
$scripts[] = 'gd_naver.js';
// redirect_uri는 등록 콜백URL과 동일한 고정 경로 (strict match), 동적값은 state로 이관
$naverRedirectUri = $request->getDomainUrl() . '/member/naver/naver_login.php';
$naverState = $naver->createState([
'referer' => $request->getDomainUrl() . parse_url($request->getRequestUri(), PHP_URL_PATH),
'returnUrl' => $returnUrl,
'naverType' => '',
]);
$this->setData('naverUrl', $naver->getLoginURL($naverRedirectUri, $naverState));
}
$this->setData('useNaverLogin', $useNaver);
 
 
회원등급 평가방법 설정의 '주문금액' 표기를 '실결제금액'으로 변경
Admin/member/member_group_appraisal_config.php+5-5
변경 전member_group_appraisal_config.php
</label>
</th>
<td colspan="3">
주문금액, 상품주문건수, 주문상품후기횟수를 종합하여 평가하는 방법입니다. 회원등급별 평가기준을 입력하세요.
<div class="notice-info">수동 평가 선택 시 회원등급이 자동으로 평가되지 않고 <a href="#" class="js-btn-appraisal">[회원등급 수동평가]</a>를 눌러 회원등급을 평가합니다.</div>
</td>
</tr>
<th>
<label class="checkbox-inline">
<input type="checkbox" name="appraisalPointOrderPriceFl" value="y" <?= gd_isset($checked['appraisalPointOrderPriceFl']['y']); ?> data-target-class="terms_p01"/>
주문금액
</label>
</th>
<td>
?>
<label class="checkbox-inline">
<input type="checkbox" name="<?= $chknm1 ?>" value="y" <?= $checked['apprFigureOrderPriceFl'][$val['sno']]['y'] ?> data-target-class="<?= $checkClass1 ?>"/>
주문금액
</label>
<br/>
<label class="checkbox-inline">
var gd_group_list = {
layerRule: null,
message: {
figure: "현재 설정된 평가방법은 실적수치제이며, 회원등급별 평가기준에 따라 주문금액, 상품주문건수, 주문상품후기횟수를 종합하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?",
point: "현재 설정된 평가방법은 실적점수제이며, 주문금액, 상품주문건수, 주문상품후기횟수, 로그인횟수를 점수로 환산하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?"
}
};
 
 
변경 후member_group_appraisal_config.php
</label>
</th>
<td colspan="3">
실결제금액, 상품주문건수, 주문상품후기횟수를 종합하여 평가하는 방법입니다. 회원등급별 평가기준을 입력하세요.
<div class="notice-info">수동 평가 선택 시 회원등급이 자동으로 평가되지 않고 <a href="#" class="js-btn-appraisal">[회원등급 수동평가]</a>를 눌러 회원등급을 평가합니다.</div>
</td>
</tr>
<th>
<label class="checkbox-inline">
<input type="checkbox" name="appraisalPointOrderPriceFl" value="y" <?= gd_isset($checked['appraisalPointOrderPriceFl']['y']); ?> data-target-class="terms_p01"/>
실결제금액
</label>
</th>
<td>
?>
<label class="checkbox-inline">
<input type="checkbox" name="<?= $chknm1 ?>" value="y" <?= $checked['apprFigureOrderPriceFl'][$val['sno']]['y'] ?> data-target-class="<?= $checkClass1 ?>"/>
실결제금액
</label>
<br/>
<label class="checkbox-inline">
var gd_group_list = {
layerRule: null,
message: {
figure: "현재 설정된 평가방법은 실적수치제이며, 회원등급별 평가기준에 따라 실결제금액, 상품주문건수, 주문상품후기횟수를 종합하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?",
point: "현재 설정된 평가방법은 실적점수제이며, 실결제금액, 상품주문건수, 주문상품후기횟수, 로그인횟수를 점수로 환산하여 회원등급이 평가됩니다. 회원등급 평가를 진행하시겠습니까?"
}
};