Commit Graph
40 Commits
Author SHA1 Message Date
HeuJung 3945b6f1b3 feat(core,extensions): 구동 에셋 자체 제공 · 자산 실패 폴백 · 운영자 추가 에셋(custom/)
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.

브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.

자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.

두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.

FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.

동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
2026-08-27 16:47:14 +09:00
HeuJung 0253c1aa68 fix(core): sudo 업데이트의 root 캐시 산출물 소유권 정상화 + 캐시 쓰기 fail-soft
클린 7.0.9→7.0.10 실서버 업그레이드에서 전면 500 재발. 치명점을 캐시 파일
내용 역산으로 확정 — sha1('g7:_idx:g7:core') = 모든 remember 가 갱신하는
G7 캐시 키 인덱스 파일이 업데이트 마지막 root 쓰기(cache:clear 직후 버전
bump·상태/훅 캐시 재생성)로 root 소유 생성되면, 웹 프로세스의 모든 캐시
쓰기가 인덱스 갱신에서 Permission denied 로 죽어 부팅 경로가 500 이 된다
(로거 도달 전이라 laravel.log 공백). 직전 커밋의 자식측 게시 게이트만으로
부족했던 이유: 마지막 root 쓰기의 주체가 구코드 부모라 그 이후에 신코드가
개입할 지점이 없다.

- 원인 수정: CoreUpdateService::normalizeRuntimeOwnershipAfterRootRun —
 root 실행 시 storage/framework/cache·bootstrap/cache·storage/app/ext-bundles
 를 디렉토리 소유자 기준 재귀 정상화 + 그룹 쓰기 동기(업그레이드 로그
 정상화 선례 확장). 부모·자식 커맨드의 모든 흐름 종료부에 짝으로 배선 —
 이후 업데이트는 root 산출물을 남기지 않고 과거 잔재도 재귀 자가 수복
- 안전망: AbstractCacheDriver 쓰기 fail-soft — put/forget/putMany 는 false,
 remember 는 콜백 1회 실행 보장 후 저장 실패를 삼키고 결과 반환(무캐시
 동작), 경고 로그는 조합당 프로세스 1회. 구코드가 남긴 오염처럼 신코드가
 선제 개입 못 하는 상황에서도 화면이 죽지 않는다 — 캐시는 최적화다
- red→green: CacheDriverWriteFailSoftTest 4케이스, CoreUpdateRuntimeOwnershipTest
 (비-root no-op + 종료부 짝 호출 소스 훑기)
2026-08-25 19:00:12 +09:00
HeuJung 5ba7a83597 feat(core,engine): 부트스트랩 리소스 정적 게시(bake) 및 폴백 체계 도입
공개 제보 https://github.com/gnuboard/g7/issues/122 대응 — 초기 부트스트랩
리소스(다국어 병합·컴포넌트 정의·라우트·확장 번들·템플릿 dist)를 캐시 버전
디렉토리(public/build/ext/{v}/)에 실파일로 게시해 웹서버가 rewrite 전에 직접
서빙한다. 부트 임계 경로의 PHP 왕복을 제거하고(실측 TTFB 131~144ms → 1~7ms),
미게시·부분게시·GC 직후에는 fetch·태그·번들 3계층이 종전 API 로 즉시 폴백한다.

- 게시: 원자적 tmp→rename→manifest(존재=완료), 캐시 락 단일 실행, 인라인 GC
 (현재+직전 1개), incrementExtensionCacheVersion 단일 지점 terminating 트리거
 + blade 자가 치유 + 설치기 태스크(best_effort) + 일일 cleanup 스케줄,
 sudo 업데이트 대비 소유권 정상화(normalizeOwnership)·prune/백업 제외
- 프론트(engine-v1.61.0): blade 주입 cache_version 1급 시드(이중 부트 로드 제거),
 fetchStaticFirst 즉시 폴백, ComponentRegistry 버전 키드 매니페스트,
 ModuleAssetLoader 번들 정적→레거시 폴백, asset-url-recovery staticToLegacy 역변환
- 폴백 API 품질: lang/components/routes ETag+304 + 환경 분기 Cache-Control,
 열화 라우트 스냅샷 공개 캐시 금지(서버측 캐시 회피와 대칭), 게시 .htaccess
 mod_deflate + nginx gzip 스니펫(압축 전송량 회귀 방지)
- SEO 정합: 봇 HTML 은 GC 대상 정적 URL 미사용(allowStatic:false), props $switch
 봇측 해석 구현(engine-v1.56.0 패리티), 패리티 룰 expression-dialect 그룹 신설,
 상주 allow 헤더 제거로 잠금 복원, _comment* 접두 주석 키 분류
