Commit Graph
83 Commits
Author SHA1 Message Date
HeuJung bd215bc586 feat(identity): NHN KCP 휴대폰 본인확인 플러그인 추가
메일 인증 대신 휴대폰 본인확인을 쓸 수 있게 하는 플러그인을 추가한다.
가입·비밀번호 재설정·성인인증 등 코어 IDV 가 강제하는 모든 지점에서 동작하며,
테스트 모드로 계약 없이 전 흐름을 확인할 수 있다.

구현·검수 과정에서 드러난 저장소 전역 결함을 함께 처리했다.

- 외래키 컬럼의 한국어 comment 가 방출되지 않던 문제 (소스 38건 교정 +
 기설치본 백필 업그레이드 스텝 7종). ->comment 를 ->constrained 뒤에
 체인하면 예외 없이 통과하면서 comment 가 조용히 사라진다
- 로케일 전환 후 확장 액션 핸들러가 소실되던 문제 (플러그인 3종에
 재등록 진입점 노출)
- 본인인증 상태 폴링 응답이 누적 시도 횟수를 함께 내보내던 문제
- 인증 안내 문구와 실제 진행 방식의 폭 경계 불일치 (768~1023px).
 엔진과 같은 값을 같은 방법으로 읽도록 교정
- transition_overlay_target 이 replace 없이 무효이던 관리자 목록 40곳.
 로딩 표시가 나오지 않아도 경고가 남지 않아 화면을 봐야만 알 수 있었다

두 인증 플러그인의 코어 최소 요구 버전을 7.0.6 으로 올린다. 운영 모드
자격증명 필수 검사가 7.0.6 의 설정 저장 검증 표면에 의존하며, 그보다 낮은
코어에서는 그 검사가 조용히 통과해 빈 자격증명으로 저장할 수 있었다.

재발 차단으로 정적 검사 룰 4종을 신설하고, 검사 도구가 미커밋 신규 확장을
"검사 파일 0" 으로 통과시키던 사각을 함께 막았다.
2026-07-31 00:32:46 +09:00
HeuJung 7b56d5f23a feat(core,board,ecommerce,page,ckeditor5,kginicis): 업로드 이미지 축소·로그인 2단계 인증 도입 + 파생 결함 수정
인터뷰에서 방향만 정하고 남겨 뒀던 후속 5건을 구현하고, 커밋 전 종합 점검에서
발견한 결함 2건을 함께 고쳤다.

기능 도입:
- 업로드 이미지 축소 — `upload.image_max_width` / `image_max_height` / `image_quality`
 는 저장·검증만 되고 리사이즈 코드가 0건이었다. 비율 유지, 상한 이내면 재인코딩하지
 않음(불필요한 화질 손실 방지), 한계 미설정이면 무동작(기존 사이트 이미지 보호).
 PNG·GIF·WebP 투명도 보존. 실패해도 업로드는 계속되고 원본이 저장된다.
- 로그인 2단계 인증 — `security.two_factor_auth` 는 설정 항목만 있고 구현도 화면
 컨트롤도 없었다. 코어 IDV 인프라를 재사용해 purpose `login` 을 추가하고, 비밀번호
 확인 통과 후 challenge 만 발행한다(토큰 미발급 + logout). `POST /api/auth/login/two-factor`
 가 코드를 확인해야 토큰이 나가며, 로그인과 같은 요청 제한을 적용한다. challenge 의
 purpose 를 대조해 다른 흐름의 challenge 로 로그인하는 것을 막는다. 코드 발송이
 실패하면 실패로 알린다 — 2단계를 건너뛰고 로그인시키면 통제가 조용히 열린다.

결함 수정:
- 게시글 수정 시 사용자 경로가 `attachment_ids` 를 Service 로 넘기지 않아, 검증과
 권한은 통과하고 200 을 받는데 첨부만 조용히 연결되지 않았다. 관리자 경로는 넘기고
 있어 같은 요청이 화면에 따라 다르게 동작했다.
- 설정 화면 라디오가 `pointer-events-none` + Label click 으로만 상태를 바꿔, 키보드
 방향키(change) 조작 시 화면 표시와 저장될 값이 어긋났다. 동일 패턴 4개 레이아웃
 12개 라디오에 change 액션을 부여하고 params 를 click 과 동일하게 고정한다.
- 주문완료 화면이 결제수단 확인보다 영수증 조회를 앞세워, PG 를 거치지 않는 주문에서도
 매번 404 요청이 나갔다.
- 개별 게시판 폼의 숫자 항목에 경계값 바인딩이 없어 환경설정 화면과 안내가 달랐다.

커밋 전 점검에서 발견:
- 2단계 인증이 반쪽으로 들어가 있었다. enum 에 case 만 추가하고 코어 purpose
 레지스트리와 ko/en 라벨을 빠뜨려, 목적 목록 API 에서 통째로 빠지고 `hasPurpose`
 가 false 였으며 라벨 대신 i18n 키 원문이 반환됐다. 관리자가 이 목적의 메시지
 템플릿·정책을 만들 수 없는 상태였다. 세 곳을 enum 기준으로 전수 대조하는 회귀
 테스트를 함께 둔다.
- 업로드 이미지 축소가 적용되지 않은 8번째 서비스(레이아웃 편집기 첨부)가 있었다.
 열거형 커버리지 테스트는 내 조사 결과를 green 으로 고정할 뿐 전수를 증명하지
 못한다 — 같은 규약을 조건으로 표현한 audit 룰이 첫 실행에서 검출했다.

재발 방지:
- audit 룰 `upload-service-image-resize` (신규 업로드 서비스의 축소 누락 검출)
- 트러블슈팅 사례 20 (코어 설정이 일부 경로에만 적용되는 증상의 진단 절차)
- `sirsoft-ckeditor5` 의 코어 최소 요구 버전을 7.0.6 으로 상향 (신설 클래스 의존)

공개 이슈: gnuboard/g7
2026-07-30 07:57:06 +09:00
HeuJung d17ea28da5 fix(core,board,ecommerce,page,kginicis): 정책값 하드코딩·계약 분리 결함 일괄 수정
관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다.
설정 화면(min/max)·저장 검증(FormRequest)·런타임 적용(Service) 세 계층이 각자
리터럴을 들고 있어 서로 조용히 갈라지던 것이 공통 원인이다.

경계값 SSoT 도입:
- config/core.php `settings_limits` 를 단일 출처로 두고, FormRequest 는 이 값으로
 규칙을 만들고 화면은 설정 응답의 `_meta.limits` 로 같은 값을 받아 바인딩한다.
- 게시판·이커머스도 각 모듈 config 의 limits 를 동일 구조로 노출한다.

적용되지 않던 정책을 실제로 적용:
- 비밀번호 최소 길이·특수문자 필수(신규 지정 7개 경로. 로그인은 제외 — 정책을
 올렸을 때 기존 회원이 자기 계정에 접근조차 못 하는 데드락을 막기 위함)
- 계정 잠금 시간 0 = 무한대(전용 컬럼 분리. timestamp 상한과 NULL 규약 때문에
 기존 컬럼 재사용이 불가능하다) + 관리자 수동 해제 경로
- 업로드 최대 크기·허용 확장자, 게시판 첨부 개수·댓글 깊이, 페이지 첨부 정책,
 장바구니 수량·구매 한도, 마일리지 사용 한도, 주문 부가세

파생 결함:
- 큐 Job(sync 드라이버)이 진행 중인 요청의 Request 바인딩을 덮어써, 뒤이어 실행되는
 훅 리스너가 헤더를 잃고 조용히 건너뛰었다. Job 이 원 바인딩을 복원하도록 고치고,
 전송 헤더가 필요한 리스너는 동기 실행으로 선언한다.
- 확장 업데이트 실패 시 상태만 되돌리고 상태 캐시를 비우지 않아, 그 확장의 관리자
 화면만 캐시 TTL(기본 하루) 동안 404 로 남았다. 실패 경로에도 무효화를 대칭으로
 두고, 빈 목록 자체를 캐시하지 않는다.
- 글 작성 시 첨부 파일이 Service 로 전달되지 않아 검증·권한은 통과하고 첨부만 사라졌다.
- 환경설정 저장 실패 안내가 내부 설정 키를 그대로 노출했다(105개 항목에 라벨 부여).

재발 방지:
- audit 룰 3종(레이아웃 경계 리터럴 / FormRequest 경계 불일치 / Service 재클램프)
- 테스트 실행 범위 강제 훅(check-test-scope)
- 트러블슈팅 사례 2건(큐 Job 요청 오염 / 확장 상태 캐시 고착)

공개 이슈: gnuboard/g7
2026-07-30 07:52:24 +09:00
chym1217 0a6a8d6968 fix(message_bizppurio): 결과코드 5002 언어팩 누락 + 알림톡 템플릿 저장 승인검증 부재 수정
PR 준비 최종 검토 중 문제코드 처리 대조표를 재검증하며 발견한 결함 2건.

- ResultCodeResolver 가 5002(요청 과다)를 재시도 대상으로 분류하지만 ko/en/ja
 언어팩 어디에도 사유가 없어 코드값만 노출되던 결함 수정
- 알림톡 템플릿 연결 저장이 승인 상태(RDY/ACT) 재검증 없이 그대로 upsert되어,
 화면 드롭다운(승인 템플릿만 노출)을 우회한 API 직접 호출 시 미승인 템플릿도
 저장 가능했던 결함 수정 — bind 에 카카오 승인 목록 대조 추가, 조회 자체
 실패 시에도 안전측으로 저장 거부
2026-07-28 16:56:13 +09:00
chym1217 8aa395c3a1 fix(notification): 비활성 확장이 제공하던 알림 채널을 발송 대상에서 제외
비활성 상태의 확장이 등록했던 발송 수단(문자·알림톡 등)을 코어 via가
저장된 채널 설정만 보고 계속 발송·"건너뜀" 로깅하던 문제를 수정한다.
사용 가능한 채널 목록에 없는 채널은 로그 없이 조용히 제외하며,
알림 설정·템플릿은 그대로 유지되어 확장을 다시 켜면 복구된다.

