knowledge(경쟁사): STmate 재구현 가능성 검증 — 문서 오류 셋 정정·요율 좌표 확정

문서 주장을 믿지 않고 원본 파일과 직접 대조한 결과.

바로잡은 것
- COSTN.DBF 는 암호화돼 있지 않음. 맛보기 71행·봉화 248행을 그대로 읽음.
  암호화된 것은 BDQTY 와 SYSINFO 둘뿐. 단가 네 성분이 평문이라 골든셋을
  단가 수준까지 뽑을 수 있음
- TAMT=MAMT+LAMT+SAMT 에 예외가 있음. 59행 중 2행(관급 흄관)이 어긋남.
  하드 오류로 막으면 실데이터를 거부하게 됨
- 프로젝트 RATE 표는 4종이 아니라 6종. RATE_A(25x10)·RATE_B(35x10) 추가 발견.
  값이 0~1 계수로 성격이 다르며 수공형식 밑수 배분표로 보임

새로 확정한 것 — 통제시험 T1 의 절반을 정적으로 풀었음
- 출력 XLSX 비고 칸의 실제 요율을 프로젝트 RATE 표에서 역추적해 좌표 확정
  산재 RATE_R[45,1] · 건강 [46,1] · 연금 [46,6] · 장기요양 [46,11]
  고용보험 등급1~7 [45,11..17] · 산안비 [20,17] 율 [20,18] 기초액
  간접노무비 [10,1]
- 5개 연도 시계열이 실제 법정요율 변천과 일치해 A등급
- RATE_R 행 45~49 가 부속표 영역, 행 0~44 가 규모·기간 행렬임을 가름

