Merge remote-tracking branch 'origin/dev' into main_desktop_1
This commit is contained in:
@@ -31,6 +31,10 @@ export interface StructureOptionField {
|
||||
/** 입력 시점 — B05는 유무·종류·위치만 받고 상세 치수(detail)는 B06/B07에서 받는다
|
||||
* (2026-08-17 사용자 확정). detail이면 required여도 B05 폼에 그리지 않는다. */
|
||||
phase?: "b05" | "detail";
|
||||
/** **비워 두는 것이 뜻인 칸** — 비면 계산 쪽이 기준값으로 돌고 그 사실이 사유로 뜬다.
|
||||
* ⚠ 이 칸의 select 는 **빈 보기를 둔다** — 첫 보기를 슬쩍 고르면 기준값과 다른 값이
|
||||
* 조용히 저장된다(2026-09-13: `fill_concrete_mpa` 가 180 으로 박혀 엔진 기준 210 과 갈렸다). */
|
||||
empty_means?: string | null;
|
||||
}
|
||||
|
||||
/** 구조물 배치 폼을 어느 화면이 쓰는가 — B05 는 유무·종류·위치만, **B06/B07 은 상세
|
||||
|
||||
@@ -924,6 +924,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1086,6 +1097,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1441,6 +1463,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1991,6 +2024,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -325,9 +325,15 @@ export function createStructuresSection(
|
||||
const choices = option.choices.map((choice) => [choice, choice] as [string, string]);
|
||||
// 기본값 없는 필수 항목은 **빈 칸으로** — 첫 항목을 슬쩍 고르면 근거 없는 값이 나간다.
|
||||
const mustPick = option.required === true && (option.default ?? "") === "";
|
||||
if (mustPick) choices.unshift(["", "— 선택 —"]);
|
||||
// **비워 두는 것이 뜻인 칸**(`empty_means`)도 같다 — 비면 계산 쪽이 기준값으로 도는데
|
||||
// 첫 보기가 골라져 저장되면 기준과 다른 값이 조용히 나간다(2026-09-13:
|
||||
// `fill_concrete_mpa` 가 180 으로 박혀 엔진 기준 210 과 갈렸다). 기본값이 **있는** 칸은
|
||||
// 그대로 첫 보기를 쓴다 — 2026-08-17 「빈 보기 두지 않음」 지시가 그 자리다.
|
||||
const meansEmpty = (option.empty_means ?? "") !== "" && (option.default ?? "") === "";
|
||||
const keepEmpty = mustPick || meansEmpty;
|
||||
if (keepEmpty) choices.unshift(["", mustPick ? "— 선택 —" : "— 안 정함 —"]);
|
||||
input = select(choices);
|
||||
input.value = String(preset || (mustPick ? "" : (option.choices[0] ?? "")));
|
||||
input.value = String(preset || (keepEmpty ? "" : (option.choices[0] ?? "")));
|
||||
} else if (option.input === "number") {
|
||||
input = numberInput("0.1", "0");
|
||||
input.value = String(preset ?? "");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writeCrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import { createDesignSync } from "./B06_Section_UI_Page_Design_Sync";
|
||||
import { createGradeEdit } from "./B06_Section_UI_Page_Grade_Edit";
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { leaveForDashboard } from "../A00_Common/b_missing_data_guard";
|
||||
import { readByKey, stateKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
@@ -23,36 +24,20 @@ import {
|
||||
type StandardCrossSection,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { createStationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
|
||||
import {
|
||||
bermSpansFromStructures,
|
||||
confirmCurrentSections,
|
||||
createCutSlopeStore,
|
||||
createRockBoundaryStore,
|
||||
readBermSpans,
|
||||
saveCurrentSections,
|
||||
writeBermSpans,
|
||||
type SectionPersistContext,
|
||||
} from "./B06_Section_UI_Page_Persist";
|
||||
import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
||||
import {
|
||||
createProfileEditStore,
|
||||
readAlignmentDraft,
|
||||
type ProfileEditStore,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
adjustStation,
|
||||
buildAlignment,
|
||||
toAlignmentBase,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
import {
|
||||
readStructurePick,
|
||||
writeStructurePick,
|
||||
} from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import { applyStructurePick } from "./B06_Section_UI_Page_Structure_Pick";
|
||||
import { type CrossDesignChange, createSectionView } from "./B06_Section_UI_Section_View";
|
||||
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
||||
import { createSectionView } from "./B06_Section_UI_Section_View";
|
||||
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import {
|
||||
applyPipeOptionsToCache,
|
||||
@@ -226,142 +211,19 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 그룹 제목 행 클릭 시 접기/펼치기(N-4-1). 액션 버튼 행은 collapsible 아님.
|
||||
attachCollapsible(leftForm);
|
||||
|
||||
/**
|
||||
* 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응),
|
||||
* (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음.
|
||||
* 설정 패널 편집값을 요청에 실어 요청값 → DB 저장 옵션 → config 기본값 우선순위를 지킨다.
|
||||
*/
|
||||
async function handleDesignChange(chainageM: number, change: CrossDesignChange): Promise<void> {
|
||||
if (!projectId || currentRouteId === null || !sectionDetail) return;
|
||||
const target = sectionDetail.cross_sections.find(
|
||||
(section) => Math.abs(section.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
if (!target) return;
|
||||
|
||||
// (1) 즉시 로컬 반영: 선택 버튼만 갱신(숫자·설계선은 기존값 유지) → 해당 카드만 교체.
|
||||
if (target.design) {
|
||||
target.design = {
|
||||
...target.design,
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? target.design.ditch_side,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
ditch_choice: change.ditch_choice,
|
||||
};
|
||||
sectionView.refreshCard(chainageM);
|
||||
}
|
||||
|
||||
// (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
||||
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
||||
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
||||
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? null,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
ditch_choice: change.ditch_choice,
|
||||
});
|
||||
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 현재 design 값에서 재계산용 change를 복원한다(암 경계 오프셋 변경 시 재계산 트리거). */
|
||||
function changeFromDesign(chainageM: number): CrossDesignChange | null {
|
||||
const target = sectionDetail?.cross_sections.find(
|
||||
(section) => Math.abs(section.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
const design = target?.design;
|
||||
if (!design) return null;
|
||||
return {
|
||||
ground_type: design.ground_type,
|
||||
section_mode: design.section_mode,
|
||||
ditch_side: design.ditch_side ?? null,
|
||||
ditch_type: design.ditch_type ?? "standard",
|
||||
paved: design.paved,
|
||||
two_stage_slope: design.two_stage_slope ?? true,
|
||||
ditch_choice: design.ditch_choice ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 서버 재계산한다. */
|
||||
function recomputeIfRock(chainageM: number): void {
|
||||
const change = changeFromDesign(chainageM);
|
||||
if (change && change.ground_type !== "soil") void handleDesignChange(chainageM, change);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로드 시 stale design을 최신 엔진·최신 종단 계획고로 자동 재계산한다(E-1 + N-6).
|
||||
* 대상: (1) 2단계 경사 필드(`two_stage_slope`)가 없는 옛 암 측점, (2) B05에서 종단이
|
||||
* 변경·확정돼 저장된 계산 기준 계획고(`design.design_elevation_m`)가 현재 계획선
|
||||
* (`design_profiles`) 보간값과 어긋난 측점. 종단이 안 바뀐 측점은 0건이라 불필요한 API
|
||||
* 호출이 없다.
|
||||
*
|
||||
* 재계산은 측점별 순차 호출이 아니라 **일괄 프리뷰 1회**로 한다(2026-08-04 사용자 확인
|
||||
* — 예전 for-await 루프는 stale 측점 수만큼 왕복하며 카드가 하나씩 바뀌어 "이력 재생"처럼
|
||||
* 보였고 제일 느렸다). 편집 델타는 B05 세션 초안이 있으면 그것을(두 화면 동일 계획선),
|
||||
* 없으면 저장분(profile_alignment.edits)을 쓴다. 측점별 사용자 선택값(지반유형·단면유형·
|
||||
* 측구·암 경계)은 서버가 저장분에서 유지하고, 세션에만 있는 암 경계 오프셋은 함께 실어 보낸다.
|
||||
*/
|
||||
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
|
||||
if (!sectionDetail || !projectId || currentRouteId === null) return;
|
||||
const draft = readAlignmentDraft(currentRouteId);
|
||||
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 판정 — 계획고 어긋남 +
|
||||
// 옛 암 2단계 필드 누락). 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다.
|
||||
//
|
||||
// 다만 **미저장 세션 편집이 있으면 판정을 건너뛰고 무조건 맞춘다**. 서버에서 갓 받은
|
||||
// 저장분끼리는 늘 일치해 「낡지 않음」으로 나오는데, B05가 세션에 남긴 편집은 그 안에
|
||||
// 없어 B06이 재계산을 통째로 건너뛰었다 — 같은 시점에 B05 절토 4,774.1㎥ ↔ B06
|
||||
// 4,515.0㎥ 로 갈렸다(2026-09-03 실측). 재계산은 브라우저 안에서 끝나 값싸다.
|
||||
if (!options?.force && !draft && !hasStaleDesigns(sectionDetail)) return;
|
||||
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
|
||||
// (CLAUDE.md 5장).
|
||||
try {
|
||||
const alignment = sectionDetail.longitudinal.profile_alignment as
|
||||
| {
|
||||
edits?: {
|
||||
station_offsets?: Record<string, number>;
|
||||
curve_radii?: Record<string, number>;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
const edits = draft ?? {
|
||||
station_offsets: alignment?.edits?.station_offsets ?? {},
|
||||
curve_radii: alignment?.edits?.curve_radii ?? {},
|
||||
};
|
||||
// 재계산은 B05와 **같은 창구**를 쓴다 — 인자가 갈리면 같은 데이터가 두 화면에서
|
||||
// 다른 값이 된다(2026-09-03 사용자 지시로 일원화).
|
||||
const updated = await refreshCrossDesigns({
|
||||
// 설계 선택 반영·재계산·소단 동기화는 따로 뗀 모듈이 맡는다(2026-09-13 분리).
|
||||
const {
|
||||
handleDesignChange,
|
||||
recomputeIfRock,
|
||||
reconcileStaleDesigns,
|
||||
syncBermSpans,
|
||||
applyPanelToAll,
|
||||
} = createDesignSync({
|
||||
projectId,
|
||||
routeId: currentRouteId,
|
||||
detail: sectionDetail,
|
||||
edits,
|
||||
routeId: () => currentRouteId,
|
||||
detail: () => sectionDetail,
|
||||
view: () => sectionView,
|
||||
});
|
||||
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
|
||||
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
|
||||
sectionView.refreshCards(updated);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물 목록의 **소단**(C군 사면안정)을 세션 사본으로 편다 — 재계산이 측점마다 읽는 값이다.
|
||||
*
|
||||
* 사용자는 「구조물 배치」에서 놓고(2026-09-07 확정), 계산은 그 사본만 본다. 달라졌을 때만
|
||||
* 다시 계산한다 — 목록은 화면을 열 때도 오므로 매번 돌리면 진입이 느려진다.
|
||||
*/
|
||||
function syncBermSpans(structures: ReadonlyArray<StructureInstance>): void {
|
||||
if (!projectId || currentRouteId === null) return;
|
||||
const next = bermSpansFromStructures(structures);
|
||||
if (JSON.stringify(next) === JSON.stringify(readBermSpans(projectId, currentRouteId))) return;
|
||||
writeBermSpans(projectId, currentRouteId, next);
|
||||
void reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 구조물(C군 벽)이 바뀌면 횡단 제원이 달라진다 — 초안을 얹고 다시 그린다. */
|
||||
async function refreshDetailForStructures(): Promise<void> {
|
||||
@@ -418,28 +280,6 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
showToast(L("B06_View_Apply_Success"), "success");
|
||||
}
|
||||
|
||||
/** 패널 [전체 반영](N-2-1): design 보유 전 측점을 패널 최신값으로 순차 재계산한다.
|
||||
* handleDesignChange가 standardPanel.getValues()를 실어 보내므로 표준단면 수치만
|
||||
* 갱신되고 측점별 버튼 선택값은 보존된다. 순차 await로 동시 API 호출 수를 제한한다. */
|
||||
let applyingAll = false;
|
||||
async function applyPanelToAll(): Promise<void> {
|
||||
if (applyingAll || !sectionDetail || !projectId || currentRouteId === null) return;
|
||||
const targets = sectionDetail.cross_sections.filter((section) => section.design);
|
||||
if (!targets.length) return;
|
||||
// 화면을 잠그지 않는다 — 카드가 하나씩 갱신되는 것이 곧 진행 표시다(CLAUDE.md 5장).
|
||||
// 대신 도는 동안 다시 누르는 것만 막는다.
|
||||
applyingAll = true;
|
||||
try {
|
||||
for (const section of targets) {
|
||||
const change = changeFromDesign(section.chainage_m);
|
||||
if (change) await handleDesignChange(section.chainage_m, change);
|
||||
}
|
||||
showToast(L("B06_Std_ApplyAll_Success"), "success");
|
||||
} finally {
|
||||
applyingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 암 경계선 오프셋(측점별) 세션 저장소는 저장 흐름 모듈이 맡는다(2026-09-02 분리).
|
||||
const rockStore = createRockBoundaryStore({
|
||||
sessionKey: () => stateKey("rockb", projectId, currentRouteId),
|
||||
@@ -493,62 +333,13 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
|
||||
structureMarksSink = (structures, types) => sectionView.setStructureMarks(structures, types);
|
||||
/**
|
||||
* 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만
|
||||
* 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcileStale…`),
|
||||
* 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장).
|
||||
*/
|
||||
let gradeStore: ProfileEditStore | null = null;
|
||||
let gradeRouteId: number | null = null;
|
||||
/** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아
|
||||
* 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */
|
||||
const GRADE_RECONCILE_DEBOUNCE_MS = 120;
|
||||
let gradeReconcileTimer = 0;
|
||||
const scheduleGradeReconcile = (): void => {
|
||||
window.clearTimeout(gradeReconcileTimer);
|
||||
gradeReconcileTimer = window.setTimeout(() => {
|
||||
void reconcileStaleDesigns({ force: true }).then(() =>
|
||||
sectionView.setGradeEdit(gradeEditFor),
|
||||
);
|
||||
}, GRADE_RECONCILE_DEBOUNCE_MS);
|
||||
};
|
||||
const gradeEditFor = (): ReturnType<
|
||||
NonNullable<Parameters<typeof sectionView.setGradeEdit>[0]>
|
||||
> => {
|
||||
const detail = sectionDetail;
|
||||
if (!detail || currentRouteId === null) return null;
|
||||
const stored = readAlignment(detail.longitudinal);
|
||||
if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다.
|
||||
if (!gradeStore || gradeRouteId !== currentRouteId) {
|
||||
gradeRouteId = currentRouteId;
|
||||
gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||
}
|
||||
const store = gradeStore;
|
||||
const base = toAlignmentBase(stored);
|
||||
const alignment = buildAlignment(base, store.edits());
|
||||
return {
|
||||
alignment,
|
||||
stepM: alignment.policy.edit_step_m,
|
||||
onStation: (chainageM, delta) => {
|
||||
store.replace(adjustStation(base, store.edits(), chainageM, delta));
|
||||
// ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만
|
||||
// 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다).
|
||||
const detail = sectionDetail;
|
||||
if (detail) {
|
||||
detail.longitudinal.design_profiles = [
|
||||
toDesignProfile(
|
||||
buildAlignment(base, store.edits()),
|
||||
detail.longitudinal.design_profiles?.[0],
|
||||
),
|
||||
];
|
||||
}
|
||||
sectionView.setGradeEdit(gradeEditFor);
|
||||
// ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙).
|
||||
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다.
|
||||
scheduleGradeReconcile();
|
||||
},
|
||||
};
|
||||
};
|
||||
// 계획선 편집(▲/▼) 제공자는 따로 뗀 모듈이 맡는다(2026-09-13 분리).
|
||||
const gradeEditFor = createGradeEdit({
|
||||
routeId: () => currentRouteId,
|
||||
detail: () => sectionDetail,
|
||||
view: () => sectionView,
|
||||
reconcile: () => reconcileStaleDesigns({ force: true }),
|
||||
});
|
||||
sectionView.setGradeEdit(gradeEditFor);
|
||||
|
||||
// 종단 그래프 우클릭 — B05 와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자: B05·B06 은 한
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Page_Design_Sync.ts
|
||||
* 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
||||
* 동작은 그대로다(2026-09-13 분리).
|
||||
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
|
||||
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
|
||||
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
|
||||
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
||||
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
||||
* ========================================================================== */
|
||||
|
||||
import { writeCrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
|
||||
import {
|
||||
bermSpansFromStructures,
|
||||
readBermSpans,
|
||||
writeBermSpans,
|
||||
} from "./B06_Section_UI_Page_Persist";
|
||||
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
||||
import { L } from "./B06_Section_UI_Page_Common";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_Section_View";
|
||||
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
|
||||
export interface DesignSyncContext {
|
||||
projectId: string | null;
|
||||
routeId: () => number | null;
|
||||
detail: () => SectionDetailResponse | null;
|
||||
/** 뷰는 이 모듈보다 **뒤에** 만들어지므로 그때 채워지는 참조를 통해 부른다. */
|
||||
view: () => SectionViewController;
|
||||
}
|
||||
|
||||
export interface DesignSyncController {
|
||||
handleDesignChange: (chainageM: number, change: CrossDesignChange) => Promise<void>;
|
||||
recomputeIfRock: (chainageM: number) => void;
|
||||
reconcileStaleDesigns: (options?: { force?: boolean }) => Promise<void>;
|
||||
syncBermSpans: (structures: ReadonlyArray<StructureInstance>) => void;
|
||||
applyPanelToAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||||
const { projectId } = ctx;
|
||||
|
||||
/**
|
||||
* 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응),
|
||||
* (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음.
|
||||
* 설정 패널 편집값을 요청에 실어 요청값 → DB 저장 옵션 → config 기본값 우선순위를 지킨다.
|
||||
*/
|
||||
async function handleDesignChange(chainageM: number, change: CrossDesignChange): Promise<void> {
|
||||
const sectionDetail = ctx.detail();
|
||||
const currentRouteId = ctx.routeId();
|
||||
if (!projectId || currentRouteId === null || !sectionDetail) return;
|
||||
const target = sectionDetail.cross_sections.find(
|
||||
(section) => Math.abs(section.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
if (!target) return;
|
||||
|
||||
// (1) 즉시 로컬 반영: 선택 버튼만 갱신(숫자·설계선은 기존값 유지) → 해당 카드만 교체.
|
||||
if (target.design) {
|
||||
target.design = {
|
||||
...target.design,
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? target.design.ditch_side,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
ditch_choice: change.ditch_choice,
|
||||
};
|
||||
ctx.view().refreshCard(chainageM);
|
||||
}
|
||||
|
||||
// (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
||||
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
||||
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
||||
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
||||
ground_type: change.ground_type,
|
||||
section_mode: change.section_mode,
|
||||
ditch_side: change.ditch_side ?? null,
|
||||
ditch_type: change.ditch_type,
|
||||
paved: change.paved,
|
||||
two_stage_slope: change.two_stage_slope,
|
||||
ditch_choice: change.ditch_choice,
|
||||
});
|
||||
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
||||
await reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 현재 design 값에서 재계산용 change를 복원한다(암 경계 오프셋 변경 시 재계산 트리거). */
|
||||
function changeFromDesign(chainageM: number): CrossDesignChange | null {
|
||||
const target = ctx
|
||||
.detail()
|
||||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01);
|
||||
const design = target?.design;
|
||||
if (!design) return null;
|
||||
return {
|
||||
ground_type: design.ground_type,
|
||||
section_mode: design.section_mode,
|
||||
ditch_side: design.ditch_side ?? null,
|
||||
ditch_type: design.ditch_type ?? "standard",
|
||||
paved: design.paved,
|
||||
two_stage_slope: design.two_stage_slope ?? true,
|
||||
ditch_choice: design.ditch_choice ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 서버 재계산한다. */
|
||||
function recomputeIfRock(chainageM: number): void {
|
||||
const change = changeFromDesign(chainageM);
|
||||
if (change && change.ground_type !== "soil") void handleDesignChange(chainageM, change);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로드 시 stale design을 최신 엔진·최신 종단 계획고로 자동 재계산한다(E-1 + N-6).
|
||||
* 대상: (1) 2단계 경사 필드(`two_stage_slope`)가 없는 옛 암 측점, (2) B05에서 종단이
|
||||
* 변경·확정돼 저장된 계산 기준 계획고(`design.design_elevation_m`)가 현재 계획선
|
||||
* (`design_profiles`) 보간값과 어긋난 측점. 종단이 안 바뀐 측점은 0건이라 불필요한 API
|
||||
* 호출이 없다.
|
||||
*
|
||||
* 재계산은 측점별 순차 호출이 아니라 **일괄 프리뷰 1회**로 한다(2026-08-04 사용자 확인
|
||||
* — 예전 for-await 루프는 stale 측점 수만큼 왕복하며 카드가 하나씩 바뀌어 "이력 재생"처럼
|
||||
* 보였고 제일 느렸다). 편집 델타는 B05 세션 초안이 있으면 그것을(두 화면 동일 계획선),
|
||||
* 없으면 저장분(profile_alignment.edits)을 쓴다. 측점별 사용자 선택값(지반유형·단면유형·
|
||||
* 측구·암 경계)은 서버가 저장분에서 유지하고, 세션에만 있는 암 경계 오프셋은 함께 실어 보낸다.
|
||||
*/
|
||||
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
|
||||
const sectionDetail = ctx.detail();
|
||||
const currentRouteId = ctx.routeId();
|
||||
if (!sectionDetail || !projectId || currentRouteId === null) return;
|
||||
const draft = readAlignmentDraft(currentRouteId);
|
||||
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 판정 — 계획고 어긋남 +
|
||||
// 옛 암 2단계 필드 누락). 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다.
|
||||
//
|
||||
// 다만 **미저장 세션 편집이 있으면 판정을 건너뛰고 무조건 맞춘다**. 서버에서 갓 받은
|
||||
// 저장분끼리는 늘 일치해 「낡지 않음」으로 나오는데, B05가 세션에 남긴 편집은 그 안에
|
||||
// 없어 B06이 재계산을 통째로 건너뛰었다 — 같은 시점에 B05 절토 4,774.1㎥ ↔ B06
|
||||
// 4,515.0㎥ 로 갈렸다(2026-09-03 실측). 재계산은 브라우저 안에서 끝나 값싸다.
|
||||
if (!options?.force && !draft && !hasStaleDesigns(sectionDetail)) return;
|
||||
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
|
||||
// (CLAUDE.md 5장).
|
||||
try {
|
||||
const alignment = sectionDetail.longitudinal.profile_alignment as
|
||||
| {
|
||||
edits?: {
|
||||
station_offsets?: Record<string, number>;
|
||||
curve_radii?: Record<string, number>;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
const edits = draft ?? {
|
||||
station_offsets: alignment?.edits?.station_offsets ?? {},
|
||||
curve_radii: alignment?.edits?.curve_radii ?? {},
|
||||
};
|
||||
// 재계산은 B05와 **같은 창구**를 쓴다 — 인자가 갈리면 같은 데이터가 두 화면에서
|
||||
// 다른 값이 된다(2026-09-03 사용자 지시로 일원화).
|
||||
const updated = await refreshCrossDesigns({
|
||||
projectId,
|
||||
routeId: currentRouteId,
|
||||
detail: sectionDetail,
|
||||
edits,
|
||||
});
|
||||
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
|
||||
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
|
||||
ctx.view().refreshCards(updated);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물 목록의 **소단**(C군 사면안정)을 세션 사본으로 편다 — 재계산이 측점마다 읽는 값이다.
|
||||
*
|
||||
* 사용자는 「구조물 배치」에서 놓고(2026-09-07 확정), 계산은 그 사본만 본다. 달라졌을 때만
|
||||
* 다시 계산한다 — 목록은 화면을 열 때도 오므로 매번 돌리면 진입이 느려진다.
|
||||
*/
|
||||
function syncBermSpans(structures: ReadonlyArray<StructureInstance>): void {
|
||||
const currentRouteId = ctx.routeId();
|
||||
if (!projectId || currentRouteId === null) return;
|
||||
const next = bermSpansFromStructures(structures);
|
||||
if (JSON.stringify(next) === JSON.stringify(readBermSpans(projectId, currentRouteId))) return;
|
||||
writeBermSpans(projectId, currentRouteId, next);
|
||||
void reconcileStaleDesigns({ force: true });
|
||||
}
|
||||
|
||||
/** 패널 [전체 반영](N-2-1): design 보유 전 측점을 패널 최신값으로 순차 재계산한다.
|
||||
* handleDesignChange가 standardPanel.getValues()를 실어 보내므로 표준단면 수치만
|
||||
* 갱신되고 측점별 버튼 선택값은 보존된다. 순차 await로 동시 API 호출 수를 제한한다. */
|
||||
let applyingAll = false;
|
||||
async function applyPanelToAll(): Promise<void> {
|
||||
const sectionDetail = ctx.detail();
|
||||
if (applyingAll || !sectionDetail || !projectId || ctx.routeId() === null) return;
|
||||
const targets = sectionDetail.cross_sections.filter((section) => section.design);
|
||||
if (!targets.length) return;
|
||||
// 화면을 잠그지 않는다 — 카드가 하나씩 갱신되는 것이 곧 진행 표시다(CLAUDE.md 5장).
|
||||
// 대신 도는 동안 다시 누르는 것만 막는다.
|
||||
applyingAll = true;
|
||||
try {
|
||||
for (const section of targets) {
|
||||
const change = changeFromDesign(section.chainage_m);
|
||||
if (change) await handleDesignChange(section.chainage_m, change);
|
||||
}
|
||||
showToast(L("B06_Std_ApplyAll_Success"), "success");
|
||||
} finally {
|
||||
applyingAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleDesignChange,
|
||||
recomputeIfRock,
|
||||
reconcileStaleDesigns,
|
||||
syncBermSpans,
|
||||
applyPanelToAll,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Page_Grade_Edit.ts
|
||||
* 종단 계획선 편집(▲/▼) 제공자. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
||||
* 동작은 그대로다(2026-09-13 분리).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createProfileEditStore,
|
||||
type ProfileEditStore,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
adjustStation,
|
||||
buildAlignment,
|
||||
toAlignmentBase,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { SectionViewController } from "./B06_Section_UI_Section_View";
|
||||
|
||||
type GradeEditProvider = NonNullable<Parameters<SectionViewController["setGradeEdit"]>[0]>;
|
||||
|
||||
export interface GradeEditContext {
|
||||
routeId: () => number | null;
|
||||
detail: () => SectionDetailResponse | null;
|
||||
view: () => SectionViewController;
|
||||
/** 전 측점 횡단 재계산 — 손을 뗀 뒤 한 번만 돈다. */
|
||||
reconcile: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만
|
||||
* 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcile`),
|
||||
* 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장).
|
||||
*/
|
||||
export function createGradeEdit(ctx: GradeEditContext): GradeEditProvider {
|
||||
let gradeStore: ProfileEditStore | null = null;
|
||||
let gradeRouteId: number | null = null;
|
||||
/** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아
|
||||
* 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */
|
||||
const GRADE_RECONCILE_DEBOUNCE_MS = 120;
|
||||
let gradeReconcileTimer = 0;
|
||||
const scheduleGradeReconcile = (): void => {
|
||||
window.clearTimeout(gradeReconcileTimer);
|
||||
gradeReconcileTimer = window.setTimeout(() => {
|
||||
void ctx.reconcile().then(() => ctx.view().setGradeEdit(gradeEditFor));
|
||||
}, GRADE_RECONCILE_DEBOUNCE_MS);
|
||||
};
|
||||
const gradeEditFor: GradeEditProvider = () => {
|
||||
const detail = ctx.detail();
|
||||
const currentRouteId = ctx.routeId();
|
||||
if (!detail || currentRouteId === null) return null;
|
||||
const stored = readAlignment(detail.longitudinal);
|
||||
if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다.
|
||||
if (!gradeStore || gradeRouteId !== currentRouteId) {
|
||||
gradeRouteId = currentRouteId;
|
||||
gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined);
|
||||
}
|
||||
const store = gradeStore;
|
||||
const base = toAlignmentBase(stored);
|
||||
const alignment = buildAlignment(base, store.edits());
|
||||
return {
|
||||
alignment,
|
||||
stepM: alignment.policy.edit_step_m,
|
||||
onStation: (chainageM, delta) => {
|
||||
store.replace(adjustStation(base, store.edits(), chainageM, delta));
|
||||
// ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만
|
||||
// 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다).
|
||||
const current = ctx.detail();
|
||||
if (current) {
|
||||
current.longitudinal.design_profiles = [
|
||||
toDesignProfile(
|
||||
buildAlignment(base, store.edits()),
|
||||
current.longitudinal.design_profiles?.[0],
|
||||
),
|
||||
];
|
||||
}
|
||||
ctx.view().setGradeEdit(gradeEditFor);
|
||||
// ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙).
|
||||
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다.
|
||||
scheduleGradeReconcile();
|
||||
},
|
||||
};
|
||||
};
|
||||
return gradeEditFor;
|
||||
}
|
||||
@@ -279,6 +279,7 @@ export function putStandardSheetSpec(
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
blinding_concrete: string | null;
|
||||
},
|
||||
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
|
||||
return requestJson(`/projects/${projectId}/standard-sheets/spec`, {
|
||||
|
||||
@@ -37,7 +37,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY, face_slope_ratio
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
|
||||
STONE_MASONRY,
|
||||
face_slope_ratio,
|
||||
weep_hole_spec,
|
||||
)
|
||||
from common_util.common_util_excavation import (
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_BLINDING_WIDTH_M,
|
||||
@@ -223,6 +227,9 @@ def build_figure(
|
||||
entities.append(base)
|
||||
|
||||
# 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀).
|
||||
# ⚠ 값은 **그 장의 제원**에서 읽는다 — 수량표와 같은 함수를 쓴다(2026-09-13). 기본 상수를
|
||||
# 적으면 사용자가 지름·면적을 고쳐도 그림 주석만 옛 값으로 남아 표와 갈린다.
|
||||
weep_area_m2, weep_diameter_mm, _weep_basis = weep_hole_spec(sheet.get("options") or {})
|
||||
weep_y = height_m * 0.5
|
||||
entities.append(
|
||||
_line_entity(
|
||||
@@ -252,7 +259,7 @@ def build_figure(
|
||||
"left",
|
||||
),
|
||||
(
|
||||
f"물구멍 — {STONE_MASONRY['weep_hole_area_m2']:g}㎡당 1개소",
|
||||
f"물구멍 Ø{weep_diameter_mm} — {weep_area_m2:g}㎡당 1개소",
|
||||
(slope * weep_y + top_t + 0.16, weep_y),
|
||||
"left",
|
||||
),
|
||||
|
||||
@@ -35,6 +35,7 @@ EDITABLE_KEYS: tuple[str, ...] = (
|
||||
"fill_concrete_mpa",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
"blinding_concrete",
|
||||
)
|
||||
|
||||
#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`).
|
||||
@@ -59,6 +60,7 @@ FIELD_LABELS: dict[str, str] = {
|
||||
"fill_concrete_mpa": "채움 강도",
|
||||
"thickness_top_m": "상부 두께",
|
||||
"thickness_bottom_m": "하부 두께",
|
||||
"blinding_concrete": "버림 콘크리트",
|
||||
}
|
||||
|
||||
#: 야면석 계수를 어느 열에서 읽나 — 확정 ⑨ 「품셈 열이 기본, 사용자가 고를 수 있게」.
|
||||
@@ -68,6 +70,10 @@ STONE_COEFF_CHOICES: tuple[str, ...] = ("품셈", "실무 관행")
|
||||
#: 180 은 국가기준 하한(돌쌓기 전용), 210 은 콘크리트 구조물 몸체 쪽 기준.
|
||||
FILL_CONCRETE_CHOICES: tuple[str, ...] = ("180", "210")
|
||||
|
||||
#: 버림 콘크리트 넣음/안 넣음 — 확정 ⑭ 「기본은 넣고, 빼고 싶으면 뺀다」(두께는 KDS 100㎜).
|
||||
#: ⚠ 문구는 B08 `wants_blinding` 이 「빼기」로 읽는 말과 같아야 한다 — 다르면 골라도 안 빠진다.
|
||||
BLINDING_CHOICES: tuple[str, ...] = ("넣음", "안 넣음")
|
||||
|
||||
|
||||
def _clean_slope(value: Any) -> tuple[float | None, str | None]:
|
||||
"""전면 기울기 — `(값, 안내)`. 비면 `(None, None)` 이고 그것이 「자동」의 뜻이다."""
|
||||
@@ -134,6 +140,7 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
||||
for key, choices in (
|
||||
("stone_coeff_basis", STONE_COEFF_CHOICES),
|
||||
("fill_concrete_mpa", FILL_CONCRETE_CHOICES),
|
||||
("blinding_concrete", BLINDING_CHOICES),
|
||||
):
|
||||
if key not in spec:
|
||||
continue
|
||||
|
||||
@@ -38,6 +38,16 @@ PER_PLACE_OPTION_KEYS: frozenset[str] = frozenset(
|
||||
"station_m",
|
||||
"side",
|
||||
"length_m",
|
||||
# 구간 규약(C군) — 기준 측점에서 앞뒤로 얼마나 걸치나. **자리**이지 제원이 아니다
|
||||
# (2026-09-13 실측: 제원이 같은 찰쌓기 두 기가 이 칸 때문에 두 장으로 갈렸음).
|
||||
"before_m",
|
||||
"after_m",
|
||||
"inlet_revet_before_m",
|
||||
"inlet_revet_after_m",
|
||||
"outlet_revet_before_m",
|
||||
"outlet_revet_after_m",
|
||||
"inlet_basin_before_m",
|
||||
"inlet_basin_after_m",
|
||||
"note",
|
||||
"memo",
|
||||
"label",
|
||||
|
||||
@@ -75,6 +75,7 @@ class StandardSheetSpecRequest(BaseModel):
|
||||
fill_concrete_mpa: str | None = None
|
||||
thickness_top_m: float | str | None = None
|
||||
thickness_bottom_m: float | str | None = None
|
||||
blinding_concrete: str | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/standard-sheets/spec")
|
||||
|
||||
@@ -213,13 +213,33 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
const infoPanelHost = document.createElement("div");
|
||||
infoPanelHost.className = "b07-info-host";
|
||||
|
||||
/**
|
||||
* [여기서 고치기] — 도면 하단표에서 **값을 정하는 자리**로 데려간다(확정 6 ㉮
|
||||
* 「한 곳에서 정하고 도면은 비추기만」). 표는 CAD 안에 그려져 단추를 못 달고, 제원 폼은
|
||||
* 도면 목록 아래라 장이 많으면 화면 밖으로 밀린다 — 그래서 **고정 액션 줄**에 둔다
|
||||
* (떠 있는 띠는 CAD 리본과 겹쳐 2026-09-02 에 걷어낸 자리라 다시 띄우지 않는다).
|
||||
* ⚠ 값을 여기로 복사하지 않는다 — 폼 한 벌이 그대로 정본 창구다.
|
||||
*/
|
||||
const specJumpButton = createButton({
|
||||
label: "여기서 고치기",
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
infoPanelHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
infoPanelHost.querySelector<HTMLElement>("select, input")?.focus();
|
||||
},
|
||||
});
|
||||
specJumpButton.title = "이 표의 값을 정하는 제원 칸으로 갑니다.";
|
||||
specJumpButton.hidden = true;
|
||||
|
||||
const showStandardSpec = (drawing: DesignDrawingItem): void => {
|
||||
const index = Number(/_(\d+)$/.exec(drawing.id)?.[1] ?? "1") - 1;
|
||||
const sheet = standardSheets[index];
|
||||
if (!projectId || !sheet) {
|
||||
infoPanelHost.replaceChildren();
|
||||
specJumpButton.hidden = true;
|
||||
return;
|
||||
}
|
||||
specJumpButton.hidden = false;
|
||||
// 판정된 기울기는 **칸에 적지 않고 도움말로만** 비춘다 — 적어 두면 「안 정함」이 사라진다.
|
||||
const judged = /1:([\d.]+)/.exec(drawing.label)?.[1] ?? null;
|
||||
infoPanelHost.replaceChildren(
|
||||
@@ -259,6 +279,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
showStandardSpec(drawing);
|
||||
return;
|
||||
}
|
||||
specJumpButton.hidden = true; // 표준도 장이 아니면 갈 곳이 없다
|
||||
if (drawing.kind !== "cross") {
|
||||
infoPanelHost.replaceChildren();
|
||||
return;
|
||||
@@ -658,7 +679,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김).
|
||||
const confirmButtonRow = document.createElement("div");
|
||||
confirmButtonRow.className = "b07-drawing-actions__row";
|
||||
confirmButtonRow.append(frameEditor.button, confirmButton);
|
||||
confirmButtonRow.append(specJumpButton, frameEditor.button, confirmButton);
|
||||
confirmActions.append(frameEditor.banner, confirmButtonRow);
|
||||
|
||||
drawingPanel.append(infoPanelHost, confirmActions);
|
||||
|
||||
@@ -21,6 +21,8 @@ const FOUNDATIONS = ["기초유", "기초버림"] as const;
|
||||
const COEFF_BASES = ["품셈", "실무 관행"] as const;
|
||||
/** 확정 2차 ⑩ — 기본 210. 180 은 국가기준 하한(돌쌓기 전용). */
|
||||
const FILL_MPA = ["180", "210"] as const;
|
||||
/** 확정 8-2·14 — 기본은 넣음(KDS 100㎜), 빼고 싶으면 「안 넣음」. 문구가 곧 서버가 읽는 말이다. */
|
||||
const BLINDINGS = ["넣음", "안 넣음"] as const;
|
||||
|
||||
/** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */
|
||||
export interface StandardSheetSpec {
|
||||
@@ -42,6 +44,7 @@ export interface StandardSpecResult {
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
blinding_concrete: string | null;
|
||||
}
|
||||
|
||||
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
|
||||
@@ -123,6 +126,7 @@ export function buildStandardSpecPanel(
|
||||
const foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —");
|
||||
const coeff = select(COEFF_BASES, options.stone_coeff_basis, "— 안 정함(품셈) —");
|
||||
const mpa = select(FILL_MPA, options.fill_concrete_mpa, "— 안 정함(210) —");
|
||||
const blinding = select(BLINDINGS, options.blinding_concrete, "— 안 정함(넣음) —");
|
||||
|
||||
const slope = document.createElement("input");
|
||||
slope.className = "b07-spec__input";
|
||||
@@ -156,6 +160,7 @@ export function buildStandardSpecPanel(
|
||||
),
|
||||
field("야면석 계수", coeff, "비우면 품셈 열을 씁니다."),
|
||||
field("채움 강도 (MPa)", mpa, "비우면 210. 180 은 국가기준 하한입니다."),
|
||||
field("버림 콘크리트", blinding, "비우면 넣습니다(두께 100㎜) — 빼려면 「안 넣음」."),
|
||||
);
|
||||
if (THICKNESS_TYPES.has(sheet.type_id)) {
|
||||
panel.append(
|
||||
@@ -199,6 +204,7 @@ export function buildStandardSpecPanel(
|
||||
foundation: foundation.value || null,
|
||||
stone_coeff_basis: coeff.value || null,
|
||||
fill_concrete_mpa: mpa.value || null,
|
||||
blinding_concrete: blinding.value || null,
|
||||
thickness_top_m: THICKNESS_TYPES.has(sheet.type_id)
|
||||
? topThickness.value.trim() || null
|
||||
: null,
|
||||
|
||||
@@ -93,7 +93,7 @@ STONE_WEIGHT_BASIS = (
|
||||
#: ⚠ **잠정이다** — 확정 문구가 「나중에 실무자 협의 후 명확히」이므로 화면 근거에 적는다.
|
||||
STONE_PACKING_RATIO = 0.77 # 채움률 — 돌 사이 공극을 뺀 몫
|
||||
STONE_DENSITY_TON_M3 = 2.65 # 석재 비중
|
||||
#: 계산식이 안 맞아 **관측표**를 쓰는 종류. 안 고른 경우도 이쪽(지금까지 쓰던 값이 그것).
|
||||
#: 계산식이 안 맞아 **관측표**를 쓰는 종류. 안 고른 경우는 **계산식**으로 선다(`stone_masonry` 주석).
|
||||
STONE_WEIGHT_OBSERVED_KINDS = frozenset({"야면석·호박돌", "야면석"})
|
||||
STONE_WEIGHT_FORMULA_BASIS = (
|
||||
"⭐ 확정 5차 — 뒷길이 × 0.77(채움률) × 2.65(비중). ⚠ 잠정 — 실무자 협의 후 명확히 할 값"
|
||||
|
||||
@@ -22,6 +22,7 @@ import os
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceKind
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis_UnitBasis import similar_occupation_note
|
||||
|
||||
_MANIFEST = ("resources", "data_cost_input_value", "_manifest.json")
|
||||
_MASTER_DIR = ("resources", "data_work_item_master")
|
||||
@@ -118,6 +119,9 @@ def work_item_basis(build: Any) -> list[dict[str, Any]]:
|
||||
source = build.factor_sources.get(work_item_code)
|
||||
if source and source not in notes:
|
||||
notes.append(str(source))
|
||||
similar = similar_occupation_note(work_item_code)
|
||||
if similar and similar not in notes:
|
||||
notes.append(similar)
|
||||
if not notes:
|
||||
continue
|
||||
rows.append(
|
||||
|
||||
@@ -43,6 +43,21 @@ _RE_BASIS = re.compile(r"^(㎡|㎥|㏊|m|㎝|개|본|주|ton|톤|kg|㎏)\s*당$"
|
||||
#: 밑수 표기 → 그 갈래가 갖는 단위.
|
||||
_BASIS_UNIT = {"㏊": "ha", "톤": "ton", "㎏": "kg"}
|
||||
|
||||
#: 품셈이 **노임표에 없는 직종명**을 쓴 자리 → 유사 직종
|
||||
#: (2026-09-09 사용자 확정 8-1 「바꿔쓰기 허용」).
|
||||
#: 근거 — 「미조사 직종은 유사 직종 단가에 준하여 적용」(재경원 회계 45101-45).
|
||||
#: ⚠ **공종 단위로만 건다** — 「인부」를 전역으로 읽으면 확정 밖 공종까지 조용히 붙는다.
|
||||
SIMILAR_OCCUPATION = {("FP-13-02-04", "인부"): "보통인부"}
|
||||
|
||||
|
||||
def similar_occupation_note(work_item_code: str) -> str:
|
||||
"""그 공종이 유사 직종으로 바꿔 쓴 자리면 산출근거 문구, 아니면 빈 문자열."""
|
||||
return " · ".join(
|
||||
f"품셈 「{written}」 → {used} 준용(유사 직종 · 재경원 회계 45101-45 · 사용자 확정 8-1)"
|
||||
for (code, written), used in SIMILAR_OCCUPATION.items()
|
||||
if code == work_item_code
|
||||
)
|
||||
|
||||
|
||||
def _tight(text: str) -> str:
|
||||
return "".join(str(text or "").split())
|
||||
@@ -93,7 +108,8 @@ def match_unit_basis_table(
|
||||
continue
|
||||
if basis_at > 0:
|
||||
# 이름이 앞에 붙은 줄 — 「인 부 | ㎡당 | 0.11 | …」. 이름을 여기서 갱신한다.
|
||||
found = _resolve(catalog, cells[0])
|
||||
name_cell = SIMILAR_OCCUPATION.get((work_item_code, _tight(cells[0])), cells[0])
|
||||
found = _resolve(catalog, name_cell)
|
||||
if found is None:
|
||||
result.unmatched.append(
|
||||
UnmatchedRow(
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"not_here": {
|
||||
"note": "이 표가 정하지 않는 것.",
|
||||
"items": [
|
||||
"돌중량(ton/㎡) — 우리 값 0.575/0.88/1.10 은 **실무 관측**(울진 라이브러리)이다. 품셈은 「돌의 중량은 형상·종류·부피를 고려하고 건설품셈 1-3-3 재료의 단위중량을 참고하여 계상한다」로만 두어 **표를 안 준다.** 돌 종류 축과 짝이 맞는지 사용자 확정 대기.",
|
||||
"돌중량(ton/㎡) — 품셈은 「돌의 중량은 형상·종류·부피를 고려하고 건설품셈 1-3-3 재료의 단위중량을 참고하여 계상한다」로만 두어 **표를 안 준다.** ⭐ 사용자 확정 5차 큰 것 7(2026-09-09) — **계산식 `뒷길이 × 0.77 × 2.65` 이 기본**이고 야면석만 실무 관측(울진 라이브러리 0.575/0.88/1.10)으로 갈림. 식과 종류 갈래는 `B08_Quantity_Engine_UnitQuantity.stone_weight_per_m2` 한 벌. ⚠ 잠정 — 「실무자 협의 후 명확히」.",
|
||||
"전면면적이 정면적인지 비탈면적인지 — 2026 개정안 [주] 「시공량은 석재의 **전면면적**(㎡)을 기준한다」로 용어만 확인됐다."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# STmate 경쟁 프로그램 분석 — 요약과 읽는 법
|
||||
|
||||
> 재구현 상세판(2026-09-13): 00~04는 전체 구조와 데이터 계보, 05~09는 동일 계산 엔진 구현 규칙, 10은 System 전수 장부, 11은 실행모듈에서 복원한 계산 코어, 12는 원가계산 4형식과 RATE표, 13은 남은 경계를 가를 통제시험, 14는 최종 완료 보고서다.
|
||||
>
|
||||
> **처음이면 14번(최종 보고서)부터 읽는 것이 빠르다.**
|
||||
|
||||
| 문서 | 용도 | Orca 아티팩트 |
|
||||
|---|---|---|
|
||||
| 00 분석 요약과 읽는 법 (이 문서) | 전체 인덱스, 결론, Aislo 비교 | https://share.onorca.dev/a/uHEsfWTvedw1 |
|
||||
| 01 전체 구조 | 실행 기술, 모듈, 파일 흐름, 기능 구조 | https://share.onorca.dev/a/jjxDJTOtRGq7 |
|
||||
| 02 데이터셋과 파일형식 | DBF 스키마, 프로젝트 패키지, 기준자료, 암호화 경계 | https://share.onorca.dev/a/boO2-Fo4CFNi |
|
||||
| 03 계산로직과 Aislo 비교 | 확인된 공식, 계층 합산, 단가·제비율 모델, 차이와 권고 | https://share.onorca.dev/a/fyN0SVUu3Oum |
|
||||
| 04 계산 데이터계보 | 입력 필드 → 계산 → 결과 필드 단계별 추적 | https://share.onorca.dev/a/S1E38cIhGiz6 |
|
||||
| 05 재구현 판정기준과 근거목록 | 확정등급, 조사 표본, 파이프라인, 중간값 계약 | https://share.onorca.dev/a/8ktCQIIDvMuP |
|
||||
| 06 규칙1·7 | RATE 좌표, 간접비 순서, 끝전·VAT | https://share.onorca.dev/a/z2v9o5Wfaim0 |
|
||||
| 07 규칙2·3·4·5 | 직접비 단가, 중기, BDQTY, 자재 채택 | https://share.onorca.dev/a/o0HLxAXG8Lhg |
|
||||
| 08 규칙6·8·9 | 재계산, 변경설계, 일괄변경 | https://share.onorca.dev/a/z58ZjH9Nu_Pb |
|
||||
| 09 통합 재구현 명세 | 내부 데이터셋, 계산 순서, 호환 어댑터, 골든시험 | https://share.onorca.dev/a/7eULzjbfyZHM |
|
||||
| 10 System 186개 파일 분석장부 | 전 파일별 판독 상태, 실제 형식, 핵심 모듈과 기준자료 구조 | https://share.onorca.dev/a/zHj5IbV4dfnL |
|
||||
| 11 CPM 계산코어 정적분석 | 식 평가, 반올림, 간접비·안전비·변경설계·끝전조정 재구현 명세 | https://share.onorca.dev/a/U6HIpHCbt9XF |
|
||||
| 12 원가계산 4형식과 RATE표 복원 | 일반·수공·실적일반·실적수공 공식, 화면 열, 내부 선택값 | https://share.onorca.dev/a/y-R9TQqgEFfo |
|
||||
| 13 통제시험 목록 | 정적 분석으로 못 가르는 경계를 시험 11개로 분리, 우선순위·완료판정 | https://share.onorca.dev/a/lcmxdrG2SWvE |
|
||||
| 14 최종 분석완료 보고서 | 도달 수준, A등급 확정 목록, 남은 것, 완료 판정 기준, 산출물 | https://share.onorca.dev/a/AmgldP7q_Qzc |
|
||||
|
||||
중요: C등급으로 표시한 규칙은 “분석 누락”이 아니라 보유 정적 자료에 입력 전후 대응값이 없거나 레코드가 난독화되어 확정 불가능한 경계다. 문서에는 각 경계를 확정할 STmate 통제 대조시험까지 명시했다. System 폴더는 186/186개 모두 내부 구조까지 판독했으며 미판독 파일은 0개다.
|
||||
|
||||
**등급 갱신 이력 (2026-09-13)** — 11·12번의 폼 리소스 복원 결과로 06·08의 다음 판정이 올라갔다.
|
||||
|
||||
| 항목 | 이전 | 현재 | 근거 |
|
||||
|---|---|---|---|
|
||||
| RATE 네 표의 행·열 라벨 | 미확정 | 그리드별 열 제목·행 수 확정 (남은 것은 시작 좌표) | 12번 §8·§9 |
|
||||
| 요율 순환 의존성의 STmate 처리 | B | **A** — 고정점 반복 없음, 사용자 수동 확정 | 11번 §8 |
|
||||
| `RATE_RA.DBF` | C | **A/B** — 실적형식 규모·기간·안전비 그리드 대응 | 12번 §9 |
|
||||
| 변경설계 간접비 계산모드 | 권고 수준 | **A** — 방법 A/B 공식과 `cBx_CH_BO` 3모드를 코드에서 확인 | 11번 §10, 12번 §10.3 |
|
||||
| 끝전조정 보존 필드 | 3칸 | **6칸** — 함수·자리수가 설정별로 따로 | 11번 §11, 12번 §10.2 |
|
||||
| 반올림 음수·절반값 방향 | C | **A** — 최근접-짝수(은행가), 0 대칭. `SetRoundMode` 미사용·`SimpleRoundTo` 미임포트로 확정 | 11번 §4.2 |
|
||||
| 내부 계산 ↔ 출력 XLSX 반올림 | 같다고 가정 | **다름** — 20개 경계 중 10개가 원 단위에서 갈림 | 11번 §4.2 경계표 |
|
||||
| 간접비 항목별 밑수 | 추정 서술 | **A** — 화면 그리드 묶음 제목에 공식이 그대로 적혀 있음 | 06번 §3 |
|
||||
| 환경보전비·보증수수료 밑수 | 직접공사비 | **정정** — `재료비+직접노무비+산출경비` (실적형식만 직접공사비) | 06번 §3, 04번 §7 |
|
||||
| `JB_R` 35열 확장 시점 | "2020 이후" | **2020-05-27 확정** — 기존 1~20열은 한 셀도 안 바뀜 | 10번 §4 |
|
||||
| `JB_R` 열 의미 | 미상 | **1~5 간접노무비 / 6~10 기타경비** (공종 5개 순서 확정), 21·26 국가유산 | 10번 §4 |
|
||||
| 원가계산 설정 선택축 | 16축 목록 | **83축 전수** — 공통 29 + 형식별 12/11/16/15 | 12번 §10.1-2 |
|
||||
|
||||
분석일: 2026-09-13
|
||||
대상: `C:\경영정보(주)`
|
||||
비교 대상: Aislo 저장소의 B08 수량 산출·B09 견적 계산 영역
|
||||
|
||||
## 결론
|
||||
|
||||
STmate는 **Delphi 6 네이티브 모듈 + Apollo/Six 계열 DBF 저장소 + ZIP 기반 교환 파일**로 구성된 로컬 설계예산 프로그램이다. 핵심 업무 모델은 다음 네 층으로 요약된다.
|
||||
|
||||
1. 기초 단가·코드 자료를 시기별 파일로 배포한다.
|
||||
2. 자재(M)·노임(L)·중기 취득가(S)를 원천으로 시간당 중기사용료(X), 일위대가(B), 단가산출(D)을 조립한다.
|
||||
3. 공종별 수량에 재료·노무·경비 단가를 곱해 내역 행을 만든다.
|
||||
4. 행을 공종 계층으로 합산하고 제비율·법정경비·일반관리비·이윤·부가세를 적용한다.
|
||||
|
||||
분석의 중심은 특정 프로젝트 금액이 아니라 **값의 출처와 전달 경로**다. STmate는 공통 기초자료에서 자원 단가를 가져오고, 공종별 산출근거가 자원 코드와 품을 참조해 복합단가를 만들며, 프로젝트 수량이 그 단가를 받아 내역금액이 되고, 내역의 비목별 합계가 제비율 계산의 밑수로 전달되는 구조다.
|
||||
|
||||
```text
|
||||
노임·자재·중기 기초자료
|
||||
→ 출처별 후보단가와 채택단가
|
||||
→ 일위대가·단가산출(자원코드 × 품, 할증, 기계 Q식)
|
||||
→ 공종별 재료/노무/경비 단가
|
||||
→ 프로젝트 수량 × 단가
|
||||
→ 직접재료비·직접노무비·산출경비
|
||||
→ 제비율별 고유 밑수 × 적용률
|
||||
→ 순공사원가 → 일반관리비 → 이윤 → 부가세 → 총공사비
|
||||
```
|
||||
|
||||
## Aislo와의 핵심 비교
|
||||
|
||||
| 관점 | STmate | Aislo | 판단 |
|
||||
|---|---|---|---|
|
||||
| 제품 범위 | 견적·내역 작성 중심 | 임도 선형·횡단·구조물 수량부터 견적까지 | Aislo가 상류 설계 자동화 범위가 넓다 |
|
||||
| 실행 구조 | 단일 Windows 데스크톱, 다수 CPM 플러그인 | FastAPI/Python 계산 + TypeScript 웹 UI | Aislo가 배포·협업·감사에 유리, STmate는 오프라인 운영이 단순 |
|
||||
| 단가 모델 | 코드 기반 계층, 4안·복수 단가 슬롯 | 같은 계층을 명시적 타입과 재귀 계산으로 구현 | 핵심 모델은 유사하나 Aislo가 의미와 오류를 코드로 더 명확히 표현 |
|
||||
| 저장 형식 | DBF/SMT, 일부 레코드 암호화, STC ZIP | UTF-8 JSON, 매니페스트, SHA-256, 프로젝트 스냅샷 | Aislo가 이식성·검증성·버전 추적에서 우세 |
|
||||
| 결측 처리 | 데이터 필드에서 0과 미입력의 구분이 불명확한 자리 존재 | 채택 단가가 없으면 오류/경고로 금액 확정을 차단 | Aislo 방식 유지 권장 |
|
||||
| 재현성 | 프로젝트 파일 안에 단가·제비율 테이블을 함께 보관 | 기준자료 버전과 단가 스냅샷 지문을 기록 | 둘 다 과거 결과 고정 지향, Aislo가 근거 추적을 더 명시화 |
|
||||
| 계산 투명성 | 결과 DBF에는 성분별 단가·금액이 남음 | 산출 근거·출처·미결·단수 위치를 별도 모델로 제공 | Aislo 우세 |
|
||||
|
||||
## 당장 반영할 가치가 높은 점
|
||||
|
||||
- STmate의 **프로젝트 한 묶음 교환**은 실용적이다. Aislo도 프로젝트 내보내기에 입력, 채택 단가 스냅샷, 계산 설정, 산출 결과, 매니페스트를 한 파일로 묶으면 장기 보관과 인수인계가 쉬워진다.
|
||||
- STmate의 **당초/변경 및 최대 4개 안 병렬 필드**는 설계변경 비교에 유용하다. Aislo에는 DB 열을 네 벌 복제하기보다 기존 프로젝트 버전/스냅샷을 이용한 비교 화면이 더 적합하다.
|
||||
- STmate의 **보고서 서식 데이터화**(`SET_REP`)는 출력물 요구가 많은 실무에 강하다. Aislo도 보고서별 열 이름·폭·표시 여부를 데이터로 관리할 가치가 있다.
|
||||
- 단가 슬롯은 이미 Aislo가 6개 원천과 채택 슬롯으로 모델링하고 있어 방향이 맞다.
|
||||
|
||||
## 문서 구성
|
||||
|
||||
전체 13개 문서의 용도와 링크는 맨 위 표에 있다. 읽는 순서는 다음을 권한다.
|
||||
|
||||
1. **처음 보는 경우** — 00(이 문서) → 01 전체 구조 → 02 데이터셋
|
||||
2. **계산을 재구현하려는 경우** — 05 판정기준 → 11 계산코어 → 12 4형식·RATE → 06·07·08 규칙 → 09 통합 명세
|
||||
3. **파일 하나를 추적하려는 경우** — 10 전수 장부 → `system_inventory.csv`
|
||||
4. **값의 출처를 따라가려는 경우** — 04 계산 데이터계보
|
||||
|
||||
## 분석 경계와 신뢰도
|
||||
|
||||
이 분석은 설치 파일과 맛보기 프로젝트를 수정하지 않은 정적 분석이다. 정상 실행을 시도했으나 프로세스가 창을 노출하지 않아 UI 동작은 확인하지 못했다. 암호화된 기준자료는 비밀번호 추측이나 우회를 하지 않았다.
|
||||
|
||||
- **확인**: 파일 헤더, DBF 스키마, 평문 레코드, ZIP 목록, 실행 파일 버전 정보, Aislo 소스
|
||||
- **강한 추론**: 모듈명과 스키마가 함께 가리키는 업무 흐름
|
||||
- **미확인**: 각 그리드가 `RATE*` 행렬에 적재되는 시작 행 좌표, 구간 경계의 포함 방향(`<`/`<=`), `ROUNDUP`·`MINT`의 음수 방향, 암호화 기준자료의 실제 값, UI 조작 순서
|
||||
|
||||
제비율 표의 **화면상 명칭 자체는 더 이상 미확인이 아니다** — 폼 리소스에서 15개 그리드의 열 제목과 행 수를 복원했다(12번 §8).
|
||||
|
||||
미확인 항목은 모두 **13번 통제시험 목록**에 시험 단위로 옮겨 놓았다. 어느 시험이 무엇을 확정하는지, 우선순위는 무엇인지가 그 문서에 있다.
|
||||
|
||||
## 용어 대조 — 같은 것을 다르게 부르는 자리
|
||||
|
||||
문서마다 출처가 달라 용어가 갈리는 곳이 있다. 아래는 **같은 것**이다.
|
||||
|
||||
| 이 문서군의 표기 | 같은 뜻의 다른 표기 | 비고 |
|
||||
|---|---|---|
|
||||
| 제비율 | 간접비, 경비율 | STmate 화면은 「제비율」 |
|
||||
| 산출경비 | 직접경비 | 03번만 「직접경비」를 쓴다 |
|
||||
| 국가유산(수리) | 문화재(수리) | 2024년 법령 개정으로 명칭이 바뀐 같은 공종 |
|
||||
| 제잡비 표준시장방식 | 제잡비 실적공사방식 | **화면과 내부 이름이 다르다** — 12번 §10.1-3 |
|
||||
| 산림조합 형식 | 임 협 식 | 부가세 옵션. 역시 화면≠내부 |
|
||||
| 실적형식 `TA`/`SA` | 표준시장단가 형식 | 같은 것 |
|
||||
|
||||
**등급 표기**는 05번 §「확정등급」이 유일한 기준이다 — `A 확정 / B 강한 추정 / C 미확정`. `A/B`는 구조는 확정이나 한 칸이 남은 경우다.
|
||||
@@ -0,0 +1,106 @@
|
||||
# STmate 전체 구조 분석
|
||||
|
||||
## 1. 제품 식별
|
||||
|
||||
- 주 실행 파일: `System\STmate8.Exe`
|
||||
- 파일 설명: 설계예산시스템
|
||||
- 제품명: 내역서작성 프로그램
|
||||
- 회사: 경영정보(주)
|
||||
- 파일 버전: 26.9.1.0
|
||||
- 제품 버전: w26.09
|
||||
- 실행 파일 크기: 145,408바이트
|
||||
|
||||
주 실행 파일이 작은 대신 기능 대부분을 `*.CPM` 모듈로 분리했다. CPM은 헤더가 `MZ`인 Windows PE 실행 모듈이므로 단순 데이터 파일이 아니라 동적으로 적재되는 코드 모듈로 판단된다.
|
||||
|
||||
## 2. 기술 스택
|
||||
|
||||
설치 파일에는 `RTL60.BPL`, `DBRTL60.BPL`, `BDERTL60.BPL`, `designide60.bpl`이 있다. 이는 Delphi 6 런타임/DB 런타임 구성과 일치한다. `Apollo5D6.bpl`, `SDE32/50.DLL`, `SDECDX/NSX/NTX50.DLL`, `IDDBAS32.DLL`, `IDPDX32.DLL`은 DBF·dBase·Paradox 계열 데이터 접근을 뒷받침한다. `RealGrid6*.bpl`은 표 기반 UI를 담당하는 것으로 보인다.
|
||||
|
||||
구조는 대략 다음과 같다.
|
||||
|
||||
```text
|
||||
STmate8.Exe
|
||||
├─ Delphi/Borland 런타임(BPL)
|
||||
├─ DBF 엔진(Apollo/Six/BDE 계열)
|
||||
├─ RealGrid 표 UI
|
||||
├─ ST_CPM/*.CPM 기능 모듈
|
||||
├─ ST_BSC/* 기준 단가·제비율 배포자료
|
||||
└─ 프로젝트 폴더(DBF+SMT+INI) ↔ STC 압축 파일
|
||||
```
|
||||
|
||||
## 3. 기능 모듈 분해
|
||||
|
||||
파일명에 기반한 기능 구분이다. 괄호 안은 신뢰도다.
|
||||
|
||||
| 모듈 | 해석 |
|
||||
|---|---|
|
||||
| `ST_Boq.CPM` | 내역서 편집·집계 핵심(높음) |
|
||||
| `STmateQty8.CPM` | 수량 관련 기능(높음) |
|
||||
| `ST_EdiLWi.CPM`, `ST_P_iLWi.CPM` | 일위대가 편집·출력(높음) |
|
||||
| `ST_EdSan.CPM`, `ST_P_San.CPM`, `ST_SanCal.CPM` | 단가산출서 편집·출력·계산(높음) |
|
||||
| `ST_Jabpum.CPM` | 잡품/자원 자료 관리(중간) |
|
||||
| `ST_KanJub.CPM` | 간접비·제비율 계산(높음) |
|
||||
| `ST_P_ConEst.CPM` | 원가계산서 출력(높음) |
|
||||
| `ST_P_Collect.CPM` | 자원 집계표 출력(높음) |
|
||||
| `ST_P_Excav.CPM`, `ST_P_Joong.CPM` | 굴착/중기 관련 출력(중간) |
|
||||
| `ST_P_Nae.CPM`, `ST_P_List.CPM` | 내역·목록 출력(높음) |
|
||||
| `ST_P_XLS.CPM` | Excel 내보내기(높음) |
|
||||
| `ST_Prt.CPM` | 공통 인쇄(높음) |
|
||||
| `ST_U_Archive.CPM` | 프로젝트 압축·보관(높음) |
|
||||
| `ST_U_ChgAmt.CPM`, `ST_U_ChgCode.CPM` | 단가/코드 일괄 변경(높음) |
|
||||
| `ST_U_Convert.CPM` | 외부/구형 형식 변환(중간) |
|
||||
| `ST_U_Merge.CPM`, `ST_U_ConBD.CPM` | 프로젝트·내역 병합(중간) |
|
||||
| `ST_U_DelCode.CPM`, `ST_U_DelPrj.CPM` | 코드·프로젝트 삭제(높음) |
|
||||
| `ST_U_Indx.CPM` | 색인 재구축(높음) |
|
||||
|
||||
이름이 `Ed`, `P`, `U`로 반복되는 점은 편집(Editor), 출력/인쇄(Print), 유틸리티(Utility) 계열을 접두어로 구분한 플러그인 구조를 시사한다.
|
||||
|
||||
## 4. 업무 흐름
|
||||
|
||||
정적 자료가 가리키는 전체 흐름은 다음과 같다.
|
||||
|
||||
```text
|
||||
시기별 기준자료
|
||||
├─ 노임·기계경비
|
||||
├─ 자재단가
|
||||
├─ 표준일위대가
|
||||
├─ 표준시장단가
|
||||
└─ 제비율
|
||||
↓ 선택/가져오기
|
||||
프로젝트 단가 마스터(COSTN/COSTA + SMT)
|
||||
↓ 코드 참조와 상세 구성
|
||||
수량/공종 자료(BDQTY, QCODE, KONGJONG)
|
||||
↓ 수량 × 3성분 단가
|
||||
내역서(BOQ/BOQ11/BQM)
|
||||
↓ 계층 소계·제비율
|
||||
원가계산/집계/보고서(SET_REP 설정)
|
||||
↓
|
||||
인쇄·Excel·STC 보관
|
||||
```
|
||||
|
||||
## 5. 프로젝트와 기준자료의 분리
|
||||
|
||||
`ST_BSC`는 공급자가 배포하는 시기별 기준자료이고, `STmate 자료\설계자료\실습(맛보기)`는 프로젝트 작업본이다. 프로젝트 STC를 풀면 확장 폴더와 바이트 단위로 동일한 23개 파일이 나온다. 즉 STC는 별도 논리 포맷이 아니라 **프로젝트 디렉터리의 무손실 ZIP 패키지**다.
|
||||
|
||||
이 분리는 중요한 설계 원칙을 보여준다.
|
||||
|
||||
- 기준자료가 업데이트되어도 프로젝트가 가진 작업본은 독립적으로 보존된다.
|
||||
- 프로젝트 전달은 단일 파일로 가능하다.
|
||||
- 계산 결과뿐 아니라 설정·단가·제비율·보고서 설정이 함께 이동한다.
|
||||
|
||||
## 6. Aislo 구조와 비교
|
||||
|
||||
Aislo는 `B08_Quantity`에서 설계 결과를 공종·자재 인계 데이터로 만들고 `B09_Estimation`에서 단가 계층, 내역서, 법정경비, 원가계산서를 생성한다. STmate가 하나의 데스크톱 프로그램 안에서 표 편집 중심으로 모든 기능을 묶은 반면 Aislo는 다음처럼 책임을 분리한다.
|
||||
|
||||
```text
|
||||
GIS/선형/횡단/구조물(B03~B07)
|
||||
↓ 명시적 handoff
|
||||
수량 산출(B08)
|
||||
↓ work_items + materials
|
||||
단가·일위대가·내역·원가(B09)
|
||||
↓ API
|
||||
웹 UI 및 프로젝트 저장
|
||||
```
|
||||
|
||||
STmate의 장점은 사용자가 프로젝트 폴더만 이해하면 되는 단순한 오프라인 운영이다. Aislo의 장점은 설계와 수량의 자동 연결, 계산 책임 분리, 서버 기반 협업, 테스트 가능한 계산 모듈이다.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# STmate 데이터셋과 파일 형식 분석
|
||||
|
||||
## 1. 파일군 개요
|
||||
|
||||
| 확장자 | 수량 | 역할 |
|
||||
|---|---:|---|
|
||||
| `.CPM` | 46 | Windows PE 기능 모듈 |
|
||||
| `.DBF` | 19 | 프로젝트의 구조화 테이블 |
|
||||
| `.SMT` | 4 | DBF memo 보조 저장소(Six memo) |
|
||||
| `.STBSC` | 28 | 암호화 ZIP 기반 기준자료 배포 패키지 |
|
||||
| `.BSC` | 7 | 대형 기준 코드/단가 테이블, 암호화 DBF 계열 |
|
||||
| `.JB_R` | 27 | 일반 공사 제비율 표 |
|
||||
| `.JB_RA` | 14 | 표준시장단가용 제비율 표 |
|
||||
| `.CSTA` | 6 | 표준시장단가 대형 테이블 |
|
||||
| `.STC` | 1 | 프로젝트 전체 ZIP 패키지 |
|
||||
|
||||
## 2. STC 프로젝트 패키지
|
||||
|
||||
`실습(맛보기).STC`는 암호화되지 않은 표준 ZIP이다. 내부 파일은 확장된 프로젝트 폴더와 SHA-256 기준으로 모두 동일하다.
|
||||
|
||||
```text
|
||||
QCODE.SMT, Start8x.Ini, BDQTY.DBF, BOQ.DBF, BOQ11.DBF,
|
||||
BQM.DBF, COSTA.DBF, COSTN.DBF, GRASS.DBF, KONGJONG.DBF,
|
||||
PRJLST.DBF, QCODE.DBF, RATE*.DBF, SET_REP.DBF, SYSINFO.DBF,
|
||||
STini.MEM, COSTN.SMT, PRJLST.SMT
|
||||
```
|
||||
|
||||
프로젝트 하나에 입력, 계산 중간물, 결과, 출력 설정이 함께 들어간다. 장점은 보관과 전달이며, 단점은 같은 사실이 여러 파생 테이블에 중복될 수 있다는 점이다.
|
||||
|
||||
## 3. 주요 DBF 테이블
|
||||
|
||||
### `PRJLST.DBF` — 프로젝트 메타데이터
|
||||
|
||||
1개 레코드, 48필드. 프로젝트명·코드·분류와 4개 안별 재료/노무/경비/총액, 메모, 최종 단계, 옵션을 가진다.
|
||||
|
||||
주요 필드: `PNUM`, `PNAME`, `PCODE`, `PCLASS`, `MCOST1..4`, `LCOST1..4`, `SCOST1..4`, `TCOST1..4`, `COSTW1..4`, `PRJ_MEMO`, `FINL_LV`, `ACTTYP`, `ACTOPT`.
|
||||
|
||||
### `COSTN.DBF` — 단가 제목/마스터
|
||||
|
||||
71개 레코드, 101필드. 코드·명칭·규격·단위와 네 안별 총/재료/노무/경비 단가를 저장한다. 6개 가격 원천과 쪽수, 채택 관련 필드도 가진다.
|
||||
|
||||
주요 필드:
|
||||
|
||||
- 식별: `CODE`, `NAME`, `SIZE`, `UNIT`
|
||||
- 연결: `BQCODE`, `EQCODE`, `CONTCODE`, `WQCODE`, `JDCODE`
|
||||
- 성분별 단가: `TAMT1..4`, `LAMT1..4`, `MAMT1..4`, `SAMT1..4`
|
||||
- 6개 원천/4개 안: `AMT_11..64`, `PG_11..54`
|
||||
- 표시·분류: `GUN`, `GUN2`, `IFORM`, `ACTTYP`, `EDT_DATE`
|
||||
|
||||
본문은 암호화되어 있으나 스키마는 평문이다. memo 내용은 `COSTN.SMT`에 저장된다.
|
||||
|
||||
### `COSTA.DBF` — 별도 단가 자료
|
||||
|
||||
53필드. 구조가 COSTN의 축약형이며 `CODEA`, `AAMT1..4`, 성분별 단가, 환율/달러 관련 필드를 포함한다. 맛보기에는 레코드가 없다.
|
||||
|
||||
### `BOQ11.DBF` — 내역서
|
||||
|
||||
36개 레코드, 83필드. 말단 공종과 계층 제목·계·합계를 같은 테이블에 저장한다.
|
||||
|
||||
- 식별: `ITNUM`, `NAME1`, `SIZE1`, `UNIT1`, `BDCODE1`
|
||||
- 수량: `QTY1..4`
|
||||
- 단가: `TAMT1..4`, `LAMT1..4`, `MAMT1..4`, `SAMT1..4`
|
||||
- 금액: `T_AMT1..4`, `L_AMT1..4`, `M_AMT1..4`, `S_AMT1..4`
|
||||
- 계층: `SORTCODE`, `ENDCODE`, `ITM_TYP`
|
||||
- 특수 수량: `QTY1_B`, `QTY1_N`, `QTY1_O`, `QTY1_H`
|
||||
|
||||
`BOQ.DBF`는 같은 스키마의 빈 작업 테이블이고, `BOQ11.DBF`가 맛보기의 실제 내역 판이다.
|
||||
|
||||
### `BDQTY.DBF` — 단가/공종별 수량 상세
|
||||
|
||||
212개 레코드, 37필드. `BDCODE`, 참조 `CODE`, 명칭·규격·단위, 네 안별 수량(`QTY1..4`), 비율(`RATE`), 원수량·할증 계열(`X_QTY1..3`, `X_HAL1..3`)을 가진다. 본문은 암호화되어 값 자체는 확인하지 않았다.
|
||||
|
||||
### `QCODE.DBF` — 수량 코드/식
|
||||
|
||||
15필드. `CODE`, `NAME`, `BQCODE`, `QTY0..9`, `IFORM`, `XCODE`로 구성된다. 맛보기에는 레코드가 없으나 수량 계산식 또는 수량 슬롯을 코드화하는 자리로 보인다.
|
||||
|
||||
### `KONGJONG.DBF` — 공종 분류
|
||||
|
||||
`CODE`, `NAME`, `SIZE` 세 필드의 작은 분류표다.
|
||||
|
||||
### `RATE*.DBF` — 제비율 행렬
|
||||
|
||||
- `RATE`: 41행 × 10열
|
||||
- `RATE_A`: 25행 × 10열
|
||||
- `RATE_B`: 35행 × 10열
|
||||
- `RATE_M`: 25행 × 10열
|
||||
- `RATE_R`: 50행 × 35열
|
||||
- `RATE_RA`: 50행 × 20열
|
||||
|
||||
모두 수치 행렬이며 열 이름은 `RATE0..9` 또는 `RATE_01..35`다. 이름·밑수 규칙이 행렬에 함께 담기지 않아 프로그램 코드/화면 정의와 결합되어야 의미가 생긴다. 이 점은 외부 분석과 장기 유지보수에 불리하다.
|
||||
|
||||
### `SET_REP.DBF` — 보고서 설정
|
||||
|
||||
75개 레코드, 28필드. 보고서 식별자, 명칭, 머리말, 필드 수, 최대 11개 열의 이름과 폭, 페이지 부가설정을 가진다. 맛보기에는 재료비·노무비·경비·일식/견적 목록표 등이 등록되어 있다.
|
||||
|
||||
### `SYSINFO.DBF` — 시스템 설정
|
||||
|
||||
`PART`, `NUM`, `LOG`, `CHR`, `DATE`의 범용 키-값 구조다. 본문은 암호화되어 있다.
|
||||
|
||||
## 4. 암호화와 압축 경계
|
||||
|
||||
- STC 프로젝트 패키지: ZIP, 암호화 없음.
|
||||
- STBSC 기준자료: ZIP 헤더이며 각 엔트리의 암호화 플래그가 켜져 있다. 내부 이름은 `CH_NAME.ST`, `COST.ST`, `info.ST`, `MEM.ST`, `XDET.ST` 등이다.
|
||||
- DBF: 버전 바이트 `03`인 표는 평문, `06/E5/E6`인 일부 표는 스키마만 읽히고 레코드 본문은 암호화되어 있다. 설치된 Apollo/Six 계열 엔진과 일치하는 현상이다.
|
||||
- SMT: 헤더에 `SiXMemo`가 확인된다.
|
||||
|
||||
암호화는 기준자료 무단 열람 억제와 제품 종속성 유지에는 유리하지만 데이터 이식, 자동 검증, 장애 복구에는 불리하다.
|
||||
|
||||
## 5. 시기별 기준자료 체계
|
||||
|
||||
폴더 이름이 데이터의 버전 메타데이터 역할을 한다.
|
||||
|
||||
- 노임·기계경비: 2025 상/하, 2026 상/하
|
||||
- 표준시장단가: 2025 상/하, 2026 상/하 및 기관별 변형
|
||||
- 표준일위대가: 2026 상반기, 건축/기계설비/전기/토목/통신 분리
|
||||
- 시설공통자재: 2025 상/하, 2026 상반기와 수정판
|
||||
- 제비율: 2007~2026의 여러 시행일, 일반(`JB_R`)과 표준시장(`JB_RA`) 분리
|
||||
|
||||
파일명에는 연도·반기·기관·수정일이 있으나 기계 판독 가능한 매니페스트나 해시 목록은 보이지 않는다.
|
||||
|
||||
## 6. Aislo 데이터셋과 비교
|
||||
|
||||
Aislo는 `resources/data_cost_input_value`에 JSON 자료와 `_manifest.json`을 둔다. 각 항목에는 `dataset_id`, 시행일, SHA-256, 크기가 있고 미확보·선택대기 값도 별도로 기록한다. 프로젝트가 채택한 단가는 UTF-8 JSON 스냅샷으로 저장하고 다시 읽을 때 해시를 검증한다.
|
||||
|
||||
| 속성 | STmate | Aislo |
|
||||
|---|---|---|
|
||||
| 구조 표현 | 고정폭 DBF 열 | 명명된 JSON 객체/배열 |
|
||||
| 문자 인코딩 | CP949 중심 | UTF-8 |
|
||||
| 버전 | 파일명·내부 날짜 | 매니페스트의 시행일/ID |
|
||||
| 무결성 | 관찰 가능한 해시 없음 | SHA-256 검증 |
|
||||
| 결측 | 빈칸/0 혼용 가능 | `None`·미결 목록·오류로 구분 |
|
||||
| 단가 출처 | AMT/PG 슬롯 | 슬롯 + 출처/근거/provenance |
|
||||
| 과거 프로젝트 고정 | 프로젝트에 테이블 동봉 | 프로젝트 단가 스냅샷 + 데이터셋 버전 |
|
||||
| 교환 | 단일 STC | 현재는 구성요소 중심; 단일 패키지 보강 가치 있음 |
|
||||
|
||||
권장 방향은 STmate 형식을 모방하는 것이 아니라, Aislo의 명시적 JSON/매니페스트 모델을 유지하면서 **단일 프로젝트 패키지**라는 운영상 장점만 가져오는 것이다.
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# STmate 계산 로직과 Aislo 비교
|
||||
|
||||
## 1. 내역 계산의 의미
|
||||
|
||||
`BOQ11.DBF`의 말단 행은 프로젝트 수량과 단가 마스터의 성분별 단가가 만나는 지점이다.
|
||||
|
||||
```text
|
||||
총단가 = 재료단가 + 노무단가 + 경비단가
|
||||
총금액 = 수량 × 총단가
|
||||
재료금액 = 수량 × 재료단가
|
||||
노무금액 = 수량 × 노무단가
|
||||
경비금액 = 수량 × 경비단가
|
||||
총금액 = 재료금액 + 노무금액 + 경비금액
|
||||
```
|
||||
|
||||
특정 프로젝트의 숫자는 로직 근거로 사용하지 않는다. 단수 처리와 밑수 전달은 STmate가 생성한 여러 실무 XLSX의 셀 수식을 교차해 판단하며, 필드 단위 데이터 계보는 별도 `04_계산_데이터계보.md`에 정리했다.
|
||||
|
||||
## 2. 계층 합산
|
||||
|
||||
말단 행 위에 제목과 계/합계/총계 행을 같은 테이블에 둔다. `BDCODE`가 행의 성격을 구분한다.
|
||||
|
||||
- `B...`, `D...`: 계산 가능한 말단 단가/내역 행
|
||||
- `T2..T5`: 계층 제목/상위 집계
|
||||
- `E2..E5`: 계, 합계, 총계, 총합계
|
||||
- `:`: 페이지 넘김 같은 출력 제어 행
|
||||
|
||||
모든 집계는 재료·노무·경비 성분을 따로 합산한 뒤 총액과 맞춘다. 제목·소계 행 자체가 새로운 계산 원천은 아니며, 하위 말단 행 결과를 그룹별로 집계한 파생 결과다.
|
||||
|
||||
## 3. 단가 조립 계층
|
||||
|
||||
DBF 코드·필드와 모듈 구성을 종합하면 단가 계층은 다음과 같다.
|
||||
|
||||
```text
|
||||
M 자재 ──────────────┐
|
||||
L 노임 ───────┐ │
|
||||
S 중기 취득가 ─┴→ X 시간당 중기사용료
|
||||
├→ B 일위대가 → 내역서
|
||||
└→ D 단가산출 → 내역서
|
||||
W 일식/견적 ─────────────────────→ 내역서
|
||||
```
|
||||
|
||||
각 상위 단가는 하위 코드의 수량을 곱해 재료·노무·경비로 나누어 합산하는 참조 모델이다. `COSTN`의 `CODE`, `BQCODE`, `EQCODE`, 다수 `AMT`, `PG` 필드가 이 구조를 뒷받침한다.
|
||||
|
||||
이 부분은 Aislo `PriceBook`의 모델과 매우 가깝다. Aislo는 `PriceKind`로 M/L/S/X/B/D/W를 명시하고 `PriceDetail(parent_code, ref_code, quantity)`를 재귀 해석한다. 순환 참조, 상세 없음, 채택 단가 없음은 오류로 중단한다.
|
||||
|
||||
## 4. 복수 단가 원천
|
||||
|
||||
STmate 설정에는 가격 정보, 물가 자료, 물가 정보, 거래 가격, 유통 물가, 적용 단가가 있고 `COSTN`은 원천별 금액(`AMT_`)과 쪽수(`PG_`)를 가진다. 프로젝트/내역 테이블은 네 벌의 수량·단가·금액 슬롯을 가진다.
|
||||
|
||||
두 축을 구분해야 한다.
|
||||
|
||||
- **원천 슬롯**: 같은 자재/노임의 여러 조사·공표 단가 중 무엇을 채택할지
|
||||
- **설계 안 슬롯**: 당초/변경 또는 최대 4개 대안을 나란히 저장
|
||||
|
||||
Aislo는 6개 단가 원천과 채택 슬롯을 이미 명시적으로 구현한다. 네 안을 물리 열 네 벌로 복제하지는 않는다. Aislo의 프로젝트 버전 모델로 비교하는 편이 데이터 중복과 열 확장을 피할 수 있다.
|
||||
|
||||
## 5. 수량 계산 구조
|
||||
|
||||
STmate에는 `BDQTY`의 `QTY1..4`, `X_QTY1..3`, `X_HAL1..3`, `RATE`, 그리고 `QCODE`의 `QTY0..9`, `IFORM`, `XCODE`가 있다. 이는 수량 원값, 할증/비율, 산출식 형태를 저장하는 구조로 보인다. 다만 관련 본문이 암호화되고 UI를 열지 못했으므로 정확한 산식 언어와 평가 순서는 미확인이다.
|
||||
|
||||
Aislo는 이 지점에서 제품 성격이 다르다. B03~B07의 지형·선형·횡단·구조물 계산 결과를 B08이 공종별 수량으로 변환하고, 겹침 제거·자연/다짐 상태·구조물 규격·운반거리 같은 의미를 명시적으로 보존한다. STmate 맛보기는 견적 입력 수량을 받는 구조는 보이지만 GIS 설계에서 수량까지의 자동 생성 근거는 확인되지 않는다.
|
||||
|
||||
## 6. 제비율과 원가계산
|
||||
|
||||
STmate는 시행일별 `JB_R/JB_RA`와 프로젝트 `RATE*.DBF` 행렬을 사용한다. `ST_KanJub.CPM`과 `ST_P_ConEst.CPM`은 간접비 계산과 원가계산서 출력을 담당하는 것으로 보인다. RATE 행렬에는 숫자만 있고 각 행의 명칭, 적용 밑수, 구간 선택 규칙은 프로그램 코드에 결합되어 있다.
|
||||
|
||||
Aislo는 반대로 `rates_2026.json`에 변수, 구간, 밑수, 처리 규칙을 명명해 두고 다음 흐름을 코드에서 그대로 드러낸다.
|
||||
|
||||
```text
|
||||
재료비 = 직접재료비 + 간접재료비
|
||||
직접공사비 = 재료비 + 직접노무비 + 직접경비
|
||||
간접노무비 = 직접노무비 × 선택 구간율
|
||||
노무비 = 직접노무비 + 간접노무비
|
||||
경비 = 직접경비 + 법정경비
|
||||
순공사원가 = 재료비 + 노무비 + 경비
|
||||
일반관리비 = 순공사원가 × 구간율
|
||||
이윤 = ((순공사원가 + 일반관리비) - 재료비) × 이윤율
|
||||
총원가 = 순공사원가 + 일반관리비 + 이윤
|
||||
도급공사비 = 총원가 + 부가세
|
||||
총공사비 = 도급공사비 + 관급자재대 + 폐기물처리비
|
||||
```
|
||||
|
||||
Aislo는 추정가격 구간이 계산 결과에 다시 영향을 주는 순환을 고정점 반복으로 풀고, 구간 진동 시 높은 구간을 택했다는 기록을 남긴다. STmate가 같은 경계를 어떻게 처리하는지는 정적 파일만으로 확인하지 못했다.
|
||||
|
||||
## 7. 단수 처리
|
||||
|
||||
STmate의 데이터 열은 수량 4자리, 단가 3자리, 제비율 4~6자리 같은 고정 소수 자릿수를 가진다. 그러나 저장 자릿수가 실제 반올림/절사 시점을 증명하지는 않는다.
|
||||
|
||||
Aislo는 `Decimal`로 전정밀 계산하고 출력 위치별 단수 규칙을 분리한다.
|
||||
|
||||
- 내역 행·제경비 행: 1원 미만 절사
|
||||
|
||||
> ⚠ 아래 「반올림」은 출력물(XLSX) 기준이다. STmate **내부** 계산의 `ROUND`는 Excel 사사오입이 아니라 최근접-짝수(은행가)다 — 11번 §4.2.
|
||||
|
||||
- 자원 집계표: 1원 단위 반올림
|
||||
- 일위대가 금액란: 0.1원 미만 절사
|
||||
- 일위대가 계금: 1원 미만 절사
|
||||
- 관급자재대 총액: 천원 올림
|
||||
- 설계서 총액: 천원 미만 절사
|
||||
|
||||
이 설계는 DBF 필드 자릿수에 묵시적으로 의존하는 것보다 검증과 변경에 유리하다.
|
||||
|
||||
## 8. 실패·결측 처리 비교
|
||||
|
||||
STmate의 평문 내역에는 계층 행의 수량/단가가 0으로 채워지고 실제 합계는 금액 필드에만 들어간다. 따라서 0이 정상값인지, 해당 없음인지, 미확보인지 스키마만으로 구별하기 어렵다.
|
||||
|
||||
Aislo는 채택 슬롯이 비어 있거나 상세 단가가 없거나 참조가 순환하면 0원으로 확정하지 않고 오류 또는 미결 사유를 남긴다. 경쟁 프로그램과의 호환을 이유로 이 원칙을 약화시키면 안 된다.
|
||||
|
||||
## 9. 권고 우선순위
|
||||
|
||||
### 우선 1 — 프로젝트 단일 패키지
|
||||
|
||||
Aislo의 기존 JSON, 데이터셋 버전, SHA-256, 입력 파일, 계산 설정, 산출 결과를 ZIP 하나로 내보내고 가져오는 기능이 가장 실용적인 차용점이다. STC의 단순한 운영 장점을 얻되 매니페스트와 스키마 버전을 포함해야 한다.
|
||||
|
||||
### 우선 2 — 버전 비교 화면
|
||||
|
||||
당초/변경 및 대안 비교를 한 화면에서 수량, 단가, 재료/노무/경비, 총액 차이로 보여준다. 저장은 기존 프로젝트 버전/스냅샷을 재사용하고 STmate처럼 모든 열을 네 벌 복제하지 않는다.
|
||||
|
||||
### 우선 3 — 출력 서식 메타데이터
|
||||
|
||||
보고서별 열 이름, 폭, 표시 여부, 머리말을 데이터로 분리한다. 현재 출력 요구가 실제로 반복될 때만 도입하며 계산 모델과 섞지 않는다.
|
||||
|
||||
### 유지해야 할 Aislo 우위
|
||||
|
||||
- 설계→수량→견적의 자동 인계
|
||||
- UTF-8 JSON과 기계 판독 가능한 매니페스트
|
||||
- 데이터셋/스냅샷 SHA-256
|
||||
- `Decimal` 기반 계산과 출력 위치별 단수 처리
|
||||
- 결측 단가를 0원으로 대체하지 않는 보호장치
|
||||
- 코드·근거·시행일·미결 사유의 provenance
|
||||
- 계산 모듈별 자동 테스트
|
||||
|
||||
## 10. 추가 분석에 필요한 것
|
||||
|
||||
정확한 STmate 제비율 매핑과 수량식 언어까지 확인하려면 프로그램 UI가 정상 표시되는 환경 또는 공급자가 제공한 출력 보고서가 필요하다. 다음 자료가 있으면 암호 우회 없이 분석을 확장할 수 있다.
|
||||
|
||||
- 맛보기 프로젝트의 내역서·일위대가표·단가산출서·원가계산서 출력 PDF/Excel
|
||||
- 정상 실행 화면 캡처 또는 화면 녹화
|
||||
- 서로 다른 입력 수량/거리/할증으로 저장한 두 프로젝트 파일
|
||||
- 동일 프로젝트의 당초/변경 두 판
|
||||
|
||||
두 저장본의 합법적 차분이 있으면 어떤 입력 필드가 어떤 결과 필드에 영향을 주는지 훨씬 정확하게 식별할 수 있다.
|
||||
@@ -0,0 +1,573 @@
|
||||
# STmate 계산 데이터 계보 — 어디에서 무엇을 가져와 무엇을 만드는가
|
||||
|
||||
## 문서 목적
|
||||
|
||||
이 문서는 특정 공사의 값이 아니라 STmate의 **계산 의미와 데이터 이동**을 정리한다. 각 단계마다 입력 출처, 선택·계산 규칙, 결과 저장처, 다음 단계 소비자를 연결한다.
|
||||
|
||||
## 1. 전체 계산 파이프라인
|
||||
|
||||
```text
|
||||
[A. 공통 기초자료]
|
||||
노임 / 자재 / 중기 취득가 / 환율 / 제비율 기준표
|
||||
↓
|
||||
[B. 자원별 적용단가]
|
||||
복수 가격원천 중 채택 + 관급/사급 구분
|
||||
↓
|
||||
[C. 품·복합단가 계산]
|
||||
중기품셈 / 시간당 중기사용료(X) / 일위대가(B)
|
||||
작업단가·표준시장단가 / 단가산출(D)
|
||||
= 참조 자원 단가 × 품 + 비율 항목
|
||||
↓
|
||||
[D. 프로젝트 공사비 계산]
|
||||
공종 수량 × 복합단가 → 재료·노무·경비별 내역금액
|
||||
↓
|
||||
[E. 직접비 집계]
|
||||
직접재료비 / 직접노무비 / 산출경비
|
||||
↓
|
||||
[F. 간접비 계산]
|
||||
항목별 밑수 × 조건별 제비율, 행별 절사
|
||||
↓
|
||||
[G. 원가계산]
|
||||
순공사원가 → 일반관리비 → 이윤 → 총원가 → 부가세
|
||||
↓
|
||||
[H. 별도 금액과 최종 출력]
|
||||
관급자재대·조달수수료·폐기물 등 + 보고서/Excel/STC
|
||||
```
|
||||
|
||||
교본도 STmate를 “기초자료~단가계산은 공사 종류와 무관하게 통합관리, 공사비계산~간접비계산은 공사별 독립관리”라고 설명한다. 즉 C까지는 재사용 가능한 단가 라이브러리이고 D 이후가 프로젝트 계산이다.
|
||||
|
||||
공식 흐름도에는 계산선과 별도로 `수정 → 검토 → 통합관리`가 전체 단계를 되먹임한다. 따라서 STmate의 계산은 한 번 실행하고 끝나는 선형 배치가 아니라, 기초단가·품·수량·원가계산기준을 고치면 그 값을 참조하는 하위 결과를 재계산하는 의존 그래프다.
|
||||
|
||||
## 2. A단계 — 공통 기초자료가 어디에서 오는가
|
||||
|
||||
### 입력 원천
|
||||
|
||||
| 의미 | 배포/설정 위치 | 프로젝트 저장 대응 |
|
||||
|---|---|---|
|
||||
| 노임단가 | `ST_BSC/노임기계/*.STBSC` | `COSTN`의 L 코드 |
|
||||
| 자재단가 | `ST_BSC/자재단가/*.STBSC`, 물가지·견적 입력 | `COSTN`의 M 코드 |
|
||||
| 중기 취득가·기계경비 기초 | `ST_BSC/노임기계/*.STBSC` | `COSTN`의 S 코드와 중기 연결 필드 |
|
||||
| 표준 일위대가 | `ST_BSC/일위대가/*.STBSC` | B 코드와 산출 메모 |
|
||||
| 표준시장단가 | `ST_BSC/실적단가/*.STBSC` | 실적단가/대안 단가 계열 |
|
||||
| 제비율 | 시행일별 `JB_R`, `JB_RA` | 프로젝트 `RATE*` 테이블 |
|
||||
| 환율·단가 조사시점·소수점·계산방식 | 프로젝트 초기화 화면 | `Start8x.Ini`, `COSTN.BQCODE`, 관련 플래그 |
|
||||
|
||||
### 의미
|
||||
|
||||
STmate는 계산식을 시작하기 전에 “어느 시점의 가격판과 제비율판을 쓸지”를 고른다. 기준자료를 프로젝트 안으로 복사해 두므로 나중에 공통 DB가 갱신되어도 기존 프로젝트 계산판은 유지된다.
|
||||
|
||||
## 3. B단계 — 자원별 적용단가를 어떻게 정하는가
|
||||
|
||||
### 자재 M 코드
|
||||
|
||||
`COSTN` 한 행이 하나의 자재를 나타낸다.
|
||||
|
||||
```text
|
||||
자재 식별
|
||||
CODE + NAME + SIZE + UNIT + BQCODE
|
||||
|
||||
가격 후보
|
||||
AMT_11 조달가격
|
||||
AMT_21 물가정보
|
||||
AMT_31 물가자료
|
||||
AMT_41 적산정보
|
||||
AMT_51 견적단가
|
||||
AMT_61 적용단가
|
||||
|
||||
근거
|
||||
PG_11~PG_51 = 게재 페이지·식별정보
|
||||
|
||||
선택
|
||||
PG_1 = 채택 출처 인덱스
|
||||
|
||||
결과
|
||||
MAMT1 = 계산에 전달되는 재료단가
|
||||
TAMT1 = 해당 자원의 총단가
|
||||
```
|
||||
|
||||
중요한 경계: 6개 후보를 저장하고 채택 결과를 저장하는 것은 확인됐지만, **자동 최저가·법적 우선순위·사용자 수동선택 중 어느 알고리즘인지 아직 확정되지 않았다**. 관측상 채택 인덱스는 주로 복수 시세가 있는 M 코드에만 의미가 있고 L/S/X/B/D는 대부분 0이다.
|
||||
|
||||
### 노임 L 코드
|
||||
|
||||
협회 공표 노임 등 단일 기준값을 받아 `LAMT1`에 노무단가로 둔다. 자재처럼 복수 물가지 중 하나를 고르는 구조의 필요성이 작아 채택 인덱스가 관측상 사용되지 않는다.
|
||||
|
||||
### 중기 취득가 S 코드
|
||||
|
||||
기계의 기초가격과 제원·환율을 받아 경비 자원의 기초가 된다. 그 자체가 공종 단가는 아니며 다음 단계의 시간당 중기사용료 X가 소비한다.
|
||||
|
||||
### 관급/사급 분기
|
||||
|
||||
관급 플래그가 있는 자재는 도급 내역의 재료단가를 0원 처리하고 자재대와 조달수수료를 총원가 밖에서 별도 집계하는 경로를 탄다. 사급 자재는 채택단가가 일위대가의 재료비로 들어간다.
|
||||
|
||||
## 4. C단계 — 단가가 어떻게 조립되는가
|
||||
|
||||
### 공통 참조 문법
|
||||
|
||||
`COSTN.IFORM`이 `COSTN.SMT`의 산출 메모를 가리킨다. 메모에는 다음 의미가 들어 있다.
|
||||
|
||||
| 문법 | 의미 | 가져오는 값 | 만드는 값 |
|
||||
|---|---|---|---|
|
||||
| `&M#####M&` | 자재 참조 | M 코드의 채택 재료단가 | 재료비 성분 |
|
||||
| `&L#####L&` | 노임 참조 | L 코드의 노임단가 | 노무비 성분 |
|
||||
| `&S#####S&` | 기계/경비 참조 | S 코드의 경비단가 | 경비 성분 |
|
||||
| `&X#####L/M/S&` | 시간당 중기 참조 | X 코드의 성분별 시간당 단가 | 공종의 노·재·경 성분 |
|
||||
| `수량'단위'` | 품·원단위 | 품셈 또는 설계조건 | 참조단가에 곱할 계수 |
|
||||
| `율'%'` | 할증·손료 | 품셈/설계자 선택률 | 지정 밑수에 대한 비율 비용 |
|
||||
| `Q = ...` | 기계 작업량/시간계수 | 장비 제원·작업조건 | 단위 공종당 장비시간 |
|
||||
| `:=A01` | 중간 결과 | 앞 계산 결과 | 뒤 수식의 재사용 값 |
|
||||
|
||||
### 시간당 중기사용료 X
|
||||
|
||||
```text
|
||||
입력
|
||||
S: 기계 취득가·손료 기초
|
||||
L: 운전원 노임
|
||||
M: 연료·소모재료 단가
|
||||
장비 제원, 연간/시간 운전 조건, 환율 등
|
||||
|
||||
계산
|
||||
시간당 노무비 + 시간당 재료비 + 시간당 경비
|
||||
|
||||
출력
|
||||
X 코드의 LAMT/MAMT/SAMT 및 TAMT
|
||||
|
||||
소비자
|
||||
일위대가 B 또는 단가산출 D의 기계 행
|
||||
```
|
||||
|
||||
정확한 감가·정비·관리·연료 공식은 설치 데이터만으로 전부 복원되지 않았으므로 현행 품셈의 기계경비 산식을 우선 근거로 보아야 한다.
|
||||
|
||||
### 일위대가 B / 단가산출 D
|
||||
|
||||
```text
|
||||
각 상세 행 금액 = 참조 자원 적용단가 × 품(원단위)
|
||||
|
||||
재료단가 = Σ(M 참조 × 품) + 재료비 기준 비율항목
|
||||
노무단가 = Σ(L 참조 × 품) + 노무 할증
|
||||
경비단가 = Σ(S/X 참조 × 품) + 노무비 등 지정 밑수의 손료
|
||||
총단가 = 재료단가 + 노무단가 + 경비단가
|
||||
```
|
||||
|
||||
여기서 `B#####` 또는 `D#####` 결과가 하나의 공종 단가가 된다. 핵심은 결과 숫자를 직접 보관하는 것만이 아니라 **어떤 자원 코드에서 얼마의 품을 가져왔는지**가 SMT 산출 메모에 남는다는 점이다.
|
||||
|
||||
### 작업단가·표준시장단가 경로
|
||||
|
||||
공식 흐름도는 `일위대가`와 `작업단가`를 같은 단가계산의 입력으로 병렬 배치한다. 설치 자료도 표준시장단가를 별도 `CSTA/STBSC`로 공급한다.
|
||||
|
||||
```text
|
||||
일위대가 경로
|
||||
자재·노임·중기사용료 × 품 → 자원별 상세 전개가 있는 계산단가
|
||||
|
||||
작업단가/표준시장단가 경로
|
||||
기관·공종·규격·시행시점별 공표 단가 → 해당 공종에 직접 연결되는 단가
|
||||
|
||||
공통 결과
|
||||
공종 단가 → 공사수량과 결합 → 공사비
|
||||
```
|
||||
|
||||
따라서 모든 공종을 자원 원단위로 재귀 분해한다고 가정하면 안 된다. 어떤 행은 일위대가로 조립되고 어떤 행은 표준시장/실적 단가를 직접 채택한다. 직접 채택 단가가 재료·노무·경비로 분해되어 제공되는지, 총액만 제공되어 배분하는지는 자료 종류별 확인이 필요하다.
|
||||
|
||||
## 5. D단계 — 프로젝트 수량이 단가를 만나는 지점
|
||||
|
||||
### 수량 출처
|
||||
|
||||
프로젝트의 공종 수량은 `BOQ11.QTY1`에 들어가고, 세부 전개·할증 관련 정보는 `BDQTY`에 있다.
|
||||
|
||||
```text
|
||||
BDQTY.BDCODE ─────────────┐
|
||||
BDQTY.QTY / X_QTY / X_HAL │ 수량·할증 전개
|
||||
BDQTY.BIMOK │ 비목 구분
|
||||
↓
|
||||
BOQ11.BDCODE1 ───────→ COSTN.CODE 또는 단가 호표
|
||||
BOQ11.QTY1 ─────→ 프로젝트 적용수량
|
||||
COSTN 성분별 단가 ───→ BOQ11.LAMT1/MAMT1/SAMT1
|
||||
```
|
||||
|
||||
`BDQTY` 본문은 난독화되어 있어 `X_QTY`가 할증 전 수량이고 `X_HAL`이 할증률이라는 해석은 강한 추정이지만 아직 실기 확인 전이다. 따라서 수량 산식 자체를 STmate에서 가져왔다고 단정할 수 없다.
|
||||
|
||||
### 내역 행 계산
|
||||
|
||||
```text
|
||||
BOQ11.L_AMT1 = ROUNDDOWN(QTY1 × LAMT1, 원)
|
||||
BOQ11.M_AMT1 = ROUNDDOWN(QTY1 × MAMT1, 원)
|
||||
BOQ11.S_AMT1 = ROUNDDOWN(QTY1 × SAMT1, 원)
|
||||
BOQ11.T_AMT1 = ROUNDDOWN(QTY1 × TAMT1, 원)
|
||||
```
|
||||
|
||||
이 단수 규칙은 STmate가 출력한 2024~2025년 XLSX 5개 공사지의 셀 수식에서 반복 확인됐다. DBF 자릿수만 보고 추정한 것이 아니다.
|
||||
|
||||
### 트리 합산
|
||||
|
||||
`BDCODE` 유형이 제목(`T5→T4→T3→T2`), 단가참조 말단(`B/D 코드`), 소계(`E2` 등)를 구분한다. 말단 금액을 재료·노무·경비별로 각각 합산해 공종·공사·공구·총괄 금액을 만든다. `SORTCODE`는 256 간격 정렬키, `ENDCODE`는 제목 블록 끝을 가리키는 포인터 역할로 보인다.
|
||||
|
||||
## 6. E단계 — 직접비 결과가 무엇인가
|
||||
|
||||
내역의 모든 말단 행을 비목별로 합산하면 다음 세 값이 생긴다.
|
||||
|
||||
```text
|
||||
직접재료비 = Σ BOQ11.M_AMT
|
||||
직접노무비 = Σ BOQ11.L_AMT
|
||||
산출경비 = Σ BOQ11.S_AMT
|
||||
직접공사비 = 직접재료비 + 직접노무비 + 산출경비
|
||||
```
|
||||
|
||||
이 값들은 `PRJLST.MCOST/LCOST/SCOST/TCOST`에 프로젝트 차수별 합계로 저장되고, 동시에 다음 간접비 계산의 밑수가 된다.
|
||||
|
||||
## 7. F단계 — 각 제비율이 어느 값을 가져오는가
|
||||
|
||||
STmate 출력 XLSX의 수식을 여러 공사지에서 교차한 결과, 데이터 전달 관계는 다음과 같다. 적용률은 프로젝트에 복사된 `RATE*` 기준표에서 공사종류·규모·기간·시행일 조건으로 선택되는 것으로 보인다.
|
||||
|
||||
| 결과 항목 | 가져오는 밑수 | 추가 조건/의존 | 결과가 쓰이는 다음 단계 |
|
||||
|---|---|---|---|
|
||||
| 간접노무비 | 직접노무비 | 공사규모·기간·종류의 적용률 | 노무비 합계 |
|
||||
| 노무비 합계 | 직접노무비 + 간접노무비 | — | 산재·고용·기타경비·이윤 |
|
||||
| 산재보험료 | 노무비 합계 | 건설업 적용률 | 경비 합계 |
|
||||
| 고용보험료 | 노무비 합계 | 공사조건·등급 | 경비 합계 |
|
||||
| 건강보험료 | 직접노무비 | 기간 조건 | 경비 합계 |
|
||||
| 국민연금보험료 | 직접노무비 | 연도·기간 조건 | 경비 합계 |
|
||||
| 노인장기요양보험료 | 건강보험료 결과 | 연도별 비율 | 경비 합계 |
|
||||
| 퇴직공제부금 | 직접노무비 | 적용대상 조건 | 경비 합계 |
|
||||
| 산업안전보건관리비 | 재료비 + 직접노무비 + 도급자설치 관급금액 | 규모 구간, 경우에 따라 기초액 | 경비 합계 |
|
||||
| 기타경비 | 재료비 + 노무비 합계 *(일반·수공)* / 직접공사비 *(실적)* | 공사규모·기간·종류 | 경비 합계 |
|
||||
| 환경보전비 | 재료비 + 직접노무비 + 산출경비 *(일반·수공)* / 직접공사비 *(실적일반)* | 공사종류·최소금액 조건 | 경비 합계 |
|
||||
| 하도급대금·건설기계 보증수수료 | 재료비 + 직접노무비 + 산출경비 | 규모·계약조건 | 경비 합계 |
|
||||
| 공사이행보증수수료 | (직접공사비 − 기준금액) × 율 + 추가금액, 다시 × 공기(년) | 규모·계약조건 | 경비 합계 |
|
||||
|
||||
**2026-09-13 정정** — 환경보전비와 보증수수료의 밑수를 `직접공사비`로 적었던 것을 바로잡았다. RATE 편집 화면(`TWM_KAN_RATE`)의 그리드 묶음 제목에 밑수 공식이 그대로 적혀 있으며, **밑수가 원가계산 형식(`T`/`S`/`TA`/`SA`)에 따라 달라진다.** 항목별 전체 표는 06번 §3에 있다.
|
||||
|
||||
각 항목은 `밑수 × 율`을 계산한 뒤 **그 행에서 원 단위 절사**되고, 절사된 정수 결과가 다음 단계 밑수에 전달된다. 마지막에 한 번만 절사하는 구조가 아니다.
|
||||
|
||||
> ⚠ 이 문서의 `ROUND`·`ROUNDDOWN`은 **STmate 출력 XLSX의 Excel 수식** 표기다. STmate **내부** Delphi 계산의 `ROUND`는 최근접-짝수(은행가) 반올림이라 정확히 절반인 값에서 결과가 갈린다. 11번 §4.2 경계표를 함께 볼 것.
|
||||
|
||||
## 8. G단계 — 원가계산 결과가 만들어지는 순서
|
||||
|
||||
```text
|
||||
재료비 = 직접재료비 + 간접재료비
|
||||
|
||||
노무비 = 직접노무비 + 간접노무비
|
||||
|
||||
경비 = 산출경비
|
||||
+ 산재·고용·건강·연금·장기요양 등 법정경비
|
||||
+ 안전관리비·기타경비·환경보전비·보증수수료 등
|
||||
|
||||
순공사원가 = 재료비 + 노무비 + 경비
|
||||
|
||||
일반관리비 = ROUNDDOWN(순공사원가 × 적용률, 원)
|
||||
|
||||
이윤 = ROUNDDOWN((노무비 + 경비 + 일반관리비) × 적용률, 원)
|
||||
= ROUNDDOWN((순공사원가 + 일반관리비 - 재료비) × 적용률, 원)
|
||||
|
||||
총원가 = 순공사원가 + 일반관리비 + 이윤
|
||||
|
||||
부가가치세 = ROUNDDOWN(총원가 × 10%, 원) ± 면세·끝수 조정
|
||||
|
||||
도급공사비 = 총원가 + 부가가치세
|
||||
```
|
||||
|
||||
변경설계에서는 접미 1/2/3/4 슬롯이 당초와 변경 1~3차의 수량·단가·금액을 나란히 보존한다. 일부 경비는 변경 시 전체 재계산이 아니라 당초금액과 증감액을 결합하는 예외가 관측되어 항목별 변경 규칙 확인이 필요하다.
|
||||
|
||||
## 9. H단계 — 관급자재와 최종 금액
|
||||
|
||||
```text
|
||||
관급자재 순자재대 = 관급 플래그 자재의 수량 × 조달단가 집계
|
||||
조달수수료 = ROUNDDOWN(관급자재 순자재대 × 수수료율, 원)
|
||||
관급자재대 총액 = ROUNDUP(순자재대 + 조달수수료, 천원)
|
||||
|
||||
총공사비 = 도급공사비 + 관급자재대 총액 (+ 별도 폐기물처리비 등)
|
||||
```
|
||||
|
||||
관급자재대는 총원가 안의 재료비와 중복되면 안 된다. 다만 도급자 설치 관급금액은 산업안전보건관리비의 대상액에는 포함될 수 있으므로 “총원가 밖”과 “모든 계산에서 제외”는 다른 의미다.
|
||||
|
||||
## 10. 같은 값도 출력 위치에 따라 단수 처리가 다르다
|
||||
|
||||
| 출력 위치 | 처리 |
|
||||
|---|---|
|
||||
| 설계내역서 행 금액 | 원 단위 절사 |
|
||||
| 제경비 각 행 | 원 단위 절사 |
|
||||
| 조달수수료 | 원 단위 절사 |
|
||||
| 재료·노무·경비·중기 자원 집계표 | 원 단위 반올림 |
|
||||
| 관급자재대 총액 | 천원 단위 올림 |
|
||||
|
||||
따라서 내역서와 자원 집계표가 같은 원자료를 사용해도 원 단위 합계가 다를 수 있다. 이는 입력 오류가 아니라 출력 위치별 처리 규칙이다.
|
||||
|
||||
## 11. STmate와 Aislo의 단계별 대응
|
||||
|
||||
| STmate 의미 | STmate 저장 | Aislo 대응 | 비교 |
|
||||
|---|---|---|---|
|
||||
| 자원 가격후보와 채택 | `COSTN.AMT_*`, `PG_*`, `T/L/M/SAMT` | `PriceTitle.slots`, `slot_pages`, `adopted_slot` | Aislo가 타입과 결측을 명시 |
|
||||
| 단가 상세 전개 | `COSTN.SMT` 문자열 문법 | `PriceDetail` 목록 | Aislo가 파싱 없이 구조 자체로 저장 |
|
||||
| 단가 재귀 참조 | 자원 코드 마커 | `PriceBook.resolve()` | 의미상 동일 |
|
||||
| 프로젝트 수량 | `BOQ11.QTY`, `BDQTY` | B08 handoff `work_items/materials` | Aislo는 설계 근거와 상태를 더 많이 보존 |
|
||||
| 내역 3성분 | `LAMT/MAMT/SAMT`, `L/M/S_AMT` | `Money3`, `BillRow` | 의미상 동일 |
|
||||
| 직접비 합계 | `PRJLST.M/LCOST/SCOST/TCOST` | `DirectCostBreakdown`, `CostInput` | 의미상 동일 |
|
||||
| 제비율 | 라벨 없는 `RATE*` 행렬 + 프로그램 코드 | 명명된 `rates_2026.json` 처리규칙 | Aislo가 밑수와 조건을 감사 가능하게 노출 |
|
||||
| 원가계산 | 모듈 내부 + 출력 결과 | `calculate_cost()` 단계별 행 | Aislo가 계산 순서를 직접 검증 가능 |
|
||||
| 과거 판 고정 | 프로젝트에 DBF 동봉 | 단가 스냅샷 + 데이터셋 버전/해시 | Aislo가 무결성 확인 가능 |
|
||||
|
||||
## 12. 비교 결과
|
||||
|
||||
STmate에서 가져올 핵심은 숫자나 DBF 형식이 아니다.
|
||||
|
||||
1. **공통 단가 라이브러리와 프로젝트 계산을 분리**한다.
|
||||
2. **모든 파생 단가는 원천 자원 코드와 품을 통해 추적**할 수 있어야 한다.
|
||||
3. **재료·노무·경비 성분을 계산 전 과정에서 잃지 않는다.**
|
||||
4. **간접비는 항목마다 밑수가 다르고 계산 순서가 의존적**이다.
|
||||
5. **단수 처리는 수치 종류가 아니라 결과가 놓이는 표 위치에 묶인다.**
|
||||
6. **변경설계는 당초값과 변경값의 계보를 보존**해야 한다.
|
||||
7. **프로젝트가 사용한 기준자료 판을 함께 고정**해야 한다.
|
||||
|
||||
Aislo는 1~5와 7을 현재 구조에서 더 명시적으로 구현하고 있다. 추가 가치가 큰 부분은 프로젝트 패키지와 변경설계 비교다. STmate의 라벨 없는 RATE 행렬이나 고정폭 다중차수 열을 그대로 모방할 이유는 없다.
|
||||
|
||||
## 13. 아직 확정하면 안 되는 부분
|
||||
|
||||
- 자재 6개 후보 중 채택단가를 자동으로 고르는 정확한 알고리즘
|
||||
- `BDQTY.X_QTY/X_HAL`의 실제 계산 순서와 편집 플래그 의미
|
||||
- `RATE*` 행·열 전체의 화면 라벨과 구간 인덱스
|
||||
- 중기 시간당 사용료의 버전별 세부 감가·정비 공식
|
||||
- 이윤/부가세 끝수 조정의 모든 예외
|
||||
- 변경설계에서 각 법정경비를 재계산하는지 증감식으로 처리하는지
|
||||
|
||||
이 항목은 서로 다른 입력을 저장한 전후 프로젝트 또는 정상 UI 조작 결과가 있어야 인과관계를 확정할 수 있다.
|
||||
|
||||
## 14. 누락 감사에서 보강한 자원단가 분기
|
||||
|
||||
### 노임은 단순히 L 단가 하나가 아니다
|
||||
|
||||
```text
|
||||
건설업 시중노임
|
||||
→ 일반 공종의 사람 품
|
||||
→ 중기 운전사의 운전경비
|
||||
|
||||
제조업 조사노임
|
||||
→ 공장 제작·가공품의 제조원가에만 제한적으로 사용
|
||||
```
|
||||
|
||||
노임 원문에는 직종번호·직종명·적용일뿐 아니라 조사현장 부족 표시 `*`·`**`가 있다. 이 표시는 단가를 바꾸는 계수가 아니라 **신뢰도 메타데이터**다. 연장·야간·휴일·도서오지 가산은 노임 원천값 자체를 바꾸는 것이 아니라 별도의 노임 할증 계산이다. 산림품셈의 경사·작업시기 할증은 다시 그와 다르게 **품(인/단위)에 곱하는 계수**다.
|
||||
|
||||
### 자재 수량 할증과 잡재료비는 다른 계산이다
|
||||
|
||||
```text
|
||||
자재 할증
|
||||
순수량 × (1 + 손실률) → 내역에 투입할 자재수량 증가
|
||||
|
||||
잡재료·공구손료
|
||||
주재료비 × 선택률 → 일위대가의 별도 재료비/경비 행
|
||||
```
|
||||
|
||||
둘을 동시에 같은 자재에 적용할 때도 계산 위치가 다르며 중복 계상 여부를 확인해야 한다. 품셈 항목이 이미 할증을 포함하면 자재 할증을 다시 적용하지 않는다. 잡재료비의 밑수는 하위 일위대가가 품고 온 재료비가 아니라 해당 단계의 **직접 주재료비**여야 중복되지 않는다.
|
||||
|
||||
### 관급·사급 외에도 면세 분기가 있다
|
||||
|
||||
면세 자재가 있으면 부가가치세를 단순히 `총원가 × 10%`로 끝내지 않고 면세품목 공제·조정이 들어갈 수 있다. STmate 실무 출력에서 조정 사례는 확인됐으나 일반식과 적용 조건은 아직 전수 확정되지 않았다. Aislo 비교 시 “부가세 10%”만으로 완결됐다고 판단하면 안 된다.
|
||||
|
||||
## 15. 누락 감사에서 보강한 중기 계산 전체식
|
||||
|
||||
기존 계보는 X 코드의 입력·출력만 설명했지만, 실제 중기경비는 다음 네 묶음이다.
|
||||
|
||||
```text
|
||||
시간당 기계손료
|
||||
= 취득가격 × (시간당 상각비계수 + 정비비계수 + 관리비계수)
|
||||
|
||||
시간당 운전경비
|
||||
= 연료·전력·윤활유 + 운전사 노임 + 정비비 외 소모품비
|
||||
|
||||
현장 작업 단가
|
||||
= (시간당 기계손료 + 시간당 운전경비) ÷ 시간당 작업량 Q
|
||||
|
||||
총 기계 관련 원가
|
||||
= 현장 작업 단가 + 현장 수송비 + 필요 시 분해조립비
|
||||
```
|
||||
|
||||
입력 계보:
|
||||
|
||||
| 입력 | 가져오는 곳 | 영향을 받는 결과 |
|
||||
|---|---|---|
|
||||
| 국산 표준시가 또는 수입 C.I.F 가격 | 기계 기초자료 | 취득가격 |
|
||||
| 환율 | 환율및기초자료 | 수입기계 취득가격과 모든 손료 성분 |
|
||||
| 상각·정비·관리 계수 | 품셈 기계표 | 시간당 기계손료 |
|
||||
| 지역 유가 | 현장 소재지 기준 유가 | 시간당 연료비 |
|
||||
| 연료 소비량 | 기종·규격 기초자료 | 시간당 연료비 |
|
||||
| 운전사 직종 | 기종 분류 | 시간당 노무비 |
|
||||
| 싸이클 수·1회 작업량·체적환산·효율 | 품셈과 현장조건 | 시간당 작업량 `Q=n·q·f·E` |
|
||||
| 이동거리·속도·상하차/대기 | 현장 운반조건 | 수송비·운반 단가 |
|
||||
| 암질·기종·톤급 | 공종/장비 속성 | 손료 보정 여부와 가산률 |
|
||||
|
||||
암석작업 보정은 전체 중기사용료가 아니라 원칙적으로 **관리비를 제외한 기계손료**에 붙는다. 전용덤프트럭 18톤 이상과 불도저 19톤 이상은 보정 제외이며 타이어·습지 불도저는 예외다. 이 분기를 빼면 암 공종 장비비를 잘못 계산한다.
|
||||
|
||||
또한 운반은 단순한 `거리 × 단가`가 아니다. 작업량 계산에는 적재·주행·하차·회차·대기 싸이클과 도로 상태별 속도, 토량 상태 환산이 들어간다. STmate 산출 메모의 Q식은 이 결과인 “단위수량당 장비시간”을 일위대가에 넘긴다.
|
||||
|
||||
## 16. 누락 감사에서 보강한 수량→원가 경계
|
||||
|
||||
STmate는 설계기하를 직접 계산하는 시스템이라기보다 **산출된 공종 수량을 금원화하는 시스템**으로 봐야 한다. 따라서 다음 두 계산을 분리한다.
|
||||
|
||||
```text
|
||||
설계수량 계산
|
||||
도면·측량·구조물 규격 → ㎥/㎡/m/개소 등의 순수량
|
||||
|
||||
원가 투입수량 계산
|
||||
순수량 → 단위 정규화 → 재료손실/품 할증/상태환산 → 내역 수량
|
||||
```
|
||||
|
||||
필수 의미 항목:
|
||||
|
||||
- 토량의 자연·흐트러진·다짐 상태와 환산계수
|
||||
- 수량 표기 자릿수와 합계 자릿수
|
||||
- 소운반 포함 거리와 초과 운반
|
||||
- 구조물 공제하지 않는 체적·면적
|
||||
- 재료 할증이 포함된 품인지 별도 적용인지
|
||||
- 관급 자재수량과 도급 시공품의 분리
|
||||
- 같은 공종 안의 토사·리핑암·발파암, 육상·용수, 심도·규격 분기
|
||||
|
||||
`BOQ11.QTY` 하나만 비교하면 이 의미가 사라진다. Aislo와의 진짜 비교 단위는 최종 숫자가 아니라 `quantity_basis`, `volume_state`, `variant`, `surcharge`, `source geometry` 같은 수량 계보다.
|
||||
|
||||
## 17. 누락 감사에서 보강한 재귀 단가의 계산 순서
|
||||
|
||||
상위 단가 안의 비율 행은 일반 자원 참조와 순서가 다르다.
|
||||
|
||||
```text
|
||||
1. 직접 자재 행을 합산 → 직접 주재료비 밑수 확보
|
||||
2. 직접 사람 품을 합산 → 직접노무비 밑수 확보
|
||||
3. 품 할인·할증을 노무 품에 적용
|
||||
4. 잡재료·공구손료를 해당 밑수에 적용
|
||||
5. 중기 X와 하위 일위대가 B/D를 재귀 해석
|
||||
6. 재료·노무·경비 성분을 합산
|
||||
7. 단가표 출력 위치 규칙으로 단수 처리
|
||||
```
|
||||
|
||||
자기 자신을 가리키는 비율 행을 일반 참조처럼 먼저 풀면 순환이 생긴다. 동일한 하위 단가를 여러 상위 공종이 재사용할 수 있으므로 코드 참조 그래프의 순환 검사가 필요하다. STmate는 기초자료를 바꾸면 관련항목 전부를 재계산하는 특성이 교본에 명시되어 있어, 의존 그래프를 따라 파생 단가를 갱신하는 구조임을 뒷받침한다.
|
||||
|
||||
## 18. 누락 감사에서 보강한 직접비·자원집계의 이중 결과
|
||||
|
||||
같은 상세 내역에서 서로 다른 두 결과 묶음이 나온다.
|
||||
|
||||
### 원가 산입 내역
|
||||
|
||||
공종별 `수량 × 단가`를 원 단위 절사하여 BOQ에 저장하고 직접재료비·직접노무비·산출경비로 합산한다. 이 값이 원가계산의 밑수다.
|
||||
|
||||
### 자원 총괄표
|
||||
|
||||
공종별 상세 전개를 자원 코드별로 다시 모아 총 소요량과 금액을 만든다.
|
||||
|
||||
```text
|
||||
자원별 총수량 = Σ(각 공종 수량 × 그 공종의 자원 원단위)
|
||||
자원별 금액 = ROUND(자원별 총수량 × 채택 자원단가, 원)
|
||||
```
|
||||
|
||||
자원집계는 반올림이고 내역 행은 절사이므로 둘의 합계가 원 단위에서 다를 수 있다. 자원집계 결과를 원가계산의 직접비로 다시 사용하면 STmate와 결과가 어긋나므로 **검산/출력 경로와 원가 산입 경로를 구분**해야 한다.
|
||||
|
||||
## 19. 누락 감사에서 보강한 규모·기간 선택과 순환
|
||||
|
||||
제비율의 율은 고정 상수가 아니라 최소 다음 조건의 함수다.
|
||||
|
||||
```text
|
||||
적용률 = f(기준 시행일, 공사종류, 추정가격/규모 구간, 공사기간, 업체 등급, 적용대상 여부)
|
||||
```
|
||||
|
||||
주의할 순환:
|
||||
|
||||
- 규모 구간을 고르는 기준금액이 원가계산 결과에 의존할 수 있다.
|
||||
- 산업안전보건관리비는 관급 포함 대상액과 도급금액 조건이 얽힌다.
|
||||
- 부가세·관급 끝수 보정을 이윤에 역반영한 실무 사례가 있다.
|
||||
|
||||
STmate가 이 순환을 푸는 내부 알고리즘은 확인되지 않았다. Aislo는 총원가로 구간이 고정될 때까지 반복하고 진동 시 높은 구간 채택 사실을 남긴다. 두 프로그램 비교에서는 최종률만 보지 말고 **구간 판정에 사용한 기준금액과 반복/보정 방식**을 함께 비교해야 한다.
|
||||
|
||||
## 20. 누락 감사에서 보강한 변경설계 계산
|
||||
|
||||
접미 1/2/3/4는 단순 대안이 아니라 당초·변경차수 계보로 해석하는 것이 타당하다.
|
||||
|
||||
```text
|
||||
차수별 보존 대상
|
||||
수량 / 자원 채택단가 / 복합단가 / 비목별 금액 / 관급자재대 / 총공사비
|
||||
|
||||
변경 결과
|
||||
변경차수 금액 - 직전차수 금액 = 증감액
|
||||
```
|
||||
|
||||
하지만 모든 제경비가 “변경판 전체를 처음부터 재계산”되는 것은 아니다. 산업안전보건관리비처럼 당초금액에 변경 증감액을 더하는 실무 방식이 관측됐다. 따라서 Aislo 비교 시 다음 셋을 항목별로 구분해야 한다.
|
||||
|
||||
1. 새 밑수와 새 율로 전액 재계산
|
||||
2. 당초 확정액 + 변경분 계산
|
||||
3. 사용자 조정액을 별도 반영
|
||||
|
||||
## 21. 완전성 체크리스트
|
||||
|
||||
| 계산 영역 | 입력 출처 | 핵심 변환 | 출력/다음 소비자 | 현재 확인 수준 |
|
||||
|---|---|---|---|---|
|
||||
| 기준판 선택 | STBSC/JB, 초기화 설정 | 시행일·업종·지역 선택 | 프로젝트 기준자료 | 구조 확인, UI 선택법 미확인 |
|
||||
| 환율 | 기준일·통화별 환율 | 수입기계 C.I.F 가격 환산 | 기계 취득가·손료 | 공식 흐름 확인 |
|
||||
| 자재 채택 | 6개 가격슬롯·페이지 | 채택 인덱스 선택 | M 적용단가 | 저장 구조 확인, 선택 알고리즘 미확인 |
|
||||
| 노임 | 건협/제조업 직종표 | 직종 매핑·별도 할증 | L 적용단가 | 의미 확인 |
|
||||
| 중기 손료 | 취득가·환율·손료계수 | 상각+정비+관리 | X 경비 성분 | 공식 의미 확인, STmate 세부 구현 미확인 |
|
||||
| 중기 운전비 | 유가·소비량·운전사 | 재료+노무+소모품 | X 재료/노무 성분 | 공식 의미 확인 |
|
||||
| 시공능력 | n·q·f·E·거리·속도 | Q와 단위당 시간 | B/D 기계 품 | 의미 확인, 전 기종 구현 미확인 |
|
||||
| 중기품셈 | 기종별 제원·계수·작업조건 | 시공능력·소요시간 산정 | 중기사용료·단가산출 | 공식 단계 확인, STmate 내부표 매핑 미확인 |
|
||||
| 일위대가 | 자원코드·품·비율행 | 재귀 조립·3성분 합산 | B/D 단가 | SMT 문법 확인 |
|
||||
| 작업/표준시장단가 | 기관별 공표단가·시행시점 | 공종·규격 매칭 및 직접 채택 | 공종 단가 | 별도 자료경로 확인, 성분배분 미확인 |
|
||||
| 설계수량 | 도면/수량산출 | 단위·상태·규격 분기 | 순수량 | STmate 밖 상류로 봄 |
|
||||
| 원가 투입수량 | 순수량·할증·환산 | 적용수량 결정 | BOQ.QTY | 필드 구조 확인, BDQTY 내부 미확인 |
|
||||
| 내역 행 | 수량·B/D 단가 | 성분별 곱·절사 | BOQ 금액 | 다수 XLSX 수식으로 확인 |
|
||||
| 계층 합계 | BOQ 말단 금액 | T/E 트리 합산 | 공종·공구·총괄 | 확인 |
|
||||
| 자원집계 | BOQ+단가 상세 | 코드별 수량 재집계·반올림 | 자원 총괄표 | 다수 XLSX 수식으로 확인 |
|
||||
| 직접비 | BOQ 3성분 | 말단 성분 합계 | PRJLST 및 제비율 밑수 | 확인 |
|
||||
| 원가계산기준 | 시행일·공사종류·규모·기간·등급 | 적용 조건과 율 선택 | 간접비 계산 파라미터 | 공식 단계 확인, RATE 인덱스 미확인 |
|
||||
| 간접비 | 직접비·기간·규모·율 | 항목별 밑수×율·행별 절사 | 경비/노무 합계 | 다수 XLSX 교차 확인 |
|
||||
| 일반관리·이윤 | 순공사원가·비목합 | 순서 의존 계산 | 총원가 | 확인 |
|
||||
| 부가세 | 총원가·면세 조정 | 10%·절사·조정 | 도급공사비 | 기본 확인, 예외식 미확인 |
|
||||
| 관급 | 관급 수량·조달단가 | 수수료 절사·총액 천원 올림 | 총공사비 별도 합산 | 다수 XLSX 교차 확인 |
|
||||
| 변경설계 | 차수별 값 | 전액/증감/조정 분기 | 변경 총액·증감액 | 슬롯 확인, 항목별 규칙 일부 미확인 |
|
||||
| 재현·출력 | SET_REP·INI·스냅샷 | 표별 단수·열 구성 | XLSX/인쇄/STC | 구조 확인 |
|
||||
| 수정·재계산 | 기초단가·품·수량·기준 변경 | 참조 의존성 따라 무효화·재산출 | 관련 단가·공사비·간접비 | 교본 기능 확인, 내부 갱신 범위 미확인 |
|
||||
|
||||
이 표에서 `미확인`으로 남은 것은 누락이 아니라 **현재 증거로 확정할 수 없는 경계**다. 특히 자재 자동채택, BDQTY 내부 수량식, 전 기종 중기사용료, 면세·끝수 예외, 변경설계 항목별 처리의 다섯 부분은 실제 조작 전후 차분이 필요하다.
|
||||
|
||||
## 22. 공식 STmate 흐름도 역대조 결과
|
||||
|
||||
임도 교본의 STmate 전산처리 흐름도를 문서 전체와 역대조했다.
|
||||
|
||||
| 공식 흐름도 상자 | 이 문서 대응 | 감사 결과 |
|
||||
|---|---|---|
|
||||
| 기초자료: 노임·자재·중기단가·경비 | §2~3, §14 | 포함 |
|
||||
| 환율 | §2, §15 | 포함 |
|
||||
| 중기품셈 | §15, §21 | **독립 단계로 보강** |
|
||||
| 중기사용료 계산 | §4, §15 | 포함 |
|
||||
| 일위대가 | §4, §17 | 포함 |
|
||||
| 작업단가 | §4 작업단가 경로 | **별도 경로로 보강** |
|
||||
| 단가계산 | §4, §17 | 포함 |
|
||||
| 공사수량 | §5, §16 | 포함 |
|
||||
| 공사비계산 | §5~6 | 포함 |
|
||||
| 원가계산기준 | §7, §19, §21 | **입력 단계로 보강** |
|
||||
| 간접비계산 | §7~8 | 포함 |
|
||||
| 수정·검토 | §1, 아래 영향전파 | **피드백 루프로 보강** |
|
||||
| 단가표·중기사용료·단가산출서 | §4 및 출력 경로 | 포함 |
|
||||
| 수량산출서·설계내역서 | §5, §16 | 포함 |
|
||||
| 예산서 | §7~10 | 포함 |
|
||||
|
||||
### 수정 시 영향 전파
|
||||
|
||||
| 수정한 값 | 다시 계산되어야 하는 결과 |
|
||||
|---|---|
|
||||
| 자재단가 | 해당 자재를 참조하는 B/D/X → 해당 BOQ 행 → 직접재료비 → 관련 제경비 → 총액 |
|
||||
| 노임단가 | 해당 L 참조 B/D/X → 직접노무비 → 간접노무비·보험료·기타경비·이윤 → 총액 |
|
||||
| 유가·환율·중기 취득가 | X 중기사용료 → X를 참조하는 B/D → 산출경비 → 제경비·이윤 → 총액 |
|
||||
| 품·Q·할증 | 해당 복합단가 → BOQ 행 → 비목별 직접비 → 간접비 → 총액 |
|
||||
| 공사수량 | 해당 BOQ 행 금액 → 공종합계·직접비 → 간접비 → 총액 |
|
||||
| 제비율판·기간·규모·등급 | 해당 간접비 행과 그 뒤 순서 의존 항목 → 총액 |
|
||||
| 관급/사급 플래그 | 도급 재료비·관급자재대·안전관리비 대상액·부가세 경로 → 총액 |
|
||||
|
||||
STmate의 경쟁력은 이 영향 전파가 통합 단가 코드와 프로젝트 참조키로 연결된다는 점이다. Aislo 비교에서도 최종 결과 일치만 검사하지 말고 입력 하나를 바꿨을 때 위 하류 결과만 정확히 무효화·재계산되는지 검증해야 한다.
|
||||
|
||||
## 23. 두 번째 감사 후 남은 실제 정보 공백
|
||||
|
||||
계산 단계 자체는 공식 흐름도 기준으로 모두 문서에 들어왔다. 다만 다음은 “단계 누락”이 아니라 **경쟁 프로그램 내부 규칙이 아직 관찰되지 않은 정보 공백**이다.
|
||||
|
||||
1. `RATE*`의 행·열 인덱스가 공사종류·금액구간·기간·등급에 매핑되는 정확한 표
|
||||
2. 표준시장/작업단가를 BOQ의 재료·노무·경비에 배분하는 규칙
|
||||
3. 중기 S→X 계산에서 STmate가 쓰는 계수표와 중간 단수 위치
|
||||
4. `BDQTY`의 순수량→할증수량→비목별 수량 전개 순서
|
||||
5. 단가 후보를 채택하거나 `적용단가` 슬롯으로 복사하는 UI 알고리즘
|
||||
6. 기초자료 수정 후 캐시/저장 단가 중 무엇을 자동 갱신하고 무엇을 확정값으로 보존하는지
|
||||
7. 면세·관급 부가세 끝수와 이윤 역보정의 일반식
|
||||
8. 변경설계 시 항목별 전액 재계산/증감계산/고정액 유지 규칙
|
||||
9. 물가변동·일괄 단가변경 모듈이 기준시점과 비교시점 가격을 반영하는 방식
|
||||
|
||||
이 아홉 항목을 확인하지 않고 “STmate 계산 로직 완전 복원”이라고 표현하면 안 된다. 정상 실행 화면에서 한 입력만 바꾸어 저장 전후 파일과 출력 수식을 비교하는 실험이 필요하다.
|
||||
@@ -0,0 +1,59 @@
|
||||
# STmate 재구현 분석 — 판정기준과 근거 목록
|
||||
|
||||
## 목적
|
||||
|
||||
이 문서는 화면을 비슷하게 만드는 명세가 아니다. 동일 입력을 넣었을 때 STmate와 같은 중간값과 최종 금액을 내는 계산 엔진을 만들기 위한 증거 장부다.
|
||||
|
||||
## 확정등급
|
||||
|
||||
| 등급 | 의미 | 구현 처리 |
|
||||
|---|---|---|
|
||||
| A 확정 | STC 필드, 5개 STmate XLSX의 실제 수식, 복수 문서가 일치 | 호환 계산식으로 구현 가능 |
|
||||
| B 강한 추정 | 필드와 출력은 일치하지만 내부 선택 분기 하나가 직접 관찰되지 않음 | 정책 테이블로 분리하고 회귀시험 필요 |
|
||||
| C 미확정 | 정적 파일에 결과만 있거나 레코드가 난독화되어 분기를 복원하지 못함 | STmate 대조실험 전에는 기본값 확정 금지 |
|
||||
|
||||
## 직접 조사한 표본
|
||||
|
||||
- STC 7건: 2024년 임도 3건, 2025년 계류보전 1건, 울진 소광 원설계·통합변경·변경1차 3건
|
||||
- STmate 생성 XLSX 5건: 위 사업 중 대응 가능한 2024/2025 내역서
|
||||
- STC 안의 DBF: `PRJLST`, `BOQ11`, `BOQ12`, `COSTN`, `BDQTY`, `RATE`, `RATE_M`, `RATE_R`, `RATE_RA`, `SET_REP` 등
|
||||
- 단가 구성 메모: `COSTN.SMT`
|
||||
- 프로그램 모듈·설정: `STmate8.Exe`, `*.CPM`, `Start8x.Ini`
|
||||
- 원가·품셈 기술자료와 과거 STmate 처리 흐름도
|
||||
|
||||
## 관찰된 스키마 변형
|
||||
|
||||
- `BOQ11`: 83필드와 94필드가 모두 존재한다.
|
||||
- `BDQTY`: 37필드와 38필드가 존재한다.
|
||||
- `COSTN`: 99필드와 101필드가 존재한다.
|
||||
- `PRJLST`: 47·48·55필드가 존재한다.
|
||||
- 소광 통합 파일에는 `BOQ11` 90행과 `BOQ12` 89행이 동시에 존재한다. 별도 변경1차 파일에는 `BOQ11`만 115행 존재한다.
|
||||
|
||||
따라서 재구현 저장 모델은 DBF 열 순서를 그대로 도메인 모델로 삼으면 안 된다. 파일 버전별 입력 어댑터가 열 이름을 정규화하고, 계산 코어는 `revision_id`가 붙은 동일한 내부 모델을 사용해야 한다.
|
||||
|
||||
## 계산 파이프라인
|
||||
|
||||
1. 기초자료: 노임·자재 후보가격·중기 기초값·경비·환율을 적재한다.
|
||||
2. 중기품셈: 작업량 `Q`와 단위당 기계시간 `1/Q`를 산출한다.
|
||||
3. 중기사용료: 손료·운전경비·운전원·연료 등을 재료/노무/경비 성분으로 분리한다.
|
||||
4. 단가계산: 일위대가, 작업단가, 표준시장단가, 견적단가를 재료비(`MAMT`)·노무비(`LAMT`)·경비(`SAMT`)와 총계(`TAMT`)로 정규화한다.
|
||||
5. 내역계산: 설계수량과 세 비용 성분을 곱하고 총계와 상호 검산하여 직접공사비를 집계한다.
|
||||
6. 간접비계산: 제비율 행을 순차 계산하며 각 행 절사 결과를 다음 행의 기초로 전달한다.
|
||||
7. 끝전조정·부가세·관급 합계를 계산한다.
|
||||
8. 차수별 결과를 덮어쓰지 않고 스냅샷으로 보존한다.
|
||||
|
||||
## 중간값 보존 원칙
|
||||
|
||||
STmate 호환성은 최종 합계만 비교하면 검증할 수 없다. 다음 값을 모두 저장해야 한다.
|
||||
|
||||
- 선택 전 6개 자재 후보가격과 출처·페이지
|
||||
- 채택가격과 선택 방식(자동/수동/직접입력)
|
||||
- 단가 항목별 원수량, 할증 전 수량, 할증률, 할증 후 수량
|
||||
- 단가 성분별 원금액과 절사 후 금액
|
||||
- 내역 행별 재료비·노무비·경비·합계
|
||||
- 간접비 행별 산정대상액, 요율, 계산 전 값, 절사액, 조정액
|
||||
- 원설계·변경설계 차수와 이전 스냅샷
|
||||
|
||||
## 남은 한계
|
||||
|
||||
`BDQTY`와 `SYSINFO` 레코드 본문은 보유 STC에서 난독화되어 있다. 비밀번호 우회나 암호 해제는 하지 않았다. 따라서 BDQTY의 세부 플래그와 UI 이벤트에 따른 자동갱신 범위는 C등급이며, 동일 프로그램 수준의 확정에는 정상 실행되는 STmate에서 통제 입력 대조시험이 필요하다.
|
||||
@@ -0,0 +1,140 @@
|
||||
# 내부규칙 1·7 — RATE 매핑, 간접비, 끝전조정
|
||||
|
||||
## 1. RATE 계열의 역할
|
||||
|
||||
| 테이블 | 크기 | 관찰 의미 | 대응 화면 그리드 (12번 문서 §8·§9) | 등급 |
|
||||
|---|---:|---|---|---|
|
||||
| `RATE.DBF` | 41×10 | 구버전·공통 요율 행렬 | 단일 그리드로 특정되지 않음 | B |
|
||||
| `RATE_M.DBF` | 25×10 | 행자부기준 간접노무비 요율군 | `rGrd_RateM` (14행, 구분·간접노무비) | A/B |
|
||||
| `RATE_R.DBF` | 50×35 | 일반형식 규모·기간 요율 + 안전·환경·보증 부속표 | `rGrd_Rate_R`, `_R1`, `_R2`, `_Kobo`, `_Anjun`, `_Hwan`, `_Bojng`, `_Hado`, `_Eqip` | B |
|
||||
| `RATE_RA.DBF` | 50×20 | 실적형식 규모·기간 요율 + 실적 안전비 공식표 | `rGrd_Rate_RA`, `_RA1`, `_An_A`, `_An_A1` | A/B |
|
||||
|
||||
7개 STC 모두 네 테이블의 행·열 수와 필드명이 같다. `RATE` 첫 행들은 2024 표본에서 `10.8, 11.3, 11.8, 15.3 ...`, 2025 표본에서 일부가 `11.0, 11.5, 12.0 ...`으로 바뀐다. 즉 이 표는 프로젝트 결과가 아니라 연도/기준표 원본을 프로젝트에 복사해 둔 것으로 보는 것이 타당하다.
|
||||
|
||||
`RATE_R`은 첫 행의 21열=13.1, 26열=8.0, 31열=6.0처럼 일부 열만 채워지고 `RATE_RA`는 표본 대부분이 0이다. 희소한 이유는 한 행렬이 위 여러 그리드의 행을 순서대로 이어 붙여 적재하는 구조이기 때문이며, 해당 형식에서 쓰지 않는 구간이 0으로 남는다.
|
||||
|
||||
DBF 자체에는 행·열 라벨이 없다. 다만 폼 리소스 복원으로 **각 그리드의 열 제목과 행 수는 확정**됐다(12번 문서 §8). 남은 미확정은 라벨 자체가 아니라 **각 그리드가 행렬의 몇 번째 행부터 적재되는지, 즉 시작 좌표**다. 재구현 시에는 숫자 배열을 하드코딩하지 말고 `rate_standard(year, work_type, cost_item, amount_band, duration_band)`로 명명된 기준 테이블에 옮겨야 한다.
|
||||
|
||||
## 2. 요율 선택 입력
|
||||
|
||||
요율 하나를 선택하는 최소 입력은 다음과 같다.
|
||||
|
||||
- 기준연도·적용시점
|
||||
- 공사 종류와 발주 유형
|
||||
- 추정금액 또는 직접공사비 구간
|
||||
- 공사기간 구간
|
||||
- 요율 항목(간접노무비, 산재보험료, 안전관리비 등)
|
||||
- 항목별 적용/제외 플래그
|
||||
- 사용자 지정률과 조정액
|
||||
|
||||
원가계산 설정 화면의 **실제 선택축 16개와 각 선택지의 내부 코드값**은 폼 복원으로 확정됐다(12번 문서 §10·§10.1). 화면 순번과 내부값이 어긋나는 축이 여럿이므로(공사종류 `0,2,4,3,1,5,8,9`, 적용기준 `2,0,1`, 사급비 위치 `3,1,2,0`) 화면 인덱스를 그대로 저장하면 안 된다.
|
||||
|
||||
프로젝트 규모는 제비율을 거쳐 총액을 만들고, 총액은 다시 구간 선택에 쓰일 수 있어 순환 의존성이 생긴다.
|
||||
|
||||
**STmate가 택한 방식은 확정됐다 — A등급.** 실행모듈 문자열에 "공사규모에 따른 제비율 변경으로 공사규모가 계속 변경되는 무한루프"가 생길 수 있다는 경고와, 그때 일반관리비·이윤 등의 요율을 **사용자가 직접 수정**하라는 안내가 그대로 들어 있다(11번 문서 §8). 즉 STmate는 고정점 반복으로 자동 수렴시키지 않고 **순환이 생기면 사용자 수동 확정으로 넘긴다.**
|
||||
|
||||
재구현도 같은 계약을 따른다. ① 명시 입력인 추정금액 구간을 우선 사용하고 ② 자동 재선택은 최대 반복 횟수와 상태 이력으로 순환을 검출하며 ③ 구간이 왕복하면 임의의 한쪽을 택하지 않고 사용자 확정 상태로 전환하고 ④ 확정된 요율·원 표 좌표·사용자 수정 여부를 프로젝트 스냅샷에 보존한다.
|
||||
|
||||
## 3. XLSX로 확정되는 계산 순서
|
||||
|
||||
5개 STmate XLSX에서 내역 행은 `ROUNDDOWN(수량×단가, 0)`이다. 간접비도 각 행을 원 단위 절사한 뒤 그 절사액이 다음 행 참조에 사용된다. 자원 집계표·일위대가수량금액집계표·중기시간금액집계표는 `ROUND(수량×단가, 0)`를 사용한다. 동일 곱셈이라도 출력 목적에 따라 함수가 다르다.
|
||||
|
||||
권장 순서는 다음과 같다.
|
||||
|
||||
1. 직접재료비, 직접노무비, 직접경비 및 직접공사비를 내역 행 절사액으로 합산한다.
|
||||
2. 간접노무비, 노무 기반 보험, 산업안전보건관리비, 환경보전비, 기타경비를 각각 고유 밑수에 요율을 곱해 구한다.
|
||||
3. 일반관리비 → 이윤 → 부가가치세 → 관급자재 관련 합계 순으로 마무리한다.
|
||||
|
||||
### 항목별 밑수 — A등급 (2026-09-13 폼 복원으로 확정)
|
||||
|
||||
RATE 편집 화면(`TWM_KAN_RATE`)의 각 그리드는 **묶음 제목에 밑수 공식을 그대로 달고 있다.** 추정이 아니라 화면 문자열이다.
|
||||
|
||||
| 항목 | 화면에 적힌 밑수 공식 | 대응 그리드 |
|
||||
|---|---|---|
|
||||
| 간접노무비 | `(직접노무비) × 율(%)` | `rGrd_Rate_R1` |
|
||||
| 기타경비 (일반형식) | `(재료비+노무비) × 율(%)` | `rGrd_Rate_R1` |
|
||||
| 기타경비 (실적형식) | `직접공사비 × 율` | `rGrd_Rate_RA1` |
|
||||
| 고용보험료 | `(노무비) × 율` | `rGrd_Rate_Kobo`, `rGrd_Rate_R2` |
|
||||
| 산업안전보건관리비 (일반) | `(재료비+직접노무비+관급자재) × 율 + 추가금액` | `rGrd_Rate_Anjun` |
|
||||
| 산업안전보건관리비 (실적) | `(직접공사비+관급자재) × 율 + 추가금액` | `rGrd_Rate_An_A1` |
|
||||
| 환경보전비 | `(재료비+직접노무비+산출경비) × 율` | `rGrd_Rate_Hwan` |
|
||||
| 공사이행보증수수료 | `[추가금액 + (직접공사비-기준금액) × 율] × 공기(년)` | `rGrd_Rate_Bojng` |
|
||||
| 건설하도급대금 지급보증서 발급수수료 | `(재료비+직접노무비+산출경비) × 율` | `rGrd_Rate_Hado` |
|
||||
| 건설기계대여대금 지급보증서 발급금액 | `(재료비+직접노무비+산출경비) × 율` | `rGrd_Rate_Eqip` |
|
||||
| 일반관리비 | `(재+노+경) × 율` | `rGrd_Rate_R`, `rGrd_Rate_RA` |
|
||||
| 이윤 | `(노+경+일) × 율` | `rGrd_Rate_R`, `rGrd_Rate_RA` |
|
||||
|
||||
바로잡을 점 셋.
|
||||
|
||||
- **환경보전비의 밑수는 직접공사비가 아니다** — `재료비+직접노무비+산출경비`다. 이 문서의 이전 서술을 정정한다.
|
||||
- **기타경비의 밑수에 간접노무비가 따로 붙지 않는다** — 일반형식은 `재료비+노무비`(노무비는 직접+간접), 실적형식은 `직접공사비`로 **형식에 따라 밑수 자체가 다르다.**
|
||||
- **이윤의 밑수는 일반형식에서 `노+경+일`로 확정** — 일반관리비가 밑수에 들어간다. `(소계−재료비)`, `(순공사원가+일반관리비−재료비)`로 적힌 곳들과 같은 식이다.
|
||||
- ⚠ **실적형식은 화면 라벨과 복원 공식이 어긋난다.** `rGrd_Rate_RA`의 라벨은 `(노+경+일)*율`인데 12번 §5·§6이 복원한 실적 공식은 `(직접공사비+간접공사비+일반관리비) × 율`로 **재료비를 포함한다.** 둘 중 어느 쪽이 실제 동작인지는 통제시험 **T11**로 가른다(13번 문서).
|
||||
|
||||
`rGrd_RateB`(수공 12행)의 열은 `재 / 직노 / 노 / 재+직노 / 재+노`가 토목·건축 두 열군으로 나뉜다. 수공형식은 **경비 항목마다 이 다섯 밑수 중 하나를 고르는** 구조다.
|
||||
|
||||
정확한 포함항목은 사업유형과 XLSX 양식에 따라 달라질 수 있으므로 `base_components`를 항목별 정책 데이터로 보존해야 한다. 하나의 공통 `subtotal * rate` 함수에 항목명을 분기문으로 박아 넣으면 변경설계와 연도 변경에서 깨진다.
|
||||
|
||||
## 4. 끝전조정과 세금
|
||||
|
||||
실제 XLSX에서 다음 형식이 관찰됐다.
|
||||
|
||||
```text
|
||||
차액 = 기준금액 - ROUNDDOWN(기준금액, -자리수)
|
||||
세전조정 = ROUND(차액 / 1.1, 0)
|
||||
조건부조정 = ABS(차액) × IF(조건, 부호 또는 적용값)
|
||||
최종이윤 = 산식이윤 - 참조조정액들
|
||||
부가세 = ROUNDDOWN(과세표준 × 세율 / 100, 0) + 별도조정액
|
||||
관급자재합계 = ROUNDUP(대상합계, -3)
|
||||
```
|
||||
|
||||
핵심은 최종 도급액을 먼저 천원 단위로 자르고 부가세를 다시 구하는 단순 역산이 아니라, 차액의 세전 상당액을 이윤 또는 조정행에 배분하고 부가세 행에도 조정액을 전달한다는 점이다.
|
||||
|
||||
실행모듈에서 `[C_AD_AMT]`에 `ROUND` / `ROUNDDOWN` / `ROUNDUP` 세 선택이 모두 확인됐고(11번 문서 §11), 설정 화면에는 간접비·공급가액·도급액·관급(도급자설치)·관급(관급자설치)·총액/이윤보정의 **절사 자리와 함수가 각각 따로** 있다(12번 문서 §10.2). 따라서 전역 반올림 설정 하나로 구현하면 안 되고, 재구현 모델에는 다음 여섯 칸이 필요하다.
|
||||
|
||||
```text
|
||||
raw_amount
|
||||
rounded_amount
|
||||
adjustment_amount
|
||||
adjustment_target
|
||||
rounding_function
|
||||
rounding_digits
|
||||
```
|
||||
|
||||
또 `이윤 보정=0`은 "0원 이윤"이 아니라 이윤보정 미사용 옵션이므로 0과 미설정을 구분해야 한다.
|
||||
|
||||
## 5. 의사코드
|
||||
|
||||
```text
|
||||
for cost_item in ordered_indirect_cost_items:
|
||||
base = sum(already_truncated[item] for item in cost_item.base_components)
|
||||
raw = base * selected_rate / 100
|
||||
amount = truncate(raw, cost_item.rounding_digits)
|
||||
already_truncated[cost_item.code] = amount
|
||||
|
||||
target_total = truncate(grand_total_before_adjustment, -target_digits)
|
||||
difference = grand_total_before_adjustment - target_total
|
||||
pretax_adjustment = round_half_away_or_excel(difference / (1 + vat_rate), 0)
|
||||
profit_final = profit_raw - pretax_adjustment - other_adjustments
|
||||
vat_final = truncate(tax_base * vat_rate, 0) + vat_adjustment
|
||||
```
|
||||
|
||||
Excel `ROUND`의 .5 처리와 Python 기본 `round`는 다를 수 있다. 금액 계산은 이진 부동소수점 대신 10진수와 Excel 호환 반올림 함수를 써야 한다.
|
||||
|
||||
## 6. 미확정 항목과 대조시험
|
||||
|
||||
폼 리소스 복원으로 아래 두 항목은 이 목록에서 **내려갔다.**
|
||||
|
||||
- ~~RATE 네 표의 정확한 행·열 라벨~~ → 그리드별 열 제목·행 수 확정 (12번 문서 §8)
|
||||
- ~~요율 순환 의존성의 STmate 처리 방식~~ → 사용자 수동 확정으로 확정 (11번 문서 §8)
|
||||
|
||||
남은 미확정은 다음과 같다.
|
||||
|
||||
- 각 그리드가 `RATE*` 행렬의 **몇 번째 행부터 적재되는지** — 시작 좌표
|
||||
- 규모 구간 경계의 포함 방향(`<`, `<=`)
|
||||
- 공사기간 경계 처리 — 화면 목록은 4줄인데 내부값은 `30일 미만`·`37개월 이상`을 포함한 5개로 개수가 어긋남 (12번 문서 §10.1)
|
||||
- 특정 보험·안전비의 예외 포함항목
|
||||
- 복합 보험식 `RAT_1`~`RAT_4`, `RAT_KY`의 화면 항목명
|
||||
|
||||
시험은 동일 프로젝트를 복제하고 추정금액을 각 경계의 직전/동일/직후로, 기간도 동일하게 바꾼 뒤 선택된 요율과 모든 간접비 행을 비교한다. 이 결과 없이는 시작 좌표 매핑을 A등급으로 올릴 수 없다.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# 내부규칙 2·3·4·5 — 직접비 단가, 중기, BDQTY, 자재 채택
|
||||
|
||||
## 1. 모든 단가의 공통 출력 계약
|
||||
|
||||
STmate의 단가 항목은 출처가 달라도 최종적으로 세 비용 성분과 총계를 낸다.
|
||||
|
||||
```text
|
||||
TAMT = MAMT(material) + LAMT(labor) + SAMT(expense)
|
||||
```
|
||||
|
||||
`COSTN.TAMTn/LAMTn/MAMTn/SAMTn`은 차수 n의 총계·노무·재료·경비다. BOQ의 `T_AMT/L_AMT/M_AMT/S_AMT`도 같은 구조다. 바이너리 내부 조건식 `TAMT1<>MAMT1+LAMT1+SAMT1`이 이 관계를 직접 검증한다. 재구현할 때 표준시장단가나 견적단가를 총액 하나로만 저장하면 내역은 맞아도 간접비 산정기초가 달라진다.
|
||||
|
||||
## 2. 일위대가·작업단가·표준시장단가
|
||||
|
||||
### 입력
|
||||
|
||||
- 항목 코드·명칭·규격·단위
|
||||
- 구성 자원 코드와 유형(재료 M, 노무 L, 중기/경비 X 등)
|
||||
- 구성 수량 또는 산식
|
||||
- 할증·보정 전후 수량
|
||||
- 구성 자원의 채택단가, 재료·노무·경비 성분 및 총계
|
||||
- 직접 입력 또는 성분 배분값
|
||||
|
||||
### 계산
|
||||
|
||||
```text
|
||||
component_amount[k] = quantity_after_adjustment × referenced_unit_component[k]
|
||||
unit_component[k] = sum(component_amount[k])
|
||||
```
|
||||
|
||||
`COSTN.SMT`에는 `&M00034M&`, `&L00010L&`, `&X00055M&` 같은 참조 토큰과 수량·백분율·산식 문자열이 남는다. 마지막 글자는 참조 항목에서 가져올 성분을 뜻하는 것으로 강하게 추정된다. 따라서 계산기는 메모 표시 문자열이 아니라 토큰을 파싱한 의존 그래프를 먼저 만들고 위상순서로 단가를 계산해야 한다. 순환 참조는 오류로 보고해야 한다.
|
||||
|
||||
표준시장단가·작업단가는 일위대가와 병렬 입력이다. 직접 성분이 주어진 경우 그대로 쓰고, 총액만 주어진 경우 STmate와 동일한 성분 배분 자료가 없으면 임의 분배하지 않는다. 총액 전부를 경비로 넣는 편의 구현은 간접비를 바꾸므로 금지한다.
|
||||
|
||||
## 3. 중기 시간당 단가
|
||||
|
||||
### 계산 단계
|
||||
|
||||
```text
|
||||
hourly_ownership = acquisition_price × (depreciation_factor + maintenance_factor + management_factor)
|
||||
hourly_fuel = fuel_consumption_per_hour × adopted_fuel_price
|
||||
hourly_lubricant = lubricant_rule(hourly_fuel or consumption)
|
||||
hourly_labor = operator_daily_wage / daily_hours × labor_adjustments
|
||||
hourly_operating = hourly_fuel + hourly_lubricant + hourly_labor + consumables
|
||||
hourly_machine = hourly_ownership + hourly_operating + transport_hourly + assembly_hourly
|
||||
production_per_hour Q = n × q × f × E
|
||||
machine_hours_per_unit = 1 / Q
|
||||
unit_machine_cost = machine_hours_per_unit × hourly_machine
|
||||
```
|
||||
|
||||
중기 성분 참조 토큰이 M/L/S로 나뉘므로 연료·소모품은 재료, 운전원은 노무, 손료·정비·관리·운반·조립은 경비로 분리하여 상위 단가에 전달해야 한다. 암질 보정은 모든 장비에 일괄 적용하지 않고 제외 장비를 가진 정책으로 처리해야 한다.
|
||||
|
||||
XLSX의 `중기시간금액집계표`에서는 시간×중기목록표 단가를 `ROUND(...,0)`한다. 하지만 중기 내부의 각 세부항목 절사 시점은 출력 수식만으로 완전히 확정되지 않았다. 내부 원값, 표시값, 상위 전달값을 분리해 저장해야 한다.
|
||||
|
||||
## 4. BDQTY 수량·할증
|
||||
|
||||
확인된 필드는 `BDCODE`, `CODE`, `NAME`, `SIZE`, `UNIT`, `QTY1..4`, `DEVD1..4`, `X_QTY1..3`, `X_HAL1..3`, `RATE`, `X_RATE`, `S_RATE`, `PRPT`, `TPRPT`, `PRIN`, `PROUT`, `EQCODE`, `SBMOK` 등이다.
|
||||
|
||||
최소 의미 모델은 다음과 같다.
|
||||
|
||||
```text
|
||||
source_quantity # 품셈 또는 사용자가 입력한 원수량
|
||||
pre_surcharge_qty # X_QTY 계열
|
||||
surcharge_rate # X_HAL/RATE 계열
|
||||
post_surcharge_qty # 단가계산에 실제 투입되는 수량
|
||||
revision_quantity # QTY1..4
|
||||
waste_or_output # PRIN/PROUT 등 투입·산출 관계
|
||||
```
|
||||
|
||||
재료 수량 할증과 잡재료/공구손료 같은 금액 가산은 다른 연산이다. 전자는 수량을 바꾼 뒤 단가와 곱하고, 후자는 특정 성분 소계에 비율을 곱해 별도 비용행을 만든다. 둘을 하나의 `surcharge` 필드로 합치면 안 된다.
|
||||
|
||||
BDQTY 본문은 난독화되어 필드별 실제 값 연결은 C등급이다. 구현 전 대조시험은 하나의 단가에 재료·노무·중기 각 1개만 넣고, 각 할증 입력을 하나씩 바꿔 `QTY`, `X_QTY`, `X_HAL`, 세 비용 성분과 총계 변화를 저장하는 방식이어야 한다.
|
||||
|
||||
## 5. 자재 6개 가격과 채택가격
|
||||
|
||||
`COSTN`은 차수마다 `AMT_11..AMT_61`처럼 6개 후보가격을 보관하고, 페이지/출처 보조필드 `PG_11..PG_51`, 채택 출처 인덱스 `PG_1..PG_4`, 채택 관련 플래그 `AMT_1..AMT_4`를 가진다. 재료의 최종 채택가는 `MAMT1..4`이며 `TAMT1..4`는 해당 자원의 총단가다.
|
||||
|
||||
7개 STC 교차비교 결과:
|
||||
|
||||
- 재료 M 항목의 채택가는 파일마다 후보 1~6 중 여러 슬롯과 일치한다.
|
||||
- 같은 채택가가 여러 후보 슬롯에 중복되어 한 레코드만으로 선택 슬롯을 역추정할 수 없다.
|
||||
- 2025 소광 표본은 M 43건 중 후보6과 채택가가 일치하는 행이 37건으로 많지만, 다른 후보와도 중복 일치한다.
|
||||
- 노무 L은 다수 파일에서 후보6과 채택가가 거의 일치한다. 이는 후보6이 범용 “채택/적용” 칸일 가능성을 높이지만 자동선정 규칙 자체를 증명하지는 않는다.
|
||||
|
||||
`Start8x.Ini`의 표시명은 설치/기관별로 바뀔 수 있으므로 슬롯 번호에 특정 가격지 이름을 코드로 고정하면 안 된다. 데이터 모델은 다음처럼 출처를 별도 행으로 둔다.
|
||||
|
||||
```text
|
||||
price_candidate(material_id, revision_id, slot_no, source_name, page, observed_price, observed_at)
|
||||
price_adoption(material_id, revision_id, chosen_slot_no?, adopted_price, method, user_override)
|
||||
```
|
||||
|
||||
자동선정은 현재 C등급이다. 최저가·최신가·특정 슬롯 우선 중 어느 하나라고 단정할 근거가 없다. 동일 구현 전에는 후보 조합을 통제한 STmate 시험이 필수다: 6개 슬롯에 서로 다른 값, 동률, 0, 공란을 배치하고 채택값과 `PG_1` 변화를 기록한다.
|
||||
|
||||
## 6. 반올림 경계
|
||||
|
||||
- 내역서 행 금액: `ROUNDDOWN(수량×단가,0)` — A
|
||||
- 자원/일위대가/중기 집계표 행 금액: `ROUND(수량×단가,0)` — A
|
||||
- 단가 내부 구성행과 성분 소계: 표본별 좌표 추가 대조 필요 — B/C
|
||||
|
||||
계산 엔진은 출력표마다 다시 계산하지 말고 동일 원수량·원단가에서 각 보고서의 반올림 정책을 명시적으로 적용해야 한다.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 내부규칙 6·8·9 — 재계산, 변경설계, 일괄변경
|
||||
|
||||
## 1. 재계산 의존 그래프
|
||||
|
||||
재계산 범위는 화면 단위가 아니라 데이터 의존성으로 결정해야 한다.
|
||||
|
||||
```text
|
||||
노임/자재/환율 ─┐
|
||||
중기 기초값 ────┼→ 중기시간단가 → 일위·작업단가 ─┐
|
||||
품셈/할증 ──────┘ ├→ 내역 직접비 → 간접비 → VAT/끝전 → 출력
|
||||
설계수량 ────────────────────────────────────────┘
|
||||
요율기준 ─────────────────────────────────────────────────→ 간접비 이후
|
||||
```
|
||||
|
||||
### 변경별 최소 무효화 범위
|
||||
|
||||
| 변경 | 반드시 다시 계산할 대상 |
|
||||
|---|---|
|
||||
| 자재 채택가 | 해당 자재를 참조하는 중기·단가의 모든 상위 노드, 내역, 간접비 |
|
||||
| 노임 | 해당 직종을 참조하는 중기·단가, 직접·간접노무 기반 비용 |
|
||||
| 유류·환율 | 해당 중기, 이를 참조하는 단가와 상위 내역 |
|
||||
| 품셈 수량/할증 | 해당 단가와 상위 단가·내역 |
|
||||
| 설계수량 | 해당 내역 행과 상위 공종 합계, 간접비. 단가표 자체는 유지 |
|
||||
| 요율표/규모/기간 | 직접비 유지, 간접비 이후 재계산 |
|
||||
| 출력 서식 | 계산 캐시 유지, 보고서만 재생성 |
|
||||
|
||||
`COSTN.SMT` 참조 토큰은 이 역의존성 인덱스를 만드는 핵심 근거다. 문자열을 계산할 때마다 전수 검색하지 말고 저장 시 `dependency(parent_code, child_code, component)`로 정규화한다.
|
||||
|
||||
STmate UI에서 어떤 버튼이 부분계산 또는 전체계산을 수행하는지는 정적 자료로 확정되지 않았다. 그러나 동일 결과를 만드는 데 필요한 최소 범위는 위 표로 명세할 수 있다. 계산 결과에는 입력 버전 해시를 남겨 오래된 캐시가 출력되는 일을 막는다.
|
||||
|
||||
## 2. 변경설계 데이터 모델
|
||||
|
||||
STC에는 두 형태가 동시에 확인된다.
|
||||
|
||||
1. 한 파일 안에 `BOQ11`, `BOQ12`를 함께 저장하는 통합형: 소광 표본은 각각 90행, 89행이다.
|
||||
2. 변경 차수를 별도 STC로 저장하면서 그 파일의 `BOQ11`을 현재안으로 쓰는 분리형: 소광 변경1차 표본은 115행이다.
|
||||
|
||||
또한 `COSTN`과 여러 필드의 접미사 1~4는 원설계와 최대 3개 변경차수의 값을 나란히 담을 수 있다. 따라서 “접미사 n=항상 BOQ1n”으로 기계적으로 가정하지 말고 프로젝트 메타데이터와 실제 존재 테이블을 함께 해석해야 한다.
|
||||
|
||||
### STmate의 실제 변경설계 계산모드 — A등급
|
||||
|
||||
실행모듈에 간접비 변경분 산식 두 개가 그대로 들어 있다 (11번 문서 §10).
|
||||
|
||||
```text
|
||||
방법 A = C_AMT_B
|
||||
+ ROUNDDOWN(C_AMT_B × ((C_TAMT_C-C_TAMT_B)/C_TAMT_B), digits)
|
||||
|
||||
방법 B = C_AMT_B
|
||||
+ ROUNDDOWN((C_TAMT_C-C_TAMT_B) × RAT_1, digits)
|
||||
```
|
||||
|
||||
여기에 보험료 사후정산 항목의 **당초금액 직접입력**(직접입력 체크가 없으면 적용 거부)과, 계약 제잡비율 적용 시 간접노무비부터 부가세 직전까지를 `직접비 변경분 × 계약 제잡비율`로 조정하는 경로가 더 있다. 보험료 변경(`cBx_CH_BO`)은 별도로 세 모드를 가진다 (12번 문서 §10.3).
|
||||
|
||||
```text
|
||||
0 직접노무비 × 요율
|
||||
1 당초액 + 직접노무비 증감 × 요율
|
||||
2 당초액 + 증감액(비율)
|
||||
```
|
||||
|
||||
즉 **STmate는 각 차수를 전부 독립 재계산하는 한 가지 방식이 아니라, 항목별로 계산모드를 고르는 구조다.**
|
||||
|
||||
### 재구현 규칙
|
||||
|
||||
- 원설계를 불변 revision 1로 보존한다.
|
||||
- 변경 시작 시 이전 차수의 단가·수량·요율 선택을 복사한 스냅샷을 만든다.
|
||||
- 변경한 입력과 자동 재계산 결과를 구분한다.
|
||||
- 비교표는 `original`, `previous`, `current`, `delta`를 모두 계산한다.
|
||||
- 삭제 항목은 물리 삭제하지 않고 현재 차수에 0/제외 상태로 남겨 코드 대응을 보존한다.
|
||||
- 신규 항목은 안정적인 내부 ID와 차수별 표시 순서를 가진다.
|
||||
- 간접비 항목마다 `recalculated`, `proportional_from_original`(방법 A), `delta_times_rate`(방법 B), `carry_original`, `manual`을 **계산모드 값으로 저장**한다. 절사 비선형성 때문에 모드가 다르면 원 단위 결과가 갈리므로, 모드를 하나로 고정하면 STmate 결과를 재현할 수 없다.
|
||||
- Aislo 자체 기본값으로는 전 차수 독립 재계산(`recalculated`)을 권장하되, **STmate 호환 모드와 구분해서** 둔다.
|
||||
|
||||
## 3. 일괄 단가변경·물가변동
|
||||
|
||||
프로그램 모듈명과 필드(`EDT_DATE`, `BQCODE`, 후보가격, 환율 등)로 일괄변경 기능의 존재는 확인되지만 세부 필터와 우선순위는 정적 자료만으로 확정되지 않았다.
|
||||
|
||||
동일 프로그램 수준의 안전한 처리 계약은 다음과 같다.
|
||||
|
||||
1. 대상 스냅샷을 먼저 확정한다: 차수, 코드 유형, 품목 범위, 가격 출처, 기준일.
|
||||
2. 각 레코드에 변경 전 후보가·채택가·선택방식과 변경 후 값을 기록한다.
|
||||
3. 수동 고정값을 자동 변경이 덮을지 정책을 명시한다. 기본적으로 수동고정은 제외한다.
|
||||
4. 가격을 바꾼 뒤 역의존 그래프로 영향받는 항목만 재계산한다.
|
||||
5. 변경 전/후 직접비, 간접비, VAT, 총액을 함께 미리보기한다.
|
||||
6. 확정 시 새 revision으로 원자적으로 저장하며 이전 차수는 보존한다.
|
||||
|
||||
물가변동은 단순히 모든 단가에 같은 배율을 곱하는 기능으로 만들면 안 된다. 노무·재료·중기·견적의 기준일과 출처가 다르고, 중기는 연료·노무·손료 성분을 다시 계산해야 하기 때문이다.
|
||||
|
||||
## 4. 반드시 필요한 STmate 대조시험
|
||||
|
||||
- 같은 자재를 참조하는 단가 2개와 그중 하나를 참조하는 상위 단가를 만든 뒤 자재가 1원 변경
|
||||
- 설계수량만 변경하여 단가표 불변 여부 확인
|
||||
- 요율만 변경하여 직접비 불변 여부 확인
|
||||
- 원설계에서 항목 추가·삭제·수량변경·단가변경을 각각 한 건씩 만들어 변경 STC 저장
|
||||
- 일괄변경에서 수동 채택가, 0원, 공란, 환율적용 품목이 섞인 표본 실행
|
||||
- 각 시험 전후 STC의 모든 DBF 필드와 XLSX 모든 계산행 비교
|
||||
|
||||
이 시험 전까지 UI 버튼별 재계산 범위, 수동값 우선순위, 물가변동 필터는 C등급이다. 다만 **변경설계 간접비 계산모드 자체는 코드에서 공식을 직접 확인해 A등급**이며(§2), 이 시험의 목적은 "어느 모드가 어느 조건에서 선택되는가"를 가르는 데 있다.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# STmate 호환 계산 엔진 — 통합 재구현 명세
|
||||
|
||||
## 1. 완료 정의
|
||||
|
||||
다음 조건을 모두 만족해야 “동일 프로그램을 만들 수 있는 분석”이 완료된다.
|
||||
|
||||
- 같은 입력에서 단가의 재료·노무·경비 성분과 그 총계가 각각 일치한다.
|
||||
- 내역 행, 공종 소계, 직접비, 모든 간접비 행, 이윤, VAT, 관급합계가 각각 일치한다.
|
||||
- 원설계와 변경설계가 독립 재현되고 차액이 일치한다.
|
||||
- 출력 목적별 `ROUND`, `ROUNDDOWN`, `ROUNDUP` 차이가 일치한다.
|
||||
- 기초가격 하나를 바꿀 때 영향받는 항목과 보존되는 항목이 일치한다.
|
||||
- 자동선택과 수동고정의 우선순위가 일치한다.
|
||||
|
||||
최종 총액만 일치하는 것은 합격이 아니다.
|
||||
|
||||
## 2. 내부 데이터셋
|
||||
|
||||
### 프로젝트와 차수
|
||||
|
||||
```text
|
||||
project(id, name, work_type, owner_type, base_date, duration, estimate_band)
|
||||
revision(id, project_id, sequence, kind, parent_revision_id, created_at)
|
||||
```
|
||||
|
||||
### 기초가격
|
||||
|
||||
```text
|
||||
resource(id, code, type, name, size, unit)
|
||||
price_candidate(resource_id, revision_id, slot, source, page, price, date)
|
||||
price_adoption(resource_id, revision_id, chosen_slot, adopted_price, method, locked)
|
||||
```
|
||||
|
||||
### 단가 구성과 계산 결과
|
||||
|
||||
```text
|
||||
unit_item(id, code, kind, name, size, unit)
|
||||
unit_component(unit_item_id, revision_id, seq, ref_id, ref_component,
|
||||
source_qty, pre_surcharge_qty, surcharge_rate, applied_qty, expression)
|
||||
unit_result(unit_item_id, revision_id, material, labor, expense, total)
|
||||
dependency(parent_id, child_id, component)
|
||||
```
|
||||
|
||||
### 내역과 간접비
|
||||
|
||||
```text
|
||||
boq_item(id, revision_id, parent_id, seq, code, ref_unit_item_id, design_qty, state)
|
||||
boq_result(boq_item_id, material_unit, labor_unit, expense_unit, total_unit,
|
||||
material_amount, labor_amount, expense_amount, total_amount)
|
||||
rate_standard(year, work_type, cost_code, amount_band, duration_band, rate)
|
||||
indirect_rule(cost_code, order_no, base_components, rounding_mode, rounding_digits)
|
||||
indirect_result(revision_id, cost_code, base_amount, rate, raw_amount,
|
||||
rounded_amount, adjustment_amount)
|
||||
```
|
||||
|
||||
`base_components`는 문자열 수식보다 검증 가능한 코드 목록/연산 트리로 저장한다.
|
||||
|
||||
## 3. 계산 순서
|
||||
|
||||
```text
|
||||
validate inputs
|
||||
select adopted resource prices
|
||||
build dependency graph; reject cycles
|
||||
calculate machine hourly rates
|
||||
calculate unit items in topological order
|
||||
calculate BOQ leaf rows with BOQ rounding policy
|
||||
aggregate BOQ tree bottom-up
|
||||
select rates from project classification
|
||||
calculate indirect rows in fixed order using already-rounded predecessors
|
||||
calculate profit/end adjustment/VAT/government-material totals
|
||||
freeze revision result and generate reports
|
||||
```
|
||||
|
||||
## 4. 숫자 규칙
|
||||
|
||||
- 모든 금액·수량·요율은 decimal로 계산한다.
|
||||
- 입력 원값과 표시 반올림값을 분리한다.
|
||||
- 내역 행은 원 단위 내림, 자원집계는 원 단위 Excel 반올림, 관급합계는 천원 단위 올림 등 출력별 정책을 코드가 아닌 데이터로 명명한다.
|
||||
- 간접비 다음 행은 앞 행의 원값이 아니라 절사된 확정액을 참조한다.
|
||||
- 합계 후 절사와 행별 절사 후 합계를 혼용하지 않는다.
|
||||
|
||||
## 5. 호환 어댑터
|
||||
|
||||
- STC는 ZIP 컨테이너로 읽고 파일명 대소문자를 무시한다.
|
||||
- DBF 03/E5/06 변형과 필드 수 차이를 버전 어댑터에서 흡수한다.
|
||||
- `BOQ11..14`와 접미사 1..4를 revision으로 정규화하되 실제 테이블 존재 여부와 프로젝트 메타데이터로 판정한다.
|
||||
- `COSTN.SMT` 토큰을 파싱해 구성항목과 의존성을 복원한다.
|
||||
- 난독화된 `BDQTY`는 합법적으로 확보된 평문/API 또는 STmate 대조실험 결과 없이는 임의 복호화하지 않는다.
|
||||
|
||||
## 6. 모듈 경계
|
||||
|
||||
화면이나 보고서와 계산 코어를 분리한다. 필요한 모듈은 여섯 개면 충분하다.
|
||||
|
||||
1. `stc_adapter`: STC/DBF/SMT를 정규화한다.
|
||||
2. `price_engine`: 후보가격과 채택을 관리한다.
|
||||
3. `unit_engine`: BDQTY, 중기, 일위·작업·시장·견적 단가를 계산한다.
|
||||
4. `boq_engine`: 계층 내역과 직접비를 계산한다.
|
||||
5. `indirect_engine`: RATE 선택, 제비율, 끝전, VAT를 계산한다.
|
||||
6. `revision_engine`: 원설계·변경설계 스냅샷과 차이를 관리한다.
|
||||
|
||||
별도 규칙 엔진 프레임워크나 범용 수식 언어는 필요 없다. 관찰된 연산 트리와 정책 테이블이면 충분하다.
|
||||
|
||||
## 7. 시험 골든셋
|
||||
|
||||
각 보유 STC/XLSX 쌍에서 다음 JSON을 추출해 골든셋으로 둔다.
|
||||
|
||||
```text
|
||||
resource adopted prices
|
||||
unit four-component prices
|
||||
BOQ quantities and four-component row amounts
|
||||
indirect row: base/rate/raw/rounded/adjustment
|
||||
revision comparison
|
||||
report-specific rounded aggregates
|
||||
```
|
||||
|
||||
시험은 ① 현재 5개 실문서 회귀, ② 경계값 합성시험, ③ STmate 통제 입력 대조시험으로 나눈다. A등급 규칙은 즉시 회귀시험으로 고정하고, B/C등급은 정책 플래그 아래 구현하여 대조결과가 나올 때만 기본값을 확정한다.
|
||||
|
||||
## 8. 현재 구현 가능 범위
|
||||
|
||||
지금 자료만으로 A등급 구현 가능한 부분은 STC 컨테이너와 주요 스키마, 단가의 재료·노무·경비와 총계 구조, BOQ 계산 및 계층집계, 주요 간접비 흐름, 보고서별 반올림, 변경 차수 보존 구조다.
|
||||
|
||||
STmate와 100% 동일하다고 아직 보증할 수 없는 부분은 RATE 좌표 라벨·경계, BDQTY 세부 플래그, 자재 자동채택 우선순위, 중기 내부 절사 시점, UI 명령별 재계산 범위, 일괄변경의 수동값 우선순위다. 이 여섯 항목은 구현상의 사소한 선택이 아니라 결과 금액을 바꾸는 호환성 차단점이다.
|
||||
|
||||
## 9. 구현 착수 순서
|
||||
|
||||
1. 5개 XLSX에서 골든셋을 추출하고 반올림 함수를 고정한다.
|
||||
2. COSTN·BOQ 어댑터와 단가 의존 그래프를 구현한다.
|
||||
3. 직접비와 간접비를 골든셋에 맞춘다.
|
||||
4. 중기와 자재 후보가격을 연결한다.
|
||||
5. revision 스냅샷과 변경 비교를 구현한다.
|
||||
6. 여섯 미확정 항목에 대해 STmate 통제 대조시험을 수행한다.
|
||||
7. 대조결과를 정책 기본값으로 승격하고 전체 회귀를 다시 통과시킨다.
|
||||
@@ -0,0 +1,148 @@
|
||||
# STmate System 186개 파일 분석 장부
|
||||
|
||||
분석 대상: `C:\경영정보(주)\System`
|
||||
기준일: 2026-09-13
|
||||
판정 원칙: 파일명 추정은 완료로 세지 않으며, 바이너리 내부 근거와 계산 입출력 연결이 확인되어야 `계산 의미 완료`로 판정한다.
|
||||
|
||||
## 1. 현재 숫자
|
||||
|
||||
| 수준 | 완료 | 남음 | 의미 |
|
||||
|---|---:|---:|---|
|
||||
| 전체 파일 등록·SHA-256·크기 | 186 | 0 | 전 파일 변경 추적 가능 |
|
||||
| 매직·컨테이너/PE/DBF 형식 확인 | 186 | 0 | 파일명 아닌 실제 헤더 기준 |
|
||||
| 핵심 CPM 내부구조 추출 | 12 | 0 | 우선대상 12개 모두 PE 섹션·import/export·문자열 추출 |
|
||||
| 전체 파일 내부구조 추출 | 186 | 0 | PE 82, DBF 계열 61, ZIP 28, 기타 포맷 15 |
|
||||
| 계산 의미 교차검증 완료 파일 | 0 | 186 | 함수 입력→중간값→출력이 모두 확인된 파일은 아직 없음 |
|
||||
|
||||
형식별 실제 판정은 PE 82개, ZIP 28개, DBF 계열 61개, 텍스트 8개, BTLF 2개, BTCV·SIxMemo·DOS-MZ·난독화 로그·팁 데이터 각 1개다. 파일별 전체 장부는 함께 생성한 `system_inventory.csv`에 있으며 UTF-8 BOM과 SHA-256을 포함한다.
|
||||
|
||||
## 2. 핵심 CPM 12개 1차 정적 분석
|
||||
|
||||
모든 모듈은 32비트 Delphi 6 계열 PE이며 `CODE`, `DATA`, `BSS`, `.idata`, `.edata`, `.reloc`, `.rsrc` 섹션을 가진다. 공통적으로 Delphi 런타임, Apollo DBF 엔진 및 내부 모듈 `ST_COM_V.CPM`, `ST_FUNC_C.CPM`, `DP_COMMON.CPM`을 사용한다.
|
||||
|
||||
| 모듈 | 확인된 export | 내부 근거로 확인된 역할 | 상태 |
|
||||
|---|---|---|---|
|
||||
| `ST_Boq.CPM` | `Edit_Boq`, `Edit_Bqm`, `Add_From_MRG`, `Calc_Kanjub` | BOQ 편집, 병합 입력, 간접비 호출, BDQTY/COSTN/PRJLST 접근 | 내부구조 추출 |
|
||||
| `ST_EdiLWi.CPM` | `Edit_iLW`, `Calc_Kanjub` | 일위대가 편집, BDQTY 수량·할증 필드 접근 | 내부구조 추출 |
|
||||
| `ST_KanJub.CPM` | `Edit_KanJub`, `Edit_KanJub_K`, `Edit_Kan_Rate`, `Prn_Kan_Rate`, `Calc_Kanjub` | 제비율 편집·인쇄·계산 | 내부구조 추출 |
|
||||
| `ST_SanCal.CPM` | `Calq_San`, `Calc_Kanjub` | 단가산출, `GET_ROUND_CUT`, `RoundTo`, 성분 소계 | 내부구조 추출 |
|
||||
| `STmateQty8.CPM` | `ST_QTY`, `FIND_QCODE`, `Free_Qty`, `Calc_Kanjub` | QTY0~QTY9, IFORM, Excel 수량 가져오기, BOQ 전송 | 내부구조 추출 |
|
||||
| `ST_P_Joong.CPM` | `Prn_Joong*`, `Calc_Kanjub` | 중기 재료·노무·경비, HalM/HalL/HalS 출력 | 내부구조 추출 |
|
||||
| `ST_P_Collect.CPM` | `Prn_Col_Dan*`, `Prn_Col_X*`, `Calc_Kanjub` | 단가·중기 자원 역집계, `CON_QTY11.DBF` | 내부구조 추출 |
|
||||
| `ST_P_ConEst.CPM` | `Prn_Nae_*`, `Prn_Jab_K`, `Calc_Kanjub` | 원가·내역 출력, BOQ11 연동 | 내부구조 추출 |
|
||||
| `ST_U_ChgAmt.CPM` | `CHANGE_AMT`, `Calc_Kanjub` | COSTN/COSTA 단가 일괄변경 | 내부구조 추출 |
|
||||
| `ST_U_ChgCode.CPM` | `Chg_Code_*`, `Exec_Change_Code`, `Exec_Replace_Code`, `Tran_SAN_Format` 등 | 코드 일괄·선택 변경, Excel 왕복, CH_CODE 임시표 | 내부구조 추출 |
|
||||
| `ST_U_ConBD.CPM` | `CON_QTY`, `CON_QTY9`, `CONBD_USD_QTY`, `Calc_Kanjub` | BOQ↔BDQTY 수량 연결, 행유형 필터 | 내부구조 추출 |
|
||||
| `ST_U_Convert.CPM` | `Conv_BOQ`, `Conv_Equip`, `Conv_ST8_XML`, `Conv_XML_ST8`, `MK_SET_REP` 등 | 구형/ST8/XML/조달 형식 변환, RATE/JB 복사 | 내부구조 추출 |
|
||||
|
||||
## 3. 바이너리에서 새로 확정된 계산 규칙
|
||||
|
||||
### 단가 성분 관계 — A등급
|
||||
|
||||
세 모듈에서 다음 검증 조건식이 그대로 발견됐다.
|
||||
|
||||
```text
|
||||
TAMT1 <> MAMT1 + LAMT1 + SAMT1
|
||||
```
|
||||
|
||||
또한 자원 유형별 검증도 존재한다.
|
||||
|
||||
```text
|
||||
CODE="L" → LAMT1 = TAMT1
|
||||
CODE="M" → MAMT1 = TAMT1
|
||||
CODE="S" → SAMT1 = TAMT1
|
||||
복합/W 코드 → TAMT1 = MAMT1 + LAMT1 + SAMT1
|
||||
```
|
||||
|
||||
따라서 `TAMT`는 네 번째 비용 성분이 아니라 총계다. 기존 05·07·09 문서를 이에 맞게 정정했다.
|
||||
|
||||
### 변경설계 산식 템플릿 — 코드 존재 A, 항목 대응 B
|
||||
|
||||
12개 모듈 공통 코드에 다음 두 템플릿이 포함돼 있다.
|
||||
|
||||
```text
|
||||
C_AMT_B + ROUNDDOWN(C_AMT_B × ((C_TAMT_C-C_TAMT_B)/C_TAMT_B), 자리)
|
||||
C_AMT_B + ROUNDDOWN((C_TAMT_C-C_TAMT_B) × RAT_1, 자리)
|
||||
```
|
||||
|
||||
이는 변경금액을 무조건 전체 재계산하는 것이 아니라 당초 확정액에 기초금액 증감률 또는 증감액×요율을 더하는 두 경로가 있음을 확인한다.
|
||||
|
||||
### 간접비 산정기초 템플릿 — A등급
|
||||
|
||||
바이너리에 다음 원형이 존재한다.
|
||||
|
||||
```text
|
||||
ROUNDDOWN(LAMT × RAT_1, 자리)
|
||||
ROUNDDOWN((L_AMT + LAB) × RAT_1, 자리)
|
||||
ROUNDDOWN(C_KUNBO × RAT_1, 자리)
|
||||
ROUNDDOWN(T_AMT × RAT_1, 0)
|
||||
ROUNDDOWN((T_AMT - M_AMT) × RAT_1, 0)
|
||||
ROUNDDOWN(M_AMT × RAT_1, 0)
|
||||
ROUNDDOWN((M_AMT + L_AMT + S_AMT ...) × RAT_1, ...)
|
||||
ROUND/ROUNDDOWN/ROUNDUP(C_AD_AMT, -자리)
|
||||
```
|
||||
|
||||
XLSX에서 관찰한 산식이 출력물에 우연히 생긴 것이 아니라 STmate 공통 계산 코드에 들어 있는 템플릿임을 확인했다.
|
||||
|
||||
### 복합 보험성 산식 — B등급
|
||||
|
||||
다음 중첩 절사식도 확인됐다.
|
||||
|
||||
```text
|
||||
ROUNDDOWN(
|
||||
ROUNDDOWN(LAMT×RAT_1×RAT_2/100×1.4,0)
|
||||
+ ROUNDDOWN(LAMT×RAT_KY×RAT_3/100,0)
|
||||
+ ROUNDDOWN(LAMT×RAT_1×RAT_2/100×RAT_4/100,0),
|
||||
자리)
|
||||
```
|
||||
|
||||
각 항을 먼저 원 단위 절사한 뒤 합계를 다시 절사한다. `RAT_1..4`, `RAT_KY`의 정확한 비용항목 명칭 연결은 다음 단계에서 RATE/JB와 UI 라벨을 교차해야 한다.
|
||||
|
||||
## 4. 제비율 기준파일과 프로젝트 RATE의 관계
|
||||
|
||||
확장자와 실제 DBF 구조가 다음처럼 대응한다.
|
||||
|
||||
| 배포파일 | DBF 구조 | 보유 개수 | 프로젝트 테이블 |
|
||||
|---|---|---:|---|
|
||||
| `JB_1` | 41행 × 10열 | 1 (2011) | `RATE.DBF` |
|
||||
| `JB_M` | 25행 × 10열 | 1 (2007) | `RATE_M.DBF` |
|
||||
| `JB_R` 구형 | 50행 × 20열 | 9 (2015 ~ 2020-01-02) | `RATE_R.DBF` |
|
||||
| `JB_R` 신형 | 50행 × 35열 | 18 (2020-05-27 ~ 2026) | `RATE_R.DBF` |
|
||||
| `JB_RA` | 50행 × 20열 | 13 (2015 ~ 2026, **확장 없음**) | `RATE_RA.DBF` |
|
||||
|
||||
**열 확장 시점은 2020-05-27로 확정됐다.** `2020(상_0102)`이 20열, `2020(상_0527)`이 35열이다. 두 파일의 **1~20열은 500개 셀 중 한 개도 다르지 않다.** 즉 확장은 기존 열을 재배치하지 않고 **뒤에 덧붙인 것**이다. 구형 파일을 읽을 때 열 의미를 바꿔 해석할 필요가 없다.
|
||||
|
||||
늘어난 15개 열 가운데 실제로 값이 들어가는 것은 **21, 26, 27, 28, 31** 다섯 개뿐이고, 값이 놓이는 행은 `0~3, 5~8, 10~13, 15~18`과 `48`이다. 2020-05-27은 **국가유산(문화재수리) 공종이 제비율에 들어온 시점**이며, 화면 `rGrd_Rate_R1`의 간접노무비·기타경비가 각각 `국가유산` 열을 하나씩 더 가지는 것과 개수가 맞는다.
|
||||
|
||||
### 행렬의 물리 배치 — 실측
|
||||
|
||||
- **행은 5행 묶음**이다. 묶음 시작은 `0, 5, 10, 15, 20, 25, 30, 35, 40, 45`이고, 각 묶음은 **값 4행 + 빈 1행**으로 끝난다. 화면 `rGrd_Rate_R1`(20행)·`rGrd_Rate_RA1`(20행)이 `5 공사규모 × 4 공사기간`인 것과 맞는다.
|
||||
- **열 1~5 = 간접노무비**, **열 6~10 = 기타경비**이며 둘 다 `토목 / 산.설(토목) / 조경 / 건축 / 산.설(건축)` 다섯 공종 순이다. 신형의 **열 21 = 국가유산 간접노무비**, **열 26 = 국가유산 기타경비**다.
|
||||
- 열 17·18은 짝으로 움직인다(예: `2.28 / 4325`, `2.53 / 3300`). **율과 기초액 한 쌍**으로 읽는 것이 맞다.
|
||||
- `ST_KanJub.CPM`에는 `RATE_nn` 리터럴이 22개뿐이고 `'RATE_'`·`'RATE'`·`'00'` 조각이 따로 있다. 열 접근은 **문자열을 실행 중에 조립**하므로, 시작 좌표를 코드 상수로 읽어낼 수는 없다.
|
||||
|
||||
레코드 전체가 정확히 일치한 실제 사례도 확인됐다.
|
||||
|
||||
- 영월 2024: `RATE=2011 JB_1`, `RATE_M=2007 JB_M`, `RATE_R=2023-01-02 JB_R`, `RATE_RA=2023 계열 JB_RA`
|
||||
- 영덕 2025: `RATE=2011 JB_1`, `RATE_M=2007 JB_M`, `RATE_R=2025-01-01 JB_R`
|
||||
- 봉화 2024: `RATE_R=2024-03-15 JB_R`
|
||||
- 울진 2024: `RATE_R=2020-05-27 JB_R`
|
||||
|
||||
일치하지 않는 프로젝트 표도 있으며, 이는 사용자 수정·구버전 설치자료·후속 기준 갱신 중 하나다. 특히 소광 2025의 `RATE_R`은 2025-01-01 기준과 5개 셀만 다르다. 따라서 프로젝트 RATE는 단순 외래키가 아니라 기준파일을 복사한 뒤 수정 가능한 스냅샷이다.
|
||||
|
||||
## 5. 다음 분석 순서
|
||||
|
||||
1. 12개 CPM의 전체 계산 문자열과 Delphi 폼 리소스를 기능별로 분리한다.
|
||||
2. `ST_KanJub.CPM`의 UI 라벨과 `RAT_*` 자리표시자를 연결한다.
|
||||
3. 43개 JB 파일을 연도별 셀 차이로 비교해 행·열 의미를 역추적한다.
|
||||
4. BSC/CSTA의 DBF 스키마와 레코드 난독화 범위를 판정한다.
|
||||
5. STBSC는 정상 프로그램 로드 전후 생성 파일을 관찰한다.
|
||||
6. BDQTY·자재채택·변경설계의 통제실험을 수행한다.
|
||||
|
||||
## 6. 산출 원자료
|
||||
|
||||
- `system_inventory.csv`: 186개 파일 경로·크기·해시·실제 형식·상태
|
||||
- `core_cpm_static.json`: 핵심 CPM 12개의 PE/import/export/도메인 문자열
|
||||
- `core_formula_strings.json`: 바이너리에서 검출한 계산식과 포함 모듈
|
||||
|
||||
세 파일은 분석 재현용이며 원본 `System` 폴더에는 아무것도 쓰지 않았다.
|
||||
@@ -0,0 +1,399 @@
|
||||
# STmate CPM 계산 코어 정적 분석
|
||||
|
||||
분석일: 2026-09-13
|
||||
대상: `C:\경영정보(주)\System` 186개 파일
|
||||
목적: 화면과 예시 금액을 복제하는 것이 아니라, 같은 입력에서 같은 중간값과 결과를 만드는 계산 엔진을 재구현하기 위한 명세
|
||||
|
||||
## 1. 분석 범위와 판정 기준
|
||||
|
||||
186개 파일 전부를 SHA-256과 실제 파일 헤더로 식별했다. 파일명 확장자가 아니라 내부 형식으로 분류했으며, PE 모듈은 섹션·가져오기·내보내기·문자열, DBF 계열은 헤더·필드·레코드 상태, ZIP은 엔트리와 암호화 여부까지 읽었다.
|
||||
|
||||
계산 핵심 12개 CPM은 export 진입점을 역어셈블하고, 코드가 참조하는 산식 문자열·필드명·화면 설명을 교차 확인했다. 다음 세 등급을 사용한다.
|
||||
|
||||
- **A — 직접 확인**: DBF 스키마/레코드, 모듈 export, 실행코드가 참조하는 산식 문자열이 일치한다.
|
||||
- **B — 강한 추론**: 모듈·필드·화면 설명이 일치하지만 입력/출력 쌍을 동적 실행으로 재현하지 못했다.
|
||||
- **C — 미확정**: 암호화 기준자료 또는 화면 상태가 필요해 정적 자료만으로 경계를 확정할 수 없다.
|
||||
|
||||
정적 분석은 타사 프로그램을 수정하지 않았고 암호화 자료의 암호를 추측하거나 우회하지 않았다.
|
||||
|
||||
## 2. 계산 책임 모듈
|
||||
|
||||
| 모듈 | 확인된 진입점/문자열 | 재구현 책임 | 등급 |
|
||||
|---|---|---|---|
|
||||
| `DP_Common.CPM` | `CALC_STR`, `CALC_STR_PART`, `GET_ROUND_CUT`, `MROUNDUD`, `MROUNDUP`, `MTRUNC`, `MINT` | 식 평가, 자리수 절사·올림·반올림 | A |
|
||||
| `ST_Func_C.CPM` | `CON_TOT`, `SUM_TOT`, `ZERO_TOT`, `PCN_TO_T`, `OPTIMIZ` | 조건별 집계, 0 정리, 총계 정규화 | A/B |
|
||||
| `ST_KanJub.CPM` | `Calc_Kanjub`; 간접비 공식 템플릿과 적용기준 편집 문구 | 제비율 선택, 간접비 순차 계산, 추가금액·끝전조정 | A |
|
||||
| `ST_SanCal.CPM` | `Calq_San`; `GET_ROUND_CUT`; `ROUND(2.45,1)=2.5`, `ROUNDDOWN(2.45,1)=2.4` | 단가산출/일위대가의 재료·노무·경비 산정 | A |
|
||||
| `ST_Boq.CPM` | 합계 불일치 검사, 수량 소수부 검사, `%` 단위 처리 | 내역서 행 계산과 검증 | A |
|
||||
| `ST_EdiLWi.CPM` | `TAMT=MAMT+LAMT+SAMT` 검증 | 내역 편집 시 성분 합계 불변조건 | A |
|
||||
| `ST_EdSan.CPM` | `Edit_EQ_SYNC`, `Edit_SanKun` | 산출근거/장비 동기화와 편집 | B |
|
||||
| `ST_Jabpum.CPM` | `Edit_Dan`, `Edit_Strg`, `Edt_Hal_Amt`, `Edt_Hal_Rate` | 품·할증·단가 편집 | B |
|
||||
| `STmateQty8.CPM` | `QTY0`~`QTY9`, 이름/코드로 BOQ 전송 | 외부 수량의 내역 연결 | B |
|
||||
| `ST_U_ChgAmt.CPM` | 변경금액 공식 문자열 | 당초/변경 금액 재계산 | A |
|
||||
| `ST_U_ChgCode.CPM` | 전체·추가·선택 코드 변경, Excel 왕복 | 코드 치환 후 종속 계산 갱신 | B |
|
||||
| `ST_U_ConBD.CPM` | `BOQ`, `BDQTY`, 코드·수량 필터 문자열 | 공종 수량을 내역으로 전개 | B |
|
||||
|
||||
같은 공식 문자열이 12개 CPM에 반복되어 있다. 이는 서로 다른 계산식 12개가 아니라 공통 계산 단위가 Delphi 정적 링크 과정에서 여러 모듈에 포함된 흔적으로 판단한다. 재구현에서는 모듈마다 복제하지 않고 하나의 계산 코어로 모아야 한다.
|
||||
|
||||
## 3. 금액 필드 불변조건
|
||||
|
||||
실행 모듈 세 곳에서 같은 검증식이 발견됐다.
|
||||
|
||||
```text
|
||||
TAMT = MAMT + LAMT + SAMT
|
||||
```
|
||||
|
||||
- `TAMT`: 총금액
|
||||
- `MAMT`: 재료비
|
||||
- `LAMT`: 노무비
|
||||
- `SAMT`: 경비
|
||||
- 접미사 `1`, `2`: 당초/변경 또는 비교 안의 병렬 슬롯
|
||||
|
||||
코드별 추가 불변조건도 있다.
|
||||
|
||||
```text
|
||||
CODE="L" → LAMT=TAMT
|
||||
CODE="M" → MAMT=TAMT
|
||||
CODE="S" → SAMT=TAMT
|
||||
CODE="W" 또는 복합행 → TAMT=MAMT+LAMT+SAMT
|
||||
```
|
||||
|
||||
따라서 총액을 네 번째 독립 비목으로 저장하거나 계산하면 안 된다. 총액은 세 성분의 합이며, 단일 비목행에서는 해당 성분만 총액과 같고 나머지는 0이다. **등급 A**.
|
||||
|
||||
## 4. 공통 계산 함수의 의미
|
||||
|
||||
### 4.1 식 평가
|
||||
|
||||
`CALC_STR`/`CALC_STR_PART`가 식 문자열을 평가한다. `Calc_Kanjub`는 `[LAMT]`, `[RAT_1]` 같은 자리표시자가 포함된 식 템플릿을 만들고, 실제 금액·요율·옵션에 따라 치환한 뒤 평가하는 구조다.
|
||||
|
||||
재구현 계약:
|
||||
|
||||
```text
|
||||
evaluate(template, named_values, options)
|
||||
1. 선택 토큰([+LAB], [+ANJ_B] 등)을 옵션에 따라 포함/제거
|
||||
2. [이름]을 Decimal 값으로 치환
|
||||
3. 허용된 산술·비교·ROUND 계열만 평가
|
||||
4. 임의 코드 실행은 금지
|
||||
5. 사용한 원본 템플릿과 치환값을 계산 추적에 보존
|
||||
```
|
||||
|
||||
### 4.2 반올림 계열
|
||||
|
||||
#### 임포트 실측 — 무엇이 실제로 쓰이는가
|
||||
|
||||
`C:\경영정보(주)\System`의 PE 186개 전수를 훑어 수치 런타임 심볼의 실제 임포트를 셌다.
|
||||
|
||||
| 런타임 심볼 | STmate 모듈이 임포트하나 | 뜻 |
|
||||
|---|---|---|
|
||||
| `@Math@RoundTo$qqrxdxzc` | **예 — 17개 모듈** (`ST_KanJub`, `ST_Boq`, `ST_SanCal`, `ST_EdiLWi`, `ST_EdSan`, `ST_P_Joong`, `ST_Prt`, `ST_P_iLWi`, `ST_Jabpum`, `ST_FindUse`, `ST_Sys`, `STmate.Cpm`, `ST_U_Convert`, `ST_U_ChgCode`, `ST_U_DelCode`, `ST_P_XLS` 등) | 금액 반올림의 실제 원시함수 |
|
||||
| `@Math@SimpleRoundTo$qqrxdxzc` | **아니오** — `RTL60.BPL`만 내보냄 | 0에서 멀어지는 변형을 **쓰지 않음** |
|
||||
| `@Math@Ceil$qqrxg` | **아니오** — `RTL60.BPL`만 | `ROUNDUP`을 `Ceil`로 만들지 않음 |
|
||||
| `@Math@Floor$qqrxg` | 예 — 18개 모듈, 단 호출은 모듈당 2곳 | 금액 절사 주경로가 아님 |
|
||||
| `@Math@SetRoundMode` / `GetRoundMode` | **아니오** — `RTL60.BPL`만 | FPU 반올림 모드를 **바꾸지 않음** |
|
||||
| `Set8087CW` / `Default8087CW` / `_control87` | **아니오** — `RTL60.BPL`만 | 같음 |
|
||||
|
||||
핵심 계산 모듈 6개(`ST_KanJub`, `ST_SanCal`, `ST_Boq`, `ST_EdiLWi`, `ST_P_Joong`, `ST_Prt`)에서 임포트 썽크로 들어가는 `E8 rel32` 호출을 세면 **모듈마다 정확히 `RoundTo` 12곳, `Floor` 2곳**으로 같다. 공용 계산 유닛이 각 CPM에 정적 링크된 구조다.
|
||||
|
||||
`ST_KanJub.CPM`의 `0x0043A240`~`0x0043A3DF` 한 함수 안에 `RoundTo` 호출 12곳이 모여 있고, 호출 앞뒤에 `FCHS`(부호반전)나 `FABS`(절대값)가 **하나도 없다.** 즉 음수를 뒤집어 양수로 처리하는 흉내가 없다. 자리수는 `F6 D8`(`neg al`)로 부호만 뒤집어 `RoundTo`의 `ADigit`에 넘긴다.
|
||||
|
||||
#### 이로부터 확정되는 것 — A등급
|
||||
|
||||
Delphi 6 `Math.RoundTo(v, ADigit)`는 `Round(v / 10^ADigit) * 10^ADigit`이고, `Round`는 **FPU 현재 반올림 모드**를 따른다. STmate가 모드를 바꾸는 호출을 전혀 하지 않으므로 모드는 Delphi 기본값인 **최근접-짝수(은행가 반올림)**로 고정이다. 음수 전용 분기도 없으므로 **0을 기준으로 대칭**이다.
|
||||
|
||||
| 원 함수 | 실제 구현 | 음수 | 정확히 절반 | 등급 |
|
||||
|---|---|---|---|---|
|
||||
| `MROUNDUD`/`ROUND` | `Math.RoundTo` → FPU 최근접-짝수 | 대칭 | **짝수 쪽**(사사오입 아님) | **A** |
|
||||
| `MTRUNC`/`ROUNDDOWN` | `Ceil`·`Floor` 미사용, Delphi `Trunc` 관용구(`FNSTCW`→`FLDCW`→`FISTP`→복원) | **0 방향** (Excel과 같음) | 해당없음 | B |
|
||||
| `MROUNDUP`/`ROUNDUP` | `Ceil` 미사용 → 절사 후 나머지 보정 계열 | 미확정 | 해당없음 | B |
|
||||
| `MINT` | `DP_Common.CPM`에만 존재 | 미확정 | 해당없음 | C |
|
||||
| `GET_ROUND_CUT` | `DP_Common`·`ST_SanCal`·`ST_U_Convert` — 설정에 따라 위를 고름 | — | — | B |
|
||||
|
||||
#### 경계표 — 내부 계산과 STmate 가 뽑은 XLSX 가 갈리는 자리
|
||||
|
||||
STmate 출력 XLSX 안의 수식은 **Excel** `ROUND`(0에서 멀어지는 사사오입)이고, 내부 Delphi 계산은 위처럼 **최근접-짝수**다. 같은 값이 두 곳에서 다르게 나온다.
|
||||
|
||||
| 입력 | 자리 | STmate 내부 `ROUND` | Excel `ROUND` (출력 XLSX) | 일치 |
|
||||
|---:|---:|---:|---:|:--:|
|
||||
| 2.44 | 소수1 | 2.4 | 2.4 | ○ |
|
||||
| 2.45 | 소수1 | **2.4** | 2.5 | ✗ |
|
||||
| 2.46 | 소수1 | 2.5 | 2.5 | ○ |
|
||||
| -2.45 | 소수1 | **-2.4** | -2.5 | ✗ |
|
||||
| 0.5 | 원 | **0** | 1 | ✗ |
|
||||
| 1.5 | 원 | 2 | 2 | ○ |
|
||||
| 2.5 | 원 | **2** | 3 | ✗ |
|
||||
| 3.5 | 원 | 4 | 4 | ○ |
|
||||
| -2.5 | 원 | **-2** | -3 | ✗ |
|
||||
| -3.5 | 원 | -4 | -4 | ○ |
|
||||
| 1250 | 100원 | **1200** | 1300 | ✗ |
|
||||
| 1350 | 100원 | 1400 | 1400 | ○ |
|
||||
| 2500 | 1000원 | **2000** | 3000 | ✗ |
|
||||
| 3500 | 1000원 | 4000 | 4000 | ○ |
|
||||
| -2500 | 1000원 | **-2000** | -3000 | ✗ |
|
||||
|
||||
시험한 20개 경계 중 **10개가 갈린다**. 재현 스크립트는 이 폴더의 `round_table.py`(경계표), `rounding_symbol_matrix.py`(임포트 실측), `rounding_callsites.py`(썽크 호출 집계), `rounding_dispatcher_dump.py`(함수 바이트 덤프)다.
|
||||
|
||||
`2.45`가 소수1자리에서 `2.4`가 되는 이유는 두 겹이다. ① 은행가 반올림이고 ② `RoundTo`가 `IntPower(10,-1)`로 만든 `0.1`이 이진수로 정확하지 않아 `2.45/0.1`이 `24.4999…`가 된다. 따라서 **STmate 호환 모드는 `Decimal` 사사오입으로 흉내 낼 수 없고, 배정밀도 나눗셈까지 그대로 재현해야 한다.**
|
||||
|
||||
#### 재구현 지침
|
||||
|
||||
- Aislo 자체 계산은 금액형을 `Decimal`로 두고 정책을 명시한다. 자리수 `0`은 원 단위, 음수는 십·백·천원 단위 조정으로 해석한다.
|
||||
- **STmate 결과와 원 단위까지 맞춰야 하는 자리**에서는 위 배정밀도 경로를 그대로 흉내 내는 `stmate_round(value, digit)`를 따로 두고, 어느 경로로 계산했는지 계산 추적에 남긴다.
|
||||
- 내부 계산값과 출력 XLSX 값이 원 단위에서 다를 수 있다는 점을 검증 기준에 반영한다. 둘이 다르다고 곧 오류가 아니다.
|
||||
|
||||
## 5. 직접비 계산
|
||||
|
||||
### 5.1 단가와 금액
|
||||
|
||||
각 행은 재료·노무·경비 단가를 별도로 가지며 총단가는 세 성분의 합이다. 내역 금액은 보유 XLSX와 실행모듈 양쪽에서 다음 계약으로 확인된다.
|
||||
|
||||
```text
|
||||
component_amount = ROUNDDOWN(quantity × component_unit_price, configured_digits)
|
||||
total_amount = material_amount + labor_amount + expense_amount
|
||||
```
|
||||
|
||||
자원·일위대가·중기 집계 출력에는 `ROUND(quantity×unit_price,0)`가 나타나므로, 내역 행의 절사와 집계표의 반올림을 하나로 합치면 안 된다. 계산 자리마다 `rounding_policy`를 명시해야 한다.
|
||||
|
||||
### 5.2 수량 표시와 계산값
|
||||
|
||||
`ST_Boq.CPM`은 비목 코드가 `M/L/S/W/B/D/X/A` 계열이고 수량 문자열의 소수 마지막 자리가 0이 아닌지 검사하며, `%` 단위행은 별도로 취급한다. 이는 화면 표시 소수점과 내부 계산 수량을 구분하고 있음을 뜻한다. 저장 모델에는 최소한 원수량, 계산수량, 표시자리수를 분리한다. **등급 B**.
|
||||
|
||||
## 6. 간접비 공식 템플릿
|
||||
|
||||
실행코드가 직접 참조하는 템플릿은 다음 계열이다. 대괄호 이름은 런타임 치환 변수다.
|
||||
|
||||
```text
|
||||
ROUNDDOWN([LAMT] * [RAT_1], digits)
|
||||
ROUNDDOWN(([L_AMT][+LAB]) * [RAT_1], digits)
|
||||
ROUNDDOWN([T_AMT] * [RAT_1], digits)
|
||||
ROUNDDOWN(([M_AMT]+[L_AMT]+[S_AMT]...) * [RAT_1], digits)
|
||||
ROUNDDOWN([M_AMT] * [RAT_1], 0)
|
||||
ROUNDDOWN(([T_AMT]-[M_AMT]) * [RAT_1], 0)
|
||||
ROUNDDOWN(([TAMT]-[LAMT][+ANJ_B][+PMJ_B]+[B_AD_AMT]) * [RAT_1], 0)
|
||||
```
|
||||
|
||||
기본형은 모두 **이미 계산된 밑수 × 요율을 지정 자리에서 절사**한다. `[+...]` 토큰은 특정 항목을 산정기초에 더하거나 빼는 옵션이다. 한 항목의 결과가 다음 항목의 밑수에 들어가므로 순서를 보존해야 한다.
|
||||
|
||||
### 6.1 복합 보험/부담금형
|
||||
|
||||
다음 중첩식도 코드에 포함돼 있다.
|
||||
|
||||
```text
|
||||
ROUNDDOWN(
|
||||
ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*1.4, 0)
|
||||
+ ROUNDDOWN([LAMT]*[RAT_KY]*[RAT_3]/100, 0)
|
||||
+ ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*[RAT_4]/100, 0),
|
||||
digits
|
||||
)
|
||||
```
|
||||
|
||||
각 부분을 먼저 원 단위 절사한 뒤 합계를 다시 절사한다. 합계를 한 번에 계산한 뒤 마지막에만 절사하면 결과가 달라진다. 변수의 화면상 정확한 항목 매핑은 C등급이지만 **부분별 절사 순서 자체는 A등급**이다.
|
||||
|
||||
### 6.2 확인된 간접비 항목 사전
|
||||
|
||||
원가계산 모듈에는 다음 항목이 명시돼 있다.
|
||||
|
||||
- 간접노무비, 주휴수당
|
||||
- 산재보험료, 고용보험료, 건강보험료, 노인장기요양보험료, 연금보험료
|
||||
- 퇴직공제부금비, 산업안전보건관리비, 건설공사 안전점검비
|
||||
- 기타경비, 수도광열비, 복리후생비, 소모품·사무용품비, 여비·교통비·통신비, 세금·공과금, 시공도작성비
|
||||
- 환경보전비, 석면피해구제분담금, 임금채권부담금
|
||||
- 공사이행보증수수료, 하도급대금지급보증수수료, 건설기계대여금지급보증서수수료
|
||||
- 고용개선지원비(PS), 고용개선 건강·연금, 고용개선장려금
|
||||
- 공사보험료/공사손해보험료, 일반관리비, 이윤, 부가가치세
|
||||
|
||||
명칭이 존재한다고 모든 프로젝트에 자동 적용되는 것은 아니다. 항목별 `enabled`, `rate_source`, `base_components`, `rounding`, `manual_override`, `stage`를 저장해야 한다.
|
||||
|
||||
## 7. 산업안전보건관리비 선택식
|
||||
|
||||
화면 설명과 계산 템플릿에서 다음 선택식이 확인된다.
|
||||
|
||||
```text
|
||||
(직·간재 + 직노 + (관급+사급)/1.1 - 내역서안전관리비) × rate
|
||||
(재료비 + 직노 + (관급+사급)/1.1 - 내역서안전관리비) × rate
|
||||
(직·간재 + 직노 + 관급/1.1 + 사급재 - 내역서안전관리비) × 3.15% + 기초액
|
||||
```
|
||||
|
||||
세 번째 계열은 다음 비교·상한/하한 선택을 동반한다.
|
||||
|
||||
```text
|
||||
기준식 결과
|
||||
vs
|
||||
((직·간재 + 직노 - 내역서안전관리비) × 3.15% + 기초액) × 1.2
|
||||
```
|
||||
|
||||
기존 안전관리비를 차감하지 않는 변형도 존재한다. 관급·사급 처리 옵션은 다음 문자열로 확인된다.
|
||||
|
||||
- `관급자재대/1.1`, `관급자재대`
|
||||
- `사급자재대/1.1`, `사급자재대`
|
||||
- `관급/1.1+사급재`
|
||||
- `관급재+사급/1.1`
|
||||
- `관급재+사급재`
|
||||
- `(관급+사급)/1.1`
|
||||
|
||||
따라서 안전관리비는 고정 공식 하나가 아니라 기준연도·공사종류·재료 공급형태·기존 계상액 포함 여부에 따른 정책 선택이다. 공식 선택 결과와 선택 근거를 결과에 저장해야 한다. 정확한 선택 조건은 RATE 좌표/화면 대조가 필요한 C등급이다.
|
||||
|
||||
## 8. 요율 선택 축과 순환 의존성
|
||||
|
||||
모듈이 제시하는 선택 축은 다음과 같다.
|
||||
|
||||
- 발표기관/기준군: 조달청, 건기연, 행자부
|
||||
- 편집군: 기본제비율, 실적공사비 적용요율, 행자부기준
|
||||
- 공사종류: 일반·산업설비·조경·건축, 토목·중건설·특수건설 등
|
||||
- 공사기간: 30일 미만, 6개월 미만/이하, 7~12개월, 13~36개월, 37개월 이상 등
|
||||
- 공사규모: 5천만·3억·5억·10억·30억·50억·70억·100억·120억·250억·300억·500억·1,000억·800억 등의 기준군별 구간
|
||||
- 공사등급: 1~6등급, 7등급 이하
|
||||
|
||||
원 프로그램은 “공사규모에 따른 제비율 변경으로 공사규모가 계속 변경되는 무한루프”가 발생할 수 있다고 명시하고, 일반관리비·이윤 등의 요율을 사용자가 직접 수정하도록 안내한다.
|
||||
|
||||
따라서 재구현의 안전한 계약은 다음과 같다.
|
||||
|
||||
1. 요율 선택의 기준금액과 기준군을 입력값으로 고정한다.
|
||||
2. 자동 재선택은 최대 반복 횟수와 상태 이력으로 순환을 검출한다.
|
||||
3. 구간이 왕복하면 임의의 한쪽을 택하지 않고 사용자 확정 상태로 전환한다.
|
||||
4. 확정된 요율, 원 표 좌표, 사용자 수정 여부를 프로젝트 스냅샷에 보존한다.
|
||||
|
||||
## 9. 추가금액과 적용 단계
|
||||
|
||||
추가금액은 단순 메모가 아니라 총계 트리의 특정 위치에 삽입된다. 화면 문자열로 다음 단계가 확인됐다.
|
||||
|
||||
| 단계 | 삽입 위치 | 합계 영향 |
|
||||
|---:|---|---|
|
||||
| 3 | 일반관리비 직전 | 순공사원가의 경비에 포함 |
|
||||
| 4 | 이윤 이후~공급가액 전 | 공급가액에 포함 |
|
||||
| 5 | 도급공사비 직전 | 도급공사비에 포함 |
|
||||
| 6 | 관급자재~총공사비 구간 | 총공사비에 포함 |
|
||||
|
||||
별도로 “이윤 이후(공사원가 포함)”, “사급자재비 이후” 위치가 존재한다. 산출경비 직접비에 이미 포함된 금액은 경비금액에 다시 계상하지 않으며, 기성내역서 등록 시 삭제되고 XML 호환규정 변환 대상이 아니라는 경고도 있다.
|
||||
|
||||
추가금액은 `amount`, `component`, `insertion_stage`, `include_in_cost_base`, `source`, `conversion_policy`를 가진 독립 행으로 구현해야 한다. **등급 B**.
|
||||
|
||||
## 10. 변경설계 공식
|
||||
|
||||
코드에 두 가지 공식이 직접 포함돼 있다.
|
||||
|
||||
```text
|
||||
방법 A = C_AMT_B
|
||||
+ ROUNDDOWN(C_AMT_B × ((C_TAMT_C-C_TAMT_B)/C_TAMT_B), digits)
|
||||
|
||||
방법 B = C_AMT_B
|
||||
+ ROUNDDOWN((C_TAMT_C-C_TAMT_B) × RAT_1, digits)
|
||||
```
|
||||
|
||||
- `C_AMT_B`: 해당 간접비의 당초금액
|
||||
- `C_TAMT_B`: 기준 직접비/공사비의 당초 합계
|
||||
- `C_TAMT_C`: 같은 기준의 변경 합계
|
||||
- `RAT_1`: 항목 요율
|
||||
|
||||
방법 A는 당초금액에 전체 증감비율을 적용하고, 방법 B는 증감 기초금액에 항목 요율을 적용한다. 보험료 사후정산 항목은 당초금액과 동일하게 직접 입력하는 옵션도 있다. 직접입력 체크가 없으면 적용을 거부한다.
|
||||
|
||||
또 계약 제잡비율 적용 시 간접노무비부터 부가세 직전 항목의 변경분을 `직접비 변경분 × 계약 제잡비율`로 조정하는 기능이 있다. 재구현은 `recalculated`, `proportional_from_original`, `delta_times_rate`, `carry_original`, `manual`을 서로 다른 계산 모드로 저장해야 한다.
|
||||
|
||||
## 11. 끝전조정
|
||||
|
||||
코드에는 `[C_AD_AMT]`에 대한 다음 선택이 존재한다.
|
||||
|
||||
```text
|
||||
ROUND([C_AD_AMT], -digits)
|
||||
ROUNDDOWN([C_AD_AMT], -digits)
|
||||
ROUNDUP([C_AD_AMT], -digits)
|
||||
```
|
||||
|
||||
다른 산출물에서 확인된 흐름은 총액 차액을 세전 상당액과 부가세 조정으로 나눠 이윤 또는 조정행에 전달하는 방식이다. 따라서 다음 값을 분리 보존한다.
|
||||
|
||||
```text
|
||||
raw_amount
|
||||
rounded_amount
|
||||
adjustment_amount
|
||||
adjustment_target
|
||||
rounding_function
|
||||
rounding_digits
|
||||
```
|
||||
|
||||
`이윤 보정=0`은 “0원 이윤”이 아니라 이윤보정을 사용하지 않는 옵션이다. 0과 미설정을 구분해야 한다.
|
||||
|
||||
## 12. 공종·제외금액 분류
|
||||
|
||||
간접비 밑수에서 별도 취급하는 공종/금액 분류가 확인됐다.
|
||||
|
||||
- 일반공종
|
||||
- 안전관리비, 품질관리비
|
||||
- 미확정설계공종(PS)
|
||||
- 제비율 적용제외공종
|
||||
- 기존 시설물 철거·해체·이설 발생분
|
||||
- 작업설 등 감액 항목
|
||||
- 매입세
|
||||
- 직접공사비에 포함되지 않은 제외금액
|
||||
- 비과세자재대
|
||||
- 도급자설치/관급자설치 관급자재대
|
||||
- 도급자설치 관급에 포함된 조달수수료
|
||||
- 내역서 기존 안전관리비
|
||||
- 폐기물수수료, 골재사용료
|
||||
|
||||
자동 옵션은 내역서에 해당 공종구분을 가진 행이 없으면 선택되지 않는다. 사용자는 다른 내역 금액을 더블클릭으로 밑수에 불러올 수 있다. 즉 밑수는 단순 총계가 아니라 공종 태그로 필터한 합계와 수동 연결값의 조합이다.
|
||||
|
||||
## 13. 재구현 데이터 계약
|
||||
|
||||
```text
|
||||
RateStandard
|
||||
source, effective_date, standard_group, work_type
|
||||
amount_band, duration_band, grade, item_code
|
||||
rate, base_formula_id, rounding_policy_id
|
||||
|
||||
CostComponent
|
||||
material, labor, expense, total(derived)
|
||||
|
||||
IndirectCostRule
|
||||
item_code, ordered_stage, enabled
|
||||
base_component_codes[], include_tags[], exclude_tags[]
|
||||
formula_template, rate_ref, rounding_ref
|
||||
manual_override, adjustment_mode
|
||||
|
||||
CalculationTrace
|
||||
rule_id, source_record, source_coordinate
|
||||
template, substituted_values, raw_result
|
||||
rounding_function, digits, final_result
|
||||
user_override, override_reason
|
||||
```
|
||||
|
||||
모든 결과는 “어떤 기준자료의 어느 좌표 → 어떤 밑수 항목 → 어떤 요율 → 어떤 절사 → 어떤 결과”인지 역추적 가능해야 한다. 숫자만 저장하면 다음 연도 기준 변경과 변경설계 재현이 불가능하다.
|
||||
|
||||
## 14. 구현 순서
|
||||
|
||||
1. `Decimal` 기반 ROUND/ROUNDDOWN/ROUNDUP/MINT 호환 함수를 만든다.
|
||||
2. `TAMT=MAMT+LAMT+SAMT` 불변조건을 가진 직접비 행 계산기를 만든다.
|
||||
3. 간접비 규칙을 순서 있는 데이터로 정의하고 선택 토큰을 포함한 제한식 평가기를 만든다.
|
||||
4. 안전관리비 정책 변형과 관급·사급 VAT 처리 방식을 별도 전략 값으로 둔다.
|
||||
5. 추가금액 삽입단계와 제비율 제외 태그를 합계 트리에 적용한다.
|
||||
6. 당초/변경은 값 열 복제가 아니라 두 스냅샷과 계산모드로 처리한다.
|
||||
7. 끝전조정과 VAT 조정을 마지막 단계에서 수행하고 전후 값을 보존한다.
|
||||
8. 각 단계에 원본 좌표와 계산 추적을 남긴다.
|
||||
|
||||
## 15. 아직 동적 대조가 필요한 경계
|
||||
|
||||
다음은 숨기지 않고 미확정으로 남긴다.
|
||||
|
||||
1. `RATE`, `RATE_M`, `RATE_R`, `RATE_RA`의 각 행·열과 화면 항목의 완전한 좌표 매핑
|
||||
2. 금액·기간 구간 경계의 `<`/`<=` 방향
|
||||
3. ~~음수 및 정확히 절반인 값의 ROUND 처리~~ → **확정(A)**: 최근접-짝수·0 대칭 (§4.2). 남은 것은 `ROUNDUP`·`MINT`의 음수 방향과 `GET_ROUND_CUT`이 설정값을 어느 함수로 잇는지
|
||||
4. 각 안전관리비 변형을 선택하는 정확한 기준연도·공사종류 조건
|
||||
5. 복합 보험식의 `RAT_1`~`RAT_4`, `RAT_KY` 화면 항목명
|
||||
6. 추가금액 단계 번호 1·2 및 일부 특수 위치의 정확한 합계 영향
|
||||
7. `CODE=W/B/D/X/A`의 모든 세부 계산 분기
|
||||
|
||||
이 일곱 항목은 누락이 아니라 정적 파일만으로 단정하면 오히려 잘못된 구현이 되는 경계다. 동일 프로젝트 복제본에서 한 변수만 바꾸는 통제시험으로 확정해야 한다.
|
||||
|
||||
## 16. 재현 시험표
|
||||
|
||||
| 시험 | 입력 | 확인 결과 |
|
||||
|---|---|---|
|
||||
| 반올림 | ±2.44, ±2.45, ±2.46; digits 1,0,-1 | 네 함수별 기대값 |
|
||||
| 성분합 | M/L/S 단일행과 복합행 | 총액 불변조건 및 오류 차단 |
|
||||
| 중간절사 | 절사 전 소수부가 있는 3개 부분식 | 부분별 절사 후 합산 여부 |
|
||||
| 요율경계 | 각 금액·기간 경계 직전/동일/직후 | 선택 RATE 좌표 |
|
||||
| 요율순환 | 총액이 인접 구간을 왕복하는 값 | 순환 검출 및 수동확정 |
|
||||
| 안전비 | 관급/사급, VAT 포함, 기존 안전비 조합 | 선택식과 밑수 |
|
||||
| 추가금액 | 동일 금액을 단계 3~6에 각각 삽입 | 순공사원가·공급가액·도급액·총액 영향 |
|
||||
| 변경설계 | 당초/변경 직접비와 요율 독립 변경 | A/B/직접입력 방식별 결과 |
|
||||
| 끝전 | 양수·음수 차액, 백/천원 단위 | 이윤·VAT 조정 전달 |
|
||||
|
||||
이 표를 모두 통과해야 “비슷한 결과”가 아니라 계산 순서와 중간값까지 같은 프로그램이라고 판정할 수 있다.
|
||||
@@ -0,0 +1,408 @@
|
||||
# 원가계산 4형식과 RATE 표 구조 복원
|
||||
|
||||
분석일: 2026-09-13
|
||||
직접 근거: `ST_KanJub.CPM`의 Delphi 폼 리소스 `TWM_KANJUB`, `TWM_KAN_RATE`, `TPM_KAN_RATE`
|
||||
|
||||
## 1. 복원 방법
|
||||
|
||||
PE의 `RT_RCDATA`에 포함된 Delphi 6 바이너리 폼(`TPF0`)을 속성·자식 컨트롤·컬렉션 단위로 파싱했다. 이에 따라 단순 문자열 검색으로는 잃었던 다음 관계를 복원했다.
|
||||
|
||||
- 탭 이름과 계산 형식
|
||||
- 계산서에 표시되는 항목 순서
|
||||
- 각 항목 옆에 인쇄되는 산정기초 공식
|
||||
- 요율표 그리드 이름, 행 수, 열 제목
|
||||
- 자동계산 선택 입력과 추가금액 삽입 위치
|
||||
|
||||
이 문서의 화면 문구와 열 구조는 **A등급 직접 확인**이다. DBF 물리 좌표와 화면 셀의 완전한 연결은 일부 B/C등급이다.
|
||||
|
||||
## 2. 원가계산의 네 형식
|
||||
|
||||
`TWM_KANJUB`에는 네 탭이 있다.
|
||||
|
||||
| 내부 접미사 | 탭 | 의미 |
|
||||
|---|---|---|
|
||||
| `T` | 일반형식 | 일반 제비율 기반 원가계산 |
|
||||
| `S` | 수공형식 | 세부 경비 항목을 풀어 계산하는 수공식 |
|
||||
| `TA` | 실적일반 | 실적공사비 적용의 일반형식 |
|
||||
| `SA` | 실적수공 | 실적공사비 적용의 수공형식 |
|
||||
|
||||
네 형식은 출력 모양만 다른 것이 아니다. 직접공사비/순공사원가의 묶음, 보험료 밑수, 기타경비의 세분 정도, 일반관리비와 이윤의 밑수가 달라진다.
|
||||
|
||||
## 3. 일반형식 `T`
|
||||
|
||||
### 3.1 계산 순서
|
||||
|
||||
```text
|
||||
나. 순공사원가 = ㄱ + ㄴ + ㄷ
|
||||
ㄱ. 재료비
|
||||
ㄴ. 노무비
|
||||
1) 직접노무비
|
||||
2) 간접노무비 = 직접노무비 × rate
|
||||
ㄷ. 경비
|
||||
1) 내역서경비
|
||||
2) 산재보험료 = 노무비 × rate
|
||||
3) 고용보험료 = 노무비 × rate
|
||||
4) 건강보험료 = 직접노무비 × rate
|
||||
5) 장기요양보험료 = 건강보험료 × rate
|
||||
6) 연금보험료 = 직접노무비 × rate
|
||||
7) 퇴직공제부금비 = 직접노무비 × rate
|
||||
8) 산업안전보건관리비
|
||||
9) 안전관리비 = 별도내역
|
||||
10) 품질관리비 = 별도내역
|
||||
11) 기타경비 = (재료비 + 노무비) × rate
|
||||
12) 공사이행수수료 = 기초금액 + (직접공사비 - 기준규모) × rate
|
||||
13) 하도급대금지급보증수수료 = (재료비 + 직접노무비 + 산출경비) × rate
|
||||
14) 환경보전비 = (재료비 + 직접노무비 + 산출경비) × rate
|
||||
15) 건설기계대여금지급보증서수수료 = (재료비 + 직접노무비 + 산출경비) × rate
|
||||
16) 석면피해구제분담금 = 노무비 × rate
|
||||
17) 임금채권부담금 = 노무비 × rate
|
||||
5) 일반관리비 = (순공사원가 + 관리품목자재대) × rate
|
||||
다. 소계 = 순공사원가 + 일반관리비
|
||||
6) 이윤 = (소계 - 재료비) × 이윤율 - 보정액
|
||||
라. 공급가액
|
||||
부가가치세
|
||||
공사보험료
|
||||
작업설 등 감액
|
||||
사급자재대
|
||||
비과세자재대
|
||||
매입세
|
||||
마. 도급공사비
|
||||
관급자재대(도급자설치)
|
||||
사급자재대
|
||||
관급자재대(관급자설치)
|
||||
바. 총공사비
|
||||
```
|
||||
|
||||
### 3.2 고용개선지원비
|
||||
|
||||
일반형식에는 다음 별도 묶음이 존재한다.
|
||||
|
||||
```text
|
||||
고용개선지원비(PS단가) = 주휴수당 + 사회보험 + 장려금
|
||||
주휴수당 = 직접노무비 × RAT_1 × RAT_2 × 1.4(제경비)
|
||||
국민연금·건강보험료 = 직접노무비 × 관련률 × 관련률
|
||||
```
|
||||
|
||||
이는 11번 문서의 중첩 `ROUNDDOWN` 세 부분식과 일치한다. 각 부분은 원 단위 절사 후 합산된다.
|
||||
|
||||
## 4. 수공형식 `S`
|
||||
|
||||
### 4.1 계산 순서
|
||||
|
||||
```text
|
||||
1. 순공사원가 = 가 + 나 + 다
|
||||
가. 순공사비
|
||||
나. 간접노무비 = 직접노무비 × rate
|
||||
다. 경비
|
||||
1) 산재보험료 = (직접노무비 + 간접노무비) × rate
|
||||
2) 고용보험료 = (직접노무비 + 간접노무비) × rate
|
||||
3) 건강보험료 = 직접노무비 × rate
|
||||
4) 장기요양보험료 = 건강보험료 × rate
|
||||
5) 연금보험료 = 직접노무비 × rate
|
||||
6) 퇴직공제부금비 = 직접노무비 × rate
|
||||
7) 산업안전보건관리비
|
||||
8) 안전관리비 = 별도내역
|
||||
9) 품질관리비 = 별도내역
|
||||
10) 수도광열비 = (직노 + 간노 + 재료비) × rate
|
||||
11) 복리후생비 = 같은 밑수 × rate
|
||||
12) 소모품·사무용품비 = 같은 밑수 × rate
|
||||
13) 여비·교통비·통신비 = 같은 밑수 × rate
|
||||
14) 세금·공과금 = 같은 밑수 × rate
|
||||
15) 폐기물처리비 = 같은 밑수 × rate
|
||||
16) 도서인쇄비 = 같은 밑수 × rate
|
||||
17) 지급수수료 = 같은 밑수 × rate
|
||||
18) 공사이행수수료 = 기초금액 + (직접공사비 - 기준규모) × rate
|
||||
19) 하도급대금지급보증수수료 = (재료비 + 직노 + 산출경비) × rate
|
||||
20) 환경보전비 = (재료비 + 직노 + 산출경비) × rate
|
||||
21) 건설기계대여금지급보증서수수료 = 같은 밑수 × rate
|
||||
22) 석면피해구제분담금 = (직노 + 간노) × rate
|
||||
23) 임금채권부담금 = (직노 + 간노) × rate
|
||||
2. 일반관리비 = (순공사비 + 간접노무비 + 경비) × rate
|
||||
3. 이윤 = (순공사원가 + 일반관리비 - 재료비) × rate - 보정액
|
||||
4. 공급가액
|
||||
5. 도급공사비
|
||||
6. 총공사비
|
||||
```
|
||||
|
||||
시공도작성비는 `(순공사원가+일반관리비+이윤+사급비) × rate × 별도계수` 형식이다.
|
||||
|
||||
## 5. 실적일반 `TA`
|
||||
|
||||
실적일반은 직접비와 간접비를 먼저 나눈다.
|
||||
|
||||
```text
|
||||
1. 직접공사비
|
||||
2. 간접공사비
|
||||
1) 간접노무비 = 직접노무비 × rate
|
||||
2) 산재보험료 = 노무비 × rate
|
||||
3) 고용보험료 = 노무비 × rate
|
||||
4) 건강보험료 = 직접노무비 × rate
|
||||
5) 장기요양보험료 = 건강보험료 × rate
|
||||
6) 연금보험료 = 직접노무비 × rate
|
||||
7) 퇴직공제부금비 = 직접노무비 × rate
|
||||
8) 산업안전보건관리비
|
||||
9) 안전관리비
|
||||
10) 품질관리비
|
||||
11) 기타경비 = (재료비 + 노무비) × rate
|
||||
12) 공사이행수수료
|
||||
13) 하도급보증수수료 = 직접공사비 × rate
|
||||
14) 환경보전비 = 직접공사비 × rate
|
||||
15) 건설기계대여보증수수료 = 직접공사비 × rate
|
||||
16) 석면분담금 = 노무비 × rate
|
||||
17) 임금채권부담금 = 노무비 × rate
|
||||
3. 일반관리비 = (직접공사비 + 간접공사비 + 관리품목자재) × rate
|
||||
4. 이윤 = (직접공사비 + 간접공사비 + 일반관리비) × rate - 보정액
|
||||
5. 공급가액
|
||||
6. 도급공사비
|
||||
7. 총공사비
|
||||
```
|
||||
|
||||
일반형식 `T`와 달리 하도급·환경·건설기계 보증수수료의 밑수가 직접공사비 전체다.
|
||||
|
||||
## 6. 실적수공 `SA`
|
||||
|
||||
실적수공은 실적공사비 묶음과 수공 경비 세분을 함께 사용한다.
|
||||
|
||||
```text
|
||||
1. 직접공사비
|
||||
2. 간접공사비
|
||||
1) 간접노무비 = 직접노무비 × rate
|
||||
2) 산재보험료 = (직접노무비 + 간접노무비) × rate
|
||||
3) 고용보험료 = (직접노무비 + 간접노무비) × rate
|
||||
4) 건강보험료 = 직접노무비 × rate
|
||||
5) 장기요양보험료 = 건강보험료 × rate
|
||||
6) 연금보험료 = 직접노무비 × rate
|
||||
7) 퇴직공제부금비 = 직접노무비 × rate
|
||||
9) 산업안전보건관리비
|
||||
10) 품질관리비
|
||||
11~18) 수도광열비부터 지급수수료까지 수공 경비
|
||||
19) 공사이행수수료
|
||||
20) 하도급보증수수료
|
||||
21) 환경보전비
|
||||
22) 건설기계대여보증수수료
|
||||
22) 석면분담금
|
||||
23) 임금채권부담금
|
||||
3. 일반관리비 = (직접공사비 + 간접노무비) × rate
|
||||
4. 이윤 = (직접공사비 + 간접공사비 + 일반관리비) × rate - 보정액
|
||||
5. 공급가액
|
||||
6. 도급공사비
|
||||
7. 총공사비
|
||||
```
|
||||
|
||||
화면에 22번이 두 번 표시된다. 재구현 데이터 키는 화면 번호가 아니라 고유 항목코드를 써야 한다.
|
||||
|
||||
## 7. 네 형식의 핵심 차이
|
||||
|
||||
| 규칙 | T | S | TA | SA |
|
||||
|---|---|---|---|---|
|
||||
| 산재·고용 밑수 | 노무비 | 직노+간노 | 노무비 | 직노+간노 |
|
||||
| 기타경비 | 묶음 1개 | 세부 경비 8개 | 묶음 1개 | 세부 경비 8개 |
|
||||
| 하도급·환경·장비 보증 밑수 | 재+직노+산출경비 | 재+직노+산출경비 | 직접공사비 | 재+직노+산출경비 |
|
||||
| 일반관리비 밑수 | 순공사원가+관리품목자재 | 순공사비+간노+경비 | 직공비+간접비+관리품목자재 | 직공비+간접노무비 |
|
||||
| 이윤 밑수 | 소계-재료비 | 순공사원가+일반관리비-재료비 | 직공비+간접비+일반관리비 | 동일 |
|
||||
|
||||
따라서 형식을 출력 템플릿으로만 구현하면 안 된다. `calculation_scheme`이 공식 집합과 합계 트리를 선택해야 한다.
|
||||
|
||||
## 8. RATE 편집 화면의 실제 표
|
||||
|
||||
| 그리드 | 행 수 | 열 제목 | 역할 |
|
||||
|---|---:|---|---|
|
||||
| `rGrd_Rate_Kobo` | 7 | 등급, 요율 | 고용보험 등급별 요율 |
|
||||
| `rGrd_Rate_Anjun` | 4 | 공종, 5억 미만, 5~50억 미만, 추가, 50억 이상, 800억 이상 | 산업안전보건관리비 규모식 |
|
||||
| `rGrd_Rate_R2` | 6 | 구분, 요율 | 단일 요율 목록 |
|
||||
| `rGrd_Rate_Hwan` | 7 | 구분, 요율 × 2쌍 | 환경보전비 |
|
||||
| `rGrd_Rate_Bojng` | 5 | 공종, `[요율+추가]×공기` | 공사이행보증수수료 |
|
||||
| `rGrd_Rate_Hado` | 6 | 공사규모, 요율 | 하도급대금지급보증수수료 |
|
||||
| `rGrd_Rate_Eqip` | 6 | 구분, 요율 × 2쌍 | 건설기계대여금 지급보증수수료 |
|
||||
| `rGrd_Rate_R` | 5 | 공사규모(국가유산), 일반건설, 전문, 국가유산, 이윤 | 일반형식 규모 요율 |
|
||||
| `rGrd_Rate_R1` | 20 | 공사기간과 토목·산업설비·조경·건축·국가유산 열 2군 | 일반형식 기간 요율 |
|
||||
| `rGrd_Rate_RA` | 5 | 공사규모, 일반건설, 전문, 이윤 | 실적형식 규모 요율 |
|
||||
| `rGrd_Rate_RA1` | 20 | 공사기간과 토목·조경·산업설비·건축 열 2군 | 실적형식 기간 요율 |
|
||||
| `rGrd_Rate_An_A` | 4 | 공사규모 | 실적 안전비 규모 |
|
||||
| `rGrd_Rate_An_A1` | 16 | 공사종류, 산업안전보건관리비 공식, 비고/규모비율 | 실적 안전비 공식표 |
|
||||
| `rGrd_RateM` | 14 | 구분, 간접노무비 | 행자부 기준 간접노무비 |
|
||||
| `rGrd_RateB` | 12 | 구분, 재·직노·노·재+직노·재+노의 두 열군 | 수공 경비별 밑수 요율 |
|
||||
|
||||
## 9. JB 파일과 화면 표의 대응
|
||||
|
||||
| 파일 | 물리 크기 | 확인된 화면군 | 판정 |
|
||||
|---|---:|---|---|
|
||||
| `*.JB_1` | 41행×10열 | 구버전/공통 요율 데이터 | B |
|
||||
| `*.JB_M` | 25행×10열 | `rGrd_RateM`, 행자부기준 | A/B |
|
||||
| `*.JB_R` 구형 | 50행×20열 | 일반형식 `Rate_R/Rate_R1` 및 부속표 | B |
|
||||
| `*.JB_R` 2020 이후 | 50행×35열 | 일반형식에 안전·환경·보증 부속표 확장 | B |
|
||||
| `*.JB_RA` | 50행×20열 | 실적형식 `Rate_RA/Rate_RA1/An_A/An_A1` | A/B |
|
||||
|
||||
DBF 필드는 `RATE0..RATE9` 또는 `RATE_01..RATE_35`, 레코드는 고정 25/41/50행이다.
|
||||
|
||||
**폼 패널별 행수 합계가 파일 행수와 맞는다.**
|
||||
|
||||
| 폼 패널 | 담은 그리드 | 행수 합 | 같은 행수의 파일 |
|
||||
|---|---|---:|---|
|
||||
| `Panel9` (부속표) | Kobo 7 + Anjun 4 + R2 6 + Hwan 7 + Bojng 5 + Hado 6 + Eqip 6 | **41** | `JB_1` 41행 |
|
||||
| `Panel8` (일반형식) | Rate_R 5 + Rate_R1 20 | **25** | `JB_M` 25행 |
|
||||
| `Panel22` (실적형식) | Rate_RA 5 + Rate_RA1 20 + An_A 4 + An_A1 16 | **45** | — |
|
||||
| `sc_Bx_M` | RateM 14 | 14 | — |
|
||||
| `sc_Bx_B` | RateB 12 | 12 | — |
|
||||
|
||||
행수가 맞는 것은 **강한 단서이지 증명은 아니다.** `JB_1`의 실제 값 분포를 보면 빈 행이 `4,5`와 `10,11`에 오는데 `Panel9` 그리드 경계(7·11·17·24·29·35)와는 어긋난다. 두 가지 중 하나다 — ① `JB_1`은 2011년 한 개뿐인 구형이라 현재 폼과 배치가 다르다, ② 적재가 단순 이어붙이기가 아니다.
|
||||
|
||||
**확정된 것**(10번 문서에 실측 근거)
|
||||
|
||||
- 행은 `0,5,10,…,45`에서 시작하는 **5행 묶음**이며 각 묶음은 값 4행 + 빈 1행이다.
|
||||
- `JB_R` 열 1~5는 간접노무비, 6~10은 기타경비이고 둘 다 `토목 / 산.설(토목) / 조경 / 건축 / 산.설(건축)` 순이다.
|
||||
- 2020-05-27 확장으로 **열 21 = 국가유산 간접노무비, 열 26 = 국가유산 기타경비**가 붙었고 기존 1~20열은 한 셀도 바뀌지 않았다.
|
||||
- `JB_RA`는 43개 전 기간 20열 그대로다. 실적형식에는 국가유산 공종이 없다.
|
||||
|
||||
**남은 것** — 각 부속표 그리드(고용보험·환경보전·보증수수료 계열)가 `JB_1`/`JB_R`의 몇 번째 행부터 적재되는지. 열 접근이 실행 중 문자열 조립이라 코드 상수로 읽을 수 없으므로, 저장 이벤트 역추적 또는 통제 동적 시험이 필요하다.
|
||||
|
||||
## 10. 계산 설정 입력
|
||||
|
||||
원가계산 설정 화면에 다음 16개 선택축이 번호로 존재한다.
|
||||
|
||||
1. 기준 유형(화면 첫 항목)
|
||||
2. 공사의 종류
|
||||
3. 공사기간
|
||||
4. 고용보험료
|
||||
5. 퇴직공제부금비
|
||||
6. 관급/사급 내용
|
||||
7. 산업안전보건관리비
|
||||
8. 환경보전비
|
||||
9. 건설기계대여금 지급수수료
|
||||
10. 일반관리비
|
||||
11. 공급가액 절사기준
|
||||
12. 부가가치세
|
||||
13. 사급비 위치
|
||||
14. 낙찰 방식
|
||||
15. 이행보증 방식
|
||||
16. 적용기준
|
||||
|
||||
추가 입력에는 조달수수료, 상시근로자 노무비, 내역서 안전관리비, 안전·품질관리비, PQ 공사손해보험요율, 공사이행 기간, 관급자재 분리 여부가 있다. 이 설정 스냅샷 없이는 같은 원본 내역으로도 원가계산 결과를 재현할 수 없다.
|
||||
|
||||
### 10.1 실제 선택값 사전
|
||||
|
||||
폼의 `Items.Strings`와 내부 `Values.Strings`를 함께 복원했다.
|
||||
|
||||
| 설정 | 화면 선택지 | 내부값/주의 |
|
||||
|---|---|---|
|
||||
| 제잡비 방식 | 원가계산방식, 표준시장방식 | 내부 명칭은 실적공사방식 |
|
||||
| 공사종류 | 일반(토목), 조경, 산업설비(토목), 건축, 산업설비(건축), 국가유산수리, 폐기물처리, 소규모용역 | `0,2,4,3,1,5,8,9` — 화면 순번과 내부값이 다름 |
|
||||
| 적용기준 | 일반(국가계약법), 일반(지방계약법), 행자부기준 | `2,0,1` — 역시 화면 순번과 다름 |
|
||||
| 고용보험 | 없음, 1~6등급, 7등급 이하 | 등급명 |
|
||||
| 퇴직공제 | 토목, 준설, 건축, 기타 | 명칭 |
|
||||
| 안전비 공종 | 건축, 토목, 중건설, 특수건설 | `0,1,3,4` |
|
||||
| 환경보전 | 없음, 도로, 플랜트, 지하철, 철도, 상하수도, 항만 2종, 댐, 택지, 주택 3종, 조경, 기타토목 | 명칭 |
|
||||
| 건설기계 보증 | 없음, 일반공사, 전문공사 세부 5군 | `-1,0,1,2,3,4,5` |
|
||||
| 관급/사급 | 관급/1.1, 관급, 사급/1.1, 사급, 혼합 4종 | 식 문자열 자체가 값 |
|
||||
| 사급비 위치 | 재료비 포함, 이윤 다음, 도급공사비 직전, 관급자재대 다음 | `3,1,2,0` |
|
||||
| 부가세 | 공급가액의 10%, 재료비의 10%, 없음, 산림조합 형식, 산림조합 면세품 | 선택 문자열 |
|
||||
| 낙찰방식 | 종합심사 외, 종합심사 대상, 턴키(대안) | 값 목록 |
|
||||
| 계약형식 | 일반계약, 최저가·기술제안 | `0,1` |
|
||||
| 관급 인쇄 | 분리인쇄, 통합인쇄 | `0,1` |
|
||||
|
||||
공사기간 축(`cmBx_Kong_Kikan`)은 화면과 내부가 **줄 수부터 다르다.**
|
||||
|
||||
```text
|
||||
화면 Items (4줄) : 6개월이하 | 7개월~12개월이하 | 13개월~36개월이하 | 36개월이상
|
||||
내부 Values (5값) : 30일 미만 | 6개월이하 | 7개월~12개월이하 | 13개월~36개월이하 | 37개월이상
|
||||
```
|
||||
|
||||
맨 앞 `30일 미만`이 내부에만 있고, 마지막 줄은 화면이 `36개월이상`인데 내부는 `37개월이상`이다. **같은 칸을 두 이름으로 부르는 것이라 36개월 정각이 어느 쪽에 붙는지가 화면만 봐서는 안 갈린다.** 30일 미만의 실제 선택 경로와 36/37개월 경계는 동적 시험 전까지 C등급이다.
|
||||
|
||||
이 5구간은 `JB_R`·`JB_RA` 행렬이 **5행 묶음**(시작 행 0,5,10,…,45, 값 4행 + 빈 1행)인 것과 개수가 맞는다. 빈 한 행이 화면에 없는 구간에 해당하는 것으로 보이나, 어느 구간이 비는지는 아직 B등급이다.
|
||||
|
||||
### 10.1-2 형식별 선택축 — 전수 83개
|
||||
|
||||
설정 폼(`TWM_KANJUB`)에서 선택지를 가진 컴포넌트를 모두 뽑으면 83개다. 전문은 이 폴더의 `settings_axis_map.txt`에 있고, 재현 스크립트는 `settings_axis_map.py`다.
|
||||
|
||||
| 묶음 | 축 수 | 자리 |
|
||||
|---|---:|---|
|
||||
| 공통 (형식과 무관) | 29 | `Panel1`, `Panel3`, `pnSet_Optn`, `pn_CH_BO`, `tabAdd_BSC` |
|
||||
| 일반 `T` | 12 | `Pn01_T` ~ `Pn05_T` |
|
||||
| 수공 `S` | 11 | `Pn01_S` ~ `Pn05_S` |
|
||||
| 실적일반 `TA` | 16 | `Pn01_TA` ~ `Pn05_TA` |
|
||||
| 실적수공 `SA` | 15 | `Pn01_SA` ~ `Pn05_SA` |
|
||||
|
||||
**형식마다 패널 한 벌을 따로 가진다.** 같은 이름의 축이 `_T`/`_S`/`_TA`/`_SA` 네 벌로 존재하므로, 설정 스냅샷은 **형식별로 따로 저장**해야 한다. 한 벌만 저장하면 형식을 바꿀 때 값이 섞인다.
|
||||
|
||||
패널의 쓰임은 공통이다.
|
||||
|
||||
| 패널 | 담은 것 |
|
||||
|---|---|
|
||||
| `Pn01_x` | 안전관리비 밑수·1.2배 비교, 건강/연금 보험 밑수, (실적만) 노무비·재료비 산정방식 |
|
||||
| `pn02_x` | 이윤 / 수수료 택일 — **`T`와 `TA`에만 있고 `S`·`SA`에는 없다** |
|
||||
| `Pn03_x` | 공급가액 절사 자리 |
|
||||
| `Pn04_x` | 도급공사비/도급예정액 택일, 관급(도급자설치)·관급(관급자설치)의 절사 자리와 절상/반올림/절사 |
|
||||
| `Pn05_x` | 총액·이윤보정 절사 자리 |
|
||||
|
||||
**실적형식(`TA`/`SA`)에만 있는 축 넷** — `Ed_C_LTYP_x`(노무비 유형), `Ed_C_RateMxA_x`, `Ed_C_RateMxK_x`(조정계수), `Ed_C_MTYP_x`(재료비 산정방식). 일반·수공에는 아예 없다.
|
||||
|
||||
### 10.1-3 화면 순번과 내부값이 어긋나는 축 — 그대로 저장하면 틀리는 자리
|
||||
|
||||
| 축 | 화면 순서 | 내부값 순서 |
|
||||
|---|---|---|
|
||||
| `cmBx_Kong_Type` 공사종류 | 일반(토목), 조경, 산업설비(토목), 건축, 산업설비(건축), 국가유산수리, 폐기물처리, 소규모용역 | `0, 2, 4, 3, 1, 5, 8, 9` |
|
||||
| `cmBx_Set_Base` 적용기준 | 일반(국가계약법), 일반(지방계약법), 행자부기준 | `2, 0, 1` |
|
||||
| `cmBx_Pos_Sagup` 사급비 위치 | 재료비에 포함, 이윤 다음, 도급공사비 직전, 관급자재대 다음 | `3, 1, 2, 0` |
|
||||
| `cmBx_AnJun_Type` 안전비 공종 | 건축, 토목, 중건설, 특수건설 | `0, 1, 3, 4` — **2번이 비어 있다** |
|
||||
| `cmBx_EQIP` 건설기계 보증 | 없음(국가유산수리) 포함 7줄 | `-1, 0, 1, 2, 3, 4, 5` |
|
||||
| `Ed_C_MTYP_TA` / `_SA` 재료비 산정 | ① 직접공사비 × 요율 ② 조정계수 입력 | `1, 0` — **뒤집혀 있다** |
|
||||
| `cmBx_AnPum` 안전·품질관리비 | ① 산출경비로 포함 ② 산출경비로 미포함 | `1, 0` — **뒤집혀 있다** |
|
||||
|
||||
`Ed_C_MTYP`와 `cmBx_AnPum`은 **화면 순번을 그대로 저장하면 정반대 공식이 걸린다.** 재구현에서 화면 인덱스를 값으로 쓰면 안 되는 이유가 이 두 축에 그대로 드러난다.
|
||||
|
||||
`cBx_W_Type`은 화면이 `제잡비 표준시장방식`인데 내부 이름은 `제잡비 실적공사방식`이고, `cmBx_Buga1`은 화면 `(3) 산림조합 형식`이 내부 `(3) 임 협 식`이다. **같은 것을 두 이름으로 부르므로 문서·코드에서 이름으로 대조하면 안 된다.**
|
||||
|
||||
### 10.2 절사·절상 선택지
|
||||
|
||||
간접비 일괄 절사:
|
||||
|
||||
```text
|
||||
없음, 10원, 100원, 1,000원, 10,000원, 100,000원, 1,000,000원 미만 절사
|
||||
```
|
||||
|
||||
공급가액·도급액·총공사비/이윤보정 자리:
|
||||
|
||||
```text
|
||||
조정 없음 또는 직접입력,
|
||||
10원, 100원, 1,000원, 10,000원, 100,000원,
|
||||
1,000,000원, 10,000,000원, 100,000,000원 미만
|
||||
```
|
||||
|
||||
관급자재 조정은 자리수와 함께 `절상/반올림/절사`를 선택한다. 도급액에는 별도의 “도급액 버림없음” 선택이 있다. 즉 하나의 전역 반올림 설정이 아니라 간접비·공급가액·도급액·관급(도급자설치)·관급(관급자설치)·총액/이윤보정 설정을 따로 저장해야 한다.
|
||||
|
||||
### 10.3 변경설계와 안전비의 실제 모드
|
||||
|
||||
보험료 변경 계산(`cBx_CH_BO`)은 세 모드다.
|
||||
|
||||
```text
|
||||
0 직접노무비 × 요율
|
||||
1 당초액 + 직접노무비 증감 × 요율
|
||||
2 당초액 + 증감액(비율)
|
||||
```
|
||||
|
||||
안전비의 1.2배 비교에는 `소수 버림 후 1.2배`와 `소수 버림 전 1.2배`가 따로 있다. 상한비도 `자동계산`, `강제적용`, `적용안함`의 세 모드다. 이 두 옵션은 같은 공식이라도 원 단위 결과를 바꾸므로 계산 추적에 반드시 남긴다.
|
||||
|
||||
실적형식의 노무비 산정은 `직접공사비×조정계수` 또는 `내역서 직접노무비`, 재료비 산정은 `직접공사비×요율` 또는 `조정계수 입력` 중 선택한다.
|
||||
|
||||
## 11. 추가금액 데이터 형태
|
||||
|
||||
추가금액 그리드는 다음 열을 가진다.
|
||||
|
||||
```text
|
||||
추가비용 명칭, 선택, 입력/자동, 금액,
|
||||
1단계, 2단계, ... 9단계, 최종 합계 위치
|
||||
```
|
||||
|
||||
별도 탭은 `일반관리비 직전`, `이윤~공급가액`, `도급공사비 직전`, `관급자재~총공사비`로 나뉜다. 따라서 추가금액은 단계별 포함 여부를 최대 9개 저장하고 최종 삽입 위치를 갖는 행이다.
|
||||
|
||||
## 12. 구현 시 필수 검증
|
||||
|
||||
1. 네 형식을 같은 입력으로 계산해 밑수 차이가 표대로 발생하는지 확인한다.
|
||||
2. 건강보험료를 바꾸면 장기요양보험료만 그 절사 결과를 밑수로 다시 계산하는지 확인한다.
|
||||
3. 수공형식의 세부 경비 합이 일반형식 기타경비와 자동으로 같다고 가정하지 않는다.
|
||||
4. 안전비는 상한비율, 기초금액, 1.2배 비교, 기존 안전비 차감의 중간값을 각각 기록한다.
|
||||
5. 추가금액은 1~9단계 포함 여부에 따라 뒤 항목의 밑수가 달라지는지 확인한다.
|
||||
6. `1원보정(계산결과+1원)` 옵션을 VAT/공급가액 경계에서 시험한다.
|
||||
|
||||
이 문서로 네 계산 형식의 항목 순서와 공식 뼈대는 재구현할 수 있다. 남은 핵심은 JB 행렬의 화면 셀 시작 좌표와 경계 비교 연산이며, 이는 저장 이벤트 역추적 또는 통제 동적 시험으로 확정해야 한다.
|
||||
@@ -0,0 +1,141 @@
|
||||
# STmate 통제 대조시험 목록
|
||||
|
||||
> 정적 분석만으로는 확정할 수 없는 경계를 **시험 단위로 갈라 놓은 유일한 목록**이다. 05~12번 문서에 흩어져 있던 "미확정·C등급" 항목을 여기로 모았다. 각 문서는 이제 이 목록을 가리킨다.
|
||||
|
||||
기준일: 2026-09-13
|
||||
대상: `C:\경영정보(주)` — **원본은 읽기만 한다.** 시험은 프로젝트 **복제본**에서 한다.
|
||||
|
||||
## 0. 왜 시험이 필요한가
|
||||
|
||||
정적 파일에는 **결과만** 남고 **분기 조건**이 안 남는 자리가 있다. 값을 하나만 바꿔 앞뒤를 대조하면 그 분기가 드러난다. 아래 시험을 다 통과해야 "비슷한 금액"이 아니라 **계산 순서와 중간값까지 같은 엔진**이라고 판정할 수 있다.
|
||||
|
||||
## 1. 공통 규칙
|
||||
|
||||
1. 시험마다 **한 변수만** 바꾼다. 두 개를 같이 바꾸면 어느 쪽이 결과를 움직였는지 못 가른다.
|
||||
2. 시험 전후로 STC 안의 **모든 DBF 필드**와 출력 XLSX의 **모든 계산행**을 저장해 대조한다.
|
||||
3. 원설계를 복제해 쓰고, **설치 폴더와 기준자료 원본에는 쓰지 않는다.**
|
||||
4. 결과는 `시험ID / 바꾼 값 / 바뀐 필드 / 안 바뀐 필드`로 적는다. **안 바뀐 것도 증거다.**
|
||||
|
||||
## 2. 시험 목록
|
||||
|
||||
### T1 — 요율 좌표 매핑 (최우선)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 화면 그리드 15개가 `RATE`·`RATE_M`·`RATE_R`·`RATE_RA` 행렬의 **몇 번째 행부터** 적재되는지 |
|
||||
| 지금 등급 | 열 제목·행 수 **확정(A)** / 시작 행 **미확정** |
|
||||
| 어떻게 | RATE 편집 화면에서 그리드 한 칸만 눈에 띄는 값(예: `99.99`)으로 고치고 저장 → 프로젝트 `RATE_*.DBF`에서 `99.99`가 앉은 (행, 열)을 찾는다. 그리드마다 첫 칸·마지막 칸 두 번씩 |
|
||||
| 확정되는 것 | 그리드 → 행렬 좌표의 완전 매핑. `rate_standard()` 기준표를 하드코딩 없이 만들 수 있게 됨 |
|
||||
| 근거 문서 | 12번 §8·§9, 10번 §4 |
|
||||
|
||||
이미 잡아 둔 것 — 행은 `0,5,10,…,45` 시작의 **5행 묶음**, `JB_R` 열 1~5=간접노무비·6~10=기타경비(토목/산설(토목)/조경/건축/산설(건축)), 신형 열 21·26=국가유산. **T1은 나머지 부속표의 시작 행만 남긴 시험이다.**
|
||||
|
||||
### T2 — 금액·기간 구간 경계 방향
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 구간 경계에서 `<`인지 `<=`인지. 공사기간은 36개월 정각이 어느 칸에 붙는지 |
|
||||
| 지금 등급 | C |
|
||||
| 어떻게 | 같은 프로젝트를 복제해 추정금액을 각 경계의 **직전 / 정각 / 직후** 세 값으로 두고, 공사기간도 같은 식으로 둔다. 선택된 요율과 간접비 전 행을 비교 |
|
||||
| 왜 까다롭나 | 화면 목록은 `…13~36개월이하 / 36개월이상` 4줄인데 내부값은 `30일 미만 … 37개월이상` 5값이다. **같은 칸을 두 이름으로 부른다** |
|
||||
| 근거 문서 | 12번 §10.1, 06번 §6 |
|
||||
|
||||
### T3 — `ROUNDUP`·`MINT`의 음수 방향
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 올림·정수화가 음수에서 0에서 멀어지는지, +∞ 쪽인지 |
|
||||
| 지금 등급 | `ROUND`는 **확정(A)** — 최근접-짝수·0 대칭. `ROUNDUP`·`MINT`만 남음 |
|
||||
| 어떻게 | 끝전조정에 음수 차액이 나오는 프로젝트를 만들고 `절상`·`반올림`·`절사`를 차례로 골라 결과를 비교. 값은 `±2.44 / ±2.45 / ±2.46`, 자리 `1, 0, -1` |
|
||||
| 왜 중요한가 | 내부 계산은 은행가 반올림인데 출력 XLSX 수식은 Excel 사사오입이라 **20개 경계 중 10개가 이미 갈린다** |
|
||||
| 근거 문서 | 11번 §4.2 |
|
||||
|
||||
### T4 — 안전관리비 변형 선택 조건
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 어느 기준연도·공사종류에서 어느 안전비 식이 걸리는지, 1.2배 비교와 상한비 모드가 원 단위 결과를 어떻게 바꾸는지 |
|
||||
| 지금 등급 | 선택지 목록은 **확정(A)**, 선택 조건은 C |
|
||||
| 어떻게 | 안전비 공종 4가지(`0,1,3,4` — **2번이 비어 있다**) × 관급/사급 조합 × `소수 버림 전/후 1.2배` × 상한비 `자동/강제/적용안함`을 돌리고 중간값(상한비율·기초금액·1.2배 비교·기존 안전비 차감)을 각각 기록 |
|
||||
| 근거 문서 | 12번 §10.3, 11번 §7 |
|
||||
|
||||
### T5 — 복합 보험식의 항목명 연결
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 중첩 절사식의 `RAT_1`~`RAT_4`, `RAT_KY`가 화면의 어느 비용항목인지 |
|
||||
| 지금 등급 | 식은 **확정(A)**, 이름 연결은 C |
|
||||
| 어떻게 | 제비율 화면에서 항목을 하나씩 0으로 만들고 어느 항이 사라지는지 본다 |
|
||||
| 근거 문서 | 11번 §6.1, 10번 §3 |
|
||||
|
||||
### T6 — 추가금액 단계 1·2와 특수 위치
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 단계 1·2, `이윤 이후(공사원가 포함)`, `사급자재비 이후`가 어느 합계를 움직이는지 |
|
||||
| 지금 등급 | 단계 3~6은 확정, 1·2는 C |
|
||||
| 어떻게 | 같은 금액을 단계 1~9에 하나씩 넣고 순공사원가·공급가액·도급액·총액 넷을 비교 |
|
||||
| 근거 문서 | 11번 §9, 12번 §11 |
|
||||
|
||||
### T7 — `CODE = W/B/D/X/A` 세부 분기
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 복합·일위대가·단가산출·중기·기타 코드의 성분 합산 분기 |
|
||||
| 지금 등급 | `TAMT=MAMT+LAMT+SAMT` 불변조건은 **확정(A)**, 코드별 분기는 C |
|
||||
| 어떻게 | 코드 유형마다 표본 행을 만들어 성분 단가를 1원씩 움직이고 상위 단가·내역의 반응을 본다 |
|
||||
| 근거 문서 | 11번 §3·§5 |
|
||||
|
||||
### T8 — 재계산 범위와 역의존
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | UI 버튼별로 부분계산인지 전체계산인지, 수동 고정값을 자동 변경이 덮는지 |
|
||||
| 지금 등급 | C |
|
||||
| 어떻게 | ① 같은 자재를 참조하는 단가 2개와 그중 하나를 참조하는 상위 단가를 만든 뒤 자재가 **1원** 변경 ② 설계수량만 변경해 단가표 불변 확인 ③ 요율만 변경해 직접비 불변 확인 |
|
||||
| 근거 문서 | 08번 §1·§4 |
|
||||
|
||||
### T9 — 변경설계 계산모드 선택 조건
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 방법 A(당초액 × 전체 증감비율) / 방법 B(증감 기초 × 항목 요율) / 직접입력 / 계약 제잡비율 중 **어느 조건에서 어느 것이 걸리는지** |
|
||||
| 지금 등급 | 공식 자체는 **확정(A)** — 코드에서 직접 확인. 선택 조건만 C |
|
||||
| 어떻게 | 원설계에서 항목 추가·삭제·수량변경·단가변경을 각각 한 건씩 만들어 변경 STC로 저장하고, 보험료 변경 모드(`cBx_CH_BO` 0/1/2)를 바꿔 가며 비교 |
|
||||
| 근거 문서 | 11번 §10, 08번 §2, 12번 §10.3 |
|
||||
|
||||
### T10 — 일괄변경·물가변동 필터
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 대상 선별 조건과 수동 채택가 보호 여부 |
|
||||
| 지금 등급 | C |
|
||||
| 어떻게 | 수동 채택가, 0원, 공란, 환율적용 품목이 섞인 표본으로 일괄변경을 돌리고 각 유형이 바뀌었는지 본다 |
|
||||
| 근거 문서 | 08번 §3 |
|
||||
|
||||
### T11 — 형식 간 밑수 차이 확인
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 무엇을 가르나 | 네 형식(`T`/`S`/`TA`/`SA`)이 같은 입력에서 표대로 다른 밑수를 쓰는지 |
|
||||
| 지금 등급 | 밑수 공식은 **확정(A)** — 화면 묶음 제목에서 확인. 실제 적용 확인만 남음 |
|
||||
| 어떻게 | 같은 내역으로 형식만 바꿔 네 번 계산하고 각 간접비 행의 밑수를 대조. 건강보험료를 바꿔 장기요양보험료만 그 절사 결과를 밑수로 다시 계산하는지 확인 |
|
||||
| 근거 문서 | 12번 §7·§12, 06번 §3 |
|
||||
|
||||
## 3. 우선순위
|
||||
|
||||
| 순위 | 시험 | 까닭 |
|
||||
|---:|---|---|
|
||||
| 1 | **T1** 요율 좌표 | 이것 없이는 기준표를 못 만든다. 다른 시험의 전제 |
|
||||
| 2 | **T3** 반올림 방향 | 원 단위 차이가 전 항목에 번진다 |
|
||||
| 3 | **T2** 구간 경계 | 요율 선택 자체가 갈린다 |
|
||||
| 4 | **T11** 형식별 밑수 | 형식을 잘못 고르면 전부 틀린다 |
|
||||
| 5 | **T4** 안전관리비 | 금액이 크고 변형이 많다 |
|
||||
| 6 | T9 · T8 | 변경설계·재계산 |
|
||||
| 7 | T5 · T6 · T7 · T10 | 나머지 |
|
||||
|
||||
## 4. 완료 판정
|
||||
|
||||
- T1~T4·T11을 통과하면 **원설계 한 건을 원 단위까지 재현**할 수 있다.
|
||||
- T8·T9까지 통과하면 **변경설계를 포함해 재현**할 수 있다.
|
||||
- 전부 통과하면 11번 §16 재현 시험표를 그대로 돌려 **동일 엔진 판정**이 선다.
|
||||
@@ -0,0 +1,181 @@
|
||||
# STmate 경쟁 프로그램 분석 — 최종 완료 보고서
|
||||
|
||||
분석 기간: 2026-09-12 ~ 2026-09-13
|
||||
대상: `C:\경영정보(주)` (설치 폴더 `System` 186개 파일 + 프로젝트 STC 7건 + STmate 생성 XLSX 5건)
|
||||
목적: 화면을 닮게 만드는 것이 아니라, **같은 입력에서 같은 중간값과 같은 최종 금액을 내는 계산 엔진**을 만들 수 있는 수준까지 규칙을 확보하는 것
|
||||
원본 취급: **읽기만 했다.** 설치 폴더·기준자료·맛보기 프로젝트에 쓰기 없음. 암호 추측·우회 없음.
|
||||
|
||||
## 1. 어디까지 왔나
|
||||
|
||||
| 층 | 상태 |
|
||||
|---|---|
|
||||
| 파일 전수 등록 (경로·크기·SHA-256) | **186 / 186** |
|
||||
| 실제 형식 판정 (파일명 아닌 헤더 기준) | **186 / 186** — PE 82, DBF 계열 61, ZIP 28, 기타 15 |
|
||||
| 내부구조 추출 | **186 / 186** — 미판독 0 |
|
||||
| 핵심 CPM 정적 분석 | **12 / 12** — PE 섹션·import·export·문자열 |
|
||||
| 화면 폼 리소스 복원 | 제비율 편집·계산설정·인쇄 3벌 |
|
||||
| 계산 규칙 A등급 확정 | 아래 3장 |
|
||||
| 남은 경계 | **13번 통제시험 목록**에 시험 11개로 분리 |
|
||||
|
||||
**정적 분석으로 갈 수 있는 데까지 갔다.** 남은 것은 분석 누락이 아니라, 정적 파일에 **결과만 남고 분기 조건이 안 남는** 자리다. 그 자리는 값을 하나만 바꿔 앞뒤를 대조하는 통제시험으로만 갈린다.
|
||||
|
||||
## 2. STmate 가 무엇인가
|
||||
|
||||
Delphi 6 네이티브 모듈 + Apollo/Six 계열 DBF 저장소 + ZIP 기반 교환 파일로 만든 **로컬 설계예산 프로그램**이다. 값의 흐름은 이렇다.
|
||||
|
||||
```text
|
||||
노임·자재·중기 기초자료
|
||||
→ 출처별 후보단가와 채택단가
|
||||
→ 일위대가·단가산출 (자원코드 × 품, 할증, 기계 Q식)
|
||||
→ 공종별 재료/노무/경비 단가
|
||||
→ 프로젝트 수량 × 단가
|
||||
→ 직접재료비·직접노무비·산출경비
|
||||
→ 제비율별 고유 밑수 × 적용률
|
||||
→ 순공사원가 → 일반관리비 → 이윤 → 부가세 → 총공사비
|
||||
```
|
||||
|
||||
## 3. 확정된 것 — A등급
|
||||
|
||||
### 3.1 금액 성분 불변조건
|
||||
|
||||
```text
|
||||
TAMT = MAMT + LAMT + SAMT
|
||||
CODE="L" → LAMT = TAMT CODE="M" → MAMT = TAMT CODE="S" → SAMT = TAMT
|
||||
```
|
||||
|
||||
세 모듈에서 검증 조건식 `TAMT1 <> MAMT1+LAMT1+SAMT1`이 그대로 발견됐다. **`TAMT`는 네 번째 비용 성분이 아니라 총계다.**
|
||||
|
||||
### 3.2 반올림 — 내부는 은행가 반올림
|
||||
|
||||
전 PE 186개의 수치 런타임 심볼을 실측했다.
|
||||
|
||||
- `Math.RoundTo` 임포트 — 계산 모듈 17개, 모듈마다 호출 정확히 12곳
|
||||
- `SimpleRoundTo`(0에서 멀어지는 변형)·`Ceil`·`SetRoundMode`·`Set8087CW` — **전부 미임포트**
|
||||
- 반올림 함수 본체에 `FCHS`·`FABS` 없음 → 음수 특별처리 없음
|
||||
|
||||
⇒ **내부 `ROUND`는 최근접-짝수(은행가) 반올림이고 0을 기준으로 대칭이다.** 사사오입이 아니다.
|
||||
|
||||
**STmate가 뽑아 주는 XLSX 안의 수식은 Excel `ROUND`(사사오입)**이므로 내부값과 출력값이 갈린다. 시험한 20개 경계 중 **10개가 원 단위에서 다르다.**
|
||||
|
||||
| 값 | 자리 | 내부 | 출력 XLSX |
|
||||
|---:|---:|---:|---:|
|
||||
| 2.5 | 원 | 2 | 3 |
|
||||
| 2,500 | 천원 | 2,000 | 3,000 |
|
||||
| −2,500 | 천원 | −2,000 | −3,000 |
|
||||
|
||||
`2.45`는 한 겹 더 어긋난다 — `RoundTo`가 `IntPower(10,-1)`로 만든 `0.1`이 이진수로 정확하지 않아 `2.45/0.1`이 `24.4999…`가 된다. **`Decimal` 사사오입으로는 STmate 결과를 흉내 낼 수 없다.**
|
||||
|
||||
### 3.3 간접비 항목별 밑수
|
||||
|
||||
RATE 편집 화면의 그리드 묶음 제목에 **밑수 공식이 그대로 적혀 있다.** 추정이 아니다. 전체 표는 06번 §3에 있고, 정정된 곳은 셋이다.
|
||||
|
||||
- 환경보전비 밑수는 직접공사비가 아니라 `재료비+직접노무비+산출경비`다 *(실적일반만 직접공사비)*
|
||||
- 기타경비 밑수는 형식에 따라 `재료비+노무비`(일반·수공)와 `직접공사비`(실적)로 갈린다
|
||||
- 이윤 밑수는 일반형식에서 `노+경+일`이다
|
||||
|
||||
### 3.4 원가계산 네 형식
|
||||
|
||||
일반 `T` · 수공 `S` · 실적일반 `TA` · 실적수공 `SA`. **출력 템플릿이 아니라 공식 집합과 합계 트리가 통째로 다르다.**
|
||||
|
||||
| 규칙 | T | S | TA | SA |
|
||||
|---|---|---|---|---|
|
||||
| 산재·고용 밑수 | 노무비 | 직노+간노 | 노무비 | 직노+간노 |
|
||||
| 기타경비 | 묶음 1개 | 세부 8개 | 묶음 1개 | 세부 8개 |
|
||||
| 일반관리비 밑수 | 순공사원가+관리품목자재 | 순공사비+간노+경비 | 직공비+간접비+관리품목자재 | 직공비+간접노무비 |
|
||||
|
||||
### 3.5 변경설계 계산모드
|
||||
|
||||
실행모듈에 산식 두 개가 그대로 있다.
|
||||
|
||||
```text
|
||||
방법 A = C_AMT_B + ROUNDDOWN(C_AMT_B × ((C_TAMT_C-C_TAMT_B)/C_TAMT_B), digits)
|
||||
방법 B = C_AMT_B + ROUNDDOWN((C_TAMT_C-C_TAMT_B) × RAT_1, digits)
|
||||
```
|
||||
|
||||
여기에 당초금액 직접입력, 계약 제잡비율 적용이 더 있고 보험료 변경은 별도 3모드다. **항목마다 계산모드를 고르는 구조**이므로 모드를 하나로 고정하면 재현할 수 없다.
|
||||
|
||||
### 3.6 기준자료 파일 구조
|
||||
|
||||
| 파일 | 형태 | 개수 |
|
||||
|---|---|---:|
|
||||
| `JB_1` | 41행 × 10열 | 1 (2011) |
|
||||
| `JB_M` | 25행 × 10열 | 1 (2007) |
|
||||
| `JB_R` 구형 | 50행 × 20열 | 9 (2015 ~ 2020-01-02) |
|
||||
| `JB_R` 신형 | 50행 × 35열 | 18 (2020-05-27 ~ 2026) |
|
||||
| `JB_RA` | 50행 × 20열 | 13 (확장 없음) |
|
||||
|
||||
- **확장 시점은 2020-05-27**이고 기존 1~20열은 **500개 셀 중 하나도 바뀌지 않았다.** 구형을 읽을 때 해석을 바꿀 필요가 없다.
|
||||
- 행은 `0,5,10,…,45` 시작의 **5행 묶음**(값 4행 + 빈 1행)이다. 설정의 공사기간 내부값이 5구간인 것과 개수가 맞는다.
|
||||
- `JB_R` 열 1~5는 간접노무비, 6~10은 기타경비이고 둘 다 `토목 / 산.설(토목) / 조경 / 건축 / 산.설(건축)` 순이다. 신형 열 21·26이 국가유산이다.
|
||||
- 프로젝트의 `RATE_*`는 외래키가 아니라 **기준파일을 복사한 뒤 수정 가능한 스냅샷**이다.
|
||||
|
||||
### 3.7 계산 설정
|
||||
|
||||
설정 폼의 선택축 **83개를 전수 확보**했다 — 공통 29, 일반 12, 수공 11, 실적일반 16, 실적수공 15. **형식마다 패널 한 벌을 따로 가지므로 설정 스냅샷도 형식별로 저장해야 한다.**
|
||||
|
||||
화면 순번과 내부값이 어긋나는 축이 7개이고, 그중 `Ed_C_MTYP`(재료비 산정방식)와 `cmBx_AnPum`(안전·품질관리비)은 **순서가 뒤집혀 있어 화면 인덱스를 그대로 저장하면 정반대 공식이 걸린다.**
|
||||
|
||||
## 4. 남은 것
|
||||
|
||||
전부 **13번 통제시험 목록**에 시험 11개(T1~T11)로 옮겼다. 우선순위 상위 다섯은 이렇다.
|
||||
|
||||
| 순위 | 시험 | 무엇을 가르나 |
|
||||
|---:|---|---|
|
||||
| 1 | T1 | 화면 그리드가 `RATE*` 행렬의 몇 번째 행부터 적재되는지 |
|
||||
| 2 | T3 | `ROUNDUP`·`MINT`의 음수 방향 |
|
||||
| 3 | T2 | 금액·기간 구간 경계의 `<` / `<=` |
|
||||
| 4 | T11 | 네 형식이 실제로 표대로 다른 밑수를 쓰는지 |
|
||||
| 5 | T4 | 안전관리비 변형의 선택 조건 |
|
||||
|
||||
## 5. 완료 판정 기준
|
||||
|
||||
| 단계 | 판정 |
|
||||
|---|---|
|
||||
| **정적 분석 완료** | ✅ 달성 — 186/186 판독, 계산 규칙 A등급 확보, 남은 경계를 시험으로 분리 |
|
||||
| 원설계 재현 | T1·T2·T3·T4·T11 통과 시 — 원 단위까지 일치 |
|
||||
| 변경설계 포함 재현 | 위 + T8·T9 통과 시 |
|
||||
| **동일 엔진 판정** | T1~T11 전부 통과 + 11번 §16 재현 시험표 전 항목 통과 |
|
||||
|
||||
「비슷한 금액」은 판정 기준이 아니다. **계산 순서와 중간값까지 같아야** 동일 엔진이다.
|
||||
|
||||
## 6. Aislo 가 가져갈 것
|
||||
|
||||
- **프로젝트 한 묶음 교환** — 입력·채택 단가 스냅샷·계산 설정·산출 결과·매니페스트를 한 파일로
|
||||
- **보고서 서식 데이터화**(`SET_REP`) — 보고서별 열 이름·폭·표시 여부를 데이터로
|
||||
- **당초/변경 비교** — DB 열을 네 벌 복제하지 말고 기존 프로젝트 버전·스냅샷으로
|
||||
- **반올림 정책을 자리마다 명시** — 하나의 전역 설정으로 두면 안 된다. STmate는 간접비·공급가액·도급액·관급 2종·총액/이윤보정을 **따로** 가진다
|
||||
- **유지할 것** — 채택 단가가 없으면 오류로 금액 확정을 막는 Aislo 방식. STmate는 0과 미입력의 구분이 흐린 자리가 있다
|
||||
|
||||
## 7. 산출물
|
||||
|
||||
### 문서 15개 (00~14)
|
||||
|
||||
전체 인덱스와 링크는 **00번**에 있다.
|
||||
|
||||
### 원자료
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `system_inventory.csv` | 186개 파일 경로·크기·SHA-256·실제 형식·상태 |
|
||||
| `core_cpm_static.json` | 핵심 CPM 12개의 PE/import/export/도메인 문자열 |
|
||||
| `system_structures.json` | 186개 전 파일 내부구조 |
|
||||
| `export_disassembly.json` | export 함수 디스어셈블 |
|
||||
| `core_formula_strings.json` | 바이너리에서 검출한 계산식 |
|
||||
| `TWM_KANJUB` · `TWM_KAN_RATE` · `TPM_KAN_RATE` (`.dfm`/`.json`) | 복원한 화면 폼 |
|
||||
| `settings_axis_map.txt` | 설정 선택축 83개 전문 |
|
||||
|
||||
### 재현 스크립트
|
||||
|
||||
| 스크립트 | 하는 일 |
|
||||
|---|---|
|
||||
| `analyze_system.py` | 186개 전수 등록·해시·형식 판정 |
|
||||
| `evidence_extract.py` · `disassemble_exports.py` | 계산식·export 추출 |
|
||||
| `parse_dfm.py` | 폼 리소스 → JSON |
|
||||
| `rounding_symbol_matrix.py` | 반올림 런타임 심볼 임포트 실측 |
|
||||
| `rounding_callsites.py` | 썽크 경유 호출 자리 집계 |
|
||||
| `rounding_dispatcher_dump.py` | 반올림 함수 바이트 덤프 |
|
||||
| `round_table.py` | 내부 ↔ Excel 반올림 경계표 |
|
||||
| `jb_shapes.py` · `jb_block_map.py` · `jb_expansion_diff.py` | JB 43개 형태·블록·확장 차이 |
|
||||
| `rate_grid_defs.py` · `settings_axis_map.py` | 폼에서 그리드·선택축 추출 |
|
||||
|
||||
모든 스크립트는 `C:\경영정보(주)`를 **읽기만** 한다.
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"CODE=\"W\" .and. (TAMT1<>MAMT1+LAMT1+SAMT1 or TAMT2<>MAMT2+LAMT2+SAMT2)": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM"
|
||||
],
|
||||
"CODE=\"W\" .and. TAMT1<>MAMT1+LAMT1+SAMT1": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM"
|
||||
],
|
||||
"(SUBSTR(CODE,1,1)$\"LMS\" and (TAMT1<>MAMT1+LAMT1+SAMT1 or TAMT2<>MAMT2+LAMT2+SAMT2)) or (CODE=\"L\" and (LAMT1<>TAMT1 or LAMT2<>TAMT2)) or (CODE=\"M\" and (MAMT1<>TAMT1 or MAMT2<>TAMT2)) or (CODE=\"S\" and (SAMT1<>TAMT1 or SAMT2<>TAMT2))": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM"
|
||||
],
|
||||
"(SUBSTR(CODE,1,1)$\"LMS\" and TAMT1<>MAMT1+LAMT1+SAMT1) or (CODE=\"L\" and LAMT1<>TAMT1) or (CODE=\"M\" and MAMT1<>TAMT1) or (CODE=\"S\" and SAMT1<>TAMT1)": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM"
|
||||
],
|
||||
"ROUNDDOWN( [LAMT] * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([L_AMT][+LAB]) * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"[C_AMT_B]+ROUNDDOWN( [C_AMT_B] * (([C_TAMT_C]-[C_TAMT_B])/[C_TAMT_B]), ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"+ROUNDDOWN( ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"[C_AMT_B]+ROUNDDOWN( ([C_TAMT_C]-[C_TAMT_B]) * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( [C_KUNBO] * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([LAMT][+LAB]) * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN(": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"IF(([LAMT]+[M_AMT])>ROUNDDOWN(": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"*[TAMT],0),ROUNDDOWN(": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ( ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"IF(([L_AMT]+[M_AMT])>ROUNDDOWN(": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ((": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([TAMT]": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([T_AMT]+": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( [T_AMT] * [RAT_1], ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( [T_AMT] * [RAT_1], 0 )": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([T_AMT]-[M_AMT]) * [RAT_1], 0 )": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*1.4,0) + ROUNDDOWN([LAMT]*[RAT_KY]*[RAT_3]/100,0) + ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*[RAT_4]/100,0), ": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( ([M_AMT]+[L_AMT]+[S_AMT]": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN([T_AMT] * [RAT_1], 0)": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN([M_AMT] * [RAT_1], 0)": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN(([TAMT]-[LAMT]": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUND( [C_AD_AMT] , -": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN( [C_AD_AMT] , -": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDUP( [C_AD_AMT] , -": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"ROUNDDOWN([T_AMT]": [
|
||||
"ST_CPM\\ST_Boq.CPM",
|
||||
"ST_CPM\\ST_EdiLWi.CPM",
|
||||
"ST_CPM\\ST_KanJub.CPM",
|
||||
"ST_CPM\\ST_P_Collect.CPM",
|
||||
"ST_CPM\\ST_P_ConEst.CPM",
|
||||
"ST_CPM\\ST_P_Joong.CPM",
|
||||
"ST_CPM\\ST_SanCal.CPM",
|
||||
"ST_CPM\\ST_U_ChgAmt.CPM",
|
||||
"ST_CPM\\ST_U_ChgCode.CPM",
|
||||
"ST_CPM\\ST_U_ConBD.CPM",
|
||||
"ST_CPM\\ST_U_Convert.CPM",
|
||||
"ST_CPM\\STmateQty8.CPM"
|
||||
],
|
||||
"( SUBSTR(BDCODE1,1,1)$\"MLSW\" .and. \"%\"$UNIT1 ) .or. ( SUBSTR(BDCODE1,1,1)$\"MLSWBDXA\" .and. SUBSTR(STR(QTY1,20,4),LEN(STR(QTY1,20,4)),1)<>\"0\" )": [
|
||||
"ST_CPM\\ST_Boq.CPM"
|
||||
],
|
||||
"(SUBSTR(BDCODE1,1,1)$\"TEC\" or (QTY": [
|
||||
"ST_CPM\\ST_U_ConBD.CPM"
|
||||
],
|
||||
"ROUND(": [
|
||||
"ST_CPM\\ST_U_Convert.CPM"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,565 @@
|
||||
### 원가계산 설정 — 형식별 선택축
|
||||
|
||||
====================================================================================================
|
||||
## 공통 선택축 29개
|
||||
|
||||
■ Cmbx_PORD1_H (Panel1) 2개
|
||||
0 당초 / 변경 대비인쇄
|
||||
1 변 경 분 만 인 쇄
|
||||
|
||||
■ cBx_K_JeolSa (Panel1) 7개
|
||||
0 간접비 절사없음
|
||||
1 간접비 10원미만 절사
|
||||
2 간접비 100원미만 절사
|
||||
3 간접비 1,000원미만 절사
|
||||
4 간접비 10,000원미만 절사
|
||||
5 간접비 100,000원미만 절사
|
||||
6 간접비 1,000,000원미만 절사
|
||||
|
||||
■ cBx_Chasu (Panel1) 4개
|
||||
0 원설계
|
||||
1 1차변경
|
||||
2 2차변경
|
||||
3 3차변경
|
||||
|
||||
■ cBx_W_Type (Panel1) 2개 ※화면≠내부
|
||||
0 제잡비 원가계산방식
|
||||
1 제잡비 표준시장방식 → 내부 `제잡비 실적공사방식`
|
||||
|
||||
■ cmBx_Kobo_Type (Panel3) 8개
|
||||
0 없음
|
||||
1 1 등급
|
||||
2 2 등급
|
||||
3 3 등급
|
||||
4 4 등급
|
||||
5 5 등급
|
||||
6 6 등급
|
||||
7 7 등급이하
|
||||
|
||||
■ cmBx_JAB_TYPE (Panel3) 6개
|
||||
0 일반 형식
|
||||
1 토지개발공사 형식
|
||||
2 인천상수도 형식
|
||||
3 수자원공사 형식
|
||||
4 지역난방공사 형식
|
||||
5 고용개선지원(서울)
|
||||
|
||||
■ cmBx_Kong_Type (Panel3) 8개 ※화면≠내부
|
||||
0 (1) 일반(토목)공사 → 내부 `0`
|
||||
1 (2) 조경공사 → 내부 `2`
|
||||
2 (3) 산업설비(토목) → 내부 `4`
|
||||
3 (4) 건축공사 → 내부 `3`
|
||||
4 (5) 산업설비(건축) → 내부 `1`
|
||||
5 (6) 국가유산수리 → 내부 `5`
|
||||
6 (7) 폐기물처리 → 내부 `8`
|
||||
7 (8) 소규모용역 → 내부 `9`
|
||||
|
||||
■ cmBx_JeolSa (Panel3) 9개
|
||||
0 총공사비에서 조정
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cmBx_Buga (Panel3) 5개
|
||||
0 (0) 공급가액의 10%
|
||||
1 (1) 재료비의 10%
|
||||
2 (2) 없음
|
||||
3 (3) 산림조합 형식
|
||||
4 (4) 산림조합-면세품
|
||||
|
||||
■ cmBx_TBugum (Panel3) 4개
|
||||
0 토목공사
|
||||
1 준설공사
|
||||
2 건축공사
|
||||
3 기타
|
||||
|
||||
■ cmBx_Hwan (Panel3) 15개
|
||||
0 ◈ 없음(전기, 정보통신, 소방시설, 국가유산수리)
|
||||
1 ◈ 도로(교량, 터널, 활주로)
|
||||
2 ◈ 플랜트(발전소, 쓰레기소각로)
|
||||
3 ◈ 지하철
|
||||
4 ◈ 철도
|
||||
5 ◈ 상하수도(폐수, 하수처리장, 정수장)
|
||||
6 ◈ 항만(오탁, 준설토, 방지막설치필요시(간척, 준설))
|
||||
7 ◈ 항만(방지막설치불필요시(간척, 준설))
|
||||
8 ◈ 댐
|
||||
9 ◈ 택지개발
|
||||
10 ◈ 주택(재개발, 재건축)
|
||||
11 ◈ 주택(신축)
|
||||
12 ◈ 주택(재개발, 재건축, 주택(신축))외 건축
|
||||
13 ◈ 조경
|
||||
14 ◈ 기타토목(하천등)
|
||||
|
||||
■ cmBx_AnJun_Type (Panel3) 4개 ※화면≠내부
|
||||
0 건축공사 → 내부 `0`
|
||||
1 토목공사 → 내부 `1`
|
||||
2 중건설공사 → 내부 `3`
|
||||
3 특수건설공사 → 내부 `4`
|
||||
|
||||
■ cmBx_Kwan_Sa (Panel3) 8개
|
||||
0 관급자재대/1.1
|
||||
1 관급자재대
|
||||
2 사급자재대/1.1
|
||||
3 사급자재대
|
||||
4 관급/1.1+사급재
|
||||
5 관급재+사급/1.1
|
||||
6 (관급+사급)/1.1
|
||||
7 관급재+사급재
|
||||
|
||||
■ cmBx_Pos_Sagup (Panel3) 4개 ※화면≠내부
|
||||
0 재료비에 포함 → 내부 `3`
|
||||
1 이윤 다음 → 내부 `1`
|
||||
2 도급공사비 직전 → 내부 `2`
|
||||
3 관급자재대 다음 → 내부 `0`
|
||||
|
||||
■ cmBx_Kun_Type (Panel3) 7개
|
||||
0 청사(11층이하)
|
||||
1 청사(12층이상)
|
||||
2 학교
|
||||
3 국가유산
|
||||
4 관람집회시설
|
||||
5 전시시설
|
||||
6 기타
|
||||
|
||||
■ cmBx_KONGSU (Panel3) 2개 ※화면≠내부
|
||||
0 일반계약 → 내부 `0`
|
||||
1 최저가,기술제안 → 내부 `1`
|
||||
|
||||
■ cmBx_Set_Base (Panel3) 3개 ※화면≠내부
|
||||
0 일반(국가계약법) → 내부 `2`
|
||||
1 일반(지방계약법) → 내부 `0`
|
||||
2 행자부기준 → 내부 `1`
|
||||
|
||||
■ cmBx_Kong_Kikan (Panel3) 4개 ⚠개수불일치(선택지4/내부값5) ※화면≠내부
|
||||
0 6개월이하 → 내부 `30일 미만`
|
||||
1 7개월 ~ 12개월이하 → 내부 `6개월이하`
|
||||
2 13개월 ~ 36개월이하 → 내부 `7개월 ~ 12개월이하`
|
||||
3 36개월이상 → 내부 `13개월 ~ 36개월이하`
|
||||
▶ 화면에 없는 내부값: ['37개월이상']
|
||||
|
||||
■ cBx_iLBan (Panel3) 2개
|
||||
0 (주)공사
|
||||
1 전문공사
|
||||
|
||||
■ cmBx_EQIP (Panel3) 7개 ※화면≠내부
|
||||
0 ◈ 없음(국가유산수리공사) → 내부 `-1`
|
||||
1 ◈ 일반공사(토목,산업설비,조경,건축) → 내부 `0`
|
||||
2 ◈ 준설, 포장공사, 토공사, 비계·구조물해제 → 내부 `1`
|
||||
3 ◈ 상하수도설비, 보링·그라우팅, 수중공사 → 내부 `2`
|
||||
4 ◈ 석공사, 시설물유지관리, 철근·콘크리트, 가스시설공사(1종) → 내부 `3`
|
||||
5 ◈ 조경시설물설치, 조경식재, 철도궤도, 도장공사, 철강재설치공사 → 내부 `4`
|
||||
6 ◈ 기계설비공사업 및 그외 → 내부 `5`
|
||||
|
||||
■ cmBx_ANJUN (pnSet_Optn) 2개 ※화면≠내부
|
||||
0 사용자 수동계산 → 내부 `0`
|
||||
1 제비율표자동계산 → 내부 `1`
|
||||
|
||||
■ cmBx_Buga1 (pnSet_Optn) 5개 ※화면≠내부
|
||||
0 (0) 공급가액의 10%
|
||||
1 (1) 재료비의 10%
|
||||
2 (2) 없음
|
||||
3 (3) 산림조합 형식 → 내부 `(3) 임 협 식`
|
||||
4 (4) 산림조합-면세품
|
||||
|
||||
■ cmBx_Kwan_Sa1 (pnSet_Optn) 8개
|
||||
0 관급자재대/1.1
|
||||
1 관급자재대
|
||||
2 사급자재대/1.1
|
||||
3 사급자재대
|
||||
4 관급/1.1+사급재
|
||||
5 관급재+사급/1.1
|
||||
6 (관급+사급)/1.1
|
||||
7 관급재+사급재
|
||||
|
||||
■ cBx_Dog_Jeol (pnSet_Optn) 9개
|
||||
0 도급액 버림없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_CH_BO (pn_CH_BO) 3개 ※화면≠내부
|
||||
0 직접노무비 x 요율 → 내부 `0`
|
||||
1 당초액 + 직노증감x요율 → 내부 `1`
|
||||
2 당초액 + 증감액(비율) → 내부 `2`
|
||||
|
||||
■ cmBx_Anjun_DN (pnSet_Optn) 2개 ※화면≠내부
|
||||
0 소수 버림 후 1.2배 → 내부 `0`
|
||||
1 소수 버림 전 1.2배 → 내부 `1`
|
||||
|
||||
■ cmBx_AnJun_Type1 (pnSet_Optn) 4개 ※화면≠내부
|
||||
0 건축공사 → 내부 `0`
|
||||
1 토목공사 → 내부 `1`
|
||||
2 중건설공사 → 내부 `3`
|
||||
3 특수건설공사 → 내부 `4`
|
||||
|
||||
■ cmBx_AnPum (pnSet_Optn) 2개 ※화면≠내부
|
||||
0 산출경비로 포함 → 내부 `1`
|
||||
1 산출경비로 미포함 → 내부 `0`
|
||||
|
||||
■ cmBx_Kwan_Comb (tabAdd_BSC) 2개 ※화면≠내부
|
||||
0 (0) 분리인쇄 → 내부 `0`
|
||||
1 (1) 통합인쇄 → 내부 `1`
|
||||
|
||||
====================================================================================================
|
||||
## 일반 T 선택축 12개
|
||||
|
||||
■ cBx_Jeol_T (Pn03_T) 9개
|
||||
0 총공사비에서 조정
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ Ed_C_AnJun_K_T (Pn01_T) 3개
|
||||
0 없음
|
||||
1 관급/사급 내용
|
||||
2 사급자재대
|
||||
|
||||
■ Ed_C_AnJun_B_T (Pn01_T) 2개
|
||||
0
|
||||
1 의 1.2배
|
||||
|
||||
■ Ed_C_KUNBO_T (Pn01_T) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_YUNBO_T (Pn01_T) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_IYUN_T (pn02_T) 2개 ※화면≠내부
|
||||
0 이윤 → 내부 `0`
|
||||
1 수수료 → 내부 `1`
|
||||
|
||||
■ Ed_C_DOGUP_T (Pn04_T) 2개 ※화면≠내부
|
||||
0 도급공사비 → 내부 `0`
|
||||
1 도급예정액 → 내부 `1`
|
||||
|
||||
■ cBx_KWN_Jeol_T (Pn04_T) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_UPDN_T (Pn04_T) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_KWN_K_Jeol_T (Pn04_T) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_K_UPDN_T (Pn04_T) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_Tot_Jeol_T (Pn05_T) 9개
|
||||
0 이윤보정액 입력
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
====================================================================================================
|
||||
## 수공 S 선택축 11개
|
||||
|
||||
■ cBx_Jeol_S (Pn03_S) 9개
|
||||
0 총공사비에서 조정
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ Ed_C_AnJun_K_S (Pn01_S) 2개
|
||||
0 없음
|
||||
1 관급/사급 내용
|
||||
|
||||
■ Ed_C_AnJun_B_S (Pn01_S) 2개
|
||||
0
|
||||
1 의 1.2배
|
||||
|
||||
■ Ed_C_KUNBO_S (Pn01_S) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_YUNBO_S (Pn01_S) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_DOGUP_S (Pn04_S) 2개 ※화면≠내부
|
||||
0 도급공사비 → 내부 `0`
|
||||
1 도급예정액 → 내부 `1`
|
||||
|
||||
■ cBx_KWN_Jeol_S (Pn04_S) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_UPDN_S (Pn04_S) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_KWN_K_Jeol_S (Pn04_S) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_K_UPDN_S (Pn04_S) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_Tot_Jeol_S (Pn05_S) 9개
|
||||
0 이윤보정액 입력
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
====================================================================================================
|
||||
## 실적일반 TA 선택축 16개
|
||||
|
||||
■ cBx_Jeol_TA (Pn03_TA) 9개
|
||||
0 총공사비에서 조정
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ Ed_C_AnJun_K_TA (Pn01_TA) 3개
|
||||
0 없음
|
||||
1 관급/사급 내용
|
||||
2 사급자재대
|
||||
|
||||
■ Ed_C_AnJun_B_TA (Pn01_TA) 2개
|
||||
0
|
||||
1 의 1.2배
|
||||
|
||||
■ Ed_C_KUNBO_TA (Pn01_TA) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_YUNBO_TA (Pn01_TA) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_LTYP_TA (Pn01_TA) 2개
|
||||
0 노무비 = 직공비 x 조정계수
|
||||
1 노무비 = 내역서 직접노무비
|
||||
|
||||
■ Ed_C_RateMxA_TA (Pn01_TA) 3개
|
||||
0 상한비 자동계산
|
||||
1 상한비 강제적용
|
||||
2 상한비 적용안함
|
||||
|
||||
■ Ed_C_RateMxK_TA (Pn01_TA) 3개
|
||||
0 상한비 자동계산
|
||||
1 상한비 강제적용
|
||||
2 상한비 적용안함
|
||||
|
||||
■ Ed_C_MTYP_TA (Pn01_TA) 2개 ※화면≠내부
|
||||
0 직접공사비 * 요율 방식 → 내부 `1`
|
||||
1 조정계수 입력방식 → 내부 `0`
|
||||
|
||||
■ Ed_C_IYUN_TA (pn02_TA) 2개 ※화면≠내부
|
||||
0 이윤 → 내부 `0`
|
||||
1 수수료 → 내부 `1`
|
||||
|
||||
■ Ed_C_DOGUP_TA (Pn04_TA) 2개 ※화면≠내부
|
||||
0 도급공사비 → 내부 `0`
|
||||
1 도급예정액 → 내부 `1`
|
||||
|
||||
■ cBx_KWN_Jeol_TA (Pn04_TA) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_UPDN_TA (Pn04_TA) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_KWN_K_Jeol_TA (Pn04_TA) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_K_UPDN_TA (Pn04_TA) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_Tot_Jeol_TA (Pn05_TA) 9개
|
||||
0 이윤보정액 입력
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
====================================================================================================
|
||||
## 실적수공 SA 선택축 15개
|
||||
|
||||
■ cBx_Jeol_SA (Pn03_SA) 9개
|
||||
0 총공사비에서 조정
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ Ed_C_AnJun_K_SA (Pn01_SA) 2개
|
||||
0 없음
|
||||
1 관급/사급 내용
|
||||
|
||||
■ Ed_C_AnJun_B_SA (Pn01_SA) 2개
|
||||
0
|
||||
1 의 1.2배
|
||||
|
||||
■ Ed_C_KUNBO_SA (Pn01_SA) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_YUNBO_SA (Pn01_SA) 2개 ※화면≠내부
|
||||
0 ㄴ.1)직노 → 내부 `ㄴ.1)직접노무비`
|
||||
1 당해 상시근로자노무비
|
||||
|
||||
■ Ed_C_LTYP_SA (Pn01_SA) 2개
|
||||
0 노무비 = 직공비 x 조정계수
|
||||
1 노무비 = 내역서 직접노무비
|
||||
|
||||
■ Ed_C_RateMxA_SA (Pn01_SA) 3개
|
||||
0 상한비 자동계산
|
||||
1 상한비 강제적용
|
||||
2 상한비 적용안함
|
||||
|
||||
■ Ed_C_RateMxK_SA (Pn01_SA) 3개
|
||||
0 상한비 자동계산
|
||||
1 상한비 강제적용
|
||||
2 상한비 적용안함
|
||||
|
||||
■ Ed_C_MTYP_SA (Pn01_SA) 2개 ※화면≠내부
|
||||
0 직접공사비 * 요율 방식 → 내부 `1`
|
||||
1 조정계수 입력방식 → 내부 `0`
|
||||
|
||||
■ Ed_C_DOGUP_SA (Pn04_SA) 2개 ※화면≠내부
|
||||
0 도급공사비 → 내부 `0`
|
||||
1 도급예정액 → 내부 `1`
|
||||
|
||||
■ cBx_KWN_Jeol_SA (Pn04_SA) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_UPDN_SA (Pn04_SA) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_KWN_K_Jeol_SA (Pn04_SA) 9개
|
||||
0 절상(절사) 없음
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
|
||||
■ cBx_KWN_K_UPDN_SA (Pn04_SA) 3개
|
||||
0 절상
|
||||
1 반올림
|
||||
2 절사
|
||||
|
||||
■ cBx_Tot_Jeol_SA (Pn05_SA) 9개
|
||||
0 이윤보정액 입력
|
||||
1 10원 미만
|
||||
2 100원 미만
|
||||
3 1,000원 미만
|
||||
4 10,000원 미만
|
||||
5 100,000원 미만
|
||||
6 1,000,000원 미만
|
||||
7 10,000,000원 미만
|
||||
8 100,000,000원 미만
|
||||
@@ -0,0 +1,503 @@
|
||||
Call DeactivateFontsInfo before calling this.
|
||||
Background
|
||||
Foreground
|
||||
Foreground@
|
||||
ctHalfBlock
|
||||
eoHalfPageScroll
|
||||
TBdQtyRecP
|
||||
QTY1
|
||||
QTY2
|
||||
QTY3
|
||||
QTY4
|
||||
TAMT1
|
||||
LAMT1
|
||||
MAMT1
|
||||
SAMT1
|
||||
RATE
|
||||
KWNGP
|
||||
EQCODE
|
||||
X_RATE
|
||||
X_QTY1
|
||||
X_QTY2
|
||||
X_QTY3
|
||||
X_HAL1
|
||||
X_HAL2
|
||||
X_HAL3
|
||||
S_RATE
|
||||
select
|
||||
NAG_HAL
|
||||
ST_MHAL
|
||||
EQ_LHAL
|
||||
EQ_MHAL
|
||||
EQ_SHAL
|
||||
BDQTY.DBF
|
||||
LAMT
|
||||
MAMT
|
||||
SAMT
|
||||
TAMT
|
||||
RATE_L
|
||||
X_QTY
|
||||
X_HAL
|
||||
IFORM
|
||||
CODE="W" .and. (TAMT1<>MAMT1+LAMT1+SAMT1 or TAMT2<>MAMT2+LAMT2+SAMT2)
|
||||
CODE="W" .and. TAMT1<>MAMT1+LAMT1+SAMT1
|
||||
TAMT2
|
||||
MAMT2
|
||||
LAMT2
|
||||
SAMT2
|
||||
(SUBSTR(CODE,1,1)$"LMS" and (TAMT1<>MAMT1+LAMT1+SAMT1 or TAMT2<>MAMT2+LAMT2+SAMT2)) or (CODE="L" and (LAMT1<>TAMT1 or LAMT2<>TAMT2)) or (CODE="M" and (MAMT1<>TAMT1 or MAMT2<>TAMT2)) or (CODE="S" and (SAMT1<>TAMT1 or SAMT2<>TAMT2))
|
||||
(SUBSTR(CODE,1,1)$"LMS" and TAMT1<>MAMT1+LAMT1+SAMT1) or (CODE="L" and LAMT1<>TAMT1) or (CODE="M" and MAMT1<>TAMT1) or (CODE="S" and SAMT1<>TAMT1)
|
||||
BDQTY
|
||||
S_ADJ
|
||||
ADJ_L
|
||||
ADJ_M
|
||||
ADJ_S
|
||||
COSTN.DBF
|
||||
Find_BOQ
|
||||
Find_BOQ
|
||||
QTY1_B
|
||||
QTY1_N
|
||||
QTY1_H
|
||||
Calc Boq
|
||||
CON_USD*.DBF
|
||||
CON_QTY*.DBF
|
||||
.DBF
|
||||
STmateQty8.CPM
|
||||
ST_QTY
|
||||
Free_Qty
|
||||
EQSYNC.DBF
|
||||
CPM_INFO.DBF
|
||||
CPM_INFO.SMT
|
||||
CPM_INFO_$.DBF
|
||||
KBOQ
|
||||
PRJLST.DBF
|
||||
COSTA.DBF
|
||||
SYSINFO.DBF
|
||||
Select * from Win32_NetworkAdapterConfiguration Where IPEnabled=TRUE
|
||||
Excel.Application
|
||||
Microsoft Excel
|
||||
Excel
|
||||
;COSTK.DBF;COSTN1.DBF;BDQTYD.DBF;BDQTYL.DBF;BDQTYM.DBF;BBOQ.DBF;BOQC.DBF;BOQJ.DBF;BOQM.DBF;BRJ.DBF;PRJ.DBF;ITQTY.DBF;MITQTY.DBF;UITQTY.DBF;PRJLST1.DBF;CHANGE.DBF;IMGINFO.DBF;ILWI.DBF;SAN.DBF;PRLMTM.DBF;PRNDB1.DBF;RATE_A.DBF;RATE_B.DBF;SEED.DBF;START.INI;
|
||||
GrdBoqBigo
|
||||
C_PSAMT_B
|
||||
B_PSAMT_B
|
||||
I_NAG_RATE
|
||||
ROUNDDOWN( [LAMT] * [RAT_1],
|
||||
ROUNDDOWN( ([L_AMT][+LAB]) * [RAT_1],
|
||||
[C_AMT_B]+ROUNDDOWN( [C_AMT_B] * (([C_TAMT_C]-[C_TAMT_B])/[C_TAMT_B]),
|
||||
[C_TAMT_B]
|
||||
+ROUNDDOWN(
|
||||
(([LAMT][+LAB])-
|
||||
[C_AMT_B]+ROUNDDOWN( ([C_TAMT_C]-[C_TAMT_B]) * [RAT_1],
|
||||
ROUNDDOWN(
|
||||
([LAMT][+LAB])
|
||||
ROUNDDOWN( [C_KUNBO] * [RAT_1],
|
||||
ROUNDDOWN( ([LAMT][+LAB]) * [RAT_1],
|
||||
[LAMT][+LAB]
|
||||
([TAMT])
|
||||
([LAMT]+[M_AMT]
|
||||
ROUNDDOWN(
|
||||
*[TAMT],0)
|
||||
IF(([LAMT]+[M_AMT])>ROUNDDOWN(
|
||||
*[TAMT],0),ROUNDDOWN(
|
||||
*[TAMT],0),([LAMT]+[M_AMT]))
|
||||
ROUNDDOWN( (
|
||||
IF(([L_AMT]+[M_AMT])>ROUNDDOWN(
|
||||
*[TAMT],0),([L_AMT]+[M_AMT]))
|
||||
ROUNDDOWN( ((
|
||||
+([TAMT]
|
||||
ROUNDDOWN( ([TAMT]
|
||||
ROUNDDOWN( ([T_AMT]+
|
||||
ROUNDDOWN( [T_AMT] * [RAT_1],
|
||||
ROUNDDOWN( [T_AMT] * [RAT_1], 0 )
|
||||
ROUNDDOWN( ([T_AMT]-[M_AMT]) * [RAT_1], 0 )
|
||||
ROUNDDOWN( ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*1.4,0) + ROUNDDOWN([LAMT]*[RAT_KY]*[RAT_3]/100,0) + ROUNDDOWN([LAMT]*[RAT_1]*[RAT_2]/100*[RAT_4]/100,0),
|
||||
ROUNDDOWN( ([M_AMT]+[L_AMT]+[S_AMT]
|
||||
ROUNDDOWN([T_AMT] * [RAT_1], 0)
|
||||
ROUNDDOWN([M_AMT] * [RAT_1], 0)
|
||||
ROUNDDOWN(([TAMT]-[LAMT]
|
||||
ROUND( [C_AD_AMT] , -
|
||||
ROUNDDOWN( [C_AD_AMT] , -
|
||||
ROUNDUP( [C_AD_AMT] , -
|
||||
ROUNDDOWN([T_AMT]
|
||||
halH
|
||||
FormActivate
|
||||
AddSeparateDirs
|
||||
Please insert last disk in set
|
||||
Please insert disk volume %.1d of %.1d
|
||||
Please insert disk volume %.1d
|
||||
TBackgroundLayout
|
||||
BackgroundLayout
|
||||
tb_BdQtyx
|
||||
ds_BdQty
|
||||
tb_BdQty_X
|
||||
ds_BdQty_X
|
||||
tb_BdQty_Im
|
||||
tb_CostA
|
||||
ds_CostA
|
||||
Grd_Rate
|
||||
gBx_Rate0
|
||||
Act_Set_RateP
|
||||
cBx_SetARate
|
||||
Grd_RateDrawCell
|
||||
Grd_RateColumnTitleClick
|
||||
Grd_RateDblClick
|
||||
Grd_RateKeyDown
|
||||
Grd_RateCellExit
|
||||
Grd_RateExit
|
||||
Grd_RateBeforePost
|
||||
Grd_RateCellEnter
|
||||
Grd_RateRowChange
|
||||
Act_Set_RateExecute
|
||||
Grd_RateEnter
|
||||
cBx_SetARateClick
|
||||
LAMT1_R
|
||||
CODE="W" .and. (LEN(TRIM(BQCODE))>5 .or. LEN(TRIM(EQCODE))>5)
|
||||
TwM_Xls_Comp
|
||||
TwM_Xls_ComplpN
|
||||
Xls_Comp
|
||||
COMP_C.DBF
|
||||
POS_D_XLS_COMP
|
||||
MAMT1_O
|
||||
PG_11_O
|
||||
PG_21_O
|
||||
PG_31_O
|
||||
PG_41_O
|
||||
PG_51_O
|
||||
EQCODE_O
|
||||
PG_1_O
|
||||
PG_11
|
||||
PG_21
|
||||
PG_31
|
||||
PG_41
|
||||
PG_51
|
||||
PG_1
|
||||
LAMT1_O
|
||||
SAMT1_O
|
||||
TAMT1_O
|
||||
PG_2
|
||||
PG_3
|
||||
PG_4
|
||||
PG_5
|
||||
ed_EQCODE,
|
||||
"$UPPER(EQCODE))
|
||||
"$UPPER(JDCODE+NAME+SIZE+UNIT+GNCODE+PSCODE+EQCODE+ST_NAME)
|
||||
wBx_MAMT
|
||||
ed_Hal
|
||||
Act_Set_Hal
|
||||
ed_HalEnter
|
||||
ed_HalKeyDown
|
||||
Act_Set_HalExecute
|
||||
*.STBSC
|
||||
;NAME;SIZE;UNIT;BQCODE;EQCODE;EDT_DATE;JDCODE;
|
||||
BSC_COSTN.DBF
|
||||
BSC_BDQTY.DBF
|
||||
BSC_COSTN.*
|
||||
BSC_BDQTY.*
|
||||
CostNavi
|
||||
Act_FindBoq
|
||||
sBtnLstEqCode
|
||||
Act_LstEqCode
|
||||
Act_Ed_Qty<
|
||||
pn_LHal
|
||||
pn_MHal
|
||||
sBtnHaLmCost
|
||||
Chk_Auto_Hal
|
||||
pn_XHal
|
||||
sBtnHaLKike
|
||||
Pop_Rate_L
|
||||
Pop_Rate_6
|
||||
mi_SetAmt6
|
||||
mi_Edt_Amt6_Rate
|
||||
mi_HalLt
|
||||
mi_Ed_HalDx
|
||||
mi_Ed_HalL
|
||||
mi_View_HalL
|
||||
Act_FindBoqExecute
|
||||
Act_Ed_QtyExecute
|
||||
Rate_LClick
|
||||
Chk_Auto_HalClick
|
||||
mi_View_HalLClick
|
||||
Auto_Hal
|
||||
PRT.DBF
|
||||
ADJ_
|
||||
"$UPPER(CODE+NAME+SIZE+UNIT+BQCODE+EQCODE+JDCODE+EDT_DATE
|
||||
Edt_Hal_Rate
|
||||
Edt_Hal_Amt
|
||||
.CSTA;*_
|
||||
.CSTA
|
||||
CSTA
|
||||
COSTA.CSTA
|
||||
P_XLS.CPM
|
||||
MAKE_XLS
|
||||
MAKE_XLS
|
||||
SHOW_XLS
|
||||
SHOW_XLS
|
||||
FREE_XLS
|
||||
FREE_XLS
|
||||
)= ROUNDDOWN(
|
||||
= ROUNDDOWN(
|
||||
tb_BdQtyh
|
||||
ds_BdQtyx
|
||||
tb_BdQty_X|
|
||||
mBtn_EDBOQ$
|
||||
mBtn_EDBOQClick
|
||||
.dbf
|
||||
CostNaviP
|
||||
sBtnLstEqCode|
|
||||
ActFindBoq1
|
||||
Act_LstEqCodeExecute
|
||||
"$UPPER(CODE+NAME+SIZE+UNIT+BQCODE+EQCODE+JDCODE+EDT_DATE+CONTCODE
|
||||
tb_BdQty`
|
||||
tb_Qty
|
||||
FormDeactivate
|
||||
CON_QTY9
|
||||
CON_QTY
|
||||
CONBD_USD_QTY
|
||||
/select,"
|
||||
OnActivate@
|
||||
OnDeactivate
|
||||
CostNavi|
|
||||
tb_BdQty
|
||||
Act_W2SMt
|
||||
TwM_JajeXLS
|
||||
JajeXLS
|
||||
tb_Boq_
|
||||
rGrd_Boq
|
||||
CBoxSetHalMLSW
|
||||
CBoxSetHal_NBUGAM
|
||||
rGrd_BoqCellEnter
|
||||
rGrd_BoqDblClick
|
||||
rGrd_BoqDrawCell
|
||||
rGrd_BoqExit
|
||||
rGrd_BoqKeyDown
|
||||
rGrd_BoqRowChange
|
||||
MK_CONT_BOQ_
|
||||
Conv Boq
|
||||
ActNewBoq
|
||||
ActMkBoq,
|
||||
ActEditBoq<
|
||||
mi_MkXLST
|
||||
Act_CalBoq
|
||||
Act_Mk_Boq10
|
||||
Mk_Xls2Boq
|
||||
mi_DefXls_GS
|
||||
Mk_DefXls
|
||||
tb_BoqXXP
|
||||
ds_BoqXXT
|
||||
RDbGrid_Boq\
|
||||
mi_View_BOQp
|
||||
sBtn_View_BOQt
|
||||
Act_View_BOQ8
|
||||
MkBoq
|
||||
CopyBoq
|
||||
ActEditBoqExecute
|
||||
ActMkBoqExecute
|
||||
Act_CalBoqExecute
|
||||
Mk_DefXlsExecute
|
||||
Mk_Xls2BoqExecute
|
||||
RDbGrid_BoqDrawCell
|
||||
Act_View_BOQExecute
|
||||
POS_F_PRJLST
|
||||
(STmate8x)\BOQ_INFO
|
||||
MAMT3
|
||||
LAMT3
|
||||
SAMT3
|
||||
TAMT3
|
||||
qty1
|
||||
BOQ.dbf
|
||||
ST_Boq.CPM
|
||||
Edit_Boq
|
||||
.xls
|
||||
ComP_CurBOQ
|
||||
MK_BSCBOQ_EXL
|
||||
BOQ.DBF
|
||||
MK_EXL_BSCBOQ
|
||||
BOQ_E.DBF
|
||||
TAMT4
|
||||
update
|
||||
UhalX
|
||||
lbTax_Email
|
||||
ed_Tax_Email
|
||||
VAT_C
|
||||
VAT_C5
|
||||
VAT_C10
|
||||
VAT_C15
|
||||
VAT_G
|
||||
VAT_G5
|
||||
VAT_G10
|
||||
VAT_G15
|
||||
TAX_EMAIL
|
||||
#,#(VAT)
|
||||
( Tax:
|
||||
tb_PrjLst
|
||||
img_Lst_PrjLst
|
||||
List_PrjLst
|
||||
:COSTN.DBF:
|
||||
:COSTN.SMT:
|
||||
:COSTA.DBF:
|
||||
:BDQTY.DBF:
|
||||
:PRJLST.DBF:
|
||||
:PRJLST.SMT:
|
||||
:SYSINFO.DBF:
|
||||
PRJLST.SMT
|
||||
:BOQ
|
||||
.DBF:
|
||||
COSTN.SMT
|
||||
COSTN.NSX
|
||||
COSTA.NSX
|
||||
BDQTY.NSX
|
||||
PRJLST.NSX
|
||||
DOWN_LST.DBF
|
||||
DOWN_LST.SMT
|
||||
UPDATE
|
||||
VAT_P
|
||||
VAT_P5
|
||||
VAT_P10
|
||||
VAT_P15
|
||||
VAT_S
|
||||
VAT_S1
|
||||
Act_PrjLst0
|
||||
Act_Halt
|
||||
tb_Mrg_COSTN
|
||||
mi_Sel_Xls
|
||||
mi_UseExcel
|
||||
Act_PrjLstExecute
|
||||
Act_HaltExecute
|
||||
Act_Use_Xls_Execute
|
||||
mi_Sel_XlsClick
|
||||
ST_CPM\ST_Boq
|
||||
ST_CPM\ST_P_XLS
|
||||
ST_PLIST.DBF
|
||||
SET_REP.DBF
|
||||
QCODE.DBF
|
||||
STmate-Exec_Merge_ADD_BOQ
|
||||
MRG_COSTN.DBF
|
||||
MRG_BDQTY.DBF
|
||||
MRG_IMG.DBF
|
||||
MRG_COSTA.DBF
|
||||
MRG_QCODE.DBF
|
||||
MRG_BOQ.DBF
|
||||
MRG_COSTN.*
|
||||
MRG_COSTA.*
|
||||
MRG_BDQTY.*
|
||||
MRG_BOQ.*
|
||||
(STmate8x)\BOQ_INFO\
|
||||
PRJ_DBF
|
||||
PRJ_SMT
|
||||
ST_PLIST.SMT
|
||||
ST_PLIST_DBF
|
||||
ST_PLIST_SMT
|
||||
sAmTb
|
||||
@System@@Halt0$qqrv
|
||||
@System@@ROUND$qqrv
|
||||
SetForegroundWindow
|
||||
GetForegroundWindow
|
||||
CreateHalftonePalette
|
||||
@Forms@TCustomForm@WMActivate$qqrr20Messages@TWMActivate
|
||||
@Forms@TScrollingWinControl@AdjustClientRect$qqrr11Types@TRect
|
||||
@Classes@TBasicActionLink@Update$qqrv
|
||||
@Classes@TStringList@Insert$qqrix17System@AnsiString
|
||||
@Classes@TStringList@Delete$qqri
|
||||
@Classes@TCollection@Delete$qqri
|
||||
@Classes@TCollection@Update$qqrp23Classes@TCollectionItem
|
||||
@Classes@TCollection@Insert$qqri
|
||||
@Classes@TList@Insert$qqripv
|
||||
@Classes@TList@Delete$qqri
|
||||
@Math@RoundTo$qqrxdxzc
|
||||
@Graphics@TCanvas@RoundRect$qqriiiiii
|
||||
@Controls@TWinControl@Update$qqrv
|
||||
@Controls@TWinControl@AdjustClientRect$qqrr11Types@TRect
|
||||
@Controls@TControl@Update$qqrv
|
||||
@Stdctrls@TCustomCombo@AdjustDropDown$qqrv
|
||||
@Db@TDataSet@Insert$qqrv
|
||||
@Comctrls@TListItem@Delete$qqrv
|
||||
@Apodset@TApolloTable@Delete$qqrv
|
||||
@Apodset@TApolloTable@DBFilter$qqrv
|
||||
@Apodset@TApolloTable@DbfEncrypt$qqrv
|
||||
@Apodset@TApolloTable@DbfDecrypt$qqrv
|
||||
MROUNDUP
|
||||
MROUNDUD
|
||||
@Graphutil@ColorAdjustLuma$qqruiii
|
||||
)JajeXLS
|
||||
OnActivate
|
||||
Delete selected tip
|
||||
= 360,000 + 36,000(VAT)
|
||||
wM_Dm_1.ds_BdQty
|
||||
wM_Dm_1.ds_BdQty_X
|
||||
QTY0
|
||||
QTY5
|
||||
QTY6
|
||||
QTY7
|
||||
QTY8
|
||||
QTY9
|
||||
wM_Dm_1.ds_CostA
|
||||
Act_Ed_Qty
|
||||
mi_HalL
|
||||
mi_Ed_HalD
|
||||
Rate.dbf
|
||||
PADL(TRIM(PG_11),12," ")
|
||||
PADL(TRIM(PG_21),12," ")
|
||||
PADL(TRIM(PG_31),12," ")
|
||||
PADL(TRIM(PG_41),12," ")
|
||||
PADL(TRIM(PG_51),12," ")
|
||||
PG_12
|
||||
PADL(TRIM(PG_12),12," ")
|
||||
PG_22
|
||||
PADL(TRIM(PG_22),12," ")
|
||||
PG_32
|
||||
PADL(TRIM(PG_32),12," ")
|
||||
PG_42
|
||||
PADL(TRIM(PG_42),12," ")
|
||||
PG_52
|
||||
PADL(TRIM(PG_52),12," ")
|
||||
gBx_Rate
|
||||
Act_Set_Rate
|
||||
o_Rate_L
|
||||
GRASS.DBF
|
||||
wM_Dm_2.ds_BdQty
|
||||
LAMT4
|
||||
MAMT4
|
||||
SAMT4
|
||||
wM_Dm_2.ds_BdQty_X
|
||||
wM_JajeXLS
|
||||
ed_EQCODE
|
||||
STBSC
|
||||
|*.STBSC
|
||||
= 360,000 + 366,000(VAT)
|
||||
= 396,000 + 36,000(VAT)
|
||||
mBtn_EDBOQ
|
||||
Color Halftone Settings
|
||||
|Vat
|
||||
Act_PrjLst
|
||||
Excel
|
||||
DataPathAlias
|
||||
Delete|Erases the selection
|
||||
&Select All|Selects the entire document
|
||||
wM_Xls_Comp
|
||||
BOQ_C
|
||||
sBtn_View_BOQ
|
||||
Act_View_BOQ
|
||||
RDbGrid_Boq
|
||||
ds_BoqXX
|
||||
ActEditBoq
|
||||
ActMkBoq
|
||||
Act_CalBoq
|
||||
mi_MkXLS
|
||||
mi_View_BOQ
|
||||
tb_BoqXX
|
||||
BOQ11.NSX
|
||||
Boq11.dbf
|
||||
TWM_JAJEXLS
|
||||
TWM_XLS_COMP
|
||||
Select a Date
|
||||
Dr. Halo images
|
||||
*_표준시장.CSTA;*_실적.CSTA
|
||||
실적(표준시장)단가목록|*_표준시장.CSTA;*_실적.CSTA
|
||||
북측노무비($/월)= ROUNDDOWN(기준단가 *
|
||||
북측노무비($/일)= ROUNDDOWN(북측노무비($/월) / 25, 4)
|
||||
북측 = ROUNDDOWN(기준단가 * 북측노무비($/일) *
|
||||
남측 = ROUNDDOWN(기준단가 *
|
||||
SOFTWARE\경영정보(주)\설계예산(STmate8x)\BOQ_INFO
|
||||
자재수량집계표양식.xls
|
||||
SOFTWARE\경영정보(주)\설계예산(STmate8x)\BOQ_INFO\
|
||||
@@ -0,0 +1,187 @@
|
||||
path,bytes,sha256,extension,format,analysis_level,core,detail_summary
|
||||
Apollo5D6.bpl,339456,4364f64c43032e1595ba45ec1c2bc1dade5d68d80a74e4bac6ec3858e68b42a4,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 54, ""exports"": 1302}"
|
||||
BANTAM.DLL,101376,01c8fac9f5f519dfd0314ee7eb70da64afc53bf82f7831a9c16add63fde0b729,.DLL,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 2, ""exports"": 43}"
|
||||
BDERTL60.BPL,222208,b56d3e643fb1eef7018aa120ddab53ae0402ef997e1441a1ad7ff4ce25f79658,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 25, ""exports"": 1036}"
|
||||
BORLNDMM.DLL,22016,e7b3218d1ef0d16fe29674144dc2f887f281687476b5ba4625cbdd680c22a92a,.DLL,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 6, ""exports"": 12}"
|
||||
CHARSET.CVB,144454,07cd1afc401f3ec6c913f319813810839f9f20123506b28381e179bf921f6b0c,.CVB,BTCV,internal_structure_extracted,False,"{""header"": ""BTCV##BaseCV\u0000\u0000\u0000\u0000\u0000\u0002�\f\u0000\u0000\u0000\u0000\u0000\u0000 \u0003�\u0000A""}"
|
||||
CPMARC.CPM,132096,17d182e5d20dc1e4767d7e5e122566cbaa7e462eaadcf558de87c039f412a38a,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 3, ""exports"": 4}"
|
||||
CPMUNA.CPM,117760,decb3ca906c76a44b2417b998d7abb07b7a39d17969b37831d819039326ee654,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 3, ""exports"": 4}"
|
||||
DBRTL60.BPL,254464,6f7d8767276c74fbf80fce4df1e6e9fd470d80e37e7bc6f51095509af3054379,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 16, ""exports"": 1314}"
|
||||
Dclhrp60.bpl,418816,6d96304c04aa11e9dae0163e99bbd0abe2a9dd4dd665aa8d201f7afb7b891ae7,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 75, ""exports"": 1333}"
|
||||
DELPHIMM.DLL,9216,7030bfa308e4b0916349bab32d7783e76f2d5c48dc36e7eda3535d80b3e540e7,.DLL,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 5, ""exports"": 10}"
|
||||
designide60.bpl,701440,80df9d758d68e5d9e6b0536e102eb6e7744f70cfa95b9306694b93e1a55272a4,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 58, ""exports"": 2837}"
|
||||
DP_Common.CPM,436736,5f55d301c6b2139816ff8832e46becb852c4f0a0c9b5da199371156b6aaf30e0,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 15, ""exports"": 73}"
|
||||
DP_MinMsg.CPM,82432,2059455faa4f92a8897754494a1e28daa9ecce42c3d79c6c9ac02c58dc631de7,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 51, ""exports"": 3}"
|
||||
EQSYNC.DBF,2938,3d7dd1d1d1e5d015aaf2d60628368e65251a1a3a2f6c8f8c449b5566e472391f,.DBF,DBF-like,internal_structure_extracted,False,"{""version"": ""0xE6"", ""records"": 54, ""header_len"": 129, ""record_len"": 52}"
|
||||
EQSYNC.SMT,12311,2537657ec0c036501afb7a9c98c4ef17024b578d537ec0cb59958c5d8c3f4248,.SMT,SIxMemo,internal_structure_extracted,False,"{""block_size"": 64, ""first_value"": 193}"
|
||||
FAREAST.BTL,517810,27dc4e384c01d6fcadaa90973b018adb6f11be28750d2b97e831215ef22b72f5,.BTL,BTLF,internal_structure_extracted,False,"{""header"": ""BTLF01.001\u0000Chinese_TW:dBASE:ANSI""}"
|
||||
FTS32.DLL,43520,05e98046789d7508b18dfcbe14c3724c49f06eb22688675e3646d9d790966b1d,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 1, ""exports"": 16}"
|
||||
HRRES32.DLL,19456,4a7d8f8a58e0f82d8a5be2336a79e63a5aa42f6f32a82863c9c957513b25cc26,.DLL,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 5, ""exports"": 1}"
|
||||
IDAPI32.DLL,589312,fc2faec98fd7167cf7d25fa773ec8ecead0dfc678ca1ffaf6b34f78bac91a91e,.DLL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 6, ""exports"": 754}"
|
||||
IDDBAS32.DLL,454144,9955fcc1e214b15dce432a61d489d2de758169a70306d92a27f1c20ea6b45025,.DLL,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 4, ""exports"": 67}"
|
||||
IDPDX32.DLL,255488,516741a7d632a458e1746137b184a1b050395e974c6b428fca1524ea3a166c6f,.DLL,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 3, ""exports"": 15}"
|
||||
IDR20009.DLL,116736,b41e0f4c1b63d66f68c6e193742287d0481645fb1c7b95f6858b32435648b360,.DLL,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 2, ""exports"": 4}"
|
||||
JDLCHK.Size.dll,1206784,da4d1755947d88336f81d1a6c5766a8df0fee71664e6485399cc3ac9bac8e5de,.dll,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 17, ""exports"": 4}"
|
||||
JDLCHK.Tab.dll,1214976,d8e5d9733b14e18695a1f30ceb2aca7fe14894a040e068ed4a37490a26d105ae,.dll,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 15, ""exports"": 4}"
|
||||
JDLCHK.Tab_33.dll,1229824,c5052736204cd4b956a3f59a6f7d41b36e298258846ce64fd7231407db8dd43f,.dll,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 15, ""exports"": 4}"
|
||||
MINIEXTR.DLL,40960,4edc83a1b2d5037820e4ec66b6f172082aa663636b8f9c343dba79e6fb3ce935,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 2, ""exports"": 13}"
|
||||
RAR.EXE,185212,fe079b67547706ce650f2755b16d9f7efb01a1e5ea07ba464bfe585ae9846003,.EXE,DOS-MZ,internal_structure_extracted,False,"{""magic"": ""4d5a7c016a01990840033903ffff282d""}"
|
||||
RealGrid6.bpl,402944,d1efca11ace3f581e7f4f5408afef2b29d77ce5a4aeb73bc1fa2a94548832ea8,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 55, ""exports"": 1630}"
|
||||
RealGridDB6.bpl,111104,c4963618fdcea616b208d2f4891f870e91494e270db36becebfc66466e492d3b,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 72, ""exports"": 272}"
|
||||
RealGridEX6.bpl,71680,c8f5ec65c1e2af4f0a818e14d791e267b5e47d8d87cf491ecf17e346661aa231,.bpl,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 59, ""exports"": 142}"
|
||||
RTL60.BPL,685056,aa4a46b7921f225910414422ec7ff5533cd5fad87e2fe2cca248f25eb9899480,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 16, ""exports"": 4698}"
|
||||
SDE32.DLL,132096,b23ef38c5099fc74ffd1b5c72c5684e5cc64d598114637dd1bf2462222121260,.DLL,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 2, ""exports"": 179}"
|
||||
SDE50.DLL,155648,3a8748c8823f2c2a7fd179d9ba045c9afd13734e06fa8450a74a8e509b2450fc,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 2, ""exports"": 179}"
|
||||
SDECDX50.DLL,274502,2f09909441d8e4f0bb93463f5a7d6f28db329cc1f5f43e6dbbcbf229dacb1bbf,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 3, ""exports"": 81}"
|
||||
SDENSX50.DLL,266240,6e0a5b4967a6d0cc6faee9b471659419540872d9703700fe60a0dc3045ba024c,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 3, ""exports"": 81}"
|
||||
SDENTX50.DLL,258118,a1d5fccff1b5249de5000de1b548216d8c165b1a6f6162cf9f465f1a2a6cb31e,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 3, ""exports"": 81}"
|
||||
ST_BdSN.CPM,122368,9b7320455bc15f5412f9dd11b40fee13fab44780990c4389c2c5f6beb5f6a395,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 118, ""exports"": 3}"
|
||||
ST_BSC\2007_제비율.JB_M,3379,7139579616921b4e8ac6055d497722b64a2cb048c353861858a179dfa277fc7e,.JB_M,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 25, ""header_len"": 353, ""record_len"": 121}"
|
||||
ST_BSC\2011(상_0112)_제비율.JB_1,5315,cc5f14790cc4f185f44b0b3e3f5be6c78e52df6a373903f14fcb99134c9b4551,.JB_1,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 41, ""header_len"": 353, ""record_len"": 121}"
|
||||
"ST_BSC\2015(상_0301,실적)_제비율.JB_RA",12724,ba85cf902cf1846bcc38583c5f8173046404dc804cfa9b21a3a35a95991335e4,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2015(상_0309)_제비율.JB_R,12724,236ffcc157269a8438aac9993f18e4f6d699e4493e692fada8a16102cd68407c,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
"ST_BSC\2016(상_0301,실적)_제비율.JB_RA",12724,a34469eb1b823dc33e10d96985c7fed44428b93b90c53491edc219cb4b3d677d,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2016(상_0307)_제비율.JB_R,12724,b2e87dc37da04744197040ecd863b1b38563ad63ac975b77f3126bf412d13c97,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2017(상_0215)_제비율.JB_R,12724,dab9bf2fb5e62c41c65e9a01dbc6c7e08f06ff8d14a0d5ec90e24f2134c8e61a,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2018(상_0327)_제비율.JB_R,12724,6ec8ef38aaf747e54117451cb0e5ebcb6b4bbb084bc824163a5dc465ed23d8b3,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
"ST_BSC\2018(하_0701,표준시장)_제비율.JB_RA",12724,e771db1af3a0d4725289f5b01b97fbd4982eb5b89776c6130d87b9127857dbe8,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2018(하_0801)_제비율.JB_R,12724,608a7a9b61884dd8ee3557078db32ab94ad9b6e88341c010ce14643327171e4a,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
"ST_BSC\2019(상_0101,표준시장)_제비율.JB_RA",12724,e771db1af3a0d4725289f5b01b97fbd4982eb5b89776c6130d87b9127857dbe8,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2019(상_0107)_제비율.JB_R,12724,1462a445d32cea9d9a054bd3d114b66bdbc5693f1e464771deecd9797ac373fa,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2019(상_0417)_제비율.JB_R,12724,1c42ea998ca321ac9f8c19bc54736360ee38b8c7f4153e8a94e6c02892c42014,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2019(상_0628)_제비율.JB_R,12724,0401a772ef13213c8f49c8ff4f40b1f3b1960e87868b229a4119750829d51a85,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2020(상_0102)_제비율.JB_R,12724,e0069fa4687aabf12f0466a21bc80d064de4d0a391561cfe2cad47e4bb893680,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
"ST_BSC\2020(상_0102,표준시장)_제비율.JB_RA",12724,49610b1853c84922fc47092d32d01a1ab65844eecf95dfeaff48ac4db56b42a0,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2020(상_0527)_제비율.JB_R,22204,aa722b3c970616b2f402719baa199009cdbf7185f6e2ab04c1cbe48fc92dc508,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2021(상_0104)_제비율.JB_R,22204,b43e44c96edb0be4c3b50f46c015ed39eb87995f749f055b165d104d97c75b49,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2021(상_0421)_제비율.JB_R,22204,c267385aa0d65e4339223ee7d965e44f2bb7e6c351a4689e09ed3071e3d3565d,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2021(상_0520)_제비율.JB_R,22204,bbb678008ba42ac8e99ab874884c6219a41c8d474ec4497813b9887edcea0d28,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2021(하_0501,표준시장)_제비율.JB_RA",12724,b0d7975bb563191dfe2f765d30ccf9083feb9b7ff6cac45198499db86bd9b427,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2021(하_0701)_제비율.JB_R,22204,f73382e53a575ccd88222f18323caad6c24d6c1002567e55d11f3a6ac4c45847,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2022(상_0103)_제비율.JB_R,22204,45d132ad0de6fdd3b41f63743fadcf4ba4bb1db633ab773af850bc4a2c6cbd62,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2022(상_0103,표준시장)_제비율.JB_RA",12724,b0d7975bb563191dfe2f765d30ccf9083feb9b7ff6cac45198499db86bd9b427,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2022(상_0425)_제비율.JB_R,22204,82d7fdab0dbc6162d0fec4e0e99feb27d20d50031d1ff13ac0d6ce056db09977,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2022(하_0501,표준시장)_제비율.JB_RA",12724,95220f1fb9b4169143f9d0fe88cfc6f83dead1c4f464467003cd0ca755da7265,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2023(상_0102)_제비율.JB_R,22204,1aa1fca4090dc67e28c2820dead8b82daf50a73a20ff82dd1905040bb58f71b0,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2023(상_0102,표준시장)_제비율.JB_RA",12724,95220f1fb9b4169143f9d0fe88cfc6f83dead1c4f464467003cd0ca755da7265,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2023(상_0428)_제비율.JB_R,22204,2cf8869231d0a4b9dede802b6a5d3363e3e956db147c0c851370b06e4af9190f,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2024(상_0101)_제비율.JB_R,22204,e594487cc11755b486cf4bfe3b5876107ec044458313f5989f32e0713681bcac,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2024(상_0101,표준시장)_제비율.JB_RA",12724,39af7fa4ebc6a1d12e32cb298538bb24c6658a65249cec57541440044fe46191,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2024(상_0315)_제비율.JB_R,22204,4ed224783fb0d72c66d7c3def6c983f33129011782600d7d78a18cb46ee87a97,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2025(상_0101)_제비율.JB_R,22204,196ecc70fa0b3f1b17ed8f2d5ce5b0339ef92bd3e06dde625c87759707a0f2e4,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2025(상_0101,표준시장)_제비율.JB_RA",12724,49542b8fe77b734c6eefd5d4673a1841f00bcdde8205fc71b6289d1ea05c289b,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2025(상_0401)_제비율.JB_R,22204,dc678b3617b203fa88d6fffcb5e0e04a827da5c31545405a1d1c27c72e200839,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2025(상_0501)_제비율.JB_R,22204,3f978a6793fc131afc10e614dca9bc84b2064ab9faae71d29fee3a1b79440106,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2025(하_0501,표준시장)_제비율.JB_RA",12724,5f07b35b604129203be578323e260fd37a76ac90bd593a450ed728e407aac7c8,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2025(하_0809)_제비율.JB_R,22204,1463341b3da28f5ae7e24446c3003e9cfdc2177cf9962807f86c0ff14805f090,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2026(상_0101)_제비율.JB_R,22204,6fa9bb61555a54f2e6ee95583d76e0c5ab818534b6181783b24a5ed4fb3422e2,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2026(상_0101,표준시장)_제비율.JB_RA",12724,5f07b35b604129203be578323e260fd37a76ac90bd593a450ed728e407aac7c8,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\2026(상_0313)_제비율.JB_R,22204,af27255d84bc9cbdc1dc9ba6ea823d9649fd5f69f4121bba14864113917422d4,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
ST_BSC\2026(상_0413)_제비율.JB_R,22204,19b3fdea3aa891d3e7d37b89c43e7e57fe85dbf39f30c42a9d407f34475f7901,.JB_R,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 1153, ""record_len"": 421}"
|
||||
"ST_BSC\2026(하_0508,표준시장)_제비율.JB_RA",12724,68778043bc15e24688d17ad8fc3fa775e61291897038b4a6e2824897314bc08f,.JB_RA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x03"", ""records"": 50, ""header_len"": 673, ""record_len"": 241}"
|
||||
ST_BSC\GRPCOD_E.BSC,340484,2ab6eb0215184f6c442702f94cd82934acef43bc8b398e8227cee58e56cbe9c0,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 2539, ""header_len"": 257, ""record_len"": 134}"
|
||||
ST_BSC\GRPCOD_Z.BSC,1625008,b6c58c548256fdf3d7be2780d0a4b8b23b34d85ba61c8899e5ab38f8e35eaa2c,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 12125, ""header_len"": 257, ""record_len"": 134}"
|
||||
ST_BSC\JDCODE_L.BSC,156358,d967e15b87c6a2168251977119fe8263898bce84e3ef545eb09c78bdf6e465fd,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 460, ""header_len"": 417, ""record_len"": 339}"
|
||||
ST_BSC\JDCODE_M.BSC,19520038,69073533ba84c01bd58ec4ea7cfddbd427e61ee62142a8654284d2bf6db272c7,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 57580, ""header_len"": 417, ""record_len"": 339}"
|
||||
ST_BSC\JDCODE_S.BSC,288907,39a3e525399a842e0df1e27e8f2a76683db42e5c1fefa20c61eb3c74494f4df0,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 851, ""header_len"": 417, ""record_len"": 339}"
|
||||
ST_BSC\JDCODE_X.BSC,282805,6304d8e0bb717efaaed72e9626a688cb4a955866a113b5a9c3e1a4d19bee9163,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 833, ""header_len"": 417, ""record_len"": 339}"
|
||||
ST_BSC\JDCODE_Z.BSC,11501671,decbd6a76ebe98751020f78b74b190970cdeba834316409ebe9db3c3fb7cd10e,.BSC,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 33927, ""header_len"": 417, ""record_len"": 339}"
|
||||
ST_BSC\Valid_Xml.vsl,58807,84f2e2a950139d00fbc96540b24d8883f5a733ba419c3369f8ecbed9f04b6290,.vsl,text,internal_structure_extracted,False,"{""encoding"": ""utf-8"", ""lines"": 1429}"
|
||||
"ST_BSC\노임기계\2025(상_노임,기계경비).STBSC",103229,fc6f62bcff5ce69d83d50af61a7f7d415e17a2e0ec10f3321ae4fd8445fd513c,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\노임기계\2025(하_노임,기계경비).STBSC",103027,c99902383b569c5941c6bafe6707a0358beb9f81ec19c2dd486d96ea7a152025,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\노임기계\2026(상_노임,기계경비).STBSC",104166,74f886bb8cc1ae69e69fe1d92845d23e60c35ff0f06f75631368112a4ca3f1c3,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\노임기계\2026(하_노임,기계경비).STBSC",104284,48d7196fed12d887a94a2e391f98b212af7aa9040a8e51555bfdf895869c94bc,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(상,국교,해수부,조달청)_표준시장.CSTA",2427111,5c8d0d7bf9e9208e72e6acaafdf8544f88b972a46b7f74ec3b32acc0a3d458af,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4921, ""header_len"": 1057, ""record_len"": 493}"
|
||||
"ST_BSC\실적단가\2025\2025(상_시장시공,조달청).STBSC",24865,79e534e050664f0248732c9923d9b131a52340bbe4a93c79730d17c285b0e367,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(상_표준시장,조달청).STBSC",234037,fa4e340e60845f38a3bfe44966e3415690da9e9eaa0a236d057ff4986a065694,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(상_표준시장[노임외 경비적용],국교,해수부).STBSC",168543,432f365195096b67a21f5ba2b804cde79fe24534697294e091858a17cf23bdc6,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(상_표준시장[노임외 재료비적용],국교,해수부).STBSC",167083,a66c9af574fba4a6a5d43bfb04de42c0c4b941091a1b2aba1ab2d659e4503f19,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(하,국교,해수부,조달청)_표준시장.CSTA",2427111,68cba31f353bfb2c78909a9633e96b8581276d21044ceecce21399f26fd030a4,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4921, ""header_len"": 1057, ""record_len"": 493}"
|
||||
"ST_BSC\실적단가\2025\2025(하_시장시공,조달청).STBSC",25501,e1fb2b89d26cb19ee88ffa5b40a8401cedcc651de4b80f11b4520db88248cad2,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(하_표준시장,조달청).STBSC",234106,ca134e164233b9477ce9d34da9d8f36db902c693ce36e4325bc2319daea17cf2,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(하_표준시장[노임외 경비적용],국교,해수부).STBSC",168600,e2a4669ccf5ff3ede5457b1d00a463263303ce2467361d5dd4d5ff10983ab596,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2025\2025(하_표준시장[노임외 재료비적용],국교,해수부).STBSC",167122,cb139c7278f120a6aa6dcc3e1dde9bb5d0b67b8fd62aa728797e6a3d50c3dd0b,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\실적단가\2025\COSTA.CSTA,2427111,68cba31f353bfb2c78909a9633e96b8581276d21044ceecce21399f26fd030a4,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4921, ""header_len"": 1057, ""record_len"": 493}"
|
||||
ST_BSC\실적단가\2025\표준시장단가(2025_04_21).Info,206,d4a1fd8bb95b8715a423748f2ccfb516807cdace287a039d3a5a71ba148c805a,.Info,DBF-like,internal_structure_extracted,False,"{""version"": ""0x32"", ""records"": 3139494579, ""header_len"": 47603, ""record_len"": 45533}"
|
||||
ST_BSC\실적단가\2025\표준시장단가(2025_08_04).Info,342,4b31c62515d0692aaee658adc19cb36cdc3e8963251154401918e7aa0c5b7f64,.Info,DBF-like,internal_structure_extracted,False,"{""version"": ""0x32"", ""records"": 3340821171, ""header_len"": 47567, ""record_len"": 45533}"
|
||||
"ST_BSC\실적단가\2026\2026(상,국교,해수부,조달청)_표준시장.CSTA",2446831,3cf6ff7b7acb709a9faee154fbe2dbcfc718c6112e317d98868ac493dae666d5,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4961, ""header_len"": 1057, ""record_len"": 493}"
|
||||
"ST_BSC\실적단가\2026\2026(상_시장시공,조달청).STBSC",25187,303abd1b10ef21a8c7e30f2af9845400346ff82f0f25bf7f9beb6277b43f696c,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(상_표준시장,조달청).STBSC",236533,7f85e77265679b46807b2b22311b1fa07ffee463c50db67d6426ad38b5773f07,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(상_표준시장[노임외 경비적용],국교,해수부).STBSC",170135,4f09af8780b54dcaeb1da7d268ac7cab253298c8402e15790749bf50ac47f482,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(상_표준시장[노임외 재료비적용],국교,해수부).STBSC",168661,19c589ee1b564aa7593e87e556b0ab1855a7291ef0cf894df29a36a1e78170e2,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(하,국교,해수부,조달청)_표준시장.CSTA",2446831,a5b26c95d44c47dc7dd7f5997a2e6be68edb1436003fc914fd1402588d68edfc,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4961, ""header_len"": 1057, ""record_len"": 493}"
|
||||
"ST_BSC\실적단가\2026\2026(하_표준시장,조달청).STBSC",236961,cd3394de4e0ab08b6bab3c0db8cb5d439983ddaece5df5016243b0c32d6bece7,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(하_표준시장[노임외 경비적용],국교,해수부).STBSC",170373,dfe6e5a1c922cad51fa49ad3682292857450f0bff5f84f7fa2762a61685aed44,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
"ST_BSC\실적단가\2026\2026(하_표준시장[노임외 재료비적용],국교,해수부).STBSC",168877,fffae2c8fd2ef78af32da16711dba26e952fc0858d25b73e643d2379e66174ad,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 4, ""encrypted"": 4, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\실적단가\2026\COSTA.CSTA,2446831,a5b26c95d44c47dc7dd7f5997a2e6be68edb1436003fc914fd1402588d68edfc,.CSTA,DBF-like,internal_structure_extracted,False,"{""version"": ""0x06"", ""records"": 4961, ""header_len"": 1057, ""record_len"": 493}"
|
||||
ST_BSC\실적단가\2026\표준시장단가(2026_05_04).Info,224,64934740ee03e4f01fe112ec1ae9cddc01497da48b515f6cdbbb82487c7c75a7,.Info,DBF-like,internal_structure_extracted,False,"{""version"": ""0x32"", ""records"": 3139494579, ""header_len"": 47603, ""record_len"": 45533}"
|
||||
ST_BSC\실적단가\2026\표준시장단가(2026_05_27).Info,206,207506f87c9c3da5ec86b8e16efab8a4f35570b0cb8934d7443c82502c5803e9,.Info,DBF-like,internal_structure_extracted,False,"{""version"": ""0x32"", ""records"": 3340821171, ""header_len"": 47567, ""record_len"": 45533}"
|
||||
ST_BSC\일위대가\2026\2026(상_표준일위대가_건축).STBSC,107988,10c39a2e5d1e797b7d9f82f9ff1d099d74ad83a84e9a9c936e192bdbf4744f5a,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\일위대가\2026\2026(상_표준일위대가_기계설비).STBSC,142723,ef64cde3312b77e99e3f0f9565bfcf8dc6e747a9a4b414d8921f2499a65d6663,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\일위대가\2026\2026(상_표준일위대가_전기).STBSC,22104,08c88f23a731e05bd6c926e0686ed1565c3900318f2df5779db92b762e026614,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\일위대가\2026\2026(상_표준일위대가_토목).STBSC,199599,42b6bbad12827b3a68991ee59a78d4a81d43928433043a2b025a77169abe4830,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\일위대가\2026\2026(상_표준일위대가_통신).STBSC,52561,9bf846f887815e228f4122ca3b9dd6de4b6cd6b26b6f3e647dc7ad8055fc9352,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""MEM.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\일위대가\2026\표준일위대가(2026_03_17).Info,542,84b673e857218323d8eabb7a6cce309979a6bd65490a6311382f9f2af275b3db,.Info,text,internal_structure_extracted,False,"{""encoding"": ""cp949"", ""lines"": 11}"
|
||||
ST_BSC\자재단가\시설공통자재\2025-상-시설공통자재.STBSC,184968,7ac21732736a8b83d732f21797dc1434a4376319cea1f8777f86fc37f209593a,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""info.ST"", ""XDET.ST"", ""Start.ST"", ""COST.ST"", ""CH_NAME.ST""]}"
|
||||
ST_BSC\자재단가\시설공통자재\2025-하-시설공통자재.STBSC,178357,f7f7f884e7d0cfd83be9fd618c99d540c83ca621f26df1d1fa8db6de82afd4c1,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""Start.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\자재단가\시설공통자재\2026-상(06)-시설공통자재.STBSC,177153,cfcc27abb1759a916983b51737361c348d277ed540b18188da9976fe56cef034,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""Start.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\자재단가\시설공통자재\2026-상-시설공통자재.STBSC,176293,926f18edcf6390f34391bd9a26a6bf589d5e6b8e10128498a2cdb50341a70185,.STBSC,ZIP,internal_structure_extracted,False,"{""entries"": 5, ""encrypted"": 5, ""names"": [""CH_NAME.ST"", ""COST.ST"", ""info.ST"", ""Start.ST"", ""XDET.ST""]}"
|
||||
ST_BSC\호환규정코드(2026_09_01),108,7f12adccd919919ef7739f3efce9439e1f183989980c8daeb170bb1e65fdcade,,text,internal_structure_extracted,False,"{""encoding"": ""cp949"", ""lines"": 3}"
|
||||
ST_Com_V.CPM,648192,d66b620110244d0814513fb18fa3fca6611ad57ffa300e193c0b85af59b2f574,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 17, ""exports"": 10}"
|
||||
ST_CPM\ST_Boq.CPM,2119168,180707d0423dd2dcb8403d89ba1dbaf2731c0db5c6bb332029479fd9d9923ab3,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 132, ""exports"": 4}"
|
||||
ST_CPM\ST_EdiLWi.CPM,508928,9e83c2a450d31353953361c8bb460bae700cc76927605145a0ae406ca31702ca,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 128, ""exports"": 2}"
|
||||
ST_CPM\ST_EdSan.CPM,628224,101d1f31adff9d46d30a46135ccd6398af41fcee9b2434a91dae5ee466e13753,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 128, ""exports"": 3}"
|
||||
ST_CPM\ST_FindUse.CPM,436224,c0c2b770e805cfcb78ef091a4657185160f58408434dab09cbef20d982d59a9e,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 3}"
|
||||
ST_CPM\ST_Jabpum.CPM,411136,d960f6e6b5ffa395c201eabf126cb4dcf8dbe026c2afaa804a7bfa6429164f51,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 126, ""exports"": 6}"
|
||||
ST_CPM\ST_KanJub.CPM,1718272,ef8d55b259b37916496e80cab6a4a2ba46216e0bd7c5199083dbda632bd50d03,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 131, ""exports"": 5}"
|
||||
ST_CPM\ST_P_Collect.CPM,570880,fd1b468d77398a72705537e28321c93a3736fbc1ea384997659eea2fe9b71737,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 125, ""exports"": 5}"
|
||||
ST_CPM\ST_P_ConEst.CPM,735744,027c72cc019e46fdec78e6aebc68b621aa77c7fc1ac88362f04675f6dab69fab,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 125, ""exports"": 9}"
|
||||
ST_CPM\ST_P_Excav.CPM,809472,62ea015c8fcb05c351d394a8bdb3214ea516d099fb986c25a7503358160fb0d0,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 15}"
|
||||
ST_CPM\ST_P_iLWi.CPM,634880,1308680f806f9e0298f99ce68d08c5c3e80eb5d7e8cf86fb7a89d7678539ae32,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 3}"
|
||||
ST_CPM\ST_P_Joong.CPM,797696,30c28a8ace160c06fa79948ea20678504aaeae2d20304daff719300e5f919ee7,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 125, ""exports"": 7}"
|
||||
ST_CPM\ST_P_List.CPM,631808,e14d17b10ee1f82f5abc0f7c90f1b658516f9623452fe092450396eae9deb071,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 11}"
|
||||
ST_CPM\ST_P_Nae.CPM,1398272,9ecb6a663c988f249548a06a6c7da66abc71d8c245baba141450c329391f1c47,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 30}"
|
||||
ST_CPM\ST_P_San.CPM,570368,5d364a818883fd6f69839dd08c7e3243e0545fd23062454f5816b3cc0bae941c,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 3}"
|
||||
ST_CPM\ST_P_XLS.CPM,2314752,1d3f7ed8702ee9e4ea2309126028f3600497617c576c1b91659b0c8c60083da0,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 127, ""exports"": 72}"
|
||||
ST_CPM\ST_PList.CPM,1586176,638c8167ff5817f454633c9270b9b01b2ef72044339c58bd8d5190b1a8e4766c,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 143, ""exports"": 3}"
|
||||
ST_CPM\ST_Prt.CPM,1170432,0698a6343ba04a5b379f297da60e68a9fc3f18d5ac0ad262570f70ca98dd3c19,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 130, ""exports"": 4}"
|
||||
ST_CPM\ST_SanCal.CPM,270336,153967ee6429ba40285939af68a9b2b02daae9888e95440e95dc017acdf0ad54,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 120, ""exports"": 2}"
|
||||
ST_CPM\ST_Sys.CPM,566784,e55845df186ecb2d0ea6de4408b299529cd0d697030da8c114a0a48b3ce0a81d,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 126, ""exports"": 2}"
|
||||
ST_CPM\ST_U_ADL.CPM,189952,833af9af1263205f3e0e04162079114d451e6136c2e5271776f63532e19a4031,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 48, ""exports"": 6}"
|
||||
ST_CPM\ST_U_Archive.CPM,473088,1d00333bca0277a3355826419d11c57996852ec9013b7bb508d16ac9ec6ed553,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 125, ""exports"": 16}"
|
||||
ST_CPM\ST_U_ChgAmt.CPM,130560,37ca3715c216f68274eabd3dd944bcd7922404bd2721b1ba0b73c428e843e3d8,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 120, ""exports"": 2}"
|
||||
ST_CPM\ST_U_ChgCode.CPM,656896,6fe80a7a21e1458852e940bb1efdf3eee618ae23d0090a4e7323c908efb9257d,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 124, ""exports"": 10}"
|
||||
ST_CPM\ST_U_ConBD.CPM,196608,fb0351c1fa96be282b8da6be31ba97673e626e1a5d8e30acb83831f4c72e5d6e,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 121, ""exports"": 4}"
|
||||
ST_CPM\ST_U_Convert.CPM,1825792,0166beb69d2692732eb3d5e897201a96aa1b2955026a67aa53989ffa28431649,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 133, ""exports"": 20}"
|
||||
ST_CPM\ST_U_DelCode.CPM,544768,30cc2e234a67fd2adeb51629b00b3d3d60e2fa2026b1f0b1ff9af958a1b38f9c,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 123, ""exports"": 6}"
|
||||
ST_CPM\ST_U_DelPrj.CPM,161280,2e47370f7ad85542f375f03e6b3a0aaf88252750d8f6ad1ce631ea02125c2a3d,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 121, ""exports"": 3}"
|
||||
ST_CPM\ST_U_Indx.CPM,137728,cbe19214915433afe71b856c3f82a7eb4c106ea40df72cb726eb8fcf8e73f2c9,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 124, ""exports"": 2}"
|
||||
ST_CPM\ST_U_Merge.CPM,718848,350085f3a498628703ec66486bfc4994683169fdf0c4f8e8cc7f9c842b81271a,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 124, ""exports"": 2}"
|
||||
ST_CPM\STmateQty8.CPM,224768,f92ec517ff931328dac76154a1fedcee8e9cca5ffb64fddfc207af3887c19b37,.CPM,PE,internal_structure_extracted,True,"{""sections"": 7, ""imports"": 124, ""exports"": 4}"
|
||||
ST_Func_C.CPM,657920,f2d4b2f6ebda2d57e47d343907071400cdb4b9f01fa5cc7756f34eb5f94e2ca6,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 19, ""exports"": 7}"
|
||||
ST_JD_Lst.CPM,143370,64d6d0dd7629249dc2f3fbdcd7372a9e09a8422fe2ede94ea59a8fedd25f3497,.CPM,text,internal_structure_extracted,False,"{""encoding"": ""cp949"", ""lines"": 26}"
|
||||
ST_log\ST_log.stlog,149,298b9c72ce903c0a06241dc04bd2f5cca0c525ae2822b09cdeccdda36f9df932,.stlog,obfuscated-log,internal_structure_extracted,False,"{""bytes"": 149}"
|
||||
ST_Merge_Code.CPM,656896,c3bcc5851d680da5dc69e04b4d9e389baf36c45dc1c2203c5b88e039e6d56c63,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 133, ""exports"": 1}"
|
||||
ST_Tips.dat,1636,f7de77c90911587bf98c2a75c58d6484bbc166d49b306feb04e895fdc93f4d6a,.dat,tips-data,internal_structure_extracted,False,"{""utf8_html"": true, ""bytes"": 1636}"
|
||||
ST_U_APatch.CPM,172032,50b79f7c127ec681db22d2cff6814314c143eca73cc473f01d26c71197fb6ed5,.CPM,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 49, ""exports"": 0}"
|
||||
ST_V_Info.CPM,49,671f6214ec519d846e5b489ffbd37d1a6036945a46169ae1d6294319eba10ce0,.CPM,text,internal_structure_extracted,False,"{""encoding"": ""utf-8"", ""lines"": 3}"
|
||||
ST_Viewer.exe,1329664,2eeee446f9563a6d64e7a415f7f0373262132f7adfc672ac9b8b108a6e069df9,.exe,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 21, ""exports"": 0}"
|
||||
ST_VSetup.EXE,1788882,d9e04e9fa0d7886c868d122a20a2143ac913f2ced8fec1c5127ca9fd38a14b81,.EXE,PE,internal_structure_extracted,False,"{""sections"": 4, ""imports"": 4, ""exports"": 2}"
|
||||
ST_Wal.CPM,325632,da56d1a27a664b0ff7b6af60c8b891bd7c831ffdede9b104ee05d33cad80e82d,.CPM,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 121, ""exports"": 0}"
|
||||
Stma.URL,48,de22235fba718eff232f134680d246ce485ff5e80178e7f654a1eb605878ce87,.URL,text,internal_structure_extracted,False,"{""encoding"": ""utf-8"", ""lines"": 2}"
|
||||
STmate.Cpm,3525632,99d5c05675ba1399c51a71db8e26ebd5ff39fd78d02360147c7705deb399b5f5,.Cpm,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 144, ""exports"": 1}"
|
||||
STmate8.Exe,145408,ee029ac9acf684e894a2ea4743a37288755439391f36107565f4aa87f3499b06,.Exe,PE,internal_structure_extracted,False,"{""sections"": 6, ""imports"": 52, ""exports"": 0}"
|
||||
STmate_Ins.log,15958,9dd9f8e827f64c593afd6b9d15fe041a8d1139c61c189f02e70ed693c122efc3,.log,text,internal_structure_extracted,False,"{""encoding"": ""cp949"", ""lines"": 288}"
|
||||
STmexe.Cpm,96256,5ff4005a52c8f56be88d7f263c9dcec782c7ea50618a3c1509e5dc2a06539a6e,.Cpm,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 37, ""exports"": 2}"
|
||||
SX32W.CPM,4568352,ae0f65b43b5a04ccb1cc181db66cd5e69e7ab5254891c8c6c6810130ff9922de,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 3, ""exports"": 29}"
|
||||
UNWISE.EXE,149504,49ef36bd01b8ebf38c7b807a5fb44cbaf47c9d4efa883b01c41494c61ae4a2e2,.EXE,PE,internal_structure_extracted,False,"{""sections"": 4, ""imports"": 6, ""exports"": 4}"
|
||||
UNWISE.INI,51,d8848d6d476fcab45337043e90121cccc24e4090977a5c54d713c36e06e4ced7,.INI,text,internal_structure_extracted,False,"{""encoding"": ""cp949"", ""lines"": 3}"
|
||||
USA.BTL,36458,4b9819331d4ad485dce6a16609dbe8ae6e18aa4085e8deb1c8f5333735c5e76e,.BTL,BTLF,internal_structure_extracted,False,"{""header"": ""BTLF01.001\u0000English_US:Paradox:OE""}"
|
||||
VCL60.BPL,1326080,b1b14dfad40e71fec495316a901dffc14d1add020172f99a28b06f953238a885,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 41, ""exports"": 6339}"
|
||||
VCLDB60.BPL,262656,ab75cf8adaf04dc782dab0272cf02e064700870ae962affeeb135e2f4561f788,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 58, ""exports"": 931}"
|
||||
VCLJPG60.BPL,96256,fe1eaf525e2ac3161e57e36333af7eed2b92597fedb5814f0b8824a40b9917cd,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 16, ""exports"": 60}"
|
||||
VCLSMP60.BPL,62464,ded7e1bbdada951183532d60e95a8ef6805118a1249b6c8ea885d8345b3195f3,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 38, ""exports"": 136}"
|
||||
VCLX60.BPL,213504,741b8250412fe40fd3124de2814a506af94f65017e6c90ae2af27a9b54d81052,.BPL,PE,internal_structure_extracted,False,"{""sections"": 8, ""imports"": 51, ""exports"": 617}"
|
||||
View_Old_San.CPM,667136,debe9a5cbeb477ca5c889269b9bcc69fa074989fe7074be2e347ab421260fd67,.CPM,PE,internal_structure_extracted,False,"{""sections"": 7, ""imports"": 15, ""exports"": 1}"
|
||||
WLHA32.DLL,90624,96452faeed5affd7dcd88f3aa14930ab5e5709f5916262c87a10927d40838329,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 2, ""exports"": 5}"
|
||||
WUNARJ32.DLL,44032,257b4812c9b4a127b2e7a04eff3bd19166ff8ca20cf03d85684b1ec890b804d5,.DLL,PE,internal_structure_extracted,False,"{""sections"": 5, ""imports"": 2, ""exports"": 2}"
|
||||
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,221 @@
|
||||
"""STmate System 폴더 정적 인벤토리/PE 근거 추출. 원본은 읽기만 한다."""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
SYSTEM = Path(r"C:\경영정보(주)\System")
|
||||
OUT = Path(__file__).resolve().parent
|
||||
CORE = {
|
||||
"ST_KanJub.CPM", "ST_SanCal.CPM", "ST_Boq.CPM", "ST_EdiLWi.CPM",
|
||||
"STmateQty8.CPM", "ST_P_Joong.CPM", "ST_U_ChgAmt.CPM",
|
||||
"ST_U_ChgCode.CPM", "ST_U_ConBD.CPM", "ST_P_ConEst.CPM",
|
||||
"ST_P_Collect.CPM", "ST_U_Convert.CPM",
|
||||
}
|
||||
TOKENS = re.compile(
|
||||
r"DBF|SMT|COSTN|COSTA|BDQTY|BOQ(?:11|12|13|14)?|PRJLST|RATE(?:_M|_R|_RA)?|"
|
||||
r"TAMT|LAMT|MAMT|SAMT|QTY|ROUND|HAL|ADJ|PG_[1-6]|KWNGP|EQCODE|IFORM|"
|
||||
r"STBSC|CSTA|JB_R|VAT|TAX|EXCEL|XLS|\bSELECT\b|\bUPDATE\b|\bINSERT\b|\bDELETE\b",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def u16(data: bytes, off: int) -> int:
|
||||
return struct.unpack_from("<H", data, off)[0]
|
||||
|
||||
|
||||
def u32(data: bytes, off: int) -> int:
|
||||
return struct.unpack_from("<I", data, off)[0]
|
||||
|
||||
|
||||
def cstr(data: bytes, off: int, limit: int = 512) -> str:
|
||||
end = data.find(b"\0", off, min(len(data), off + limit))
|
||||
if end < 0:
|
||||
end = min(len(data), off + limit)
|
||||
return data[off:end].decode("ascii", errors="replace")
|
||||
|
||||
|
||||
def strings(data: bytes) -> list[str]:
|
||||
found = [m.group().decode("ascii", errors="replace") for m in re.finditer(rb"[\x20-\x7e]{4,}", data)]
|
||||
found += [m.group().decode("utf-16le", errors="replace") for m in re.finditer(rb"(?:[\x20-\x7e]\x00){4,}", data)]
|
||||
# CP949 문자열은 정확한 경계 검출이 어려워 printable run을 보수적으로 추가한다.
|
||||
for raw in re.findall(rb"(?:[\x20-\x7e\x81-\xfe]){6,}", data):
|
||||
try:
|
||||
text = raw.decode("cp949")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if any("가" <= ch <= "힣" for ch in text):
|
||||
found.append(text)
|
||||
return list(dict.fromkeys(found))
|
||||
|
||||
|
||||
def pe_info(data: bytes) -> dict | None:
|
||||
if data[:2] != b"MZ" or len(data) < 0x40:
|
||||
return None
|
||||
pe = u32(data, 0x3C)
|
||||
if pe + 24 > len(data) or data[pe:pe + 4] != b"PE\0\0":
|
||||
return None
|
||||
machine, nsec, timestamp, _, _, opt_size, characteristics = struct.unpack_from("<HHIIIHH", data, pe + 4)
|
||||
opt = pe + 24
|
||||
magic = u16(data, opt)
|
||||
data_dir = opt + (96 if magic == 0x10B else 112)
|
||||
sections = []
|
||||
sec_off = opt + opt_size
|
||||
for i in range(nsec):
|
||||
off = sec_off + i * 40
|
||||
name = data[off:off + 8].split(b"\0")[0].decode("ascii", errors="replace")
|
||||
vsize, va, raw_size, raw_off = struct.unpack_from("<IIII", data, off + 8)
|
||||
sections.append({"name": name, "va": va, "vsize": vsize, "raw_off": raw_off, "raw_size": raw_size})
|
||||
|
||||
def rva_off(rva: int) -> int | None:
|
||||
for sec in sections:
|
||||
if sec["va"] <= rva < sec["va"] + max(sec["vsize"], sec["raw_size"]):
|
||||
return sec["raw_off"] + rva - sec["va"]
|
||||
return rva if rva < len(data) else None
|
||||
|
||||
imports = []
|
||||
if data_dir + 16 <= len(data):
|
||||
imp_rva = u32(data, data_dir + 8)
|
||||
imp_off = rva_off(imp_rva) if imp_rva else None
|
||||
if imp_off is not None:
|
||||
for i in range(512):
|
||||
off = imp_off + i * 20
|
||||
if off + 20 > len(data):
|
||||
break
|
||||
original, _, _, name_rva, thunk = struct.unpack_from("<IIIII", data, off)
|
||||
if not any((original, name_rva, thunk)):
|
||||
break
|
||||
name_off = rva_off(name_rva)
|
||||
if name_off is not None:
|
||||
imports.append(cstr(data, name_off))
|
||||
|
||||
exports = []
|
||||
if data_dir + 8 <= len(data):
|
||||
exp_rva = u32(data, data_dir)
|
||||
exp_off = rva_off(exp_rva) if exp_rva else None
|
||||
if exp_off is not None and exp_off + 40 <= len(data):
|
||||
n_names = u32(data, exp_off + 24)
|
||||
names_rva = u32(data, exp_off + 32)
|
||||
names_off = rva_off(names_rva)
|
||||
if names_off is not None:
|
||||
for i in range(min(n_names, 10000)):
|
||||
name_off = rva_off(u32(data, names_off + i * 4))
|
||||
if name_off is not None:
|
||||
exports.append(cstr(data, name_off))
|
||||
return {
|
||||
"machine": f"0x{machine:04X}", "sections": sections, "imports": imports,
|
||||
"exports": exports, "timestamp": timestamp, "characteristics": f"0x{characteristics:04X}",
|
||||
}
|
||||
|
||||
|
||||
def classify(data: bytes, path: Path) -> tuple[str, dict]:
|
||||
pe = pe_info(data)
|
||||
if pe:
|
||||
return "PE", pe
|
||||
if data[:2] == b"PK":
|
||||
extra = {}
|
||||
try:
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
extra = {"entries": len(zf.infolist()), "encrypted": sum(bool(x.flag_bits & 1) for x in zf.infolist()), "names": [x.filename for x in zf.infolist()]}
|
||||
except Exception as exc:
|
||||
extra = {"zip_error": type(exc).__name__}
|
||||
return "ZIP", extra
|
||||
if data and data[0] in (0x02, 0x03, 0x04, 0x06, 0x30, 0x31, 0x32, 0x43, 0x63, 0x83, 0x8B, 0xCB, 0xF5, 0xE5, 0xE6, 0xFB):
|
||||
if len(data) >= 32:
|
||||
return "DBF-like", {"version": f"0x{data[0]:02X}", "records": u32(data, 4), "header_len": u16(data, 8), "record_len": u16(data, 10)}
|
||||
if data[:2] == b"MZ":
|
||||
return "DOS-MZ", {"magic": data[:16].hex()}
|
||||
if data[8:16] == b"SIxMemo\0":
|
||||
return "SIxMemo", {"block_size": u32(data, 4), "first_value": u32(data, 0)}
|
||||
if data[:4] in (b"BTLF", b"BTCV"):
|
||||
return data[:4].decode("ascii"), {"header": data[:32].rstrip(b"\0").decode("ascii", errors="replace")}
|
||||
if data.lstrip().startswith((b"<?xml", b"[")) or path.suffix.lower() in (".info", ".url", ".ini", ".log") or not path.suffix:
|
||||
for encoding in ("utf-8", "cp949", "latin1"):
|
||||
try:
|
||||
text = data.decode(encoding)
|
||||
return "text", {"encoding": encoding, "lines": len(text.splitlines())}
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if path.name == "ST_Tips.dat":
|
||||
return "tips-data", {"utf8_html": b"<FONT" in data, "bytes": len(data)}
|
||||
if path.suffix.lower() == ".stlog":
|
||||
return "obfuscated-log", {"bytes": len(data)}
|
||||
return "other", {"magic": data[:16].hex()}
|
||||
|
||||
|
||||
def dbf_structure(data: bytes) -> dict:
|
||||
fields, off = [], 32
|
||||
while off + 32 <= len(data) and data[off] != 0x0D:
|
||||
raw = data[off:off + 32]
|
||||
fields.append({
|
||||
"name": raw[:11].split(b"\0")[0].decode("ascii", errors="replace"),
|
||||
"type": chr(raw[11]), "length": raw[16], "decimals": raw[17],
|
||||
})
|
||||
off += 32
|
||||
return {
|
||||
"version": f"0x{data[0]:02X}", "records": u32(data, 4),
|
||||
"header_len": u16(data, 8), "record_len": u16(data, 10), "fields": fields,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not SYSTEM.is_dir():
|
||||
raise SystemExit(f"not found: {SYSTEM}")
|
||||
rows, core_details, structures = [], {}, {}
|
||||
for path in sorted(SYSTEM.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
data = path.read_bytes()
|
||||
kind, detail = classify(data, path)
|
||||
rel = str(path.relative_to(SYSTEM))
|
||||
level = "format_checked"
|
||||
if kind == "PE":
|
||||
ss = strings(data)
|
||||
hits = [s for s in ss if TOKENS.search(s)]
|
||||
structures[rel] = {"format": kind, "pe": detail, "strings_total": len(ss), "domain_strings": hits[:1000]}
|
||||
if path.name in CORE:
|
||||
core_details[rel] = structures[rel]
|
||||
level = "internal_structure_extracted"
|
||||
elif kind == "DBF-like":
|
||||
structures[rel] = {"format": kind, "dbf": dbf_structure(data)}
|
||||
level = "internal_structure_extracted"
|
||||
elif kind == "ZIP":
|
||||
structures[rel] = {"format": kind, "zip": detail}
|
||||
level = "internal_structure_extracted"
|
||||
elif kind in ("DOS-MZ", "SIxMemo", "BTLF", "BTCV", "text", "tips-data", "obfuscated-log"):
|
||||
structures[rel] = {"format": kind, "detail": detail, "strings": strings(data)[:1000]}
|
||||
level = "internal_structure_extracted"
|
||||
rows.append({
|
||||
"path": rel, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest(),
|
||||
"extension": path.suffix, "format": kind, "analysis_level": level,
|
||||
"core": path.name in CORE, "detail_summary": json.dumps(detail if kind != "PE" else {"sections": len(detail["sections"]), "imports": len(detail["imports"]), "exports": len(detail["exports"])}, ensure_ascii=False),
|
||||
})
|
||||
with (OUT / "system_inventory.csv").open("w", encoding="utf-8-sig", newline="") as fh:
|
||||
writer = csv.DictWriter(fh, fieldnames=rows[0].keys())
|
||||
writer.writeheader(); writer.writerows(rows)
|
||||
(OUT / "core_cpm_static.json").write_text(json.dumps(core_details, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
(OUT / "system_structures.json").write_text(json.dumps(structures, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
formula_map: dict[str, list[str]] = {}
|
||||
for module, detail in core_details.items():
|
||||
for text in detail["domain_strings"]:
|
||||
upper = text.upper()
|
||||
if any(mark in upper for mark in ("ROUNDDOWN(", "ROUNDUP(", "ROUND(", "TAMT1<>", "SUBSTR(BDCODE", 'CODE="W"')):
|
||||
formula_map.setdefault(text, []).append(module)
|
||||
(OUT / "core_formula_strings.json").write_text(json.dumps(formula_map, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
counts = Counter(r["analysis_level"] for r in rows)
|
||||
formats = Counter(r["format"] for r in rows)
|
||||
assert len(rows) == 186 and len(core_details) == 12
|
||||
assert sum(formats.values()) == len(rows) and len({r["path"] for r in rows}) == len(rows)
|
||||
print(json.dumps({"files": len(rows), "levels": counts, "formats": formats, "core": len(core_details)}, ensure_ascii=False, default=dict))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""선택한 32비트 PE export를 역어셈블하고 문자열 참조를 붙인다."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
from capstone import CS_ARCH_X86, CS_MODE_32, Cs
|
||||
|
||||
SYSTEM = Path(r"C:\경영정보(주)\System")
|
||||
OUT = Path(__file__).resolve().parent / "export_disassembly.json"
|
||||
TARGETS = {
|
||||
"DP_Common.CPM": {"GET_ROUND_CUT", "MROUNDUD", "MROUNDUP", "MTRUNC", "MINT", "CALC_STR", "CALC_STR_PART"},
|
||||
"ST_Func_C.CPM": {"CON_TOT", "SUM_TOT", "ZERO_TOT", "PCN_TO_T", "OPTIMIZ"},
|
||||
r"ST_CPM\ST_KanJub.CPM": {"Calc_Kanjub"},
|
||||
r"ST_CPM\ST_SanCal.CPM": {"Calq_San"},
|
||||
}
|
||||
|
||||
|
||||
def u16(b: bytes, o: int) -> int: return struct.unpack_from("<H", b, o)[0]
|
||||
def u32(b: bytes, o: int) -> int: return struct.unpack_from("<I", b, o)[0]
|
||||
|
||||
|
||||
def parse(path: Path):
|
||||
b = path.read_bytes(); pe = u32(b, 0x3C); opt = pe + 24
|
||||
image_base = u32(b, opt + 28); nsec = u16(b, pe + 6); opt_size = u16(b, pe + 20)
|
||||
sections = []
|
||||
for i in range(nsec):
|
||||
o = opt + opt_size + i * 40
|
||||
sections.append((u32(b, o + 12), u32(b, o + 8), u32(b, o + 20), u32(b, o + 16)))
|
||||
def rva_off(rva):
|
||||
for va, vs, ro, rs in sections:
|
||||
if va <= rva < va + max(vs, rs): return ro + rva - va
|
||||
return rva
|
||||
exp = rva_off(u32(b, opt + 96)); n_names = u32(b, exp + 24)
|
||||
funcs = rva_off(u32(b, exp + 28)); names = rva_off(u32(b, exp + 32)); ords = rva_off(u32(b, exp + 36))
|
||||
exports = {}
|
||||
for i in range(n_names):
|
||||
no = rva_off(u32(b, names + i * 4)); end = b.find(b"\0", no)
|
||||
name = b[no:end].decode("ascii"); ordinal = u16(b, ords + i * 2)
|
||||
exports[name] = u32(b, funcs + ordinal * 4)
|
||||
return b, image_base, rva_off, exports
|
||||
|
||||
|
||||
def string_at(b: bytes, off: int) -> str | None:
|
||||
if not 0 <= off < len(b): return None
|
||||
for encoding in ("ascii", "cp949"):
|
||||
end = b.find(b"\0", off, min(len(b), off + 300))
|
||||
if end - off < 4: continue
|
||||
try: text = b[off:end].decode(encoding)
|
||||
except UnicodeDecodeError: continue
|
||||
if all(ch.isprintable() or ch in "\r\n\t" for ch in text): return text
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
md = Cs(CS_ARCH_X86, CS_MODE_32); md.detail = True
|
||||
result = {}
|
||||
for rel, wanted in TARGETS.items():
|
||||
b, base, rva_off, exports = parse(SYSTEM / rel); module = {}
|
||||
for name in sorted(wanted):
|
||||
rva = exports.get(name)
|
||||
if rva is None: continue
|
||||
off = rva_off(rva); code = b[off:off + 8192]; lines = []
|
||||
for i, ins in enumerate(md.disasm(code, base + rva)):
|
||||
note = None
|
||||
# Delphi code frequently pushes absolute addresses of ANSI strings.
|
||||
for op in ins.operands:
|
||||
if op.type == 2 and base <= op.imm < base + len(b) + 0x100000:
|
||||
note = string_at(b, rva_off(op.imm - base))
|
||||
if note: break
|
||||
lines.append({"address": f"0x{ins.address:08X}", "mnemonic": ins.mnemonic, "op_str": ins.op_str, "string": note})
|
||||
if ins.mnemonic.startswith("ret") or i >= 1999: break
|
||||
module[name] = {"rva": f"0x{rva:X}", "instructions": lines}
|
||||
result[rel] = module
|
||||
OUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
assert result["DP_Common.CPM"]["GET_ROUND_CUT"]["instructions"]
|
||||
print({m: {f: len(x["instructions"]) for f, x in fs.items()} for m, fs in result.items()})
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,136 @@
|
||||
"""보유 STmate STC/XLSX에서 재구현 근거를 기계적으로 추출한다."""
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import zipfile
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SOURCE = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
||||
OUT = Path(__file__).with_name("evidence_extract.json")
|
||||
|
||||
|
||||
def dbf_rows(blob: bytes) -> list[dict[str, str | float | None]]:
|
||||
count = struct.unpack("<I", blob[4:8])[0]
|
||||
header_len = struct.unpack("<H", blob[8:10])[0]
|
||||
record_len = struct.unpack("<H", blob[10:12])[0]
|
||||
fields, offset = [], 32
|
||||
while blob[offset] != 0x0D:
|
||||
raw = blob[offset : offset + 32]
|
||||
fields.append((raw[:11].split(b"\0")[0].decode("ascii"), chr(raw[11]), raw[16]))
|
||||
offset += 32
|
||||
rows = []
|
||||
for number in range(count):
|
||||
record = blob[header_len + number * record_len : header_len + (number + 1) * record_len]
|
||||
if not record or record[0:1] == b"*":
|
||||
continue
|
||||
row, cursor = {}, 1
|
||||
for name, kind, length in fields:
|
||||
raw = record[cursor : cursor + length]
|
||||
cursor += length
|
||||
text = raw.decode("cp949", errors="replace").strip(" \0")
|
||||
if kind in "NF":
|
||||
try:
|
||||
row[name] = float(text) if text else None
|
||||
except ValueError:
|
||||
row[name] = text
|
||||
else:
|
||||
row[name] = text
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
|
||||
def normalize_formula(value: str) -> str:
|
||||
value = re.sub(r"\$?[A-Z]{1,3}\$?\d+", "CELL", value.upper())
|
||||
value = re.sub(r"\d+(?:\.\d+)?", "N", value)
|
||||
return value
|
||||
|
||||
|
||||
def nearby_label(ws, row: int, col: int) -> str:
|
||||
values = []
|
||||
for r in range(max(1, row - 1), row + 2):
|
||||
for c in range(max(1, col - 5), col):
|
||||
value = ws.cell(r, c).value
|
||||
if isinstance(value, str) and not value.startswith("="):
|
||||
value = " ".join(value.split())
|
||||
if value and value not in values:
|
||||
values.append(value)
|
||||
return " | ".join(values[-3:])
|
||||
|
||||
|
||||
def workbook_evidence(path: Path) -> dict:
|
||||
wb = load_workbook(path, data_only=False, read_only=True)
|
||||
result = {"path": str(path.relative_to(SOURCE)), "sheets": {}, "key_formulas": []}
|
||||
key_words = re.compile("간접|산재|고용|건강|연금|노인|퇴직|안전|환경|기타경비|일반관리|이윤|부가가치|관급|중기|일위|시장|견적")
|
||||
for ws in wb.worksheets:
|
||||
formulas, kinds = [], Counter()
|
||||
for row in ws.iter_rows():
|
||||
for cell in row:
|
||||
value = cell.value
|
||||
if isinstance(value, str) and value.startswith("="):
|
||||
formulas.append((cell.coordinate, value))
|
||||
kinds[normalize_formula(value)] += 1
|
||||
label = nearby_label(ws, cell.row, cell.column)
|
||||
if key_words.search(label) or key_words.search(ws.title):
|
||||
result["key_formulas"].append({"sheet": ws.title, "cell": cell.coordinate, "label": label, "formula": value})
|
||||
if formulas:
|
||||
result["sheets"][ws.title] = {
|
||||
"formula_count": len(formulas),
|
||||
"top_patterns": kinds.most_common(12),
|
||||
"cross_sheet_count": sum("!" in formula for _, formula in formulas),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def stc_evidence(path: Path) -> dict:
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
rate = {name: dbf_rows(zf.read(name)) for name in ("RATE.DBF", "RATE_M.DBF", "RATE_R.DBF", "RATE_RA.DBF")}
|
||||
costn = dbf_rows(zf.read("COSTN.DBF"))
|
||||
boq_names = sorted(name for name in zf.namelist() if re.fullmatch(r"BOQ\d\d\.DBF", name.upper()))
|
||||
boq = {name: dbf_rows(zf.read(name)) for name in boq_names}
|
||||
source_stats = defaultdict(lambda: {"count": 0, "selected_match": Counter()})
|
||||
for row in costn:
|
||||
code = str(row.get("CODE") or "")[:1]
|
||||
adopted = row.get("TAMT1")
|
||||
if adopted is None:
|
||||
adopted = row.get("AMT1")
|
||||
source_stats[code]["count"] += 1
|
||||
for slot in range(1, 7):
|
||||
candidates = [row.get(f"AMT_{slot}1"), row.get(f"AMT{slot}1"), row.get(f"AMT_{slot}")]
|
||||
if adopted is not None and adopted in candidates:
|
||||
source_stats[code]["selected_match"][str(slot)] += 1
|
||||
return {
|
||||
"path": str(path.relative_to(SOURCE)),
|
||||
"rate": rate,
|
||||
"costn_fields": list(costn[0]) if costn else [],
|
||||
"costn_source_stats": {k: {"count": v["count"], "selected_match": dict(v["selected_match"])} for k, v in source_stats.items()},
|
||||
"boq_tables": {name: {"rows": len(rows), "fields": list(rows[0]) if rows else []} for name, rows in boq.items()},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
stcs = [Path(p) for p in glob.glob(str(SOURCE / "**" / "*.STC"), recursive=True)]
|
||||
likely = []
|
||||
for p in glob.glob(str(SOURCE / "**" / "*.xlsx"), recursive=True):
|
||||
path = Path(p)
|
||||
name = path.name
|
||||
if any(token in name for token in ("간선임도사업(기번3)", "산불진화임도 실시설계", "임도시설사업2차", "계류보전사업(기번1 영덕", "산불진화임도 신설사업(기번8 울진")):
|
||||
likely.append(path)
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "all"
|
||||
data = {
|
||||
"stc": [stc_evidence(p) for p in sorted(stcs)] if mode in ("all", "stc") else [],
|
||||
"xlsx": [workbook_evidence(p) for p in sorted(likely)] if mode in ("all", "xlsx") else [],
|
||||
}
|
||||
OUT.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"STC={len(stcs)} XLSX={len(likely)} -> {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""JB 43개를 연도축으로 겹쳐 셀 사용패턴으로 블록 경계를 찾는다. 원본은 읽기만."""
|
||||
import pathlib, struct, sys, io, collections
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System\ST_BSC")
|
||||
|
||||
def read_dbf(p):
|
||||
b = p.read_bytes()
|
||||
nrec, hlen, rlen = struct.unpack_from("<IHH", b, 4)
|
||||
flds=[]; off=32
|
||||
while b[off] != 0x0D:
|
||||
flds.append((b[off:off+11].rstrip(b"\0").decode("cp949","replace"), b[off+16])); off += 32
|
||||
rows=[]
|
||||
for i in range(nrec):
|
||||
ro = hlen + i*rlen; rec = b[ro:ro+rlen]
|
||||
if len(rec) < rlen: break
|
||||
vals=[]; c=1
|
||||
for nm, ln in flds:
|
||||
s = rec[c:c+ln].decode("cp949","replace").strip(); c += ln
|
||||
try: vals.append(float(s) if s else 0.0)
|
||||
except ValueError: vals.append(0.0)
|
||||
rows.append(vals)
|
||||
return [f[0] for f in flds], rows
|
||||
|
||||
groups = collections.defaultdict(list)
|
||||
for p in sorted(ROOT.glob("*.JB_*")):
|
||||
groups[p.suffix.upper()].append(p)
|
||||
|
||||
for ext, files in sorted(groups.items()):
|
||||
shapes = collections.Counter()
|
||||
data = []
|
||||
for p in files:
|
||||
flds, rows = read_dbf(p)
|
||||
shapes[(len(rows), len(flds))] += 1
|
||||
data.append((p.name, flds, rows))
|
||||
print("\n" + "="*112)
|
||||
print("### %s 파일 %d개 형태: %s" % (ext, len(files), dict(shapes)))
|
||||
# 형태별로 다시 가름
|
||||
byshape = collections.defaultdict(list)
|
||||
for nm, flds, rows in data: byshape[(len(rows), len(flds))].append((nm, rows))
|
||||
for shape, items in sorted(byshape.items()):
|
||||
R, C = shape
|
||||
used = [[0]*C for _ in range(R)] # 몇 개 파일에서 이 셀이 0이 아닌가
|
||||
varies = [[set() for _ in range(C)] for _ in range(R)]
|
||||
for nm, rows in items:
|
||||
for r in range(R):
|
||||
for c in range(C):
|
||||
v = rows[r][c]
|
||||
if v: used[r][c] += 1
|
||||
varies[r][c].add(round(v, 4))
|
||||
print("\n-- 형태 %d행 × %d열 (%d개 파일)" % (R, C, len(items)))
|
||||
print(" 행별 '값이 든 열' 지도 (· 없음 / 숫자=몇 개 파일에 값 있음, 최대 %d)" % len(items))
|
||||
hdr = " 행 " + "".join(("%d"%((c+1)//10) if (c+1)>=10 else " ") for c in range(C))
|
||||
print(hdr); print(" " + "".join(str((c+1)%10) for c in range(C)))
|
||||
for r in range(R):
|
||||
line = "".join(("·" if used[r][c]==0 else ("#" if used[r][c]==len(items) else "+")) for c in range(C))
|
||||
nz = sum(1 for c in range(C) if used[r][c])
|
||||
print(" %2d %s 열%d개" % (r, line, nz))
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pathlib, struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System\ST_BSC")
|
||||
def rd(p):
|
||||
b=p.read_bytes(); nrec,hlen,rlen=struct.unpack_from("<IHH",b,4)
|
||||
f=[];o=32
|
||||
while b[o]!=0x0D: f.append((b[o:o+11].rstrip(b"\0").decode("cp949","replace"),b[o+16]));o+=32
|
||||
R=[]
|
||||
for i in range(nrec):
|
||||
r=b[hlen+i*rlen:hlen+(i+1)*rlen]
|
||||
if len(r)<rlen: break
|
||||
v=[];c=1
|
||||
for nm,ln in f:
|
||||
s=r[c:c+ln].decode("cp949","replace").strip();c+=ln
|
||||
try:v.append(float(s) if s else 0.0)
|
||||
except:v.append(0.0)
|
||||
R.append(v)
|
||||
return [x[0] for x in f], R
|
||||
|
||||
fo,Ro = rd(ROOT/"2020(상_0102)_제비율.JB_R")
|
||||
fn,Rn = rd(ROOT/"2020(상_0527)_제비율.JB_R")
|
||||
print("2020-01-02 (20열) vs 2020-05-27 (35열) — 5개월 차, 같은 해")
|
||||
|
||||
d=[(r,c+1,Ro[r][c],Rn[r][c]) for r in range(50) for c in range(20) if abs(Ro[r][c]-Rn[r][c])>1e-9]
|
||||
print("\n1~20열에서 값이 달라진 셀 %d개" % len(d))
|
||||
for r,c,a,b2 in d[:25]: print(" 행%2d 열%-2d %g -> %g" % (r,c,a,b2))
|
||||
|
||||
print("\n21~35열(새로 생긴 열)에서 값이 든 자리")
|
||||
rows={}
|
||||
for r in range(50):
|
||||
h=[(c+1,Rn[r][c]) for c in range(20,35) if Rn[r][c]]
|
||||
if h: rows[r]=h
|
||||
for r,h in rows.items(): print(" 행%2d : %s" % (r, " ".join("열%d=%g"%x for x in h)))
|
||||
print("\n새 열 중 실제로 쓰인 열 번호: %s" % sorted({c for h in rows.values() for c,_ in h}))
|
||||
print("새 열에 값이 든 행 번호: %s" % sorted(rows))
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pathlib, struct, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System\ST_BSC")
|
||||
for p in sorted(ROOT.glob("*.JB_*")):
|
||||
b=p.read_bytes(); nrec,hlen,rlen=struct.unpack_from("<IHH",b,4)
|
||||
n=0;o=32
|
||||
while b[o]!=0x0D: n+=1;o+=32
|
||||
print("%-46s %s %2d행 x %2d열 rec=%dB" % (p.name, p.suffix, nrec, n, rlen))
|
||||
@@ -0,0 +1,137 @@
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, data: bytes):
|
||||
self.data = data
|
||||
self.pos = 0
|
||||
|
||||
def take(self, size: int) -> bytes:
|
||||
value = self.data[self.pos : self.pos + size]
|
||||
if len(value) != size:
|
||||
raise EOFError(self.pos)
|
||||
self.pos += size
|
||||
return value
|
||||
|
||||
def byte(self) -> int:
|
||||
return self.take(1)[0]
|
||||
|
||||
def short_string(self) -> str:
|
||||
raw = self.take(self.byte())
|
||||
for encoding in ("cp949", "utf-8", "latin1"):
|
||||
try:
|
||||
return raw.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return raw.hex()
|
||||
|
||||
def integer(self) -> int:
|
||||
kind = self.byte()
|
||||
formats = {2: "<b", 3: "<h", 4: "<i", 19: "<q"}
|
||||
if kind not in formats:
|
||||
raise ValueError((self.pos - 1, "integer", kind))
|
||||
fmt = formats[kind]
|
||||
return struct.unpack(fmt, self.take(struct.calcsize(fmt)))[0]
|
||||
|
||||
def value(self):
|
||||
kind = self.byte()
|
||||
if kind == 0:
|
||||
return None
|
||||
if kind == 1:
|
||||
values = []
|
||||
while self.data[self.pos] != 0:
|
||||
values.append(self.value())
|
||||
self.pos += 1
|
||||
return values
|
||||
if kind in (2, 3, 4, 19):
|
||||
self.pos -= 1
|
||||
return self.integer()
|
||||
if kind == 5:
|
||||
return {"extended_hex": self.take(10).hex()}
|
||||
if kind in (6, 7):
|
||||
return self.short_string()
|
||||
if kind == 8:
|
||||
return False
|
||||
if kind == 9:
|
||||
return True
|
||||
if kind == 10:
|
||||
size = struct.unpack("<i", self.take(4))[0]
|
||||
return {"binary_size": size, "binary_hex": self.take(size).hex()[:64]}
|
||||
if kind == 11:
|
||||
values = []
|
||||
while True:
|
||||
item = self.short_string()
|
||||
if not item:
|
||||
return values
|
||||
values.append(item)
|
||||
if kind in (12, 20):
|
||||
size = struct.unpack("<i", self.take(4))[0]
|
||||
raw = self.take(size)
|
||||
encoding = "utf-8" if kind == 20 else "cp949"
|
||||
return raw.decode(encoding, errors="replace")
|
||||
if kind == 13:
|
||||
return None
|
||||
if kind == 14:
|
||||
items = []
|
||||
while self.data[self.pos] != 0:
|
||||
if self.byte() != 1:
|
||||
raise ValueError((self.pos - 1, "collection item"))
|
||||
props = self.properties()
|
||||
items.append(props)
|
||||
self.pos += 1
|
||||
return items
|
||||
if kind == 15:
|
||||
return struct.unpack("<f", self.take(4))[0]
|
||||
if kind in (16, 17, 21):
|
||||
raw = self.take(8)
|
||||
return struct.unpack("<d", raw)[0] if kind in (17, 21) else {"currency_raw": raw.hex()}
|
||||
if kind == 18:
|
||||
size = struct.unpack("<i", self.take(4))[0]
|
||||
return self.take(size * 2).decode("utf-16le", errors="replace")
|
||||
raise ValueError((self.pos - 1, "value", kind))
|
||||
|
||||
def properties(self) -> dict:
|
||||
props = {}
|
||||
while True:
|
||||
name = self.short_string()
|
||||
if not name:
|
||||
return props
|
||||
props[name] = self.value()
|
||||
|
||||
def component(self, root=False) -> dict:
|
||||
flags = []
|
||||
child_pos = None
|
||||
class_name = self.short_string()
|
||||
name = self.short_string()
|
||||
props = self.properties()
|
||||
children = []
|
||||
while self.data[self.pos] != 0:
|
||||
children.append(self.component())
|
||||
self.pos += 1
|
||||
return {
|
||||
"class": class_name,
|
||||
"name": name,
|
||||
"flags": flags,
|
||||
"child_pos": child_pos,
|
||||
"properties": props,
|
||||
"children": children,
|
||||
}
|
||||
|
||||
|
||||
def parse(path: Path) -> dict:
|
||||
reader = Reader(path.read_bytes())
|
||||
assert reader.take(4) == b"TPF0"
|
||||
result = reader.component(root=True)
|
||||
assert reader.pos == len(reader.data), (reader.pos, len(reader.data))
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for argument in sys.argv[1:]:
|
||||
source = Path(argument)
|
||||
output = source.with_suffix(".json")
|
||||
output.write_text(json.dumps(parse(source), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(output)
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json, sys, io, pathlib
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
BASE = pathlib.Path(__file__).resolve().parent.parent / "30_원자료"
|
||||
d = json.load(open(BASE/"TWM_KAN_RATE.json", encoding="utf-8"))
|
||||
|
||||
order = []
|
||||
def walk(n, parent=""):
|
||||
if isinstance(n, dict):
|
||||
if n.get("class") == "TRealGrid": order.append((n.get("name"), parent, n.get("properties") or {}))
|
||||
for c in n.get("children",[]) or []: walk(c, n.get("name",""))
|
||||
walk(d)
|
||||
|
||||
print("폼 자식 순서대로 나열한 RATE 편집 그리드")
|
||||
print("="*112)
|
||||
tot = {}
|
||||
for nm, parent, pr in order:
|
||||
cols = pr.get("Columns") or []
|
||||
grps = pr.get("Groups") or []
|
||||
rc = pr.get("RowCount")
|
||||
titles = [ (c.get("Title.Caption") or "").replace("\r"," / ").strip() for c in cols ]
|
||||
gtit = [ (g.get("Title.Caption") or "").replace("\r"," / ").strip() for g in grps ]
|
||||
tot.setdefault(parent, 0)
|
||||
tot[parent] += rc or 0
|
||||
print("\n■ %-18s parent=%-10s RowCount=%-3s 열 %d개" % (nm, parent, rc, len(cols)))
|
||||
for g in gtit: print(" [묶음] %s" % g)
|
||||
print(" 열: " + " | ".join(t if t else "(무제목)" for t in titles))
|
||||
print("\n" + "="*112)
|
||||
print("패널별 행수 합계:")
|
||||
for k, v in tot.items(): print(" %-12s %d행" % (k, v))
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Delphi 6 Math.RoundTo 재현 vs Excel ROUND 비교표."""
|
||||
import sys, io, decimal
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
D = decimal.Decimal
|
||||
|
||||
def delphi_intpower(base, e):
|
||||
# Delphi Math.IntPower: 반복 제곱 후 음수 지수면 역수
|
||||
r = 1.0; n = abs(e); b = float(base)
|
||||
while n:
|
||||
if n & 1: r *= b
|
||||
b *= b; n >>= 1
|
||||
return 1.0/r if e < 0 else r
|
||||
|
||||
def delphi_roundto(v, adigit):
|
||||
f = delphi_intpower(10.0, adigit)
|
||||
# Delphi Round() = FPU 현재모드 = 기본 half-to-even (파이썬 round 와 같음)
|
||||
return round(v / f) * f
|
||||
|
||||
def excel_round(v, places):
|
||||
q = D(1).scaleb(-places)
|
||||
return float(D(repr(v)).quantize(q, rounding=decimal.ROUND_HALF_UP))
|
||||
|
||||
def excel_rounddown(v, places):
|
||||
q = D(1).scaleb(-places)
|
||||
return float(D(repr(v)).quantize(q, rounding=decimal.ROUND_DOWN))
|
||||
|
||||
CASES = [(2.44,1),(2.45,1),(2.46,1),(-2.44,1),(-2.45,1),(-2.46,1),
|
||||
(0.5,0),(1.5,0),(2.5,0),(3.5,0),(-0.5,0),(-1.5,0),(-2.5,0),(-3.5,0),
|
||||
(1250.0,-2),(1350.0,-2),(-1250.0,-2),(2500.0,-3),(3500.0,-3),(-2500.0,-3)]
|
||||
|
||||
print("| 입력 | 자리 | STmate 내부 `ROUND`\n(Delphi `Math.RoundTo`) | Excel `ROUND`\n(STmate 출력 XLSX) | 일치 |")
|
||||
print("|---:|---:|---:|---:|:--:|")
|
||||
diff = 0
|
||||
for v, places in CASES:
|
||||
adigit = -places # RoundTo 의 ADigit 는 10의 지수
|
||||
d = delphi_roundto(v, adigit)
|
||||
x = excel_round(v, places)
|
||||
ok = abs(d - x) < 1e-9
|
||||
if not ok: diff += 1
|
||||
print("| %s | %s | %s | %s | %s |" % (
|
||||
("%g"%v), ("소수%d"%places if places>0 else ("원" if places==0 else "%d원"%(10**-places))),
|
||||
("%g"%d), ("%g"%x), "○" if ok else "**✗**"))
|
||||
print("\n불일치 %d / %d" % (diff, len(CASES)))
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""STmate CPM 모듈에서 Math.RoundTo / Math.Floor 임포트 썽크의 실제 호출 자리를 센다."""
|
||||
import pathlib, struct, re, sys
|
||||
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System")
|
||||
TARGETS = ["ST_KanJub.CPM","ST_SanCal.CPM","ST_Boq.CPM","ST_EdiLWi.CPM","ST_P_Joong.CPM","ST_Prt.CPM"]
|
||||
WANT = {b"@Math@RoundTo$qqrxdxzc":"RoundTo", b"@Math@Floor$qqrxg":"Floor",
|
||||
b"@Math@Ceil$qqrxg":"Ceil", b"@Math@SimpleRoundTo$qqrxdxzc":"SimpleRoundTo",
|
||||
b"@Math@SetRoundMode$qqrx21Math":"SetRoundMode"}
|
||||
|
||||
def pe(b):
|
||||
e = struct.unpack_from("<I", b, 0x3C)[0]
|
||||
assert b[e:e+4] == b"PE\0\0"
|
||||
nsec = struct.unpack_from("<H", b, e+6)[0]
|
||||
optsz = struct.unpack_from("<H", b, e+20)[0]
|
||||
opt = e+24
|
||||
magic = struct.unpack_from("<H", b, opt)[0]
|
||||
imgbase = struct.unpack_from("<I", b, opt+28)[0]
|
||||
ddoff = opt + (96 if magic==0x10b else 112)
|
||||
imp_rva, imp_sz = struct.unpack_from("<II", b, ddoff+8)
|
||||
secs=[]
|
||||
so = opt+optsz
|
||||
for i in range(nsec):
|
||||
s = b[so+i*40: so+i*40+40]
|
||||
name = s[:8].rstrip(b"\0").decode("latin1")
|
||||
vsz, va, rsz, ptr = struct.unpack_from("<IIII", s, 8)
|
||||
secs.append((name, va, vsz, ptr, rsz))
|
||||
return imgbase, imp_rva, secs
|
||||
|
||||
def r2o(rva, secs):
|
||||
for n,va,vsz,ptr,rsz in secs:
|
||||
if va <= rva < va+max(vsz,rsz): return ptr + (rva-va)
|
||||
return None
|
||||
|
||||
for t in TARGETS:
|
||||
ps = list(ROOT.rglob(t))
|
||||
if not ps: print("!! %s 없음"%t); continue
|
||||
b = ps[0].read_bytes()
|
||||
imgbase, imp_rva, secs = pe(b)
|
||||
io_ = r2o(imp_rva, secs)
|
||||
found = {}
|
||||
off = io_
|
||||
while True:
|
||||
oft, tds, fwd, nm, fth = struct.unpack_from("<IIIII", b, off)
|
||||
if nm == 0: break
|
||||
thunk_rva = oft or fth
|
||||
to = r2o(thunk_rva, secs); io2 = r2o(fth, secs)
|
||||
i = 0
|
||||
while True:
|
||||
v = struct.unpack_from("<I", b, to + 4*i)[0]
|
||||
if v == 0: break
|
||||
if not (v & 0x80000000):
|
||||
no = r2o(v, secs)
|
||||
nmend = b.index(b"\0", no+2)
|
||||
sym = b[no+2:nmend]
|
||||
if sym in WANT:
|
||||
found[WANT[sym]] = imgbase + fth + 4*i # IAT 절대주소
|
||||
i += 1
|
||||
off += 20
|
||||
txt = [(n,va,vsz,ptr,rsz) for n,va,vsz,ptr,rsz in secs if n.upper() in ("CODE",".TEXT")]
|
||||
print("\n### %s (imagebase 0x%X)" % (t, imgbase))
|
||||
if not found: print(" Math 임포트 없음"); continue
|
||||
for sym, iat in sorted(found.items()):
|
||||
pat = b"\xFF\x15" + struct.pack("<I", iat)
|
||||
total = 0
|
||||
for n,va,vsz,ptr,rsz in txt:
|
||||
total += b.count(pat, ptr, ptr+rsz)
|
||||
pat2 = b"\xFF\x25" + struct.pack("<I", iat) # 썽크 jmp
|
||||
thunks = b.count(pat2)
|
||||
print(" %-14s IAT=0x%08X 직접 call %d곳, jmp 썽크 %d개" % (sym, iat, total, thunks))
|
||||
|
||||
print("\n\n========== 썽크 경유 호출(E8 rel32) 집계 ==========")
|
||||
for t in TARGETS:
|
||||
ps = list(ROOT.rglob(t))
|
||||
if not ps: continue
|
||||
b = ps[0].read_bytes()
|
||||
imgbase, imp_rva, secs = pe(b)
|
||||
io_ = r2o(imp_rva, secs); off = io_; found = {}
|
||||
while True:
|
||||
oft, tds, fwd, nm, fth = struct.unpack_from("<IIIII", b, off)
|
||||
if nm == 0: break
|
||||
to = r2o(oft or fth, secs); i = 0
|
||||
while True:
|
||||
v = struct.unpack_from("<I", b, to+4*i)[0]
|
||||
if v == 0: break
|
||||
if not (v & 0x80000000):
|
||||
no = r2o(v, secs); sym = b[no+2:b.index(b"\0", no+2)]
|
||||
if sym in WANT: found[WANT[sym]] = imgbase + fth + 4*i
|
||||
i += 1
|
||||
off += 20
|
||||
code = [(n,va,vsz,ptr,rsz) for n,va,vsz,ptr,rsz in secs if n.upper() in ("CODE",".TEXT")]
|
||||
print("\n### %s" % t)
|
||||
for sym, iat in sorted(found.items()):
|
||||
pat = b"\xFF\x25" + struct.pack("<I", iat)
|
||||
k = b.find(pat)
|
||||
if k < 0: print(" %-10s 썽크 못 찾음" % sym); continue
|
||||
# 썽크 파일오프셋 -> VA
|
||||
thunk_va = None
|
||||
for n,va,vsz,ptr,rsz in secs:
|
||||
if ptr <= k < ptr+rsz: thunk_va = imgbase + va + (k-ptr); break
|
||||
calls = 0
|
||||
for n,va,vsz,ptr,rsz in code:
|
||||
for m in re.finditer(b"\xE8", b[ptr:ptr+rsz]):
|
||||
o = ptr + m.start()
|
||||
if o+5 > ptr+rsz: continue
|
||||
rel = struct.unpack_from("<i", b, o+1)[0]
|
||||
site_va = imgbase + va + (o-ptr)
|
||||
if site_va + 5 + rel == thunk_va: calls += 1
|
||||
print(" %-10s 썽크VA=0x%08X 호출 %d곳" % (sym, thunk_va, calls))
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pathlib, struct, re, sys, io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System")
|
||||
b = list(ROOT.rglob("ST_KanJub.CPM"))[0].read_bytes()
|
||||
e = struct.unpack_from("<I", b, 0x3C)[0]
|
||||
nsec = struct.unpack_from("<H", b, e+6)[0]; optsz = struct.unpack_from("<H", b, e+20)[0]
|
||||
opt = e+24; imgbase = struct.unpack_from("<I", b, opt+28)[0]
|
||||
secs=[]; so=opt+optsz
|
||||
for i in range(nsec):
|
||||
s=b[so+i*40:so+i*40+40]; nm=s[:8].rstrip(b"\0").decode("latin1")
|
||||
vsz,va,rsz,ptr=struct.unpack_from("<IIII", s, 8); secs.append((nm,va,vsz,ptr,rsz))
|
||||
def v2o(v):
|
||||
for n,va,vsz,ptr,rsz in secs:
|
||||
if imgbase+va <= v < imgbase+va+rsz: return ptr+(v-imgbase-va)
|
||||
|
||||
START, END = 0x0043A200, 0x0043A400
|
||||
o1, o2 = v2o(START), v2o(END)
|
||||
blk = b[o1:o2]
|
||||
|
||||
print("=== ST_KanJub.CPM 0x%08X..0x%08X (RoundTo 12곳이 몰린 함수) ===" % (START, END))
|
||||
# mov dl, imm8 (B2 xx) = RoundTo 두번째 인자(자리수, signed char)
|
||||
digits = [(START+m.start(), struct.unpack_from("<b", blk, m.start()+1)[0]) for m in re.finditer(b"\xB2", blk)]
|
||||
print("\nRoundTo 2번째 인자(자리수) 로드 — `mov dl, imm8`:")
|
||||
for va, d in digits: print(" 0x%08X 자리수 = %d" % (va, d))
|
||||
|
||||
IDIOM = {
|
||||
b"\xD9\x7D": "FNSTCW [ebp-x] (FPU 제어워드 저장)",
|
||||
b"\xD9\x6D": "FLDCW [ebp-x] (FPU 제어워드 로드 = 모드 변경)",
|
||||
b"\xDB\x5D": "FISTP dword (정수 저장, 현재 모드 적용)",
|
||||
b"\xDF\x7D": "FISTP qword",
|
||||
b"\xD9\xE0": "FCHS", b"\xD9\xE1": "FABS", b"\xD9\xFC": "FRNDINT",
|
||||
}
|
||||
print("\n이 구간의 x87 모드/정수화 관용구:")
|
||||
seen = {}
|
||||
for pat, desc in IDIOM.items():
|
||||
n = blk.count(pat)
|
||||
if n: print(" %-3s x %s" % (n, desc))
|
||||
if not any(blk.count(p) for p in IDIOM): print(" 없음")
|
||||
|
||||
print("\n원시 바이트 (16B/줄):")
|
||||
for i in range(0, len(blk), 16):
|
||||
print("0x%08X %s" % (START+i, " ".join("%02X"%c for c in blk[i:i+16])))
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pathlib, re, collections
|
||||
ROOT = pathlib.Path(r"C:\경영정보(주)\System")
|
||||
|
||||
SYMS = {
|
||||
"RoundTo": rb"@Math@RoundTo\$qqrxdxzc",
|
||||
"SimpleRoundTo":rb"@Math@SimpleRoundTo\$qqrxdxzc",
|
||||
"Floor": rb"@Math@Floor\$qqrxg",
|
||||
"Ceil": rb"@Math@Ceil\$qqrxg",
|
||||
"SetRoundMode": rb"@Math@SetRoundMode\$",
|
||||
"IntPower": rb"@Math@IntPower\$",
|
||||
"MROUNDUD": rb"MROUNDUD",
|
||||
"MROUNDUP": rb"MROUNDUP",
|
||||
"MTRUNC": rb"MTRUNC",
|
||||
"MINT": rb"\x00MINT\x00",
|
||||
"GET_ROUND_CUT":rb"GET_ROUND_CUT",
|
||||
"CALC_STR": rb"CALC_STR",
|
||||
"ROUNDDOWN": rb"ROUNDDOWN",
|
||||
"ROUNDUP": rb"ROUNDUP",
|
||||
"ROUND(": rb"ROUND\(",
|
||||
}
|
||||
order = list(SYMS)
|
||||
|
||||
rows = []
|
||||
for p in sorted(ROOT.rglob("*")):
|
||||
if not p.is_file(): continue
|
||||
try: b = p.read_bytes()
|
||||
except Exception: continue
|
||||
if b[:2] != b"MZ": continue
|
||||
c = {k: len(re.findall(v, b)) for k, v in SYMS.items()}
|
||||
if c["RoundTo"] or c["Floor"] or c["MROUNDUD"] or c["GET_ROUND_CUT"] or c["MTRUNC"] or c["MINT"]:
|
||||
rows.append((p.name, c))
|
||||
|
||||
w = max(len(r[0]) for r in rows)
|
||||
hdr = "모듈".ljust(w) + " " + " ".join(s[:7].rjust(7) for s in order)
|
||||
print(hdr); print("-"*len(hdr))
|
||||
for name, c in sorted(rows, key=lambda r: -sum(r[1].values())):
|
||||
print(name.ljust(w) + " " + " ".join(str(c[s] or "").rjust(7) for s in order))
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""원가계산 설정을 4형식(_T/_S/_TA/_SA)과 패널로 갈라 정리한다."""
|
||||
import json, sys, io, pathlib, re, collections
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
BASE = pathlib.Path(__file__).resolve().parent.parent / "30_원자료"
|
||||
d = json.load(open(BASE/"TWM_KANJUB.json", encoding="utf-8"))
|
||||
|
||||
def walk(n, parent="", out=None):
|
||||
if out is None: out=[]
|
||||
if isinstance(n, dict):
|
||||
out.append((n.get("class",""), n.get("name",""), n.get("properties") or {}, parent))
|
||||
for c in n.get("children",[]) or []: walk(c, n.get("name",""), out)
|
||||
return out
|
||||
nodes = walk(d)
|
||||
|
||||
def get(pr, suffix):
|
||||
for k, v in pr.items():
|
||||
if k.lower().endswith(suffix): return v
|
||||
return None
|
||||
|
||||
FORM = {"_T":"일반 T", "_S":"수공 S", "_TA":"실적일반 TA", "_SA":"실적수공 SA"}
|
||||
def form_of(nm):
|
||||
for suf in ("_TA","_SA","_T","_S"):
|
||||
if nm.endswith(suf): return FORM[suf]
|
||||
return "공통"
|
||||
|
||||
rows = collections.defaultdict(list)
|
||||
for cls, nm, pr, parent in nodes:
|
||||
items = get(pr, "items.strings") or (pr.get("Items") if isinstance(pr.get("Items"), list) else None)
|
||||
if not items: continue
|
||||
vals = get(pr, "values.strings")
|
||||
rows[form_of(nm)].append((nm, parent, [str(x).strip() for x in items],
|
||||
[str(x).strip() for x in vals] if isinstance(vals, list) else None))
|
||||
|
||||
print("### 원가계산 설정 — 형식별 선택축")
|
||||
for f in ["공통","일반 T","수공 S","실적일반 TA","실적수공 SA"]:
|
||||
if f not in rows: continue
|
||||
print("\n" + "="*100)
|
||||
print("## %s 선택축 %d개" % (f, len(rows[f])))
|
||||
for nm, parent, items, vals in rows[f]:
|
||||
mismatch = " ⚠개수불일치(선택지%d/내부값%d)" % (len(items), len(vals)) if vals and len(vals)!=len(items) else ""
|
||||
# 화면문자열과 내부값이 다른 축만 상세 표시
|
||||
differ = vals and any(a != b for a, b in zip(items, vals))
|
||||
print("\n ■ %-20s (%s) %d개%s%s" % (nm, parent, len(items), mismatch, " ※화면≠내부" if differ else ""))
|
||||
for i, s in enumerate(items):
|
||||
v = vals[i] if vals and i < len(vals) else None
|
||||
if v is not None and v != s: print(" %2d %-40s → 내부 `%s`" % (i, s[:40], v))
|
||||
else: print(" %2d %s" % (i, s[:60]))
|
||||
if vals and len(vals) > len(items):
|
||||
print(" ▶ 화면에 없는 내부값: %s" % vals[len(items):])
|
||||
@@ -0,0 +1,73 @@
|
||||
# STmate 분석
|
||||
|
||||
경쟁사 설계예산 프로그램 **STmate**(경영정보(주)) 를 정적으로 뜯어본 기록.
|
||||
원본은 저장소 밖 `C:\경영정보(주)` — **원본은 손대지 않음**, 여기에만 정리물을 둠.
|
||||
옆 폴더 `../오솔길 분석/` 과 같은 얼개.
|
||||
|
||||
> ⚠ `resources/knowledge/original/원가계산/STmate/` 는 **다른 것** — 그쪽은 STmate 가 **뽑아낸 산출물**(STC·XLSX)을 뜯어본 기록이고, 여기는 **프로그램 자체**를 뜯어본 기록임.
|
||||
|
||||
## STmate 가 무엇인가
|
||||
|
||||
- **Delphi 6 네이티브 데스크톱 프로그램.** 캐드 플러그인 아님. 본체 `STmate.Cpm` + 기능별 `*.CPM` 플러그인 수십 개.
|
||||
- 저장소는 **Apollo/Six 계열 DBF**, 교환 파일은 **ZIP 기반 `.STC`**.
|
||||
- 하는 일 — 기초 단가·코드 자료로 일위대가·단가산출을 조립하고, 공종 수량을 곱해 내역을 만들고, 제비율·법정경비·일반관리비·이윤·부가세를 얹어 총공사비를 냄.
|
||||
- 실행은 확인 못 함(프로세스가 창을 안 띄움). **파일·바이너리 정적 분석으로만** 확인한 것임.
|
||||
|
||||
## 어디까지 봤나 — 세 겹
|
||||
|
||||
| 겹 | 무엇을 함 | 볼 수 있나 |
|
||||
| --- | --- | --- |
|
||||
| **계산 코어** (`*.CPM` 실행모듈) | 식 평가, 반올림, 간접비·안전비·변경설계·끝전조정 | ⚠ 부분 — PE 섹션·임포트·문자열·호출자리까지. 명령어 단위 역공학은 안 함 |
|
||||
| **기준자료·프로젝트 파일** (DBF·STC·XLSX) | 요율표, 단가, 수량, 결과 금액 | ✅ 전부 읽힘 |
|
||||
| **화면 폼 리소스** (DFM) | 그리드 열 제목, 선택지와 내부 코드값, **밑수 공식** | ✅ 복원 완료 — 이번 분석의 최대 수확 |
|
||||
|
||||
즉 **「어느 명령어가 언제 도는가」는 못 보고, 「어떤 값에 어떤 밑수와 어떤 반올림이 걸리는가」는 대부분 보임.**
|
||||
|
||||
## 문서 — `20_분석/`
|
||||
|
||||
읽는 순서는 **`14_최종_분석완료_보고서.md` 부터**가 빠름. 전체 인덱스는 `00_분석_요약과_읽는법.md` 에 있음.
|
||||
|
||||
| 번호 | 내용 |
|
||||
| --- | --- |
|
||||
| 00 | 전체 인덱스, 결론, Aislo 비교, 등급 갱신 이력, 용어 대조 |
|
||||
| 01~02 | 실행 기술·모듈 구조, DBF 스키마·파일형식 |
|
||||
| 03~04 | 계산로직과 Aislo 비교, 입력 → 결과 데이터 계보 |
|
||||
| 05 | 확정등급 기준, 조사 표본, 중간값 계약 |
|
||||
| 06~08 | 내부규칙 아홉 — RATE·간접비·끝전 / 직접비·단가·중기·자재 / 재계산·변경설계·일괄변경 |
|
||||
| 09 | 통합 재구현 명세 |
|
||||
| 10 | `System` 186개 전수 장부 |
|
||||
| 11 | CPM 계산코어 정적분석 — **반올림 경계표** |
|
||||
| 12 | 원가계산 4형식과 RATE표 복원 — **설정 83축** |
|
||||
| 13 | 통제시험 목록 T1~T11 — 아직 못 가른 경계 |
|
||||
| 14 | 최종 분석완료 보고서 |
|
||||
|
||||
## 확정된 굵직한 것
|
||||
|
||||
- `TAMT = MAMT + LAMT + SAMT` — 총액은 네 번째 성분이 아니라 **세 성분의 합**
|
||||
- **내부 반올림은 은행가 반올림**(최근접-짝수)이고 사사오입이 아님. STmate 가 뽑은 엑셀은 Excel 사사오입이라 **20개 경계 중 10개가 원 단위에서 갈림**
|
||||
- **간접비 항목별 밑수**를 화면 그리드 묶음 제목에서 확정 — 환경보전비 밑수는 직접공사비가 아님
|
||||
- 원가계산 **네 형식**(일반·수공·실적일반·실적수공)은 출력 모양이 아니라 **공식 집합이 통째로 다름**
|
||||
- 기준자료 `JB_R` 35열 확장 시점 **2020-05-27**, 기존 1~20열은 한 칸도 안 바뀜
|
||||
- 설정 **83축** 전수. 화면 순번과 내부값이 **뒤집힌 축 둘** 있음 — 그대로 저장하면 정반대 공식이 걸림
|
||||
|
||||
## 원자료 — `30_원자료/`
|
||||
|
||||
`system_inventory.csv`(186개 해시 장부) · `system_structures.json`(전 파일 내부구조) · `core_cpm_static.json` · `export_disassembly.json` · `core_formula_strings.json` · 복원한 폼 세 벌(`.dfm`/`.json`) · 문자열 덤프 · `settings_axis_map.txt`.
|
||||
|
||||
## 재현 스크립트 — `40_스크립트/`
|
||||
|
||||
전부 `C:\경영정보(주)` 를 **읽기만** 함. 원자료는 `../30_원자료/` 를 봄.
|
||||
|
||||
| 스크립트 | 하는 일 |
|
||||
| --- | --- |
|
||||
| `analyze_system.py` | 186개 전수 등록·해시·형식 판정 |
|
||||
| `evidence_extract.py` · `disassemble_exports.py` | 계산식·export 추출 |
|
||||
| `parse_dfm.py` | 폼 리소스 → JSON |
|
||||
| `rounding_symbol_matrix.py` · `rounding_callsites.py` · `rounding_dispatcher_dump.py` | 반올림 심볼 임포트·호출자리·함수 덤프 |
|
||||
| `round_table.py` | 내부 ↔ Excel 반올림 경계표 |
|
||||
| `jb_shapes.py` · `jb_block_map.py` · `jb_expansion_diff.py` | JB 43개 형태·블록·확장 차이 |
|
||||
| `rate_grid_defs.py` · `settings_axis_map.py` | 폼에서 그리드·선택축 추출 |
|
||||
|
||||
## 다음
|
||||
|
||||
`13_통제시험_목록.md` 의 T1~T11. **프로그램을 직접 돌려 값 하나씩 바꿔 보는 일**이라 정적 분석으로는 못 가름. 우선순위 1은 T1(요율 좌표 매핑).
|
||||
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""구조물 폼의 **`empty_means` select 는 첫 보기를 박지 않는다** (2026-09-13).
|
||||
|
||||
2026-08-17 지시로 select 에는 빈 보기를 두지 않았다 — 「첫 보기가 곧 기본값」이기 때문이다.
|
||||
그런데 2026-09-09 에 셋째 갈래가 생겼다: **비워 두는 것이 뜻인 칸**(`empty_means`). 이 칸까지
|
||||
첫 보기를 박으니 **엔진 기준값과 다른 값이 조용히 저장**됐다.
|
||||
|
||||
실측 사고 — `fill_concrete_mpa` 는 등록부 기본이 없고 「비우면 계산 쪽 기준 강도」인데 폼이
|
||||
「180」을 박아 저장했다. 엔진 기준은 확정 ⑩ 의 **210** 이라 채움콘크리트 규격·단가가 갈렸다.
|
||||
|
||||
⚠ 기본값이 **있는** 칸은 그대로 첫 보기를 쓴다 — 2026-08-17 지시가 그 자리다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
|
||||
|
||||
PANEL = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(encoding="utf-8")
|
||||
API = (ROOT / "B05_Profile" / "B05_Profile_Api_Structures.ts").read_text(encoding="utf-8")
|
||||
COMMIT = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel_Commit.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _registry_options(type_id: str) -> dict[str, dict]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
)
|
||||
item = next(entry for entry in payload["types"] if entry["type_id"] == type_id)
|
||||
return {option["key"]: option for option in item["options"]}
|
||||
|
||||
|
||||
def test_폼이_empty_means_를_보고_빈_보기를_둔다() -> None:
|
||||
"""소스 검사 — 빈 보기와 초기값이 **같은 판정**(`keepEmpty`)을 쓴다."""
|
||||
assert "option.empty_means" in PANEL
|
||||
assert "const keepEmpty = mustPick || meansEmpty;" in PANEL
|
||||
assert 'if (keepEmpty) choices.unshift(["", mustPick ? "— 선택 —" : "— 안 정함 —"]);' in PANEL
|
||||
# 빈 보기만 넣고 초기값을 첫 보기로 두면 화면은 「안 정함」인데 값은 첫 보기가 된다.
|
||||
assert 'String(preset || (keepEmpty ? "" : (option.choices[0] ?? "")))' in PANEL
|
||||
# 클라이언트 옵션 타입에 칸이 있어야 서버 응답의 `empty_means` 가 살아 온다.
|
||||
assert "empty_means?: string | null;" in API
|
||||
# 빈 칸은 아예 안 실린다 — 이 규칙이 있어야 「안 정함」이 정본에 안 박힌다.
|
||||
assert "if (input.isEmpty()) return;" in COMMIT
|
||||
|
||||
|
||||
def test_기본값_있는_칸은_빈_보기를_안_둔다() -> None:
|
||||
"""`empty_means` 가 적혀 있어도 **기본값이 있으면** 그 값이 뜬다(2026-08-17 지시 유지)."""
|
||||
options = _registry_options("masonry_wet")
|
||||
# 버림 콘크리트 — empty_means 가 있으나 기본이 「넣음」이다.
|
||||
assert options["blinding_concrete"]["empty_means"]
|
||||
assert options["blinding_concrete"]["default"] == "넣음"
|
||||
# 채움 강도·야면석 계수 — 기본이 없어 빈 보기가 서야 하는 칸.
|
||||
for key in ("fill_concrete_mpa", "stone_coeff_basis"):
|
||||
assert options[key]["empty_means"], key
|
||||
assert options[key]["default"] is None, key
|
||||
assert options[key]["required"] is False, key
|
||||
assert '(option.default ?? "") === ""' in PANEL # 기본값 있으면 빈 보기를 안 둠
|
||||
|
||||
|
||||
def _wall(**options) -> dict:
|
||||
return {
|
||||
"structure_id": "w",
|
||||
"type_id": "masonry_wet",
|
||||
"name": "돌쌓기(찰)",
|
||||
"start_m": 0.0,
|
||||
"end_m": 10.0,
|
||||
"length_m": 10.0,
|
||||
"height_m": 2.5,
|
||||
"options": {
|
||||
"height_m": 2.5,
|
||||
"length_m": 10.0,
|
||||
"back_len_cm": 45,
|
||||
"stone_kind": "깬돌",
|
||||
"foundation": "기초유",
|
||||
**options,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _fill_row(**options) -> dict:
|
||||
table = build_table([_wall(**options)], {"masonry_wet": "돌쌓기(찰)"}, {})
|
||||
rows = table["structures"][0]["components"]
|
||||
return next(row for row in rows if row["name"] == "채움콘크리트")
|
||||
|
||||
|
||||
def test_칸을_비우면_강도가_확정_210_으로_선다() -> None:
|
||||
"""⚠ 폼이 180 을 박던 그 자리 — 비면 **엔진 기준값**이 서고 그 사실이 근거에 남는다."""
|
||||
row = _fill_row()
|
||||
assert row["spec"] == "210"
|
||||
assert "안 정해 기본값" in row["basis"]
|
||||
|
||||
|
||||
def test_고른_값은_그대로_이긴다() -> None:
|
||||
row = _fill_row(fill_concrete_mpa="180")
|
||||
assert row["spec"] == "180"
|
||||
assert "저장 제원에서 고른 값" in row["basis"]
|
||||
@@ -21,7 +21,10 @@ COMMON = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Section_Common.ts").rea
|
||||
B05_PANEL = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Panel.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
B06_PAGE = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page.ts").read_text(encoding="utf-8")
|
||||
# 설계 재계산 자리는 700줄 제한으로 `_UI_Page_Design_Sync` 로 떨어져 나갔다(2026-09-13).
|
||||
B06_PAGE = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page.ts").read_text(encoding="utf-8") + (
|
||||
PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _stale(profile_samples, sections, tol=1e-3):
|
||||
|
||||
@@ -39,7 +39,9 @@ def test_B05_는_종전대로_유무_종류_위치만():
|
||||
def test_기본값_없는_필수_항목은_빈_칸으로_연다():
|
||||
"""첫 항목을 슬쩍 고르면 **근거 없는 값**(뒷길이 25㎝ 같은)이 수량·단가로 흘러간다."""
|
||||
panel = _PANEL.read_text(encoding="utf-8")
|
||||
assert 'choices.unshift(["", "— 선택 —"])' in panel
|
||||
# 2026-09-13 — 빈 보기를 두는 까닭이 둘이 되어(필수 + `empty_means`) 한 줄로 합쳐졌다.
|
||||
# 필수 칸의 글자는 그대로 「— 선택 —」이다.
|
||||
assert 'choices.unshift(["", mustPick ? "— 선택 —" : "— 안 정함 —"])' in panel
|
||||
assert 'option.required === true && (option.default ?? "") === ""' in panel
|
||||
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ def test_선택이_저장_patch_까지_실린다() -> None:
|
||||
|
||||
for name in (
|
||||
"B06_Section_Cross_Design_Session.ts", # 캐시 칸
|
||||
"B06_Section_UI_Page.ts", # 캐시에 쓰는 자리
|
||||
"B06_Section_UI_Page_Design_Sync.ts", # 캐시에 쓰는 자리
|
||||
"B06_Section_Cross_Refresh.ts", # 브라우저 재계산이 읽는 자리
|
||||
"B06_Section_UI_Page_Persist.ts", # [저장]·[확정] patch
|
||||
):
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""표준도 장 나눔 — **자리 칸(앞뒤 걸침)은 장을 가르지 않는다** (2026-09-13).
|
||||
|
||||
장 나눔 축은 「제원 조합」이다(PLAN 4-3). 구간 규약의 `before_m`·`after_m` 는 기준 측점에서
|
||||
앞뒤로 얼마나 걸치나일 뿐이라, 제원이 같은 두 기가 이 칸 때문에 두 장이 되면 안 된다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Sheet import sheet_key # noqa: E402
|
||||
|
||||
|
||||
def _stone(**options) -> dict:
|
||||
return {"type_id": "masonry_wet", "height_m": 2.0, "options": {"back_len_cm": 45, **options}}
|
||||
|
||||
|
||||
def test_앞뒤_걸침이_달라도_같은_장() -> None:
|
||||
assert sheet_key(_stone(before_m=5, after_m=5)) == sheet_key(_stone(before_m=0, after_m=12))
|
||||
assert sheet_key(_stone(outlet_revet_before_m=3)) == sheet_key(_stone())
|
||||
|
||||
|
||||
def test_제원이_다르면_여전히_장이_갈린다() -> None:
|
||||
assert sheet_key(_stone(before_m=5)) != sheet_key(_stone(before_m=5, back_len_cm=55))
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""표준도(구조물도) 하단표에 **칸이 서는지** — 날개벽·집수정·옹벽 (계획서 4-13).
|
||||
|
||||
4-13 표의 「⚠ 칸만 오면 됨」 줄을 닫는 자리다. 하단표는 B08 전개를 접기만 하므로
|
||||
(`build_standard_sheets`), **관측 원단위가 들어온 종류는 저절로 선다**. 반대로 자료가
|
||||
없는 규격은 줄이 0개이고 **사유만** 뜬다 — 값을 지어내지 않는 것이 확정이다(확정 5차 3번).
|
||||
|
||||
⚠ 이 시험은 수량값을 검산하지 않는다(그것은 `test_b08_wing_wall` · `test_b08_observed_unit`
|
||||
몫이다). 여기서 보는 것은 **표준도 하단표까지 값이 실려 오는가**다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Sheet import ( # noqa: E402
|
||||
build_standard_sheets,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
|
||||
|
||||
|
||||
def _sheets(structures: list[dict], names: dict[str, str]) -> dict[str, dict]:
|
||||
payload = build_standard_sheets(build_table(structures, names, {}), {})
|
||||
return {sheet["type_id"]: sheet for sheet in payload["sheets"]}
|
||||
|
||||
|
||||
def _배수관(**options) -> dict:
|
||||
return {
|
||||
"structure_id": "p1",
|
||||
"type_id": "pipe",
|
||||
"name": "배수관 1",
|
||||
"start_m": 60.0,
|
||||
"end_m": 68.0,
|
||||
"length_m": 8.0,
|
||||
"options": {"pipe_diameter_mm": "800", "station": 60.0, **options},
|
||||
}
|
||||
|
||||
|
||||
def _옹벽(form: str, height_m: float) -> dict:
|
||||
return {
|
||||
"structure_id": "w1",
|
||||
"type_id": "retaining_wall",
|
||||
"name": "옹벽",
|
||||
"start_m": 100.0,
|
||||
"end_m": 110.0,
|
||||
"length_m": 10.0,
|
||||
"height_m": height_m,
|
||||
"options": {"form": form, "height_m": height_m, "length_m": 10.0, "station": 100.0},
|
||||
}
|
||||
|
||||
|
||||
def test_날개벽_집수정_칸이_선다() -> None:
|
||||
"""관에 형식을 채우면 **딸린 두 장**이 저절로 서고 줄마다 단위당 값이 붙는다."""
|
||||
sheets = _sheets(
|
||||
[
|
||||
_배수관(
|
||||
inlet_basin_form="돌집수정 ㄷ형",
|
||||
inlet_basin_material="콘크리트",
|
||||
wing_wall_type="C-TYPE",
|
||||
)
|
||||
],
|
||||
{"pipe": "배수관"},
|
||||
)
|
||||
for type_id in ("pipe_inlet_basin", "pipe_wing_wall"):
|
||||
sheet = sheets.get(type_id)
|
||||
assert sheet is not None, f"{type_id} 장이 안 섰다"
|
||||
# 개소당 — 관측 원단위가 개소 기준이라 연장으로 접지 않는다(4-1: 단위를 통일하지 않는다).
|
||||
assert sheet["billing_unit"] == "개소"
|
||||
assert sheet["rows"], f"{type_id} 하단표가 비었다"
|
||||
# 「값이 없다」를 0 으로 때우지 않는다 — 단위당을 못 낸 줄이 있으면 이름으로 드러난다.
|
||||
assert sheet["unpriced_rows"] == []
|
||||
assert all(row["amount"] > 0 for row in sheet["rows"])
|
||||
|
||||
|
||||
def test_형식을_안_고르면_장이_안_선다() -> None:
|
||||
"""안 놓은 것과 같다 — 빈 장을 만들지 않는다(`ATTACHMENTS` 문턱)."""
|
||||
sheets = _sheets([_배수관()], {"pipe": "배수관"})
|
||||
assert "pipe_inlet_basin" not in sheets
|
||||
assert "pipe_wing_wall" not in sheets
|
||||
|
||||
|
||||
def test_옹벽은_자료가_있는_규격만_선다() -> None:
|
||||
"""반중력식 H=2.0 은 소광리 「옹벽2.0」에서 왔고, 식생옹벽블럭은 **자료가 없다**."""
|
||||
있음 = _sheets([_옹벽("반중력식", 2.0)], {"retaining_wall": "옹벽"})["retaining_wall"]
|
||||
assert 있음["rows"], "관측 원단위가 있는데 하단표가 비었다"
|
||||
assert 있음["unpriced_rows"] == []
|
||||
|
||||
없음 = _sheets([_옹벽("식생옹벽블럭", 2.0)], {"retaining_wall": "옹벽"})["retaining_wall"]
|
||||
assert 없음["rows"] == [], "자료 없는 규격에 값이 지어졌다"
|
||||
assert any("자료에 없습니다" in note for note in 없음["notes"]), 없음["notes"]
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""표준도 제원 — 버림 콘크리트 칸(확정 8-2·14)과 물빼기 그림 주석(확정 5차 작은 것 2).
|
||||
|
||||
**버림 콘크리트** — 셈은 이미 「기본은 넣고, 「안 넣음」이면 뺀다」로 서 있었는데
|
||||
(`wants_blinding`) **끄는 칸이 화면에 없었다**. 등록부 · 표준도 입력 · 폼 세 자리를 함께
|
||||
이어 놓은 것을 굳힌다.
|
||||
⚠ **문구가 폴리시다** — 폼이 보내는 말이 `wants_blinding` 이 「빼기」로 읽는 말과 한 글자라도
|
||||
다르면 골라도 안 빠진다. 그래서 선택지 문구를 **엔진에 직접 물어** 확인한다.
|
||||
|
||||
**물빼기 주석** — 그림이 기본 상수를 적고 있어, 구조물별로 지름·면적을 고쳐도 **표는 바뀌고
|
||||
그림 글자만 옛 값**으로 남았다(확정 10 「구조물별로 다르게도 가능」과 어긋남).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( # noqa: E402
|
||||
build_figure,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Edit import ( # noqa: E402
|
||||
BLINDING_CHOICES,
|
||||
EDITABLE_KEYS,
|
||||
clean_spec,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wants_blinding # noqa: E402
|
||||
|
||||
#: 버림이 서는 종류 — 돌쌓기 식(`stone_masonry`)·큰돌쌓기를 타는 것들.
|
||||
BLINDING_TYPES = ("masonry_wet", "masonry_dry", "boulder_masonry", "revetment")
|
||||
|
||||
|
||||
def _registry() -> dict[str, dict]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
)
|
||||
return {item["type_id"]: item for item in payload["types"]}
|
||||
|
||||
|
||||
def test_등록부에_버림_칸이_있다() -> None:
|
||||
"""칸이 없으면 저장소가 그 제원을 거절한다(`drop_unregistered` 의 그 자리)."""
|
||||
registry = _registry()
|
||||
for type_id in BLINDING_TYPES:
|
||||
options = {option["key"]: option for option in registry[type_id]["options"]}
|
||||
field = options.get("blinding_concrete")
|
||||
assert field is not None, f"{type_id} 등록부에 버림 칸이 없다"
|
||||
assert field["choices"] == list(BLINDING_CHOICES)
|
||||
# 기본은 **넣음** — 확정 ⑭. 빈 칸도 넣음이지만 화면에 보여야 한다.
|
||||
assert field["default"] == "넣음"
|
||||
assert field["phase"] == "detail"
|
||||
|
||||
|
||||
def test_표준도가_버림_칸을_받는다() -> None:
|
||||
assert "blinding_concrete" in EDITABLE_KEYS
|
||||
spec, notes = clean_spec({"blinding_concrete": "안 넣음"})
|
||||
assert spec == {"blinding_concrete": "안 넣음"} and notes == []
|
||||
# 빈 값은 「정한 적 없음」 — 키를 지운다(그러면 기본인 「넣음」으로 돈다).
|
||||
spec, _ = clean_spec({"blinding_concrete": ""})
|
||||
assert spec == {"blinding_concrete": None}
|
||||
# 없는 갈래는 막지 않고 **알린다**(표준도 입력의 규칙).
|
||||
_spec, notes = clean_spec({"blinding_concrete": "빼기도"})
|
||||
assert notes and "버림 콘크리트" in notes[0]
|
||||
|
||||
|
||||
def test_선택지_문구가_엔진이_읽는_말과_같다() -> None:
|
||||
"""⚠ 이 시험이 없으면 「제외」처럼 다른 말로 바뀌어도 아무 데서도 안 걸린다."""
|
||||
넣음, 안넣음 = BLINDING_CHOICES
|
||||
assert wants_blinding({"blinding_concrete": 넣음}) is True
|
||||
assert wants_blinding({"blinding_concrete": 안넣음}) is False
|
||||
assert wants_blinding({}) is True # 정한 적 없음 = 넣음
|
||||
|
||||
|
||||
def _figure_labels(**options) -> list[str]:
|
||||
sheet = {
|
||||
"key": "masonry_wet",
|
||||
"title": "돌쌓기(찰)",
|
||||
"type_id": "masonry_wet",
|
||||
"height_m": 2.0,
|
||||
"options": {"height_m": 2.0, "back_len_cm": 45, **options},
|
||||
}
|
||||
entities, _height = build_figure(
|
||||
"d1", sheet, "layer", (0.0, 0.0), "#000000", "#111111", "#222222"
|
||||
)
|
||||
return [str((item.get("shapeData") or {}).get("label") or "") for item in entities]
|
||||
|
||||
|
||||
def test_물빼기_주석이_구조물_값을_쓴다() -> None:
|
||||
labels = _figure_labels(weep_hole_diameter_mm=75, weep_hole_area_m2=3)
|
||||
물구멍 = [text for text in labels if "물구멍" in text]
|
||||
assert 물구멍, labels
|
||||
assert "Ø75" in 물구멍[0] and "3㎡당" in 물구멍[0], 물구멍[0]
|
||||
|
||||
|
||||
def test_안_정하면_국가기준_기본이_뜬다() -> None:
|
||||
물구멍 = [text for text in _figure_labels() if "물구멍" in text]
|
||||
assert 물구멍 and "Ø50" in 물구멍[0] and "2㎡당" in 물구멍[0], 물구멍
|
||||
|
||||
|
||||
def test_여기서_고치기_단추가_고정_액션_줄에_있다() -> None:
|
||||
"""확정 6 ㉮ — 표는 CAD 안이라 단추를 못 달아 **고정 액션 줄**에 둔다(값 복사본 없음)."""
|
||||
page = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_UI_Page.ts").read_text(encoding="utf-8")
|
||||
assert '"여기서 고치기"' in page
|
||||
assert "confirmButtonRow.append(specJumpButton" in page
|
||||
# 표준도 장에서만 뜬다 — 다른 도면에는 갈 곳이 없다.
|
||||
assert "specJumpButton.hidden = false" in page
|
||||
assert "specJumpButton.hidden = true" in page
|
||||
# 폼에 버림 칸이 섰다 — 끄는 자리가 화면에 있다(확정 8-2).
|
||||
form = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_UI_StandardSpec.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "blinding_concrete" in form
|
||||
@@ -0,0 +1,57 @@
|
||||
"""PLAN 7장 V-12 — 준비공 줄이 B08 인계에서 **B09 내역서까지 하나도 안 새나** (2026-09-13).
|
||||
|
||||
「안 하면 표토제거 2,359.03㎥ 가 다시 통째로 사라짐」 자리. 보내는 쪽(B08)과 받는 쪽(B09)을
|
||||
**마주 걸어** 본다 — 한쪽만 보면 양쪽 다 조용히 틀린다(PLAN 7장 머리 규칙).
|
||||
|
||||
⚠ 겨누는 것 둘
|
||||
① 준비공·부대시설 줄은 **내역 줄 아니면 제외 줄**로 반드시 선다 — 어디에도 없는 줄이 0
|
||||
② 금액이 서는 줄(`in_bill`)은 **수량째** 내역 본체에 선다 — 표토제거 2,359.035㎥
|
||||
⚠ 단가가 붙는지는 여기서 안 본다 — 표토 축(시트 9번)·임목축적 입력이 정해져야 서는 자리다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_prep # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill # noqa: E402
|
||||
|
||||
SLOPE = {
|
||||
"face_dressing_fill": 9000.0,
|
||||
"face_dressing_cut": 6726.9,
|
||||
"tree_removal_fill": 9000.0,
|
||||
"tree_removal_cut": 6726.9,
|
||||
"bench_cut_fill": 9000.0,
|
||||
}
|
||||
|
||||
|
||||
def _handoff_and_bill():
|
||||
handoff = build_handoff(preparation_table=build_prep(SLOPE, [], [], 0.15))
|
||||
return handoff, build_bill(handoff)
|
||||
|
||||
|
||||
def test_준비공_줄은_내역_아니면_제외로_반드시_선다() -> None:
|
||||
handoff, bill = _handoff_and_bill()
|
||||
sent = [r for r in handoff["work_items"] if r["origin"] == "preparation"]
|
||||
assert len(sent) >= 6
|
||||
body = [r.name for r in bill.rows if not r.is_group]
|
||||
excluded = [r.name for r in bill.excluded]
|
||||
for row in sent:
|
||||
landed = body if row["in_bill"] else excluded
|
||||
assert row["name"] in landed, (row["name"], row["in_bill"])
|
||||
|
||||
|
||||
def test_금액이_서는_줄은_수량째_본체에_선다() -> None:
|
||||
handoff, bill = _handoff_and_bill()
|
||||
topsoil = next(r for r in bill.rows if not r.is_group and r.name == "표토제거")
|
||||
assert Decimal(str(topsoil.quantity)).quantize(Decimal("0.01")) == Decimal("2359.04")
|
||||
in_bill = [
|
||||
r["name"] for r in handoff["work_items"] if r["origin"] == "preparation" and r["in_bill"]
|
||||
]
|
||||
assert {"표토제거", "제근·뿌리다듬기", "뿌리 적재"} <= set(in_bill)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""유사 직종 바꿔쓰기 — 야면석 채집 「인 부」 → 보통인부 (2026-09-13 · 사용자 확정 8-1).
|
||||
|
||||
⚠ 겨누는 것 셋
|
||||
① 야면석 채집(13-2-4)의 「인 부」가 **못 붙은 줄에서 빠지고** 보통인부 노임으로 붙는다
|
||||
② 바꿔쓰기는 **그 공종에만** 걸린다 — 전역 별칭이 아니다
|
||||
③ 산출기초에 **준용 근거**가 함께 뜬다(조용히 바꾸지 않음)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation.B09_Estimation_BasisSheet import work_item_basis # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_ResourceAxis_UnitBasis import ( # noqa: E402
|
||||
SIMILAR_OCCUPATION,
|
||||
similar_occupation_note,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402
|
||||
|
||||
CODE = "FP-13-02-04"
|
||||
|
||||
|
||||
def test_인부가_보통인부로_붙는다() -> None:
|
||||
build = cached_build()
|
||||
assert "인 부" not in build.unattached.get(CODE, [])
|
||||
titles = [code for code in build.book.titles if code.startswith(f"B-{CODE}#")]
|
||||
# 뒷길이 다섯 × 밑수 둘(㎡당·㎥당)
|
||||
assert len(titles) == 10, titles
|
||||
for code in titles:
|
||||
names = {build.book.title(d.ref_code).name for d in build.book.details.get(code, [])}
|
||||
assert "보통인부" in names, (code, names)
|
||||
assert build.book.resolve(code).labor > 0
|
||||
|
||||
|
||||
def test_바꿔쓰기는_그_공종에만_걸린다() -> None:
|
||||
assert {code for code, _ in SIMILAR_OCCUPATION} == {CODE}
|
||||
assert similar_occupation_note("FP-12-10") == ""
|
||||
|
||||
|
||||
def test_산출기초에_준용_근거가_뜬다() -> None:
|
||||
rows = [row for row in work_item_basis(cached_build()) if row["code"].startswith(f"B-{CODE}#")]
|
||||
assert rows
|
||||
for row in rows:
|
||||
assert any("보통인부 준용" in note and "45101-45" in note for note in row["notes"]), row
|
||||
@@ -19,7 +19,10 @@ REPO = _read("B06_Section/B06_Section_Repository.py")
|
||||
B06_ROUTER = _read("B06_Section/B06_Section_Router.py")
|
||||
REFRESH = _read("B06_Section/B06_Section_Cross_Refresh.ts")
|
||||
B05_PREVIEW = _read("B05_Profile/B05_Profile_UI_Profile_Preview.ts")
|
||||
B06_PAGE = _read("B06_Section/B06_Section_UI_Page.ts")
|
||||
# 설계 재계산 자리는 700줄 제한으로 `_UI_Page_Design_Sync` 로 떨어져 나갔다(2026-09-13).
|
||||
B06_PAGE = _read("B06_Section/B06_Section_UI_Page.ts") + _read(
|
||||
"B06_Section/B06_Section_UI_Page_Design_Sync.ts"
|
||||
)
|
||||
|
||||
|
||||
def test_workflow_route_context_has_no_confirmed_filter():
|
||||
|
||||
Reference in New Issue
Block a user