아울러 발송이 타임아웃·연결실패로 최종 실패할 때 pending 으로 방치되던
이력을 failed 로 마감한다(webhook 이 먼저 확정한 건은 덮어쓰지 않음).
2026-07-28 12:40:13 +09:00
chym1217 cd11fc60ce feat(auth): 회원가입 시 휴대폰·전화번호 선택 입력 추가
마이페이지·관리자 회원 폼에는 이미 연락처 필드가 있으나 회원가입 폼에만
빠져 두 화면이 불일치했다. DB·모델·Resource는 기존 존재하므로 회원가입
경로에만 검증·저장·입력 UI를 연결한다. 연락처는 선택 항목이다.
2026-07-28 12:40:12 +09:00
HeuJung 3d8890b118 feat(core,admin_basic): 번들 비-ko 언어팩 자동 프로비저닝 + 미설치·드리프트 발견성
신규 설치/복구 시 비-ko/en 번들 언어팩(ja 등)이 자동으로 채워지지 않아
런타임에 오류 없이 조용히 ko 로 폴백하고, 운영자가 이를 알아채기 어려웠다.
두 결손을 함께 해소한다.

프로비저닝(SSoT):
- language-pack:provision 멱등 커맨드 신설. supported_locales ∖ {ko,en} 의
 미설치 번들 팩을 설치하고, 설치본 파일이 사라진 드리프트 팩(슬롯 점유로
 getUninstalledBundledPacks 가 못 잡는)도 복구 경로로 겸한다. 재실행 시 0건 수렴.

발견성:
- CLI language-pack:list 를 Service::list 경유로 전환해 미설치(uninstalled)와
 드리프트(active·파일 없음)를 함께 표면화.
- files_missing / bundled_source_available 파생 플래그를 Resource 에 노출.
- 관리자 언어팩 목록에 "파일 없음" 배지 + 원클릭 재설치(기존 install-bundled 모달
 재사용). 재설치 경로만 auto_activate 로 바인딩해 복구 후 active 를 유지한다.

회귀 방지:
- 정적 룰 lang-pack-supported-locale-has-bundle(선언 로케일 ↔ 번들 소스 부재) 신설,
 lang-pack-installed-version-drift 를 프로비저닝된 환경 한정으로 확장.
- 백엔드/레이아웃/E2E/시나리오 테스트 + 룰 fixture 추가.

부수 수정:
- 프로비저닝 작업 중 표면화된 ecommerce 언어팩 시더 테스트 격리결함 수정
 (modules 등록 행을 매 테스트가 직접 보장 — 프로세스당 1회 시딩 전제 붕괴 대응).
2026-07-27 14:58:48 +09:00
HeuJung 0dfb45951b fix(core,basic): 검색 봇 화면 렌더러 패리티 결손 일괄 수정
일반 화면(React)과 봇 화면(PHP)은 같은 레이아웃을 각각 렌더하는데 봇
화면 쪽 지원 범위가 뒤처져, 확장이 교체한 본문이 통째로 빈 채 나갔다
(공개 이슈 ). 같은 결손 계열을 전수 조사해 함께 닫는다.

- 확장 주입 props 를 렌더 진입 시점에 해석해 자손 전체에 상속
 → 게시글·답글 원글·댓글·페이지·상품 설명 5개 지점 복구
- 사용자 작성 HTML 정화기 신설 — isHtml 판정에 따라 이스케이프/정화.
 미정화 상태에서 봇 경로로 저장형 스크립트가 실제 실행됐다
- 노드 키 패리티: conditions / type:iterator / responsive 매칭 규칙 /
 children 문자열 / text 우선순위 / $t:defer / 레이아웃 초기 상태
- 표현식: 단일 바인딩 판정 교체, ||·&& 를 값 반환(JS 시맨틱)으로,
 비교식이 raw 경로에서 null 이 되던 문제
- 데이터소스: 엔드포인트 표현식 전면 해석 + 레이아웃 8곳 선언 보강
- 봇 페이지 캐시 1회 전량 무효화 업그레이드 스텝

지원 범위 SSoT 표를 문서에 고정하고, 한쪽만 고치는 것을 막는 검사 룰과
레이아웃 전수 순회 테스트로 잠갔다.
2026-07-27 10:03:06 +09:00
HeuJung b505ac7ba7 fix(core,board,ecommerce,page,gdpr): 목록 컨텍스트 왕복·엔진 렌더 파이프 결함 일괄 수정 + 공개문서 정리
세 갈래의 결함을 한 브랜치에서 정리한다.

## 목록 컨텍스트 왕복 시 URL 상태 소실 ( @jiwonpapa 님께서 제보해주셨습니다.)

목록에서 상세·형제 상세(이전/다음)·작성/수정 폼에 다녀오면 보고 있던
page/search/category/filters 가 사라지던 문제를 전 도메인에서 수정했다.

- 엔진(engine-v1.54.2): `mergeQuery: true` 만 적고 `query` 를 생략하면 병합이
 통째로 건너뛰어지던 함정을 교정 — `ActionDispatcher.handleNavigate`/`handleReplaceUrl`.
- 게시판·이커머스·페이지·회원·마이페이지·gdpr 등 9개 확장 레이아웃의 왕복 leg 전수
 적용(mergeQuery: true). 의도적 리셋(검색/필터 초기화·탭 전환·프리셋)은 면제 주석으로 구분.
- 무한스크롤 목록(브랜드·상품 공통정보·고시정보)의 새로고침이 URL 검색·정렬을 떨구던
 결함 수정.
- 재발 차단: audit 룰 `layout-list-context-navigate-merge-query`(목록 클러스터 자동 도출,
 page/필터 URL 신호 4종) + `layout-navigate-path-absolute`(navigate path 동작 키워드 금지).

## cellChildren 등 반복 렌더에서 단일 바인딩 파이프 미적용 ( @glitter-gim 님께서 제보해주셨습니다.)

목록 표의 각 칸에 넣은 날짜·숫자 서식(`{{row.x | datetime(...)}}`)이 빈 값이 되거나
서식 없는 원본으로 나오던 문제를, 표현식 판정 로직이 엔진 전역에 복제되며 갈라진
구조적 결함으로 진단하고 판정 경로를 단일화했다(engine-v1.54.3).

- `RenderHelpers`(renderItemChildren·evaluateIfCondition)·`ConditionEvaluator`·
 `DataBindingEngine.resolveObject`·`DynamicRenderer` props 5곳에 단일 바인딩 파이프 분기 추가.
- 계획: `g7-scalable-lobster.md`(렌더 경로 비대칭 결함 일괄 수정).

## 공개 문서 내부 도구 귀속 제거

release 에 포함되는 공개 문서(`docs/**`)에서 내부 audit 룰 ID 귀속 서술을
도구 비귀속 표현("정적 검사")으로 정리. 재발 차단 룰 `public-no-internal-audit-reference` 신설.

전 계층 테스트(PHPUnit·Vitest·Playwright)·회귀 테스트 동반, 버전/CHANGELOG/활성 디렉토리 동기 완료.
2026-07-26 15:17:56 +09:00
HeuJung abb40ad210 fix(core,board,ecommerce,page): 중첩 리소스 스코프·계층 무결성 결함 12건 일괄 수정
중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의
하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로
통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가
서로 다른 강도로 검증하던 부분도 맞췄다.

계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고,
검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다.
설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해
게시판 설정이 실제로 적용되게 했다.

쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로
새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동).

재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다.

공개 이슈: gnuboard/g7
2026-07-24 16:17:44 +09:00
HeuJung 9bcd97098b fix(core): 끝 슬래시가 붙은 경로(/admin/)가 라우트에 매칭되지 않던 문제
라우트 패턴은 끝 슬래시 없는 형태(*/admin 등)로만 정의되는데(routes.json
규약), Router.match 가 들어온 경로의 끝 슬래시를 정규화하지 않아 /admin/
(끝 슬래시)이 어떤 라우트에도 매칭되지 않고 404 로 떨어졌다. 특히 미인증
상태로 /admin/ 진입 시 대시보드 → 로그인 리다이렉트 흐름 대신 404 가 노출됐다.

match 진입에 normalizePathname 을 두어 루트(/)를 제외한 경로의 끝
슬래시(연속 포함)를 제거한 뒤 대조한다. match 는 라우팅 단일 진입점이라
모든 호출부가 함께 정상화되고, 다운스트림(auth back-URL, getAuthType)은
원본 pathname 을 그대로 써 의미 변화가 없다.

기존 릴리스부터 있던 라우터 견고성 결함이며 자산 URL 이중 모드 변경과는
무관하다(matchPattern 은 미변경, routes.json 은 81건 정상 로드 확인).
2026-07-23 21:34:44 +09:00
HeuJung 65f1c70ffe refactor(admin-template): 에셋 파일 서빙 방식을 지역화에서 전용 카드로 분리
에셋 파일 서빙 방식은 언어·시간대와 무관한 인프라(서버 라우팅) 설정이라
지역화 카드에 묶이는 것이 의미상 어긋난다. tab_content_general 하위에
전용 카드(card_asset_serving)를 신설해 필드·자동 감지 버튼·인라인 안내를
이동하고, 라벨/설명을 카드 헤더로 승격해 다른 카드와 형태를 맞췄다.

배치 회귀 가드: 레이아웃 구조 테스트 + E2E 로 전용 카드에 항목이 있고
지역화 카드에는 없음을 단언.
2026-07-23 21:13:39 +09:00
HeuJung 05e81c6017 fix(admin-template): 자산 URL 감지 UI 버튼 일관성·인라인 결과·문구 정비
관리자 환경설정 > 일반의 에셋 파일 서빙 방식 필드를 브라우저 검수 지적대로 다듬었다.

- "자동 감지" 버튼이 btn 베이스 클래스 없이 btn-secondary 만 갖고 있어 다른 버튼과
 달리 평문처럼 보였다. btn 을 더해 동일한 박스 스타일로 맞췄다.
- 감지 결과를 토스트로 띄우면 사라져 확인이 어려웠다. 핸들러가 결과를 로컬 폼
 상태로 기록하고, 필드 하단에 상태별 인라인 안내로 상시 노출하도록 바꿨다.
