From 13e65131f49889594d565cfd0e5c62cd0dffcb1c Mon Sep 17 00:00:00 2001
From: HeuJung
Date: Sat, 11 Jul 2026 07:00:34 +0900
Subject: [PATCH] =?UTF-8?q?fix(vendor-bundle):=20=EC=BD=94=EC=96=B4=20?=
=?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EB=AC=B4=ED=95=9C=20=ED=96=89=20=ED=95=B4?=
=?UTF-8?q?=EA=B2=B0=20+=20=EC=9D=B4=EC=BB=A4=EB=A8=B8=EC=8A=A4=20?=
=?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=9D=BC=EB=B2=A8=20raw=20=EB=85=B8?=
=?UTF-8?q?=EC=B6=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
이커머스 알림 정의 화면에서 order_pending_deposit·order_delivered 두 알림이
라벨 대신 raw 다국어 키로 표시되던 문제를 수정. settings.json 3로케일에 라벨
추가 + 렌더 회귀 테스트 + 브라우저 실측.
vendor-bundle 시스템 결함 5종 해결:
- (A/B) manifest 해시 기준을 활성이 아닌 출력(_bundled) 디렉토리로 통일해
확장 업데이트가 composer_json_sha_mismatch 순환에 빠지던 문제 해소
- (C) 코어 빌드 시 composer autoload dump 의 async 손자 프로세스가 부모
파이프 핸들을 상속·점유해 무한 대기하던 교착을, stdout/stderr 를 파이프
아닌 파일 descriptor 로 받아 원천 차단 (파이프=행/파일=완주 A/B 실측으로 확정)
- (D) 빌드 실패 시 원본 번들 유실 방지 — 임시파일 성공 후 원자적 교체
- (E) Windows .bat 실행 시 bypass_shell 통일
코어 7.0.4 / 이커머스·언어팩 1.0.3 bump.
---
.env.example | 2 +-
.env.testing.example | 2 +-
CHANGELOG.md | 7 +
INSTALL.md | 2 +-
README.md | 2 +-
app/Extension/Vendor/EnvironmentDetector.php | 16 +-
app/Extension/Vendor/VendorBundler.php | 210 +++++++++++++-----
config/app.php | 2 +-
docs/extension/vendor-bundle.md | 33 ++-
lang-packs/_bundled/g7-core-ja/CHANGELOG.md | 6 +
.../g7-core-ja/backend/ja/exceptions.php | 1 +
.../_bundled/g7-core-ja/language-pack.json | 2 +-
.../CHANGELOG.md | 6 +
.../frontend/partial/admin/settings.json | 4 +-
.../language-pack.json | 2 +-
lang/en/exceptions.php | 1 +
lang/ko/exceptions.php | 1 +
.../_bundled/sirsoft-ecommerce/CHANGELOG.md | 6 +
.../_bundled/sirsoft-ecommerce/composer.json | 2 +-
.../_bundled/sirsoft-ecommerce/module.json | 2 +-
.../sirsoft-ecommerce/package-lock.json | 4 +-
.../_bundled/sirsoft-ecommerce/package.json | 2 +-
.../notificationDefinitionTypeLabels.test.tsx | 182 +++++++++++++++
.../lang/partial/en/admin/settings.json | 2 +
.../lang/partial/ko/admin/settings.json | 2 +
.../sirsoft-ecommerce/vendor-bundle.json | 6 +-
.../sirsoft-ecommerce/vendor-bundle.zip | Bin 435549 -> 435549 bytes
.../Extension/Vendor/VendorBundlerTest.php | 186 +++++++++++++++-
vendor-bundle.json | 6 +-
vendor-bundle.zip | Bin 14750349 -> 14750349 bytes
30 files changed, 620 insertions(+), 79 deletions(-)
create mode 100644 modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/notificationDefinitionTypeLabels.test.tsx
diff --git a/.env.example b/.env.example
index 2db6325b..74fb9072 100644
--- a/.env.example
+++ b/.env.example
@@ -3,7 +3,7 @@ APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
-APP_VERSION=7.0.3
+APP_VERSION=7.0.4
APP_LOCALE=ko
APP_FALLBACK_LOCALE=ko
diff --git a/.env.testing.example b/.env.testing.example
index 7a637226..94e4f911 100644
--- a/.env.testing.example
+++ b/.env.testing.example
@@ -3,7 +3,7 @@ APP_ENV=testing
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
-APP_VERSION=7.0.3
+APP_VERSION=7.0.4
APP_LOCALE=ko
APP_FALLBACK_LOCALE=ko
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dc3db24..00fa2103 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
+## [7.0.4] - 2026-07-11
+
+### Fixed
+
+- 외부 라이브러리를 쓰는 확장의 버전을 올린 뒤 그 확장을 업데이트하면 "번들 파일의 무결성 검증에 실패했습니다"라는 오류로 업데이트가 막히던 문제를 수정했습니다. 오류를 풀려면 업데이트가 필요한데 그 업데이트 자체가 막혀 빠져나올 수 없는 상태였습니다. 아울러 확장 버전만 올린 경우에도 번들 재빌드가 필요함을 `--check` 점검이 올바르게 알려주도록 바로잡았습니다. (이미 배포된 확장을 내려받아 쓰는 환경에는 영향이 없으며, 확장을 직접 개발·수정하는 경우에 해당합니다.)
+- Windows 개발 환경에서 코어 의존성 번들을 다시 만들 때, 마지막 정리 단계에서 진행이 멈춘 채 끝나지 않던 문제를 수정했습니다. 이제 번들 빌드가 정상적으로 완료됩니다. 아울러 빌드가 중간에 실패하더라도 기존 번들 파일이 손상되지 않도록 새 결과가 완성된 뒤에만 교체하도록 개선했습니다. (확장을 직접 개발·수정하는 환경에 해당하며, 배포본을 내려받아 쓰는 환경에는 영향이 없습니다.)
+
## [7.0.3] - 2026-07-10
### Fixed
diff --git a/INSTALL.md b/INSTALL.md
index ad65f5a1..573b2221 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -246,7 +246,7 @@ unzip g7-release.zip
# 압축 해제 결과 확인 — 루트 디렉토리가 g7이 아니면 이름 변경
ls -la
-# (필요 시) mv g7-7.0.3 g7
+# (필요 시) mv g7-7.0.4 g7
# ZIP 파일 정리 (선택)
rm g7-release.zip
diff --git a/README.md b/README.md
index 0c1abe3e..906de834 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/app/Extension/Vendor/EnvironmentDetector.php b/app/Extension/Vendor/EnvironmentDetector.php
index 574d8abf..91c1402a 100644
--- a/app/Extension/Vendor/EnvironmentDetector.php
+++ b/app/Extension/Vendor/EnvironmentDetector.php
@@ -71,7 +71,7 @@ class EnvironmentDetector
* → PATH 검색 → composer.phar
*
* @param string|null $hint 바이너리 경로 힌트 (null 이면 config/ENV/PATH 순으로 자동 탐색)
- * @return string|null 발견된 composer 바이너리 경로. 없으면 null
+ * @return string|null 발견된 composer 바이너리 경로. 없으면 null
*/
public function findComposerBinary(?string $hint = null): ?string
{
@@ -120,7 +120,7 @@ class EnvironmentDetector
* proc_open 사용 가능 + composer 바이너리 발견 + `composer --version` 종료 코드 0.
*
* @param string|null $hint composer 바이너리 경로 힌트 (캐시 우회용)
- * @return bool composer 가 현재 환경에서 실행 가능한지 여부
+ * @return bool composer 가 현재 환경에서 실행 가능한지 여부
*/
public function canExecuteComposer(?string $hint = null): bool
{
@@ -178,7 +178,7 @@ class EnvironmentDetector
* 지정 경로에 쓰기 가능한지 확인.
*
* @param string $targetPath vendor 디렉토리 경로 (존재하지 않아도 부모 디렉토리 쓰기 권한 검사)
- * @return bool 부모 디렉토리가 존재하고 쓰기 가능한지 여부
+ * @return bool 부모 디렉토리가 존재하고 쓰기 가능한지 여부
*/
public function canWriteVendor(string $targetPath): bool
{
@@ -254,9 +254,15 @@ class EnvironmentDetector
/**
* composer 실행 명령 문자열 구성.
*
- * @param array $args
+ * 반환된 문자열은 `proc_open(..., ['bypass_shell' => true])` 와 함께 사용해야 한다.
+ * Windows 에서 `bypass_shell` 없이 실행하면 `cmd.exe` 가 따옴표로 시작하는 `.bat`
+ * 경로의 앞뒤 따옴표를 벗겨 첫 인자(`install` 등)를 실행 파일로 오인한다.
+ *
+ * @param string $binary composer 바이너리 경로 (findComposerBinary 반환값)
+ * @param array $args composer 하위 명령과 옵션 (예: ['install', '--no-dev'])
+ * @return string proc_open 에 넘길 실행 명령 문자열
*/
- private function buildComposerCommand(string $binary, array $args): string
+ public function buildComposerCommand(string $binary, array $args): string
{
$escaped = array_map('escapeshellarg', $args);
diff --git a/app/Extension/Vendor/VendorBundler.php b/app/Extension/Vendor/VendorBundler.php
index 7084a4bc..8a3f5b6e 100644
--- a/app/Extension/Vendor/VendorBundler.php
+++ b/app/Extension/Vendor/VendorBundler.php
@@ -82,7 +82,7 @@ class VendorBundler
* 코어는 소스 = 출력 경로가 base_path() 로 동일합니다.
*
* @param bool $force manifest 해시 일치 여부와 무관하게 강제 재빌드
- * @return VendorBundleResult 번들 생성 결과 (zip 경로, manifest 경로, 패키지 수 등)
+ * @return VendorBundleResult 번들 생성 결과 (zip 경로, manifest 경로, 패키지 수 등)
*/
public function buildForCore(bool $force = false): VendorBundleResult
{
@@ -106,7 +106,7 @@ class VendorBundler
* @param string $type 확장 타입 ('module' 또는 'plugin')
* @param string $identifier 확장 식별자 (vendor-name 형식, 예: 'sirsoft-ecommerce')
* @param bool $force manifest 해시 일치 여부와 무관하게 강제 재빌드
- * @return VendorBundleResult 번들 생성 결과 (활성 디렉토리 부재 시 skipped=true)
+ * @return VendorBundleResult 번들 생성 결과 (활성 디렉토리 부재 시 skipped=true)
*/
public function buildForExtension(string $type, string $identifier, bool $force = false): VendorBundleResult
{
@@ -164,7 +164,7 @@ class VendorBundler
*
* @param string $sourcePath composer.json/lock 을 읽을 경로 (활성 디렉토리)
* @param string|null $outputPath 번들 출력 경로 (null 이면 sourcePath 와 동일 — 코어 케이스)
- * @return bool 번들이 stale 한지 여부 (해시 불일치 또는 manifest 부재). 외부 의존성이 없으면 항상 false
+ * @return bool 번들이 stale 한지 여부 (해시 불일치 또는 manifest 부재). 외부 의존성이 없으면 항상 false
*/
public function isStale(string $sourcePath, ?string $outputPath = null): bool
{
@@ -187,16 +187,19 @@ class VendorBundler
return true;
}
- if (file_exists($composerJsonPath)) {
- $currentHash = $this->integrityChecker->computeFileHash($composerJsonPath);
+ // 해시 비교 대상은 manifest 가 놓인 출력 디렉토리 기준 ({@see build()} 참조).
+ // 소스만 검사하면 _bundled 의 composer.json 변경을 up-to-date 로 오보한다.
+ $hashJsonPath = $this->resolveHashTarget($sourcePath, $outputPath, 'composer.json');
+ if ($hashJsonPath !== null) {
+ $currentHash = $this->integrityChecker->computeFileHash($hashJsonPath);
if (($manifest['composer_json_sha256'] ?? null) !== $currentHash) {
return true;
}
}
- $composerLockPath = $sourcePath.DIRECTORY_SEPARATOR.'composer.lock';
- if (file_exists($composerLockPath)) {
- $currentHash = $this->integrityChecker->computeFileHash($composerLockPath);
+ $hashLockPath = $this->resolveHashTarget($sourcePath, $outputPath, 'composer.lock');
+ if ($hashLockPath !== null) {
+ $currentHash = $this->integrityChecker->computeFileHash($hashLockPath);
if (($manifest['composer_lock_sha256'] ?? null) !== $currentHash) {
return true;
}
@@ -205,6 +208,35 @@ class VendorBundler
return false;
}
+ /**
+ * manifest 해시 비교 기준이 될 파일 경로를 결정합니다.
+ *
+ * manifest 와 zip 은 출력 디렉토리(_bundled)에 함께 저장되어 그대로 배포되며,
+ * 런타임 검증({@see VendorIntegrityChecker::verify()})은 zip 이 놓인 디렉토리
+ * (_bundled 또는 설치/업데이트 시 그 복사본인 _pending/staging)의 composer.json 을
+ * 대조합니다. 따라서 해시도 그 디렉토리를 기준으로 계산해야 생성자와 검증자의
+ * 기준이 일치합니다.
+ *
+ * 출력 디렉토리에 파일이 없으면 소스(활성 디렉토리)로 폴백합니다.
+ * 코어는 소스와 출력이 동일하므로 어느 쪽을 골라도 같은 경로가 됩니다.
+ *
+ * @param string $sourcePath composer.json/lock 과 vendor/ 를 읽는 경로 (활성 디렉토리)
+ * @param string $outputPath manifest 와 zip 을 쓰는 경로 (_bundled, 코어는 소스와 동일)
+ * @param string $filename 'composer.json' 또는 'composer.lock'
+ * @return string|null 해시 대상 파일의 절대 경로. 양쪽 모두 없으면 null
+ */
+ private function resolveHashTarget(string $sourcePath, string $outputPath, string $filename): ?string
+ {
+ $outputFile = $outputPath.DIRECTORY_SEPARATOR.$filename;
+ if (file_exists($outputFile)) {
+ return $outputFile;
+ }
+
+ $sourceFile = $sourcePath.DIRECTORY_SEPARATOR.$filename;
+
+ return file_exists($sourceFile) ? $sourceFile : null;
+ }
+
/**
* 빌드 실행.
*
@@ -258,20 +290,25 @@ class VendorBundler
);
}
- // 기존 파일 제거
- if (file_exists($zipPath)) {
- @unlink($zipPath);
- }
- if (file_exists($manifestPath)) {
- @unlink($manifestPath);
- }
-
// === 스테이징 빌드 ===
// 개발 머신의 vendor/ 를 직접 사용하지 않고, composer install --no-dev 를
// 스테이징 디렉토리에서 새로 실행하여 dev 의존성과 완전히 분리된 번들을 생성한다.
+ //
+ // 새 zip/manifest 는 임시 경로(.tmp)에 먼저 쓰고, 빌드가 성공한 뒤에만 원본을
+ // 교체한다. 기존 파일을 빌드 전에 삭제하면 composer install 실패 시 새 파일이
+ // 생성되지 못한 채 원본만 유실되어 이후 module:update 가 번들 부재로 실패한다.
$stagingDir = storage_path('app/vendor-bundle-staging/'.uniqid('build-', true));
File::ensureDirectoryExists($stagingDir, 0755);
+ $tmpZipPath = $zipPath.'.tmp';
+ $tmpManifestPath = $manifestPath.'.tmp';
+ if (file_exists($tmpZipPath)) {
+ @unlink($tmpZipPath);
+ }
+ if (file_exists($tmpManifestPath)) {
+ @unlink($tmpManifestPath);
+ }
+
try {
// 1. composer.json + composer.lock 을 스테이징으로 복사
File::copy($composerJsonPath, $stagingDir.DIRECTORY_SEPARATOR.'composer.json');
@@ -292,8 +329,8 @@ class VendorBundler
);
}
- // 4. zip 생성 (EXCLUDE_PATTERNS 만 적용 — 화이트리스트 불필요)
- [$zipSize, $fileCount] = $this->writeZip($stagedVendor, $zipPath);
+ // 4. zip 생성 — 임시 경로에 먼저 쓴다 (EXCLUDE_PATTERNS 만 적용)
+ [$zipSize, $fileCount] = $this->writeZip($stagedVendor, $tmpZipPath);
// 5. manifest 작성 — 스테이징의 composer.lock 을 기준으로 패키지 목록 추출
$stagedLockPath = $stagingDir.DIRECTORY_SEPARATOR.'composer.lock';
@@ -301,27 +338,35 @@ class VendorBundler
? $this->collectPackages($stagedLockPath)
: [];
+ // manifest 의 해시는 **출력 디렉토리** 의 composer.json/lock 기준.
+ // manifest·zip·composer.json 이 함께 배포되는 곳이 출력 디렉토리이며,
+ // 런타임 검증도 그 디렉토리를 대조한다 ({@see resolveHashTarget()}).
+ $hashJsonPath = $this->resolveHashTarget($sourcePath, $outputPath, 'composer.json');
+ $hashLockPath = $this->resolveHashTarget($sourcePath, $outputPath, 'composer.lock');
+
$manifest = [
'schema_version' => self::SCHEMA_VERSION,
'generated_at' => date('c'),
'generator' => 'g7 vendor-bundle:build',
'target' => $target,
- // manifest 의 해시는 **소스** 의 composer.json/lock 기준 — 런타임에서
- // 소스 디렉토리(또는 번들과 함께 배포된) composer.json 과 비교하여
- // stale 여부를 감지한다.
- 'composer_json_sha256' => $this->integrityChecker->computeFileHash($composerJsonPath),
- 'composer_lock_sha256' => file_exists($sourceLockPath)
- ? $this->integrityChecker->computeFileHash($sourceLockPath)
+ 'composer_json_sha256' => $this->integrityChecker->computeFileHash($hashJsonPath ?? $composerJsonPath),
+ 'composer_lock_sha256' => $hashLockPath !== null
+ ? $this->integrityChecker->computeFileHash($hashLockPath)
: null,
- 'zip_sha256' => $this->integrityChecker->computeFileHash($zipPath),
+ 'zip_sha256' => $this->integrityChecker->computeFileHash($tmpZipPath),
'zip_size' => $zipSize,
'package_count' => count($packages),
- 'php_requirement' => $this->extractPhpRequirement($composerJsonPath),
+ 'php_requirement' => $this->extractPhpRequirement($hashJsonPath ?? $composerJsonPath),
'g7_version' => config('app.version'),
'packages' => $packages,
];
- File::put($manifestPath, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+ File::put($tmpManifestPath, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+
+ // 6. 원자적 교체 — 여기까지 성공했으므로 임시 파일을 최종 경로로 옮긴다.
+ // 이 지점 이전에 실패하면 기존 번들이 그대로 보존된다.
+ $this->promoteAtomic($tmpZipPath, $zipPath);
+ $this->promoteAtomic($tmpManifestPath, $manifestPath);
Log::info('Vendor 번들 빌드 완료', [
'target' => $target,
@@ -340,10 +385,42 @@ class VendorBundler
reason: 'built',
);
} finally {
- // 스테이징 정리 (실패/성공 무관)
+ // 스테이징 및 미승격 임시 파일 정리 (실패/성공 무관)
if (File::isDirectory($stagingDir)) {
File::deleteDirectory($stagingDir);
}
+ if (file_exists($tmpZipPath)) {
+ @unlink($tmpZipPath);
+ }
+ if (file_exists($tmpManifestPath)) {
+ @unlink($tmpManifestPath);
+ }
+ }
+ }
+
+ /**
+ * 임시 파일을 최종 경로로 원자적으로 교체합니다.
+ *
+ * Windows 에서 `rename()` 은 대상이 이미 존재하면 실패하므로, 기존 파일을 먼저
+ * 제거한 뒤 이동한다. 이 메서드는 빌드가 성공적으로 완료된 뒤에만 호출되므로,
+ * 여기서 기존 파일을 제거해도 (제거 후 이동 실패 확률이 극히 낮아) 안전하다.
+ *
+ * @param string $tmpPath 임시 파일 경로 (성공적으로 생성 완료된 상태)
+ * @param string $finalPath 최종 경로
+ *
+ * @throws VendorInstallException 이동 실패 시
+ */
+ private function promoteAtomic(string $tmpPath, string $finalPath): void
+ {
+ if (file_exists($finalPath)) {
+ @unlink($finalPath);
+ }
+
+ if (! @rename($tmpPath, $finalPath)) {
+ throw new VendorInstallException(
+ errorKey: 'bundle_build_promote_failed',
+ context: ['from' => $tmpPath, 'to' => $finalPath],
+ );
}
}
@@ -377,30 +454,37 @@ class VendorBundler
);
}
- $phpBinary = config('process.php_binary', 'php');
-
- if (str_contains($binary, ' ')) {
- $composerCmd = $binary;
- } elseif (str_ends_with(strtolower($binary), '.phar')) {
- $composerCmd = escapeshellarg($phpBinary).' '.escapeshellarg($binary);
- } else {
- $composerCmd = escapeshellarg($binary);
- }
-
- $command = $composerCmd.' install --no-dev --no-scripts --prefer-dist --no-interaction --no-progress 2>&1';
+ // 명령 조립과 실행 옵션은 canExecuteComposer 와 동일한 방식으로 통일한다.
+ // buildComposerCommand + bypass_shell:true 를 함께 써야 Windows 에서 .bat
+ // 경로가 cmd.exe 의 따옴표 처리로 깨지지 않는다.
+ $command = $detector->buildComposerCommand($binary, [
+ 'install', '--no-dev', '--no-scripts', '--prefer-dist', '--no-interaction', '--no-progress',
+ ]);
Log::info('vendor-bundle 빌드: composer install 시작', [
'target' => $target,
'staging' => $stagingDir,
]);
- $descriptors = [
- 0 => ['pipe', 'r'],
- 1 => ['pipe', 'w'],
- 2 => ['pipe', 'w'],
- ];
+ // stdout/stderr 는 파이프가 아닌 파일 descriptor 로 받는다.
+ // (Windows 회귀) composer 는 autoload dump 단계에서 임시 추출물을
+ // `rmdir /S /Q` async `cmd.exe` 손자 프로세스로 정리하는데, stdout/stderr 를
+ // 파이프로 열면 이 손자들이 부모 파이프 핸들을 상속받아 붙잡은 채 종료하지 않아
+ // 부모(php)의 read/proc_close 가 자식 완료 신호를 영영 받지 못하고 무한 대기한다.
+ // 파일 descriptor 는 손자에게 상속될 열린 파이프 핸들이 없어 이 교착을 원천 차단한다
+ // (실측: 파이프=행, 파일=140s 완주 EXIT 0 + autoload 정상 생성).
+ $outPath = $stagingDir.DIRECTORY_SEPARATOR.'_composer_stdout.log';
+ $errPath = $stagingDir.DIRECTORY_SEPARATOR.'_composer_stderr.log';
+ $descriptors = self::composerOutputDescriptors($outPath, $errPath);
- $process = @proc_open($command, $descriptors, $pipes, $stagingDir, EnvironmentDetector::buildComposerEnv());
+ $process = @proc_open(
+ $command,
+ $descriptors,
+ $pipes,
+ $stagingDir,
+ EnvironmentDetector::buildComposerEnv(),
+ ['bypass_shell' => true],
+ );
if (! is_resource($process)) {
throw new VendorInstallException(
@@ -409,12 +493,17 @@ class VendorBundler
);
}
- fclose($pipes[0]);
- $output = stream_get_contents($pipes[1]);
- fclose($pipes[1]);
- fclose($pipes[2]);
+ // stdin(파이프)만 즉시 닫고, stdout/stderr 는 파일이므로 배수 불필요 —
+ // proc_close 가 자식 종료까지 블록 대기한다.
+ if (isset($pipes[0]) && is_resource($pipes[0])) {
+ fclose($pipes[0]);
+ }
$exitCode = proc_close($process);
+ $stdout = is_file($outPath) ? (string) @file_get_contents($outPath) : '';
+ $stderr = is_file($errPath) ? (string) @file_get_contents($errPath) : '';
+ $output = trim($stdout.\PHP_EOL.$stderr);
+
if ($exitCode !== 0) {
throw new VendorInstallException(
errorKey: 'bundle_build_composer_failed',
@@ -428,13 +517,34 @@ class VendorBundler
]);
}
+ /**
+ * composer install 의 stdout/stderr 를 받을 proc_open descriptor 배열을 구성합니다.
+ *
+ * stdout/stderr 를 파이프가 아닌 파일로 여는 이유는 runComposerInstall 주석 참조 —
+ * Windows 에서 composer autoload dump 의 async 손자 프로세스가 상속받는 파이프 핸들로
+ * 인한 무한 대기(행)를 원천 차단한다. 이 메서드는 순수 함수로, descriptor 구조를
+ * 회귀 테스트로 고정하기 위해 분리했다.
+ *
+ * @param string $outPath stdout 로그 파일 경로
+ * @param string $errPath stderr 로그 파일 경로
+ * @return array> proc_open 2번째 인자용 descriptor 배열
+ */
+ public static function composerOutputDescriptors(string $outPath, string $errPath): array
+ {
+ return [
+ 0 => ['pipe', 'r'],
+ 1 => ['file', $outPath, 'w'],
+ 2 => ['file', $errPath, 'w'],
+ ];
+ }
+
/**
* vendor/ 디렉토리를 재귀적으로 zip에 쓰기.
*
* 스테이징에서 composer install --no-dev 로 새로 생성된 vendor/ 를 대상으로 하므로
* 화이트리스트 필터링은 불필요하며, EXCLUDE_PATTERNS (tests/, docs/ 등) 만 적용한다.
*
- * @return array{0: int, 1: int} [zipSize, fileCount]
+ * @return array{0: int, 1: int} [zipSize, fileCount]
*/
private function writeZip(string $vendorPath, string $zipPath): array
{
diff --git a/config/app.php b/config/app.php
index 4885bc61..df842759 100644
--- a/config/app.php
+++ b/config/app.php
@@ -231,7 +231,7 @@ return [
|
*/
- 'version' => env('APP_VERSION', '7.0.3'),
+ 'version' => env('APP_VERSION', '7.0.4'),
/*
|--------------------------------------------------------------------------
diff --git a/docs/extension/vendor-bundle.md b/docs/extension/vendor-bundle.md
index 4a099b11..58a184fe 100644
--- a/docs/extension/vendor-bundle.md
+++ b/docs/extension/vendor-bundle.md
@@ -30,6 +30,28 @@ G7 코어와 모듈/플러그인은 PHP 의존성 관리에 Composer를 사용
이 네 파일(`composer.json` / `composer.lock` / `vendor-bundle.json` / `vendor-bundle.zip`)은 코어 루트 및 각 `_bundled` 확장 루트에 함께 존재하며 모두 Git 으로 추적한다. 추출 결과인 `vendor/` 디렉토리만 `.gitignore` 대상이다. `composer.lock` 이 누락되면 `VendorIntegrityChecker` 가 `composer_lock_sha256` 을 검증할 수 없으므로, `composer update` 직후 생성된 `composer.lock` 과 재빌드된 번들 두 파일을 같은 커밋에 함께 포함한다.
+### 1.1 소스와 출력의 분리
+
+확장의 번들 빌드는 읽는 곳과 쓰는 곳이 다르다.
+
+| 구분 | 경로 | 근거 |
+|------|------|------|
+| 소스 (`vendor/` 를 읽는 곳) | 활성 디렉토리 (`modules/{id}`) | `composer install` 이 실행되어 실제 패키지가 설치된 곳 |
+| 출력 (`vendor-bundle.{json,zip}` 을 쓰는 곳) | `_bundled` 디렉토리 | Git 추적 배포 아티팩트 저장 위치 (`_bundled` 오염 방지) |
+
+반면 manifest 에 기록하는 `composer_json_sha256` / `composer_lock_sha256` 은 **출력 디렉토리**의 `composer.json` / `composer.lock` 을 기준으로 계산한다 (`VendorBundler::resolveHashTarget()`). 출력 디렉토리에 해당 파일이 없으면 소스로 폴백하며, 코어는 소스와 출력이 동일하므로 결과가 같다.
+
+이유는 검증자의 대조 위치에 있다. `VendorIntegrityChecker::verify($sourceDir)` 의 `$sourceDir` 은 zip 과 manifest 가 놓인 디렉토리를 뜻하고, 실제 호출 경로는 모두 `_bundled` 계열을 넘긴다.
+
+| 경로 | verify 에 넘기는 디렉토리 |
+|------|--------------------------|
+| 신규 설치 | `_pending/{id}` (`_bundled` 복사본) |
+| 업데이트 | `_pending/{id}_updating_*` (`_bundled` 복사본) |
+| 코어 업데이트 | pending 경로 (소스 = 출력) |
+| `vendor-bundle:verify-all` | `_bundled/{id}` |
+
+해시를 소스(활성)에서 계산하면 생성 기준과 검증 기준이 어긋난다. 두 `composer.json` 의 내용이 같을 때만 우연히 통과하며, `_bundled` 의 `composer.json` 만 변경된 상태(버전 bump 직후 등)에서는 manifest 가 영구히 불일치하여 `module:update` 가 `composer_json_sha_mismatch` 로 차단된다. 그 오류를 해소할 유일한 명령이 `module:update` 이므로 순환이 발생한다. 같은 이유로 `isStale()` 도 출력 디렉토리를 검사해야 하며, 소스만 보면 `--check` 가 stale 을 `up-to-date` 로 오보한다.
+
`vendor-bundle.json` 스키마 (v1.0):
```json
@@ -212,11 +234,11 @@ Content-Type: application/json
### 7.1 SHA256 검증
-`VendorIntegrityChecker::verify()` 가 다음을 검증합니다:
+`VendorIntegrityChecker::verify($sourceDir)` 가 다음을 검증합니다. `$sourceDir` 은 zip 과 manifest 가 놓인 디렉토리이며, 확장의 경우 `_bundled` 또는 그 복사본(`_pending`)입니다 (§1.1).
- vendor-bundle.zip 파일의 실제 SHA256 vs manifest 기록 값
-- composer.json SHA256 (소스에 존재 시)
-- composer.lock SHA256 (소스에 존재 시)
+- composer.json SHA256 (해당 디렉토리에 존재 시)
+- composer.lock SHA256 (해당 디렉토리에 존재 시)
검증 실패 시 `VendorInstallException` 이 발생하며 설치가 중단됩니다.
@@ -230,6 +252,9 @@ Content-Type: application/json
## 8. 자동 트리거
`composer.json` 또는 `composer.lock` 파일이 변경되면 번들이 stale 상태가 됩니다.
+확장의 경우 판정 기준은 `_bundled` 디렉토리의 두 파일이다 (§1.1). 확장 버전을 올려
+`_bundled/composer.json` 의 `version` 만 바뀐 경우에도 stale 이 되므로 재빌드가 필요하다.
+
다음 명령으로 재빌드해야 합니다:
```bash
@@ -269,6 +294,8 @@ CI 통합 예시:
| `composer_not_available` | composer 모드 강제했으나 실행 불가 | --vendor-mode=auto 또는 bundled로 변경 |
| `no_vendor_strategy_available` | composer 불가 + 번들 없음 | vendor-bundle.zip 업로드 또는 호스팅 변경 |
| `bundle_contains_unsafe_path` | 외부 zip 파일이 zip slip 시도 | 신뢰할 수 있는 소스에서만 다운로드 |
+| `bundle_build_promote_failed` | 빌드 산출물을 최종 경로로 원자적 교체 실패 | 디스크 공간/권한 확인 후 재빌드 (원본 번들은 promoteAtomic 으로 보존됨) |
+| 빌드가 `Generating optimized autoload files` 직후 무한 대기 (Windows) | composer autoload dump 의 async 손자 프로세스가 부모 파이프 핸들을 상속·점유 | 코어 반영됨 — stdout/stderr 를 파이프가 아닌 파일 descriptor 로 실행 (`VendorBundler::composerOutputDescriptors`). 최신 코어에서는 재발하지 않음 |
## 11. 참고
diff --git a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md
index d59a08bd..ec217b20 100644
--- a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md
+++ b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md
@@ -4,6 +4,12 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
+## [1.0.3] - 2026-07-11
+
+### Added
+
+- vendor-bundle 빌드 결과 반영 실패 안내 메시지 일본어 번역 추가 (`exceptions.vendor.bundle_build_promote_failed`) — 빌드 산출물을 최종 경로로 원자적 교체하는 데 실패했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
+
## [1.0.2] - 2026-07-08
### Added
diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php b/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php
index cf6b007f..f77cd321 100644
--- a/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php
+++ b/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php
@@ -60,6 +60,7 @@ return [
'no_composer_lock' => 'composer.lockファイルが必要です。まずcomposer installを実行してください: :path',
'vendor_dir_not_found' => 'vendor/ディレクトリが存在しません。まずcomposer installを実行してください: :path',
'composer_binary_not_found' => 'Composerバイナリが見つかりません。COMPOSER_BINARY環境変数を設定するか、composerをPATHに追加してください。',
+ 'bundle_build_promote_failed' => 'vendor-bundle ビルド結果を最終パスに反映できませんでした (:from → :to)',
],
'concurrent_modification' => '別のユーザーが先に保存しました。(リソース: :resource、現在のバージョン: :current、送信されたバージョン: :expected)',
];
diff --git a/lang-packs/_bundled/g7-core-ja/language-pack.json b/lang-packs/_bundled/g7-core-ja/language-pack.json
index 0acc94e5..6f9a724c 100644
--- a/lang-packs/_bundled/g7-core-ja/language-pack.json
+++ b/lang-packs/_bundled/g7-core-ja/language-pack.json
@@ -12,7 +12,7 @@
"en": "G7 core Japanese language pack (bundled)",
"ja": "G7 コア 日本語 言語パック(バンドル)"
},
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"scope": "core",
"target_identifier": null,
diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md
index 00bebad6..b80766cb 100644
--- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md
+++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md
@@ -4,6 +4,12 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
+## [1.0.3] - 2026-07-10
+
+### Added
+
+- 알림 설정의 "무통장 입금 안내"·"배송 완료" 알림 이름에 대한 일본어 번역을 추가했습니다.
+
## [1.0.2] - 2026-07-10
### Removed
diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json
index 29a74807..fc0635e2 100644
--- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json
+++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json
@@ -458,7 +458,9 @@
"new_order_admin": "新規注文(管理者)",
"inquiry_received": "商品問い合わせ受け取り(管理者)",
"inquiry_replied": "商品問い合わせ回答",
- "mileage_expiring_soon": "マイレージ失効予定"
+ "mileage_expiring_soon": "マイレージ失効予定",
+ "order_pending_deposit": "銀行振込のご案内",
+ "order_delivered": "配送完了"
},
"btn_search": "検索",
"btn_reset_filter": "初期化",
diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json
index 814f617c..0cd77c21 100644
--- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json
+++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json
@@ -12,7 +12,7 @@
"en": "G7 module (sirsoft-ecommerce) Japanese language pack (bundled)",
"ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック(バンドル)"
},
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"scope": "module",
"target_identifier": "sirsoft-ecommerce",
diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php
index 4ffdea8f..7e2c0016 100644
--- a/lang/en/exceptions.php
+++ b/lang/en/exceptions.php
@@ -57,6 +57,7 @@ return [
'composer_not_available' => 'Composer cannot be executed in this environment. Use bundled mode instead.',
'composer_not_available_for_build' => 'Building vendor-bundle requires Composer execution. Install/configure Composer in the development environment and retry.',
'bundle_build_composer_failed' => 'composer install failed while building vendor-bundle (exit :exit): :message',
+ 'bundle_build_promote_failed' => 'failed to move vendor-bundle build result to its final path (:from → :to)',
'composer_execution_failed' => 'Composer execution failed: :message',
'bundle_zip_missing' => 'vendor-bundle.zip not found: :path',
'bundle_manifest_missing' => 'vendor-bundle.json not found: :path',
diff --git a/lang/ko/exceptions.php b/lang/ko/exceptions.php
index e9703807..61bef32e 100644
--- a/lang/ko/exceptions.php
+++ b/lang/ko/exceptions.php
@@ -57,6 +57,7 @@ return [
'composer_not_available' => 'Composer를 실행할 수 없는 환경입니다. 번들 모드를 사용하세요.',
'composer_not_available_for_build' => 'vendor-bundle 빌드는 Composer 실행이 필요합니다. 개발 환경에서 Composer를 설치/설정한 후 다시 시도하세요.',
'bundle_build_composer_failed' => 'vendor-bundle 빌드 중 composer install 실행 실패 (exit :exit): :message',
+ 'bundle_build_promote_failed' => 'vendor-bundle 빌드 결과를 최종 경로로 반영하지 못했습니다 (:from → :to)',
'composer_execution_failed' => 'Composer 실행에 실패했습니다: :message',
'bundle_zip_missing' => 'vendor-bundle.zip 파일을 찾을 수 없습니다: :path',
'bundle_manifest_missing' => 'vendor-bundle.json 파일을 찾을 수 없습니다: :path',
diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md
index 287f7454..777310d0 100644
--- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md
+++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md
@@ -4,6 +4,12 @@
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
+## [1.0.3] - 2026-07-10
+
+### Fixed
+
+- 쇼핑몰 환경설정 > 알림 설정에서 "무통장 입금 안내"와 "배송 완료" 두 알림의 이름이 사람이 읽을 수 없는 내부 문자열로 표시되던 문제를 수정했습니다. 이제 다른 알림과 동일하게 이름이 표시되며, 알림을 기본값으로 되돌릴 때 뜨는 확인창과 알림 편집 화면에서도 함께 바로잡혔습니다.
+
## [1.0.2] - 2026-07-10
### Changed
diff --git a/modules/_bundled/sirsoft-ecommerce/composer.json b/modules/_bundled/sirsoft-ecommerce/composer.json
index d7af07c0..cc0f1bb7 100644
--- a/modules/_bundled/sirsoft-ecommerce/composer.json
+++ b/modules/_bundled/sirsoft-ecommerce/composer.json
@@ -2,7 +2,7 @@
"name": "modules/sirsoft-ecommerce",
"description": "Ecommerce module for Gnuboard7",
"type": "library",
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"autoload": {
"psr-4": {
diff --git a/modules/_bundled/sirsoft-ecommerce/module.json b/modules/_bundled/sirsoft-ecommerce/module.json
index 8463b86d..673f89fa 100644
--- a/modules/_bundled/sirsoft-ecommerce/module.json
+++ b/modules/_bundled/sirsoft-ecommerce/module.json
@@ -5,7 +5,7 @@
"ko": "이커머스",
"en": "Ecommerce"
},
- "version": "1.0.2",
+ "version": "1.0.3",
"license": "MIT",
"description": {
"ko": "그누보드7 이커머스 모듈 - 상품, 주문, 결제 관리",
diff --git a/modules/_bundled/sirsoft-ecommerce/package-lock.json b/modules/_bundled/sirsoft-ecommerce/package-lock.json
index dc9901a7..33a0593a 100644
--- a/modules/_bundled/sirsoft-ecommerce/package-lock.json
+++ b/modules/_bundled/sirsoft-ecommerce/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@g7/sirsoft-ecommerce",
- "version": "1.0.2",
+ "version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@g7/sirsoft-ecommerce",
- "version": "1.0.2",
+ "version": "1.0.3",
"devDependencies": {
"jsdom": "^27.4.0",
"typescript": "^5.3.3",
diff --git a/modules/_bundled/sirsoft-ecommerce/package.json b/modules/_bundled/sirsoft-ecommerce/package.json
index 08a25b1e..88de7fcf 100644
--- a/modules/_bundled/sirsoft-ecommerce/package.json
+++ b/modules/_bundled/sirsoft-ecommerce/package.json
@@ -1,6 +1,6 @@
{
"name": "@g7/sirsoft-ecommerce",
- "version": "1.0.2",
+ "version": "1.0.3",
"description": "그누보드7 이커머스 모듈 프론트엔드 에셋",
"private": true,
"type": "module",
diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/notificationDefinitionTypeLabels.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/notificationDefinitionTypeLabels.test.tsx
new file mode 100644
index 00000000..e07d3e06
--- /dev/null
+++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/notificationDefinitionTypeLabels.test.tsx
@@ -0,0 +1,182 @@
+// e2e:allow 라벨 데이터(settings.json) + Vitest 렌더 회귀 테스트만 변경 — 레이아웃 동작/구조 무변경. 알림 정의 화면의 실제 렌더 경로($t: 동적 키 해석)는 본 render 테스트가 커버하고, 화면 수준 검증은 별도 브라우저 실측(handoff §10)으로 확인.
+/**
+ * @file notificationDefinitionTypeLabels.test.tsx
+ * @description 이커머스 알림 정의 화면 — 타입 라벨 다국어 키 raw 노출 회귀 가드.
+ *
+ * 배경: `order_pending_deposit`, `order_delivered` 두 알림 타입이 module.php 에는
+ * 정의됐으나 라벨을 읽는 settings.json 의 `notification_definitions.types` 에 이름이
+ * 등록되지 않아, 알림 정의 화면에서
+ * `sirsoft-ecommerce.admin.settings.notification_definitions.types.order_pending_deposit`
+ * 같은 raw 키가 그대로 노출됐다.
+ *
+ * 본 테스트는 _tab_notification_definitions.json 이 사용하는 실제 바인딩
+ * `$t:sirsoft-ecommerce.admin.settings.notification_definitions.types.{{def.type}}` 을
+ * 재현하고, **실제 settings.json** 라벨을 주입해 두 타입이 raw 키가 아닌 사람이 읽는
+ * 라벨로 렌더되는지 검증한다. settings.json 에서 어느 한 키가 제거되면 이 테스트가
+ * 다시 red 로 떨어져 회귀를 차단한다.
+ */
+
+import React from 'react';
+import { describe, it, expect, afterEach, vi } from 'vitest';
+import {
+ createLayoutTest,
+ createMockComponentRegistryWithBasics,
+ screen,
+ type MockComponentRegistry,
+} from '@core/template-engine/__tests__/utils/layoutTestUtils';
+
+import settingsKo from '../../../lang/partial/ko/admin/settings.json';
+import settingsEn from '../../../lang/partial/en/admin/settings.json';
+
+// 실제 화면(_tab_notification_definitions.json line ~371)의 타입 라벨 바인딩을 그대로 축약 재현.
+// 알림 목록 항목마다 H4 에 `$t:...types.{{def.type}}` 로 이름을 표시한다.
+const LABEL_BINDING =
+ '$t:sirsoft-ecommerce.admin.settings.notification_definitions.types.{{def.type}}';
+
+function makeLayout() {
+ return {
+ name: 'notification_definition_type_labels_probe',
+ version: '1.0.0',
+ components: [
+ {
+ type: 'basic',
+ name: 'Div',
+ children: [
+ {
+ type: 'basic',
+ name: 'Div',
+ iteration: {
+ source: '{{ecommerceNotificationDefinitions?.data?.data ?? []}}',
+ item_var: 'def',
+ index_var: 'defIdx',
+ },
+ children: [
+ {
+ type: 'basic',
+ name: 'H4',
+ text: LABEL_BINDING,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+}
+
+// 실제 서빙 데이터를 모사한 알림 정의 목록 — 문제의 두 타입 포함.
+const DEFINITIONS = [
+ { id: 1, type: 'order_pending_deposit', templates: [], variables: [] },
+ { id: 2, type: 'order_delivered', templates: [], variables: [] },
+];
+
+// settings.json 은 `notification_definitions` 를 admin.settings 하위가 아닌 파일 루트에 둔다.
+// 화면 바인딩은 `sirsoft-ecommerce.admin.settings.notification_definitions...` 네임스페이스를
+// 쓰므로, 주입 시 admin.settings 아래로 매핑한다(활성 언어팩 로더의 병합 결과와 동일 구조).
+function buildTranslations(settings: Record) {
+ return {
+ 'sirsoft-ecommerce': {
+ admin: {
+ settings: settings,
+ },
+ },
+ };
+}
+
+let registry: MockComponentRegistry;
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('알림 정의 타입 라벨 — 다국어 키 raw 노출 회귀 가드', () => {
+ it('ko: order_pending_deposit / order_delivered 가 라벨로 렌더된다 (raw 키 미노출)', async () => {
+ registry = createMockComponentRegistryWithBasics();
+ const { render, cleanup } = createLayoutTest(makeLayout(), {
+ componentRegistry: registry as any,
+ locale: 'ko',
+ translations: buildTranslations(settingsKo),
+ initialData: {
+ ecommerceNotificationDefinitions: { data: { data: DEFINITIONS } },
+ },
+ });
+
+ await render();
+
+ expect(screen.getByText('무통장 입금 안내')).toBeInTheDocument();
+ expect(screen.getByText('배송 완료')).toBeInTheDocument();
+
+ // raw 키가 화면에 남아있으면 회귀
+ expect(
+ screen.queryByText(/notification_definitions\.types\.order_pending_deposit/),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/notification_definitions\.types\.order_delivered/),
+ ).not.toBeInTheDocument();
+
+ cleanup();
+ });
+
+ it('en: order_pending_deposit / order_delivered 가 라벨로 렌더된다 (raw 키 미노출)', async () => {
+ registry = createMockComponentRegistryWithBasics();
+ const { render, cleanup } = createLayoutTest(makeLayout(), {
+ componentRegistry: registry as any,
+ locale: 'en',
+ translations: buildTranslations(settingsEn),
+ initialData: {
+ ecommerceNotificationDefinitions: { data: { data: DEFINITIONS } },
+ },
+ });
+
+ await render();
+
+ expect(screen.getByText('Bank Transfer Payment Guide')).toBeInTheDocument();
+ expect(screen.getByText('Order Delivered')).toBeInTheDocument();
+
+ expect(
+ screen.queryByText(/notification_definitions\.types\.order_pending_deposit/),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/notification_definitions\.types\.order_delivered/),
+ ).not.toBeInTheDocument();
+
+ cleanup();
+ });
+
+ it('라벨 제거 시 raw 키가 노출됨 — 회귀 가드가 실제로 결함을 감지한다', async () => {
+ // 이 테스트는 위 두 가드가 "우연히 통과"하는 것이 아님을 증명한다:
+ // settings.json 에서 두 타입 라벨을 제거하면 화면에 raw 키가 그대로 노출된다.
+ registry = createMockComponentRegistryWithBasics();
+ const stripped = JSON.parse(JSON.stringify(settingsKo));
+ delete stripped.notification_definitions.types.order_pending_deposit;
+ delete stripped.notification_definitions.types.order_delivered;
+
+ const { render, cleanup } = createLayoutTest(makeLayout(), {
+ componentRegistry: registry as any,
+ locale: 'ko',
+ translations: buildTranslations(stripped),
+ initialData: {
+ ecommerceNotificationDefinitions: { data: { data: DEFINITIONS } },
+ },
+ });
+
+ await render();
+
+ expect(screen.queryByText('무통장 입금 안내')).not.toBeInTheDocument();
+ expect(
+ screen.getByText(/notification_definitions\.types\.order_pending_deposit/),
+ ).toBeInTheDocument();
+
+ cleanup();
+ });
+
+ it('settings.json(ko/en) 의 types 에 두 신규 타입 라벨이 존재한다', () => {
+ const koTypes = (settingsKo as any).notification_definitions.types;
+ const enTypes = (settingsEn as any).notification_definitions.types;
+
+ expect(koTypes.order_pending_deposit).toBe('무통장 입금 안내');
+ expect(koTypes.order_delivered).toBe('배송 완료');
+ expect(enTypes.order_pending_deposit).toBe('Bank Transfer Payment Guide');
+ expect(enTypes.order_delivered).toBe('Order Delivered');
+ });
+});
diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json
index 33d6470d..13a76973 100644
--- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json
+++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json
@@ -447,7 +447,9 @@
"save_error": "Failed to save notification settings.",
"types": {
"order_confirmed": "Order Confirmed",
+ "order_pending_deposit": "Bank Transfer Payment Guide",
"order_shipped": "Order Shipped",
+ "order_delivered": "Order Delivered",
"order_completed": "Purchase Completed",
"order_cancelled": "Order Cancelled",
"new_order_admin": "New Order (Admin)",
diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json
index 975c5e24..b6993c20 100644
--- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json
+++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json
@@ -447,7 +447,9 @@
"save_error": "알림 설정 저장에 실패했습니다.",
"types": {
"order_confirmed": "주문 확인",
+ "order_pending_deposit": "무통장 입금 안내",
"order_shipped": "배송 시작",
+ "order_delivered": "배송 완료",
"order_completed": "구매 확정",
"order_cancelled": "주문 취소",
"new_order_admin": "신규 주문 (관리자)",
diff --git a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json
index 42c229a6..a0448d27 100644
--- a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json
+++ b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json
@@ -1,11 +1,11 @@
{
"schema_version": "1.0",
- "generated_at": "2026-07-10T12:05:27+00:00",
+ "generated_at": "2026-07-10T20:22:38+00:00",
"generator": "g7 vendor-bundle:build",
"target": "module:sirsoft-ecommerce",
- "composer_json_sha256": "aa8f74063b7f9cd969cb4effb120e3c50bb944a4d3b2e16fae7c31bd8232fce8",
+ "composer_json_sha256": "5195d789bc9edd0b5140c42aeedd7c87be67c7252b477a879ff90607b4787935",
"composer_lock_sha256": "876ca9c2273a33baff878d25050a567018a946412db053930a7f548c4add595d",
- "zip_sha256": "ef77515e4d920a52a8adba1e6d7e310389c6f3f459586d3cd5c24749f24d7fb0",
+ "zip_sha256": "c1767e8eca61163667e9289a3963454bab92806ce4e8a3651777f1e1aea14081",
"zip_size": 435549,
"package_count": 1,
"php_requirement": "^8.2",
diff --git a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip
index 0f7ac28fcbfbcd3d6338aed5a9f5728ac80fbe83..a823e196546fdb95530a316d1dbe6992d69c266d 100644
GIT binary patch
delta 1438
zcmZ8geNa?Y6yJUC-MhQME3=e~39
zvktpkhuxKFTUkymVI*>iY3;9l%HO0czv`Ezfv^TKKMAWr#`=rGvz!eGSEK;K;Dz%U
zU~1*rt5_rNoqnR5b>%_UUp>zxgh?fm)tp4)DItY6N|5!
z2X&|XaLN{8Jvia!^I@$@(G}fUue0y!NLmOZe%i#u`<1Hsj^){Y^DCQwMgsS-M8|DayOit;V(u-wk(3Mh6G}G;Gf?z!x
z9;!6k@0-(03d)bJ>&+dX9I5r5*_=0T4O{%;>bsjV);k1CNX3>z>z9U76XI`P!6TON
zgy}P1Yuja`4Lfjq&+|-3;y0n&jD@b7<8H|Ic-)JBJ9VVJy252<4c5E*U-x#8b-B2=
zD(!HXDSwA@_Vy*#?rk@HmeV+H=p9g9DK7t5)tC8W!&&b~QxZCC_iS~Bo;L^G630><
z5&*>ckN{2qdh@!CKxo2K6ae-|Gvb_zW6oy?42hJXIDPDCrJ<=<`6B^c8SN7#=d!BTL6
zZ3_Ym|6ROAhaLa(HGE6lp9E()!*Pv(s+zb&m;)W{=34z9Mz=iR6tr)IJF)?6e0%o8
z3NyF
zZ300B;$cB)ARVxv7XW-^LFoY0R~QUpM^UPlpd14+#kGJ+Pkh&1%z&!`k;M2a1-hRW_BU)hSf
z8zP$*(Qy{g>fe#h2}J23-g^<@Pv8yqNAr2IqnSt9=J|i_PIv!Ytn%5NQ;a0zTFzYT{25D-iq
H_$}Grpg>R&
delta 1438
zcmZ8gdr(wW7~lIiXP14jf;(y~OCS?U4g)Q?B+6sMl2;ZUsq?&O%cnOCjmN$rF+VX!6k(b>b!j~`xOemJ6`2b(@zw)|R~&JdY;
z{Kbi|jeWPKyjO1N@$TAI8|r@YFq*BXS6@8ZL0|5+J^b#fX~*EelEi&+m11h#(yt5H
zJU$dwM2hL!%B9C%i;E9QvlUgT{Iw2;<&~TtTUu5J|Md33`2}agrm~mYHeBpG=}eHe
z>QkN{O@7bS*<|!vz5m`PcjhQth5SrdYsBfEnvM?=jYQYf+TLR}U%l3+qeYio*Xkb@
zs<=L}E>nL_JGO4p?eC0^A!qfx&U`RP+7t45!TR&l+YI^$*}O+dlG!q+R5VE%E2CEG
zzbptozE^&0f_R{OP>|dg>uCJOCjU97FwnJq>-Wm~+-IiLc~@gqLaS>7W`(hQ7U&N|
zTcAJ91Iw?$5kROz3@!k+hm*pb@*>-|@C*|wMtV!sAw{RL?khhCaMNO5?2nT0tGM2
zz=)2iN+%SIw*tGeH?jyq`9BL&ya(x+{xU*U7PpIF<93k)FF+Nq17r#m{`3GDE(LVD
zmHa*O-y=j$lM4366dCISYPf`YQwyjjj2egmAD2YW81t~4>CeZQJyk*-4FyZ~DXKmI
zEZlc-788~|^EF&cJe~t*AxYEKfQoDA7?=ZlxtX@Ecp6=IpLSu}FVQ7y!N#>`JIsLL
zLb=pS5N4f=5G#~RN
z$l3+8j{@X-2bts`5(S+148q^S9p*!`!7(hwlTd{{C&xy(#%!n(r^i8*H3`2DHk^!a
z3gW#~?D8~+nq8iO{}2R4Caw}tQx?7gRoR{S*o+_nS6?H9`IvAfP~|4F
zP>f9i+E9u!gaA1D`EtDVKV_pUakJ1IE>_LbU*H)*Ew*x7Sq;vIEz4~~-~bE=gAKSs
F{14o6Z#e(}
diff --git a/tests/Unit/Extension/Vendor/VendorBundlerTest.php b/tests/Unit/Extension/Vendor/VendorBundlerTest.php
index 8bcb3416..6fcb0d5a 100644
--- a/tests/Unit/Extension/Vendor/VendorBundlerTest.php
+++ b/tests/Unit/Extension/Vendor/VendorBundlerTest.php
@@ -2,8 +2,10 @@
namespace Tests\Unit\Extension\Vendor;
+use App\Extension\Vendor\EnvironmentDetector;
use App\Extension\Vendor\Exceptions\VendorInstallException;
use App\Extension\Vendor\VendorBundler;
+use App\Extension\Vendor\VendorBundleResult;
use App\Extension\Vendor\VendorIntegrityChecker;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
@@ -79,7 +81,7 @@ class VendorBundlerTest extends TestCase
*
* 테스트는 단일 testDir 을 소스/출력 동일하게 사용 (코어 케이스와 유사).
*/
- private function invokeBuild(string $target, bool $force = false, ?string $sourcePath = null, ?string $outputPath = null): \App\Extension\Vendor\VendorBundleResult
+ private function invokeBuild(string $target, bool $force = false, ?string $sourcePath = null, ?string $outputPath = null): VendorBundleResult
{
$reflection = new \ReflectionClass($this->bundler);
$method = $reflection->getMethod('build');
@@ -239,6 +241,157 @@ class VendorBundlerTest extends TestCase
$this->assertFileDoesNotExist($sourceDir.'/vendor-bundle.json');
}
+ /**
+ * 분리 구조에서 manifest 의 composer 해시는 **출력(_bundled)** 의 composer.json 기준이어야 합니다.
+ *
+ * 런타임 검증(VendorIntegrityChecker::verify)은 manifest 와 같은 디렉토리(_bundled 또는 그 복사본인
+ * staging)의 composer.json 을 대조한다. 해시를 활성 디렉토리에서 계산하면, _bundled 의 composer.json
+ * 만 변경된 상태(예: 버전 bump 직후)에서 manifest 가 영구히 불일치하여 module:update 가 막힌다.
+ */
+ public function test_build_hashes_output_composer_json_not_source(): void
+ {
+ $sourceDir = $this->testDir.'/active';
+ $outputDir = $this->testDir.'/bundled';
+ File::ensureDirectoryExists($sourceDir);
+ File::ensureDirectoryExists($outputDir);
+
+ // 활성 — 구버전 (아직 update 가 반영되지 않은 상태)
+ File::put($sourceDir.'/composer.json', json_encode([
+ 'name' => 'test/project',
+ 'version' => '1.0.2',
+ 'require' => ['php' => '^8.2', 'test/lib' => '^1.0'],
+ ]));
+ File::put($sourceDir.'/composer.lock', json_encode([
+ 'packages' => [
+ ['name' => 'test/lib', 'version' => '1.0.0', 'type' => 'library'],
+ ],
+ ]));
+
+ // _bundled — 신버전 (작업자가 version bump 한 상태)
+ File::put($outputDir.'/composer.json', json_encode([
+ 'name' => 'test/project',
+ 'version' => '1.0.3',
+ 'require' => ['php' => '^8.2', 'test/lib' => '^1.0'],
+ ]));
+
+ $this->invokeBuild('test:split-hash', sourcePath: $sourceDir, outputPath: $outputDir);
+
+ $manifest = json_decode(File::get($outputDir.'/vendor-bundle.json'), true);
+
+ $this->assertSame(
+ $this->checker->computeFileHash($outputDir.'/composer.json'),
+ $manifest['composer_json_sha256'],
+ 'manifest 의 composer_json_sha256 은 출력(_bundled) 의 composer.json 해시여야 합니다.'
+ );
+
+ // 검증자가 outputDir 을 sourceDir 로 verify 할 때 통과해야 한다 (module:update 경로 재현)
+ $this->assertTrue(
+ $this->checker->verify($outputDir)->valid,
+ 'build 직후 _bundled 를 verify 하면 무결성 검증을 통과해야 합니다.'
+ );
+ }
+
+ /**
+ * 분리 구조에서 isStale 은 **출력(_bundled)** 의 composer.json 변경을 감지해야 합니다.
+ *
+ * 활성 디렉토리만 검사하면 _bundled 의 stale 을 up-to-date 로 오보하여, --check 가 재빌드 필요를
+ * 놓치고 이후 module:update 가 무결성 오류로 실패한다.
+ */
+ public function test_is_stale_detects_output_composer_json_change(): void
+ {
+ $sourceDir = $this->testDir.'/active';
+ $outputDir = $this->testDir.'/bundled';
+ File::ensureDirectoryExists($sourceDir);
+ File::ensureDirectoryExists($outputDir);
+
+ $composerJson = json_encode([
+ 'name' => 'test/project',
+ 'version' => '1.0.2',
+ 'require' => ['php' => '^8.2', 'test/lib' => '^1.0'],
+ ]);
+ File::put($sourceDir.'/composer.json', $composerJson);
+ File::put($sourceDir.'/composer.lock', json_encode([
+ 'packages' => [
+ ['name' => 'test/lib', 'version' => '1.0.0', 'type' => 'library'],
+ ],
+ ]));
+ File::put($outputDir.'/composer.json', $composerJson);
+
+ $this->invokeBuild('test:split-stale', sourcePath: $sourceDir, outputPath: $outputDir);
+ $this->assertFalse(
+ $this->bundler->isStale($sourceDir, $outputDir),
+ '빌드 직후에는 stale 이 아니어야 합니다.'
+ );
+
+ // _bundled 의 composer.json 만 변경 (활성은 그대로) — 버전 bump 시나리오
+ File::put($outputDir.'/composer.json', json_encode([
+ 'name' => 'test/project',
+ 'version' => '1.0.3',
+ 'require' => ['php' => '^8.2', 'test/lib' => '^1.0'],
+ ]));
+
+ $this->assertTrue(
+ $this->bundler->isStale($sourceDir, $outputDir),
+ '_bundled 의 composer.json 이 변경되면 stale 로 판정해야 합니다.'
+ );
+ }
+
+ /**
+ * 재빌드가 실패하면 기존 번들 파일이 보존되어야 합니다.
+ *
+ * 기존 zip/manifest 를 staging 빌드 이전에 삭제하면, composer install 실패 시
+ * 새 파일이 생성되지 못한 채 원본만 유실되어 다음 module:update 가 번들 부재로
+ * 실패한다 (Windows 등에서 composer 실행이 불안정할 때 재현). 삭제는 재생성
+ * 성공이 확정된 뒤에 이뤄져야 한다.
+ */
+ public function test_failed_rebuild_preserves_existing_bundle(): void
+ {
+ $this->createFakeProject();
+
+ // 1차 빌드 성공 — 정상 번들 생성
+ $this->invokeBuild('test:project');
+ $this->assertFileExists($this->testDir.'/vendor-bundle.zip');
+ $this->assertFileExists($this->testDir.'/vendor-bundle.json');
+
+ $originalZip = File::get($this->testDir.'/vendor-bundle.zip');
+ $originalManifest = File::get($this->testDir.'/vendor-bundle.json');
+
+ // composer install 이 실패하도록 러너 교체
+ $this->bundler->setComposerInstallRunner(function (): void {
+ throw new VendorInstallException(
+ errorKey: 'bundle_build_composer_failed',
+ context: ['exit' => 1, 'message' => 'simulated composer failure'],
+ );
+ });
+
+ // 2차 빌드(force) — 실패해야 하지만 기존 번들은 보존되어야 함
+ try {
+ $this->invokeBuild('test:project', force: true);
+ $this->fail('composer install 실패 시 빌드는 예외를 던져야 합니다.');
+ } catch (VendorInstallException) {
+ // 예상된 실패
+ }
+
+ $this->assertFileExists(
+ $this->testDir.'/vendor-bundle.zip',
+ '재빌드 실패 시 기존 zip 이 보존되어야 합니다.'
+ );
+ $this->assertFileExists(
+ $this->testDir.'/vendor-bundle.json',
+ '재빌드 실패 시 기존 manifest 가 보존되어야 합니다.'
+ );
+ $this->assertSame(
+ $originalZip,
+ File::get($this->testDir.'/vendor-bundle.zip'),
+ '보존된 zip 은 원본과 동일해야 합니다.'
+ );
+ $this->assertSame(
+ $originalManifest,
+ File::get($this->testDir.'/vendor-bundle.json'),
+ '보존된 manifest 는 원본과 동일해야 합니다.'
+ );
+ }
+
/**
* 외부 의존성 없는 확장은 isStale 이 항상 false 를 반환해야 합니다.
*/
@@ -306,7 +459,7 @@ class VendorBundlerTest extends TestCase
*/
public function test_build_throws_when_composer_not_available(): void
{
- $detector = \Mockery::mock(\App\Extension\Vendor\EnvironmentDetector::class);
+ $detector = \Mockery::mock(EnvironmentDetector::class);
$detector->shouldReceive('canExecuteComposer')->andReturn(false);
$bundler = new VendorBundler($this->checker, $detector);
@@ -321,4 +474,33 @@ class VendorBundlerTest extends TestCase
$method->setAccessible(true);
$method->invoke($bundler, $this->testDir, $this->testDir, 'test:no-composer', false);
}
+
+ /**
+ * composer install 의 stdout/stderr 는 파이프가 아닌 파일 descriptor 로 열려야 한다.
+ *
+ * (Windows 회귀 가드) stdout/stderr 를 파이프로 열면 composer autoload dump 단계의
+ * async `cmd.exe` 손자 프로세스가 부모 파이프 핸들을 상속·점유해 proc_close 가
+ * 자식 완료 신호를 받지 못하고 무한 대기(행)한다. 실측: 파이프=행, 파일=140s 완주.
+ * descriptor 구조를 파이프로 되돌리면 이 테스트가 red 로 회귀를 차단한다.
+ */
+ public function test_composer_output_descriptors_use_files_not_pipes(): void
+ {
+ $descriptors = VendorBundler::composerOutputDescriptors('/tmp/out.log', '/tmp/err.log');
+
+ // stdin 은 파이프(입력 없음 즉시 닫힘) 로 유지.
+ $this->assertSame(['pipe', 'r'], $descriptors[0]);
+
+ // stdout/stderr 는 파일 descriptor 여야 한다 (파이프 금지 — 손자 핸들 상속 차단).
+ $this->assertSame('file', $descriptors[1][0], 'stdout 은 파일 descriptor 여야 함 (파이프 시 Windows 행 회귀)');
+ $this->assertSame('/tmp/out.log', $descriptors[1][1]);
+ $this->assertSame('w', $descriptors[1][2]);
+
+ $this->assertSame('file', $descriptors[2][0], 'stderr 은 파일 descriptor 여야 함 (파이프 시 Windows 행 회귀)');
+ $this->assertSame('/tmp/err.log', $descriptors[2][1]);
+ $this->assertSame('w', $descriptors[2][2]);
+
+ // 어떤 채널도 'pipe','w' 로 열리지 않아야 함 (손자 상속 가능한 열린 파이프 부재).
+ $this->assertNotSame('pipe', $descriptors[1][0]);
+ $this->assertNotSame('pipe', $descriptors[2][0]);
+ }
}
diff --git a/vendor-bundle.json b/vendor-bundle.json
index 5bc88ef4..3e5c26e5 100644
--- a/vendor-bundle.json
+++ b/vendor-bundle.json
@@ -1,15 +1,15 @@
{
"schema_version": "1.0",
- "generated_at": "2026-06-30T04:00:54+00:00",
+ "generated_at": "2026-07-10T21:33:36+00:00",
"generator": "g7 vendor-bundle:build",
"target": "core",
"composer_json_sha256": "76294945639dbbe4b6d054c0aea4b78377a55bcdffcd1c9136beb294bc7cfc90",
"composer_lock_sha256": "b4d173c40088a78c9fe42b3ca6d23436bee6cc850a17bb653cbc1aca59ca3a81",
- "zip_sha256": "0a11e8d918f58465d231bfe2ba84a0d9f749112ca4b79e19ac0224145abc0ef1",
+ "zip_sha256": "d5d21fdd930350332d2d58e5ab386ba9203795f72f7767eb3c214d4702152245",
"zip_size": 14750349,
"package_count": 104,
"php_requirement": "^8.2",
- "g7_version": "7.0.0",
+ "g7_version": "7.0.3",
"packages": [
{
"name": "aws/aws-crt-php",
diff --git a/vendor-bundle.zip b/vendor-bundle.zip
index 761e8d31c8b5e3eb6345a7da750db662660aff3f..2bffbd055a8e08d0efc0fa4a9a0f1caf6a748d46 100644
GIT binary patch
delta 229591
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