본문으로 건너뛰기

디자인에디터 페이지 SEO 태그 설정 기능

PHP 8 · GODO26
변경 파일 수: 6개
변경 파일 목록
총 6개
변경 파일 코드
추가삭제
Component/DesignEditor/DesignEditorAssetRepositoryStub.php-132
변경 전DesignEditorAssetRepositoryStub.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\DesignEditor;
 
/**
* 디자인 에디터 에셋 저장소 Stub (테스트용)
*
* 단위 테스트를 위한 인메모리 구현체
* 실제 DB 스키마(es_designEditorAsset, es_designEditorFolder)와 동일한 필드 구조 사용
*
* @author NHN godo
*/
class DesignEditorAssetRepositoryStub implements DesignEditorAssetRepositoryInterface
{
/** @var array 임시 에셋 데이터 */
protected array $assets = [];
 
public function __construct()
{
$this->initializeStubData();
}
 
/**
* 임시 에셋 데이터 초기화
*/
protected function initializeStubData(): void
{
$this->assets = [
[
'assetSno' => 1,
'folderSno' => 1,
'skinSno' => 1, // JOIN 결과로 포함
'folderSlug' => 'custom', // JOIN 결과로 포함
'assetCode' => 'custom--test_page',
'assetTitle' => '테스트 페이지',
'assetType' => 'html',
'assetIcon' => null,
'assetContents' => json_encode([
'version' => '1.0.0',
'page' => [
'id' => 'custom-test-page',
'title' => '테스트 페이지',
'seo' => [
'title' => '테스트 페이지 SEO 타이틀',
'description' => '테스트 페이지 설명입니다.',
'keywords' => '테스트, 페이지',
],
'sections' => [],
],
]),
'assetPath' => 'custom/test_page.html',
'pageUrl' => 'custom/test_page.php',
'deleteFl' => 'n',
'regDt' => date('Y-m-d H:i:s'),
'modDt' => null,
],
[
'assetSno' => 2,
'folderSno' => 1,
'skinSno' => 1,
'folderSlug' => 'custom',
'assetCode' => 'custom--event_page',
'assetTitle' => '이벤트 페이지',
'assetType' => 'html',
'assetIcon' => null,
'assetContents' => null,
'assetPath' => 'custom/event_page.html',
'pageUrl' => 'custom/event_page.php',
'deleteFl' => 'n',
'regDt' => date('Y-m-d H:i:s'),
'modDt' => null,
],
];
}
 
/**
* 커스텀 에셋 추가 (테스트용)
*
* @param array $asset
*/
public function addAsset(array $asset): void
{
$this->assets[] = $asset;
}
 
/**
* {@inheritdoc}
*/
public function findByPageUrl(int $skinSno, string $pageUrl): ?array
{
foreach ($this->assets as $asset) {
if ($asset['skinSno'] === $skinSno
&& $asset['pageUrl'] === $pageUrl
&& $asset['deleteFl'] === 'n') {
return $asset;
}
}
return null;
}
 
/**
* {@inheritdoc}
*/
public function findByAssetCode(int $skinSno, string $assetCode): ?array
{
foreach ($this->assets as $asset) {
if ($asset['skinSno'] === $skinSno
&& $asset['assetCode'] === $assetCode
&& $asset['deleteFl'] === 'n') {
return $asset;
}
}
return null;
}
 
/**
* {@inheritdoc}
*/
public function findAllBySkin(int $skinSno): array
{
return array_filter($this->assets, function ($asset) use ($skinSno) {
return $asset['skinSno'] === $skinSno && $asset['deleteFl'] === 'n';
});
}
}
 
변경 후DesignEditorAssetRepositoryStub.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Component/DesignEditor/DesignEditorRouter.php+24
변경 전DesignEditorRouter.php
return $this->repository->findByPageUrl($skinSno, $pageUrl);
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* URI 정규화
*
 
변경 후DesignEditorRouter.php
return $this->repository->findByPageUrl($skinSno, $pageUrl);
}
 
/**
* PC(front) 스킨 기준으로 pageUrl 에셋 매칭
*
* 요청 디바이스와 무관하게 운영중인 PC 스킨에서 조회한다.
* 디자인 에디터 에셋은 반응형 스킨(skinDevice=front)에만 생성되므로,
* 관리자 화면에서 페이지 존재 여부를 확인할 때 이 경로를 사용한다.
*
* 스킨 활성 여부(isDesignEditorSkinEnabled)는 확인하지 않는다.
* 적응형 스킨도 es_designEditorSkin 에 등록되어 있어 판별력이 없고,
* 실제 판별은 에셋 조회가 담당한다.
*
* @param string $pageUrl 페이지 URL (예: custom/my_page.php)
* @return array|null 에셋 정보 또는 null (매칭 실패)
*/
public function matchFrontPageUrl(string $pageUrl): ?array
{
$skinSno = $this->skinResolver->getFrontSkinSno();
if (is_null($skinSno)) {
return null;
}
 
return $this->repository->findByPageUrl($skinSno, $this->normalizeUri($pageUrl));
}
 