- 검증: 전 수정 red→green 4단계, Playwright 라이브 21건, Chrome MCP 24축+M1~M3,
 봇 curl 3축, 캐시 저장소(file/redis/database) 축 판정, 히스토리·공개이슈·커밋
 이력 전수 재조사 반영
- 코어 7.0.10, engine-v1.61.0. kill-switch: G7_STATIC_CACHE=false
2026-08-25 17:02:53 +09:00
HeuJung 03cbb99196 fix(extension): 확장 수명주기 캐시 무효화 순서 회귀 및 실패 사유 전달
route:cache 는 새 앱을 부팅해 라우트를 수집하고, 그 부팅의 확장 라우트 프로바이더는
DB 가 아니라 캐시된 활성 확장 목록을 읽는다. 그래서 rebuild 가
invalidate*StatusCache 보다 앞서면 방금 바뀐 상태가 빠진 채 라우트가 박제되고,
라우트 캐시에는 스캔 폴백이 없어 오류도 로그도 없이 404 가 된다. 무효화를 굽기 직전이
아니라 DB 상태 쓰기 직후로 올려, 같은 목록을 읽는 훅 매핑 캐시까지 함께 바로잡았다.
update 경로는 Updating 전이 직후에 비우면 그 창의 오토로드 갱신이 확장을 비활성으로
판정하므로, 상태 복원 직후에 비운 뒤 훅 캐시를 다시 굽는다.

플러그인 라우트 프로바이더에는 활성 게이트가 없어 비활성 플러그인의 API 가 계속
응답했다. 화면·메뉴만 사라지고 기능은 살아 있는 상태였다. 모듈과 같은 기준을 적용했다.

실패 사유가 하위 계층에서 버려져 관리자 화면에 :error 자리표시자가 그대로 노출되던
문제도 고쳤다. 반환 경로를 깨지 않도록 배열 키 reason 과 뒤에 붙인 선택적 out
파라미터로 사유를 실어 올리고, 확장이 수명주기 훅에서 사유를 남길 수 있는 통로를
추가했다. 설치 경로의 광역 RuntimeException catch 는 도메인 예외로 좁혀 원본 키와
파라미터를 응답에 싣는다 — 상태코드 422 는 유지해 사용자 계약을 함께 바꾸지 않는다.
언어팩 화면은 프로덕션에서 예외 원문을 싣지 않는 것이 확정된 계약이므로, 자리를
일반 문구로 채우는 대신 치환 자리 자체를 제거했다. 원문은 종전대로 errors 통로를
거쳐 디버그 모드에서 도달한다.
2026-08-21 17:09:30 +09:00
HeuJung e819db9d42 fix(core): 공개 템플릿 config 캐시 무효화 누락과 확장 캐시 삭제 경로 동종 결함 정비
공개 config.json 의 고정 캐시 키(template.config.{identifier})가 어떤 갱신
경로에서도 forget 되지 않아 template:update/cache-clear/비활성화/삭제 후에도
최대 1시간(TTL) 이전 manifest 로 응답되던 결함(공개 )을 수정한다.

- TemplateManager::clearTemplateCache 공개화 — config/components_manifest(두
 변종)/현재 버전 routes·language 키 능동 forget, updateTemplate cleanup 호출
- forgetLayoutCacheKeys 가 편집기용 .with_source_meta 변종까지 삭제
- ext.cache_version 쓰기를 incrementExtensionCacheVersion 단일 지점으로 통일
- module/plugin:cache-clear 의 유령 키 forget 을 실재 캐시(레이아웃 서빙/해석
 캐시, 상태 키, 미들웨어 인덱스, 버전 bump) 무효화로 교체
- 공개 routes/language/layout API 의 ?v 생략 폴백을 0 → 현재 버전으로 교정
2026-08-17 13:22:48 +09:00
HeuJung cfc2caba1b feat(ckeditor5,board,ecommerce,page,core,admin_basic): 미참조 에디터 업로드 이미지 정리 신설 + 업로드 고아 동종 결함 7건 전수 해소
공개 (@Tuwasduliebst 제보) 대응.

- ckeditor5: 미참조 이미지 정리 엔진 신설 — 참조 판정(hash + 파일명 2토큰,
 filter_reference_sources 훅으로 확장 콘텐츠 등록) + 정리 커맨드(기본 꺼짐 옵트인,
 3중 게이트) + 업로드 관리 화면(권한 분리·참조 배지·개별/선택 삭제) + 기설치본
 설정 백필 upgrade step
