feat(815-2): M01 요소 표 「별칭」 한 열 · 마스터 기계 별칭 다섯 옮김
- 인력 · 기계 · 재료 자재 · 유가전력 표에 비고 앞 「별칭」 열 — 눌러 고침 · 콤마로 여러 개 · 빈 글이면 칸 없음 - 저장 길 = 그대로 `/api/m01/save`(서버가 앞뒤 빈칸 뗌 · 빈 글 = 칸 지움 · 그 줄만 다시 씀) - check_master — 요소 줄 열 한 벌 밖 덧칸 `별칭` 허용(빈 글 · 글 아님은 흠) - 마스터 기계 별칭 다섯(맞춤.json) → 기계 줄 15 개 `별칭` · 옛 마스터 장부 파일 걷음 - M01 로직 1,513 셈 지문 전후 같음 - 새 시험 test_815_2_alias_column.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,12 @@ def save(batch: list[dict], confirmed: bool = False) -> dict:
|
||||
"""`/save` — 로직 정본 줄 고침은 `confirmed`(「원문 개정 반영」) 없으면 409(`Store_Revise`) ·
|
||||
자체 파일 새 줄(키 없음)은 서버가 칸을 잡고 GX 다음 키 — {files, added, warnings}."""
|
||||
revise.guard(batch, confirmed)
|
||||
for part in batch: # 별칭(815-2) = 글 그대로 · 앞뒤 빈칸만 뗌 · 빈 글이면 칸 없음
|
||||
for ch in part.get("changes") or []:
|
||||
row = ch.get("row")
|
||||
if isinstance(row, dict) and "별칭" in row:
|
||||
text = str(row["별칭"] or "").strip()
|
||||
row.update(별칭=text) if text else row.pop("별칭")
|
||||
fresh = False
|
||||
for part in batch:
|
||||
if part.get("file") != OWN:
|
||||
|
||||
@@ -37,6 +37,9 @@ const MARKET = "재료_자재품목.json";
|
||||
const LINKED = "재료_품셈재료.json";
|
||||
const JOB = ["인력.json"];
|
||||
const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음
|
||||
// 별칭(815-2 · 요구 764) = 비고 앞 한 열 · 콤마로 여러 개 · 빈 글이면 칸을 지움 · 이 넷만
|
||||
const ALIAS = "별칭";
|
||||
const ALIASED = ["인력.json", "기계.json", MARKET, "재료_유가전력.json"];
|
||||
const FALLBACK: Row = { 키: "", 원문번호: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" };
|
||||
|
||||
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
|
||||
@@ -44,16 +47,16 @@ function blank(sample: Row): Row {
|
||||
const out: Row = {};
|
||||
for (const [k, v] of Object.entries(sample)) {
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row);
|
||||
else out[k] = typeof v === "string" && k !== "값" ? "" : null;
|
||||
else if (k !== ALIAS) out[k] = typeof v === "string" && k !== "값" ? "" : null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 비고 칸 고침 — 빈 글이면 칸을 지움 */
|
||||
function withNote(row: Row, text: string): Row {
|
||||
/** 비고 · 별칭 칸 고침 — 빈 글이면 칸을 지움(별칭은 앞뒤 빈칸 뗌) */
|
||||
function withNote(row: Row, text: string, col = NOTE): Row {
|
||||
const out = { ...row };
|
||||
if (text.trim()) out[NOTE] = text;
|
||||
else delete out[NOTE];
|
||||
if (text.trim()) out[col] = col === ALIAS ? text.trim() : text;
|
||||
else delete out[col];
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -117,8 +120,9 @@ export function renderRows(
|
||||
const cols: string[] = [];
|
||||
for (const row of [...adds, ...d.rows]) {
|
||||
for (const k of Object.keys(flatten(row)))
|
||||
if (!hidden(file, k) && k !== NOTE && !cols.includes(k)) cols.push(k);
|
||||
if (!hidden(file, k) && k !== NOTE && k !== ALIAS && !cols.includes(k)) cols.push(k);
|
||||
}
|
||||
const aliased = ALIASED.includes(file);
|
||||
const heads = (HEADS[file] ?? []).map((h) =>
|
||||
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
|
||||
);
|
||||
@@ -127,6 +131,7 @@ export function renderRows(
|
||||
el("th"),
|
||||
...cols.map((c) => el("th", { text: c })),
|
||||
...(JOB.includes(file) ? [] : heads),
|
||||
...(aliased ? [el("th", { text: ALIAS })] : []),
|
||||
el("th", { text: NOTE }),
|
||||
...(JOB.includes(file) ? heads : []),
|
||||
],
|
||||
@@ -145,12 +150,13 @@ export function renderRows(
|
||||
}),
|
||||
);
|
||||
}
|
||||
tr.append(
|
||||
buildCell(show(row[NOTE]), {
|
||||
changed: true,
|
||||
onEdit: (text) => setAdd(file, i, withNote(row, text)),
|
||||
}),
|
||||
);
|
||||
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
|
||||
tr.append(
|
||||
buildCell(show(row[col]), {
|
||||
changed: true,
|
||||
onEdit: (text) => setAdd(file, i, withNote(row, text, col)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(el("td"));
|
||||
body.append(tr);
|
||||
});
|
||||
@@ -186,15 +192,16 @@ export function renderRows(
|
||||
? undefined
|
||||
: (next: Row): void => editRow(file, d.version, key, row, next);
|
||||
if (file === LINKED) tr.append(linkedCell(row, cur, put));
|
||||
tr.append(
|
||||
buildCell(show(cur[NOTE]), {
|
||||
changed: !same(cur[NOTE], row[NOTE]),
|
||||
onEdit:
|
||||
deleted || !isScalar(row[NOTE])
|
||||
? undefined
|
||||
: (text) => editRow(file, d.version, key, row, withNote(cur, text)),
|
||||
}),
|
||||
);
|
||||
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
|
||||
tr.append(
|
||||
buildCell(show(cur[col]), {
|
||||
changed: !same(cur[col], row[col]),
|
||||
onEdit:
|
||||
deleted || !isScalar(row[col])
|
||||
? undefined
|
||||
: (text) => editRow(file, d.version, key, row, withNote(cur, text, col)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(wageCell(row, cur, put));
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
@@ -149,6 +149,11 @@ COLS = {
|
||||
),
|
||||
}
|
||||
COLS["한국은행환율"] = COLS["유가전력"]
|
||||
ALIAS = "별칭" # 요소 줄 덧칸(815-2) — 열 한 벌 밖 · 있으면 빈 글 아님
|
||||
|
||||
|
||||
def _cols_of(row: dict) -> tuple:
|
||||
return tuple(k for k in row if k != ALIAS)
|
||||
|
||||
|
||||
def _detail(name: str) -> str:
|
||||
@@ -310,7 +315,9 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
out.append(f"{where} · 출처 모양 「{row['출처']}」")
|
||||
if data.get("그룹") == "인력":
|
||||
out += _check_labor(where, row)
|
||||
if cols and tuple(row) != cols:
|
||||
if ALIAS in row and not (isinstance(row[ALIAS], str) and row[ALIAS].strip()):
|
||||
out.append(f"{where} · 「{ALIAS}」 이 빈 글 · 글 아님(빈 칸이면 칸을 두지 않음)")
|
||||
if cols and _cols_of(row) != cols:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}」")
|
||||
if kind == "품셈재료":
|
||||
out += _check_linked(where, row)
|
||||
@@ -328,7 +335,7 @@ def check_form(name: str, data: dict) -> list[str]:
|
||||
def _check_labor(where: str, row: dict) -> list[str]:
|
||||
"""인력 줄 — 열 한 벌(차례까지 같음) · 상태 값 · 옛이름 목록."""
|
||||
out = []
|
||||
if tuple(row) != LABOR_COLS:
|
||||
if _cols_of(row) != LABOR_COLS:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(LABOR_COLS)}」")
|
||||
if row.get("상태") not in LABOR_STATUS:
|
||||
out.append(f"{where} · 상태 「{row.get('상태')}」 — {' · '.join(LABOR_STATUS)} 아님")
|
||||
|
||||
@@ -12574,7 +12574,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-4",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
|
||||
},
|
||||
"비고": "조종원직종 원문 이름: 건설기계운전사 · 주연료종류: 원문 글을 가름"
|
||||
"비고": "조종원직종 원문 이름: 건설기계운전사 · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "아스팔트페이버(피니셔)"
|
||||
},
|
||||
{
|
||||
"키": "EQ000193",
|
||||
@@ -12635,7 +12636,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-4",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
|
||||
},
|
||||
"비고": "조종원직종 원문 이름: 건설기계운전사 · 주연료종류: 원문 글을 가름"
|
||||
"비고": "조종원직종 원문 이름: 건설기계운전사 · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "아스팔트페이버(피니셔)"
|
||||
},
|
||||
{
|
||||
"키": "EQ000194",
|
||||
@@ -16031,9 +16033,8 @@
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: (m/m) · 주연료종류: 원문 글을 가름",
|
||||
"값원문": {
|
||||
"8-4-5 주연료": "휘발유1.0"
|
||||
}
|
||||
"값원문": { "8-4-5 주연료": "휘발유1.0" },
|
||||
"별칭": "바이브레이트 봉상후렉시블"
|
||||
},
|
||||
{
|
||||
"키": "EQ000249",
|
||||
@@ -25212,7 +25213,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-7",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "주연료종류: 원문 글을 가름"
|
||||
"비고": "주연료종류: 원문 글을 가름",
|
||||
"별칭": "워터제트"
|
||||
},
|
||||
{
|
||||
"키": "EQ000401",
|
||||
@@ -26734,7 +26736,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "주연료종류: 원문 글을 가름"
|
||||
"비고": "주연료종류: 원문 글을 가름",
|
||||
"별칭": "버킷준설기"
|
||||
},
|
||||
{
|
||||
"키": "EQ000426",
|
||||
@@ -31131,7 +31134,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000498",
|
||||
@@ -31192,7 +31196,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000499",
|
||||
@@ -31253,7 +31258,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000500",
|
||||
@@ -31314,7 +31320,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000501",
|
||||
@@ -31375,7 +31382,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000502",
|
||||
@@ -31436,7 +31444,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000503",
|
||||
@@ -31497,7 +31506,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000504",
|
||||
@@ -31558,7 +31568,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000505",
|
||||
@@ -31619,7 +31630,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000506",
|
||||
@@ -31680,7 +31692,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000507",
|
||||
@@ -31741,7 +31754,8 @@
|
||||
"조종원": "건설품셈 공통 8-4-8",
|
||||
"조종원직종": "건설품셈 공통 8-1-3 5"
|
||||
},
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름"
|
||||
"비고": "규격 단위: 기종 · 규격(㎾) · 주연료종류: 원문 글을 가름",
|
||||
"별칭": "디젤 엔진"
|
||||
},
|
||||
{
|
||||
"키": "EQ000508",
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"줄": {
|
||||
"아스팔트페이버(피니셔)||시간": {
|
||||
"원래": {
|
||||
"품명": "아스팔트페이버(피니셔)",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000193",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"디젤엔진||시간": {
|
||||
"원래": {
|
||||
"품명": "디젤 엔진",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000500",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"워터제트||시간": {
|
||||
"원래": {
|
||||
"품명": "워터제트",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000400",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"버킷준설기||시간": {
|
||||
"원래": {
|
||||
"품명": "버킷준설기",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000425",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"바이브레이트|봉상후렉시블|시간": {
|
||||
"원래": {
|
||||
"품명": "바이브레이트",
|
||||
"규격": "봉상후렉시블",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000248",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"""PLAN 815-2 — M01 요소 「별칭」 한 열(요구 764 · 765).
|
||||
|
||||
① 별칭 칸 저장 = 그 줄만 바뀜 · 앞뒤 빈칸 뗌 · 빈 글이면 칸 없음(임시 사본 폴더만)
|
||||
② M01 길은 시스템 관리자만(아니면 403)
|
||||
③ 마스터 기계 별칭 다섯 → 기계 줄 `별칭` · 옛 장부 파일 걷음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util import common_util_auth as auth
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REAL = store.FOLDER
|
||||
MACHINE = "기계.json"
|
||||
KEY = "EQ000001"
|
||||
FIVE = {
|
||||
"EQ000192": "아스팔트페이버(피니셔)",
|
||||
"EQ000193": "아스팔트페이버(피니셔)",
|
||||
**{f"EQ000{n}": "디젤 엔진" for n in range(497, 508)},
|
||||
"EQ000400": "워터제트",
|
||||
"EQ000425": "버킷준설기",
|
||||
"EQ000248": "바이브레이트 봉상후렉시블",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def copy(tmp_path: Path, monkeypatch) -> Path:
|
||||
for path in REAL.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)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _app(role: str = "SYSTEM_ADMIN") -> TestClient:
|
||||
app = FastAPI()
|
||||
app.dependency_overrides[auth.verify_session] = lambda: {
|
||||
"user_id": 1,
|
||||
"name": "시험",
|
||||
"role": role,
|
||||
"company_id": 1,
|
||||
}
|
||||
deps = [Depends(auth.verify_session), Depends(auth.require_system_admin)]
|
||||
app.include_router(router_module.router, dependencies=deps)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _save_alias(client: TestClient, text: str) -> None:
|
||||
one = client.get("/api/m01/rows", params={"file": MACHINE, "q": KEY}).json()
|
||||
row = next(r for r in one["rows"] if r["키"] == KEY)
|
||||
part = {
|
||||
"file": MACHINE,
|
||||
"version": one["version"],
|
||||
"changes": [{"op": "edit", "key": KEY, "row": {**row, "별칭": text}}],
|
||||
}
|
||||
res = client.post("/api/m01/save", json={"files": [part]})
|
||||
assert res.status_code == 200, res.text
|
||||
|
||||
|
||||
def _around(text: str) -> tuple[str, str, str]:
|
||||
"""(그 줄 앞 글, 그 줄 글, 뒤 글)."""
|
||||
start = text.rfind("{", 0, text.index(f'"키": "{KEY}"'))
|
||||
_, end = json.JSONDecoder().raw_decode(text, start)
|
||||
return text[:start], text[start:end], text[end:]
|
||||
|
||||
|
||||
def test_별칭_저장_그_줄만_빈칸이면_칸_없음(copy: Path) -> None:
|
||||
client = _app()
|
||||
before = (copy / MACHINE).read_bytes()
|
||||
_save_alias(client, " 시험별칭A, 시험별칭B ")
|
||||
after = (copy / MACHINE).read_bytes().decode("utf-8")
|
||||
row = next(r for r in json.loads(after)["줄"] if r["키"] == KEY)
|
||||
assert row["별칭"] == "시험별칭A, 시험별칭B" # 글 그대로 · 앞뒤 빈칸만 뗌
|
||||
old, new = _around(before.decode("utf-8")), _around(after)
|
||||
assert old[0] == new[0] and old[2] == new[2] # 그 줄 밖 글 · 서식 그대로
|
||||
assert json.loads(new[1]) == {**json.loads(old[1]), "별칭": "시험별칭A, 시험별칭B"}
|
||||
_save_alias(client, " ")
|
||||
last = _around((copy / MACHINE).read_bytes().decode("utf-8"))
|
||||
assert last[0] == old[0] and last[2] == old[2]
|
||||
assert json.loads(last[1]) == json.loads(old[1]) # 빈 글 = 칸 없음 = 처음 값
|
||||
|
||||
|
||||
def test_M01_길은_시스템_관리자만() -> None:
|
||||
for role in ("USER", "COMPANY_ADMIN"):
|
||||
res = _app(role).post("/api/m01/save", json={"files": []})
|
||||
assert res.status_code == 403
|
||||
main = (ROOT / "main.py").read_text(encoding="utf-8")
|
||||
assert "include_router(m01_master_data_router, dependencies=system_admin_only)" in main
|
||||
|
||||
|
||||
def test_마스터_기계_별칭_다섯_옮김_장부_걷음() -> None:
|
||||
rows = {r["키"]: r for r in json.loads((REAL / MACHINE).read_text(encoding="utf-8"))["줄"]}
|
||||
assert {k: r.get("별칭") for k, r in rows.items() if r.get("별칭")} == FIVE
|
||||
assert not (ROOT / "resources" / "master_template" / "실무자료" / "맞춤.json").exists()
|
||||
Reference in New Issue
Block a user