"""등록표가 정한 값을 **제 맘대로 저장하지 않는지** — 키도, 저장소도. 왜 (2026-09-07 실사고) — 암 경계선 오프셋은 쓰는 쪽이 `stateKey("rockb", …)` 로 `aislo:draft:rockb:{p}:{r}` 에 쓰는데, 읽는 쪽이 옛 키 `b06:rockb:{p}:{r}` 를 날문자열로 읽고 있었다. 옛 키는 아무도 안 쓰므로 **읽는 쪽이 늘 빈 값**을 받았고, 계획선을 고쳐 횡단을 다시 계산할 때 사용자가 옮긴 암 경계선이 안 실려 나갔다(토사/암 나눔 = 수량). 키 이름을 정하는 자리는 `A00_Common/b_page_state.ts` 의 등록표 하나뿐이다. 세션을 직접 만지는 모듈이 있어도 **키는 `stateKey` 로 받아야** 한다. 이 시험은 날문자열 키를 막는다. """ import os import re from pathlib import Path ROOT = Path(__file__).resolve().parents[2] #: 등록표 자신 — 여기서만 키 문자열을 만든다. _REGISTRY = ROOT / "A00_Common" / "b_page_state.ts" #: 세션을 직접 만지는 호출. 인자가 따옴표·백틱으로 시작하면 **날문자열 키**다. _RAW_CALL = re.compile(r"sessionStorage\.(?:get|set|remove)Item\(\s*[\"'`]") #: 훑을 화면 폴더 — 저장소 전체를 rglob 하면 `node_modules` 정션까지 타서 느리다. _ROOTS = ( "A00_Common", "B05_Profile", "B06_Section", "B07_DesignDetail", "common_util", "ui_template", ) def _sources() -> list[Path]: """대상 폴더를 훑되 `node_modules` 는 **들어가기 전에** 가지친다. (걸러내기만 하면 B07 CAD 앱의 의존성 트리를 통째로 걸어 몇 분씩 걸린다.)""" files: list[Path] = [] for name in _ROOTS: folder = ROOT / name if not folder.is_dir(): continue for current, dirs, names in os.walk(folder): dirs[:] = [d for d in dirs if d not in ("node_modules", "dist", ".git")] for filename in names: if not filename.endswith(".ts"): continue path = Path(current) / filename if path != _REGISTRY: files.append(path) return files def test_등록표_밖에서_날문자열_세션_키를_안_쓴다(): offenders = [] for path in _sources(): text = path.read_text(encoding="utf-8") for number, line in enumerate(text.splitlines(), start=1): if _RAW_CALL.search(line): offenders.append(f"{path.relative_to(ROOT)}:{number}: {line.strip()[:80]}") assert not offenders, "세션 키를 등록표 없이 만든 자리:\n" + "\n".join(offenders) def test_옛_암경계_키가_코드에_안_남아_있다(): """실사고 자리 못박기 — 읽는 쪽이 등록표를 거치는지.""" persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8") # 344-3 — 측점 열쇠 창구(readKeyed → 등록표 readState)로 읽음 assert 'readKeyed("rockb", projectId, routeId)' in persist, "등록표를 안 거침" # 옛 키가 **코드로** 되살아나는 것만 막는다 — 주석에 사연으로 적힌 것은 남겨 둔다. code = "\n".join( line for line in persist.splitlines() if not line.lstrip().startswith(("*", "//", "/*")) ) assert "b06:rockb:" not in code, "옛 키가 코드에 남아 있음" def test_시험이_빈_목록으로_통과하지_않는다(): """훑는 대상이 실제로 있는지 — 경로가 틀리면 조용히 통과한다.""" files = _sources() assert len(files) > 100, f"TS 파일을 {len(files)}개밖에 못 찾음 — 경로 규칙 확인" assert any("B06_Section" in str(path) for path in files) #: 저장소를 직접 이름으로 고르는 것이 **정당한** 자리. #: `b_page_state` 는 고르는 곳 자신이고, 나머지는 등록표 밖의 제 값을 쓰는 자리다. _STORAGE_PICKERS_OK = { "b_page_state.ts", # 여기가 유일하게 고르는 곳(`storageFor`/`storageOf`) } #: 등록표가 관리하지 않는 앱 전역 값 — 이 키를 쓰는 줄은 이 시험의 대상이 아니다. #: `frd_current_project_id`(`config_frontend.ts:29`)는 「지금 보고 있는 프로젝트」 가리개라 #: 화면 상태가 아니고, 워크플로우 전체가 그 한 값을 본다. _NON_REGISTRY_KEYS = ("CURRENT_PROJECT_ID_KEY",) def test_등록표_키를_쓰면서_저장소를_직접_고르지_않는다(): """오늘 두 번째로 같은 계통이 났다 — 키는 등록표에서 받는데 **저장소는 제 맘대로** 고르는 자리. `rockb` 는 키를 날문자열로 만들어 샜고(위 시험이 막음), 화면 취향은 키는 맞았는데 저장소를 직접 골라 **절반만 옮겨졌다**(접힘 4개는 옮겨지고 높이 3개는 세션에 남음). 저장소 선택도 올려보내기도 `readByKey`/`writeByKey` 한 창구만 하도록 못박는다. """ offenders = [] for path in _sources(): if path.name in _STORAGE_PICKERS_OK: continue text = path.read_text(encoding="utf-8") if "stateKey(" not in text and "ByKey(" not in text: continue # 등록표와 무관한 제 값을 쓰는 자리 — 이 시험의 대상이 아니다. for number, line in enumerate(text.splitlines(), start=1): if any(name in line for name in _NON_REGISTRY_KEYS): continue if re.search(r"(?