/**
* URI 정규화
*
 
Component/DesignEditor/DesignEditorSkinRepositoryInterface.php+30
변경 전DesignEditorSkinRepositoryInterface.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
변경 후DesignEditorSkinRepositoryInterface.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\DesignEditor;
 
/**
* 디자인 에디터 스킨 저장소 인터페이스
*
* 스킨 리졸버가 사용하는 스킨 조회 인터페이스
* 실제 DB 구현체와 테스트용 Stub 구현체를 교체할 수 있도록 인터페이스로 분리
*
* @author NHN godo
*/
interface DesignEditorSkinRepositoryInterface
{
/**
* 스킨 코드·스킨 디바이스로 스킨 정보 조회
*
* @param string $skinCode 스킨 코드 (design.skin 정책의 frontLive / mobileLive)
* @param string $skinDevice 스킨 디바이스 (front | mobile)
* @return array 스킨 정보 배열 (skinSno·skinCode·skinType·skinDevice·skinName 등), 없으면 빈 배열
*/
public function findBySkinCodeAndSkinDevice(string $skinCode, string $skinDevice): array;
}
 
Component/DesignEditor/DesignEditorSkinResolver.php+45-9
변경 전DesignEditorSkinResolver.php
 
namespace Bundle\Component\DesignEditor;
 
use Origin\Model\Design\DesignEditorSkin;
 
/**
* 디자인 에디터 스킨 리졸버
*
/** @var array|null 캐시된 디자인 에디터 스킨 정보 */
protected ?array $designEditorSkin = null;
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 디자인 에디터 스킨 사용 여부 확인
*
return $this->doGetCurrentSkinName();
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* 디자인 에디터 스킨 사용 여부 확인 (구현)
*
// 디바이스 타입 결정
$skinDevice = \Request::isMobile() ? 'mobile' : 'front';
 
// es_designEditorSkin 테이블에서 해당 스킨 조회
$skin = DesignEditorSkin::query()
->where('skinCode', $skinCode)
->where('skinDevice', $skinDevice)
->first();
 
if ($skin) {
$this->designEditorSkin = $skin->toArray();
}
 
return $this->designEditorSkin;
 
return $this->skinConfig;
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
}
 
변경 후DesignEditorSkinResolver.php
 
namespace Bundle\Component\DesignEditor;
 
 
 
/**
* 디자인 에디터 스킨 리졸버
*
/** @var array|null 캐시된 디자인 에디터 스킨 정보 */
protected ?array $designEditorSkin = null;
 
/** @var DesignEditorSkinRepositoryInterface 스킨 저장소 */
protected DesignEditorSkinRepositoryInterface $skinRepository;
 
/**
* @param DesignEditorSkinRepositoryInterface|null $skinRepository 스킨 저장소. 미주입(인자 없는 new - 튜닝 호환) 시 컨테이너에서 가져온다
*/
public function __construct(?DesignEditorSkinRepositoryInterface $skinRepository = null)
{
$this->skinRepository = $skinRepository ?? \App::getInstance(DesignEditorSkinRepositoryInterface::class);
}
 
/**
* 디자인 에디터 스킨 사용 여부 확인
*
return $this->doGetCurrentSkinName();
}
 
/**
* PC(front) 디자인 에디터 스킨 번호 조회
*
* 요청 디바이스와 무관하게 운영중인 PC 스킨을 기준으로 조회한다.
* 관리자 화면처럼 프론트 요청 문맥이 아닌 곳에서 사용한다.
*
* @return int|null 스킨 일련번호 또는 null
*/
public function getFrontSkinSno(): ?int
{
return $this->doGetFrontSkinSno();
}
 
/**
* 디자인 에디터 스킨 사용 여부 확인 (구현)
*
// 디바이스 타입 결정
$skinDevice = \Request::isMobile() ? 'mobile' : 'front';
 
// es_designEditorSkin 테이블에서 해당 스킨 조회 - 스킨 번호가 있는 행만 유효한 스킨으로 본다
$skin = $this->skinRepository->findBySkinCodeAndSkinDevice($skinCode, $skinDevice);
 
 
 
 
if (!empty($skin['skinSno'])) {
$this->designEditorSkin = $skin;
}
 
return $this->designEditorSkin;
 
return $this->skinConfig;
}
 
/**
* PC(front) 스킨 번호 조회 (구현)
*
* @return int|null
*/
protected function doGetFrontSkinSno(): ?int
{
$skinCode = $this->getSkinConfig()['frontLive'] ?? null;
if (empty($skinCode)) {
return null;
}
 
$skin = $this->skinRepository->findBySkinCodeAndSkinDevice($skinCode, 'front');
 
return $skin['skinSno'] ?? null;
}
}
 
Component/DesignEditor/DesignEditorSkinResolverStub.php-84
변경 전DesignEditorSkinResolverStub.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\DesignEditor;
 
