perf(M01): 로직·조합 목록이 마스터를 한 번만 읽고 막힘은 미리 세어 둠 (일감 32)

- `_Store_Cache` 신설 — 자국(시각·크기)이 바뀐 파일만 다시 읽음 · 목록은 검사를 돌리지 않고 세어 둔 막힘을 씀
- 요소·표가 바뀌면 모든 로직을, 로직 파일이 바뀌면 그 파일과 그 로직을 부르던 줄만 다시 셈
- 저장 뒤에는 고친 파일을 바로 다시 읽게 알려 줌 — 저장 직후 목록 2.5초 → 0.06초
- `/logics` 둘째 번부터 2.5초 → 0.02초 · `/combos` 0.3초 → 0.01초 · 켠 뒤 첫 번만 3.4초
- 시험 `test_m01_cache.py` 다섯 — 세어 둔 막힘이 줄마다 검사한 값과 같음 · 거름 그대로 · 저장 직후 비침

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-22 02:17:49 +09:00
co-authored by Claude Opus 5
parent e3e8fc15e2
commit 3c6e68231c
6 changed files with 270 additions and 14 deletions
+21 -8
View File
@@ -24,6 +24,7 @@ import master_copy as mcp # noqa: E402,F401 (`_Store_Make` 가 `store.mcp` 로
import master_keys as mk # noqa: E402
import master_text as mtx # noqa: E402
from M01_MasterData import M01_MasterData_Store_Cache as cache # noqa: E402
from M01_MasterData.M01_MasterData_Store_Shape import dump, keep_shape # noqa: E402
FOLDER: Path = cm.MASTER # 시험은 사본 폴더로 바꿈
@@ -56,6 +57,17 @@ def read(file: str) -> tuple[dict, str]:
return json.loads(raw, parse_float=Decimal, parse_int=Decimal), version_of(raw)
# ── 한 번 읽어 두기(`_Store_Cache`) ────────────────────────────────────
def loaded() -> tuple[dict[str, dict], object]:
"""(파일 묶음, 색인) — **읽기 전용**. 고칠 것이면 `cm.load` 로 새로 읽을 것."""
return cache.loaded(FOLDER)
def blocks() -> dict[str, list[str]]:
"""로직 키마다 막힘 까닭 — 미리 세어 둔 값(목록에서 검사를 돌리지 않음)."""
return cache.blocks(FOLDER)
def items_key(data: dict) -> str:
return "" if data.get("그룹") in cm.mf.TABLE_GROUPS else ""
@@ -318,9 +330,10 @@ def _logic_files(files: dict[str, dict]):
def logics(sub: str, detail: str, q: str, blocked: int | None, owner: str = "") -> list[dict]:
"""로직 목록 — 구분 · 상세구분 · 소유 · 찾기로 거름(장 차례대로 · 파일을 가로지름)."""
files = cm.load(folder=FOLDER)
whole = cm.mf.Master(files)
"""로직 목록 — 구분 · 상세구분 · 소유 · 찾기로 거름(장 차례대로 · 파일을 가로지름).
막힘은 미리 세어 둔 값(`blocks`) — 목록에서 줄마다 검사를 돌리지 않음."""
files, _ = loaded()
why_of = blocks()
out = []
for name, data in sorted(_logic_files(files), key=lambda f: f[1].get("차례") or 0):
for row in data.get("") or []:
@@ -330,7 +343,7 @@ def logics(sub: str, detail: str, q: str, blocked: int | None, owner: str = "")
continue
if not _hit(row, q):
continue
reasons = cm.mf.check_logic(whole, row)[0]
reasons = why_of.get(str(row.get("")), [])
if blocked is not None and bool(reasons) != bool(blocked):
continue
out.append(
@@ -358,11 +371,10 @@ def logics(sub: str, detail: str, q: str, blocked: int | None, owner: str = "")
def logic(key: str) -> dict:
"""로직 하나 — 키는 전체에서 하나."""
files = cm.load(folder=FOLDER)
files, whole = loaded()
for name, data in _logic_files(files):
for row in data.get("") or []:
if str(row.get("")) == key:
whole = cm.mf.Master(files)
reasons = cm.mf.check_logic(whole, row)[0]
_, version = read(name)
return {
@@ -427,7 +439,7 @@ def elements(group: str, q: str, limit: int) -> dict:
if group not in cm.mf.GROUPS:
raise StoreError(404, f"없는 그룹 「{group}")
hits = []
for name, data in cm.load(folder=FOLDER).items():
for name, data in loaded()[0].items():
if data.get("그룹") != group:
continue
for row in data.get(items_key(data)) or []:
@@ -491,7 +503,7 @@ def materials(sub: str, detail: str, spec: str, region: str, unit: str, limit: i
"""재료 고르기 조건 안 후보 목록 — 화면·테스트 컨테이너가 같이 씀.
`unit` = 호표 줄 단위 — 주면 그 단위 줄만(단위가 다르면 다른 물건).
`기본` = 시험 계산이 쓸 줄(대표 줄이 없을 때의 첫 줄) · 그 줄을 `items` 맨 앞에 둠."""
whole = cm.master(folder=FOLDER)
whole = loaded()[1]
cond = {"구분": sub, "상세구분": detail or None, "규격": spec or None}
env = {cm.mf.REGION: region} if region else {}
keys = cm.mf.candidates(whole, cond, env, unit or None)
@@ -664,4 +676,5 @@ def save(batch: list[dict]) -> list[dict]:
tmp.write_text(text, encoding="utf-8", newline="\n")
os.replace(tmp, FOLDER / name)
out.append({"file": name, "version": version_of(text.encode("utf-8"))})
cache.stale(dict.fromkeys(names)) # 저장 뒤에는 그 파일을 바로 다시 읽게
return out