- 라벨을 "에셋 파일 서빙 방식" 으로 바꾸고(전 화면·전 로케일), 확장자 가로채기
 안내에 원인이 되는 nginx location ~* \.(js|css|...)$ 정적 최적화 블록을 담아
 인프라 담당자가 바로 인지할 수 있게 했다. ko/en/ja 및 인스톨러까지 일관 반영.

레이아웃 test util 은 _local 조건부 렌더를 시드하지 못하므로(엔진은 로컬상태
provider 로 if 를 평가) 인라인 안내의 렌더는 레이아웃 구조 테스트 + 핸들러 단위 +
E2E 세 계층으로 커버했다.
2026-07-23 15:10:28 +09:00
HeuJung d6e1f98a4f fix(core): 확장 오토로드 캐시 공집합 덮어쓰기로 인한 전역 500 + 소스맵 노출 차단 복원
오토로드 캐시(psr4)가 공집합으로 덮어써져 모든 모듈/플러그인 컨트롤러가
해석 불가능해지고 확장 화면 전체가 500 이 됐다. 이 매핑은 "DB 설치 목록 ×
디스크 활성 디렉토리" 교집합이라 DB 쪽만 일시적으로 비어도(테이블 부재·
설치 중간 실패·testing DB) 정상 매핑이 통째로 소실된다. 코어 라우트는 200
인데 확장 라우트만 전부 500 인 비대칭이 이 결함의 진단 단서다.

- 디스크에 확장 디렉토리가 남아 있는데 매핑이 0건이면 기록하지 않고 보존.
 정상적인 0건(신규 설치·마지막 확장 삭제)은 디스크도 함께 비어 통과한다
- generateAutoloadFile 에 testing 실 캐시 경로 쓰기 차단. updateComposerAutoload
 에만 있던 가드를 생성기 자체로 옮겨 어느 경로로 들어와도 막히게 했다
- extension:update-autoload 가 훅 캐시 재생성 전에 런타임 오토로드를 재등록.
 미재등록 시 깨진 매핑으로 훅 캐시가 modules/plugins 0건으로 생성되어
 확장 훅이 조용히 사라지고, 복구에 커맨드 2회 실행이 필요했다

소스맵 서빙은 에서 차단했으나 이번 브랜치에서 화이트리스트에 재추가되어
운영에서 원본 코드 전문(sourcesContent)이 노출될 수 있었다. 이 화이트리스트가
확장 에셋 서빙의 유일한 방어선이다. dev 빌드의 sourceMappingURL 404 를 피하면서
운영을 막도록 local 환경에서만 허용하도록 분기하고, 산출물을 production 으로
재빌드해 docroot 에 남아 있던 소스맵을 제거했다. --production 빌드는 소스맵을
생성하지 않을 뿐 기존 파일을 지우지 않는다.

CoreUpdateCommandSpawnFailureTest 는 in-process fallback 이 실제 업그레이드
스텝을 돌려 코어 역할·권한·메뉴를 커밋하는데 RefreshDatabase 가 없어, 이후
모든 테스트 클래스가 중복 키로 깨졌다(단독 실행 시 미재현).
2026-07-23 13:04:59 +09:00
HeuJung 327c433250 fix(core): 번들 인스턴스 조회 시 클래스 재선언으로 프로세스가 죽던 문제
`_bundled`(또는 `_pending`) 확장의 상세 정보를 읽는 두 메서드가 파일을 가드 없이
require 했다. 같은 확장의 활성 디렉토리 사본이 이미 로드돼 있으면 두 파일은 경로가 달라
중복 제거가 통하지 않으므로 같은 클래스명을 두 번 선언하게 되고, 그 자리에서 죽는다.

 Cannot declare class Modules\Sirsoft\Board\Module, because the name is already in use

특히 위험한 것은 클래스 재선언이 Exception 이 아니라 Error 라는 점이다. 두 메서드 모두
try/catch 로 감싸여 있어 안전해 보이지만 실제로는 잡히지 않아 프로세스가 통째로 종료된다.
운영에서는 해당 요청이 죽고, 테스트에서는 요약 줄도 없이 스위트가 중단된다.

같은 파일의 다른 로드 지점들은 이미 "이미 선언돼 있으면 임시 클래스명으로 로드" 패턴을
갖고 있었고 이 두 곳만 빠져 있었다. 기존 패턴을 그대로 적용하고, 놓치던 Error 까지 잡도록
catch 범위를 넓혔다.

회귀 테스트는 처음에 실제 번들 확장으로 짰다가 폐기했다 — 테스트 부팅이 번들 쪽을 먼저
로드하므로 같은 경로를 다시 읽는 셈이라 위험한 조합이 만들어지지 않았고, 가드를 지워도
통과했다. 활성/번들 두 사본을 갖는 임시 확장 루트를 만들어 조합을 직접 재현하고, 어느 쪽
파일을 읽었는지까지 단언하도록 다시 작성했다. 가드를 제거하면 예측한 메시지 그대로 죽는다.

이 경로가 앞서 관측된 스위트 중단과 같은 사건인지는 단정하지 않는다. 메시지와 메커니즘은
일치하지만 그 중단은 이후 재현되지 않아 수정 전후 비교가 불가능했다.

검증: 신규 2건 + 확장 매니저 회귀 120건 green, 인스톨러·업그레이드 통합 581건 green.
2026-07-23 11:39:59 +09:00
HeuJung 7e569efe4c test(core): 개발 환경 파일을 건드리는 테스트 격리
테스트가 개발자의 `.env` 를 삭제한 사고 이후 부트스트랩에 복원 안전망을 넣었지만,
그것은 프로세스 종료 시점 복원이라 같은 실행 안에서 뒤 테스트가 훼손된 파일을 읽는 구간이
남아 있었다. 복원이 아니라 애초에 건드리지 않는 구조로 바꾼다.

앱 루트를 임시 디렉토리로 돌리면 `base_path` 를 쓰는 제품 코드는 한 줄도 바뀌지 않은 채
격리된 파일만 다룬다. 공용 트레이트로 묶어 세 클래스가 같은 방식을 쓰게 했다.

격리 자체를 증명하기 위해 안전망을 임시로 끄고 돌렸다. 안전망 없이도 `.env` 가 바이트
단위로 그대로였다 — "green 이니 됐다" 로는 안전망이 뒤에서 복원해 준 것과 구분되지 않는다.

이 과정에서 격리로는 막을 수 없는 유형을 하나 더 찾았다. 별도 프로세스를 띄워 업그레이드
커맨드를 실행하는 테스트가 자식 쪽에서 `.env` 의 버전 값을 덮어쓰고 있었다. 부모의 앱 루트
전환도, 테스트 부트스트랩의 안전망도 자식에는 닿지 않는다. 테스트 환경 지정은 읽는 파일만
바꿀 뿐 쓰기 대상은 언제나 프로젝트 루트의 `.env` 다. 그 테스트가 검증하는 것은 자식이 방금
쓴 클래스를 읽어들이는지 뿐이므로, 커맨드가 이미 제공하는 부수효과 차단 옵션을 붙였다.

같은 구조의 문제가 업그레이드 스텝 임시 파일에도 있었다 — 실제 `upgrades/` 에 쓰고 정리를
tearDown 에 의존하므로 중단되면 개발 체크아웃에 정체불명의 스텝이 남는다. 실제로 두 번
발견해 지웠고, 부트스트랩 셧다운 훅이 그 패턴을 쓸어내도록 했다.

검증: 세 클래스 개별 green, 안전망을 끈 통합 실행 28건 green + `.env` 불변,
자식 프로세스 테스트 green, 전 구간 `.env` 해시 불변, audit 위반 0.

알려진 잔여 문제: 인스톨러와 업그레이드 스위트를 한 프로세스로 합쳐 돌리면 모듈 클래스
중복 선언 fatal 로 중단된다. 각각 따로 돌리면 297 / 301 건 모두 통과하며, 이번 변경 이전에도
같은 증상이었다. 별도 조사가 필요해 이번 범위에서 다루지 않았다.
2026-07-23 11:39:59 +09:00
HeuJung 072b3f50d9 feat(core,admin-template): 자산 URL 방식 감지 지점 보강 + 테스트 격리 결함 수정
계획서 §검증 후반부를 다시 대조해 미구현 3건을 확인하고 모두 구현했다. 앞선 커밋에서
"미구현 1건" 으로 보고한 것은 §12 불변식 표만 파일로 대조하고 §검증 섹션의 표 두 개는
확인하지 않은 결과였다.

- 설치 2단계 요구사항 화면에 자산 URL 방식 항목 추가. 판정은 서버가 아니라 브라우저가
 한다 — 서버에서 자기 APP_URL 로 요청하면 loopback 이 vhost·프록시 체인을 우회해
 실제 방문자와 다른 답을 낸다. 2단계와 3단계가 같은 판정 함수를 쓰도록 추출했다
- 대시보드가 스스로 프로브를 던져 **저장된** 방식과 대조하고 어긋나면 안내한다.
 런타임 값이 아니라 저장값을 보는 이유는, 자가 복구가 이미 전환해 놓았을 때가 바로
 알려야 할 상황이기 때문이다 — 봇은 자바스크립트를 실행하지 않아 자가 복구가 닿지 않는다.
 알리기만 하고 저장하지 않으므로 클라이언트가 서버 설정을 뒤집지 않는다
- 레이아웃 렌더링 테스트를 두 모드로 추가. 핵심 단언은 두 모드의 DOM 이 완전히 동일하다는
 것이다 — 모드가 화면 구조로 새어나가면 확장자 없는 환경에서만 깨지고 그 환경은 개발 중
 거의 재현되지 않는다

함께 고친 테스트 격리 결함 4건 (이번 기능과 무관하나 같은 세션에서 발견):

- 인스톨러 공유 클래스 로드가 오토로드를 막고 있어, 앱 루트를 임시 디렉토리로 바꾸는
 테스트에서 없는 파일을 읽으려다 죽었다 (8건 fatal)
