fix(일위대가실무): 324 다듬기 — 줄 표 줄바꿈 없음 · 고르기 창 [닫기] 안 갈라짐 · 뜻 낱말 안 겹치는 후보 숨김 · 다시 씀 · 새로 덮음 알림 글
- 줄 표: 갈래 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 입력값 · 환산 · 단추 열 줄바꿈 없음(글 폭) · 입력값 길면 줄임표 · 칸 여백 좁힘 · 품명 · 규격 · 대상이 남는 폭 - 고르기 창 머리: [닫기] 단추 줄바꿈 없음 · 안 줄어듦 - 후보: 정리 이름 뜻 낱말(괄호 밖 두 글자 이상 덩어리 · 낱글자 붙임)이 하나도 안 겹치면 뺌(잔토처리 · 임도표지석 → 후보 없음) · 다 걸러지면 「가까운 것 없음」 + [다른 로직 · 자재 찾기] - 알림: 있던 사전조합 = 「다시 씀 — 빠진 줄 N」 · [새로 만들기] 뒤 = 「새로 만들어 덮음」(토스트 · 판 글) - 시험 hopyo 넷 42 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
차례 = ① 층 기억(같은 층 → 위 층 · 확정분) ② 이름 같음(정규화) ·
|
차례 = ① 층 기억(같은 층 → 위 층 · 확정분) ② 이름 같음(정규화) ·
|
||||||
씨앗(일위대가조합.json 메모 · 읽기만)
|
씨앗(일위대가조합.json 메모 · 읽기만)
|
||||||
③ 정리 이름 같음(괄호 · 기계 낱말) ④ 후보 5(글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤) —
|
③ 정리 이름 같음(괄호 · 기계 낱말) ④ 후보 5(글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 ·
|
||||||
|
뜻 낱말이 하나도 안 겹치면 뺌 — 「잔토처리」 ↔ 「소금처리」) —
|
||||||
③ ④ 는 사용자 확정(층 기억) 뒤에만 들어감.
|
③ ④ 는 사용자 확정(층 기억) 뒤에만 들어감.
|
||||||
줄 갈래: 단가산출 · 기계 · 기타 = 로직 / 자재 · 별도계상 = 자재(M01 자재품목) / 호표 = 그 호표 /
|
줄 갈래: 단가산출 · 기계 · 기타 = 로직 / 자재 · 별도계상 = 자재(M01 자재품목) / 호표 = 그 호표 /
|
||||||
% = 안 들어감(로직 덧줄이 셈).
|
% = 안 들어감(로직 덧줄이 셈).
|
||||||
@@ -80,6 +81,27 @@ def tidy(name: Any) -> str:
|
|||||||
return mm.plain(_PAREN.sub("", text))
|
return mm.plain(_PAREN.sub("", text))
|
||||||
|
|
||||||
|
|
||||||
|
def words(name: Any) -> frozenset[str]:
|
||||||
|
"""뜻 낱말 — 괄호 밖 한글 덩어리 · 영문 두 글자 넘는 것
|
||||||
|
(낱글자 덩어리는 붙임 「면 목」 = 면목)."""
|
||||||
|
text = unicodedata.normalize("NFKC", str(name or "")).lower()
|
||||||
|
for old, new in MACH_WORDS:
|
||||||
|
text = text.replace(old, new)
|
||||||
|
out, single = [], ""
|
||||||
|
for piece in re.findall(r"[가-힣]+|[a-z]{2,}", _PAREN.sub(" ", text)):
|
||||||
|
if len(piece) == 1:
|
||||||
|
single += piece
|
||||||
|
continue
|
||||||
|
out += [single, piece]
|
||||||
|
single = ""
|
||||||
|
return frozenset(w for w in [*out, single] if len(w) > 1)
|
||||||
|
|
||||||
|
|
||||||
|
def near(mine: frozenset[str], theirs: frozenset[str]) -> bool:
|
||||||
|
"""뜻 낱말이 하나라도 겹침(한쪽이 다른 쪽에 듦) — 내 낱말이 없으면 거르지 않음."""
|
||||||
|
return not mine or any(a in b or b in a for a in mine for b in theirs)
|
||||||
|
|
||||||
|
|
||||||
def _spec_text(text: Any) -> str:
|
def _spec_text(text: Any) -> str:
|
||||||
out = str(text or "")
|
out = str(text or "")
|
||||||
for old, new in MACH_WORDS:
|
for old, new in MACH_WORDS:
|
||||||
@@ -221,13 +243,15 @@ def _targets(whole_id: int) -> dict[str, Any]:
|
|||||||
"단위": str(row.get("단위") or ""),
|
"단위": str(row.get("단위") or ""),
|
||||||
"plain": mm.plain(name),
|
"plain": mm.plain(name),
|
||||||
"spec": mm.plain_spec(row.get("규격")),
|
"spec": mm.plain_spec(row.get("규격")),
|
||||||
"tidy": tidy(re.sub(r"-(사급|관급)$", "", name.strip())),
|
"base": (base := re.sub(r"-(사급|관급)$", "", name.strip())),
|
||||||
|
"tidy": tidy(base),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
names: dict[str, dict[str, list]] = {"로직": {}, "자재": {}}
|
names: dict[str, dict[str, list]] = {"로직": {}, "자재": {}}
|
||||||
for pool, group in (("로직", logics), ("자재", mats)):
|
for pool, group in (("로직", logics), ("자재", mats)):
|
||||||
for one in group:
|
for one in group:
|
||||||
one["grams"] = _grams(one["tidy"])
|
one["grams"] = _grams(one["tidy"])
|
||||||
|
one["words"] = words(one["base"] if "base" in one else one["이름"])
|
||||||
names[pool].setdefault(one["plain"], []).append(one)
|
names[pool].setdefault(one["plain"], []).append(one)
|
||||||
return {"로직": logics, "자재": mats, "이름": names, "씨앗": _seeds()}
|
return {"로직": logics, "자재": mats, "이름": names, "씨앗": _seeds()}
|
||||||
|
|
||||||
@@ -278,8 +302,9 @@ def candidates(
|
|||||||
grams, unit = _grams(me), line.get("단위")
|
grams, unit = _grams(me), line.get("단위")
|
||||||
text = mm.plain_spec(f"{line.get('이름')}{line.get('규격')}")
|
text = mm.plain_spec(f"{line.get('이름')}{line.get('규격')}")
|
||||||
scored, seen = [], set()
|
scored, seen = [], set()
|
||||||
|
mine = words(line.get("이름"))
|
||||||
for one in (got or targets())[pool]:
|
for one in (got or targets())[pool]:
|
||||||
if not grams & one["grams"]:
|
if not grams & one["grams"] or not near(mine, one["words"]):
|
||||||
continue
|
continue
|
||||||
score = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
score = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"]))
|
||||||
score += 1 if one["tidy"] == me else 0
|
score += 1 if one["tidy"] == me else 0
|
||||||
|
|||||||
@@ -49,6 +49,33 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 줄 표 — 갈래 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 입력값 · 환산 · 단추 = 줄바꿈 없음(글자가 세로로 안
|
||||||
|
갈라짐 · 폭 = 글 폭) · 품명 · 규격 · 대상이 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */
|
||||||
|
.m02-hopyo__table.m02-hopyo__lines :is(th, td) {
|
||||||
|
padding: var(--spacing-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo__table.m02-hopyo__lines
|
||||||
|
:is(th, td):is(
|
||||||
|
:nth-child(1),
|
||||||
|
:nth-child(4),
|
||||||
|
:nth-child(5),
|
||||||
|
:nth-child(6),
|
||||||
|
:nth-child(7),
|
||||||
|
:nth-child(9),
|
||||||
|
:nth-child(10),
|
||||||
|
:nth-child(11)
|
||||||
|
) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 입력값 — 길면 줄임표(전체 글은 칸 풍선 도움말) */
|
||||||
|
.m02-hopyo__table.m02-hopyo__lines td:nth-child(9) {
|
||||||
|
max-width: 24em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.m02-hopyo__line.is-out {
|
.m02-hopyo__line.is-out {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
@@ -109,6 +136,12 @@
|
|||||||
background: var(--color-surface-raised);
|
background: var(--color-surface-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 고르기 창 머리 — 제목이 두 줄이어도 [닫기] 글이 안 갈라지고 안 줄어듦 */
|
||||||
|
.m02-hopyo__choose .ui-modal__top > .ui-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.m02-hopyo__cands {
|
.m02-hopyo__cands {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ export const inputsText = (values: Record<string, unknown>, unread: string[]): s
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" · ");
|
.join(" · ");
|
||||||
|
|
||||||
|
/** 만들기 결과 낱말 — [새로 만들기] = 「새로 만들어 덮음」 · 있던 것 = 「다시 씀」 · 그 밖 「만듦」 */
|
||||||
|
export const builtWord = (got: Pick<HopyoBuilt, "다시씀">, fresh: boolean): string =>
|
||||||
|
fresh ? "새로 만들어 덮음" : got.다시씀 ? "다시 씀" : "만듦";
|
||||||
|
|
||||||
/** 확정 한 줄 몸 — 대상 · 입력값 · 환산(0 넘는 수) · 없으면 null */
|
/** 확정 한 줄 몸 — 대상 · 입력값 · 환산(0 넘는 수) · 없으면 null */
|
||||||
export function memoryOf(
|
export function memoryOf(
|
||||||
target: HopyoTarget | null,
|
target: HopyoTarget | null,
|
||||||
@@ -200,7 +204,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* ── 호표 하나 — 줄 맞춤 ────────────────────────────────────── */
|
/* ── 호표 하나 — 줄 맞춤 ────────────────────────────────────── */
|
||||||
async function drawOne(no: number, built?: HopyoBuilt): Promise<void> {
|
async function drawOne(no: number, built?: HopyoBuilt & { 새로: boolean }): Promise<void> {
|
||||||
const got = await load(
|
const got = await load(
|
||||||
() => readHopyoOne(opt.scope, key, no),
|
() => readHopyoOne(opt.scope, key, no),
|
||||||
() => void drawOne(no),
|
() => void drawOne(no),
|
||||||
@@ -381,7 +385,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
body.append(
|
body.append(
|
||||||
el("p", { className: "m02-hopyo__note", text: list.length ? "후보" : "후보 없음" }),
|
el("p", { className: "m02-hopyo__note", text: list.length ? "후보" : "가까운 것 없음" }),
|
||||||
el("div", { className: "m02-hopyo__cands", children: list }),
|
el("div", { className: "m02-hopyo__cands", children: list }),
|
||||||
more,
|
more,
|
||||||
);
|
);
|
||||||
@@ -411,11 +415,10 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
|||||||
try {
|
try {
|
||||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: key, 번: no };
|
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: key, 번: no };
|
||||||
const got = await buildHopyo(opt.scope, from, fresh);
|
const got = await buildHopyo(opt.scope, from, fresh);
|
||||||
showToast(
|
const word = builtWord(got, fresh);
|
||||||
got.빠진줄.length ? `만듦 — 빠진 줄 ${got.빠진줄.length}` : "만듦",
|
const miss = got.빠진줄.length;
|
||||||
got.빠진줄.length ? "warning" : "success",
|
showToast(miss ? `${word} — 빠진 줄 ${miss}` : word, miss ? "warning" : "success");
|
||||||
);
|
await drawOne(no, { ...got, 새로: fresh });
|
||||||
await drawOne(no, got);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fail(error);
|
fail(error);
|
||||||
}
|
}
|
||||||
@@ -444,11 +447,15 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
function builtBlock(no: number, got: HopyoBuilt): HTMLElement {
|
function builtBlock(no: number, got: HopyoBuilt & { 새로: boolean }): HTMLElement {
|
||||||
const c = got.비교;
|
const c = got.비교;
|
||||||
const again = got.다시씀
|
const again = got.다시씀
|
||||||
? [
|
? [
|
||||||
note(`있던 사전조합 ${got.참조.키} 를 다시 씀`),
|
note(
|
||||||
|
got.새로
|
||||||
|
? `사전조합 ${got.참조.키} 를 새로 만들어 덮음`
|
||||||
|
: `있던 사전조합 ${got.참조.키} 를 다시 씀`,
|
||||||
|
),
|
||||||
createButton({
|
createButton({
|
||||||
label: "새로 만들기",
|
label: "새로 만들기",
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ function loadTs(file) {
|
|||||||
view.memoryOf({ 로직: "GF000101" }, {}, 0),
|
view.memoryOf({ 로직: "GF000101" }, {}, 0),
|
||||||
view.memoryOf(null, {}, 1),
|
view.memoryOf(null, {}, 1),
|
||||||
],
|
],
|
||||||
|
word: [
|
||||||
|
view.builtWord({ 다시씀: false }, false),
|
||||||
|
view.builtWord({ 다시씀: true }, false),
|
||||||
|
view.builtWord({ 다시씀: true }, true),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
process.stdout.write(JSON.stringify(out));
|
process.stdout.write(JSON.stringify(out));
|
||||||
|
|||||||
@@ -185,3 +185,18 @@ def test_로직_추가_고르기_줄은_이_층과_위_층(client):
|
|||||||
assert three["맞춤"][1] == 4
|
assert three["맞춤"][1] == 4
|
||||||
system = client.get("/api/m02/layers/system/hopyo-picks").json()["줄"]
|
system = client.get("/api/m02/layers/system/hopyo-picks").json()["줄"]
|
||||||
assert {r["층"] for r in system} == {"system"} # 마스터 = 마스터만
|
assert {r["층"] for r in system} == {"system"} # 마스터 = 마스터만
|
||||||
|
|
||||||
|
|
||||||
|
def test_뜻_낱말이_안_겹치면_후보_아님():
|
||||||
|
assert hm.words("면 목") == {"면목"} and hm.words("구조물되메우기(인력100%)") == {
|
||||||
|
"구조물되메우기"
|
||||||
|
}
|
||||||
|
assert hm.words("되메우기 및 다짐(소형장비)") == {"되메우기", "다짐"}
|
||||||
|
soil = {"이름": "잔토처리(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
|
||||||
|
assert hm.candidates(soil, "로직") == [] # 「처리」 두 글자만 겹침(소금처리 · 바탕처리)
|
||||||
|
stone = {"이름": "임도표지석(받침,글새김포함)", "규격": "", "단위": "㎥", "갈래": "자재"}
|
||||||
|
assert hm.candidates(stone, "자재") == [] # 속도표지판 · 도로표지용페인트
|
||||||
|
got = hm.match_line(soil, [], {})
|
||||||
|
assert got["상태"] == "없음" and got["후보"] == []
|
||||||
|
fill = {"이름": "구조물되메우기(인력100%)", "규격": "", "단위": "㎥", "갈래": "단가산출"}
|
||||||
|
assert hm.candidates(fill, "로직")[0]["이름"] == "되메우기" # 한쪽 낱말에 듦 = 겹침
|
||||||
|
|||||||
@@ -99,6 +99,21 @@ def test_본_화면_글_도우미():
|
|||||||
None, # 환산 0 = 확정 못 함
|
None, # 환산 0 = 확정 못 함
|
||||||
None, # 대상 없음
|
None, # 대상 없음
|
||||||
]
|
]
|
||||||
|
assert view["word"] == ["만듦", "다시 씀", "새로 만들어 덮음"] # 알림 · 판 글
|
||||||
|
|
||||||
|
|
||||||
|
def test_줄_표_줄바꿈_없음과_고르기_창_머리():
|
||||||
|
css = _text("M02_MasterTemplete_Hopyo.css")
|
||||||
|
rule = css.split(".m02-hopyo__table.m02-hopyo__lines\n :is(th, td):is(")[1].split("}")[0]
|
||||||
|
cols = [int(n) for n in re.findall(r":nth-child\((\d+)\)", rule)]
|
||||||
|
assert cols == [1, 4, 5, 6, 7, 9, 10, 11] and "white-space: nowrap;" in rule
|
||||||
|
heads = ["갈래", "품명", "규격", "수량", "단위", "엑셀 단가", "맞춤", "대상", "입력값", "환산"]
|
||||||
|
view = _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||||
|
flat = re.sub(r"\s+", "", view)
|
||||||
|
assert re.sub(r"\s+", "", ",".join(f'"{h}"' for h in heads)) in flat # 열 차례 그대로
|
||||||
|
top = css.split(".m02-hopyo__choose .ui-modal__top > .ui-btn {")[1].split("}")[0]
|
||||||
|
assert "flex-shrink: 0;" in top and "white-space: nowrap;" in top
|
||||||
|
assert '"가까운 것 없음"' in view and "builtWord(got, fresh)" in view
|
||||||
|
|
||||||
|
|
||||||
def test_좌측_판_자리와_권한():
|
def test_좌측_판_자리와_권한():
|
||||||
|
|||||||
Reference in New Issue
Block a user