diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/00_진행현황.md b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/00_진행현황.md index 9b1c4d968..a19f90ac2 100644 --- a/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/00_진행현황.md +++ b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/00_진행현황.md @@ -123,3 +123,4 @@ REA 6.7.0 / Ghidra 12.1.4 / 기존 JDK 21.0.7. Ghidra는 LOCALAPPDATA/STMATE-REA - 2026-11-14 36단계: [계산 엔진 잔여 규칙](36_계산엔진_잔여_작업자료_20261114/36_계산엔진_잔여규칙.md) — A 코드 = COSTA · S_JDA 참 = 저장 단가 · ACTTYP·ACTOPT = PRJLST(0x45c938) · EXRNM 참 = S 줄 MROUNDUD(×율, ECD, 0) · 거짓 = 상위 B·천원 줄 수량 칸 = 율 → 두 경로 ECD 버림만큼 다름 · 수량 계산기 = MROUNDUD 선택 1 · 적용기준 Values 2·0·1(행자부 = 1) · 행자부 RATE_M 줄 b+11/b+15·23·24. Ghidra 읽기만, 원본 실행 없음. - 2026-11-14 38단계: [보고서 잔여 서식 정적 분석](38_보고서_잔여_작업자료_20261114/38_보고서_잔여_정적분석.md) — 엑셀판 = ST_P_XLS · 설계내역서 쪽 채움(LNP_CNT · 0x520e34 · 원본 3건 행 번호 일치) · 인쇄 영역 $A:$열·탭 색 · 0605 금회 = BOQ QTY2·T_AMT2 · 0615 견적·기준 = xAMT1·xAMT2 두 행 · 0695 회사분 0·도급분 DTP 참조 · 0651 시기금·구기금 = 고른 1·2 차수 T_AMT · 토개공·수자원 글 없음. 바이트 읽기만, 원본 실행 없음. - 2026-11-14 37단계: [파일 입출력 잔여 정적 분석](37_파일입출력_잔여_작업자료_20261114/37_파일입출력_잔여_정적분석.md) — JDL TEXT = 탭 구분(검사기 고정폭 표는 죽은 고리 · 34 §17.9 바로잡음) · 단가 목록 7종·Main·Global.Ini 줄 규칙 · 세부 3종 열 이름 · 구버전 판 가리기·필드 대응(B→X 조건 = 단위 X) · 엑셀 역등록 구성 줄 PRPT·PRIN 뜻 · wM_JajeXLS = 재료비 수량집계표 내보내기만 · 그림 = COSTN I 코드 IFORM · SIZE = 그림 클래스 이름 +- 2026-11-14 38b F5-31: [원가 줄 엑셀 수식](38_보고서_잔여_작업자료_20261114/38b_F5-31_원가줄수식.md) — 총괄 원가 줄 D·E 식 · H 숨은 메타(ST_P_XLS 0x4b8c6c) · I~P 보정·요율 칸 · 이윤 끝전 J·K·L 사슬 · 공사원가계산서 참조·비고·바로가기 · 산재·기타경비 겉 괄호 = 25 판 틀(0x494b53) · 원본 5건 1392칸 글·값 일치. Ghidra 읽기만, 원본 실행 없음. diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/38b_F5-31_원가줄수식.md b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/38b_F5-31_원가줄수식.md new file mode 100644 index 000000000..e04ab2d7e --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/38b_F5-31_원가줄수식.md @@ -0,0 +1,92 @@ +# 38b F5-31 원가 줄 엑셀 수식 — 총괄설계내역서 원가 줄 · 공사원가계산서 + +- 분석일: 2026-11-14 +- 방법: 원본 STMATE 출력 엑셀 5건 수식 글(openpyxl 수식 읽기)·셈 값(data_only) 실측 · REA MCP(Ghidra 12.1.4) ST_P_XLS 의사코드 +- 근거 파일: `evidence/ST_P_XLS_cost_meta_0x4b8c6c.c`(숨은 메타) · `evidence/ST_P_XLS_cost_tpl_0x494b53_excerpt.c`(식 틀) +- 원본 엑셀: 봉화 2024(24.04) · 영월 2024(24.02) · 대흥 2024(24.04) · 울진 착공(25.1) · 영덕 변경 2차수(25.05) — 괄호 = STC PRG_V +- 앞 단계: [38 §8 F5-31](38_보고서_잔여_정적분석.md) · [35 §3 식 틀 문자열](../35_보고서양식_수량산출서_작업자료_20261114/35_보고서양식_수량산출서_정적분석.md) + +확신 표기: **높음** = 원본 엑셀 5건 글 그대로 + Ghidra 틀 확인 · **중간** = 원본 엑셀 일치, 코드 주소 미해독 · **미확인** = 표본·코드 없음. + +## 1. 총괄설계내역서 원가 줄 칸 — 중간~높음 + +g = 가. 순공사비계 행(차수마다) · r = 그 줄 · J = `-I_KAN_JEOLSA`(0 이면 `0`) + +| 줄 | D | E | 숨은 칸 | +|---|---|---|---| +| 간접노무비 | `=ROUNDDOWN(E{g}*M{r}/100,J)` | `="직접노무비 x "&ROUND(M{r},5)&"%"` | M = 율 | +| 산재·고용 | 24 판 `=ROUNDDOWN((E{g}+D{간노})*M/100,J)` · 25 판 `((E{g}+D{간노}))` | 같은 꼴 | M | +| 건강·연금 | `=ROUNDDOWN(E{g}*M/100,J)` | 같은 꼴 | J = `=E{g}`(고정 줄도) | +| 노인장기요양 | `=ROUNDDOWN(D{건강}*M/100,J)` | 같은 꼴 | J = `=D{건강}` | +| 안전관리비(A 관급재/1.1) | `=ROUNDDOWN(((E{g}+(F{g}))+(I{관급}-O{관급})/1.1)*M/100+기초액,0)` | 계산 글(수식 아님) | J = `=` 밑 식 · N = 기초액(C_ANJUN_G) | +| 안전관리비(변경 · 자동 2) | `=D{당초}+ROUNDDOWN(D{당초}*((J{r}-J{당초})/J{당초}),J)` | `당초금액 + 안전관리비 증감액` | J = 밑 식(관급 없으면 `((E{g}+(F{g})))`) | +| 기타경비 | 24 판 `=ROUNDDOWN((((E{g}+D{간노})+(F{g})))*M/100,J)` · 25 판 괄호 하나 더 | 같은 꼴 | M | +| 환경보전·건설기계 | `=ROUNDDOWN((D{g})*M/100,J)` | 같은 꼴 | M | +| 나. 소계 | `=(D{g}+D…)` 가.~나. 사이 줄 | — | 0 | +| 일반관리비 | `=ROUNDDOWN(((D{나})+0)*M/100,0)` | `="(나.소 계) x "&ROUND(M,5)&"%"` | M | +| 다. 소계 | `=(D{나}+D{일관})` | — | I = I_TOT_JEOL · J·L = `=D{r}` | +| 이윤 | `=ROUNDDOWN(((D{다})-(F{g}))*M/100,0)-I{r}-K{r}` | `="(다.소계 - 재료비) x "&ROUND(M,5)&"% = "&TEXT(J{r},"#,##0")` | 아래 §2 | +| 추가금액(이윤 뒤·부가세 뒤) | 값 | — | J·L = `=D{r}` | +| 라. 공급가액 | `=(D{다}+D{이윤}+D{추가2…})` | — | J·L 같은 꼴 | +| 부가세 I_BUGA 0 | `=ROUNDDOWN((D{라})*M/100,0)+I{r}` | `="공급가액 x "&ROUND(M,5)&"%"` | J·L = J·L 열 같은 꼴 · I = B_BUGA_UP · O = I_BUGA | +| 부가세 I_BUGA 4(면세품) | `=ROUNDDOWN((D{g}-E{g}-면세액)*M/100,0)+I{r}` | `="(재료비 - 면세품목:₩… + 산출경비) x "&ROUND(M,5)&"%"` | J·L = D 와 같은 글 · N = C_BUGA_M · O = 4 | +| 마. 도급공사비 | `=(D{라}+D{부가세}+D{추가3…})` | — | J·L · O = I_TOT_JEOL | +| 관급자재대 | `=ROUNDUP(I{r},-I_KWN_JEOLSA)` | `="원자재대: "&TEXT(I,"#,0")&" (조달수수료: "&TEXT(O,"#,0")&" 포함)"` | I = 원자재대 · M = I_KWN_JEOLSA · O = 조달수수료 | +| 바. 총공사비 | `=(D{마}+D{관급}+D{추가4…})` | — | J·L · O = I_TOT_JEOL | + +- 원가 줄 숨은 칸 I·M·N·O·P = 위 값 · 그 밖 0(가. 줄은 없음) +- D_ 고정 항목 = D 값 · E 없음 · J 밑 식은 남김(울진 건강·연금·안전 · 영덕 당초) +- 변경 양식(영덕) = 줄마다 당초·변경 두 줄 · 각 줄은 자기 차수 g·행 참조 · 나. 소계는 두 줄 건너 합(`D42+D44+D46…`) + +## 2. 이윤 끝전 사슬 — 높음(5건 값 일치) + +- J 열 = 보정 없는 사슬 · L 열 = 보정액 I 만 뺀 사슬 · D 열 = I·K 둘 다 뺌 · 끝 = 바. (없으면 마.) +- I_TOT_JEOL > 0: I = `=ROUND((J{끝}-ROUNDDOWN(J{끝},-I{다}))/1.1,0)`(I_BUGA 0) · `=J{끝}-ROUNDDOWN(J{끝},-I{다})`(그 밖 — 영덕 4) · K = `=ABS(L{끝}-ROUNDDOWN(J{끝},-I{다}))*IF(L{끝}>ROUNDDOWN(J{끝},-I{다}),1,-1)` +- I_TOT_JEOL = 0(대흥): I = C_IYUN_M 값 · K 칸 없음(D 식의 `-K` 는 남음) +- kanjub_calc 의 `m0 = I_BUGA 0 ? 반올림(diff/1.1) : diff` 와 같은 갈래 + +## 3. 숨은 메타(H) — ST_P_XLS 0x4b8c6c · 높음(글 5건 일치) + +- 글 = `0x07` + `` `키|값 `` + `0x05` … + `` ` `` (엑셀 칸 글 `_x0007_` · `_x0005_`) +- 첫 차수 줄: `DTP|D_TYP` · `QTY{n}|{n}` · `BDC|BDCODE1` · `SRE|S_REC` / 둘째 차수 줄: `DTP` · `QTY{n}|{n}` 만 +- 덧붙임 차례: `DRT|T`(고정) → `CBO|값`(필드 1·2) → 안전(D_TYP 306) RAT5 `%10≠0` 이면 `ANB|1` · `÷10` 1~5 면 `ANK|÷10` → (312·313 `ANPUM|1`) → BDCODE1 `A1`~`A4` 면 `ADN|이름` `ADB|비고` (+ BUGA·ADDT 갈래) +- D_TYP: 간노 201 · 산재 301 · 고용 302 · 건강 303 · 연금 304 · 퇴직 305 · 안전 306 · 장기요양 310 · 환경 309 · 건설기계 311 · 기타 321 · 나 400 · 일관 401 · 이윤 402 · 다 410 · 라 500 · 부가세 502 · 마 600 · 관급 653 · 바 700 · 추가금액 461(이윤 뒤 1) · 561(부가세 뒤 1) +- S_REC 실측: 간노 LA · 간접비 SA · 일관·이윤·부가세 TA · 소계 TA-F · 관급 TA-3 · 추가금액 빈칸 +- 재현: RAT5 = I_ANJUN_K×10 + I_ANJUN_B(봉화 ANK|1 · 울진 ANB|1) · CBO = 변경 차수 건강·연금·장기요양 I_BF_CH_BO · 안전 I_ANJUNAUTO(영덕 2) + +## 4. 식 틀 출처 — ST_P_XLS 0x494b53 둘레 · 높음 + +- 산재·고용 = `ROUNDDOWN( ([L_AMT][+LAB]) * [RAT_1], ` + `-I_KAN_JEOLSA` + `)` · 기타경비 = `ROUNDDOWN( ((` + `([L_AMT]+[M_AMT])` + `)[+LAB]) * [RAT_1], ` … +- [L_AMT] = `(E{g}+D{간노})` · [M_AMT] = `(F{g})` 로 바꾸면 25 판 출력(울진·영덕)과 글 그대로 → 지금 설치본 = 25 판 꼴 +- 24 판 출력(봉화·영월·대흥)은 겉 괄호 하나 적음 — 재현 앱은 STC PRG_V < 25 면 24 판 꼴(govLabel 25 판 갈래와 같은 기준) +- 안전 변경(PCLASS 7 · 계열 ≥ 2 · 자동 2 · 당초 밑 ≥ 1) = `[C_AMT_B]+ROUNDDOWN( [C_AMT_B] * (([C_TAMT_C]-[C_TAMT_B])/[C_TAMT_B]), ` — 영덕 D57 과 같음 + +## 5. 공사원가계산서 — 중간(5건 글·값 일치) + +| 칸 | 꼴 | +|---|---| +| 직접재료비 D | `=총괄!{재료 열}{g}-D{간재}+D{작업설}` | +| 직접노무비·산출경비 D | `=총괄!{노무·경비 열}{g}` | +| 항목 줄 D | `=총괄!D{그 줄}` · 총원가 = 라. · 도급 = 마. · 총공사비 = 바.(없으면 `=D{도급}`) | +| 소계 | `=+D…+D…-D…`(재료 = 직재+간재−작업설 · 노무 = 직노+간노 · 경비 = 산출경비~앞 줄) | +| 구성비 E | `=D{r}/D{끝}*100` · 끝 줄 100 값 | +| 비고 F | 설명 있는 총괄 줄 = `=총괄!E{줄}` · 일관·이윤·부가세 = `="<원가계산서 설명> x "&ROUND(총괄!M{줄},5)&"%"` | +| 바로가기 H | `=HYPERLINK("#"&총괄!H2&"!{열}"&ROW(총괄!{열}{줄}),"총괄표 →")` · 소계·0 줄 없음 | +| 변경 양식 | D 당초 · E 변경 참조 · F `=E-D` · G `=E/E{끝}*100` · H 비고 = 변경 줄 · J 바로가기 = 당초 줄 | + +## 6. 재현 앱 대조 + +- report_cost_fmla.js — 총괄 원가 줄 · 공사원가계산서 수식 · report_boq 줄 열쇠(_c) · 차수 재료(_kan) +- 원본 5건 수식·수 칸 1392칸 = 1392칸 글 그대로 · 수식 다시 셈 값 = 원본 셈 값 · 총괄 D = 계산 엔진 금액 +- 시험: `resources/tester/test_stmate_report_cost_fmla.py` + +## 7. 미확인 — 값 그대로 둠 + +| 갈래 | 까닭 | +|---|---| +| 안전관리비 B 상한 · 관급 없는 계산 · 기초액 0 | 원본 표본 없음(D 값) | +| 산재·고용·기타경비 [+LAB](C_EIS_HOLLAB ≠ 0) · 기타경비 틀 IIF 덧셈 | 표본 없음 | +| 퇴직공제·상시근로자 밑·수공(S_) 항목 | 표본 없음 | +| 부가세 I_BUGA 1·2·3 · 관급 I_KWN_UPDN 1·2(ROUND·ROUNDDOWN 으로 둠) | 표본 없음 | +| 추가금액 1·4 단계 DTP(361·661 으로 둠)·J·L 사슬 · BUGA·ADDT 메타 | 표본 없음 | +| 공사원가 엑셀판 본 함수(0x504e60) | Ghidra 응답 크기 초과로 디컴파일 실패 | diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_meta_0x4b8c6c.c b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_meta_0x4b8c6c.c new file mode 100644 index 000000000..d3eaf5cd4 --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_meta_0x4b8c6c.c @@ -0,0 +1,575 @@ + +void FUN_004b8c6c(undefined4 param_1,int param_2,undefined4 param_3,undefined4 param_4) + +{ + undefined *puVar1; + undefined1 *puVar2; + undefined4 *puVar3; + undefined **ppuVar4; + char cVar5; + short sVar6; + undefined4 uVar7; + int iVar8; + int iVar9; + undefined4 *in_FS_OFFSET; + undefined4 local_1c8; + undefined4 local_1c4; + undefined4 *local_1c0; + undefined4 local_1bc; + undefined4 local_1b8; + undefined4 local_1b4; + undefined4 local_1b0; + undefined4 *local_1ac; + undefined4 local_1a8 [4]; + undefined4 local_198; + undefined1 local_194 [16]; + undefined4 local_184; + undefined4 local_180; + undefined4 local_17c; + undefined4 local_178; + undefined4 local_174; + undefined4 local_170; + undefined4 local_16c; + undefined4 local_168; + undefined4 local_164; + undefined *local_160; + undefined4 *local_15c; + undefined4 local_158; + undefined1 local_154 [16]; + undefined4 local_144; + undefined4 local_140; + undefined4 local_13c; + undefined4 *local_138; + undefined4 local_134; + char *local_130; + char *local_12c; + undefined4 *local_128; + char *local_124; + undefined4 local_120 [4]; + undefined4 local_110; + undefined4 local_10c; + undefined4 *local_108; + undefined4 local_104; + undefined4 local_100; + undefined1 *local_fc; + undefined4 local_f8; + undefined4 local_f4; + undefined4 uStack_f0; + undefined4 uStack_ec; + undefined4 uStack_e8; + undefined4 local_e4; + undefined *local_e0; + undefined4 *local_dc; + undefined4 local_d8; + undefined4 *local_d4; + uint local_d0; + undefined4 local_cc; + undefined4 local_c8; + undefined **local_c4; + undefined4 uStack_c0; + char *pcStack_bc; + char *pcStack_b8; + undefined4 *local_b4; + char *local_b0; + char *local_ac; + undefined4 *local_a8; + char *local_a4; + char *local_a0; + undefined4 *local_9c; + char *local_98; + char *local_94; + undefined *local_90; + char *pcStack_8c; + char *pcStack_88; + undefined *puStack_84; + undefined4 local_80; + char *local_7c; + char *local_78; + char *local_74; + undefined *local_70; + undefined4 local_6c; + undefined *puStack_68; + undefined *puStack_64; + char *pcStack_60; + char *local_5c; + undefined *local_58; + undefined *local_54; + undefined4 *local_50; + undefined4 local_4c; + undefined4 local_48; + undefined4 local_44; + undefined4 local_40; + undefined4 local_3c; + undefined4 local_38; + undefined4 local_34; + undefined4 local_30; + undefined4 local_2c; + undefined4 uStack_28; + undefined1 *puStack_24; + undefined1 *puStack_20; + char *local_10; + undefined *local_c; + int local_8; + + iVar9 = 0x38; + do { + local_c = (undefined *)0x0; + iVar9 = iVar9 + -1; + } while (iVar9 != 0); + LOCK(); + UNLOCK(); + local_8 = 0; + puStack_20 = (undefined1 *)0x4b8c91; + _System__LStrAddRef_qqrpv(0); + puVar1 = PTR_DAT_006120a4; + puStack_24 = &LAB_004b9743; + uStack_28 = *in_FS_OFFSET; + *in_FS_OFFSET = &uStack_28; + iVar9 = 0; + if (((*(short *)(*(int *)puVar1 + 0x1750) == 7) && ('\x01' < *(char *)(*(int *)puVar1 + 0x1788))) + && ((int)*(char *)(*(int *)puVar1 + 0x1788) != *(short *)(*(int *)puVar1 + 0x1754) + 1)) { + iVar9 = (*(short *)(*(int *)puVar1 + 0x1754) + 1) - (int)*(char *)(*(int *)puVar1 + 0x1788); + } + local_2c = 0x4b8cfa; + puStack_20 = &stack0xfffffffc; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_3c,0); + local_2c = local_30; + local_30 = local_34; + local_34 = local_38; + local_38 = local_3c; + local_50 = (undefined4 *)CONCAT22(local_50._2_2_,0x2200); + local_50 = (undefined4 *)CONCAT31(local_50._1_3_,1); + local_3c = 0x4b8d1a; + uVar7 = _System__VarFromPStr_qqrr14System_Variantrx28System__SmallString_iuc_255_ + (&local_4c,&local_50); + local_3c = local_40; + local_40 = local_44; + local_44 = local_48; + local_48 = local_4c; + local_4c = CONCAT31((int3)((uint)uVar7 >> 8),local_8 != 0); + local_50 = &local_2c; + local_54 = (undefined *)0x4b8d37; + IIF(); + local_54 = (undefined *)0x4b8d42; + _System__VarToLStr_qqrr17System_AnsiStringrx14System_Variant(&local_c,&local_2c); + local_54 = (undefined *)0x4b8d51; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,"BDCODE1",&local_10); + local_54 = (undefined *)0x4b8d5d; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 == 0x28d) { + local_54 = (undefined *)0x4b8d89; + _System__LStrCat3_qqrv + (&local_58,&DAT_004b9780, + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_54 = (undefined *)0x4b8d96; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,local_58,&local_54); + local_58 = &DAT_004b9790; + local_5c = (char *)0x4b8da4; + cVar5 = MPOS(); + if (cVar5 != '\0') { + iVar8 = 0x28b; + } + } + if (param_2 == 1) { + local_54 = local_c; + local_58 = &DAT_004b97a4; + local_5c = "`DTP|"; + pcStack_60 = (char *)0x4b8e3b; + _Sysutils_IntToStr_qqri(iVar8,&local_74); + pcStack_60 = local_74; + puStack_64 = &DAT_004b97c0; + puStack_68 = &DAT_004b97cc; + local_6c = *(undefined4 *)(PTR_PTR_006120e4 + 4); + local_70 = &DAT_004b97dc; + local_74 = (char *)(*(char *)(*(int *)puVar1 + 0x1789) - iVar9); + local_78 = (char *)0x1; + local_7c = (char *)CONCAT31((int3)((uint)*(int *)puVar1 >> 8), + *(char *)(*(int *)puVar1 + 0x1788) == '\x01'); + local_80 = 0x4b8e77; + iVar8 = IFI(); + local_80 = *(undefined4 *)(PTR_PTR_006120e4 + iVar8 * 4); + puStack_84 = &DAT_004b97c0; + pcStack_88 = "`BDC|"; + pcStack_8c = (char *)0x4b8e95; + _Sysutils_TrimRight_qqrx17System_AnsiString(local_10,&local_78); + pcStack_8c = local_78; + local_90 = &DAT_004b97c0; + local_94 = "`SRE|"; + local_98 = (char *)0x4b8eb1; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,"S_REC",&local_80); + local_98 = (char *)0x4b8ebc; + _Sysutils_TrimRight_qqrx17System_AnsiString(local_80,&local_7c); + local_98 = local_7c; + local_9c = (undefined4 *)0x4b8ecc; + _System__LStrCatN_qqrv(&local_70,0xf); + local_9c = (undefined4 *)0x4b8ed7; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_6c,local_70); + local_9c = &local_6c; + local_a0 = (char *)0x4b8ee8; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_a0,0); + local_a0 = local_94; + local_a4 = local_98; + local_a8 = local_9c; + local_ac = local_94; + local_b0 = (char *)0x4b8f10; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_b0,&DAT_004b9818); + local_b0 = local_a4; + local_b4 = local_a8; + pcStack_b8 = local_ac; + pcStack_bc = local_a4; + uStack_c0 = 0x4b8f4b; + _System__LStrCat3_qqrv + (&local_b4,"ITM_TYP_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1789) * 4)); + uStack_c0 = 0x4b8f58; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_b4); + uStack_c0 = CONCAT31((int3)((uint)iVar8 >> 8),0 < iVar8); + local_c4 = &local_90; + local_c8 = 0x4b8f6a; + IIF(); + ppuVar4 = local_c4; + local_c4 = (undefined **)0x4b8f76; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(ppuVar4,&local_90); + local_c4 = (undefined **)&local_6c; + local_c8 = 0x4b8f87; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_c4,0); + ppuVar4 = local_c4; + local_c4 = (undefined **)0x4b8f93; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(ppuVar4,&local_c4); + local_c4 = (undefined **)0x4b8f9e; + _System__VarToLStr_qqrr17System_AnsiStringrx14System_Variant(&local_c,&local_6c); + } + else { + local_54 = local_c; + local_58 = &DAT_004b97a4; + local_5c = "`DTP|"; + pcStack_60 = (char *)0x4b8dcc; + _Sysutils_IntToStr_qqri(iVar8,&local_5c); + pcStack_60 = local_5c; + puStack_64 = &DAT_004b97c0; + puStack_68 = &DAT_004b97cc; + local_6c = *(undefined4 *)(PTR_PTR_006120e4 + param_2 * 4); + local_70 = &DAT_004b97dc; + local_74 = (char *)(*(char *)(*(int *)puVar1 + 0x1788 + param_2) - iVar9); + local_78 = (char *)0x1; + local_7c = (char *)CONCAT31((int3)((uint)*(int *)puVar1 >> 8), + *(char *)(*(int *)puVar1 + 0x1788) == '\x01'); + local_80 = 0x4b8e08; + iVar8 = IFI(); + local_80 = *(undefined4 *)(PTR_PTR_006120e4 + iVar8 * 4); + puStack_84 = (undefined *)0x4b8e1e; + _System__LStrCatN_qqrv(&local_c,9); + } + if (*(short *)(*(int *)puVar1 + 0x1750) == 7) { + puStack_84 = (undefined *)0x4b8fba; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 != 0x12f) { + puStack_84 = (undefined *)0x4b8fcd; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 != 0x130) { + puStack_84 = (undefined *)0x4b8fe0; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 != 0x131) { + puStack_84 = (undefined *)0x4b8ff3; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 != 0x136) { + puStack_84 = (undefined *)0x4b9006; + iVar8 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar8 != 0x132) goto LAB_004b904f; + } + } + } + } + if ((*(char *)(*(int *)puVar1 + 0x1788) != '\x01') && + ((iVar9 < 1 || + ((short)*(char *)(*(int *)puVar1 + 0x1788 + param_2) != *(short *)(*(int *)puVar1 + 0x1754)) + ))) goto LAB_004b904f; + puStack_84 = local_c; + pcStack_88 = "\x05"; + pcStack_8c = "`DRT|T"; + local_90 = (undefined *)0x4b904d; + _System__LStrCatN_qqrv(&local_c,3); + } + else { +LAB_004b904f: + puStack_84 = (undefined *)0x4b9072; + _System__LStrCat3_qqrv + (&local_c8,&DAT_004b984c, + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + puStack_84 = (undefined *)0x4b907f; + sVar6 = _Apodset_TApolloTable_GetLogical_qqr17System_AnsiString(param_1,local_c8); + if (sVar6 != 0) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`DRT|T"; + local_9c = (undefined4 *)0x4b909e; + _System__LStrCatN_qqrv(&local_c,3); + } + } + local_90 = (undefined *)0x4b90c1; + _System__LStrCat3_qqrv + (&local_cc,&DAT_004b985c, + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)); + local_90 = (undefined *)0x4b90ce; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_cc); + if (iVar9 - 1U < 2) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`CBO|"; + local_9c = (undefined4 *)0x4b9104; + _System__LStrCat3_qqrv + (&local_d0,&DAT_004b985c, + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_9c = (undefined4 *)0x4b9111; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_d0); + local_9c = *(undefined4 **)(PTR_PTR_006120e4 + iVar9 * 4); + local_a0 = (char *)0x4b9127; + _System__LStrCatN_qqrv(&local_c,4); + } + local_90 = (undefined *)0x4b9133; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar9 == 0x132) { + local_90 = (undefined *)0x4b9161; + _System__LStrCat3_qqrv + (&local_d4,"RAT5_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_90 = (undefined *)0x4b916e; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_d4); + if (iVar9 != 0) { + local_90 = (undefined *)0x4b9199; + _System__LStrCat3_qqrv + (&local_d8,"RAT5_", + *(undefined4 *) + (PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)); + local_90 = (undefined *)0x4b91a6; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_d8); + if (iVar9 % 10 != 0) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`ANB|1"; + local_9c = (undefined4 *)0x4b91d2; + _System__LStrCatN_qqrv(&local_c,3); + } + if (iVar9 / 10 - 1U < 5) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`ANK|"; + local_9c = (undefined4 *)0x4b9206; + _Sysutils_IntToStr_qqri(iVar9 / 10,&local_dc); + local_9c = local_dc; + local_a0 = (char *)0x4b9219; + _System__LStrCatN_qqrv(&local_c,4); + } + } + } + local_90 = (undefined *)0x4b9225; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar9 != 0x138) { + local_90 = (undefined *)0x4b9238; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,"D_TYP"); + if (iVar9 != 0x139) goto LAB_004b92a4; + } + local_90 = (undefined *)0x4b9262; + _System__LStrCat3_qqrv + (&local_e4,&DAT_004b9780, + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)); + local_90 = (undefined *)0x4b9275; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,local_e4,&local_e0); + local_90 = local_e0; + local_94 = &DAT_004b98ac; + local_98 = (char *)0x4b9286; + cVar5 = MPOS(); + if (cVar5 != '\0') { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`ANPUM|1"; + local_9c = (undefined4 *)0x4b92a4; + _System__LStrCatN_qqrv(&local_c,3); + } +LAB_004b92a4: + if ((*local_10 == 'A') && ((byte)(local_10[1] - 0x31U) < 4)) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`ADN|"; + local_9c = (undefined4 *)0x4b92f0; + _System__LStrCat3_qqrv + (&local_104,"NAME_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_9c = (undefined4 *)0x4b9303; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,local_104,&local_100); + local_9c = (undefined4 *)0x4b9314; + _Sysutils_TrimRight_qqrx17System_AnsiString(local_100,&local_fc); + local_9c = (undefined4 *)local_fc; + local_a0 = "\x05"; + local_a4 = "`ADB|"; + local_a8 = (undefined4 *)0x4b9347; + _System__LStrCat3_qqrv + (&local_110,"BIGO_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_a8 = (undefined4 *)0x4b935a; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,local_110,&local_10c); + local_a8 = (undefined4 *)0x4b936b; + _Sysutils_TrimRight_qqrx17System_AnsiString(local_10c,&local_108); + local_a8 = local_108; + local_ac = (char *)0x4b9381; + _System__LStrCatN_qqrv(&local_f8,7); + local_ac = (char *)0x4b9392; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_f4,local_f8); + local_ac = (char *)&local_f4; + local_b0 = (char *)0x4b93a9; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_130,&DAT_004b9910); + local_b0 = local_124; + local_b4 = local_128; + pcStack_b8 = local_12c; + pcStack_bc = local_130; + uStack_c0 = 0x4b93ce; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_140,0); + uStack_c0 = local_134; + local_c4 = (undefined **)local_138; + local_c8 = local_13c; + local_cc = local_140; + local_d0 = 0x4b9409; + _System__LStrCat3_qqrv + (&local_144,"BUGA_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_d0 = 0x4b9416; + sVar6 = _Apodset_TApolloTable_GetLogical_qqr17System_AnsiString(param_1,local_144); + local_d0 = (uint)(sVar6 == 0); + local_d4 = local_120; + local_d8 = 0x4b942b; + IIF(); + puVar3 = local_d4; + local_d4 = (undefined4 *)0x4b9437; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(puVar3,local_120); + local_d4 = &local_f4; + local_d8 = 0x4b9461; + _System__LStrCat3_qqrv + (&local_170,"ADDT_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_d8 = 0x4b9474; + _Apodset_TApolloTable_GetString_qqr17System_AnsiString(param_1,local_170,&local_16c); + local_d8 = 0x4b948a; + _System__LStrCat3_qqrv(&local_168,&DAT_004b9944,local_16c); + local_d8 = 0x4b949b; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_164,local_168); + local_d8 = local_158; + local_dc = local_15c; + local_e0 = local_160; + local_e4 = local_164; + uStack_e8 = 0x4b94c0; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_180,0); + uStack_e8 = local_174; + uStack_ec = local_178; + uStack_f0 = local_17c; + local_f4 = local_180; + local_f8 = 0x4b94fb; + _System__LStrCat3_qqrv + (&local_184,"ADDT_", + *(undefined4 *)(PTR_PTR_006120e4 + *(char *)(*(int *)puVar1 + 0x1788 + param_2) * 4)) + ; + local_f8 = 0x4b9508; + iVar9 = _Apodset_TApolloTable_GetInteger_qqr17System_AnsiString(param_1,local_184); + local_f8 = CONCAT31((int3)((uint)iVar9 >> 8),iVar9 == 0); + local_fc = local_154; + local_100 = 0x4b951a; + IIF(); + puVar2 = local_fc; + local_fc = (undefined1 *)0x4b9526; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(puVar2,local_154); + local_fc = (undefined1 *)0x4b9534; + _System__VarToLStr_qqrr17System_AnsiStringrx14System_Variant(&local_c,&local_f4); + } + if (local_8 != 0) { + local_90 = local_c; + local_94 = "\x05"; + local_98 = "\"&"; + local_9c = (undefined4 *)local_8; + local_a0 = "&\""; + local_a4 = (char *)0x4b955c; + _System__LStrCatN_qqrv(&local_c,5); + } + local_90 = local_c; + local_94 = "\x05"; + local_98 = "`"; + local_9c = (undefined4 *)0x4b9579; + _System__LStrCatN_qqrv(&local_198,3); + local_9c = (undefined4 *)0x4b958a; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(local_194,local_198); + local_9c = (undefined4 *)local_194; + local_a0 = (char *)0x4b959e; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_1b8,0); + local_a0 = (char *)local_1ac; + local_a4 = (char *)local_1b0; + local_a8 = (undefined4 *)local_1b4; + local_ac = (char *)local_1b8; + local_50 = (undefined4 *)CONCAT22(local_50._2_2_,0x2200); + local_50 = (undefined4 *)CONCAT31(local_50._1_3_,1); + local_b0 = (char *)0x4b95cd; + uVar7 = _System__VarFromPStr_qqrr14System_Variantrx28System__SmallString_iuc_255_ + (&local_1c8,&local_50); + local_b0 = local_1bc; + local_b4 = local_1c0; + pcStack_b8 = (char *)local_1c4; + pcStack_bc = (char *)local_1c8; + uStack_c0 = CONCAT31((int3)((uint)uVar7 >> 8),local_8 != 0); + local_c4 = (undefined **)local_1a8; + local_c8 = 0x4b95f9; + IIF(); + ppuVar4 = local_c4; + local_c4 = (undefined **)0x4b9605; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(ppuVar4,local_1a8); + local_c4 = (undefined **)0x4b9613; + _System__VarToLStr_qqrr17System_AnsiStringrx14System_Variant(param_4,local_194); + *in_FS_OFFSET = uStack_c0; + pcStack_b8 = &LAB_004b974d; + pcStack_bc = (char *)0x4b9636; + _System__FinalizeArray_qqrpvt1ui(&local_1c8,__xp_14System_Variant_exref,3); + pcStack_bc = (char *)0x4b9641; + _System__LStrClr_qqrpv(&local_198); + pcStack_bc = (char *)0x4b964c; + _System__VarClr_qqrr14System_Variant(local_194); + pcStack_bc = (char *)0x4b9657; + _System__LStrClr_qqrpv(&local_184); + pcStack_bc = (char *)0x4b9662; + _System__VarClr_qqrr14System_Variant(&local_180); + pcStack_bc = (char *)0x4b9672; + _System__LStrArrayClr_qqrpvi(&local_170,3); + pcStack_bc = (char *)0x4b9688; + _System__FinalizeArray_qqrpvt1ui(&local_164,__xp_14System_Variant_exref,2); + pcStack_bc = (char *)0x4b9693; + _System__LStrClr_qqrpv(&local_144); + pcStack_bc = (char *)0x4b96a9; + _System__FinalizeArray_qqrpvt1ui(&local_140,__xp_14System_Variant_exref,3); + pcStack_bc = (char *)0x4b96b9; + _System__LStrArrayClr_qqrpvi(&local_110,7); + pcStack_bc = (char *)0x4b96c4; + _System__VarClr_qqrr14System_Variant(&local_f4); + pcStack_bc = (char *)0x4b96d4; + _System__LStrArrayClr_qqrpvi(&local_e4,8); + pcStack_bc = (char *)0x4b96df; + _System__VarClr_qqrr14System_Variant(&local_c4); + pcStack_bc = (char *)0x4b96ea; + _System__LStrClr_qqrpv(&local_b4); + pcStack_bc = (char *)0x4b9700; + _System__FinalizeArray_qqrpvt1ui(&local_b0,__xp_14System_Variant_exref,3); + pcStack_bc = (char *)0x4b970d; + _System__LStrArrayClr_qqrpvi(&local_80,5); + pcStack_bc = (char *)0x4b9715; + _System__VarClr_qqrr14System_Variant(&local_6c); + pcStack_bc = (char *)0x4b9722; + _System__LStrArrayClr_qqrpvi(&local_5c,3); + pcStack_bc = (char *)0x4b9735; + _System__FinalizeArray_qqrpvt1ui(&local_4c,__xp_14System_Variant_exref,3); + pcStack_bc = (char *)0x4b9742; + _System__LStrArrayClr_qqrpvi(&local_10,3); + return; +} + diff --git a/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_tpl_0x494b53_excerpt.c b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_tpl_0x494b53_excerpt.c new file mode 100644 index 000000000..eacf03b27 --- /dev/null +++ b/resources/knowledge/original/경쟁사 프로그램/STmate/REA 분석/38_보고서_잔여_작업자료_20261114/evidence/ST_P_XLS_cost_tpl_0x494b53_excerpt.c @@ -0,0 +1,196 @@ +/* ST_P_XLS 0x494b53 둘레 함수 발췌(Ghidra 12.1.4 의사코드 글 그대로) — 산재·고용 · 안전 변경 · 기타경비 식 틀 */ + uStack_b2 = 0; + local_b8 = (char **)0x494b0d; + FUN_004947fc(param_5 + 9,param_5 + 0x24,0); + } + local_ac = 0; + local_b0 = -*(int *)(param_4 + 0x1e4); + fVar22 = (((float10)param_5[7] + (float10)param_5[0x5c]) * (float10)*(double *)(param_4 + 0x48)) / + (float10)100.0; + uStack_bc = SUB104(fVar22,0); + local_b8 = (char **)((unkuint10)fVar22 >> 0x20); + local_b4 = (undefined2)((unkuint10)fVar22 >> 0x40); + uStack_c4 = (double)CONCAT44(0x494b36,(undefined4)uStack_c4); + MROUNDUD(); + uStack_c4 = (double)in_ST3; + local_c8 = (undefined *)0x0; + uStack_cc = 0x494b53; + FUN_004947fc(param_5 + 9,param_5 + 0x25,*(undefined1 *)((int)param_5 + 0x103ad)); + uStack_c4._4_4_ = "ROUNDDOWN( ([L_AMT][+LAB]) * [RAT_1], "; + uStack_c4._0_4_ = 0x494b6b; + _Sysutils_IntToStr_qqri(-*(int *)(param_4 + 0x1e4),&local_b0); + uStack_c4 = (double)CONCAT44(uStack_c4._4_4_,local_b0); + local_c8 = &DAT_00499ba4; + uStack_cc = 0x494b86; + _System__LStrCatN_qqrv(param_5 + 0x205d,3); + uStack_cc = 0; + iStack_d0 = -*(int *)(param_4 + 0x1e4); + local_dc._0_10_ = + (((float10)param_5[7] + (float10)param_5[0x5c]) * (float10)*(double *)(param_4 + 0x50)) / + (float10)100.0; + local_e4 = (double)CONCAT44(0x494baf,(undefined4)local_e4); + MROUNDUD(); + local_e4 = (double)in_ST4; + local_e8 = (undefined *)0x0; + local_ec._0_2_ = 0x4bcc; + local_ec._2_2_ = 0x49; + FUN_004947fc(param_5 + 9,param_5 + 0x26,*(undefined1 *)((int)param_5 + 0x103ae)); + local_e4._4_4_ = "ROUNDDOWN( ([L_AMT][+LAB]) * [RAT_1], "; + local_e4._0_4_ = 0x494be4; + _Sysutils_IntToStr_qqri(-*(int *)(param_4 + 0x1e4),&local_b4); + local_e4._0_2_ = local_b4; + local_e4._2_2_ = uStack_b2; + local_e8 = &DAT_00499ba4; + local_ec._0_2_ = 0x4bff; + local_ec._2_2_ = 0x49; + _System__LStrCatN_qqrv((int)param_5 + 0x102ec,3); + if (*(int *)(param_4 + 0x1dc) - 1U < 2) { + local_ec._0_2_ = (undefined2)*(undefined4 *)((int)param_5 + 0x154); + local_ec._2_2_ = (undefined2)((uint)*(undefined4 *)((int)param_5 + 0x154) >> 0x10); + local_f0 = *(char ***)(param_5 + 0x2a); + local_f4 = 0; + uStack_f2 = 0; +/* ... */ + *(undefined4 *)(param_4 + 400) = 1; + } + } + if ((((DAT_00613928 != 7) || (param_1 < 2)) || (*(int *)(param_4 + 0x1d4) != 2)) || + (param_5[0x21] < 1.0)) { + fVar29 = fVar25; + if (*(int *)(param_4 + 400) == 1) { + local_328 = (char *)0x0; + iStack_32c = -*(int *)(param_4 + 0x1e4); + fVar27 = _DAT_00499f1c * (float10)local_14; + local_338 = SUB104(fVar27,0); + uStack_334 = (undefined4)((unkuint10)fVar27 >> 0x20); + uStack_330 = (undefined2)((unkuint10)fVar27 >> 0x40); + local_33c = 0x766f; + uStack_33a = 0x49; + MROUNDUD(); + dVar1 = (double)fVar28; + local_340 = SUB84(dVar1,0); + local_33c = (undefined2)((ulonglong)dVar1 >> 0x20); + uStack_33a = (undefined2)((ulonglong)dVar1 >> 0x30); + local_344 = (char *)0x0; + local_348._0_2_ = 0x768c; + local_348._2_2_ = 0x49; + FUN_004947fc(param_5 + 9,param_5 + 0x35,*(undefined1 *)((int)param_5 + 0x103b3)); + local_33c = 0x9dec; + uStack_33a = 0x49; + local_340 = *(undefined4 *)(param_5 + 0x2062); + local_344 = " * 1.2,"; + local_348._0_2_ = 0x76af; + local_348._2_2_ = 0x49; + _Sysutils_IntToStr_qqri(-*(int *)(param_4 + 0x1e4),local_adc); + local_348._0_2_ = local_adc._0_2_; + local_348._2_2_ = local_adc._2_2_; + local_34c = &DAT_00499ba4; + local_350 = 0x76ca; + uStack_34e = 0x49; + _System__LStrCatN_qqrv(param_5 + 0x2062,5); + *(undefined4 *)(param_4 + 0x78) = *(undefined4 *)(param_4 + 0x80); + *(undefined4 *)(param_4 + 0x7c) = *(undefined4 *)(param_4 + 0x84); + *(undefined4 *)(param_5 + 0x2e) = *(undefined4 *)(param_5 + 0x2f); + *(undefined4 *)((int)param_5 + 0x174) = *(undefined4 *)((int)param_5 + 0x17c); + } + else { + local_328 = (char *)0x0; + iStack_32c = -*(int *)(param_4 + 0x1e4); + fVar27 = (float10)(double)fVar27; + local_338 = SUB104(fVar27,0); + uStack_334 = (undefined4)((unkuint10)fVar27 >> 0x20); + uStack_330 = (undefined2)((unkuint10)fVar27 >> 0x40); + local_33c = 0x7710; + uStack_33a = 0x49; + MROUNDUD(); + dVar1 = (double)fVar28; + local_34c = SUB84(dVar1,0); + local_348._0_2_ = (undefined2)((ulonglong)dVar1 >> 0x20); + local_348._2_2_ = (undefined2)((ulonglong)dVar1 >> 0x30); + local_350 = 0; + uStack_34e = 0; + local_354 = 0x49772d; + FUN_004947fc(param_5 + 9,param_5 + 0x35,*(undefined1 *)((int)param_5 + 0x103b3)); + if (*(int *)(param_4 + 0x18c) == 1) { + *(undefined4 *)(param_4 + 0x78) = *(undefined4 *)(param_4 + 0x88); + *(undefined4 *)(param_4 + 0x7c) = *(undefined4 *)(param_4 + 0x8c); + *(undefined4 *)(param_5 + 0x2e) = *(undefined4 *)(param_5 + 0x30); + *(undefined4 *)((int)param_5 + 0x174) = *(undefined4 *)((int)param_5 + 0x184); + } + } + } + else { + local_328 = "[C_AMT_B]+ROUNDDOWN( [C_AMT_B] * (([C_TAMT_C]-[C_TAMT_B])/[C_TAMT_B]), "; + iStack_32c = 0x49747e; + _Sysutils_IntToStr_qqri(-*(int *)(param_4 + 0x1e4),&local_a88); + iStack_32c = local_a88; + uStack_330 = 0x9ba4; + uStack_32e = 0x49; + uStack_334 = 0x497499; + _System__LStrCatN_qqrv(param_5 + 0x2062,3); + uStack_334 = 0; + local_338 = (char *)-*(int *)(param_4 + 0x1e4); + fVar27 = (((float10)param_5[0x3a] - (float10)param_5[0x21]) * (float10)param_5[0x20]) / + (float10)param_5[0x21]; +/* ... */ + local_3b8 = 0x497b18; + FUN_004947fc(param_5 + 9,param_5 + 0x3c,*(undefined1 *)((int)param_5 + 0x103b4)); + pcStack_3ac = (char *)0x497b32; + _System__LStrCat3_qqrv(&local_b5c,"ROUNDDOWN( ((",*(undefined4 *)(*(int *)PTR_DAT_006120c0 + 4)) + ; + pcStack_3ac = (char *)0x497b43; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(local_b58,local_b5c); + pcStack_3ac = local_b58; + uStack_3b0 = 0x7b57; + uStack_3ae = 0x49; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(&local_b7c,0); + uStack_3b0 = (undefined2)local_b70; + uStack_3ae = (undefined2)((uint)local_b70 >> 0x10); + local_3b4 = local_b74; + local_3b8 = local_b78; + local_3bc = local_b7c; + fVar22 = (float10)_local_24; + local_3c8._0_2_ = SUB102(fVar22,0); + local_3c8._2_2_ = (undefined2)((unkuint10)fVar22 >> 0x10); + local_3c4 = (int)((unkuint10)fVar22 >> 0x20); + local_3c0 = (undefined2)((unkuint10)fVar22 >> 0x40); + local_3cc = (char **)0x497b84; + _Sysutils_FloatToStr_qqrg(&local_b94); + local_3cc = (char **)0x497b9a; + _System__LStrCat3_qqrv(&local_b90,&DAT_00499ec0,local_b94); + local_3cc = (char **)0x497bab; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(local_b8c,local_b90); + local_3cc = local_b80; + local_3d0 = local_b84; + local_3d4 = local_b88; + local_3d8 = local_b8c._0_2_; + uStack_3d6 = local_b8c._2_2_; + local_3dc = (char *)CONCAT31((int3)(CONCAT22(extraout_var_11, + (ushort)(_local_24 < 0.0) << 8 | + (ushort)NAN(_local_24) << 10 | + (ushort)(_local_24 == 0.0) << 0xe) >> 8), + (_local_24 == 0.0) == 0); + local_3e0 = local_b6c; + local_3e4 = (char *)0x497bdf; + IIF(); + pcVar13 = local_3e0; + local_3e0 = (char *)0x497beb; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(pcVar13,local_b6c); + local_3e0 = local_b58; + local_3e4 = (char *)0x497c02; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(local_ba4,")[+LAB]) * [RAT_1], "); + pcVar13 = local_3e0; + local_3e0 = (char *)0x497c0e; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(pcVar13,local_ba4); + local_3e0 = local_b58; + local_3e4 = (char *)0x497c28; + _Sysutils_IntToStr_qqri(-*(int *)(param_4 + 0x1e4),&local_bb8); + local_3e4 = (char *)0x497c39; + _System__VarFromLStr_qqrr14System_Variantx17System_AnsiString(local_bb4,local_bb8); + pcVar13 = local_3e0; + local_3e0 = (char *)0x497c45; + _System__VarAdd_qqrr14System_Variantrx14System_Variant(pcVar13,local_bb4); + local_3e0 = local_b58; + local_2c8 = CONCAT22(local_2c8._2_2_,0x2900); + local_2c8 = CONCAT31(local_2c8._1_3_,1); + local_3e4 = (char *)0x497c6c; diff --git a/resources/tester/test_stmate_report_cost_fmla.py b/resources/tester/test_stmate_report_cost_fmla.py new file mode 100644 index 000000000..e47263766 --- /dev/null +++ b/resources/tester/test_stmate_report_cost_fmla.py @@ -0,0 +1,264 @@ +"""STMATE 재현 앱 보고서 F5-31 — 총괄내역서 원가 줄 · 공사원가계산서 엑셀 수식을 원본 STMATE 엑셀과 대조. + +정답 = 실무 STC 옆 원본 출력 엑셀(.xlsx) — 수식 글(openpyxl 수식 읽기) · 셈한 값(data_only) 그대로. +앱 쪽 = report_cost_fmla.js(원가 줄 D·E 식 · H 숨은 메타 · I~P 보정·요율 칸 · 원가계산서 참조·비고·바로가기). +값 = 앱 수식을 작은 엑셀 셈틀로 다시 셈(다른 시트 참조 칸 = 앱 모형 값) → 원본 셈 값과 맞댐. +대조 범위: 총괄 가. 줄 아래 C(글)·D 값 칸·E 글 칸 뺀 수식·수 칸 / 원가계산서 5행부터 수식 칸. +""" + +import re +import sys +import warnings +from decimal import ROUND_DOWN, ROUND_HALF_UP, ROUND_UP, Decimal +from functools import lru_cache +from io import BytesIO +from pathlib import Path + +import openpyxl +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from test_stmate_report_link import formulas, mine_formulas # noqa: E402 +from test_stmate_report_sheets import DOCS, PAIRS, book, server # noqa: E402 + +SIDS = ("총괄내역서", "공사원가계산서") + + +def _first(sh, sid, name): + """대조 시작 행 — 총괄 = 가. 줄(변경 양식은 두 줄) 다음 · 원가계산서 = 5행.""" + if sid == "공사원가계산서": + return 5 + ga = next(i for i, r in enumerate(sh["rows"], 1) if r.get("_c") == "GA") + return ga + (2 if PAIRS[name][2] > 1 else 1) + + +@lru_cache(maxsize=None) +def pairs(name, sid): + """(원본 수식·수 칸, 앱 수식·수 칸) — 같은 범위.""" + sh = book(name)[sid] + lo = _first(sh, sid, name) + orig = formulas(name, sh["name"]) + cols = "DEFGHIJKLMNOP" if sid == "총괄내역서" else "DEFGHIJ" + + def keep(k, v): + if k[1] < lo or k[0] not in cols: + return False + if sid == "공사원가계산서" or k[0] in "DE": + return isinstance(v, str) and v.startswith("=") + return True + + want = {k: v for k, v in orig.items() if keep(k, v)} + got = {k: v for k, v in mine_formulas(sh).items() if k[1] >= lo and k[0] in cols} + return want, got + + +@pytest.mark.parametrize("name", list(PAIRS)) +@pytest.mark.parametrize("sid", SIDS) +def test_cost_formulas_match_original(name, sid): + """원가 줄 수식 글 · 숨은 메타 · 보정·요율 칸 · 원가계산서 참조·비고·바로가기 — 원본 글 그대로.""" + want, got = pairs(name, sid) + bad = [(f"{c}{r}", want.get((c, r)), got.get((c, r))) for (c, r) in set(want) | set(got)] + bad = sorted(b for b in bad if b[1] != b[2]) + assert len(want) > 60, f"{name} {sid} 원본 수식 없음" + assert not bad, f"{name} {sid} 수식 불일치 {len(bad)}건 {bad[:4]}" + + +# ── 작은 엑셀 셈틀 — 원가 줄 수식에 나오는 꼴만(ROUNDDOWN·ROUNDUP·ROUND·ABS·IF·TEXT·& · 비교) ── +TOK = re.compile( + r'\s*(?:(?P"(?:[^"]|"")*")|(?P(?:(?:\'[^\']+\'|[^\W\d][\w]*)!)?\$?[A-Z]{1,2}\$?\d+)' + r"|(?P\d+(?:\.\d+)?)|(?P[A-Z]+)\(|(?P<>|>=|<=|[-+*/&(),<>=]))" +) + + +def _num(v): + return 0 if v in (None, "") else v + + +def _text(v): + if isinstance(v, str): + return v + v = _num(v) + return str(int(v)) if float(v).is_integer() else ("%.15g" % v) + + +def _rnd(x, d, mode): + q = Decimal(1).scaleb(-int(d)) + return float(Decimal(format(_num(x), ".15g")).quantize(q, rounding=mode)) + + +FUNCS = { + "ROUNDDOWN": lambda x, d: _rnd(x, d, ROUND_DOWN), + "ROUNDUP": lambda x, d: _rnd(x, d, ROUND_UP), + "ROUND": lambda x, d: _rnd(x, d, ROUND_HALF_UP), + "ABS": lambda x: abs(_num(x)), + "IF": lambda c, a, b: a if c else b, + "TEXT": lambda v, f: f"{round(_num(v)):,}", +} + + +class Book: + """시트 모형 묶음 셈 — live {시트: 첫 행} 아래 _f 수식만 셈 · 그 밖(공종 줄 · 다른 시트)은 모형 값.""" + + def __init__(self, sheets, live): + self.by = {s["name"]: s for s in sheets.values()} + self.live = dict(live) + self.memo = {} + + def cell(self, sheet, col, row): + key = (sheet, col, row) + if key in self.memo: + return self.memo[key] + rows = self.by[sheet]["rows"] + r = rows[row - 1] if row <= len(rows) else {} + f = (r.get("_f") or {}).get(col) if row >= self.live.get(sheet, 10**9) else None + if isinstance(f, str) and f.startswith("="): + v = self.eval(f[1:], sheet) + elif f is not None and not isinstance(f, str): + v = f + else: + v = r.get(col) + self.memo[key] = v + return v + + def eval(self, text, sheet): + toks = [m for m in TOK.finditer(text)] + self.t, self.i, self.sheet = toks, 0, sheet + return self.cmp() + + def peek(self, kind=None, val=None): + if self.i >= len(self.t): + return None + m = self.t[self.i] + if kind and m.group(kind) is None: + return None + if val is not None and m.group("op") != val: + return None + return m + + def take(self): + self.i += 1 + return self.t[self.i - 1] + + def cmp(self): + a = self.cat() + while self.peek("op") and self.peek().group("op") in (">", "<", "=", "<>"): + op = self.take().group("op") + b = self.cat() + a = {">": a > b, "<": a < b, "=": a == b, "<>": a != b}[op] + return a + + def cat(self): + a = self.add() + while self.peek(val="&"): + self.take() + a = _text(a) + _text(self.add()) + return a + + def add(self): + a = self.mul() + while self.peek("op") and self.peek().group("op") in "+-": + op = self.take().group("op") + b = self.mul() + a = _num(a) + _num(b) if op == "+" else _num(a) - _num(b) + return a + + def mul(self): + a = self.unary() + while self.peek("op") and self.peek().group("op") in "*/": + op = self.take().group("op") + b = self.unary() + a = _num(a) * _num(b) if op == "*" else _num(a) / _num(b) + return a + + def unary(self): + if self.peek("op") and self.peek().group("op") in "+-": + op = self.take().group("op") + v = _num(self.unary()) + return -v if op == "-" else v + return self.prim() + + def prim(self): + m = self.take() + if m.group("s"): + return m.group("s")[1:-1].replace('""', '"') + if m.group("n"): + return float(m.group("n")) + if m.group("ref"): + ref = m.group("ref").replace("$", "") + sh, _, at = ref.rpartition("!") + sh = sh.strip("'") if sh else self.sheet + c, r = re.match(r"([A-Z]+)(\d+)", at).groups() + state = (self.t, self.i, self.sheet) + v = self.cell(sh, c, int(r)) + self.t, self.i, self.sheet = state + return v + if m.group("fn"): + args = [] + while not self.peek(val=")"): + args.append(self.cmp()) + if self.peek(val=","): + self.take() + self.take() + return FUNCS[m.group("fn")](*args) + if m.group("op") == "(": + v = self.cmp() + self.take() + return v + raise ValueError(m.group(0)) + + +@lru_cache(maxsize=None) +def original_values(name, sheet): + warnings.filterwarnings("ignore") + wb = openpyxl.load_workbook(DOCS / PAIRS[name][1], data_only=True, read_only=True) + return { + (c.column_letter, c.row): c.value + for row in wb[sheet].iter_rows() + for c in row + if c.value is not None + } + + +@pytest.mark.parametrize("name", list(PAIRS)) +def test_cost_formula_values_match_original(name): + """앱 수식을 다시 셈한 값 = 원본 엑셀 셈 값(절사·끝전 보정 포함) · 총괄 D = 앱 계산 엔진 금액.""" + bk = book(name) + live = {bk[s]["name"]: _first(bk[s], s, name) for s in SIDS} + ev = Book(bk, live) + bad, n = [], 0 + for sid in SIDS: + sh = bk[sid] + want, got = pairs(name, sid) + ov = original_values(name, sh["name"]) + for (c, r), f in got.items(): + if not (isinstance(f, str) and f.startswith("=")) or "HYPERLINK" in f: + continue + v, o = ev.cell(sh["name"], c, r), ov.get((c, r)) + n += 1 + same = abs(v - o) <= 1e-9 * max(1, abs(o)) if isinstance(o, (int, float)) else v == o + if not same: + bad.append((sid, f"{c}{r}", o, v)) + if c == "D" and sid == "총괄내역서" and v != sh["rows"][r - 1].get("D"): + bad.append((sid, f"{c}{r} 엔진", sh["rows"][r - 1].get("D"), v)) + assert n > 80 and not bad, f"{name} 값 불일치 {len(bad)}/{n} {bad[:4]}" + + +def test_xlsx_writes_cost_formulas_meta_and_numbers(): + """엑셀 쓰기(수식 모드) — 총괄 원가 줄 D 식 · H 숨은 메타 글 · I~P 수 칸 · 원가계산서 참조 식이 원본 칸 그대로.""" + from fastapi.testclient import TestClient + + name = "봉화2024" + bk = book(name) + body = {"sheets": list(bk.values()), "filename": "c.xlsx", "formulas": True} + res = TestClient(server.app).post("/api/report/xlsx", json=body) + warnings.filterwarnings("ignore") + wb = openpyxl.load_workbook(BytesIO(res.content)) + checked = 0 + for sid in SIDS: + sh = bk[sid] + want, _ = pairs(name, sid) + ws = wb[sh["name"]] + for (c, r), v in want.items(): + assert ws[f"{c}{r}"].value == v, f"{sid} {c}{r}" + checked += 1 + assert checked > 200 diff --git a/tools/stmate-test/PLAN.md b/tools/stmate-test/PLAN.md index c24f85aac..60b49b64e 100644 --- a/tools/stmate-test/PLAN.md +++ b/tools/stmate-test/PLAN.md @@ -29,7 +29,7 @@ ## 진행률 -- 합계: 전체 246 · 구현 226 · 부분 19 · 없음 1 → 진행률 95.7%((226 + 19×0.5) / 246) · 구현만 세면 91.9% +- 합계: 전체 246 · 구현 227 · 부분 18 · 없음 1 → 진행률 95.9%((227 + 18×0.5) / 246) · 구현만 세면 92.3% ### G1 화면·메뉴 (구현 51 · 부분 2 · 없음 1) — 96.3% @@ -217,7 +217,7 @@ - [x] F4-28 구버전 → Win8 자료 변환 — ST_U_Convert ST_ConvToWin8 · 현재: 구현(REA 37 — 판 가리기(Chk_Data 0x459280) · 7.x/DOS COSTN·BDQTY·PRJLST·KONGJONG·BOQ·Start8x.Ini 대응 · B→X 조건 = 단위 X · 합성 자료 시험 · 간접비 이관·RATE·SYSINFO·IFORM 미확인(틀 유지)) - [x] F4-29 자동백업·자료 복원 — wM_D_Restore · wM_ST_Restore · 현재: 구현(api_file 자동 백업·복원) -### G5 보고서·내보내기 (구현 25 · 부분 11 · 없음 0) — 84.7% +### G5 보고서·내보내기 (구현 26 · 부분 10 · 없음 0) — 86.1% - [x] F5-01 재료비·노무비·경비 목록표(0101~0103) — Prn_SanMok · 현재: 구현(report_* · 원본 STMATE 엑셀 셀 대조 3건(test_stmate_report_sheets)) - [x] F5-02 일식·견적(W코드) 목록표(0104) — Prn_SanMok · 현재: 구현(일식견적 목록 0104 · 울진 변경1차 당초 차수 셀 일치) @@ -249,7 +249,7 @@ - [x] F5-28 안내 시트(※※안내※※) — 19번 §3 · 현재: 구현(REA 38 — 설계내역서 쪽 채움 LNP_CNT(0x520e34) · 원본 3건 행 수·빈 줄·숨은 총합계 줄 일치 · 안내 줄 번호 일치(test_stmate_report_link)) - [x] F5-29 〓 INITIAL 〓 설정 스냅샷 시트 — 18번 §2.3 · 현재: 구현(report_init INITIAL 시트 · 3건 일치(파일 자리·작성자·날짜 제외)) - [ ] F5-30 제잡비 요율표 인쇄 — PM_KAN_RATE · 현재: 부분(REA 38 — 엑셀판·인쇄 분기 없음(ST_KanJub 전용) · 견본 없음) -- [ ] F5-31 엑셀 다중 시트 내보내기·수식 보존 — ST_P_XLS Prn_FMLA_INFO · 현재: 부분(REA 38 — 원가 줄·원가계산서 원본 수식 꼴 기록(§8) · 미구현) +- [x] F5-31 엑셀 다중 시트 내보내기·수식 보존 — ST_P_XLS Prn_FMLA_INFO · 현재: 구현(REA 38b — 총괄 원가 줄 D·E 수식·H 숨은 메타·I~P 보정·요율 칸·이윤 끝전 사슬 + 공사원가계산서 참조·소계·구성비·비고·바로가기(변경 양식 포함) — 원본 5건 1392칸 수식 글·값 일치(test_stmate_report_cost_fmla) · 24·25판 괄호 구분 · 표본 없는 갈래(안전 B상한·퇴직공제·부가세 1~3 등) 값만) - [ ] F5-32 보고서 인쇄·미리보기 — PM_* · 현재: 부분(REA 38 — 인쇄 영역 $A:$열 · 탭 색 · 안내·INITIAL 세로 · 숨은 열 — 원본 3건 일치 · 배율·여백·바닥글 미확인) - [ ] F5-33 가로형·토개공·수자원·산근양식 변형 — PM_*_L · PM_Prt · 현재: 부분(REA 38 — 토개공·수자원 글 설치본 전체에 없음 · 가로형 방향만) - [x] F5-34 출력별 반올림(내역 절사·집계 사사오입) — 06번 §3 · 18번 §2.4 · 현재: 구현(출력별 반올림 · 집계표 사사오입 원본 일치) diff --git a/tools/stmate-test/report_boq.js b/tools/stmate-test/report_boq.js index 5c572fe6c..c83d08c71 100644 --- a/tools/stmate-test/report_boq.js +++ b/tools/stmate-test/report_boq.js @@ -290,13 +290,16 @@ const StmateReportBoq = (() => { C.push(sh, h3); const now = summaryRows(dbf, stage, design._tree, opts); const old = base ? summaryRows(dbf, base.stage, base.tree, opts) : null; + const F = globalThis.STMATE_REPORT_COST_FMLA || require("./report_cost_fmla.js"); if (!old || old.length !== now.length) { for (const r of now) C.push(sh, r); + sh._kan = [F.kanInfo(stage, dbf)]; // 원가 줄 수식 재료(report_cost_fmla) return sh; } + sh._kan = [F.kanInfo(base.stage, dbf), F.kanInfo(stage, dbf)]; now.forEach((r, i) => { C.push(sh, old[i]); - const second = Object.assign({}, r); + const second = Object.assign({}, r, { _s: 1 }); for (const c of ["A", "B", "C"]) delete second[c]; C.push(sh, second); }); @@ -323,22 +326,28 @@ const StmateReportBoq = (() => { const k = stage.kanjub; const L = costLines(k, stage.cfg); const direct = tree.finalRow ? tree.finalRow.node.sum : { M: k.dM, L: k.dL, S: k.dS }; - const ga = { A: " 가.", B: "순공사비계", _k: "sub" }; + const ga = { A: " 가.", B: "순공사비계", _k: "sub", _c: "GA" }; put4(ga, direct); out.push(ga); let no = 0; - const line = (label, amount, note, extra) => { + // _c = 원가 줄 열쇠(report_cost_fmla 수식) · _rt = 요율(숨은 M 칸) + const line = (label, amount, note, extra, c, rt) => { no += 1; - const cells = { B: `${String(no).padStart(2)}. ${label}`, D: amount, _k: "cost" }; + const cells = { B: `${String(no).padStart(2)}. ${label}`, D: amount, _k: "cost", _c: c }; + if (rt != null) cells._rt = num(rt); if (note) cells.E = note; out.push(Object.assign(cells, extra || {})); }; - const sub = (a, label, amount) => - out.push({ A: ` ${a}`, B: ` ${label}`, D: amount, _k: "sub" }); + const sub = (a, label, amount, c) => + out.push({ A: ` ${a}`, B: ` ${label}`, D: amount, _k: "sub", _c: c }); + const addLine = (a, g, i) => line(a.name, num(a.amount), "", { _add: [g, i + 1] }, "ADD"); line( "간접노무비", k.kanno, L.fixedKeys.has("KANNO") ? "" : `직접노무비 x ${rateText(k.itemMap.KANNO.rate)}%`, + null, + "KANNO", + k.itemMap.KANNO.rate, ); for (const e of L.exp.filter((x) => x.code !== "KANNO")) { const note = @@ -352,33 +361,43 @@ const StmateReportBoq = (() => { e.code === "ANJUN" && L.govMat > 0 && !L.fixedKeys.has("ANJUN") ? { C: "A)관급재/1.1포함 적용" } : null, + e.code, + e.rate, ); } - for (const a of L.adds(1)) line(a.name, num(a.amount)); + L.adds(1).forEach((a, i) => addLine(a, 1, i)); const na = k.pureConstructionCost; - sub("나.", "소 계", na); - line( - "일반관리비", - k.ilbanAmt, - L.fixedKeys.has("ILBAN") ? "" : `(나.소 계) x ${rateText(num(stage.cfg?.rates?.ILBAN))}%`, - ); - sub("다.", "소 계", na + k.ilbanAmt); - const iyunR = rateText(num(stage.cfg?.rates?.IYUN)); - line("이 윤", k.iyunFinal, `(다.소계 - 재료비) x ${iyunR}% = ${commas(k.iyunRaw)}`); - for (const a of L.adds(2)) line(a.name, num(a.amount)); - sub("라.", "공급가액", k.supplyFinal); + sub("나.", "소 계", na, "NA"); + const ilR = num(stage.cfg?.rates?.ILBAN); + const ilN = L.fixedKeys.has("ILBAN") ? "" : `(나.소 계) x ${rateText(ilR)}%`; + line("일반관리비", k.ilbanAmt, ilN, null, "ILBAN", ilR); + sub("다.", "소 계", na + k.ilbanAmt, "DA"); + const iyunR = num(stage.cfg?.rates?.IYUN); + const iyN = `(다.소계 - 재료비) x ${rateText(iyunR)}% = ${commas(k.iyunRaw)}`; + line("이 윤", k.iyunFinal, iyN, null, "IYUN", iyunR); + L.adds(2).forEach((a, i) => addLine(a, 2, i)); + sub("라.", "공급가액", k.supplyFinal, "RA"); const vn = vatNote(stage.cfg); - line("부가가치세", k.vatFinal, vn === "총 원 가 x 10%" ? "공급가액 x 10%" : vn); - for (const a of L.adds(3)) line(a.name, num(a.amount)); - sub("마.", "도급공사비", k.contractFinal); + line( + "부가가치세", + k.vatFinal, + vn === "총 원 가 x 10%" ? "공급가액 x 10%" : vn, + null, + "VAT", + 10, + ); + L.adds(3).forEach((a, i) => addLine(a, 3, i)); + sub("마.", "도급공사비", k.contractFinal, "MA"); if (k.govFinal) line( govLabel(dbf, stage, false), k.govFinal, `원자재대: ${commas(L.govMat)} (조달수수료: ${commas(k.govFeeTotal)} 포함)`, + null, + "GOV", ); - for (const a of L.adds(4)) line(a.name, num(a.amount)); - if (k.govFinal || L.adds(4).length) sub("바.", "총공사비", k.grandTotal); + L.adds(4).forEach((a, i) => addLine(a, 4, i)); + if (k.govFinal || L.adds(4).length) sub("바.", "총공사비", k.grandTotal, "BA"); return out; } @@ -469,7 +488,10 @@ const StmateReportBoq = (() => { C.put(sh, 3, { A: "구 분 ", D: "금 액", E: "구 성 비", F: "비 고", _k: "head" }); C.put(sh, 4, { A: " 비 목", _k: "head" }); now.forEach((r, i) => { - const c = Object.assign({ D: r.amount, E: (r.amount / grand) * 100, _k: "cost" }, r.cells); + const c = Object.assign( + { D: r.amount, E: (r.amount / grand) * 100, _k: "cost", _c: r.key }, + r.cells, + ); if (r.note) c.F = r.note; C.put(sh, 5 + i, c); }); @@ -479,7 +501,10 @@ const StmateReportBoq = (() => { const old = Object.fromEntries(wonRows(dbf, base).map((r) => [r.key, r])); now.forEach((r, i) => { const o = old[r.key] ? old[r.key].amount : 0; - const c = Object.assign({ D: o, E: r.amount, F: r.amount - o, _k: "cost" }, r.cells); + const c = Object.assign( + { D: o, E: r.amount, F: r.amount - o, _k: "cost", _c: r.key }, + r.cells, + ); c.G = (r.amount / grand) * 100; if (r.note) c.H = r.note; C.put(sh, 5 + i, c); diff --git a/tools/stmate-test/report_cost_fmla.js b/tools/stmate-test/report_cost_fmla.js new file mode 100644 index 000000000..538d9a3bd --- /dev/null +++ b/tools/stmate-test/report_cost_fmla.js @@ -0,0 +1,372 @@ +/* -*- coding: utf-8 -*- */ +/** + * 원가 줄 엑셀 수식 (report_cost_fmla.js) — 총괄내역서(0601) 원가 줄 · 공사원가계산서(0608) 칸 수식 + * 근거: 지식DB STmate/REA 분석/38_보고서_잔여_작업자료_20261114/38b_F5-31_원가줄수식.md + * (ST_P_XLS 0x494b53 식 틀 · 0x4b8c6c 숨은 메타 · 원본 출력 엑셀 5건 수식 실측) + * 정답 대조: resources/tester/test_stmate_report_cost_fmla.py(원본 수식 글 · 값 그대로) + * 총괄 원가 줄(가. 아래) — D 금액 식 · E 설명 식 · H 숨은 메타 · I~P 보정·요율·추가액 칸(이윤 끝전 J·K·L 사슬) + * 공사원가계산서 — D(변경 양식 D·E) = 총괄 행 참조 · 구성비 = D/총공사비 · 비고 = 총괄 E 참조 · 바로가기 + * 원본에 없는 갈래(수식 꼴 미확인)는 값 그대로 둠 — 값은 계산 엔진(kanjub_calc) 값 + */ +const StmateReportCostFmla = (() => { + const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0); + // 세부공종코드(D_TYP) — 조달청 공사제경비 코드(report_pps 와 같은 표) · 원본 메타 `DTP|` 실측 + const DTP = { KANNO: 201, SANBO: 301, KOBO: 302, KUNBO: 303, SLVBO: 310, YUNBO: 304 }; + Object.assign(DTP, { TBUGUM: 305, ANJUN: 306, KITA: 321, HWAN: 309, EQIP: 311 }); + Object.assign(DTP, { NA: 400, ILBAN: 401, DA: 410, IYUN: 402, RA: 500, VAT: 502 }); + Object.assign(DTP, { MA: 600, GOV: 653, BA: 700 }); + const SUBS = new Set(["NA", "DA", "RA", "MA", "BA"]); + const sre = (c) => + c === "KANNO" + ? "LA" + : SUBS.has(c) + ? "TA-F" + : c === "ILBAN" || c === "IYUN" || c === "VAT" + ? "TA" + : c === "GOV" + ? "TA-3" + : c === "ADD" + ? "" + : "SA"; + const CHG = new Set(["KUNBO", "SLVBO", "YUNBO", "TBUGUM"]); + const ITEM_CODES = ["KANNO", "SANBO", "KOBO", "KUNBO", "SLVBO", "YUNBO", "TBUGUM", "ANJUN"]; + ITEM_CODES.push("KITA", "HWAN", "EQIP", "ILBAN", "IYUN"); + + /** 차수 하나의 수식 재료 — 설정 키(차수 접두 뗌) · 계산 결과 · 판 번호(PRG_V) */ + function kanInfo(stage, dbf) { + const g = globalThis.STMATE_KANJUB_PARTS || require("./kanjub_parts.js"); + const P = g({ num }); + const cfg = (stage && stage.cfg) || {}; + const o = P.reader(cfg, null); + const k = stage.kanjub || {}; + const ini = (dbf && dbf.start_ini) || {}; + const fixed = ITEM_CODES.filter((c) => o.fix(c) != null); + const keysOut = k.keysOut || {}; + return { + slot: num(stage.slot) || 1, + v25: num(ini.PRG_V) >= 25, + totJeol: o.k("I_TOT_JEOL"), + buga: o.k("I_BUGA"), + bugaUp: num(keysOut.B_BUGA_UP ?? o.k("B_BUGA_UP")), + exempt: o.k("C_BUGA_M"), + iyunM: num(k.pretaxProfitAdj), + kanJeol: o.k("I_KAN_JEOLSA"), + kwnJeol: o.k("I_KWN_JEOLSA"), + kwnUpdn: o.k("I_KWN_UPDN"), + rat5: o.k("I_ANJUN_K") * 10 + o.k("I_ANJUN_B"), + anjunAuto: o.k("I_ANJUNAUTO"), + bfch: o.k("I_BF_CH_BO"), + hol: o.k("C_EIS_HOLLAB"), + anjunAdd: num(cfg.anjunBaseAdd ?? o.k("C_ANJUN_G")), + govMat: num(k.govRawTotal) + num(k.govFeeTotal), + govFee: num(k.govFeeTotal), + fixed, + }; + } + + const dig = (n) => (num(n) ? String(-num(n)) : "0"); + const setF = (row, col, v) => ((row._f = row._f || {}), (row._f[col] = v)); + // 설명 식 — 「밑 x 율%」 글의 율 자리를 M 칸 ROUND 로(원본 꼴) + const pctNote = (text, mref) => { + const m = /^(.*) x ([\d.]+)%$/s.exec(String(text || "")); + return m ? `="${m[1]} x "&ROUND(${mref},5)&"%"` : null; + }; + + /** + * 숨은 메타(H) — ST_P_XLS 0x4b8c6c: 0x07 + `키|값 0x05 … + ` · 첫 차수 줄 = DTP·QTY·BDC·SRE + * 둘째 차수 줄 = DTP·QTY · 덧붙임 DRT(고정) · CBO(변경 당초액 잇기) · ANB·ANK(안전 RAT5) · ADN·ADB(추가금액) + */ + function meta(row, info, first) { + const c = row._c; + const add = c === "ADD" ? row._add : null; + const dtp = add ? (add[0] + 2) * 100 + 60 + add[1] : DTP[c]; + if (!dtp) return null; + const n = info.slot; + const p = [`DTP|${dtp}`, `QTY${n}|${n}`]; + if (first) + p.push(`BDC|${add ? `A${add[0]}${String(add[1]).padStart(2, "0")}` : ""}`, `SRE|${sre(c)}`); + if (info.fixed.includes(c)) p.push("DRT|T"); + if (n > 1 && CHG.has(c) && (info.bfch === 1 || info.bfch === 2)) p.push(`CBO|${info.bfch}`); + if (n > 1 && c === "ANJUN" && (info.anjunAuto === 1 || info.anjunAuto === 2)) + p.push(`CBO|${info.anjunAuto}`); + if (c === "ANJUN" && info.rat5) { + if (info.rat5 % 10) p.push("ANB|1"); + const kk = Math.floor(info.rat5 / 10); + if (kk >= 1 && kk <= 5) p.push(`ANK|${kk}`); + } + if (add) p.push(`ADN|${String(row.B || "").replace(/^\s*\d+\.\s*/, "")}`, "ADB|"); + return "_x0007_" + p.map((x) => "`" + x + "_x0005_").join("") + "`"; + } + + /** 총괄 원가 줄 — 차수(_s)마다 가. 줄부터 차례로 */ + function linkSummaryCost(sm) { + const kan = sm._kan || []; + const at = kan.map(() => ({ seq: [] })); + sm.rows.forEach((r, i) => { + if (!r._c || !at[r._s || 0]) return; + const p = at[r._s || 0]; + p.seq.push([r, i + 1]); + if (r._c !== "ADD" && p[r._c] == null) p[r._c] = i + 1; + }); + at.forEach((p, s) => { + if (p.GA == null) return; + p.last = p.BA || p.MA; + p.seq.forEach(([r, n]) => costRow(r, n, kan[s], p, at[0], s)); + }); + } + + // 사이 합 — from 열쇠 줄부터 to 열쇠 줄 앞까지 · 열(D·J·L) + const span = (p, from, to, col) => { + const i = p.seq.findIndex(([r]) => r._c === from); + const k = p.seq.findIndex(([r]) => r._c === to); + return ( + "=(" + + p.seq + .slice(i, k) + .map(([, n]) => `${col}${n}`) + .join("+") + + ")" + ); + }; + + function costRow(r, n, info, p, p0, s) { + const c = r._c; + if (c === "GA") return; + const g = p.GA, + J = dig(info.kanJeol); + const fx = info.fixed.includes(c); + for (const col of "IMNOP") setF(r, col, 0); + if (r._rt != null) setF(r, "M", r._rt); + const h = meta(r, info, s === 0); + if (h) setF(r, "H", h); + const note = String(r.E || ""); + const pctD = (base) => setF(r, "D", `=ROUNDDOWN(${base}*M${n}/100,${J})`); + const pctE = () => { + const e = pctNote(note, `M${n}`); + if (e) setF(r, "E", e); + }; + const v25 = info.v25; // 25 판 = ST_P_XLS 0x494b53 「([L_AMT][+LAB])」 겉 괄호 · 24 판 출력은 없음(원본 실측) + const lab = `(E${g}+D${p.KANNO})`; + const chain = () => (setF(r, "J", `=D${n}`), setF(r, "L", `=D${n}`)); + const sum3 = (from, to) => { + for (const col of "DJL") setF(r, col, span(p, from, to, col)); + }; + switch (c) { + case "KANNO": + if (!fx && note.startsWith("직접노무비 x ")) (pctD(`E${g}`), pctE()); + break; + case "SANBO": + case "KOBO": + if (!fx && !info.hol && note.startsWith("(직접노무비+간접노무비) x ") && p.KANNO) + (pctD(v25 ? `(${lab})` : lab), pctE()); + break; + case "KUNBO": + case "YUNBO": + setF(r, "J", `=E${g}`); + if (!fx && note.startsWith("직접노무비 x ")) (pctD(`E${g}`), pctE()); + break; + case "SLVBO": + if (p.KUNBO) setF(r, "J", `=D${p.KUNBO}`); + if (!fx && p.KUNBO && note.startsWith("건강보험료 x ")) (pctD(`D${p.KUNBO}`), pctE()); + break; + case "ANJUN": { + const gov = info.govMat > 0 && p.GOV ? `+(I${p.GOV}-O${p.GOV})/1.1` : ""; + const base = `((E${g}+(F${g}))${gov})`; + setF(r, "J", `=${base}`); + setF(r, "N", info.anjunAdd); + const b0 = p0.ANJUN; + if (fx) break; + if (s > 0 && b0 && note.startsWith("당초금액")) + setF(r, "D", `=D${b0}+ROUNDDOWN(D${b0}*((J${n}-J${b0})/J${b0}),${J})`); + else if (gov && String(r.C || "").startsWith("A)")) + setF(r, "D", `=ROUNDDOWN(${base}*M${n}/100+${info.anjunAdd},0)`); + break; + } + case "KITA": + if (!fx && !info.hol && note.startsWith("(직접노무비+간접노무비+재료비) x ") && p.KANNO) { + const b = `(${lab}+(F${g}))`; + (pctD(v25 ? `((${b}))` : `(${b})`), pctE()); + } + break; + case "HWAN": + case "EQIP": + if (!fx && note.startsWith("(재료비 + 직접노무비 + 산출경비) x ")) + (pctD(`(D${g})`), pctE()); + break; + case "NA": + setF(r, "D", span(p, "GA", "NA", "D")); + break; + case "ILBAN": + if (!fx && note.startsWith("(나.소 계) x ")) + (setF(r, "D", `=ROUNDDOWN(((D${p.NA})+0)*M${n}/100,0)`), pctE()); + break; + case "DA": + setF(r, "D", `=(D${p.NA}+D${p.ILBAN})`); + setF(r, "I", info.totJeol); + chain(); + break; + case "IYUN": { + const da = p.DA, + ba = p.last, + ga = `-(F${g}))*M${n}/100,0)`; + setF(r, "D", `=ROUNDDOWN(((D${da})${ga}-I${n}-K${n}`); + setF(r, "J", `=ROUNDDOWN(((J${da})${ga}`); + setF(r, "L", `=ROUNDDOWN(((L${da})${ga}-I${n}`); + const m = /^(.*) x ([\d.]+)% = [\d,]+$/s.exec(note); + if (m) setF(r, "E", `="${m[1]} x "&ROUND(M${n},5)&"% = "&TEXT(J${n},"#,##0")`); + if (info.totJeol > 0 && ba) { + const t = `ROUNDDOWN(J${ba},-I${da})`; + setF(r, "I", info.buga === 0 ? `=ROUND((J${ba}-${t})/1.1,0)` : `=J${ba}-${t}`); + setF(r, "K", `=ABS(L${ba}-${t})*IF(L${ba}>${t},1,-1)`); + } else setF(r, "I", info.iyunM); + break; + } + case "RA": + sum3("DA", "RA"); + break; + case "VAT": { + setF(r, "I", info.bugaUp); + setF(r, "N", info.buga === 4 ? info.exempt : 0); + setF(r, "O", info.buga); + const ex = `(D${g}-E${g}-${info.exempt})`; + const f = + info.buga === 0 + ? (col) => `=ROUNDDOWN((${col}${p.RA})*M${n}/100,0)+I${n}` + : info.buga === 4 + ? () => `=ROUNDDOWN(${ex}*M${n}/100,0)+I${n}` + : null; + if (f && p.RA) { + for (const col of "DJL") setF(r, col, f(col)); + pctE(); + } + break; + } + case "MA": + sum3("RA", "MA"); + setF(r, "O", info.totJeol); + break; + case "GOV": { + setF(r, "I", info.govMat); + setF(r, "M", info.kwnJeol); + setF(r, "O", info.govFee); + const fn = ["ROUNDUP", "ROUND", "ROUNDDOWN"][info.kwnUpdn]; + if (fn && note.startsWith("원자재대:")) { + for (const col of "DJL") setF(r, col, `=${fn}(I${n},-${info.kwnJeol})`); + const t = `TEXT(I${n},"#,0")&" (조달수수료: "&TEXT(O${n},"#,0")&" 포함)"`; + setF(r, "E", `="원자재대: "&${t}`); + } + break; + } + case "BA": + sum3("MA", "BA"); + setF(r, "O", info.totJeol); + break; + case "ADD": + if (r._add[0] > 1) chain(); // 이윤 뒤 추가금액부터 끝전 사슬(J·L)에 듦 + break; + default: + break; + } + } + + /** + * 공사원가계산서 — 총괄 행 참조 · 소계 식 · 구성비 · 비고(총괄 E 참조 / 율 ROUND) · 「총괄표 →」 바로가기 + * 변경 양식(당초·변경) = D 당초 · E 변경 · F 증감 · G 구성비 · H 비고(변경 줄) · 바로가기 = 당초 줄 + */ + function linkWon(won, sm, ini, L) { + const kan = sm._kan || []; + const two = won.rows.some((r) => r._k === "head" && r.D === "당 초"); + if (!kan.length || (two && kan.length < 2)) return; + const pos = kan.map(() => ({ adds: [] })); + sm.rows.forEach((r, i) => { + const p = pos[r._s || 0]; + if (!r._c || !p) return; + if (r._c === "ADD") p.adds.push([r, i + 1]); + else if (p[r._c] == null) p[r._c] = i + 1; + }); + const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js"); + const scol = {}; + C.bimokOrder(ini).forEach((k, i) => (scol[k] = "DEFG"[i])); + const S = L.q(sm.name); + const rows = won.rows; + const idx = {}; + rows.forEach((r, i) => r._c && (idx[r._c] = i + 1)); + const last = rows.length; + const lc = L.linkCol(won); + const addRow = (p, key) => { + const m = /^A(\d)(.*)$/s.exec(key); + const hit = m && p.adds.find(([r]) => r._add[0] === +m[1] && r.B.endsWith(`. ${m[2]}`)); + return hit ? hit[1] : 0; + }; + // 열쇠 → [총괄 열, 총괄 행] + const ref = (key, p) => { + const t = { dM: ["M"], dL: ["L"], dS: ["S"] }[key]; + if (t) return [scol[t[0]], p.GA]; + const m = { SUP: "RA", CON: "MA", GRAND: "BA" }[key] || key; + if (/^A\d/.test(key)) return ["D", addRow(p, key)]; + return p[m] ? ["D", p[m]] : null; + }; + const vc = two ? ["D", "E"] : ["D"]; + const ps = two ? [pos[0], pos[1]] : [pos[0]]; + const sumKeys = { sM: ["dM", "iM", "by"], sL: ["dL", "KANNO"] }; + rows.forEach((r, i) => { + const n = i + 1; + if (!r._c || n < 5) return; + const key = r._c; + vc.forEach((col, s) => { + const p = ps[s]; + if (key === "sM") setF(r, col, `=+${col}${idx.dM}+${col}${idx.iM}-${col}${idx.by}`); + else if (sumKeys[key]) setF(r, col, "=+" + sumKeys[key].map((k) => col + idx[k]).join("+")); + else if (key === "sS") { + const parts = []; + for (let q = idx.dS; q < n; q++) parts.push(col + q); + setF(r, col, "=+" + parts.join("+")); + } else if (key === "GRAND" && !p.BA) setF(r, col, `=${col}${idx.CON}`); + else { + const rf = ref(key, p); + if (!rf || !rf[1]) return; + setF( + r, + col, + `=${S}!${rf[0]}${rf[1]}` + (key === "dM" ? `-${col}${idx.iM}+${col}${idx.by}` : ""), + ); + } + }); + // 증감 · 구성비 + if (two) { + setF(r, "F", `=E${n}-D${n}`); + if (n < last) setF(r, "G", `=E${n}/E${last}*100`); + } else if (n < last) setF(r, "E", `=D${n}/D${last}*100`); + // 비고 — 총괄 E 참조(설명 있는 줄) · 일반관리비·이윤·부가세 = 원가계산서 설명 + 총괄 M 율 + const nc = two ? "H" : "F"; + const p = ps[ps.length - 1]; + const rf = ref(key, p); + if (rf && rf[0] === "D" && rf[1]) { + const srow = sm.rows[rf[1] - 1]; + if (key === "ILBAN" || key === "IYUN" || key === "VAT") { + const e = pctNote(r[nc], `${S}!M${rf[1]}`); + if (e) setF(r, nc, e); + } else if (srow && srow.E != null && srow.E !== "") setF(r, nc, `=${S}!E${rf[1]}`); + } + // 바로가기 — 당초(첫) 줄 + const r0 = ref(key, ps[0]); + if (lc && r0 && r0[1]) { + r[lc] = "총괄표 →"; + r._links = Object.assign(r._links || {}, { [lc]: `${S}!${r0[0]}${r0[1]}` }); + setF(r, lc, L.jump(sm, r0[0], r0[1], "총괄표 →")); + } + }); + } + + function linkCost(by, ini, L) { + const sm = by.총괄내역서; + if (!sm || !sm._kan) return; + linkSummaryCost(sm); + if (by.공사원가계산서) linkWon(by.공사원가계산서, sm, ini, L); + } + + return { kanInfo, linkCost, DTP }; +})(); + +if (typeof window !== "undefined") window.STMATE_REPORT_COST_FMLA = StmateReportCostFmla; +if (typeof module !== "undefined" && module.exports) module.exports = StmateReportCostFmla; diff --git a/tools/stmate-test/report_link.js b/tools/stmate-test/report_link.js index 7e934f0a5..58f48461b 100644 --- a/tools/stmate-test/report_link.js +++ b/tools/stmate-test/report_link.js @@ -515,6 +515,9 @@ const StmateReportLink = (() => { linkComposition(by, by.중기사용료, ini); linkDesign(by, ini); linkSummary(by, ini); + // 총괄 원가 줄 · 공사원가계산서 수식(F5-31) + const CF = globalThis.STMATE_REPORT_COST_FMLA || require("./report_cost_fmla.js"); + CF.linkCost(by, ini, { q, jump, linkCol }); return sheets; } diff --git a/tools/stmate-test/report_xlsx.py b/tools/stmate-test/report_xlsx.py index 24b3337c3..3b89dc278 100644 --- a/tools/stmate-test/report_xlsx.py +++ b/tools/stmate-test/report_xlsx.py @@ -105,7 +105,11 @@ def write_sheet(ws, sheet: dict, formulas: bool = False, prn: dict | None = None c_idx = column_index_from_string(col) max_col = max(max_col, c_idx) fml = fmls.get(col) - out = fml.replace("{r}", str(r_idx)) if isinstance(fml, str) else val + out = ( + fml.replace("{r}", str(r_idx)) + if isinstance(fml, str) + else (fml if isinstance(fml, (int, float)) and not isinstance(fml, bool) else val) + ) # 수 _f = 숨은 보정·요율 칸(총괄 원가 줄 I~P) cell = ws.cell(row=r_idx, column=c_idx, value=out) bold = bolds.get(col, kind in BOLD_KINDS or kind == "title") size = sizes.get(col) or (16 if kind == "title" else font_size) diff --git a/tools/stmate-test/scr_report.js b/tools/stmate-test/scr_report.js index 87fdeeb9e..d1b05c5c6 100644 --- a/tools/stmate-test/scr_report.js +++ b/tools/stmate-test/scr_report.js @@ -16,6 +16,7 @@ [ "report_core.js", "report_unit.js", + "report_cost_fmla.js", "report_boq.js", "report_coll.js", "report_san.js", diff --git a/tools/stmate-test/server.py b/tools/stmate-test/server.py index d8bee2d28..6d7500f98 100644 --- a/tools/stmate-test/server.py +++ b/tools/stmate-test/server.py @@ -39,6 +39,15 @@ app = FastAPI(title="STMATE w 8.0 Full DBF/SMT Engine") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) +@app.middleware("http") +async def _no_stale_cache(request, call_next): + """화면 파일(js·html·css)은 매번 새 판인지 확인 — 고친 뒤 옛 파일이 섞여 도는 것을 막음(ETag 로 바뀐 것만 받음).""" + res = await call_next(request) + if not request.url.path.startswith("/api/"): + res.headers["Cache-Control"] = "no-cache" + return res + + def fnum(val: object, default: float = 0.0) -> float: try: s = str(val or "").strip().replace(",", "")