ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
130 lines
6.9 KiB
Python
Executable File
130 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""정적 내보내기한 웹 UI(`web/out`)의 HTML 에 중국어(한자)가 새어 나왔는지 검사한다.
|
||
|
||
이 포크의 핵심 성과이자 차별점은 "사용자에게 보이는 화면에 중국어가 없다"는 것이다
|
||
(백로그 B4 가 수립한 `out` HTML 한자 0 속성). 그 속성을 손검증에만 맡기면, 기여자가
|
||
중국어 문자열을 하드코딩하거나 번역이 누락되는 회귀가 생겨도 다음 사람이 눈으로
|
||
발견할 때까지 조용히 깨진 채 남는다. 이 스크립트는 그 회귀를 머지 게이트에서 막는다.
|
||
|
||
검사 방식
|
||
---------
|
||
`web/out` 아래 모든 `.html` 파일의 **원시 내용**(렌더된 마크업뿐 아니라 인라인
|
||
스크립트·React Server Components 플라이트 데이터까지)을 훑어 한자가 하나라도 있으면
|
||
종료 코드 1 로 끝난다. 원시 내용을 그대로 보는 이유는, 사용자에게 보이는 텍스트가
|
||
플라이트 데이터(JSON 꼴 문자열)에 실려 HTML 에 함께 내려오기 때문이다. 태그만
|
||
벗겨 내면 그 경로의 누출을 놓친다.
|
||
|
||
검사 범위 밖 (정직한 한계)
|
||
-----------------------
|
||
이 게이트는 정적 내보내기 결과물만 본다. 그래서 **MOCK 모드(`NEXT_PUBLIC_MOCK=1`,
|
||
공개 데모가 쓰는 모드)에서 브라우저가 실행 중에 그려 내는 내용**까지는 검사하지 못한다.
|
||
`web/src/lib/mock/data.ts`·`handler.ts` 와 각 페이지의 하드코딩 샘플은 하이드레이션
|
||
뒤 `useEffect` 가 `mockHandle()` 로 가져와 React 상태로 렌더하므로, `out` HTML 이
|
||
아니라 런타임 JS 번들에만 실린다. 즉 이 경로로 들어온 중국어는 이 게이트를 통과한다
|
||
(과거 공개 데모의 `/system/logs` 샘플 로그에 중국어가 그렇게 조용히 남아 있었고,
|
||
백로그 G114 에서 한국어로 고쳤다).
|
||
|
||
그 표면은 소스 검토로 지킨다. 렌더 대상 파일(`web/src` 의 `.tsx`·클라이언트 데이터)에
|
||
남는 중국어는 반드시 다음 중 하나여야 한다: 코드 주석, `t.has(...)` 로 막혀 실제로는
|
||
쓰이지 않는 i18n 폴백, 백엔드 와이어 포맷 토큰(챗 멘션 분류 라벨·`[模型]` 결정 출처
|
||
센티넬), locale 가 `zh` 일 때만 보이는 분기 리터럴, 또는 성능 보존을 위해 번역하지
|
||
않는 에이전트 두뇌(段 [A]) 프롬프트를 그대로 비추는 미러. 이 가운데 어디에도 속하지
|
||
않고 "그냥 화면에 보이는 중국어"라면 한국어로 번역한다(백로그 G115 가 이 분류를
|
||
전수 확인했다).
|
||
|
||
허용·탐지 범위
|
||
-------------
|
||
- **한글(`가-힣` 등)은 허용**한다. 한자 블록과 한글 블록은 유니코드에서 겹치지 않으므로,
|
||
한자 블록만 탐지하면 한글은 자동으로 통과한다.
|
||
- **한자(CJK 한자)만 탐지**한다: 통합 한자와 확장 A(`㐀`–`鿿`), 호환 한자
|
||
(`豈`–``), 그리고 보충 평면의 확장 B 이상(`𠀀`–). 가나·전각 문장부호는
|
||
"한자"가 아니므로 이 게이트의 대상이 아니다(원문 보존 코드 블록이나 증거 인용에
|
||
섞여 들어와 거짓 양성을 내는 것을 막기 위해 범위를 한자로 좁힌다).
|
||
|
||
상류 보존이 필요한 의도적 한자
|
||
-----------------------------
|
||
현재 `out` HTML 에는 한자가 한 글자도 없어 허용 목록이 비어 있다. 앞으로 성능 보존을
|
||
위해 원문(중국어)을 일부러 화면에 남겨야 하는 경우(예: 언어 스위처의 `中文` 라벨)가
|
||
생기면, 그 글자를 아래 `ALLOWED_HAN` 에 추가하고 왜 남기는지 주석으로 적는다. 그래야
|
||
게이트는 엄격하게 유지되면서도 의도된 예외만 좁게 통과시킬 수 있다.
|
||
|
||
표준 라이브러리만 쓰고 네트워크에 접속하지 않는다.
|
||
실행: `python3 -I scripts/check-web-cjk.py [out 디렉터리]`
|
||
(기본값은 저장소 루트의 `web/out`. CI 는 `web` 작업 디렉터리에서
|
||
`python3 -I ../scripts/check-web-cjk.py` 로 부른다.)
|
||
"""
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
# CJK 한자 블록만. 한글(AC00–D7A3)·가나·전각 문장부호는 들어 있지 않다.
|
||
# 범위: 확장 A + 통합 한자(U+3400–U+9FFF = 㐀–鿿), 호환 한자(U+F900–U+FAFF = 豈–),
|
||
# 보충 평면 확장 B 이상(U+20000–U+2FFFF).
|
||
HAN = re.compile(r"[㐀-鿿豈-\U00020000-\U0002ffff]")
|
||
|
||
# 화면에 일부러 남기는 한자(예: 언어 스위처 `中文`). 지금은 비어 있다 —
|
||
# 비우면 어떤 한자든 게이트에 걸린다. 예외를 더할 때는 글자와 사유를 함께 적는다.
|
||
# 예) ALLOWED_HAN = {"中", "文"} # 언어 스위처 '中文' 라벨(상류 대조용)
|
||
ALLOWED_HAN: set = set()
|
||
|
||
|
||
def find_out_dir() -> str:
|
||
"""검사할 `out` 디렉터리를 정한다.
|
||
|
||
인자로 경로를 주면 그것을, 없으면 git 저장소 루트의 `web/out` 를 쓴다.
|
||
"""
|
||
if len(sys.argv) > 1:
|
||
return os.path.abspath(sys.argv[1])
|
||
root = subprocess.check_output(
|
||
["git", "rev-parse", "--show-toplevel"], text=True
|
||
).strip()
|
||
return os.path.join(root, "web", "out")
|
||
|
||
|
||
def main() -> int:
|
||
out_dir = find_out_dir()
|
||
if not os.path.isdir(out_dir):
|
||
print(
|
||
f"오류: 검사할 디렉터리가 없습니다 — {out_dir}\n"
|
||
"먼저 `npm run build:static` 로 정적 내보내기를 만든 뒤 실행하세요."
|
||
)
|
||
return 2
|
||
|
||
checked = 0
|
||
offenders = [] # (상대경로, 한자 개수, 미리보기)
|
||
for dirpath, _dirnames, filenames in os.walk(out_dir):
|
||
for name in filenames:
|
||
if not name.endswith(".html"):
|
||
continue
|
||
checked += 1
|
||
path = os.path.join(dirpath, name)
|
||
with open(path, encoding="utf-8", errors="ignore") as fh:
|
||
text = fh.read()
|
||
found = [ch for ch in HAN.findall(text) if ch not in ALLOWED_HAN]
|
||
if found:
|
||
rel = os.path.relpath(path, out_dir)
|
||
kinds = "".join(sorted(set(found)))
|
||
offenders.append((rel, len(found), kinds))
|
||
|
||
print(f"검사한 HTML {checked}개 (대상: {out_dir})")
|
||
if offenders:
|
||
print(f"중국어(한자) 누출 {len(offenders)}개 파일 — 사용자 노출 HTML 에 한자가 있습니다:")
|
||
for rel, count, kinds in offenders:
|
||
preview = kinds if len(kinds) <= 30 else kinds[:30] + "…"
|
||
print(f" out/{rel}: 한자 {count}개 · 종류 {preview}")
|
||
print(
|
||
"\n한글로 번역하거나, 상류 보존이 꼭 필요한 의도적 한자라면 "
|
||
"scripts/check-web-cjk.py 의 ALLOWED_HAN 에 사유와 함께 추가하세요."
|
||
)
|
||
return 1
|
||
if checked == 0:
|
||
print("경고: 검사한 HTML 이 없습니다 — 빌드가 제대로 되었는지 확인하세요.")
|
||
return 2
|
||
print("중국어(한자) 누출 0 — 사용자 노출 HTML 이 전부 한국어/비한자입니다.")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|