- board: 임시 첨부 GC(상시) + 소프트삭제 첨부 영구 정리(옵트인) + 설정 UI,
 저장 payload 누락(F4)·빈 temp 디렉토리 잔존(F5) 수정
- ecommerce: 임시 상품 이미지 GC + 카테고리 삭제 시 이미지 파일 미삭제(S4) 수정
- page: 임시 첨부 GC
- 코어: 고아 첨부 정리 커맨드(옵트인, site_logo 보호) + 사이트 로고 교체 시
 구 로고 재편입 차단(S6) + DB 백업 미구현 501 정직화(B3) + 플러그인 관리자
 메뉴 설치·업데이트 자동 동기화(F1·F7)
- 동반: 플러그인 설정 검증 오류 필드명 라벨화(F3), Toggle 노드 type 정정
 수평 전개(marketing 1.0.2 bump·gdpr·ecommerce), ja 언어팩 4종, API 문서,
 hooks.md, 시나리오 매니페스트 8종, Playwright E2E 4종
2026-08-17 02:44:54 +09:00
HeuJung 71abdeaf60 fix(security): KVE-2026-1914/1915/1919 remediation 전건
위임 관리자(부관리자)가 권한·역할·표현식·비밀 콘텐츠 경계를 우회하던
결함군을 계층 대칭성 원칙으로 전건 차단한다. 약한 경로가 정상 응답을
내보내는 것이 유일한 증상이라, 게이트를 생산 지점 한 곳(SSoT)에 두고
같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유하도록 맞췄다.

- 등급 상한(rank ceiling): 슈퍼관리자 보호·역할/사용자 역할 배정(추가·제거
 대칭)·일괄 상태변경·순서변경을 상세 경로와 동일 강도로 재적용. 가드는
 DB 쓰기에 선행하여 거부 시 상태 불변.
- 레이아웃 표현식: new Function/with 실행을 AST 화이트리스트 평가기로 교체.
 비-문자열 computed 키 정규화(normalizeKey)·Object facade(리플렉션 static
 제거)·legacy 접근자 차단. 저장측 검증·정적 검사와 3계층 동형.
- secret 게이트: 비밀글의 댓글·첨부·문의 독립 경로 재적용, hash 파일서빙
 소유권·비밀·발행 상태 검사 통일.
- 신뢰 스크립트 호스트: 확장 선언 기반 + same-origin 브라우저 정규화를
 런타임·저장측·정적검사 3층 동형화.
- 회귀 감지: 단위·Feature·E2E·시나리오 매니페스트 전축 + audit 룰 4종 신설.
2026-08-17 01:45:09 +09:00
HeuJung d970117149 fix(core,ecommerce,board,admin_basic): 고아 카탈로그 차단 + 설정 저장 파이프라인 정합
확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다.
그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다.
예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다.

- 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용.
 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라
 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다.
- 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리.
 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다.
- 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의
 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다.
- 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을
 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지
 알 수 없었다.

시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects
검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다.
그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
2026-08-14 17:28:49 +09:00
HeuJung 057e002ad1 feat(core,ecommerce,ckeditor5,board,admin_basic): 공개 자산 직접 URL(CDN) 서빙 옵트인
StorageInterface::url 의 public 하드코딩을 완화하고 core.storage.filter_url 훅을
신설해, 완전 공개 자산(상품·카테고리·리뷰·에디터 이미지)의 직접 URL 서빙을 옵트인으로
열었다. 미설정 시 기존 스트리밍 100% 보존.

디스크 결정은 확장 개별 설정 > 코어 전역 > 미설정 순이며, 서빙·삭제·이동은 행에 기록된
disk 를 따라 전환 전후 자산이 혼재해도 각자 실제 저장 위치를 향한다. 고아 disk(공급
플러그인 비활성화)는 스트리밍으로 폴백해 500 이 되지 않는다.

공개 이슈 (@lyg-kaban 건의) 대응.
2026-08-14 08:05:54 +09:00
HeuJung c77ddee03b fix(core,ecommerce): Windows 확장 업데이트 파일 잠금 해소 + 통화 삭제 영속화
Windows 는 하위 트리에 열린 핸들이 하나라도 있으면 디렉토리 rename 을 막는다.
잠금 프로세스를 찾아 종료하던 기존 대응은 디렉토리 핸들을 감지하지 못했고
사용 중인 편집기를 예고 없이 죽였다. rename 이 막히면 파일 단위 연산으로
폴백해 어떤 프로세스도 건드리지 않고 교체를 끝낸다. 커밋 전 점검에서
pint.json 이 코어 업데이트 대상에 빠져 있던 것도 함께 등재했다.

