Merge remote-tracking branch 'origin/dev' into sub_laptop_1
This commit is contained in:
@@ -278,6 +278,29 @@ def _columns(kind: str, all_rows: list[dict[str, Any]]) -> list[str]:
|
||||
return list(keys)
|
||||
|
||||
|
||||
class SortError(ValueError):
|
||||
"""모르는 열로 세우라 함 — 조용히 원본 차례로 돌아가지 않음(브레인 ④)."""
|
||||
|
||||
|
||||
def _order_key(value: Any) -> tuple:
|
||||
"""섞인 칸도 세움 — 수 먼저, 그 뒤 글자."""
|
||||
if isinstance(value, bool):
|
||||
return (1, str(value))
|
||||
if isinstance(value, (int, float)):
|
||||
return (0, float(value))
|
||||
if isinstance(value, (dict, list)):
|
||||
return (1, json.dumps(value, ensure_ascii=False, default=str))
|
||||
return (1, str(value))
|
||||
|
||||
|
||||
def _sorted(rows: list[dict[str, Any]], column: str, desc: bool) -> list[dict[str, Any]]:
|
||||
"""⚠ 빈 칸은 오름·내림 **어느 쪽이든 끝** — 미공표 노임 14 · 손료계수 없는 기계 226 이
|
||||
첫 쪽을 통째로 채우면 표를 못 씀(브레인 ②)."""
|
||||
filled = [r for r in rows if r.get(column) not in (None, "")]
|
||||
empty = [r for r in rows if r.get(column) in (None, "")]
|
||||
return sorted(filled, key=lambda r: _order_key(r[column]), reverse=desc) + empty
|
||||
|
||||
|
||||
def notice(kind: str) -> list[str]:
|
||||
"""표 수준 알림 — 화면이 따로 가지면 문구가 두 벌이 됨(브레인 ③)."""
|
||||
return [_SCOPE_NOTICE] + ([_RATE_NOTICE] if kind == "rate" else [])
|
||||
@@ -349,7 +372,12 @@ def public(row: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def table(
|
||||
kind: str, page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE, q: str = ""
|
||||
kind: str,
|
||||
page: int = 1,
|
||||
size: int = tables.DEFAULT_PAGE_SIZE,
|
||||
q: str = "",
|
||||
sort: str = "",
|
||||
desc: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
all_rows = rows(kind)
|
||||
columns = _columns(kind, all_rows)
|
||||
@@ -363,10 +391,16 @@ def table(
|
||||
if needle
|
||||
else all_rows
|
||||
)
|
||||
if sort: # ⚠ 거른 뒤에 세움 — 거꾸로 하면 차례가 깨짐(브레인 ③)
|
||||
if sort not in columns:
|
||||
raise SortError(f"없는 열로 세울 수 없음: {sort}")
|
||||
hits = _sorted(hits, sort, desc)
|
||||
size = max(1, min(size, tables.MAX_PAGE_SIZE))
|
||||
start = (max(page, 1) - 1) * size
|
||||
return {
|
||||
"columns": [column_meta(kind, c) for c in columns],
|
||||
# 못 고치는 칸(계산값)도 **세우기는 됨** — 화면은 이 목록으로 눌릴 제목을 가림
|
||||
"sortable": list(columns),
|
||||
"rows": [public(r) for r in hits[start : start + size]],
|
||||
"total": len(hits),
|
||||
**spec(kind, columns),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
GET /api/master-data/tree 갈래 → 파일 → 표
|
||||
GET /api/master-data/rows?file=&table=&page=&size=&q= 표 줄(쪽 나누기 · 검색)
|
||||
GET /api/master-data/base-prices/{kind}?page=&size=&q= 기초단가 한 표(labor|machine|material|oil|rate)
|
||||
GET /api/master-data/base-prices/{kind}?page=&size=&q=&sort=&desc= 기초단가 한 표(labor|machine|material|oil|rate)
|
||||
PUT /api/master-data/base-prices/{kind}/{row_id} {values:{열:값}} → 덮개에만 씀 · null = 되돌리기
|
||||
GET /api/master-data/overrides 고친 것·원본 바뀐 것·주인 없는 것(서버 정렬)
|
||||
⚠ 권한은 등록하는 쪽(`main.py` · 랩탑 서브)이 `dependencies=[verify_session, require_system_admin]` 로 붙임.
|
||||
@@ -50,9 +50,17 @@ def _kind(kind: str) -> str:
|
||||
|
||||
@router.get("/base-prices/{kind}")
|
||||
def get_base_prices(
|
||||
kind: str, page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE, q: str = ""
|
||||
kind: str,
|
||||
page: int = 1,
|
||||
size: int = tables.DEFAULT_PAGE_SIZE,
|
||||
q: str = "",
|
||||
sort: str = "",
|
||||
desc: int = 0,
|
||||
) -> dict:
|
||||
return base_prices.table(_kind(kind), page=page, size=size, q=q)
|
||||
try:
|
||||
return base_prices.table(_kind(kind), page=page, size=size, q=q, sort=sort, desc=bool(desc))
|
||||
except base_prices.SortError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
|
||||
|
||||
@router.put("/base-prices/{kind}/{row_id:path}")
|
||||
|
||||
@@ -496,3 +496,55 @@ def test_열_이름은_이름표에서만_옴(client: TestClient) -> None:
|
||||
assert not [c for c in _get(client, kind, size=1)["columns"] if c["label"] == c["key"]], (
|
||||
kind
|
||||
)
|
||||
|
||||
|
||||
def _sorted_rows(client: TestClient, kind: str, column: str, desc: int = 0, **params) -> list:
|
||||
table = _get(client, kind, sort=column, desc=desc, size=500, **params)
|
||||
return [r.get(column) for r in table["rows"]]
|
||||
|
||||
|
||||
def test_차례_세우기는_서버가_전체_줄로(client: TestClient) -> None:
|
||||
"""브레인 — 자재 140 쪽이라 화면이 받은 50 줄만 세우면 1 쪽 안에서만 맞는 가짜 차례."""
|
||||
table = _get(client, "material", size=1)
|
||||
assert set(table["sortable"]) == {c["key"] for c in table["columns"]}
|
||||
first = _get(client, "material", sort="price_krw", size=1)
|
||||
last = _get(client, "material", sort="price_krw", desc=1, size=1)
|
||||
assert first["total"] == last["total"] == 6999
|
||||
assert first["rows"][0]["price_krw"] < last["rows"][0]["price_krw"]
|
||||
page2 = _get(client, "material", sort="price_krw", size=50, page=2)["rows"]
|
||||
assert first["rows"][0]["price_krw"] <= page2[0]["price_krw"] # 쪽을 넘어도 이어짐
|
||||
values = _sorted_rows(client, "material", "price_krw")
|
||||
assert values == sorted(values)
|
||||
|
||||
|
||||
def test_거른_뒤에_세움_모르는_열은_거절(client: TestClient) -> None:
|
||||
hits = _get(client, "material", q="육각볼트", sort="price_krw", desc=1, size=500)
|
||||
values = [r["price_krw"] for r in hits["rows"]]
|
||||
assert values == sorted(values, reverse=True) and hits["total"] == len(values)
|
||||
assert all("육각볼트" in json.dumps(r, ensure_ascii=False) for r in hits["rows"])
|
||||
bad = client.get("/api/master-data/base-prices/material", params={"sort": "없는열"})
|
||||
assert bad.status_code == 400 and "없는열" in bad.text # 조용히 원본 차례로 안 돌아감
|
||||
|
||||
|
||||
def test_빈_칸은_어느_쪽으로_세워도_끝(client: TestClient) -> None:
|
||||
"""미공표 노임 14 · 손료계수 없는 기계 226 이 첫 쪽을 채우면 표를 못 씀."""
|
||||
for desc in (0, 1):
|
||||
wages = _sorted_rows(client, "labor", "daily_wage_krw", desc)
|
||||
filled = [v for v in wages if v is not None]
|
||||
assert wages[: len(filled)] == sorted(filled, reverse=bool(desc))
|
||||
assert wages[len(filled) :] == [None] * (len(wages) - len(filled)) and len(filled) < len(
|
||||
wages
|
||||
)
|
||||
hourly = _sorted_rows(client, "machine", "hourly_total_krw", 1)
|
||||
assert hourly[0] is not None and hourly[-1] is None # 계산 열도 세워짐
|
||||
|
||||
|
||||
def test_덮개_얹은_값으로_세움(client: TestClient) -> None:
|
||||
_put(client, "labor", "labor_const/1003", {"daily_wage_krw": 9_000_000})
|
||||
top = _get(client, "labor", sort="daily_wage_krw", desc=1, size=1)["rows"][0]
|
||||
assert top["@id"] == "labor_const/1003" and top["daily_wage_krw"] == 9_000_000
|
||||
|
||||
|
||||
def test_sort_가_없으면_원본_차례(client: TestClient) -> None:
|
||||
assert _get(client, "rate", size=1)["rows"][0]["@id"] == "rate_sanjae"
|
||||
assert _get(client, "machine", size=1)["rows"][0]["@id"] == "0101-0007"
|
||||
|
||||
Reference in New Issue
Block a user