Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -17,6 +18,9 @@ from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
|
||||
router = APIRouter(prefix="/api/m01", tags=["M01 MasterData"])
|
||||
|
||||
if "pytest" not in sys.modules: # 시험은 사본 폴더를 써서 정본을 미리 읽을 까닭이 없음
|
||||
store.warm() # 서버가 뜰 때 뒤에서 마스터를 한 번 읽어 둠 — 첫 목록을 기다리지 않게
|
||||
|
||||
|
||||
class CalcBody(BaseModel):
|
||||
key: str
|
||||
|
||||
@@ -68,6 +68,11 @@ def blocks() -> dict[str, list[str]]:
|
||||
return cache.blocks(FOLDER)
|
||||
|
||||
|
||||
def warm() -> None:
|
||||
"""서버가 뜰 때 뒤에서 미리 읽어 둠 — 첫 목록을 기다리지 않게."""
|
||||
cache.warm(FOLDER)
|
||||
|
||||
|
||||
def items_key(data: dict) -> str:
|
||||
return "표" if data.get("그룹") in cm.mf.TABLE_GROUPS else "줄"
|
||||
|
||||
@@ -660,8 +665,10 @@ def save(batch: list[dict]) -> list[dict]:
|
||||
for part in batch:
|
||||
_apply(after[part["file"]], part.get("changes") or [], part["file"], book)
|
||||
mk.finish(book)
|
||||
old = set(cm.check_saved(before, names, mk.load_book(book_path)))
|
||||
new = [x for x in cm.check_saved(after, names, book) if x not in old]
|
||||
# 고친 줄과 그 줄을 부르던 줄만 검사 — 마스터 전체 검사는 `check_master` 몫
|
||||
narrowed = cache.narrow(FOLDER, batch, before, after)
|
||||
old = set(cache.check_saved(before, names, mk.load_book(book_path), narrowed))
|
||||
new = [x for x in cache.check_saved(after, names, book, narrowed) if x not in old]
|
||||
if new:
|
||||
raise StoreError(422, {"errors": new})
|
||||
if book["다음"] != issued:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
자국(파일 시각·크기)이 바뀐 파일만 다시 읽고, 로직 막힘도 미리 세어 둠 —
|
||||
목록은 검사를 돌리지 않고 세어 둔 값을 씀. 고치는 길(`calc` · `text` · `save`)은
|
||||
사본을 만지므로 그대로 `cm.load` 로 새로 읽음.
|
||||
저장 전 검사(`check_saved`)도 여기 — 세어 둔 부르기 그림으로 고친 줄 둘레만 봄(`narrow`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -112,3 +113,55 @@ def blocks(folder: Path) -> dict[str, list[str]]:
|
||||
for key in [k for k, (_, calls) in got.items() if calls & moved]:
|
||||
got[key] = cm.mf.check_logic(whole, rows[key])
|
||||
return {key: why for _, got in _BLOCK.values() for key, (why, _) in got.items()}
|
||||
|
||||
|
||||
def graph(folder: Path) -> dict[str, set]:
|
||||
"""로직 키마다 부르는 로직 키 — 미리 세어 둔 것(저장 때 검사를 좁히는 데 씀)."""
|
||||
with _FRESH:
|
||||
blocks(folder)
|
||||
return {key: calls for _, got in _BLOCK.values() for key, (_, calls) in got.items()}
|
||||
|
||||
|
||||
def narrow(folder: Path, batch: list[dict], before: dict, after: dict):
|
||||
"""(검사할 로직 키, 안 보는 줄이 부르는 키) — 저장이 건드린 줄과 그 줄을 부르던 줄만.
|
||||
|
||||
로직 밖 파일(요소 · 표)이 섞이면 어느 로직이 흔들릴지 모르므로 None — 전부 검사.
|
||||
"""
|
||||
if any(after[part["file"]].get("그룹") != "로직" for part in batch):
|
||||
return None
|
||||
calls, touched, gone = graph(folder), set(), set()
|
||||
for part in batch:
|
||||
was, now = [
|
||||
{str(r.get("키")) for r in side[part["file"]].get("줄") or []}
|
||||
for side in (before, after)
|
||||
]
|
||||
touched |= now - was # 더한 줄(키는 저장 때 대장이 냄)
|
||||
gone |= was - now
|
||||
touched |= {str(c.get("key")) for c in part.get("changes") or [] if c.get("op") != "add"}
|
||||
only = touched | {key for key, called in calls.items() if called & touched}
|
||||
return only, {key: called for key, called in calls.items() if key not in gone}
|
||||
|
||||
|
||||
def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> list[str]:
|
||||
"""저장 전 검사(M01) — 고친 파일의 틀 + 로직의 변수 · 키 + 조합.
|
||||
|
||||
`files` = 폴더 전부(고친 뒤) · `book` = 키 대장 · `narrowed` = `narrow` 가 준 좁힘
|
||||
(None 이면 전부 검사 — 마스터 전체 검사는 `check_master` 몫).
|
||||
"""
|
||||
whole = cm.mf.Master(files)
|
||||
out = [x for name in changed for x in cm.check_form(name, files[name])]
|
||||
return out + cm.check_logics(files, whole, book, narrowed) + cm.mcb.check_all(files, whole)
|
||||
|
||||
|
||||
def warm(folder: Path) -> threading.Thread:
|
||||
"""서버가 뜰 때 뒤에서 한 번 읽어 둠 — 켠 뒤 첫 목록이 3초 걸리던 것을 없앰."""
|
||||
|
||||
def run() -> None:
|
||||
try:
|
||||
blocks(folder)
|
||||
except Exception: # 마스터가 깨져 있어도 서버 시동을 막지 않음 — 목록에서 다시 드러남
|
||||
pass
|
||||
|
||||
thread = threading.Thread(target=run, name="m01-warm", daemon=True)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
@@ -47,3 +47,19 @@
|
||||
- 막힘 거름(`blocked=0·1`)·구분·상세구분·찾기 결과와 줄 내용은 그대로 — 시험 `resources/tester/test_m01_cache.py` 다섯이 줄마다 검사한 값과 맞댐.
|
||||
- 남은 늦은 자리(이 일감 밖) — **저장 자체가 6.7초**: `check_saved` 가 저장 앞뒤로 1,354줄을 다 검사함. 켠 뒤 첫 목록 3.4초도 서버가 뜰 때 미리 한 번 읽어 두면 없앨 수 있음(서버 시동 자리라 이 창 담당 밖).
|
||||
- 시험 — `test_m01_*` · `test_masterdata_*` 195 통과 · 4 실패. 실패 넷은 모두 `test_m01_combo.py`(정본 조합 파일이 빈 채라고 보는 옛 기대값 — 일감 31 이 견본 조합 `UA000001` 을 정본에 넣어 어긋남) · 이 고침과 무관(첫 실패는 서버를 거치지 않고 정본 파일만 읽음). `check_master` 틀 0 · 본문 0 · 로직 0 · 조합 0 · 단위 14.
|
||||
|
||||
## 고친 뒤 2 (일감 33)
|
||||
|
||||
남은 늦음 둘 — 잰 자리는 위와 같음(이 창 · 사본 폴더 · `TestClient`).
|
||||
|
||||
| 길 | 앞 | 뒤 |
|
||||
|---|---|---|
|
||||
| 로직 한 줄 저장(`POST /save`) | 6.8초 | **0.8초** |
|
||||
| 켠 뒤 첫 목록 | 3.2초 | **0.03초**(미리 읽기 2.7초는 뒤에서 · 기다리지 않음) |
|
||||
|
||||
- **저장 검사 좁힘** — 고친 줄과 **그 줄을 부르던 줄**만 검사. 안 보는 줄이 부르는 키는 목록이 미리 세어 둔 것을 그대로 씀(돌고 도는 길 찾기는 그대로 전체 그림으로 봄). 로직 줄만 고쳤으면 품셈재료 연결·인력 준용 검사(`check_links`)도 건너뜀 — 로직 줄이 못 건드리는 자리. 저장 검사(`check_saved`)는 M01 쪽(`_Store_Cache`)으로 옮김 — 좁히는 값이 거기 있고 `check_master` 가 700줄에 닿아서.
|
||||
- 로직 밖 파일(요소 · 표 · 조합)이 섞인 저장은 어느 로직이 흔들릴지 모르므로 **예전처럼 전부 검사**(그 자리는 그대로 6.8초). 마스터 전체 검사는 `check_master` 몫.
|
||||
- **미리 읽기** — M01 라우터가 뜰 때 뒤에서 한 번 읽어 둠(`cache.warm` · 데몬 실). 시동을 막지 않고, 시험은 사본 폴더를 쓰므로 건너뜀.
|
||||
- 저장 뒤에도 잡히는지 — 견본 넷이 모두 422 로 막힘: 없는 로직 키(GC999999) · 없는 인력 요소 키(LB999999) · 돌고 도는 참조 · 고친 줄을 **다른 파일에서** 부르던 줄이 깨짐(입력 하나 더함). 시험 `resources/tester/test_m01_cache.py` 아홉(넷이 이번 것).
|
||||
- 단위 경고(`check_units`)는 애초부터 저장 검사가 아니라 `check_master 단위` 몫 — 14건 그대로.
|
||||
- 남은 늦은 자리 — 저장 0.8초 가운데 0.6초는 마스터를 두 번(고치기 전·후) 새로 읽는 것. **요소 파일 저장은 그대로 7.0초**(인력 한 줄 실측): 요소를 가리키는 로직을 키 글자로만 찾으면 원문번호로 가리키는 자리를 놓쳐 저장 검사에 구멍이 나므로 좁히지 않음 — 좁히려면 「요소를 부르는 로직」 지도부터 만들어야 함.
|
||||
|
||||
@@ -174,16 +174,6 @@ def master(folder: Path = MASTER) -> mf.Master:
|
||||
return mf.Master(load(folder=folder))
|
||||
|
||||
|
||||
def check_saved(files: dict[str, dict], changed: list[str], book: dict | None = None) -> list[str]:
|
||||
"""저장 전 검사(M01) — 고친 파일의 틀 + 모든 로직의 변수 · 키 + 조합.
|
||||
|
||||
`files` = 폴더 전부(고친 뒤) · `book` = 키 대장.
|
||||
"""
|
||||
whole = mf.Master(files)
|
||||
out = [x for name in changed for x in check_form(name, files[name])]
|
||||
return out + check_logics(files, whole, book) + mcb.check_all(files, whole)
|
||||
|
||||
|
||||
def calc(files: dict[str, dict], ref: str, given: dict) -> dict:
|
||||
"""시험 계산(M01) — `ref` = 로직 키 · 멈추면 FormulaError."""
|
||||
return mf.run(mf.Master(files), ref, given)
|
||||
@@ -562,17 +552,25 @@ def _elements_into(files: dict[str, dict], by_section: dict) -> None:
|
||||
|
||||
|
||||
# ── (3) 로직 ──────────────────────────────────────────────────────────
|
||||
def check_logics(files: dict[str, dict], whole: mf.Master, book: dict | None = None) -> list[str]:
|
||||
out, graph = [], {}
|
||||
def check_logics(
|
||||
files: dict[str, dict], whole: mf.Master, book: dict | None = None, narrow=None
|
||||
) -> list[str]:
|
||||
"""`narrow` = (검사할 로직 키, 안 보는 줄이 부르는 키) — 주면 그 키만 검사하고
|
||||
품셈재료 연결 · 인력 준용(`check_links`)도 건너뜀(로직 줄 고침이 못 건드리는 자리)."""
|
||||
only, graph = narrow or (None, {})
|
||||
out, graph = [], dict(graph)
|
||||
for name, data in files.items():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄", []):
|
||||
key = str(row.get("키"))
|
||||
if only is not None and key not in only:
|
||||
continue
|
||||
found, calls = mf.check_logic(whole, row)
|
||||
out += [f"{name} · {x}" for x in found]
|
||||
graph[str(row.get("키"))] = calls
|
||||
graph[key] = calls
|
||||
out += [f"돌고 도는 참조 · {' → '.join(loop)}" for loop in mf.find_loops(graph)]
|
||||
return out + duplicate_keys(files, book) + check_links(whole)
|
||||
return out + duplicate_keys(files, book) + ([] if only else check_links(whole))
|
||||
|
||||
|
||||
def duplicate_keys(files: dict[str, dict], book: dict | None = None) -> list[str]:
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
목록(`/logics` · `/combos`)은 요청마다 마스터를 새로 읽지 않고 막힘도 미리 세어 둔 값을 씀.
|
||||
여기서 보는 것 = ① 세어 둔 값이 줄마다 검사한 것과 같음 ② 파일이 바뀌면 바로 다시 읽음
|
||||
③ 저장 직후 목록에 바로 비침 ④ 다른 파일의 로직이 바뀌면 그것을 부르던 줄도 다시 셈.
|
||||
③ 저장 직후 목록에 바로 비침 ④ 다른 파일의 로직이 바뀌면 그것을 부르던 줄도 다시 셈
|
||||
⑤ 저장 검사를 고친 줄 둘레로 좁혀도 흠(없는 키 · 없는 요소 · 돌고 도는 참조)을 그대로 잡음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -110,3 +112,80 @@ def test_부르는_로직이_바뀌면_다른_파일의_줄도_다시_센다(cli
|
||||
_write("로직_건설품셈_08장_건설기계.json", add_input) # 부르는 쪽 파일은 그대로
|
||||
caller = [x for x in _logics(client) if x["키"] == CALLER][0]
|
||||
assert caller["blocked"] is True and any("새입력" in x for x in caller["reasons"])
|
||||
|
||||
|
||||
def _save(client: TestClient, key: str, spoil) -> object:
|
||||
"""그 로직 줄을 고쳐 저장 길로 보냄 — 저장 검사가 흠을 잡는지 보려고."""
|
||||
one = client.get("/api/m01/logic", params={"key": key}).json()
|
||||
row = json.loads(json.dumps(one["logic"], ensure_ascii=False))
|
||||
spoil(row)
|
||||
return client.post(
|
||||
"/api/m01/save",
|
||||
json={
|
||||
"files": [
|
||||
{
|
||||
"file": one["file"],
|
||||
"version": one["version"],
|
||||
"changes": [{"op": "edit", "key": key, "row": row}],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _errors(res) -> list[str]:
|
||||
assert res.status_code == 422, res.text
|
||||
return res.json()["detail"]["errors"]
|
||||
|
||||
|
||||
def test_저장_검사를_좁혀도_흠을_잡는다(client: TestClient) -> None:
|
||||
"""고친 줄과 그 줄을 부르던 줄만 검사하지만 없는 키 · 없는 요소는 그대로 걸림."""
|
||||
_logics(client) # 미리 세어 둔 값이 선 평소 모양
|
||||
|
||||
def no_logic(row: dict) -> None:
|
||||
at = next(i for i, h in enumerate(row["호표"]) if h["종류"] == "로직")
|
||||
row["호표"][at]["요소"] = "로직(GC999999, 기계='5401-0017', 지역=지역)"
|
||||
|
||||
def no_element(row: dict) -> None:
|
||||
at = next(i for i, h in enumerate(row["호표"]) if h["종류"] == "인력")
|
||||
row["호표"][at]["요소"] = "LB999999"
|
||||
|
||||
for spoil, word in ((no_logic, "GC999999"), (no_element, "LB999999")):
|
||||
assert any(word in x for x in _errors(_save(client, CALLER, spoil)))
|
||||
|
||||
|
||||
def test_돌고_도는_참조도_저장이_막는다(client: TestClient) -> None:
|
||||
_logics(client)
|
||||
|
||||
def call_back(row: dict) -> None:
|
||||
row.setdefault("호표", []).append(
|
||||
{
|
||||
"종류": "로직",
|
||||
"요소": f"로직({CALLER}, 지반구분='토사', 크레인='2104-0010', 지역=지역)",
|
||||
"이름": "되부르기",
|
||||
"단위": "시간",
|
||||
"수량": "1",
|
||||
}
|
||||
)
|
||||
|
||||
assert any("돌고 도는" in x for x in _errors(_save(client, CALLEE, call_back)))
|
||||
|
||||
|
||||
def test_고친_줄을_부르던_줄이_깨지면_저장이_막힌다(client: TestClient) -> None:
|
||||
"""좁힌 검사가 「그 줄을 부르는 줄」 을 빠뜨리지 않는지 — 다른 파일에 있는 줄."""
|
||||
_logics(client)
|
||||
|
||||
def add_input(row: dict) -> None:
|
||||
row["입력"] = [*row.get("입력", []), {"이름": "새입력", "종류": "수"}]
|
||||
|
||||
assert any("새입력" in x for x in _errors(_save(client, CALLEE, add_input)))
|
||||
|
||||
|
||||
def test_미리_읽어_두면_첫_목록이_기다리지_않는다(client: TestClient) -> None:
|
||||
store.cache.forget()
|
||||
thread = store.cache.warm(store.FOLDER)
|
||||
thread.join(120)
|
||||
assert not thread.is_alive()
|
||||
start = time.perf_counter()
|
||||
assert _logics(client)
|
||||
assert time.perf_counter() - start < 1.0 # 미리 안 읽으면 2~3초
|
||||
|
||||
Reference in New Issue
Block a user