- 첫 줄이 수량 없음이어도 금액 없으면(레미콘 별도계산) 줄 — 머리는 금액 있을 때만 - 기억한 열 짝을 다른 꼴(엑셀꼴 호표 칸 · STmate 꼴)에 붙이면 서버가 한 칸 밀어 맞춤 - 다시 잇기 저절로 고름 = 후보 하나 · 붙인 단가 있음만(별도계상 0 원 · 후보 여럿은 사람이 고름) · 빈 칸 글 없앰 · 고름 칸 앞으로 · 칸 폭 1100 안 - B09 알림에서 연 다시 잇기가 붙이기 전 캐시를 읽던 흠 — 붙인 뒤 캐시를 들고 감 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
387 lines
18 KiB
Python
387 lines
18 KiB
Python
"""705-2 엑셀 붙여넣기 — 붙인 글(탭 · 줄)을 서버가 읽어 고정 줄 · 호표마다 사전조합(요구서 3장 10 · 5-A ⑤-2).
|
|
|
|
- 실무 xlsx 넷(거창 · 영월 · 울진 대흥 · 소광) 일위대가 시트를 엑셀 보이는 글(쉼표 · 0 빈칸)로 복사한 꼴
|
|
→ 시트 통째 = 파일 올리기와 호표 · 줄 · 합계 같음 · 만듦 = 엑셀 합계 = 우리 합계
|
|
- 호표 하나 · 머리 글 없음(열 짝 짐작 + 물음) · 기억한 열 짝 · 줄만(열린 문서) · 합계 · 근거 줄 거름 · % 줄
|
|
층은 임시 폴더(공용 storage 안 씀).
|
|
"""
|
|
|
|
import json
|
|
import warnings
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import pytest
|
|
|
|
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
|
from M02_MasterTemplete import M02_Hopyo_Paste as hp
|
|
|
|
# ruff: noqa: F811
|
|
from resources.tester.test_m02_hopyo_excel import client, master_hash # noqa: F401
|
|
|
|
URL = "/api/m02/layers/personal/hopyo-paste"
|
|
DOCS = Path(__file__).resolve().parents[2] / "resources" / "knowledge" / "original" / "실무문서"
|
|
FILES = { # 이름 · 엑셀 합계 = 우리 합계 수(영월 23 — 붙인 글엔 하위 호표 코드가 없어 엑셀 값 그대로 · 파일은 22)
|
|
"거창": (
|
|
"01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
|
|
14,
|
|
),
|
|
"영월": ("05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", 23),
|
|
"대흥": (
|
|
"2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx",
|
|
34,
|
|
),
|
|
"소광": (
|
|
"25년 산불진화임도(기번8)(울진소광)/"
|
|
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
|
25,
|
|
),
|
|
}
|
|
|
|
|
|
def _shown(v):
|
|
"""엑셀이 클립보드에 내는 글 — 천 단위 쉼표 · 0 = 빈칸(STmate 표시꼴) · 줄바꿈 = 띄움."""
|
|
if v is None or v == 0:
|
|
return ""
|
|
if isinstance(v, (int, float)):
|
|
return f"{int(v):,}" if float(v).is_integer() else f"{v:,.6f}".rstrip("0").rstrip(".")
|
|
return str(v).replace("\n", " ")
|
|
|
|
|
|
def _rows(tag):
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
book = openpyxl.load_workbook(DOCS / FILES[tag][0], read_only=True, data_only=True)
|
|
sheet = "일위대가" if "일위대가" in book.sheetnames else "일위대가표"
|
|
first = 1 if sheet == "일위대가" else 0 # 엑셀꼴 = B 열(호표)부터 · STmate = A 열부터
|
|
width = 14 if first else 13
|
|
return [
|
|
[_shown(v) for v in list(r)[first : first + width]]
|
|
for r in book[sheet].iter_rows(values_only=True)
|
|
]
|
|
|
|
|
|
def _text(rows):
|
|
return "\n".join("\t".join(r) for r in rows)
|
|
|
|
|
|
def _paste(client, text, **body):
|
|
res = client.post(URL, json={"글": text, **body})
|
|
assert res.status_code == 200, res.text
|
|
return res.json()
|
|
|
|
|
|
@pytest.mark.parametrize("tag", list(FILES))
|
|
def test_시트_통째_붙여넣기_파일과_같음(tag):
|
|
got = hp.read_text(_text(_rows(tag)))
|
|
book = hx.read_book(FILES[tag][0], (DOCS / FILES[tag][0]).read_bytes())
|
|
assert got["물음"] is False # 머리 글이 붙어 옴 = 열 짝 저절로
|
|
key = lambda h: (h["번"], h["이름"], h["규격"], h["단위"], h["금액"].get("계"), len(h["줄"])) # noqa: E731
|
|
assert [key(h) for h in got["호표"]] == [key(h) for h in book["호표"]]
|
|
zero = lambda m: {k: round(v or 0, 4) for k, v in m.items()} # noqa: E731 · 엑셀 0 = 빈칸으로 옴
|
|
line = lambda x: (x["이름"], x["수량"] or 0, x["단위"], zero(x["단가"]), zero(x["금액"])) # noqa: E731
|
|
assert [line(x) for h in got["호표"] for x in h["줄"]] == [
|
|
line(x) for h in book["호표"] for x in h["줄"]
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("tag", list(FILES))
|
|
def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag):
|
|
before = master_hash()
|
|
got = _paste(client, _text(_rows(tag)))
|
|
assert got["갈래"] == "만듦" and got["빠짐"] == 0 and got["실패"] == []
|
|
assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"])
|
|
assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"]
|
|
assert master_hash() == before
|
|
rows = client.get("/api/m02/layers/personal/hopyo").json()["줄"]
|
|
assert rows[0]["파일"].startswith("붙여넣기") and rows[0]["시트"] == "붙여넣기"
|
|
|
|
|
|
def _block(rows, no):
|
|
"""호표 하나 줄 — 「제N호표」 줄부터 다음 호표 앞까지(머리 글 없이)."""
|
|
at = next(i for i, r in enumerate(rows) if any(hx._HOPYO.fullmatch(c.replace(" ", "")) and int(hx._HOPYO.fullmatch(c.replace(" ", "")).group(1)) == no for c in r)) # fmt: skip
|
|
end = next((i for i in range(at + 1, len(rows)) if any(hx._HOPYO.fullmatch(c.replace(" ", "")) for c in rows[i])), len(rows)) # fmt: skip
|
|
return rows[at:end]
|
|
|
|
|
|
def test_호표_하나_머리_없음_물음_뒤_기억한_열_짝(client):
|
|
rows = _block(_rows("대흥"), 17) # 돌기슭막이(찰쌓기) H=1.5
|
|
ask = _paste(client, _text(rows))
|
|
assert ask["갈래"] == "물음" and ask["짐작"] is True and ask["보기"]
|
|
cols = ask["열"]
|
|
assert (cols["품명"], cols["수량"], cols["단위"], cols["노"]) == (0, 2, 3, 6)
|
|
got = _paste(client, _text(rows), 열=cols) # 화면이 한 번 묻고 기억한 열 짝
|
|
assert got["갈래"] == "만듦" and got["호표"] == 1 and got["같음"] == 1
|
|
assert got["만듦"][0]["이름"].startswith("돌기슭막이")
|
|
|
|
|
|
def test_엑셀꼴_호표_여럿_머리_없음(client):
|
|
rows = _rows("거창")
|
|
some = _block(rows, 2) + _block(rows, 3)
|
|
cols = hp.read_text(_text(some))["열"]
|
|
assert cols["품명"] == 1 # 「제N호표」 칸이 따로 = 그 다음 칸이 품명
|
|
got = _paste(client, _text(some), 열=cols)
|
|
assert (got["호표"], got["같음"]) == (2, 2)
|
|
|
|
|
|
def test_줄만_고정_줄_합계_근거_거름():
|
|
text = "\n".join(
|
|
[
|
|
"석공\t\t0.08\t인\t268,908\t21,512.6\t268,908\t21,512.6\t\t\t\t\t",
|
|
"보통인부\t\t0.04\t인\t169,804\t6,792.1\t169,804\t6,792.1\t\t\t\t\t",
|
|
"공구손료\t노무비의\t3\t%\t28,304.7\t849.1\t\t\t\t\t849.1\t849.1\t",
|
|
"건설표준품셈\t6-7-2\t\t\t\t\t\t\t\t\t\t\t",
|
|
"합 계\t\t\t\t\t29,153\t\t28,304\t\t\t\t849\t",
|
|
]
|
|
)
|
|
cols = {"품명": 0, "규격": 1, "수량": 2, "단위": 3, "금액": 5, "노": 6, "재": 8, "경": 10}
|
|
parsed = hp.read_text(text, cols)
|
|
(one,) = parsed["호표"]
|
|
assert [x["이름"] for x in one["줄"]] == ["석공", "보통인부", "공구손료"] # 근거 · 합계 줄 뺌
|
|
assert one["금액"]["계"] == 29153 and one["줄"][0]["수량"] == 0.08
|
|
out = hp.fixed_lines(parsed, {"고정|석공||인"})
|
|
names = [x["고정"]["이름"] for x in out["줄"]]
|
|
assert names == ["석공 (2)", "보통인부", "공구손료"] # 있는 줄 가름 피함
|
|
assert out["줄"][2]["고정"]["비율"] == {"기준": "노무비", "비목": "경비"}
|
|
assert out["줄"][0]["고정"]["단가"]["노무비"] == 268908 and out["엑셀"] == 29153
|
|
|
|
|
|
def test_줄만_길_저장_없음(client):
|
|
text = "석공\t\t0.08\t인\t268,908\t21,512.6\t268,908\t21,512.6"
|
|
cols = {"품명": 0, "규격": 1, "수량": 2, "단위": 3, "금액": 5, "노": 6, "재": 8, "경": 10}
|
|
got = _paste(client, text, 열=cols, 줄만=True)
|
|
assert got["갈래"] == "줄" and len(got["줄"]) == 1 and got["줄"][0]["수량"] == {"손": 0.08}
|
|
assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("text", "cols", "word"),
|
|
[
|
|
("", None, "빔"),
|
|
("가\t나", {"품명": 0, "수량": 0, "단위": 1}, "같은 칸"),
|
|
("가\t나", {"품명": 0, "규격": 1}, "꼭"),
|
|
("합계\t\t\t\t1,000", {"품명": 0, "수량": 2, "단위": 3, "금액": 4}, "줄이 없음"),
|
|
],
|
|
)
|
|
def test_틀린_글_400(client, text, cols, word):
|
|
res = client.post(URL, json={"글": text, **({"열": cols} if cols else {})})
|
|
assert res.status_code == 400 and word in res.json()["detail"]
|
|
|
|
|
|
def test_쉼표_빈칸_수():
|
|
assert hx._num("1,415,169") == Decimal("1415169") and hx._num("") is None
|
|
|
|
|
|
# ── 이 공사 단가로 다시 잇기 ──────────────────────────────────────────────────
|
|
RELINK = "/api/m02/layers/personal/hopyo-relink"
|
|
|
|
|
|
def test_다시_잇기_후보_바꿈_되돌리기(client):
|
|
from resources.tester.test_m02_hopyo_excel import _post
|
|
|
|
assert _post(client).status_code == 200 # 북부청 — 이음 13 · 이음 후보 31(705-3)
|
|
made = client.post(
|
|
"/api/m02/layers/personal/hopyo-build-all", json={"층": "personal", "키": "HP-000001"}
|
|
).json()
|
|
keys = [x["키"] for x in made["만듦"]]
|
|
assert len(keys) == 68
|
|
rows = client.post(f"{RELINK}/rows", json={"키": keys}).json()["줄"]
|
|
near = [r for r in rows if r["후보"] and not any(r["이음"].values())]
|
|
assert near, "단가 다른 이음 후보 줄"
|
|
linked = [r for r in rows if any(r["이음"].values())]
|
|
assert len(linked) >= 13 and all(r["지금"] for r in linked)
|
|
one = near[0]
|
|
pick = one["후보"][0]
|
|
change = {"키": one["키"], "줄": one["줄"], pick["비목"]: pick["키"]}
|
|
got = client.post(RELINK, json={"바꿈": [change]})
|
|
assert got.status_code == 200, got.text
|
|
done = got.json()
|
|
(doc,) = done["문서"]
|
|
assert done["바뀜"] == 1 and doc["전"] != doc["후"] # 붙인 단가 → 단가 책 값
|
|
again = client.post(f"{RELINK}/rows", json={"키": [one["키"]]}).json()["줄"]
|
|
assert next(r for r in again if r["줄"] == one["줄"])["이음"][pick["비목"]] == pick["키"]
|
|
back = client.post(RELINK, json={"바꿈": done["되돌림"]}).json() # 되돌리기
|
|
assert back["문서"][0]["후"] == doc["전"]
|
|
bad = {"키": one["키"], "줄": one["줄"], "노임": "LB999999"}
|
|
assert client.post(RELINK, json={"바꿈": [bad]}).status_code == 400
|
|
assert client.post(RELINK, json={"바꿈": [{**change, "줄": "고정|없음||"}]}).status_code == 404
|
|
num = client.post(RELINK, json={"바꿈": [change], "금액": 1})
|
|
assert num.status_code == 422 # 숫자 칸 안 받음
|
|
|
|
|
|
def test_다시_잇기_열린_문서_후보만(client):
|
|
fx = {"이름": "보통인부", "규격": "", "단위": "인", "단가": {"노무비": 1}, "이음": {}}
|
|
doc = {"담은로직": [{"고정": fx, "수량": {"손": 0.5}}]}
|
|
(row,) = client.post(f"{RELINK}/rows", json={"문서": doc}).json()["줄"]
|
|
assert row["키"] is None and row["후보"][0]["비목"] == "노임" and row["후보"][0]["단가"] > 1
|
|
|
|
|
|
# ── B09 펼친 일위대가 붙여넣기(덮개 @add) ─────────────────────────────────────
|
|
def _b09():
|
|
from resources.tester import test_145_2_b09_labor_calc as b09
|
|
|
|
return b09
|
|
|
|
|
|
def _added(user):
|
|
from B09_Estimation import B09_Estimation_Daega_Paste as dp
|
|
|
|
text = "\n".join(
|
|
[
|
|
"석공\t\t0.08\t인\t258,935\t20,714.8\t258,935\t20,714.8\t\t\t\t\t",
|
|
"잡재료\t\t1\t식\t1,000\t1,000\t\t\t1,000\t1,000\t\t\t",
|
|
"공구손료\t노무비의\t3\t%\t20,714.8\t621.4\t\t\t\t\t621.4\t621.4\t",
|
|
]
|
|
)
|
|
cols = {"품명": 0, "규격": 1, "수량": 2, "단위": 3, "금액": 5, "노": 6, "재": 8, "경": 10}
|
|
return dp.paste(user, "C:DG9", text, cols)
|
|
|
|
|
|
def test_B09_붙인_줄_거울_정리_수요():
|
|
from B09_Estimation.B09_Estimation_Calc_Labor import ADD, added_keys, compute_labor
|
|
from B09_Estimation.B09_Estimation_Store import clean_user
|
|
|
|
b09 = _b09()
|
|
got = _added({})
|
|
assert got["갈래"] == "줄" and got["줄"] == 3 and got["엑셀"] == 22335
|
|
cover = got["user"]["daega_cover"]
|
|
lines = cover["C:DG9"][ADD]
|
|
assert [ln["kind"] for ln in lines] == ["fixed", "fixed", "pct"]
|
|
assert all(ln["cid"].startswith("@add|고정|") for ln in lines)
|
|
again = _added(got["user"]) # 같은 글 두 번 = 이름 「 (2)」 가름 · 줄 더함
|
|
names = [ln["name"] for ln in again["user"]["daega_cover"]["C:DG9"][ADD]]
|
|
assert names[3:] == ["석공 (2)", "잡재료 (2)", "공구손료 (2)"]
|
|
assert clean_user(again["user"]) == again["user"] # 저장 정리 그대로
|
|
bad = clean_user(
|
|
{"daega_cover": {"C:x": {ADD: [{"cid": "x", "kind": "fixed"}, {"kind": "?"}]}}}
|
|
)
|
|
assert "daega_cover" not in bad # cid 머리 틀림 · 모르는 갈래 버림
|
|
linked = {**lines[0], "link": {"labor": "L00005", "material": None, "machine": None}}
|
|
cover = {"C:DG9": {ADD: [linked, *lines[1:]], linked["cid"]: {"qty": 0.1}}}
|
|
assert added_keys(cover) == ["L00005"]
|
|
blocks = json.loads(json.dumps(b09.EXCEL))
|
|
for b in blocks["blocks"]:
|
|
b["cover"] = f"C:{b['id']}"
|
|
for n, ln in enumerate(b["lines"]):
|
|
ln["cid"] = f"{ln['kind']}:{ln.get('key')}#{n}"
|
|
py = json.loads(json.dumps(compute_labor(blocks, b09.SHEETS, b09.SANGEUN, cover)))
|
|
ts = b09._ts(blocks, b09.SHEETS, b09.SANGEUN, cover)
|
|
assert b09._same(py, ts["out"])
|
|
dg9 = b09._block(py, "DG9")
|
|
stone = next(ln for ln in dg9["lines"] if ln["cid"] == linked["cid"])
|
|
assert stone["prices"]["labor"] == 258935 and stone["qty"] == 0.1 # 이은 단가 · 덮개
|
|
pct = next(ln for ln in dg9["lines"] if ln["kind"] == "pct" and ln["cid"].startswith(ADD))
|
|
assert pct["amounts"]["expense"] > 0 and py["daega_orphans"] == []
|
|
|
|
|
|
# ── 704-4 뒷부분 — [구조물 조합으로] · 규격 묶음 ─────────────────────────────────
|
|
def _upload(client, tag):
|
|
from resources.tester.test_m02_hopyo_excel import _post
|
|
|
|
path = DOCS / FILES[tag][0]
|
|
res = _post(client, data=path.read_bytes(), name=path.name)
|
|
assert res.status_code == 200, res.text
|
|
return res.json()["키"]
|
|
|
|
|
|
def _item_doc(column):
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
|
|
with store.at(layers.personal_dir(7, 42)):
|
|
table = store.read("table", store.STRUCTURE_TABLE)["문서"]
|
|
col = next(c for c in table["열"] if c.get("id") == column)
|
|
return store.read("labor", column)["문서"], col
|
|
|
|
|
|
def _total(doc, col=None):
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
from M02_MasterTemplete import M02_Labor_Spec as spec
|
|
|
|
doc = {**doc, "규격": {**doc["규격"], "기본": col}} if col else doc # 셈 = 기본 열
|
|
return labor.calc(labor.clean(spec.at(doc, col)), {})["반영합"]["계"]
|
|
|
|
|
|
def layers_dir():
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
|
|
return layers.personal_dir(7, 42)
|
|
|
|
|
|
def test_구조물_조합으로_규격_묶음_울진_돌기슭막이(client):
|
|
before = master_hash()
|
|
key = _upload(client, "대흥")
|
|
sib = client.get(f"/api/m02/layers/personal/hopyo/{key}/17/siblings").json()
|
|
assert sib["이름"] == "H" and [(r["번"], r["열"]) for r in sib["묶음"]] == [
|
|
(17, "1.5"), (24, "2.0"), (25, "2.5")
|
|
] # fmt: skip
|
|
body = {"층": "personal", "키": key, "번": 17, "묶음": [24, 25]}
|
|
res = client.post("/api/m02/layers/personal/hopyo-item", json=body)
|
|
assert res.status_code == 200, res.text
|
|
got = res.json()
|
|
assert got["열수"] == 3 and got["이름"].endswith("돌기슭막이(찰쌓기,기초무,구입)")
|
|
doc, col = _item_doc(got["열"])
|
|
assert col["경로"] == ["실무자료", "돌기슭막이(찰쌓기,기초무,구입)"] and doc["규격"]["열"] == [
|
|
"1.5",
|
|
"2.0",
|
|
"2.5",
|
|
]
|
|
assert doc["규격"]["기본"] == "1.5" and all("고정" in x for x in doc["담은로직"])
|
|
excel = {
|
|
h["번"]: h["금액"]["계"]
|
|
for h in client.get(f"/api/m02/layers/personal/hopyo/{key}").json()["호표"]
|
|
if h["번"] in (17, 24, 25)
|
|
}
|
|
for no, c in ((17, "1.5"), (24, "2.0"), (25, "2.5")):
|
|
assert (
|
|
abs(_total(doc, c) - excel[no]) <= excel[no] * 1e-4
|
|
) # 열마다 = 그 호표 엑셀 합계(끝수 차)
|
|
one = client.post("/api/m02/layers/personal/hopyo-item", json={**body, "묶음": []}).json()
|
|
single, _ = _item_doc(one["열"])
|
|
assert not single.get("규격") and one["이름"].endswith("H=1.5 L3=45cm")
|
|
bad = client.post("/api/m02/layers/personal/hopyo-item", json={**body, "묶음": [3]})
|
|
assert bad.status_code == 400 # 줄 구성 다른 호표
|
|
precombo = layers_dir() / "precombo"
|
|
assert not list(precombo.glob("SC-*.json")) # 사전조합 문서 안 만듦
|
|
assert master_hash() == before
|
|
|
|
|
|
def test_구조물_조합으로_남의_층_403(client):
|
|
key = _upload(client, "대흥")
|
|
body = {"층": "personal", "키": key, "번": 17}
|
|
assert client.post("/api/m02/layers/company/hopyo-item", json=body).status_code == 403
|
|
|
|
|
|
def test_규격_열_이름():
|
|
from M02_MasterTemplete import M02_Hopyo_Item as item
|
|
|
|
assert item.columns(["H=1.5 L3=45cm", "H=2.0 L3=45cm"]) == ("H", ["1.5", "2.0"])
|
|
assert item.columns(["일반형", "ㄷ형"]) == ("규격", ["일반형", "ㄷ형"])
|
|
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
|