28 Commits
Author SHA1 Message Date
HeuJung f506c040c1 fix(core): 공개 확장 번들의 캐시 우선 서빙 + SEO 봇 캐시 상한
공개 번들 엔드포인트가 캐시 파일이 있어도 요청마다 다시 병합했다. 캐시 키는
(type, kind, version) 인자만으로 계산되는데 "병합 결과가 비면 파일을 만들지
않는다" 는 규칙을 먼저 두느라 빌드를 앞세운 것이 원인이다. 그래서 캐시 적중
경로에도 활성 확장 열거와 산출물 전량 읽기가 붙었고, 원본이 소실되면 멀쩡한
캐시를 두고 빈 경로가 반환되어 503 이 됐다. 응답은 정상 200 이라 타이밍 말고는
드러나는 증상이 없다.

프로덕션에서 캐시 존재를 병합보다 먼저 확인하고, 캐시 미스는 같은 키의 잠금으로
1회 빌드에 수렴시킨 뒤 잠금 뒤 캐시를 재확인한다. 잠금 대기 초과·저장소 장애는
실패로 바꾸지 않고 각자 빌드로 폴백한다.

병합 결과가 비어도 선언 산출물이 전부 존재하거나 선언이 0이면 0바이트 캐시를
만들어 정적 게시까지 보낸다. 만들지 않으면 그 구성의 자산 URL 이 API 로 폴백해
방문자의 모든 페이지 로드가 PHP 를 거치고, 그 요청마다 컨트롤러가 열거를 세 번
반복한다. 캐시하지 않는 것은 산출물 소실(503 판정 보존)과 디스크 쓰기 실패뿐이라
응답 계약은 바뀌지 않는다 — 컨트롤러와 트레이트는 손대지 않았다.

같은 결의 결함이 검색봇 캐시에도 있었다. 봇 판정은 User-Agent 문자열뿐인데 캐시
키가 경로 + 전체 쿼리여서, 물음표 뒤 값만 바꾼 반복 요청이 매번 미스가 되고 그
미스마다 레이아웃 병합·표현식 평가·자기 API 루프백 호출이 일어나며 결과가 무제한
저장됐다. 키를 정규화하고(시스템 파라미터 제외·개수/길이 상한), IP 당 분당 미스
렌더 예산과 저장 규모 상한을 뒀다. 초과분은 차단이 아니라 일반 SPA 응답을 받는다
— 봇에게 오류를 주면 그 URL 이 색인에서 빠지기 때문이다.

저장 상한은 만료 항목을 걷어낸 뒤 판정한다. 인덱스는 페이지보다 오래 살아
(30일 vs 기본 2시간) 정리 없이 세면 상한이 "지금 저장된 양"이 아니라 "과거에
저장한 적이 있는 양"을 재게 되어 일방향 래치가 된다. 정리는 인덱스 전체를
훑으므로 최소 60초 간격으로만 수행한다. put 과 putWithLayout 은 같은 자원을
쓰므로 단일 저장 경로로 합쳤다 — 한쪽만 상한 밖이면 그쪽이 우회로가 되고,
인터페이스는 확장에 열려 있어 "지금 호출부가 없다" 는 방어가 되지 않는다.

캐시 적중·미적중을 기록하는 호출처가 없어 관리자 SEO 통계와 seo:stats 가 항상
0 이었던 것도 함께 고쳤다. 기록 자체에도 IP 당 상한을 둬 통계 테이블이 새 증식
축이 되지 않게 했다.

(KISA 측에서 제보해주셨습니다 — KVE-2026-2191)
2026-09-08 00:31:20 +09:00
HeuJung 50007d5cc6 fix(auth): 2단계 인증을 켠 사이트의 로그인 흐름 구현
2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.

원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.

이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.

## 구현

- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
 `ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
 「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
 수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
 코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
 뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
 사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
 못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
 문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.

## 이번 점검에서 함께 고친 것

- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
 그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
 한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
 성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
 같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.

## 버전

코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
2026-09-07 17:08:14 +09:00
HeuJung de18159986 fix(core,admin_basic): 정적 게시 재게시 누락 3건 수정 + 관리자 수동 복구 UI + sudo core:update 소유권 정합
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
2026-09-06 15:22:52 +09:00
HeuJung 381fcc4e0b feat(core,admin_basic): .env 키 단위 우선 모드 추가
관리자 환경설정 저장값이 config 를 덮는 기존 설계는 공유호스팅을 1차 대상으로
삼은 결과다. .env 를 배포 기준값으로 관리하는 설치에서는 그 설계가 반대로
작동해, 설치기가 defaults 를 시드하는 순간부터 .env 의 28개 항목이 오류도
로그도 없이 사문화된다.

G7_ENV_PRIORITY 옵트인으로 그 소유권을 키 단위로 되돌린다. 스위치를 켜지 않은
설치는 전 경로가 조기 return 이라 동작이 100% 동일하다.

명시 여부는 config 빌드 시점에 캡처한다 — 런타임 env 는 config:cache 환경에서
null 로 고정되어 판정이 영구 미발동한다(attachment.disk_explicit 와 같은 함정).
판정은 strict 라 APP_DEBUG=false·REDIS_DB=0 같은 falsy 명시도 명시로 본다.

잠금은 그 키의 주입만 건너뛰는 것이지만, 제거된 키의 부재를 값으로 읽는 자리가
있으면 형제 설정까지 무너진다. 게이트(웹소켓 토글·메일 드라이버·디버그 모드)와
기본값 주입(mailgun.endpoint·ses.region), 형제 폴백(reverb server endpoint),
파생 판정(storage_driver→attachment.disk)을 유효값으로 보정했다.
2026-09-04 10:08:30 +09:00
HeuJung b6c5e1f323 feat(core): 리버스 프록시 신뢰 설정 지원 및 미설정 진단
TLS 가 앞단에서 종단되고 앱에는 HTTP 로 전달되는 구성에서 X-Forwarded-* 가 전부
무시되어 화면 백지·IP 왜곡·webhook 403 이 함께 발생했다. 코어에 신뢰 프록시 설정
지점이 아예 없던 것이 원인이다.

- config/trustedproxy.php 로 내장 TrustProxies 미들웨어에 값을 공급한다.
 bootstrap/app.php 는 건드리지 않는다 — withMiddleware 클로저는 .env 로드 전에
 평가되어 env 가 항상 null 이 되는 조용한 no-op 이다.
- 판정은 App\Support\TrustedProxyDiagnostic 단일 SSoT 에서 계산하고 대시보드
 알림·환경설정 고급 탭·설치 마법사·trusted-proxy:status 네 면이 소비한다.
 판정식은 "HTTPS 인식 실패" 가 아니라 "X-Forwarded-* 수신 중 AND 신뢰 프록시
 미설정" 이다 — HTTP 전용 사이트가 프록시 뒤에 있으면 화면은 정상인 채로
 나머지만 조용히 어긋나기 때문이다.
- 값 편집 UI 와 쓰기 엔드포인트는 두지 않는다(잠금 역설 + XFF 위조 경로).
- 혼합 콘텐츠 차단을 부트스트랩 폴백의 별도 사유로 가른다. 새로고침으로 낫지
 않으므로 버튼을 렌더하지 않고, 원인·조치는 콘솔로 운영자에게 보낸다.
- 대시보드 알림을 심각도로 배치한다 — warning 은 상단 배너, 그 외는 하단 카드.
 같은 알림이 두 곳에 뜨지 않으며, 여러 건은 간격을 두고 쌓인다.

공개 이슈: (@lyg-kaban 제보)
2026-08-28 18:09:27 +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 1ba67ca7cf feat(content): 본문 이미지 목록 썸네일·og:image 폴백 전 티어 구현
공개 이슈 https://github.com/gnuboard/g7/issues/22 — 본문에 삽입한 이미지가
카드/갤러리 목록 썸네일과 공유 미리보기(og:image)에 반영되지 않던 공백을
코어+3모듈+2템플릿에서 해소.

- 코어 7.0.9: HtmlImageExtractor(내부 이미지 한정, origin 정규화 SSoT 재사용)
 + seo.og_image_default 사이트 기본 공유 이미지 설정 + SeoMetaResolver 폴백 체인
- board 1.1.0 / ecommerce 1.2.0 / page 1.1.0: content_thumbnail_url 캐시 컬럼,
 saving 추출(html 모드 한정·첨부 우선·비밀글 게이트 유지),
 filter_content_thumbnail 훅, 기존 데이터 백필 업그레이드 스텝
- ecommerce 카테고리 og:image 생산자 신설, page og:description 死키 정정
- sirsoft-basic 1.1.2 og 바인딩 정정, admin_basic 1.0.7 SEO 탭 업로더(+ja 팩 2종)
- 검수 중 발견한 text 모드 오캐시 결함은 실패 테스트 선행 후 수정
2026-08-22 22:21:54 +09:00
HeuJung d8ca281464 chore(release): 코어 7.1.0 → 7.0.8 하향 및 7.0.7 이후 변경분 출시일 정렬
프록시 설정 추가분을 마이너(7.1.0)가 아닌 패치(7.0.8)로 내보낸다.
7.0.7 이후 변경된 코어·템플릿·플러그인·언어팩의 출시일을 2026-08-21 로 맞춘다.

>=7.1.0 을 요구하던 본인인증 플러그인의 코어 최소 버전을 >=7.0.8 로 내린다 —
그대로 두면 존재하지 않는 버전을 요구해 플러그인이 자동 비활성화된다.
앞선 작업에서 누락된 package-lock.json 버전(1.0.3)도 함께 맞춘다.
2026-08-21 13:53:42 +09:00
HeuJung a7b7c64573 feat(settings): 코어 아웃바운드 HTTP 프록시 설정 추가
접속 IP 를 제한하는 결제사 API 를 로컬·스테이징에서 연동하려면 서버가
내보내는 요청의 출발지 IP 를 바꿔야 한다. 브라우저 프록시로는 바뀌지 않는
축이라 코어 환경설정으로 도입한다.

게이트는 디버그 모드이며 판정은 OutboundProxy 한 곳이 소유한다. 화면의
조건부 렌더링은 편의일 뿐이라 저장 API 직접 호출을 막지 못하므로, 실질
게이트를 판정 지점에 둔다. 주입·적용 지점은 결과만 소비한다.

적용은 Http::globalOptions 전역 옵션이라 확장의 Http:: 호출까지 함께
경유한다. 외부 연동 규약상 curl 핸들을 직접 다뤄야 하는 확장은
OutboundProxy::curlOptions 로 같은 프록시를 탄다 — KG이니시스 본인인증
승인 요청과 CBT 연결 점검을 이 통로로 편입했다. CBT 의 TCP 443 확인은
원시 소켓으로는 프록시를 태울 수 없어 curl CONNECT_ONLY 로 교체했다.

저장 전 연결 테스트는 저장값이 아니라 제출값을 검사하고, 그 프록시를
거쳤을 때 외부에 보이는 IP 를 함께 보고한다. 운영자가 결제사에 등록할
값이라 저장하고 나서 되짚지 않도록 했다. 적용과 같은 조립을 거치므로
확인한 구성과 저장 후 적용되는 구성이 어긋나지 않는다.
2026-08-20 17:01:27 +09:00
HeuJung ea58c606a2 fix(core,admin_basic): S3 스토리지 드라이버 실동작 결함 수정 및 S3 호환 스토리지 연결 지원
공개 제보 — 파일 스토리지에서 S3 를 선택해 저장해도 실제 파일 저장이
동작하지 않던 결함의 전면 수정.

- S3 어댑터(league/flysystem-aws-s3-v3)·predis 를 코어 기본 의존성으로 포함
 — 어댑터 부재 즉사, phpredis 확장 없는 서버의 redis 선택 전면 다운 차단
 (부트 시 확장 부재 감지 → predis 자동 폴백)
- storage_driver=s3 저장 시 코어 첨부 업로드 디스크를 s3 로 전환
 (ATTACHMENT_DISK env 명시가 항상 우선, 기존 행은 저장 당시 disk 로 서빙)
- 첨부·템플릿 레이아웃 첨부 서빙을 행 disk 를 따르는 스토리지 스트림으로 교체
 — 로컬 절대 경로 전제 fileResponse 는 S3 행에서 filemtime stat 500
 (streamedFileResponse: 행 메타 기반 ETag/304/Cache-Control)
- S3 호환 스토리지(R2/MinIO/NCP) 연결 지원: 엔드포인트 URL·path-style 설정
 신설, 리전 목록 선택 → 자유 입력 전환, 연결 테스트를 실제 저장 경로와
 동일 설정(endpoint/path-style)으로 정렬
- 사용 불능 드라이버(어댑터·PHP 확장 부재)의 저장/테스트 요청을 사유와 함께
 422 로 차단하는 서버 게이트 신설 (DriverRegistryService 능력 판정)
- 웹소켓 연결 테스트에 서버(백엔드 발송용) endpoint 검사 추가 — 클라이언트만
 검사해 테스트 성공 + 실제 발송 실패가 가능하던 비대칭 해소
- env 빈 값(`KEY=`) 함정 정규화: AWS_URL/AWS_ENDPOINT/ATTACHMENT_DISK 빈 문자열을
 미설정으로 취급 (config 정규화 + 예시 파일 주석 처리)
- 플러그인 드라이버 폴백의 log 카테고리 죽은 키(logging.default) 정정 및
 websocket 유령 설정 키 제거
- 실 AWS S3 종단 검증 완료 (설정 저장 → 업로드 S3 실저장 → 서빙 200/304)
2026-08-13 15:15:19 +09:00
HeuJung c77ddee03b fix(core,ecommerce): Windows 확장 업데이트 파일 잠금 해소 + 통화 삭제 영속화
Windows 는 하위 트리에 열린 핸들이 하나라도 있으면 디렉토리 rename 을 막는다.
잠금 프로세스를 찾아 종료하던 기존 대응은 디렉토리 핸들을 감지하지 못했고
사용 중인 편집기를 예고 없이 죽였다. rename 이 막히면 파일 단위 연산으로
폴백해 어떤 프로세스도 건드리지 않고 교체를 끝낸다. 커밋 전 점검에서
pint.json 이 코어 업데이트 대상에 빠져 있던 것도 함께 등재했다.

이커머스는 기본 제공 통화 삭제가 저장 응답에서 즉시 부활했다. 항목 단위
보충 병합이 "소실" 과 "의도적 삭제" 를 구분하지 못한 탓이라, 삭제 의도를
서버가 도출해 저장본에 기록하고 병합이 그 기록을 존중하게 했다. 그 통화로
결제된 과거 주문의 표기가 흔들리지 않도록 소수 자릿수 해석도 스냅샷 우선으로
바꿨다 — 금액은 원래 스냅샷 환율을 써 안전했으나 자릿수만 현재 설정을 봤다.
2026-08-12 10:10:01 +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 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 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 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 cbc91117d7 fix(installer): Read DB 미사용 시 Write DB만 참조하도록 수정 + GitHub 이슈 템플릿 신설
설치 시 use_read_db=false 인데도 db_read_host 잔존값으로 read 커넥션을
조립해 5단계 마이그레이션이 실패하던 문제를 수정한다. "read 사용 여부"
판정이 세 지점에서 달랐던 것이 근본 원인으로, use_read_db 플래그를 단일
SSoT 로 통일한다.

- installer-runtime.php: read 블록을 use_read_db 게이트로. 미사용 시 runtime
 에 read 키 미생성 → 하류가 write 로 자동 fallback. mergeRuntimeIntoEnv 도
 read 부재 시 DB_READ_* 를 빈 값으로 기록(write 복사 제거).
- config/database.php: read env 를 Elvis(?:) fallback 으로 복원. 중첩
 env(A, env(B)) 는 DB_READ_* 가 빈 문자열일 때 fallback 이 무력화되므로,
 빈 값까지 흡수하는 ?: 를 사용(tinker 실측 확인). 설치 이후 운영 중
 DB_READ_* 공란 시에도 SELECT 가 write DB 로 정확히 향하게 한다.
- functions.php / request-handler.php: read 미사용 시 .env DB_READ_* 빈 값
 기록 + state.json 잔존 read 필드 제거(2단계 방어).

기존 설치 환경은 .env 에 DB_READ_* 가 이미 채워져 Elvis fallback 미발동 →
동작 불변이라 업그레이드 스텝 불필요(실측 확인).

무관 사전 실패 테스트(beta.7 stale-ENV 복구)는 코어 version 이 beta.7 이
아니면 skip 하도록 가드 헬퍼 추가(프로덕션 한시 가드 전제를 테스트에 반영).

자유형식 제보에서 진단 축(버전/활성 확장/커스텀 자산/환경)이 누락되기 쉬워
G7 전용 이슈 템플릿을 추가한다. bug_report / feature_request / upgrade_issue
+ config.yml(blank 비활성, Discussions·보안신고 링크).
2026-07-06 17:15:29 +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 32251b2a1d perf(core): 코어 번들 3분할로 초기 로딩 최적화
레이아웃 편집기·개발자 진단도구를 코어 번들에서 분리해 각각
편집 화면 진입 시·디버그 모드 시에만 지연 로드하도록 변경.
초기 접속 gzip payload 458KB → 221KB (52% 감소).

lazy 번들은 window.G7Core.__runtime 으로 코어 런타임을 공유해
React/컨텍스트/싱글톤 단일 인스턴스를 보장(재번들 0중복).
코어 7.0.1 bump.
2026-07-01 17:05:15 +09:00
HeuJung c4ea9a6cd1 v7.0.0 release 2026-07-01 10:30:32 +09:00
HeuJung 0e7fa04b03 v7.0.0-beta.7 release 2026-05-15 19:24:41 +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