Merge remote-tracking branch 'origin/dev' into sub_laptop_6
# Conflicts: # A00_Common/spreadsheet/spreadsheet_grid.ts # A00_Common/spreadsheet/spreadsheet_links.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 252, "CC": 341, "GC": 1104, "QC": 2060, "GX": 1, "UA": 38, "UM": 1},
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 252, "CC": 341, "GC": 1104, "QC": 2060, "GX": 1, "UA": 38},
|
||||
"폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}},
|
||||
"키": {
|
||||
"CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
| MT | 재료 자재품목 | CF · CC | 계수 산림 · 건설 |
|
||||
| MO | 재료 유가 | RT | 요율 |
|
||||
| MP | 재료 품셈재료(old/ — PLAN 7-4b) | GF · GC · GX | 로직 산림 · 건설 · 자체 |
|
||||
| EQ | 기계 | UA · UM | 일위대가 조합 · 자재 조합 |
|
||||
| EQ | 기계 | UA | 일위대가 조합 |
|
||||
|
||||
- 빌더는 옛 모양(`열쇠` · 원문별 파일)으로 쓰고 끝에 `scripts/master_keys.py` 의 `adopt` 를 부름 — 키·원문번호·한 테이블·장 파일 이름으로 받음. 옛 이름으로 읽기는 `old_view`.
|
||||
|
||||
@@ -300,10 +300,10 @@
|
||||
|
||||
## 11. 자재 조합 파일
|
||||
|
||||
`자재조합.json` 하나 — 구조물 열 재료산출이 쓰는 **자재 목록 묶음**. 머리 `그룹` · `판` · `비고` · `줄`(원문 없음).
|
||||
마스터 템플릿 `resources/master_template/자재조합.json` 하나(이 폴더 밖 · `구조물집계표조합.json` 과 같은 자리) — 구조물 열 재료산출이 쓰는 **자재 목록 묶음**. 머리 `그룹` · `판` · `비고` · `다음` · `줄`(원문 없음). 검사는 `check_master.py 틀` 이 같이 봄(`master_material_combo.check_file`).
|
||||
|
||||
- 열 한 벌 — `키` · `이름` · `담은자재` · `본뜬키` · `소유` · `비고`(차례까지 이대로 · 엔진 `scripts/master_material_combo.py`).
|
||||
- `키` = `UM` + 6자리 — 대장 `UM` 다음 번호 · 줄에 `원문번호` 를 두지 않음.
|
||||
- `키` = `UM` + 6자리 — 표 머리 `다음` 번호(`_키대장` 밖) · 지운 키는 다시 안 씀 · 줄에 `원문번호` 를 두지 않음.
|
||||
- `담은자재` 줄 = `{코드, 이름, 규격, 단위}` 뿐 · 빈 목록 안 됨 · 같은 코드 두 번 안 됨 · 수량 · 계산은 두지 않음(재료산출 몫).
|
||||
- `본뜬키` = 갈라 낸 원래 자재 조합 키 · 없으면 `null` · `소유` = `현장` · `공용`.
|
||||
- 새 줄은 M02 갈라 내기(`POST /api/m02/structures/{열}/material-combo`)가 만듦 — 같은 자재 집합이 있으면 그 키.
|
||||
|
||||
@@ -198,19 +198,7 @@
|
||||
|
||||
### 자재 조합
|
||||
|
||||
재료산출에서 쓸 **자재 목록 묶음**(파일 `자재조합.json` · 키 `UM` · `_틀.md` 11장). 담은 자재 `{코드, 이름, 규격, 단위}` 만 지님 — 수량 · 단가 없음. 요소 그룹이 아님(`/groups` · `/rows` 에 안 끼고 제 길로만).
|
||||
|
||||
| 길 | 받음 | 줌 |
|
||||
| --------------------- | ------------------------------- | ------------------------------------------- |
|
||||
| `GET /matsets` | `q?` `owner?` | `{matsets: […]}` — 줄마다 `count` (자재 수) |
|
||||
| `GET /matset` | `key` | `{file, version, matset}` |
|
||||
| `POST /matset/new` | `{matset, owner?}` | `{file, version, key, matset}` |
|
||||
| `POST /matset/edit` | `{key, version, matset, owner?}` | `{file, version, matset}` |
|
||||
| `POST /matset/delete` | `{key, version}` | `{file, version}` |
|
||||
|
||||
- 답 — 200 · 400(이름 없음 · 모르는 소유 · 없는/자기 본뜬키) · 404 · 409(낡은 판본 · 지우기 막음) · 422(없는 자재 코드 · 같은 자재 두 번 · 자재 줄 모양).
|
||||
- 지우기는 구조물집계표 · `구조물집계표조합` 의 「자재조합」 칸으로 쓰는 열이 있으면 409 `{detail: {errors: […], users: [구조물 이름…]}}` — 지우지 않음.
|
||||
- M02 화면이 새로 만드는 조합도 같은 파일 · 같은 길이라 M01 목록에 같이 보임.
|
||||
M01 에 없음 — 마스터 템플릿 표(`resources/master_template/자재조합.json` · 키 `UM` · `_틀.md` 11장) · 길과 화면은 M02(`/api/m02/material-combos…` · 좌측 「자재 조합」 컨테이너).
|
||||
|
||||
## 10. 재료 · 기계 고르기
|
||||
|
||||
|
||||
@@ -259,8 +259,6 @@ def check_combo_form(name: str, data: dict) -> list[str]:
|
||||
def check_form(name: str, data: dict) -> list[str]:
|
||||
if name == mcb.FILE:
|
||||
return check_combo_form(name, data)
|
||||
if name == mmc.FILE:
|
||||
return mmc.check_form(name, data)
|
||||
if name in SPECIAL_FILES:
|
||||
return []
|
||||
m = _FILE.match(name)
|
||||
@@ -685,6 +683,7 @@ def main(argv: list[str]) -> int:
|
||||
report = {}
|
||||
if mode in ("틀", "전부"):
|
||||
report["틀"] = [x for name, data in files.items() for x in check_form(name, data)]
|
||||
report["틀"] += mmc.check_file() # 자재 조합은 마스터 템플릿 표
|
||||
if mode in ("본문", "전부"):
|
||||
report["본문"] = check_body(files)
|
||||
if mode in ("로직", "전부"):
|
||||
|
||||
@@ -32,13 +32,12 @@ TABLES = {
|
||||
("로직", "건설품셈"): "GC",
|
||||
("로직", "자체"): "GX",
|
||||
("일위대가조합", None): "UA",
|
||||
("자재조합", None): "UM",
|
||||
}
|
||||
GROUP_OF = {tid: group for (group, _), tid in TABLES.items()}
|
||||
IDS = tuple(GROUP_OF)
|
||||
# 줄에 원문번호를 두지 않는 테이블 — 자재품목은 번호가 남의 품번이라 이름 몫을 못 하고,
|
||||
# 조합은 원문이 없어 번호 자체가 없음(대조는 대장에만)
|
||||
NO_NUMBER = ("MT", "UA", "UM")
|
||||
NO_NUMBER = ("MT", "UA")
|
||||
|
||||
|
||||
def table_id(group: str, book: str | None) -> str:
|
||||
@@ -187,7 +186,6 @@ ROW_ORDER = {
|
||||
"소유",
|
||||
"비고",
|
||||
),
|
||||
"자재조합": ("키", "이름", "담은자재", "본뜬키", "소유", "비고"),
|
||||
}
|
||||
OLD_CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈)_(\D*)(\d+)장\.json$")
|
||||
CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈|자체)_(\d\d)장_.+\.json$")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""자재 조합 — 구조물 열 재료산출이 쓰는 자재 목록 묶음(키 UM · `_틀.md` 11장).
|
||||
|
||||
담은 자재 목록만 지님(수량 · 계산은 재료산출 몫). 일위대가 조합(`master_combo`)과 같은 결 —
|
||||
갈라 내기(M02) 때 새 줄 · 본뜬키 = 원래 키.
|
||||
자리는 마스터 템플릿 `resources/master_template/자재조합.json`(구조물집계표조합.json 과 같은 결) ·
|
||||
키는 표 안 「다음」 번호(`_키대장` 밖). 담은 자재 목록만 지님(수량 · 계산은 재료산출 몫) ·
|
||||
갈라 내기(M02) 때 새 줄 · 본뜬키 = 원래 키. 저장 · 화면은 M02(`M02_MasterTemplete_Matset.py`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import master_combo as mcb
|
||||
import master_keys as mk
|
||||
|
||||
@@ -15,7 +19,8 @@ FILE = "자재조합.json"
|
||||
TID = "UM"
|
||||
OWNERS = mcb.OWNERS
|
||||
MAX_이름 = mcb.MAX_이름
|
||||
COLS = mk.ROW_ORDER[GROUP] # 키 · 이름 · 담은자재 · 본뜬키 · 소유 · 비고
|
||||
PATH = Path(__file__).resolve().parents[2] / "master_template" / FILE
|
||||
COLS = ("키", "이름", "담은자재", "본뜬키", "소유", "비고")
|
||||
ITEM_COLS = ("코드", "이름", "규격", "단위")
|
||||
|
||||
|
||||
@@ -30,12 +35,18 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
rows = data.get("줄")
|
||||
if not isinstance(rows, list):
|
||||
return out + [f"{name} · 머리 「줄」 목록 없음"]
|
||||
after = data.get("다음")
|
||||
if not isinstance(after, int) or after < 1:
|
||||
out.append(f"{name} · 머리 「다음」 이 1 이상 정수 아님")
|
||||
after = None
|
||||
seen_names: dict[str, str] = {}
|
||||
for row in rows:
|
||||
key = str(row.get("키"))
|
||||
where = f"{name} · {key} {row.get('이름') or ''}".strip()
|
||||
if not mk.KEY.fullmatch(key) or key[:2] != TID:
|
||||
out.append(f"{where} · 키 모양이 「{TID}」 + 6자리 아님")
|
||||
elif after and int(key[2:]) >= after:
|
||||
out.append(f"{where} · 키 번호가 머리 「다음」({after}) 보다 작지 않음")
|
||||
if tuple(row) != COLS:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(COLS)}」")
|
||||
plain = row.get("이름").strip() if isinstance(row.get("이름"), str) else ""
|
||||
@@ -64,3 +75,10 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
out.append(f"{where} 줄{at} · 같은 자재를 두 번 담음 「{code}」")
|
||||
codes.add(code)
|
||||
return out
|
||||
|
||||
|
||||
def check_file(path: Path = PATH) -> list[str]:
|
||||
"""마스터 템플릿 자재 조합 표 — 없으면 한 줄."""
|
||||
if not path.is_file():
|
||||
return [f"{path.name} · 마스터 템플릿에 없음"]
|
||||
return check_form(path.name, json.loads(path.read_text(encoding="utf-8")))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"그룹": "자재조합",
|
||||
"판": "2026",
|
||||
"비고": ["자재 조합 = 구조물 열 재료산출이 쓰는 자재 목록 묶음 · 수량·계산은 두지 않음(재료산출 몫)"],
|
||||
"비고": ["자재 조합 = 구조물 열 재료산출이 쓰는 자재 목록 묶음 · 수량·계산은 두지 않음(재료산출 몫) · 키 UM 은 「다음」 번호로 매김 · 지운 키는 다시 안 씀"],
|
||||
"다음": 1,
|
||||
"줄": []
|
||||
}
|
||||
@@ -58,6 +58,31 @@ describe('기준점 십자', () => {
|
||||
expect(b.origin.x - a.origin.x).toBeCloseTo(-100); // 세계 50 = 화면 100px(2배) — 화면을 옮기면 원점이 반대로 감
|
||||
});
|
||||
|
||||
it('빈 도면 맞춤은 원점을 화면 가운데로 — 십자가 귀퉁이에 안 잘림', () => {
|
||||
const { context } = recordingContext();
|
||||
const controller = new ScreenCanvasDrawController(context);
|
||||
controller.setCanvasSize(new Point(800, 600));
|
||||
controller.setScreenScale(2);
|
||||
controller.setScreenOffset(new Point(0, 0));
|
||||
controller.zoomToFitScreen();
|
||||
const origin = controller.worldToTarget(new Point(0, 0));
|
||||
expect([origin.x, origin.y]).toEqual([400, 300]);
|
||||
});
|
||||
|
||||
it('맞춘 뒤 캔버스가 커지면 다시 맞춤 · 사용자가 옮긴 뒤에는 그대로', () => {
|
||||
const { context } = recordingContext();
|
||||
const controller = new ScreenCanvasDrawController(context);
|
||||
controller.setCanvasSize(new Point(200, 100));
|
||||
controller.zoomToFitScreen();
|
||||
controller.setCanvasSize(new Point(800, 600));
|
||||
const refit = controller.worldToTarget(new Point(0, 0));
|
||||
expect([refit.x, refit.y]).toEqual([400, 300]);
|
||||
controller.panScreen(50, 0);
|
||||
controller.setCanvasSize(new Point(1000, 600));
|
||||
const kept = controller.worldToTarget(new Point(0, 0));
|
||||
expect([kept.x, kept.y]).toEqual([450, 300]);
|
||||
});
|
||||
|
||||
it('그리기만 — 도면 개체를 만들지 않음', () => {
|
||||
drawAt(1, new Point(0, 0));
|
||||
expect(getEntities()).toHaveLength(0);
|
||||
|
||||
@@ -7,6 +7,7 @@ const sel = load(path.join(SS, "spreadsheet_selection.ts"));
|
||||
const ed = load(path.join(SS, "spreadsheet_editor.ts"));
|
||||
const mouse = load(path.join(SS, "spreadsheet_mouse.ts"));
|
||||
const addr = load(path.join(SS, "spreadsheet_address.ts"));
|
||||
const spill = load(path.join(SS, "spreadsheet_spill.ts"));
|
||||
const sample = require(path.join(SS, "spreadsheet_sample.json"));
|
||||
|
||||
const s1 = sample.시트[0];
|
||||
@@ -69,6 +70,27 @@ const out = {
|
||||
(t) => ed.textToCell(t, { 서식: 4 }),
|
||||
),
|
||||
textToCellNoStyle: ed.textToCell("", {}),
|
||||
// 긴 글 넘침 — 칸 0..5 폭 50 · 3 은 값 있음 · 칸 2 에서 폭 130 글
|
||||
spill: (() => {
|
||||
const row = (full) =>
|
||||
new Map([0, 1, 2, 3, 4, 5].map((c) => [c, { w: 50, empty: !full.includes(c) }]));
|
||||
const f = (sp) => sp && [sp.left, sp.right, sp.from, sp.to, sp.clipLeft, sp.clipRight];
|
||||
return {
|
||||
left: f(spill.spillOf(row([2, 3]), 2, 50, 130, "left")),
|
||||
leftFree: f(spill.spillOf(row([2]), 2, 50, 130, "left")),
|
||||
right: f(spill.spillOf(row([2]), 2, 50, 130, "right")),
|
||||
center: f(spill.spillOf(row([2]), 2, 50, 130, "center")),
|
||||
centerBlocked: f(spill.spillOf(row([2, 3]), 2, 50, 130, "center")),
|
||||
fits: f(spill.spillOf(row([2]), 2, 50, 40, "left")),
|
||||
edge: f(spill.spillOf(new Map([[2, { w: 50, empty: false }]]), 2, 50, 130, "left")),
|
||||
};
|
||||
})(),
|
||||
// 친 모양대로 숫자 형식 · 식 대문자 · 닫는 괄호 채움(엑셀)
|
||||
typedFormat: ["50%", "12.5%", "1,234.5", "1,000", "7", "abc", "1e3%"].map(ed.typedFormat),
|
||||
upperFormula: ["sum(a1:b2)+round(c3,0)", "'시트 a'!a1*true", '"abc"&a1', "내이름*2"].map(
|
||||
ed.upperFormula,
|
||||
),
|
||||
closeParens: ["=(1+2", "=SUM(1,ROUND(2,0", "=1+", "=(1+2)", '=")"&(1'].map(ed.closeParens),
|
||||
cellToText: [
|
||||
{ 값: "=12" },
|
||||
{ 값: "12" },
|
||||
|
||||
@@ -111,3 +111,31 @@ def test_abs_cycle_and_fold(r):
|
||||
def test_links_follow_rows_cols(r):
|
||||
# 행 넣기 → 밀림 · 연결 칸 열 지움 → 빠짐 · 되돌리기 → 제자리
|
||||
assert r["links"] == ["L:B7,W:D5", "L:B7", "L:B7,W:D5", "L:B5,W:D3", "D1"]
|
||||
|
||||
|
||||
def test_typed_format(r):
|
||||
assert r["typedFormat"] == ["0%", "0.0%", "#,##0.0", "#,##0", None, None, None]
|
||||
|
||||
|
||||
def test_upper_formula(r):
|
||||
assert r["upperFormula"] == [
|
||||
"SUM(A1:B2)+ROUND(C3,0)",
|
||||
"'시트 a'!A1*TRUE",
|
||||
'"abc"&A1',
|
||||
"내이름*2",
|
||||
]
|
||||
|
||||
|
||||
def test_close_parens(r):
|
||||
assert r["closeParens"] == ["=(1+2)", "=SUM(1,ROUND(2,0))", None, None, '=")"&(1)']
|
||||
|
||||
|
||||
def test_spill(r):
|
||||
sp = r["spill"]
|
||||
assert sp["left"] is None # 오른 칸(3)에 값 → 못 넘침
|
||||
assert sp["leftFree"] == [0, 100, 2, 4, 0, 0] # 빈 칸 둘 덮음
|
||||
assert sp["right"] == [100, 0, 0, 2, 0, 0]
|
||||
assert sp["center"] == [40, 40, 1, 3, 0, 0]
|
||||
assert sp["centerBlocked"] == [40, 40, 1, 2, 0, 40] # 오른쪽 막힘 → 오른쪽 반 잘라 냄
|
||||
assert sp["fits"] is None
|
||||
assert sp["edge"] is None # 판 끝(그린 칸 없음)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""저장 뒤 식 · 값 유지 — M02 면적산출 · 재료산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음.
|
||||
|
||||
읽기만 함(저장본을 고치지 않음). 저장 때 서버가 적은 값과 지금 엔진 풀이가 어긋나면 화면을 다시 열 때
|
||||
값이 달라 보임.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_spreadsheet as spreadsheet
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
FOLDER = ROOT / "resources" / "master_template"
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def _books() -> list[tuple[str, dict]]:
|
||||
out = []
|
||||
for kind in ("area", "material"):
|
||||
for path in sorted((FOLDER / kind).glob("[!.]*.json")):
|
||||
book = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(book, dict) and book.get("시트"):
|
||||
out.append((f"{kind}/{path.stem}", book))
|
||||
return out
|
||||
|
||||
|
||||
def test_저장본_서버_재계산_같음():
|
||||
books = _books()
|
||||
assert any("계산값" in b for _, b in books), "계산값 있는 저장본이 없음"
|
||||
fresh = spreadsheet.recalc_workbooks([b for _, b in books])
|
||||
diffs = []
|
||||
for (name, book), values in zip(books, fresh):
|
||||
if "계산값" not in book:
|
||||
continue # 옛 저장본 — 풀리기만 하면 됨
|
||||
saved = book["계산값"]
|
||||
for sheet, cells in values.items():
|
||||
for addr, v in cells.items():
|
||||
if saved.get(sheet, {}).get(addr) != v:
|
||||
diffs.append(
|
||||
f"{name} {sheet}!{addr}: 저장 {saved.get(sheet, {}).get(addr)} · 지금 {v}"
|
||||
)
|
||||
for sheet, cells in saved.items():
|
||||
for addr in cells:
|
||||
if addr not in values.get(sheet, {}):
|
||||
diffs.append(f"{name} {sheet}!{addr}: 저장에만 있음")
|
||||
assert not diffs, "\n".join(diffs[:30])
|
||||
@@ -1,107 +0,0 @@
|
||||
"""M01 자재 조합(`/api/m01/matset…` · 파일 `자재조합.json` · 키 UM) — 담은 자재 목록만 지님.
|
||||
|
||||
못박는 것
|
||||
- 새로 만들면 키를 서버가 `UM` 다음 번호로 줌 · 열 한 벌 · 담은 자재는 네 칸.
|
||||
- 없는 자재 코드 · 같은 자재 두 번 · 이름 없음 · 없는 본뜬키는 막음.
|
||||
- 고치기는 낡은 판본이면 409 · 지우기는 구조물 열이 「자재조합」 칸으로 쓰면 409 + 구조물 이름들.
|
||||
- 정본은 읽기만 함(사본 폴더 · 사본 집계표).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||||
for path in store.FOLDER.glob("*.json"):
|
||||
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
|
||||
shutil.copy(path, tmp_path / path.name)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
sheet = tmp_path / "집계표.json"
|
||||
sheet.write_text(json.dumps({"열": []}), encoding="utf-8")
|
||||
monkeypatch.setattr(combo_store, "USERS", (sheet,))
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _mat(client: TestClient) -> dict:
|
||||
"""자재품목에서 실제 줄 하나 — 자재 찾기 길 그대로."""
|
||||
got = client.get("/api/m01/pick", params={"kind": "price", "q": "합판", "limit": 2}).json()
|
||||
one = got["items"][0]
|
||||
return {"코드": one["ref"], "이름": one["이름"], "규격": one["규격"], "단위": one["단위"]}
|
||||
|
||||
|
||||
def _new(client: TestClient, name: str, mats: list[dict], **more) -> dict:
|
||||
body = {"matset": {"이름": name, "담은자재": mats, **more}}
|
||||
return client.post("/api/m01/matset/new", json=body)
|
||||
|
||||
|
||||
def test_정본_자재조합_파일은_틀대로다() -> None:
|
||||
data = json.loads((ROOT / "resources/master_data/자재조합.json").read_text(encoding="utf-8"))
|
||||
assert data["그룹"] == "자재조합" and isinstance(data["줄"], list)
|
||||
assert store.cm.mmc.check_form("자재조합.json", data) == []
|
||||
assert '"UM"' in (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_새로_만들면_UM_키와_열_한_벌(client) -> None:
|
||||
mat = _mat(client)
|
||||
res = _new(client, "합판 자재", [{**mat, "덤": 1}])
|
||||
assert res.status_code == 200, res.text
|
||||
made = res.json()
|
||||
assert made["key"].startswith("UM") and len(made["key"]) == 8
|
||||
assert tuple(made["matset"]) == store.cm.mmc.COLS
|
||||
assert made["matset"]["담은자재"] == [mat] and made["matset"]["본뜬키"] is None
|
||||
rows = client.get("/api/m01/matsets").json()["matsets"]
|
||||
assert [(r["키"], r["count"]) for r in rows] == [(made["key"], 1)]
|
||||
got = client.get("/api/m01/matset", params={"key": made["key"]}).json()
|
||||
assert got["matset"]["이름"] == "합판 자재"
|
||||
|
||||
|
||||
def test_없는_자재_두_번_담기_이름_본뜬키는_막는다(client) -> None:
|
||||
mat = _mat(client)
|
||||
assert _new(client, "없는 코드", [{**mat, "코드": "MT999999"}]).status_code == 422
|
||||
assert _new(client, "두 번", [mat, mat]).status_code == 422
|
||||
assert _new(client, " ", [mat]).status_code == 400
|
||||
assert _new(client, "없는 원본", [mat], 본뜬키="UM999999").status_code == 400
|
||||
ok = _new(client, "원본", [mat]).json()["key"]
|
||||
assert _new(client, "본뜬 것", [mat], 본뜬키=ok).json()["matset"]["본뜬키"] == ok
|
||||
assert _new(client, "원본", [mat]).status_code == 422 # 이름 겹침
|
||||
|
||||
|
||||
def test_고치기는_낡은_판본이면_409(client) -> None:
|
||||
mat = _mat(client)
|
||||
key = _new(client, "고칠 것", [mat]).json()["key"]
|
||||
one = client.get("/api/m01/matset", params={"key": key}).json()
|
||||
body = {"key": key, "version": one["version"], "matset": {**one["matset"], "이름": "고침"}}
|
||||
assert client.post("/api/m01/matset/edit", json=body).status_code == 200
|
||||
assert client.post("/api/m01/matset/edit", json=body).status_code == 409 # 같은 옛 판본
|
||||
|
||||
|
||||
def test_쓰는_열이_있으면_못_지우고_없으면_지워진다(client) -> None:
|
||||
mat = _mat(client)
|
||||
key = _new(client, "쓰는 것", [mat]).json()["key"]
|
||||
cols = [{"id": "c9", "머리": ["돌붙임", "찰붙임", "", ""], "자재조합": key}]
|
||||
combo_store.USERS[0].write_text(json.dumps({"열": cols}, ensure_ascii=False), encoding="utf-8")
|
||||
version = client.get("/api/m01/matset", params={"key": key}).json()["version"]
|
||||
res = client.post("/api/m01/matset/delete", json={"key": key, "version": version})
|
||||
assert res.status_code == 409 and res.json()["detail"]["users"] == ["돌붙임 찰붙임"]
|
||||
combo_store.USERS[0].write_text(json.dumps({"열": []}), encoding="utf-8")
|
||||
assert (
|
||||
client.post("/api/m01/matset/delete", json={"key": key, "version": version}).status_code
|
||||
== 200
|
||||
)
|
||||
assert client.get("/api/m01/matset", params={"key": key}).status_code == 404
|
||||
@@ -1,8 +1,10 @@
|
||||
"""조합 갈라 내기 · 분류 더하기 복사 (PLAN 15장).
|
||||
|
||||
M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄.
|
||||
M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄 · 자재 조합 표는
|
||||
마스터 템플릿 사본(없이 시작).
|
||||
· 일위대가(UA) — 새로 만듦(본뜬키 = 원래 키) · 이 열만 새 키 · 다른 열은 옛 키 · 원래 조합 그대로 ·
|
||||
SS 장부 맞춤 · 담은 로직 집합(차례 · 메모 무시)이 같으면 있는 키 · 또 고치면 또 새로 · 같은 이름 (2)
|
||||
SS 장부 맞춤 · 담은 로직(차례 · 메모까지)이 같으면 있는 키(되돌리면 원래 키) · 차례 · 메모만
|
||||
바꿔도 새로 · 또 고치면 또 새로 · 같은 이름 (2)
|
||||
· 자재 조합(UM) — 같은 규칙 · 처음 만드는 열(본뜬키 없음) · 읽기
|
||||
· 집계표 판 어긋남 = 409 · 조합 안 만듦
|
||||
· 분류 더하기로 복사한 열(`본뜬열`) — [저장] 때 원본 키 · 세 파일 내용을 새 번호로 받음
|
||||
@@ -17,7 +19,6 @@ from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Branch as branch
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router import router
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
@@ -25,8 +26,8 @@ from resources.tester.test_m02_structure import write_sample_table
|
||||
REAL = m01.FOLDER
|
||||
A, B, C = "GF000219", "GF000220", "GF000211" # 돌쌓기 메쌓기(인력 · 장비) · 채집(인력)
|
||||
M1, M2 = (
|
||||
{"코드": "MT000001", "이름": "시멘트", "규격": "40kg", "단위": "포"},
|
||||
{"코드": "MT000002", "이름": "모래", "규격": None, "단위": "㎥"},
|
||||
{"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"},
|
||||
{"코드": "MT000009", "이름": "모래", "규격": None, "단위": "㎥"},
|
||||
)
|
||||
TABLE = "/api/m02/templates/table/구조물집계표"
|
||||
|
||||
@@ -38,11 +39,10 @@ def client(tmp_path, monkeypatch):
|
||||
for path in REAL.glob("*.json"):
|
||||
if not path.name.startswith("_") or path.name == m01.mk.BOOK.name:
|
||||
shutil.copy(path, master / path.name)
|
||||
for name in (combo_store.FILE, branch.MATERIAL_FILE):
|
||||
own = master / name
|
||||
data = json.loads(own.read_text(encoding="utf-8"))
|
||||
data["줄"] = []
|
||||
own.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
own = master / combo_store.FILE
|
||||
data = json.loads(own.read_text(encoding="utf-8"))
|
||||
data["줄"] = []
|
||||
own.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
monkeypatch.setattr(m01, "FOLDER", master)
|
||||
template = tmp_path / "template"
|
||||
template.mkdir()
|
||||
@@ -75,7 +75,9 @@ def _source(client) -> str:
|
||||
|
||||
|
||||
def _branch(client, column, source, keys, name="", 판="", want=200):
|
||||
body = {"본뜬키": source, "담은로직": [{"로직": k} for k in keys], "이름": name, "판": 판}
|
||||
"""`keys` = 로직 키 또는 (로직, 메모)"""
|
||||
lines = [{"로직": k[0], "메모": k[1]} if isinstance(k, tuple) else {"로직": k} for k in keys]
|
||||
body = {"본뜬키": source, "담은로직": lines, "이름": name, "판": 판}
|
||||
res = client.post(f"/api/m02/structures/{column}/combo", json=body)
|
||||
assert res.status_code == want, res.text
|
||||
return res.json()
|
||||
@@ -119,17 +121,27 @@ def test_새로_만듦_이_열만_새_키_원래_조합_그대로(client):
|
||||
assert combo_store.combo(got["키"])["combo"]["본뜬키"] == ua
|
||||
|
||||
|
||||
def test_같은_로직_집합이면_있는_키_또_고치면_또_새로_같은_이름은_2(client):
|
||||
def test_차례_메모까지_같으면_있는_키_또_고치면_또_새로_같은_이름은_2(client):
|
||||
ua = _source(client)
|
||||
same = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 원래 조합
|
||||
same = _branch(client, "c1", ua, [(A, ""), (B, None)]) # 빈 메모 = null → 원래 조합
|
||||
assert (same["키"], same["새로만듦"]) == (ua, False)
|
||||
order = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 새로
|
||||
assert order["새로만듦"] is True and order["키"] != ua
|
||||
got = combo_store.combo(order["키"])["combo"]
|
||||
assert ([x["로직"] for x in got["담은로직"]], got["본뜬키"]) == ([B, A], ua)
|
||||
memo = _branch(client, "c1", order["키"], [B, (A, "사면 쪽")]) # 메모만 바꿈 = 또 새로
|
||||
assert memo["새로만듦"] is True and memo["키"] not in (ua, order["키"])
|
||||
assert combo_store.combo(memo["키"])["combo"]["담은로직"][1]["메모"] == "사면 쪽"
|
||||
back = _branch(client, "c1", memo["키"], [A, B]) # 원래와 똑같이 되돌림 = 원래 키
|
||||
assert (back["키"], back["새로만듦"]) == (ua, False)
|
||||
assert _cols(client)["c1"] == ua and _ss(client)["c1"] == ua
|
||||
first = _branch(client, "rv", ua, [A, B, C], "갈래")
|
||||
again = _branch(client, "rv", first["키"], [A, C], "갈래") # 갈라 낸 것을 또 고침 = 또 새로
|
||||
assert again["새로만듦"] is True and again["키"] not in (ua, first["키"])
|
||||
assert again["이름"] == "갈래 (2)"
|
||||
assert combo_store.combo(again["키"])["combo"]["본뜬키"] == first["키"]
|
||||
back = _branch(client, "c1", ua, [C, A, B]) # 다른 열이 같게 고침 = 있는 키
|
||||
assert (back["키"], back["새로만듦"]) == (first["키"], False)
|
||||
twin = _branch(client, "c1", ua, [A, B, C]) # 다른 열이 똑같이 고침 = 있는 키
|
||||
assert (twin["키"], twin["새로만듦"]) == (first["키"], False)
|
||||
assert _cols(client)["c1"] == first["키"] and _ss(client)["c1"] == first["키"]
|
||||
|
||||
|
||||
@@ -157,8 +169,10 @@ def test_자재_조합_처음_만듦_같은_내용이면_있는_키_고치면_
|
||||
row = client.get(f"/api/m02/material-combos/{first['키']}").json()
|
||||
assert tuple(row) == ("키", "이름", "담은자재", "본뜬키", "소유", "비고")
|
||||
assert (row["본뜬키"], row["소유"], row["담은자재"]) == (None, "현장", [M1, M2])
|
||||
same = _material(client, "c1", "", [M2, M1]) # 차례만 다름 = 있는 키
|
||||
same = _material(client, "c1", "", [M1, M2]) # 같은 차례 = 있는 키
|
||||
assert (same["키"], same["새로만듦"]) == (first["키"], False)
|
||||
order = _material(client, "c1", first["키"], [M2, M1], "모래 먼저") # 차례만 다름 = 새로
|
||||
assert order["새로만듦"] is True and order["키"] != first["키"]
|
||||
fork = _material(client, "c1", first["키"], [M1], "시멘트만")
|
||||
assert fork["새로만듦"] is True and fork["키"] != first["키"]
|
||||
assert client.get(f"/api/m02/material-combos/{fork['키']}").json()["본뜬키"] == first["키"]
|
||||
@@ -168,7 +182,7 @@ def test_자재_조합_처음_만듦_같은_내용이면_있는_키_고치면_
|
||||
)
|
||||
assert _ss(client, "자재조합")["c1"] == fork["키"]
|
||||
assert client.get("/api/m02/material-combos/UM999999").status_code == 404
|
||||
_material(client, "rv", "", [{"코드": "없는모양"}], want=422) # M01 저장 검사
|
||||
_material(client, "rv", "", [{"코드": "없는모양"}], want=422) # 자재 조합 저장 검사
|
||||
|
||||
|
||||
def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_받음(client):
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""M02 자재 조합(`/api/m02/material-combos…` · 마스터 템플릿 `자재조합.json` · 키 UM).
|
||||
|
||||
못박는 것
|
||||
- 정본 표는 마스터 템플릿 자리 · 틀대로(머리 「다음」) · 키 대장에 UM 없음 · M01 길 없음.
|
||||
- 새로 만들면 키 = 표 「다음」 번호 · 열 한 벌 · 담은 자재는 네 칸 · 지운 키는 다시 안 씀.
|
||||
- 없는 자재 코드 · 같은 자재 두 번 · 이름 없음 · 이름 겹침 · 없는 본뜬키는 막음.
|
||||
- 고치기 · 지우기는 낡은 판이면 409 · 본뜬키는 고쳐도 그대로 · 쓰는 열이 있으면 못 지움.
|
||||
- 정본은 읽기만 함(사본 폴더 · 견본 집계표).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as m01_router
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Matset as matset
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router import router
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M1, M2 = (
|
||||
{"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"},
|
||||
{"코드": "MT000009", "이름": "모래", "규격": "", "단위": "㎥"},
|
||||
)
|
||||
URL = "/api/m02/material-combos"
|
||||
TABLE = "/api/m02/templates/table/구조물집계표"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||||
write_sample_table(tmp_path)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _new(client: TestClient, name: str, mats: list[dict], 판: str | None = None, **more):
|
||||
if 판 is None:
|
||||
판 = client.get(URL).json()["판"]
|
||||
return client.post(URL, json={"판": 판, "줄": {"이름": name, "담은자재": mats, **more}})
|
||||
|
||||
|
||||
def test_정본_표는_마스터_템플릿_자리_틀대로_UM_은_대장_밖() -> None:
|
||||
path = ROOT / "resources/master_template/자재조합.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert data["그룹"] == "자재조합" and isinstance(data["다음"], int)
|
||||
assert matset.mmc.check_file(path) == []
|
||||
assert not (ROOT / "resources/master_data/자재조합.json").exists()
|
||||
book = (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8")
|
||||
assert '"UM"' not in book.splitlines()[1] # 대장 머리 「다음」
|
||||
assert not any("matset" in getattr(r, "path", "") for r in m01_router.router.routes)
|
||||
|
||||
|
||||
def test_새로_만들면_다음_번호_열_한_벌_지운_키는_다시_안_씀(client) -> None:
|
||||
res = _new(client, "합판 자재", [{**M1, "덤": 1}])
|
||||
assert res.status_code == 200, res.text
|
||||
made = res.json()
|
||||
assert made["키"] == "UM000001" and tuple(made["줄"]) == matset.mmc.COLS
|
||||
assert made["줄"]["담은자재"] == [M1] and made["줄"]["본뜬키"] is None
|
||||
listed = client.get(URL).json()
|
||||
assert listed["판"] == made["판"] and [r["키"] for r in listed["줄"]] == ["UM000001"]
|
||||
assert client.get(f"{URL}/UM000001").json()["이름"] == "합판 자재"
|
||||
assert client.delete(f"{URL}/UM000001", params={"판": made["판"]}).status_code == 200
|
||||
again = _new(client, "다음 것", [M2]).json()
|
||||
assert again["키"] == "UM000002" # 지운 번호는 다시 안 씀
|
||||
assert json.loads((store.FOLDER / matset.FILE).read_text(encoding="utf-8"))["다음"] == 3
|
||||
|
||||
|
||||
def test_없는_자재_두_번_이름_본뜬키는_막는다(client) -> None:
|
||||
assert _new(client, "없는 코드", [{**M1, "코드": "MT999999"}]).status_code == 422
|
||||
assert _new(client, "두 번", [M1, M1]).status_code == 422
|
||||
assert _new(client, " ", [M1]).status_code == 400
|
||||
assert _new(client, "빈 것", []).status_code == 422
|
||||
assert _new(client, "없는 원본", [M1], 본뜬키="UM999999").status_code == 400
|
||||
ok = _new(client, "원본", [M1]).json()["키"]
|
||||
copy = _new(client, "본뜬 것", [M1, M2], 본뜬키=ok).json()
|
||||
assert copy["줄"]["본뜬키"] == ok and copy["키"] != ok
|
||||
assert _new(client, "원본", [M2]).status_code == 422 # 이름 겹침
|
||||
assert _new(client, "낡은 판", [M2], 판="0000").status_code == 409
|
||||
|
||||
|
||||
def test_고치기는_낡은_판이면_409_본뜬키는_그대로(client) -> None:
|
||||
src = _new(client, "원본", [M1]).json()["키"]
|
||||
made = _new(client, "고칠 것", [M1], 본뜬키=src).json()
|
||||
row = {**made["줄"], "이름": "고침", "담은자재": [M1, M2], "본뜬키": None}
|
||||
res = client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["줄"]["이름"] == "고침" and res.json()["줄"]["본뜬키"] == src
|
||||
assert client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row}).status_code == 409
|
||||
assert client.put(f"{URL}/UM999999", json={"판": "", "줄": row}).status_code == 404
|
||||
|
||||
|
||||
def test_쓰는_열이_있으면_못_지우고_없으면_지워진다(client) -> None:
|
||||
made = _new(client, "쓰는 것", [M1]).json()
|
||||
table = client.get(TABLE).json()
|
||||
for col in table["문서"]["열"]:
|
||||
if col["id"] == "rv":
|
||||
col["자재조합"] = made["키"]
|
||||
assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200
|
||||
res = client.delete(f"{URL}/{made['키']}", params={"판": made["판"]})
|
||||
assert client.get(URL).json()["쓰는곳"] == {made["키"]: ["돌기슭막이 (형태마다) H=(높이마다)"]}
|
||||
assert res.status_code == 409 and res.json()["detail"]["users"] == [
|
||||
"돌기슭막이 (형태마다) H=(높이마다)"
|
||||
]
|
||||
rows = client.get("/api/m02/structure-combos").json()["줄"]
|
||||
assert next(r for r in rows if r["열"] == "rv")["자재조합"] == made["키"] # SS 장부 칸
|
||||
table = client.get(TABLE).json()
|
||||
for col in table["문서"]["열"]:
|
||||
col.pop("자재조합", None)
|
||||
assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200
|
||||
assert client.delete(f"{URL}/{made['키']}", params={"판": "낡음"}).status_code == 409
|
||||
assert client.delete(f"{URL}/{made['키']}", params={"판": made["판"]}).status_code == 200
|
||||
assert client.get(f"{URL}/{made['키']}").status_code == 404
|
||||
|
||||
|
||||
def test_M01_마스터는_UM_을_모른다() -> None:
|
||||
assert "UM" not in m01.cm.mk.IDS and "자재조합" not in m01.cm.mk.ROW_ORDER
|
||||
@@ -100,6 +100,12 @@ def test_in_use_templates_not_deleted(client):
|
||||
got = client.delete(f"/api/m02/templates/{kind}/{name}")
|
||||
assert got.status_code == 409 and "쓰는 양식" in got.json()["detail"]
|
||||
assert client.get(f"/api/m02/templates/{kind}/{name}").status_code == 200
|
||||
# 목록이 쓰는 곳을 실어 화면이 확인 창 전에 막음 · 안 쓰는 양식엔 없음
|
||||
client.put("/api/m02/templates/drawing/아무도", json={"판": "", "문서": doc})
|
||||
rows = {(r["종류"], r["이름"]): r for r in client.get("/api/m02/templates").json()}
|
||||
for key, use in store.IN_USE.items():
|
||||
assert rows[key]["쓰는곳"] == use
|
||||
assert "쓰는곳" not in rows[("drawing", "아무도")]
|
||||
|
||||
|
||||
def test_empty_doc_rejected_and_file_untouched(client):
|
||||
|
||||
@@ -187,6 +187,21 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c
|
||||
assert numbers["pv_y"] == _num("02010101") # pv_b 무리 안 분류3 다음 번호
|
||||
|
||||
|
||||
def test_지운_열_번호는_다음_저장에도_다시_안_줌(client):
|
||||
"""무리 맨 끝 열(pv_jt 02010400)을 지워 저장한 뒤 새 열을 더해도 04 를 다시 안 받음."""
|
||||
store.renumber()
|
||||
url = "/api/m02/templates/table/구조물집계표"
|
||||
got = client.get(url).json()
|
||||
got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "pv_jt"]
|
||||
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
|
||||
assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["02010400"]
|
||||
got = client.get(url).json()
|
||||
new = {"id": "pv_z", "머리": ["콘크리트포장", "T=(두께마다)", "Z", None], "꼴": "수"}
|
||||
got["문서"]["열"].append(new)
|
||||
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
|
||||
assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("02010500")
|
||||
|
||||
|
||||
def test_다른_열_도면을_복사해_가져옴(client):
|
||||
store.renumber()
|
||||
src = client.get("/api/m02/templates/structure/rv").json()
|
||||
|
||||
@@ -148,6 +148,21 @@ def test_좌측_새로_본떠_지우기는_표_도면_컨테이너_안에_하나
|
||||
assert "양식: name" in side.split("function copyTemplate")[1] # 복사본 머리 이름 = 새 이름
|
||||
|
||||
|
||||
def test_좌측_고름_표시는_열린_것_한_줄뿐():
|
||||
"""카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 열 줄을 함께 그림."""
|
||||
side = _text(SIDE)
|
||||
assert "picked" not in side # 따로 든 고름 상태 없음 — 편집 칸의 지금 열린 것만 봄
|
||||
assert re.search(r"function paint\(\): void \{\s*paintList\(\);\s*paintColumns\(\);", side)
|
||||
for fn in ("async function select(", "async function selectColumn("):
|
||||
body = side.split(fn)[1].split("\n }\n")[0]
|
||||
assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음
|
||||
assert re.search(r"sel = next;\s*onOpened\?\.\(\);", _text(MAIN))
|
||||
# 자재 조합 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 자재 조합 고름 지움
|
||||
assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN))
|
||||
assert "if (opt.getOpen()) matset.clear();" in side.split("function paint(): void {")[1]
|
||||
assert "() => side.paint()" in _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
|
||||
|
||||
def test_리본_접힘_모드에서_탭으로_편_것은_밖을_누르면_다시_접힘():
|
||||
ribbon = _text(ROOT / "B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx")
|
||||
assert "if (collapsed) setPeek(true);" in ribbon # 탭 = 잠깐 펼침(접힘 모드 유지)
|
||||
|
||||
Reference in New Issue
Block a user