- 테스트용 번역 스텁이 실제 구현과의 정의 경쟁에서 이기면 주입된 번역을 무시했다.
 클래스 단독 실행은 통과하고 스위트 실행만 실패하던 원인
- 모듈 활성 목록은 "디스크 로드분 ∩ DB 활성분" 인데 부팅 시 테이블이 비어 디스크 스캔이
 일어나지 않아, 파일 캐시 잔재가 있을 때만 통과하던 순서 의존 테스트
- 라우트 프로바이더 가드 테스트가 Git 미추적 파일(.env) 존재에 의존해 신규 클론·CI 에서
 실패했다. 임시 앱 루트로 격리

또한 일부 테스트가 실제 개발 환경 파일을 삭제·재생성하고 자신의 정리 단계에서 되돌리는데,
그 테스트가 죽으면 되돌아오지 않아 복구 불가능한 유실이 발생한다. 실제로 이번 세션에서
겪었으므로 테스트 부트스트랩에 프로세스 수준 복원 안전망을 추가했다.

검증: 백엔드 344건 / 템플릿 2799건 / 레이아웃 7건 / 브라우저 9건 green, audit 위반 0.
2026-07-23 11:39:57 +09:00
HeuJung 15cd4452bf feat(core): 정적 최적화 서버 대응 자산 URL 이중 모드
nginx 의 정규식 location 은 프리픽스 location 보다 먼저 매칭되므로,
`location ~* \.(js|css|json)$` 블록이 있는 서버에서는 확장자 붙은 동적
엔드포인트가 `try_files ... /index.php` 폴백이 실행될 기회 없이 404 가 된다.
aaPanel/CyberPanel/Plesk 기본 템플릿에 들어있어 드물지 않으며, 관리자 화면조차
뜨지 않아 "서버 설정을 고치세요" 안내가 순환 참조가 된다.

관례를 깬 쪽이 G7 이므로 해소 책임도 G7 에 두고, 두 형태를 모두 서빙한 뒤
환경에 따라 택일한다. 확장자 형태는 영구 유지한다 — 제거하면 URL 을 하드코딩한
서드파티 확장이 깨진다.

- 라우트: dualSuffix / dualSuffixSegment / dualAsset 매크로로 23개 엔드포인트와
 프로브를 이중 등록. 확장자 형태를 먼저 등록한다 — 확장자 없는 쪽이 더 느슨한
 패턴이라 순서가 뒤집히면 `.json` 요청까지 삼킨다
- URL 생성: 서버 App\Support\AssetUrl, 프론트 core/support/assetUrl.ts 로 집약.
 기본 모드에서 생성 결과는 치환 이전과 문자열까지 동일하다 (쿼리 순서 포함 —
 순서가 바뀌면 의미는 같아도 HTTP 캐시 키가 갈린다)
- 자가 복구: 부트스트랩 자산 로드 실패 시 확장자 없는 형태로 1회 단방향 전환.
 역방향 금지 + 기존 재시도 예산 공유로 무한 왕복을 막는다
- 감지·전환: 인스톨러 프로브(설치 시 확정) / 관리자 환경설정 일반 탭 /
 g7:asset-url-mode. 판정은 상태코드가 아니라 매직 토큰 + Content-Type 으로 한다.
 상태코드만 보면 "404 대신 200 + 에러 HTML" 환경에서 영원히 오판한다
- 봇은 JavaScript 를 실행하지 않아 자가 복구가 닿지 않으므로, 모드 변경 시
 SEO 프리렌더 캐시를 비워 재생성시킨다

가드: audit 룰 2종(dynamic-route-static-extension / asset-url-builder-required),
Playwright 6건(정적 블록 가로채기 시뮬레이션), 루프 방지 불변식 L1~L9 전수 red 증명.

부수 정리: 양 Composer 에 중복돼 있던 확장 에셋 수집 123줄을 트레이트로 통합하고,
자산 확장자 화이트리스트 누락(.map)과 경로 정규화 우회 가능성을 함께 교정.

sir.kr 커뮤니티의 hang 님께서 제보해주셨습니다.
2026-07-23 11:34:32 +09:00
HeuJung 7f8fb6870f fix(core,board,ecommerce): 조건부 검증 결함 패밀리 일괄 수정 및 실패 안내 노출
여러 화면에서 조건부(FormRequest) 검증이 우회되거나 과도하게 제한되던 결함을 한 계열로 묶어 수정한다.

- 검증 규칙 확장 필터 이름 10개를 나머지 92개와 동일 규칙으로 통일
- 설명 길이를 바이트가 아닌 글자 수로 계산(한글 오검출 해소)
- 비활성 언어팩 번역 보존한 채 수정 허용
- 알림 템플릿 제목 nullable 존중, 미지원 발송 수단 저장 차단
- 일정/실행 이력 목록 조회 검증 정합
- 게시판 첨부 미사용 시 빈 확장자 통과, new_display_hours/최소 길이 0 허용, 일괄 적용 override_values 범위 검증(SSoT)
- 이커머스 클레임 사유 조회 검증, 용어 정정
- 원문 i18n 키 노출 다수 해소, 저장/삭제 실패 시 사유 화면 노출
- 아이콘 크기/다크모드 대비 회귀 수정

번들 확장(모듈3·플러그인6·템플릿2·언어팩5) 버전 new-bump 및 CHANGELOG 반영, ja 언어팩 동기.
2026-07-23 10:54:17 +09:00
HeuJung a3b25b2756 fix(admin-template, installer): OPcache 비활성 경고 경각심 강화 및 배치 개선
브라우저 실측에서 경고가 경고로 읽히지 않는 문제가 드러났다. 기능은 정상
동작했으나 문구가 밋밋하고, 인스톨러 카드가 통과 항목과 같은 색이며, 관리자
정보 탭에서는 경고가 카드 사이에 끼어 있었다.

문구는 영향의 크기를 구체화했다 — 페이지를 열 때마다 모든 PHP 코드를 다시
해석하며 응답이 몇 배까지 느려진다는 점을 인스톨러와 관리자 양쪽에 같은 기준으로
반영했다.

인스톨러는 상태를 아이콘에만 반영하고 있어 목록을 훑을 때 문제 항목이 눈에
들어오지 않았다. 카드 자체에 상태 수식자를 부여했다. 경고만 고치면 필수 항목
실패 카드도 통과 카드와 똑같이 보이는 같은 결함이 남으므로 fail 도 함께 다뤘다.

관리자 정보 탭에서는 경고가 2단 카드 그리드의 세 번째 셀로 들어가 있었다.
탭 최상단으로 옮기고, 여백은 호출처가 아니라 자산이 책임지는 기존 컨벤션을
따라 alert 인접 규칙에 카드 그리드를 추가했다. 배치는 눈으로만 보면 회귀를
알 수 없으므로 좌표와 직계 자식 여부를 E2E 로 고정했다.
2026-07-20 16:15:24 +09:00
HeuJung cbf8ce1555 fix(admin-template): 사이트맵 상태 wait_for 누락 + stale 레이아웃 테스트 5건 교정
작업 중 전체 Vitest 스위트에서 사전 결함 6건을 발견해 같은 세션에서 종결한다.

SEO 탭의 sitemap_status 가 transition_overlay.wait_for 에 빠져 있어, 오버레이가
먼저 걷히며 진행상황 블록이 빈 상태로 잠깐 노출될 수 있었다. 구조가 동일한
policies 는 등록돼 있어 설계 의도가 아닌 누락으로 판정했다.

나머지 5건은 로직이 아니라 테스트가 낡은 것이었다. 사이드바 테스트는 프로필 UI
부재라는 의도 대신 컨테이너 id 부재로 단언해 이후 재도입된 크로스 이동 링크에
걸렸고, 대시보드 테스트는 iteration id 보간 도입 후에도 정확 일치로만 노드를
찾고 있었다. 각각 의도에 맞게 교정하고 반대 방향 회귀 가드를 보강했다.
2026-07-20 14:39:13 +09:00
HeuJung 062a88ca04 feat(core): DB 최고권한 계정 차단 + OPcache 상태 안내
설치 단계와 코어 부팅의 정책이 엇갈려 있었다. 인스톨러는 root 를 자유롭게
허용하면서, 설치 후 부팅은 같은 값을 보고 확장 로딩 전체를 조용히 스킵했다.
설치는 성공하지만 이후 깨진 화면만 뜨고 원인은 로그 한 줄에만 남았다.

판정을 프레임워크 의존성 0 인 공용 SSoT 로 모아 인스톨러와 코어가 같은 답을
내게 하고, 설치 단계에서 서버 2경로로 차단하며, 설치 후 변경은 전용 에러
페이지로 드러낸다. 가드를 boot 가 아닌 미들웨어에 두어 artisan 이 죽지 않게
해 복구 수단을 남겼다. admin 은 일반 권한 운영 계정 오차단을 막기 위해 차단
목록에서 제외했다.

OPcache 는 상태 조회 경로 자체가 없어 성능 저하를 인지할 방법이 없었다.
설치 단계 경고(비차단)와 관리자 정보 탭 표시를 같은 판정 SSoT 로 추가했다.
확장 로드 여부만으로는 부족해 opcache.enable 지시자까지 확인하며, ini_get 이
차단된 환경은 "확인 불가"로 폴백해 경고도 차단도 하지 않는다.
2026-07-20 14:17:49 +09:00
HeuJung 8fda5d31e1 feat(seo): changefreq Enum 도입 + 사이트맵 사전결함 테스트 정정
계획서 위임 항목(Q6·Q7·Q8) 마감.

Q8: sitemaps.org 폐쇄 어휘를 App\Enums\SitemapChangeFreq 로 도입해
저장(SitemapIndexer)·렌더(SitemapXmlRenderer) 두 경계에서 normalize 하고,
생산 지점(생성기 정적 URL·3 contributor·4 리스너) 리터럴을 enum case 로 치환.
규격에 없는 changefreq 가 사이트맵에 그대로 출력되던 것을 차단(대소문자·공백 흡수).