이커머스는 기본 제공 통화 삭제가 저장 응답에서 즉시 부활했다. 항목 단위
보충 병합이 "소실" 과 "의도적 삭제" 를 구분하지 못한 탓이라, 삭제 의도를
서버가 도출해 저장본에 기록하고 병합이 그 기록을 존중하게 했다. 그 통화로
결제된 과거 주문의 표기가 흔들리지 않도록 소수 자릿수 해석도 스냅샷 우선으로
바꿨다 — 금액은 원래 스냅샷 환율을 써 안전했으나 자릿수만 현재 설정을 봤다.
2026-08-12 10:10:01 +09:00
HeuJung 7b859d4200 fix(core,basic,ecommerce): 쇼핑 화면 호출 통합 원복·검색 주소 승계·라우트 캐시 갱신 및 상한값 설정 저장 정합
합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다.

쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다.
레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다.
되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스
23줄)은 재적용했다. 성능 개선분은 전부 잔존한다.

통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가
주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로
보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다.

확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을
막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다.
같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고,
코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다.

상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를
지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는
대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다.

환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두
있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를
설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다.
범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다.

언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가
HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
2026-08-06 11:13:54 +09:00
HeuJung 2ae1972a3b perf(core): 대용량 목록 상한 총 건수·커서 계약 신설 및 요청당 반복 실행 비용 정리
공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다.
응답·행의 무게를 다룬 · 과 겹치지 않는다.

공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이
기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은
per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지
번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로
정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은
계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다.

계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면
그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다.
응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새
필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate
응답은 필드 단위로 이전과 동일하다.

요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일
재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어
화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한
변경이 종전처럼 다음 요청에 반영된다.

검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한
엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어
활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩
대신 선언형 config 로 옮겼다.

총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을
돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
2026-08-06 11:13:51 +09:00
HeuJung b10e89bef0 fix(core,board,ecommerce,page,gdpr,ckeditor5,kginicis,admin_basic,basic): 목록 조회·검증 계층 정비 및 감사 지적 전건 처리
목록 조회 성능 축(깊은 OFFSET·정렬·색인)과 그 검증 계층에서 계획서 전수검수와 3회에 걸친
감사가 지적한 항목을 처리했다. 세부 경위는 의 각 회차 문서에 있다.

검증 계층 — 컨트롤러가 base Request 를 직접 주입받던 확장 13곳을 전용 FormRequest 로 옮겼다.
옮기면서 기존 동작 계약은 그대로 두었다: 상한 초과 limit 을 거부하지 않고 상한까지 반환하던
공개 API, per_page 를 범위로 조정하던 관리자 목록, 미지원 period 를 year 로 해석하던 인기글
목록 모두 종전과 같은 응답을 낸다. 상한/폴백을 rules 로 승격시키면 200 이던 응답이 422 가
되어 기존 링크가 깨지므로, 규칙은 타입만 닫고 클램프·폴백은 접근자가 맡는다. period 는
접근자가 닫힌 집합만 반환해 캐시 키 공간도 함께 닫힌다.

ckeditor5 이미지 업로드만 ResponseHelper 봉투를 쓰지 않는다. 응답을 파싱하는 주체가 CDN 으로
로드되는 상위 CKEditor5 43.3.1 의 SimpleUploadAdapter 라 규약을 바꿀 수 없어, 각 응답 지점에
사유를 명시한 면제를 부착하고 근거를 API 문서에 남겼다.

하네스 — 룰 5개의 대상 경로 패턴이 매처와 맞지 않아 번들 확장 컨트롤러가 검사 대상에서
통째로 빠져 있었다. 패턴을 고치자 확장 위반 17건이 드러나 전건 처리했다. severity 오타가
요약 집계 양쪽에 안 잡혀 "0 error" 로 보고되던 문제도 런너 사전 검증으로 막았다.

정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
2026-08-02 15:29:29 +09:00
HeuJung 5f4a733b3b feat(core,board,ecommerce,page): 성능 계측 인프라 4축 확장 및 계측 대상 선언 지점 신설
계측 축이 "목록 SELECT" 하나였고 프로파일 12종이 커맨드 파일에 하드코딩돼
있어 확장이 자기 대상을 추가할 수 없었다. 확장이 설치·제거되는 설치본마다
"실제로 존재하는 대상"이 달라지므로, 대상을 소유자가 선언하고 코어가 수집하도록
뒤집었다.

