Merge remote-tracking branch 'origin/dev' into sub_laptop_4

This commit is contained in:
2026-09-20 16:33:09 +09:00
20 changed files with 48439 additions and 48479 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ export async function saveFiles(files: FileChanges[]): Promise<SaveResult> {
return { status, detail: typeof detail === "string" ? detail : JSON.stringify(detail) };
}
export type PickKind = "procure" | "price" | "job";
export type PickKind = "price" | "job";
export interface PickItem {
ref: string;
+16 -29
View File
@@ -103,7 +103,7 @@ def files_of(group: str) -> list[dict]:
_REF_KEY = re.compile(r"^[A-Z]{2}\d{6}$")
_REF_FILE = {
"LB": "인력.json",
"MT": "재료_시중물가.json",
"MT": "재료_자재품목.json",
"MN": "재료_나라장터자재.json",
"MP": "재료_품셈재료.json",
"EQ": "기계.json",
@@ -111,16 +111,9 @@ _REF_FILE = {
def _ref_keys(row: dict) -> list[str]:
"""줄이 가리키는 키 — 준용 · 연결 · 조달 출처(「나라장터:<키>」)."""
put = (
row.get("준용"),
row.get("연결"),
(row.get("") or {}).get("조달", {}).get("출처")
if isinstance(row.get(""), dict)
else None,
)
out = [v.removeprefix("나라장터:") for v in put if isinstance(v, str)]
return [k for k in out if _REF_KEY.match(k)]
"""줄이 가리키는 키 — 준용 · 연결."""
put = (row.get("준용"), row.get("연결"))
return [v for v in put if isinstance(v, str) and _REF_KEY.match(v)]
def refs_of(rows_: list[dict]) -> dict[str, str]:
@@ -301,29 +294,24 @@ def elements(group: str, q: str, limit: int) -> dict:
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}
_PICK = {
"procure": "재료_나라장터자재.json",
"price": "재료_시중물가.json",
"job": "인력.json",
}
_PICK = {"price": "재료_자재품목.json", "job": "인력.json"}
def _squash(text) -> str:
return "".join(str(text or "").lower().split())
def _lowest(value) -> object:
"""시중물가 줄 — 시중 다섯 칸 · 조달 값 가운데 낮은 값 · 그 밖은 `값` 그대로."""
if not isinstance(value, dict):
return value
nums = [v for v in value["시중"].values() if isinstance(v, (int, float))]
supply = value["조달"].get("")
return min(nums + ([supply] if supply is not None else []), default=None)
def _lowest(row: dict) -> object:
"""자재품목 줄 — 값 열 다섯 가운데 낮은 값(시험 계산용) · 값 칸이 있는 줄은 그 값 그대로."""
if "" in row:
return row[""]
nums = [row[s] for s in cm.mf.SLOTS if row.get(s) is not None]
return min(nums, default=None)
def pick(kind: str, q: str, limit: int) -> dict:
"""고르기 창 — `procure` 나라장터자재(ref = 나라장터:<키>) · `price` 시중물가(관급 조달 줄 포함 · ref = 키)
· `job` 값 있는 공표 노임 직종(미확보 빼고 모든 구분)(ref = 키) · 이름·규격 낱말이 모두 들어간 줄."""
"""고르기 창 — `price` 자재품목(관급 줄 포함 · ref = 키) · `job` 값 있는 공표 노임 직종
(미확보 빼고 모든 구분)(ref = 키) · 이름·규격 낱말이 모두 들어간 줄."""
if kind not in _PICK:
raise StoreError(404, f"없는 고르기 「{kind}")
words = [_squash(w) for w in q.split()]
@@ -333,13 +321,12 @@ def pick(kind: str, q: str, limit: int) -> dict:
continue
text = _squash(f"{row.get('이름')} {row.get('규격')} {' '.join(row.get('옛이름') or [])}")
if all(w in text for w in words):
key = str(row.get(""))
hits.append(
{
"ref": f"나라장터:{key}" if kind == "procure" else key,
"ref": str(row.get("")),
**{k: row.get(k) for k in ("원문번호", "구분", "이름", "규격", "단위")},
"": _lowest(row.get("")),
"관급": kind == "price" and row[""]["조달"].get("") is not None,
"": _lowest(row),
"관급": row.get("관급") is not None,
}
)
return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]}
+1 -1
View File
@@ -1,6 +1,6 @@
/* =============================================================================
* M01_MasterData_UI_Pick.ts
* 고르기 모달 — 조달 자료(나라장터) · 가격 연결(시중물가) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌
* 고르기 모달 — 가격 연결(자재품목) · 준용 직종을 이름·규격으로 찾아 고르면 ref 를 돌려줌
* ========================================================================== */
import { el, showToast } from "@ui/ui_template_elements";
+4 -38
View File
@@ -21,23 +21,18 @@ import {
buildCell,
buildPager,
coerce,
deep,
flatten,
isScalar,
SEP,
show,
withCell,
withDeep,
} from "./M01_MasterData_UI_Cells";
const SIZE = 50;
const MARKET = "재료_시중물가.json";
const MARKET = "재료_자재품목.json";
const LINKED = "재료_품셈재료.json";
const JOB = ["인력.json"];
const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음
const SUPPLY = ["값", "조달"];
const SOURCE = [...SUPPLY, "출처"];
const NARA = "나라장터:";
const FALLBACK: Row = { : "", : "", : "", : "", : "", : null, : "" };
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
@@ -58,9 +53,9 @@ function withNote(row: Row, text: string): Row {
return out;
}
/** 요소별 전용 칸으로 그리는 칸 — 일반 칸에서 뺌. */
/** 요소별 전용 칸으로 그리는 칸 · 접어 두는 칸 — 일반 칸에서 뺌. */
function hidden(file: string, col: string): boolean {
if (file === MARKET) return col === `${SEP}조달`;
if (file === MARKET) return col.split(SEP)[0] === "면수"; // 면수는 접어 둠
if (file === LINKED) return col.startsWith("연결");
return JOB.includes(file) && col === "준용";
}
@@ -71,7 +66,6 @@ const fixed = (file: string, col: string): boolean =>
(JOB.includes(file) && ["구분", "상세구분", "상태", "옛이름"].includes(col.split(SEP)[0]));
const HEADS: Record<string, string[]> = {
[MARKET]: ["조달 값", "조달 기준", "M01_ProcureCol"],
[LINKED]: ["M01_PriceCol"],
[JOB[0]]: ["M01_JobCol"],
};
@@ -179,7 +173,6 @@ export function renderRows(
const put = deleted
? undefined
: (next: Row): void => editRow(file, d.version, key, row, next);
if (file === MARKET) tr.append(...marketCells(row, cur, put));
if (file === LINKED) tr.append(linkedCell(row, cur, put));
tr.append(
buildCell(show(cur[NOTE]), {
@@ -271,34 +264,7 @@ type Put = (row: Row) => void;
const rowBtn = (text: string): HTMLButtonElement =>
el("button", { className: "m01-master__row-btn", text, attrs: { type: "button" } });
/** 시중물가 — 조달 값 · 기준 · 조달 연결(「나라장터:<키>」 · 「가격정보」 줄은 원천이라 그대로). */
function marketCells(row: Row, cur: Row, put?: Put): HTMLTableCellElement[] {
const link = show(deep(cur, SOURCE));
const td = buildCell(named(link), { changed: link !== show(deep(row, SOURCE)) });
if (put && (!link || link.startsWith(NARA))) {
const btn = rowBtn(L("M01_ProcureFind"));
btn.addEventListener("click", () =>
openPickModal({
kind: "procure",
title: L("M01_ProcureTitle"),
current: link,
seed: `${cur["이름"] ?? ""} ${cur["규격"] ?? ""}`.trim(),
onPick: (ref, name) => {
remember(ref, name);
put(withDeep(cur, SOURCE, ref));
},
}),
);
td.append(btn);
}
return [
buildCell(show(deep(cur, [...SUPPLY, "값"])), { changed: false }),
buildCell(show(deep(cur, [...SUPPLY, "기준"])), { changed: false }),
td,
];
}
/** 품셈재료 — 가격 연결(시중물가 키 · 후보 조건 · 없음) · 고르기 모달. */
/** 품셈재료 — 가격 연결(자재품목 키 · 후보 조건 · 없음) · 고르기 모달. */
function linkedCell(row: Row, cur: Row, put?: Put): HTMLTableCellElement {
const link = cur["연결"];
const td = buildCell(named(linkText(link)), {
File diff suppressed because it is too large Load Diff
+58 -30
View File
@@ -13,33 +13,33 @@
- 날짜 없음 · 마스터는 항상 최신. UTF-8 · LF · 들여쓰기 2칸 · 칸 이름은 한글.
- 그룹 = 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 · 로직
| 그룹 | 파일 이름 |
|---|---|
| 인력 | `인력.json` 하나 — 줄마다 `조사` |
| 기계 | `기계.json` 하나 — 줄마다 `세부분류` |
| 재료 | `재료_<원문>.json`시중물가 · 나라장터자재 · 오피넷유가 · 품셈재료 |
| 환율 · 요율 | `환율_한국은행환율.json` · `요율_조달청제비율.json` |
| 그룹 | 파일 이름 |
| -------------------- | --------------------------------------------------------------------------------------- |
| 인력 | `인력.json` 하나 — 줄마다 `조사` |
| 기계 | `기계.json` 하나 — 줄마다 `세부분류` |
| 재료 | `재료_<원문>.json`자재품목 · 나라장터자재 · 오피넷유가 · 품셈재료 |
| 환율 · 요율 | `환율_한국은행환율.json` · `요율_조달청제비율.json` |
| 소요량 · 계수 · 로직 | `<그룹>_<원문>_<NN>장_<장 제목>.json` — 예 `소요량_건설품셈_10장_창호_및_유리공사.json` |
- 장 파일 — 장 번호는 두 자리 · 장 제목은 원문 본문 폴더 이름 그대로 · 부문은 이름에 넣지 않음.
- 장 파일 머리 — `부문`(건설품셈만 · 공통 · 토목 · 건축 · 기계설비 · 유지관리) · `차례`(그룹 안 목록 차례: 산림 1~14장 → 건설 공통 → 토목 → 건축 → 기계설비 → 유지관리).
- 원문 이름 = 산림품셈 · 건설품셈 · 건설노임 · 제조노임 · 엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 · 준용대상 · 나라장터자재 · 오피넷유가 · 시중물가 · 한국은행환율 · 조달청제비율 · 품셈재료 · 자체
- 원문 이름 = 산림품셈 · 건설품셈 · 건설노임 · 제조노임 · 엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 · 준용대상 · 나라장터자재 · 오피넷유가 · 자재품목 · 한국은행환율 · 조달청제비율 · 품셈재료 · 자체
## 3. 키
- 모든 요소 줄 · 표 · 로직에 `키` = 테이블ID(영문 대문자 2) + 6자리 일련번호 — `LB000123`. 특수문자·한글 없음.
- 한 번 준 키는 안 바뀜 · 지운 줄의 키는 다시 쓰지 않음. 키는 전체에서 하나.
- 원문이 준 번호·옛 열쇠(직종 `1018` · 기계 `0201-0070` · `13-4-1 메쌓기` · 시중물가 코드)는 `원문번호` 칸 — 같은 테이블 안에서 하나.
- 원문이 준 번호·옛 열쇠(직종 `1018` · 기계 `0201-0070` · `13-4-1 메쌓기` · 자재품목 코드)는 `원문번호` 칸 — 같은 테이블 안에서 하나.
- 대장 `_키대장.json` = 키 ↔ 원문번호 ↔ 파일 · 테이블별 `다음` 번호. 새 줄은 대장의 다음 번호를 받음(M01 저장 · 빌더).
| ID | 테이블 | ID | 테이블 |
|---|---|---|---|
| LB | 인력 | QF · QC | 소요량 산림 · 건설 |
| MT | 재료 시중물가 | CF · CC | 계수 산림 · 건설 |
| MN | 재료 나라장터 | FX | 환율 |
| MO | 재료 유가 | RT | 요율 |
| MP | 재료 품셈재료 | GF · GC · GX | 로직 산림 · 건설 · 자체 |
| EQ | 기계 | | |
| ID | 테이블 | ID | 테이블 |
| --- | ------------- | ------------ | ----------------------- |
| LB | 인력 | QF · QC | 소요량 산림 · 건설 |
| MT | 재료 자재품목 | CF · CC | 계수 산림 · 건설 |
| MN | 재료 나라장터 | FX | 환율 |
| MO | 재료 유가 | RT | 요율 |
| MP | 재료 품셈재료 | GF · GC · GX | 로직 산림 · 건설 · 자체 |
| EQ | 기계 | | |
- 빌더는 옛 모양(`열쇠` · 원문별 파일)으로 쓰고 끝에 `scripts/master_keys.py``adopt` 를 부름 — 키·원문번호·한 테이블·장 파일 이름으로 받음. 옛 이름으로 읽기는 `old_view`.
@@ -56,10 +56,35 @@
"그룹": "인력",
"비고": ["열 한 벌 · 상태 · 준용 · 새 조사 등록 …"],
"구분": {
"건설업": { "원문": "건설노임", "기관": "대한건설협회", "판": "2026-01-01", "공표일": "2025-12-31", "원천": [{ "경로": "resources/knowledge/original/원가계산/…", "해시": "…", "역할": "primary" }], "상세구분": [], "비고": ["…"] }
"건설업": {
"원문": "건설노임",
"기관": "대한건설협회",
"판": "2026-01-01",
"공표일": "2025-12-31",
"원천": [
{ "경로": "resources/knowledge/original/원가계산/…", "해시": "…", "역할": "primary" }
],
"상세구분": [],
"비고": ["…"]
}
},
"줄": [
{ "키": "LB000018", "원문번호": "1018", "구분": "건설업", "상세구분": null, "이름": "포설공", "규격": "", "단위": "원/일", "값": 221015, "출처": "건설노임 1018", "일시간": 8, "상태": "공표", "옛이름": [], "비고": null, "준용": null }
{
"키": "LB000018",
"원문번호": "1018",
"구분": "건설업",
"상세구분": null,
"이름": "포설공",
"규격": "",
"단위": "원/일",
"값": 221015,
"출처": "건설노임 1018",
"일시간": 8,
"상태": "공표",
"옛이름": [],
"비고": null,
"준용": null
}
]
}
```
@@ -81,12 +106,19 @@
재료 값:
- `재료_시중물가` `값` = `{ "시중": { 가격정보 · 물가자료 · 유통물가 · 물가정보 · 거래가격, "면수": {…} }, "조달": { "값", "출처", "기준" } }` — 기본가·채택값 · 견적가 두지 않음. 조달 값이 없으면 비움.
- 조달 `출처` = 「가격정보」(시중물가 원천의 조달청 관급 단가 · `기준` = 원천 면수 글) · 「나라장터:<키>」(값 안 베낌 · 이름·규격·단위가 온전히 같은 짝만 · 애매하면 사람이 화면에서 이음).
- `재료_나라장터자재` = 조달 자료 — 머리 `자료` 에 밝힘.
**마스터는 값을 고르는 곳이 아니라 프로젝트에 쓸 데이터를 모으는 곳** — 최저가·견적 고르기는 프로젝트 몫.
- `재료_자재품목` = 자재 테이블 하나 · **열 한 벌** — 키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수 · 모든 줄이 모든 칸(없으면 `null`).
- 값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · `관급`(조달) — 채택값·기본가·견적가 두지 않음.
- `관급` = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 `비고` 「조달 26.7」 · 「조달 ~29.3.18」.
- 그 밖의 줄에 남은 「가격정보」 값은 출처가 조달인지 물가지인지 원문이 안 가름 — 어느 값 열에도 넣지 않고 `비고` 「가격정보 1,825 · 2026.7」 로만(72 줄).
- `구분` · `상세구분` = 원문(ESTX)에 대·중분류 칸이 없어 비움 — 지어내지 않음.
- `면수` = 물가지 값의 원문 면수 묶음 `{ 물가자료, 유통물가, 물가정보, 거래가격 }` · 없으면 `null` · 화면에는 접어 둠.
- `재료_오피넷유가` · `환율_한국은행환율` 도 열 한 벌 — 키 · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 값 · 출처 · 비고. 유가 `구분` = 전국평균 · 시도별 · `상세구분` = 지역 이름 · `규격` = 기준일. 환율은 `규격` = 기준일 · 구분·상세구분 비움.
- `재료_나라장터자재` = 조달 자료(관급 단가) — 머리 `자료` 에 밝힘 · 자재품목과 잇지 않음 · 로직 호표가 키로 바로 가리킴.
- `재료_품셈재료` = 품셈이 부르는 재료 목록 — `{ 키, 원문번호, 이름, 규격, 단위, 요구절: [출처 모양], 연결 }` · 값 칸 없음.
- `연결` = 시중물가 키 하나 · 후보 조건 `{ 이름, 규격, 지역: "입력" }`(지역이 갈리는 레미콘·골재·아스콘) · `null`(아직 못 이음).
- 단가 = 연결된 줄의 시중·조달 값 가운데 낮은 값 · 호표 결과 줄에 `출처`(그 값의 칸). 후보 조건 = 품명 뿌리가 `이름` 으로 끝남 · 이름에 로직 입력 `자재지역` 글자 · 단위 같음 · 규격이 둘 이상 갈리면 멈춤 「후보 여럿」 · 그 지역 줄이 없으면 가까운 시군을 고르지 않고 멈춤 「지역 값 없음」.
- `연결` = 자재품목 키 하나 · 후보 조건 `{ 이름, 규격, 지역: "입력" }`(지역이 갈리는 레미콘·골재·아스콘) · `null`(아직 못 이음).
- 단가 = 연결된 줄의 값 열 다섯 가운데 낮은 값 · 호표 결과 줄에 `출처`(`<키>.<열>`) — **시험 계산용**. 후보 조건 = 품명 뿌리가 `이름` 으로 끝남 · 이름에 로직 입력 `자재지역` 글자 · 단위 같음 · 규격이 둘 이상 갈리면 멈춤 「후보 여럿」 · 그 지역 줄이 없으면 가까운 시군을 고르지 않고 멈춤 「지역 값 없음」.
## 6. 표형 요소 (소요량 · 계수)
@@ -101,16 +133,14 @@
"출처": "산림품셈 13-4-1",
"조건": { "뒷길이": "수", "돌": "고르기", "쌓기": "고르기" },
"값칸": { "석공": "인", "보통인부": "인" },
"줄": [
{ "뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "석공": 0.5, "보통인부": 0.4 }
],
"줄": [{ "뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "석공": 0.5, "보통인부": 0.4 }],
"주": ["① …"]
}
```
- 건설품셈 표 · 로직의 원문번호 앞에 부문 — 「건축 1-1-1 계수」 · 「공통 8 기계 시간당 경비」.
- 조건 종류 — `수`(같은 값) · `고르기`(글자) · `범위`(줄에 `[아래, 위]` · 끝 열림은 `null` · 포함 여부는 `범위규칙` 에 「초과·이하」처럼).
- `범위규칙` 첫 마디 = 엔진이 읽는 이름 — 「초과·이하」 · 「이상·미만」 · 「이하」(위 끝만) · 「이상·이하」(양 끝 포함). 뒤 마디(` · ` 뒤)는 원문 설명. 「이하」 · 「이상·이하」는 여러 줄이 맞으면 위 끝이 가장 작은 줄.
- `범위규칙` 첫 마디 = 엔진이 읽는 이름 — 「초과·이하」 · 「이상·미만」 · 「이하」(위 끝만) · 「이상·이하」(양 끝 포함). 뒤 마디(`·` 뒤)는 원문 설명. 「이하」 · 「이상·이하」는 여러 줄이 맞으면 위 끝이 가장 작은 줄.
- 원문 「-」 칸은 줄에서 그 값 칸을 뺌. 줄 전체가 「-」 면 줄을 뺌.
- 원문에 없는 조건 칸은 줄에서 뺌 — 빈 조건 칸은 그 조건에 무엇이든 맞음.
- 값 칸에 범위(「80 ∼ 100」)는 `[아래, 위]` — 로직은 설계자 입력 칸으로 받음(기본값 두지 않음).
@@ -141,9 +171,7 @@
{ "이름": "돌", "고르기": ["견치돌", "깬돌", "깬잡석", "호박돌 및 야면석"] }
],
"중간": [{ "이름": "증가율", "식": "…" }],
"호표": [
{ "종류": "인력", "요소": "LB000002", "단위": "인", "수량": "…", "비목": "노무비" }
],
"호표": [{ "종류": "인력", "요소": "LB000002", "단위": "인", "수량": "…", "비목": "노무비" }],
"덧줄": [{ "이름": "잡재료비", "식": "노무비 * 0.05", "비목": "재료비", "출처": "…" }],
"끝수": "원 미만 버림"
}
+19 -19
View File
@@ -14,26 +14,26 @@
## 2. 읽기
| 길 | 받음 | 줌 |
|---|---|---|
| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 |
| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version, order}]}` — 머리 `차례` 순 |
| `GET /subs` | `file` | `{file, version, slot, subs: [{name, book, details}]}` — 인력 `구분` · 기계 `세부분류`(머리에 등록된 것) · 그 밖은 빈 목록 |
| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` · `sub` · `detail` | `{file, version, total, page, size, rows: [줄…]}` |
| `GET /tables` | `file` · `q` | `{file, version, tables: [{키, 원문번호, 이름, 기준, 출처, 조건, 값칸, count}]}` |
| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` |
| `GET /logics` | `book` · `chapter` · `q` · `blocked`(0/1) | `{logics: [{file, book, chapter, 키, 원문번호, 이름, 결과단위, 출처, blocked, reasons}]}` |
| `GET /logic` | `key`(`book` 은 안 씀) | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` |
| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 원문번호, 이름, 규격, 단위, 값, 값칸}]}` |
| 길 | 받음 | 줌 |
| --------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 |
| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version, order}]}` — 머리 `차례` |
| `GET /subs` | `file` | `{file, version, slot, subs: [{name, book, details}]}` — 인력 `구분` · 기계 `세부분류`(머리에 등록된 것) · 그 밖은 빈 목록 |
| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` · `sub` · `detail` | `{file, version, total, page, size, rows: [줄…]}` |
| `GET /tables` | `file` · `q` | `{file, version, tables: [{키, 원문번호, 이름, 기준, 출처, 조건, 값칸, count}]}` |
| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` |
| `GET /logics` | `book` · `chapter` · `q` · `blocked`(0/1) | `{logics: [{file, book, chapter, 키, 원문번호, 이름, 결과단위, 출처, blocked, reasons}]}` |
| `GET /logic` | `key`(`book` 은 안 씀) | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` |
| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 원문번호, 이름, 규격, 단위, 값, 값칸}]}` |
- `rows` 는 요소·로직 파일은 `줄`, 표형 파일(소요량·계수)은 `표` 를 줄로 봄.
- `q` = 찾기 — `키`·`원문번호`·`이름`·`옛이름` 에 든 글(대소문자 무시).
- `sub` = 인력 `구분`·기계 `세부분류` 거름 · `detail` = 인력 `상세구분` 거름 · 빈 글 = 전체.
- `chapter` = 「부문 NN장 장 제목」(`공통 03장 토공사` · `13장 구조물`) · 장 파일 아니면 "".
- `blocked` = 로직 검사(`check_master.py 로직`)에 걸림 · `reasons` = 걸린 까닭 글.
- `prices` = 호표 `요소``{ref, 이름, 규격, 단위, 값}`(단가 자동 칸) · 없는 요소는 `null` · `{이름}` 이 낀 원문번호·하위 로직 줄은 빠짐(계산 때 정해짐). 품셈재료는 `값` = 연결을 따라간 낮은 값 · `출처` = 그 값의 (`<시중물가 키>.시중.<칸>` · `….조달` · `<나라장터자재 키>`) · 자재지역 후보는 `값` null(계산 때 정해짐).
- `prices` = 호표 `요소``{ref, 이름, 규격, 단위, 값}`(단가 자동 칸) · 없는 요소는 `null` · `{이름}` 이 낀 원문번호·하위 로직 줄은 빠짐(계산 때 정해짐). 품셈재료는 `값` = 연결을 따라간 낮은 값(시험 계산용) · `출처` = 그 값의 (`<자재품목 키>.<열>`) · 자재지역 후보는 `값` null(계산 때 정해짐).
- `elements` = 요소 찾기 창 — 그룹 전체에서 `q` 찾기 · `ref` = 식에 넣는 키 · 표형 그룹은 `값칸` 이 옴.
- `pick` = 고르기 창 — `procure` ref `나라장터:<키>` · `price` ref 키 · `job` = `인력.json` 값 있는 공표 직종(구분 「미확보」 빼고) ref 키.
- `pick` = 고르기 창 — `price` = `재료_자재품목.json`(값 = 값 열 다섯 가운데 낮은 값 · `관급` = 관급 값 있음) ref 키 · `job` = `인력.json` 값 있는 공표 직종(구분 「미확보」 빼고) ref 키.
## 3. 시험 계산
@@ -53,13 +53,13 @@
- 화면은 조작을 캐시에 쌓고 [저장] 한 번에 보냄 — 서버는 이때만 파일에 씀.
- 순서: 모든 `version` 대조 → 모두 적용(메모리) → 검사 → 모두 씀. 하나라도 걸리면 아무것도 안 씀.
| 답 | 뜻 | 몸 |
|---|---|---|
| 200 | 씀 | `{files: [{file, version}]}` — 새 판본 |
| 답 | 뜻 | 몸 |
| --- | ------------------------------- | -------------------------------------------------- |
| 200 | 씀 | `{files: [{file, version}]}` — 새 판본 |
| 409 | 낡은 화면 — 그 사이 파일이 바뀜 | `{detail: {stale: [file…]}}` → 다시 읽고 다시 고침 |
| 422 | 검사 걸림 | `{detail: {errors: [글…]}}` |
| 404 | 없는 파일·키 | `{detail}` |
| 400 | 틀린 `op` · 고친 줄의 키를 바꿈 | `{detail}` |
| 422 | 검사 걸림 | `{detail: {errors: [글…]}}` |
| 404 | 없는 파일·키 | `{detail}` |
| 400 | 틀린 `op` · 고친 줄의 키를 바꿈 | `{detail}` |
- 검사 = `check_master.py` 의 틀(고친 파일) + 로직 변수(모든 로직 파일, 고친 뒤 마스터 기준). **고친 뒤 새로 생긴 걸림**만 막음 — 원래 있던 걸림은 막지 않음. 원문번호 겹침(테이블 안)도 걸림.
- 요소를 지워 어떤 로직이 가리키던 것이 사라지면 로직 변수 검사에 걸림(422).
@@ -1,183 +0,0 @@
"""상용 적산 자료(ESTX 2026-07) 자재 → 재료_시중물가.json — 개발용 · 배포 전 판권 구매 뒤 갱신.
ESTX 자재 = StuffcDat( : 다산소프트/ESTX_값사전_자재노무기계.md).
= 시중{가격정보·물가자료·유통물가·물가정보·거래가격 + 면수} · 조달{ · 출처 · 기준}.
Cost1..5 = 시중 다섯 · Page1..5 = 면수. TC(채택) · Cost6(견적가) 마스터 아님 .
품명에 관급 줄의 Cost1 = 조달청 관급 단가 조달 묶음으로 옮김(기준 = Page1 조달 26.7 · ~26.5.26).
나라장터 시설공통자재와 이름·규격·단위가 온전히 같고 서로 하나뿐인 줄은 조달 출처 나라장터:<열쇠> 값은 베끼지 않음.
0 · 미저장 = 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 줄은 .
ESTX ·중분류 칸이 없어 분류는 두지 않음.
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_재료_시중물가.py
원본과 대조.
"""
import collections
import hashlib
import json
import math
import random
import re
import sys
import unicodedata
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import master_keys as mk # noqa: E402
from _migrate_base import dump_master # noqa: E402
from compare_estx import ESTX, ROOT, SLOTS, Estx, bare_spec, base_name # noqa: E402
OUT = ROOT / "resources/master_data/재료_시중물가.json"
NARA = ROOT / "resources/master_data/재료_나라장터자재.json"
MARKET = SLOTS[:5] # 견적가(Cost6)는 마스터 아님
_NUM = re.compile(r"\d+(?:\.\d+)?")
_UNIT = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
def num(v):
if not v:
return None
return int(v) if float(v).is_integer() else v
def _base(s) -> str:
s = unicodedata.normalize("NFKC", str(s or "")).lower()
return re.sub(r"\s+", "", re.sub(r"[φΦ∅⌀]", "ø", s))
def _spec(s) -> str:
return _base(s).replace("×", "x").replace("*", "x")
def _unit(s) -> str:
s = _base(s).replace("", "l")
return _UNIT.get(s, s)
def _nara_spec(n: dict) -> str:
"""나라장터 규격 = 「품명, 규격」 — 앞 품명을 뗌."""
head, _, tail = n["규격"].partition(",")
return tail if tail and _base(head) == _base(n["이름"]) else n["규격"]
def nara_links(rows: list[dict], nara: list[dict]) -> dict[str, str]:
"""시중물가 열쇠 → 나라장터 열쇠 — 품명 뿌리 · 규격 수 모음이 맞는 짝 가운데
이름·규격·단위가 온전히 같고 서로 하나뿐인 것만(관급 · 애매한 것은 사람이 화면에서)."""
by = collections.defaultdict(list)
for e in rows:
if e.get("Code") and "관급" not in e.get("Name", ""):
by[base_name(e.get("Name"))].append(e)
pairs = []
for n in nara:
if not n.get(""):
continue
nums = sorted(_NUM.findall(bare_spec(n["이름"], n["규격"])))
for e in by.get(base_name(n["이름"]), []):
if (
nums == sorted(_NUM.findall(bare_spec(e.get("Name"), e.get("Size"))))
and _base(n["이름"]) == _base(e.get("Name"))
and _spec(_nara_spec(n)) == _spec(e.get("Size"))
and _unit(n["단위"]) == _unit(e.get("Unite"))
):
pairs.append((n["열쇠"], e["Code"]))
cn = collections.Counter(n for n, _ in pairs)
ce = collections.Counter(e for _, e in pairs)
return {e: n for n, e in pairs if cn[n] == 1 and ce[e] == 1}
def build(rows: list[dict], links: dict[str, str]) -> list[dict]:
out = []
for r in sorted(rows, key=lambda r: r["Index"]):
code = r.get("Code")
if not code:
continue
market = {slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 1)}
market["면수"] = {slot: r.get(f"Page{i}") or None for i, slot in enumerate(MARKET, 1)}
supply = {"": None, "출처": None, "기준": None}
if "관급" in r.get("Name", "") and market["가격정보"] is not None:
supply = {
"": market["가격정보"],
"출처": "가격정보",
"기준": market["면수"]["가격정보"],
}
market["가격정보"] = market["면수"]["가격정보"] = None
elif code in links:
supply["출처"] = f"나라장터:{links[code]}"
out.append(
{
"열쇠": code,
"이름": r.get("Name", ""),
"규격": r.get("Size", ""),
"단위": r.get("Unite", ""),
"": {"시중": market, "조달": supply},
"출처": f"시중물가 {code}",
}
)
return out
def check(rows: list[dict]) -> None:
"""원본 대조 — 줄 수 · 출처별 값 합(가격정보 = 시중 + 조달) · 표본 쉰 줄."""
data = json.loads(OUT.read_text(encoding="utf-8"))
got = {r["열쇠"]: r for r in data[""]}
raw = {r["Code"]: r for r in rows if r.get("Code")}
assert len(got) == len(data[""]) == len(raw), (len(got), len(raw))
print(
f"{len(got)} = 원본 코드 있는 줄 {len(raw)} (코드 없는 빈 줄 {len(rows) - len(raw)} 뺌)"
)
for i, slot in enumerate(MARKET, 1):
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
b = math.fsum(r[""]["시중"][slot] or 0 for r in got.values())
if slot == "가격정보":
b += math.fsum(r[""]["조달"][""] or 0 for r in got.values())
n = sum(1 for r in got.values() if r[""]["시중"][slot] is not None)
assert abs(a - b) < 1e-3, (slot, a, b)
print(f" {slot}: 시중 값 {n}줄 · 합 {b:,.1f} = 원본 {a:,.1f}")
supply = collections.Counter(
(r[""]["조달"]["출처"] or "없음").split(":")[0] for r in got.values()
)
print(f" 조달: {dict(supply)}")
random.seed(20260719)
for code in random.sample(sorted(raw), 50):
r, g = raw[code], got[code]
assert (g["이름"], g["규격"], g["단위"]) == (
r.get("Name", ""),
r.get("Size", ""),
r.get("Unite", ""),
)
moved = g[""]["조달"]["출처"] == "가격정보"
for i, slot in enumerate(MARKET, 1):
have = g[""]["조달"][""] if moved and i == 1 else g[""]["시중"][slot]
page = g[""]["조달"]["기준"] if moved and i == 1 else g[""]["시중"]["면수"][slot]
assert (have or 0) == (r.get(f"Cost{i}") or 0), (code, slot)
assert page == (r.get(f"Page{i}") or None), (code, slot)
print(" 표본 50줄 이름·규격·단위·값 5·면수 5 같음")
def main():
rows = Estx().table("StuffcDat")
head = {
"그룹": "재료",
"원문": "시중물가",
"": "2026-07",
"원천": [
{
"경로": ESTX.relative_to(ROOT).as_posix(),
"해시": hashlib.sha256(ESTX.read_bytes()).hexdigest(),
"역할": "primary",
}
],
"비고": [
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
"값 = 시중(물가지 다섯 · 면수) · 조달(값 · 출처 · 기준) — 기본가·채택값 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<키>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
],
}
nara = mk.old_view(NARA.name)[""]
dump_master(OUT, head, build(rows, nara_links(rows, nara)))
check(rows)
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 나라장터 연결 → 키
if __name__ == "__main__":
main()
@@ -0,0 +1,139 @@
"""상용 적산 자료(ESTX 2026-07) 자재 → 재료_자재품목.json — 개발용 · 배포 전 판권 구매 뒤 갱신.
ESTX 자재 = StuffcDat( : 다산소프트/ESTX_값사전_자재노무기계.md).
= · 원문번호 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 관급 · 출처 · 비고 · 면수.
Cost2..5 = 물가지 · Page2..5 = 면수. TC(채택) · Cost6(견적가) 마스터 아님 .
품명에 관급 줄의 Cost1(가격정보) = 조달청 관급 단가 관급 · 기간은 비고 조달 26.7.
밖의 줄에 남은 Cost1 출처가 조달인지 물가지인지 원문이 가름 열에 넣지 않고 비고 가격정보 로만.
0 · 미저장 = 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 줄은 .
ESTX ·중분류 칸이 없어 구분·상세구분은 비움(지어내지 않음).
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_재료_자재품목.py
원본과 대조.
"""
import hashlib
import json
import math
import random
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
import master_keys as mk # noqa: E402
from _migrate_base import dump_master # noqa: E402
from compare_estx import ESTX, ROOT, SLOTS, Estx # noqa: E402
OUT = ROOT / "resources/master_data/재료_자재품목.json"
BASE = SLOTS[0] # Cost1 「가격정보」 — 관급이면 관급 열 · 아니면 비고
MARKET = SLOTS[1:5] # 물가지 넷 · 견적가(Cost6)는 마스터 아님
def num(v):
if not v:
return None
return int(v) if float(v).is_integer() else v
def note(base, page, guard: str | None) -> str | None:
"""비고 — 관급이면 「조달 <기간>」 · 남은 가격정보는 「가격정보 <값> · <면수>」."""
if guard is not None:
return f"조달 {str(guard).removeprefix('조달 ')}".strip()
if base is None:
return None
return " · ".join(x for x in (f"가격정보 {base:,}", page) if x)
def build(rows: list[dict]) -> list[dict]:
out = []
for r in sorted(rows, key=lambda r: r["Index"]):
code = r.get("Code")
if not code:
continue
base, page = num(r.get("Cost1")), r.get("Page1") or None
public = "관급" in r.get("Name", "") and base is not None
면수 = {slot: r.get(f"Page{i}") or None for i, slot in enumerate(MARKET, 2)}
out.append(
{
"열쇠": code,
"구분": None,
"상세구분": None,
"이름": r.get("Name", ""),
"규격": r.get("Size", ""),
"단위": r.get("Unite", ""),
**{slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 2)},
"관급": base if public else None,
"출처": f"자재품목 {code}",
"비고": note(base, page, page if public else None),
"면수": 면수 if any(면수.values()) else None,
}
)
return out
def check(rows: list[dict]) -> None:
"""원본 대조 — 줄 수 · 값 열마다 합 · 관급·가격정보 가름 · 표본 쉰 줄."""
data = json.loads(OUT.read_text(encoding="utf-8"))
got = {r["열쇠"]: r for r in data[""]}
raw = {r["Code"]: r for r in rows if r.get("Code")}
assert len(got) == len(data[""]) == len(raw), (len(got), len(raw))
print(
f"{len(got)} = 원본 코드 있는 줄 {len(raw)} (코드 없는 빈 줄 {len(rows) - len(raw)} 뺌)"
)
for i, slot in enumerate((*MARKET, "관급"), 2):
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
if slot == "관급": # Cost1 가운데 관급 줄만
a = math.fsum(r.get("Cost1") or 0 for r in raw.values() if "관급" in r.get("Name", ""))
b = math.fsum(r[slot] or 0 for r in got.values())
n = sum(1 for r in got.values() if r[slot] is not None)
assert abs(a - b) < 1e-3, (slot, a, b)
print(f" {slot}: 값 {n}줄 · 합 {b:,.1f} = 원본 {a:,.1f}")
left = [r for r in got.values() if str(r["비고"] or "").startswith("가격정보")]
print(
f" 비고에만 남긴 가격정보 {len(left)}줄 · 관급 기간 비고 {sum(1 for r in got.values() if str(r['비고'] or '').startswith('조달'))}"
)
random.seed(20260719)
for code in random.sample(sorted(raw), 50):
r, g = raw[code], got[code]
assert (g["이름"], g["규격"], g["단위"]) == (
r.get("Name", ""),
r.get("Size", ""),
r.get("Unite", ""),
)
public = "관급" in r.get("Name", "")
assert (g["관급"] or 0) == ((r.get("Cost1") or 0) if public else 0), code
for i, slot in enumerate(MARKET, 2):
assert (g[slot] or 0) == (r.get(f"Cost{i}") or 0), (code, slot)
assert (g["면수"] or {}).get(slot) == (r.get(f"Page{i}") or None), (code, slot)
print(" 표본 50줄 이름·규격·단위·값 5·면수 4 같음")
def main():
rows = Estx().table("StuffcDat")
head = {
"그룹": "재료",
"원문": "자재품목",
"": "2026-07",
"원천": [
{
"경로": ESTX.relative_to(ROOT).as_posix(),
"해시": hashlib.sha256(ESTX.read_bytes()).hexdigest(),
"역할": "primary",
}
],
"비고": [
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
"값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · 관급(조달) — 채택값·기본가 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
"관급 = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 비고 「조달 26.7」",
"원문 「가격정보」 칸에 시중 값이 남은 72 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
"구분 · 상세구분 = 원문에 대·중분류 칸이 없어 비움(지어내지 않음)",
"면수 = 물가지 값의 원문 면수 · 화면에는 접어 둠",
],
}
dump_master(OUT, head, build(rows))
check(rows)
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키
if __name__ == "__main__":
main()
+41 -10
View File
@@ -43,7 +43,7 @@ BOOKS = (
"준용대상",
"나라장터자재",
"오피넷유가",
"시중물가",
"자재품목",
"한국은행환율",
"조달청제비율",
"품셈재료",
@@ -89,6 +89,39 @@ LABOR_COLS = (
"준용",
)
LABOR_STATUS = ("공표", "산정", "미공표", "미확보")
# 원문별 열 한 벌 — 모든 줄이 이 칸을 이 차례로 가짐(없으면 null)
COLS = {
"자재품목": (
"",
"원문번호",
"구분",
"상세구분",
"이름",
"규격",
"단위",
"물가자료",
"유통물가",
"물가정보",
"거래가격",
"관급",
"출처",
"비고",
"면수",
),
"오피넷유가": (
"",
"원문번호",
"구분",
"상세구분",
"이름",
"규격",
"단위",
"",
"출처",
"비고",
),
}
COLS["한국은행환율"] = COLS["오피넷유가"]
def load(names: list[str] | None = None, folder: Path = MASTER) -> dict[str, dict]:
@@ -181,13 +214,16 @@ def check_form(name: str, data: dict) -> list[str]:
out.append(f"{where} · {merged}{row.get(merged)}」 이 머리에 없음")
if division and not number.startswith(division + " "):
out.append(f"{where} · 원문번호 앞에 부문 「{division}」 없음")
for need in ROW_KEYS[kind]:
cols = COLS.get(data.get("원문"))
for need in cols or ROW_KEYS[kind]:
if need not in row:
out.append(f"{where} · 칸 「{need}」 없음")
if "출처" in row and not _source_ok(row["출처"]):
out.append(f"{where} · 출처 모양 「{row['출처']}")
if data.get("그룹") == "인력":
out += _check_labor(where, row)
if cols and tuple(row) != cols:
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(cols)}")
if kind == "품셈재료":
out += _check_linked(where, row)
if kind == "요소" and isinstance(row.get(""), dict):
@@ -214,7 +250,7 @@ def _check_labor(where: str, row: dict) -> list[str]:
def _check_linked(where: str, row: dict) -> list[str]:
"""품셈재료 — 값 칸 없음 · 요구절은 출처 모양 · 연결 = 시중물가 키 · 후보 조건 · null."""
"""품셈재료 — 값 칸 없음 · 요구절은 출처 모양 · 연결 = 자재품목 키 · 후보 조건 · null."""
out = [f"{where} · 값 칸을 두지 않음"] if "" in row else []
need = row.get("요구절")
if not (isinstance(need, list) and need and all(_source_ok(x) for x in need)):
@@ -232,22 +268,17 @@ def _check_linked(where: str, row: dict) -> list[str]:
def check_links(whole: mf.Master) -> list[str]:
"""끊긴 키 — 품셈재료 연결 · 시중물가 조달 연결 · 인력 준용이 가리키는 줄."""
"""끊긴 키 — 품셈재료 연결 · 인력 준용이 가리키는 줄."""
out = []
for key, row in whole.index.get("MP", {}).items():
link = row.get("연결")
if isinstance(link, str) and link not in whole.index.get("MT", {}):
out.append(f"{whole.label(key)} · 연결 「{link}」 이 시중물가에 없음")
out.append(f"{whole.label(key)} · 연결 「{link}」 이 자재품목에 없음")
for key, row in whole.index.get("LB", {}).items():
for col in ("준용",):
ref = row.get(col)
if ref not in (None, "") and str(ref) not in whole.index.get("LB", {}):
out.append(f"{whole.label(key)} · {col}{ref}」 없는 인력 키")
nara = whole.index.get("MN", {})
for key, row in whole.index.get("MT", {}).items():
source = str(row[""]["조달"].get("출처") or "")
if source.startswith("나라장터:") and source.split(":", 1)[1] not in nara:
out.append(f"{whole.label(key)} · 조달 「{source}」 이 나라장터자재에 없음")
return out
@@ -669,4 +669,4 @@ def find_loops(graph: dict[str, set]) -> list[list[str]]:
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
from master_material import LINKED, REGION, element # noqa: E402, F401
from master_material import LINKED, REGION, SLOTS, element # noqa: E402, F401
+7 -11
View File
@@ -18,8 +18,8 @@ KEY = re.compile(r"[A-Z]{2}\d{6}")
# (그룹, 원문) → 테이블ID · 인력·기계는 원문 구분 없이 한 테이블
TABLES = {
("인력", None): "LB",
("재료", "시중물가"): "MT",
("재료", "나라장터자재"): "MN",
("재료", "자재품목"): "MT",
("재료", "나라장터자재"): "MN", # 자재품목과 잇지 않음 — 로직 호표가 키로 바로 가리킴
("재료", "오피넷유가"): "MO",
("재료", "품셈재료"): "MP",
("기계", None): "EQ",
@@ -54,7 +54,10 @@ def load_book(path: Path = BOOK) -> dict:
def save_book(book: dict, path: Path = BOOK) -> None:
"""키 한 줄씩 — 머리 「다음」 뒤에 키 차례대로."""
lines = ["{", ' "다음": ' + json.dumps(book["다음"], ensure_ascii=False) + ",", ' "": {']
lines = ["{", ' "다음": ' + json.dumps(book["다음"], ensure_ascii=False) + ","]
if book.get("폐기"):
lines.append(' "폐기": ' + json.dumps(book["폐기"], ensure_ascii=False) + ",")
lines.append(' "": {')
items = sorted(book[""].items())
for i, (key, v) in enumerate(items):
comma = "," if i < len(items) - 1 else ""
@@ -187,7 +190,7 @@ def _old_ref(book: dict, text: str) -> str:
def _convert(book: dict, row: dict, names: dict) -> dict:
"""줄 속 옛 참조 — 준용 · 후보 글(키 옆 이름) · 품셈재료 연결 · 시중물가 조달 나라장터 연결."""
"""줄 속 옛 참조 — 준용 · 후보 글(키 옆 이름) · 품셈재료 연결."""
for col in ("준용",):
value = row.get(col)
if isinstance(value, str) and ":" in value:
@@ -202,13 +205,6 @@ def _convert(book: dict, row: dict, names: dict) -> dict:
link = row.get("연결")
if isinstance(link, str) and not KEY.fullmatch(link):
row["연결"] = lookup(book).get(("MT", link), link)
value = row.get("")
supply = value.get("조달") if isinstance(value, dict) else None
if isinstance(supply, dict) and str(supply.get("출처") or "").startswith("나라장터:"):
code = supply["출처"].split(":", 1)[1]
if not KEY.fullmatch(code): # 묶음은 새로 만듦 — 옛 줄과 나눠 쓰지 않게
source = "나라장터:" + lookup(book).get(("MN", code), code)
row[""] = {**value, "조달": {**supply, "출처": source}}
return row
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
"""품셈재료 단가 — 연결을 따라 시중물가 줄의 시중·조달 값 가운데 낮은 값 (`_틀.md` 4장 재료 값).
"""품셈재료 단가 — 연결을 따라 자재품목 줄의 값 열 다섯 가운데 낮은 값 (`_틀.md` 4장 재료 값).
낮은 고르기는 **시험 계산용**. 최저가·견적 고르기는 프로젝트 마스터는 값을 모으기만 .
`master_formula` 요소 값을 부름. 서로 부르는 모듈이라 `master_formula` 끝에서 읽음.
"""
@@ -11,7 +13,7 @@ import unicodedata
from decimal import Decimal
LINKED = "MP" # 품셈재료 테이블ID
MARKET = ("가격정보", "물가자료", "유통물가", "물가정보", "거래가격")
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
_UNITS = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
@@ -32,21 +34,9 @@ def plain_unit(s) -> str:
def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]:
"""시중물가 줄 하나의 (값, 출처) — 시중 다섯 칸 · 조달(나라장터 연결은 그 줄 값)."""
value = master.get(key)[""]
out = [
(value["시중"][slot], f"{key}.시중.{slot}")
for slot in MARKET
if value["시중"].get(slot) is not None
]
supply = value["조달"]
if supply.get("") is not None:
out.append((supply[""], f"{key}.조달"))
elif str(supply.get("출처") or "").startswith("나라장터:"):
ref = supply["출처"].split(":", 1)[1]
if master.get(ref).get("") is not None:
out.append((master.get(ref)[""], ref))
return out
"""자재품목 줄 하나의 (값, 출처) — 값이 있는 열마다 하나 · 출처는 그 열 이름."""
row = master.get(key)
return [(row[slot], f"{key}.{slot}") for slot in SLOTS if row.get(slot) is not None]
def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]:
File diff suppressed because it is too large Load Diff
@@ -15,44 +15,45 @@
}
],
"비고": [
"시도별 유가 — 품셈 8-1-7 5호 「유류가격은 해당지역의 가격으로 한다」. 코드 00(전국)은 원문 그대로 두되 지역 선택지에서는 전국평균 판이 맡는다. ⚠ 원천이 시도를 17줄로 주는데 그 가름이 행정구역과 다르다 — 코드 20 이 「전남광주」 한 줄이고 07(전남)·16(광주)은 아예 없다. 원문 이름을 그대로 두고 우리가 쪼개지 않는다."
"시도별 유가 — 품셈 8-1-7 5호 「유류가격은 해당지역의 가격으로 한다」. 코드 00(전국)은 원문 그대로 두되 지역 선택지에서는 전국평균 판이 맡는다. ⚠ 원천이 시도를 17줄로 주는데 그 가름이 행정구역과 다르다 — 코드 20 이 「전남광주」 한 줄이고 07(전남)·16(광주)은 아예 없다. 원문 이름을 그대로 두고 우리가 쪼개지 않는다.",
"규격 = 기준일 · 구분 = 전국평균/시도별 · 상세구분 = 지역 이름"
],
"줄": [
{ "키": "MO000001", "원문번호": "휘발유:전국평균", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1863.25, "출처": "오피넷유가 B027", "구분": "전국평균", "지역": "전국평균", "기준일": "2026-08-14", "상품코드": "B027" },
{ "키": "MO000002", "원문번호": "경유:전국평균", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1846.39, "출처": "오피넷유가 D047", "구분": "전국평균", "지역": "전국평균", "기준일": "2026-08-14", "상품코드": "D047" },
{ "키": "MO000003", "원문번호": "휘발유:전국", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1858.93, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "전국", "지역코드": "00", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000004", "원문번호": "휘발유:서울", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1905.02, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "서울", "지역코드": "01", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000005", "원문번호": "휘발유:경기", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1855.78, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "경기", "지역코드": "02", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000006", "원문번호": "휘발유:강원", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1870.23, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "원", "지역코드": "03", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000007", "원문번호": "휘발유:충북", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1866.12, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "충북", "지역코드": "04", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000008", "원문번호": "휘발유:충남", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1864.78, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "충남", "지역코드": "05", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000009", "원문번호": "휘발유:전북", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1859.7, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "전북", "지역코드": "06", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000010", "원문번호": "휘발유:경북", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1852.71, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "경북", "지역코드": "08", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000011", "원문번호": "휘발유:경남", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1854.71, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "경남", "지역코드": "09", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000012", "원문번호": "휘발유:부산", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1842.66, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "부산", "지역코드": "10", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000013", "원문번호": "휘발유:제주", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1889.27, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "제주", "지역코드": "11", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000014", "원문번호": "휘발유:대구", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1831.78, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "대구", "지역코드": "14", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000015", "원문번호": "휘발유:인천", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1845.64, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "인천", "지역코드": "15", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000016", "원문번호": "휘발유:대전", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1836.79, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "대전", "지역코드": "17", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000017", "원문번호": "휘발유:울산", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1838.49, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "울산", "지역코드": "18", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000018", "원문번호": "휘발유:세종", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1855.8, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "세종", "지역코드": "19", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000019", "원문번호": "휘발유:전남광주", "이름": "휘발유", "규격": "", "단위": "원/L", "값": 1862.28, "출처": "오피넷유가 B027", "구분": "시도별", "지역": "전남광주", "지역코드": "20", "기준일": "2026-09-09", "상품코드": "B027" },
{ "키": "MO000020", "원문번호": "경유:전국", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1843.79, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "전국", "지역코드": "00", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000021", "원문번호": "경유:서울", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1886.15, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "서울", "지역코드": "01", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000022", "원문번호": "경유:경기", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1838.22, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "경기", "지역코드": "02", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000023", "원문번호": "경유:강원", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1859.9, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "원", "지역코드": "03", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000024", "원문번호": "경유:충북", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1851.55, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "충북", "지역코드": "04", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000025", "원문번호": "경유:충남", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1850.08, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "충남", "지역코드": "05", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000026", "원문번호": "경유:전북", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1846.86, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "전북", "지역코드": "06", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000027", "원문번호": "경유:경북", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1838.1, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "경북", "지역코드": "08", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000028", "원문번호": "경유:경남", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1837.71, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "경남", "지역코드": "09", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000029", "원문번호": "경유:부산", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1825.55, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "부산", "지역코드": "10", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000030", "원문번호": "경유:제주", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1869.16, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "제주", "지역코드": "11", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000031", "원문번호": "경유:대구", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1815.72, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "대구", "지역코드": "14", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000032", "원문번호": "경유:인천", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1829.14, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "인천", "지역코드": "15", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000033", "원문번호": "경유:대전", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1824.58, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "대전", "지역코드": "17", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000034", "원문번호": "경유:울산", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1826.63, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "울산", "지역코드": "18", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000035", "원문번호": "경유:세종", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1841.72, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "세종", "지역코드": "19", "기준일": "2026-09-09", "상품코드": "D047" },
{ "키": "MO000036", "원문번호": "경유:전남광주", "이름": "자동차용경유", "규격": "", "단위": "원/L", "값": 1850.47, "출처": "오피넷유가 D047", "구분": "시도별", "지역": "전남광주", "지역코드": "20", "기준일": "2026-09-09", "상품코드": "D047" }
{ "키": "MO000001", "원문번호": "휘발유:전국평균", "구분": "전국평균", "상세구분": "전국평균", "이름": "휘발유", "규격": "2026-08-14", "단위": "원/L", "": 1863.25, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000002", "원문번호": "경유:전국평균", "구분": "전국평균", "상세구분": "전국평균", "이름": "자동차용경유", "규격": "2026-08-14", "단위": "원/L", "": 1846.39, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000003", "원문번호": "휘발유:전국", "구분": "시도별", "상세구분": "전국", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1858.93, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000004", "원문번호": "휘발유:서울", "구분": "시도별", "상세구분": "서울", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1905.02, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000005", "원문번호": "휘발유:경기", "구분": "시도별", "상세구분": "경기", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1855.78, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000006", "원문번호": "휘발유:강원", "구분": "시도별", "상세구분": "강원", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1870.23, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000007", "원문번호": "휘발유:충북", "구분": "시도별", "상세구분": "충북", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1866.12, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000008", "원문번호": "휘발유:충남", "구분": "시도별", "상세구분": "충남", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1864.78, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000009", "원문번호": "휘발유:전북", "구분": "시도별", "상세구분": "전북", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1859.7, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000010", "원문번호": "휘발유:경북", "구분": "시도별", "상세구분": "경북", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1852.71, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000011", "원문번호": "휘발유:경남", "구분": "시도별", "상세구분": "경남", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1854.71, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000012", "원문번호": "휘발유:부산", "구분": "시도별", "상세구분": "부산", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1842.66, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000013", "원문번호": "휘발유:제주", "구분": "시도별", "상세구분": "제주", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1889.27, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000014", "원문번호": "휘발유:대구", "구분": "시도별", "상세구분": "대구", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1831.78, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000015", "원문번호": "휘발유:인천", "구분": "시도별", "상세구분": "인천", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1845.64, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000016", "원문번호": "휘발유:대전", "구분": "시도별", "상세구분": "대전", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1836.79, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000017", "원문번호": "휘발유:울산", "구분": "시도별", "상세구분": "울산", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1838.49, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000018", "원문번호": "휘발유:세종", "구분": "시도별", "상세구분": "세종", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1855.8, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000019", "원문번호": "휘발유:전남광주", "구분": "시도별", "상세구분": "전남광주", "이름": "휘발유", "규격": "2026-09-09", "단위": "원/L", "": 1862.28, "출처": "오피넷유가 B027", "비고": null },
{ "키": "MO000020", "원문번호": "경유:전국", "구분": "시도별", "상세구분": "전국", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1843.79, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000021", "원문번호": "경유:서울", "구분": "시도별", "상세구분": "서울", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1886.15, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000022", "원문번호": "경유:경기", "구분": "시도별", "상세구분": "경기", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1838.22, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000023", "원문번호": "경유:강원", "구분": "시도별", "상세구분": "강원", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1859.9, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000024", "원문번호": "경유:충북", "구분": "시도별", "상세구분": "충북", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1851.55, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000025", "원문번호": "경유:충남", "구분": "시도별", "상세구분": "충남", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1850.08, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000026", "원문번호": "경유:전북", "구분": "시도별", "상세구분": "전북", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1846.86, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000027", "원문번호": "경유:경북", "구분": "시도별", "상세구분": "경북", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1838.1, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000028", "원문번호": "경유:경남", "구분": "시도별", "상세구분": "경남", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1837.71, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000029", "원문번호": "경유:부산", "구분": "시도별", "상세구분": "부산", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1825.55, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000030", "원문번호": "경유:제주", "구분": "시도별", "상세구분": "제주", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1869.16, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000031", "원문번호": "경유:대구", "구분": "시도별", "상세구분": "대구", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1815.72, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000032", "원문번호": "경유:인천", "구분": "시도별", "상세구분": "인천", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1829.14, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000033", "원문번호": "경유:대전", "구분": "시도별", "상세구분": "대전", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1824.58, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000034", "원문번호": "경유:울산", "구분": "시도별", "상세구분": "울산", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1826.63, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000035", "원문번호": "경유:세종", "구분": "시도별", "상세구분": "세종", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1841.72, "출처": "오피넷유가 D047", "비고": null },
{ "키": "MO000036", "원문번호": "경유:전남광주", "구분": "시도별", "상세구분": "전남광주", "이름": "자동차용경유", "규격": "2026-09-09", "단위": "원/L", "": 1850.47, "출처": "오피넷유가 D047", "비고": null }
]
}
File diff suppressed because it is too large Load Diff
+91 -91
View File
@@ -4,86 +4,86 @@
"판": "2026-09-19",
"비고": [
"품셈이 부르는 재료 목록 — 이름·규격·단위·요구절 · 값 칸 없음",
"연결 = 시중물가 열쇠 하나 · 후보 조건{이름 · 규격 · 지역: 입력} · null(아직 못 이음 — 화면에서 사람이 이음)",
"단가 = 연결된 시중물가 줄의 시중·조달 값 가운데 낮은 값 — 엔진이 호표 줄에 출처를 밝힘",
"연결 = 자재품목 열쇠 하나 · 후보 조건{이름 · 규격 · 지역: 입력} · null(아직 못 이음 — 화면에서 사람이 이음)",
"단가 = 연결된 자재품목 줄의 시중·조달 값 가운데 낮은 값 — 엔진이 호표 줄에 출처를 밝힘",
"후보 조건 = 품명 뿌리가 이름으로 끝나고 이름에 로직 입력 「자재지역」 글자 · 단위 같음 · 규격이 둘 이상 갈리면 멈춤 · 그 지역 줄이 없으면 가까운 시군을 고르지 않고 멈춤"
],
"줄": [
{ "키": "MP000001", "원문번호": "레미콘", "이름": "레디믹스트콘크리트", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-9-1", "산림품셈 12-11-1", "산림품셈 12-11-2-1", "산림품셈 12-12", "산림품셈 12-13", "산림품셈 12-15", "산림품셈 12-16", "산림품셈 12-17-2", "산림품셈 12-17-3", "산림품셈 13-4-4 주①", "산림품셈 13-4-5", "산림품셈 13-5-1 주⑤", "산림품셈 13-5-2", "건설품셈 공통 7-3-1 주⑥ 채움콘크리트"], "연결": {"이름": "레미콘", "규격": "", "지역": "입력"}, "비고": "규격(강도·골재·슬럼프)별 값이 다르면 관리자가 줄을 나눔 · 후보 조건: 지역 입력" },
{ "키": "MP000002", "원문번호": "시멘트", "이름": "시멘트", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤⑥", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 5-4-9", "건설품셈 건축 9-1-1", "건설품셈 건축 9-2-3", "건설품셈 건축 9-3-1"], "연결": null, "비고": "시중물가 후보 36줄 — 지역·규격 선택 필요 (M0005253746, M0005254234, M0005254236 외)" },
{ "키": "MP000003", "원문번호": "모래(중량)", "이름": "모래", "규격": "건조상태", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null },
{ "키": "MP000002", "원문번호": "시멘트", "이름": "시멘트", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤⑥", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 5-4-9", "건설품셈 건축 9-1-1", "건설품셈 건축 9-2-3", "건설품셈 건축 9-3-1"], "연결": "MT004176", "비고": "연결: 자재품목 시멘트 전국 kg 줄" },
{ "키": "MP000003", "원문번호": "모래(중량)", "이름": "모래", "규격": "건조상태", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null, "비고": "자재품목 모래는 ㎥ — kg 환산 안 함" },
{ "키": "MP000004", "원문번호": "모래", "이름": "모래", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-11-3", "산림품셈 12-17-1-2 주③", "건설품셈 공통 6-3-1 주⑤", "건설품셈 건축 9-1-1", "건설품셈 건축 9-3-1", "건설품셈 기계설비 13-6-3", "건설품셈 기계설비 13-6-6"], "연결": {"이름": "모래", "규격": "", "지역": "입력"}, "비고": "후보 조건: 지역 입력" },
{ "키": "MP000005", "원문번호": "자갈(중량)", "이름": "자갈 또는 부순돌", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null },
{ "키": "MP000005", "원문번호": "자갈(중량)", "이름": "자갈 또는 부순돌", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-1-3 주⑤"], "연결": null, "비고": "품명 둘(자갈·부순돌) · 자갈 값은 ㎥" },
{ "키": "MP000006", "원문번호": "자갈", "이름": "자갈", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-17-1-2 주③"], "연결": {"이름": "자갈", "규격": "", "지역": "입력"}, "비고": "후보 조건: 지역 입력" },
{ "키": "MP000007", "원문번호": "철근", "이름": "철근", "규격": "", "단위": "ton", "요구절": ["산림품셈 12-15", "산림품셈 12-16"], "연결": null, "비고": "시중물가 후보 496줄 — 지역·규격 선택 필요 (M0005255422, M0005255423, M0005255424 외)" },
{ "키": "MP000008", "원문번호": "이형철근 SD300 D16", "이름": "이형철근", "규격": "SD300, D16", "단위": "ton", "요구절": ["산림품셈 12-9-1"], "연결": null, "비고": "시중물가 후보 16줄 — 지역·규격 선택 필요 (M0005264522, M0005264546, M0005264570 외)" },
{ "키": "MP000009", "원문번호": "이형철근 SD300 D13", "이름": "이형철근", "규격": "SD300, D13", "단위": "ton", "요구절": ["산림품셈 12-12"], "연결": null, "비고": "시중물가 후보 16줄 — 지역·규격 선택 필요 (M0005264521, M0005264545, M0005264569 외)" },
{ "키": "MP000010", "원문번호": "원형철근 ∅29㎜", "이름": "원형철근", "규격": "∅29㎜, L=500㎜", "단위": "ton", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000011", "원문번호": "강봉 D32㎜", "이름": "강봉", "규격": "D32㎜, L=1000㎜", "단위": "EA", "요구절": ["산림품셈 12-30-1"], "연결": null },
{ "키": "MP000012", "원문번호": "사다리", "이름": "사다리", "규격": "∅19㎜ 아연도금 및 스테인레스", "단위": "ton", "요구절": ["산림품셈 12-16"], "연결": null },
{ "키": "MP000013", "원문번호": "고철", "이름": "고철", "규격": "", "단위": "kg", "요구절": ["산림품셈 9-8-1 주4", "산림품셈 12-34-3"], "연결": null, "비고": "시중물가 후보 6줄 — 지역·규격 선택 필요 (M0005257627, M0005257628, M0005257631 외)" },
{ "키": "MP000014", "원문번호": "폼타이", "이름": "폼타이(Form Tie)", "규격": "D형 1/2인치", "단위": "본", "요구절": ["산림품셈 12-4 주⑦", "건설품셈 공통 6-3-1 주④"], "연결": null },
{ "키": "MP000015", "원문번호": "문양 스티로폴", "이름": "문양 스티로폴(자재비 포함)", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-5"], "연결": null },
{ "키": "MP000016", "원문번호": "커터 블레이드", "이름": "커터 블레이드", "규격": "", "단위": "개", "요구절": ["산림품셈 12-7-1 주③"], "연결": null },
{ "키": "MP000017", "원문번호": "물", "이름": "물", "규격": "", "단위": "", "요구절": ["산림품셈 12-7-1 주④"], "연결": null },
{ "키": "MP000018", "원문번호": "철재 거푸집", "이름": "철재 거푸집(콘크리트 포장)", "규격": "1본 3m", "단위": "본", "요구절": ["산림품셈 12-8 주①④"], "연결": null },
{ "키": "MP000019", "원문번호": "핀폴", "이름": "핀폴", "규격": "", "단위": "개", "요구절": ["산림품셈 12-8 주①④"], "연결": null },
{ "키": "MP000020", "원문번호": "유공관 ∅200㎜", "이름": "유공관", "규격": "∅200㎜", "단위": "m", "요구절": ["산림품셈 12-10"], "연결": null, "비고": "시중물가 후보 2줄 — 지역·규격 선택 필요 (M0005257301, M0005266041)" },
{ "키": "MP000021", "원문번호": "부직포", "이름": "부직포", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-10", "산림품셈 12-23", "건설품셈 공통 2-11-1 재료량", "건설품셈 공통 5-4-1 재료량"], "연결": null, "비고": "시중물가 후보 9줄 — 지역·규격 선택 필요 (M0005256448, M0005256449, M0005256450 외)" },
{ "키": "MP000022", "원문번호": "잡석", "이름": "잡석", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-10", "산림품셈 13-4-2", "산림품셈 13-5-2"], "연결": null },
{ "키": "MP000023", "원문번호": "VR관 ∅800㎜", "이름": "VR관(소켓식)", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000024", "원문번호": "고무링 ∅800㎜", "이름": "고무링(VR관)", "규격": "∅800㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000025", "원문번호": "흄관 ∅800㎜", "이름": "흄관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "시중물가 후보 2줄 — 지역·규격 선택 필요 (M0005259749, M0005265039)" },
{ "키": "MP000026", "원문번호": "파형강관 ∅800㎜", "이름": "파형강관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "시중물가 후보 16줄 — 지역·규격 선택 필요 (M0005265237, M0005265238, M0005265239 외)" },
{ "키": "MP000027", "원문번호": "커플링밴드 ∅800㎜", "이름": "커플링밴드(파형강관)", "규격": "∅800㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null },
{ "키": "MP000028", "원문번호": "VR관 ∅1000㎜", "이름": "VR관(소켓식)", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000029", "원문번호": "고무링 ∅1000㎜", "이름": "고무링(VR관)", "규격": "∅1000㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000030", "원문번호": "흄관 ∅1000㎜", "이름": "흄관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005265041)" },
{ "키": "MP000031", "원문번호": "파형강관 ∅1000㎜", "이름": "파형강관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "시중물가 후보 25줄 — 지역·규격 선택 필요 (M0005265243, M0005265244, M0005265245 외)" },
{ "키": "MP000032", "원문번호": "커플링밴드 ∅1000㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1000㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null },
{ "키": "MP000033", "원문번호": "VR관 ∅1200㎜", "이름": "VR관(소켓식)", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000034", "원문번호": "고무링 ∅1200㎜", "이름": "고무링(VR관)", "규격": "∅1200㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000035", "원문번호": "흄관 ∅1200㎜", "이름": "흄관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005265043)" },
{ "키": "MP000036", "원문번호": "파형강관 ∅1200㎜", "이름": "파형강관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "시중물가 후보 21줄 — 지역·규격 선택 필요 (M0005265246, M0005265247, M0005265248 외)" },
{ "키": "MP000037", "원문번호": "커플링밴드 ∅1200㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1200㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": null },
{ "키": "MP000038", "원문번호": "지수활제", "이름": "지수활제", "규격": "", "단위": "g", "요구절": ["산림품셈 12-11-1"], "연결": null },
{ "키": "MP000039", "원문번호": "모르타르 1:2", "이름": "접합몰탈", "규격": "1:2", "단위": "㎥", "요구절": ["산림품셈 12-11-2-1", "건설품셈 건축 2-1-4", "건설품셈 건축 3-2-2"], "연결": null },
{ "키": "MP000040", "원문번호": "스틸그레이팅", "이름": "집수정 뚜껑", "규격": "스틸그레이팅", "단위": "개", "요구절": ["산림품셈 12-15"], "연결": null, "비고": "시중물가 후보 86줄 — 지역·규격 선택 필요 (M0005256959, M0005256960, M0005256961 외)" },
{ "키": "MP000041", "원문번호": "원형거푸집 벽체", "이름": "원형거푸집(PE 10회)", "규격": "벽체", "단위": "조", "요구절": ["산림품셈 12-16"], "연결": null },
{ "키": "MP000042", "원문번호": "원형거푸집 스라브 및 기초", "이름": "원형거푸집(PE 10회)", "규격": "스라브 및 기초", "단위": "조", "요구절": ["산림품셈 12-16"], "연결": null },
{ "키": "MP000043", "원문번호": "철물", "이름": "철물(강관비계)", "규격": "", "단위": "개", "요구절": ["산림품셈 12-19"], "연결": null, "비고": "시중물가 후보 22줄 — 지역·규격 선택 필요 (M0005250499, M0005250551, M0005250552 외)" },
{ "키": "MP000044", "원문번호": "강관동바리 내관", "이름": "강관동바리 내관", "규격": "48.6㎜×2.4㎜", "단위": "본", "요구절": ["산림품셈 12-20"], "연결": null },
{ "키": "MP000045", "원문번호": "강관동바리 외관", "이름": "강관동바리 외관", "규격": "60.6㎜×2.3㎜", "단위": "본", "요구절": ["산림품셈 12-20"], "연결": null },
{ "키": "MP000046", "원문번호": "아스팔트 ㏊-500", "이름": "아스팔트", "규격": "㏊-500", "단위": "kg", "요구절": ["산림품셈 12-21"], "연결": null, "비고": "나라장터 후보 2줄 — 규격 선택 필요 (20143141, 20143142) · 시중물가 후보 3줄 — 지역·규격 선택 필요 (M0005256192, M0005256193, M0005256251)" },
{ "키": "MP000047", "원문번호": "PVC 파이프 50㎜", "이름": "P.V.C 파이프", "규격": "50㎜", "단위": "", "요구절": ["산림품셈 12-22"], "연결": null, "비고": "원문 단위 없음" },
{ "키": "MP000048", "원문번호": "JOINT FILLER", "이름": "JOINT FILLER", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-27-2", "산림품셈 12-34-4"], "연결": null },
{ "키": "MP000049", "원문번호": "JOINT FILLER 접착제", "이름": "접착제(JOINT FILLER)", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-27-2"], "연결": null },
{ "키": "MP000050", "원문번호": "실런트", "이름": "실런트", "규격": "20×25㎜", "단위": "kg", "요구절": ["산림품셈 12-27-3"], "연결": null, "비고": "해석: 원문 단위 ㎥ 이나 비고 식 0.02×0.025×1400kg/㎥ = 0.7 — kg" },
{ "키": "MP000051", "원문번호": "에폭시 접착제 콘크리트용", "이름": "에폭시 접착제", "규격": "콘크리트용", "단위": "kg", "요구절": ["산림품셈 12-28"], "연결": null, "비고": "나라장터 후보 2줄 — 규격 선택 필요 (20277692, 20277694) · 시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005256159)" },
{ "키": "MP000052", "원문번호": "스페이셔 몰탈 블록", "이름": "스페이셔(몰탈 블록)", "규격": "", "단위": "", "요구절": ["산림품셈 12-29"], "연결": null, "비고": "원문 단위 없음" },
{ "키": "MP000053", "원문번호": "PVC 파이프 Ø35㎜", "이름": "P.V.C 파이프", "규격": "Ø35㎜", "단위": "m", "요구절": ["산림품셈 12-30-1"], "연결": null },
{ "키": "MP000054", "원문번호": "탄성고무받침", "이름": "탄성고무받침", "규격": "150×150×15", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000055", "원문번호": "스티로폴 T20㎜", "이름": "스티로폴", "규격": "T=20㎜", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000056", "원문번호": "TAR PAPER", "이름": "TAR PAPER", "규격": "T=15㎜", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000057", "원문번호": "채움재 아스팔트", "이름": "채움재(아스팔트)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000058", "원문번호": "다웰바캡", "이름": "다웰바캡", "규격": "D=50㎜", "단위": "m", "요구절": ["산림품셈 12-30-2"], "연결": null },
{ "키": "MP000059", "원문번호": "물끊기 홈 재료", "이름": "물끊기 홈(NOTCH) 재료", "규격": "", "단위": "", "요구절": ["산림품셈 12-31"], "연결": null, "비고": "원문 재료 이름·단위 없음" },
{ "키": "MP000060", "원문번호": "하드너", "이름": "콘크리트 표면 강화재(하드너)", "규격": "", "단위": "", "요구절": ["산림품셈 12-35"], "연결": null, "비고": "원문 단위 없음" },
{ "키": "MP000061", "원문번호": "소재", "이름": "소재(원목)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 10-8-2"], "연결": null },
{ "키": "MP000062", "원문번호": "각재·판재", "이름": "각재·판재", "규격": "", "단위": "㎥", "요구절": ["산림품셈 10-8-2"], "연결": null },
{ "키": "MP000063", "원문번호": "목재", "이름": "목재(규준틀)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 11-2", "산림품셈 11-3", "건설품셈 공통 2-5-4", "건설품셈 공통 2-11-1 재료량"], "연결": null, "비고": "해석: 원문 단위 없음 — ㎥ · 시중물가 후보 6줄 — 지역·규격 선택 필요 (M0005250463, M0005250465, M0005250467 외)" },
{ "키": "MP000064", "원문번호": "줄눈 모르타르", "이름": "줄눈메꿈 모르타르", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-4-4 주④", "산림품셈 13-5-1 주③"], "연결": null },
{ "키": "MP000065", "원문번호": "틈메우기돌", "이름": "틈메우기돌", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-5-1 주④"], "연결": null },
{ "키": "MP000066", "원문번호": "철망태", "이름": "철망태(사각형 돌망태)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-11-4"], "연결": null },
{ "키": "MP000067", "원문번호": "돌망태 채움재", "이름": "채움재(돌망태)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-11-4"], "연결": null },
{ "키": "MP000007", "원문번호": "철근", "이름": "철근", "규격": "", "단위": "ton", "요구절": ["산림품셈 12-15", "산림품셈 12-16"], "연결": null, "비고": "SD300·400·500 갈림 — 원문 강도 없음" },
{ "키": "MP000008", "원문번호": "이형철근 SD300 D16", "이름": "이형철근", "규격": "SD300, D16", "단위": "ton", "요구절": ["산림품셈 12-9-1"], "연결": "MT014464", "비고": "연결: 이형철근(SD300) D16 전국 줄" },
{ "키": "MP000009", "원문번호": "이형철근 SD300 D13", "이름": "이형철근", "규격": "SD300, D13", "단위": "ton", "요구절": ["산림품셈 12-12"], "연결": "MT014463", "비고": "연결: 이형철근(SD300) D13 전국 줄" },
{ "키": "MP000010", "원문번호": "원형철근 ∅29㎜", "이름": "원형철근", "규격": "∅29㎜, L=500㎜", "단위": "ton", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000011", "원문번호": "강봉 D32㎜", "이름": "강봉", "규격": "D32㎜, L=1000㎜", "단위": "EA", "요구절": ["산림품셈 12-30-1"], "연결": null, "비고": "강봉보강재뿐 · 값은 m — EA 아님" },
{ "키": "MP000012", "원문번호": "사다리", "이름": "사다리", "규격": "∅19㎜ 아연도금 및 스테인레스", "단위": "ton", "요구절": ["산림품셈 12-16"], "연결": null, "비고": "자재품목에 같은 품명 없음 · 단위 ton" },
{ "키": "MP000013", "원문번호": "고철", "이름": "고철", "규격": "", "단위": "kg", "요구절": ["산림품셈 9-8-1 주4", "산림품셈 12-34-3"], "연결": null, "비고": "경량·중량·STS·알루미늄 갈림" },
{ "키": "MP000014", "원문번호": "폼타이", "이름": "폼타이(Form Tie)", "규격": "D형 1/2인치", "단위": "본", "요구절": ["산림품셈 12-4 주⑦", "건설품셈 공통 6-3-1 주④"], "연결": null, "비고": "폼타이 값은 「조」 — 본 환산 안 함" },
{ "키": "MP000015", "원문번호": "문양 스티로폴", "이름": "문양 스티로폴(자재비 포함)", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-5"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000016", "원문번호": "커터 블레이드", "이름": "커터 블레이드", "규격": "", "단위": "개", "요구절": ["산림품셈 12-7-1 주③"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000017", "원문번호": "물", "이름": "물", "규격": "", "단위": "", "요구절": ["산림품셈 12-7-1 주④"], "연결": "MT005758", "비고": "연결: 자재품목 물(상수도) 한 줄" },
{ "키": "MP000018", "원문번호": "철재 거푸집", "이름": "철재 거푸집(콘크리트 포장)", "규격": "1본 3m", "단위": "본", "요구절": ["산림품셈 12-8 주①④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000019", "원문번호": "핀폴", "이름": "핀폴", "규격": "", "단위": "개", "요구절": ["산림품셈 12-8 주①④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000020", "원문번호": "유공관 ∅200㎜", "이름": "유공관", "규격": "∅200㎜", "단위": "m", "요구절": ["산림품셈 12-10"], "연결": null, "비고": "THP·PE·파형강관 유공관 갈림" },
{ "키": "MP000021", "원문번호": "부직포", "이름": "부직포", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-10", "산림품셈 12-23", "건설품셈 공통 2-11-1 재료량", "건설품셈 공통 5-4-1 재료량"], "연결": null, "비고": "두께·중량별 갈림 — 원문 규격 없음" },
{ "키": "MP000022", "원문번호": "잡석", "이름": "잡석", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-10", "산림품셈 13-4-2", "산림품셈 13-5-2"], "연결": null, "비고": "깬잡석 값은 EA — ㎥ 환산 안 함" },
{ "키": "MP000023", "원문번호": "VR관 ∅800㎜", "이름": "VR관(소켓식)", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null, "비고": "VR관 값은 「본」 — m 환산 안 함" },
{ "키": "MP000024", "원문번호": "고무링 ∅800㎜", "이름": "고무링(VR관)", "규격": "∅800㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009721", "비고": "연결: VR관고무링 D800 이름·규격 같음" },
{ "키": "MP000025", "원문번호": "흄관 ∅800㎜", "이름": "흄관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000026", "원문번호": "파형강관 ∅800㎜", "이름": "파형강관", "규격": "∅800㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000027", "원문번호": "커플링밴드 ∅800㎜", "이름": "커플링밴드(파형강관)", "규격": "∅800㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015309", "비고": "연결: 파형강관 커플링밴드 Φ800 관급 줄" },
{ "키": "MP000028", "원문번호": "VR관 ∅1000㎜", "이름": "VR관(소켓식)", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null, "비고": "VR관 값은 「본」 — m 환산 안 함" },
{ "키": "MP000029", "원문번호": "고무링 ∅1000㎜", "이름": "고무링(VR관)", "규격": "∅1000㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009723", "비고": "연결: VR관고무링 D1000 이름·규격 같음" },
{ "키": "MP000030", "원문번호": "흄관 ∅1000㎜", "이름": "흄관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000031", "원문번호": "파형강관 ∅1000㎜", "이름": "파형강관", "규격": "∅1000㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000032", "원문번호": "커플링밴드 ∅1000㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1000㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015311", "비고": "연결: 파형강관 커플링밴드 Φ1000 관급 줄" },
{ "키": "MP000033", "원문번호": "VR관 ∅1200㎜", "이름": "VR관(소켓식)", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-1"], "연결": null, "비고": "VR관 값은 「본」 — m 환산 안 함" },
{ "키": "MP000034", "원문번호": "고무링 ∅1200㎜", "이름": "고무링(VR관)", "규격": "∅1200㎜", "단위": "개", "요구절": ["산림품셈 12-11-1"], "연결": "MT009725", "비고": "연결: VR관고무링 D1200 이름·규격 같음" },
{ "키": "MP000035", "원문번호": "흄관 ∅1200㎜", "이름": "흄관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-2-1"], "연결": null, "비고": "A·B형 갈림 · 값은 「본」 — m 아님" },
{ "키": "MP000036", "원문번호": "파형강관 ∅1200㎜", "이름": "파형강관", "규격": "∅1200㎜", "단위": "m", "요구절": ["산림품셈 12-11-3"], "연결": null, "비고": "두께 1.6·2.0·2.7T 갈림 — 원문 없음" },
{ "키": "MP000037", "원문번호": "커플링밴드 ∅1200㎜", "이름": "커플링밴드(파형강관)", "규격": "∅1200㎜", "단위": "EA", "요구절": ["산림품셈 12-11-3 주①"], "연결": "MT015313", "비고": "연결: 파형강관 커플링밴드 Φ1200 관급 줄" },
{ "키": "MP000038", "원문번호": "지수활제", "이름": "지수활제", "규격": "", "단위": "g", "요구절": ["산림품셈 12-11-1"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000039", "원문번호": "모르타르 1:2", "이름": "접합몰탈", "규격": "1:2", "단위": "㎥", "요구절": ["산림품셈 12-11-2-1", "건설품셈 건축 2-1-4", "건설품셈 건축 3-2-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000040", "원문번호": "스틸그레이팅", "이름": "집수정 뚜껑", "규격": "스틸그레이팅", "단위": "개", "요구절": ["산림품셈 12-15"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000041", "원문번호": "원형거푸집 벽체", "이름": "원형거푸집(PE 10회)", "규격": "벽체", "단위": "조", "요구절": ["산림품셈 12-16"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000042", "원문번호": "원형거푸집 스라브 및 기초", "이름": "원형거푸집(PE 10회)", "규격": "스라브 및 기초", "단위": "조", "요구절": ["산림품셈 12-16"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000043", "원문번호": "철물", "이름": "철물(강관비계)", "규격": "", "단위": "개", "요구절": ["산림품셈 12-19"], "연결": null, "비고": "이음·조임·받침 철물 갈림" },
{ "키": "MP000044", "원문번호": "강관동바리 내관", "이름": "강관동바리 내관", "규격": "48.6㎜×2.4㎜", "단위": "본", "요구절": ["산림품셈 12-20"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000045", "원문번호": "강관동바리 외관", "이름": "강관동바리 외관", "규격": "60.6㎜×2.3㎜", "단위": "본", "요구절": ["산림품셈 12-20"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000046", "원문번호": "아스팔트 ㏊-500", "이름": "아스팔트", "규격": "㏊-500", "단위": "kg", "요구절": ["산림품셈 12-21"], "연결": null, "비고": "AP-3·AP-5 갈림 — 원문 규격과 안 맞음" },
{ "키": "MP000047", "원문번호": "PVC 파이프 50㎜", "이름": "P.V.C 파이프", "규격": "50㎜", "단위": "", "요구절": ["산림품셈 12-22"], "연결": null, "비고": "원문 단위 없음 — 못 맞댐" },
{ "키": "MP000048", "원문번호": "JOINT FILLER", "이름": "JOINT FILLER", "규격": "", "단위": "㎡", "요구절": ["산림품셈 12-27-2", "산림품셈 12-34-4"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000049", "원문번호": "JOINT FILLER 접착제", "이름": "접착제(JOINT FILLER)", "규격": "", "단위": "kg", "요구절": ["산림품셈 12-27-2"], "연결": null, "비고": "접착제 종류 갈림" },
{ "키": "MP000050", "원문번호": "실런트", "이름": "실런트", "규격": "20×25㎜", "단위": "kg", "요구절": ["산림품셈 12-27-3"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000051", "원문번호": "에폭시 접착제 콘크리트용", "이름": "에폭시 접착제", "규격": "콘크리트용", "단위": "kg", "요구절": ["산림품셈 12-28"], "연결": null, "비고": "에폭시 접착제 GF800 한 줄 — 규격 다름" },
{ "키": "MP000052", "원문번호": "스페이셔 몰탈 블록", "이름": "스페이셔(몰탈 블록)", "규격": "", "단위": "", "요구절": ["산림품셈 12-29"], "연결": null, "비고": "원문 단위 없음 — 못 맞댐" },
{ "키": "MP000053", "원문번호": "PVC 파이프 Ø35㎜", "이름": "P.V.C 파이프", "규격": "Ø35㎜", "단위": "m", "요구절": ["산림품셈 12-30-1"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000054", "원문번호": "탄성고무받침", "이름": "탄성고무받침", "규격": "150×150×15", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000055", "원문번호": "스티로폴 T20㎜", "이름": "스티로폴", "규격": "T=20㎜", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000056", "원문번호": "TAR PAPER", "이름": "TAR PAPER", "규격": "T=15㎜", "단위": "㎡", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000057", "원문번호": "채움재 아스팔트", "이름": "채움재(아스팔트)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "아스팔트 값은 kg — ㎥ 환산 안 함" },
{ "키": "MP000058", "원문번호": "다웰바캡", "이름": "다웰바캡", "규격": "D=50㎜", "단위": "m", "요구절": ["산림품셈 12-30-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000059", "원문번호": "물끊기 홈 재료", "이름": "물끊기 홈(NOTCH) 재료", "규격": "", "단위": "", "요구절": ["산림품셈 12-31"], "연결": null, "비고": "원문 단위 없음 — 못 맞댐" },
{ "키": "MP000060", "원문번호": "하드너", "이름": "콘크리트 표면 강화재(하드너)", "규격": "", "단위": "", "요구절": ["산림품셈 12-35"], "연결": null, "비고": "원문 단위 없음 — 못 맞댐" },
{ "키": "MP000061", "원문번호": "소재", "이름": "소재(원목)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 10-8-2"], "연결": null, "비고": "대·중·소경목 갈림 — 원문 지름 없음" },
{ "키": "MP000062", "원문번호": "각재·판재", "이름": "각재·판재", "규격": "", "단위": "㎥", "요구절": ["산림품셈 10-8-2"], "연결": null, "비고": "미송·외송 갈림 · 각재와 판재 둘" },
{ "키": "MP000063", "원문번호": "목재", "이름": "목재(규준틀)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 11-2", "산림품셈 11-3", "건설품셈 공통 2-5-4", "건설품셈 공통 2-11-1 재료량"], "연결": null, "비고": "규준틀 목재 줄 없음 — 합성목재뿐" },
{ "키": "MP000064", "원문번호": "줄눈 모르타르", "이름": "줄눈메꿈 모르타르", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-4-4 주④", "산림품셈 13-5-1 주③"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000065", "원문번호": "틈메우기돌", "이름": "틈메우기돌", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-5-1 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000066", "원문번호": "철망태", "이름": "철망태(사각형 돌망태)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-11-4"], "연결": null, "비고": "개비온 값은 「조」 — ㎥ 환산 안 함" },
{ "키": "MP000067", "원문번호": "돌망태 채움재", "이름": "채움재(돌망태)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 13-11-4"], "연결": null, "비고": "돌망태 채움돌 값 줄 없음" },
{ "키": "MP000068", "원문번호": "농약(Fluroxypyr-meptyl+Triclypyr-TEA 미탁제)", "이름": "농약", "규격": "Fluroxypyr-meptyl+Triclypyr-TEA 미탁제", "단위": "병", "요구절": ["산림품셈 2-1-5-1", "산림품셈 6-4-2"], "연결": null },
{ "키": "MP000069", "원문번호": "우드칩", "이름": "우드칩(목재칩)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 6-8"], "연결": null },
{ "키": "MP000069", "원문번호": "우드칩", "이름": "우드칩(목재칩)", "규격": "", "단위": "㎥", "요구절": ["산림품셈 6-8"], "연결": null, "비고": "우드칩 값은 「포」 — ㎥ 환산 안 함" },
{ "키": "MP000070", "원문번호": "깃발", "이름": "깃발", "규격": "", "단위": "개", "요구절": ["산림품셈 8-6-1"], "연결": null },
{ "키": "MP000071", "원문번호": "폭약", "이름": "폭약", "규격": "", "단위": "㎏", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "시중물가 후보 6줄 — 지역·규격 선택 필요 (M0005255728, M0005255729, M0005255730 외)" },
{ "키": "MP000072", "원문번호": "뇌관", "이름": "뇌관", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "시중물가 후보 3줄 — 지역·규격 선택 필요 (M0005255735, M0005255736, M0005255737)" },
{ "키": "MP000073", "원문번호": "비트", "이름": "비트(소형브레이커 천공)", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "나라장터 후보 2줄 — 규격 선택 필요 (20590632, 20590636) · 시중물가 후보 49줄 — 지역·규격 선택 필요 (M0005255743, M0005255744, M0005255747 외)" },
{ "키": "MP000074", "원문번호": "초류종자", "이름": "종자(초류)", "규격": "", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null },
{ "키": "MP000075", "원문번호": "복합비료", "이름": "비료", "규격": "복합비료", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null, "비고": "시중물가 후보 7줄 — 지역·규격 선택 필요 (M0005256545, M0005256547, M0005256548 외)" },
{ "키": "MP000071", "원문번호": "폭약", "이름": "폭약", "규격": "", "단위": "㎏", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "지름·제품별 갈림 — 원문 규격 없음" },
{ "키": "MP000072", "원문번호": "뇌관", "이름": "뇌관", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "전기뇌관 각선 길이 갈림" },
{ "키": "MP000073", "원문번호": "비트", "이름": "비트(소형브레이커 천공)", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 3-3-7", "건설품셈 공통 3-3-9"], "연결": null, "비고": "코어·버튼·착암기 비트 갈림" },
{ "키": "MP000074", "원문번호": "초류종자", "이름": "종자(초류)", "규격": "", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null, "비고": "초종별 갈림 — 원문 「초류」 만" },
{ "키": "MP000075", "원문번호": "복합비료", "이름": "비료", "규격": "복합비료", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": "MT006489", "비고": "연결: 복합비료 규격 없는 kg 줄" },
{ "키": "MP000076", "원문번호": "피복제", "이름": "피복제", "규격": "화이버/펄프류", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null },
{ "키": "MP000077", "원문번호": "침식방지안정제", "이름": "침식방지안정제", "규격": "합성접착제", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null },
{ "키": "MP000078", "원문번호": "착색제", "이름": "색소", "규격": "착색제", "단위": "㎏", "요구절": ["건설품셈 공통 4-1-3 재료량"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005256798)" },
@@ -91,18 +91,18 @@
{ "키": "MP000080", "원문번호": "낙석방지망 철망", "이름": "철망", "규격": "낙석방지망용", "단위": "㎡", "요구절": ["건설품셈 토목 1-9-4 낙석방지망 재료(참고자료)"], "연결": null, "비고": "나라장터 후보 8줄 — 규격 선택 필요 (20161168, 20161169, 20161170 외) · 시중물가 후보 6줄 — 지역·규격 선택 필요 (M0005256755, M0005257222, M0005257223 외)" },
{ "키": "MP000081", "원문번호": "결속선", "이름": "결속선", "규격": "", "단위": "m", "요구절": ["건설품셈 토목 1-9-4 낙석방지망 재료(참고자료)"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005257390)" },
{ "키": "MP000082", "원문번호": "에폭시", "이름": "에폭시", "규격": "", "단위": "kg", "요구절": ["건설품셈 토목 1-9-4 낙석방지망 재료(참고자료)"], "연결": null, "비고": "포켓식만 · 시중물가 후보 4줄 — 지역·규격 선택 필요 (M0005257763, M0005257764, M0005257989 외)" },
{ "키": "MP000083", "원문번호": "에폭시 신구콘크리트 접착제", "이름": "Epoxy 신구-콘크리트 접착제", "규격": "", "단위": "kg", "요구절": ["건설품셈 공통 6-1-5"], "연결": null },
{ "키": "MP000084", "원문번호": "에폭시 콘크리트고무 접착제", "이름": "Epoxy-콘크리트 고무 접착제", "규격": "", "단위": "kg", "요구절": ["건설품셈 공통 6-1-5"], "연결": null },
{ "키": "MP000085", "원문번호": "폼타이 9.5㎜", "이름": "폼타이(Form Tie)", "규격": "9.5㎜", "단위": "본", "요구절": ["건설품셈 공통 6-3-1 주④"], "연결": null },
{ "키": "MP000086", "원문번호": "폼타이 7.9㎜", "이름": "폼타이(Form Tie)", "규격": "7.9㎜", "단위": "본", "요구절": ["건설품셈 공통 6-3-1 주④"], "연결": null },
{ "키": "MP000087", "원문번호": "강재거푸집", "이름": "강재거푸집", "규격": "두께 3.2㎜", "단위": "㎡", "요구절": ["건설품셈 공통 6-3-2"], "연결": null },
{ "키": "MP000088", "원문번호": "문양거푸집 판넬", "이름": "문양거푸집(판넬)", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 6-3-4"], "연결": null },
{ "키": "MP000089", "원문번호": "합성수지 원형맨홀거푸집 ø740", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø740", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null },
{ "키": "MP000090", "원문번호": "합성수지 원형맨홀거푸집 ø900", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø900", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null },
{ "키": "MP000091", "원문번호": "합성수지 원형맨홀거푸집 ø1200", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1200", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null },
{ "키": "MP000092", "원문번호": "합성수지 원형맨홀거푸집 ø1500", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1500", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null },
{ "키": "MP000093", "원문번호": "합성수지 원형맨홀거푸집 ø1800", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1800", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null },
{ "키": "MP000094", "원문번호": "쉬즈관", "이름": "쉬즈관", "규격": "", "단위": "m", "요구절": ["건설품셈 공통 6-4-1 주④", "건설품셈 공통 6-4-2 주④"], "연결": null, "비고": "시중물가 후보 2줄 — 지역·규격 선택 필요 (M0005256308, M0005256309)" },
{ "키": "MP000083", "원문번호": "에폭시 신구콘크리트 접착제", "이름": "Epoxy 신구-콘크리트 접착제", "규격": "", "단위": "kg", "요구절": ["건설품셈 공통 6-1-5"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000084", "원문번호": "에폭시 콘크리트고무 접착제", "이름": "Epoxy-콘크리트 고무 접착제", "규격": "", "단위": "kg", "요구절": ["건설품셈 공통 6-1-5"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000085", "원문번호": "폼타이 9.5㎜", "이름": "폼타이(Form Tie)", "규격": "9.5㎜", "단위": "본", "요구절": ["건설품셈 공통 6-3-1 주④"], "연결": null, "비고": "폼타이 값은 「조」 — 본 환산 안 함" },
{ "키": "MP000086", "원문번호": "폼타이 7.9㎜", "이름": "폼타이(Form Tie)", "규격": "7.9㎜", "단위": "본", "요구절": ["건설품셈 공통 6-3-1 주④"], "연결": null, "비고": "폼타이 값은 「조」 — 본 환산 안 함" },
{ "키": "MP000087", "원문번호": "강재거푸집", "이름": "강재거푸집", "규격": "두께 3.2㎜", "단위": "㎡", "요구절": ["건설품셈 공통 6-3-2"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000088", "원문번호": "문양거푸집 판넬", "이름": "문양거푸집(판넬)", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 6-3-4"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000089", "원문번호": "합성수지 원형맨홀거푸집 ø740", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø740", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000090", "원문번호": "합성수지 원형맨홀거푸집 ø900", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø900", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000091", "원문번호": "합성수지 원형맨홀거푸집 ø1200", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1200", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000092", "원문번호": "합성수지 원형맨홀거푸집 ø1500", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1500", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000093", "원문번호": "합성수지 원형맨홀거푸집 ø1800", "이름": "합성수지(P.E) 원형 맨홀 거푸집", "규격": "ø1800", "단위": "조", "요구절": ["건설품셈 공통 6-3-5 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000094", "원문번호": "쉬즈관", "이름": "쉬즈관", "규격": "", "단위": "m", "요구절": ["건설품셈 공통 6-4-1 주④", "건설품셈 공통 6-4-2 주④"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000095", "원문번호": "조립식 가설건축물 BASECHANNEL", "이름": "BASECHANNEL", "규격": "두께 2.0㎜ 이상", "단위": "m", "요구절": ["건설품셈 공통 2-2-3 주자재"], "연결": null },
{ "키": "MP000096", "원문번호": "조립식 가설건축물 TOPCHANNEL", "이름": "TOPCHANNEL", "규격": "두께 2.0㎜ 이상", "단위": "m", "요구절": ["건설품셈 공통 2-2-3 주자재"], "연결": null },
{ "키": "MP000097", "원문번호": "조립식 가설건축물 외부PANEL(벽)", "이름": "외부PANEL(벽)", "규격": "1,200×2,400㎜", "단위": "매", "요구절": ["건설품셈 공통 2-2-3 주자재"], "연결": null },
@@ -140,15 +140,15 @@
{ "키": "MP000129", "원문번호": "보호막", "이름": "보호막", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 2-10-1 재료량"], "연결": null },
{ "키": "MP000130", "원문번호": "방진망", "이름": "방진망", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 2-10-2 재료량"], "연결": null },
{ "키": "MP000131", "원문번호": "풀", "이름": "풀(보양지)", "규격": "", "단위": "kg", "요구절": ["건설품셈 공통 2-11-1 재료량", "건설품셈 건축 5-3-7"], "연결": null },
{ "키": "MP000132", "원문번호": "토목섬유 매트", "이름": "매트(합성수지 토목섬유)", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null },
{ "키": "MP000132", "원문번호": "토목섬유 매트", "이름": "매트(합성수지 토목섬유)", "규격": "", "단위": "㎡", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null, "비고": "매트 종류 갈림 — 원문은 재질만" },
{ "키": "MP000133", "원문번호": "PP로프 9㎜", "이름": "P.P로프", "규격": "9㎜", "단위": "m", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null, "비고": "시중물가 후보 6줄 — 지역·규격 선택 필요 (M0005257642, M0005257643, M0005257644 외)" },
{ "키": "MP000134", "원문번호": "모래주머니", "이름": "모래주머니", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null },
{ "키": "MP000135", "원문번호": "철근 19㎜", "이름": "철근", "규격": "19㎜", "단위": "m", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null },
{ "키": "MP000134", "원문번호": "모래주머니", "이름": "모래주머니", "규격": "", "단위": "개", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null, "비고": "자재품목에 같은 품명 없음" },
{ "키": "MP000135", "원문번호": "철근 19㎜", "이름": "철근", "규격": "19㎜", "단위": "m", "요구절": ["건설품셈 공통 5-2-1 매트고정 재료량"], "연결": null, "비고": "철근 값은 Ton — m 환산 안 함" },
{ "키": "MP000136", "원문번호": "드레인 보드", "이름": "드레인 보드(연직배수재)", "규격": "", "단위": "m", "요구절": ["건설품셈 공통 5-2-3 주②"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005256446)" },
{ "키": "MP000137", "원문번호": "지오콤포지트", "이름": "지오콤포지트", "규격": "6.0㎜", "단위": "㎡", "요구절": ["건설품셈 공통 5-4-1 재료량"], "연결": null },
{ "키": "MP000138", "원문번호": "벤토나이트매트", "이름": "벤토나이트매트", "규격": "6.0㎜", "단위": "㎡", "요구절": ["건설품셈 공통 5-4-1 재료량"], "연결": null },
{ "키": "MP000138", "원문번호": "벤토나이트매트", "이름": "벤토나이트매트", "규격": "6.0㎜", "단위": "㎡", "요구절": ["건설품셈 공통 5-4-1 재료량"], "연결": null, "비고": "S·LS·K 갈림 · 값은 6.5㎜" },
{ "키": "MP000139", "원문번호": "HDPE 시트", "이름": "HDPE 시트", "규격": "22.5㎜", "단위": "㎡", "요구절": ["건설품셈 공통 5-4-1 재료량"], "연결": null },
{ "키": "MP000140", "원문번호": "아세틸렌()", "이름": "아세틸렌", "규격": "", "단위": "", "요구절": ["건설품셈 공통 6-2-5"], "연결": null, "비고": "원문 ℓ 단위(나라장터 20204095 는 kg) · 시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005253222)" },
{ "키": "MP000140", "원문번호": "아세틸렌()", "이름": "아세틸렌", "규격": "", "단위": "", "요구절": ["건설품셈 공통 6-2-5"], "연결": "MT003164", "비고": "연결: 아세틸렌 줄 하나" },
{ "키": "MP000141", "원문번호": "노면파쇄기 팁(날)", "이름": "팁(날)", "규격": "노면파쇄기 절삭용", "단위": "개", "요구절": ["건설품셈 유지관리 2-1-3 주④", "건설품셈 유지관리 2-1-4 주④", "건설품셈 유지관리 2-1-5 주②"], "연결": null },
{ "키": "MP000142", "원문번호": "프로판가스", "이름": "프로판가스", "규격": "", "단위": "kg", "요구절": ["건설품셈 유지관리 2-1-15 4 자재수량"], "연결": null, "비고": "시중물가 후보 1줄 — 지역·규격 선택 필요 (M0005253233)" },
{ "키": "MP000143", "원문번호": "서비스볼트", "이름": "서비스볼트", "규격": "", "단위": "본", "요구절": ["건설품셈 건축 1-1-2"], "연결": null },
@@ -257,7 +257,7 @@
{ "키": "MP000246", "원문번호": "에어해머 대구경 보링용", "이름": "에어해머", "규격": "대구경 보링용", "단위": "개", "요구절": ["건설품셈 토목 8-4-2"], "연결": null },
{ "키": "MP000247", "원문번호": "버튼비트 대구경 보링용", "이름": "버튼비트", "규격": "대구경 보링용", "단위": "개", "요구절": ["건설품셈 토목 8-4-2"], "연결": null, "비고": "시중물가 후보 5줄 — 지역·규격 선택 필요 (M0005255749, M0005255750, M0005255751 외)" },
{ "키": "MP000248", "원문번호": "보온 성형물", "이름": "보온 성형물", "규격": "", "단위": "m", "요구절": ["건설품셈 기계설비 13-3-1"], "연결": null },
{ "키": "MP000249", "원문번호": "보온용 철선", "이름": "철선", "규격": "", "단위": "m", "요구절": ["건설품셈 기계설비 13-3-1"], "연결": null, "비고": "시중물가 후보 35줄 — 지역·규격 선택 필요 (M0005250237, M0005250238, M0005250239 외)" },
{ "키": "MP000249", "원문번호": "보온용 철선", "이름": "철선", "규격": "", "단위": "m", "요구절": ["건설품셈 기계설비 13-3-1"], "연결": null, "비고": "굵기(#8~#20) 갈림 — 원문 규격 없음" },
{ "키": "MP000250", "원문번호": "Lagging Sheet", "이름": "Lagging Sheet", "규격": "", "단위": "㎡", "요구절": ["건설품셈 기계설비 13-3-1"], "연결": null },
{ "키": "MP000251", "원문번호": "Sheet Metal Screw", "이름": "Sheet Metal Screw", "규격": "", "단위": "개", "요구절": ["건설품셈 기계설비 13-3-1"], "연결": null },
{ "키": "MP000252", "원문번호": "기계유", "이름": "기름", "규격": "", "단위": "", "요구절": ["건설품셈 기계설비 13-4-2"], "연결": null },
@@ -337,7 +337,7 @@
{ "키": "MP000326", "원문번호": "Grinder Wheel", "이름": "Grinder Wheel", "규격": "", "단위": "매", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
{ "키": "MP000327", "원문번호": "Oil Stone", "이름": "Oil Stone", "규격": "", "단위": "개", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
{ "키": "MP000328", "원문번호": "File", "이름": "File", "규격": "", "단위": "개", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
{ "키": "MP000329", "원문번호": "아연도철선", "이름": "아연도철선", "규격": "", "단위": "㎏", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null, "비고": "시중물가 후보 15줄 — 지역·규격 선택 필요 (M0005250272, M0005250273, M0005250274 외)" },
{ "키": "MP000329", "원문번호": "아연도철선", "이름": "아연도철선", "규격": "", "단위": "㎏", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null, "비고": "굵기별 갈림 — 원문 규격 없음" },
{ "키": "MP000330", "원문번호": "Drill", "이름": "Drill", "규격": "", "단위": "개", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
{ "키": "MP000331", "원문번호": "사포", "이름": "사포", "규격": "", "단위": "매", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
{ "키": "MP000332", "원문번호": "비닐시드", "이름": "비닐시드", "규격": "", "단위": "㎡", "요구절": ["건설품셈 기계설비 13-11-5"], "연결": null },
@@ -9,11 +9,12 @@
"역할": "primary"
}
],
"비고": ["규격 = 기준일 · 구분 · 상세구분은 두되 비움"],
"줄": [
{ "키": "FX000001", "원문번호": "0000001", "이름": "USD", "규격": "", "단위": "원/USD", "값": 1414.9, "출처": "한국은행환율 0000001", "기준일": "2026-08-14" },
{ "키": "FX000002", "원문번호": "0000002", "이름": "JPY100", "규격": "", "단위": "원/JPY100", "값": 887.33, "출처": "한국은행환율 0000002", "기준일": "2026-08-14" },
{ "키": "FX000003", "원문번호": "0000003", "이름": "EUR", "규격": "", "단위": "원/EUR", "값": 1631.87, "출처": "한국은행환율 0000003", "기준일": "2026-08-14" },
{ "키": "FX000004", "원문번호": "0000012", "이름": "GBP", "규격": "", "단위": "원/GBP", "값": 1908.7, "출처": "한국은행환율 0000012", "기준일": "2026-08-14" },
{ "키": "FX000005", "원문번호": "0000053", "이름": "CNY", "규격": "", "단위": "원/CNY", "값": 209.75, "출처": "한국은행환율 0000053", "기준일": "2026-08-14" }
{ "키": "FX000001", "원문번호": "0000001", "구분": null, "상세구분": null, "이름": "USD", "규격": "2026-08-14", "단위": "원/USD", "값": 1414.9, "출처": "한국은행환율 0000001", "비고": null },
{ "키": "FX000002", "원문번호": "0000002", "구분": null, "상세구분": null, "이름": "JPY100", "규격": "2026-08-14", "단위": "원/JPY100", "값": 887.33, "출처": "한국은행환율 0000002", "비고": null },
{ "키": "FX000003", "원문번호": "0000003", "구분": null, "상세구분": null, "이름": "EUR", "규격": "2026-08-14", "단위": "원/EUR", "값": 1631.87, "출처": "한국은행환율 0000003", "비고": null },
{ "키": "FX000004", "원문번호": "0000012", "구분": null, "상세구분": null, "이름": "GBP", "규격": "2026-08-14", "단위": "원/GBP", "값": 1908.7, "출처": "한국은행환율 0000012", "비고": null },
{ "키": "FX000005", "원문번호": "0000053", "구분": null, "상세구분": null, "이름": "CNY", "규격": "2026-08-14", "단위": "원/CNY", "값": 209.75, "출처": "한국은행환율 0000053", "비고": null }
]
}
+8 -9
View File
@@ -273,9 +273,8 @@ def test_main_은_시스템관리자만() -> None:
assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main
def test_고르기_나라장터_시중물가_직종(client: TestClient) -> None:
nara = _get(client, "/api/m01/pick", kind="procure", q="육각볼트 M6*20")
assert nara["total"] >= 1 and nara["items"][0]["ref"].startswith("나라장터:")
def test_고르기_자재품목_직종(client: TestClient) -> None:
assert client.get("/api/m01/pick", params={"kind": "procure"}).status_code == 404
market = _get(client, "/api/m01/pick", kind="price", q="H형강 관급", limit=5)
assert market["items"] and market["items"][0]["관급"] and market["items"][0][""] > 0
job = _get(client, "/api/m01/pick", kind="job", q="보통인부")
@@ -305,7 +304,7 @@ def test_구분_목록(client: TestClient) -> None:
"건설품셈",
"산림품셈",
]
assert _get(client, "/api/m01/subs", file="재료_시중물가.json")["subs"] == []
assert _get(client, "/api/m01/subs", file="재료_자재품목.json")["subs"] == []
def test_못_이은_줄만_거름(client: TestClient) -> None:
@@ -318,7 +317,7 @@ def test_품셈재료_단가는_연결의_낮은_값과_출처(client: TestClien
key = key_of("GC:기계설비 13-2-4 강판 전기아크용접 전기아크용접(V형)")
one = _get(client, "/api/m01/logic", key=key)
power = one["prices"][key_of("MP:전력")]
assert power[""] > 0 and power["출처"].startswith(key_of("MT:M0005250064") + ".")
assert power[""] > 0 and power["출처"] == key_of("MT:M0005250064") + ".물가자료"
def test_하위_거름과_참조_이름(client: TestClient) -> None:
@@ -335,18 +334,18 @@ def test_하위_거름과_참조_이름(client: TestClient) -> None:
assert keyed and all(linked["refs"][r["연결"]] for r in keyed)
def test_시중물가_저장은_띄어쓰기를_안_바꿈(client: TestClient) -> None:
market = "재료_시중물가.json"
def test_자재품목_저장은_띄어쓰기를_안_바꿈(client: TestClient) -> None:
market = "재료_자재품목.json"
old = (store.FOLDER / market).read_text(encoding="utf-8").split("\n")
rows = _get(client, "/api/m01/rows", file=market, size=1)
row = rows["rows"][0]
row[""]["시중"]["가격정보"] = 1
row["물가자료"] = 1
change = {"op": "edit", "key": row[""], "row": row}
body = {"files": [{"file": market, "version": rows["version"], "changes": [change]}]}
assert client.post("/api/m01/save", json=body).status_code == 200
new = (store.FOLDER / market).read_text(encoding="utf-8").split("\n")
diff = [(a, b) for a, b in zip(old, new) if a != b]
assert len(new) == len(old) and len(diff) == 1
assert diff[0][1].replace('"가격정보": 1,', '"가격정보": X,').count("{ ") == diff[0][0].count(
assert diff[0][1].replace('"물가자료": 1,', '"물가자료": X,').count("{ ") == diff[0][0].count(
"{ "
)
+20 -18
View File
@@ -435,8 +435,9 @@ def test_토목_보링_심도_가산():
for row in files["인력.json"][""]:
if row[""] is None: # 관리자가 채울 값 — 시험용 가짜 값(파일엔 안 씀)
row[""] = Decimal(1000)
fake = {"시중": {"물가자료": Decimal(1000)}, "조달": {"": None, "출처": None}}
files["재료_시중물가.json"][""].append({"": "MT999999", "원문번호": "가짜", "": fake})
files["재료_자재품목.json"][""].append(
{"": "MT999999", "원문번호": "가짜", "물가자료": Decimal(1000)}
)
for row in files["재료_품셈재료.json"][""]:
row["연결"] = row["연결"] or "MT999999" # 못 이은 재료 — 시험용 가짜 연결
given = {"": "점토층", "규격": "BX", "깊이": 35, "심도가산율": 5}
@@ -669,25 +670,26 @@ def test_노임_갈래_파일_원문_대조():
def test_품셈재료_연결_낮은_값과_출처(whole):
"""연결 하나 — 시중 다섯 칸 · 조달 가운데 낮은 값 · 출처 = 그 칸."""
row = whole.get("MT:M0005250064")[""]
low = min(v for k, v in row["시중"].items() if k != "면수" and v is not None)
"""연결 하나 — 값 열 다섯 가운데 낮은 값 · 출처 = 「<키>.<열>」."""
row = whole.get("MT:M0005250064")
low = min(row[s] for s in mm.SLOTS if row.get(s) is not None)
value, source = mf.element(whole, "MP:전력", {})
assert value == low and source.startswith(whole.get("MT:M0005250064")[""] + ".시중.")
assert value == low and source.split(".") == [row[""], "물가자료"]
def test_시중물가_조달_묶음():
"""관급 줄 가격정보 → 조달 · 나라장터 짝은 값 없이 연결만 · 채택·견적가 칸 없음."""
master = cm.master()
steel = master.get("MT:M0005251695")[""]
assert steel["조달"] == {"": Decimal(1228830), "출처": "가격정보", "기준": "26.6.12~"}
assert (
steel["시중"]["가격정보"] is None and "채택" not in steel and "견적가" not in steel["시중"]
)
bolt = master.get("MT:M0005252462")
nara = master.get("MN:10023398")
assert bolt[""]["조달"] == {"": None, "출처": "나라장터:" + nara[""], "기준": None}
assert (nara[""], nara[""]) in mm._offers(master, bolt[""])
def test_자재품목_열_한_벌():
"""모든 줄이 같은 열 한 벌 · 관급은 조달 값 · 기간은 비고 · 남은 가격정보는 비고에만."""
data = cm.load(["재료_자재품목.json"])["재료_자재품목.json"]
rows = data[""]
assert {tuple(r) for r in rows} == {cm.COLS["자재품목"]}
steel = next(r for r in rows if r["원문번호"] == "M0005251695")
assert steel["관급"] == Decimal(1228830) and steel["비고"] == "조달 26.6.12~"
assert all(steel[s] is None for s in ("물가자료", "유통물가", "물가정보", "거래가격"))
left = [r for r in rows if str(r["비고"] or "").startswith("가격정보")]
assert len(left) == 72 and all(r["관급"] is None for r in left)
oil = next(r for r in rows if r["원문번호"] == "M0003039885") # 휘발유
assert oil["비고"] == "가격정보 1,825 · 2026.7" and mm._offers(cm.master(), oil[""]) == []
assert {r["구분"] for r in rows} == {None} and {r["상세구분"] for r in rows} == {None}
def test_품셈재료_자재지역_후보(whole):