Q6: Upgrade_7_0_0_beta_3Test 의 "동결 TARGETS === 살아있는 config" 단언은
동결 규정과 충돌하는 무효 불변식(config 는 에서 정당하게 진화)이므로
의도적 divergence 를 검증하도록 재작성.

Q7: 코어 20건 사전 실패를 격리 실측으로 재분류 — 6클러스터는 전 스위트
OOM/싱글톤 누수 오염(격리 시 통과), GenericNotificationViaTest 는 readiness
게이트를 mock 하지 않아 빈 settings 로 mail 이 항상 제외되던 stale test 를 정정.
2026-07-19 20:13:05 +09:00
HeuJung 124433148c feat(seo): 사이트맵 재생성 알림·폴링 폴백 + 코어 7.0.6 확정
이슈 후속(S7) 마감 + 릴리스 버전 확정.

기능(S7):
- 사이트맵 전체 재생성 완료/실패 시 실행 관리자에게 알림(수동 실행 한정).
 스케줄러·증분·봇 재생성은 알림 없음(triggered_by 부재 시 skip).
- 최종 실패 전용 알림 + 재생성 전용 큐 + WS 진행상황 payload 봉투 정정.
- Reverb OFF 폴링 폴백 경로 수정(실브라우저 폴링에서만 드러난 결함).

버전(D28 — 정정, D20 번복):
- 릴리스를 7.0.5→7.0.6(patch)로 확정. 7.1.0(minor)은 다른 브랜치에서 별도 진행.
- config/app.php + .env + README + INSTALL + CHANGELOG + 모듈 3 requires 를 7.0.6 로.
- D19 SEO 캐시 이행 업그레이드 스텝을 Upgrade_7_0_6 로 리네임.
 이유: 스텝 버전 게이팅이 version<=toVersion 이라, 7.1.0 이름이면
 7.0.5→7.0.6 업그레이드에서 이행이 실행되지 않는다.

검증: composer test-smoke OK(2·29) · 재생성 알림/업그레이드 이행/진행상황 테스트 green ·
version-sync/version-bump(new-bump)/extension-require green · audit 0 error.
2026-07-19 18:57:51 +09:00
HeuJung 3dd34104cc feat(seo): 사이트맵 대용량 하네스 + 코어 7.1.0 마감
S6(하네스 + 마감): 큐/생성기의 볼륨 붕괴 재발을 정적으로 강제하고,
S1~S5 사이트맵 개편의 공개 표면을 문서화하며 코어를 7.1.0 으로 확정한다.

하네스(주안점 5):
- test-scenario 매니페스트에 scale 축(n≥100000 + assert) 도입.
 내장 YAML fallback 파서 제약으로 block-array of flow objects 형태.
- 룰 2종: scenario-scale-axis-required(batch 매니페스트 scale 강제),
 job-generator-needs-scale-test(Jobs·*Generator 변경 시 scale 시나리오 요구).
 *Command 은 볼륨 비의존이라 제외 — tags:[batch] opt-in 으로 커버.
- tests/scenarios/sitemap-generation.yaml + @scale 마킹 3(Writer/Progress/ManagerMode).

문서: seo-system(증분 저장소·모드·진행상황·lazy contributor)·testing-guide(scale 절)·
broadcasting(sitemap 채널)·hooks(2훅)·database-guide(loc_hash)·cheatsheet.

버전: 코어 7.0.5→7.1.0(config+env+README+INSTALL+CHANGELOG) +
모듈 board/ecommerce/page requires.g7_version >=7.1.0.
2026-07-18 20:56:47 +09:00
HeuJung face022073 feat(seo): 사이트맵 재생성 진행상황 + 실시간/폴링 UI
관리자 "전체 재생성"을 큐(Full 모드)로 던지고 진행상황을 SEO 탭에서 확인한다.
Reverb ON 이면 방송 채널 구독으로 실시간 갱신, OFF 면 상태 API 폴링으로 폴백한다.

- SitemapProgress 진행상황 스토어(queued→running→writing→completed/failed,
 캐시 TTL 3600, 방송 스로틀) + SitemapManager.getStatus 확장(progress + realtime_enabled)
- GET /api/admin/seo/sitemap/status(core.settings.read) 신규
- 관리자 재생성을 항상 Full 모드로 큐잉 + 진행률 기록, Job 최종 실패 시 progress=failed 고정
- core.admin.seo.sitemap 방송 채널 인증(core.settings.read)
- SEO 탭 진행상황 UI(상태 배지/단계/누적 URL/실시간·폴링 안내) + 폴링 분기
- 재생성 중 누적 URL 표기(replaceAllForContributor 삽입 건수 반환 — count 쿼리 없이 스트림 누적)
- ja 언어팩 진행상황 UI 번역 동기화
2026-07-18 18:22:16 +09:00
HeuJung 1629684156 feat(seo): 사이트맵 증분 저장소 + 리소스 리스너 재배선
사이트맵을 리소스 단위로 영속화(sitemap_urls)해 진정한 증분 갱신과
도메인 재쿼리 없는 파일 재작성을 구현했다. 리소스 공개/비공개/삭제 시
해당 항목만 색인이 갱신되고, 관리자=full·스케줄러=auto·리소스 변경=증분으로 동작한다.

- sitemap_urls 마이그레이션 + SitemapUrl 모델. loc(2048) 를 unique 에 직접 넣으면
 InnoDB 키 길이를 초과하므로 loc_hash(sha256) 컬럼을 identity 로 사용
- SitemapUrlRepository(멱등 upsert / remove / 스트리밍) + SitemapIndexer
 (+ 제3자용 sitemap.index.collect_for_resource 필터 훅)
- 4 리스너 재배선: 공개→색인 / 비공개·삭제→색인 해제 + 재생성 잡 디바운스 디스패치.
 3 기여자가 resource_type/resource_id 를 함께 emit 해 전체·증분 identity 를 일치
- 재생성 모드를 SitemapGenerationMode Backed Enum 으로 통일(Command→Job→Manager)
- seo:generate-sitemap 에 --rebuild / --mode 옵션 추가
2026-07-18 16:19:49 +09:00
HeuJung 1a6fca0d6b feat(seo): 사이트맵 기여자 지연 스트리밍 + hreflang 계산 선형화
AbstractSitemapContributor 브리지 base 로 인터페이스를 바꾸지 않고
board/ecommerce/page 기여자를 getUrlsLazy 지연 스트리밍으로 전환해,
게시글이 수백만 건인 사이트에서도 URL 배열을 통째로 적재하지 않는다.
SitemapGenerator 는 capability 를 감지해 지연 경로를 쓰고, base 를
상속하지 않은 제3자 구현체는 기존 getUrls 경로로 그대로 동작한다.

hreflang alternate 집합을 base URL 당 한 번만 계산해 로케일별 <url>
에 재사용하도록 바꿔 계산 비용을 로케일 수의 제곱에서 선형으로 줄였다
(정상 구성 출력은 불변). 다국어 대체 링크 사용 여부 토글과 로케일 수
상한을 추가하고 설정/검증/관리자 UI/다국어(ko·en·ja) 표면을 동반했다.
2026-07-18 15:05:56 +09:00
HeuJung 1c99547bc1 feat(seo): 사이트맵 안전 서빙·잡 견고화·분할 설정 + 캐시 설정 정합
봇 요청 스레드의 동기 생성을 제거하고 디스크 스트리밍 서빙으로 전환했다.
신선도가 만료되면 생성 잡만 예약하고 기존 세트(stale) 또는 503+Retry-After 로
응답해, 대용량에서 봇 요청이 OOM/타임아웃을 유발하던 경로를 끊었다.

- 잡: ShouldBeUnique(고정 uniqueId)+uniqueFor+retryUntil, timeout 300→1800, backoff
- 컨트롤러: 인덱스 stale/503 분기 + 자식 라우트(.xml/.xml.gz 동일 액션)
- 스케줄 withoutOverlapping, 관리자 재생성 큐 이관
- 설정 4키(분할 기준·압축·stale 서빙·수집기 상한) + 고급탭 Sitemap 캐시 짝
- SitemapManager 응답 메시지 다국어화

설정 저장 테스트가 실제 설정 파일(사이트명/언어 포함)을 덮어쓰던 사전결함을
TestCase 전역 격리로 차단하고, 두 탭이 같은 다국어 키를 공유해 한쪽에 틀린
한도를 안내하던 사전결함을 키 분리로 해소했다.
2026-07-18 11:32:11 +09:00
HeuJung 21f666e4b5 feat(seo): 사이트맵 스트리밍 생성 + SEO 캐시 설정 정합화
대용량 사이트에서 사이트맵 생성이 메모리 부족으로 반복 실패하던 문제(공개 )의
1단계로, 전체 URL을 메모리에 적재하던 구조를 스트리밍 분할 생성으로 교체한다.

사이트맵 스트리밍 코어:
- SitemapWriter — 자식 파일 1개 분량만 버퍼링하고 임계 도달 시 flush.
 StorageInterface가 append를 제공하지 않아(put이 전체 덮어쓰기) 택한 구조이며,
 최대 메모리가 "자식 파일 1개 크기"로 유계가 된다. URL 수(50,000)와
 바이트(45MB) 두 임계로 분할해 sitemaps.org 프로토콜을 지킨다.
- 커밋은 _tmp 전량 기록 → live 재배치 → manifest 마지막 기록 순. manifest 존재가
 커밋 완료 신호다. 기존 manifest를 먼저 지우지 않아 스왑 중 봇에게 503이 나가지 않는다.
- SitemapFileStore — 읽기측 + 파일 경로 SSoT. SitemapXmlRenderer — escape 단일 출처.

데이터 접근 계층 정리:
- contributor 3종이 쿼리를 직접 조립하던 것을 Repository 위임으로 전환.
 MySQL 버퍼드 쿼리에서 cursor는 결과셋 전체를 적재하므로 lazyById를 쓴다.
- SeoCacheStatsService → SeoCacheStatRepository 신설 (집계 4쿼리 → 1쿼리).
- audit 룰 service-direct-data-access의 사각 2건 수정 — 확장 디렉토리 정규식이
 상대경로를 매칭하지 못해 *Contributor처럼 접미사 없는 클래스가 전부 미검출이었다.