- 선언 지점 두 곳, 스키마 하나: 코어 config/benchmark.php,
 확장 getBenchmarkProfiles (모듈/플러그인). 수집은 method_exists 기반이라
 인터페이스를 직접 구현한 서드파티 확장이 깨지지 않는다.
- 축 4종(목록/화면/쓰기/배치)을 실행기로 분리하고 컨테이너 태그로 주입 —
 축이 늘 때 커맨드를 고치지 않는다.
- 화면 축은 HTTP 커널로 내부 요청을 처리한다. 라우트만 dispatch 하면 전역
 미들웨어를 건너뛰어 화면과 다른 것을 재게 된다. 인증은 프로파일이 선언한
 권한만 가진 임시 계정이 기본이며 --as 로 기존 계정을 지목할 수 있다.
- 화면/쓰기 축은 계측 계정 생성부터 처리까지 롤백되는 트랜잭션 안에서 실행해
 운영 DB 에도 잔여 데이터를 남기지 않는다. 배치 축은 내부 커밋·락 보유 때문에
 감싸지 않고 --allow-write 로만 통제한다.
- 잘못된 선언은 조용히 버리지 않고 사유를 경고로 남긴다. 버려진 선언이 곧
 계측 사각이 되기 때문이다. 전부 건너뛴 실행은 종료 코드 1 로 끝난다.
- 리포트는 환경 정보를 함께 적어 저장소 밖(storage/app/benchmarks)에 남긴다.
 측정값이 실행 머신에 종속되므로 저장소에 축적하면 비교 불가능한 수치가 섞인다.

작업 중 발견해 함께 고친 것:

- 이관 프로파일 3종의 soft_delete 선언이 실제 스키마와 어긋나 있었다
 (users/product_inquiries/pages 에 deleted_at 없음).
- 주문 목록이 상태 미지정 시 임시 주문 상태를 NOT IN 으로 제외하는데 선언에
 빠져 있었다. filters 가 등가 비교만 지원한 것이 근본 원인이라 연산자 형태를
 받도록 넓히고, 미허용 연산자는 측정 전에 거부한다.
- 댓글 프로파일은 지배적 술어가 상관 서브쿼리라 선언형으로 재현할 수 없어
 제거했다. 맨 테이블 스캔은 어느 화면도 내지 않는 수치가 된다.

g7:bench:pagination → g7:bench 로 개명(4축을 재므로). 이전 이름은 별칭으로 남겼다.

테스트 61건 통과, 라이브 18종 전건 측정.
2026-08-02 14:44:33 +09:00
HeuJung 1eedbbdda1 fix(core,engine): OFFSET 순회 중 필터 컬럼 변경으로 인한 처리 누락 수정
Laravel chunk/each 는 OFFSET 기반이라, 콜백이 행의 필터 소속을 바꾸면
(필터 컬럼을 채우거나 매칭 행을 삭제하면) 다음 조회가 그만큼 앞으로 밀려
미처리 행을 조용히 건너뛴다. 250건/청크 100 이면 정확히 100건이 남는다.
코어·번들 확장 전수 조사로 확정한 6곳을 chunkById(키셋 순회)로 통일했다.

브라우저 검수(28축)에서 계획에 없던 결함 12건이 드러나 같은 사슬에서
수정했다. 이 중 layout_strategy=keep 무효화와 수정 감지 실패 시 "수정 없음"
오보는 사용자 작업 소실에 직결된다.

재발 방지로 audit 룰을 추가했고, 수정 6곳을 원복해 red 6건 / 복원 후 green
0건으로 판별력을 검증했다.
2026-07-31 21:50:45 +09:00
HeuJung b127afcea7 fix(identity): 잔여 과제 전건 처리 + 로딩 표시 실렌더 확정
앞선 커밋이 CHANGELOG 에 주장한 "로딩 표시가 나오지 않던 문제 수정" 이 실제로
화면에 나타나는지 브라우저로 확정했다. 전달 경로부터 DOM 마운트·해제까지 측정했고,
지연·에러 응답에서도 영구 로딩이 남지 않음을 확인했다.

측정 과정에서 앞선 세션의 판단 하나가 틀렸음을 확인했다 — "오버레이 DOM 이 어느
화면에서도 관측되지 않는다" 는 관측 대상 노드를 잘못 잡은 것이었다. 스피너 방식
레이아웃은 다른 노드를 쓰며, 대조군에서도 안 보였던 이유가 이것이다.

미뤄 두었던 과제 4건을 함께 구현했다.

