fix(705-2 붙여넣기 · 다시 잇기): 화면 검증에서 나온 흠 — 첫 줄 머리 짐작 · 열 짝 한 칸 밀림 · 저절로 고름 · 칸 폭 · B09 최신 캐시
- 첫 줄이 수량 없음이어도 금액 없으면(레미콘 별도계산) 줄 — 머리는 금액 있을 때만 - 기억한 열 짝을 다른 꼴(엑셀꼴 호표 칸 · STmate 꼴)에 붙이면 서버가 한 칸 밀어 맞춤 - 다시 잇기 저절로 고름 = 후보 하나 · 붙인 단가 있음만(별도계상 0 원 · 후보 여럿은 사람이 고름) · 빈 칸 글 없앰 · 고름 칸 앞으로 · 칸 폭 1100 안 - B09 알림에서 연 다시 잇기가 붙이기 전 캐시를 읽던 흠 — 붙인 뒤 캐시를 들고 감 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -254,8 +254,9 @@ async function post(path: string, body: Row): Promise<Row> {
|
|||||||
const COST_PART = { 노무비: "labor", 재료비: "material", 경비: "expense" } as const;
|
const COST_PART = { 노무비: "labor", 재료비: "material", 경비: "expense" } as const;
|
||||||
|
|
||||||
/** 붙인 줄 다시 잇기(705-2) — 후보 = 서버(M02 이음 짝) · 단가 = 이 공사 단가 책 · 바꿈 = 캐시 + 서버 셈 */
|
/** 붙인 줄 다시 잇기(705-2) — 후보 = 서버(M02 이음 짝) · 단가 = 이 공사 단가 책 · 바꿈 = 캐시 + 서버 셈 */
|
||||||
function relinkFold(ctx: TabContext, cover: string): RelinkSource {
|
function relinkFold(ctx: TabContext, cover: string, from: Row): RelinkSource {
|
||||||
const added = (): Row[] => ((ctx.user.daega_cover || {})[cover] || {})[ADD] || [];
|
let now = from; // 붙인 뒤 캐시(ctx.user 는 그리기 전 값) — 바꿀 때마다 새 값
|
||||||
|
const added = (): Row[] => ((now.daega_cover || {})[cover] || {})[ADD] || [];
|
||||||
const book = (kind: "노임" | "자재", key: string): number | null | undefined =>
|
const book = (kind: "노임" | "자재", key: string): number | null | undefined =>
|
||||||
(ctx.payload.inputs?.[kind === "노임" ? "labor" : "materials"] || []).find(
|
(ctx.payload.inputs?.[kind === "노임" ? "labor" : "materials"] || []).find(
|
||||||
(r: Row) => r.key === key,
|
(r: Row) => r.key === key,
|
||||||
@@ -302,9 +303,10 @@ function relinkFold(ctx: TabContext, cover: string): RelinkSource {
|
|||||||
if ("노임" in c) ln.link.labor = c.노임 ?? null;
|
if ("노임" in c) ln.link.labor = c.노임 ?? null;
|
||||||
if ("자재" in c) ln.link.material = c.자재 ?? null;
|
if ("자재" in c) ln.link.material = c.자재 ?? null;
|
||||||
}
|
}
|
||||||
const all: Row = JSON.parse(JSON.stringify(ctx.user.daega_cover || {}));
|
const all: Row = JSON.parse(JSON.stringify(now.daega_cover || {}));
|
||||||
all[cover] = { ...(all[cover] || {}), [ADD]: lines };
|
all[cover] = { ...(all[cover] || {}), [ADD]: lines };
|
||||||
const user = { ...ctx.user, daega_cover: all };
|
const user = { ...now, daega_cover: all };
|
||||||
|
now = user;
|
||||||
Object.assign(ctx.payload, await post("calc", { user })); // 새 이음 키 = 이 공사 단가 책 수요
|
Object.assign(ctx.payload, await post("calc", { user })); // 새 이음 키 = 이 공사 단가 책 수요
|
||||||
ctx.setUser(user);
|
ctx.setUser(user);
|
||||||
return { back, note: `바뀐 줄 ${back.length} — [저장]으로 남김` };
|
return { back, note: `바뀐 줄 ${back.length} — [저장]으로 남김` };
|
||||||
@@ -333,7 +335,7 @@ async function pasteFold(ctx: TabContext, cover: string, text: string): Promise<
|
|||||||
`고정 줄 ${got.줄} 붙임 · 엑셀 합계 ${won} · 이음 ${got.이음} · 후보 ${got.이음후보} — [저장]으로 남김 · 이 알림을 누르면 이 공사 단가로 다시 잇기`,
|
`고정 줄 ${got.줄} 붙임 · 엑셀 합계 ${won} · 이음 ${got.이음} · 후보 ${got.이음후보} — [저장]으로 남김 · 이 알림을 누르면 이 공사 단가로 다시 잇기`,
|
||||||
"success",
|
"success",
|
||||||
9000,
|
9000,
|
||||||
() => void openRelink(relinkFold(ctx, cover)),
|
() => void openRelink(relinkFold(ctx, cover, got.user)),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(`붙여넣기 실패 — ${error instanceof Error ? error.message : String(error)}`, "error");
|
showToast(`붙여넣기 실패 — ${error instanceof Error ? error.message : String(error)}`, "error");
|
||||||
|
|||||||
@@ -113,6 +113,23 @@ def check_cols(cols: Any) -> dict[str, int]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def fit(cols: dict[str, int], rows: list[list[str]]) -> dict[str, int]:
|
||||||
|
"""기억한 열 짝이 다른 꼴에 붙을 때 — 칸 전체를 -1 · 0 · +1 밀어 보고 품명 글 · 수량 수가 가장 많이
|
||||||
|
맞는 쪽(엑셀꼴 「호표」 칸 있음 · STmate 꼴 없음 · 같으면 그대로)."""
|
||||||
|
|
||||||
|
def score(d: int) -> int:
|
||||||
|
got = 0
|
||||||
|
for r in rows:
|
||||||
|
name, qty = _cell(r, cols["품명"] + d), _cell(r, cols["수량"] + d)
|
||||||
|
got += bool(name and hx._num(name) is None) + (hx._num(qty) is not None)
|
||||||
|
return got
|
||||||
|
|
||||||
|
best = max((0, 1, -1), key=lambda d: (score(d), d == 0))
|
||||||
|
if best == 0 or min(cols.values()) + best < 0:
|
||||||
|
return cols
|
||||||
|
return {k: v + best for k, v in cols.items()}
|
||||||
|
|
||||||
|
|
||||||
def _money_cols(cols: dict[str, int]) -> dict[str, int]:
|
def _money_cols(cols: dict[str, int]) -> dict[str, int]:
|
||||||
out = {"노무비": cols.get("노"), "재료비": cols.get("재"), "경비": cols.get("경")}
|
out = {"노무비": cols.get("노"), "재료비": cols.get("재"), "경비": cols.get("경")}
|
||||||
if cols.get("금액") is not None:
|
if cols.get("금액") is not None:
|
||||||
@@ -148,7 +165,7 @@ def read_text(text: str, cols: Any = None) -> dict[str, Any]:
|
|||||||
below = [_flat(v) for v in rows[start]] if start < len(rows) else []
|
below = [_flat(v) for v in rows[start]] if start < len(rows) else []
|
||||||
start += "단가" in below or "금액" in below
|
start += "단가" in below or "금액" in below
|
||||||
elif cols is not None:
|
elif cols is not None:
|
||||||
used = check_cols(cols)
|
used = fit(check_cols(cols), rows)
|
||||||
else:
|
else:
|
||||||
used, asked = guess(rows), True
|
used, asked = guess(rows), True
|
||||||
money = _money_cols(used)
|
money = _money_cols(used)
|
||||||
@@ -206,9 +223,10 @@ def read_text(text: str, cols: Any = None) -> dict[str, Any]:
|
|||||||
qty = hx._num(_cell(row, used["수량"]))
|
qty = hx._num(_cell(row, used["수량"]))
|
||||||
if not unit and not any(v for v in (*price.values(), *amount.values())):
|
if not unit and not any(v for v in (*price.values(), *amount.values())):
|
||||||
continue # 근거 줄 「건설표준품셈 | 6-7-2 | 0」
|
continue # 근거 줄 「건설표준품셈 | 6-7-2 | 0」
|
||||||
if not hopyos: # 첫 줄 수량 없음 = 호표 머리(「제N호표」 칸 없이 복사)
|
if not hopyos: # 첫 줄 수량 없음 + 금액 있음 = 엑셀꼴 호표 머리(「제N호표」 칸 없이 복사)
|
||||||
open_hopyo(0, row if qty is None else None)
|
head_row = qty is None and any(amount.values())
|
||||||
if qty is None:
|
open_hopyo(0, row if head_row else None)
|
||||||
|
if head_row:
|
||||||
continue
|
continue
|
||||||
remark = _cell(row, note)
|
remark = _cell(row, note)
|
||||||
ref = hx._REF.match(_flat(remark))
|
ref = hx._REF.match(_flat(remark))
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ export async function openRelink(src: RelinkSource): Promise<void> {
|
|||||||
const load = async (): Promise<void> => {
|
const load = async (): Promise<void> => {
|
||||||
rows = await src.rows();
|
rows = await src.rows();
|
||||||
pick = rows.map((r) => (r.후보.length && !linked(r) ? 0 : r.후보.length));
|
pick = rows.map((r) => (r.후보.length && !linked(r) ? 0 : r.후보.length));
|
||||||
on = rows.map((r) => !!r.후보.length && !linked(r));
|
// 저절로 고름 = 후보 하나 · 붙인 단가 있음(별도계상 0 원 · 후보 여럿은 사람이 고름)
|
||||||
|
on = rows.map((r) => r.후보.length === 1 && !linked(r) && r.붙인[COST[r.후보[0].비목]] > 0);
|
||||||
};
|
};
|
||||||
const choice = (i: number): RelinkRow["후보"][number] | "keep" | "free" => {
|
const choice = (i: number): RelinkRow["후보"][number] | "keep" | "free" => {
|
||||||
const r = rows[i];
|
const r = rows[i];
|
||||||
@@ -146,19 +147,19 @@ export async function openRelink(src: RelinkSource): Promise<void> {
|
|||||||
});
|
});
|
||||||
const table = createDataTable({
|
const table = createDataTable({
|
||||||
columns: [
|
columns: [
|
||||||
{ key: "name", label: "품명 · 규격", width: 220 },
|
{ key: "name", label: "품명 · 규격", width: 190 },
|
||||||
{ key: "unit", label: "단위", width: 48 },
|
{ key: "pick", label: "고름", width: 44 },
|
||||||
{ key: "qty", label: "수량", kind: "number", decimals: 4, trim: true, width: 70 },
|
{ key: "unit", label: "단위", width: 44 },
|
||||||
{ key: "pasted", label: "붙인 단가", kind: "number", decimals: 1, width: 90 },
|
{ key: "qty", label: "수량", kind: "number", decimals: 4, trim: true, width: 60 },
|
||||||
{ key: "now", label: src.priceLabel, width: 100 },
|
{ key: "pasted", label: "붙인 단가", kind: "number", decimals: 1, width: 84 },
|
||||||
{ key: "cand", label: "이음 후보 (누름 = 다음)", width: 260 },
|
{ key: "now", label: src.priceLabel, width: 84 },
|
||||||
{ key: "price", label: "후보 단가", kind: "number", decimals: 1, width: 90 },
|
{ key: "cand", label: "이음 후보 (누름 = 다음)", width: 210 },
|
||||||
{ key: "gap", label: "금액 차이", kind: "number", decimals: 1, width: 90 },
|
{ key: "price", label: "후보 단가", kind: "number", decimals: 1, width: 84 },
|
||||||
{ key: "pick", label: "고름", width: 48 },
|
{ key: "gap", label: "금액 차이", kind: "number", decimals: 1, width: 84 },
|
||||||
],
|
],
|
||||||
rows: data,
|
rows: data,
|
||||||
frozen: 1,
|
frozen: 1,
|
||||||
empty: "고정 줄 없음",
|
empty: "",
|
||||||
onCellClick: (i, key) => {
|
onCellClick: (i, key) => {
|
||||||
const r = rows[i];
|
const r = rows[i];
|
||||||
if (key === "cand") {
|
if (key === "cand") {
|
||||||
|
|||||||
@@ -334,7 +334,9 @@ def test_구조물_조합으로_규격_묶음_울진_돌기슭막이(client):
|
|||||||
if h["번"] in (17, 24, 25)
|
if h["번"] in (17, 24, 25)
|
||||||
}
|
}
|
||||||
for no, c in ((17, "1.5"), (24, "2.0"), (25, "2.5")):
|
for no, c in ((17, "1.5"), (24, "2.0"), (25, "2.5")):
|
||||||
assert abs(_total(doc, c) - excel[no]) <= excel[no] * 1e-4 # 열마다 = 그 호표 엑셀 합계(끝수 차)
|
assert (
|
||||||
|
abs(_total(doc, c) - excel[no]) <= excel[no] * 1e-4
|
||||||
|
) # 열마다 = 그 호표 엑셀 합계(끝수 차)
|
||||||
one = client.post("/api/m02/layers/personal/hopyo-item", json={**body, "묶음": []}).json()
|
one = client.post("/api/m02/layers/personal/hopyo-item", json={**body, "묶음": []}).json()
|
||||||
single, _ = _item_doc(one["열"])
|
single, _ = _item_doc(one["열"])
|
||||||
assert not single.get("규격") and one["이름"].endswith("H=1.5 L3=45cm")
|
assert not single.get("규격") and one["이름"].endswith("H=1.5 L3=45cm")
|
||||||
@@ -357,3 +359,28 @@ def test_규격_열_이름():
|
|||||||
assert item.columns(["H=1.5 L3=45cm", "H=2.0 L3=45cm"]) == ("H", ["1.5", "2.0"])
|
assert item.columns(["H=1.5 L3=45cm", "H=2.0 L3=45cm"]) == ("H", ["1.5", "2.0"])
|
||||||
assert item.columns(["일반형", "ㄷ형"]) == ("규격", ["일반형", "ㄷ형"])
|
assert item.columns(["일반형", "ㄷ형"]) == ("규격", ["일반형", "ㄷ형"])
|
||||||
assert item.columns(["D800", "D1000"]) == ("D", ["800", "1000"])
|
assert item.columns(["D800", "D1000"]) == ("D", ["800", "1000"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_줄만_첫_줄_수량_없음_별도계산은_줄():
|
||||||
|
rows = _block(_rows("거창"), 2)[
|
||||||
|
1:
|
||||||
|
] # 「제2호표」 머리 줄 빼고 — 첫 줄 레미콘(별도계산 · 수량 없음)
|
||||||
|
got = hp.read_text(
|
||||||
|
_text(rows),
|
||||||
|
{"품명": 1, "규격": 2, "수량": 3, "단위": 4, "금액": 6, "노": 7, "재": 9, "경": 11},
|
||||||
|
)
|
||||||
|
(one,) = got["호표"]
|
||||||
|
assert one["번"] == 0 and one["이름"] == "붙여넣기" and one["줄"][0]["이름"] == "레미콘"
|
||||||
|
assert len(one["줄"]) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_기억한_열_짝_한_칸_밀림_맞춤():
|
||||||
|
stm = {"품명": 0, "규격": 1, "수량": 2, "단위": 3, "금액": 5, "노": 6, "재": 8, "경": 10}
|
||||||
|
rows = _block(_rows("거창"), 2)[1:] # 엑셀꼴(앞 호표 칸) 줄만 — STmate 꼴로 기억한 짝
|
||||||
|
got = hp.read_text(_text(rows), stm)
|
||||||
|
assert got["열"]["품명"] == 1 and [x["이름"] for x in got["호표"][0]["줄"]][:2] == [
|
||||||
|
"레미콘",
|
||||||
|
"콘크리트공",
|
||||||
|
]
|
||||||
|
same = hp.read_text(_text(_block(_rows("대흥"), 17)), stm)
|
||||||
|
assert same["열"] == stm
|
||||||
|
|||||||
Reference in New Issue
Block a user