SEO 캐시 설정 정합화:
- SEO 설정의 캐시 3키가 cache 카테고리 형제 키에 항상 져서 운영자 입력이 조용히
 무시되고 있었다(설정을 cache 카테고리로 이관하며 SEO 탭 옛 칸을 미제거).
 고급 탭을 기준값으로 두되 SEO 탭에 지정이 있으면 오버라이드하도록 SeoCacheSettings
 단일 출처로 정리. "지정 여부"는 null로 판정하므로 기본값을 비웠다.
- 기존 설치는 Upgrade_7_1_0이 이행 — 옛 기본값과 같으면 미설정으로 비우고, 다르면
 운영자 의도로 보아 보존한다.

코어 버전 bump와 루트 CHANGELOG는 계획서 결정(D20)에 따라 마감 단계에서 일괄 수행한다.
2026-07-17 19:59:40 +09:00
HeuJung f157ed1cc9 fix(core-update): sudo 재생성 시 public/storage 링크 소유자가 root 로 바뀌는 문제
sudo 로 실행된 종료 시점 복구가 symlink 로 public/storage 를 재생성하면
소유권 인자가 없어 링크가 root:root 로 남아, 원래 앱 실행 유저 소유였던
링크의 소유권이 바뀌었다. 소유권 복원(restoreOwnership) 이후 재생성되어
복원 대상에서도 빠졌다.

StorageLinkHelper::createLink 가 symlink 성공 직후 부모 public/ 의
owner/group 을 lchown/lchgrp(링크 자체 대상 — chown 은 target 을 따라감)로
상속시켜, 이 프로젝트의 "신규 항목은 부모 소유권 상속" 컨벤션과 일치시킨다.
비-POSIX/함수 부재/권한 부족 시 무해하게 skip.
2026-07-16 14:28:34 +09:00
HeuJung 0a6ff23132 fix(core-update): --prune 시 public/storage symlink 삭제로 업로드 파일 404 되는 문제
--prune 은 릴리즈 소스에 없는 public/storage 런타임 symlink 를 orphan 으로
삭제해 업로드 파일이 404 되었다. 심층 방어 2층으로 차단한다.

- 층1(예방): FilePermissionHelper 에 preserveLinkPaths 화이트리스트 추가.
 applyUpdate 가 public 타깃에 ['storage'] 전달 → 매칭 orphan 링크/junction 만
 삭제 제외 (정밀 보호)
- 층2(복구): StorageLinkHelper::ensurePublicStorageLink 신규 정적 헬퍼.
 CoreUpdateCommand 종료 시점(정상+핸드오프) 호출로 부재/손상 링크 멱등 재생성
 (Windows junction 폴백). beta.5 migration 05 는 이 헬퍼로 위임(상위호환)
2026-07-16 12:42:54 +09:00
HeuJung 7598b76498 fix(seo): 봇 SEO HTML의 코어 app.css 404 참조 제거 (, )
SeoRenderer.getCssPath가 구 Vite manifest 구조만 파싱해 존재하지 않는 고정 경로 /build/assets/app.css 를 반환, 봇 렌더링 HTML(seo.blade)에서 CSS 404가 발생했다. 해당 파일은 실제 사용자 화면에서 미참조하는 Laravel 기본 빌드 산출물이므로, 담당자 답변 방향대로 참조 자체와 getCssPath 메서드를 제거했다. 봇 스타일은 템플릿 CSS($stylesheets)가 그대로 담당한다.
2026-07-16 11:16:55 +09:00
HeuJung c0e0d82459 refactor(extension): 확장 미들웨어 선언적 self-gate 정규화
확장이 SP Kernel 미들웨어 그룹을 직접 조작하거나 라우트 파일에 미들웨어
FQCN 을 직접 부착하던 임시 방식을, 확장이 부착 대상(targets)을 명시 선언하고
코어가 요청 시점에 라우트명·URI 로 매칭해 실행하는 self-gate 로 정규화.
번들 7건(ecommerce/gdpr/pay 3종) 이전, audit 룰 3건·문서·버전 동기화 동반.

부수적으로 generate-skills.cjs 가 재생성 시 신의성실(부수의무) 섹션을 소실
시키던 회귀를 수정 — 스킬별 불릿을 스크립트 SSoT 로 편입해 재생성에도 보존.
2026-07-16 10:22:51 +09:00
HeuJung 8b9bfd0c91 refactor(ecommerce,pay): 간편결제 브랜드 마크 카탈로그 편입 + 애플페이 iOS 게이팅 이관
3사 PG 플러그인의 체크아웃 DOM 인젝터가 렌더 후 브랜드 마크를 주입하고
injector끼리 같은 버튼을 두고 경합하던 구조를 제거한다. 시각정보(색 배지/
SVG 로고)를 결제수단 카탈로그(_cached_brand_mark)로 편입해 코어 체크아웃
레이아웃의 신규 BrandMark 컴포넌트가 데이터 기반으로 직접 렌더한다.
reconcileKginicisPatchedDuplicates(중복 방어)도 함께 제거했다.

애플페이 iOS 게이팅도 injector 밖으로 이관: 이커머스 모듈 DetectDevice
미들웨어가 UA로 기기를 판정해 코어 core.frontend.filter_app_config 훅으로
appConfig.isIos를 주입하고(1차), 템플릿 부트스트랩이 iPadOS 데스크탑 UA를
클라 신호로 보정한다(2차). 코어는 훅만 제공하고 감지 로직은 모듈이 소유한다.

미들웨어는 Router::pushMiddlewareToGroup 이 아닌 HTTP Kernel::appendMiddlewareToGroup
으로 web 그룹에 등록한다(전자는 Router 레지스트리만 갱신해 SSR 셸 요청 파이프라인에
미반영 → isIos 항상 false). sirsoft-gdpr 선례와 동일.

코어 최소 요구 버전 7.0.5 상향(appConfig 필터 훅 도입).
2026-07-15 17:50:39 +09:00
HeuJung e896a47d4f feat(ecommerce,pay): 확장 결제수단(간편결제) 1급화
PG 플러그인이 등록하는 간편결제(네이버페이 등)를 코어가 정식 결제수단으로
인식하도록 결제수단 카탈로그를 SSoT 로 전환했다. 확장 결제수단이
pg_provider=null 로 등록돼 서버가 "PG 결제가 아닌 주문"으로 오인, 결제 실패
주문에 관리자 신규주문 알림이 발송되고 TempOrder 가 즉시 삭제돼 재결제가
막히던 두 결함을 하나의 원인(능력 해석을 enum 이 답하지 못함)으로 근본 해결.

- PaymentMethodResolver 신규: 능력(PG 필요/라벨/환불수단/PG 고정) 해석을
 카탈로그 SSoT 로 통일, enum case 없는 확장 ID 도 동일 판정
- 프론트 결제수단 위장(card) 제거, provider-agnostic 결제 진입(pg_payment_handler)
- 관리자 주문설정 결제수단 목록 PG 고정 3분기 UI (pg_locked/needs_pg)
- 저장된 pg_provider=null 자가치유(read-time merge) + 백필 업그레이드 스텝
- 사용자 주문완료 화면 즉시 결제완료 헤더를 입금대기 여집합으로 판정(basic 1.0.3)
- 전 계층 회귀 테스트: 훅 발화 스파이(관리자 알림 미발화/PG 환불 훅 진입),
 주문 응답 계약(requires_pg_payment/pg_payment_handler)+TempOrder 보존,
 관리자 3분기 렌더, 확장 E2E 인프라
2026-07-15 11:23:51 +09:00
HeuJung ce8e55b390 fix(security): 스케줄러 Shell/Artisan 명령 실행 하드닝
미출시 예약 작업 기능에서 스케줄 권한만 위임받은 계정이 Shell/Artisan
명령으로 임의 코드를 실행할 수 있던 문제를 선제적으로 차단(KVE-2026-1567).
Shell 은 기본 차단 + 실행 파일 화이트리스트 + 셸 미경유 인자배열 실행,
Artisan 은 차단목록. 저장 시점(FormRequest)과 실행 시점(Service) 이중 검증.

아울러 공개 release 보다 앞선 미출시 버전들의 CHANGELOG 출시 날짜를
2026-07-14 로 통일.
2026-07-14 14:48:33 +09:00
HeuJung 47a2e815aa test(security): 활성 확장 디렉토리 소스맵 잔존 회귀 가드 추가
브라우저 실측(제기자 관점 재검증) 중 발견: 기존 가드 3종이 모두 git 추적
파일만 검사해, 실제 서빙 대상인 활성 확장 디렉토리(gitignore 대상)를 통째로
놓치고 있었다. _bundled 를 --production 으로 재빌드해도 {type}:update 를
돌리지 않은 확장은 활성 디렉토리에 예전 .map 과 sourceMappingURL 을 그대로
안고 있었고(6건), 그 상태로 테스트는 전부 green 이었다.