- 인증창을 그냥 닫고 이탈하면 그 인증 요청을 서버에도 종료로 표시한다.
 이전에는 브라우저만 알고 있어 인증 이력에 "보낸 뒤 소식 없는" 항목이 남았다
- 플러그인 설정 화면이 저장된 비밀값을 다시 내려받던 것을 가려서 표시한다.
 그 항목을 건드리지 않고 저장하면 저장된 값이 그대로 유지된다
- 언어팩 반영 누락 검사가 버전만 비교해 "버전 같고 내용 다름" 을 놓치던 사각을
 콘텐츠 비교로 닫았다
- 확장이 검증 규칙을 추가할 때 쓰는 필터 이름 6개를 표준 형태로 통일했다.
 예전 이름도 함께 발행하므로 기존 확장은 수정 없이 동작한다

정적 검사 룰 2종의 오탐·사각을 함께 고쳤고, 개명으로 검사 대상에 편입된 파일의
사전 결함 8건을 해소했다. 검수용 데이터는 전량 정리했다.
2026-07-31 00:32:48 +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
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 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 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 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 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 c26b49a997 fix(core): installer_completed fast-path가 빈 DB 배포 시 부팅을 깨뜨리는 문제 수정 및 7.0.3 릴리스
INSTALLER_COMPLETED=true 가 적힌 .env 를 빈 DB 새 서버에 복사한 뒤
php artisan migrate 로 테이블을 만들기 전에 앱이 부팅되면, installer_completed
fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 table not found 로
부팅을 깨뜨렸다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는
닭-달걀 상태가 된다.

fast-path 자체(매 요청 information_schema 쿼리 제거 성능 최적화)는 유지하되,
이미 존재하던 참조 구현(LanguagePackServiceProvider::isRegistryReady)의 방어
패턴을 InstallerContext::isSchemaMutatingCommand 로 SSoT 화하여 나머지 5곳에
통일 적용했다.

- app/Support/InstallerContext.php: 마이그레이션 계열 콘솔 명령 판정 헬퍼
 (migrate/migrate:fresh/migrate:refresh/migrate:rollback/migrate:reset/db:wipe)
- Caches{Module,Plugin,Template}Status: fast-path 앞에 && 가드
- {Module,Plugin}RouteServiceProvider: hasTable 검증 진입 조건에 || 가드로
 무방비 pluck 도달 차단
- LanguagePackServiceProvider: 인라인 argv 검사를 헬퍼로 치환(migrate:refresh 보강)

부류 B(NotificationHookListener, IdentityPolicyRepository)는 fast-path 이후
쿼리가 이미 try/catch 로 보호되어 빈 DB 에서도 안전하므로 수정 제외.

최초 설치 흐름은 migrate 시점에 완료 플래그가 없어(false) 이번 가드의 영향권
밖이며 원래대로 hasTable 폴백을 탄다. 가드가 실제 발동하는 유일 케이스는
"플래그 true + 빈 DB" 뿐이고, 정상 운영 웹 요청은 fast-path 성능이 유지된다.

회귀 테스트: InstallerContextTest 8건 + InstallerCompletedGuardTest 5건 추가
(수정 전 5건 fail → 수정 후 green). 관련 스위트 355 passed.

7.0.3 출시 버전 bump: 코어 버전 SSoT 7곳 + CHANGELOG 동기화.
2026-07-08 22:01:53 +09:00
HeuJung 487283b33a fix(core): core:update 증분 모드가 _bundled 확장 파일을 반영하도록 protected 오버라이드
targets 에 명시된 {modules,plugins,templates,lang-packs}/_bundled 가
상위 protected(modules 등)에 걸려 3-way apply 목록에서 제외되던 결함 수정.
targets 가 protected 보다 더 구체적이면 오버라이드해 통과시켜, 코어 업데이트로
번들 확장 갱신 파일(composer.json·vendor-bundle.json 등)이 정상 반영되도록 한다.
자동 발견 폴백의 방어 및 prune 롤백 경로는 불변.

아울러 이슈 템플릿의 사용법 문의 링크를 Discussions 에서 sir.kr Q&A 로 전환.
2026-07-08 13:54:18 +09:00
HeuJung d0943d6cb6 feat(core): core:update 기본을 코어 변경분만 적용(3-way skip)으로 전환
core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여
사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를
차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한
파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재
디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면
--prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전
회귀 + 안내를 출력한다.

기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune
컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune
커맨드 표면 테스트를 추가했다.