판정 — 기초단가 원천(자재 5.7만건·노임·중기·표준시장단가)이 전부 암호화라
STmate 복제는 불가. 계산 엔진은 구현 가능하고 Aislo 목적에는 충분함.
문서 15 신규, 검증 스크립트 5개 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Prk9BCHG1EMAywk9k8wegA
This commit is contained in:
2026-09-13 12:08:44 +09:00
co-authored by Claude Opus 5
parent 7bd6d10145
commit b656e5944d
12 changed files with 512 additions and 12 deletions
@@ -6,21 +6,26 @@
| 문서 | 용도 | 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 |
| 00 분석 요약과 읽는 법 (이 문서) | 전체 인덱스, 결론, Aislo 비교 | https://share.onorca.dev/a/jAFkUlfWjAOv |
| 01 전체 구조 | 실행 기술, 모듈, 파일 흐름, 기능 구조 | https://share.onorca.dev/a/YrTXvA_VN7OI |
| 02 데이터셋과 파일형식 | DBF 스키마, 프로젝트 패키지, 기준자료, 암호화 경계 | https://share.onorca.dev/a/dkOGvoT4jO8S |
| 03 계산로직과 Aislo 비교 | 확인된 공식, 계층 합산, 단가·제비율 모델, 차이와 권고 | https://share.onorca.dev/a/CPOaNTIjmPDA |
| 04 계산 데이터계보 | 입력 필드 → 계산 → 결과 필드 단계별 추적 | https://share.onorca.dev/a/j231bE3HQqNw |
| 05 재구현 판정기준과 근거목록 | 확정등급, 조사 표본, 파이프라인, 중간값 계약 | https://share.onorca.dev/a/NbIR-TMqLPjc |
| 06 규칙1·7 | RATE 좌표, 간접비 순서, 끝전·VAT | https://share.onorca.dev/a/U1-yAvd5qfbj |
| 07 규칙2·3·4·5 | 직접비 단가, 중기, BDQTY, 자재 채택 | https://share.onorca.dev/a/c7n8XytHiqGt |
| 08 규칙6·8·9 | 재계산, 변경설계, 일괄변경 | https://share.onorca.dev/a/G21A30euiRNl |
| 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 |
| 15 재구현 가능성 검증 | **무엇을 만들 수 있고 무엇이 막혀 있나** — 층별 확보표, 문서 오류 정정, T1 좌표 확정 | https://share.onorca.dev/a/t6HGy0HlbEBl |
> **정본은 저장소 파일이다** — `resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/`.
> 위 Orca 링크는 보기용이며, 문서를 지식DB 자리로 옮기면서 주소가 한 번 바뀌었다.
> 09·10·11·12·13·14번 링크는 재발행이 속도제한에 걸려 **아직 옛 주소**다 — 그 여섯은 저장소 파일을 볼 것.
중요: C등급으로 표시한 규칙은 “분석 누락”이 아니라 보유 정적 자료에 입력 전후 대응값이 없거나 레코드가 난독화되어 확정 불가능한 경계다. 문서에는 각 경계를 확정할 STmate 통제 대조시험까지 명시했다. System 폴더는 186/186개 모두 내부 구조까지 판독했으며 미판독 파일은 0개다.
@@ -40,6 +45,10 @@
| `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 |
| `COSTN.DBF` 암호화 | 본문 암호화라고 적음 | **평문 — 읽힘.** 암호화는 `BDQTY`·`SYSINFO` 둘뿐 | 02번, 15번 §3 |
| `TAMT=MAMT+LAMT+SAMT` | 예외 없는 불변조건 | **예외 있음** — 59행 중 2행(관급 흄관) | 11번 §3, 15번 §3 |
| 프로젝트 RATE 표 | 4종 | **6종**`RATE_A`·`RATE_B` 추가 발견 | 02번, 15번 §3 |
| 보험 요율 좌표 | 미확정 | **A** — 산재·건강·연금·장기요양·고용 등급1~7·산안비 좌표 확정 | 12번 §9.1, 15번 §4 |
분석일: 2026-09-13
대상: `C:\경영정보(주)`
@@ -47,7 +47,9 @@ STini.MEM, COSTN.SMT, PRJLST.SMT
- 6개 원천/4개 안: `AMT_11..64`, `PG_11..54`
- 표시·분류: `GUN`, `GUN2`, `IFORM`, `ACTTYP`, `EDT_DATE`
본문은 암호화되어 있으나 스키마는 평문이다. memo 내용은 `COSTN.SMT`에 저장된다.
**2026-09-13 정정 — `COSTN.DBF` 본문은 암호화돼 있지 않다.** 맛보기 STC의 71개 레코드를 그대로 읽었다(`B00001 흄관기초 D=800mm M TAMT1=23441 MAMT1=2719 LAMT1=20722 SAMT1=0`). 실무 STC 7건도 같다. 암호화된 것은 `BDQTY.DBF``SYSINFO.DBF` 둘뿐이다. memo 내용은 `COSTN.SMT`에 저장된다.
이 정정은 재구현 가능성에 크게 작용한다 — **단가의 네 성분이 프로젝트 파일 안에 평문으로 들어 있으므로 골든셋을 단가 수준까지 뽑을 수 있다.**
### `COSTA.DBF` — 별도 단가 자료
@@ -74,6 +76,25 @@ STini.MEM, COSTN.SMT, PRJLST.SMT
15필드. `CODE`, `NAME`, `BQCODE`, `QTY0..9`, `IFORM`, `XCODE`로 구성된다. 맛보기에는 레코드가 없으나 수량 계산식 또는 수량 슬롯을 코드화하는 자리로 보인다.
### `RATE*.DBF` — 프로젝트에 복사된 제비율 표 (6종)
맛보기 STC에는 **여섯 종**이 들어 있다. 기존 문서가 다루던 넷 말고 `RATE_A`·`RATE_B`가 더 있다.
| 테이블 | 형태 | 값이 든 행 | 대응 기준파일 | 비고 |
|---|---|---:|---|---|
| `RATE.DBF` | 41행 × 10열 | 37 | `JB_1` | |
| `RATE_M.DBF` | 25행 × 10열 | 11 | `JB_M` | 행자부기준 간접노무비. 값이 `15.0`처럼 **백분율** |
| `RATE_R.DBF` | 50행 × 35열 | 38 | `JB_R` 신형 | |
| `RATE_RA.DBF` | 50행 × 20열 | 27 | `JB_RA` | |
| **`RATE_A.DBF`** | 25행 × 10열 | 21 | **대응 JB 파일 없음** | 값이 `0.642 / 0.232 / 0.270 / 0.875 / 0.912`처럼 **0~1 계수**. 앞 5열만 씀 |
| **`RATE_B.DBF`** | 35행 × 10열 | 35 | **대응 JB 파일 없음** | 같은 0~1 계수를 **10열 모두** 씀 |
`RATE_A``RATE_M`과 형태가 같지만 **값의 성격이 다르다**(계수 vs 백분율). 전 행이 다르므로 같은 표가 아니다.
`RATE_B`의 10열은 RATE 편집 화면 `rGrd_RateB`의 숫자열 개수와 정확히 맞는다 — `재 / 직노 / 노 / 재+직노 / 재+노`가 토목·건축 두 열군으로 10개다. 따라서 **`RATE_B`는 수공형식(`S`)의 경비별 밑수 배분 계수표**로 보인다. **등급 B.**
**두 표는 맛보기 STC에만 있고 실무 STC 7건에는 없다.** 수공형식을 쓰지 않는 프로젝트에는 만들어지지 않는 것으로 보이며, 이는 **수공형식 재현 시험(T11)의 전제 자료가 실무 표본에 없다**는 뜻이다.
### `KONGJONG.DBF` — 공종 분류
`CODE`, `NAME`, `SIZE` 세 필드의 작은 분류표다.
@@ -60,6 +60,19 @@ CODE="W" 또는 복합행 → TAMT=MAMT+LAMT+SAMT
따라서 총액을 네 번째 독립 비목으로 저장하거나 계산하면 안 된다. 총액은 세 성분의 합이며, 단일 비목행에서는 해당 성분만 총액과 같고 나머지는 0이다. **등급 A**.
#### ⚠ 실데이터에는 예외가 있다 (2026-09-13 실측)
맛보기 STC의 `COSTN.DBF` 71행 중 값이 있는 59행을 대조하면 **57행은 맞고 2행이 어긋난다.**
| CODE | 명칭 | 규격 | `TAMT1` | `MAMT1+LAMT1+SAMT1` | 차이 |
|---|---|---|---:|---:|---:|
| `M00010` | 흄관 | 800mm | 65,880 | 41,608 | **+24,272** |
| `M00011` | 흄관 | 900mm | 83,920 | 53,784 | **+30,136** |
둘 다 `CODE``M`(자재)이고 흄관 — 관급자재로 다루는 품목이다. 즉 **바이너리의 조건식은 「항상 참인 불변조건」이 아니라 「어긋나면 경고할 검사식」**이며, 관급·비과세처럼 별도로 취급하는 자재에는 예외가 있다.
**재구현 지침** — 이 관계를 **하드 오류로 막으면 실데이터를 거부하게 된다.** 경고로 두고 `component_sum_exempt` 같은 사유 필드를 함께 저장할 것. 예외의 정확한 조건은 통제시험 **T7**(13번 문서) 대상이다.
## 4. 공통 계산 함수의 의미
### 4.1 식 평가
@@ -254,7 +254,32 @@ DBF 필드는 `RATE0..RATE9` 또는 `RATE_01..RATE_35`, 레코드는 고정 25/4
- 2020-05-27 확장으로 **열 21 = 국가유산 간접노무비, 열 26 = 국가유산 기타경비**가 붙었고 기존 1~20열은 한 셀도 바뀌지 않았다.
- `JB_RA`는 43개 전 기간 20열 그대로다. 실적형식에는 국가유산 공종이 없다.
**남은 것** — 각 부속표 그리드(고용보험·환경보전·보증수수료 계열)가 `JB_1`/`JB_R`의 몇 번째 행부터 적재되는지. 열 접근이 실행 중 문자열 조립이라 코드 상수로 읽을 수 없으므로, 저장 이벤트 역추적 또는 통제 동적 시험이 필요하다.
### 9.1 확정된 좌표 (2026-09-13, 15번 문서)
출력 XLSX 「공사원가계산서」 비고 칸에 **실제 쓰인 밑수와 요율이 적혀 있다.** 그 값을 같은 프로젝트의 `RATE_*.DBF`에서 되찾아 좌표를 역추적하고, 실무 7건 교차로 후보가 하나뿐인 자리만 취했다.
| 항목 | 좌표 | 근거 |
|---|---|---|
| 간접노무비 | `RATE_R[10, 1]` | 2건 유일 |
| 산재보험료 | `RATE_R[45, 1]` | 3건 유일 |
| 건강보험료 | `RATE_R[46, 1]` | 2건 유일 |
| 연금보험료 | `RATE_R[46, 6]` | 시계열 |
| 노인장기요양보험료 | `RATE_R[46, 11]` | 1건 유일 + 시계열 |
| 고용보험료 등급 1~7 | `RATE_R[45, 11..17]` | 4건 유일 + 시계열 |
| 산업안전보건관리비 | `RATE_R[20, 17]` 율 · `[20, 18]` 기초액 | 3건 유일 |
**시계열이 실제 법정요율 변천과 일치해 A등급이다.**
| 좌표 | 2019 | 2022 | 2024 | 2025 | 2026 |
|---|---:|---:|---:|---:|---:|
| `[45,1]` 산재 | 3.75 | 3.70 | 3.56 | 3.56 | 3.56 |
| `[46,1]` 건강 | 3.23 | 3.495 | 3.545 | 3.545 | 3.595 |
| `[46,6]` 연금 | 4.5 | 4.5 | 4.5 | 4.5 | 4.75 |
| `[46,11]` 장기요양 | 8.51 | 12.27 | 12.95 | 12.95 | 13.14 |
**`RATE_R` 행 45~49는 요율 행렬이 아니라 부속표 영역**이고, 행 0~44가 규모·기간 행렬이다. `[45,11..17]``rGrd_Rate_Kobo`(7행, 등급·요율) 그대로다.
**남은 것**`연금보험료`·`일반관리비`·`이윤`·`환경보전비`·`부가가치세`는 같은 값이 표 안 여러 곳에 있어 단독으로는 못 가른다. 그 값만 특이한 숫자로 바꿔 저장하는 통제시험(13번 T1)이 필요하다. 부속표 그리드(환경보전·보증수수료 계열)의 시작 행도 같은 시험 대상이다.
## 10. 계산 설정 입력
@@ -28,7 +28,19 @@
| 확정되는 것 | 그리드 → 행렬 좌표의 완전 매핑. `rate_standard()` 기준표를 하드코딩 없이 만들 수 있게 됨 |
| 근거 문서 | 12번 §8·§9, 10번 §4 |
이미 잡아 둔 것 — 행은 `0,5,10,…,45` 시작의 **5행 묶음**, `JB_R` 열 1~5=간접노무비·6~10=기타경비(토목/산설(토목)/조경/건축/산설(건축)), 신형 열 21·26=국가유산. **T1은 나머지 부속표의 시작 행만 남긴 시험이다.**
**2026-09-13 — T1 의 절반이 정적으로 풀렸다.** 출력 XLSX 비고 칸의 실제 요율을 프로젝트 `RATE_*`에서 역추적해 다음이 확정됐다(15번·12번 §9.1).
```text
간접노무비 RATE_R[10, 1]
산재보험료 RATE_R[45, 1] 건강보험료 RATE_R[46, 1]
연금보험료 RATE_R[46, 6] 장기요양 RATE_R[46, 11]
고용보험 등급1~7 RATE_R[45, 11..17]
산업안전보건관리비 RATE_R[20, 17] 율 · [20, 18] 기초액
```
시계열이 실제 법정요율 변천과 맞아 **A등급**이다. 행 45~49가 부속표 영역, 행 0~44가 규모·기간 행렬임도 갈렸다.
**T1 에 남은 것**`연금보험료`·`일반관리비`·`이윤`·`환경보전비`·`부가가치세`처럼 **같은 값이 표 안 여러 곳에 있는 항목**과, 환경보전·보증수수료 부속표의 시작 행. 이것들은 그 칸만 특이한 값(예: `99.99`)으로 바꿔 저장해 어디에 앉는지 보는 수밖에 없다.
### T2 — 금액·기간 구간 경계 방향
@@ -0,0 +1,154 @@
# 재구현 가능성 검증 — 모인 정보로 같은 프로그램을 만들 수 있나
검증일: 2026-09-13
방법: 05~14번 문서의 주장을 믿지 않고, **구현에 꼭 필요한 것을 층별로 세어 원본 파일과 직접 대조**함.
대상: `C:\경영정보(주)` 설치 폴더 186개 + 실무 STC 8건 + STmate 생성 XLSX 29건. **읽기만 함.**
## 1. 한 줄 답
**계산 엔진은 만들 수 있음. 기준자료를 갖춘 완제품은 못 만듦.**
STmate가 배포하는 **기초단가 원천이 전부 암호화**되어 있어 자재·노임·중기 단가를 한 건도 못 읽음. 반면 **프로젝트 파일(STC) 안에는 단가 결과가 평문으로 들어 있어** 「이 단가를 넣으면 이 내역·이 간접비가 나온다」는 재현은 가능함.
Aislo 입장에서는 이것으로 충분함 — Aislo는 **자기 기초단가 원천**(노임·자재·유가·환율)을 이미 따로 갖고 있으므로, STmate에서 가져올 것은 **계산 규칙**이지 기초자료가 아님.
## 2. 층별 확보 상태
| 층 | 구현에 필요한 것 | 확보 | 판정 |
|---|---|---|---|
| **1 파일 읽기** | STC = 무암호 ZIP | 8건 모두 암호 없음 | ✅ |
| | 프로젝트 표 18종 스키마·본문 | 16종 평문 | ✅ |
| | `BDQTY.DBF` (수량·할증 상세) | **본문 암호화** | ❌ |
| | `SYSINFO.DBF` | **본문 암호화** | ❌ |
| **2 기초단가 원천** | `JDCODE_M.BSC` 자재 57,580건 | **본문 암호화** | ❌ **치명** |
| | `JDCODE_L.BSC` 노임 460건 | **본문 암호화** | ❌ |
| | `JDCODE_X.BSC` 중기 833건 | **본문 암호화** | ❌ |
| | `JDCODE_Z.BSC` 33,927건 · `GRPCOD_Z` 12,125건 | **본문 암호화** | ❌ |
| | 표준시장단가 `*.CSTA` 4,921~4,961건 × 6 | **본문 암호화** | ❌ |
| | `*.STBSC` 28개 (노임·기계경비 배포) | ZIP 컨테이너, 내부 암호 | ❌ |
| **3 단가 계산** | `COSTN.DBF` 단가 네 성분 | **평문 — 읽힘** | ✅ |
| | `COSTN.SMT` 구성 토큰 | 평문, 문법 문서화됨 | ✅ |
| | 중기 손료 계수(감가상각·정비·관리율) 실제 값 | **어디에도 없음** | ❌ |
| | 자재 6후보 중 자동채택 규칙 | C등급 | ❌ |
| **4 내역** | `BOQ11.DBF` 수량·단가·금액 | 평문 | ✅ |
| | 계층 집계(`SORTCODE`·`ENDCODE`·`ITM_TYP`) | 문서화됨 | ✅ |
| **5 간접비** | 항목 사전 30여 개 | 문서화됨 | ✅ |
| | 형식별 계산 순서 | 네 형식 모두 번호 매겨 복원 | ✅ |
| | 항목별 밑수 공식 | 화면 묶음 제목에서 확정 | ✅ |
| | 요율표 좌표 | **6개 항목 확정** (아래 4장), 나머지 진행 중 | ⚠→✅ |
| | `RATE_A`·`RATE_B` 표 | 이번에 발견, 의미 B등급 | ⚠ |
| **6 반올림** | 내부 함수·음수·절반값 | 은행가 반올림 확정 | ✅ |
| | 출력별 정책 | 확정 | ✅ |
| **7 출력** | `SET_REP.DBF` 75행 × 28열 | 평문 | ✅ |
## 3. 이번 검증에서 나온 문서 오류 셋
기존 문서가 실제와 어긋난 자리를 찾아 고침.
### ① `COSTN.DBF`는 암호화돼 있지 않음
02번이 「본문은 암호화되어 있으나 스키마는 평문」이라 적었으나 **사실이 아님.** 맛보기 71행, 봉화 248행을 그대로 읽음.
```text
B00001 흄관기초 D=800mm M TAMT1=23,441 MAMT1=2,719 LAMT1=20,722 SAMT1=0
```
**이 정정이 결론을 바꿈** — 단가의 네 성분이 프로젝트 안에 평문으로 있으므로 **골든셋을 단가 수준까지 뽑을 수 있음.** 암호화된 것은 `BDQTY``SYSINFO` 둘뿐임.
### ② `TAMT = MAMT + LAMT + SAMT` 는 예외가 있음
A등급 불변조건으로 적혀 있으나 실데이터에 **59행 중 2행이 어긋남.**
| CODE | 명칭 | `TAMT1` | `M+L+S` | 차이 |
|---|---|---:|---:|---:|
| `M00010` | 흄관 800mm | 65,880 | 41,608 | +24,272 |
| `M00011` | 흄관 900mm | 83,920 | 53,784 | +30,136 |
둘 다 관급으로 다루는 자재임. **하드 오류로 막으면 실데이터를 거부하게 됨** — 경고로 두고 사유를 함께 저장할 것.
### ③ 프로젝트 RATE 표는 넷이 아니라 여섯
맛보기 STC에 **`RATE_A`(25×10)·`RATE_B`(35×10)** 가 더 있음. 값이 `0.642 / 0.232 / 0.875` 같은 **0~1 계수**로, 백분율인 다른 표와 성격이 다름. `RATE_B`의 10열은 화면 `rGrd_RateB`의 숫자열 개수(`재/직노/노/재+직노/재+노` × 토목·건축)와 정확히 맞아 **수공형식 경비 밑수 배분표**로 보임.
**두 표는 맛보기에만 있고 실무 STC 7건에는 없음** — 수공형식 재현 시험의 전제 자료가 표본에 없다는 뜻임.
## 4. T1(요율 좌표)을 정적으로 깬 결과
**출력 엑셀 「공사원가계산서」의 비고 칸에 쓰인 밑수와 요율이 그대로 적혀 있음.**
```text
간접노무비 직접노무비 x 14.5%
산재보험료 (직접노무비+간접노무비) x 3.56%
노인장기요양보험료 건강보험료 x 12.95%
이윤 (노무비+경비+일반관리비) x 15%
```
이 값을 그 프로젝트의 `RATE_*.DBF` 안에서 되찾아 좌표를 역추적함. 실무 7건 전수 교차로 **후보가 하나뿐인 자리**만 취함.
| 항목 | 확정 좌표 | 근거 |
|---|---|---|
| 간접노무비 | `RATE_R[10, 1]` | 2건에서 유일 |
| 산재보험료 | `RATE_R[45, 1]` | 3건에서 유일 |
| 건강보험료 | `RATE_R[46, 1]` | 2건에서 유일 |
| 고용보험료 | `RATE_R[45, 17]` | 4건에서 유일 |
| 노인장기요양보험료 | `RATE_R[46, 11]` | 1건에서 유일 |
| 산업안전보건관리비 | `RATE_R[20, 17]` (율), `[20, 18]` (기초액) | 3건에서 유일 |
### 시계열 교차 검증 — A등급
기준파일 `JB_R`의 같은 자리를 연도별로 늘어놓으면 **실제 법정요율 변천과 정확히 일치함.**
| 좌표 | 2019 | 2022 | 2024 | 2025 | 2026 | 무엇인가 |
|---|---:|---:|---:|---:|---:|---|
| `[45, 1]` | 3.75 | 3.70 | 3.56 | 3.56 | 3.56 | 산재보험료율 |
| `[46, 1]` | 3.23 | 3.495 | 3.545 | 3.545 | 3.595 | 건강보험료율 |
| `[46, 6]` | 4.5 | 4.5 | 4.5 | 4.5 | 4.75 | 연금보험료율 |
| `[46, 11]` | 8.51 | 12.27 | 12.95 | 12.95 | 13.14 | 노인장기요양보험료율 |
`[45, 11..17]`은 **고용보험 등급 1~7**임. 값이 내림차순이고(2024: `1.57 / 1.30 / 1.13 / 1.06 / 1.03 / 1.02 / 1.01`) 개수가 화면 `rGrd_Rate_Kobo`의 7행과 맞음. 검증한 프로젝트는 모두 `열17`(7등급 이하)을 씀.
**`RATE_R`의 행 45~49는 요율 행렬이 아니라 부속표 영역**이고, 행 0~44가 규모·기간 행렬임.
### 아직 좌표가 안 갈리는 항목
`연금보험료(4.5%)`·`일반관리비(6%)`·`이윤(15%)`·`환경보전비(0.8%)`·`부가가치세(10%)`**같은 값이 표 안 여러 곳에 있어** 단독으로는 못 가름. 확정하려면 **한 프로젝트에서 그 값만 특이한 숫자로 바꿔 저장**하는 통제시험이 필요함(13번 T1).
## 5. 그래서 무엇을 만들 수 있나
### 지금 자료로 가능한 것
1. **STC 읽기·쓰기 어댑터** — 표 16종 평문, ZIP 무암호
2. **내역 계산기** — 수량 × 단가 → 행 금액 → 계층 집계. 반올림 정책 확정
3. **간접비 계산기** — 항목 사전·형식별 순서·밑수·반올림 확정, 보험 요율 좌표 6개 확정
4. **원설계 골든셋 회귀** — 실무 7건의 `BOQ11` + `COSTN` + `RATE_*` + 출력 XLSX를 짝지어 **원 단위 대조** 가능
5. **변경설계 비교** — 소광 원설계/통합변경/변경1차 세 벌 보유
### 지금 자료로 불가능한 것
1. **기초단가 배포 체계** — 자재 5.7만 건·노임·중기·표준시장단가를 못 읽음. 암호를 풀지 않는 한 영구 불가
2. **수량 할증 재현**`BDQTY` 암호화로 `X_QTY`·`X_HAL` 실값을 못 봄
3. **중기 시간당 단가 재현** — 손료 계수(감가상각률·정비율·관리비율) 실제 값이 자료에 없음
4. **자재 자동채택** — 6후보 중 무엇을 고르는지 규칙 없음
5. **수공형식 검증**`RATE_A`·`RATE_B`가 실무 표본에 없음
## 6. 판정
| 물음 | 답 |
|---|---|
| STmate와 **똑같은 프로그램**을 만들 수 있나 | ❌ — 기초단가 원천이 암호화됨 |
| STmate와 **같은 답을 내는 계산 엔진**을 만들 수 있나 | ⚠ 대부분 — 직접비·간접비·반올림은 가능, 중기·할증·자재채택은 불가 |
| STmate 프로젝트 파일을 **읽고 검산**할 수 있나 | ✅ — `BDQTY` 빼고 전부 |
| **Aislo 원가계산에 쓸 규칙**을 얻었나 | ✅ — 밑수·순서·반올림·요율 좌표 확보 |
**Aislo 목적으로는 필요한 정보가 모였음.** 「STmate 복제」는 애초에 불가능하고 목표도 아님.
## 7. 다음에 할 것
| 순위 | 할 일 | 왜 |
|---:|---|---|
| 1 | **골든셋 회귀 구현** — 실무 7건의 STC + XLSX로 내역·간접비를 원 단위 대조 | 지금 자료만으로 되고, 규칙 확정도를 한 번에 끌어올림 |
| 2 | 남은 요율 좌표(연금·일반관리비·이윤·환경보전비) 확정 | 통제시험 T1 나머지 |
| 3 | 중기 손료 계수를 **법령·품셈에서** 확보 | STmate 자료로는 영구 불가 — 국가 기준으로 대체 |
| 4 | `BDQTY` 대조시험 (T7) | 할증 규칙 |
3번은 STmate를 뜯어서 얻을 수 없음. `resources/knowledge/technical_info/` 의 **건설기계 경비 산정 기준**으로 가야 함.
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""기준자료를 실제로 읽을 수 있는지 판정. 복호화 시도는 하지 않음 — 읽히나만 봄."""
import pathlib, struct, sys, io, collections
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
ROOT = pathlib.Path(r"C:\경영정보(주)\System")
def probe(p):
try: b = p.read_bytes()
except Exception as e: return "열기실패", 0, 0
if b[:2] == b"PK": return "ZIP", 0, 0
if b[:1] not in (b"\x03", b"\x04", b"\x05", b"\x30", b"\xE5", b"\x06", b"\xF5", b"\x8B"):
return "DBF아님", 0, 0
try:
nrec, hlen, rlen = struct.unpack_from("<IHH", b, 4)
n = 0; o = 32
while o < len(b) and b[o] != 0x0D: n += 1; o += 32
if not (1 <= n <= 300 and hlen and rlen): return "머리깨짐", 0, 0
# 첫 레코드가 사람이 읽을 수 있는 문자인가
rec = b[hlen:hlen+rlen]
printable = sum(1 for c in rec if 32 <= c < 127 or c >= 0xA1)
ratio = printable / max(len(rec), 1)
return ("평문" if ratio > 0.85 else "본문암호화"), nrec, n
except Exception:
return "해석실패", 0, 0
groups = collections.defaultdict(lambda: collections.Counter())
detail = collections.defaultdict(list)
for p in sorted(ROOT.rglob("*")):
if not p.is_file(): continue
ext = p.suffix.upper() or "(없음)"
if ext not in (".DBF",".BSC",".STBSC",".CSTA",".JB_R",".JB_RA",".JB_1",".JB_M",".SMT",".MEM"): continue
st, nrec, nfld = probe(p)
groups[ext][st] += 1
detail[ext].append((p.name, st, nrec, nfld))
print("### 기준자료·프로젝트 테이블의 읽힘 상태 (복호화 시도 없음)")
print("%-8s %-5s %s" % ("확장자","개수","판정"))
for ext in sorted(groups):
tot = sum(groups[ext].values())
print("%-8s %-5d %s" % (ext, tot, dict(groups[ext])))
print("\n### 단가 원천으로 쓰이는 대형 기준파일")
for ext in (".BSC", ".CSTA", ".STBSC"):
for nm, st, nrec, nfld in detail.get(ext, [])[:8]:
print(" %-42s %-10s rec=%-7s fld=%s" % (nm[:42], st, nrec or "-", nfld or "-"))
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
import pathlib, zipfile, struct, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
def rd(zf, name):
b = zf.read(name)
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
rows=[]
for i in range(nrec):
r = b[hlen+i*rlen:hlen+(i+1)*rlen]; v=[]; c=1
for nm,ln in f: v.append(r[c:c+ln].decode("cp949","replace").strip()); c+=ln
rows.append(v)
return [x[0] for x in f], rows
Z = zipfile.ZipFile(pathlib.Path(r"C:\경영정보(주)\STmate 압축자료\실습(맛보기).STC"))
fn, rows = rd(Z, "COSTN.DBF")
ix = {n:i for i,n in enumerate(fn)}
def g(r,k):
try: return float(r[ix[k]] or 0)
except: return 0.0
print("### TAMT ≠ MAMT+LAMT+SAMT 인 행")
bad=0
for r in rows:
T,M,L,S = g(r,"TAMT1"), g(r,"MAMT1"), g(r,"LAMT1"), g(r,"SAMT1")
if not (T or M or L or S): continue
if abs(T-(M+L+S)) >= 0.005:
bad+=1
print(" %-8s %-14s %-12s T=%10.0f M+L+S=%10.0f 차=%+.0f" % (
r[ix["CODE"]], r[ix["NAME"]][:14], r[ix["SIZE"]][:12], T, M+L+S, T-(M+L+S)))
print(" 불일치 %d" % bad)
print("\n### 프로젝트 RATE 표 전수 (맛보기)")
import re
for nm in sorted(n for n in Z.namelist() if re.match(r"RATE", n, re.I)):
f2, r2 = rd(Z, nm)
nz = sum(1 for r in r2 if any((x and x not in ("0.0000","0.00","0")) for x in r))
print(" %-14s %2d× %2d열 값이 든 행 %d" % (nm, len(r2), len(f2), nz))
print("\n### RATE_A 와 RATE_M 이 같은 표인가")
fa, ra = rd(Z,"RATE_A.DBF"); fm, rm = rd(Z,"RATE_M.DBF")
same = sum(1 for a,b in zip(ra,rm) if a==b)
print("%d개 중 완전히 같은 행 %d개 → %s" % (len(ra), same, "같은 표 아님" if same<len(ra) else "같은 표"))
print(" RATE_A 첫행: %s" % " ".join(x or "·" for x in ra[0]))
print(" RATE_M 첫행: %s" % " ".join(x or "·" for x in rm[0]))
print("\n### 다른 프로젝트에도 RATE_A/RATE_B 가 있나")
for p in sorted(pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서").rglob("*.STC")):
try:
zz = zipfile.ZipFile(p)
rs = sorted(n for n in zz.namelist() if re.match(r"RATE", n, re.I))
print(" %-46s %s" % (p.name[:46], ", ".join(rs)))
except Exception as e: print(" %s 열기실패" % p.name)
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""STC 프로젝트 안의 표들이 실제로 읽히는지 본다. 복호화 시도 없음."""
import pathlib, zipfile, struct, sys, io, collections
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
roots = [pathlib.Path(r"C:\경영정보(주)"),
pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서")]
stcs = []
for r in roots:
if r.exists(): stcs += sorted(r.rglob("*.STC"))
print("찾은 STC %d" % len(stcs))
for s in stcs[:3]: print(" %s" % s)
def dbf_state(b):
if len(b) < 64: return "짧음", 0, 0
try:
nrec, hlen, rlen = struct.unpack_from("<IHH", b, 4)
n = 0; o = 32
while o < len(b) and b[o] != 0x0D: n += 1; o += 32
if not (1 <= n <= 300 and hlen and rlen): return "머리깨짐", 0, 0
if nrec == 0: return "빈표", 0, n
rec = b[hlen:hlen+rlen]
pr = sum(1 for c in rec if 32 <= c < 127 or c >= 0xA1) / max(len(rec),1)
return ("평문" if pr > 0.85 else "본문암호화"), nrec, n
except Exception: return "해석실패", 0, 0
if stcs:
for s in stcs[:2]:
print("\n### %s" % s.name)
try:
z = zipfile.ZipFile(s)
except Exception as e:
print(" ZIP 열기 실패: %s" % e); continue
enc = any(i.flag_bits & 0x1 for i in z.infolist())
print(" ZIP 암호: %s 파일 %d" % ("걸림" if enc else "없음", len(z.infolist())))
for i in z.infolist():
if not i.filename.upper().endswith((".DBF",)): continue
try: b = z.read(i)
except Exception as e: print(" %-16s 읽기실패" % i.filename); continue
st, nrec, nf = dbf_state(b)
print(" %-16s %-10s rec=%-6s fld=%s" % (i.filename, st, nrec, nf))
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import sys, io, pathlib, struct, collections
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
M=[]
for i in range(nrec):
r=b[hlen+i*rlen:hlen+(i+1)*rlen]; v=[];c=1
for nm,ln in f:
s=r[c:c+ln].decode("cp949","replace").strip();c+=ln
try: v.append(round(float(s),4) if s else 0.0)
except ValueError: v.append(0.0)
M.append(v)
return M
print("### JB_R 45·46행을 연도별로 — 부속표 영역의 열 의미 굳히기\n")
for name in ["2019(상_0107)_제비율.JB_R","2022(상_0103)_제비율.JB_R",
"2024(상_0101)_제비율.JB_R","2025(상_0101)_제비율.JB_R","2026(상_0101)_제비율.JB_R"]:
p = ROOT/name
if not p.exists(): continue
M = rd(p)
for r in (45, 46):
vals = [(c+1, M[r][c]) for c in range(len(M[r])) if M[r][c]]
print("%-26s%2d : %s" % (name[:26], r, " ".join("%d=%g"%v for v in vals)))
print()
print("### 해석")
print("""
행45 열1 = 산재보험료 (%) 출력 엑셀에서 확인
행45 열11~17 = 고용보험료 등급 1~7 (%) rGrd_Rate_Kobo 7(등급·요율) 것과 일치,
값이 내림차순이고 프로젝트는 열17(7등급이하)
행46 열1 = 건강보험료 (%)
행46 열6 = 연금보험료 (%)
행46 열11 = 노인장기요양보험료 (%) 건강보험료에 곱함
행20 열17·18 = 산업안전보건관리비 (%)·기초액()
""")
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""T1 확장 — 짝을 폴더 기준으로 넓게 잡고, 항목별 좌표 교집합을 낸다."""
import sys, io, pathlib, re, zipfile, struct, collections, warnings, difflib
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
warnings.filterwarnings("ignore")
import openpyxl
ROOT = pathlib.Path(r"D:\aislo-wt\sub\resources\knowledge\original\실무문서")
def dbf(zf, name):
b = zf.read(name); 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
M=[]
for i in range(nrec):
r=b[hlen+i*rlen:hlen+(i+1)*rlen]; v=[]; c=1
for nm,ln in f:
s=r[c:c+ln].decode("cp949","replace").strip(); c+=ln
try: v.append(round(float(s),4) if s else 0.0)
except ValueError: v.append(0.0)
M.append(v)
return M
RATE_RE = re.compile(r"([\d]+(?:\.[\d]+)?)\s*%")
def sheet_rates(xp):
wb = openpyxl.load_workbook(xp, data_only=True, read_only=True)
if "공사원가계산서" not in wb.sheetnames: return []
ws = wb["공사원가계산서"]; out=[]; seen=set()
for row in ws.iter_rows(min_row=1, max_row=40, max_col=8, values_only=True):
cells = ["" if v is None else str(v) for v in row]
label = re.sub(r"\s+","", next((c for c in cells[:3] if c.strip()), ""))
note = next((c for c in cells[4:7] if "%" in c), "")
if not note or not label: continue
for m in RATE_RE.finditer(note):
k=(label, m.group(1))
if k in seen: continue
seen.add(k); out.append((label, float(m.group(1))))
return out
# 폴더 안의 STC 와 xlsx 를 이름 비슷한 순으로 짝지음
pairs=[]
for stc in sorted(ROOT.rglob("*.STC")):
best=None; bs=0
for x in list(stc.parent.rglob("*.xlsx")) + list(stc.parent.parent.rglob("*.xlsx")):
if x.name.startswith("~$"): continue
s = difflib.SequenceMatcher(None, stc.stem, x.stem).ratio()
if s > bs: bs, best = s, x
if best and bs > 0.55: pairs.append((stc, best, bs))
print("%d" % len(pairs))
for s,x,r in pairs: print(" %.2f %s" % (r, s.stem[:52]))
coord = collections.defaultdict(collections.Counter) # 항목 -> 좌표 카운트
uniq = collections.defaultdict(collections.Counter) # 항목 -> 유일히 잡힌 좌표
for stc, xp, _ in pairs:
try: rates = sheet_rates(xp)
except Exception: continue
if not rates: continue
z = zipfile.ZipFile(stc)
tabs = {}
for n in z.namelist():
if re.match(r"RATE.*\.DBF$", n, re.I):
try: tabs[n.upper().replace(".DBF","")] = dbf(z,n)
except Exception: pass
for label, v in rates:
found=[]
for tn,M in tabs.items():
for r,row in enumerate(M):
for c,x in enumerate(row):
if x and abs(x-v) < 1e-6: found.append("%s[%d,%d]"%(tn,r,c+1))
for f in found: coord[label][f]+=1
if len(found)==1: uniq[label][found[0]]+=1
print("\n=== 유일하게 잡힌 좌표 (다른 후보 없이 딱 하나) ===")
for lab in sorted(uniq):
for k,n in uniq[lab].most_common():
print(" %-22s%-16s 프로젝트 %d건에서 유일" % (lab[:22], k, n))
print("\n=== 여러 후보 중 모든 프로젝트에 공통인 좌표 ===")
N = len(pairs)
for lab in sorted(coord):
all_hit = [k for k,n in coord[lab].items() if n == max(coord[lab].values()) and n >= 3]
if all_hit and lab not in uniq:
print(" %-22s%s (각 %d건)" % (lab[:22], ", ".join(all_hit[:5]), max(coord[lab].values())))
@@ -40,6 +40,7 @@
| 12 | 원가계산 4형식과 RATE표 복원 — **설정 83축** |
| 13 | 통제시험 목록 T1~T11 — 아직 못 가른 경계 |
| 14 | 최종 분석완료 보고서 |
| 15 | **재구현 가능성 검증** — 무엇을 만들 수 있고 무엇이 막혀 있나 |
## 확정된 굵직한 것
@@ -49,6 +50,8 @@
- 원가계산 **네 형식**(일반·수공·실적일반·실적수공)은 출력 모양이 아니라 **공식 집합이 통째로 다름**
- 기준자료 `JB_R` 35열 확장 시점 **2020-05-27**, 기존 1~20열은 한 칸도 안 바뀜
- 설정 **83축** 전수. 화면 순번과 내부값이 **뒤집힌 축 둘** 있음 — 그대로 저장하면 정반대 공식이 걸림
- 보험 요율의 **표 좌표 확정** — 산재 `RATE_R[45,1]`, 건강 `[46,1]`, 연금 `[46,6]`, 장기요양 `[46,11]`, 고용 등급1~7 `[45,11..17]`. 5개 연도 시계열이 실제 법정요율 변천과 맞음
- ⚠ **기초단가 원천은 전부 암호화** — 자재 5.7만 건·노임·중기·표준시장단가를 한 건도 못 읽음. 「계산 엔진」은 만들 수 있으나 「기준자료를 갖춘 완제품」은 못 만듦 (15번)
## 원자료 — `30_원자료/`