- 활성 디렉토리(templates|modules|plugins)/{id}/dist/js/*.map 를 glob 로 전수 검사
- 배포본 포함 여부는 _bundled 원본 존재로 판정. DB 레코드로 판정하면 테스트 DB 에
 확장 행이 0건이라 전부 skip 되어 영구 green 이 된다(실제로 이 함정에 빠져
 .map 을 심어도 통과했다)
- 가드 실효성은 설치된 확장에 .map 을 심어 red 확인 후 제거해 green 복귀로 실증

서빙 계층이 .map 을 거부하므로 원본 유출로는 이어지지 않았으나(브라우저 실측:
모든 .map 요청이 non-JSON, 원본 TS 4종 전부 404), 배포본 위생상 잔존해서는 안 된다.
2026-07-14 13:13:42 +09:00
HeuJung 90f154ecc0 fix(security): 배포본 소스맵 노출 차단 + 관리자 설정 탭 지연로딩 + 동반 결함 수정
## 소스맵 노출 차단 (공개 이슈 )

배포본에 .map 이 포함돼 원본 TS/TSX 전문(sourcesContent, 한글 주석 포함)이
그대로 노출됐다. 생성·서빙·저장소 유입 3계층을 모두 막는다.

- 빌드 config 15곳(코어 3 + _bundled 12): sourcemap 하드코딩 제거.
 G7_BUILD_SOURCEMAP 판정식으로 교체 — 미설정(로컬 npm run build)이면 생성해
 개발 디버깅 경험을 보존하고, 0 이면 미생성.
- Build{Core,Module,Plugin,Template}Command: 죽어 있던 --production 플래그 복구.
 $productionMode 를 콘솔 문자열에만 쓰고 npm 프로세스에 env 를 넘기지 않아
 산출물이 개발 빌드와 동일했다. runNpmCommand 에 $env 를 추가해 주입
 (--watch 에는 미주입). Symfony Process 는 부모 환경에 병합하므로 PATH 유실 없음.
- Allowed{Module,Plugin}FileType / {Module,Plugin}Service::getMimeType:
 허용 확장자 map 제거 — 익명 200 서빙 차단(템플릿은 원래 미허용, 3종 정책 일치).
- ExtensionBundleService: CSS 번들의 /*# sourceMappingURL=... */ strip 추가.
 CSS 는 블록 주석이라 JS 의 //# 패턴으로는 검출되지 않던 구멍.
- 추적 .map 13개 삭제 + .gitignore 전역 *.map 규칙(경로 한정 시 새 확장에서 누락).

실측: 산출 JS 의 sourceMappingURL 13 → 0, 추적 .map 13 → 0,
확장 디렉토리 npm run build 는 여전히 map 생성(개발 경험 회귀 없음).

## 관리자 환경설정 탭 지연 로딩

코어/이커머스/게시판 환경설정 화면이 진입 즉시 전 탭의 데이터소스를 한꺼번에
호출했다. 각 데이터소스에 활성 탭 조건(if)을 걸어 해당 탭에서만 fetch 하도록
변경하고, 결제 플러그인 3종의 테스트모드 상태 조회도 주문설정 탭으로 한정.

## 동반 결함 수정 (무관 결함 — 재발 방지 목적으로 같은 세션 처리)

- 나이스페이먼츠: 체크아웃 캐시 프리페치가 라우트를 가리지 않아 플러그인 번들이
 로드되는 모든 페이지(홈/게시판/로그인)에서 주문서를 조회 → 비회원 매번 422.
 형제 플러그인(kginicis)의 기존 가드 패턴을 적용해 체크아웃 경로로 한정.
- admin smoke 8건: 대부분 구현이 아니라 테스트 결함이었다 — URL 오타(단수 board),
 존재하지 않는 권한키, 앱 로케일이 en 인데 한국어 라벨 로케이터, DataGrid 버튼을
 먼저 잡던 휴리스틱 로케이터. 레이아웃에는 sticky className / 안정 id 를 부여.
- PHPDoc ↔ Pint 충돌 3건: 설명 없는 @param 은 no_superfluous_phpdoc_tags 가
 삭제해 audit phpdoc-public-method 와 무한 충돌. 한국어 설명을 붙여 양쪽 충족
 (리스너 3종의 설명 누락 메서드도 전수 보강).
- Log::spy 500 3건: spy 가 Log::channel('activity') 를 null 로 만들고
 logActivity 가 null->info 로 \Error → 같은 줄 catch(\Exception) 은 못 잡아
 통과. 운영에선 채널이 항상 로거이므로 제품 무결 → 테스트에 activity 채널
 보존 헬퍼 추가.
- NhnKcpApiServiceTest 5건: 스텁이 Unix 바이너리만 만들어 Windows 분기
 (pp_cli_exe.exe)에서만 실패. #!/bin/sh 는 Windows 실행 불가 → 파일 내 기존
 컨벤션대로 skip 가드.

## 테스트

- 소스맵 범위 130 passed (신규: BuildCommandSourcemapEnv, NoSourcemapArtifacts,
 ExtensionBundleService CSS strip, Allowed*FileType 반전, 자산 서빙 거부)
- 결제 3플러그인 505 passed / 0 failed (skipped 9 = Windows 전용 CLI, 사유 명시)
- 나이스페이먼츠 프론트 38 passed (가드 제거 시 red 실증 후 복원)
- E2E: production-sourcemap-exposure.spec.ts 6건 + 시나리오 매니페스트

버전: sirsoft-admin_basic 1.0.2 → 1.0.3, sirsoft-board 1.0.1 → 1.0.2
(각 manifest/package/lock/composer 동기화). 코어 7.0.4 는 미출시 누적.
2026-07-14 12:50:23 +09:00
HeuJung c4beb116ad fix(security): outbound URL 내부주소·host 검증 전수 강화
서버가 외부 입력으로 목적지가 제어되는 outbound HTTP 요청을 보내는 지점을
전수 조사해, 내부망(사설 IP·루프백·링크로컬·메타데이터)과 host 위조를 차단한다.
KVE-2026-1546(이니시스 인증 URL 접두사 매칭 우회)과 동일 클래스의
지점(언어팩 URL 설치, 스케줄 URL 호출, 배송비 계산 API, GitHub URL 판정)을
코어 공용 유틸 App\Support\OutboundUrlValidator 로 수렴시켰다.

- 접두사 매칭을 host 완전일치로 교체 — userinfo(@) 위장과 접미사 확장 도메인
 우회 벡터 차단.
- 사내 서버 호출이 정당한 관리자 지점(스케줄·외부 API)은 신규 설정
 security.allow_internal_outbound_urls(기본 off)로 옵트인.
- 언어팩 URL 설치는 원격 코드 다운로드라 옵트인과 무관하게 내부 주소 항상 차단.
- 웹소켓 연결 테스트는 localhost·사설 IP 가 정상 구성이므로 구조적 위조만 차단.

각 지점에 red 재현 → 수정 → green 회귀 테스트 + 실브라우저 E2E 로 검증.
수기 API 문서가 자동 재생성으로 소실되는 사고를 막는 PreToolUse 훅을 함께 추가한다.
2026-07-14 09:17:00 +09:00
HeuJung cc705c2f21 test(installer): 엔드포인트 조립 계층 회귀 가드 추가
escapeEnvValue 재선언 fatal 이 통과한 사각지대를 구조적으로 봉인한다.

인스톨러는 Laravel 오토로드 밖의 플레인 PHP require 체인이라, 엔드포인트가
"어떤 파일을 어떤 순서로 조립하는가" 가 곧 함수 선언 순서다. 기존 단위 테스트는
함수를 직접 호출하므로 이 조립 계층을 전혀 검증하지 못했고, 그래서 재선언
fatal(빈 응답 → 프론트 JSON 파싱 에러)이 그대로 통과했다.

추가: 엔드포인트 5종(install-process / state-management / finalize-env /
install-worker / index)의 실제 require 체인을 격리 프로세스로 재현하고,
그 엔드포인트가 호출하는 함수가 모두 정의되는지 + fatal 이 없는지 검증.

이로써 "Call to undefined function"(정의 파일 미포함) 과 "Cannot redeclare"
(가드 없는 중복 선언) 두 fatal 유형이 모두 차단된다 — 증상은 빈 응답으로
동일하지만 원인이 달라 각각 잡아야 한다.

가드 제거 시 9건 중 3건이 red 로 전환됨을 확인 (거짓 통과 아님).
2026-07-13 17:00:59 +09:00
HeuJung 1922003917 fix(installer): 설치 시작 즉시 빈 응답 — escapeEnvValue 재선언 fatal
직전 커밋이 install-process.php 에 installer-runtime.php require 를
functions.php 앞에 추가하면서 발생한 회귀.

installer-runtime.php 는 escapeEnvValue 를 function_exists 가드와 함께
polyfill 로 선언하는데, functions.php 는 같은 함수를 가드 없이 선언하고 있었다.
로드 순서가 뒤바뀌자 "Cannot redeclare escapeEnvValue" fatal 이 발생하고,
JSON 이 출력되기 전에 스크립트가 죽어 응답 본문이 비었다. 브라우저의
res.json 이 이를 "Unexpected end of JSON input" 으로 보고해 설치 시작
즉시 실패했다 (SSE / 폴링 양 모드 — 둘 다 install-process.php 를 경유).

수정:
- functions.php 의 escapeEnvValue 에 function_exists 가드 추가 (근본 방어 —
 require 순서만 되돌리면 다음 사람이 순서를 바꿀 때 재발한다)
- install-process.php 의 require 순서를 다른 엔드포인트와 동일하게 정렬

인스톨러 전역 전수 스캔 결과 중복 선언 함수는 escapeEnvValue 하나뿐이며
이제 양쪽 모두 가드를 갖는다.

회귀 가드: InstallerIncludeOrderTest — 격리 프로세스로 로드 순서를 바꿔가며
fatal 부재 검증 + 중복 선언 함수의 가드 보유를 정적 검사 (수정 전 3/4 fail 확인).
2026-07-13 17:00:59 +09:00
HeuJung 9be8faaea2 fix(installer): 설치 상태 파일에 관리자 비밀번호가 평문으로 잔존하는 문제
비밀을 state.json(0664, 실패 시 무기한 잔존)에 기록하지 않고
runtime.php(0600, finalize 시 삭제)로 채널을 일원화한다.

정상 완료 경로에서는 finalize 가 state.json 을 삭제하지만, finalize
미호출 / unlink 실패 / 설치 중단 시에는 평문이 영구히 남았고, finalize
가드가 재호출을 410 차단해 자가 회복도 불가능했다. 삭제라는 단일 경로에
의존해 평문을 보호하던 구조 자체를 제거한다.

SSE 워커는 세션에 접근할 수 없어 파일 경유가 불가피한데, 그 파일이 0664 의
state.json 이었던 것이 구조적 원인이다. 세션을 보유한 유일한 공통 시작점인
install-process.php 를 비밀 이송 지점으로 삼고, db_seed 가 소비한 즉시
제거한다. 레거시 / 실패 경로는 완료·중단·초기화·finalize 시점 redact 로 방어.