번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서
동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어
번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
2026-07-07 22:45:16 +09:00
HeuJung a958443d69 feat(core): 모듈/플러그인 프론트엔드 에셋 서버측 번들 병합
활성 모듈/플러그인마다 프론트엔드 IIFE JS·CSS 가 개별 HTTP 요청으로 로드되어
확장 수에 비례해 요청이 늘어나는 오버헤드를, 종류별 1개 번들로 서버 병합해 줄인다.
각 확장 IIFE 자가등록 계약(핸들러/리스너/preblocker)은 priority 순 물리 병합으로
실행 순서가 보존되어 그대로 동작한다.

## 백엔드
- 신규 ExtensionBundleService — 정렬(uasort priority)·필터(global)·concat(\n;\n
 구분자로 ASI 경계 보호)·소스맵(prod strip / dev rewrite)·version-in-path 디스크
 캐시·확장별 fine-grained try/catch(한 확장 실패가 번들 전체를 붕괴시키지 않음)
- AbstractModule/AbstractPlugin::getBuiltAssetAbsolutePaths — _bundled 경로
 정확 해석용 절대경로 게터(getAssetFilePath 하드코딩 복제 회피)
- PublicModule/PluginController::serveBundle{Js,Css} + routes/api.php 4개
 (/api/{modules,plugins}/bundle.{js,css}?v={version}, fileResponse ETag/304 재사용)
- config/filesystems.php ext-bundles 디스크 + storage/.gitignore + 루트 .gitignore
- 신규 ext-bundles:cleanup 커맨드 + {module,plugin,template}:cache-clear 번들
 정리 통합 + dev-dashboard 버튼

## 프론트엔드 (engine-v1.52.0)
- TemplateApp.loadExtensionAssets — 개별 로딩 → bundleUrls 기반 모듈→플러그인
 순 번들 로딩. bundleUrls 부재 시 개별 로딩 폴백(회귀 안전)
- ModuleAssetLoader.loadBundle + parseBundleUrlsFromConfig + ExtensionBundleUrls
- TemplateComposer/UserTemplateComposer bundleUrls 뷰변수 + app/admin.blade.php
- core:build 재빌드 산출물(public/build/core/*)

## 버전
- 코어 7.0.1 → 7.0.2 bump. 7.0.1 이 이미 2026-07-03 공개 릴리스이므로 그 위 신규
 기능은 +1 bump(new-bump). config/app.php(SSoT) + .env{.example,.testing.example,
 .testing} + README 뱃지 + INSTALL 예시 + 루트 CHANGELOG 신규 [7.0.2] 섹션 동기화.
- 확장 g7_version 상향 안 함(>=7.0.0 유지) — 6개 프론트 에셋 확장이 신규 코어
 표면을 자기 코드에서 호출하지 않고 개별 서빙 라우트 존치+프론트 폴백으로 코어
 7.0.0 에서도 동작 → 실제 의존 없음( 결정).
- 부수 결함 수정: config/app.php 편집으로 활성화된 core-update-targets-coverage
 룰이 v1.51.0 에서 추가된 vite.config.{editor,devtools}.js 가 app.update.targets
 에 미등재됨을 검출 → 추가. 코어 업데이트 시 편집기/devtools 빌드 설정 갱신 누락
 방지(CHANGELOG [7.0.2] Fixed).

## 규정/audit/문서/테스트
- ("확장 번들 병합" 절+파일유형표+E2E경로) / AGENTS.md /
 docs/extension/module-assets.md / docs/cheatsheet.md
- coverage.json 4항목 + codified 룰 4종(url-same-origin, concat-separator,
 asset-path-getter, clears-template-caches-no-file-delete)
- 루트 CHANGELOG [7.0.2] Added/Fixed / 엔진 CHANGELOG engine-v1.52.0 Added
- Vitest 11(loadBundle + concat 자가등록) + PHPUnit 17(Service 12 + Feature 5)
 + Playwright spec + 시나리오 매니페스트
- 브라우저 실측(g7.dev): 홈/관리자 번들 각 1건·개별 iife 0건, gdpr 인터셉터
 정상, 활성화 토글 반영, 장바구니→체크아웃·게시판 글쓰기 정상. g7_2.dev 미적용
 대비 확장 요청 11~12→4건, DOMContentLoaded ~843~957→605~741ms(페이로드 무손실)

## 도구 보완
- pre-commit-review 스킬 에 엔진 경로 감지 게이트(5-pre + 보고 5e행) 신설 —
 매핑 표에 엔진 파일이 있으면 엔진 CHANGELOG 판정을 강제(이번 초기 누락 재발 방지)
2026-07-05 21:57:39 +09:00
HeuJung c08dae7cb0 perf(core): 서빙 API 부팅 비용 최적화 (information_schema 가드 + 훅/오토로드/config 캐시)
매 요청 균일하게 발생하던 부팅 오버헤드를 4개 축에서 제거한다.

- 설치 완료 상태에서 매 요청 반복되던 information_schema 조회(알림·본인인증
 테이블 hasTable)를 installer_completed 가드로 스킵. 미설치 환경은 기존
 hasTable 폴백 유지.
- 코어+모듈+플러그인 정적 훅 매핑을 bootstrap/cache/hooks.php 에 사전 계산해
 매 요청 디렉토리 스캔·리플렉션·클래스 로딩을 제거(route:cache 동형).
 등록↔발화 계약·매핑 바이트 동일. 확장/코어 변경 시 자동 재생성, 캐시
 부재·손상은 스캔 폴백.
- 확장 소스(Modules\*/Plugins\*)를 autoload-extensions.php 의 classmap 에
 편입해 findFile 파일시스템 스캔을 제거(느린 FS·cold OPcache 환경 직격).
 클래스 로딩은 여전히 lazy, PSR-4 폴백 유지.
