Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.0.2/migrations/PruneEmptyShippingCountryNameLocales.php
T
HeuJung de18159986 fix(core,admin_basic): 정적 게시 재게시 누락 3건 수정 + 관리자 수동 복구 UI + sudo core:update 소유권 정합
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
2026-09-06 15:22:52 +09:00

140 lines
5.4 KiB
PHP

<?php
namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_0_2\Migrations;
use App\Extension\Helpers\FilePermissionHelper;
use App\Extension\Upgrade\DataMigration;
use App\Extension\UpgradeContext;
use App\Support\ExtensionStoragePath;
use Illuminate\Support\Facades\File;
/**
* 배송가능 국가명(available_countries[].name)의 빈 로케일 키를 제거.
*
* 구 국가 추가 폼은 한국어/영문 두 입력칸만 렌더했고, 운영자가 한쪽을 비운 채 저장하면
* `{"ko":"프랑스","en":""}` 처럼 빈 문자열이 저장본에 박혔다. 빈 문자열은 "값이 있음" 도
* "부재" 도 아닌 어중간한 상태다.
*
* 이 시스템의 계약은 "부재 로케일은 비워 둔다" 이다 — 기본 국가는 언어팩
* (`sirsoft-ecommerce::settings.countries.{code}.name`)이 읽기 시점에 보강하고
* (EcommerceSettingsService::getAllSettings), 운영자가 직접 추가한 국가는 운영자가 채운다.
* 따라서 빈 문자열 키를 제거해 "부재" 로 정규화한다.
*
* 이름을 새로 채우지는 않는다 — 언어팩 보강이 그 역할을 하며, 저장본에 값을 박으면
* 운영자 편집값으로 승격되어 이후 언어팩 갱신이 반영되지 않는다.
*
* name 이 배열이 아닌 구 스키마 잔재(문자열)는 이 마이그레이션의 책임이 아니라 건드리지 않는다.
*
* idempotent: 빈 키가 없고 모든 값이 trim 상태면 no-op (파일 쓰기 없음).
*
* V-1 안전: Illuminate\Support\Facades\File + 로컬 헬퍼만 사용.
*/
class PruneEmptyShippingCountryNameLocales implements DataMigration
{
private const MODULE_IDENTIFIER = 'sirsoft-ecommerce';
public function name(): string
{
return 'PruneEmptyShippingCountryNameLocales';
}
public function run(UpgradeContext $context): void
{
$path = $this->settingsFilePath();
if (! File::exists($path)) {
$context->logger->info('[ecommerce:1.0.2] shipping.json 미존재 — 국가명 빈 로케일 청소 스킵');
return;
}
$settings = json_decode((string) File::get($path), true);
if (! is_array($settings) || ! isset($settings['available_countries']) || ! is_array($settings['available_countries'])) {
$context->logger->info('[ecommerce:1.0.2] shipping.json 에 배송가능 국가 목록 없음 — 국가명 빈 로케일 청소 스킵');
return;
}
$countries = $settings['available_countries'];
$prunedKeys = 0;
foreach ($countries as $idx => $country) {
$name = $country['name'] ?? null;
// 구 스키마 잔재(문자열 name)는 대상 아님
if (! is_array($name)) {
continue;
}
$cleaned = $this->pruneEmptyLocales($name);
if ($cleaned !== $name) {
$prunedKeys += count($name) - count($cleaned);
$countries[$idx]['name'] = $cleaned;
}
}
if ($countries === $settings['available_countries']) {
$context->logger->info('[ecommerce:1.0.2] 국가명에 빈 로케일 키 없음 — 변경 없음 (idempotent)');
return;
}
$settings['available_countries'] = $countries;
// 인코딩 플래그는 EcommerceSettingsService 의 저장부와 동일해야 한다.
// 다르면 이 스텝이 국가명과 무관한 값(URL 등)의 이스케이프까지 조용히 바꿔 쓴다.
File::put($path, json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// sudo 코어 업데이트 경로에서 root 로 실행되면 새로 만든 파일이 root 소유로 남는다 — 부모 소유권 상속 (#651)
FilePermissionHelper::inheritOwnershipFromParent($path);
$context->logger->info(sprintf(
'[ecommerce:1.0.2] 국가명 빈 로케일 키 %d개 제거 완료 (국가 %d개 검사)',
$prunedKeys,
count($countries),
));
}
/**
* 다국어 이름 맵에서 빈 값(공백 포함) 로케일 키를 제거하고 남은 값을 trim 합니다.
*
* 모든 로케일이 비어 있으면 빈 배열을 반환합니다 — null 이 아니라 빈 배열이어야
* 백엔드 검증(`shipping.available_countries.*.name` => array)이 깨지지 않습니다.
*
* @param array<string, mixed> $name 로케일 => 국가명 맵
* @return array<string, string> 빈 로케일이 제거되고 trim 된 맵
*/
private function pruneEmptyLocales(array $name): array
{
$cleaned = [];
foreach ($name as $locale => $value) {
if (! is_string($value)) {
continue;
}
$trimmed = trim($value);
if ($trimmed === '') {
continue;
}
$cleaned[$locale] = $trimmed;
}
return $cleaned;
}
/**
* shipping.json 의 저장 경로를 반환합니다.
*
* 테스트 환경에서는 운영 storage 오염을 막기 위해 framework/testing 경로를 사용합니다
* (EcommerceSettingsService 의 저장 경로 분기와 동일).
*
* @return string 설정 파일 절대 경로
*/
private function settingsFilePath(): string
{
return ExtensionStoragePath::module(self::MODULE_IDENTIFIER, 'settings').'/shipping.json';
}
}