동반 수정:
- 세션 config 재병합으로 무력화되던 기존 DB 비밀번호 마스킹 복구
- db_seed 후 putenv 미해제로 모든 exec 자식에 평문이 ENV 상속되던 누출 차단
- runtime read 비밀번호 폴백이 write 값으로 잘못 대체되던 문제 ( 인접)

이미 설치된 사이트는 7.0.4 업그레이드 스텝이 잔존 파일을 정리한다.
완료 증거가 있으면 삭제하고, 설치 진행 중일 수 있으면 비밀만 제거한다.
2026-07-13 17:00:59 +09:00
HeuJung d85ff9971a test(engine): 감사 지적 3건 반영 — 컴포넌트 번들 E2E 커버 + 약한 단언 실질화
D-4 (실질 커버리지 공백):
- 코어 번들은 "브라우저가 abort 된 script 를 스스로 재요청한다" 는 실측 근거로
 상시-부재 폴백 테스트를 대체본으로 뒀으면서, 형제 경로인 템플릿 컴포넌트
 번들에는 그 대체본조차 없었다. 6경로 중 이 1경로만 E2E 무커버였다.
 상시-부재 → 폴백 안내 E2E 추가 (수정 전 blade 에서 red 확인).

B-9 (제목↔단언 불일치):
- 제목은 "렌더는 진행된다" 인데 단언은 executeInitActions 가 reject 하지
 않는다는 것뿐이라, 그 뒤 init 체인이 죽어도 통과했다. renderTemplate 직접
 단언은 라우터/레이아웃 로드를 거치는 별개 경로라 단위에서는 모킹을 시험하는
 꼴이 되므로, 실제 본체인 "초기화가 에러 화면 없이 완주한다" 로 단언을 바꾸고
 제목도 그에 맞췄다.

B-1 (spy 미사용):
- setRoutes spy 를 선언만 하고 쓰지 않아 "에러 화면이 안 떴다" 만 단언했다.
 라우트가 비어도 에러 화면은 안 뜬다. 재시도로 받아온 routes 가 실제로
 라우터에 반영됐음을 단언한다 (재시도 제거 시 red 확인).
2026-07-12 17:09:03 +09:00
HeuJung 793afb95dc fix(engine): 감사 지적 반영 — 언어팩 버전 bump + 회귀 가드 실질화
전수조사에서 지적된 5건을 코드로 재확인하고 조치했다.

차단성:
- ja 언어팩 1.0.3 → 1.0.4. 1.0.3 은 이미 배포된 버전이라, 같은 버전에 키를
 추가하면 기설치 사이트가 업데이트를 받지 못해 번역이 영영 도달하지 않는다.

회귀 가드 실질화 (green ≠ 회귀를 잡는다):
- 미등록 핸들러 억제 테스트가 동어반복이었다. 값을 손으로 대입하고 그 값을
 읽어, 억제를 삭제해도 통과했다. 실제 dispatch 후 표시 계층이 호출되지
 않음을 감시하도록 교체 (억제 제거 시 red 확인).
- script 번들의 "1건 취소 → 복구" E2E 는 수정 전에도 통과한다. 브라우저가
 abort 된 script 를 스스로 재요청하기 때문 (수정 전 blade 로 실측: 재시도
 훅 0개인데 hits=2, 렌더 정상). 거짓 안심이므로 제거하고, blade 가 고유하게
 보장하는 것 — 재시도/폴백 훅 설치 + 정상 경로 오발동 없음 — 을 단언하도록
 교체 (수정 전 red 확인).
- 재시도 횟수 단언을 하한(>=2)에서 정확값(==2)으로. 과잉 재시도 회귀를
 통과시키던 구멍을 막는다.

정확도:
- waitForHandlers 가 전역 실패 플래그로 판정해, 무관한 확장의 실패가 정상
 로드된 확장의 핸들러 대기까지 포기시켰다. 실패 키 단위로 정밀화한다.
2026-07-12 16:30:38 +09:00
HeuJung 68cbee0e83 fix(engine): 좁은 창에서 레이아웃 편집기가 찌그러지는 문제
레이아웃 편집기는 라우트 트리(280px 고정) + 디바이스 미리보기 캔버스 +
라벨 있는 툴바 버튼 10여 개가 나란히 놓이는 대화면 전용 도구다. 그런데
셸에 너비 하한이 없어 창을 좁히면 이들이 창 너비에 맞춰 압착됐고, 툴바가
flex row 라 각 항목 폭이 깎이면서 라벨이 글자 단위로 줄바꿈됐다("요 소 추 가").

반응형으로 재배치하는 대신 셸이 최소 너비(EDITOR_MIN_WIDTH=1280)를 유지하고
모자란 폭은 브라우저 가로 스크롤이 흡수하도록 했다. 편집기 UI 는 축약 대상이
아니며, 압착을 허용하면 캔버스 실측 폭이 뷰포트에 종속돼 디바이스 미리보기의
의미 자체가 사라진다.

툴바 압착 차단은 직접 자식 전체를 덮는 CSS 규칙으로 뒀다. 버튼마다 인라인
style 로 붙이면 자체 style 을 가진 하위 컴포넌트(템플릿 전환기·언어 전환기)와
이후 추가될 항목이 빠져 같은 결함이 재발한다.

jsdom 은 레이아웃 엔진이 없어 단위 테스트로는 "style 선언 존재" 까지만 잠긴다.
실제로 압착이 멈췄는지는 렌더 박스를 재는 브라우저에서만 증명되므로 Playwright
spec 을 함께 추가했다(수정 되돌림 시 red 확인). 아울러 이 파일의 e2e:allow 면제
주석은 사유("브라우저 가시 UI 무변경")가 더 이상 참이 아니라 제거했다.
2026-07-12 16:13:51 +09:00
HeuJung bfaf551d41 fix(engine): 페이지 로드 요청 1건의 일시 실패가 앱 전체를 죽이는 문제
모바일에서 새로고침을 연타하면 진행 중이던 요청이 취소되는데, 화면 구성에
필요한 요청 하나만 그렇게 끊겨도 전면 "초기화 실패" 로 끝났다. 재시도가 전혀
없었고, 로컬은 왕복이 수 ms 라 재현되지 않아 오래 남아 있었다.

fetch 는 4xx/5xx 로 reject 하지 않고 Response.ok=false 로 resolve 한다.
따라서 TypeError 는 "응답 자체가 없었다"(취소·커넥션 유실)는 뜻이며, 그 경우만
재시도해야 한다. HTTP 응답은 그대로 호출부에 넘겨 기존 상태코드 분기와 이중으로
겹치지 않게 했다.

- routes.json / components.json / 레이아웃 JSON: 네트워크 실패에만 지수 백오프 재시도
- 실패한 promise 가 레이아웃 캐시에 남아 영구 실패가 되던 문제 수정
- 문서 이탈(pagehide) 중에는 에러 화면을 그리지 않는다 (버려질 문서)
- 확장 에셋 로더의 실패 계약을 resolve → reject 로 (실패를 발생 지점에서 표면화)
- 번들 로드가 실패로 확정되면 오지 않을 핸들러를 5초간 기다리지 않는다
- 미등록 핸들러의 raw 내부 식별자가 토스트로 노출되던 것을 차단
- 번들이 끝내 부재해도 백지 대신 새로고침 버튼이 있는 안내 화면

정상 경로 성능은 종전과 같다. script 태그는 정적으로 유지해 프리로드 스캐너
이점을 보존하고, 실패했을 때만 onerror 에서 동적 재시도를 건다.
2026-07-12 15:39:07 +09:00
HeuJung 25c0ab46ec fix(vendor-bundle): 확장에 새 패키지 추가 시 번들 누락 수정
빌드가 composer install 의 입력(스테이징에 복사할 composer.json/lock)만 활성
디렉토리에서 가져오고 manifest 해시는 _bundled 기준으로 기록해, 두 판단이 서로
다른 파일을 보고 있었다.

개발자는 규정상 _bundled 에서만 작업하므로 새 패키지를 추가하면 _bundled 에만
반영되고 활성은 다음 update 전까지 구버전으로 남는다. 그 결과 구버전 lock 으로
설치한 zip 에 신버전 해시가 붙어, 무결성 검증은 통과하는데 새 패키지가 번들에서
누락되고 설치 후 런타임에 클래스 not found 로 터졌다.

설치 입력·해시 기준·의존성 판정·stale 판정 네 가지를 모두 resolveHashTarget
(출력 우선, 없으면 소스 폴백) 으로 통일. 코어는 source == output 이라 무영향.

실증: 이커머스에 psr/log 추가 → 빌드 2 packages(수정 전 1) → zip 에 vendor/psr/log
포함 → module:update → 활성 vendor 배포 + Psr\Log\LoggerInterface 런타임 로드 확인.
2026-07-11 19:50:06 +09:00
HeuJung 13e65131f4 fix(vendor-bundle): 코어 빌드 무한 행 해결 + 이커머스 알림 라벨 raw 노출 수정
이커머스 알림 정의 화면에서 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.
2026-07-11 07:00:34 +09:00
HeuJung 809409f3cf fix(api-docs): 문서 샘플 언어팩이 전역 지원 로케일을 오염시키는 문제 수정
api:docgen --seed 가 생성하는 샘플 언어팩이 scope=core + status=active 로
만들어져 getActiveCoreLocales 승격 쿼리에 걸렸다. 그 결과 시스템 전역
supported_locales/translatable_locales 가 넓어지고, 그 아래에서 시드된
다국어 데이터에 실재하지 않는 로케일 키가 박혔다. 샘플을 비활성화하면
그 키가 TranslatableField 검증을 통과하지 못해 저장이 막힌다.

문서용 샘플은 조회 예시일 뿐 시스템 동작을 바꿀 자격이 없으므로,
승격 경로를 벗어난 조합(module 스코프 + installed 상태 + 실재 번들 로케일)
으로 생성하도록 바꾸고 audit 룰로 재발을 차단한다.

README 재생성 시 사람이 덧붙인 헤딩 섹션이 통째로 소실되던 사전 결함도
함께 수정했다(경계를 임의의 ## 이 아닌 확장 목차 헤딩으로 특정).
2026-07-10 19:27:27 +09:00