fix(M02 실무자료): 361-1 지역만 다른 자재 = 전국 — 줄 글 지역 · (전국) 품목 · 없으면 키 없는 전국 자리표 · 고르기 창 지역 변형 접음 · 전국 되돌림 · 만들기 본뜬.전국자재

- 맞춤: 줄 글 지역 = 그 지역 변형(꼬리표 「지역 — 줄 글」) · 지역 없는 변형((전국) 품목 먼저) · 없으면(레미콘) 「전국」 자리표(바탕 이름(전국)-관급/사급 · 규격 · 단위 · 키 · 단가 없음 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 옛 「못 가림 — N개 중 첫째」 없앰
- 후보 = 그 품목 변형 모두(`변형`) · 고르기 창 1위 / 전국 / 지역 변형(접힘) · 대상 꼴 `{전국: true}`
- 미리 셈 · 만들기: 전국 대상은 서버가 줄에서 다시 셈 · 일위대가 자재 칸이 M01 키만 받아 사전조합 `본뜬.전국자재` 에 적고 빠진 줄 까닭 · 장부엔 안 남김 · 만들기 몸 꼴에 `전국` 칸
- 시험: 전국 · 줄 글 지역 · (전국) 품목 · 고르기 지역 → 전국 되돌림 · 만들기 결과 · 화면 도우미

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 23:03:56 +09:00
co-authored by Claude Opus 5.5
parent 8799cf0c97
commit a2c2ad92de
14 changed files with 266 additions and 32 deletions
+33 -4
View File
@@ -14,7 +14,10 @@
사전조합으로 감싸 참조(같은 로직 두 번 금지) ·
자재 = {코드, 수량 {손}, 세 칸}(같은 자재 = 수량 더함 · 세 칸 = `Material.marks`
별도계상 · 자재구분 · 운반비 · 358-2) · 로직 자재와 같은 품목 = 사용자 자재에서 빼고
세 칸만 그 로직 `자재표시` 로(빠진 줄에 까닭) · 호표 = 확인된 사전조합 참조.
세 칸만 그 로직 `자재표시` 로(빠진 줄에 까닭) · 호표 = 확인된 사전조합 참조 ·
「전국」 자재(361-1 · 키 · 단가 없음 · 서버가 줄에서 다시 셈) = 일위대가 자재 칸이 M01 키만
받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 ·
지역은 프로젝트에서) · 장부엔 안 남김.
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 확인한 줄은 별칭 장부에 남음
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
빠진 줄(확인 안 함 · 뺌 · %) = 엑셀 금액과 함께 보고 · 만든 값 ↔ 엑셀 호표 단가(참고).
@@ -44,6 +47,7 @@ UNCHECKED = "확인 안 함"
PERCENT = "% 줄 — 우리 로직 덧줄이 셈"
WHOLE = "호표 통째로 대신"
TWICE = "로직 자재와 같은 품목 — 세 칸만 로직 자재표시로(수량 = 로직 셈)"
NATIONAL = "전국 — 지역은 프로젝트에서(자재 키 · 단가 없음 · 본뜬.전국자재)"
def mark(key: str, no: int, at: int | None = None) -> str:
@@ -142,7 +146,9 @@ class Builder:
raise _bad(f"없는 호표 「제{no}호표」", 404)
# ── 재검사 ───────────────────────────────────────────────────────
def _target(self, raw: dict[str, Any], where: str) -> dict[str, Any]:
def _target(self, raw: dict[str, Any], where: str, line: dict | None = None) -> dict[str, Any]:
if raw.get("전국"):
return self._national(raw, where, line)
if raw.get("호표") is None:
return ledger.check_pair(
{k: v for k, v in raw.items() if k != "호표"}, where, self.known
@@ -157,6 +163,16 @@ class Builder:
times, part = ledger.times_of(raw, where)
return {"호표": child, "배": times, "몫": part}
def _national(self, raw: dict[str, Any], where: str, line: dict | None) -> dict[str, Any]:
"""「전국」 대상 — 줄에서 서버가 다시 셈(이름 · 규격 · 단위 = 서버 것)."""
if set(raw) - {"전국", "배", "몫", "입력값"} or raw.get("입력값"):
raise _bad(f"전국 대상은 전국 · 배 · 몫만 「{where}」")
one = hmat.national(line) if line is not None else None
if one is None:
raise _bad(f"전국 자재 없음 — 지역 변형만 있는 자재 줄만 「{where}」")
times, part = ledger.times_of(raw, where)
return {**{k: one[k] for k in ("전국", "이름", "규격", "단위")}, "배": times, "몫": part}
def check(self, lines: list[dict[str, Any]], whole: dict[str, Any] | None) -> tuple[dict, list]:
"""몸 재검사 → ({차례: 확인 줄}, 통째 대상) — 틀리면 400 · 하위 호표 409."""
if whole is not None and lines:
@@ -180,7 +196,7 @@ class Builder:
if len(targets) > 20:
raise _bad(f"대상은 스물 이하 「{where}」")
out[at] = {"뺌": raw.get("뺌")} if raw.get("뺌") else {
"대상": [self._target(t, where) for t in targets]
"대상": [self._target(t, where, have[at]) for t in targets]
} # fmt: skip
pairs = [self._target(t, f"제{self.no}호표 통째") for t in (whole or {}).get("대상") or []]
if any("호표" in p for p in pairs) or len(pairs) > 20:
@@ -204,10 +220,21 @@ class Builder:
stamp = datetime.now().isoformat(timespec="seconds")
hopyo, file = self.hopyo, self.doc.get("파일") or ""
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
nation: dict[tuple, dict[str, Any]] = {}
def put(line: dict[str, Any], one: dict[str, Any], alias: str | None) -> None:
qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
memo = _memo(line, one)
if "전국" in one:
at = (one["이름"], one["규격"], one["단위"])
have = nation.get(at)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
k: have[k] for k in labor.MARKS if k in have
})} # fmt: skip
missing.append(_missing(line, NATIONAL))
return
if "자재" in one:
have = mats.get(one["자재"])
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
@@ -270,7 +297,7 @@ class Builder:
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
for one in got["대상"]:
put(line, one, alias)
pairs = [p for p in got["대상"] if "호표" not in p]
pairs = [p for p in got["대상"] if "호표" not in p and "전국" not in p]
if pairs:
book[ledger.key(line)] = self._row(line, "짝", pairs)
for code in self._inside(lines, mats): # 로직 자재와 같은 품목 = 세 칸만 로직 자재표시로
@@ -285,6 +312,8 @@ class Builder:
"자재": list(mats.values()),
"본뜬": self._origin(mark(self.key, self.no), stamp),
}
if nation:
doc["본뜬"]["전국자재"] = list(nation.values())
ref, again = _save(doc)
wrote = ledger.write(self.folder, book, self.who, file or None)
return {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
+17 -6
View File
@@ -198,7 +198,8 @@ def _names_only(line: dict, ctx: Ctx, why: str, full: bool) -> dict:
def _material(line: dict, ctx: Ctx, full: bool) -> dict:
"""자재 · 별도계상 줄(358-2 · `M02_Hopyo_Material`) — 「별도계상」 글 = 꼬리표 · 맞음 =
이름 · 규격 같음 하나 · 또는 바탕 이름 · 규격 수 같은 한 품목(변형 = 첫째 + 꼬리표)."""
이름 · 규격 같음 하나 · 또는 바탕 이름 · 규격 수 같은 한 품목(줄 글 지역 · 지역 없는 것 ·
「전국」 자리표 · 361-1) · 후보 = 그 품목 변형 모두(`변형` · 고르기 창이 접음)."""
me, apart = hmat.clean(line)
tags = [hmat.APART] if apart else []
found = hm._exact(me, "자재", ctx.got)
@@ -207,14 +208,14 @@ def _material(line: dict, ctx: Ctx, full: bool) -> dict:
sure, more = hmat.sure(me)
found, text, tags = [r["one"] for r in sure], "바탕 이름 · 규격 같음", tags + more
if found:
one = found[0]
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
target.update(몫=None, 배=conv["배"])
target = _mat_target(found[0], line)
conv = target["환산"]
ok = conv["배"] is not None and conv["갈래"] != "추정"
many = len(found) > 1
cands = [
{**hm._public(x), "점수": None, "판정": "맞음" if not at else "후보", "글": text}
for at, x in enumerate(found[:6])
| ({"변형": True} if many and "자재" in x else {})
for at, x in enumerate(found)
]
out = {"상태": "맞음", "체크": ok, "대상": [target], "후보": cands if full else []}
return {**out, "글": text, "꼬리표": tags}
@@ -223,6 +224,14 @@ def _material(line: dict, ctx: Ctx, full: bool) -> dict:
return {"상태": state, "체크": False, "대상": [], "후보": cands, "꼬리표": tags}
def _mat_target(one: dict, line: dict, times: Any = None) -> dict:
"""자재 · 「전국」 자리표 → 대상(배 = 환산 · `times` 주면 그것)."""
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
ok = isinstance(times, (int, float)) and not isinstance(times, bool) and times > 0
return {**target, "몫": None, "배": times if ok else conv["배"]}
def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
"""줄 → 맞춤(설계 3-3) — `full` 아니면 상태 · 체크만(호표 목록 수)."""
out: dict[str, Any] = {"열쇠": hm.line_key(line), "체크": False, "대상": [], "후보": []}
@@ -421,6 +430,8 @@ def guess(
targets.append(one)
elif t.get("자재") in ctx.mats:
targets.append(_pair({"자재": t["자재"], "배": t.get("배")}, line, ctx))
elif t.get("전국") and (one := hmat.national(line)):
targets.append(_mat_target(one, line, t.get("배")))
else:
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
qty = _qty(entry, {"우리품": option.ours_qty(total)})
+1 -1
View File
@@ -273,7 +273,7 @@ def _seeds() -> dict[str, set[str]]:
def _public(one: dict[str, Any]) -> dict[str, Any]:
keep = ("로직", "자재", "이름", "규격", "단위")
keep = ("로직", "자재", "전국", "이름", "규격", "단위")
return {k: one[k] for k in keep if k in one}
+47 -8
View File
@@ -6,8 +6,12 @@
자재 규격 수에 있는 몫(단위 배 1 · 10 · 1000 · 2 % 허용 · 면목 0.03 m = 30 mm) ·
줄 괄호 글 낱말(「몸통」)은 자재 이름에 들어야 함.
맞음 = ① 이름 · 규격 같음 하나(`Match._exact`) ② 바탕 이름 · 단위 · 규격 맞음 1 이 한 품목뿐
(지역 · 관급/사급만 다른 변형 = 한 품목 · 같은 단위 먼저 · 줄 글의 관급/사급 · 지역 먼저 ·
없으면 별도계상 줄 = 관급 · 재료비 줄 = 사급 · 지역 없는 것 · M01 차례 → 꼬리표 「못 가림」).
(지역 · 관급/사급만 다른 변형 = 한 품목 · 같은 단위 먼저 · 줄 글의 관급/사급 먼저 · 없으면
별도계상 줄 = 관급 · 재료비 줄 = 사급).
지역(361-1) = 줄 글에 지역 낱말 → 그 지역 변형(꼬리표 「지역 — 줄 글」) · 없으면 지역 없는
변형((전국) 품목) · 그것도 없으면(레미콘) 「전국」 = 키 · 단가 없는 자리표(`national` ·
바탕 이름(전국)-관급/사급 · 규격 · 단위 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 자동 고름 없음 ·
지역 변형은 고르기 창에서 사용자가.
그 밖 = 후보 다섯(변형은 하나로 접고 글에 수). 곳 낱말 = M01 레미콘 이름에서(자료).
가져오기 세 칸(`marks`) = 별도계상(표시 → 별도계상 · 아니면 직접계산) · 자재구분(고른
자재 이름 · 줄 글) · 운반비(「운반비 미포함 · 별도」 → 미포함). 로직 판정과 무관.
@@ -33,6 +37,9 @@ REGIONS = (
*"여주 이천 가평 연천".split(),
)
SKIP_WORDS = frozenset({"조합공통품목"})
NATIONAL = "전국"
NATIONAL_TAG = "전국 — 지역은 프로젝트에서"
REGION_TAG = "지역 — 줄 글"
_APART = re.compile(r"\(?\s*별도\s*계상\s*\)?")
_NUM = re.compile(r"\d+(?:\.\d+)?")
_RANGE = re.compile(r"(\d+(?:\.\d+)?)\s*(<=|≤|<)\s*[A-Za-z]\s*(<=|≤|<)\s*(\d+(?:\.\d+)?)")
@@ -60,8 +67,8 @@ def _paren_words(name: str) -> set[str]:
return {w for g in re.findall(r"\(([^()]*)\)", name) for w in re.findall(r"[가-힣]{2,}", g)}
def base(name: Any) -> str:
"""바탕 이름 — 괄호 글 · 끝 지역 · 관급/사급 꼬리 뗌(정규화)."""
def _bare(name: Any) -> str:
"""괄호 글 · 끝 지역 · 관급/사급 꼬리 뗀 글(정규화 전 · 「레미콘(서울)-사급」 → 레미콘)."""
text = _nfkc(name)
while "(" in text and (cut := re.sub(r"\([^()]*\)", "", text)) != text:
text = cut
@@ -71,7 +78,12 @@ def base(name: Any) -> str:
text = text[: m.start()]
for w in SUPPLY:
text = text[: -len(w)] if text.endswith(w) and len(text) > len(w) else text
return mm.plain(text)
return text.strip()
def base(name: Any) -> str:
"""바탕 이름 — `_bare` 정규화."""
return mm.plain(_bare(name))
def clean(line: dict[str, Any]) -> tuple[dict[str, Any], bool]:
@@ -166,16 +178,21 @@ def _wants(line: dict[str, Any]) -> tuple[str | None, list[str]]:
return supply, [w for w in words if _region(w) and w not in NO_REGION]
def _hit(r: dict[str, Any], regions: list[str]) -> bool:
return any(w[:2] == x[:2] for w in regions for x in r["region"])
def _prefer(rows: list[dict[str, Any]], line: dict[str, Any]) -> list[dict[str, Any]]:
"""변형 차례 — 줄 글의 관급/사급 · 지역 먼저 · 지역 없는 것 · 엑셀(M01) 차례."""
supply, regions = _wants(line)
def rank(r: dict[str, Any]) -> tuple:
hit = any(w[:2] == x[:2] for w in regions for x in r["region"])
hit = _hit(r, regions)
return (
supply is not None and r["supply"] not in (None, supply),
bool(regions) and not hit,
bool(r["region"]),
not any(w in r["one"]["이름"] for w in NO_REGION), # 「(전국)」 품목 먼저(361-1)
r["at"],
)
@@ -201,8 +218,30 @@ def sure(line: dict[str, Any]) -> tuple[list[dict[str, Any]], list[str]]:
if not full or len({r["key"] for r in full}) > 1:
return [], []
full = _prefer(full, line)
tags = [f"지역 · 관급/사급 못 가림 — {len(full)}개 중 첫째"] if len(full) > 1 else []
return full, tags
supply, regions = _wants(line)
if any(_hit(r, regions) for r in full):
return full, [REGION_TAG]
if len(full) == 1 or any(not r["region"] for r in full):
return full, []
return [_national(full[0], supply), *full], [NATIONAL_TAG]
def _national(row: dict[str, Any], supply: str | None) -> dict[str, Any]:
"""「전국」 자리표(361-1) — 지역 변형만 있는 품목 · 키 · 단가 없음 · 이름(전국)-관급/사급."""
one = row["one"]
name = f"{_bare(one['이름'])}({NATIONAL})" + (f"-{supply}" if supply else "")
spec = _nfkc(one["규격"])
for w in SKIP_WORDS: # 「조합공통품목, 25-21-120」(관급) → 25-21-120
spec = re.sub(rf"{w}[\s,]*", "", spec)
return {"one": {"전국": True, "이름": name, "규격": spec.strip(" ,"), "단위": one["단위"]}}
def national(line: dict[str, Any]) -> dict[str, Any] | None:
"""자재 · 별도계상 줄(다듬기 전)의 「전국」 자리표 — 없으면 None(만들기 · 미리 셈 재검사)."""
if line.get("갈래") not in hm.MATERIAL_KINDS:
return None
rows, _ = sure(clean(line)[0])
return next((r["one"] for r in rows if r["one"].get("전국")), None)
def candidates(line: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
+1
View File
@@ -334,6 +334,7 @@ class BuildTarget(_Strict):
로직: str | None = None
자재: str | None = None
호표: int | None = None
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
입력값: dict[str, Any] = Field(default_factory=dict)
배: float = 1
몫: float | None = None
@@ -89,6 +89,10 @@ export interface HopyoTarget {
로직?: string;
자재?: string;
호표?: number;
/** 「전국」 자리표(361-1) — 지역 변형만 있는 자재 · 키 · 단가 없음 */
전국?: boolean;
/** 같은 품목 지역 · 관급/사급 변형(361-1 · 고르기 창이 접음) */
변형?: boolean;
이름?: string;
규격?: string;
단위?: string | null;
@@ -190,6 +194,8 @@ export interface BuildTarget {
로직?: string;
자재?: string;
호표?: number;
/** 「전국」 자재(361-1) — 이름 · 규격 · 단위는 서버가 줄에서 다시 셈 */
전국?: boolean;
입력값?: Record<string, unknown>;
배: number;
몫?: number | null;
@@ -117,7 +117,11 @@ export function checkable(line: HopyoLine, d: CheckDraft): boolean {
/** 대상 → 만들기 몸 대상(이름 · 품 같은 보이기 칸은 안 보냄) */
export function toTarget(p: HopyoPart): BuildTarget {
if (p.호표 !== undefined) return { 호표: p.호표, 배: p.배 ?? 1 };
const head = p.로직 ? { 로직: p.로직, 입력값: p.입력값 ?? {} } : { 자재: p.자재 };
const head = p.로직
? { 로직: p.로직, 입력값: p.입력값 ?? {} }
: p.전국
? { 전국: true }
: { 자재: p.자재 };
return { ...head, 배: p.배 ?? 1, ...(p.몫 != null ? { 몫: p.몫 } : {}) };
}
@@ -120,7 +120,8 @@ function partLines(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLE
times.addEventListener("change", () =>
ctx.times(line, at, times.value === "" ? null : Number(times.value)),
);
const key = p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : "");
const key =
p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : p.전국 ? "전국" : "");
return el("div", {
className: "m02-hopyo__part",
attrs: { title: p.어디 ?? "" },
@@ -247,7 +248,7 @@ function matRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
el("td", { className: "m02-labrow__no", text: String(line.차례 + 1) }),
el("td", {
className: "m02-labrow__code",
text: parts.map((x) => x.자재 ?? "").join(" · ") || "—",
text: parts.map((x) => x.자재 ?? (x.전국 ? "전국" : "")).join(" · ") || "—",
}),
el("td", {
className: "m02-labrow__name m02-hopyo__parts",
@@ -6,6 +6,7 @@
* 뜻 낱말 안 겹치는 「후보」는 회색 끝) · 같은 이름이면 책(산림 · 건설) · 판정 꼬리표 · 근거 꼬리표(근거 맞음 초록)
* · 고름 = [후보](한 로직) · 또는 체크 + 몫 칸 → [일괄 적용(n)](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움
* · [다른 로직 검색] = `openLogicPicker` 에 품명 첫 뜻 낱말 · [다른 자재 검색] = 자재 줄 · 재료비 있는 줄(358-2)
* · 자재 줄(361-1) = 1위 / 전국(자리표 · 지역 고른 뒤 되돌림) / 지역 변형(접힘 · 하나 고르면 그 지역) / 그 밖
* 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감.
* ========================================================================== */
@@ -25,6 +26,10 @@ import { pickMaterial } from "./M02_Material_Pick";
import "./M02_MasterTemplete_Hopyo.css";
const MATERIAL = new Set(["자재", "별도계상"]);
const FOLD_TEXT: Record<string, string> = {
"지역 변형": "하나 고르면 그 지역 · 「전국」으로 되돌림은 위",
숫자: "이름 · 출처 안 닮음 · 숫자 지문만",
};
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
const num = (v: number): string => String(Math.round(v * 10000) / 10000);
@@ -131,6 +136,8 @@ export function candGroups(
if (material)
return [
{ 제목: "1위", 접힘: false, 후보: top },
{ 제목: "전국", 접힘: false, 후보: take(all.filter((c) => c.전국)) },
{ 제목: "지역 변형", 접힘: true, 후보: take(all.filter((c) => c.변형)) },
{ 제목: "그 밖 후보", 접힘: false, 후보: take(all.slice(1)) },
].filter((g) => g.후보.length);
const named = m.이름후보 ?? [];
@@ -156,7 +163,7 @@ export function sameNames(groups: CandGroup[]): Set<string> {
/** 후보 단추 글 — 「이름 [단위] · 키」 · 같은 이름이면 책 두 글자(산림 · 건설) */
export const candText = (c: HopyoTarget, twins: Set<string>): string =>
`${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}${twins.has(c.이름 ?? "") && c.책 ? ` · ${c.책.slice(0, 2)}` : ""}`;
`${targetText(c)} · ${c.로직 ?? c.자재 ?? "키 없음"}${twins.has(c.이름 ?? "") && c.책 ? ` · ${c.책.slice(0, 2)}` : ""}`;
/** 자재 고름 — 자재 · 별도계상 줄 · 재료비 있는 단가산출 줄(358-2 · P.V.C파이프설치 등) */
export const canPickMaterial = (line: Pick<HopyoLine, "갈래" | "단가">): boolean =>
@@ -165,7 +172,7 @@ export const canPickMaterial = (line: Pick<HopyoLine, "갈래" | "단가">): boo
/** 고른 후보 → 만들기 대상(배 1 · 몫 있으면 배 = 몫) */
export const pickedTargets = (picked: { c: HopyoTarget; 몫: number | null }[]): BuildTarget[] =>
picked.map(({ c, 몫 }) => ({
...(c.로직 ? { 로직: c.로직 } : { 자재: c.자재 }),
...(c.로직 ? { 로직: c.로직 } : c.전국 ? { 전국: true } : { 자재: c.자재 }),
입력값: {},
배: 몫 ?? 1,
몫,
@@ -220,7 +227,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
const tag = basisTag(c.근거);
return el("div", {
className: `m02-hopyo__cand${weakCand(c) ? " is-weak" : ""}`,
attrs: { "data-cand": c.로직 ?? c.자재 ?? "" },
attrs: { "data-cand": c.로직 ?? c.자재 ?? (c.전국 ? "전국" : "") },
children: [
box,
createButton({
@@ -260,7 +267,10 @@ export function openHopyoChoose(opt: ChooseOptions): void {
const fold = el("details", {
className: "m02-hopyo__group",
attrs: { "data-group": g.제목 },
children: [el("summary", { text: `${title} — 이름 · 출처 안 닮음 · 숫자 지문만` }), list],
children: [
el("summary", { text: `${title} — ${FOLD_TEXT[g.제목] ?? FOLD_TEXT.숫자}` }),
list,
],
});
return fold;
});