/**
* 디자인 에디터 스킨 리졸버 Stub (테스트용)
*
* 단위 테스트를 위한 설정 가능한 구현체
*/
class DesignEditorSkinResolverStub extends DesignEditorSkinResolver
{
/**
* @param bool $enabled 디자인 에디터 스킨 활성화 여부
* @param int|null $skinSno 스킨 번호
* @param string|null $skinName 스킨명
*/
public function __construct(
protected bool $enabled = true,
protected ?int $skinSno = 1,
protected ?string $skinName = 'test_skin'
) {
}
 
/**
* 활성화 여부 설정
*
* @param bool $enabled
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
 
/**
* 스킨 번호 설정
*
* @param int|null $skinSno
*/
public function setSkinSno(?int $skinSno): void
{
$this->skinSno = $skinSno;
}
 
/**
* 스킨명 설정
*
* @param string|null $skinName
*/
public function setSkinName(?string $skinName): void
{
$this->skinName = $skinName;
}
 
/**
* {@inheritdoc}
*/
protected function doIsDesignEditorSkinEnabled(): bool
{
return $this->enabled;
}
 
/**
* {@inheritdoc}
*/
protected function doGetCurrentSkinSno(): ?int
{
return $this->skinSno;
}
 
/**
* {@inheritdoc}
*/
protected function doGetCurrentSkinName(): ?string
{
return $this->skinName;
}
}
 
변경 후DesignEditorSkinResolverStub.php
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Component/Policy/SeoTag.php+38-1
변경 전SeoTag.php
namespace Bundle\Component\Policy;
use App;
 
use Component\Database\DBTableField;
use Encryptor;
use Framework\Utility\StringUtils;
$realPath = gd_implode(DIRECTORY_SEPARATOR, [SYSSRCPATH, 'Bundle', 'Controller', $viewDirectory, ucfirst($_tmp['dirname']), (gd_implode("", array_map("ucfirst", explode("_", $_tmp['filename'])))) . 'Controller.php']);
}
}
if (!is_file($realPath)) {
return ['result' => false, 'code' => 'NOT FILE'];
}
if($etcPageFl === true){
}
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/**
* saveSeoTag
 
변경 후SeoTag.php
namespace Bundle\Component\Policy;
use App;
use Bundle\Component\DesignEditor\DesignEditorRouter;
use Component\Database\DBTableField;
use Encryptor;
use Framework\Utility\StringUtils;
$realPath = gd_implode(DIRECTORY_SEPARATOR, [SYSSRCPATH, 'Bundle', 'Controller', $viewDirectory, ucfirst($_tmp['dirname']), (gd_implode("", array_map("ucfirst", explode("_", $_tmp['filename'])))) . 'Controller.php']);
}
}
if (!is_file($realPath) && !$this->existsDesignEditorPage($arrData['path'], $arrData['deviceFl'])) {
return ['result' => false, 'code' => 'NOT FILE'];
}
if($etcPageFl === true){
}
}
/**
* 디자인 에디터로 생성된 페이지 존재 여부
*
* 디자인 에디터 페이지는 컨트롤러 파일 없이 에셋 레코드로만 존재하므로
* 파일 기반 검증으로는 통과할 수 없다. 에셋은 반응형 스킨(skinDevice=front)에만
* 생성되므로 PC 쇼핑몰 등록에 한해 조회한다.
*
* 쿼리스트링이 붙은 경로는 제외한다. 기타 페이지 SEO 출력 조회는 REQUEST_URI
* 원본과 비교하므로 기존에도 지원하지 않던 형태이며, 이번 변경으로 열리지 않게 한다.
*
* 조회 중 예외는 종전 판정(NOT FILE)으로 되돌린다. 이 분기는 is_file() 실패 뒤에만
* 실행되므로, 여기서 예외가 새면 기존에 "존재하지 않습니다" 였던 모든 입력이
* "처리 중 오류" 로 바뀐다. 런타임 라우팅(Application::dispatchDesignEditorPage)과 같은 방침이다.
*
* @param string $path 입력 페이지 경로 (예: promotion/test01.php)
* @param string $deviceFl 디바이스 구분 (p: PC 쇼핑몰, m: 모바일 쇼핑몰)
*
* @return bool
*/
protected function existsDesignEditorPage(string $path, string $deviceFl): bool
{
if ($deviceFl !== 'p' || str_contains($path, '?')) {
return false;
}
try {
// 컨테이너 등록 키(ContainerDefinitionsOrigin)가 FQCN 이라 Bundle\ 접두사로 참조한다 - Application::dispatchDesignEditorPage 와 동일
$router = \App::getInstance(DesignEditorRouter::class);
return $router->matchFrontPageUrl($path) !== null;
} catch (\Throwable $e) {
Logger::channel('designEditor')->warning(__METHOD__ . ' : 디자인 에디터 에셋 조회 실패', ['exception' => $e, 'path' => $path]);
return false;
}
}
/**
* saveSeoTag