- config 캐시를 변경 지점(설정 저장/코어·확장 업데이트/APP_KEY 재생성/설치
 완료)에서 clear 후 즉시 재생성하도록 ConfigCacheHelper 로 일원화. clear 만
 하고 방치돼 캐시가 영구 비활성으로 남던 성능 손실 제거. HookCacheManager::read
 요청당 1회 로드(memo)로 중복 파싱 제거.

부팅 매핑 fingerprint 스캔↔캐시 완전 동일(229 action + 35 filter). 신규/수정
8개 스위트 54 pass. 순수 내부 부팅 인프라 — 확장 공개 표면·훅 계약 불변.
2026-07-03 17:47:23 +09:00
HeuJung 27901dd254 fix(core): 코어 업데이트가 특정 상황에서 중단되던 두 결함 수정
Windows JUNCTION 복사 실패:
- public/storage 는 storage:link 가 Windows 에서 생성하는 JUNCTION 으로,
 PHP is_link/is_dir 이 모두 false 를 반환해 copyDirectory 가 파일로
 오판 → copy(디렉토리) 예외로 코어 업데이트가 중단됐다.
- isReparsePoint 로 junction 을 정확 감지(link/file/dir 모두 false +
 readlink 성공)하고, copyLink 가 symlink 실패 시 mklink /J 로 폴백 복원.
 removeOrphanItems 도 junction orphan 을 rmdir 로 링크만 제거해 target
 재귀 삭제 사고를 차단.

실행할 스텝이 없는 업데이트를 실패로 오판:
- spawn 자식이 exit=0 + step 0건으로 종료하면 부모 가드가 from<to 만으로
 fail-fast 를 발동해, 스텝이 필요 없는 패치 릴리즈(예: 7.0.0→7.0.1)를
 실패로 처리하고 확장 업데이트 단계까지 도달하지 못했다.
- [STEPS_EXECUTED] 신호에 discovered(범위 내 발견된 스텝 파일 수)를 추가하고,
 handleSpawnExit 가 executed=0 && discovered=0 은 정상 통과, discovered>0
 일 때만 fail-fast 하도록 구분. 구버전 자식(discovered 부재)은 레거시 판정 유지.
 정상 통과 시 확장 일괄 업데이트 단계까지 진행된다.

테스트: FilePermissionHelperSymlinkTest / CoreUpdateCommandSpawnFailureTest /
CoreUpdateCommandHandoffTest / MultiVersionUpgradePathTest 전부 green.
2026-07-02 12:30:23 +09:00
HeuJung c4ea9a6cd1 v7.0.0 release 2026-07-01 10:30:32 +09:00
HeuJung 61f5874fd2 v7.0.0-beta.6 release 2026-05-14 11:13:08 +09:00
HeuJung eb467aca79 v7.0.0-beta.5 release 2026-05-12 17:58:34 +09:00
HeuJung 1db039ff34 v7.0.0-beta.4 release 2026-05-11 11:29:41 +09:00
HeuJung 05db17887d v7.0.0-beta.3 release 2026-04-23 17:36:31 +09:00
HeuJung fa8eba7a0d v7.0.0-beta.2 release 2026-04-20 20:37:49 +09:00
HeuJung 6595fd0eb5 v7.0.0-beta.1 release 2026-04-01 10:30:52 +09:00