diff --git a/CHANGELOG.md b/CHANGELOG.md index 1517bfdb..04df1e86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [7.0.8] - 2026-08-21 +## [7.0.8] - 2026-08-22 ### Added diff --git a/app/Rules/ValidOutboundProxyUrl.php b/app/Rules/ValidOutboundProxyUrl.php index 195060ee..bf5279b8 100644 --- a/app/Rules/ValidOutboundProxyUrl.php +++ b/app/Rules/ValidOutboundProxyUrl.php @@ -33,7 +33,9 @@ class ValidOutboundProxyUrl implements ValidationRule } if (! is_string($value)) { - $fail(__('validation.settings.outbound_proxy_invalid')); + $fail(__('validation.settings.outbound_proxy_invalid', [ + 'schemes' => implode(', ', OutboundProxy::ALLOWED_SCHEMES), + ])); return; } diff --git a/app/Services/ModuleService.php b/app/Services/ModuleService.php index 28ad15d1..7d765025 100644 --- a/app/Services/ModuleService.php +++ b/app/Services/ModuleService.php @@ -783,6 +783,13 @@ class ModuleService } throw $e; } + } catch (ModuleOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest + // 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진 + // catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존. + throw new ModuleOperationException('modules.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); @@ -836,6 +843,12 @@ class ModuleService } throw $e; } + } catch (ModuleOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드 + // 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존. + throw new ModuleOperationException('modules.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); diff --git a/app/Services/PluginService.php b/app/Services/PluginService.php index 2af946db..3767f183 100644 --- a/app/Services/PluginService.php +++ b/app/Services/PluginService.php @@ -899,6 +899,13 @@ class PluginService } throw $e; } + } catch (PluginOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest + // 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진 + // catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존. + throw new PluginOperationException('plugins.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); @@ -952,6 +959,12 @@ class PluginService } throw $e; } + } catch (PluginOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드 + // 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존. + throw new PluginOperationException('plugins.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php index 37cc899f..153c0667 100644 --- a/app/Services/TemplateService.php +++ b/app/Services/TemplateService.php @@ -1936,6 +1936,13 @@ class TemplateService } throw $e; } + } catch (TemplateOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // ZipInstallHelper 등 설치 원본 처리의 raw RuntimeException(깨진 zip·manifest + // 누락 같은 사용자 입력 오류)을 도메인 예외로 승격한다 — 컨트롤러의 좁혀진 + // catch 가 인프라 예외와 구분해 종전 422 계약을 유지하고, 사유는 :error 로 보존. + throw new TemplateOperationException('templates.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); @@ -1989,6 +1996,12 @@ class TemplateService } throw $e; } + } catch (TemplateOperationException $e) { + throw $e; + } catch (\RuntimeException $e) { + // GithubHelper·ZipInstallHelper 의 raw RuntimeException(잘못된 URL·다운로드 + // 실패·manifest 오류)을 도메인 예외로 승격한다 — 종전 422 계약 유지, 사유 보존. + throw new TemplateOperationException('templates.errors.install_failed', ['error' => $e->getMessage()], $e); } finally { if (File::exists($extractPath)) { File::deleteDirectory($extractPath); diff --git a/docs/extension/module-basics.md b/docs/extension/module-basics.md index 428c9888..49091097 100644 --- a/docs/extension/module-basics.md +++ b/docs/extension/module-basics.md @@ -143,6 +143,7 @@ | `getMetadata()` | `[]` | 메타데이터 | | `getMiddleware()` | `[]` | 확장 미들웨어 선언 (self-gate targets) — `{class, groups, timing?, targets}` ([middleware.md](../backend/middleware.md)) | | `getBenchmarkProfiles()` | `[]` | 성능 계측 대상 선언 (`g7:bench` 가 수집) — 목록/화면/쓰기/배치 4축 ([benchmark.md](../backend/benchmark.md)) | +| `upgrades()` | `[]` | 업그레이드 스텝 (`upgrades/` 디렉토리 자동 발견). **`g7_version >= 7.0.0-beta.5` 인 모듈은 신규 step 이 `AbstractUpgradeStep` 상속 의무** ([upgrade-step-guide §13](upgrade-step-guide.md)) — 미상속 시 `ModuleManager::runUpgradeSteps` 가 `RuntimeException` throw | #### 수명주기 훅이 실패를 알리는 방법 @@ -165,7 +166,6 @@ public function activate(): bool - 사유는 **이미 번역된 문장**이어야 한다 — 확장의 언어 파일 키는 코어가 해석할 수 없다. - 사유를 남기지 않고 `false` 만 돌려주면 코어가 일반 문구로 대체한다(동작은 그대로). - 같은 규칙이 플러그인(`AbstractPlugin`)에도 동일하게 적용된다. -| `upgrades()` | `[]` | 업그레이드 스텝 (`upgrades/` 디렉토리 자동 발견). **`g7_version >= 7.0.0-beta.5` 인 모듈은 신규 step 이 `AbstractUpgradeStep` 상속 의무** ([upgrade-step-guide §13](upgrade-step-guide.md)) — 미상속 시 `ModuleManager::runUpgradeSteps` 가 `RuntimeException` throw | #### 동적 권한/역할/메뉴 보존 규칙 diff --git a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md index 06279d53..ed9e34e5 100644 --- a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.7] - 2026-08-21 +## [1.0.7] - 2026-08-22 ### Added 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 2f252148..1e790bd5 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.1.2] - 2026-08-21 +## [1.1.2] - 2026-08-22 ### Added diff --git a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md index e9b4b3ca..1bc252fc 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.6] - 2026-08-21 +## [1.0.6] - 2026-08-22 ### Added diff --git a/modules/_bundled/sirsoft-board/CHANGELOG.md b/modules/_bundled/sirsoft-board/CHANGELOG.md index 11cdf3a5..00d1a696 100644 --- a/modules/_bundled/sirsoft-board/CHANGELOG.md +++ b/modules/_bundled/sirsoft-board/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.5] - 2026-08-21 +## [1.0.5] - 2026-08-22 ### Security diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md index ee1e895d..a7345482 100644 --- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md +++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.1.2] - 2026-08-21 +## [1.1.2] - 2026-08-22 ### Security diff --git a/plugins/_bundled/sirsoft-marketing/CHANGELOG.md b/plugins/_bundled/sirsoft-marketing/CHANGELOG.md index 1f09afa4..e81bd506 100644 --- a/plugins/_bundled/sirsoft-marketing/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-marketing/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.3] - 2026-08-21 +## [1.0.3] - 2026-08-22 ### Security diff --git a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md index 562adda7..98377c59 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md @@ -4,7 +4,7 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.1.2] - 2026-08-21 +## [1.1.2] - 2026-08-22 ### Changed diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md index 5d35e317..071e7f59 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md @@ -4,12 +4,17 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.2] - 2026-08-19 +## [1.0.3] - 2026-08-22 ### Security - 관리자 주문 조회·에스크로 배송등록 API가 세부 권한을 검사하도록 수정했습니다. 이전에는 관리자 계정이면 주문 권한이 없어도 주문번호·결제정보·수령인 연락처와 주소를 조회하고 배송등록까지 할 수 있었습니다. 이제 조회는 주문 조회 권한, 배송등록은 주문 수정 권한이 필요하며, 다른 결제대행사 연동과 같은 기준이 적용됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1893) - 가상계좌 입금통보 주소 조회와 시스템 점검 API에도 설정 조회 권한 검사를 추가했습니다. + +## [1.0.2] - 2026-08-19 + +### Security + - 에스크로 배송등록 요청의 운송장번호·택배사 코드에 형식·길이 검증을 추가했습니다. ### Changed diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json b/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json index 81920c80..d866151a 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-pay_nhnkcp", "description": "NHN KCP PG Plugin for G7 platform", - "version": "1.0.2", + "version": "1.0.3", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json b/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json index c847ad60..fd265ec4 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.2", + "version": "1.0.3", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/package.json b/plugins/_bundled/sirsoft-pay_nhnkcp/package.json index a10bc4a2..18a5d567 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/package.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.2", + "version": "1.0.3", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json b/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json index 58774553..5d5688d1 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json @@ -5,7 +5,7 @@ "ko": "NHN KCP", "en": "NHN KCP" }, - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp/blob/main/CHANGELOG.md", diff --git a/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md b/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md index 49fdac4e..98f38f78 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this plugin will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [1.0.4] - 2026-08-21 +## [1.0.4] - 2026-08-22 ### Changed diff --git a/resources/js/core/template-engine/CHANGELOG.md b/resources/js/core/template-engine/CHANGELOG.md index be74a094..3366bea2 100644 --- a/resources/js/core/template-engine/CHANGELOG.md +++ b/resources/js/core/template-engine/CHANGELOG.md @@ -5,7 +5,7 @@ > > 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) -## [engine-v1.60.6] - 2026-08-21 +## [engine-v1.60.6] - 2026-08-22 ### Fixed diff --git a/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md b/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md index 257a45ea..09626a51 100644 --- a/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md +++ b/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md @@ -4,13 +4,18 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [1.0.6] - 2026-08-21 +## [1.0.6] - 2026-08-22 ### Added - 환경설정 > 고급 > 디버그 카드에 아웃바운드 프록시 설정이 추가되었습니다 — 디버그 모드를 켜면 사이트가 외부로 보내는 요청이 거쳐 갈 프록시 주소와, 프록시를 거치지 않을 주소 목록을 입력할 수 있습니다. - 프록시 주소 옆에 「연결 테스트」 버튼이 추가되었습니다 — 저장하기 전에 연결 여부를 확인하고, 성공 시 외부 서비스에 보이는 IP 주소를 함께 표시합니다. +### Fixed + +- 환경설정 > 인증 > 정책 관리에서 검색 버튼을 눌러도 입력 중인 검색어가 반영되지 않던 문제를 수정했습니다. Enter 키로는 검색되지만 버튼으로는 직전 검색어로만 다시 조회되어, 검색어를 바꿔 버튼을 눌러도 결과가 그대로였습니다. +- 같은 화면의 검색 초기화 버튼을 눌러도 입력창의 검색어가 지워지지 않고 남아 있던 문제를 수정했습니다. 목록은 초기화되는데 입력창만 이전 검색어를 보여 주어 초기화가 안 된 것처럼 보였습니다. + ## [1.0.5] - 2026-08-19 ### Added diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json index dd8d36c1..53ab9fba 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json @@ -290,14 +290,24 @@ { "type": "basic", "name": "Input", + "comment": "검색 필터는 설정 폼의 일부가 아니다 — 자동바인딩되면 검색어가 설정 폼 상태(filter.search)에 남아 초기화 후에도 입력창에 되살아나고, 설정 dirty 판정까지 오염시킨다.", "props": { "type": "text", "name": "filter.search", - "value": "{{query.search ?? ''}}", + "autoBinding": false, + "value": "{{_local.identityPolicySearch ?? query.search ?? ''}}", "placeholder": "$t:admin.identity.policies.search_placeholder", "className": "md:flex-1" }, "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "identityPolicySearch": "{{$event.target.value}}" + } + }, { "type": "keypress", "key": "Enter", @@ -331,10 +341,25 @@ }, "actions": [ { + "comment": "Enter 키와 동일하게 입력값을 쿼리로 실어 이동한다 — refetch 만 하면 입력 중인 검색어가 반영되지 않는다.", "type": "click", - "handler": "refetchDataSource", + "handler": "navigate", "params": { - "dataSourceId": "policies" + "path": "/admin/settings", + "replace": true, + "mergeQuery": true, + "query": { + "tab": "identity", + "search": "{{(_local.identityPolicySearch ?? query.search) || undefined}}", + "page": 1 + }, + "transition_overlay_target": "policy_table_card", + "scroll": { + "container": "#right_content_area", + "to": "#identity_section_policies", + "offset": 180, + "block": "start" + } } } ], @@ -355,6 +380,15 @@ "className": "px-4 py-2 text-sm font-medium bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700" }, "actions": [ + { + "comment": "입력창의 미제출 검색어도 함께 비운다 — 상태만 남으면 초기화가 안 된 것으로 보인다.", + "type": "click", + "handler": "setState", + "params": { + "target": "local", + "identityPolicySearch": "" + } + }, { "comment": "audit:allow layout-list-context-navigate-merge-query 필터 초기화 — 걸려 있던 조건을 전부 비우는 것이 이 버튼의 목적이므로 병합하지 않는다.", "type": "click", diff --git a/tests/Feature/Api/Admin/ModuleControllerTest.php b/tests/Feature/Api/Admin/ModuleControllerTest.php index 114c214c..cdfe1016 100644 --- a/tests/Feature/Api/Admin/ModuleControllerTest.php +++ b/tests/Feature/Api/Admin/ModuleControllerTest.php @@ -569,6 +569,40 @@ class ModuleControllerTest extends TestCase $response->assertStatus(500); } + /** + * 실제 서비스 경로: module.json 없는 ZIP 업로드는 422 를 유지한다. + * + * ZipInstallHelper 는 이런 사용자 입력 오류를 raw `\RuntimeException` 으로 던진다. + * 컨트롤러 catch 가 도메인 예외로 좁혀지면서 서비스가 이를 승격하지 않으면, 종전 + * 422 였던 실패가 500(인프라 장애)으로 위장된다 — 서비스 계층의 승격을 고정한다. + * (mock 이 아닌 실제 ZIP 으로 서비스 실경로를 태운다.) + */ + public function test_install_from_file_with_zip_missing_manifest_returns_422(): void + { + $zipPath = tempnam(sys_get_temp_dir(), 'g7zip').'.zip'; + $zip = new \ZipArchive; + $zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); + $zip->addFromString('readme.txt', 'manifest 없는 ZIP'); + $zip->close(); + + try { + $file = new UploadedFile($zipPath, 'module.zip', 'application/zip', null, true); + + $response = $this->authRequest()->postJson('/api/admin/modules/install-from-file', [ + 'file' => $file, + ]); + + $response->assertStatus(422); + $this->assertStringContainsString( + __('modules.errors.module_json_not_found'), + (string) $response->json('message'), + '실패 사유(:error 자리)가 응답 메시지에 보존되지 않았습니다.' + ); + } finally { + @unlink($zipPath); + } + } + /** * ModuleService에서 일반 Exception 발생 시 500 반환 */ diff --git a/tests/Feature/Settings/OutboundProxySettingTest.php b/tests/Feature/Settings/OutboundProxySettingTest.php index 7801ac0f..ecceed1f 100644 --- a/tests/Feature/Settings/OutboundProxySettingTest.php +++ b/tests/Feature/Settings/OutboundProxySettingTest.php @@ -100,6 +100,37 @@ class OutboundProxySettingTest extends TestCase $this->assertArrayHasKey('advanced.outbound_proxy', $validator->errors()->messages()); } + /** + * 비문자열 값 거부 시에도 오류 문구의 :schemes 치환 자리가 채워집니다. + * + * 비문자열 분기가 치환 파라미터 없이 번역을 호출하면 번역기가 자리표시자를 그대로 둔 + * 문장을 돌려줘 관리자 화면에 `:schemes` 리터럴이 노출됩니다 — 실패했을 때만 드러나는 + * 결함이라 정상 흐름 테스트로는 잡히지 않습니다. + * + * @scenario debug_mode=on, proxy_value=invalid, bypass_list=empty + * + * @effects proxy_setting_rejects_invalid_url + */ + public function test_non_string_proxy_value_error_message_has_schemes_substituted(): void + { + $validator = $this->validatorForAdvanced([ + 'outbound_proxy' => ['not', 'a', 'string'], + ]); + + $this->assertTrue($validator->fails(), '비문자열 프록시 값이 검증을 통과했습니다.'); + + $messages = $validator->errors()->get('advanced.outbound_proxy'); + $this->assertNotEmpty($messages); + + foreach ($messages as $message) { + $this->assertStringNotContainsString( + ':schemes', + $message, + '치환 자리가 채워지지 않아 :schemes 리터럴이 관리자 화면에 노출됩니다.' + ); + } + } + /** * 유효한 주소와 빈 값은 검증을 통과합니다. * diff --git a/tests/Playwright/specs/admin/settings-identity-policy-search.spec.ts b/tests/Playwright/specs/admin/settings-identity-policy-search.spec.ts new file mode 100644 index 00000000..f4e78d94 --- /dev/null +++ b/tests/Playwright/specs/admin/settings-identity-policy-search.spec.ts @@ -0,0 +1,66 @@ +/** + * E2E 회귀: 환경설정 > 인증 > 정책 관리 — 검색 버튼/검색 초기화 + * + * 결함(2026-08-22 실측): 검색 버튼이 refetchDataSource 만 호출해 입력 중인 검색어가 + * URL 쿼리로 실리지 않았다 — Enter 로는 검색되는데 버튼으로는 직전 검색어로만 재조회됐다. + * 검색 초기화는 URL·목록은 비웠지만 입력창 텍스트가 설정 폼 자동바인딩(filter.search)에 + * 남아 초기화되지 않은 것처럼 보였다. + * + * 고정 계약: + * 1. 검색 버튼 클릭 = Enter 와 동일하게 입력값을 쿼리로 실어 이동 (재검색 포함) + * 2. 검색 초기화 = URL 쿼리 + 입력창 텍스트 모두 비움 + */ +import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; + +const PERMISSIONS = [ + 'core.settings.read', + 'core.settings.update', + 'core.admin.identity.policies.read', +] as const; + +const SEARCH_INPUT = 'input[name="filter.search"]'; + +async function gotoPoliciesTab(page: import('@playwright/test').Page): Promise { + await page.goto('/admin/settings?tab=identity&sub_tab=policies'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect(page.locator(SEARCH_INPUT)).toBeAttached({ timeout: 20_000 }); +} + +test('정책 검색 버튼이 입력 중인 검색어를 반영하고, 검색어를 바꿔 다시 눌러도 재검색된다', async ({ page }) => { + await authenticatePage(page, issueToken(...PERMISSIONS)); + await gotoPoliciesTab(page); + + const searchButton = page + .locator('#tab_content_identity_policies button', { hasText: '검색' }) + .first(); + + await page.locator(SEARCH_INPUT).fill('회원가입'); + await searchButton.click(); + await expect(page).toHaveURL(/search=%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85/, { timeout: 15_000 }); + + // 회귀 축: 검색어를 바꾼 뒤 버튼으로 재검색 — 종전에는 URL 이 그대로였고 + // 직전 검색어(회원가입)로만 재조회됐다. + await page.locator(SEARCH_INPUT).fill('주문'); + await searchButton.click(); + await expect(page).toHaveURL(/search=%EC%A3%BC%EB%AC%B8/, { timeout: 15_000 }); +}); + +test('검색 초기화가 URL 쿼리와 입력창 텍스트를 모두 비운다', async ({ page }) => { + await authenticatePage(page, issueToken(...PERMISSIONS)); + await gotoPoliciesTab(page); + + await page.locator(SEARCH_INPUT).fill('회원가입'); + await page.locator(SEARCH_INPUT).press('Enter'); + await expect(page).toHaveURL(/search=/, { timeout: 15_000 }); + + // 초기화 직전에 다른 검색어를 입력해 둔다 — 종전에는 이 텍스트가 + // 설정 폼 자동바인딩에 남아 초기화 후에도 입력창에 되살아났다. + await page.locator(SEARCH_INPUT).fill('주문'); + + await page + .locator('#tab_content_identity_policies button', { hasText: '검색 초기화' }) + .click(); + + await expect(page).not.toHaveURL(/search=/, { timeout: 15_000 }); + await expect(page.locator(SEARCH_INPUT)).toHaveValue('', { timeout: 10_000 }); +}); diff --git a/tests/Playwright/specs/admin/settings-outbound-proxy.spec.ts b/tests/Playwright/specs/admin/settings-outbound-proxy.spec.ts index 2059f61f..2dd92e11 100644 --- a/tests/Playwright/specs/admin/settings-outbound-proxy.spec.ts +++ b/tests/Playwright/specs/admin/settings-outbound-proxy.spec.ts @@ -19,7 +19,7 @@ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth' * * 이 화면은 하드 로드 시 설정 응답이 도착하기 전에도 폼이 렌더되어 조작할 수 있고, * 그 조작은 뒤늦게 도착한 `initLocal` 시드에 덮인다(실측 노출 창 300~430ms). - * 제품 차원의 잠금은 두지 않기로 했으므로(#600 PO 결정), 테스트가 시드 완료를 + * 제품 차원의 잠금은 두지 않는다는 결정이 확정되어 있으므로, 테스트가 시드 완료를 * 기다린 뒤에 조작한다. */ async function gotoAdvancedTab(page: import('@playwright/test').Page): Promise { diff --git a/tests/scenarios/bootstrap-parse-failure.yaml b/tests/scenarios/bootstrap-parse-failure.yaml index a488ed13..8698f237 100644 --- a/tests/scenarios/bootstrap-parse-failure.yaml +++ b/tests/scenarios/bootstrap-parse-failure.yaml @@ -30,13 +30,11 @@ effects: - incompatible_strings_defined_per_locale - core_source_has_no_lookbehind - variable_extraction_contract_unchanged - - shipped_bundle_lookbehind_blocked_by_static_check test_files: - tests/Playwright/specs/smoke/bootstrap-parse-failure.spec.ts - tests/Feature/View/BootstrapFallbackDiagnosisTest.php - resources/js/core/template-engine/__tests__/DataBindingEngine.test.ts - - .claude/scripts/audit/__tests__/shipped-bundle-browser-baseline.test.cjs notes: | 로케일 축은 E2E cross product 에 넣지 않는다 — 화면 문구는 서버가 렌더한 문자열을 @@ -46,3 +44,6 @@ notes: | 하한 미만 실기기(iOS 15) 에서 "깨진 채로 이용 가능한가" 는 재현 수단이 없어 이 매니페스트 범위 밖이다 — 최신 WebKit(Playwright) 도 lookbehind 를 지원해 결함 자체가 재현되지 않는다. + + 배포 번들에 lookbehind 등 파싱 하한 초과 문법이 재유입되는 회귀는 위 테스트와 별개로 + 저장소의 정적 검사가 차단한다 (이 매니페스트의 검증 범위 밖).