feat(M01): 단가산출 로직 상세 차례 · 오른쪽 흐름도/원문 두 패널
- 좌측 차례 = 기본정보 › 시험 계산 › 단가산출 상세 › 텍스트 수식 - 오른쪽 패널 둘(흐름도 · 원문) — 하나만 열림 · 둘 다 접기 · 머리 토글 단추 · 열린 패널 계정 취향(m01-logic-panel) - 흐름도 패널 접힘을 부르는 쪽이 정하는 길(folded · onFold · fold) - 원문 패널 = 출처 글로 품셈 본문 절을 찾아 보임(GET /api/m01/logic/source) · 못 찾으면 「원문 연결 없음」 + 출처 - 시험 test_m01_logic_source.py · 화면 시나리오 ui_checks/44-4_로직상세_패널.js Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -103,6 +103,79 @@ def sections(book: str, division: str = "", chapter: str = "", q: str = "", limi
|
||||
return {"total": len(hits), "sections": hits[: min(max(limit, 1), 1000)]}
|
||||
|
||||
|
||||
# ── 로직 원문(출처 글 → 품셈 본문 절) ──────────────────────────────────
|
||||
_NUMBER = re.compile(r"(?<![\d-])\d+(?:-\d+)+(?![\d-])")
|
||||
|
||||
|
||||
def _blocks(lines: list[str]) -> list[dict]:
|
||||
"""md 줄 → 화면 조각 — 제목 {h} · 표 {table} · 글 {p} · 주석 줄은 뺌."""
|
||||
out: list[dict] = []
|
||||
for line in lines:
|
||||
text = line.strip()
|
||||
if not text or text.startswith("<!--"):
|
||||
continue
|
||||
if text.startswith("#"):
|
||||
out.append({"h": len(text) - len(text.lstrip("#")), "text": text.lstrip("#").strip()})
|
||||
elif text.startswith("|"):
|
||||
cells = [c.strip() for c in text.strip("|").split("|")]
|
||||
if all(re.fullmatch(r":?-{3,}:?", c) for c in cells):
|
||||
continue # 머리 · 몸 가름 줄
|
||||
if not out or "table" not in out[-1]:
|
||||
out.append({"table": []})
|
||||
out[-1]["table"].append(cells)
|
||||
else:
|
||||
out.append({"p": text})
|
||||
return out
|
||||
|
||||
|
||||
def _section(book: str, division: str, number: str) -> dict | None:
|
||||
"""번호 절을 품셈 본문에서 찾음 — 없으면 끝 마디를 하나씩 떼며 위 절로(두 마디까지)."""
|
||||
parts = number.split("-")
|
||||
folders = [f for d, no, _t, f in _chapters(book) if d == division and int(no) == int(parts[0])]
|
||||
for upto in range(len(parts), 1, -1):
|
||||
want = "-".join(parts[:upto])
|
||||
for folder in folders:
|
||||
for md in sorted(folder.glob("*.md")):
|
||||
lines = md.read_text(encoding="utf-8").split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
m = _HEAD.match(line)
|
||||
if not m or m.group(1) != want:
|
||||
continue
|
||||
level = len(line) - len(line.lstrip("#"))
|
||||
end = next(
|
||||
(
|
||||
j
|
||||
for j in range(i + 1, len(lines))
|
||||
if _HEAD.match(lines[j])
|
||||
and len(lines[j]) - len(lines[j].lstrip("#")) <= level
|
||||
),
|
||||
len(lines),
|
||||
)
|
||||
return {
|
||||
"number": number,
|
||||
"matched": want,
|
||||
"file": md.relative_to(BODY.parent).as_posix(),
|
||||
"blocks": _blocks(lines[i:end]),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def source(src: str) -> dict:
|
||||
"""로직 출처 글(예 「건설품셈 토목 1-1-2」 · 「산림품셈 3-4-2 1」)로 찾은 품셈 원문 절 —
|
||||
번호가 여럿이면(「8-1-5 · 8-1-6」) 절마다 · 못 찾으면 found 빈 목록(화면 「원문 연결 없음」)."""
|
||||
words = src.split()
|
||||
book = words[0] if words else ""
|
||||
if book not in BOOKS:
|
||||
return {"source": src, "found": []}
|
||||
division = (
|
||||
next((d for d in cm.DIVISIONS if src.startswith(f"{book} {d} ")), "")
|
||||
if book == "건설품셈"
|
||||
else ""
|
||||
)
|
||||
found = [_section(book, division, n) for n in dict.fromkeys(_NUMBER.findall(src))]
|
||||
return {"source": src, "found": [x for x in found if x]}
|
||||
|
||||
|
||||
# ── 표 조건 값 ─────────────────────────────────────────────────────────
|
||||
def table_options(file: str, key: str) -> dict:
|
||||
"""표 조건 칸의 값만 — 표 몸을 통째로 받지 않으려는 자리(만들기 입력 제안)."""
|
||||
|
||||
Reference in New Issue
Block a user