From 252fbf7fee89441cabda91eea3414d39914ba0af Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 17:45:01 +0900 Subject: [PATCH 01/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC721):=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EC=9D=B4=EB=A6=84=20=EB=8C=80=20=ED=92=88?= =?UTF-8?q?=EC=85=88=20=EC=9B=90=EB=AC=B8=20=EC=A0=9C=EB=AA=A9=20=EC=A1=B0?= =?UTF-8?q?=EC=82=AC=20=EB=AA=A9=EB=A1=9D=20=C2=B7=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .../요구_1104/_목록_721_기초이름_원문제목.xlsx | Bin 0 -> 78380 bytes docs/raw/요구_1104/사용자요구_1104.md | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 docs/raw/요구_1104/_목록_721_기초이름_원문제목.xlsx diff --git a/docs/raw/요구_1104/_목록_721_기초이름_원문제목.xlsx b/docs/raw/요구_1104/_목록_721_기초이름_원문제목.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..502fdd4a70fadacadb921d5acbdf7d2c2d7d8d6a GIT binary patch literal 78380 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%0AC+^T5cyFd@_U9dU6h38#GY$U+n_dTC;4ZWb*8g6 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

WESOUYv3CL#Q+2>%W-wuGpkiG`;&}Y6&(nOcJ@2 zZ+lEcb6)6iK(w%P7*;j2e)DrZAZN3gPN=Yv+Oy&V;p1ON%Bnm#{7hjK+))Wyk^rP!mE(;9>c0T#M1s+|?A1(&800J9&t~xZ<5O-_xnc+M}4P*W^;m?P&_QZmF+dX8MK$ksG zD5u)r_Xmg}t3K3#((My0BG$4Rzr{aJy9~cg%qZkARD1iSB@h~2@N-nrYsLkweWYtq z!|`q|D6u}=p!0R>6MwaCrA-Q~(t#9;bL$XZ1L-F+}Z;|Ki<+Lw$nj>YT))m{$ zNIgzz5_L(vU1LCNIXEr4acsbU{Tu%FsefvS-y((D}vFA~tw~h|o)cb8lmYE+A{y`*_lZqh!97?Hgw}S> zh{NwnRF-j3EwQ2dVEV*YehwK$r$K0xwF7&gDTuk{XwjMgpfPF_wzyhZtC13#* z7eH~~0kTDS%gZil1)nB38pcVTn$L!WWNvS#%iW%w!-ed;Ces=tt_(4-ooXT`l@gT` zy^VOa$CB1zfO;T$FEhaZn3jLTqEWCHD~-?Lw6GzTge-$upkJrxcYbP_Hyb ztK)P7jr*k+PYJ2I1inpsLEY{}@?;&KyLdt4sDC_nBo>LI?Ckh8=+gMk1uCi5l_^vM zbZ%ydnU)I<({oO|FoY~tin<*#Piz-*Ek)yo5WkNqQf20PqwR0O;3+%VYCgyHZH#99 zI(H!tj-wW5_OHjc9YkI%FisaXR@`D4TH<%V&QAt%M^r;j`lEx9Vu!;(3aXqG+}x+N zd1CzsRTAPZSYPBzh8Km7r*j88Y3N*xm8eXQ~Xk&oDRzM|OYU$%~VOk!k<>-GnDUBuhafY3p+R z{FZ5!ctcFoRq-b!J+@Apo1KR8GhvWv|NhQE^T&c6!}eUtZOT1hJ$O@P(COm1slr@%L4--E#hy@ z8-j+eFziUiX?W6%C`|Z3#zH%v-8-W(!7E!o<^ZHgis$eF-x z?)w_AZACAe0oGn8g9ED>GsHjO)}f-4w6Gt;kOXdreV?61B&xF;A$2YC&ImV)AI!MbQ5|KIa& z?{&qQc6EgWo#{Y%>9OMdA#(VeruaU`&UT23xE*#4{1+%cAwDkK2U@N069C zi8q7DZKdpN4S82$1Y`&po>6a43BKT-5gM#F>PlLA8!4;%BfIj}ZNyXH(;+@^HG$Misjp3CPD z3vh=kPqtFmZTVL}?{_0`sj3O#tjjy3D*d`-1g^<|EGYz#HOGi+rbz!2Nm`6`7_++0 zpc=+CQm2r7Eev2a^w)|W=g^URcTjtZmg0dP5b`5e&&(1sny|jt@Z8WQmIPtdG?VAy3wI zb$+x$u8q0_w0weSTMyVzod;h^|ut zluBJ%!-Z8pQ5wwpe_U8C+;=Nl#w76&m*Yrj{t+WE!QraKu_Yci_tI_uvit&N=@cKy zT>{fDXJq{GD)A>P+6hXs?Z63|t?WV}nTxgy)96}=>T9R8%ZsZ(+cwFYZj(Seky{|5 zrGBtwSLSdDq$AF(Ltf8KaT%r4}`U zm!)@|i2qE6#DZ`?AT!%6j|v*ge1Gfg#^aEE(I_6hJj)s7zcg|BAZ1TQaDU>)+Pcqk zq9`zj%PiZK?X3yI2M0fvYtJw+TNCx_sS)S{P{<39CB-2?9v$~En3Oh659{+!e#UM9 zG)KH7p<5RdN4&{=Z$#f}D2lciZ~S5K8M=g36)L>*heVqDj!01bCFoa!BT(KfU3$K> zYZD)Hzh4?KGAQ&mpp`PHF798<9?5{5(0S_0Rj}VG2IN+suzpG~sTzz3a>pRtt!W z4Sq#95Ii>ro=~O>e-6KnT+11Icwburx3V8am{4HFeDQ1D zj2(_ZSBrWVs#@ii-Wl;gW{%tikQh5OE)lRiUi zCFF1wezylsWg82oXGM-BBeTRm^{e@Nry}_hz;D*-sXcejk>Q3l#wMON12eX9il>wC za)8&MvXN-4T&Y|NO?4-Vm>yv=y(3|?%uZ((Q^y_RU4j3~p7UOY7#hq|2Z!I4hDQqi z+AWtCdRq265GCwqb7QGhzW!oqT}uEBkI?GLOg?=TV)5FO5xJmzTICBh_W1m2D5e&AGp9V<}1*1 z)Qn^wikx6-WpecR9!A=9UkWh}fAp71XWSh5He8qM2{0~p5{c_z5zbD*Km}44ah>OQ zj6-wY%=WIYOtKK1usUM-<1-`o%#^U46(`(&tR>h z6rT60D`#p)YP2xI@(aIxehf2kb{2(qF!|DcF9ROQJlXFZik49U$ZR)GH*cl3wI=q z_Sswcv*r1IQnWxN+mJW&-4_TQ=ye#=&E;_Lf&2C5a4;%G{^D@r`l$BiJ5ucWx6U;) zPs3sDarIm1*=#V%WdG*iIQLph>!Kpj}HRT0Dtv$5%6(; z?(7Azv-9>6`12vm@9XLkpQhnHuR;=X^D&fzE<%Z4trJ~MVXXi{gcXXZH4bll|0=0K zy{3AdtSrWY^Ch}ObT&zJ7Iz4fM^YI5bbNe?>`CcKndME?o<4=LXnT#or!M=h^ttqv zwE3K1dC0x+UW{2sY|3Jo=a04u>=D9sv)%`_5g9I}d#IX-xzAv|u(ZS~Gv0NniW*11 zna14`eZFDm=1ZqJ^TCA0N6mpGRc$pJ@*ObxT1&k=VapcRT2Y{bX*|N`jJ5diS1A>_ zwZVfKg9b~0GFgikXjsdM?E4X0M-n@ry7#S&;n`3~K*PGfKZg`(cAqUwAo44Z-O{=D-|2wCZK z5%TVaIwQW{R&>7>=AHb~i6xR5;F&V&K6MjcTvhsvjsg;QqwFX}=zhhtWj$U_p4Ex_Mk4mmYdbQKj*DawQ~Cf;Kl0?U`31{mJH#L*bzsanhcU=qP4h1D_oYuy~td&AbDCQRYiFE!%Tat z(edKlh19T16hYDy&wY`%Pj~FKM@LIXFDb9sI7q!HG(V}}jyv@D8I1B3E%X@$T?KpG z$B0b6!(XyoKydjoR!Cu09Cd(`SJ=I^XFyv2jx7dOOkvt4)3?iym?rX*CxNNYy-UNl zp%^{EdD05rtutdf+y24VqaL9Wq>vL8vZr!#D6pJ6E{42pyUWUlX}>tb=1AZ$hW@b) zYr@=Z&`qLIF>+K4kEK>4sX-3SE}q$w-eNbri0)He|MU(gx_7NASb2RT#KlFU`a=`Z zVv?jBk2Pnd^?TCiWulr4DLa%4KH@TF}wANqDKczI;c}&zSL%~Dh34Dm+`VKtE!?R6ItG#`8|R&^p)?IerT8G8 zg?1WW?wK74o-O!(wceLpKgMwB-t&{lKJd}IcvN+L=RZUJ6V{=MJ~{}b3WPf0U!m^h z?dM|WbsOJ{#`7`rf+SazYs6(K?=o6g{mSm^LPq-bl)xLN+L&!E?>ktpE}U4uH)XEr zUl?aDr%OtSI1~wuSNL!sa*4Cr*UO6bX19A=_YBfb+c8Y4ZmgwhWIxeXzB{g2bO>^g zboOWFO6Rk0U^dX)@^1JP_Kka%Ls|sVtnJ{K>}p&+H9IL2kJg>WmD3^Is?Ec$?5b=r z`X;HA1|0rj{IMQU9VMG38MCWj&+R3qW4`{Z}H1h}aj30#aByb{bi zPqkSiwgOz>VC`&^`sQ6xaG<6;&C1Hi=OM9$rA9iX%+&R2mPpFqN`hJ2TY7ZgnSNTrY%>SqZ$5>~mL^P;m^{c38r#x^&3g->y1)M%sPxRm zK_(GhPxMM$Kl9^B=#_7B$Fy+5Ve{D#m~ z8!^fft-E4{kFF@7HRQ*MQ}9|mjiKL@(?{{~l{IH3QXl<1vkkzSTaiqD2JtnEv%Q)f zDrCjJQ$YXkpOG$?FrNAk;{g@m>P)IEJ9ii{X6k81Q92&i^M+4*J zm}f~6c6ivJFU&+T6cT!@=${MpFsfuqy&~~foaN9`*}@@@drzpazLkZ#c~~8=eLQ}T zNdi+)$x(V`^%W2IeRM3UI(Xz$f^* zor{+M|LuErvW6SF5P?E1nH^ZOd;ndj>T8|St2-^x-wV?RUfT|woW+0bxd$QDxXH=k zt0flAdr9v#U+gj@IAAen{Gb~2-kHlf$Fsnc`+mtqAmv3t2au&Oaj*D$=;J(PMo8!qc3RzTlwqSAer&(n@cK13I)_-28s;(rb z4;lyr11^;KuPD2nP;K1Zo&U_Eda@cA4@s_gMy^2uc(j7+E(A^rhH*v6g|Usu-QoIn zHPO_F+d~W*lA32~TJ(60N%)*)p_9e`-m972j`X18CCz|}6G-0YS6TS{S{hNJjbhGN zuxNdf(W-KSqDnUPJNA|pQ80C~xV*xGdXQt7O7#~4Q4f1PwGSweT#Qi_^J`6o$MV$ssLSN_#N&-)tS464DVNv&sHS_U z>HeeB!C0@c#TE*u#qb!{MMaaz#g;6ijj?^Vqxr;-=R-_Ly5i5NT{L8GNSGv;*Y3EP zzm`rVjz}81PzjoLwHrk>^%HG$*&H^n%rrK2f&O!hDy^RL!-27h2gZo@uQB?|AOG)2 z{qfDgWG(zKK@yc(vgaUG*X@F`O1A|n0TS=a-huGZH#?iZ`CB9MT72Xs32Mk*o@Kz>p%((st7e-qBwx z!e1<={;Jy;S?&S%95i*6QoSzbeB+*EfLFLQggHWrCGsSuL%n#S@-hCeX1zecm7%1AjxIwhgt5I+b|3#|9p#l9B2<~=1>gy$W7zo+=a{x&(N&m zo@X@W{_d#n@X^sJ30lMWlf($ftJN@lQ?X^yk0iZb#Y70lRV3URsUQlcq$fGKq*v*J@K@P55&oS3ds6xM%FZI|Y-kprnT9 ztl5^Y*%^i7knIR;?2OnLg2f_|*9ICM?Mp&wUDAn~n9-xDN;$0Q0Y`ZGIvt@q&8spi z=Sko0lN{Re{eCw7UPR zZR`Hr?!T$0TV%12V1qze9Dfbx-xfm9J^}^-^zIF@bG7^HUFg3VcxAY>b&nnmqzJ%( zTfqPK(FD7L`(NSzf3@=Ovf&>qPg&9a-y-7Q_u}7(XqudVto(yd{;m4&`Q#r} z1UK6Mon8K|`0xDukK%it|2^}+v-Q7~|D8DgQ8woLqx}D*k$>CycToOgN0A@xzlEr- V1{N^rAP_$A6AompWPw{}`yXe$x%>bC literal 0 HcmV?d00001 diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 45f5e65a1..7beb0d39f 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -601,3 +601,10 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 720 (11-09 · 사용자 · 수정을 어떻게 요청했는지 물음) 뭐지 어떻게 수정을 요청한거야? + +## 요구 721 (11-09 · 사용자 · 작업 이어서 · 기계 · 옵션 때문에 품셈 원문 제목과 다르게 표현된 단가산출 기초 조사 — 200 개쯤 예상 · 1,300 → 1,500 늘어난 까닭) + +같은 기종인데 규격만 다른 경우은 지금의 수정으로 가능한거 아니야? + +1. 작업은 이어서 진행, 문제는 없어보임. +2. 우리의 단가산출은 기계나 특정 옵션에 의해 품셈 원문 제목과는 다르게 표현된 항목들이 거기에 해당 될것 같은데. 그것때문에 제대로 연결이 안되는게 아닐까? 어떤게 있는지 먼저 조사가 필요해봐. 그렇다 하더라도 품셈원문 제목에 품셈원문 제목이 있다면 모를까 하지만 제목에 기계가 들어가는 경우는 드물것같아. . 내가 예상하기로는 200개 정도 될것같은데 확인해봐. 이전에는 1300개정도에서 갑자기 1500개로 단가산출 기초가 늘었거든. From f60c593686dea455206cfeb194bb4182b64479e5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:00:26 +0900 Subject: [PATCH 02/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC722):=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EC=9D=B4=EB=A6=84=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EB=8B=A4=EC=84=AF=20=E2=80=94=20=EB=8C=80?= =?UTF-8?q?=EB=8B=B5=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 7beb0d39f..598653f95 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -608,3 +608,12 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 1. 작업은 이어서 진행, 문제는 없어보임. 2. 우리의 단가산출은 기계나 특정 옵션에 의해 품셈 원문 제목과는 다르게 표현된 항목들이 거기에 해당 될것 같은데. 그것때문에 제대로 연결이 안되는게 아닐까? 어떤게 있는지 먼저 조사가 필요해봐. 그렇다 하더라도 품셈원문 제목에 품셈원문 제목이 있다면 모를까 하지만 제목에 기계가 들어가는 경우는 드물것같아. . 내가 예상하기로는 200개 정도 될것같은데 확인해봐. 이전에는 1300개정도에서 갑자기 1500개로 단가산출 기초가 늘었거든. + +## 요구 722 (11-09 · 사용자 · 기초 이름 정리 방향 다섯 — 수정 말고 대답만 · 방향성부터) + +1. 제목 + 옵션, 조건 덧말은 조심해야 원문제목과 대조할 필요가 있어보임 +2. 제목과 다르지만 분기가 필요해 셋으로 나눈건 괜찮음. +3. 보조문서는 기계와 연계된 정보이니 단가산출이 아닌걸로 봐야하는게 아닌지. 자동으로 연결될 것같은데? +4. 기계 이름이 품셈 제목에 없는데붙는 경우는 수정해야 될것같음. +5. 위에처럼 정리하고 나면 이름으로 구분안되는 정보들이 나올텐데 이때는 삭제하는게 맞겠지? +수정하지말고 대답해줘. 방향성 부터 봐야지. From d25894a47362045e6fe84bf7fac8633d2c689e43 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:08:30 +0900 Subject: [PATCH 03/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC723):=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EC=9D=B4=EB=A6=84=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EB=8F=99=EC=9D=98=20=C2=B7=20=EC=9B=90?= =?UTF-8?q?=EB=AC=B8=20=ED=8C=A8=EB=84=90=20=EA=B2=BD=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=ED=81=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 598653f95..5a82025ef 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -617,3 +617,15 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 4. 기계 이름이 품셈 제목에 없는데붙는 경우는 수정해야 될것같음. 5. 위에처럼 정리하고 나면 이름으로 구분안되는 정보들이 나올텐데 이때는 삭제하는게 맞겠지? 수정하지말고 대답해줘. 방향성 부터 봐야지. + +## 요구 723 (11-09 · 사용자 · 방향 다섯 동의(3 은 둘 자리 협의) · 원문 패널 md 경로 삭제 · [주] 글자 크기 반대로 · 반영 · 다시 조사 · 작업 이어서 — sub3 과 충돌 없으면 다른 워크트리에) + +1. 동의 +2. 동의 +3. 동의는 하나, 단가산출 기초 리스트에서 빼서 어디에 둘건지는 협의 필요 +4. 동의 +5. 동의 +추가로 +6. 지금 단가산출의 우측 공통 원문 패널보기 내용 중에 md파일의 경로를 알려주고 있음. 이부분은 다른 워크트리에게 경로를 볼필요없으니 삭제해달라고 해줘. +7. 원문 패널의 내용 중 [주] 내용의 글자가 더크고 본문의 내용이 작음. 반대가 되는게 맞음. +위 내용을 반영하고 다시 조사하고 작업을 이어서 하는데 너가 판단해보고 현재 작업중인 서브3과 충돌이 없다면 다른 워크트리에게 작업을 시키자. From 852aedf849433b11e58d30862653306bbabe2ee4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:10:12 +0900 Subject: [PATCH 04/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC724):=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0=20=EB=AC=B8=EC=84=9C=20=EB=91=98=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=98=EA=B2=AC=20=EB=AC=BC=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 5a82025ef..6ddac12e1 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -629,3 +629,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 6. 지금 단가산출의 우측 공통 원문 패널보기 내용 중에 md파일의 경로를 알려주고 있음. 이부분은 다른 워크트리에게 경로를 볼필요없으니 삭제해달라고 해줘. 7. 원문 패널의 내용 중 [주] 내용의 글자가 더크고 본문의 내용이 작음. 반대가 되는게 맞음. 위 내용을 반영하고 다시 조사하고 작업을 이어서 하는데 너가 판단해보고 현재 작업중인 서브3과 충돌이 없다면 다른 워크트리에게 작업을 시키자. + +## 요구 724 (11-09 · 사용자 · 보조 문서를 단가산출 목록에서 빼면 어디에 둘지 의견 물음) + +단가산출 목록에서 빼면 어디다 두는게 좋을것같아? From f072aab2487240dd135dde54f50072a3bfd7cb0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:18:09 +0900 Subject: [PATCH 05/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC725):=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0=20=EB=AC=B8=EC=84=9C=20=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=20=EC=9A=94=EC=B2=AD=20=C2=B7=20=EC=A0=95?= =?UTF-8?q?=ED=95=A0=20=EA=B2=83=20=EC=84=A4=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 6ddac12e1..94057b00a 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -633,3 +633,8 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 724 (11-09 · 사용자 · 보조 문서를 단가산출 목록에서 빼면 어디에 둘지 의견 물음) 단가산출 목록에서 빼면 어디다 두는게 좋을것같아? + +## 요구 725 (11-09 · 사용자 · 보조 문서 자리 = 권장으로 해도 될 듯 · 조사해 맞는지 먼저 보고 판단 · 「정할 것」 설명 필요) + +1. 권장으로 해도 될것같음. 하지만 조사해보고 맞는 먼저 보고 판단하자. + 2. 정할것에 대한 설명이 필요. From e6cbe3ddaca63b379ec4a967d74ab20dd792e3a5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:24:43 +0900 Subject: [PATCH 06/29] =?UTF-8?q?knowledge(813-1):=20=EA=B8=B0=EC=B4=88=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=A0=95=EB=A6=AC=20=EC=A1=B0=EC=82=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=E2=80=94=201,519=20=EB=8C=80=20=EC=9B=90?= =?UTF-8?q?=EB=AC=B8=20=EC=A0=88=20=EC=A0=9C=EB=AA=A9=20=C2=B7=20=EB=8D=A7?= =?UTF-8?q?=EB=A7=90=20=EA=B0=80=EB=A6=84(=EC=9C=97=EC=A0=88=20157=20?= =?UTF-8?q?=C2=B7=20=EC=9B=90=EB=AC=B8=20268=20=C2=B7=20=EC=A7=80=EC=9D=80?= =?UTF-8?q?=20=EB=A7=90=2019=20=C2=B7=20=EA=B0=99=EC=9D=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=EB=A7=8C=206)=20=C2=B7=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=97=90=20=EA=B8=B0=EA=B3=84=2045=20=C2=B7=20=EC=83=88=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=95=88=2062=20=C2=B7=20=EA=B2=B9?= =?UTF-8?q?=EC=B9=A8=200=20=C2=B7=20=ED=95=A9=EC=B9=A8=200=20=C2=B7=20?= =?UTF-8?q?=EB=B6=80=EB=A5=B4=EB=8A=94=20=EA=B3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../raw/요구_1104/_목록_813-1_기초이름정리.xlsx | Bin 0 -> 67163 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/raw/요구_1104/_목록_813-1_기초이름정리.xlsx diff --git a/docs/raw/요구_1104/_목록_813-1_기초이름정리.xlsx b/docs/raw/요구_1104/_목록_813-1_기초이름정리.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ee4b7dc2d661dd93f624392430cbc250cf06abaa GIT binary patch literal 67163 zcmZ^JV{m4{*6thIb|$uMXX0csv2EM7ZEKQDY}>YNCvP$_ZqE7s-0xQ1AH8>V)$V6K zy?R$KD$0O?qXPf{XaH)Gg^q+~AAI2VtnPate-C3jBSl9$dnZN%dwT|V8*8~(IkoE8a$E~_LZNAoD6}=RL9$xB5iorWS=Q;ScY@0%o)i2|_I>KJ%9fDyZ09sf zW~%2jtNYlHl}@5&yB7n9o}^a@#s9^Zd@LpQ>YL{T2mpZcfAcl6b2Rx6;gp0v%K;|% zP`#4F+);Un{NaBkMr8<)wAPoV)=lhDXhv3KK5yh2S+3S;1XW)PlrS=A!Dl1_BMkav zVHWJqrE>kja;{{spu`!Zo;i^T<|Fny;x!iD!(iYx0uLYs#(yE>imNCPl9-U9n0aZd zG?MpNIU0I#dm;Ioll`&qtLY3L&SpZnHAa85g`B^2pH^nzaqizfK0LE9=5=r+*dmxp zYm&-}$?5!F5Z_~CL$!?r^1XHVyRyq8JHxh)F^pL?b?xvQR~Vb*IOR6{i>(s(pj7`5 zX!Cv03+ZmKz4``JX5A zf3$O@spYiJh340P9DW87HoXkpymRbwGT5oW?EtGVrkEs@o2 z(zE~!aQTd(p$=>YMnTKCqfabr?`q*Pk}jV&ZfvOrTUZxhOB(C@=+c=B&Hh%>w9_MQ zJR)L!#+djWV^RcyrY20$7TySau^xI~-xe^LIpZN<39x0tQz!PDwktc7*9_nL-9OlC zX(p&@H8MB<(?4*`CmEt$>al161DXYamnTPk+SU7U@G38QeBGJN26y-1&Yi1yVm`_q zAADD-Bey^2!Mdkd7^bnvV%B)|alJaBv?4}XVd2SV=OW8I8#YEuVZq96YaupbxN6oN zq5DoDY@j+~xN_F{!h(YGquQ$Y4AWdh;^4v)6{G1$Xn`1@ZtuE0IS-bqOa1DSjJrnA zu_dg1tA5QLf$}*#1ioxBkCVzK;g}N-fe7FnoRd}bt`TmI-0e@C*N$JFp>9&dh0Yue z_$L|oV`#JH<6m0GSTU<-jt|eYRkEK&B}my-N7hWzyVL1mURlzLoXE6&Cmqb)8>yB- z7^N*Qnj;UsnTYqy<6}YzIm^t=3+#nFpBa&AYOT)9%wcY{BWcIhqq@(df}}0!N_lSs z$x5;WnRwnPefuE_SCbS13z{HwPH4KNB0gv_40CEh=yT)nUzA-mpO$Xu(Vdv^r%Jm_ zezgL1srBGzWC`5;Y^P(VIqNs55{bD-mCntgG<6J+H0&L=3U<1oW%rccyQ*wH0yNaA zWH?CS&@?g%)N*cLxu0mCv?c4G*$YCq$&6&L(g|@E4Wwnx6jP_HxP1~ecrIb)^L(D4 z5HH#6u=ojh1fXVk4H7?sEjn%qYsg|VtNKxBMFb3SIB*@CFaw$$%7gVaQHST;<>wnw z-ZlQRCkg0}UQQ*rOfM%Oq9p2aet$b16_3(G7|JD`c~$734*rp=J0R6}8aQG4(drwz z#A;0!`y{!DC4GoQQ6_lC!Uk28sR7d!tZJE85ymF)hmZvp;;xNQ;K8VowL94Ik1=8kHKfAr zJliwnf6^%{M>TC!t$bRCXEg=R0(3Jke0nF1Q*f-JEsiqh&B@(O6kO5|b(8k;gcI<* zW_*bR&2ISw>~D`cu7{*0Ov-7Zt+*oHU^eYJn><{(=?QJH3%}y+242pF+;R80*TCC+ z($#EER_Es4PWqH;i*EQ*2}Z(!g+HzfLepmkM8iD1^wP~zh85Y+ZLI&2(Mh!Ex31jL zq3;^&)8|74)hK}if2rs*YYl_Z#us|aMvI@?pQLH)9n2*jY?PWb+160I;nDG&-!`f1Qz4j9(E`?XTkV#wwLAyvI@QKB1KwRN?9o zXKhAJ!|psd9&wOYZbfx%MgnDN>)0B+HWMx--lDNL6 zx%KG-KkUaOk&bvz^d59Xt9>804qk1Ocz?=fdkmp~IE#alrr<+jVzC@5UMI#wqJc~$ zkBQxa1{#;*`=`Icbpheqs4=Fi1i1ag>4HM#b?3=Z(Iac&A%0*v9^Bj{S-M59ywU~O z1pj6+Ucwp|jp*ZNbrfCwO@3d!P5~FPvHyFj1Gk$(UuMA5Hiw?_YeRRDzY&XH;=U+j zm#`tsDa*69U;OWv`ZD1Q%MQ2w2G2H_?lBsjXt8gfPh_M&N~K|UdyT>ic{XpO*IfW{ z*8B2C@#EGmy!T?IW{EK^il7XXH&G&_QMA?rn_e8cVO42e)$lqqyobN~J$!*F*G`yt zCNAqz)L-rxSUce*p(DYKMFjIl1UCEx2wmokW{#pmd04*9&l`l={=wM2tEe83|7}2i z;$)GYe;bg9Z~p-Me+-D5ouieLxrvFh6XSnh|Ft4H>N*M=s@R{jk_i;tSUE-`FZm=1 z%km2Xja4-3t{QFn#FnV6BgxkcLXj*?R;&8OHX;KOcz@E-p=}0IZRUNYy-u8fM31>f zBOHBDtG0lP6lP6Y1!;E|EAGz(V{e@}a;?T8a9D%WZm6-s+jG7j2~ull*z_f_&Vaty$o! z2nC6FD5vd-si~Z+=w$3nIqQ0`sV_Seq&>Cgh3pZmW?k|^)aYy}YXVa71?PS&Ocu~Z zUW*E1INZ}oZRKVzN}-e`0h_$!s7WfRx}bl@=c+VyW}(D?yydzAg(yW6a%A90&=3UR z>$a5WsyySDl*MAkn&1pBXR&bZz1>;G=>4}%o_sYh$+LUVcP@}sLy5%PvD^3@Vg=V9 zuC4ICMgUuaR6ilyfzEhEgs@4`9n)cj6JXj&EkkSnreaDpw!Af|(AJ?k;G3A56RM0CoA& zfdC97^|o2bjtxn78>F5;!NSzSRN}B<((Z~86F%0~NvaqL$1qULC_>4o<@STcVd;lI zTop!bq2}CGMH*7YDwh}kKU zK3PbS!C3d%r!Ah#^X_YaXb~l%Xeg193{_H!0ikF28uU$WN8o%Gd2a>d&xAQuW&W`} z+ixJdRt>_myo-~ zjsSgFGkzWI?EM>c#scBpY12#wGXXtRv{_CwJLH0+^E5dKOZoN*6NuqFId z%fz1HctB`0}^>sM$gc|?-%Gyb*;lQ`9NeDP;+h51|FNw7GJWK)3o~|tz zCF^ES70*g0R9pihb<1-bKnfCC_EGRj5o4mj@>KBaVlE42y2;phs0P_HeUK|Ms;vA5n1aQz{cHa2Nf+ zlIxCm+9EaNw_rl?V%B%)P--mkXjrui{2KpPCWzY?k1#Ni-df5rJ(Dn|4WQ#yRjM#T|&`pQr9^SnVS1c5Xi z{JUQ=P#|(=ez5DsPz=ie4K~9T`MRt5_)~;w@<-h4(HBWKI$tBqQHQ6{F=&H#wF1U0 zQ|cz!O64OByWIv!ezv0?W8S@+wnh&d_p3K1j<7Xg_Etb;H#Z~z_E09VMp$!nMzPyR zSSfLlpB$qkP?YDbai@@apu_8Vtw2a#3mOrA)rAheS34F#^-)Y8!CP1vF@L#m5koC% zOckPro83(t1RQAQQ|k^5Qk3O*8^pf)H+}()O`TyONRTxVS$!u9?BXqtHWW=?uU8Y9 zsl7UnxjNn!P0Qz|l|3CZ4 z@}DT{u*)V%Yb1f;IS1t(iPG8V%5;+=#4X>zii9qbX_A+xiIDQ zZW7X{Xbh5D&Q<2J8J8qeM0xkmn5ox>K`l(;Zg@1_-kZW@_!e|_&}vWZKSuz%|6k8~ zL?I0)HwlU?ghMBq24H!Gaz@I(fgSD7XClFuu%jc|K0&=s`?Dms?bk3`H36EQk4H+P z&jH|&Q(kua`y7z`Zfr;I^Lf9Gc0Z5k>-2+q#zS9$*i)=W;%!?NCo z;>Yzz+}C+nnxD^6|BKATjUT=@@FEIp#^;^!hC#3OJc zrqe~!+x2>Y_TzUDE)HSIH*V;6ySYAXI*eh<|V% zr?pNP$Le;2Xo;c}X5iXr{g!&Zf3B7qXyTWd7QyI{Z5KH4j+|Sin0Vf+)QcuH^R}nn z`|eG6qz>UlO;Y0-CX=ka8${?kSY*5H5~4Sw7VYOV-_{+%+Y8IBn;ZR=q17ok?Wz(WCUioA0y)wp%Hy~4AU>1j4DMEg?~W#%ho4 zSLSx&u|gMKWmGq%c!iKM3sDWCjMI*t?h!!47UM;O|U?@%z@ntwC8&JIQDA4${|b%(Y-NHx-%Jl^nyA`B0g8D$zV0 zl11t`x(xc%X5wBB0kd)N71-Xe($ktgP%dqWD{gg`8N%(8J;MObB<(l%e z$xdJAs^l7N_=6$bvB3mJOQeA9Bv&Vh|Zq^vCqsgHHRxF%p?ZWNXZ5J7y9F55^r7CgH zdOQrzdacdlF^%yg5OjsS$h?$i+siPSjykN+ftW6Pbz50d`8EfO(_!*hJY}cb4qK@< z2DBPGL!V^(y1me_&r8g*4{jMo4j~y-Iu1VB5jSfAs-t;ti!i}^;c*c2Sq)sP@ualS z+|I!}V<5y5J@+yJp$r@+jkA7B>r}v4355SXBRwDd(32LDq9@xi7?*Y$QlL)SaHivy zvn9HVltXq}rp*pSq3eyyUvdD&gATEslddCsKyCU23=!*{&k9tEt1c3F*pp|z{!^2` z+*=I_yR6eYr{rsm7F)ZYsB?(9-D1T|ZKjYy0O3_?89$F?4W*(MDjn5(%<0}i_iAz0 zTVQMU7FH#GEw0BhDBsm{QMxHE6nn^la@Nods8Zn!A0`nr{%eP1?ZT~_<4Zv8^97P$ zl1FPBXS0dW(~yHlJ0s@CLWaT21J!lLaDc+z8N~d8a5|EEyHx>bd_k61um}QY85Rv7 zszdRPBQ-3}%Sg+66Kmw?pj+LV$Sjm=p$OXy7m8asgPcViB3xdgM&bf{BojhNS zDN;3%avsfVJuh^cc9D=UH~;R;XBqr1)cA=H?0s(^p}ig|_eAl9{o>40Tu$JPzP_a1 z_2Ri92ri%N_85~y;r(P`g%eY82;}1Wi!3C(*|I2%zJ!uRHM<1r5-@k2q_W6#ElKJ1 zOcEROsmG+?){pEyDxiv!8VAB!e@^%<>m@ge>ii7@; z2`D@$wh@S0hjLW@>AAOprymP3s@+Zj1G{lLu9jL%YcB2BUf9|ewpFQJi$cy`and>N z4LcUSNgc*WAXw^`BnF(CESz+A^+cbQXC+x*gU?6@5gUPN^Q5=ryC&$mzOkkaY34Nk z&IxpVz9t2@c+Maa;6orCII>CQZwxDSf09WGD|9RcRQ~rBL%v%x`=c=?` z2|+dhEPMC)*wq)X4$>B9usxR`z<(?x=}B<2YU*qZEx&F=`W5#obit(7a_R4pGxS z{-=mNIKdu7YG*bw9RimXNV@BwQlw~T!oFf7{6AZzI#na?{pdD$q3xsa)C0*s;-e@U zrUu+3P!Y~zvH24y+&~(mt>f@X_~1z@upa}XLR6-sWURzAJ0!N&qo+qjqIXWc1V79s z1pq*Xk;dYRBFrBnvc?ptMHIpm1S@b%5ljvlTo&xn_JDnVQ zW!b)Uj=&Nv7GxMY2@6(OH#Bg8paCjuI#6LCll~>Ydpm(MLscZf{gEgO2Nv%8s^TtT z)JFZi9p&%s$Z-SZIZZ;BS&L>pLs2vtO;6WFi536SGswVue{fyej{arCSl#Woj5do` z0?T2LsY;zD9vo~}bssivWBkU@`^ND3#&|M;%wR1db_zu?W-uL{L?-5Wggw5U!%@Hj z^LV1g=HCj!EI7+G!AvD}jQ=Tl2;GjKKzid1VFanhTC{5!ilR+#I%*9?eC`e5#C04! z5**O^h7s*5J4238V7GO+As)5!n!t{{2t!_Pf47a zhT)_;CeCPwhDc2$$UeMGwAio-3Ka~C6fq+)5$N}_6ro9ELsxW>NfwdtAvz6D&~VMy z)*hXtfb0z~>+Sp#;%$Hjr`>1?x6p%XUE*Xiemj{+>FGYyp)i)_oB)Hv?KBeygVAGAQd+YZ10_Dq7Gt!cStJbN}rH zRrghxry7X49>Mr5)F-P2olfNM8)hSB_aiT_wDM^NJ6o*ErJa=@9c+->T*qJ|{pe7e zsofqzZS&|v7W(jHlGxN0eQ?q-FkOQs97fFNL)%-1?sqKhlE@p4IpIxd&_?K&=QsOeSI?0%n-H5oyJy)-au{6s9z_xkIKV zQGPw@?)@N{hY=ED@O#9Rrr~(mBk@X;924Pd$8gT0Fu}`>o*xo&m{pOM{eH$FDv2np zkVXM~au5^3kp4r8W&Ny^$bl!1__Fg;+=0~)NRqy%lMEf%O+s=zwd|4M$Ji)y-T|8d zAcaQPI7sMaUn|k-fgFWzS>JBsYiiXCejzv$M_7s7D(IbpsPMRkhbhoWmC)4xO2_0! z45S5!`ELSj;tt!r{appGr#qv4sn>I!w<~RF30+;cFGby9@{|~btkXA~u&XFPIh}l$ z_tjgio#6wjWldSOsdXiNjuGg|W4OadG-Lv2u@8-hDc$FX6q+3AR6+8B!SBt4zS_NHM#=HtAQEc-}!`Ek_YJXAO=W+)!*fN7=C z!rXQ$pZ@15)AK(kV&L`%;{WUj5yVJz>~^Pnb_uXRNIYo&ZUGt^LP zFG_nY9%I74%F$OaS)*~K(P5ZRkZvnjLxGaNZ-YFaNZl>iErt#c55({9W9cwO1b0B7 zbLH1iRUdoBh1{J~Yk4Vn1JY4QVmsbZaUo@tZx_x(tg57Pvgfv2i_gG`w_4UuMDs~= zFI#k*8Ro*P7m^GzP%$Hcuod2qoGYPpQuNHvC)0=eCgiIc@uscq1x@5FU(=3aoIa-! z=DglFb9~YkK2$4NM6=yyqnkZ=?d?fsPYesLUZzl5h-2K!^!QA3jf;}$QWY@KjJK@2 zbCnij`-C<_%r3rVz4zu^Nz@cCyG92GHqsj6l)?zu!mF7JLv-c#txfozZQ`+T#G&h- zS^qdWj04g-vi3e93riH_1KK0FN7d8DWIujlg05v{Dc#p~Bei!G!hNkiLnWARsA>e> zL}#QQ;y{)Dt-_8jNIAHeGl8zvQBG+8?5Z&?kYa8R-MF$>lNG*_v+!TC>UwN)@1BR8 z`xoCopk_IaI{EBL>aC%iCukA&g!duQ)w72OzU`BVDgU#)2l zi$@n4fy+8K67b0753wWAsM~*{BB$J!OupJhX2@5yQRo=`cr}zsol{O* znzNI*FPA~oH|Ig8x07I{kpqHcPcAFcrMZy`N2w)vhIU@ag~$C`E+y>F)sRqfx|Qc4#Lqb%Fv{#m{L2{;9Qlg>Ld>RZKG{mXE)^ZDxB~6vow&0B zWoOLitH2p?JDDjDxbtU@3?o&(^sF4D(E`y^t(R2{>`iYo^4vy~bv?a;s6{{wgDQ4AbVgL$^HulNI8Hm-3Z|nJ_5WZC<-044F zms6#+a-jE}oNUqa!#9>M`5f7LD35+U*pEmMrJklbXo%>S-5ZVj#RZH;kNutScM(ji ziC5wY@zBR%gB^zp;ps@}MhO~2mU|aOd%bp@TO3aYhX`<7W&pu1*F>QkYL9)mMUEcP zDU8nt9Htp*Zkgs%MunSuP11EEN4{)xe3^%7sqh zZ0XZL(i}fnPY~0uyjbiCn}>5wm=t;BWPX%#t3Mw_1G8UGD^Va{YWqPL%f;zZ$D|;- zzEGA6D~apRQSai*Gcy{msj6+~s`o4Ukp$e10(}7NUFxH3gHiH)f?2V1v9z%?6mrQb z^D41VH^G&1*6%V9iYzRFGrV}LZO_C`fE@4`q8BW+y8bXms0`H6As~qJ=teE5;N=1B z5cpvkKV9rj?5?}tmJ_lpQ)E6m!#tjd9QP0>0?#m~CBp#`Z-yXd(5y@-exk0W+l3gJT?R zlobdL6lZxO2oII48Q{eeZoF&r)Y7&Kg+ge{ROzAMtN%aSt2YhBdsF;ogyi=X$nu%@Qr(&TYqjBB@BXw-- z1?gC=LO#m9zcsJfAft!Euj%0;@3 zMG^jHnkVU)oI(73oY033fkHguY6OslN?jCgW1P(@t%XgPI$u7sU-cXX*JW>WkD(v- zT%M3=e+Vr=lo~LuY6cy|Uob@oJH-)XO6?$e<#&EnU{}>>lDAFXkS2C{J76iiP?^{` zTm+lFCUbyIG?7I-tPBKA3yFkU7Z&YEa&lrWg#kZ*Wn?t8cl>d!MB;HIoSD#ZD`X8zb0%v%Hn92^sQ z{MJXT`0k+@c)g#AIcfIApyoz)b%=v0h)`n+f6~81o_aOPV6oSUKRFI%>MNvcS<~A} zNwI8H!cZ#EVr(oB;nCKbZ?-<87j zI;FS6iZ$g25z~20#U|EGZF;$43#5%@^$#4$EUSnol)lGcq6lguY!iIAdp?sMU8bta z^1e~w@7XH`L!*k&*|&o|MF1~H9HyY@h~%! zScNNs_5zVy4QWUUYw*@y;-!d!+G#`>S{sKwMU+yeOUc7?me=cp#Zqm@eYb2>EolE?e3k}hu?M4|-y$hkeXGiesd{(8+v zGbe@I>oUE`qQ@x4&_l&e4B)69FRAsedvY|gclB;T`7EHlhe2A8GKyoZm7f_2nPD1C#hmolPK-E#1=)-Wf=b~4=F0Ud?Gk))s7krpFd z;%Y>xiaeV}vkN2ms%ksR_Bp)b#0?jO83sxpus2FZfrLTb58`0g|AQ|qp;B7|>Z}!+ z`3BgxD5Yk?SjL*&#Am!7CK<3zLOGj7>Lwm=nvOKVlJJCBxSr%&MED?R1sz*)aASqw z0XzO{1B>7PVxwWum%e*SE$88i8_Gw?2^G8NJb-(=iXag0tU0Kxb+pe9b1H6|a}==gddc?b^;{BLoMQEg zj>(vOm%tXrHujXYJC6h}7$-LhIM^hXBbm|3JSO0Kuq5}MNGRWPDW8f1PPy!|W)8Pj zFFI%ffENgC3)7UaNcyPEsWoh4tK3lAN=}`Vx<`dLEB;jKGixa?=$W`}%F5Hms4`y^ z38#3OG8q{lLrSJX2}wBdl$;aac$8{2;xQv#MJpdUHYKxr@&8t_T8&D7Mym^120AT8} z$F82VGL9b8XNUl#@8lc2$!N#wsAL4emh9uAWDw?olRgk@-1oeFFl?v8{s^k)`Pr+d z9T;?LA(@+wNj0PLQaVRjk(hKBZJ>1W^(z1Ro5D}Y*NZtvdX6_Uby&g4G?`iH&{%es z9m@YtCS>)$b%j@6gf?ot=ALL>o@)nQgxl3%sa*OWuStImuM4Q9FlGx+yg&maQPr`D zG}^{Pm{ha>bfRLhRQDz1te<6Dx2RJiPLS1s|72luqKYiSk0hZC3#?`Wh>eWS?ySOF zg&~qmQo4cml97Pw$s|JOixZ03lY+ph@ek_w&PW#UYjocFr{*^C8LdZ2o*|3N^N2-6 z!+SwhrrZOVE88n%YHpqB;YnAerPp5ENx(AL$q-4AOfGuM>UzxVRJx0xQTtN? z*t{BjTK6LGaO$^}15MLZ!*o!Ja5n z9aF%L0difuP*n$`S%p>tX9+Xv#P`Die>8U{c~q8(Iu=znG8;Sz^+O&4jVhYjL1j68 zQ3WfQVDv3S9!%&FIgxK^kSF+7g**&-mL%>6xb}BA;*=6%5RI#s+2Tv{dxfeGq%4Qe z%W_?gO>6MyoynCTVL=LHcY9)|scC((Lo)eQs_#aFhM@ zoJrG9#5ReTjiMYRI%+W$4}pKF*gXc9T=Q1jrQWe(S+&O(@Qi@G)m#mFR!|$%qa9w) z@wW*$jisgZ6fh2E#eO89-+|}KdC0%Y0^?$)g}JN}p+lgvM2em1^&?CA6ThkP~}8;0!+z0Cwv?C7IRT8djUN8qC&uX5kgSfgnH5IL1~ zSkA&d!LJ7Rb#9_k8MvP0&*Ghxl)V?#VLl)kkzrIdpROxQ4H@ir8v5qIKz79ee`8i! zkSt+AMV=j+IKBWULQF%+mvOMReO6w`~uTVMS9xnpFX(xd5M0FggrlJX6_SMdm| zp}ArnVD(Ef5hkCzLbjug>i2QOiov0Vp#O1oy}tx!Xu3N?sJ-HZk!Iu*7g)z%ukV&P zWkq=WCgRQYOP>X3mbNIYf5!FkXFD75*H{7-Iq@aFa?Tlrbb_9%1}a}o=B#pmm2!VZ zIG~7hi!GCZ6oMK0%)#c}giFx=BIrUyqI1qvu!Qz6I5Vd9La3FU`bl$lNlJXZ;;wB7 z0*OegrhyC@g`OBCh~Ej~XQ%@afNrhv-LaY@MR=;Ur}9rEq!iO2*ooP^Uv`Ts<`<`Q zwimp#F{}V`j2zn1h0+_bJ@=x8KR6>30LWoGJ3&8-`By%~44cT;9w4RqoycCntUaVu z7viB{eamMX7oe(RpeMdR4>;{**3vTKZzEnlNHln}dPTdVz<9qC-^E=!NlFChTW`x; zXBSY&oyvMLPIwAzPk_RAUg?>D;JH=F?csUd+Y9XSSy>9C`xfWo)h}bcP=jp17xa8v z|2v$s4Nn?wP$nm8=_2h&)Qe9@7Ez*bO-cWbtPkFvxaBMmFX(iIL8JtlfH#qlAWnmQFFGV>&p|Su#BW+ELe;BbRevD!+KCChuPx zp3%WG=tMz$VJtl^poz`0gJF924`^ib4f^+gyl0C;L&T)Mh(v5&TUrh|z_X1Art%J8KEl;ZYf|QjKG-rkCeZJX@ zKMx-nAi7mackhX0~;>?X8(JqR2ys8UI(k!y{)>&GWnwL zk?DJ~`d3q2waShc9|KO|rR=Yz2Z+o%J&v5`cI^ zkY8jy)GzU-w3{dK6J~wjpTh4GB6=hMz{z7bfaS$?Op{Rkw#>&JJyF*%xLEn28xA?= z(V&qJ;BQUz7$Htd^thPtU@%KqjnrXq3k00?YZ1EV%f2#MdFc1$J?<$zxJ96RG#ws* z%vlgM(<2?DJwY(}R$r9kp0(%mYp;kyJljlw>8m^e0kGeEtvq_8n;BvXt7M|ht$4(9 z!$AuTEwFa58Kc^->=HYc06|xY0(;8=GMe)uZI%hu(2H|Vj)rRBSVJ2Knh=GW^GEUc z9$g+B(v&~H?8*#7Kz(vm{;k@IUJ;LQl3Pk?)w7*VO(I{-Q;Bs9LeG^dAC6HGTe{y* zWKI;Epapz0L!4TSb~evPTq)>Vvn!LU6!N6HbhM%vEH*m{Hk-KGSZ?~1CvC5Wg{opV zjutzoj{{??E*Axtti1bohx^@Yu&iej`v5yG?B~|t*Fn3G_GA_CseOepq4QZ|TUX81 zXwEzpt~BZxfl@DPHo9d%Tw!N`RfSYIJaK#KGP~oBzsx~E2@XOmTLA)Oqn5_}<)4o` zTe5G3FN-25lt?KHO$s7xz&&r%8h@l;A21Sk)>9_w1kC6^ei3#PB)6EJ;bY82`UWmutTM#b&HOy!o6E8bii3iB37v1rP`VP2a!^i3mCN_7Xf98_LGr#q-m||! z@>q*}Ws3gnppu@amD;gD7499_Hg5kq;S7=ST@Satp?{x>Qvmm56e9RsyrHI%|N83o zHJ^C{C>S!HCIy2nqz+IvSSrkeoO*aMZt5kKyzXQ1%R&LnoqO}tdg@$-*kNKX#Pm*)$qkBgOy^o!mz`Urq>SYyNEVmo}PH?&B>H`(%o z_z(CDF37PdyoW$@jw|Y8A^xs%FqF1UDeyyL1(MEmew@MFg0Yvyx~8yQ`So2yQnJkH zL`wXGzc!}`c{SyMAhZGx848W)d+KMR!}n$!fGOg9OHfDr{FFc8!p6NnvDbg$}h!21yPXNY0`$5Vd zYk~4ff`ZtTj4Njxchg`n@O$9}?AZ83s;yvhllMW4S%LiX(q6v;gN{bG{oZ>`Dx(gJ zyk7V7TpLnNc8~X2FjB@b8ovaWDoet@iW6(ZOr2d6m{kbTDR19^VUK%S5Zy_}{lFra z3z>^k#9ISw);~G&NARJ{{p%X-pS+1z{5uye&CU~8&M(wDh>_7yBy6otkXfSH*kfz4 zYj39#P9Ww+>7sYNf2}2yL4w;80xAXM9IcaHdXvHK^%@sgj8@KmZ`*51Jn&?-(3K1I zx=&i@{im5`Kr-x{13m2~@RadUCywcH)~$hK=cw}M8^f&WI*lwAWS>oGTcrR3z72qt zFAIs5&#S#|Bo1NA_f>TLEyLm5xkBGvkn~p$eLRfgPY&y}?u;F=p(z_xrE6=qI0Y5MD48cmQe z(U*rKRrG`2KQfJ+*K4>F<$I0+<~_}+F;UA|jy_v*m7V|f<@!0ww$Tdm)rfMdzzX{L zLe66jV|>H~%%_K&6O$x38Fa)IuA_#uH;-I%ZHo6*uY>(Vg)jkjcK(_pBxtn?X4(BK zr&rfWGx^7=Ves=pG_s#>#I`h8J+oF}+LpbZE+etCXf_DQ^J8p}o(#P2gIl!=LBWS`q_uv_9rYQcH+pw&Hazs#3c{0rKZuda6dIuuF4SbqR==N&ASn1=zCf6lIBW?h>JOLV zGYI|nvm;1n?Dk0Graw-J`v7y(qpBN8Nx7~yl+&6CF!fS)6=q%qM|-eW3)1|&iCO1k zQ>5aU%K$b~ul)EzQ6(YO=GF$xrMw6OOQG|p`8DG1>re`yH{#epsYbZ92D|5l)ICl% z=hF%6Pbz!7j>yB1=hfWeUWZ>;g%;iFZh!-itA%F$7;V(?4*g)IMvoDZ3Cd(BsvsG1 zs5ISdp~)q^H#p9qV+PI$5*sq5VjL-Sh*uy5nH6qdG&g?Jchv~kI?EMw5*g0Zd{!80}vGAmYx#h|9AdD>JnmUZwOWL2VTr8`&| z0IPy>M@PU-X--<#`1!%%Sy3;md4WIS@Lz0}PXb3h<9s#`2=@dn{wUMw-#-)Rc`k)m zaTckQ>ezs0|50j}OA1h?EJBRZA7pTYc-}XsO;OPJRGECsWW*a@&D_A=kXuhuzRO@vKU(~87Rwp;Y@+6{2gwU*IO>u4ihefK8D0&= z0F&%vXkLP3Fk<}QA9B1@oN+&F4w;L&M}YSa2{OdI*ZEyoHG^~*S9)kU`VO-=aR&pB z>iCQGxxGhqIGQTy68qd#aO21M_y%P`j(|dKUG@Zraq) zY!q~{{kovnytFh#>AY5vsyz!81Kfy{d^)PEAjjYw#BmV&Z|R{q%!aEt(*-UvfxPhq zZnK})Roe@xR^Sl!tHMfNbcoVW(27>>|B-9RO@%bRpHF9y7E}+&Mzjiiy)skzw@>ql>N>B+!!5#Q^CggEa?!RrIqaOs;o* zDVAN1>y36&q~$p>Sx_GRW30P%9C13E|7(2}2daOxb%-iLi z5o+UK8`~I@KQ)gxKIDvVi92xfDs_J(^A_}YHAfr#mY~d%t^KXs z+^nEMh+kh-oWCTrH}a_NY9c6Cwlb6V=9;ku zlg+Ab#5?c;M;v?SUZ8_ANp|KS=SiVB4>OrXs&V8;mzwDyQIsv66+;U~g1B?Mpt!mZ zKqMvk2>nHFSYx2_LpY+;JQC%rodD3NF_g5IHKVlf#v>IsjGbu;2xzW>pJ|R1HBJ5h zMjVgG$_(z*wDQ<~2PZl`Obpw{-u&V2){RRWGF=&5aNaai$P1BlS&o6?;D<5ADL90u zOt#kI3MyKC^QxCvb!zk12+u?s)bc-yagsl|20@&nz7QbP7D(|@Z^U3o_)6~*` zn1Huza6)69i0Z-;0hX^vJ(Y?2`m`VRd6l-ekg4tr+fj~+8VIj*XQukj*rG07aySze z@_ZBhp7rQ#E6ZavSj?K8&gSF5khnzN5v&#ZcpdjWN&S2w`RU)CEdZVNK?Ks z;B-EDL`F1X#XT05v+b})zudpWk9fmrNQ72%3JmeA<+&KFY_NCoRv0#ec+s&ylXJI? z7Bm`sX3fSH^qF8vWVfizulhdh_W3m!9L8u%1@rNlvqk>iyJ9{)wM?K;d2upS*!7xe zjR*opVnSs#a*Q9XG36IG62Pl!&X>Nkp~x)(ywA8m<^S+>7Hn}X!MYxtKyY^m?iOTl z0u1gB2|75z-7UCla1HKm3GVLh?(TMHpL3r31A0~WOt0#$s<*ynEGfmzch{}!Uqa?A zhL`7C0bwDq{Ld-ZmZ8vSHKPY&EOIYojAsuv!uls+(0E)at#)m|z!sw0w?9%EjF^ol z@j?%%@XK`z#DFY802HwYpw{Fk&2%bpxRzA=7J_zo+xBJWmA1WG=dt&(-1ZrTyg+J# z+ed$1>8br=}4*nal#8Wg|* zIQaFi12mQoQZR#$vr{J`Z4o||;V1Df$#Q8($R|COGS@4fU*zR&=9T6L35PjqgQt@j zzKLiIoZccQTvS5&?I+AoCJd6(W+on0u*%EvgyXUMCoR_TK>oV>{T|EXT`YxAk9A6o z4#y?VN0x>N8X+D8Myp)|6QHJo7Sb0e?SRMUe@tn|j>Emsyjf}wbFqbdg9q2Se3Rp* z8w`rm(#EKrRLZkaXiKX1(ZW_e)T{GS{=*z9In%PY@7%F}_8I$h+y}MF8+zV;XkQzb z8~l22H1P)(F`xhjW@@~DituyOO>gcjcFGKS<_!06yL7^%vHNgo82R$vm6-7Xd)%*G ze>wnttj+z83Nd7tn!D?54!*S;sefn@+x}iIBfQTM7-Z2Y{YO{Vp+=3E>OvrQ#OK`m zgVwMVp#)s>%Utr;(r)F+&Hll3J)IX7V<;L?LOI03^t$<@b9G8EZ)z=jWh;qh2& zL)KFt<-NU@;av_vv+r7@rBg(zxF9-PZpLiu<>=Mu@=Z?XT^Z4Kc6Y613e|n9M>~B5 zT`Vt7E!hO8IcEmsXYKpoZx{C-r5I&oDX1 z`5?e!PG04rZ(1h+6@;sv94DbJHT7q~&)TVpvi{=2Izm+b{`x&+^xu>G!bc#X|yv;sOfn1Rq4uEqk__7 z-?VjrFsM>3IgU}Cobz^FqSik9q_hy;tosCI>h7A(s?26X_hH3&K=Nj)PT` zn)ws&$8_|rY{h6v7TQFr9`*chuTjKrw8-z{j3^qB$XpFmospRG+T73H`m0~O0-?y! z(dK-XR~@b_j<&m36H)eTyxALAQ>bF-1k+Q|YU-Fv^Ehm;Yvw$QQ$^7sayH>x+Iky& z?bS-e)Cbyfd?i;)lPTF56vN6KRP@-~Q6i6`y7T}%zV!e+FB7L`ZYlzKKMp6-*xvJZ zIeCE9bMkrAdLHo|HP-xt8j7#G&9_LpHX=)xSgu#~rvXFPVtur% z(s6k@lI^M%hNfdyp93NpRM=!2a(HOU8ma*)Ci()%`%2CjTvff63|B%nhnqK^_87A4 zX*1WSm@I0bKtK%`;6_ve{xkEMEkFQ}PmlscFh33Ss+$|Z8Ch>I($T&E{nC^rPB4`= z=Kg2~l=>zmdbvXik}Kq#fn@(w;Ks>Zit|Q}b!}fozuq;6Q35ILA$E!B!A!F-VNm=( zO17VkTZrxNH+;7ry#Yc6-~po#9~LU9YA73?%3iT zp6=8-dB2akhS-$bL)!@kF{WZ^Gn9G4IsLxjdniC z#WiO~?i}@8wq^x_to{+tJRgT(;I3PH+iR+?b{v!X?9mGHN0oZI3xsd15ak%0AI4G# zJ%<(*+&-3;yhV%6J!30_1nxvY&^oA5yhsmb>BHgpja*@FcnrUGfevM9CM+!i+*hfjENw^Nzb{{+ch>lNT5v!1SNwS!AsrFBMuoG zT)w095-^)?d>`cuGvjD#nL|&aYJwMjK#xJyZJ8Q9`Z0HkE4 z92guK)G1X3XlrNH)^+QQkJ977aD_Ul=`z3r9HW+*+NiFSaP2nrn(Wv>)V>5K_^3H7 z&nwz-&SdZ>X{V#Cy{mGO8F688#$Y3`Z1PY0*aNon%S!6e3`|y~z*l0@0(@TLERz%} z)Cp+RRVy6S;H^uyvu>!b#6Hc*LmvMyJHp^T^Q2SSf*p4WmWY=yGpkX+JEVywzPh?K z!m*K1ylxnQghshYf0rdIppMmkn@V@$^g!cZ2HYwjLvLk*!bP(jVIu#G zqQsK86>+rk^??~LeIfaV5T{wHi0tWzpMd@5DnGMSD%4{$hh=pjet)bGWP!&_a>i<8 z2FLzuK5eJRTpZLt?;#-+0vI zeiY)*EUergd6e}c_Yg)pdF`@Jm8_b3Q-M+-*pnt~XrGDE3DB=z20i9R=YiAhN%9*fhu!-1WIIm1jTVbip?wwYrkIYH+1bqw(`tJpo>!nZpDQ| zCZ2eI^$pD@tNovWEap-F4X;r(i*IGgXEdKMLF6MzpiD(y8!Mo1&U**wDrpCN+lMlBPb@cW}UFz7NN`zA*A2{pQK@ zb63EgRKbJ9Y5AvPQHOH}$~UQUu%;eo<#D0_gBzU@Q#kXeDjyv4rrm!dhv>_$1OW;g zkb1ax3K7*Pk!ZI7tyHr(!YBVzgYvcWjq~RxmQVtt4S7ta(G_k~k?Kj!4y1ZrhYb@a z#Iv@~qD2=RyKDbOq8|35-1M`sE_CyDyhXV0*ABSc8LH}xlHVL8I7Cj-_-*2I^w(8v zQ2n0hHT`jA&l@5ubN(h%9Ny{#x35^WP&5T#I566MvypWgL>Y|6IhaW}rn8L7z%vv( zfCJRu;4)R27IWzhtCO#$q)$B?E_&dn3==U5r;L^fwuU>|pt+~SctR4U2JrDP$q4Ag zY|x%?AOqHvJQzXyM=OyZ(?YztmW&NzLl^tK9j=V@>~1TffhG-k_T<@*==`3FQ$6aSLQOYmqnOmT)sIC z7uR{e{(dyq2<*JiDN*&6O zcTa>;0ZlJEqp}j2gP%=3AwR5GD*o8RN=MS!`eW>kXH|8Q0f zSzSH~UW+=CS2#%JzpU5ul+=;iFs8Q(KDQMir2Yh-Vy)YD!wo;i{-A19P6g`-!>@Qq zF?Sc%UW1yq)Yk=x5)u11ss8>WU7fnV)vo%XdTVJd!NUXJN15d0d!`g^yxnv8q*L`5 zbKPgLs>$e?x&I(-+fn8;ye?eAUCYyaHM4!mV*PWR2T2) z0!QB{7$p!xM#Gkzr|DE^HXaaeujW(atZ?zxO{>mfmB=~?Y>6*D7GCuX}H2&-MX&`_FbE*461`CD|` zZ4UdF99bG|sMjb2oXw{aS=se<~Qr@DaiU-%nlL?6`0rCm-t)kCy)=J~3b$~O+< zh;{dnmm)Tb5G{Ys$^r9~r*((E!?VHe=UCF}ubd^h%N9KEUf2;CY{*8#j({0 z<~DYnkp}PBr3pO5c4I^*$Pwx!z&P`|6-39^#roNFPeOS`8~hIQpsR=s&o)P=UaUbQ zk7b)zK9t<0+uM%bm8^g;Lb&=;azEHxxkZ?-fo`@DrVJZTDvPM0`y`7Q*1kyf~4T`=Rqav$z%XB&HeqwH2-H2lr^ z_@iZk3ZAQVY|2m0e9j_i*axRP*4D?7hr}gAOjCnZl2H(y4C_yKayE)fT1^?9_(m)c zVPLobex`xW8{;VO^(Aak+Y?ZzOlR+qgmQB*oDyoeJg=1)xxh^57#=~*QzZ2g!CA*X=O)n&EcpsNQth{aT5=^`^o);Mbj#U>?k(ST z_*y!eh3Q+U@CRv|mG5RKk499Kl0(l(yq}Yx)s!?gX$ZjzWhda}!PjSbtQ82?56POf zmhXM}p-62}nG4_WqPu49%LujB`D4K*K{Ku${oxCt_>4rMBu_CaDtFYf#dO!3d>ZU( zwaXBqRSLf0nOv{}8dR++)ntP}z@bf>E4XEiM6}j@6KgtH;BqEmAIeh6fz;2AyWRtb zp&0!$Hys@<5wBlCx|sr;F$!*ml5gEVxtRc>Q5J%(Q%uU=nL}F1W;sDu2t_?9D-3_1 z-;>+F3}Dazl)U&Jd+QLKs@1FSr(*V@o0A{M4V^p{dA&dSO`XI>GgE|HFkh|zS9^mG zPc~8{GFQqS^HU@AHNHsFiaS2W z?EwuzOESbAW_X6FQj$5HnozzS3cZ&^^9u>h{jcj*4?H&b>`qg(Z;&9+k3ZJzo0^P= zTRjPv)$o~Gx5vd1qdP3M-Xi_sM{HD)B!-QR$j~4P9lHYzNAfD9?7$VyJN^Pg_fEH+ zfDDktKzln>hF!gXEtJHF4c|PDXAoOH$Pfm-ph=6@QFZz@_s`@b^8CsL7qBJb(|=AC z_#5^KydlhdV|fu%S^M_?)eF>k)Ym&iW9SY~J07}JgQTLF`w|l8&Cu}`N00)hh#qS# z+u76TT;pGG-(kWvHK+2C(2QPJk1idk4-k9A=4snyXK0lN5cyRkV$UNgFc7Tdr8!bX z#L(R!wx|5~N*r00XdurCZj-%i*&5$*{h!yd>w!{r&b{tl(*^7eDiO(xCDY0k4i& zj{EA7{q5nFw*JL2fm8-GE%u34uwX`eWEBEIsIut!;(FK7hOQowH2laCvciQ0RJ-$K z!ukJK7}@^G6x{PdF^xLWd+Fdtvww;@G4_X!paq*cLzaSgz4i(=;55OTPP2;{xD}*7 z#Jb;?lr$n z!QY|g`>M%aWj1UL*)DIa728;8LU_dKaXcIvkT%icy$9Q?9^2Q00B*C7@r%IO61n$0 z;)CAhk01}xNwS&^*k%GsSy2BVCZ#b|N?{5l(@C0JdR8ikl3P9W&|4uJphwcR%$RP3`Ti09*hc)s<)F*7ZW zYl39_r_U{)y@Ox}Q9(odFq=^Tc+Y&v>wvAOTLyy0{u23L7T8%l4mswwyHb3A<6t)L zK{kY4@o}K)UV8uRTv>dmC4ZhuF7c0{6<0Es-tIt`LG6=uh+(m5`oUr@rCg+$uL9Y- zB2z*`kdTcEq4O+ZHRm=SBT;WVv2bE?fdinufVi~?qy~kE!}})EBsj(4G+R51Y*?& zPX*o+1(5ojp=_b~3U-nu5@g5fJo?ZG>W%3;4};6b!x{%$M?4wDq~9c-nwvhhj9Z`o z{dox$9S(J8wJ*IeJESxb(fF5M)|!8IxDVqk5r5Z@5BtkZ;UoOzJ_i?IY_4WV7{c>t zmvb2Ri}EOg4+!WU`}sddUHdd*GYdO(CG`l#cXIyHPDbhG7a3ekC#v8tU+-5Bv7F`* zge*O`Ms$?;QXAaqo12NtG`Eb4kuR4KrhafemJD|*W)J1wi0YC`DF1YBVXIR$XEjCQ zKj8jvN3IG`a6TS-;#IzG;*t4YXlnYfag5B7uh}}`ag+#G{ADQU7IuP5P=q~_*vkIc z>v6>E)vB17YiAB&b#FAK|MNav$b?|cbVhY(1Z$guPdhK0?ARrup#itLnCJva0_1dD zHT!zAF6$rh9M@(^nMu&>UAn;r_*;`c1v#Y(Ks%pl3v3&W4(DroM$q76%}FH{RA;^# z3mK*P_Qc4|qi8iW;rKY0`8q0MUa5hU%do!-`b1i9eC2-Py3FXNm);tsXAEqaI;5r~ z{M*aBq+hGHhES_4yDlSm)Q3EE(`xr^Xv6%Ct+XR=#{n^2UN_tAmg*-nYpC>W;Sh4^ z(L_{xakEgqY^l-(ROpir-BiXY*vJxI8?73(qbM6+?zStBf!OUY78hxV>n@71k&0Ry z9$$*83=S%bxZ6wn6>R+0pX6JqCG`&aNo@qkEe_Q=vgcHfKxMJw7z(!_zajF z0=3_IAEle;E)X0+fndB~o9Ei+FZ*w5vidoJjCrfodgov;Xpgf*SxxVK)Xt0H>kXu+ z)gyA-ia{!PxB!3Nflm7OT<{19WvB!rfFsqb@r+Z45c zo;oE^g>h-LZf1wpl8j;cHG95Aw<~luhsL( z)nlLLldVT<&v7~$R=|p*Rv1rsKM@51YzbdEN+Q_-Z1G=ZoEV7WaXi8>n7ydh%LzTN zomAb_kgY0p+7YKd*oefPJ_C}!Tz{G?lmr0oz9-jBy28+q3rN*hvrxImKMsqNDrl#B zT<4?-O*irK#nF6Lt{PF*cD=Ls8px3?oXY13;_O``ov*2qW3Z|YiBnU_!#a;Gzm(Iy zxP6aWjKbedRs4kn>LE^~xVel!lHr73?_@(Z_Jbpd+>+uu5JB}T94;PyA=*OZjH6B% zCJ@+MZC9dFeTyv@;+peZjyLN!VcKeqY(hgrDP{IV1c_x5(Jfu82s)Fh zrkcWas=D%x+%$w^GUTpH!0-@$^}rz<0ocU{psCK!H~w>MNS``6v-6fu!Ew_`;2|B9 zEz4fW%a2|i!4m5?<-D)7M^R4>5i%I}!84}O0jszNif6oFgeSit$FADl^1U7>yIo-q z4<}qIl07VF2HS$09(DuvZVG>i+_aEEaZi0Ccd&w-s}|5u)PkROL5{lZNJN~mE-E2m zx$eemXbZu!G+)yMeU@Sx>#8pU1mY6Li&*p`(RSTV_1F?bI7$M#@IjbT(f`=+hJqn% z(R+iNH|2-dK9U~g*WZ~zfa24F__Ouzi2V?D;p@~UIL%Ghd$N%u07fFgpYljbX&Y)# z7J=!1+?rRN`6CVG&KF7j)CkC_1YvvF;h7O_(KYZQIv|Y)jaeJdQ8tT}A+Frk4>)|sE9FDcZC<>g&-FuiBW0quwmjWMTu)*ZjAOoBvHMoT8jQ>!b z)Ead*-_7;G_rSwqWy?$)E%fp4%PzndNeBjrfieWffZExVF+}9r?r1x8Vpn-2TDam} z(uN6&bIzzMxO!vsI;eK$dfKIiU7(tIsjNob*s#{S;K;hhbi+3H0BODnQY57mwWU~4 z1-WA;HC8bKnw>+^({BfW{2!L`S!Ax)Il~<%IxlOV75zAcGLHNZ{h5z~_k7?mib+wF zl1m!K@c2|y7Ns_-qzR5Lqo^o(qP$X!IX4ZRId)8rPq3_v(!}|vj5lSIou#Fcsmxih zkn#HLEVJW{if_n}gbrFj1js;OtfWyyzb_gL{GOaUtMaoR zV3#5AX0^jcZC_1K*h#w|EeZP9371+6@Y8d(Cd7@Jji$%7n%M9iU)Y5XMm-cox%sB4 zYEdXkhWecyTdsU~-#@eplwxj0q*q?dz*+Ir^$RNF_d>mwKl{9!;e)hjo?hm&V7 zthV~djOf~gJ6l^_^jZ!bD+0n19xj6*?56F5=E>GO=bxR$#pd_IfqX)$0 zCXD!%3&c)iX#Bn6SnZ(3@)fcYZ~NiwIyf@_?eX=7e66F&kYe2{T!@^Q=Zfh>D2JL_M!9%;> zwNM_2WzP=4LL3kTYb96ENt?+0@Ms70-w?2By`hssjEnTg=3WYbP*B?oRpt|9mXEv5 z9Z-|Ms>mBh;vai%oO4RT^f%pzdsJ`I>=Z15UBop@T(mLGXgK?|$r6W3IANom(eW=OS&atxkS= z-R7_5t>EtPD*OJk6d5B4wY>bp=_C6NhaoT=Vm^+BTu=m6UeG5G4=W#=ZI? z=$CQO=g%UvYrNHaLo!ImO-Afy0tMy#1B*92!X)pG%CBolo|0%pSD!>IM$5V+)LZG2 z=zDW-tMp5#wR9_ojt>3)?7{wtv`+p^nf?b1T!97C%iJw5FqbXNhX z>j4?!uSN`tHxX7qTb~wxEEjPcZnpqynpW*RlUsaRI3QY`@}$@8SGX}nNfW2B;F9{J zF^ELu$NA3XjUbMmO=I^RD~d~Sws73*nihe^i`vIfD>G%zQHU&RTeh_t;IpRfFIN{e zZBq9_Jw1i?CI0x7v@&ic_-E@Mv)<&oR0lT*Z8g4wjupq2$8GivE}Zqv=o#p8>^hTf zs~=}YTX%)4$jd(?g<_S5Jb$ie^^DCA;V}wykM{{7D==?@vgo&9u{YUX!)5DX$_cTmt{z#6s(v*b9+e{s8Grt ze;OF}9SzHfft6q<=y|?LKwwIIfBzmFxtjHvvZ)9RtUv${IX@OTU&Wt&51F9PhgkT+ zuBxBb``y?*VG(7t$s4E9-hsphzy~}DLV17M{&h`Dmic1$l-cy3%xc133`DM!xt^CN zOO9zgN>OrFm70&V9r+Yi3aE|fn8=b)KXN16^Q`c>ICuC8k~;`&xf1u~db}}1`0Lx z6)>J)Ek`Ew;G4|#_>sNN)U0hlK#9!MGYHLyeN>Gei{ksh>qU3cA11t)EZ^Z%P2SET zVBXszd5!!)l&owelyXl_Tv|L8-(Vfl(S19(2UkZTG)yZC>7dZK6S&KX_-{?~12{C|ro$e$N9d|hcal?Vd@S{(kKz zJxr~!09#F2DnPXp%nUO*#DiMCpU!m>THWMRown%0A1LFeA)GPl#KBIJ?LGJ2JawO% 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 Date: Mon, 9 Nov 2026 18:25:11 +0900 Subject: [PATCH 07/29] =?UTF-8?q?feat(813-0):=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=E2=80=94=20md=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=A7=80=EC=9B=80=20=C2=B7=20[=EC=A3=BC]?= =?UTF-8?q?=20=EA=B8=80=EC=9E=90=20=ED=95=9C=20=EB=8B=A8=20=EC=9E=91?= =?UTF-8?q?=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원문 패널(M01 로직 상세 · M02 단가산출 · 일위대가 모달 공용)에서 파일 경로 글 제거 — 가까운 절 · 항 안내만 남김 - [주] 줄부터 다음 제목 앞까지 글 줄 = 본문(--text-body)보다 한 단 작은 --text-body-sm Co-Authored-By: Claude Sonnet 5.5 --- .../M01_MasterData_UI_LogicLab_Side.css | 5 +++ .../M01_MasterData_UI_LogicLab_Source.ts | 40 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css index eb4724990..977a12f4d 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css @@ -40,6 +40,11 @@ line-height: 1.6; } +/* [주] 글 — 본문(--text-body)보다 한 단 작게 */ +.m01lab-src__note { + font-size: var(--text-body-sm); +} + .m01lab-src__file { margin: 0; font-size: var(--text-body-sm); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts index b587d1892..ede0ecc99 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts @@ -65,6 +65,18 @@ function rawNode(b: Block): HTMLElement { return el("p", { className: "m01lab-src__p", text: b.p ?? "" }); } +/** 「[주]」 줄부터 다음 제목 앞까지 글 줄 = 한 단 작은 글자(`m01lab-src__note`) */ +function noteMarked(blocks: Block[]): HTMLElement[] { + let inNote = false; + return blocks.map((b) => { + if (b.h) inNote = false; + else if (b.p?.trim().startsWith("[주]")) inNote = true; + const node = blockNode(b); + if (inNote && b.p !== undefined) node.classList.add("m01lab-src__note"); + return node; + }); +} + export interface SourceView { root: HTMLElement; /** 그 출처 글의 원문을 그림 — 지금 보이는 것과 같으면 안 부름 */ @@ -122,19 +134,21 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView { if (shown !== src) return; if (!found.length) return none(); body.replaceChildren( - ...found.flatMap((f) => [ - el("p", { - className: "ui-text ui-text--muted m01lab-src__file", - text: [ - f.file, - f.matched === f.number ? "" : ts("Near", f.number), - f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "", - ] - .filter(Boolean) - .join(" · "), - }), - ...f.blocks.map(blockNode), - ]), + ...found.flatMap((f) => { + // 파일 경로는 안 보임 — 가까운 절 · 항 안내만 + const note = [ + f.matched === f.number ? "" : ts("Near", f.number), + f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "", + ] + .filter(Boolean) + .join(" · "); + return [ + ...(note + ? [el("p", { className: "ui-text ui-text--muted m01lab-src__file", text: note })] + : []), + ...noteMarked(f.blocks), + ]; + }), ); // 강조 항을 패널 몸 가운데로 — 몸만 굴림(85-5 · scrollIntoView 는 메인 창까지 굴림) requestAnimationFrame(() => { From 7dd5bd76083a7d6c4a0e7bc06da675bb1b543375 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:26:28 +0900 Subject: [PATCH 08/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC726):=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B0=20=EB=AC=B8=EC=84=9C=20=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EB=8B=B5=20=C2=B7=20=ED=86=A0=EC=82=AC=EC=A0=88=EC=B7=A8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EC=9A=94=EC=B2=AD=20=C2=B7=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 94057b00a..6544d50e9 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -638,3 +638,11 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 1. 권장으로 해도 될것같음. 하지만 조사해보고 맞는 먼저 보고 판단하자. 2. 정할것에 대한 설명이 필요. + +## 요구 726 (11-09 · 사용자 · 전용 작업량도 기계 항목으로 · 작업량당 기계경비 예시 요청 · 기계 무관 = 제안대로 · 토사절취(백호 0.7㎥) 대화창 출력 · 1~4 한 번에 진행 예정 — 일단 대기) + +1. 전용 일지라도 결과적으로는 다른곳에서 사용할수도 있으니 기계라면 당연히 기계항목으로 들어가는게 맞아보임. 현재는 하나지만 나중에는 늘어날수도 있잖아. +2. 작업량당 기계경비는 어떤걸말하는것임? 예를 들어 보여줘. +3. 기계무관은 너의 제안대로 +4. 토사절취(백호 0.7㎥) 단가산출을 대화창에 출력해줘. +5. 1~4번은 한번에 진행할 생각이야. 그러니 일단대기하고 4번을 대화창에 출력 From c2e84eaa3de7e0c389ecdc9c6a0f32afc9a3ddd0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:30:05 +0900 Subject: [PATCH 09/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC727):=20?= =?UTF-8?q?=ED=86=A0=EC=82=AC=EC=A0=88=EC=B7=A8=20=EC=98=88=EC=8B=9C?= =?UTF-8?q?=EC=9D=98=20=EB=AC=B8=EC=A0=9C=20=EB=AC=BC=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 6544d50e9..d59fa61e8 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -646,3 +646,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 3. 기계무관은 너의 제안대로 4. 토사절취(백호 0.7㎥) 단가산출을 대화창에 출력해줘. 5. 1~4번은 한번에 진행할 생각이야. 그러니 일단대기하고 4번을 대화창에 출력 + +## 요구 727 (11-09 · 사용자 · 토사절취 예시의 문제가 무엇인지 물음 — 시간당이 아니라 단위당으로 나오는 것이 문제냐) + +이게 뭐가 문제야? 원래는 기계의 돈은 시간당으로 나와야하는데 단위면적당으로 나온다는 거야? From dbc1263232b726c4f073c96563e883fda9394462 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:31:58 +0900 Subject: [PATCH 10/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC728):=20?= =?UTF-8?q?=EC=83=88=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20+=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=ED=95=98=EB=82=98=20=3D=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EB=9F=89=EB=8B=B9=20=EA=B8=B0=EA=B3=84=EA=B2=BD?= =?UTF-8?q?=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index d59fa61e8..288aa8331 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -650,3 +650,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 727 (11-09 · 사용자 · 토사절취 예시의 문제가 무엇인지 물음 — 시간당이 아니라 단위당으로 나오는 것이 문제냐) 이게 뭐가 문제야? 원래는 기계의 돈은 시간당으로 나와야하는데 단위면적당으로 나온다는 거야? + +## 요구 728 (11-09 · 사용자 · 신규 단가산출에 기계 하나만 지정하면 작업량당 기계경비와 같은 것) + +아니 그러니까 내가 만약에 신규 단가산출을 만들고 거기에 기계만 하나 지정하면 같은게 되는거잖아. From 462e79abe2ba1304bfb60ee596d8932f7914c058 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:33:38 +0900 Subject: [PATCH 11/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC729):=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=99=98=EC=82=B0=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=98=20=EC=9E=90=EB=A6=AC=20=3D=20=EB=8B=A8=EA=B0=80?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=20=EA=B8=B0=EC=B4=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 288aa8331..464a9471f 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -654,3 +654,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 728 (11-09 · 사용자 · 신규 단가산출에 기계 하나만 지정하면 작업량당 기계경비와 같은 것) 아니 그러니까 내가 만약에 신규 단가산출을 만들고 거기에 기계만 하나 지정하면 같은게 되는거잖아. + +## 요구 729 (11-09 · 사용자 · 결국 표현 단위 차이 — 단위 환산 로직을 단가산출 기초에 두는 게 맞지 않나) + +결국은 표현의 단위차이라는 말 아니야? 그렇다면 표현하는 단위 환산로직을 단가산출 기초에 넣어두는게 더 맍지 않아? From b0c0c923fc28579c0b840bbe2327b23b3c47b140 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:37:13 +0900 Subject: [PATCH 12/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC730):=20?= =?UTF-8?q?=ED=91=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=A7=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 464a9471f..ecf333148 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -658,3 +658,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 729 (11-09 · 사용자 · 결국 표현 단위 차이 — 단위 환산 로직을 단가산출 기초에 두는 게 맞지 않나) 결국은 표현의 단위차이라는 말 아니야? 그렇다면 표현하는 단위 환산로직을 단가산출 기초에 넣어두는게 더 맍지 않아? + +## 요구 730 (11-09 · 사용자 · 표로 다시 · 짧게) + +테이블로 다시 표현해봐 . 이해하기 힘들어. 짧게 설명해줘. From cdf0e46914b8c50cbea404711b1560bc9c377aad Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:38:45 +0900 Subject: [PATCH 13/29] feat(est-test): expand native file editing --- resources/tester/test_est_issues.py | 23 +- resources/tester/test_est_jdl.py | 237 +++++++++- resources/tester/test_est_native_export.py | 259 ++++++++++ .../tester/test_est_quantity_move_export.py | 95 ++++ .../tester/test_est_quantity_plain_export.py | 68 +++ .../tester/test_est_quantity_workflow.py | 19 + resources/tester/test_est_rea_parity.py | 26 + resources/tester/test_est_text_parser.py | 190 ++++++++ tools/est-test/PLAN.md | 67 +++ tools/est-test/README.md | 7 +- tools/est-test/export-estx.cjs | 24 +- tools/est-test/export-quantity.cjs | 444 +++++++++++++++++- tools/est-test/jdl.cjs | 191 +++++++- tools/est-test/native-exchange.js | 9 +- tools/est-test/native-issues.js | 7 + tools/est-test/native-machine.js | 9 +- tools/est-test/native-quantity.cjs | 20 +- tools/est-test/native-rows.js | 9 + tools/est-test/native-text.js | 69 ++- tools/est-test/native-wonga.js | 5 +- 20 files changed, 1714 insertions(+), 64 deletions(-) create mode 100644 resources/tester/test_est_quantity_move_export.py create mode 100644 resources/tester/test_est_quantity_plain_export.py create mode 100644 resources/tester/test_est_text_parser.py diff --git a/resources/tester/test_est_issues.py b/resources/tester/test_est_issues.py index 524e54061..f308f75f7 100644 --- a/resources/tester/test_est_issues.py +++ b/resources/tester/test_est_issues.py @@ -1,6 +1,28 @@ from test_est_rea_parity import run +def test_machine_fuel_role_name_is_not_a_source_name_mismatch(): + result = run( + "math", + """(() => { + const p=EstNative.empty(); + p.tables.StuffcDat=[{Code:'M1',Name:'경유',Unite:'L'}]; + p.tables.MachMainDat=[ + {Index:1,Code:'E1'}, + {Index:-2,Name:'주연료',Code:'M1',LinkIndex:5,Unite:'L'}, + {Index:-2,Name:'주연료',Code:'MISSING',LinkIndex:5}, + {Index:-2,Name:'주연료',Code:'M1',LinkIndex:5,Unite:'kg'}, + {Index:-2,Name:'다른재료',Code:'M1',LinkIndex:5,Unite:'L'} + ]; + return EstIssues.scan(p).map(x=>[x.index,x.message]); + })()""", + ) + assert [1, "연결 품명 차이"] not in result + assert [2, "연결 자료 없음"] in result + assert [3, "연결 단위 차이"] in result + assert [4, "연결 품명 차이"] in result + + def test_data_issues_detect_duplicate_missing_and_mismatched_links_without_modifying_data(): result = run( "math", @@ -533,4 +555,3 @@ def test_data_issues_detect_percentage_unit_mismatch(): assert ("HeadMainDat", "R1", "식") in items assert ("HeadMainDat", "R2", "M") in items assert ("EstMainDat", "R5", "원") in items - diff --git a/resources/tester/test_est_jdl.py b/resources/tester/test_est_jdl.py index 21cb119d8..493b57d11 100644 --- a/resources/tester/test_est_jdl.py +++ b/resources/tester/test_est_jdl.py @@ -6,6 +6,55 @@ from test_est_rea_parity import run ROOT = Path(__file__).resolve().parents[2] +def test_jdl_duplicate_sources_and_internal_codes_do_not_change_estimate_amounts(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const duplicate=structuredClone(empty);duplicate.name='duplicate'; + duplicate.tables.StuffcDat=[{Code:'M1',Name:'first',TC:100},{Code:'M1',Name:'second',TC:200},{Code:'M1',Name:'third',TC:300}]; + duplicate.tables.EstMainDat=[{Code:'M1',Name:'item',LinkIndex:5,GN:2,GU:200,GC:400,TC:400},{LinkIndex:54}]; + const first=j.output(duplicate),loaded=j.read(first.bytes,empty); + const collision=structuredClone(empty);collision.name='collision'; + collision.tables.EstiMateDat=[{Code:'LITERAL2',Name:'existing',GC:999,TC:999}]; + collision.tables.EstMainDat=[{Code:'fixed',Name:'fixed',CalcIndex:1,GN:0,GC:80,TC:80},{LinkIndex:54}]; + const second=j.output(collision),files=a.unpack(second.bytes),parts=j.parse('mainpart.txt',files.get('mainpart.txt')); + const sources=j.parse('w.txt',files.get('w.txt')),target=sources.find(r=>r.F_CODE===parts[0].F_CODE1); + process.stdout.write(JSON.stringify({duplicateTotal:loaded.tables.EstMainDat.at(-1).TC, + duplicateSources:j.parse('m.txt',a.unpack(first.bytes).get('m.txt')).length, + warning:first.warnings.some(x=>x.includes('중복')),fixedPrice:target.F_MPRICE,sourceCount:sources.length}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == { + "duplicateTotal": 400, + "duplicateSources": 3, + "warning": True, + "fixedPrice": 80, + "sourceCount": 2, + } + + +def test_jdl_duplicate_unit_codes_do_not_select_a_wrong_unit_group(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')),p=structuredClone(empty);p.name='units'; + p.tables.StuffcDat=[{Code:'M',Name:'material',TC:100}]; + p.tables.HeadTitleDat=[{Code:'U',Name:'first',GC:200},{Code:'U',Name:'second',GC:300}]; + p.tables.HeadMainDat=[{Index:1,Code:'U'},{Index:0,Code:'M',LinkIndex:5,GN:2,GU:100,GC:200}]; + p.tables.EstMainDat=[{Code:'U',Name:'item',LinkIndex:103,GN:1,GU:300,GC:300,TC:300},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),loaded=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({units:j.parse('unit.txt',files.get('unit.txt')).length, + total:loaded.tables.EstMainDat.at(-1).TC,warning:out.warnings.some(x=>x.includes('중복'))}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == {"units": 0, "total": 300, "warning": True} + + def test_jdl_basic_tables_use_native_separators_encoding_and_preserve_direct_money(): p = run( "math", @@ -68,9 +117,10 @@ def test_basic_jdl_unit_price_parts_import_with_recorded_costs_and_warn_for_spec const common={F_CODE:'U1',F_CODE1:'M1',F_DES:'material',F_QQTY:2,F_MPRICE:100,F_MCOST:200,F_LOADBM:'T',F_SAVEBM:'T',F_SPROPER:'0',F_KWAN:'0',F_CLASS:'0'}; const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], ['m.txt',table('m.txt',[{F_CODE:'M1',F_DES:'material',F_MPRICE:100}])], - ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:200}])], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:300}])], ['unitpart.txt',table('unitpart.txt',[{...common,F_SEQ:'1'},{...common,F_SEQ:'2',F_SPROPER:'1'}])]]); - const out=s.calculate(j.read(a.pack(files),empty));process.stdout.write(JSON.stringify({rows:out.tables.HeadMainDat,warnings:out.importWarnings}));""" + const out=s.calculate(j.read(a.pack(files),empty));out.options.AutoCalc=true;out.options.SourceRefresh=true; + const recalculated=s.calculate(structuredClone(out));process.stdout.write(JSON.stringify({rows:out.tables.HeadMainDat,warnings:out.importWarnings,title:recalculated.tables.HeadTitleDat[0]}));""" result = json.loads( subprocess.check_output( ["node", "-e", script], @@ -78,9 +128,11 @@ def test_basic_jdl_unit_price_parts_import_with_recorded_costs_and_warn_for_spec cwd=ROOT, ).decode("utf-8") ) - header, detail = result["rows"] - assert header["Index"] == 1 and header["TC"] == 200 + header, detail, remainder = result["rows"] + assert header["Index"] == 1 and header["TC"] == 300 assert detail["LinkIndex"] == 5 and detail["GN"] == 2 and detail["GC"] == 200 + assert remainder["CalcIndex"] == 1 and remainder["GC"] == 100 + assert result["title"]["TC"] == 300 assert any("특수 구성" in warning for warning in result["warnings"]) @@ -111,3 +163,180 @@ def test_basic_jdl_unit_price_parts_export_and_reimport(): assert result["rows"][1]["LinkIndex"] == 5 and result["rows"][1]["GC"] == 200 assert result["main"][1]["LinkIndex"] == 103 assert result["specialUnits"] == 0 and result["specialWarnings"] > 0 + + +def test_jdl_machine_saved_amount_is_editable_and_survives_recalculation(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100.25,F_LPRICE:50.12,F_EPRICE:25.03}])], + ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_CODE1:'M1'}])], + ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], + ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'J1',F_DES:'machine',F_QQTY:2,F_MPRICE:100,F_LPRICE:50,F_EPRICE:25}])]]); + const original=a.pack(files),p=j.markOriginal(s.calculate(j.read(original,empty))); + const same=original.equals(j.output(s.calculate(structuredClone(p))).bytes); + p.tables.MachMainDat[1].GC=120.25; + p.options.AutoCalc=true;p.options.SourceRefresh=true; + const q=s.calculate(JSON.parse(JSON.stringify(p))); + process.stdout.write(JSON.stringify({same,rows:q.tables.MachMainDat,title:q.tables.MachTitleDat[0], + estimate:q.tables.EstMainDat[1],warnings:p.importWarnings,errors:q.calculation.errors}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], + input=json.dumps(project, ensure_ascii=False).encode("utf-8"), + cwd=ROOT, + ).decode("utf-8") + ) + assert result["same"] + assert result["rows"][1]["CalcIndex"] == 1 + assert abs(result["rows"][1]["TC"] - 195.4) < 1e-8 + assert abs(result["title"]["TC"] - 195.4) < 1e-8 + assert result["estimate"]["TC"] == 390 + assert result["warnings"] and not result["errors"] + + +def test_jdl_machine_simple_detail_is_editable_and_mismatches_remain_summary(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100,F_LPRICE:20,F_EPRICE:10}, + {F_CODE:'J2',F_DES:'mismatch',F_MPRICE:50},{F_CODE:'J3',F_DES:'quantity',F_MPRICE:50}])], + ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_CODE1:'M1',F_DES:'fuel',F_QQTY:1,F_MPRICE:60,F_EPRICE:10}, + {F_CODE:'J1',F_SEQ:'2',F_CODE1:'L1',F_DES:'operator',F_QQTY:1,F_MPRICE:40,F_LPRICE:20}, + {F_CODE:'J2',F_SEQ:'1',F_DES:'incomplete',F_QQTY:1,F_MPRICE:10}, + {F_CODE:'J3',F_SEQ:'1',F_DES:'unknown quantity semantics',F_QQTY:2,F_MPRICE:50}])]]); + const p=j.read(a.pack(files),empty);p.options.AutoCalc=true;p.options.SourceRefresh=true; + p.tables.MachMainDat[1].GC=70;const q=s.calculate(p); + process.stdout.write(JSON.stringify({rows:q.tables.MachMainDat,titles:q.tables.MachTitleDat,errors:q.calculation.errors,warnings:p.importWarnings}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], + input=json.dumps(project, ensure_ascii=False).encode("utf-8"), + cwd=ROOT, + ).decode("utf-8") + ) + assert [row["Name"] for row in result["rows"][:3]] == ["machine", "fuel", "operator"] + assert result["titles"][0]["TC"] == 140 + assert result["titles"][1]["TC"] == 50 + assert result["titles"][2]["TC"] == 50 + assert "상세 산식 미변환" in result["rows"][-1]["Name"] + assert result["warnings"] and not result["errors"] + + +def test_jdl_cost_saved_amount_calculates_and_can_be_edited(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['danga.txt',table('danga.txt',[{F_CODE:'D1',F_DES:'cost',F_MPRICE:100.25,F_LPRICE:50.12,F_EPRICE:25.03,F_MEMO:'source'},{F_CODE:'D2',F_EPRICE:0.000000000001}])], + ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], + ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'D1',F_DES:'cost',F_QQTY:2,F_MPRICE:100,F_LPRICE:50,F_EPRICE:25}])]]); + const p=j.read(a.pack(files),empty);p.tables.CostMainDat[1].LineData='120.25 = \\x12100.25\\x12'; + p.options.AutoCalc=true;p.options.SourceRefresh=true;const q=s.calculate(JSON.parse(JSON.stringify(p))); + process.stdout.write(JSON.stringify({title:q.tables.CostTitleDat[0],tiny:q.tables.CostTitleDat[1],estimate:q.tables.EstMainDat[1],errors:q.calculation.errors,warnings:p.importWarnings}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], + input=json.dumps(project, ensure_ascii=False).encode("utf-8"), + cwd=ROOT, + ).decode("utf-8") + ) + assert abs(result["title"]["TC"] - 195.4) < 1e-8 + assert result["tiny"]["JC"] == 0.000000000001 + assert result["estimate"]["TC"] == 390 + assert result["warnings"] and not result["errors"] + + +def test_jdl_complete_unit_preserves_difference_from_saved_title(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['m.txt',table('m.txt',[{F_CODE:'M1',F_MPRICE:100}])], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_MPRICE:205.25},{F_CODE:'U2',F_LPRICE:30.12}])], + ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_CODE1:'M1',F_QQTY:2,F_MPRICE:100,F_MCOST:200}])]]); + const p=j.read(a.pack(files),empty);p.options.AutoCalc=true;p.options.SourceRefresh=true; + const q=s.calculate(p);process.stdout.write(JSON.stringify({rows:q.tables.HeadMainDat,titles:q.tables.HeadTitleDat}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], + input=json.dumps(project, ensure_ascii=False).encode("utf-8"), + cwd=ROOT, + ).decode("utf-8") + ) + assert result["rows"][2]["GC"] == 5.25 + assert [row["TC"] for row in result["titles"]] == [205.25, 30.12] + + +def test_jdl_unit_inconsistent_detail_keeps_recorded_amount_without_double_counting(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['m.txt',table('m.txt',[{F_CODE:'M1',F_MPRICE:100}])], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_MPRICE:200}])], + ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_CODE1:'M1',F_QQTY:2,F_MPRICE:100,F_MCOST:0}])]]); + const p=j.read(a.pack(files),empty);p.options.AutoCalc=true;p.options.SourceRefresh=true; + const q=s.calculate(p);process.stdout.write(JSON.stringify({rows:q.tables.HeadMainDat,title:q.tables.HeadTitleDat[0],warnings:p.importWarnings}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], + input=json.dumps(project, ensure_ascii=False).encode("utf-8"), + cwd=ROOT, + ).decode("utf-8") + ) + assert result["rows"][1]["CalcIndex"] == 1 + assert result["rows"][2]["GC"] == 200 + assert result["title"]["TC"] == 200 + assert any("고정" in warning for warning in result["warnings"]) + + +def test_edge_jdl_machine_amount_edit(): + from playwright.sync_api import sync_playwright + import pytest + + script = """const a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + process.stdout.write(a.pack(new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100,F_LPRICE:50,F_EPRICE:25}])], + ['danga.txt',table('danga.txt',[{F_CODE:'D1',F_DES:'cost',F_MPRICE:100,F_LPRICE:50,F_EPRICE:25}])]])));""" + data = subprocess.check_output(["node", "-e", script], cwd=ROOT) + with sync_playwright() as p: + browser = p.chromium.launch(channel="msedge", headless=True) + page = browser.new_page() + try: + page.goto("http://100.65.141.15:5175", timeout=5000) + except Exception: + browser.close() + pytest.skip("EST server unavailable") + page.on("dialog", lambda dialog: dialog.accept()) + page.wait_for_function("() => Boolean(EstApp.state.project)") + page.locator("#jdl-file").set_input_files( + {"name": "sample.jdl", "mimeType": "application/zip", "buffer": data} + ) + page.wait_for_function("() => EstApp.state.project.tables.MachMainDat.length === 2") + assert page.locator("#dialog").is_visible() + assert "상세 산식" in page.locator("#dialog-content").inner_text() + page.locator("#dialog").evaluate("(dialog) => dialog.close()") + page.evaluate("""() => { + EstApp.state.project.options.AutoCalc=true; + EstApp.navigate('MachMainDat','J1'); + }""") + amount = page.locator('[data-table="MachMainDat"][data-row="1"][data-field="GC"]') + amount.fill("120") + amount.press("Tab") + page.wait_for_function("() => EstApp.state.project.tables.MachTitleDat[0].TC === 195") + assert page.evaluate("EstApp.state.project.tables.MachMainDat[1].TC") == 195 + page.evaluate("EstApp.navigate('CostMainDat','D1')") + formula = page.locator('[data-table="CostMainDat"][data-row="1"][data-field="LineData"]') + formula.fill(formula.input_value().replace("100", "130.25", 1)) + formula.press("Tab") + page.wait_for_function("() => EstApp.state.project.tables.CostTitleDat[0].TC === 205.25") + browser.close() diff --git a/resources/tester/test_est_native_export.py b/resources/tester/test_est_native_export.py index a205d6d82..bdfb4a586 100644 --- a/resources/tester/test_est_native_export.py +++ b/resources/tester/test_est_native_export.py @@ -8,6 +8,265 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] +def test_quantity_list_append_roundtrips_without_changing_indexes_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),sheet=p.quantitySheets[0]; + const before=structuredClone(sheet),codes=new Set(sheet.items.map(x=>x.Code));let n=0; + while(codes.has('r'+String(n).padStart(6,'0')))n++; + const item={Code:'r'+String(n).padStart(6,'0'),Name:'추가',Size:'',Unite:'m',GN:0,TargetCode:''}; + sheet.items.push(item);sheet.Name='목록 시험';const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out); + const indexes=[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name))); + const added=q.quantitySheets[0].items.at(-1),sameRows=JSON.stringify(before.rows)===JSON.stringify(q.quantitySheets[0].rows); + const existing=JSON.stringify(before.items)===JSON.stringify(q.quantitySheets[0].items.slice(0,-1)); + sheet.items.push({...item});let duplicate=false;try{e.exportEstx(p)}catch{duplicate=true} + process.stdout.write(JSON.stringify({added,name:q.quantitySheets[0].Name,indexes,sameRows,existing,duplicate}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=100).decode( + "utf-8" + ) + ) + assert result["added"]["Name"] == "추가" + assert result["name"] == "목록 시험" + assert result["added"]["Unite"] == "m" + assert result["added"]["GN"] == 0 + assert all(result[key] for key in ("indexes", "sameRows", "existing", "duplicate")) + + +def test_quantity_list_append_rejects_unrepresentable_data(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const base={Code:'N1',Name:'sheet',options:{},rows:[],items:[{Code:'r000001',GN:0}]}; + const bad=[{Code:'r000001'},{Code:'r000002',TargetCode:'E1'}, + {Code:'r000002',Unite:'123456789'},{Code:'r000002',GN:'1'}, + {Code:'r000002',Unknown:'lost'}]; + const rejects=bad.map(item=>{const next=structuredClone(base);next.items.push(item); + try{w.editDocument('',base,next);return false}catch{return true}}); + process.stdout.write(JSON.stringify(rejects));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + assert all(result) + + +def test_quantity_list_remove_unreferenced_item_roundtrips_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes); + const sheet=p.quantitySheets.find(s=>s.items.some(item=>!item.TargetCode&&!item.LinkCode&&!item.LinkIndex&& + !s.rows.some(r=>r.markers.includes(item.Code)||r.LineData.includes(item.Code)))); + const old=structuredClone(sheet),i=sheet.items.findIndex(item=>!item.TargetCode&&!item.LinkCode&&!item.LinkIndex&& + !sheet.rows.some(r=>r.markers.includes(item.Code)||r.LineData.includes(item.Code))); + const removed=sheet.items.splice(i,1)[0],out=e.exportEstx(p),q=m.importEstx(out); + const reread=q.quantitySheets.find(s=>s.Code===sheet.Code),expected=old.items.filter(x=>x.Code!==removed.Code); + const a=m.unzip(bytes),b=m.unzip(out); + process.stdout.write(JSON.stringify({items:JSON.stringify(reread.items)===JSON.stringify(expected), + rows:JSON.stringify(reread.rows)===JSON.stringify(old.rows), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=120).decode( + "utf-8" + ) + ) + assert all(result.values()) + + +def test_quantity_list_remove_referenced_item_is_rejected(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const old={Code:'N1',Name:'sheet',options:{},rows:[{LineData:'Q',markers:['r000001']}],items:[{Code:'r000001'}]}; + const next=structuredClone(old);next.items=[]; + try{w.editDocument('',old,next);process.stdout.write('false')}catch(e){process.stdout.write(JSON.stringify(e.message.includes('참조')))}""" + assert json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + + +def test_quantity_list_remove_rejects_hidden_native_reference(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const sheet={Code:'N1',Name:'sheet',options:{},rows:[],items:[{Code:'r000001'}]}; + const row={Code:'N1',Editor:{binary:'AA=='},MulList:{binary:'AA=='}}; + const target={MulCode:{binary:Buffer.from('N1\tr000001').toString('base64')}}; + const base={quantitySheets:[sheet],tables:{MulMainDat:[row],EstMainDat:[target]}}; + const project=structuredClone(base);project.quantitySheets[0].items=[]; + const rejected=()=>{try{w.updateQuantities(project,base,()=>{},()=>{});return false} + catch(e){return e.message.includes('내역에서 참조')}}; + const original=rejected();base.tables.EstMainDat=[];const changed=rejected(); + process.stdout.write(JSON.stringify({original,changed}));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + assert result == {"original": True, "changed": True} + + +def test_quantity_list_existing_missing_codes_can_still_edit_names_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const p=m.importEstx(fs.readFileSync(process.argv[1])); + const sheet=p.quantitySheets.find(s=>s.items.some(x=>!x.Code)); + if(!sheet){process.stdout.write('null');process.exit()} + sheet.items[0].Name='수정';const q=m.importEstx(e.exportEstx(p)); + process.stdout.write(JSON.stringify(q.quantitySheets.find(s=>s.Code===sheet.Code).items[0].Name));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == "수정" + + +def test_quantity_sheet_name_roundtrips_without_modifying_document_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),sheet=p.quantitySheets[0]; + const before=structuredClone(p.tables.MulMainDat.find(row=>row.Code===sheet.Code)); + sheet.Name='수량산출서 시험';const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out); + const after=q.tables.MulMainDat.find(row=>row.Code===sheet.Code); + process.stdout.write(JSON.stringify({name:q.quantitySheets[0].Name,unchanged:JSON.stringify(before)===JSON.stringify(after), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == {"name": "수량산출서 시험", "unchanged": True, "indexes": True} + + +def test_quantity_sheet_name_rejects_conflicting_title_changes(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const sheet={Code:'N1',Name:'old',options:{},rows:[],items:[]}; + const row={Code:'N1',Editor:{binary:'AA=='},MulList:{binary:'AA=='}}; + const base={quantitySheets:[sheet],tables:{MulMainDat:[row],MulTitleDat:[{Code:'N1',Name:'old'}],EstMainDat:[]}}; + const project=structuredClone(base);project.quantitySheets[0].Name='new';project.tables.MulTitleDat[0].Name='other'; + try{w.updateQuantities(project,base,()=>{},()=>{});process.stdout.write('false')} + catch(e){process.stdout.write(JSON.stringify(e.message.includes('제목 데이터 충돌')))}""" + assert json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + + +def test_quantity_list_add_to_empty_original_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),sheet=p.quantitySheets.find(s=>!s.items.length); + const before=structuredClone(sheet.rows);sheet.items.push({Code:'r000000',Name:'새 항목',Size:'',Unite:'m',GN:0,TargetCode:''}); + const out=e.exportEstx(p),q=m.importEstx(out),saved=q.quantitySheets.find(s=>s.Code===sheet.Code),a=m.unzip(bytes),b=m.unzip(out); + process.stdout.write(JSON.stringify({name:saved.items[0].Name,unit:saved.items[0].Unite,rows:JSON.stringify(before)===JSON.stringify(saved.rows), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == {"name": "새 항목", "unit": "m", "rows": True, "indexes": True} + + +def test_quantity_append_calculated_row_roundtrips_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),c=require('./tools/est-test/quantity-calculate.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes); + const sheet=p.quantitySheets.find(s=>{const x=c.calculateQuantity(s),n=s.rows.reduce((a,r)=>a+r.markers.length,0); + return s.items.length&&x.errors.length===0&&x.checks.length===n&& + x.checks.every(v=>Number.isFinite(v.stored)&&v.stored===v.calculated)&&x.items.every((v,i)=>v.GN===s.items[i].GN)}); + const before=structuredClone(sheet),codes=new Set(sheet.items.map(x=>x.Code));let n=0; + while(codes.has('r'+String(n).padStart(6,'0')))n++; + const code='r'+String(n).padStart(6,'0');sheet.items.push({Code:code,Name:'new',Size:'',Unite:'',GN:0,TargetCode:''}); + sheet.rows.push({LineData:'2+3 = \x120\x12',markers:[code]}); + const out=e.exportEstx(p),q=m.importEstx(out),saved=q.quantitySheets.find(s=>s.Code===sheet.Code),a=m.unzip(bytes),b=m.unzip(out); + process.stdout.write(JSON.stringify({value:saved.items.at(-1).GN,line:saved.rows.at(-1), + rows:JSON.stringify(before.rows)===JSON.stringify(saved.rows.slice(0,-1)), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result["value"] == 5 + assert result["line"]["LineData"] == "2+3 = \x125\x12" + assert result["rows"] and result["indexes"] + + +def test_quantity_append_row_rejects_duplicate_and_unrepresentable_markers(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const doc='

1
'; + const sheet={items:[{Code:'r000001'},{Code:'r000002'}]}; + const bad=[{LineData:'1=\x120\x12',markers:['r000001']}, + {LineData:'1=\x120\x12',markers:['r000003']}, + {LineData:'1=\x120\x12\x121\x12',markers:['r000002']}, + {LineData:'^(2)=\x120\x12',markers:['r000002']}]; + process.stdout.write(JSON.stringify(bad.map(row=>{try{w.appendRows(doc,sheet,[row]);return false}catch{return true}})));""" + assert all(json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT))) + + +def test_quantity_insert_calculated_row_in_middle_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),c=require('./tools/est-test/quantity-calculate.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes); + const sheet=p.quantitySheets.find(s=>{const x=c.calculateQuantity(s),n=s.rows.reduce((a,r)=>a+r.markers.length,0); + return s.rows.length>3&&s.items.length&&x.errors.length===0&&x.checks.length===n&& + x.checks.every(v=>Number.isFinite(v.stored)&&v.stored===v.calculated)&&x.items.every((v,i)=>v.GN===s.items[i].GN)}); + const before=structuredClone(sheet),codes=new Set(sheet.items.map(x=>x.Code));let n=0; + while(codes.has('r'+String(n).padStart(6,'0')))n++; + const code='r'+String(n).padStart(6,'0');sheet.items.push({Code:code,Name:'new',Size:'',Unite:'',GN:0,TargetCode:''}); + sheet.rows.splice(2,0,{LineData:'2+3 = \x120\x12',markers:[code]}); + const out=e.exportEstx(p),q=m.importEstx(out),saved=q.quantitySheets.find(s=>s.Code===sheet.Code),a=m.unzip(bytes),b=m.unzip(out); + const rows=saved.rows.slice();const added=rows.splice(2,1)[0]; + process.stdout.write(JSON.stringify({value:saved.items.at(-1).GN,line:added.LineData, + rows:JSON.stringify(before.rows)===JSON.stringify(rows), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == {"value": 5, "line": "2+3 = \x125\x12", "rows": True, "indexes": True} + + +def test_quantity_insert_rejects_position_before_default_style(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const doc='
original
',sheet={items:[{Code:'r000001'}]}; + const row={LineData:'1=\x120\x12',markers:['r000001']}; + try{w.insertRows(doc,sheet,[{row,at:0}]);process.stdout.write('false')} + catch(e){process.stdout.write(JSON.stringify(e.message.includes('기본 글자 서식')))}""" + assert json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + + +def test_quantity_delete_calculated_row_roundtrips_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),sheet=p.quantitySheets.find(s=>s.Code==='N0000000092'); + const old=structuredClone(sheet),index=sheet.rows.findIndex(row=>row.markers.includes('r000007')); + sheet.rows.splice(index,1);sheet.items=sheet.items.filter(item=>item.Code!=='r000007'); + const out=e.exportEstx(p),q=m.importEstx(out),saved=q.quantitySheets.find(s=>s.Code===sheet.Code),a=m.unzip(bytes),b=m.unzip(out); + old.rows.splice(index,1);old.items=old.items.filter(item=>item.Code!=='r000007'); + process.stdout.write(JSON.stringify({rows:JSON.stringify(saved.rows)===JSON.stringify(old.rows), + items:JSON.stringify(saved.items)===JSON.stringify(old.items), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == {"rows": True, "items": True, "indexes": True} + + +def test_quantity_delete_preserves_styles_and_rejects_shared_tag(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs'); + const doc='
1
'; + const sheet=q.fromWpt('N1','sheet',doc,[{Code:'r000001'}],{}),codes=new Set(['r000001']); + const result=w.removeRows(doc,sheet,codes);let rejected=false; + try{w.removeRows(doc+'',sheet,codes)}catch{rejected=true} + process.stdout.write(JSON.stringify({style:result.includes(''),removed:!result.includes(''
'+value+'
'; + const doc=''+div(1,'r000001','1')+div(2,'r000002','2'); + const sheet=q.fromWpt('N','sheet',doc,[],{}),rows=[sheet.rows[1],sheet.rows[0]],out=w.reorderRows(doc,sheet,rows); + const parsed=q.fromWpt('N','sheet',out.text,[],{});let rejected=false; + const late=''+div(1,'r000001','1')+'
2
'; + const other=q.fromWpt('N','sheet',late,[],{}); + try{w.reorderRows(late,other,[other.rows[1],other.rows[0]])}catch{rejected=true} + const redefined=''+div(1,'r000001','1')+''+div(2,'r000002','2'); + const third=q.fromWpt('N','sheet',redefined,[],{});let changed=false; + try{w.reorderRows(redefined,third,[third.rows[1],third.rows[0]])}catch{changed=true} + process.stdout.write(JSON.stringify({rows:JSON.stringify(parsed.rows)===JSON.stringify(rows),rejected,changed}));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + assert result == {"rows": True, "rejected": True, "changed": True} + + +def test_append_handles_many_existing_tag_numbers(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'); + const doc=''+Array.from({length:150000},(_,i)=>'').join(''); + const row={LineData:'1=\x120\x12',markers:['r000001']}; + const out=w.appendRows(doc,{items:[{Code:'r000001'}]},[row]); + process.stdout.write(JSON.stringify(out.includes(''
1
'; + const noise=Array.from({length:10000},(_,i)=>'').join(''); + const doc=''+noise+div(1,'r000001')+div(10,'r000002'); + const sheet=q.fromWpt('N','sheet',doc,[],{}),rows=[sheet.rows[1],sheet.rows[0]]; + const output=w.reorderRows(doc,sheet,rows);let rejected=false; + try{w.reorderRows(doc+'',sheet,rows)}catch{rejected=true} + process.stdout.write(JSON.stringify({rows:JSON.stringify(q.fromWpt('N','sheet',output.text,[],{}).rows)===JSON.stringify(rows),rejected}));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT, timeout=30)) + assert result == {"rows": True, "rejected": True} + + +def test_move_and_edit_keeps_original_row_marker(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs'); + const div=(tag,code,value)=>'
Q='+value+'='+value+'
'; + const doc=''+div(1,'r000001','1')+div(2,'r000002','2'); + const sheet=q.fromWpt('N','sheet',doc,[],{}),rows=structuredClone([sheet.rows[1],sheet.rows[0]]); + rows[0].LineData=rows[0].LineData.replace('Q=2','Q=3'); + const moved=w.reorderRows(doc,sheet,rows),old={...sheet,rows:moved.rows},edited={...sheet,rows}; + const result=w.editDocument(moved.text,old,edited),parsed=q.fromWpt('N','sheet',result,[],{}); + process.stdout.write(JSON.stringify(parsed.rows));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + assert result[0]["markers"] == ["r000002"] + assert result[0]["LineData"].startswith("Q=3") + assert result[1]["markers"] == ["r000001"] + + +def test_move_quantity_rows_in_installed_file_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),z=require('zlib'),m=require('./tools/est-test/import-estx.cjs'), + e=require('./tools/est-test/export-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs'),c=require('./tools/est-test/quantity-calculate.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes);let candidate=null; + const valid=s=>{const x=c.calculateQuantity(s),n=s.rows.reduce((a,r)=>a+r.markers.length,0); + return !x.errors.length&&x.checks.length===n&&x.checks.every(v=>Number.isFinite(v.stored)&&v.stored===v.calculated)&&x.items.every((v,i)=>v.GN===s.items[i].GN)}; + for(const s of p.quantitySheets){if(!valid(s))continue;const r=p.tables.MulMainDat.find(x=>x.Code===s.Code); + let b=Buffer.from(r.Editor.binary,'base64');if(b[0]===120)b=z.inflateSync(b);const doc=new TextDecoder('euc-kr').decode(b); + for(let i=1;ix.Code===s.Code),a=m.unzip(bytes),b=m.unzip(out); + process.stdout.write(JSON.stringify({rows:JSON.stringify(saved.rows)===JSON.stringify(s.rows), + items:JSON.stringify(saved.items)===JSON.stringify(before.items), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=90).decode( + "utf-8" + ) + ) + assert result == {"rows": True, "items": True, "indexes": True} diff --git a/resources/tester/test_est_quantity_plain_export.py b/resources/tester/test_est_quantity_plain_export.py new file mode 100644 index 000000000..d9d73258c --- /dev/null +++ b/resources/tester/test_est_quantity_plain_export.py @@ -0,0 +1,68 @@ +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] + + +def test_delete_plain_row_rejects_document_bookmark_and_preserves_styles(): + script = r"""const w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs'); + const doc='
plain
',sheet=q.fromWpt('N','sheet',doc,[],{}); + const out=w.removeRows(doc,sheet,new Set(),new Set([0])); + const marked='
title
'; + const other=q.fromWpt('N','sheet',marked,[],{});let rejected=false; + try{w.removeRows(marked,other,new Set(),new Set([0]))}catch{rejected=true} + process.stdout.write(JSON.stringify({style:out.includes(''),removed:!out.includes('plain'),rejected}));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT)) + assert result == {"style": True, "removed": True, "rejected": True} + + +@pytest.mark.parametrize("edit", [False, True]) +def test_delete_plain_row_in_installed_file_when_sample_exists(edit): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),z=require('zlib'),m=require('./tools/est-test/import-estx.cjs'), + e=require('./tools/est-test/export-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs'),c=require('./tools/est-test/quantity-calculate.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes);let candidate=null; + const valid=s=>{const x=c.calculateQuantity(s),n=s.rows.reduce((a,r)=>a+r.markers.length,0); + return !x.errors.length&&x.checks.length===n&&x.checks.every(v=>Number.isFinite(v.stored)&&v.stored===v.calculated)&&x.items.every((v,i)=>v.GN===s.items[i].GN)}; + for(const s of p.quantitySheets){if(!valid(s))continue;const r=p.tables.MulMainDat.find(x=>x.Code===s.Code); + let b=Buffer.from(r.Editor.binary,'base64');if(b[0]===120)b=z.inflateSync(b);const doc=new TextDecoder('euc-kr').decode(b); + for(let i=0;iJSON.stringify(x)===JSON.stringify(row)).length!==1)continue; + const rows=s.rows.filter((_,j)=>j!==i); + if(process.argv[2]==='edit'){ + const j=rows.findIndex(row=>row.markers.length===1&&rows.filter(x=>JSON.stringify(x.markers)===JSON.stringify(row.markers)).length===1); + if(j<0)continue;rows[j]={...rows[j],LineData:' '+rows[j].LineData}; + } + try{const source=w.removeRows(doc,s,new Set(),new Set([i]));if(!valid({...s,rows}))continue; + w.editDocument(source,{...s,rows:s.rows.filter((_,j)=>j!==i)},{...s,rows}); + candidate={sheet:s,rows};break}catch{}} + if(candidate)break; + } + if(!candidate)throw Error('no safe plain-row fixture'); + const s=candidate.sheet,before=structuredClone(s);s.rows=candidate.rows; + const out=e.exportEstx(p),q=m.importEstx(out),saved=q.quantitySheets.find(x=>x.Code===s.Code),a=m.unzip(bytes),b=m.unzip(out); + process.stdout.write(JSON.stringify({rows:JSON.stringify(saved.rows)===JSON.stringify(s.rows), + items:JSON.stringify(saved.items)===JSON.stringify(before.items), + indexes:[...a].filter(([name])=>name.endsWith('.idx')).every(([name,data])=>data.equals(b.get(name)))}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script, str(sample), "edit" if edit else "plain"], cwd=ROOT, timeout=90 + ).decode("utf-8") + ) + assert result == {"rows": True, "items": True, "indexes": True} + + +def test_delete_duplicate_plain_row_is_rejected_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'); + const p=m.importEstx(fs.readFileSync(process.argv[1])),sheet=p.quantitySheets.find(s=>s.Code==='N0000000071'); + const index=sheet.rows.findIndex((r,i)=>!r.markers.length&&sheet.rows.some((x,j)=>i!==j&&JSON.stringify(x)===JSON.stringify(r))); + if(index<0)throw Error('duplicate fixture unavailable');sheet.rows.splice(index,1); + try{e.exportEstx(p);process.stdout.write('false')}catch(error){process.stdout.write(JSON.stringify(error.message.includes('중복')))}""" + assert json.loads(subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT)) diff --git a/resources/tester/test_est_quantity_workflow.py b/resources/tester/test_est_quantity_workflow.py index 24e085695..3692342db 100644 --- a/resources/tester/test_est_quantity_workflow.py +++ b/resources/tester/test_est_quantity_workflow.py @@ -249,6 +249,25 @@ def test_insert_and_move_quantity_rows_keeps_item_results_linked(): assert result["forward"]["errors"] +def test_new_quantity_codes_avoid_items_and_existing_markers(): + result = run( + "math", + """(()=>{ + const handlers={};globalThis.document={addEventListener:(name,fn)=>handlers[name]=fn}; + const sheet={Code:'N1',Name:'test',options:{},items:[{Code:'r000000'}], + rows:[{LineData:'1 = \\x120\\x12',markers:['r000001']}]}; + const state={quantitySheet:'N1',project:{quantitySheets:[sheet],tables:{EstMainDat:[]}}}; + globalThis.EstApp={state,change:fn=>fn(),render:()=>{}};EstQuantity.install(); + const click=selector=>handlers.click({target:{closest:s=>s===selector?{dataset:{}}:null}}); + click('[data-quantity-add]');click('[data-quantity-add-item]'); + const code=sheet.rows[1].markers[0]; + const doc='
1
'; + return {codes:sheet.items.map(x=>x.Code),parsed:EstQuantity.fromWpt('N1','test',doc,[],{}).rows[0].markers}; + })()""", + ) + assert result == {"codes": ["r000000", "r000002", "r000003"], "parsed": ["r000002"]} + + def test_manual_and_selected_sheet_totals_validate_before_application(): result = run( "math", diff --git a/resources/tester/test_est_rea_parity.py b/resources/tester/test_est_rea_parity.py index 18ec845ed..4385b388c 100644 --- a/resources/tester/test_est_rea_parity.py +++ b/resources/tester/test_est_rea_parity.py @@ -651,3 +651,29 @@ def test_text_total_without_result_marker_evaluates_its_expression(): assert result["rows"][0]["TC"] == 20 assert result["rows"][1]["TC"] == 20 assert result["rows"][2]["TC"] == 0 + + +def test_numbered_speed_variable_attached_to_korean_label(): + result = run( + "math", + r"""EstText.calculate([ + {Index:1,Code:'C'}, + {LineData:' 주유소V41=25 입구 V51=12'}, + {LineData:' Q = V41 + V51 = \x1237\x12'} + ])""", + ) + assert result["errors"] == [] + assert result["rows"][2]["TC"] == 37 + + +def test_invalid_unmarked_component_does_not_reuse_previous_declaration(): + result = run( + "math", + r"""EstText.calculate([ + {Index:1,Code:'C'}, + {SCheck:-3,LineData:' C = \x03100\x03'}, + {SCheck:-3,LineData:' C = \x03200\x03 + MISSING'} + ])""", + ) + assert result["errors"] + assert result["rows"][2].get("TC") != 100 diff --git a/resources/tester/test_est_text_parser.py b/resources/tester/test_est_text_parser.py new file mode 100644 index 000000000..daaceaec0 --- /dev/null +++ b/resources/tester/test_est_text_parser.py @@ -0,0 +1,190 @@ +from test_est_rea_parity import run + + +def test_mean_sea_level_labels_do_not_break_variable_declarations(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'D3 = {+}1.9m [ M.S.L ]'}, + {Index:0, LineData:'1개소당 50㎥ q = 1.53 M.S.L h1 = 3.841'}, + {Index:0, LineData:'Q = D3 + q + h1 = \\x127.27\\x12'} + ]; + return EstText.calculate(rows); + })()""", + ) + assert result["errors"] == [] + assert result["checks"][0]["calculated"] == 7.27 + + +def test_equals_in_description_is_not_a_formula_separator(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'강판{T=22} 67.659kg × 317.40 = \\x1221474.97\\x12'} + ]; + return EstText.calculate(rows); + })()""", + ) + assert result["errors"] == [] + assert result["checks"][0]["calculated"] == 21474.97 + + +def test_korean_length_unit_ja_is_not_a_variable(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, SCheck:-1, LineData:'\\x0e목형원(제조)\\x0e : \\x03115706\\x03 × 0.16인 ×80자 = \\x061\\x06\\x121481036.8\\x12'} + ]; + return EstText.calculate(rows); + })()""", + ) + assert result["errors"] == [] + assert result["checks"][0]["calculated"] == 1481036.8 + + +def test_selected_group_can_be_assigned_to_a_variable(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'0.15 = \\x06A\\x06\\x120.15\\x12'}, + {Index:0, LineData:'0.87 = \\x06A\\x06\\x120.87\\x12'}, + {Index:0, LinkIndex:62, LineData:'Q = (\\x07A\\x07\\x1a0.15 + 0.87\\x1a) = \\x121\\x12'}, + {Index:0, LineData:'Q × 2 = \\x122\\x12'} + ]; + return EstText.calculate(rows); + })()""", + ) + assert result["errors"] == [] + assert result["checks"][-1]["calculated"] == 2.04 + + +def test_independent_results_on_one_line_do_not_replace_the_declared_variable(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'Q = 3.43'}, + {Index:0, LineData:'L = 2인×2조÷(Q×8hr) = \\x120.15\\x12 2×2÷(1×8) = \\x120.5\\x12'}, + {Index:0, SCheck:-1, LineData:'노무비 : 226122 × L = \\x1233918.3\\x12'} + ]; + const initial = EstText.calculate(rows); + rows[1].LineData = 'Q = 4'; + const changed = EstText.calculate(rows); + return {initial, changed}; + })()""", + ) + assert result["initial"]["errors"] == [] + assert [check["calculated"] for check in result["initial"]["checks"][:2]] == [0.5, 0.15] + assert result["initial"]["rows"][3]["TC"] == 33918.3 + assert [check["calculated"] for check in result["changed"]["checks"][:2]] == [0.5, 0.13] + assert result["changed"]["rows"][3]["TC"] == 29395.8 + + +def test_unmarked_cost_component_declaration_is_available_to_later_formulas(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, SCheck:-3, LineData:'D = 1,466원'}, + {Index:0, SCheck:-3, LineData:'E = D ×0.075 = 1,466 ×0.075 = \\x12109.9\\x12'} + ]; + const first = EstText.calculate(rows); + rows[1].LineData = 'D = 2,000원'; + const second = EstText.calculate(rows); + return {first, second}; + })()""", + ) + assert result["first"]["errors"] == [] + assert result["first"]["rows"][2]["TC"] == 109.9 + assert result["second"]["rows"][2]["TC"] == 150 + + +def test_repeated_saved_subtotal_tracks_the_preceding_subtotal(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, SCheck:-1, LineData:'100 = \\x12100\\x12'}, + {Index:0, SCheck:-3, LineData:'50 = \\x1250\\x12'}, + {Index:0, SCheck:-5, LineData:'소계 : \\x12150\\x12'}, + {Index:0, SCheck:-5, LineData:'소계 : \\x12150\\x12'} + ]; + const first=EstText.calculate(rows); + rows[1].LineData='110 = \\x12100\\x12'; + const second=EstText.calculate(rows); + return {first, second}; + })()""", + ) + assert [row["TC"] for row in result["first"]["rows"][-2:]] == [150, 150] + assert [row["TC"] for row in result["second"]["rows"][-2:]] == [160, 160] + + +def test_comment_only_declaration_waits_for_a_later_numeric_value(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'L = {공사구간 1/4}'}, + {Index:0, LineData:'L = 1.213 km'}, + {Index:0, LineData:'Q = L × 2 = \\x122.43\\x12'} + ]; + const normal=EstText.calculate(rows); + rows[1].LineData='L ='; + const invalid=EstText.calculate(rows); + return {normal, invalid}; + })()""", + ) + assert result["normal"]["errors"] == [] + assert result["normal"]["rows"][3]["TC"] == 2.43 + assert result["invalid"]["errors"] + + +def test_unmarked_linked_price_declaration_excludes_name_and_specification_tags(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, SCheck:-3, LinkIndex:5, + LineData:'C = \\x0e구역화물\\x0e\\x13(10.5톤,230km)\\x13 \\x03293,690\\x03'}, + {Index:0, LineData:'Q = C × 0.1 = \\x1229369\\x12'} + ]; + const original = EstText.calculate(rows); + const refreshed = EstText.calculate(rows, {}, () => ({JC: 400000})); + return {original, refreshed}; + })()""", + ) + assert result["original"]["errors"] == [] + assert result["original"]["rows"][2]["TC"] == 29369 + assert result["refreshed"]["rows"][2]["TC"] == 40000 + + +def test_tonnes_per_area_is_a_unit_in_a_numeric_formula(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'0.1 ㎥/㎡ × 2.10 t/㎡ = \\x120.21\\x12'} + ]; + const first=EstText.calculate(rows); + rows[1].LineData='0.1 ㎥/㎡ × 2.50 t/㎡ = \\x120.21\\x12'; + const second=EstText.calculate(rows); + return {first, second}; + })()""", + ) + assert result["first"]["errors"] == [] + assert result["first"]["rows"][1]["TC"] == 0.21 + assert result["second"]["rows"][1]["TC"] == 0.25 diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 4c03a34fe..9028ccd41 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -423,6 +423,73 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 백그라운드 상태 감시 및 자동 알림: `tools/est-test/monitor.cjs`를 통해 15초 주기로 `orca.exe` CLI(`shell: false`, timeout 4~5s)를 호출하여 CODEX 워크트리의 안티그래비티 작업 상태와 EST 5175 서버 헬스를 조회하고, 상태 변경 시에만 `monitor.log`에 기록(최대 512KB 순환). 안티그래비티 완료/대기 전이 시 Codex Orchestration Run(`run_cdb1558b9280`)으로 단발성 자동 알림(`status`/`messageId=msg_2c7bca748f91`)이 실제 전달됨을 확인(중복 폭주 방지 플래그 적용, 민감 환경변수 미기록, 재시작/자동지시 없음). ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] JDL 출력에서 중복 자원 코드와 내부 고정금액 코드 충돌로 다른 단가에 연결되는 문제를 차단하고 왕복 검증한다. + - 자체 검증: 중복 자원 세 개를 모두 내보내며 모호한 내역은 저장 금액으로 독립 전개하고, 기존 `LITERAL2` 코드와 내부 고정금액이 충돌하지 않음 확인. JDL·업무 회귀 30개, 중복 자원 세 건과 중복 일위대가의 잘못된 상세 연결 방지 별도 시험 통과. 설치 교량 자료의 JDL 출력·재가져오기 내역 합계 7,989,852,162원 유지. 변환 불가 구성 경고는 유지한다. + +- [x] 수량산출 행 이동·삭제 시 문서 표지의 반복 전체 검색을 줄이고 공유 표지 보호 및 실제 ESTX 저장을 검증한다. + - 자체 검증: 표지 빈도를 한 번 집계하여 공유 표지 삭제·이동 차단. 숫자가 비슷한 표지와 추가 표지 1만 개 시험, 실제 설치 ESTX 이동·삭제·재열기 및 인덱스 보존을 포함한 수량·원본 저장 회귀 55개 통과(6분 21초). 문서 해석의 다른 반복 처리에는 이 최적화를 적용하지 않았다. + +- [x] 선택 구간 합계를 변수에 대입한 산식을 계산한다. + - 자체 검증: 가시설 원본의 Q 대입과 이후 변수 참조 재현, 산근·수량 업무 시험 63개 통과. 설치 자료 24개 변경 전후 비교에서 이번 산식 보완 전체의 오류 25→15건, 저장 결과 일치 114,371→114,378건, 파일별 일치 감소 없음. + +- [x] 산출근거 숫자 뒤 길이 단위 `자`를 인식한다. + - 자체 검증: 교량 자료의 목형 산식 재현, 산근·수량 업무 시험 62개 통과. 설치 자료 24개 변경 전후 비교에서 이번 산식 보완 전체의 오류 25→16건, 저장 결과 일치 114,371→114,377건, 파일별 일치 감소 없음. + +- [x] 산출근거 설명의 중괄호 안 등호를 계산식 구분에서 제외한다. + - 자체 검증: 강판 규격 `{T=22}` 원본 산식 재현, 산근·수량 업무 시험 61개 통과. 설치 자료 24개 변경 전후 비교에서 앞선 M.S.L 보완을 포함해 수식 오류 25→18건, 저장 결과 일치 114,371→114,375건, 파일별 일치 감소 없음. + +- [ ] 원본 수식 옆 수평 구분선을 분수로 인식하는 사례를 조사한다. + - 진행: 리터럴 옆 구분선만 제외하는 변경으로 오류가 추가 감소했지만 해양 자료의 저장 결과 일치가 30건 감소해 변경을 되돌렸다. 후속 변수와 원본 저장값의 관계를 확인한 뒤 처리한다. 원인: 같은 원본의 H1 식은 음의 수심 변수를 사용하지만 바로 뒤 숫자 대입은 양수이며 저장값 5.3도 양수 대입과 일치한다. 이를 임의 수정하지 않으면 후속 Q가 200.15 대신 229.12가 되어 차이가 전파된다. + +- [x] 산출근거의 평균해수면 표기(M.S.L)를 수식 변수와 구분한다. + - 자체 검증: 실제 해양 자료의 대괄호 및 같은 줄 표기 재현 시험, 산근 회귀 45개 통과. 설치 자료 24개 변경 전후 비교에서 수식 오류 25→22건, 저장 결과 일치 114,371건 유지, 파일별 일치 건수 감소 없음. 나머지 오류는 계속 조사한다. + +- [x] 수량산출서의 ESTX 저장 안내를 현재 지원 범위에 맞춘다. + - 자체 검증: 화면과 README의 이전 안내를 수정하고 수량 업무 시험 15개 통과, 변경 코드 포맷 확인. 복합 원본 구조와 새 산출서는 계속 제한된다. + +- [x] 기존 수량산출서의 목록 항목 추가를 ESTX 메모리 표에 저장한다. 기존 항목과 문서·인덱스 보존, 실제 파일 저장·재읽기 및 잘못된 코드 거부를 검증한다. + - 자체 검증: 원본 메모리 표의 신규 행 형식이 확인되는 기존 문서에 한해, 연결되지 않은 항목 추가·재읽기를 지원. 설치 main.estx의 항목 추가 후 기존 목록·산출 행·전체 인덱스 보존 및 중복 코드 거부 시험, 잘못된 연결·길이·속성 거부 시험 통과. 관련 회귀 31개 통과, 최종 변경 후 추가 시험 2개 통과. 항목 삭제·내역 연결과 빈 원본 목록은 계속 제한. 원본 EST 실행 검증은 하지 못함. +- [x] 수량산출서에서 참조되지 않는 목록 항목 삭제를 기존 메모리 표에 저장하고 참조 항목은 보호한다. 실제 ESTX 재읽기와 인덱스 보존을 검증한다. + - 자체 검증: 설치 main.estx의 미참조 목록 삭제 후 나머지 목록·산출 행·전체 인덱스 보존 및 재읽기, 문서 표지와 숨겨진 원본·현재 내역 연결에 의한 삭제 거부 확인. 코드가 없는 기존 목록 항목도 이름 수정 가능함을 실제 파일로 확인. 관련 회귀 35개 및 최종 추가·삭제 보호 5개, 기존 코드 누락 호환 1개와 원본·현재 연결 보호 3개 통과. 원본 EST 실행 검증은 하지 못함. +- [ ] 설치된 JDL 검사 DLL의 호출 규약과 결과를 조사해 생성 파일 검사에 활용한다. + - 진행: 32비트 DLL의 `GetDLLVersion` 호출은 1.03을 반환. `IsCertifiedJDL`은 디렉터리 인자를 받는 경로로 확인했으며, 기본 내보내기 파일을 임시 폴더에 풀어 호출한 결과 하위 바이트 0. `JDLCheck`는 한 문자열 인자의 stdcall 경로로 확인했으나 임시 자료 호출이 15초 안에 끝나지 않아 별도 프로세스를 종료했다. 반환 의미와 화면 없이 완료하는 방법은 미확인이며 인증 실패나 검사 성공으로 단정하지 않는다. +- [x] 기존 수량산출서 이름을 원본 제목 표에 저장한다. 기존 문서·목록·인덱스 보존과 실제 파일 재읽기를 검증한다. + - 자체 검증: 설치 main.estx에서 이름 변경 후 재읽기와 문서·목록·인덱스 불변 확인. 제목 표가 따로 수정된 충돌을 거부하는 시험과 숨겨진 연결 보호 시험 통과. 원본 EST 실행 검증은 하지 못함. +- [x] 원본 수량 목록이 비어 있는 기존 산출서에도 항목 추가를 지원하고 실제 파일로 재읽기·기존 서식 보존을 검증한다. + - 자체 검증: 설치 main.estx의 빈 목록 5개에서 같은 필드 구성을 확인하고, 한 문서에 첫 항목을 추가해 재읽기·산출 행·전체 인덱스 보존 및 입력 거부 시험 2개 통과. 확인된 필드 구성이 아닌 빈 목록은 거부한다. 관련 전체 회귀 38개는 직전 변경까지 통과. 원본 EST 실행 검증은 하지 못함. +- [x] 신규 수량 항목과 산출 행의 결과 코드를 원본 WPTools가 인식하는 숫자 형식으로 생성하고 기존 코드·표지와 충돌하지 않도록 검증한다. + - 자체 검증: 기존 목록 코드와 문서 결과 표지를 모두 피해 `r000000` 형식의 새 코드를 생성하고, 생성한 표지를 WPTools 파서가 인식하는지 확인. 수량 업무 시험 15개 통과. 기존 JSON의 코드는 변경하지 않으며 산출 행 ESTX 추가 저장은 아직 미지원. +- [x] 기존 수량산출서 끝에 단일 결과 표지를 가진 산출 행을 추가해 ESTX에 저장하고, 문서 표지·계산 수량·기존 서식·인덱스를 실제 파일로 검증한다. + - 자체 검증: 설치 main.estx에서 새 산출식 `2+3`의 결과 표지·목록 수량 5를 저장 후 재읽고 기존 산출 행·전체 인덱스 보존 확인. 중복 표지·연결 누락·복합 표지 거부 및 수량 업무 시험 통과. 관련 전체 회귀 42개 통과. 중간 삽입·이동·삭제와 원본 EST 실행 검증은 남음. +- [x] 기존 수량산출서 중간에 새 산출 행을 삽입해 ESTX에 저장하고 기존 행·서식·계산 수량을 검증한다. + - 자체 검증: 설치 main.estx 중간 삽입·끝 추가 후 결과 수량 5, 기존 행·인덱스 보존과 재읽기 확인. 기본 서식 선언보다 앞선 삽입은 거부. 관련 전체 회귀 43개 통과(최종 서식 위치 보호는 별도 시험 2개 추가 통과). 원본 EST 실행 검증은 하지 못함. +- [x] 원본 수량산출서에서 단일 결과 표지 산출 행의 삭제를 지원하고 참조·서식 보존을 실제 파일로 검증한다. + - 자체 검증: 설치 main.estx의 산출 행과 연결 목록 항목을 삭제해 재읽기, 나머지 행·항목·전체 인덱스 보존 확인. 글자 서식 선언 보존, 공유 표지·원본/현재 내역 연결 삭제 거부 확인. 관련 회귀 46개 통과. 복합 표지·내역에 연결된 행은 계속 제한하며 원본 EST 실행 검증은 하지 못함. +- [x] 기존 산출 행을 이동할 때 문서 표지와 글자 서식을 보존해 ESTX에 저장한다. 위험한 공유 표지·서식 구조는 거부하고 실제 파일로 검증한다. + - 자체 검증: 설치 main.estx의 결과 표지 행 두 개를 이동해 재읽기·목록 수량·전체 인덱스 보존 확인. 이동 전 선언되지 않은 글자 서식, 이동 범위 내 서식 재정의, 공유 표지 거부 및 기존 표지 15만 개에서도 신규 표지 생성 확인. 전체 회귀 48개 통과 후 보강 범위 18개 추가 통과. 이동과 동시에 행 내용을 편집하거나 복잡한 공유 서식을 이동하는 경우는 제한하며 원본 EST 실행 검증은 하지 못함. +- [x] 고유 결과 표지가 있는 산출 행을 이동하면서 내용을 함께 수정할 때 표지로 원본 행을 찾아 서식을 보존한다. + - 자체 검증: 합성 문서에서 이동과 산식 문자 수정을 동시에 수행해 원래 결과 표지 유지 확인. 설치 main.estx에서도 표지 행을 이동·편집 후 재읽기, 수량·인덱스 보존 확인. 이동 관련 시험 4개 및 전체 수량·저장 회귀 50개 통과. 중복 표지와 복잡한 공유 서식의 이동은 계속 제한하며 원본 EST 실행 검증은 하지 못함. +- [x] JDL 기계 상세 중 수량 1이고 비목별 합계가 원본 제목 금액과 일치하는 일반 행을 편집 가능한 상세 행으로 가져온다. 특수 형식과 불일치는 원래 금액 보존 경로를 유지하고 재계산을 검증한다. + - 자체 검증: 합성 JDL에서 두 상세 행의 비목 금액 분리와 개별 수정 후 기계 합계 130→140, 불일치 및 수량 2의 보존 경로 확인. JDL·업무 회귀 29개 통과. 상세 산식·기초 단가 자동 연결과 원본 검사 DLL 인증은 미지원으로 경고하며 원본 JDL 실자료 비교는 하지 못함. +- [x] 결과 표지가 없는 일반 산출 행의 삭제를 지원하고, 모호한 중복 행과 문서 표지·서식은 보호한다. + - 자체 검증: 설치 main.estx에서 일반 행을 삭제한 후 문서·수량 목록·전체 인덱스의 재읽기 확인. 실제 중복 행 삭제와 문서 표지가 있는 행 삭제 거부, 글자 서식 선언 보존 확인. 관련 전체 회귀 53개 통과. 삭제와 다른 행 편집을 함께 하거나 복합 표지가 있는 행은 제한하며 원본 EST 실행 검증은 하지 못함. +- [x] 일반 산출 행 삭제와 고유 표지 산출 행 편집을 함께 저장하고 두 행의 대응을 검증한다. + - 자체 검증: 설치 main.estx에서 일반 행 삭제와 고유 결과 표지 행 편집을 함께 저장하고 재읽기·수량 목록·인덱스 보존 확인. 일반 삭제, 문서 표지 보호 및 중복 행 거부를 포함해 관련 전체 회귀 54개 통과. 이동이나 표지 없는 다른 행 편집을 함께 한 경우는 제한하며 원본 EST 실행 검증은 하지 못함. + +- [ ] 설치 ESTX 24개 대조에서 나온 수식 오류와 계산값 차이를 조사하고 근거 있는 파서 오류를 수정한다. 원본 오류와 미확인 사항을 구분하고 관련 시험과 실제 파일 재대조로 확인한다. + - 진행: 한 줄의 독립 산식 결과를 앞선 결과와 이어 붙이거나 최초 선언 변수를 덮던 문제, 결과 표지 없는 비목 선언 누락, 연속된 동일 소계의 0원 처리 수정. 설치 파일 24개 재대조에서 일치 991,444→991,456/991,517, 수식 오류 35→34, 다른 파일의 일치 수 감소 없음. 관련 회귀 82개와 수정 시나리오 3개 통과. 붙은 변수 처리의 다른 항목 회귀 69건은 재현 후 해당 변경을 되돌림. 잔여 차이·오류는 미완료. + - 추가 진행: 설명만 있는 선언, 표지로 감싼 비목 단가, `t/㎡` 단위와 한글 설명에 붙은 번호 속도 변수 처리 수정. 설치 파일 24개 재대조에서 991,457/991,517 일치, 수식 오류 25건(기존 35건), 파일별 일치 수 감소 없음. 관련 회귀 86개 통과. 원본 산식과 저장값이 모순되는 경우 등을 계속 조사한다. + - 추가 확인: 오류가 있는 비목 선언이 이전 변수값을 재사용하지 않는 시험 통과. 산림토목·조경·측량설계의 일부 잔여 차이는 저장 정수와 현재 소수 계산값의 정밀도 차이이며, 근거 없이 정밀도를 변경하지 않는다. 상수공사 1건은 결과 표지가 `1.7`에만 적용되고 뒤에 `9`가 남은 원본 형식이다. 테스트 서버는 테일스케일 주소에서 HTTP 200, 자동 재시작 감시 프로세스 동작 확인. + +- [x] 두 사용자 실무 자료의 편집·업무·저장·출력 경로 확인. + - 자체 검증: 첫 파일 보정본과 두 번째 원본에서 Edge 수량 수정 및 취소, 당초 비교, 기성, 물가변동, 연차 40/60 계산, 분리된 시험 저장소 저장·재열기 통과. 내역 인쇄 미리보기 각각 103·144행 전체 확인. 전체 Excel 및 ESTX 내보내기→재가져오기에서 계산 오류 0·도급액 508,328,000원·1,352,416,000원 유지. 관련 회귀 70개 통과. 원본 EST 프로그램 실행 비교는 수행하지 않음. + +- [x] 기계경비 주연료 역할명과 연결 자재명이 다른 정상 행의 오류 오탐 수정. 연결 누락과 규격·단위 오류는 유지. + - 자체 검증: 오류 검색 시험 13개 통과. 두 실무 파일의 주연료 오탐 23건·3건 제거; 연결 누락·단위 불일치와 일반 자재 품명 차이는 계속 검출. 첫 파일은 원본 괄호 오류가 남아 있으며 저장된 0.56을 유지하는 보정 작업본에서 계산 오류·자료 오류 0, 내역 103행 금액과 도급액 불변을 확인. 원본 EST 실행 동작은 미확인. + +- [x] JDL 기계경비·단가산출 저장 금액을 편집 가능한 호표로 가져오기, 미변환 일위 구성 금액 보존. 상세 산식은 원본에 보존하고 경고. + - 자체 검증: 고정 금액 변경→자동계산→내역 전파, 소수 금액, 일위 상세 불일치의 금액 중복 방지 및 미변환 잔액, 변경 없는 JDL 바이트 보존 시험. Edge에서 JDL 불러오기 경고와 기계·단가산출 편집 확인. 전용 회귀 270개 통과 후 최종 관련 계산·입출력 시험 60개 통과. 기존 원가 계산 시험에서 누락된 조정 추적값도 복구. + - [x] 수량산출 항목을 대용량 내역에 반영할 때 반복 검색을 줄이고 기존 연결·중복 오류 판정을 유지한다. - 자체 검증: 5,000개 항목과 내역 연결 및 중복 식별자 거부 확인. 수량 기능 시험 13개 통과. - [x] 수량산출 화면에서 내역 선택 목록을 필요할 때 생성하여 대용량 화면의 과도한 옵션 생성을 줄인다. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 9fa19fe44..f53810045 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -43,8 +43,9 @@ - **Excel `.xlsx`**: CAD를 제외한 전체 표의 스칼라 필드, 직접 작성 수식표·예정공정표·계약심사 입력·연차 수식·미확정 기성과 수량산출서의 문서 정보·산출 행·항목을 내보내고 다시 읽어 해당 자료를 교체합니다. 수량산출서는 `QDocuments`, `QLines`, `QItems`, 미확정 기성은 `ProgressDrafts`, `ProgressInputs` 시트를 함께 가져옵니다. 확정 기성의 계약 자료와 누계는 Excel 가져오기 시 유지합니다. 수식표·수량산출서·공정표의 내역 행 연결도 보존합니다. 기존 PkID의 바이너리는 보존하며 코드 변경·중복과 미계산 수식/오류 셀을 거부합니다. 산출근거의 내부 표지는 Excel이 열 수 있는 형식으로 바꿔 기록하고 다시 읽을 때 복원합니다. 출처 단가 수정은 연결 내역에 전파합니다. 파일에 바이너리·원본 글자 서식·확정 기성 자료·당초 사본은 넣지 않으므로 전체 백업은 JSON을 사용합니다. - **물가변동 Excel**: `Escalation` 시트에서 입찰 당시 단가, 적용 제외 관련 수량과 자동 여부를 편집하고 `EscalationSettings`에서 조사 도급액을 입력합니다. 행 식별자가 없거나 중복되면 가져오기를 중단합니다. - **조달청 XML**: DB에 보관된 공사원가호환규정 XSD 1.0.5의 요소 순서와 필드를 사용합니다. 작성업체·공사구분을 입력하며 코드/문구 길이와 연결 자료를 검사합니다. 내역·일위·산근·기계·기초 단가·원가를 내보내고 다시 읽습니다. 연결 없는 고정 금액·기계 상세는 독립 단가로 전개하며 변환 전에 알립니다. 가져온 저장 금액과 원문을 보존하고 자동계산을 끕니다. 이는 제출기관 인증/등록 검사나 최신 규정 적합 판정을 뜻하지 않습니다. -- **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 저장 금액과 함께 불러오며 특수 행은 원문에 보존하고 경고합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다. -- **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 수량산출서는 기존 서식 구간 안의 텍스트 편집을 저장하며 표지와 문서 구조는 보존합니다. 한 줄에서 표지로 구분된 여러 구간을 함께 수정하거나 연속 글자 서식에 걸친 텍스트를 수정할 수도 있습니다. 새 텍스트에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 문서, 행 추가·삭제·인덱스 키 변경, 표지나 위첨자 등 복잡한 구조를 가로지르는 편집과 목록·연결 변경, 웹 기성·연차·당초 사본은 지원하지 않아 저장을 거부합니다. 이 범위는 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다. +- **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 불러오고 미변환 구성은 저장 금액으로 보존합니다. 기계경비와 단가산출은 저장된 비목별 금액을 편집 가능한 호표로 가져오며 상세 산식은 원본 JDL에 보존합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다. +- 중복 자원 코드로 연결 대상을 구분할 수 없는 JDL 출력은 해당 내역의 저장 금액을 독립 단가로 기록하고 경고합니다. 자원 행 자체는 각각 보존합니다. +- **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 기존 수량산출서의 이름과 항목을 수정하고, 확인된 형식의 항목과 단일 결과 표지 산출 행을 추가·삭제할 수 있습니다. 고유한 일반 행 삭제와 조건에 맞는 산출 행 이동도 지원합니다. 연결된 항목이나 중복·복합 표지, 불명확한 서식 구조는 보호를 위해 일부 작업을 거부합니다. 기존 서식 구간의 텍스트를 편집할 때 새 글에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 수량산출서, 인덱스 키 변경, 수량의 내역 연결 변경, 웹 기성·연차·당초 사본은 ESTX 저장을 지원하지 않아 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다. - **예정공정표**: 내역 행을 공정으로 일괄 추가하고 내역 금액으로 가중치를 갱신할 수 있습니다. 코드가 없거나 같은 코드가 반복된 행은 내역의 행 식별자로 구분합니다. 기존 공정의 기간·진도·이름은 유지합니다. 공정별 기간 막대와 입력 진도의 가중평균을 표시하며, 날짜 역전·잘못된 진도를 차단합니다. 공휴일/공기 산정 기준을 임의 적용하지 않습니다. - **기성 진도 반영**: 예정공정표에서 최근 확정 회차의 누계 수량을 해당 회차 계약 수량으로 나누어 연결된 공정의 진도를 갱신할 수 있습니다. 수량이 0이거나 기성 내역에 연결되지 않은 공정은 기존 진도를 유지합니다. 0~100%를 벗어나는 결과는 갱신을 중단합니다. - **기성 초기화**: 기성 화면에서 모든 회차와 확정 결과를 한 번에 초기화할 수 있습니다. 내역과 다른 업무 자료는 유지하고 실행 취소로 복원할 수 있습니다. @@ -67,7 +68,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요 수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다. -원본 ESTX의 기존 수량산출 항목은 품명·규격을 수정해 ESTX에 저장할 수 있습니다. 연결 변경과 새 항목 추가·삭제는 계속 JSON으로 보관합니다. +원본 ESTX의 기존 수량산출 항목은 품명·규격을 수정하고, 지원되는 형식의 항목과 산출 행을 추가·삭제해 ESTX에 저장할 수 있습니다. 내역 연결 변경은 JSON으로 보관합니다. 품명·규격만 변경한 경우 기존 산식의 저장 수량과 재계산 수량이 다르더라도 산식과 저장 수량을 그대로 유지합니다. 산출서 문구를 수정해도 계산 결과와 결과 표지가 바뀌지 않으면 원래 저장 수량을 보존합니다. 원본 결과가 재계산값과 다른 문서에서 계산 결과를 바꾸는 편집은 JSON으로 보관해야 합니다. diff --git a/tools/est-test/export-estx.cjs b/tools/est-test/export-estx.cjs index d0c0e207c..3ca460415 100644 --- a/tools/est-test/export-estx.cjs +++ b/tools/est-test/export-estx.cjs @@ -2,7 +2,7 @@ const zlib = require("node:zlib"); const { importEstx, unzip, Values, memoryRows } = require("./import-estx.cjs"); const { pack } = require("./archive.cjs"); const { updateOptions } = require("./export-options.cjs"); -const { updateQuantities } = require("./export-quantity.cjs"); +const { updateQuantities, quantityFields } = require("./export-quantity.cjs"); const { updatePrintOptions } = require("./export-print.cjs"); const signature = Buffer.from("068abe8e592364cb403d71d2e3bc64d0", "hex"); const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null); @@ -68,7 +68,7 @@ function value(data) { return Buffer.concat([Buffer.from([1]), ...data.map(value), Buffer.from([0])]); throw Error("지원하지 않는 TPF0 값"); } -function memory(buffer, rows) { +function memory(buffer, rows, allowListChanges = false) { const compressed = buffer[0] === 0x78, bytes = compressed ? zlib.inflateSync(buffer) : buffer, reader = new Values(bytes), @@ -76,10 +76,26 @@ function memory(buffer, rows) { while (reader.position < bytes.length) values.push(reader.value()); const names = values[1].filter((v, i) => i % 10 === 0), entries = values[4]; - if (rows.length !== entries.length) + if (rows.length !== entries.length && !allowListChanges) throw Error("원가 서식 행 추가/삭제는 ESTX 인덱스 재생성이 필요합니다."); + const oldRows = allowListChanges ? memoryRows(buffer) : [], + oldCodes = new Map(oldRows.map((row, index) => [row.Code, index])), + sameCodes = + allowListChanges && + same( + oldRows.map((row) => row.Code), + rows.map((row) => row.Code), + ); + if (allowListChanges && !sameCodes && oldCodes.size !== oldRows.length) + throw Error("수량 목록의 기존 코드가 중복됩니다."); + if ( + allowListChanges && + rows.some((row) => !oldCodes.has(row.Code)) && + ((!entries.length && !same(names, quantityFields)) || entries.some((entry) => entry[0] !== 1)) + ) + throw Error("수량 목록의 신규 행 형식을 확인할 수 없습니다."); values[4] = rows.map((row, i) => [ - entries[i][0], + entries[allowListChanges && !sameCodes ? oldCodes.get(row.Code) : i]?.[0] ?? 1, ...names.flatMap((name) => (row[name] == null ? [true] : [false, row[name]])), ]); const result = Buffer.concat([Buffer.from("TPF0"), ...values.map(value)]); diff --git a/tools/est-test/export-quantity.cjs b/tools/est-test/export-quantity.cjs index 87be104cd..35fef94e1 100644 --- a/tools/est-test/export-quantity.cjs +++ b/tools/est-test/export-quantity.cjs @@ -1,7 +1,19 @@ const zlib = require("node:zlib"); const quantity = require("./native-quantity.cjs"); const { calculateQuantity } = require("./quantity-calculate.cjs"); +const { memoryRows } = require("./import-estx.cjs"); const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); +const quantityFields = [ + "Index", + "Code", + "LinkIndex", + "LinkCode", + "Name", + "Size", + "Unite", + "GN", + "GN_", +]; function escapeText(text) { return [...text] @@ -91,9 +103,62 @@ function editStyledRuns(div, before, after, parse) { // Change text inside an existing styled run, never WPTools tags or bookmarks. function editDocument(text, oldSheet, sheet) { const withoutLabels = (items) => items.map(({ Name, Size, Unite, ...rest }) => rest); + const oldCodes = new Set(oldSheet.items.map((item) => item.Code)); + const sameCodes = same( + oldSheet.items.map((item) => item.Code), + sheet.items.map((item) => item.Code), + ); + const codes = new Set(); + for (const item of sheet.items) { + if (!sameCodes && (!item.Code || codes.has(item.Code))) + throw Error("수량 항목 코드가 없거나 중복됩니다."); + codes.add(item.Code); + } + for (const item of oldSheet.items) { + if (codes.has(item.Code)) continue; + if ( + item.TargetCode || + item.TargetKey || + item.LinkCode || + item.LinkIndex || + oldSheet.rows.some( + (row) => (row.markers || []).includes(item.Code) || row.LineData.includes(item.Code), + ) + ) + throw Error("참조 중인 수량 항목은 ESTX에서 삭제할 수 없습니다."); + } + const added = sheet.items.filter((item) => !oldCodes.has(item.Code)); + for (const item of added) { + if (!/^r\d{6}$/.test(item.Code || "")) throw Error("수량 항목 코드가 없거나 중복됩니다."); + if (item.TargetCode || item.TargetKey || item.LinkCode || item.LinkIndex) + throw Error("신규 수량 항목의 내역 연결은 ESTX 저장을 지원하지 않습니다."); + if ( + [ + [item.Name, 80], + [item.Size, 80], + [item.Unite, 8], + ].some( + ([value, limit]) => value != null && (typeof value !== "string" || value.length > limit), + ) || + (item.GN != null && (typeof item.GN !== "number" || !Number.isFinite(item.GN))) + ) + throw Error("신규 수량 항목의 이름·규격·단위 또는 수량 형식 오류"); + if ( + Object.keys(item).some( + (key) => !["Code", "Name", "Size", "Unite", "GN", "TargetCode"].includes(key), + ) + ) + throw Error("신규 수량 항목에 저장할 수 없는 속성이 있습니다."); + } if ( - !same({ ...oldSheet, rows: [], items: [] }, { ...sheet, rows: [], items: [] }) || - !same(withoutLabels(oldSheet.items), withoutLabels(sheet.items)) || + !same( + { ...oldSheet, Name: "", rows: [], items: [] }, + { ...sheet, Name: "", rows: [], items: [] }, + ) || + !same( + withoutLabels(oldSheet.items.filter((item) => codes.has(item.Code))), + withoutLabels(sheet.items.filter((item) => oldCodes.has(item.Code))), + ) || oldSheet.rows.length !== sheet.rows.length ) throw Error("수량산출서 목록·연결·행 구조 변경은 ESTX 저장을 지원하지 않습니다."); @@ -151,18 +216,274 @@ function editDocument(text, oldSheet, sheet) { return result; } +function appendRows(text, sheet, rows) { + if (!/]*>/gi)] + .map((match) => quantity.entities(match[0]).match(/Name:["']([^"']+)["']/)?.[1]) + .filter(Boolean), + ); + let nextTag = 1; + for (const match of text.matchAll(/\bTag=(\d+)/gi)) + nextTag = Math.max(nextTag, Number(match[1]) + 1); + for (const row of rows) { + const matches = [...row.LineData.matchAll(/\x12([^\x12]*)\x12/g)]; + const code = row.markers?.[0]; + if ( + matches.length !== 1 || + row.markers?.length !== 1 || + !/^r\d{6}$/.test(code || "") || + used.has(code) + ) + throw Error("추가 산출 행은 단일 결과 표지와 숫자 코드가 필요합니다."); + if (!sheet.items.some((item) => item.Code === code)) + throw Error("추가 산출 행에 연결된 수량 항목이 없습니다."); + used.add(code); + const [match] = matches; + const plain = + row.LineData.slice(0, match.index) + row.LineData.slice(match.index + match[0].length); + if (/[\x00-\x1f]|\^\(/.test(plain) || /[\x00-\x1f]/.test(match[1])) + throw Error("추가 산출 행의 복합 표지나 위첨자는 ESTX 저장을 지원하지 않습니다."); + const tag = nextTag++; + text += `
${escapeText(row.LineData.slice(0, match.index))}${escapeText(match[1])}${escapeText(row.LineData.slice(match.index + match[0].length))}
\r\n`; + } + return text; +} + +function insertRows(text, sheet, inserts) { + let staged = text; + const snippets = inserts.map(({ row, at }) => { + const next = appendRows(staged, sheet, [row]); + const snippet = next.slice(staged.length); + staged = next; + return { at, snippet }; + }); + const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + let index = 0; + const stylePosition = text.search(/]*>[\s\S]*?<\/div>/gi, (div, offset) => { + if (!quantity.fromWpt("", "", declarations + div, [], {}).rows.length) return div; + const before = snippets + .filter((item) => item.at === index) + .map((item) => item.snippet) + .join(""); + if (before && offset < stylePosition) + throw Error("추가 위치 앞에 기본 글자 서식이 없어 ESTX에 저장할 수 없습니다."); + index++; + return before + div; + }); + return ( + updated + + snippets + .filter((item) => item.at === index) + .map((item) => item.snippet) + .join("") + ); +} + +function removeRows(text, sheet, codes, plainIndexes = new Set()) { + const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + const tags = tagCounts(text); + let index = 0; + const removed = new Set(); + const removedPlain = new Set(); + const document = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { + const parsed = quantity.fromWpt("", "", declarations + div, [], {}).rows; + if (!parsed.length) return div; + const rowIndex = index++, + row = sheet.rows[rowIndex], + code = row?.markers?.[0]; + if (!row || !same(parsed[0], row)) throw Error("수량산출서 원본 행 대응 오류"); + if (!codes.has(code) && !plainIndexes.has(rowIndex)) return div; + if (plainIndexes.has(rowIndex)) { + if (row.markers?.length || /<(?!\/?div\b|c\b|cs\b)[^>]+>/i.test(div)) + throw Error("문서 표지나 특수 서식이 있는 산출 행은 ESTX에서 삭제할 수 없습니다."); + removedPlain.add(rowIndex); + return [...div.matchAll(/]*>/gi)].map((match) => match[0]).join(""); + } + const item = sheet.items.find((value) => value.Code === code); + const begins = [...div.matchAll(/]*\bTag=(\d+)[^>]*>/gi)]; + if ( + row.markers.length !== 1 || + removed.has(code) || + !item || + item.TargetCode || + item.TargetKey || + item.LinkCode || + item.LinkIndex || + begins.length !== 1 || + tags.get(begins[0]?.[1]) !== 2 || + /<(?!\/?div\b|c\b|cs\b|begin\b|end\b)[^>]+>/i.test(div) + ) + throw Error("다른 표지나 연결을 포함한 산출 행은 ESTX에서 삭제할 수 없습니다."); + removed.add(code); + return [...div.matchAll(/]*>/gi)].map((match) => match[0]).join(""); + }); + if ( + index !== sheet.rows.length || + removed.size !== codes.size || + removedPlain.size !== plainIndexes.size + ) + throw Error("삭제할 산출 행을 원본 문서에서 찾을 수 없습니다."); + return document; +} + +function tagCounts(text) { + const counts = new Map(); + for (const match of text.matchAll(/\bTag=(\d+)(?=\D|$)/gi)) + counts.set(match[1], (counts.get(match[1]) || 0) + 1); + return counts; +} + +function markerCounts(values) { + const counts = new Map(); + for (const row of values) { + if (!row.markers?.length) continue; + const key = JSON.stringify(row.markers); + counts.set(key, (counts.get(key) || 0) + 1); + } + return counts; +} + +function reorderRows(text, sheet, rows) { + const before = markerCounts(sheet.rows), + after = markerCounts(rows); + const keyFor = (row) => { + const markers = JSON.stringify(row.markers); + return row.markers?.length && before.get(markers) === 1 && after.get(markers) === 1 + ? `marker:${markers}` + : `row:${JSON.stringify(row)}`; + }; + const remaining = new Map(); + sheet.rows.forEach((row, index) => { + const key = keyFor(row); + if (!remaining.has(key)) remaining.set(key, { indexes: [], next: 0 }); + remaining.get(key).indexes.push(index); + }); + const order = []; + for (const row of rows) { + const entry = remaining.get(keyFor(row)); + if (!entry || entry.next >= entry.indexes.length) return { text, rows: sheet.rows }; + order.push(entry.indexes[entry.next++]); + } + if (order.length !== sheet.rows.length || order.every((value, index) => value === index)) + return { text, rows: sheet.rows }; + const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + const divs = [...text.matchAll(/]*>[\s\S]*?<\/div>/gi)].filter( + (match) => quantity.fromWpt("", "", declarations + match[0], [], {}).rows.length, + ); + if (divs.length !== sheet.rows.length) throw Error("수량산출서 원본 행 대응 오류"); + if ( + divs.some( + (match, index) => + !same(quantity.fromWpt("", "", declarations + match[0], [], {}).rows[0], sheet.rows[index]), + ) + ) + throw Error("수량산출서 원본 행 대응 오류"); + const changed = order + .map((source, index) => (source === index ? -1 : index)) + .filter((n) => n >= 0); + const first = divs[changed[0]].index; + const styles = new Set( + [...text.slice(0, first).matchAll(/]*\bnr=(\d+)\b/gi)].map((m) => m[1]), + ); + const last = divs[changed.at(-1)]; + const redefined = new Set( + [...text.slice(first, last.index + last[0].length).matchAll(/]*\bnr=(\d+)\b/gi)].map( + (m) => m[1], + ), + ); + const tags = tagCounts(text); + for (const index of changed) { + const div = divs[index][0]; + const referenced = [ + ...[...div.matchAll(/]*\bcs=(\d+)/gi)].map((m) => m[1]), + ...[...div.matchAll(/]*\bnr=(\d+)/gi)].map((m) => m[1]), + ]; + const begins = [...div.matchAll(/]*\bTag=(\d+)[^>]*>/gi)].map((m) => m[1]); + const ends = [...div.matchAll(/]*\bTag=(\d+)[^>]*>/gi)].map((m) => m[1]); + if ( + / !styles.has(id) || redefined.has(id)) || + /<(?!\/?div\b|c\b|begin\b|end\b)[^>]+>/i.test(div) || + !same(begins.slice().sort(), ends.slice().sort()) || + begins.some((id) => tags.get(id) !== 2) + ) + throw Error("공유 표지나 글자 서식이 있는 산출 행은 ESTX에서 이동할 수 없습니다."); + } + let index = 0; + const document = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { + if (!quantity.fromWpt("", "", declarations + div, [], {}).rows.length) return div; + return divs[order[index++]][0]; + }); + return { text: document, rows: order.map((source) => sheet.rows[source]) }; +} + function updateQuantities(project, base, cp949, memory) { const sheets = project.quantitySheets || [], originals = base.quantitySheets || []; if (sheets.length !== originals.length) throw Error("수량산출서 추가·삭제는 지원하지 않습니다."); const rows = project.tables.MulMainDat.map((r) => ({ ...r })); + const titles = (project.tables.MulTitleDat || []).map((r) => ({ ...r })); const savedSheets = sheets.slice(); for (let i = 0; i < sheets.length; i++) { if (same(sheets[i], originals[i])) continue; const row = rows.find((r) => r.Code === originals[i].Code); const oldRow = base.tables.MulMainDat.find((r) => r.Code === originals[i].Code); + if (sheets[i].Name !== originals[i].Name) { + const matches = titles.filter((title) => title.Code === originals[i].Code), + oldMatches = (base.tables.MulTitleDat || []).filter( + (title) => title.Code === originals[i].Code, + ); + if (matches.length !== 1 || oldMatches.length !== 1 || matches[0].Name !== oldMatches[0].Name) + throw Error("수량산출서 원본 제목 데이터 충돌"); + if (typeof sheets[i].Name !== "string" || !sheets[i].Name.trim()) + throw Error("수량산출서 이름을 입력하세요."); + matches[0].Name = sheets[i].Name; + } if (!row?.Editor?.binary || !same(row.Editor, oldRow.Editor)) throw Error("수량산출서 원본 편집기 데이터 충돌"); + const removed = new Set( + originals[i].items + .filter((item) => !sheets[i].items.some((next) => next.Code === item.Code)) + .map((item) => item.Code), + ); + if (removed.size) + for (const item of [ + ...(base.tables.EstMainDat || []), + ...(project.tables.EstMainDat || []), + ]) { + if (!item.MulCode?.binary) continue; + const bytes = Buffer.from(item.MulCode.binary, "base64"); + let text; + try { + const wide = + bytes.length > 1 && + bytes.length % 2 === 0 && + [...bytes].every((byte, index) => index % 2 === 0 || byte === 0); + text = wide + ? bytes.toString("utf16le") + : new TextDecoder("euc-kr", { fatal: true }).decode(bytes); + } catch { + throw Error("원본 수량 연결을 확인할 수 없어 항목을 삭제할 수 없습니다."); + } + const [sheetCode, itemCode] = text.replace(/\0+$/, "").split("\t"); + if (sheetCode === originals[i].Code && removed.has(itemCode)) + throw Error("내역에서 참조 중인 수량 항목은 ESTX에서 삭제할 수 없습니다."); + } + const fields = Object.keys(memoryRows(Buffer.from(oldRow.MulList.binary, "base64"))[0] || {}); + const originalCodes = new Set(originals[i].items.map((item) => item.Code)); + const savedItems = (items) => + items.map((item) => + originalCodes.has(item.Code) + ? item + : Object.fromEntries( + (fields.length ? fields : quantityFields).map((field) => [ + field, + item[field] ?? null, + ]), + ), + ); const packed = Buffer.from(row.Editor.binary, "base64"), compressed = packed[0] === 0x78; const bytes = compressed @@ -170,21 +491,103 @@ function updateQuantities(project, base, cp949, memory) { : packed; const text = new TextDecoder("euc-kr", { fatal: true }).decode(bytes); if (!cp949(text).equals(bytes)) throw Error("수량산출서 원본 인코딩 보존 불가"); - let document = editDocument(text, originals[i], sheets[i]); + const presentMarkers = new Set(sheets[i].rows.flatMap((line) => line.markers || [])); + const deletedRows = new Set( + originals[i].rows.flatMap((line) => + (line.markers || []).filter((code) => removed.has(code) && !presentMarkers.has(code)), + ), + ); + const originalMarkers = new Set(originals[i].rows.flatMap((line) => line.markers || [])); + const existingRows = [], + inserts = []; + for (const line of sheets[i].rows) { + const marker = line.markers?.[0]; + if (line.markers?.length === 1 && !originalMarkers.has(marker) && !originalCodes.has(marker)) + inserts.push({ row: line, at: existingRows.length }); + else existingRows.push(line); + } + let retainedRows = originals[i].rows.filter( + (line) => !(line.markers || []).some((code) => deletedRows.has(code)), + ); + const plainIndexes = new Set(); + if (existingRows.length < retainedRows.length) { + const counts = new Map(); + const oldMarkers = markerCounts(retainedRows), + newMarkers = markerCounts(existingRows); + for (const line of originals[i].rows) { + const key = JSON.stringify(line); + counts.set(key, (counts.get(key) || 0) + 1); + } + let target = 0; + for (let n = 0; n < originals[i].rows.length; n++) { + const line = originals[i].rows[n]; + if ((line.markers || []).some((code) => deletedRows.has(code))) continue; + const key = JSON.stringify(line.markers); + const matchesMarker = + line.markers?.length && + oldMarkers.get(key) === 1 && + newMarkers.get(key) === 1 && + key === JSON.stringify(existingRows[target]?.markers); + if (target < existingRows.length && (same(line, existingRows[target]) || matchesMarker)) + target++; + else if (!line.markers?.length && counts.get(JSON.stringify(line)) === 1) + plainIndexes.add(n); + else throw Error("중복되거나 함께 수정된 산출 행은 ESTX에서 삭제할 수 없습니다."); + } + if (target !== existingRows.length) throw Error("삭제할 산출 행을 확인할 수 없습니다."); + retainedRows = originals[i].rows.filter( + (line, n) => + !plainIndexes.has(n) && !(line.markers || []).some((code) => deletedRows.has(code)), + ); + } + if (existingRows.length !== retainedRows.length) + throw Error("수량산출 행 삭제는 ESTX 저장을 지원하지 않습니다."); + const existing = { ...sheets[i], rows: existingRows }; + const source = + deletedRows.size || plainIndexes.size + ? removeRows(text, originals[i], deletedRows, plainIndexes) + : text; + let retained = { + ...originals[i], + rows: retainedRows, + items: originals[i].items.filter((item) => !deletedRows.has(item.Code)), + }; + const reordered = reorderRows(source, retained, existingRows); + retained = { ...retained, rows: reordered.rows }; + let document = editDocument(reordered.text, retained, existing); + if (inserts.length) { + document = insertRows(document, sheets[i], inserts); + if ( + !same( + quantity.fromWpt( + sheets[i].Code, + sheets[i].Name, + document, + sheets[i].items, + sheets[i].options, + ), + sheets[i], + ) + ) + throw Error("추가 산출 행 재읽기 검산 실패"); + } if (same(sheets[i].rows, originals[i].rows)) { if (!same(row.MulList, oldRow.MulList)) throw Error("수량산출서 목록 데이터 충돌"); - row.MulList = { - binary: memory(Buffer.from(row.MulList.binary, "base64"), sheets[i].items).toString( - "base64", - ), - }; + if (!same(sheets[i].items, originals[i].items)) + row.MulList = { + binary: memory(Buffer.from(row.MulList.binary, "base64"), sheets[i].items, true).toString( + "base64", + ), + }; + savedSheets[i] = { ...sheets[i], items: savedItems(sheets[i].items) }; continue; } const baseline = calculateQuantity(originals[i]), calculated = calculateQuantity(sheets[i]); if (calculated.errors.length || baseline.errors.length) throw Error(`수량산출서 계산 오류: ${calculated.errors[0] || baseline.errors[0]}`); - const markerCount = originals[i].rows.reduce((total, row) => total + row.markers.length, 0); + const markerCount = originals[i].rows.reduce((total, row) => total + row.markers.length, 0), + updatedMarkerCount = sheets[i].rows.reduce((total, row) => total + row.markers.length, 0); const results = (value) => ({ checks: value.checks.map(({ name, calculated }) => ({ name, calculated })), items: value.items.map(({ Code, GN }) => ({ Code, GN })), @@ -193,7 +596,7 @@ function updateQuantities(project, base, cp949, memory) { sheet.rows.map((row) => [...row.LineData.matchAll(/\x12[^\x12]*\x12/g)].map((m) => m[0])); if ( baseline.checks.length === markerCount && - calculated.checks.length === markerCount && + calculated.checks.length === updatedMarkerCount && baseline.checks.every((value) => Number.isFinite(value.calculated)) && calculated.checks.every((value) => Number.isFinite(value.calculated)) && same(resultMarkers(originals[i]), resultMarkers(sheets[i])) && @@ -202,17 +605,18 @@ function updateQuantities(project, base, cp949, memory) { if (!same(row.MulList, oldRow.MulList)) throw Error("수량산출서 목록 데이터 충돌"); if (!same(sheets[i].items, originals[i].items)) row.MulList = { - binary: memory(Buffer.from(row.MulList.binary, "base64"), sheets[i].items).toString( + binary: memory(Buffer.from(row.MulList.binary, "base64"), sheets[i].items, true).toString( "base64", ), }; + savedSheets[i] = { ...sheets[i], items: savedItems(sheets[i].items) }; const output = cp949(document); row.Editor = { binary: (compressed ? zlib.deflateSync(output) : output).toString("base64") }; continue; } if ( baseline.checks.length !== markerCount || - calculated.checks.length !== markerCount || + calculated.checks.length !== updatedMarkerCount || baseline.checks.some((v) => !Number.isFinite(v.stored) || v.stored !== v.calculated) || baseline.items.some((v, n) => v.GN !== originals[i].items[n].GN) ) @@ -240,11 +644,11 @@ function updateQuantities(project, base, cp949, memory) { if (!same(row.MulList, oldRow.MulList)) throw Error("수량산출서 목록 데이터 충돌"); if (!same(calculated.items, originals[i].items)) { row.MulList = { - binary: memory(Buffer.from(row.MulList.binary, "base64"), calculated.items).toString( + binary: memory(Buffer.from(row.MulList.binary, "base64"), calculated.items, true).toString( "base64", ), }; - current.items = calculated.items; + current.items = savedItems(calculated.items); } savedSheets[i] = current; const output = cp949(document); @@ -253,7 +657,15 @@ function updateQuantities(project, base, cp949, memory) { return { ...project, quantitySheets: savedSheets, - tables: { ...project.tables, MulMainDat: rows }, + tables: { ...project.tables, MulMainDat: rows, MulTitleDat: titles }, }; } -module.exports = { editDocument, updateQuantities }; +module.exports = { + editDocument, + updateQuantities, + appendRows, + insertRows, + removeRows, + reorderRows, + quantityFields, +}; diff --git a/tools/est-test/jdl.cjs b/tools/est-test/jdl.cjs index 996eecb36..f58f920fc 100644 --- a/tools/est-test/jdl.cjs +++ b/tools/est-test/jdl.cjs @@ -73,11 +73,16 @@ function output(project) { }; const tables = Object.fromEntries(Object.keys(schema).map((k) => [k, []])), warnings = [], - map = new Map(); + map = new Map(), + ambiguous = new Set(); let serial = 0; function source(kind, row) { const key = kind + ":" + row.Code; - if (map.has(key)) return map.get(key); + if (row.Code && map.has(key)) { + map.delete(key); + ambiguous.add(key); + warnings.push(`${row.Code}: 중복 자원 코드는 독립 금액으로 전개합니다.`); + } const prefix = kind === 5 ? "M" : kind === 6 ? "L" : "W", code = prefix + String(++serial).padStart(10, "0"); const price = @@ -95,7 +100,7 @@ function output(project) { ...price, F_ORICODE: "00000000000", }); - map.set(key, code); + if (row.Code && !ambiguous.has(key)) map.set(key, code); return code; } for (const [kind, table] of [ @@ -119,6 +124,8 @@ function output(project) { for (const title of project.tables.HeadTitleDat || []) { const parts = unitGroups.get(title.Code); if ( + !title.Code || + ambiguous.has("103:" + title.Code) || num(title.SC) || !parts?.length || parts.some( @@ -206,7 +213,7 @@ function output(project) { num(row.CalcIndex) === 3 || Math.abs(gn * (m + l + e) - num(row.TC)) > 0.000001 ) { - key = source(101, { + key = source(0, { Code: "LITERAL" + ++serial, Name: row.Name, Size: row.Size, @@ -320,6 +327,140 @@ function read(bytes, project) { } let unitPk = 0; const unitWarnings = []; + const machineParts = new Map(); + for (const part of rows["jpart.txt"]) { + if (!machineParts.has(part.F_CODE)) machineParts.set(part.F_CODE, []); + machineParts.get(part.F_CODE).push(part); + } + let machineDetailed = false, + machineFallback = false; + for (const title of rows["junit.txt"]) { + const gc = num(title.F_MPRICE), + nc = num(title.F_LPRICE), + jc = num(title.F_EPRICE); + const amounts = { GC: gc, NC: nc, JC: jc, SC: 0, TC: gc + nc + jc }; + const parts = machineParts.get(title.F_CODE) || []; + const uniqueSequences = new Set(parts.map((part) => part.F_SEQ)); + const basic = + parts.length > 0 && + uniqueSequences.size === parts.length && + parts.every( + (part) => + Boolean(part.F_SEQ) && + num(part.F_QQTY) === 1 && + !["%", "%"].includes(part.F_UNIT) && + ["", "0"].includes(part.F_SPROPER) && + ["", "T"].includes(part.F_LOADBM) && + ["", "T"].includes(part.F_SAVEBM) && + !num(part.F_SRATE) && + ["", "0"].includes(part.F_KWAN) && + ["", "0"].includes(part.F_CLASS), + ) && + [ + ["F_MPRICE", gc], + ["F_LPRICE", nc], + ["F_EPRICE", jc], + ].every( + ([field, value]) => + Math.abs(parts.reduce((sum, part) => sum + num(part[field]), 0) - value) < 1e-6, + ); + project.tables.MachMainDat.push({ + PkID: project.tables.MachMainDat.length + 1, + Index: 1, + Code: title.F_CODE, + JdlSummary: true, + Name: title.F_DES, + Size: title.F_SIZE, + Unite: title.F_UNIT, + ...amounts, + }); + if (basic) { + machineDetailed = true; + for (const part of parts) { + const GC = num(part.F_MPRICE), + NC = num(part.F_LPRICE), + JC = num(part.F_EPRICE); + project.tables.MachMainDat.push({ + PkID: project.tables.MachMainDat.length + 1, + Index: 0, + CalcIndex: 1, + JdlSummary: true, + JdlSourceCode: part.F_CODE1, + Name: part.F_DES, + Size: part.F_SIZE, + Unite: part.F_UNIT, + GN: 1, + GC, + NC, + JC, + SC: 0, + TC: GC + NC + JC, + }); + } + } else { + machineFallback = true; + project.tables.MachMainDat.push({ + PkID: project.tables.MachMainDat.length + 1, + Index: 0, + CalcIndex: 1, + JdlSummary: true, + Name: "JDL 저장 금액 (상세 산식 미변환)", + ...amounts, + }); + } + } + if (machineDetailed) + unitWarnings.push( + "일반 기계 상세는 저장 금액별로 표시하며, 산식과 원천 단가 연결은 원본 JDL에 보존됩니다.", + ); + if (machineFallback) + unitWarnings.push( + "일부 기계경비는 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다.", + ); + if (rows["jpart.txt"].some((r) => !byCode.has(r.F_CODE) || byCode.get(r.F_CODE).kind !== 105)) + unitWarnings.push("부모 기계 단가가 없는 상세 행은 원본 JDL에만 보존됩니다."); + for (const title of rows["danga.txt"]) { + const gc = num(title.F_MPRICE), + nc = num(title.F_LPRICE), + jc = num(title.F_EPRICE); + project.tables.CostMainDat.push({ + PkID: project.tables.CostMainDat.length + 1, + Index: 1, + Code: title.F_CODE, + JdlSummary: true, + Name: title.F_DES, + Size: title.F_SIZE, + Unite: title.F_UNIT, + GC: gc, + NC: nc, + JC: jc, + SC: 0, + TC: gc + nc + jc, + }); + // shortcut: JDL 단가 산출 상세는 원문에만 보존; 산식 형식 확인 후 상세 변환. + for (const [key, value, check] of [ + ["GC", gc, -2], + ["NC", nc, -1], + ["JC", jc, -3], + ]) { + const literal = value.toLocaleString("en-US", { + useGrouping: false, + maximumFractionDigits: 20, + }); + project.tables.CostMainDat.push({ + PkID: project.tables.CostMainDat.length + 1, + Index: 0, + SCheck: check, + JdlSummary: true, + Name: "JDL 저장 금액", + LineData: `${literal} = \x12${literal}\x12`, + [key]: value, + TC: value, + }); + } + } + if (rows["danga.txt"].length) + unitWarnings.push("단가산출은 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다."); const unitCodes = new Set(rows["unit.txt"].map((r) => r.F_CODE)); const partsByUnit = new Map(); for (const part of rows["unitpart.txt"]) { @@ -330,11 +471,15 @@ function read(bytes, project) { const gc = num(title.F_MPRICE), nc = num(title.F_LPRICE), jc = num(title.F_EPRICE); + const parts = partsByUnit.get(title.F_CODE) || []; + let incomplete = !parts.length; + const included = { GC: 0, NC: 0, JC: 0 }; project.tables.HeadMainDat.push({ PkID: ++unitPk, Index: 1, Code: title.F_CODE, TitleCode: title.F_CODE, + JdlSummary: true, Name: title.F_DES, Size: title.F_SIZE, Unite: title.F_UNIT, @@ -344,7 +489,7 @@ function read(bytes, project) { SC: 0, TC: gc + nc + jc, }); - for (const part of partsByUnit.get(title.F_CODE) || []) { + for (const part of parts) { const source = byCode.get(part.F_CODE1); const basic = source && @@ -355,12 +500,24 @@ function read(bytes, project) { ["", "0"].includes(part.F_KWAN) && ["", "0"].includes(part.F_CLASS); if (!basic) { + incomplete = true; unitWarnings.push(`일위 ${title.F_CODE}/${part.F_SEQ}: 특수 구성은 원문에만 보존`); continue; } const gc = num(part.F_MCOST), nc = num(part.F_LCOST), jc = num(part.F_ECOST); + const quantity = num(part.F_QQTY) / (["%", "%"].includes(part.F_UNIT) ? 100 : 1); + const fixedAmount = [ + [gc, num(part.F_MPRICE)], + [nc, num(part.F_LPRICE)], + [jc, num(part.F_EPRICE)], + ].some(([amount, price]) => Math.abs(amount - Math.trunc(quantity * price)) > 0.000001); + if (fixedAmount) + unitWarnings.push(`일위 ${title.F_CODE}/${part.F_SEQ}: 상세의 저장 금액을 고정합니다.`); + included.GC += gc; + included.NC += nc; + included.JC += jc; project.tables.HeadMainDat.push({ PkID: ++unitPk, Index: 0, @@ -369,7 +526,8 @@ function read(bytes, project) { Size: part.F_SIZE, Unite: part.F_UNIT, LinkIndex: source.kind, - CalcIndex: 0, + CalcIndex: fixedAmount ? 1 : 0, + ...(fixedAmount ? { JdlSummary: true } : {}), GN: num(part.F_QQTY), GU: num(part.F_MPRICE), NU: num(part.F_LPRICE), @@ -383,6 +541,27 @@ function read(bytes, project) { TU: num(part.F_MPRICE) + num(part.F_LPRICE) + num(part.F_EPRICE), }); } + if ( + incomplete || + ["GC", "NC", "JC"].some((key, i) => Math.abs([gc, nc, jc][i] - included[key]) > 0.000001) + ) { + const gc = num(title.F_MPRICE) - included.GC, + nc = num(title.F_LPRICE) - included.NC, + jc = num(title.F_EPRICE) - included.JC; + project.tables.HeadMainDat.push({ + PkID: ++unitPk, + Index: 0, + CalcIndex: 1, + JdlSummary: true, + Name: "JDL 저장 금액 차액", + GC: gc, + NC: nc, + JC: jc, + SC: 0, + TC: gc + nc + jc, + }); + unitWarnings.push(`일위 ${title.F_CODE}: 상세와 저장 합계의 차액을 보존합니다.`); + } } if (rows["unitpart.txt"].some((r) => !unitCodes.has(r.F_CODE))) unitWarnings.push("부모 일위대가가 없는 상세 행은 원문에만 보존"); diff --git a/tools/est-test/native-exchange.js b/tools/est-test/native-exchange.js index 5a63e5078..ad728eea3 100644 --- a/tools/est-test/native-exchange.js +++ b/tools/est-test/native-exchange.js @@ -579,7 +579,14 @@ const EstExchange = (() => { }); if (key !== EstApp.state.documentKey || revision !== EstApp.state.revision) throw Error("작업이 변경되었습니다. 다시 불러오세요."); - if (confirm("현재 작업 대신 JDL 내역서를 여시겠습니까?")) EstApp.setProject(next); + if (confirm("현재 작업 대신 JDL 내역서를 여시겠습니까?")) { + EstApp.setProject(next); + if (next.importWarnings?.length) + EstApp.showDialog( + "JDL 가져오기 확인", + `
    ${next.importWarnings.map((warning) => `
  • ${EstApp.esc(warning)}
  • `).join("")}
`, + ); + } } catch (error) { EstApp.state.notice = error.message; EstApp.render(); diff --git a/tools/est-test/native-issues.js b/tools/est-test/native-issues.js index fd2e89dcf..0ead044ff 100644 --- a/tools/est-test/native-issues.js +++ b/tools/est-test/native-issues.js @@ -56,6 +56,13 @@ const EstIssues = (() => { issues.push({ table, index, code: row.Code, message: "연결 코드 중복" }); else for (const field of ["Name", "Size", "Unite"]) { + if ( + table === "MachMainDat" && + Number(row.Index) === -2 && + field === "Name" && + EstNative.normalize(row.Name) === "주연료" + ) + continue; const a = row[field], b = matches[0].row[field]; if (a && b && EstNative.normalize(a) !== EstNative.normalize(b)) diff --git a/tools/est-test/native-machine.js b/tools/est-test/native-machine.js index 9d59556b5..6433ef177 100644 --- a/tools/est-test/native-machine.js +++ b/tools/est-test/native-machine.js @@ -30,9 +30,11 @@ const EstMachine = (() => { if (!head) return; components.forEach( (key) => - (head[key] = floating( - body.filter((r) => num(r.CalcIndex) < 2).reduce((n, r) => n + num(r[key]), 0), - )), + (head[key] = head.JdlSummary + ? body.filter((r) => num(r.CalcIndex) < 2).reduce((n, r) => n + num(r[key]), 0) + : floating( + body.filter((r) => num(r.CalcIndex) < 2).reduce((n, r) => n + num(r[key]), 0), + )), ); head.TC = sum(head); titles.push({ @@ -67,6 +69,7 @@ const EstMachine = (() => { continue; } if (![0, 2].includes(num(row.CalcIndex))) { + if (row.JdlSummary) row.TC = sum(row); body.push(row); continue; } diff --git a/tools/est-test/native-quantity.cjs b/tools/est-test/native-quantity.cjs index e0d7842d6..ea4c358a7 100644 --- a/tools/est-test/native-quantity.cjs +++ b/tools/est-test/native-quantity.cjs @@ -315,7 +315,18 @@ const EstQuantity = (() => { { esc, number, byId } = EstApp; byId("work-content").innerHTML = selector(state) + - `
${output.items.map((item, index) => ``).join("")}
산출코드품명규격단위저장 수량재계산 수량연결 내역코드작업
${esc(item.Code)}${number(sheet.items[index]?.GN ?? 0)}${number(item.GN)}${targetSelect(state.project, item, "sheet", index)}

${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.

${sheet.rows.map((row, index) => ``).join("")}
줄산출근거 편집작업
${index + 1}
`; + `
${output.items.map((item, index) => ``).join("")}
산출코드품명규격단위저장 수량재계산 수량연결 내역코드작업
${esc(item.Code)}${number(sheet.items[index]?.GN ?? 0)}${number(item.GN)}${targetSelect(state.project, item, "sheet", index)}

${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다.

${sheet.rows.map((row, index) => ``).join("")}
줄산출근거 편집작업
${index + 1}
`; + } + function nextItemCode(sheet) { + const codes = new Set([ + ...sheet.items.map((item) => item.Code), + ...sheet.rows.flatMap((row) => row.markers || []), + ]); + for (let index = 0; index < 1000000; index++) { + const code = `r${String(index).padStart(6, "0")}`; + if (!codes.has(code)) return code; + } + throw Error("수량 항목 코드를 더 만들 수 없습니다."); } function install() { document.addEventListener("focusin", (event) => { @@ -504,10 +515,7 @@ const EstQuantity = (() => { const addItem = event.target.closest("[data-quantity-add-item]"); if (addItem && sheet) { EstApp.change(() => { - const codes = new Set(sheet.items.map((it) => it.Code)); - let index = 0; - while (codes.has(`r${String(index).padStart(6, "0")}`)) index++; - const code = `r${String(index).padStart(6, "0")}`; + const code = nextItemCode(sheet); sheet.items.push({ Code: code, Name: "새 수량항목", @@ -540,7 +548,7 @@ const EstQuantity = (() => { const addRow = event.target.closest("[data-quantity-add]"); if (addRow && sheet) { EstApp.change(() => { - const code = `r${EstApp.uid()}`; + const code = nextItemCode(sheet); const at = addRow.dataset.index === undefined ? sheet.rows.length diff --git a/tools/est-test/native-rows.js b/tools/est-test/native-rows.js index e2d627ead..b5d28302f 100644 --- a/tools/est-test/native-rows.js +++ b/tools/est-test/native-rows.js @@ -92,6 +92,14 @@ const EstRows = (() => { function finish() { if (!current) return; Object.assign(current, add(body)); + if (current.JdlSummary) { + components.forEach((key) => { + current[key] = body + .filter((row) => num(row.CalcIndex) < 2) + .reduce((n, row) => n + num(row[key]), 0); + }); + current.TC = sum(current); + } titles.push({ ...current }); } for (const [index, row] of output.entries()) { @@ -132,6 +140,7 @@ const EstRows = (() => { } } amounts(row, options.HeadDigits || 0, options.TotalFit); + if (row.JdlSummary) row.TC = sum(row); } body.push(row); } diff --git a/tools/est-test/native-text.js b/tools/est-test/native-text.js index e014f1279..651c09cc7 100644 --- a/tools/est-test/native-text.js +++ b/tools/est-test/native-text.js @@ -30,6 +30,7 @@ const EstText = (() => { return clean( mathBraces(text) .replace(///g, "/") + .replace(/\[\s*M\.S\.L\s*\]|(?<=\s)M\.S\.L(?=\s|$)/gi, "") .replace( /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|[〔[][^〕]]*[〕]]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g, "", @@ -41,6 +42,8 @@ const EstText = (() => { "$1", ) .replace(/(?<=\d)\s*\/\s*(?:일|㎥|㎡|hr)(?=\s|$|[×*+)])/gi, "") + .replace(/(?<=\d)\s*자(?=\s*(?:[×*+÷/)=]|$))/g, "") + .replace(/(?<=\d)\s+t\s*\/\s*(?:㎡|m2)(?=\s|$|[×*+)])/gi, "") .replace(/[】]〕]\s*$/, "") .replace(/(?<=\))\s*(?:개|인|본|면|단|회|㎥|㎡)(?:\/(?:㎡|㎥|hr))?/g, "") .replace(/^\s*[-*•※⊙◎]?\s*[가-힣㎡㎥][가-힣㎡㎥\s]*\s+(?=[\d.(])/, "") @@ -72,7 +75,12 @@ const EstText = (() => { return; } components.forEach( - (key) => (head[key] = !hasAutoTotals ? floating(num(plainTotals[key])) : num(totals[key])), + (key) => + (head[key] = !hasAutoTotals + ? head.JdlSummary + ? num(plainTotals[key]) + : floating(num(plainTotals[key])) + : num(totals[key])), ); head.TC = sum(head); titles.push({ ...head }); @@ -263,6 +271,7 @@ const EstText = (() => { digits = 0; const assignedResults = new Map(); const assignedNames = new Map(); + const declaredNames = new Set(); let componentDeclaration; try { if (selectionError) throw Error(selectionError); @@ -307,10 +316,21 @@ const EstText = (() => { .replace(/=\s*$/, "") .trim(); if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); + body = body.replace(/^\s*[A-Za-z]\w*\s*=\s*/, ""); calculated = body ? EstExpression.evaluate(body, (key) => variables[key]) : 0; } } else if (matched && sc === -5) { const section = []; + const previous = rows[index - 1]; + const savedPrevious = String(input[index - 1]?.LineData || "").match(marker); + if ( + num(previous?.SCheck) === -5 && + num(previous?.Index) <= 0 && + num(previous?.Level) <= 0 && + savedPrevious && + numeric(savedPrevious[1]) === oldValue + ) + section.push(num(previous.TC)); for ( let j = index - 1; j >= 0 && @@ -336,11 +356,11 @@ const EstText = (() => { (value) => " ".repeat(value.length), ); const assignments = - sc < 0 + sc < 0 && matched ? [] : [ ...assignmentText.matchAll( - /(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z\u0370-\u03ff][A-Za-z0-9\u0370-\u03ff]*)\s*=/g, + /(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|(?<=[가-힣])(?=V\d+\s*=)|(?<=hr)\s+|[::]\s*|['"]\s*)([A-Za-z\u0370-\u03ff][A-Za-z0-9\u0370-\u03ff]*)\s*=/g, ), ].filter((match) => !/[+*/÷×−-]\s*$/.test(assignmentText.slice(0, match.index))); for (let j = 0; j < assignments.length; j++) { @@ -350,11 +370,13 @@ const EstText = (() => { if (!matched && /⇒.*─/.test(body)) body = body.split("⇒")[0]; if (!matched) body = body.split(/[≈≒]/)[0]; if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\*\s*$/, ""); + if (!matched && /^\s*\{\s*[가-힣][^{}]*\}\s*$/.test(body)) continue; body = mathBraces(body); if (/['"]\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(assignment[0])) body = body.replace(/['"]\s*$/, ""); if (!matched) body = body.split("{")[0]; body = body.split(/\s+[::]\s*/)[0].replace(/[,,]\s*$/, ""); + if (sc < 0 && !matched) body = stripLabels(body); if (body.includes("─") && calculated !== null) { variables[assignment[1]] = floating(calculated, 2, 1); continue; @@ -399,6 +421,7 @@ const EstText = (() => { variables[assignment[1]] = result ? floating(value, row.Precision ?? explicit?.digits ?? precision, explicit?.method ?? 1) : value; + declaredNames.add(assignment[1]); if (result) { const position = text.indexOf("\x12", assignment.index + assignment[0].length); assignedResults.set(position, variables[assignment[1]]); @@ -407,7 +430,7 @@ const EstText = (() => { } } if (matched && calculated === null && sc === 0 && text.includes("=")) { - let body = stripLabels(text.slice(0, text.indexOf("\x12"))) + let body = mathBraces(stripLabels(text.slice(0, text.indexOf("\x12")))) .replace(/\x06[^\x06]*\x06/g, "") .split("=") .filter((x) => x.trim()) @@ -438,11 +461,12 @@ const EstText = (() => { const results = [...text.matchAll(/\x12([-\d.,]+)\x12/g)]; const intermediate = results.length > 1 && sc < 0 && !explicit && row.Precision === undefined; - calculated = floating( - calculated, - intermediate ? (options.MidCostDiget ?? 2) : digits, - intermediate ? 2 : method, - ); + if (!row.JdlSummary || num(row.DigetIndex) || row.Precision != null || num(row.RoundMode)) + calculated = floating( + calculated, + intermediate ? (options.MidCostDiget ?? 2) : digits, + intermediate ? 2 : method, + ); const firstValue = calculated; let rendered = text.replace(marker, `\x12${calculated}\x12`); let assignmentName = assignedNames.get(results[0]?.index); @@ -450,14 +474,14 @@ const EstText = (() => { const previous = results[part - 1], next = results[part]; const format = row.ResultFormats?.[part]; - const operation = expression( - text.slice(previous.index + previous[0].length, next.index), - ).trim(); + const segment = text.slice(previous.index + previous[0].length, next.index), + operation = expression(segment).trim(), + separate = /=\s*$/.test(segment) && /^[\d.(]/.test(operation); calculated = assignedResults.has(next.index) ? assignedResults.get(next.index) : floating( EstExpression.evaluate( - expression(`${calculated}${operation}`), + separate ? operation : expression(`${calculated}${operation}`), (key) => variables[key], ), format?.digits ?? @@ -466,7 +490,7 @@ const EstText = (() => { : digits), format?.method ?? (intermediate && part < results.length - 1 ? 2 : method), ); - assignmentName = assignedNames.get(next.index) || assignmentName; + assignmentName = assignedNames.get(next.index) || (separate ? null : assignmentName); if (assignmentName) variables[assignmentName] = calculated; const old = numeric(next[1]); let occurrence = 0; @@ -512,11 +536,18 @@ const EstText = (() => { if (num(row.Level) === finalLevel) totals[component[sc]] = calculated; } } else if (!matched && component[sc] && text.includes("\x03") && !text.includes("\x1a")) { - const body = stripLabels(text.slice(text.indexOf(":") + 1)); - row.TC = floating( - EstExpression.evaluate(expression(body), (key) => variables[key]), - 1, - ); + const named = + text.indexOf(":") < 0 && + assignments.length === 1 && + declaredNames.has(assignments[0][1]); + if (named) row.TC = num(variables[assignments[0][1]]); + else { + const body = stripLabels(text.slice(text.indexOf(":") + 1)); + row.TC = floating( + EstExpression.evaluate(expression(body), (key) => variables[key]), + 1, + ); + } row.LineData = text; } else if (!matched && autoTotal(row) && component[sc]) { let value = clean(text).split(":").slice(1).join(":").trim(); diff --git a/tools/est-test/native-wonga.js b/tools/est-test/native-wonga.js index 44887c15c..a7e63c2c4 100644 --- a/tools/est-test/native-wonga.js +++ b/tools/est-test/native-wonga.js @@ -108,7 +108,8 @@ const EstWonga = (() => { function calculate(input, options = {}, bases = {}) { const rows = structuredClone(input || []), - errors = []; + errors = [], + adjustments = []; const overrides = new Map(); function pass() { @@ -127,6 +128,7 @@ const EstWonga = (() => { if (profit < 0) throw Error("도급액 고정: 조정할 이윤 행이 없습니다."); for (let iteration = 0; iteration < 100; iteration++) { const delta = num(rows[targetIndex].Cost) - target; + adjustments.push({ difference: delta }); if (delta === 0) break; overrides.set(profit, num(rows[profit].Cost) - delta); pass(); @@ -144,6 +146,7 @@ const EstWonga = (() => { return { rows, errors, + adjustments, total: targetIndex >= 0 ? num(rows[targetIndex].Cost) : num(rows.at(-1)?.Cost), }; } From 8a3ac5ad78bbf15ff24a35a7e730381b2aa6ef7c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:40:06 +0900 Subject: [PATCH 14/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC731):=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=82=98=EB=88=94=20=ED=99=95=EC=9D=B8=20=C2=B7=20?= =?UTF-8?q?=EA=B3=B5=EC=8B=9D=20=ED=8B=80=20=EC=B6=94=EA=B0=80=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=AC=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index ecf333148..9e18fa262 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -662,3 +662,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 730 (11-09 · 사용자 · 표로 다시 · 짧게) 테이블로 다시 표현해봐 . 이해하기 힘들어. 짧게 설명해줘. + +## 요구 731 (11-09 · 사용자 · 나눔 맞는 것 같음 · 단가산출 기초 화면의 공식 틀 추가는 필요 없어진 것인지) + +맞는것같네. 그럼 이제 단가산출 기초화면에서 공식 틀 추가는 필요없어진 것임? From 1724d98ae925a51319e95aeec8167f8873bd4ed1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 18:48:41 +0900 Subject: [PATCH 15/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC732):=20?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=C2=B7=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 9e18fa262..a6520956e 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -666,3 +666,8 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 731 (11-09 · 사용자 · 나눔 맞는 것 같음 · 단가산출 기초 화면의 공식 틀 추가는 필요 없어진 것인지) 맞는것같네. 그럼 이제 단가산출 기초화면에서 공식 틀 추가는 필요없어진 것임? + +## 요구 732 (11-09 · 사용자 · 「식」으로 변경 · 작업 진행 · 진행 뒤 엑셀 불러오기에서 더 결정할 것 알려 달라) + +식으로 변경하자. 이제 작업 진행하자. +진행 후 엑셀불러오기와 관련하여 뭐를 더 결정해야하는지 알랴줘. From 5c551c3105d2f1257ee335c0d2acf076c3b4d243 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:07:06 +0900 Subject: [PATCH 16/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC733):=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EB=AA=A9=EB=A1=9D=20=EB=8B=B5=20=C2=B7=20?= =?UTF-8?q?=EC=A1=B0=EC=82=AC=20=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index a6520956e..4db81abef 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -671,3 +671,14 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 식으로 변경하자. 이제 작업 진행하자. 진행 후 엑셀불러오기와 관련하여 뭐를 더 결정해야하는지 알랴줘. + +## 요구 733 (11-09 · 사용자 · 결정 목록 여섯에 대한 답 · 물음 + 드래그 그룹 이동 확인 — 수정 아님 · 조사부터) + +1. 이유 짧게 설명 +2. 규격이 다른 같으 기종의 경우 가져온 걸로 우리의 기계값을 바꾸면 되는거 아닌지. +3. 반올림 자리만 다른 경우 계산에는 정밀도 높게 계산하는지 확인하고 계산에 문제 없다면 확정. 품숫자가 다른 4개라는건 뭔지 짧게 설명 +4. 작업량 당 기계경비의 경우에는 단가산출이 어떤식으로 제목이 나오는지 확인. 기계만 있는 단가산출도 필요는 해보이는데 문제는 우리의 단가산출은 자유롭게 기계를 변경할 수 있음. 그렇다면 만약 단가산출 기초 제목으로 M02에서 찾을텐데 이때 기계명이 있는 이름의 단가산출이 있다면 사용자는 해당 단가산출 정보만 있을것으로 오해할것이기 때문에 6개의 단가산출이 기계명으로만 구분되어 있는 단가산출이라면 그냥 통합도 가능할 것으로 예상되어 물어봄(수정하라는 말은 아님) +5. 별칭 승인의 승인요청 시점이 사용자들이 저장하여 단가산출을 저장한 시점으로 보이는데 맞음? 그리고 별칭은 있어야 되는건 맞는데 혹시 경쟁사 프로그램의 출력물로 찾을 경우 이제 빈도가 줄었을 것으로 보임. 어느정도 수준인 확인 +6. 굴삭기 + 브레이카 는 사용자가 그룹 구분 없이 사용한다면 한 덩이로 그룹으로 구분한다면 자동으로 구분되겠지. 일반적으로는 구분되게 사용하는게 맞는것같음. +7. 드래그로 그룹이동 되는지 확인 +바로 수정작업 아님. 조사부터 진행. From 09f4de06073ae5ef7775a83f07f64963c2ff33af Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:14:37 +0900 Subject: [PATCH 17/29] =?UTF-8?q?feat(813-1):=20=EB=8B=A8=EA=B0=80?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=20=EA=B8=B0=EC=B4=88=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=E2=80=94=20=ED=92=88=EC=85=88=20=EC=9B=90?= =?UTF-8?q?=EB=AC=B8=20=EC=A0=9C=EB=AA=A9=EC=9C=BC=EB=A1=9C=2062=20?= =?UTF-8?q?=EB=B0=94=EA=BF=88(=EC=9D=B4=EB=A6=84=EC=97=90=20=EA=B8=B0?= =?UTF-8?q?=EA=B3=84=2044=20=C2=B7=20=EC=9A=B0=EB=A6=AC=EA=B0=80=20?= =?UTF-8?q?=EC=A7=80=EC=9D=80=20=EB=A7=90=2018)=20=C2=B7=20=ED=95=A9?= =?UTF-8?q?=EC=B9=A8=200=20=C2=B7=20=ED=8F=90=EA=B8=B0=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수식문서 · 옛 로직 JSON 「이름」 한 칸씩 · 키대장 원문번호 14 · 일위대가조합 메모 1 · 마스터 층 사본(SK 다섯 · SC 하나 · 노임 항목 하나) 「이름」 · 메모 - 빌더 794_2a2 이름 = 시멘트운반 · 철근운반(다시 돌려도 같은 이름) - 금액 전후 = 문서 1,519 · 셈 4,336(기본 + 옵션 표본) 모두 같음 · 791_1 지문 결과 1,519 같음 → 마스터 해시만 다시 뜸 - 시험 157_1 층따기 참고 줄 기대 이름을 새 이름으로(낡은 기대) - 목록 ⑥ 적용 전후 시트 Co-Authored-By: Claude Opus 5.5 --- .../요구_1104/_목록_813-1_기초이름정리.xlsx | Bin 67163 -> 71384 bytes resources/master_data/_키대장.json | 28 ++++++------ .../scripts/build_로직_M01보강_794_2a2.py | 6 +-- .../master_data/로직_건설품셈_01장_공통.json | 2 +- .../로직_건설품셈_01장_도로포장공사.json | 2 +- .../로직_건설품셈_01장_철골공사.json | 6 +-- .../로직_건설품셈_05장_기초공사.json | 4 +- .../로직_건설품셈_06장_방수공사.json | 2 +- .../로직_건설품셈_07장_돌공사.json | 12 ++--- .../로직_건설품셈_09장_기타공사.json | 4 +- .../로직_건설품셈_13장_플랜트설비공사.json | 22 ++++----- .../로직_산림품셈_03장_작업장_관리.json | 2 +- .../로직_산림품셈_04장_나무베기.json | 4 +- .../로직_산림품셈_05장_식재_및_녹화.json | 2 +- .../master_data/로직_산림품셈_08장_방제.json | 2 +- .../master_data/로직_산림품셈_09장_토공.json | 42 +++++++++--------- .../로직_산림품셈_10장_자재․장비_운반.json | 16 +++---- .../master_data/로직_산림품셈_11장_가설.json | 2 +- .../수식문서/수식_건설품셈_01장_공통.json | 2 +- .../수식_건설품셈_01장_도로포장공사.json | 2 +- .../수식문서/수식_건설품셈_01장_철골공사.json | 6 +-- .../수식문서/수식_건설품셈_05장_기초공사.json | 4 +- .../수식문서/수식_건설품셈_06장_방수공사.json | 2 +- .../수식문서/수식_건설품셈_07장_돌공사.json | 12 ++--- .../수식문서/수식_건설품셈_09장_기타공사.json | 4 +- .../수식_건설품셈_13장_플랜트설비공사.json | 22 ++++----- .../수식_산림품셈_03장_작업장_관리.json | 2 +- .../수식문서/수식_산림품셈_04장_나무베기.json | 4 +- .../수식_산림품셈_05장_식재_및_녹화.json | 2 +- .../수식문서/수식_산림품셈_08장_방제.json | 2 +- .../수식문서/수식_산림품셈_09장_토공.json | 42 +++++++++--------- .../수식_산림품셈_10장_자재․장비_운반.json | 16 +++---- .../수식문서/수식_산림품셈_11장_가설.json | 2 +- resources/master_data/일위대가조합.json | 2 +- resources/master_template/labor/item3.json | 14 +++--- .../master_template/precombo/SC-000001.json | 6 +-- .../master_template/sangeun/SK-000001.json | 4 +- .../master_template/sangeun/SK-000002.json | 8 ++-- .../master_template/sangeun/SK-000017.json | 8 ++-- .../master_template/sangeun/SK-000018.json | 12 ++--- .../master_template/sangeun/SK-000019.json | 12 ++--- .../fixtures/m01_791_1_logic_fingerprint.json | 2 +- resources/tester/test_157_1_sangeun_steps.py | 2 +- 43 files changed, 176 insertions(+), 176 deletions(-) diff --git a/docs/raw/요구_1104/_목록_813-1_기초이름정리.xlsx b/docs/raw/요구_1104/_목록_813-1_기초이름정리.xlsx index ee4b7dc2d661dd93f624392430cbc250cf06abaa..69a50438b197331495661fc022610fc9e20308b9 100644 GIT binary patch delta 7889 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 delta 3662 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 KH;Uu*2L2C*pi dict: div = "1.1 * 적재량 * 포수" if sack else "1.1 * 적재량" return { "원문번호": no, - "이름": f"{what}(구역화물 운임 + 하차비)", + "이름": what, "결과단위": unit, "출처": f"산림품셈 {no}", "소유": "공용", diff --git a/resources/master_data/로직_건설품셈_01장_공통.json b/resources/master_data/로직_건설품셈_01장_공통.json index 857adbe16..ae0f6399b 100644 --- a/resources/master_data/로직_건설품셈_01장_공통.json +++ b/resources/master_data/로직_건설품셈_01장_공통.json @@ -824,7 +824,7 @@ "원문번호": "1-2-16", "구분": "건설품셈 유지관리", "상세구분": "01장 공통", - "이름": "약제살포(동력분무기)", + "이름": "약제살포(기계)", "결과단위": "원/ℓ", "출처": "건설품셈 유지관리 1-2-16", "개정": [2019, 2022], diff --git a/resources/master_data/로직_건설품셈_01장_도로포장공사.json b/resources/master_data/로직_건설품셈_01장_도로포장공사.json index 75be33ebd..f2a07b585 100644 --- a/resources/master_data/로직_건설품셈_01장_도로포장공사.json +++ b/resources/master_data/로직_건설품셈_01장_도로포장공사.json @@ -860,7 +860,7 @@ "원문번호": "1-6-5", "구분": "건설품셈 토목", "상세구분": "01장 도로포장공사", - "이름": "콘크리트 페이버 조립 및 해체", + "이름": "기계포설 장비조립 및 해체", "결과단위": "원/회", "출처": "건설품셈 토목 1-6-5", "개정": [2021], diff --git a/resources/master_data/로직_건설품셈_01장_철골공사.json b/resources/master_data/로직_건설품셈_01장_철골공사.json index b14759e61..a3df0f7f6 100644 --- a/resources/master_data/로직_건설품셈_01장_철골공사.json +++ b/resources/master_data/로직_건설품셈_01장_철골공사.json @@ -11,7 +11,7 @@ "원문번호": "1-1-1", "구분": "건설품셈 건축", "상세구분": "01장 철골공사", - "이름": "철골 공장가공조립(기본철골공수)", + "이름": "철골 가공 조립 기본철골공수", "결과단위": "원/ton", "출처": "건설품셈 건축 1-1-1", "개정": [2008, 2013], @@ -80,7 +80,7 @@ "원문번호": "1-1-3", "구분": "건설품셈 건축", "상세구분": "01장 철골공사", - "이름": "철골 공장용접(기본용접공수)", + "이름": "철골 가공 조립 기본용접공수", "결과단위": "원/ton", "출처": "건설품셈 건축 1-1-3", "소유": "공용", @@ -443,7 +443,7 @@ "원문번호": "1-2-7", "구분": "건설품셈 건축", "상세구분": "01장 철골공사", - "이름": "타워크레인 가설 · 해체 · 수직이동", + "이름": "철골세우기용 장비의 가설 및 해체이동", "결과단위": "원/대", "출처": "건설품셈 건축 1-2-7", "소유": "공용", diff --git a/resources/master_data/로직_건설품셈_05장_기초공사.json b/resources/master_data/로직_건설품셈_05장_기초공사.json index 1f4eecfae..4782a6a18 100644 --- a/resources/master_data/로직_건설품셈_05장_기초공사.json +++ b/resources/master_data/로직_건설품셈_05장_기초공사.json @@ -477,7 +477,7 @@ "원문번호": "5-1-7", "구분": "건설품셈 공통", "상세구분": "05장 기초공사", - "이름": "앵커 천공(크롤러형 보링장비)", + "이름": "앵커 천공", "결과단위": "원/공", "출처": "건설품셈 공통 5-1-7", "개정": [2020, 2026], @@ -1501,7 +1501,7 @@ "원문번호": "5-2-3", "구분": "건설품셈 공통", "상세구분": "05장 기초공사", - "이름": "PBD천공기 조립 및 해체", + "이름": "PBD 장비조립 및 해체", "결과단위": "원/회", "출처": "건설품셈 공통 5-2-3", "개정": [2013, 2021, 2026], diff --git a/resources/master_data/로직_건설품셈_06장_방수공사.json b/resources/master_data/로직_건설품셈_06장_방수공사.json index 66f4f8c28..9dd6eabe2 100644 --- a/resources/master_data/로직_건설품셈_06장_방수공사.json +++ b/resources/master_data/로직_건설품셈_06장_방수공사.json @@ -32,7 +32,7 @@ "원문번호": "6-1-2", "구분": "건설품셈 건축", "상세구분": "06장 방수공사", - "이름": "방수프라이머 바름(롤러 1회)", + "이름": "방수프라이머 바름", "결과단위": "원/㎡", "출처": "건설품셈 건축 6-1-2", "개정": [2018], diff --git a/resources/master_data/로직_건설품셈_07장_돌공사.json b/resources/master_data/로직_건설품셈_07장_돌공사.json index 92e497e75..fd659ee52 100644 --- a/resources/master_data/로직_건설품셈_07장_돌공사.json +++ b/resources/master_data/로직_건설품셈_07장_돌공사.json @@ -10,7 +10,7 @@ "원문번호": "7-1-1", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "메쌓기(굴착기+부착용 집게)", + "이름": "메쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-1-1", "개정": [2012, 2019, 2026], @@ -49,7 +49,7 @@ "원문번호": "7-1-2", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "찰쌓기(굴착기+부착용 집게)", + "이름": "찰쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-1-2", "개정": [2012, 2018, 2019, 2026], @@ -88,7 +88,7 @@ "원문번호": "7-2-1", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "메붙임(굴착기+부착용 집게)", + "이름": "메붙임", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-2-1", "개정": [2012, 2019, 2026], @@ -127,7 +127,7 @@ "원문번호": "7-2-2", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "찰붙임(굴착기+부착용 집게)", + "이름": "찰붙임", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-2-2", "개정": [2012, 2019, 2026], @@ -166,7 +166,7 @@ "원문번호": "7-3-1", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "전석쌓기(굴착기+부착용 집게)", + "이름": "전석쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-3-1", "개정": [1992, 2012, 2018, 2026], @@ -221,7 +221,7 @@ "원문번호": "7-3-2", "구분": "건설품셈 공통", "상세구분": "07장 돌공사", - "이름": "전석깔기(굴착기+부착용 집게)", + "이름": "전석깔기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-3-2", "개정": [2018, 2026], diff --git a/resources/master_data/로직_건설품셈_09장_기타공사.json b/resources/master_data/로직_건설품셈_09장_기타공사.json index 94c81bea6..4b7ed0699 100644 --- a/resources/master_data/로직_건설품셈_09장_기타공사.json +++ b/resources/master_data/로직_건설품셈_09장_기타공사.json @@ -85,7 +85,7 @@ "원문번호": "9-2-1", "구분": "건설품셈 기계설비", "상세구분": "09장 기타공사", - "이름": "바탕만들기(블라스트 · 공구 · 와이어브러시)", + "이름": "바탕만들기", "결과단위": "원/㎡", "출처": "건설품셈 기계설비 9-2-1", "소유": "공용", @@ -270,7 +270,7 @@ "원문번호": "9-4-1", "구분": "건설품셈 기계설비", "상세구분": "09장 기타공사", - "이름": "가스배관 기밀시험", + "이름": "기밀시험", "결과단위": "원/회", "출처": "건설품셈 기계설비 9-4-1", "개정": [2015, 2019], diff --git a/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json b/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json index 5c0bda9fd..5ca29e6e6 100644 --- a/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json +++ b/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json @@ -1317,7 +1317,7 @@ "원문번호": "13-2-6", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "응력제거(Ring Burner · 전기저항)", + "이름": "응력제거(Ring Burner, Electric, Resistence Heating)", "결과단위": "원/개소", "출처": "건설품셈 기계설비 13-2-6", "소유": "공용", @@ -1884,7 +1884,7 @@ "원문번호": "13-5-1", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "화력 보일러 설치(공정별)", + "이름": "화력 보일러 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-1", "소유": "공용", @@ -2059,7 +2059,7 @@ "원문번호": "13-5-2", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "보일러 드럼 설치(공정별)", + "이름": "보일러 드럼 설치", "결과단위": "원/대", "출처": "건설품셈 기계설비 13-5-2", "소유": "공용", @@ -2275,7 +2275,7 @@ "원문번호": "13-5-4", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "덕트 설치(공정별)", + "이름": "덕트 설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-5-4", "소유": "공용", @@ -2360,7 +2360,7 @@ "원문번호": "13-5-5", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "공기예열기(Preheater) 설치(공정별)", + "이름": "공기예열기(Preheater) 설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-5-5", "소유": "공용", @@ -2555,7 +2555,7 @@ "원문번호": "13-5-8", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "터빈 설치(공정별)", + "이름": "터빈 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-8", "소유": "공용", @@ -2716,7 +2716,7 @@ "원문번호": "13-5-9", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "발전기 설치(공정별)", + "이름": "발전기 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-9", "소유": "공용", @@ -2842,7 +2842,7 @@ "원문번호": "13-5-10", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "복수기 설치(공정별)", + "이름": "복수기 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-10", "소유": "공용", @@ -3350,7 +3350,7 @@ "원문번호": "13-6-1", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "수차 설치(Kaplan · Francis · Propeller)", + "이름": "수차 설치", "결과단위": "원/ton", "출처": "건설품셈 기계설비 13-6-1", "소유": "공용", @@ -6728,7 +6728,7 @@ "원문번호": "13-7-19", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "Mill Line 기기류 조립설치(공정별)", + "이름": "Mill Line 기기류 조립설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-7-19", "소유": "공용", @@ -6772,7 +6772,7 @@ "원문번호": "13-7-20", "구분": "건설품셈 기계설비", "상세구분": "13장 플랜트설비공사", - "이름": "Roller Table 조립설치(공정별)", + "이름": "Roller Table 조립설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-7-20", "소유": "공용", diff --git a/resources/master_data/로직_산림품셈_03장_작업장_관리.json b/resources/master_data/로직_산림품셈_03장_작업장_관리.json index 7669f53be..5ac1df373 100644 --- a/resources/master_data/로직_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/로직_산림품셈_03장_작업장_관리.json @@ -103,7 +103,7 @@ "원문번호": "3-4-2", "구분": "산림품셈", "상세구분": "03장 작업장 관리", - "이름": "임산물 운반로 및 작업로 신설(굴착기 0.2㎥)", + "이름": "임산물 운반로 및 작업로 신설비 산정", "결과단위": "원/m", "출처": "산림품셈 3-4-2", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_04장_나무베기.json b/resources/master_data/로직_산림품셈_04장_나무베기.json index bd1210485..4cd52c882 100644 --- a/resources/master_data/로직_산림품셈_04장_나무베기.json +++ b/resources/master_data/로직_산림품셈_04장_나무베기.json @@ -148,7 +148,7 @@ "원문번호": "4-2-2", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "단목베기(1,000㎡당 · 굴착기 0.2㎥ + 부착용 집게)", + "이름": "단목베기(1,000㎡당)", "결과단위": "원/1,000㎡", "출처": "산림품셈 4-2-2", "개정": [2026], @@ -199,7 +199,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json b/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json index e4f409b5b..0f2566c88 100644 --- a/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json +++ b/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json @@ -1219,7 +1219,7 @@ "원문번호": "5-29", "구분": "산림품셈", "상세구분": "05장 식재 및 녹화", - "이름": "복사이식(굴착기 1.0㎥)", + "이름": "복사이식", "결과단위": "원/㎥", "출처": "산림품셈 5-29", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_08장_방제.json b/resources/master_data/로직_산림품셈_08장_방제.json index 4ebd6986f..af203729a 100644 --- a/resources/master_data/로직_산림품셈_08장_방제.json +++ b/resources/master_data/로직_산림품셈_08장_방제.json @@ -362,7 +362,7 @@ "원문번호": "8-6-3", "구분": "산림품셈", "상세구분": "08장 방제", - "이름": "지상방제 차량살포(동력분무기)", + "이름": "지상방제 차량살포", "결과단위": "원/일", "출처": "산림품셈 8-6-3", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_09장_토공.json b/resources/master_data/로직_산림품셈_09장_토공.json index b56362e4b..464589b58 100644 --- a/resources/master_data/로직_산림품셈_09장_토공.json +++ b/resources/master_data/로직_산림품셈_09장_토공.json @@ -73,7 +73,7 @@ "원문번호": "9-3-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "토사깍기(기계 · 굴착기 0.7㎥)", + "이름": "토사깍기(기계)", "결과단위": "원/㎥", "출처": "산림품셈 9-3-2", "개정": [2026], @@ -94,7 +94,7 @@ "원문번호": "9-4-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "암파쇄(대형브레이커 · 굴착기 0.7㎥)", + "이름": "암파쇄", "결과단위": "원/㎥", "출처": "산림품셈 9-4-1", "개정": [2026], @@ -178,7 +178,7 @@ "원문번호": "9-4-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "암절취 집토(굴착기 0.7㎥)", + "이름": "암절취 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-4-2", "개정": [2026], @@ -235,7 +235,7 @@ "원문번호": "9-5-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "발파암 깎기 90%(대형브레이커 · 굴착기 0.7㎥)", + "이름": "발파암 깎기(90%)", "결과단위": "원/㎥", "출처": "산림품셈 9-5-2", "개정": [2026], @@ -319,7 +319,7 @@ "원문번호": "9-5-3", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "발파암 집토(굴착기 0.7㎥)", + "이름": "발파암 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-5-3", "개정": [2026], @@ -340,7 +340,7 @@ "원문번호": "9-6-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "발파암 기계소할(대형브레이커 · 굴착기 0.7㎥)", + "이름": "발파암 기계소할(15%)", "결과단위": "원/㎥", "출처": "산림품셈 9-6-1", "개정": [2026], @@ -407,7 +407,7 @@ "원문번호": "9-7-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "콘크리트 깨기 집토(굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-7-1 주⑥", "개정": [2026], @@ -428,7 +428,7 @@ "원문번호": "9-7-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "무근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기(T=30㎝ 미만)", "결과단위": "원/㎥", "출처": "산림품셈 9-7-1", "개정": [2026], @@ -475,7 +475,7 @@ "원문번호": "9-7-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "무근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기(T=30㎝ 이상)", "결과단위": "원/㎥", "출처": "산림품셈 9-7-2", "개정": [2026], @@ -579,7 +579,7 @@ "원문번호": "9-8-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "철근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "철근콘크리트 깨기(T=30㎝ 미만)", "결과단위": "원/㎥", "출처": "산림품셈 9-8-1", "개정": [2026], @@ -626,7 +626,7 @@ "원문번호": "9-8-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "철근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "철근콘크리트 깨기(T=30㎝ 이상)", "결과단위": "원/㎥", "출처": "산림품셈 9-8-2", "개정": [2026], @@ -691,7 +691,7 @@ "원문번호": "9-10-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "기존포장 깨기 콘크리트 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "기존포장 깨기 콘크리트", "결과단위": "원/㎥", "출처": "산림품셈 9-10-1", "개정": [2026], @@ -738,7 +738,7 @@ "원문번호": "9-10-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "기존포장 깨기 아스팔트 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "기존포장 깨기 아스팔트", "결과단위": "원/㎥", "출처": "산림품셈 9-10-2", "개정": [2026], @@ -2101,7 +2101,7 @@ "원문번호": "9-14-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "다짐(플레이트콤펙터 1.5ton)", + "이름": "다짐(플레이트콤펙터)", "결과단위": "원/㎥", "출처": "산림품셈 9-14-2", "개정": [2026], @@ -2141,7 +2141,7 @@ "원문번호": "9-15-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "표토제거 답(畓)구간(굴착기 0.7㎥)", + "이름": "표토제거 답(畓)구간", "결과단위": "원/㎡", "출처": "산림품셈 9-15-1", "개정": [2026], @@ -2201,7 +2201,7 @@ "원문번호": "9-15-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "표토제거 답(畓)외구간(불도저 19ton)", + "이름": "표토제거 답(畓)외구간", "결과단위": "원/㎡", "출처": "산림품셈 9-15-2", "개정": [2026], @@ -2243,7 +2243,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [2026], @@ -2372,7 +2372,7 @@ "원문번호": "9-17-2", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "비다짐(정지 · 불도저 19ton)", + "이름": "비다짐(정지)", "결과단위": "원/㎥", "출처": "산림품셈 9-17-2", "개정": [2026], @@ -2415,7 +2415,7 @@ "원문번호": "9-18", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "층따기(굴착기 0.7㎥)", + "이름": "층따기", "결과단위": "원/㎥", "출처": "산림품셈 9-18", "개정": [2026], @@ -2478,7 +2478,7 @@ "원문번호": "9-19-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "토사면 성토면 고르기(기계시공 · 굴착기 0.6㎥)", + "이름": "토사면 성토면 고르기(기계시공)", "결과단위": "원/10㎡", "출처": "산림품셈 9-19-1", "개정": [2026], @@ -2661,7 +2661,7 @@ "원문번호": "9-22", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "섞기(굴착기 0.7㎥)", + "이름": "섞기", "결과단위": "원/㎥", "출처": "산림품셈 9-22", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json b/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json index f56db170c..9080d16db 100644 --- a/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json +++ b/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json @@ -26,7 +26,7 @@ "원문번호": "10-3-1", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "골재운반 모래(덤프 15ton)", + "이름": "골재운반 모래", "결과단위": "원/㎥", "출처": "산림품셈 10-3-1", "개정": [2026], @@ -61,7 +61,7 @@ "원문번호": "10-3-2", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "골재운반 잡석(덤프 15ton)", + "이름": "골재운반 잡석", "결과단위": "원/㎥", "출처": "산림품셈 10-3-2", "개정": [2026], @@ -96,7 +96,7 @@ "원문번호": "10-3-3", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "골재운반 혼합골재(덤프 15ton)", + "이름": "골재운반 혼합골재", "결과단위": "원/㎥", "출처": "산림품셈 10-3-3", "개정": [2026], @@ -131,7 +131,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [2026], @@ -565,7 +565,7 @@ "원문번호": "10-9-1", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "덤프트럭 자재 운반(1회 표준 운반량)", + "이름": "덤프트럭 운반(자재의 1회당 표준 운반량)", "결과단위": "원/ton", "출처": "산림품셈 10-9-1", "개정": [2026], @@ -729,7 +729,7 @@ "원문번호": "10-12-1", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "덤프운반(굴착기 0.7㎥ 적재 · 덤프트럭 15ton)", + "이름": "덤프운반 토사", "결과단위": "원/㎥", "출처": "산림품셈 10-12-1", "개정": [2026], @@ -861,7 +861,7 @@ "원문번호": "10-1", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "시멘트운반(구역화물 운임 + 하차비)", + "이름": "시멘트운반", "결과단위": "원/포", "출처": "산림품셈 10-1", "개정": [2026], @@ -964,7 +964,7 @@ "원문번호": "10-2", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "철근운반(구역화물 운임 + 하차비)", + "이름": "철근운반", "결과단위": "원/ton", "출처": "산림품셈 10-2", "개정": [2026], diff --git a/resources/master_data/로직_산림품셈_11장_가설.json b/resources/master_data/로직_산림품셈_11장_가설.json index cfd04b648..1ab42eadb 100644 --- a/resources/master_data/로직_산림품셈_11장_가설.json +++ b/resources/master_data/로직_산림품셈_11장_가설.json @@ -113,7 +113,7 @@ "원문번호": "11-4", "구분": "산림품셈", "상세구분": "11장 가설", - "이름": "쇄석·혼합석 부설(굴착기 0.7㎥)", + "이름": "쇄석·혼합석 부설", "결과단위": "원/㎥", "출처": "산림품셈 11-4", "개정": [2026], diff --git a/resources/master_data/수식문서/수식_건설품셈_01장_공통.json b/resources/master_data/수식문서/수식_건설품셈_01장_공통.json index e2e62a06d..bc730d09e 100644 --- a/resources/master_data/수식문서/수식_건설품셈_01장_공통.json +++ b/resources/master_data/수식문서/수식_건설품셈_01장_공통.json @@ -704,7 +704,7 @@ "꼴": "수식문서", "판": 1, "키": "GC001013", - "이름": "약제살포(동력분무기)", + "이름": "약제살포(기계)", "결과단위": "원/ℓ", "출처": "건설품셈 유지관리 1-2-16", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_01장_도로포장공사.json b/resources/master_data/수식문서/수식_건설품셈_01장_도로포장공사.json index efc053909..c3e51c460 100644 --- a/resources/master_data/수식문서/수식_건설품셈_01장_도로포장공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_01장_도로포장공사.json @@ -889,7 +889,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000300", - "이름": "콘크리트 페이버 조립 및 해체", + "이름": "기계포설 장비조립 및 해체", "결과단위": "원/회", "출처": "건설품셈 토목 1-6-5", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_01장_철골공사.json b/resources/master_data/수식문서/수식_건설품셈_01장_철골공사.json index 1a7f4ff05..4b6b689de 100644 --- a/resources/master_data/수식문서/수식_건설품셈_01장_철골공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_01장_철골공사.json @@ -6,7 +6,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000559", - "이름": "철골 공장가공조립(기본철골공수)", + "이름": "철골 가공 조립 기본철골공수", "결과단위": "원/ton", "출처": "건설품셈 건축 1-1-1", "변수": [ @@ -51,7 +51,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000560", - "이름": "철골 공장용접(기본용접공수)", + "이름": "철골 가공 조립 기본용접공수", "결과단위": "원/ton", "출처": "건설품셈 건축 1-1-3", "변수": [ @@ -302,7 +302,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000567", - "이름": "타워크레인 가설 · 해체 · 수직이동", + "이름": "철골세우기용 장비의 가설 및 해체이동", "결과단위": "원/대", "출처": "건설품셈 건축 1-2-7", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_05장_기초공사.json b/resources/master_data/수식문서/수식_건설품셈_05장_기초공사.json index d903713b2..995857ed5 100644 --- a/resources/master_data/수식문서/수식_건설품셈_05장_기초공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_05장_기초공사.json @@ -285,7 +285,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000169", - "이름": "앵커 천공(크롤러형 보링장비)", + "이름": "앵커 천공", "결과단위": "원/공", "출처": "건설품셈 공통 5-1-7", "변수": [ @@ -1029,7 +1029,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000184", - "이름": "PBD천공기 조립 및 해체", + "이름": "PBD 장비조립 및 해체", "결과단위": "원/회", "출처": "건설품셈 공통 5-2-3", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_06장_방수공사.json b/resources/master_data/수식문서/수식_건설품셈_06장_방수공사.json index 3c7554766..4f3610c1b 100644 --- a/resources/master_data/수식문서/수식_건설품셈_06장_방수공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_06장_방수공사.json @@ -33,7 +33,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000631", - "이름": "방수프라이머 바름(롤러 1회)", + "이름": "방수프라이머 바름", "결과단위": "원/㎡", "출처": "건설품셈 건축 6-1-2", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_07장_돌공사.json b/resources/master_data/수식문서/수식_건설품셈_07장_돌공사.json index 5f759db8f..92d4eda46 100644 --- a/resources/master_data/수식문서/수식_건설품셈_07장_돌공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_07장_돌공사.json @@ -6,7 +6,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000260", - "이름": "메쌓기(굴착기+부착용 집게)", + "이름": "메쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-1-1", "변수": [ @@ -39,7 +39,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000261", - "이름": "찰쌓기(굴착기+부착용 집게)", + "이름": "찰쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-1-2", "변수": [ @@ -72,7 +72,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000262", - "이름": "메붙임(굴착기+부착용 집게)", + "이름": "메붙임", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-2-1", "변수": [ @@ -105,7 +105,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000263", - "이름": "찰붙임(굴착기+부착용 집게)", + "이름": "찰붙임", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-2-2", "변수": [ @@ -138,7 +138,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000264", - "이름": "전석쌓기(굴착기+부착용 집게)", + "이름": "전석쌓기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-3-1", "변수": [ @@ -177,7 +177,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000265", - "이름": "전석깔기(굴착기+부착용 집게)", + "이름": "전석깔기", "결과단위": "원/㎡", "출처": "건설품셈 공통 7-3-2", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_09장_기타공사.json b/resources/master_data/수식문서/수식_건설품셈_09장_기타공사.json index 17d24ad1d..d7df00926 100644 --- a/resources/master_data/수식문서/수식_건설품셈_09장_기타공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_09장_기타공사.json @@ -78,7 +78,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000822", - "이름": "바탕만들기(블라스트 · 공구 · 와이어브러시)", + "이름": "바탕만들기", "결과단위": "원/㎡", "출처": "건설품셈 기계설비 9-2-1", "변수": [ @@ -245,7 +245,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000827", - "이름": "가스배관 기밀시험", + "이름": "기밀시험", "결과단위": "원/회", "출처": "건설품셈 기계설비 9-4-1", "변수": [ diff --git a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json index 4d2b48f66..99414a1f3 100644 --- a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json @@ -873,7 +873,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000900", - "이름": "응력제거(Ring Burner · 전기저항)", + "이름": "응력제거(Ring Burner, Electric, Resistence Heating)", "결과단위": "원/개소", "출처": "건설품셈 기계설비 13-2-6", "변수": [ @@ -1357,7 +1357,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000912", - "이름": "화력 보일러 설치(공정별)", + "이름": "화력 보일러 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-1", "변수": [ @@ -1479,7 +1479,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000914", - "이름": "보일러 드럼 설치(공정별)", + "이름": "보일러 드럼 설치", "결과단위": "원/대", "출처": "건설품셈 기계설비 13-5-2", "변수": [ @@ -1619,7 +1619,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000916", - "이름": "덕트 설치(공정별)", + "이름": "덕트 설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-5-4", "변수": [ @@ -1690,7 +1690,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000917", - "이름": "공기예열기(Preheater) 설치(공정별)", + "이름": "공기예열기(Preheater) 설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-5-5", "변수": [ @@ -1841,7 +1841,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000920", - "이름": "터빈 설치(공정별)", + "이름": "터빈 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-8", "변수": [ @@ -1929,7 +1929,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000921", - "이름": "발전기 설치(공정별)", + "이름": "발전기 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-9", "변수": [ @@ -2003,7 +2003,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000922", - "이름": "복수기 설치(공정별)", + "이름": "복수기 설치", "결과단위": "원/기", "출처": "건설품셈 기계설비 13-5-10", "변수": [ @@ -2357,7 +2357,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000931", - "이름": "수차 설치(Kaplan · Francis · Propeller)", + "이름": "수차 설치", "결과단위": "원/ton", "출처": "건설품셈 기계설비 13-6-1", "변수": [ @@ -4950,7 +4950,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000967", - "이름": "Mill Line 기기류 조립설치(공정별)", + "이름": "Mill Line 기기류 조립설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-7-19", "변수": [ @@ -4993,7 +4993,7 @@ "꼴": "수식문서", "판": 1, "키": "GC000968", - "이름": "Roller Table 조립설치(공정별)", + "이름": "Roller Table 조립설치", "결과단위": "원/식", "출처": "건설품셈 기계설비 13-7-20", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json index 2e14682f0..a8f8bf098 100644 --- a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json @@ -92,7 +92,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000004", - "이름": "임산물 운반로 및 작업로 신설(굴착기 0.2㎥)", + "이름": "임산물 운반로 및 작업로 신설비 산정", "결과단위": "원/m", "출처": "산림품셈 3-4-2", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json b/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json index 993d6d945..3119f8dd9 100644 --- a/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json +++ b/resources/master_data/수식문서/수식_산림품셈_04장_나무베기.json @@ -142,7 +142,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000288", - "이름": "단목베기(1,000㎡당 · 굴착기 0.2㎥ + 부착용 집게)", + "이름": "단목베기(1,000㎡당)", "결과단위": "원/1,000㎡", "출처": "산림품셈 4-2-2", "변수": [ @@ -181,7 +181,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000289", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json b/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json index 0b1f80795..d337de903 100644 --- a/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json +++ b/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json @@ -1119,7 +1119,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000043", - "이름": "복사이식(굴착기 1.0㎥)", + "이름": "복사이식", "결과단위": "원/㎥", "출처": "산림품셈 5-29", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_08장_방제.json b/resources/master_data/수식문서/수식_산림품셈_08장_방제.json index 24cac20b7..385d95c85 100644 --- a/resources/master_data/수식문서/수식_산림품셈_08장_방제.json +++ b/resources/master_data/수식문서/수식_산림품셈_08장_방제.json @@ -313,7 +313,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000321", - "이름": "지상방제 차량살포(동력분무기)", + "이름": "지상방제 차량살포", "결과단위": "원/일", "출처": "산림품셈 8-6-3", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_09장_토공.json b/resources/master_data/수식문서/수식_산림품셈_09장_토공.json index accc49c4a..7afc0da44 100644 --- a/resources/master_data/수식문서/수식_산림품셈_09장_토공.json +++ b/resources/master_data/수식문서/수식_산림품셈_09장_토공.json @@ -78,7 +78,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000089", - "이름": "토사깍기(기계 · 굴착기 0.7㎥)", + "이름": "토사깍기(기계)", "결과단위": "원/㎥", "출처": "산림품셈 9-3-2", "변수": [ @@ -102,7 +102,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000252", - "이름": "암파쇄(대형브레이커 · 굴착기 0.7㎥)", + "이름": "암파쇄", "결과단위": "원/㎥", "출처": "산림품셈 9-4-1", "변수": [ @@ -166,7 +166,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000091", - "이름": "암절취 집토(굴착기 0.7㎥)", + "이름": "암절취 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-4-2", "변수": [ @@ -239,7 +239,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000253", - "이름": "발파암 깎기 90%(대형브레이커 · 굴착기 0.7㎥)", + "이름": "발파암 깎기(90%)", "결과단위": "원/㎥", "출처": "산림품셈 9-5-2", "변수": [ @@ -303,7 +303,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000093", - "이름": "발파암 집토(굴착기 0.7㎥)", + "이름": "발파암 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-5-3", "변수": [ @@ -326,7 +326,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000255", - "이름": "발파암 기계소할(대형브레이커 · 굴착기 0.7㎥)", + "이름": "발파암 기계소할(15%)", "결과단위": "원/㎥", "출처": "산림품셈 9-6-1", "변수": [ @@ -378,7 +378,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000095", - "이름": "콘크리트 깨기 집토(굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기 집토", "결과단위": "원/㎥", "출처": "산림품셈 9-7-1 주⑥", "변수": [ @@ -401,7 +401,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000256", - "이름": "무근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기(T=30㎝ 미만)", "결과단위": "원/㎥", "출처": "산림품셈 9-7-1", "변수": [ @@ -432,7 +432,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000257", - "이름": "무근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "무근콘크리트 깨기(T=30㎝ 이상)", "결과단위": "원/㎥", "출처": "산림품셈 9-7-2", "변수": [ @@ -495,7 +495,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000258", - "이름": "철근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "철근콘크리트 깨기(T=30㎝ 미만)", "결과단위": "원/㎥", "출처": "산림품셈 9-8-1", "변수": [ @@ -526,7 +526,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000259", - "이름": "철근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "철근콘크리트 깨기(T=30㎝ 이상)", "결과단위": "원/㎥", "출처": "산림품셈 9-8-2", "변수": [ @@ -581,7 +581,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000260", - "이름": "기존포장 깨기 콘크리트 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "기존포장 깨기 콘크리트", "결과단위": "원/㎥", "출처": "산림품셈 9-10-1", "변수": [ @@ -612,7 +612,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000261", - "이름": "기존포장 깨기 아스팔트 깨기(대형브레이커 · 굴착기 0.7㎥)", + "이름": "기존포장 깨기 아스팔트", "결과단위": "원/㎥", "출처": "산림품셈 9-10-2", "변수": [ @@ -1624,7 +1624,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000249", - "이름": "다짐(플레이트콤펙터 1.5ton)", + "이름": "다짐(플레이트콤펙터)", "결과단위": "원/㎥", "출처": "산림품셈 9-14-2", "변수": [ @@ -1673,7 +1673,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000116", - "이름": "표토제거 답(畓)구간(굴착기 0.7㎥)", + "이름": "표토제거 답(畓)구간", "결과단위": "원/㎡", "출처": "산림품셈 9-15-1", "변수": [ @@ -1729,7 +1729,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000250", - "이름": "표토제거 답(畓)외구간(불도저 19ton)", + "이름": "표토제거 답(畓)외구간", "결과단위": "원/㎡", "출처": "산림품셈 9-15-2", "변수": [ @@ -1778,7 +1778,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000119", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "변수": [ @@ -1902,7 +1902,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000251", - "이름": "비다짐(정지 · 불도저 19ton)", + "이름": "비다짐(정지)", "결과단위": "원/㎥", "출처": "산림품셈 9-17-2", "변수": [ @@ -1951,7 +1951,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000123", - "이름": "층따기(굴착기 0.7㎥)", + "이름": "층따기", "결과단위": "원/㎥", "출처": "산림품셈 9-18", "변수": [ @@ -2023,7 +2023,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000126", - "이름": "토사면 성토면 고르기(기계시공 · 굴착기 0.6㎥)", + "이름": "토사면 성토면 고르기(기계시공)", "결과단위": "원/10㎡", "출처": "산림품셈 9-19-1", "변수": [ @@ -2212,7 +2212,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000132", - "이름": "섞기(굴착기 0.7㎥)", + "이름": "섞기", "결과단위": "원/㎥", "출처": "산림품셈 9-22", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json index 2ce7eada4..418def676 100644 --- a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json +++ b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json @@ -31,7 +31,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000134", - "이름": "골재운반 모래(덤프 15ton)", + "이름": "골재운반 모래", "결과단위": "원/㎥", "출처": "산림품셈 10-3-1", "변수": [ @@ -57,7 +57,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000135", - "이름": "골재운반 잡석(덤프 15ton)", + "이름": "골재운반 잡석", "결과단위": "원/㎥", "출처": "산림품셈 10-3-2", "변수": [ @@ -83,7 +83,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000136", - "이름": "골재운반 혼합골재(덤프 15ton)", + "이름": "골재운반 혼합골재", "결과단위": "원/㎥", "출처": "산림품셈 10-3-3", "변수": [ @@ -109,7 +109,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000137", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "변수": [ @@ -507,7 +507,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000283", - "이름": "덤프트럭 자재 운반(1회 표준 운반량)", + "이름": "덤프트럭 운반(자재의 1회당 표준 운반량)", "결과단위": "원/ton", "출처": "산림품셈 10-9-1", "변수": [ @@ -653,7 +653,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000284", - "이름": "덤프운반(굴착기 0.7㎥ 적재 · 덤프트럭 15ton)", + "이름": "덤프운반 토사", "결과단위": "원/㎥", "출처": "산림품셈 10-12-1", "변수": [ @@ -761,7 +761,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000335", - "이름": "시멘트운반(구역화물 운임 + 하차비)", + "이름": "시멘트운반", "결과단위": "원/포", "출처": "산림품셈 10-1", "변수": [ @@ -791,7 +791,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000336", - "이름": "철근운반(구역화물 운임 + 하차비)", + "이름": "철근운반", "결과단위": "원/ton", "출처": "산림품셈 10-2", "변수": [ diff --git a/resources/master_data/수식문서/수식_산림품셈_11장_가설.json b/resources/master_data/수식문서/수식_산림품셈_11장_가설.json index 724e0f78d..4c98cb0cc 100644 --- a/resources/master_data/수식문서/수식_산림품셈_11장_가설.json +++ b/resources/master_data/수식문서/수식_산림품셈_11장_가설.json @@ -114,7 +114,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000154", - "이름": "쇄석·혼합석 부설(굴착기 0.7㎥)", + "이름": "쇄석·혼합석 부설", "결과단위": "원/㎥", "출처": "산림품셈 11-4", "변수": [ diff --git a/resources/master_data/일위대가조합.json b/resources/master_data/일위대가조합.json index 75f20dbd8..7f522b1cf 100644 --- a/resources/master_data/일위대가조합.json +++ b/resources/master_data/일위대가조합.json @@ -10,7 +10,7 @@ "구분": null, "상세구분": null, "단위": "식", - "담은로직": [{ "로직": "GF000219", "메모": "메쌓기 — 돌쌓기 인력" }, { "로직": "GF000119", "메모": "노체포설 — 굴착기 0.7㎥" }, { "로직": "GF000137", "메모": "운반 — 중기운반 트레일러(20ton)" }], + "담은로직": [{ "로직": "GF000219", "메모": "메쌓기 — 돌쌓기 인력" }, { "로직": "GF000119", "메모": "노체포설 — 굴착기 0.7㎥" }, { "로직": "GF000137", "메모": "운반 — 중기운반" }], "본뜬키": null, "출처": "자체", "소유": "현장", diff --git a/resources/master_template/labor/item3.json b/resources/master_template/labor/item3.json index 8ea33eff9..4094d1ec1 100644 --- a/resources/master_template/labor/item3.json +++ b/resources/master_template/labor/item3.json @@ -36,7 +36,7 @@ "키": "SK-000019" }, "별칭": null, - "메모": "운반 — 중기운반 트레일러(20ton)", + "메모": "운반 — 중기운반", "수량": null }, { @@ -913,7 +913,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -992,7 +992,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -10625,7 +10625,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -10704,7 +10704,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -19683,7 +19683,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000137", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "변수": [ @@ -20333,7 +20333,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000119", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "변수": [ diff --git a/resources/master_template/precombo/SC-000001.json b/resources/master_template/precombo/SC-000001.json index 632f202f4..7be2301dd 100644 --- a/resources/master_template/precombo/SC-000001.json +++ b/resources/master_template/precombo/SC-000001.json @@ -786,7 +786,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ @@ -10826,7 +10826,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ @@ -20114,7 +20114,7 @@ "꼴": "수식문서", "판": 1, "키": "GF000289", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "변수": [ diff --git a/resources/master_template/sangeun/SK-000001.json b/resources/master_template/sangeun/SK-000001.json index bb1fdc7b4..6dffe1246 100644 --- a/resources/master_template/sangeun/SK-000001.json +++ b/resources/master_template/sangeun/SK-000001.json @@ -772,7 +772,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ @@ -10822,7 +10822,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ diff --git a/resources/master_template/sangeun/SK-000002.json b/resources/master_template/sangeun/SK-000002.json index ef1d23bfb..b52405db4 100644 --- a/resources/master_template/sangeun/SK-000002.json +++ b/resources/master_template/sangeun/SK-000002.json @@ -1,7 +1,7 @@ { "종류": "단가산출", "키": "SK-000002", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "단위": "본", "바탕": { "로직": "GF000289" @@ -772,7 +772,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ @@ -10823,7 +10823,7 @@ "원문번호": "4-3", "구분": "산림품셈", "상세구분": "04장 나무베기", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "개정": [ @@ -21000,7 +21000,7 @@ "꼴": "수식문서", "판": 1, "키": "SK-000002", - "이름": "위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", + "이름": "위험목 베기", "결과단위": "원/본", "출처": "산림품셈 4-3", "변수": [ diff --git a/resources/master_template/sangeun/SK-000017.json b/resources/master_template/sangeun/SK-000017.json index 236fcfbf1..fd77422f7 100644 --- a/resources/master_template/sangeun/SK-000017.json +++ b/resources/master_template/sangeun/SK-000017.json @@ -674,7 +674,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -753,7 +753,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -10392,7 +10392,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -10471,7 +10471,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ diff --git a/resources/master_template/sangeun/SK-000018.json b/resources/master_template/sangeun/SK-000018.json index 0d086a016..e58731030 100644 --- a/resources/master_template/sangeun/SK-000018.json +++ b/resources/master_template/sangeun/SK-000018.json @@ -1,7 +1,7 @@ { "종류": "단가산출", "키": "SK-000018", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "단위": "㎥", "바탕": { "로직": "GF000119" @@ -674,7 +674,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -753,7 +753,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -10391,7 +10391,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -10470,7 +10470,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -19939,7 +19939,7 @@ "꼴": "수식문서", "판": 1, "키": "SK-000018", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "변수": [ diff --git a/resources/master_template/sangeun/SK-000019.json b/resources/master_template/sangeun/SK-000019.json index 593cf631f..c89360673 100644 --- a/resources/master_template/sangeun/SK-000019.json +++ b/resources/master_template/sangeun/SK-000019.json @@ -1,7 +1,7 @@ { "종류": "단가산출", "키": "SK-000019", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "단위": "회", "바탕": { "로직": "GF000137" @@ -674,7 +674,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -753,7 +753,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -10393,7 +10393,7 @@ "원문번호": "9-16-1", "구분": "산림품셈", "상세구분": "09장 토공", - "이름": "노체포설(굴착기 0.7㎥ 무한궤도)", + "이름": "노체포설", "결과단위": "원/㎥", "출처": "산림품셈 9-16-1", "개정": [ @@ -10472,7 +10472,7 @@ "원문번호": "10-4", "구분": "산림품셈", "상세구분": "10장 자재․장비 운반", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "개정": [ @@ -19878,7 +19878,7 @@ "꼴": "수식문서", "판": 1, "키": "SK-000019", - "이름": "중기운반 트레일러(20ton)", + "이름": "중기운반", "결과단위": "원/회", "출처": "산림품셈 10-4", "변수": [ diff --git a/resources/tester/fixtures/m01_791_1_logic_fingerprint.json b/resources/tester/fixtures/m01_791_1_logic_fingerprint.json index 37e47b148..e0461b712 100644 --- a/resources/tester/fixtures/m01_791_1_logic_fingerprint.json +++ b/resources/tester/fixtures/m01_791_1_logic_fingerprint.json @@ -1520,5 +1520,5 @@ "GF000336": "a9b9e0bb0f7da1f0", "GX000004": "a3f4c454a5764d6e" }, - "마스터": "152f8ebe50d98a4c" + "마스터": "e65874951c3ab169" } diff --git a/resources/tester/test_157_1_sangeun_steps.py b/resources/tester/test_157_1_sangeun_steps.py index 65acadd77..e70170b79 100644 --- a/resources/tester/test_157_1_sangeun_steps.py +++ b/resources/tester/test_157_1_sangeun_steps.py @@ -76,7 +76,7 @@ def test_돈_하위_로직_산근_호표_번호(): got = st.render_sangeun_text(_whole(["GF000089", "GF000123"], edit), "GF000089", {}, _price_of) line = next(t for t in got if t.startswith("참고 = ")) - assert line.startswith("참고 = 층따기(굴착기 0.7㎥)(산근 3호표).노무비 = ") + assert line.startswith("참고 = 층따기(산근 3호표).노무비 = ") def test_만약_걸린_갈래_입력_설명_기계_이름(): From 5b4d6e9dafbd09ea53aa4d0bc429d28169cc63f6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:23:31 +0900 Subject: [PATCH 18/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC734):=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EB=8B=B5=20=C2=B7=20=EB=B3=84=EC=B9=AD=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 4db81abef..249e2c3da 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -682,3 +682,14 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 6. 굴삭기 + 브레이카 는 사용자가 그룹 구분 없이 사용한다면 한 덩이로 그룹으로 구분한다면 자동으로 구분되겠지. 일반적으로는 구분되게 사용하는게 맞는것같음. 7. 드래그로 그룹이동 되는지 확인 바로 수정작업 아님. 조사부터 진행. + +## 요구 734 (11-09 · 사용자 · 결정 답 — 소계는 우리 양식 · 수량 끝자리 뜻 물음 · 품 숫자 = 우리 값이 최신(가져오면 우리 값) · 작업량당 기계경비 단가산출 불필요 의견 물음 · 별칭 8 목록 요청(초기 별칭 직접 · 승인까지) · 6 · 7 문제 없음 · 1~5 한 번에 진행 예정 — 먼저 대답) + +1. 소계는 결국 항목들의 합계형식일 뿐 우리양식 맞춰 표기해도 문제 없을것같다는 생각이 듬. +2. 수량 끝자리라는게 뭐임? +3. 품 숫자가 다르다는건 결국 품셈이나 요소들에 지정된 값이 다르다라는 의미지? 그렇다면 우리가 더 최신이 맞음. 그래서 가져오기 전 비교할때는 해당정보로 계산하겠지만 가져오면 우리값으로 업데이트 되는게 맞다고 대답한 적이 있음. 그리고 만약 그래도 사용자가 이전값으로 사용하겠다면 우리 단가산출 양식에는 값변경을 할수 있으니 OK +4. 작업량당 기계경비는 결국 이걸 활용하여 다른 단가산출들에서 사용하고 있는거잖아. 기계로 넣어서 사용하고 있는거잖아. 그렇다면 작업량당 기계경비라는 단가산출을 만들어둘 필요가 없어보이네 너의 생각은? 어차피 경쟁사 데이터 불러오기 기능에도 우리 단가산출 표현에 문제도 없을거고 있으면 되려 혼란을 줄것같아서 말이야. +5. 별칭 필요한 8개의 리스트를 보여주면 초기 별칭을 내가 넣어줄게. 승인처리까지 했으면 함. +6. 문제 없음. +7. 문제 없음. +1 ~ 5번은 한번에 진행할거야. 가능한 경우에는 다른 워크트리를 사용해도됨. 그러니 먼저 대답부터 From 7317dbe4e594aae8edf1e34584afa9a305624cb5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:25:37 +0900 Subject: [PATCH 19/29] feat(est-test): preserve nested JDL details --- resources/tester/test_est_jdl.py | 163 ++++++++++++++++++ resources/tester/test_est_native_signature.py | 22 +++ .../tester/test_est_quantity_move_export.py | 16 ++ tools/est-test/PLAN.md | 27 ++- tools/est-test/README.md | 3 + tools/est-test/export-estx.cjs | 4 +- tools/est-test/export-quantity.cjs | 25 ++- tools/est-test/import-estx.cjs | 16 +- tools/est-test/jdl-machine-export.cjs | 60 +++++++ tools/est-test/jdl-unit-order.cjs | 29 ++++ tools/est-test/jdl.cjs | 48 ++++-- tools/est-test/wpt-parse.cjs | 24 +++ 12 files changed, 405 insertions(+), 32 deletions(-) create mode 100644 resources/tester/test_est_native_signature.py create mode 100644 tools/est-test/jdl-machine-export.cjs create mode 100644 tools/est-test/jdl-unit-order.cjs create mode 100644 tools/est-test/wpt-parse.cjs diff --git a/resources/tester/test_est_jdl.py b/resources/tester/test_est_jdl.py index 493b57d11..71ff5dda3 100644 --- a/resources/tester/test_est_jdl.py +++ b/resources/tester/test_est_jdl.py @@ -55,6 +55,26 @@ def test_jdl_duplicate_unit_codes_do_not_select_a_wrong_unit_group(): assert result == {"units": 0, "total": 300, "warning": True} +def test_jdl_duplicate_unit_detail_groups_do_not_select_the_last_group(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='groups'; + p.tables.StuffcDat=[{Code:'M',Name:'material',TC:100}]; + p.tables.HeadTitleDat=[{Code:'U',Name:'unit',GC:200,TC:200}]; + p.tables.HeadMainDat=[{Index:1,Code:'U'},{Index:0,Code:'M',LinkIndex:5,GN:2,GU:100,GC:200}, + {Index:1,Code:'U'},{Index:0,Code:'M',LinkIndex:5,GN:1,GU:100,GC:100}]; + p.tables.EstMainDat=[{Code:'U',Name:'item',LinkIndex:103,GN:1,GU:200,GC:200,TC:200},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({units:j.parse('unit.txt',files.get('unit.txt')).length, + total:q.tables.EstMainDat.at(-1).TC}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == {"units": 0, "total": 200} + + def test_jdl_basic_tables_use_native_separators_encoding_and_preserve_direct_money(): p = run( "math", @@ -165,6 +185,92 @@ def test_basic_jdl_unit_price_parts_export_and_reimport(): assert result["specialUnits"] == 0 and result["specialWarnings"] > 0 +def test_jdl_unit_price_with_machine_detail_survives_round_trip(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='machine unit'; + p.tables.MachTitleDat=[{Code:'J',Name:'machine',GC:100,NC:50,TC:150}]; + p.tables.MachMainDat=[{Index:1,Code:'J',Name:'machine',JdlSummary:true}, + {Index:0,CalcIndex:1,JdlSummary:true,Name:'fuel',GC:100,NC:50,TC:150}]; + p.tables.HeadTitleDat=[{Code:'U',Name:'unit',GC:100,NC:50,TC:150}]; + p.tables.HeadMainDat=[{Index:1,Code:'U'}, + {Index:0,Code:'J',Name:'machine',LinkIndex:105,GN:1,GU:100,NU:50,GC:100,NC:50,TC:150}]; + p.tables.EstMainDat=[{Code:'U',Name:'unit',LinkIndex:103,GN:2,GU:100,NU:50,GC:200,NC:100,TC:300},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({unit:j.parse('unitpart.txt',files.get('unitpart.txt')), + rows:q.tables.HeadMainDat,machine:q.tables.MachMainDat,total:q.tables.EstMainDat.at(-1).TC}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["unit"][0]["F_CODE1"].startswith("J") + assert result["rows"][1]["LinkIndex"] == 105 + assert result["machine"][1]["Name"] == "fuel" + assert result["total"] == 300 + + +def test_jdl_unit_price_preserves_other_source_costs_as_fixed_resources(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='sources'; + for(const [table,code] of [['SilJukDat','S'],['EstiMateDat','E'],['CostTitleDat','D'],['MachTitleDat','J']]) + p.tables[table]=[{Code:code,Name:code,GC:10,TC:10}]; + p.tables.HeadTitleDat=[{Code:'U',Name:'unit',GC:40,TC:40}]; + p.tables.HeadMainDat=[{Index:1,Code:'U'},...[['S',21],['E',101],['D',104],['J',105]].map(([Code,LinkIndex])=> + ({Index:0,Code,Name:Code,LinkIndex,GN:1,GU:10,GC:10,TC:10}))]; + p.tables.EstMainDat=[{Code:'U',Name:'unit',LinkIndex:103,GN:1,GU:40,GC:40,TC:40},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({parts:j.parse('unitpart.txt',files.get('unitpart.txt')).length, + links:q.tables.HeadMainDat.slice(1).map(r=>r.LinkIndex),total:q.tables.EstMainDat.at(-1).TC}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == {"parts": 4, "links": [101, 101, 101, 101], "total": 40} + + +def test_jdl_nested_units_preserve_detail_or_warn_on_forward_reference(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const blank=JSON.parse(fs.readFileSync(0,'utf8')); + function convert(order,self=false,cycle=false){const p=structuredClone(blank);p.name='nested'; + p.tables.StuffcDat=[{Code:'M',Name:'material',TC:10}]; + const titles={A:{Code:'A',Name:'parent',GC:60,TC:60},B:{Code:'B',Name:'child',GC:20,TC:20}}; + p.tables.HeadTitleDat=order.map(code=>titles[code]); + p.tables.HeadMainDat=[{Index:1,Code:'B'}, {Index:0,Code:cycle?'A':'M',Name:'material',LinkIndex:cycle?103:5,GN:2,GU:10,GC:20}, + {Index:1,Code:'A'}, {Index:0,Code:self?'A':'B',Name:'child',LinkIndex:103,GN:3,GU:20,GC:60}]; + p.tables.EstMainDat=[{Code:'A',Name:'parent',LinkIndex:103,GN:2,GU:60,GC:120,TC:120},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,structuredClone(blank)); + return {parts:j.parse('unitpart.txt',files.get('unitpart.txt')),total:q.tables.EstMainDat.at(-1).TC, + units:j.parse('unit.txt',files.get('unit.txt')),warnings:out.warnings};} + process.stdout.write(JSON.stringify({ordered:convert(['B','A']),forward:convert(['A','B']), + self:convert(['A','B'],true),cycle:convert(['A','B'],false,true)}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["ordered"]["total"] == result["forward"]["total"] == 120 + assert result["ordered"]["parts"][-1]["F_CODE1"].startswith("U") + assert result["forward"]["parts"][-1]["F_CODE1"].startswith("U") + assert not any("하위 일위대가" in warning for warning in result["forward"]["warnings"]) + assert len(result["self"]["units"]) == 1 + assert result["cycle"]["total"] == 120 + assert any("하위 일위대가" in warning for warning in result["cycle"]["warnings"]) + + +def test_jdl_nested_unit_order_handles_deep_dependencies(): + script = """const {orderUnits}=require('./tools/est-test/jdl-unit-order.cjs'),n=15000; + const titles=Array.from({length:n},(_,i)=>({Code:String(i)})),groups=new Map(); + for(let i=0;iBuffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100,F_LPRICE:50}, + {F_CODE:'J2',F_DES:'ambiguous',F_MPRICE:50},{F_CODE:'J3',F_DES:'both match',F_MPRICE:150}])], + ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_DES:'part',F_QQTY:2,F_MPRICE:50,F_LPRICE:25}, + {F_CODE:'J2',F_SEQ:'1',F_DES:'uncertain',F_QQTY:2,F_MPRICE:50}, + {F_CODE:'J3',F_SEQ:'1',F_DES:'first',F_QQTY:2,F_MPRICE:50}, + {F_CODE:'J3',F_SEQ:'2',F_DES:'second',F_QQTY:0.5,F_MPRICE:100}])], + ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], + ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'J1',F_DES:'machine',F_QQTY:2,F_MPRICE:100,F_LPRICE:50}])]]); + const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.MachMainDat); + p.options.AutoCalc=true;p.options.SourceRefresh=true;p.tables.MachMainDat[1].GC=110;const q=s.calculate(p); + const warnings=p.importWarnings.slice(),out=j.output(q),back=j.read(out.bytes,blank); + process.stdout.write(JSON.stringify({before,rows:q.tables.MachMainDat,titles:q.tables.MachTitleDat, + saved:back.tables.MachMainDat,total:back.tables.EstMainDat.at(-1).TC,warnings,errors:q.calculation.errors}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["before"][1]["GC"] == 100 + assert result["before"][1]["NC"] == 50 + assert result["before"][1]["JdlSourceQuantity"] == 2 + assert result["titles"][0]["TC"] == 160 + assert result["saved"][1]["Name"] == "part" + assert result["saved"][1]["GC"] == 110 + assert result["saved"][1]["NC"] == 50 + assert result["total"] == 320 + assert "상세 산식 미변환" in result["rows"][-3]["Name"] + assert "상세 산식 미변환" in result["rows"][-1]["Name"] + assert any("수량×단가" in warning for warning in result["warnings"]) + assert not result["errors"] + + +def test_jdl_machine_export_rejects_ambiguous_or_inconsistent_detail_groups(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8'));p.name='machine'; + p.tables.MachTitleDat=[{Code:'J1',Name:'duplicate',GC:10},{Code:'J2',Name:'mismatch',GC:20}]; + p.tables.MachMainDat=[{Index:1,Code:'J1'},{Index:0,CalcIndex:1,JdlSummary:true,GC:10}, + {Index:1,Code:'J1'},{Index:0,CalcIndex:1,JdlSummary:true,GC:10}, + {Index:1,Code:'J2'},{Index:0,CalcIndex:1,JdlSummary:true,GC:5}]; + const out=j.output(p),files=a.unpack(out.bytes); + process.stdout.write(JSON.stringify({units:j.parse('junit.txt',files.get('junit.txt')).length, + parts:j.parse('jpart.txt',files.get('jpart.txt')).length}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == {"units": 0, "parts": 0} + + def test_jdl_cost_saved_amount_calculates_and_can_be_edited(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); diff --git a/resources/tester/test_est_native_signature.py b/resources/tester/test_est_native_signature.py new file mode 100644 index 000000000..487fd76df --- /dev/null +++ b/resources/tester/test_est_native_signature.py @@ -0,0 +1,22 @@ +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] + + +def test_estx_rejects_corrupted_last_engine_signature_byte_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = r"""const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),a=require('./tools/est-test/archive.cjs'); + const bytes=fs.readFileSync(process.argv[1]),files=m.unzip(bytes),table=files.get('estmaindat.dat'); + const valid=m.importEstx(bytes).tables.EstMainDat.length;table[24]^=1;let rejected=false; + try{m.importEstx(a.pack(files))}catch(e){rejected=e.message.includes('DBISAM')} + table[24]^=1;files.get('stuffcdat.dat')[9]^=1;let missingRejected=false; + try{m.importEstx(a.pack(files))}catch(e){missingRejected=e.message.includes('stuffcdat.dat')} + process.stdout.write(JSON.stringify({valid:valid>0,rejected,missingRejected}));""" + result = json.loads(subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT)) + assert result == {"valid": True, "rejected": True, "missingRejected": True} diff --git a/resources/tester/test_est_quantity_move_export.py b/resources/tester/test_est_quantity_move_export.py index f99fe3e57..ee6fac534 100644 --- a/resources/tester/test_est_quantity_move_export.py +++ b/resources/tester/test_est_quantity_move_export.py @@ -46,6 +46,22 @@ def test_move_checks_shared_tags_without_confusing_similar_numbers(): assert result == {"rows": True, "rejected": True} +def test_row_parser_preserves_global_and_local_tags_with_many_rows(): + script = r"""const q=require('./tools/est-test/native-quantity.cjs'),{rowParser}=require('./tools/est-test/wpt-parse.cjs'); + const one=(tag,code)=>'
1
'; + const styles=Array.from({length:3000},(_,i)=>'').join(''); + const doc=''+styles+Array.from({length:3000},(_,i)=>one(i,'r'+String(i).padStart(6,'0'))).join('')+ + '
2
'+one(1,'r009999'); + const declarations=[...doc.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map(m=>m[0]).join(''); + const divs=[...doc.matchAll(/]*>[\s\S]*?<\/div>/gi)].map(m=>m[0]),parse=rowParser(doc); + const samples=[divs[0],divs[1],divs[100],divs.at(-2),divs.at(-1)]; + const same=samples.every(div=>JSON.stringify(parse(div))===JSON.stringify(q.fromWpt('','',declarations+div,[],{}).rows)); + const count=divs.reduce((n,div)=>n+parse(div).length,0); + process.stdout.write(JSON.stringify({same,count}));""" + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT, timeout=30)) + assert result == {"same": True, "count": 3002} + + def test_move_and_edit_keeps_original_row_marker(): script = r"""const w=require('./tools/est-test/export-quantity.cjs'),q=require('./tools/est-test/native-quantity.cjs'); const div=(tag,code,value)=>'
Q='+value+'='+value+'
'; diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 9028ccd41..4cf0ae1bc 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -423,6 +423,30 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 백그라운드 상태 감시 및 자동 알림: `tools/est-test/monitor.cjs`를 통해 15초 주기로 `orca.exe` CLI(`shell: false`, timeout 4~5s)를 호출하여 CODEX 워크트리의 안티그래비티 작업 상태와 EST 5175 서버 헬스를 조회하고, 상태 변경 시에만 `monitor.log`에 기록(최대 512KB 순환). 안티그래비티 완료/대기 전이 시 Codex Orchestration Run(`run_cdb1558b9280`)으로 단발성 자동 알림(`status`/`messageId=msg_2c7bca748f91`)이 실제 전달됨을 확인(중복 폭주 방지 플래그 적용, 민감 환경변수 미기록, 재시작/자동지시 없음). ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] DBISAM 엔진 서명 전체를 확인해 손상된 ESTX 테이블을 정상 자료로 읽지 않도록 하고 실제 파일·손상 사례를 검증한다. + - 자체 검증: 기존 16바이트 접두 일치 후 마지막 해시 바이트가 다른 테이블과 자재 등 주요 테이블의 서명 손상을 거부해 조용한 누락을 방지. 설치 ESTX 24개 읽기, 정상 main.estx 변조 거부, 실제 원본 저장·인덱스 보존 포함 관련 시험 18개와 추가 손상·산식 회귀 39개 통과. + +- [x] JDL 중첩 일위대가를 호표의 입력 순서와 관계없이 하위부터 출력한다. 순환 참조는 고정 단가와 경고로 보호하고 실제 금액을 검증한다. + - 자체 검증: 부모 선행·하위 선행 순서 모두 하위 일위 연결과 120원 재가져오기 유지. 순환 참조는 고정 단가 및 경고, 직접 자기 참조는 상세 출력 차단. 15,000단계 의존성 순서 계산과 JDL·업무 시험 38개 통과. 원본 JDL 검사 DLL 인증은 미확인. + +- [x] 중첩 일위대가를 JDL 상세로 내보내고, 상세를 연결할 수 없는 하위 호표는 고정 단가와 경고로 보존한다. 자기 참조 보호와 재가져오기를 검증한다. + - 자체 검증: 부모·하위 순서를 바꾸어도 재가져오기 합계 120원과 하위 연결을 유지한다. 자기 참조 및 순환 시 안전한 고정 원천으로 보존한다. JDL·업무 시험 38개 통과. + +- [x] 수량 문서 행별 해석에서 필요한 위첨자 서식만 조회하고 중복 서식과 실제 저장 결과를 검증한다. + - 자체 검증: 3,000개 서식과 3,002행, 중복 서식 선언·표지 재정의에서 기존 해석과 일치. 설치 ESTX 저장·재읽기·인덱스 보존을 포함한 수량·원본 저장 시험 56개 통과(6분 18초). + +- [x] JDL 일위대가 상세의 기계·단가산출 등 일반 연결을 가능한 범위에서 출력하고, 연결과 금액의 재가져오기를 검증한다. + - 자체 검증: 기계 상세→일위대가→내역 연결을 JDL 왕복해 300원과 기계 연결을 확인. 실적·견적·산근·일반 기계는 고정 원천으로 전개해 40원 유지, 중복 일위 상세는 상세 출력 방지. JDL·업무 시험 36개 통과. 설치 교량 자료 JDL 왕복 내역 합계 7,989,852,162원 유지. 원본 DLL 검증은 미확인. + +- [x] JDL에서 가져와 금액을 편집한 일반 기계 상세를 조건이 충족될 때 JDL 기계 상세로 출력하고 재가져오기·합계를 검증한다. 복잡하거나 불일치한 구성은 기존 고정 단가를 유지한다. + - 자체 검증: 합성 JDL 기계 상세 금액 110·50을 편집 후 내역 수량 2에 반영하고 JDL로 저장·재가져와 상세·단가 합계 160과 내역 합계 320 확인. 중복 호표·비목 합계 불일치의 상세 출력 차단 및 JDL·업무 회귀 33개 통과. 상시 테스트 서버의 실제 JDL 가져오기·변경·내보내기·재가져오기 API에서도 상세 110과 내역 합계 320 확인. 세부 금액은 고정 단가로 출력하며 원본 상세 산식·수량 관계는 복원하지 않는다. 실제 JDL과 원본 검사 DLL 인증은 미확인. + +- [x] 대용량 수량산출서의 저장 시 각 행에 필요한 표지만 읽도록 변경하고 원본 서식·공유 표지·실제 파일 저장을 검증한다. + - 자체 검증: 3,002행 합성 문서에서 전역·행별 표지 재정의와 위첨자를 기존 해석과 대조. 설치 ESTX 추가·삭제·이동·편집 후 재읽기 및 인덱스 보존을 포함한 관련 회귀 56개 통과(6분 12초). 큰 ESTX의 다른 처리 시간은 남아 있다. + +- [x] JDL 기계 상세의 수량·단가를 곱한 비목별 합계가 제목과 일치하는 경우 상세 금액을 편집 가능하게 가져온다. 해석이 모호하거나 불일치한 경우 기존 합계 보존을 유지한다. + - 자체 검증: 합성 JDL의 수량 2 상세를 비목별 100·50으로 계산해 가져오고 금액 편집 후 합계 160 확인. 단순합과 수량 적용 합이 모두 일치하는 모호한 경우 등은 합계 보존. JDL·업무·오류 검색 시험 45개 및 모호성 추가 시험 통과. 실제 JDL 자료와 원본 실행 비교는 하지 못함. + - [x] JDL 출력에서 중복 자원 코드와 내부 고정금액 코드 충돌로 다른 단가에 연결되는 문제를 차단하고 왕복 검증한다. - 자체 검증: 중복 자원 세 개를 모두 내보내며 모호한 내역은 저장 금액으로 독립 전개하고, 기존 `LITERAL2` 코드와 내부 고정금액이 충돌하지 않음 확인. JDL·업무 회귀 30개, 중복 자원 세 건과 중복 일위대가의 잘못된 상세 연결 방지 별도 시험 통과. 설치 교량 자료의 JDL 출력·재가져오기 내역 합계 7,989,852,162원 유지. 변환 불가 구성 경고는 유지한다. @@ -792,7 +816,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 자체 검증(2026-11-06): Edge에서 XML 다운로드→실제 XSD 통과→재열기 금액/상하위 계층/0원 도급액 보존, JDL 다운로드/재열기 직접비·원가 보존, 예정공정 입력/저장 복원, CAD 제외 40개 서식 선택 시 JS 오류 0. Excel 왕복 시 바이너리·소수 유지, 66행 20행/쪽 분할과 쪽 번호 5~8, 본문/설정 저장 재열기 통과. 별도 openpyxl 읽기로 xlsx 구조/자료형 확인. 설치 sample.estx의 수량 변경→수정 저장→독립 재읽기 통과, 모든 .idx 바이트 불변. 원본 EST 실행 검증은 아님. - [ ] ESTX 신규 파일/행 증감·키 변경의 인덱스 재생성 구현 가능성 조사 및 점검. - 조사 및 한계 분석(2026-11-08): - 1. 파일 구조 및 암호화 특성: ESTX 내 각 테이블의 `.idx` 파일은 DBISAM 독자적 B-Tree 인덱스 형식으로, 헤더 8~24바이트에 서명(`06 8a be 8e 59 23 64 cb 40 3d 71 d2 e3 bc 64 d0`)이 포함되어 있으며 전체 블록(4,096바이트 단위)이 암호화·해싱되어 있어 단순 XOR 또는 비트 역변환으로 역공학할 수 없음. + 1. 파일 구조: 설치 ESTX 24개에 든 `.idx` 468개의 오프셋 9~24는 모두 `8abe8e592364cb403d71d2e3bc64d001`이다. 이 값은 DBISAM 공식 문서의 기본 엔진 서명 `DBISAM_SIG`를 MD5 해시한 값과 일치한다. 바로 앞 `06`은 해시의 일부가 아니다. 교량 샘플에서는 비어 있는 페이지와 레코드 번호처럼 보이는 평문 바이트열도 확인되어 전체가 암호화됐다는 기존 추정은 잘못됐다. 페이지·트리 갱신 규칙은 미확인이므로 쓰기에는 사용하지 않는다: https://www.elevatesoft.com/manual?action=viewtopic&id=dbisam4&product=rscpp&topic=customizing_engine&version=XE . + - 추가 조사: DBISAM은 `REPAIR TABLE ... FORCEINDEXREBUILD`를 제공한다(https://www.elevatesoft.com/manual?action=viewtopic&id=dbisam4&product=rsdelphiwin64&topic=repair_table_statement&version=XE5). 이 PC의 32·64비트 ODBC 등록 목록에서 DBISAM 드라이버를 찾지 못했다. 공식 시험판 다운로드는 로그인 후 가능하며, 원본 엔진 서명·버전과의 호환성은 확인하지 못했다. 2. 무결성 검증 제약: `exportEstx`는 동일 행 수 및 비인덱스 필드(수치, 일반 텍스트, BLOB 데이터 등)의 수정 저장을 안전하게 지원하지만, 행 추가/삭제나 인덱스 키(`PkID`, `Code`, `Index`, `RecNo` 등)를 변경하면 `.idx`의 B-Tree 구조와 불일치가 발생함. 원본 Delphi/DBISAM 엔진 외부에서 인덱스 재생성을 구현할 수 있는 바이너리 규격이나 소스 근거가 부재함. 3. 구현 범위 및 처리: 따라서 ESTX 내 행 추가/삭제 및 인덱스 키 변경은 기존처럼 명확한 예외 메시지(`{key}: 행 추가/삭제는 인덱스 재생성이 필요합니다`, `{key}.{name}: 인덱스 키 변경은 지원하지 않습니다`)로 안전 차단하고, 신규 파일/행 증감/자유로운 편집이 필요한 작업은 JSON 저장소(`project.tables`)에서 완전 무결하게 지원·보존하도록 범위를 명확히 유지함. - [ ] 남은 완전 호환 범위: ESTX 신규 파일/행 증감·키 변경의 인덱스 재생성, WPTools 편집 저장, 기성/연차 원본 별도 업무 파일; JDL 확장 산식/기성·물가 자료와 원본 검사 DLL 인증; 원본과 같은 인쇄 도형/배치 및 기존 산식 잔여 오류. CAD만 제외한 전체 완료로 판정하지 않음. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index f53810045..52507e696 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -45,6 +45,9 @@ - **조달청 XML**: DB에 보관된 공사원가호환규정 XSD 1.0.5의 요소 순서와 필드를 사용합니다. 작성업체·공사구분을 입력하며 코드/문구 길이와 연결 자료를 검사합니다. 내역·일위·산근·기계·기초 단가·원가를 내보내고 다시 읽습니다. 연결 없는 고정 금액·기계 상세는 독립 단가로 전개하며 변환 전에 알립니다. 가져온 저장 금액과 원문을 보존하고 자동계산을 끕니다. 이는 제출기관 인증/등록 검사나 최신 규정 적합 판정을 뜻하지 않습니다. - **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 불러오고 미변환 구성은 저장 금액으로 보존합니다. 기계경비와 단가산출은 저장된 비목별 금액을 편집 가능한 호표로 가져오며 상세 산식은 원본 JDL에 보존합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다. - 중복 자원 코드로 연결 대상을 구분할 수 없는 JDL 출력은 해당 내역의 저장 금액을 독립 단가로 기록하고 경고합니다. 자원 행 자체는 각각 보존합니다. +- JDL 기계 상세의 수량과 단가를 곱한 금액이 비목별 저장 합계와 맞는 경우 각 상세 금액을 편집할 수 있습니다. 상세 산식과 원천 단가 연결은 원본에 보존하며, 수량 해석이 모호하면 합계만 가져옵니다. +- 가져온 JDL 기계 상세의 편집 금액과 호표 합계가 맞으면 JDL로 다시 저장할 때 각 상세를 고정 단가로 기록합니다. 원래의 상세 산식과 수량 관계는 복원하지 않습니다. +- 일위대가에 연결된 기계·실적·견적·단가산출 자료도 일반 상세 조건을 만족하면 JDL로 출력합니다. 기계 상세가 변환되지 않거나 그 외 복합 원천은 고정 단가로 전개합니다. - **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 기존 수량산출서의 이름과 항목을 수정하고, 확인된 형식의 항목과 단일 결과 표지 산출 행을 추가·삭제할 수 있습니다. 고유한 일반 행 삭제와 조건에 맞는 산출 행 이동도 지원합니다. 연결된 항목이나 중복·복합 표지, 불명확한 서식 구조는 보호를 위해 일부 작업을 거부합니다. 기존 서식 구간의 텍스트를 편집할 때 새 글에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 수량산출서, 인덱스 키 변경, 수량의 내역 연결 변경, 웹 기성·연차·당초 사본은 ESTX 저장을 지원하지 않아 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다. - **예정공정표**: 내역 행을 공정으로 일괄 추가하고 내역 금액으로 가중치를 갱신할 수 있습니다. 코드가 없거나 같은 코드가 반복된 행은 내역의 행 식별자로 구분합니다. 기존 공정의 기간·진도·이름은 유지합니다. 공정별 기간 막대와 입력 진도의 가중평균을 표시하며, 날짜 역전·잘못된 진도를 차단합니다. 공휴일/공기 산정 기준을 임의 적용하지 않습니다. - **기성 진도 반영**: 예정공정표에서 최근 확정 회차의 누계 수량을 해당 회차 계약 수량으로 나누어 연결된 공정의 진도를 갱신할 수 있습니다. 수량이 0이거나 기성 내역에 연결되지 않은 공정은 기존 진도를 유지합니다. 0~100%를 벗어나는 결과는 갱신을 중단합니다. diff --git a/tools/est-test/export-estx.cjs b/tools/est-test/export-estx.cjs index 3ca460415..e2d757b99 100644 --- a/tools/est-test/export-estx.cjs +++ b/tools/est-test/export-estx.cjs @@ -4,7 +4,7 @@ const { pack } = require("./archive.cjs"); const { updateOptions } = require("./export-options.cjs"); const { updateQuantities, quantityFields } = require("./export-quantity.cjs"); const { updatePrintOptions } = require("./export-print.cjs"); -const signature = Buffer.from("068abe8e592364cb403d71d2e3bc64d0", "hex"); +const signature = Buffer.from("068abe8e592364cb403d71d2e3bc64d001", "hex"); const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null); let characters; function cp949(text) { @@ -223,7 +223,7 @@ function exportEstx(project) { } const oldByLower = new Map(Object.keys(base.tables).map((key) => [key.toLowerCase(), key])); for (const [filename, data] of files) { - if (!filename.endsWith(".dat") || !data.subarray(8, 24).equals(signature)) continue; + if (!filename.endsWith(".dat") || !data.subarray(8, 25).equals(signature)) continue; const key = oldByLower.get(filename.slice(0, -4)); if (!key) continue; const rows = project.tables[key], diff --git a/tools/est-test/export-quantity.cjs b/tools/est-test/export-quantity.cjs index 35fef94e1..680789a7e 100644 --- a/tools/est-test/export-quantity.cjs +++ b/tools/est-test/export-quantity.cjs @@ -1,5 +1,6 @@ const zlib = require("node:zlib"); const quantity = require("./native-quantity.cjs"); +const { rowParser } = require("./wpt-parse.cjs"); const { calculateQuantity } = require("./quantity-calculate.cjs"); const { memoryRows } = require("./import-estx.cjs"); const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); @@ -162,8 +163,7 @@ function editDocument(text, oldSheet, sheet) { oldSheet.rows.length !== sheet.rows.length ) throw Error("수량산출서 목록·연결·행 구조 변경은 ESTX 저장을 지원하지 않습니다."); - const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); - const parse = (div) => quantity.fromWpt("", "", declarations + div, [], {}).rows; + const parse = rowParser(text); let rowIndex = 0; const result = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { const parsed = parse(div); @@ -258,11 +258,11 @@ function insertRows(text, sheet, inserts) { staged = next; return { at, snippet }; }); - const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + const parse = rowParser(text); let index = 0; const stylePosition = text.search(/]*>[\s\S]*?<\/div>/gi, (div, offset) => { - if (!quantity.fromWpt("", "", declarations + div, [], {}).rows.length) return div; + if (!parse(div).length) return div; const before = snippets .filter((item) => item.at === index) .map((item) => item.snippet) @@ -282,13 +282,13 @@ function insertRows(text, sheet, inserts) { } function removeRows(text, sheet, codes, plainIndexes = new Set()) { - const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + const parse = rowParser(text); const tags = tagCounts(text); let index = 0; const removed = new Set(); const removedPlain = new Set(); const document = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { - const parsed = quantity.fromWpt("", "", declarations + div, [], {}).rows; + const parsed = parse(div); if (!parsed.length) return div; const rowIndex = index++, row = sheet.rows[rowIndex], @@ -368,17 +368,12 @@ function reorderRows(text, sheet, rows) { } if (order.length !== sheet.rows.length || order.every((value, index) => value === index)) return { text, rows: sheet.rows }; - const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); + const parse = rowParser(text); const divs = [...text.matchAll(/]*>[\s\S]*?<\/div>/gi)].filter( - (match) => quantity.fromWpt("", "", declarations + match[0], [], {}).rows.length, + (match) => parse(match[0]).length, ); if (divs.length !== sheet.rows.length) throw Error("수량산출서 원본 행 대응 오류"); - if ( - divs.some( - (match, index) => - !same(quantity.fromWpt("", "", declarations + match[0], [], {}).rows[0], sheet.rows[index]), - ) - ) + if (divs.some((match, index) => !same(parse(match[0])[0], sheet.rows[index]))) throw Error("수량산출서 원본 행 대응 오류"); const changed = order .map((source, index) => (source === index ? -1 : index)) @@ -413,7 +408,7 @@ function reorderRows(text, sheet, rows) { } let index = 0; const document = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { - if (!quantity.fromWpt("", "", declarations + div, [], {}).rows.length) return div; + if (!parse(div).length) return div; return divs[order[index++]][0]; }); return { text: document, rows: order.map((source) => sheet.rows[source]) }; diff --git a/tools/est-test/import-estx.cjs b/tools/est-test/import-estx.cjs index 5b9813984..e6c774f08 100644 --- a/tools/est-test/import-estx.cjs +++ b/tools/est-test/import-estx.cjs @@ -4,6 +4,7 @@ const quantity = require("./native-quantity.cjs"); const { readPrintOptions } = require("./import-print.cjs"); const { existingTargets } = require("./native-links.cjs"); const decoder = new TextDecoder("euc-kr", { fatal: true }); +const signature = Buffer.from("068abe8e592364cb403d71d2e3bc64d001", "hex"); const LIMIT = 256 * 1024 * 1024; const crcTable = Array.from({ length: 256 }, (_, n) => { for (let i = 0; i < 8; i++) n = n & 1 ? 0xedb88320 ^ (n >>> 1) : n >>> 1; @@ -271,11 +272,18 @@ function importEstx(buffer, name = "가져온 내역서") { warnings = new Set(), templates = []; for (const [filename, data] of files) { - if ( - !filename.endsWith(".dat") || - !data.subarray(8, 24).equals(Buffer.from("068abe8e592364cb403d71d2e3bc64d0", "hex")) - ) + if (!filename.endsWith(".dat")) continue; + if (data.subarray(8, 24).equals(signature.subarray(0, 16)) && data[24] !== signature[16]) + throw Error(`${filename}: DBISAM 엔진 서명이 올바르지 않습니다.`); + if (!data.subarray(8, 25).equals(signature)) { + if ( + /^(?:stuffcdat|laborcdat|siljukdat|estimatedat|machgijundat|machmaindat|machtitledat|costmaindat|costtitledat|headmaindat|headtitledat|estmaindat|mulmaindat|multitledat|wongadat)\.dat$/.test( + filename, + ) + ) + throw Error(`${filename}: DBISAM 테이블 서명이 올바르지 않습니다.`); continue; + } let rows; try { rows = table(data, files.get(filename.replace(/\.dat$/, ".blb")), warnings); diff --git a/tools/est-test/jdl-machine-export.cjs b/tools/est-test/jdl-machine-export.cjs new file mode 100644 index 000000000..1bad7d80b --- /dev/null +++ b/tools/est-test/jdl-machine-export.cjs @@ -0,0 +1,60 @@ +function exportMachines(project, tables, map, ambiguous, num, next, source, warnings) { + const groups = new Map(); + let group; + for (const row of project.tables.MachMainDat || []) { + if (num(row.Index) > 0) { + group = []; + if (groups.has(row.Code)) groups.set(row.Code, null); + else groups.set(row.Code, group); + } else if (group) group.push(row); + } + for (const title of project.tables.MachTitleDat || []) { + const parts = groups.get(title.Code); + if ( + !title.Code || + ambiguous.has("105:" + title.Code) || + !parts?.length || + num(title.SC) || + parts.some((row) => !row.JdlSummary || num(row.CalcIndex) !== 1 || num(row.SC)) || + ["GC", "NC", "JC"].some( + (field) => + Math.abs(parts.reduce((sum, row) => sum + num(row[field]), 0) - num(title[field])) > 1e-6, + ) + ) + continue; + const code = "J" + String(next()).padStart(10, "0"); + map.set("105:" + title.Code, code); + tables["junit.txt"].push({ + F_CODE: code, + F_DES: title.Name, + F_SIZE: title.Size, + F_UNIT: title.Unite, + F_MPRICE: num(title.GC), + F_LPRICE: num(title.NC), + F_EPRICE: num(title.JC), + F_ORICODE: "00000000000", + }); + for (const [index, row] of parts.entries()) { + const code1 = source(0, { ...row, Code: "MACHINE-PART-" + next() }); + tables["jpart.txt"].push({ + F_CODE: code, + F_SEQ: String(index + 1), + F_CODE1: code1, + F_DES: row.Name, + F_SIZE: row.Size, + F_UNIT: row.Unite, + F_QQTY: 1, + F_MPRICE: num(row.GC), + F_LPRICE: num(row.NC), + F_EPRICE: num(row.JC), + F_SPROPER: "0", + F_LOADBM: "T", + F_SAVEBM: "T", + F_ORICODE: "00000000000", + }); + } + warnings.push(`${title.Code}: 기계 상세를 고정 금액으로 내보냅니다.`); + } +} + +module.exports = { exportMachines }; diff --git a/tools/est-test/jdl-unit-order.cjs b/tools/est-test/jdl-unit-order.cjs new file mode 100644 index 000000000..5e3903885 --- /dev/null +++ b/tools/est-test/jdl-unit-order.cjs @@ -0,0 +1,29 @@ +function orderUnits(titles, groups) { + const byCode = new Map(titles.filter((title) => title.Code).map((title) => [title.Code, title])); + const state = new Map(), + ordered = []; + for (const title of titles) { + if (!title.Code || state.has(title.Code)) continue; + const stack = [{ title, expanded: false }]; + while (stack.length) { + const item = stack.pop(), + code = item.title.Code; + if (item.expanded) { + state.set(code, 2); + ordered.push(item.title); + continue; + } + if (state.has(code)) continue; + state.set(code, 1); + stack.push({ title: item.title, expanded: true }); + for (const row of groups.get(code) || []) { + const dependency = Number(row.LinkIndex) === 103 && byCode.get(row.Code); + if (dependency && !state.has(dependency.Code)) + stack.push({ title: dependency, expanded: false }); + } + } + } + return ordered; +} + +module.exports = { orderUnits }; diff --git a/tools/est-test/jdl.cjs b/tools/est-test/jdl.cjs index f58f920fc..a5d82f736 100644 --- a/tools/est-test/jdl.cjs +++ b/tools/est-test/jdl.cjs @@ -3,6 +3,8 @@ const { pack, unpack } = require("./archive.cjs"); const { createHash } = require("node:crypto"); const { cp949 } = require("./export-estx.cjs"); const schema = require("./jdl-schema.json"); +const { exportMachines } = require("./jdl-machine-export.cjs"); +const { orderUnits } = require("./jdl-unit-order.cjs"); const decoder = new TextDecoder("euc-kr", { fatal: true }); function fingerprint(project) { const { originalJdlState, id, ...content } = project; @@ -113,15 +115,21 @@ function output(project) { for (const table of ["CostTitleDat", "MachTitleDat", "HeadTitleDat"]) for (const row of project.tables[table] || []) source({ CostTitleDat: 104, MachTitleDat: 105, HeadTitleDat: 103 }[table], row); + exportMachines(project, tables, map, ambiguous, num, () => ++serial, source, warnings); const unitGroups = new Map(); let unitGroup; for (const row of project.tables.HeadMainDat || []) { if (num(row.Index) > 0) { unitGroup = []; - unitGroups.set(row.Code, unitGroup); + if (unitGroups.has(row.Code)) { + if (unitGroups.get(row.Code)) + warnings.push(`${row.Code}: 중복 일위 상세는 고정 단가로 전개합니다.`); + unitGroups.set(row.Code, null); + } else unitGroups.set(row.Code, unitGroup); } else if (unitGroup) unitGroup.push(row); } - for (const title of project.tables.HeadTitleDat || []) { + const exportedUnits = new Set(); + for (const title of orderUnits(project.tables.HeadTitleDat || [], unitGroups)) { const parts = unitGroups.get(title.Code); if ( !title.Code || @@ -130,7 +138,8 @@ function output(project) { !parts?.length || parts.some( (row) => - ![5, 6].includes(num(row.LinkIndex)) || + ![5, 6, 21, 101, 103, 104, 105].includes(num(row.LinkIndex)) || + (num(row.LinkIndex) === 103 && row.Code === title.Code) || !map.has(num(row.LinkIndex) + ":" + row.Code) || ["CalcIndex", "SKind", "Gangub", "OCheck", "Biul", "SC", "SU"].some((key) => num(row[key]), @@ -140,6 +149,10 @@ function output(project) { continue; const code = "U" + String(++serial).padStart(10, "0"); map.set("103:" + title.Code, code); + for (const row of parts) + if (num(row.LinkIndex) === 103 && !exportedUnits.has(row.Code)) + warnings.push(`${title.Code}/${row.Code}: 하위 일위대가를 고정 단가로 전개합니다.`); + exportedUnits.add(title.Code); tables["unit.txt"].push({ F_CODE: code, F_DES: title.Name, @@ -333,6 +346,7 @@ function read(bytes, project) { machineParts.get(part.F_CODE).push(part); } let machineDetailed = false, + machineWeighted = false, machineFallback = false; for (const title of rows["junit.txt"]) { const gc = num(title.F_MPRICE), @@ -341,13 +355,13 @@ function read(bytes, project) { const amounts = { GC: gc, NC: nc, JC: jc, SC: 0, TC: gc + nc + jc }; const parts = machineParts.get(title.F_CODE) || []; const uniqueSequences = new Set(parts.map((part) => part.F_SEQ)); - const basic = + const ordinary = parts.length > 0 && uniqueSequences.size === parts.length && parts.every( (part) => Boolean(part.F_SEQ) && - num(part.F_QQTY) === 1 && + num(part.F_QQTY) > 0 && !["%", "%"].includes(part.F_UNIT) && ["", "0"].includes(part.F_SPROPER) && ["", "T"].includes(part.F_LOADBM) && @@ -355,15 +369,24 @@ function read(bytes, project) { !num(part.F_SRATE) && ["", "0"].includes(part.F_KWAN) && ["", "0"].includes(part.F_CLASS), - ) && + ); + const matches = (weighted) => [ ["F_MPRICE", gc], ["F_LPRICE", nc], ["F_EPRICE", jc], ].every( ([field, value]) => - Math.abs(parts.reduce((sum, part) => sum + num(part[field]), 0) - value) < 1e-6, + Math.abs( + parts.reduce( + (sum, part) => sum + num(part[field]) * (weighted ? num(part.F_QQTY) : 1), + 0, + ) - value, + ) < 1e-6, ); + const direct = ordinary && matches(false); + const weighted = ordinary && !direct && matches(true); + const basic = (direct && parts.every((part) => num(part.F_QQTY) === 1)) || weighted; project.tables.MachMainDat.push({ PkID: project.tables.MachMainDat.length + 1, Index: 1, @@ -376,16 +399,19 @@ function read(bytes, project) { }); if (basic) { machineDetailed = true; + machineWeighted ||= weighted; for (const part of parts) { - const GC = num(part.F_MPRICE), - NC = num(part.F_LPRICE), - JC = num(part.F_EPRICE); + const factor = weighted ? num(part.F_QQTY) : 1, + GC = num(part.F_MPRICE) * factor, + NC = num(part.F_LPRICE) * factor, + JC = num(part.F_EPRICE) * factor; project.tables.MachMainDat.push({ PkID: project.tables.MachMainDat.length + 1, Index: 0, CalcIndex: 1, JdlSummary: true, JdlSourceCode: part.F_CODE1, + JdlSourceQuantity: num(part.F_QQTY), Name: part.F_DES, Size: part.F_SIZE, Unite: part.F_UNIT, @@ -413,6 +439,8 @@ function read(bytes, project) { unitWarnings.push( "일반 기계 상세는 저장 금액별로 표시하며, 산식과 원천 단가 연결은 원본 JDL에 보존됩니다.", ); + if (machineWeighted) + unitWarnings.push("일부 기계 상세는 수량×단가가 저장 합계와 일치해 계산 금액으로 표시합니다."); if (machineFallback) unitWarnings.push( "일부 기계경비는 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다.", diff --git a/tools/est-test/wpt-parse.cjs b/tools/est-test/wpt-parse.cjs new file mode 100644 index 000000000..d3cea60c3 --- /dev/null +++ b/tools/est-test/wpt-parse.cjs @@ -0,0 +1,24 @@ +const quantity = require("./native-quantity.cjs"); + +function rowParser(text) { + const superscripts = new Set( + [...text.matchAll(/]*\bnr=(\d+)[^>]*>/gi)] + .filter((match) => (Number(match[0].match(/CharStyleON:(\d+)/)?.[1] || 0) & 16) !== 0) + .map((match) => match[1]), + ); + const tags = new Map(); + for (const match of text.matchAll(/]*\bTag=(\d+)[^>]*>/gi)) { + if (quantity.entities(match[0]).match(/Name:["']([^"']+)["']/)) tags.set(match[1], match[0]); + } + return (div) => { + const ids = new Set([...div.matchAll(/\bTag=(\d+)/gi)].map((match) => match[1])); + const declarations = [...ids].map((id) => tags.get(id) || "").join(""); + const styles = [...new Set([...div.matchAll(//gi)].map((m) => m[1]))] + .filter((id) => superscripts.has(id)) + .map((id) => ``) + .join(""); + return quantity.fromWpt("", "", styles + declarations + div, [], {}).rows; + }; +} + +module.exports = { rowParser }; From 7b8a5850dfc243aafd26e0870bfa95f44479cc30 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:47:35 +0900 Subject: [PATCH 20/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC735):=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EB=8F=99=EC=9D=98=20=C2=B7=20=EB=B3=84?= =?UTF-8?q?=EC=B9=AD=20=EC=95=84=ED=99=89=20=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 249e2c3da..e66f4699f 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -693,3 +693,20 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 6. 문제 없음. 7. 문제 없음. 1 ~ 5번은 한번에 진행할거야. 가능한 경우에는 다른 워크트리를 사용해도됨. 그러니 먼저 대답부터 + +## 요구 735 (11-09 · 사용자 · 1~3 동의 · 작업량당 기계경비 = 필요하면 기계 하나 넣어 만들어 씀 · 별칭 아홉 답) + +1. 동의 +2. 동의 +3. 동의 +4. 작업량당 기계경비는 본인이 필요한 경우 기계하나만 넣고 만들어 사용하면 됨. +5. 별칭 확인 + 1) 후보맞음. + 2) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력) + 3) 후보맞음. + 4) 엔진(디젤) 이렇게 되어 있음. 가솔린은 엔진(가솔린)으로 되어 있음. + 5) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력) + 6) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력) + 7) 워터젯트 + 8) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력) + 9) 후보맞음 From da8867b0e7f6acfec1d7167cc9ef6cfff8c8c4c4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:59:12 +0900 Subject: [PATCH 21/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC736):=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=EB=B8=8C=EB=A0=88=EC=9D=B4=ED=8A=B8=20=3D=20?= =?UTF-8?q?=EC=97=94=EC=A7=84=EC=8B=9D=20=EC=A7=84=EB=8F=99=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index e66f4699f..ecf8aa8b7 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -710,3 +710,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 7) 워터젯트 8) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력) 9) 후보맞음 + +## 요구 736 (11-09 · 사용자 · 바이브레이터 규격이 하나면 엔진식) + +바이브 레이터에 규격은 하나임? 하나라면 엔진식이 맞음. From 741805ffdecfd92bae20c5fe65d4a28435a6f6f8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 19:59:33 +0900 Subject: [PATCH 22/29] feat(est-test): preserve special JDL costs --- resources/tester/test_est_jdl.py | 101 ++++++++++++++++++++++- resources/tester/test_est_jdl_special.py | 93 +++++++++++++++++++++ resources/tester/test_est_text_parser.py | 31 +++++++ tools/est-test/PLAN.md | 18 ++++ tools/est-test/README.md | 4 + tools/est-test/jdl-machine-parts.cjs | 12 +++ tools/est-test/jdl-special-unit.cjs | 42 ++++++++++ tools/est-test/jdl-unit-order.cjs | 5 +- tools/est-test/jdl-unit-part.cjs | 34 ++++++++ tools/est-test/jdl.cjs | 100 ++++++++++++---------- tools/est-test/native-text.js | 15 +++- 11 files changed, 408 insertions(+), 47 deletions(-) create mode 100644 resources/tester/test_est_jdl_special.py create mode 100644 tools/est-test/jdl-machine-parts.cjs create mode 100644 tools/est-test/jdl-special-unit.cjs create mode 100644 tools/est-test/jdl-unit-part.cjs diff --git a/resources/tester/test_est_jdl.py b/resources/tester/test_est_jdl.py index 71ff5dda3..06e8f4851 100644 --- a/resources/tester/test_est_jdl.py +++ b/resources/tester/test_est_jdl.py @@ -108,6 +108,35 @@ def test_jdl_basic_tables_use_native_separators_encoding_and_preserve_direct_mon assert result["warnings"] +def test_large_jdl_import_keeps_group_order_and_rejects_orphans_and_duplicates(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const n=3000,groups=Array.from({length:n},(_,i)=>({F_CODE:String(i),F_DES:'group '+i})); + const parts=groups.slice().reverse().flatMap(g=>[{F_CODE:g.F_CODE,F_SEQ:'1',F_CODE1:'M',F_DES:'first',F_QQTY:1,F_MPRICE:3}, + {F_CODE:g.F_CODE,F_SEQ:'2',F_CODE1:'M',F_DES:'second',F_QQTY:2,F_MPRICE:3}]); + function archive(gs,ps){return a.pack(new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['m.txt',table('m.txt',[{F_CODE:'M',F_DES:'material',F_MPRICE:3}])],['gongjong.txt',table('gongjong.txt',gs)],['mainpart.txt',table('mainpart.txt',ps)]]))} + const result=j.read(archive(groups,parts),structuredClone(empty));let orphan=false,duplicate=false; + try{j.read(archive(groups.slice(0,1),parts.slice(0,1)),structuredClone(empty))}catch{orphan=true} + try{j.read(archive([groups[0],groups[0]],[]),structuredClone(empty))}catch{duplicate=true} + const rows=result.tables.EstMainDat; + process.stdout.write(JSON.stringify({count:rows.length,first:rows.slice(0,3).map(r=>r.Name),total:rows.at(-1).TC,orphan,duplicate}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT, timeout=30 + ) + ) + assert result == { + "count": 9001, + "first": ["group 0", "first", "second"], + "total": 27000, + "orphan": True, + "duplicate": True, + } + + def test_unchanged_import_preserves_original_jdl_and_edited_work_is_not_silently_discarded(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),s=require('./tools/est-test/server.cjs'); @@ -228,7 +257,51 @@ def test_jdl_unit_price_preserves_other_source_costs_as_fixed_resources(): ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT ) ) - assert result == {"parts": 4, "links": [101, 101, 101, 101], "total": 40} + assert result == {"parts": 4, "links": [101, 101, 104, 101], "total": 40} + + +def test_jdl_cost_title_survives_direct_and_unit_price_links(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='cost'; + p.tables.CostTitleDat=[{Code:'D',Name:'cost',GC:100,NC:20,JC:5,TC:125}]; + p.tables.HeadTitleDat=[{Code:'U',Name:'unit',GC:100,NC:20,JC:5,TC:125}]; + p.tables.HeadMainDat=[{Index:1,Code:'U'}, + {Index:0,Code:'D',Name:'cost',LinkIndex:104,GN:1,GU:100,NU:20,JU:5,GC:100,NC:20,JC:5,TC:125}]; + p.tables.EstMainDat=[{Code:'D',Name:'direct',LinkIndex:104,GN:2,GU:100,NU:20,JU:5,GC:200,NC:40,JC:10,TC:250}, + {Code:'U',Name:'unit',LinkIndex:103,GN:3,GU:100,NU:20,JU:5,GC:300,NC:60,JC:15,TC:375},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({danga:j.parse('danga.txt',files.get('danga.txt')).length, + costs:q.tables.CostMainDat,unit:q.tables.HeadMainDat[1].LinkIndex, + direct:q.tables.EstMainDat[1].LinkIndex,total:q.tables.EstMainDat.at(-1).TC,warnings:out.warnings}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["danga"] == 1 + assert [row["TC"] for row in result["costs"][1:]] == [100, 20, 5] + assert result["unit"] == result["direct"] == 104 + assert result["total"] == 625 + assert result["warnings"] + + +def test_jdl_duplicate_cost_titles_fall_back_without_wrong_link(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),empty=structuredClone(p);p.name='duplicates'; + p.tables.CostTitleDat=[{Code:'D',Name:'one',GC:10},{Code:'D',Name:'two',GC:20}]; + p.tables.EstMainDat=[{Code:'D',Name:'item',LinkIndex:104,GN:2,GU:20,GC:40,TC:40},{LinkIndex:54}]; + const out=j.output(p),files=a.unpack(out.bytes),q=j.read(out.bytes,empty); + process.stdout.write(JSON.stringify({danga:j.parse('danga.txt',files.get('danga.txt')).length, + kind:q.tables.EstMainDat[1].LinkIndex,total:q.tables.EstMainDat.at(-1).TC, + warning:out.warnings.some(x=>x.includes('중복'))}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result == {"danga": 0, "kind": 101, "total": 40, "warning": True} def test_jdl_nested_units_preserve_detail_or_warn_on_forward_reference(): @@ -437,6 +510,32 @@ def test_jdl_complete_unit_preserves_difference_from_saved_title(): assert [row["TC"] for row in result["titles"]] == [205.25, 30.12] +def test_jdl_fixed_unit_detail_and_saved_difference_survive_edit_and_export(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['m.txt',table('m.txt',[{F_CODE:'M1',F_MPRICE:100}])], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:205.25}])], + ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_SEQ:'1',F_CODE1:'M1',F_DES:'material',F_QQTY:2,F_MPRICE:100,F_MCOST:200}])], + ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], + ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'U1',F_DES:'unit',F_QQTY:1,F_MPRICE:205.25}])]]); + const p=j.read(a.pack(files),empty);p.options.AutoCalc=true;p.options.SourceRefresh=true; + p.tables.HeadMainDat[2].GC=7;const q=s.calculate(p),out=j.output(q),entries=a.unpack(out.bytes),back=j.read(out.bytes,blank); + process.stdout.write(JSON.stringify({parts:j.parse('unitpart.txt',entries.get('unitpart.txt')), + rows:back.tables.HeadMainDat,total:back.tables.EstMainDat.at(-1).TC,errors:q.calculation.errors}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert len(result["parts"]) == 2 + assert [row["GC"] for row in result["rows"][1:3]] == [200, 7] + assert result["total"] == 207 + assert not result["errors"] + + def test_jdl_unit_inconsistent_detail_keeps_recorded_amount_without_double_counting(): project = run("math", "EstNative.empty()") script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); diff --git a/resources/tester/test_est_jdl_special.py b/resources/tester/test_est_jdl_special.py new file mode 100644 index 000000000..e8a106577 --- /dev/null +++ b/resources/tester/test_est_jdl_special.py @@ -0,0 +1,93 @@ +import json +import subprocess +from pathlib import Path +from test_est_rea_parity import run + +ROOT = Path(__file__).resolve().parents[2] + + +def test_jdl_special_unit_detail_with_matching_totals_is_editable(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['m.txt',table('m.txt',[{F_CODE:'M1',F_DES:'material',F_MPRICE:100}])], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:300,F_LPRICE:50}])], + ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_SEQ:'1',F_CODE1:'M1',F_DES:'normal',F_QQTY:2,F_MPRICE:100,F_MCOST:200}, + {F_CODE:'U1',F_SEQ:'2',F_CODE1:'M1',F_DES:'special',F_QQTY:1,F_MCOST:100,F_LCOST:50,F_SPROPER:'1'}])], + ['gongjong.txt',table('gongjong.txt',[{F_CODE:'01',F_DES:'work'}])], + ['mainpart.txt',table('mainpart.txt',[{F_CODE:'01',F_SEQ:'1',F_CODE1:'U1',F_DES:'unit',F_QQTY:1,F_MPRICE:300,F_LPRICE:50}])]]); + const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.HeadMainDat);p.options.AutoCalc=true;p.options.SourceRefresh=true; + p.tables.HeadMainDat[2].GC=110;const q=s.calculate(p),out=j.output(q),back=j.read(out.bytes,blank); + process.stdout.write(JSON.stringify({before,title:q.tables.HeadTitleDat[0],rows:back.tables.HeadMainDat, + total:back.tables.EstMainDat.at(-1).TC,warnings:p.importWarnings,errors:q.calculation.errors}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert len(result["before"]) == 3 + assert result["before"][2]["Name"] == "special" + assert result["before"][2]["CalcIndex"] == 1 + assert result["title"]["TC"] == 360 + assert result["rows"][2]["GC"] == 110 + assert result["rows"][2]["NC"] == 50 + assert result["total"] == 360 + assert any("특수 구성" in warning for warning in result["warnings"]) + assert not result["errors"] + + +def test_jdl_special_unit_detail_without_source_keeps_recorded_amount(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['unit.txt',table('unit.txt',[{F_CODE:'U1',F_DES:'unit',F_MPRICE:70}])], + ['unitpart.txt',table('unitpart.txt',[{F_CODE:'U1',F_SEQ:'1',F_CODE1:'missing',F_DES:'special',F_QQTY:1,F_MCOST:70,F_SPROPER:'1'}])]]); + const p=j.read(a.pack(files),empty);process.stdout.write(JSON.stringify({rows:p.tables.HeadMainDat,warnings:p.importWarnings}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert len(result["rows"]) == 2 + assert result["rows"][1]["GC"] == 70 + assert result["rows"][1]["LinkIndex"] == 0 + assert result["rows"][1]["JdlSourceCode"] == "missing" + assert any("특수 구성" in warning for warning in result["warnings"]) + + +def test_jdl_special_machine_detail_is_fixed_only_when_amounts_match(): + project = run("math", "EstNative.empty()") + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'),schema=require('./tools/est-test/jdl-schema.json'),s=require('./tools/est-test/server.cjs'); + const empty=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(empty); + const table=(name,rows)=>Buffer.from(rows.map(r=>schema[name].map(f=>r[f.name]??(f.kind===6?'0':'')).join('\\t')+'\\x05\\r\\n').join('')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tSample\\t02\\x05\\r\\n')], + ['junit.txt',table('junit.txt',[{F_CODE:'J1',F_DES:'machine',F_MPRICE:100,F_LPRICE:20}, + {F_CODE:'J2',F_DES:'mismatch',F_MPRICE:50},{F_CODE:'J3',F_DES:'percent',F_MPRICE:50}, + {F_CODE:'J4',F_DES:'weighted',F_MPRICE:100}])], + ['jpart.txt',table('jpart.txt',[{F_CODE:'J1',F_SEQ:'1',F_DES:'normal',F_QQTY:1,F_MPRICE:60}, + {F_CODE:'J1',F_SEQ:'2',F_DES:'special',F_QQTY:1,F_MPRICE:40,F_LPRICE:20,F_SPROPER:'1'}, + {F_CODE:'J2',F_SEQ:'1',F_DES:'mismatch',F_QQTY:1,F_MPRICE:10,F_SPROPER:'1'}, + {F_CODE:'J3',F_SEQ:'1',F_DES:'percent',F_UNIT:'%',F_QQTY:1,F_MPRICE:50,F_SPROPER:'1'}, + {F_CODE:'J4',F_SEQ:'1',F_DES:'weighted part',F_QQTY:2,F_MPRICE:50,F_SPROPER:'1'}])]]); + const p=j.read(a.pack(files),empty),before=structuredClone(p.tables.MachMainDat);p.options.AutoCalc=true; + p.tables.MachMainDat[2].GC=50;const q=s.calculate(p),out=j.output(q),back=j.read(out.bytes,blank); + process.stdout.write(JSON.stringify({before,titles:q.tables.MachTitleDat,back:back.tables.MachMainDat, + warnings:p.importWarnings,errors:q.calculation.errors}));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["before"][2]["Name"] == "special" + assert result["before"][2]["CalcIndex"] == 1 + assert result["titles"][0]["TC"] == 130 + assert result["back"][2]["GC"] == 50 + assert result["titles"][3]["TC"] == 100 + assert "상세 산식 미변환" in result["before"][4]["Name"] + assert "상세 산식 미변환" in result["before"][6]["Name"] + assert any("특수 기계 상세" in warning for warning in result["warnings"]) + assert not result["errors"] diff --git a/resources/tester/test_est_text_parser.py b/resources/tester/test_est_text_parser.py index daaceaec0..d46594ec3 100644 --- a/resources/tester/test_est_text_parser.py +++ b/resources/tester/test_est_text_parser.py @@ -18,6 +18,37 @@ def test_mean_sea_level_labels_do_not_break_variable_declarations(): assert result["checks"][0]["calculated"] == 7.27 +def test_redundant_price_caption_keeps_quantity_and_source_updates(): + result = run( + "math", + """(() => { + const rows = [ + {Index:1, Code:'D1'}, + {Index:0, LineData:'Q = 7.35'}, + {Index:0, SCheck:-2, LinkIndex:5, + LineData:'\\x0e황동주물\\x0e \\x0324,200\\x03C × Q = 24,200 × 7.35 = \\x12177,870\\x12'} + ]; + const initial=EstText.calculate(rows); + rows[1].LineData='Q = 4'; + const changed=EstText.calculate(rows,{},()=>({GC:30000,TC:30000,Code:'M1',SourceKind:5})); + const repeat=EstText.calculate(changed.rows,{},()=>({GC:30000,TC:30000,Code:'M1',SourceKind:5})); + const conflict=structuredClone(rows);conflict.splice(1,0,{Index:0,LineData:'C = 2'}); + const declared=EstText.calculate(conflict); + const mismatch=structuredClone(rows);mismatch[2].LineData=mismatch[2].LineData.replace('= 24,200','= 24,201'); + return {initial,changed,repeat,declared,mismatch:EstText.calculate(mismatch)}; + })()""", + ) + assert result["initial"]["errors"] == [] + assert result["initial"]["rows"][2]["TC"] == 177870 + assert result["changed"]["errors"] == [] + assert result["changed"]["rows"][2]["TC"] == 120000 + assert result["changed"]["rows"][2]["ResourceFactor"] == 4 + assert result["repeat"]["errors"] == [] + assert result["repeat"]["rows"][2]["TC"] == 120000 + assert result["declared"]["errors"] + assert result["mismatch"]["errors"] + + def test_equals_in_description_is_not_a_formula_separator(): result = run( "math", diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 4cf0ae1bc..421280df1 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -423,6 +423,24 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 백그라운드 상태 감시 및 자동 알림: `tools/est-test/monitor.cjs`를 통해 15초 주기로 `orca.exe` CLI(`shell: false`, timeout 4~5s)를 호출하여 CODEX 워크트리의 안티그래비티 작업 상태와 EST 5175 서버 헬스를 조회하고, 상태 변경 시에만 `monitor.log`에 기록(최대 512KB 순환). 안티그래비티 완료/대기 전이 시 Codex Orchestration Run(`run_cdb1558b9280`)으로 단발성 자동 알림(`status`/`messageId=msg_2c7bca748f91`)이 실제 전달됨을 확인(중복 폭주 방지 플래그 적용, 민감 환경변수 미기록, 재시작/자동지시 없음). ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] JDL 기계경비의 특수 상세도 저장 금액 합계가 명확히 호표와 일치하면 개별 고정 금액으로 가져와 편집·출력한다. 특수 산식은 해석하지 않는다. + - 자체 검증: 합성 JDL의 일반·특수 상세 120을 각각 불러와 특수 금액 변경 후 합계 130, JDL 재가져오기 유지. 합계 불일치·퍼센트 행은 기존 호표 합계 경로 유지, 수량 2의 합계 일치 사례 확인. JDL·업무·오류 검사 58개 통과. 특수 산식·외부 JDL 인증은 미확인. 이후 산식·JDL·업무·오류 검색 통합 회귀 107개 통과. + +- [x] JDL 특수 일위 상세의 저장 비목 합계가 호표와 정확히 일치하면 상세별 고정 금액으로 가져와 편집·출력한다. 불일치나 행 식별이 모호한 경우 기존 합계 보존을 유지한다. + - 자체 검증: 정상·특수 상세 합계 350인 합성 JDL에서 특수 상세 금액 변경 후 360으로 내역까지 재계산·JDL 왕복. 원천이 없는 특수 행의 저장 금액 보존, 합계 불일치 시 기존 합계 경로 유지. JDL·업무·오류 검사 57개 통과. 특수 산식과 외부 JDL 인증은 미확인. + +- [x] 단가 표지 뒤 남은 미선언 C가 대입식의 단가와 동일한 설명인 경우 수식에서 구분하고, 단가·수량 변경 전파와 실제 원본 자료 회귀를 검증한다. + - 자체 검증: 교량 원본 행과 단가·수량 변경 후 반복 재계산 및 동원 수량 계수 확인. 변경된 단가가 뒤의 숫자 대입에도 반영되며, C가 선언되거나 숫자 대입이 다른 경우 보호한다. 설치 ESTX 24개 변경 전후 수식 오류 15→14, 저장 결과 일치 114,378→114,379, 파일별 일치 감소 없음. 산근 시험 49개와 추가 보호 시험 통과. 실제 교량 자료에서 원천 단가 24,200→30,000 변경 후 220,500원·동원 계수 7.35가 두 번의 계산에서 안정적으로 유지됨을 확인. + +- [x] JDL 출력에서 단가산출 호표의 비목별 저장 금액과 내역·일위 연결을 단가산출 자료로 보존하고 재가져오기를 검증한다. 원래 산출근거는 JSON에 보존하고 변환 시 알린다. + - 자체 검증: 합성 자료의 단가산출→직접 내역·일위 연결 및 직접비 625 재가져오기 확인. 설치 교량 ESTX를 JDL로 전환·재가져와 단가산출 호표 41개와 내역 합계 7,989,852,162원 유지. JDL·업무 시험 41개 통과. 상시 서버 API에서 JDL 단가산출 100→120 수정 후 저장·재열기 시 연결 104와 내역 합계 290 확인. 원래 산출근거와 외부 인증은 미지원. + +- [x] JDL 일위대가의 고정 금액 상세와 저장 합계 차액을 각각 보존해 다시 출력하고, 편집 후 재가져오기와 합계를 검증한다. + - 자체 검증: 합성 JDL의 상세 200과 저장 차액 5.25를 가져와 차액 7로 변경한 뒤 출력·재가져오기 시 두 상세와 합계 207 유지. JDL·업무 시험 40개 통과. 고정 금액을 독립 단가로 출력하므로 재가져온 정수 상세는 일반 연결 행으로 표시될 수 있다. + +- [x] 대량 JDL 공종 가져오기에서 내역을 공종별로 미리 묶어 반복 전체 검색을 없애고 공종 누락·중복 검사를 유지한다. + - 자체 검증: 3,000공종·6,000상세의 그룹 순서와 합계 27,000원, 부모 누락·중복 공종 거부 확인. JDL·업무 시험 39개 통과. 별도 주요 EST 업무·계산·입출력 회귀는 278개 통과(대용량 원본 저장과 브라우저 캐시 시험 제외). + - [x] DBISAM 엔진 서명 전체를 확인해 손상된 ESTX 테이블을 정상 자료로 읽지 않도록 하고 실제 파일·손상 사례를 검증한다. - 자체 검증: 기존 16바이트 접두 일치 후 마지막 해시 바이트가 다른 테이블과 자재 등 주요 테이블의 서명 손상을 거부해 조용한 누락을 방지. 설치 ESTX 24개 읽기, 정상 main.estx 변조 거부, 실제 원본 저장·인덱스 보존 포함 관련 시험 18개와 추가 손상·산식 회귀 39개 통과. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 52507e696..cbd94672b 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -48,6 +48,10 @@ - JDL 기계 상세의 수량과 단가를 곱한 금액이 비목별 저장 합계와 맞는 경우 각 상세 금액을 편집할 수 있습니다. 상세 산식과 원천 단가 연결은 원본에 보존하며, 수량 해석이 모호하면 합계만 가져옵니다. - 가져온 JDL 기계 상세의 편집 금액과 호표 합계가 맞으면 JDL로 다시 저장할 때 각 상세를 고정 단가로 기록합니다. 원래의 상세 산식과 수량 관계는 복원하지 않습니다. - 일위대가에 연결된 기계·실적·견적·단가산출 자료도 일반 상세 조건을 만족하면 JDL로 출력합니다. 기계 상세가 변환되지 않거나 그 외 복합 원천은 고정 단가로 전개합니다. +- JDL 일위대가의 고정 금액 상세와 저장 합계 차액은 별도 단가로 내보냅니다. 다시 가져온 정수 금액은 해당 별도 단가에 연결된 일반 행으로 표시될 수 있습니다. +- 특수 일위 상세라도 행별 저장 금액의 합계가 호표와 맞고 행 식별이 가능하면 각 금액을 고정해 편집할 수 있습니다. 특수 산식은 원본 JDL에 보존하며 변환하지 않습니다. +- 특수 기계 상세도 수량과 금액의 해석이 명확히 합계와 맞는 경우 개별 고정 금액으로 표시합니다. 퍼센트 행과 합계가 불일치하는 구성은 호표 합계를 보존합니다. +- 단가산출 호표의 저장된 비목별 금액은 JDL 단가산출 자료로 내보내 내역·일위 연결을 유지합니다. 원래 산출근거의 식은 JDL에서 복원되지 않습니다. - **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 기존 수량산출서의 이름과 항목을 수정하고, 확인된 형식의 항목과 단일 결과 표지 산출 행을 추가·삭제할 수 있습니다. 고유한 일반 행 삭제와 조건에 맞는 산출 행 이동도 지원합니다. 연결된 항목이나 중복·복합 표지, 불명확한 서식 구조는 보호를 위해 일부 작업을 거부합니다. 기존 서식 구간의 텍스트를 편집할 때 새 글에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 수량산출서, 인덱스 키 변경, 수량의 내역 연결 변경, 웹 기성·연차·당초 사본은 ESTX 저장을 지원하지 않아 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다. - **예정공정표**: 내역 행을 공정으로 일괄 추가하고 내역 금액으로 가중치를 갱신할 수 있습니다. 코드가 없거나 같은 코드가 반복된 행은 내역의 행 식별자로 구분합니다. 기존 공정의 기간·진도·이름은 유지합니다. 공정별 기간 막대와 입력 진도의 가중평균을 표시하며, 날짜 역전·잘못된 진도를 차단합니다. 공휴일/공기 산정 기준을 임의 적용하지 않습니다. - **기성 진도 반영**: 예정공정표에서 최근 확정 회차의 누계 수량을 해당 회차 계약 수량으로 나누어 연결된 공정의 진도를 갱신할 수 있습니다. 수량이 0이거나 기성 내역에 연결되지 않은 공정은 기존 진도를 유지합니다. 0~100%를 벗어나는 결과는 갱신을 중단합니다. diff --git a/tools/est-test/jdl-machine-parts.cjs b/tools/est-test/jdl-machine-parts.cjs new file mode 100644 index 000000000..9c81be78b --- /dev/null +++ b/tools/est-test/jdl-machine-parts.cjs @@ -0,0 +1,12 @@ +function ordinaryMachinePart(part, num) { + return ( + ["", "0"].includes(part.F_SPROPER) && + ["", "T"].includes(part.F_LOADBM) && + ["", "T"].includes(part.F_SAVEBM) && + !num(part.F_SRATE) && + ["", "0"].includes(part.F_KWAN) && + ["", "0"].includes(part.F_CLASS) + ); +} + +module.exports = { ordinaryMachinePart }; diff --git a/tools/est-test/jdl-special-unit.cjs b/tools/est-test/jdl-special-unit.cjs new file mode 100644 index 000000000..b3990ec6f --- /dev/null +++ b/tools/est-test/jdl-special-unit.cjs @@ -0,0 +1,42 @@ +function canPreserveSpecial(parts, title, num) { + const sequences = new Set(parts.map((part) => part.F_SEQ)); + return ( + parts.length > 0 && + sequences.size === parts.length && + parts.every((part) => Boolean(part.F_SEQ)) && + [ + ["F_MCOST", "F_MPRICE"], + ["F_LCOST", "F_LPRICE"], + ["F_ECOST", "F_EPRICE"], + ].every( + ([field, total]) => + Math.abs(parts.reduce((sum, part) => sum + num(part[field]), 0) - num(title[total])) < 1e-6, + ) + ); +} + +function fixedSpecialPart(part, source, pk, num) { + const GC = num(part.F_MCOST), + NC = num(part.F_LCOST), + JC = num(part.F_ECOST); + return { + PkID: pk, + Index: 0, + Code: source ? part.F_CODE1 : "", + JdlSourceCode: part.F_CODE1, + Name: part.F_DES, + Size: part.F_SIZE, + Unite: part.F_UNIT, + LinkIndex: source?.kind || 0, + CalcIndex: 1, + JdlSummary: true, + GN: num(part.F_QQTY), + GC, + NC, + JC, + SC: 0, + TC: GC + NC + JC, + }; +} + +module.exports = { canPreserveSpecial, fixedSpecialPart }; diff --git a/tools/est-test/jdl-unit-order.cjs b/tools/est-test/jdl-unit-order.cjs index 5e3903885..bb1b954f4 100644 --- a/tools/est-test/jdl-unit-order.cjs +++ b/tools/est-test/jdl-unit-order.cjs @@ -17,7 +17,10 @@ function orderUnits(titles, groups) { state.set(code, 1); stack.push({ title: item.title, expanded: true }); for (const row of groups.get(code) || []) { - const dependency = Number(row.LinkIndex) === 103 && byCode.get(row.Code); + const dependency = + !(row.JdlSummary && Number(row.CalcIndex) === 1) && + Number(row.LinkIndex) === 103 && + byCode.get(row.Code); if (dependency && !state.has(dependency.Code)) stack.push({ title: dependency, expanded: false }); } diff --git a/tools/est-test/jdl-unit-part.cjs b/tools/est-test/jdl-unit-part.cjs new file mode 100644 index 000000000..f293df3d1 --- /dev/null +++ b/tools/est-test/jdl-unit-part.cjs @@ -0,0 +1,34 @@ +function isFixedPart(row, num) { + return Boolean(row.JdlSummary) && num(row.CalcIndex) === 1; +} + +function exportUnitPart(row, code, index, map, source, num, next) { + const fixed = isFixedPart(row, num); + const code1 = fixed + ? source(0, { ...row, Code: "UNIT-PART-" + next() }) + : map.get(num(row.LinkIndex) + ":" + row.Code); + return { + F_CODE: code, + F_SEQ: String(index + 1), + F_CODE1: code1, + F_DES: row.Name, + F_SIZE: row.Size, + F_UNIT: row.Unite, + F_QQTY: fixed ? 1 : num(row.GN), + F_MPRICE: num(fixed ? row.GC : row.GU), + F_LPRICE: num(fixed ? row.NC : row.NU), + F_EPRICE: num(fixed ? row.JC : row.JU) + num(fixed ? row.SC : row.SU), + F_MCOST: num(row.GC), + F_LCOST: num(row.NC), + F_ECOST: num(row.JC) + num(row.SC), + F_SPROPER: "0", + F_LOADBM: "T", + F_SAVEBM: "T", + F_SRATE: 0, + F_KWAN: "0", + F_CLASS: "0", + F_ORICODE: "00000000000", + }; +} + +module.exports = { isFixedPart, exportUnitPart }; diff --git a/tools/est-test/jdl.cjs b/tools/est-test/jdl.cjs index a5d82f736..5b08bbbd8 100644 --- a/tools/est-test/jdl.cjs +++ b/tools/est-test/jdl.cjs @@ -5,6 +5,9 @@ const { cp949 } = require("./export-estx.cjs"); const schema = require("./jdl-schema.json"); const { exportMachines } = require("./jdl-machine-export.cjs"); const { orderUnits } = require("./jdl-unit-order.cjs"); +const { isFixedPart, exportUnitPart } = require("./jdl-unit-part.cjs"); +const { canPreserveSpecial, fixedSpecialPart } = require("./jdl-special-unit.cjs"); +const { ordinaryMachinePart } = require("./jdl-machine-parts.cjs"); const decoder = new TextDecoder("euc-kr", { fatal: true }); function fingerprint(project) { const { originalJdlState, id, ...content } = project; @@ -115,6 +118,21 @@ function output(project) { for (const table of ["CostTitleDat", "MachTitleDat", "HeadTitleDat"]) for (const row of project.tables[table] || []) source({ CostTitleDat: 104, MachTitleDat: 105, HeadTitleDat: 103 }[table], row); + for (const title of project.tables.CostTitleDat || []) { + if (!title.Code || ambiguous.has("104:" + title.Code)) continue; + const code = "D" + String(++serial).padStart(10, "0"); + map.set("104:" + title.Code, code); + tables["danga.txt"].push({ + F_CODE: code, + F_DES: title.Name, + F_SIZE: title.Size, + F_UNIT: title.Unite, + F_MPRICE: num(title.GC), + F_LPRICE: num(title.NC), + F_EPRICE: num(title.JC) + num(title.SC), + F_ORICODE: "00000000000", + }); + } exportMachines(project, tables, map, ambiguous, num, () => ++serial, source, warnings); const unitGroups = new Map(); let unitGroup; @@ -138,20 +156,22 @@ function output(project) { !parts?.length || parts.some( (row) => - ![5, 6, 21, 101, 103, 104, 105].includes(num(row.LinkIndex)) || - (num(row.LinkIndex) === 103 && row.Code === title.Code) || - !map.has(num(row.LinkIndex) + ":" + row.Code) || - ["CalcIndex", "SKind", "Gangub", "OCheck", "Biul", "SC", "SU"].some((key) => - num(row[key]), - ), + (!isFixedPart(row, num) && + (![5, 6, 21, 101, 103, 104, 105].includes(num(row.LinkIndex)) || + num(row.CalcIndex) || + (num(row.LinkIndex) === 103 && row.Code === title.Code) || + !map.has(num(row.LinkIndex) + ":" + row.Code))) || + ["SKind", "Gangub", "OCheck", "Biul", "SC", "SU"].some((key) => num(row[key])), ) ) continue; const code = "U" + String(++serial).padStart(10, "0"); map.set("103:" + title.Code, code); for (const row of parts) - if (num(row.LinkIndex) === 103 && !exportedUnits.has(row.Code)) + if (!isFixedPart(row, num) && num(row.LinkIndex) === 103 && !exportedUnits.has(row.Code)) warnings.push(`${title.Code}/${row.Code}: 하위 일위대가를 고정 단가로 전개합니다.`); + if (parts.some((row) => isFixedPart(row, num))) + warnings.push(`${title.Code}: 일위 상세의 고정 금액을 별도 단가로 내보냅니다.`); exportedUnits.add(title.Code); tables["unit.txt"].push({ F_CODE: code, @@ -164,28 +184,9 @@ function output(project) { F_ORICODE: "00000000000", }); for (const [index, row] of parts.entries()) - tables["unitpart.txt"].push({ - F_CODE: code, - F_SEQ: String(index + 1), - F_CODE1: map.get(num(row.LinkIndex) + ":" + row.Code), - F_DES: row.Name, - F_SIZE: row.Size, - F_UNIT: row.Unite, - F_QQTY: num(row.GN), - F_MPRICE: num(row.GU), - F_LPRICE: num(row.NU), - F_EPRICE: num(row.JU) + num(row.SU), - F_MCOST: num(row.GC), - F_LCOST: num(row.NC), - F_ECOST: num(row.JC) + num(row.SC), - F_SPROPER: "0", - F_LOADBM: "T", - F_SAVEBM: "T", - F_SRATE: 0, - F_KWAN: "0", - F_CLASS: "0", - F_ORICODE: "00000000000", - }); + tables["unitpart.txt"].push( + exportUnitPart(row, code, index, map, source, num, () => ++serial), + ); } let group = null, groupNumber = 0, @@ -347,6 +348,7 @@ function read(bytes, project) { } let machineDetailed = false, machineWeighted = false, + machineSpecial = false, machineFallback = false; for (const title of rows["junit.txt"]) { const gc = num(title.F_MPRICE), @@ -355,21 +357,13 @@ function read(bytes, project) { const amounts = { GC: gc, NC: nc, JC: jc, SC: 0, TC: gc + nc + jc }; const parts = machineParts.get(title.F_CODE) || []; const uniqueSequences = new Set(parts.map((part) => part.F_SEQ)); - const ordinary = + const identifiable = parts.length > 0 && uniqueSequences.size === parts.length && parts.every( - (part) => - Boolean(part.F_SEQ) && - num(part.F_QQTY) > 0 && - !["%", "%"].includes(part.F_UNIT) && - ["", "0"].includes(part.F_SPROPER) && - ["", "T"].includes(part.F_LOADBM) && - ["", "T"].includes(part.F_SAVEBM) && - !num(part.F_SRATE) && - ["", "0"].includes(part.F_KWAN) && - ["", "0"].includes(part.F_CLASS), + (part) => Boolean(part.F_SEQ) && num(part.F_QQTY) > 0 && !["%", "%"].includes(part.F_UNIT), ); + const ordinary = identifiable && parts.every((part) => ordinaryMachinePart(part, num)); const matches = (weighted) => [ ["F_MPRICE", gc], @@ -384,8 +378,8 @@ function read(bytes, project) { ) - value, ) < 1e-6, ); - const direct = ordinary && matches(false); - const weighted = ordinary && !direct && matches(true); + const direct = identifiable && matches(false); + const weighted = identifiable && !direct && matches(true); const basic = (direct && parts.every((part) => num(part.F_QQTY) === 1)) || weighted; project.tables.MachMainDat.push({ PkID: project.tables.MachMainDat.length + 1, @@ -400,6 +394,7 @@ function read(bytes, project) { if (basic) { machineDetailed = true; machineWeighted ||= weighted; + machineSpecial ||= !ordinary; for (const part of parts) { const factor = weighted ? num(part.F_QQTY) : 1, GC = num(part.F_MPRICE) * factor, @@ -437,10 +432,12 @@ function read(bytes, project) { } if (machineDetailed) unitWarnings.push( - "일반 기계 상세는 저장 금액별로 표시하며, 산식과 원천 단가 연결은 원본 JDL에 보존됩니다.", + "기계 상세는 저장 금액별로 표시하며, 산식과 원천 단가 연결은 원본 JDL에 보존됩니다.", ); if (machineWeighted) unitWarnings.push("일부 기계 상세는 수량×단가가 저장 합계와 일치해 계산 금액으로 표시합니다."); + if (machineSpecial) + unitWarnings.push("특수 기계 상세는 저장 금액으로 고정하며 특수 산식은 원문에 보존합니다."); if (machineFallback) unitWarnings.push( "일부 기계경비는 저장 금액으로 가져왔습니다. 상세 산식은 원본 JDL에 보존됩니다.", @@ -500,6 +497,7 @@ function read(bytes, project) { nc = num(title.F_LPRICE), jc = num(title.F_EPRICE); const parts = partsByUnit.get(title.F_CODE) || []; + const preserveSpecial = canPreserveSpecial(parts, title, num); let incomplete = !parts.length; const included = { GC: 0, NC: 0, JC: 0 }; project.tables.HeadMainDat.push({ @@ -528,6 +526,15 @@ function read(bytes, project) { ["", "0"].includes(part.F_KWAN) && ["", "0"].includes(part.F_CLASS); if (!basic) { + if (preserveSpecial) { + const row = fixedSpecialPart(part, source, ++unitPk, num); + project.tables.HeadMainDat.push(row); + for (const key of ["GC", "NC", "JC"]) included[key] += row[key]; + unitWarnings.push( + `일위 ${title.F_CODE}/${part.F_SEQ}: 특수 구성은 저장 금액으로 고정하며 산식은 원문에 보존합니다.`, + ); + continue; + } incomplete = true; unitWarnings.push(`일위 ${title.F_CODE}/${part.F_SEQ}: 특수 구성은 원문에만 보존`); continue; @@ -594,6 +601,11 @@ function read(bytes, project) { if (rows["unitpart.txt"].some((r) => !unitCodes.has(r.F_CODE))) unitWarnings.push("부모 일위대가가 없는 상세 행은 원문에만 보존"); const headings = new Set(); + const partsByGroup = new Map(); + for (const part of rows["mainpart.txt"]) { + if (!partsByGroup.has(part.F_CODE)) partsByGroup.set(part.F_CODE, []); + partsByGroup.get(part.F_CODE).push(part); + } let pk = 0; for (const group of rows["gongjong.txt"]) { if (headings.has(group.F_CODE)) throw Error("JDL 공종 코드 중복"); @@ -609,7 +621,7 @@ function read(bytes, project) { SC: 0, TC: group.F_MPRICE + group.F_LPRICE + group.F_EPRICE, }); - for (const r of rows["mainpart.txt"].filter((r) => r.F_CODE === group.F_CODE)) { + for (const r of partsByGroup.get(group.F_CODE) || []) { const source = byCode.get(r.F_CODE1); if (!source) throw Error("JDL 연결 자원 없음: " + r.F_CODE1); const row = { diff --git a/tools/est-test/native-text.js b/tools/est-test/native-text.js index 651c09cc7..3d4e92712 100644 --- a/tools/est-test/native-text.js +++ b/tools/est-test/native-text.js @@ -272,7 +272,8 @@ const EstText = (() => { const assignedResults = new Map(); const assignedNames = new Map(); const declaredNames = new Set(); - let componentDeclaration; + let componentDeclaration, + priceCaption = false; try { if (selectionError) throw Error(selectionError); if (!matched) @@ -344,6 +345,17 @@ const EstText = (() => { } else if (matched && sc < 0) { let body = stripLabels(text.slice(0, text.indexOf("\x12"))).replace(/=\s*$/, ""); if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); + const caption = original.match( + /\x03([-\d.,]+)\x03C(?=\s*[×*])[\s\S]*?=\s*([-\d.,]+)(?=\s*[×*])/, + ); + if (caption && variables.C === undefined && numeric(caption[1]) === numeric(caption[2])) { + priceCaption = true; + body = body.replace(/(\x03[-\d.,]+\x03)C(?=\s*[×*])/, "$1"); + text = text.replace( + /(\x03([-\d.,]+)\x03C(?=\s*[×*])[\s\S]*?=\s*)([-\d.,]+)(?=\s*[×*])/, + (_, prefix, price) => prefix + price, + ); + } const parts = body.split("="); if (parts.length > 1 && /^[A-Za-z가-힣][A-Za-z0-9가-힣]*$/.test(parts[0].trim())) { componentDeclaration = parts[0].trim(); @@ -505,6 +517,7 @@ const EstText = (() => { if (currentSource && text.includes("\x03")) { let body = stripLabels(text.slice(0, text.indexOf("\x12"))).split("=")[0]; if (body.includes(":")) body = body.slice(body.indexOf(":") + 1); + if (priceCaption) body = body.replace(/(\x03[-\d.,]+\x03)C(?=\s*[×*])/, "$1"); try { row.ResourceFactor = EstExpression.evaluate( expression(body.replace(/\x03[-\d.,]+\x03/g, "1")), From 3439e1ec6f09e0f39ff8e810b7cb402cf9165c89 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:00:59 +0900 Subject: [PATCH 23/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC737):=20?= =?UTF-8?q?=EB=82=A8=EC=9D=80=20=EA=B2=B0=EC=A0=95=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=AC=BC=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index ecf8aa8b7..d386f86ad 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -714,3 +714,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 736 (11-09 · 사용자 · 바이브레이터 규격이 하나면 엔진식) 바이브 레이터에 규격은 하나임? 하나라면 엔진식이 맞음. + +## 요구 737 (11-09 · 사용자 · 남은 결정 사항 물음) + +남은 결정사항은 뭐가 있어? From 1a0eaf1deead1998319ade3b7d01728154f34ccd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:10:18 +0900 Subject: [PATCH 24/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC738):=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8=20=ED=8C=A8=EB=84=90=20=ED=91=9C=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=20=C2=B7=20=EC=A0=9C=EB=AA=A9=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=C2=B7=20=EC=A1=B0=ED=95=A9=20=EA=B8=B0=EA=B3=84=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=C2=B7=20=EC=A0=95=ED=95=A9=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 11 +++++++++++ .../raw/요구_1104/요구738_원문패널_제목중복.png | Bin 0 -> 33566 bytes 2 files changed, 11 insertions(+) create mode 100644 docs/raw/요구_1104/요구738_원문패널_제목중복.png diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index d386f86ad..b6c6a948b 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -718,3 +718,14 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 737 (11-09 · 사용자 · 남은 결정 사항 물음) 남은 결정사항은 뭐가 있어? + +## 요구 738 (11-09 · 사용자 · 별칭 넷 = 단가산출 만들다 나오면 승인(할 일 없음) · 원문 패널 표 글자 16 · 제목 통합(그림 `요구738_원문패널_제목중복.png`) · 조합 기계 = 개별 그룹으로 · 공식 연결 분리 가능한지 조사 · 반영 뒤 단가산출 불러오기 정합성 확인) + +지금 정할 수 있는 것. + 1. 지금 하겠다는게 아니라 단가산출을 만들면서 해당내용이 나오면 내가 승인하겠다는거야. 그러니 이제 너가 할 일은 없음. +2 .표의 글자도 16으로 변경 +2-1. [Image #1]중복표기가 많음 제목을 통합해줘. 건설품셈 공통 7-1-1 메쌓기('12, '19, '26년 보완) 형태면 될것같음. +위 내용도 같이 반영, 반영완료 +3. 조합기계는 우리 단가산출로 가져올때 개별 그룹으로 하나씩 가져오는게 좋을것같음. +4. 공식을 연결해야 단가가 나올것같은데 분리할 수 없을것같은데 조사 +5. 위내용까지 반영 후 일단 단가산출에 대한 불러오기 정합성 확인 diff --git a/docs/raw/요구_1104/요구738_원문패널_제목중복.png b/docs/raw/요구_1104/요구738_원문패널_제목중복.png new file mode 100644 index 0000000000000000000000000000000000000000..83ab1580ddb0e464de87d9f4e6161829bcd19a69 GIT binary patch literal 33566 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

VIs~)BhePS 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 Fe*tH@N(BG_ literal 0 HcmV?d00001 From 23b12ef94e06860689ec953eaf8c3de476f86613 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:12:48 +0900 Subject: [PATCH 25/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC739):=20?= =?UTF-8?q?=EA=B8=B0=EA=B3=84=20=EC=88=98=EB=9F=89=20=C2=B7=20=EA=B3=B5?= =?UTF-8?q?=EC=8B=9D=20=E2=80=94=20=EC=9D=BC=EB=8B=A8=20=ED=98=84=EC=9E=AC?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index b6c6a948b..5efa902f7 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -729,3 +729,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 3. 조합기계는 우리 단가산출로 가져올때 개별 그룹으로 하나씩 가져오는게 좋을것같음. 4. 공식을 연결해야 단가가 나올것같은데 분리할 수 없을것같은데 조사 5. 위내용까지 반영 후 일단 단가산출에 대한 불러오기 정합성 확인 + +## 요구 739 (11-09 · 사용자 · 4 조사 결과 — 일단 현재 유지가 좋겠음 · 지금은 원문 기준으로 만든 품셈이 맞는지) + +4 조가결과에서 일단 현재를 유지하는게 좋을것같은데 일단 현재는 원문을 기준으로 만들어진 품셈이잖아. 맞지? From 603c9654ef0b7be9073953472234ebca9f3fbf58 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:17:45 +0900 Subject: [PATCH 26/29] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC740):=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EC=9C=A0=EC=A7=80=20=EB=B2=94=EC=9C=84=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- docs/raw/요구_1104/사용자요구_1104.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1104/사용자요구_1104.md b/docs/raw/요구_1104/사용자요구_1104.md index 5efa902f7..2ae5e8579 100644 --- a/docs/raw/요구_1104/사용자요구_1104.md +++ b/docs/raw/요구_1104/사용자요구_1104.md @@ -733,3 +733,7 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의 ## 요구 739 (11-09 · 사용자 · 4 조사 결과 — 일단 현재 유지가 좋겠음 · 지금은 원문 기준으로 만든 품셈이 맞는지) 4 조가결과에서 일단 현재를 유지하는게 좋을것같은데 일단 현재는 원문을 기준으로 만들어진 품셈이잖아. 맞지? + +## 요구 740 (11-09 · 사용자 · 현재 유지 범위 확인) + +현재 유지는 지금의 범위가 맞음 From ad9a50c5842d98d940d281d29671953ca60680d0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:43:15 +0900 Subject: [PATCH 27/29] =?UTF-8?q?feat(813-0b):=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=E2=80=94=20=EC=A0=9C=EB=AA=A9=20=ED=95=9C?= =?UTF-8?q?=20=EC=A4=84=EB=A1=9C=20=ED=86=B5=ED=95=A9=20=C2=B7=20=ED=91=9C?= =?UTF-8?q?=20=EA=B8=80=EC=9E=90=2016?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제목 = 책 부문 + 절 번호 + 절 제목(개정 꼬리 포함) 한 줄 · 본문 안 원문 제목 줄은 화면에서만 숨김(절 여럿이면 절마다 번호 이름 줄) - 기초 찾기로 고른 뒤 남던 작은 글 줄 지움 - 표 머리 · 칸 글자 = 본문과 같은 --text-body(16) · [주] 는 14 그대로 Co-Authored-By: Claude Sonnet 5.5 --- .../M01_MasterData_UI_LogicLab_Side.css | 9 ++++++--- .../M01_MasterData_UI_LogicLab_Source.ts | 20 ++++++++++++++++++- .../M01_MasterData_UI_LogicLab_SourceFind.ts | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css index 977a12f4d..db0b69c32 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css @@ -18,9 +18,7 @@ .m01lab-src__title { flex: 1; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; /* 제목 한 줄이 길면 접음 */ } .m01lab-src__body { @@ -61,6 +59,11 @@ white-space: pre; } +/* 표 머리 · 칸 = 본문과 같은 16 — 좁으면 둘레 칸(m01-logic__scroll)이 가로로 굴림 */ +.m01lab-src .m01lab-src__table :is(td, th) { + font-size: var(--text-body); +} + /* 출처가 가리킨 항 · 주 — 옅은 바탕 + 왼쪽 막대 */ .m01lab-src__hit { background: color-mix(in srgb, var(--color-accent) 14%, transparent); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts index ede0ecc99..8b5153334 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts @@ -38,6 +38,9 @@ interface Found { item?: string; } +/** 절 제목 블록의 글만 「번호 이름」 꼴로 */ +const headFixed = (b: Block): Block => (b.h ? { ...b, text: headText(b.text ?? "") } : b); + function blockNode(b: Block): HTMLElement { const node = rawNode(b); if (b.hit) node.classList.add("m01lab-src__hit"); @@ -65,6 +68,15 @@ function rawNode(b: Block): HTMLElement { return el("p", { className: "m01lab-src__p", text: b.p ?? "" }); } +/** 출처 글에서 첫 번호 앞까지 = 책 · 부문(예 「건설품셈 공통」) — 제목 한 줄의 앞 */ +const prefixOf = (src: string): string => { + const at = src.search(/\d+(?:-\d+)+/); + return (at < 0 ? src : src.slice(0, at)).trim(); +}; + +/** 절 제목 줄 「9-7-1. 이름」 → 「9-7-1 이름」 */ +const headText = (t: string): string => t.replace(/^(\d+(?:-\d+)*)\.\s*/, "$1 "); + /** 「[주]」 줄부터 다음 제목 앞까지 글 줄 = 한 단 작은 글자(`m01lab-src__note`) */ function noteMarked(blocks: Block[]): HTMLElement[] { let inNote = false; @@ -133,6 +145,12 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView { .then(({ found }) => { if (shown !== src) return; if (!found.length) return none(); + // 제목 한 줄 = 「책 부문 + 절 번호 · 이름(개정 꼬리)」 — 절 하나면 원문 제목 줄을 숨김 · 여럿이면 절마다 번호 · 이름 줄 둠 + const lone = found.length === 1 && found[0].blocks[0]?.h ? found[0] : null; + if (found.length > 1 || lone) { + const pre = prefixOf(src); + title.textContent = lone ? `${pre} ${headText(lone.blocks[0].text ?? "")}`.trim() : pre; + } body.replaceChildren( ...found.flatMap((f) => { // 파일 경로는 안 보임 — 가까운 절 · 항 안내만 @@ -146,7 +164,7 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView { ...(note ? [el("p", { className: "ui-text ui-text--muted m01lab-src__file", text: note })] : []), - ...noteMarked(f.blocks), + ...noteMarked(f === lone ? f.blocks.slice(1) : f.blocks.map(headFixed)), ]; }), ); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind.ts index ea60d88f9..62fde663a 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind.ts @@ -77,7 +77,7 @@ export function createSourceFind(opts: { unit: x.결과단위 ?? "", onPick: () => { find.setRows([]); - find.hint.textContent = `${x.원문번호 || x.키} ${x.이름}`; + find.hint.textContent = ""; // 고른 기초 이름은 원문 제목 한 줄이 보임 opts.onPick(x); }, })), From 58a3e039b306db0ca55ccd65fb6730f23b03037a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:49:52 +0900 Subject: [PATCH 28/29] =?UTF-8?q?feat(813-2):=20=EB=B3=B4=EC=A1=B0=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EB=B6=80=ED=92=88=20=ED=91=9C=EC=A7=80=20?= =?UTF-8?q?73=20=C2=B7=20=EC=9E=91=EC=97=85=EB=9F=89=EB=8B=B9=20=EA=B8=B0?= =?UTF-8?q?=EA=B3=84=EA=B2=BD=EB=B9=84=206=20=EC=A7=80=EC=9B=80=20=C2=B7?= =?UTF-8?q?=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=BA=8C=20=C2=B7=20=EA=B8=B0=EA=B3=84=20?= =?UTF-8?q?=E2=86=92=20=EB=B6=80=ED=92=88=20=EA=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 표지 = 수식 문서 머리 · 옛 로직 줄 칸 「부품」 — 기계경비식 9 · 작업량 틀 60(기종 = 기계표 상세구분 · 규격 열쇠) · 계수식 4 · 변환기가 옮김 · 틀 문서 9 장 - 지움 = 작업량당 기계경비 6(GC001145 · 1148 · 1151 · 1156 · 1159 · 1166 · 부르는 곳 0 · 요구 734-4 · 735-4) — 수식 문서 · 옛 로직 · 키대장 폐기 칸 · 빌더 · 변수표 - 이름 꼬리 4 = 원문 제목(지게운반 · 인력운반(기계설비) · 벽돌운반 · 수중펌프 설치 및 해체) · 키 폐기 0 - 서버 = `M01_MasterData_Parts` — M01 `/logics` · M02 `/logic-lib/logics` 목록에서 부품 뺌(1,440) · 키로 열기 · 부르기 그대로 · `/m01/parts`(기종 계산식 · 틀 · 계수식) · `/m02/logic-lib/machine-parts` · 연결 찾기 과녁에서 부품 뺌(1,444 → 1,435 · 판정 코드 그대로) - 금액 전후 = 문서 1,513 · 셈 9,626(기본 + 옵션 표본) 모두 같음 · 791_1 지문 남은 1,513 같음 → 지문 다시 뜸 · 802_2 변환 대조 1,513 · 목록 xlsx 다시 씀(요약 수) - 낡은 시험 = 1,519 · 1,518 기대 → 1,513 · GC001148 견본 → 같은 꼴 GF000089 토사깍기(기계)(블록 Q 39.293 · 합계 2,190) · 지운 6 을 셈하던 줄 걷음 · 목록 수 1,440 · 묶음 328 · 부르는 수 507 - 새 지킴 시험 test_813_2_parts Co-Authored-By: Claude Opus 5.5 --- M01_MasterData/M01_MasterData_Parts.py | 89 +++ M01_MasterData/M01_MasterData_Router.py | 10 +- M02_MasterTemplete/M02_Doc_MatchSk.py | 3 +- M02_MasterTemplete/M02_Logic_Lib_Read.py | 7 + M02_MasterTemplete/M02_Machine_Hourly.py | 11 +- .../M02_MasterTemplete_Router_Combos.py | 3 +- docs/raw/요구_1104/_목록_802-2_변환대조.xlsx | Bin 13010 -> 13009 bytes resources/master_data/_수식문서_틀.md | 11 + resources/master_data/_키대장.json | 8 +- .../scripts/_건설기계_로직_준설.py | 1 + .../build_로직_건설품셈_01장_적용기준.py | 6 +- .../build_로직_건설품셈_08장_건설기계.py | 7 +- .../master_data/scripts/master_doc_convert.py | 1 + .../master_data/scripts/변수표_796_3.json | 32 -- .../로직_건설품셈_01장_적용기준.json | 8 +- .../로직_건설품셈_08장_건설기계.json | 541 ++---------------- .../로직_건설품셈_13장_플랜트설비공사.json | 1 + .../로직_산림품셈_03장_작업장_관리.json | 1 + .../로직_산림품셈_05장_식재_및_녹화.json | 1 + .../master_data/로직_산림품셈_09장_토공.json | 14 + .../로직_산림품셈_10장_자재․장비_운반.json | 3 + .../master_data/로직_산림품셈_11장_가설.json | 1 + .../로직_산림품셈_13장_구조물.json | 1 + .../수식문서/수식_건설품셈_01장_적용기준.json | 12 +- .../수식문서/수식_건설품셈_08장_건설기계.json | 403 +++++++------ .../수식_건설품셈_13장_플랜트설비공사.json | 3 + .../수식_산림품셈_03장_작업장_관리.json | 5 + .../수식_산림품셈_05장_식재_및_녹화.json | 5 + .../수식문서/수식_산림품셈_09장_토공.json | 65 +++ .../수식_산림품셈_10장_자재․장비_운반.json | 11 + .../수식문서/수식_산림품셈_11장_가설.json | 5 + .../수식문서/수식_산림품셈_13장_구조물.json | 5 + .../fixtures/m01_791_1_logic_fingerprint.json | 8 +- resources/tester/test_713_3_list.py | 12 +- resources/tester/test_782_3_capacity_82.py | 21 +- .../tester/test_791_1_engine_semantics.py | 2 +- resources/tester/test_796_1_formula_model.py | 20 +- .../tester/test_796_2c_m02_equation_full.py | 11 +- resources/tester/test_796_2e_unlink_basis.py | 4 +- resources/tester/test_799_1_subtotals.py | 12 +- resources/tester/test_800_1_var_symbols.py | 2 +- .../tester/test_800_2_equation_layout.py | 2 +- resources/tester/test_801_3_groups.py | 6 +- resources/tester/test_802_2_convert.py | 4 +- resources/tester/test_802_4a_m02_doc.py | 2 +- resources/tester/test_802_4b2_m02_screen.py | 6 +- resources/tester/test_804_2_line_stack.py | 4 +- resources/tester/test_804_7_line_stack_fix.py | 2 +- resources/tester/test_812_3_native_layout.py | 2 +- resources/tester/test_813_2_parts.py | 111 ++++ 50 files changed, 677 insertions(+), 828 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_Parts.py create mode 100644 resources/tester/test_813_2_parts.py diff --git a/M01_MasterData/M01_MasterData_Parts.py b/M01_MasterData/M01_MasterData_Parts.py new file mode 100644 index 000000000..d3267715c --- /dev/null +++ b/M01_MasterData/M01_MasterData_Parts.py @@ -0,0 +1,89 @@ +"""부품 문서(PLAN 813-2 · 요구 722-3 · 724~735) — 수식 문서 머리 칸 `부품` 읽기. + +갈래 = 기계경비식(기계 시간당 경비 셈 방식) · 작업량 틀(`기종` = 기계표 상세구분 목록 · +`규격` = 고른 기계 규격 수를 받는 변수 열쇠) · 계수식(기계 무관 · 돈 아닌 식). +틀 = `_수식문서_틀.md` 9 장. +- 단가산출 기초 목록 · 찾아 넣기 「다른 단가산출」 목록에서 뺌(`strip`) — 키로 열기 · 셈 · 부르기는 그대로 +- 기종 → 그 기종 기계가 쓰는 기계경비식 · 작업량 틀(`of_kind` · M01 「기계」 화면) +- 기계 원문번호 → 그 기종 작업량 틀 문서(`of_machine` · 줄 쌓기 기계 넣기) +쓰기 없음 · 로그인 사용자 모두(마스터 읽기). +""" + +from __future__ import annotations + +from typing import Any + +from M01_MasterData import M01_MasterData_Doc as doc_store +from M01_MasterData import M01_MasterData_Store as store + +KINDS = ("기계경비식", "작업량 틀", "계수식") + + +def parts() -> dict[str, dict]: + """{문서 키: 부품 표지}.""" + return {k: d["부품"] for k, d in doc_store.docs().items() if isinstance(d.get("부품"), dict)} + + +def strip(rows: list[dict]) -> list[dict]: + """목록에서 부품 문서를 뺌.""" + have = parts() + return [r for r in rows if str(r.get("키")) not in have] + + +def _brief(key: str, d: dict) -> dict[str, Any]: + tag = d.get("부품") or {} + return { + "키": key, + "이름": d.get("이름") or "", + "결과단위": d.get("결과단위") or "", + "갈래": tag.get("갈래") or "", + "규격": tag.get("규격") or "", + } + + +def _machines() -> list[dict]: + files = store.loaded()[0] + return list((files.get("기계.json") or {}).get("줄") or []) + + +def _templates(kind: str) -> list[tuple[str, dict]]: + return [ + (k, d) + for k, d in doc_store.docs().items() + if (d.get("부품") or {}).get("갈래") == "작업량 틀" and kind in d["부품"].get("기종", []) + ] + + +def of_kind(group: str, detail: str = "") -> dict[str, list]: + """M01 화면 — 계수식 = 그 넷 · 기계 = 상세구분 `detail` 기계가 쓰는 기계경비식(기계 수) + + 작업량 틀.""" + docs = doc_store.docs() + if group == "계수식": + return { + "계수식": [ + _brief(k, d) for k, d in docs.items() if parts().get(k, {}).get("갈래") == "계수식" + ] + } + from M02_MasterTemplete import M02_Machine_Hourly as hourly # M02 가 M01 을 부름 — 늦게 + + used: dict[str, int] = {} + for m in _machines(): + if m.get("상세구분") != detail: + continue + got = hourly.logic_of(str(m.get("원문번호") or "")) + if got: + used[str(got["키"])] = used.get(str(got["키"]), 0) + 1 + rule = [{**_brief(k, docs[k]), "기계수": n} for k, n in used.items() if k in docs] + return {"기계경비식": rule, "작업량 틀": [_brief(k, d) for k, d in _templates(detail)]} + + +def of_machine(no: str) -> dict[str, Any]: + """줄 쌓기 기계 넣기 — 기계 원문번호 → {기계 {이름 · 규격 · 상세구분}, 틀 [{… 문서}]}.""" + row = next((m for m in _machines() if str(m.get("원문번호")) == str(no)), None) + if not row: + raise store.StoreError(404, f"없는 기계 「{no}」") + kind = str(row.get("상세구분") or "") + return { + "기계": {"이름": row.get("이름"), "규격": row.get("규격") or "", "상세구분": kind}, + "틀": [{**_brief(k, d), "문서": d} for k, d in _templates(kind)], + } diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index e4f9f3f3d..c18bd242a 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -14,6 +14,7 @@ from pydantic import BaseModel from M01_MasterData import M01_MasterData_Doc as doc_screen from M01_MasterData import M01_MasterData_Equation as equation +from M01_MasterData import M01_MasterData_Parts as parts from M01_MasterData import M01_MasterData_Sheet as line_sheet from M01_MasterData import M01_MasterData_Store as store from M01_MasterData import M01_MasterData_Store_Combo as combo_store @@ -205,7 +206,14 @@ def get_logics( sub: str = "", detail: str = "", q: str = "", blocked: int | None = None, owner: str = "" ) -> dict: """줄마다 덧칸(묶음 · 작업량 · 부르는/불리는 수 · 관련 · 713-3) — 읽기만.""" - return {"logics": store_list.attach(_call(store.logics, sub, detail, q, blocked, owner))} + rows = parts.strip(_call(store.logics, sub, detail, q, blocked, owner)) # 부품 문서 뺌(813-2) + return {"logics": store_list.attach(rows)} + + +@router.get("/parts") +def get_parts(group: str, detail: str = "") -> dict: + """부품 문서(813-2) — 계수식 · 기계 상세구분의 기계경비식 · 작업량 틀.""" + return _call(parts.of_kind, group, detail) @router.get("/logic") diff --git a/M02_MasterTemplete/M02_Doc_MatchSk.py b/M02_MasterTemplete/M02_Doc_MatchSk.py index 9cad03cce..72e7b9067 100644 --- a/M02_MasterTemplete/M02_Doc_MatchSk.py +++ b/M02_MasterTemplete/M02_Doc_MatchSk.py @@ -33,7 +33,8 @@ def index() -> dm.Index: """M01 수식 문서 색인 — 폴더가 바뀌면(M01 관리자 고침) 다시.""" docs = dc.folder() if _INDEX["문서"] is not docs: - _INDEX["문서"], _INDEX["색인"] = docs, dm.Index(docs) + aims = {k: d for k, d in docs.items() if not d.get("부품")} # 부품 문서 = 과녁 아님(813-2) + _INDEX["문서"], _INDEX["색인"] = docs, dm.Index(aims) return _INDEX["색인"] diff --git a/M02_MasterTemplete/M02_Logic_Lib_Read.py b/M02_MasterTemplete/M02_Logic_Lib_Read.py index 73e4471d1..40b0e4e8e 100644 --- a/M02_MasterTemplete/M02_Logic_Lib_Read.py +++ b/M02_MasterTemplete/M02_Logic_Lib_Read.py @@ -10,6 +10,7 @@ from __future__ import annotations from fastapi import APIRouter, HTTPException +from M01_MasterData import M01_MasterData_Parts as parts from M01_MasterData import M01_MasterData_Router as m01 from M02_MasterTemplete import M02_Machine_Hourly as machine @@ -72,3 +73,9 @@ def post_text(body: m01.CalcBody) -> dict: def get_machine_hourly(key: str) -> dict: """기계(EQ) 한 시간 값 노 · 재 · 경(736-3 · [항목 추가] 기계 고정 줄 단가).""" return machine.hourly(key) + + +@router.get("/machine-parts") +def get_machine_parts(no: str) -> dict: + """줄 쌓기 기계 넣기(813-2) — 기계 원문번호 → 그 기종 작업량 틀(문서째).""" + return m01._call(parts.of_machine, no) diff --git a/M02_MasterTemplete/M02_Machine_Hourly.py b/M02_MasterTemplete/M02_Machine_Hourly.py index dbf5ef6a0..50b2c94da 100644 --- a/M02_MasterTemplete/M02_Machine_Hourly.py +++ b/M02_MasterTemplete/M02_Machine_Hourly.py @@ -41,6 +41,12 @@ def _rank(row: dict) -> tuple: return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키"))) +def logic_of(code: str) -> dict | None: + """기계 원문번호 → 그 기계 시간당 경비 로직(짝 차례 첫째) · 없으면 None(813-2 M01 화면도 씀).""" + fit = [r for r in _family() if code in [str(c) for c in _codes(r)]] + return min(fit, key=_rank) if fit else None + + def hourly(key: str) -> dict[str, Any]: """`{ok, 이름, 규격, 로직, 원문번호, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`.""" if not key.startswith("EQ"): @@ -50,10 +56,9 @@ def hourly(key: str) -> dict[str, Any]: except cm.mf.FormulaError as e: return {"ok": False, "reason": str(e)} code = str(row.get("원문번호") or "") - fit = [r for r in _family() if code in [str(c) for c in _codes(r)]] - if not fit: + logic = logic_of(code) + if not logic: return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"} - logic = min(fit, key=_rank) args = "".join( f", {i['이름']}='{(i.get('고르기') or [''])[0]}'" for i in logic.get("입력") or [] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py index 40882864f..d09e09c5d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py @@ -19,6 +19,7 @@ from pydantic import BaseModel from common_util.common_util_auth import verify_session from common_util.common_util_http_cache import cached_file_response +from M01_MasterData import M01_MasterData_Parts as parts from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Make as make from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs @@ -40,7 +41,7 @@ def _read(fn, *args): @router.get("/logic-lib/logics", dependencies=[Depends(verify_session)]) def get_logics() -> dict: - return {"logics": _read(m01.logics, "", "", "", None)} + return {"logics": parts.strip(_read(m01.logics, "", "", "", None))} # 부품 문서 뺌(813-2) @router.get("/logic-lib/subs", dependencies=[Depends(verify_session)]) diff --git a/docs/raw/요구_1104/_목록_802-2_변환대조.xlsx b/docs/raw/요구_1104/_목록_802-2_변환대조.xlsx index ab3b94924dfa8efe810eecf696c525003472afa9..be806a05eb9a0e01eafc6338bdc0b709a11e073a 100644 GIT binary patch delta 877 zcmcbVdNGwJz?+#xgn@y9gMoA5M4ol^oC`B!wdbeuz53-<6_;yE$8FM=I%=m*6_%0oXNu0C{=W&h$)a`$3~xvOYNnXrKtT*NniDP z*P>{@I~yi8T)+FRGRkgyZqi+))Kz90hLh6duT}(FeD~$O-}N>1TX^G{q!|i7Wilqm zPM-es@8N%j!J(0BixfY7%F}PrxgNCUzT5SF*?+SyR3`I(lRviF;p&Ii_phiU2hiqg zjP`6yoC`OLb7(MgfP(5;{fzaS-MLd38UIaQ!{b!%wl}&lPpf3J)LQ%OxdDr;MPE#F z+P>=Iufx|k*K*}VC*HPtbAIvD-Id=8+ddurUh{8t&zj3&6Ro@MT#Q|Mc~SI~zdxEk zJ^r4WF!#zTbFB-pt?ypVva>&9U$3kDO62<;)zdNle8Jb>Uw!x~r|Xly`pu{JFL5pk zo))LR$8g`1nmRA*8yX$KB2DU(W<=+HT)pXbW%=uZSt6hJ_**UyO#Jm|E`yqRM`?f1 zIvLCStCt`DV@C1Z3SMDJkmvTQ^)iBJAB}1zAU*k{)^iYdllDfC@Z?n8A0TeLUL=^i zOW%eQo@gfj*Ef>~@n*)%Q5IlhV3?!Mz#t4vLNL(4cye-xffC4^3IiD?E{@58+@h0L z8we`ELer3mfuRkkQv#+1NN;O2;RY&{(hl%uWD;S3rPs{ZowpC4xervSrpAb-VfJK2 zLv@ffUWPJE8`Qwo)EWwcL?#%@FxpRE3ls-(P8-Tt?a^Xjh%ZXbDc08mk?5A}-hD{3 z4VV?a0bRzA^7=7k|!p?dt;%iJ7>kghsT86X8+5XF}YPHXm>#E zeeqh;;>T_>aq{x>omj4EEti3Jq|F6|BA&1cB ztBm$+OfhMj#W*yWg+O7oU(NrJArk{b8yf?I1jA%SL($0}3|Kd3aXT^NXME+WfX~{`sid?tiG; zwanEgWL@9ASa&J&qVB1>KaM{?{x;g+mvvQI^u;=-@>1XV^3Ue)pC(w^RsC*K>N@rI zm9M|QDyTGb{keSdn@{;KSuSdNuNS^25np6yuWFqmazvBsps?qewcCD#-h8`fcd3PM zSLMCsb1ttisQu{2Fj@M@E_coFzB$`py)69Ch~nMlyuy+o@9t6SWdzaQ8r4iddh!Xa z=OFGz?TsMe$tk)&K-@UJNHBM&z6~cl9ZmkHZzd1o&5W6&EC39pIqD1y!oXAo0}YI) zCI=fRDS*_-IQ?<{0t`)0c2H;{z_vzi4sh76G7tnSL=ARig{IulNDu_uy7TtoGxvc; zsi`rdnK);%f}uLd^`3?@OdHj}x@rssK_dNzGK>zB*8s(VoKuD}R(rJ=7~+dkbBgu# zKxBY7Ba;XNEZbzp?%sV!vkjP8z5$)ik8lh_1LLyE3Py4uQyh$B7$Yag8fk(!9Y9X- N list[dict]: ) row.pop("결과") row.pop("시험입력") + row["이름"] = "수중펌프 설치 및 해체" # 813-2 원문 제목 · 윗절 row["호표"] = [ { "종류": "인력", diff --git a/resources/master_data/scripts/build_로직_건설품셈_01장_적용기준.py b/resources/master_data/scripts/build_로직_건설품셈_01장_적용기준.py index 261b44c8c..6b433fc1d 100644 --- a/resources/master_data/scripts/build_로직_건설품셈_01장_적용기준.py +++ b/resources/master_data/scripts/build_로직_건설품셈_01장_적용기준.py @@ -375,7 +375,7 @@ def logics(key: dict) -> list[dict]: "키": None, "열쇠": "공통 1-2-7 인력운반(지게)", **HEAD, - "이름": "인력운반(지게) — 건설 공통 1-2-7", + "이름": "지게운반", "결과단위": "원/자재 단위", "출처": f"{SRC} 2 · 3", "소유": "공용", @@ -461,7 +461,7 @@ def logics(key: dict) -> list[dict]: "키": None, "열쇠": "공통 1-2-7 인력운반(기계설비)", **HEAD, - "이름": "인력운반(기계설비) — 건설 공통 1-2-7", + "이름": "인력운반(기계설비)", "결과단위": "원/자재 단위", "출처": f"{SRC} 5", "소유": "공용", @@ -534,7 +534,7 @@ def logics(key: dict) -> list[dict]: "키": None, "열쇠": "공통 1-2-7 벽돌운반", **HEAD, - "이름": "벽돌운반 — 건설 공통 1-2-7", + "이름": "벽돌운반", "결과단위": "원/1,000매", "출처": f"{SRC} 4", "소유": "공용", diff --git a/resources/master_data/scripts/build_로직_건설품셈_08장_건설기계.py b/resources/master_data/scripts/build_로직_건설품셈_08장_건설기계.py index 003bb6ba3..1dd6d49c6 100644 --- a/resources/master_data/scripts/build_로직_건설품셈_08장_건설기계.py +++ b/resources/master_data/scripts/build_로직_건설품셈_08장_건설기계.py @@ -164,12 +164,7 @@ def main() -> None: C = chooser(made, old_coef) rows = L1.part1(C) + L2.part2(C) + L3.part3(C) - q_rows = {r["열쇠"]: r for r in rows} - hourly = {r["키"]: r for r in _load(LOGIC)["줄"]} - machines: dict[str, list] = {} - for m in _load("기계.json")["줄"]: - machines.setdefault(m.get("상세구분"), []).append(m["원문번호"]) - rows += LC.costs(q_rows, hourly, machines) + [LC.breaker(hourly)] + # 작업량당 기계경비(LC.costs · LC.breaker) = 813-2 지움 — 단가산출에 기계 하나 넣어 만듦 rows.sort(key=lambda r: _sec(r["원문번호"])) for r in rows: r["키"] = mk.issue(book, "GC", r["열쇠"], LOGIC) diff --git a/resources/master_data/scripts/master_doc_convert.py b/resources/master_data/scripts/master_doc_convert.py index 12b576941..c355d0514 100644 --- a/resources/master_data/scripts/master_doc_convert.py +++ b/resources/master_data/scripts/master_doc_convert.py @@ -443,6 +443,7 @@ def _build(c: _Conv, row: dict, given: dict, lone_steps: dict) -> dict: "이름": row.get("이름") or "", "결과단위": row.get("결과단위") or "", "출처": row.get("출처") or "", + **({"부품": row["부품"]} if row.get("부품") else {}), # 813-2 부품 표지 } money: list[dict] = [] if "결과" in row: diff --git a/resources/master_data/scripts/변수표_796_3.json b/resources/master_data/scripts/변수표_796_3.json index 6855e160a..1e35fce60 100644 --- a/resources/master_data/scripts/변수표_796_3.json +++ b/resources/master_data/scripts/변수표_796_3.json @@ -277,11 +277,6 @@ "t": {"이름": "기어 변속시간", "근거": "건설품셈 공통 8-2-1 「t : 기어 변속시간(0.25분)」", "확신": "원문"}, "cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-1 「cm : 1회 싸이클 시간」", "확신": "원문"} }, -"GC001145": { - "운반거리": {"기호": "L", "근거": "건설품셈 공통 8-2-1 「L : 운반거리(m)」", "확신": "원문"}, - "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-1 「f : 체적환산계수」", "확신": "원문"}, - "작업량": {"이름": "시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-1 「Q : 시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"} -}, "GC001146": { "작업거리": {"기호": "ℓ", "근거": "건설품셈 공통 8-2-2 「ℓ : 1회의 작업거리(m)」", "확신": "원문"}, "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-2 「f : 체적환산계수」", "확신": "원문"}, @@ -296,11 +291,6 @@ "E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-3 「E : 작업효율」", "확신": "원문"}, "cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-3 「cm : 1회 싸이클 시간(초)」", "확신": "원문"} }, -"GC001148": { - "버킷용량": {"기호": "q", "근거": "건설품셈 공통 8-2-3 「q : 버킷용량(㎥)」", "확신": "원문"}, - "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-3 「f : 체적환산계수」", "확신": "원문"}, - "작업량": {"이름": "시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-3 「Q : 시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"} -}, "GC001149": { "작업거리": {"기호": "L", "근거": "건설품셈 공통 8-2-4 「L : 1열 실작업거리(편도m)」", "확신": "원문"}, "전진거리": {"기호": "L", "아래": "1", "근거": "건설품셈 공통 8-2-4 「L_1 : 1열 전진거리(m)」", "확신": "원문"}, @@ -320,12 +310,6 @@ "cm": {"이름": "1회 싸이클시간", "근거": "건설품셈 공통 8-2-5 「cm : 1회 싸이클 시간(초)」", "확신": "원문"}, "E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-5 「E : 작업효율」", "확신": "원문"} }, -"GC001151": { - "버킷용량": {"기호": "q", "근거": "건설품셈 공통 8-2-5 「q : 버킷용량(㎥)」", "확신": "원문"}, - "주행거리": {"기호": "ℓ", "근거": "건설품셈 공통 8-2-5 「ℓ : 편도주행거리(표준을 8m로 한다)」", "확신": "원문"}, - "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-5 「f : 체적환산계수」", "확신": "원문"}, - "작업량": {"이름": "운전시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-5 「Q : 운전시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"} -}, "GC001152": { "적재주행거리": {"기호": "L", "아래": "1", "근거": "건설품셈 공통 8-2-6 「L_1 : 적재시의 주행거리(m)」", "확신": "원문"}, "공차주행거리": {"기호": "L", "아래": "2", "근거": "건설품셈 공통 8-2-6 「L_2 : 공차시의 주행거리(m)」", "확신": "원문"}, @@ -375,16 +359,6 @@ "t5": {"기호": "t", "아래": "5", "근거": "열쇠 꼴 ○○+숫자 = 첨자", "확신": "추정"}, "t6": {"기호": "t", "아래": "6", "근거": "열쇠 꼴 ○○+숫자 = 첨자", "확신": "추정"} }, -"GC001156": { - "적재용량": {"기호": "T", "근거": "건설품셈 공통 8-2-8 「T : 덤프트럭의 적재용량(ton)」", "확신": "원문"}, - "체적변화율": {"기호": "L", "근거": "건설품셈 공통 8-2-8 「L : 체적환산계수에서의 체적변화율」", "확신": "원문"}, - "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-8 「f : 체적환산계수」", "확신": "원문"}, - "적재싸이클": {"기호": "cms", "근거": "건설품셈 공통 8-2-8 「cms : 적재기계의 1회 싸이클시간(초)」", "확신": "원문"}, - "적재버킷": {"기호": "q", "근거": "건설품셈 공통 8-2-8 「q : 흐트러진 상태의 덤프트럭 1회 적재량(㎥)」", "확신": "원문"}, - "버킷계수": {"기호": "k", "근거": "건설품셈 공통 8-2-8 「k : 리퍼 또는 버킷계수」", "확신": "원문"}, - "적재효율": {"기호": "Es", "근거": "건설품셈 공통 8-2-8 「Es : 적재기계의 작업효율」", "확신": "원문"}, - "작업량": {"이름": "1시간당 작업량", "기호": "Q", "근거": "건설품셈 공통 8-2-8 「Q : 1시간당 작업량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"} -}, "GC001157": { "다짐횟수": {"기호": "N", "근거": "건설품셈 공통 8-2-9 「N : 소요다짐횟수」", "확신": "원문"}, "두께": {"기호": "D", "근거": "건설품셈 공통 8-2-9 「D : 펴는 흙의 두께(m)」", "확신": "원문"}, @@ -399,12 +373,6 @@ "V": {"이름": "다짐속도", "근거": "건설품셈 공통 8-2-9 「V : 다짐속도(㎞/hr)」", "확신": "원문"}, "E": {"이름": "작업효율", "근거": "건설품셈 공통 8-2-9 「E : 작업효율」", "확신": "원문"} }, -"GC001159": { - "다짐횟수": {"기호": "N", "근거": "건설품셈 공통 8-2-9 「N : 소요다짐횟수」", "확신": "원문"}, - "두께": {"기호": "D", "근거": "건설품셈 공통 8-2-9 「D : 펴는 흙의 두께(m)」", "확신": "원문"}, - "f": {"이름": "체적환산계수", "근거": "건설품셈 공통 8-2-9 「f : 체적환산계수」", "확신": "원문"}, - "작업량": {"이름": "시간당 다짐토량", "기호": "Q", "근거": "건설품셈 공통 8-2-9 「Q : 시간당 다짐토량(㎥/hr)」 · 식 = 로직 호출(시간당 작업량)", "확신": "원문"} -}, "GC001161": { "W": {"이름": "유효혼합폭", "근거": "건설품셈 공통 8-2-11 「W : 유효혼합폭(m)」", "확신": "원문"}, "V": {"이름": "작업속도", "근거": "건설품셈 공통 8-2-11 「V : 작업속도(1,000m/hr)」", "확신": "원문"}, diff --git a/resources/master_data/로직_건설품셈_01장_적용기준.json b/resources/master_data/로직_건설품셈_01장_적용기준.json index e2ecee7d4..7d8d8966a 100644 --- a/resources/master_data/로직_건설품셈_01장_적용기준.json +++ b/resources/master_data/로직_건설품셈_01장_적용기준.json @@ -10,7 +10,7 @@ "원문번호": "1-2-7", "구분": "건설품셈 공통", "상세구분": "01장 적용기준", - "이름": "인력운반(지게) — 건설 공통 1-2-7", + "이름": "지게운반", "결과단위": "원/자재 단위", "출처": "건설품셈 공통 1-2-7 2 · 3", "개정": [2008, 2010, 2016, 2022, 2023], @@ -131,7 +131,7 @@ "원문번호": "1-2-7", "구분": "건설품셈 공통", "상세구분": "01장 적용기준", - "이름": "인력운반(기계설비) — 건설 공통 1-2-7", + "이름": "인력운반(기계설비)", "결과단위": "원/자재 단위", "출처": "건설품셈 공통 1-2-7 5", "개정": [2008, 2010, 2016, 2022, 2023], @@ -258,7 +258,7 @@ "원문번호": "1-2-7", "구분": "건설품셈 공통", "상세구분": "01장 적용기준", - "이름": "벽돌운반 — 건설 공통 1-2-7", + "이름": "벽돌운반", "결과단위": "원/1,000매", "출처": "건설품셈 공통 1-2-7 4", "개정": [2008, 2010, 2016, 2022, 2023], @@ -282,6 +282,7 @@ "이름": "건설폐기물 발생량 — 건설 공통 1-5-4", "결과단위": "ton", "출처": "건설품셈 공통 1-5-4 2", + "부품": { "갈래": "계수식" }, "개정": [2011, 2014, 2017, 2020], "소유": "공용", "비고": "발생량 = 면적 × 단위면적당 발생량(TON/㎡) × (1 + 증가율 ÷ 100) · 증가율 = 폐콘크리트류만(주③ 파일 시공 15% · 20% 이내 · 주④ 세륜 시설 개소당 3% 이내 — 설계자가 넣음 · 0 = 증가 없음) · 원문 칸 「-」 = 줄 없음(멈춤) · 건축물 해체는 설계도서 산출 우선(원문 2) · 주⑤ 현장 여건 조정", @@ -314,6 +315,7 @@ "이름": "공사용수 — 건설 공통 1-5-6", "결과단위": "㎥", "출처": "건설품셈 공통 1-5-6 3", + "부품": { "갈래": "계수식" }, "소유": "공용", "비고": "용수량 = 작업량 × 단위당 용수(㎥) × (1 + 잡용수율 ÷ 100) · 양생 물 포함([주]) · 해석: 잡용수 = 원문 「사용량비의 40∼50%」 — 잡용수율 0 = 더하지 않음(설계자가 40∼50 넣음)", "시험입력": { "구분": "콘크리트혼합 및 양생", "작업량": 100, "잡용수율": 0 }, diff --git a/resources/master_data/로직_건설품셈_08장_건설기계.json b/resources/master_data/로직_건설품셈_08장_건설기계.json index 4c05877a6..cf980a4ba 100644 --- a/resources/master_data/로직_건설품셈_08장_건설기계.json +++ b/resources/master_data/로직_건설품셈_08장_건설기계.json @@ -13,6 +13,7 @@ "이름": "굴착기(무한궤도) 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "입력": [ { "이름": "기계", "고르기": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"] }, @@ -68,6 +69,7 @@ "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "고르기는 쓰는 기계만 — 주연료·조종원이 있는 기계는 더 넣어 씀 · 암석·전석섞인토사 작업의 불도저(19톤 미만 · 타이어·습지)·로더(무한궤도)·덤프트럭(18톤 미만)은 「공통 8 기계 시간당 경비(손료 보정)」(8-1-7 1)", "입력": [ @@ -237,6 +239,7 @@ "이름": "기계 시간당 기계경비(8-1-7 손료 보정)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "8-1-7 표의 기종만 가산 — 불도저(무한궤도) 19톤 미만 · 타이어·습지 불도저 · 로더(무한궤도) · 덤프트럭 18톤 미만 · 주① 대로 불도저 19톤 이상 · 덤프트럭 18톤 이상과 표 밖 기종은 가산 0 · 굴착기(무한궤도)는 「공통 8 굴착기(무한궤도) 시간당 경비」", "입력": [ @@ -432,6 +435,7 @@ "이름": "굴착기(무한궤도)+부착용 집게 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7 · 8-4-8 주⑤", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "집게는 굴착기 부수물(8-3-1 (0250) 주 · 규격은 해당 굴착기 규격) — 집게 규격은 굴착기 규격에 맞춰 고름", "입력": [ @@ -498,6 +502,7 @@ "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7 · 8-4-8 주⑤", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "브레이커는 굴착기 부수물(8-3-1 (0230) · 규격은 해당 굴착기 규격) — 치즐 소모는 쓰는 절의 요율로 따로", "입력": [ @@ -568,6 +573,7 @@ "이름": "기계 시간당 기계경비(조종원 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "원문 운전경비 표에 조종원 칸이 빈 기종(진동기 · 엔진) — 주연료가 있는 기계만", "입력": [ @@ -930,6 +936,7 @@ "이름": "기계 시간당 기계경비(조종원직종 입력)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "원문 8-1-3 5 운전사 구분에 이름 없는 기종 — 조종원직종을 설계자가 고름", "입력": [ @@ -1008,6 +1015,7 @@ "이름": "기계 시간당 기계경비(연료 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { "갈래": "기계경비식" }, "소유": "공용", "비고": "원문 운전경비 표에 주연료가 빈 기종(공기식 크롤러드릴 등 — 동력은 공기압축기) — 손료 + 조종원", "입력": [ @@ -1053,6 +1061,7 @@ "이름": "시공능력 산정 기본식 Q = n·q·f·E — 건설 공통 8-1-4", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-1-4", + "부품": { "갈래": "계수식" }, "소유": "공용", "비고": "n = 60/cm(분) 또는 3,600/cm(초) · 끝수 Q 3자리 · n 2자리 · cm 3자리(주①) · 단위는 원문대로 ㎥/hr 또는 ton/hr", "시험입력": { "싸이클시간": 21, "시간단위": "초", "작업량": 0.9, "f": 0.7, "E": 0.75 }, @@ -1083,6 +1092,7 @@ "이름": "불도저 시간당 작업량 — 건설 공통 8-2-1", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-1", + "부품": { "갈래": "작업량 틀", "기종": ["불도저"] }, "개정": [2025], "소유": "공용", "비고": "q = q° × e · cm = L/V1 + L/V2 + t · 타이어형 속도 표는 규격이 수(15 · 28 · 33)", @@ -1140,86 +1150,6 @@ "타이어규격": { "기호": "S", "아래": "타이어규격" } } }, - { - "키": "GC001145", - "원문번호": "8-2-1", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "불도저 작업량당 기계경비 — 건설 공통 8-2-1", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-1 · 건설품셈 공통 8-1-4", - "개정": [2025], - "소유": "공용", - "비고": "시간당 기계경비(GC000269) × 1/Q — Q = 8-2-1 불도저 시간당 작업량 로직", - "시험입력": { - "종별": "무한궤도", - "급수": "19", - "운반거리": 30, - "거리구분": "30", - "현장조건": "자연상태", - "등급": "보통", - "토질명": "모래, 사질토", - "E보정": "해당 없음", - "전진단": 1, - "후진단": 1, - "f": 1, - "E입력": 0, - "기계": "0101-0007" - }, - "입력": [ - { "이름": "종별", "설명": "불도저 종별", "고르기": ["무한궤도", "타이어"], "출처": "건설품셈 공통 8-2-1 1 가" }, - { "이름": "급수", "설명": "불도저 급수(ton)", "고르기": ["4(초습지)", "7", "10", "12", "13(습지)", "19", "32", "15", "28", "33"], "출처": "건설품셈 공통 8-2-1 1 가" }, - { "이름": "운반거리", "설명": "운반거리(L)", "단위": "m", "출처": "건설품셈 공통 8-2-1 2" }, - { "이름": "거리구분", "설명": "운반거리계수(e) 표의 거리 칸", "고르기": ["10이하", "20", "30", "40", "50", "60", "70", "80"], "출처": "건설품셈 공통 8-2-1 1 나" }, - { "이름": "현장조건", "설명": "흙 상태", "고르기": ["자연상태", "흐트러진 상태"], "출처": "건설품셈 공통 8-2-1 1 다" }, - { "이름": "등급", "설명": "작업 조건 등급", "고르기": ["양호", "보통", "불량"], "출처": "건설품셈 공통 8-2-1 1 다" }, - { "이름": "토질명", "설명": "토질", "고르기": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "출처": "건설품셈 공통 8-2-1 1 다" }, - { "이름": "E보정", "설명": "작업효율에서 뺄 경우", "고르기": ["해당 없음", "정지작업을 겸하는 경우", "터파기"], "출처": "건설품셈 공통 8-2-1 1 다 주" }, - { "이름": "전진단", "설명": "전진 기어 단", "고르기": [1, 2, 3, 4], "출처": "건설품셈 공통 8-2-1 2 주" }, - { "이름": "후진단", "설명": "후진 기어 단", "고르기": [1, 2, 3], "출처": "건설품셈 공통 8-2-1 2 주" }, - { "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" }, - { "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-1" }, - { - "이름": "기계", - "설명": "불도저 규격", - "고르기": ["0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "0102-0015", "0102-0028", "0102-0033", "0121-0004", "0121-0013"] - }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - }, - { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } - ], - "중간": [ - { - "이름": "작업량", - "식": "로직(GC001144, 종별=종별, 급수=급수, 운반거리=운반거리, 거리구분=거리구분, 현장조건=현장조건, 등급=등급, 토질명=토질명, E보정=E보정, 전진단=전진단, 후진단=후진단, f=f, E입력=E입력)", - "단위": "㎥/hr" - } - ], - "호표": [{ "종류": "로직", "요소": "로직(GC000269, 기계=기계, 지역=지역, 보정작업=보정작업)", "이름": "불도저", "단위": "시간", "수량": "1 / 작업량", "출처": "건설품셈 공통 8-1-4" }], - "덧줄": [], - "끝수": null, - "변수표": { - "운반거리": { "기호": "L" }, - "f": { "이름": "체적환산계수" }, - "작업량": { "이름": "시간당 작업량", "기호": "Q" }, - "종별": { "기호": "S", "아래": "종별" }, - "급수": { "기호": "S", "아래": "급수" }, - "거리구분": { "기호": "S", "아래": "거리구분" }, - "현장조건": { "기호": "S", "아래": "현장조건" }, - "등급": { "기호": "S", "아래": "등급" }, - "토질명": { "기호": "S", "아래": "토질명" }, - "E보정": { "기호": "E", "아래": "보정" }, - "전진단": { "기호": "S", "아래": "전진단" }, - "후진단": { "기호": "S", "아래": "후진단" }, - "E입력": { "기호": "E", "아래": "입력" }, - "기계": { "기호": "S", "아래": "기계" }, - "지역": { "기호": "S", "아래": "지역" }, - "보정작업": { "기호": "S", "아래": "보정작업" } - } - }, { "키": "GC001146", "원문번호": "8-2-2", @@ -1228,6 +1158,7 @@ "이름": "리퍼(유압식) 시간당 파쇄량 — 건설 공통 8-2-2", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-2", + "부품": { "갈래": "작업량 틀", "기종": ["유압식 리퍼"] }, "개정": [2025], "소유": "공용", "비고": "cm = 0.05ℓ + 0.25(원문 산정식) · 탄성파속도는 작업효율 표에 있는 값(규격별)", @@ -1269,6 +1200,7 @@ "이름": "굴착기 시간당 작업량 — 건설 공통 8-2-3", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-3", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "버킷용량" }, "개정": [2004, 2007, 2009, 2025], "소유": "공용", "비고": "Q = 3,600·q·K·f·E/cm · 협소현장 = 주⑦ 상하수도관로부설 등 별도 E 표 · 싸이클입력 0 = 표(규격 · 선회각도)", @@ -1337,96 +1269,6 @@ "보정": { "기호": "r", "아래": "보정" } } }, - { - "키": "GC001148", - "원문번호": "8-2-3", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "굴착기(무한궤도) 작업량당 기계경비 — 건설 공통 8-2-3", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-3 · 건설품셈 공통 8-1-4", - "개정": [2004, 2007, 2009, 2025], - "소유": "공용", - "비고": "시간당 기계경비(GC000267) × 1/Q — Q = 8-2-3 굴착기 시간당 작업량 로직", - "시험입력": { - "버킷용량": 1, - "규격구분": "1.0∼1.2", - "선회각도": 135, - "버킷조건": "위의 토질보다 약간 단단한 토질로서 버킷에 거의 가득 채울 수 있는 모래, 보통토 및 조건이 좋은 점토인 경우", - "현장조건": "흐트러진 상태", - "등급": "보통", - "토질명": "모래, 사질토", - "E보정": "해당 없음", - "협소현장": "해당 없음", - "f": 0.7, - "E입력": 0, - "싸이클입력": 0, - "기계": "0201-0012" - }, - "입력": [ - { "이름": "버킷용량", "설명": "버킷용량(q)", "단위": "㎥", "출처": "건설품셈 공통 8-2-3" }, - { "이름": "규격구분", "설명": "싸이클시간 표의 규격 칸", "단위": "㎥", "고르기": ["0.12∼0.4", "0.6∼0.8", "1.0∼1.2", "2.0"], "출처": "건설품셈 공통 8-2-3 3" }, - { "이름": "선회각도", "설명": "선회 각도", "단위": "도", "고르기": [45, 90, 135, 180], "출처": "건설품셈 공통 8-2-3 3" }, - { - "이름": "버킷조건", - "설명": "버킷계수(K) 현장조건", - "고르기": [ - "용이하게 굴착할 수 있는 연한 토질로서 버킷에 산적으로 가득찰 때가 많은 조건이 좋은 모래, 보통토인 경우", - "위의 토질보다 약간 단단한 토질로서 버킷에 거의 가득 채울 수 있는 모래, 보통토 및 조건이 좋은 점토인 경우", - "버킷에 가득 채우기가 어렵거나 가벼운 발파를 필요로 하는 것으로서 단단한 점토질, 점토, 역토질인 경우", - "버킷에 넣기 어렵고 불규칙한 공극이 생기는 것으로서 발파 또는 리퍼작업 등에 의하여 얻어진 암과 파쇄암, 호박돌, 역 등인 경우" - ], - "출처": "건설품셈 공통 8-2-3 1" - }, - { "이름": "현장조건", "설명": "흙 상태", "고르기": ["자연상태", "흐트러진 상태"], "출처": "건설품셈 공통 8-2-3 2" }, - { "이름": "등급", "설명": "작업 조건 등급", "고르기": ["양호", "보통", "불량"], "출처": "건설품셈 공통 8-2-3 2" }, - { "이름": "토질명", "설명": "토질", "고르기": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "출처": "건설품셈 공통 8-2-3 2" }, - { "이름": "E보정", "설명": "작업효율에서 뺄 경우", "고르기": ["해당 없음", "터파기"], "출처": "건설품셈 공통 8-2-3 2 주" }, - { "이름": "협소현장", "설명": "주택가 상하수도관로부설 등 협소현장(주⑦)", "고르기": ["해당 없음", "상하수도관로부설 등 협소현장"], "출처": "건설품셈 공통 8-2-3 2 주⑦" }, - { "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" }, - { "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-3" }, - { "이름": "싸이클입력", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "단위": "sec", "범위": [0, 30], "출처": "건설품셈 공통 8-2-3 3" }, - { - "이름": "기계", - "설명": "굴착기(무한궤도) 규격", - "고르기": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"] - }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - }, - { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } - ], - "중간": [ - { - "이름": "작업량", - "식": "로직(GC001147, 버킷용량=버킷용량, 규격구분=규격구분, 선회각도=선회각도, 버킷조건=버킷조건, 현장조건=현장조건, 등급=등급, 토질명=토질명, E보정=E보정, 협소현장=협소현장, f=f, E입력=E입력, 싸이클입력=싸이클입력)", - "단위": "㎥/hr" - } - ], - "호표": [{ "종류": "로직", "요소": "로직(GC000267, 기계=기계, 지역=지역, 보정작업=보정작업)", "이름": "굴착기(무한궤도)", "단위": "시간", "수량": "1 / 작업량", "출처": "건설품셈 공통 8-1-4" }], - "덧줄": [], - "끝수": null, - "변수표": { - "버킷용량": { "기호": "q" }, - "f": { "이름": "체적환산계수" }, - "작업량": { "이름": "시간당 작업량", "기호": "Q" }, - "규격구분": { "기호": "S", "아래": "규격구분" }, - "선회각도": { "기호": "θ", "아래": "선회" }, - "버킷조건": { "기호": "S", "아래": "버킷조건" }, - "현장조건": { "기호": "S", "아래": "현장조건" }, - "등급": { "기호": "S", "아래": "등급" }, - "토질명": { "기호": "S", "아래": "토질명" }, - "E보정": { "기호": "E", "아래": "보정" }, - "협소현장": { "기호": "S", "아래": "협소현장" }, - "E입력": { "기호": "E", "아래": "입력" }, - "싸이클입력": { "기호": "X", "아래": "싸이클" }, - "기계": { "기호": "S", "아래": "기계" }, - "지역": { "기호": "S", "아래": "지역" }, - "보정작업": { "기호": "S", "아래": "보정작업" } - } - }, { "키": "GC001149", "원문번호": "8-2-4", @@ -1435,6 +1277,7 @@ "이름": "트랜처 시간당 작업량 — 건설 공통 8-2-4", "결과단위": "m/hr", "출처": "건설품셈 공통 8-2-4", + "부품": { "갈래": "작업량 틀", "기종": ["트랜처"] }, "소유": "공용", "비고": "cm = t1 + (L1/V1 + L2/V2) + t3 · 작업보조인부 4인 1조(주①)", "시험입력": { "작업거리": 100, "전진거리": 0, "후진거리": 0, "굴착심도": 0.7, "토질": "사질토", "작업조건": "보통", "E입력": 0 }, @@ -1483,6 +1326,7 @@ "이름": "로더 운전시간당 작업량 — 건설 공통 8-2-5", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-5", + "부품": { "갈래": "작업량 틀", "기종": ["로더"], "규격": "버킷용량" }, "개정": [2007, 2020], "소유": "공용", "비고": "cm = m·ℓ + t1 + t2 · K 표 둘(산적상태 적재 · 자연상태·기타 적재) · 주행거리 0 = 표준 8m", @@ -1563,100 +1407,6 @@ "보정": { "기호": "r", "아래": "보정" } } }, - { - "키": "GC001151", - "원문번호": "8-2-5", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "로더 작업량당 기계경비 — 건설 공통 8-2-5", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-5 · 건설품셈 공통 8-1-4", - "개정": [2007, 2020], - "소유": "공용", - "비고": "시간당 기계경비(GC000269) × 1/Q — Q = 8-2-5 로더 시간당 작업량 로직", - "시험입력": { - "기종": "타이어식", - "버킷용량": 1.34, - "작업방법": "산적상태에서 담을 때", - "적재조건": "보통인 경우", - "K표": "산적상태 적재", - "K조건": "굴착기계로 깎거나 쌓아모은 산적상태에서 적재하는 것으로 굴착력을 필요로 하지 않고 쉽게 버킷에 산적할 수 있는 것, 즉 조건이 좋은 모래, 보통토 등", - "주행거리": 0, - "현장조건": "흐트러진 상태", - "등급": "양호", - "토질명": "모래, 사질토", - "E보정": "해당 없음", - "f": 1, - "E입력": 0, - "기계": "0301-0057" - }, - "입력": [ - { "이름": "기종", "설명": "로더 기종", "고르기": ["무한궤도식", "타이어식"], "출처": "건설품셈 공통 8-2-5 1" }, - { "이름": "버킷용량", "설명": "버킷용량(q)", "단위": "㎥", "출처": "건설품셈 공통 8-2-5" }, - { "이름": "작업방법", "설명": "버킷에 담는 방법", "고르기": ["산적상태에서 담을 때", "지면부터 굴착 집토하여 담을 때"], "출처": "건설품셈 공통 8-2-5 1" }, - { "이름": "적재조건", "설명": "t1 현장조건", "고르기": ["용이한 경우", "보통인 경우", "약간곤란한 경우", "곤란한 경우"], "출처": "건설품셈 공통 8-2-5 1" }, - { "이름": "K표", "설명": "버킷계수 표 갈래", "고르기": ["산적상태 적재", "자연상태·기타 적재"], "출처": "건설품셈 공통 8-2-5 2" }, - { - "이름": "K조건", - "설명": "버킷계수 현장조건(고른 표의 줄)", - "고르기": [ - "굴착기계로 깎거나 쌓아모은 산적상태에서 적재하는 것으로 굴착력을 필요로 하지 않고 쉽게 버킷에 산적할 수 있는 것, 즉 조건이 좋은 모래, 보통토 등", - "흐트러진 산적상태에서 적재하는 것으로 위 상태보다 약간 삽날이 들어가기 어려운 토질로서 버킷에 가득 채울 수 있는 것, 즉 점토, 역질토", - "모래, 사력보통토, 점토, 역질토 등 직접 자연상태에서 굴착적재 할 수 있는 여건으로 버킷에 평적에 약간 미달되게 채울 수 있는 것", - "버킷에 가득 채울 수 없는 것으로 다른 기계로 쌓아 모아놓은 부순돌 및 점질토나 역질토로서 굳어진 덩어리상태로 되어 있는 것", - "버킷에 넣기 어렵고 허술하며 불규칙한 공극이 생긴 것, 예를 들면 발파 또는 리퍼로 깎은 암괴, 호박돌, 역 등" - ], - "출처": "건설품셈 공통 8-2-5 2" - }, - { "이름": "주행거리", "설명": "편도주행거리(ℓ) — 0 이면 표준 8m", "단위": "m", "출처": "건설품셈 공통 8-2-5" }, - { "이름": "현장조건", "설명": "흙 상태", "고르기": ["자연상태", "흐트러진 상태"], "출처": "건설품셈 공통 8-2-5 3" }, - { "이름": "등급", "설명": "작업 조건 등급", "고르기": ["양호", "보통", "불량"], "출처": "건설품셈 공통 8-2-5 3" }, - { "이름": "토질명", "설명": "토질", "고르기": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "출처": "건설품셈 공통 8-2-5 3" }, - { "이름": "E보정", "설명": "작업효율에서 뺄 경우", "고르기": ["해당 없음", "터파기"], "출처": "건설품셈 공통 8-2-5 3 주" }, - { "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" }, - { "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-5" }, - { - "이름": "기계", - "설명": "로더 규격", - "고르기": ["0301-0057", "0301-0076", "0301-0095", "0301-0115", "0301-0134", "0301-0153", "0301-0172", "0301-0287", "0302-0057", "0302-0095", "0302-0134"] - }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - }, - { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } - ], - "중간": [ - { - "이름": "작업량", - "식": "로직(GC001150, 기종=기종, 버킷용량=버킷용량, 작업방법=작업방법, 적재조건=적재조건, K표=K표, K조건=K조건, 주행거리=주행거리, 현장조건=현장조건, 등급=등급, 토질명=토질명, E보정=E보정, f=f, E입력=E입력)", - "단위": "㎥/hr" - } - ], - "호표": [{ "종류": "로직", "요소": "로직(GC000269, 기계=기계, 지역=지역, 보정작업=보정작업)", "이름": "로더", "단위": "시간", "수량": "1 / 작업량", "출처": "건설품셈 공통 8-1-4" }], - "덧줄": [], - "끝수": null, - "변수표": { - "버킷용량": { "기호": "q" }, - "주행거리": { "기호": "ℓ" }, - "f": { "이름": "체적환산계수" }, - "작업량": { "이름": "운전시간당 작업량", "기호": "Q" }, - "기종": { "기호": "S", "아래": "기종" }, - "작업방법": { "기호": "S", "아래": "작업방법" }, - "적재조건": { "기호": "S", "아래": "적재조건" }, - "K표": { "기호": "K", "아래": "표" }, - "K조건": { "기호": "K", "아래": "조건" }, - "현장조건": { "기호": "S", "아래": "현장조건" }, - "등급": { "기호": "S", "아래": "등급" }, - "토질명": { "기호": "S", "아래": "토질명" }, - "E보정": { "기호": "E", "아래": "보정" }, - "E입력": { "기호": "E", "아래": "입력" }, - "기계": { "기호": "S", "아래": "기계" }, - "지역": { "기호": "S", "아래": "지역" }, - "보정작업": { "기호": "S", "아래": "보정작업" } - } - }, { "키": "GC001152", "원문번호": "8-2-6", @@ -1665,6 +1415,7 @@ "이름": "모터 스크레이퍼 시간당 작업량 — 건설 공통 8-2-6", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-6", + "부품": { "갈래": "작업량 틀", "기종": ["스크레이퍼"] }, "소유": "공용", "비고": "q = 적재함용적 × K · cm = L1/V1 + L2/V2 + t", "시험입력": { "적재함용적": 10, "적재주행거리": 300, "공차주행거리": 300, "푸쉬도우저": "사용", "f": 1, "E입력": 0 }, @@ -1743,6 +1494,7 @@ "이름": "모터 그레이더 1시간당 작업면적 — 건설 공통 8-2-7", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-7", + "부품": { "갈래": "작업량 틀", "기종": ["모터그레이더"] }, "개정": [2025], "소유": "공용", "비고": "cm 가 = 0.06×D/V1 + t · 나 = 0.06×(D/V1 + D/V2) + 2t · 속도가 여럿이면(P1·Cm1 + P2·Cm2 …) 설계자가 나눠 셈", @@ -1808,6 +1560,7 @@ "이름": "모터 그레이더 1시간당 작업토량 — 건설 공통 8-2-7", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-7", + "부품": { "갈래": "작업량 틀", "기종": ["모터그레이더"] }, "개정": [2025], "소유": "공용", "비고": "cm 가 = 0.06×D/V1 + t · 나 = 0.06×(D/V1 + D/V2) + 2t · 속도가 여럿이면(P1·Cm1 + P2·Cm2 …) 설계자가 나눠 셈", @@ -1881,6 +1634,7 @@ "이름": "덤프트럭 1시간당 작업량 — 건설 공통 8-2-8", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-8", + "부품": { "갈래": "작업량 틀", "기종": ["덤프트럭"], "규격": "적재용량" }, "개정": [2017], "소유": "공용", "비고": "q = T/γt × L · cm = t1 + … + t6 · 적재기계 t1 = cms·n/(60·Es) · n = q/(qs·k)(2자리) · 인력 t1 = 적재시간(분/㎥) × q · t2 = 거리/속도 × 60(분)", @@ -1997,126 +1751,6 @@ "E입력": { "기호": "E", "아래": "입력" } } }, - { - "키": "GC001156", - "원문번호": "8-2-8", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "덤프트럭 작업량당 기계경비 — 건설 공통 8-2-8", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-8 · 건설품셈 공통 8-1-4", - "개정": [2017], - "소유": "공용", - "비고": "시간당 기계경비(GC000268) × 1/Q — Q = 8-2-8 덤프트럭 시간당 작업량 로직", - "시험입력": { - "적재용량": 15, - "단위중량": 1.8, - "체적변화율": 1.25, - "f": 0.7, - "운반거리": 0.08, - "도로상태": "교차가 힘든 산간지도로 및 제방 등의 도로", - "적하토질": "모래, 역, 호박돌", - "적하조건": "불량", - "적재장소": "적재장소가 넓지는 않으나 목적장소에 불편없이 진입할 수 있을 때", - "덮개": "해당 없음", - "세륜": "안 함", - "적재방법": "적재기계", - "적재싸이클": 21, - "적재버킷": 1, - "버킷계수": 0.9, - "적재효율": 0.75, - "인력종류": "토사류", - "적재시간입력": 0, - "E입력": 0, - "기계": "0602-0025" - }, - "입력": [ - { "이름": "적재용량", "설명": "덤프트럭의 적재용량(T)", "단위": "ton", "출처": "건설품셈 공통 8-2-8" }, - { "이름": "단위중량", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "단위": "ton/㎥", "출처": "건설품셈 공통 8-2-8" }, - { "이름": "체적변화율", "설명": "체적환산계수에서의 체적변화율(L = 흐트러진/자연)", "출처": "건설품셈 공통 8-2-8" }, - { "이름": "f", "설명": "체적환산계수", "출처": "건설품셈 공통 8-2-8" }, - { "이름": "운반거리", "설명": "운반거리(편도)", "단위": "㎞", "출처": "건설품셈 공통 8-2-8 2" }, - { - "이름": "도로상태", - "설명": "운반도로 상태", - "고르기": [ - "토취장 또는 토사장 등 열악한 조건의 도로", - "교차가 힘든 산간지도로 및 제방 등의 도로", - "교차가 가능한 산간지도로 및 제방도로, 미포장도로", - "2차로 이상의 공사용도로", - "2차로 교통량 및 교통대기가 많은 시가지 포장도로(7,000대/일 이상)", - "4차로 이상의 교통량 및 교통대기가 많은 시가지 포장도로(40,000대/일 이상)", - "2차로 시가지 포장도로(7,000∼2,000대/일)", - "4차로 이상의 시가지 포장도로(40,000대/일 미만)", - "2차로 교외 포장도로(2,000대/일 이상)", - "4차로 이상의 교외 포장도로(40,000대/일 이상)", - "2차로 교외 포장도로(2,000대/일 미만)", - "4차로 이상의 교외 포장도로(40,000대/일 미만)", - "2차로 고속도로 또는 교통량(편도) 1일 40,000대 이상의 4차로 고속도로", - "4차로 고속도로(편도 교통량 1일 40,000대 미만)" - ], - "출처": "건설품셈 공통 8-2-8 3" - }, - { "이름": "적하토질", "설명": "적하시간 표의 토질", "고르기": ["모래, 역, 호박돌", "점질토, 점토"], "출처": "건설품셈 공통 8-2-8 4" }, - { "이름": "적하조건", "설명": "적하 작업조건", "고르기": ["양호", "보통", "불량"], "출처": "건설품셈 공통 8-2-8 4" }, - { - "이름": "적재장소", - "설명": "적재장소 진입 조건(t4)", - "고르기": ["적재장소가 넓어서 트럭이 자유로이 목적장소에 진입할 수 있을 때", "적재장소가 넓지는 않으나 목적장소에 불편없이 진입할 수 있을 때", "적재장소가 좁아서 목적장소에 진입하는데 불편을 느낄 때"], - "출처": "건설품셈 공통 8-2-8 5" - }, - { "이름": "덮개", "설명": "적재함 덮개 설치 · 해체(t5)", "고르기": ["해당 없음", "인력에 의한 경우", "자동덮개시설의 경우"], "출처": "건설품셈 공통 8-2-8 6" }, - { "이름": "세륜", "설명": "세륜기 통과(t6)", "고르기": ["안 함", "함"], "출처": "건설품셈 공통 8-2-8 7" }, - { "이름": "적재방법", "설명": "적재시간(t1) 산출 갈래", "고르기": ["적재기계", "인력", "직접 입력"], "출처": "건설품셈 공통 8-2-8 1 · 8 · 9" }, - { "이름": "적재싸이클", "설명": "적재기계의 1회 싸이클시간(cms)", "단위": "sec", "출처": "건설품셈 공통 8-2-8 8" }, - { "이름": "적재버킷", "설명": "적재기계의 버킷용량(q)", "단위": "㎥", "출처": "건설품셈 공통 8-2-8 8" }, - { "이름": "버킷계수", "설명": "적재기계의 버킷계수(k)", "출처": "건설품셈 공통 8-2-8 8" }, - { "이름": "적재효율", "설명": "적재기계의 작업효율(Es)", "출처": "건설품셈 공통 8-2-8 8" }, - { "이름": "인력종류", "설명": "인력 적재 종류", "고르기": ["토사류", "석재류"], "출처": "건설품셈 공통 8-2-8 9" }, - { "이름": "적재시간입력", "설명": "적재시간(t1)을 직접 넣을 때(적재방법 「직접 입력」)", "단위": "분", "출처": "건설품셈 공통 8-2-8 1" }, - { "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 0.9", "범위": [0, 1], "출처": "건설품셈 공통 8-2-8" }, - { "이름": "기계", "설명": "덤프트럭 규격", "고르기": ["0602-0025", "0602-0045", "0602-0060", "0602-0105", "0602-0150"] }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - } - ], - "중간": [ - { - "이름": "작업량", - "식": "로직(GC001155, 적재용량=적재용량, 단위중량=단위중량, 체적변화율=체적변화율, f=f, 운반거리=운반거리, 도로상태=도로상태, 적하토질=적하토질, 적하조건=적하조건, 적재장소=적재장소, 덮개=덮개, 세륜=세륜, 적재방법=적재방법, 적재싸이클=적재싸이클, 적재버킷=적재버킷, 버킷계수=버킷계수, 적재효율=적재효율, 인력종류=인력종류, 적재시간입력=적재시간입력, E입력=E입력)", - "단위": "㎥/hr" - } - ], - "호표": [{ "종류": "로직", "요소": "로직(GC000268, 기계=기계, 지역=지역)", "이름": "덤프트럭", "단위": "시간", "수량": "1 / 작업량", "출처": "건설품셈 공통 8-1-4" }], - "덧줄": [], - "끝수": null, - "변수표": { - "적재용량": { "기호": "T" }, - "체적변화율": { "기호": "L" }, - "f": { "이름": "체적환산계수" }, - "적재싸이클": { "기호": "cms" }, - "적재버킷": { "기호": "q" }, - "버킷계수": { "기호": "k" }, - "적재효율": { "기호": "Es" }, - "작업량": { "이름": "1시간당 작업량", "기호": "Q" }, - "단위중량": { "기호": "W", "아래": "단위" }, - "운반거리": { "기호": "L", "아래": "운반" }, - "도로상태": { "기호": "S", "아래": "도로상태" }, - "적하토질": { "기호": "S", "아래": "적하토질" }, - "적하조건": { "기호": "S", "아래": "적하조건" }, - "적재장소": { "기호": "S", "아래": "적재장소" }, - "덮개": { "기호": "S", "아래": "덮개" }, - "세륜": { "기호": "S", "아래": "세륜" }, - "적재방법": { "기호": "S", "아래": "적재방법" }, - "인력종류": { "기호": "S", "아래": "인력종류" }, - "적재시간입력": { "기호": "T", "아래": "적재" }, - "E입력": { "기호": "E", "아래": "입력" }, - "기계": { "기호": "S", "아래": "기계" }, - "지역": { "기호": "S", "아래": "지역" } - } - }, { "키": "GC001157", "원문번호": "8-2-9", @@ -2125,6 +1759,7 @@ "이름": "롤러 시간당 다짐토량 — 건설 공통 8-2-9", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-9", + "부품": { "갈래": "작업량 틀", "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] }, "개정": [2004, 2017, 2025], "소유": "공용", "비고": "Q = 1,000·V·W·D·E·f/N · 소요다짐횟수 · 두께는 2 표(CC000063) 참고 입력", @@ -2184,6 +1819,7 @@ "이름": "롤러 시간당 다짐면적 — 건설 공통 8-2-9", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-9", + "부품": { "갈래": "작업량 틀", "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] }, "개정": [2004, 2017, 2025], "소유": "공용", "비고": "A = 1,000·V·W·E·1/N", @@ -2231,77 +1867,6 @@ "E입력": { "기호": "E", "아래": "입력" } } }, - { - "키": "GC001159", - "원문번호": "8-2-9", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "롤러 작업량당 기계경비 — 건설 공통 8-2-9", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-9 · 건설품셈 공통 8-1-4", - "개정": [2004, 2017, 2025], - "소유": "공용", - "비고": "시간당 기계경비(GC000268) × 1/Q — Q = 8-2-9 롤러토량 시간당 작업량 로직", - "시험입력": { - "다짐기계": "타이어롤러", - "규격": "8∼15", - "다짐층": "노체, 축제, 노상", - "다짐횟수": 4, - "E공종": "노체, 축제, 노상", - "E기계": "불도저", - "현장조건": "보통", - "E입력": 0, - "두께": 0.3, - "f": 1, - "기계": "1106-0010" - }, - "입력": [ - { "이름": "다짐기계", "설명": "다짐기계", "고르기": ["머캐덤롤러", "탠덤롤러", "타이어롤러", "불도저", "자주식, 양족식 롤러", "진동롤러(자주식)"], "출처": "건설품셈 공통 8-2-9 1" }, - { - "이름": "규격", - "설명": "다짐기계 규격(ton)", - "고르기": ["6∼8", "8∼10", "10∼12", "12∼15", "5∼8", "10∼14", "8∼15", "15∼25", "12", "19", "2.5", "4.4", "6.0", "10.0"], - "출처": "건설품셈 공통 8-2-9 1" - }, - { "이름": "다짐층", "설명": "표준다짐속도 칸", "고르기": ["노체, 축제, 노상", "보조기층, 기층", "표층"], "출처": "건설품셈 공통 8-2-9 1" }, - { "이름": "다짐횟수", "설명": "소요다짐횟수(N)", "단위": "회", "출처": "건설품셈 공통 8-2-9 2" }, - { "이름": "E공종", "설명": "작업효율 표의 공종", "고르기": ["표층", "기층, 보조기층", "노체, 축제, 노상"], "출처": "건설품셈 공통 8-2-9 3" }, - { "이름": "E기계", "설명": "작업효율 표의 다짐기계", "고르기": ["머캐덤롤러", "타이어롤러", "탠덤롤러", "진동롤러", "불도저", "양족식롤러(자주식)"], "출처": "건설품셈 공통 8-2-9 3" }, - { "이름": "현장조건", "설명": "작업 조건", "고르기": ["양호", "보통", "불량"], "출처": "건설품셈 공통 8-2-9 3" }, - { "이름": "E입력", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "범위": [0, 1], "출처": "건설품셈 공통 8-2-9" }, - { "이름": "두께", "설명": "펴는 흙의 두께(D · 다져진 상태)", "단위": "m", "출처": "건설품셈 공통 8-2-9 2 주②" }, - { "이름": "f", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4" }, - { - "이름": "기계", - "설명": "롤러 규격", - "고르기": ["1106-0010", "1106-0012", "1206-0008", "1305-0007", "1306-0025", "1306-0100", "1306-0120", "1406-0008", "1406-0015", "1506-0032"] - }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - } - ], - "중간": [{ "이름": "작업량", "식": "로직(GC001157, 다짐기계=다짐기계, 규격=규격, 다짐층=다짐층, 다짐횟수=다짐횟수, E공종=E공종, E기계=E기계, 현장조건=현장조건, E입력=E입력, 두께=두께, f=f)", "단위": "㎥/hr" }], - "호표": [{ "종류": "로직", "요소": "로직(GC000268, 기계=기계, 지역=지역)", "이름": "롤러", "단위": "시간", "수량": "1 / 작업량", "출처": "건설품셈 공통 8-1-4" }], - "덧줄": [], - "끝수": null, - "변수표": { - "다짐횟수": { "기호": "N" }, - "두께": { "기호": "D" }, - "f": { "이름": "체적환산계수" }, - "작업량": { "이름": "시간당 다짐토량", "기호": "Q" }, - "다짐기계": { "기호": "S", "아래": "다짐기계" }, - "규격": { "기호": "S", "아래": "규격" }, - "다짐층": { "기호": "S", "아래": "다짐층" }, - "E공종": { "기호": "E", "아래": "공종" }, - "E기계": { "기호": "E", "아래": "기계" }, - "현장조건": { "기호": "S", "아래": "현장조건" }, - "E입력": { "기호": "E", "아래": "입력" }, - "기계": { "기호": "S", "아래": "기계" }, - "지역": { "기호": "S", "아래": "지역" } - } - }, { "키": "GC001160", "원문번호": "8-2-10", @@ -2310,6 +1875,7 @@ "이름": "아스팔트 플랜트 시간당 생산능력 — 건설 공통 8-2-10", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-10", + "부품": { "갈래": "작업량 틀", "기종": ["아스팔트 믹싱플랜트"] }, "소유": "공용", "비고": "기계효율 80% 반영값 · 작업효율 적용 안 함 · 일생산 6시간(2)", "시험입력": { "플랜트규격": 60, "혼합재": "B" }, @@ -2330,6 +1896,7 @@ "이름": "스테이빌라이저(노상안정기) 시간당 작업량 — 건설 공통 8-2-11", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-11", + "부품": { "갈래": "작업량 틀", "기종": ["스테이빌라이저"] }, "소유": "공용", "비고": "A = W·V·E/P · W = Rotor 폭 − 0.4m", "시험입력": { "로터폭": 2.4, "작업조건": "보통의 경우", "혼합횟수": 0, "E입력": 0 }, @@ -2365,6 +1932,7 @@ "이름": "죠 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { "갈래": "작업량 틀", "기종": ["죠 크러셔"] }, "소유": "공용", "비고": "양호 = 최대치 · 보통 = 평균치 · 불량 = 최소치(주④) · 단위용적중량 1.6ton/㎥ 기준", "시험입력": { "규격": "025060", "출구간격": 40, "작업조건": "보통" }, @@ -2389,6 +1957,7 @@ "이름": "롤 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { "갈래": "작업량 틀", "기종": ["롤 크러셔"] }, "소유": "공용", "비고": "효율 양호 65% · 보통 50% · 불량 35%(주③)", "시험입력": { "규격": "076063", "출구간격": 25, "작업조건": "보통" }, @@ -2410,6 +1979,7 @@ "이름": "이동식 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { "갈래": "작업량 틀", "기종": ["크러셔"] }, "소유": "공용", "비고": "괄호 값도 원문 칸 그대로 씀", "시험입력": { "규격": 100, "출구간격": 25, "작업조건": "보통" }, @@ -2431,6 +2001,7 @@ "이름": "스크린 전체 통과량 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { "갈래": "작업량 틀", "기종": ["스크린"] }, "소유": "공용", "비고": "통과량 = 930㎠당 통과능력 × A×B×C×D×E × 체적면적 × 1/930(주⑤ 예시)", "시험입력": { "체규격": 25, "조합방법": "1회통과식", "택의수": 1, "B골재량": 40, "C규격": 28.0, "D골재량": 30, "체면적": 930 }, @@ -2477,41 +2048,6 @@ "통과능력": { "기호": "q", "아래": "통과" } } }, - { - "키": "GC001166", - "원문번호": "8-2-13", - "구분": "건설품셈 공통", - "상세구분": "08장 건설기계", - "이름": "대형브레이커 작업량당 기계경비 — 건설 공통 8-2-13", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-13 2", - "개정": [2014, 2017, 2025], - "소유": "공용", - "비고": "작업능력 = 구조물 헐기(CC000065) · 굴착(CC000067) 표 범위 안에서 — 적용방법 다: 넓고 장해물 없음 상한치 · 약간 지장 평균치 · 협소 하한치 · 굴착기 0.4㎥ 조합 하한치(주⑤) · 보통인부 1인 · 치즐은 따로", - "시험입력": { "작업능력": 3.3, "기계": "0201-0070", "브레이커": "0230-0007" }, - "입력": [ - { "이름": "작업능력", "설명": "대형브레이커 작업능력(Q · 표 범위 안 값)", "단위": "㎥/hr", "출처": "건설품셈 공통 8-2-13 2" }, - { "이름": "기계", "고르기": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"] }, - { "이름": "브레이커", "고르기": ["0230-0002", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010"] }, - { - "이름": "지역", - "고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], - "출처": "건설품셈 공통 8-1-7 5" - }, - { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } - ], - "중간": [], - "호표": [{ "종류": "로직", "요소": "로직(GC000271, 기계=기계, 브레이커=브레이커, 지역=지역, 보정작업=보정작업)", "이름": "굴착기+대형브레이커", "단위": "시간", "수량": "1 / 작업능력", "출처": "건설품셈 공통 8-2-13 2" }], - "덧줄": [], - "끝수": null, - "변수표": { - "작업능력": { "기호": "q", "아래": "작업" }, - "기계": { "기호": "S", "아래": "기계" }, - "브레이커": { "기호": "S", "아래": "브레이커" }, - "지역": { "기호": "S", "아래": "지역" }, - "보정작업": { "기호": "S", "아래": "보정작업" } - } - }, { "키": "GC001167", "원문번호": "8-2-14", @@ -2520,6 +2056,7 @@ "이름": "압쇄기(콘크리트 소할용) 시간당 작업량 — 건설 공통 8-2-14", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-14", + "부품": { "갈래": "작업량 틀", "기종": ["압쇄기"] }, "개정": [2004], "소유": "공용", "비고": "Q = q × E", @@ -2538,6 +2075,7 @@ "이름": "법면다짐기 시간당 작업량 — 건설 공통 8-2-15", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-15", + "부품": { "갈래": "작업량 틀", "기종": ["유압식 진동콤팩터"] }, "소유": "공용", "비고": "유압식 진동콤팩터 + 굴착기 0.7㎥ · 법면다짐판 + 굴착기 1.0㎥(1)", "시험입력": { "구분": "유압식 진동콤팩터" }, @@ -2555,6 +2093,7 @@ "이름": "골재세척설비 시간당 작업량 — 건설 공통 8-2-16", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-16", + "부품": { "갈래": "작업량 틀", "기종": ["골재세척설비"] }, "개정": [2001], "소유": "공용", "비고": "Q = q × E", @@ -2573,6 +2112,7 @@ "이름": "콘크리트 믹서 시간당 생산량 — 건설 공통 8-2-17", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-17", + "부품": { "갈래": "작업량 틀", "기종": ["콘크리트 믹서"], "규격": "믹서용량" }, "소유": "공용", "비고": "Q = 60/4 · q · E", "시험입력": { "믹서용량": 0.5, "E입력": 0 }, @@ -2593,6 +2133,7 @@ "이름": "콘크리트 배치플랜트(강제 혼합식) 시간당 작업량 — 건설 공통 8-2-18", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-18", + "부품": { "갈래": "작업량 틀", "기종": ["콘크리트 배치플랜트"] }, "개정": [2000, 2002, 2011], "소유": "공용", "비고": "Q = 60·q·E/cm · 터널 숏크리트 매우 불량은 E입력 0.40(주②)", @@ -2637,6 +2178,7 @@ "이름": "콘크리트 믹서트럭 시간당 운반량 — 건설 공통 8-2-19", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-19", + "부품": { "갈래": "작업량 틀", "기종": ["콘크리트 믹서트럭"], "규격": "적재용량" }, "소유": "공용", "비고": "cm = t1 + t2 + t3 + t4 · t1 = w/q·cmc · t2 = 거리/속도 × 60(분) · 범위(배출 2∼3 · 3∼4 · 대기 5∼10)는 가운데 기본", "시험입력": { "적재용량": 6, "플랜트실용량": 1, "플랜트싸이클": 0, "운반거리": 5, "적재속도": 30, "공차속도": 35, "슬럼프": "5㎝ 이상", "펌프조합": "아니오", "배출시간입력": 0, "대기시간입력": 0, "E입력": 0 }, @@ -2696,6 +2238,7 @@ "이름": "콘크리트 덤프트럭 시간당 운반량 — 건설 공통 8-2-19", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-19", + "부품": { "갈래": "작업량 틀", "기종": ["덤프트럭"] }, "소유": "공용", "비고": "cm = cm1 + cm2 · cm1 = 3L + 5(±5) · cm2 = w/q·cmc + t1 + t2", "시험입력": { "규격": 10.5, "플랜트실용량": 1, "플랜트싸이클": 0, "운반거리": 5, "하역시간입력": 0, "대기시간입력": 0, "E입력": 0 }, @@ -2745,6 +2288,7 @@ "이름": "기관차 시간당 작업량 — 건설 공통 8-2-20", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-20", + "부품": { "갈래": "작업량 틀", "기종": ["디젤 기관차"] }, "소유": "공용", "비고": "Q = C·N·f·E · N = 60/(t1 + L/V1 + L/V2 + t2) · C = n × q", "시험입력": { "기관차": "5t", "대차용량": 1.5, "운반거리": 500, "재료": "토사류", "f": 1, "E": 0.8 }, @@ -2789,6 +2333,7 @@ "이름": "경운기 시간당 작업량 — 건설 공통 8-2-21", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-21", + "부품": { "갈래": "작업량 틀", "기종": ["경운기"] }, "소유": "공용", "비고": "cm = L/V1 + L/V2 + t · 공차 속도 = 원문 「적하」 칸", "시험입력": { "적재량": 0.5, "거리": 100, "종류": "토사류", "작업로": "보통", "f": 1, "E입력": 0 }, @@ -2831,6 +2376,7 @@ "이름": "디젤 파일 해머 파일 1본당 시공시간 — 강관파일 — 건설 공통 8-2-22", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-22", + "부품": { "갈래": "작업량 틀", "기종": ["디젤 파일 해머"] }, "소유": "공용", "비고": "Tc = (Tb+Tw+Ts+Tt+Te)/F · Tb = 0.05·α·β·L(N+2) — 원문 강관 식 글에 β 글자가 빠져 있으나 기호 풀이(β 해머 계수) · 표 · 콘크리트 식대로 β 곱함", "시험입력": { @@ -2925,6 +2471,7 @@ "이름": "디젤 파일 해머 파일 1본당 시공시간 — 콘크리트 파일(PC · RC) — 건설 공통 8-2-22", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-22", + "부품": { "갈래": "작업량 틀", "기종": ["디젤 파일 해머"] }, "소유": "공용", "비고": "Tc = (Tb+Ts+Tt+Te)/F · Tb = 0.08α·β·L(N+2) · 용접 없음", "시험입력": { "토질": "실트・로움・모래", "α입력": 0, "파일경": 400, "해머": "2.2ton 급", "관입길이": 12, "평균N": 10, "세우기횟수": 1, "평탄성": "양호", "현장": "현장이 넓으며 작업에 장애물이 없는 경우" }, @@ -2988,6 +2535,7 @@ "이름": "유압 파일 해머 파일 1본당 시공시간 — 강관파일 — 건설 공통 8-2-23", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-23", + "부품": { "갈래": "작업량 틀", "기종": ["유압 파일 해머"] }, "소유": "공용", "비고": "Tc = α·β·Ta · 용접시간 포함(주④)", "시험입력": { "평균N": 15, "항타길이": "17∼32", "판두께": "12", "파일경": "500∼800" }, @@ -3024,6 +2572,7 @@ "이름": "유압 파일 해머 파일 1본당 시공시간 — 콘크리트 파일(PC · RC · PHC) — 건설 공통 8-2-23", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-23", + "부품": { "갈래": "작업량 틀", "기종": ["유압 파일 해머"] }, "소유": "공용", "비고": "Tc = α·Ta", "시험입력": { "평균N": 25, "항타길이": "16∼22", "파일경": "300∼600" }, @@ -3054,6 +2603,7 @@ "이름": "진동파일 해머 파일 1본당 시공시간 — H파일 — 건설 공통 8-2-24", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-24", + "부품": { "갈래": "작업량 틀", "기종": ["진동파일 해머"] }, "개정": [1996, 2026], "소유": "공용", "비고": "Tc = (Ts+Tb)/F · Tb = r×ℓ×k · r = (r1ℓ1 + r2ℓ2)/(ℓ1+ℓ2) · F = F0 + (f1+f2+f3+f4)", @@ -3119,6 +2669,7 @@ "이름": "진동파일 해머 파일 1본당 시공시간 — 강널말뚝 — 건설 공통 8-2-24", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-24", + "부품": { "갈래": "작업량 틀", "기종": ["진동파일 해머"] }, "개정": [1996, 2026], "소유": "공용", "비고": "Tc = {(0.75+γ·Nmax)·ℓ+α}·K/F · K = 원문 항타 · 인발 두 줄에 걸친 한 칸(항타 줄 값) · 인발 F0 = 원문 「항발」 칸", @@ -3164,6 +2715,7 @@ "이름": "진동파일해머(워터제트 병용 압입공) 파일 1본당 시공시간 — 건설 공통 8-2-25", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-25", + "부품": { "갈래": "작업량 틀", "기종": ["진동파일 해머", "워터젯트"] }, "소유": "공용", "비고": "Tc = To×α/F · To = 0.05L(N+42.5)+9.6 · F = F0 + (f1+f2+f3+f4)", "시험입력": { "근입길이": 12, "평균N": 40, "토질": "점성토", "α입력": 0, "f1": 0, "f2": 0, "f3": 0, "f4": 0 }, @@ -3206,6 +2758,7 @@ "이름": "유압식 압입 인발기 강널말뚝 1본당 시공시간 — 건설 공통 8-2-26", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-26", + "부품": { "갈래": "작업량 틀", "기종": ["유압식 압입 인발기"] }, "소유": "공용", "비고": "압입 Tc = (Ts+Tb)/F · 인발 Tc = (1.10ℓ+4.76)/F", "시험입력": { "작업": "압입", "파일길이": 12, "압입길이": 10, "Nmax": 20, "f1": 0, "f2": 0, "f3": 0 }, @@ -3247,7 +2800,7 @@ "원문번호": "8-2-27", "구분": "건설품셈 공통", "상세구분": "08장 건설기계", - "이름": "수중펌프 설치 및 해체(1개소당) — 건설 공통 8-2-27", + "이름": "수중펌프 설치 및 해체", "결과단위": "원/개소", "출처": "건설품셈 공통 8-2-27 5", "소유": "공용", @@ -3268,6 +2821,7 @@ "이름": "터널전단면 굴착기(TBM) 1시간당 작업량 — 건설 공통 8-2-28", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-28", + "부품": { "갈래": "작업량 틀", "기종": ["터널전단면 굴착기"] }, "소유": "공용", "비고": "A = πD²/4 · cm = T1 + T2 · T1 = ℓ/(R×Pe)×100", "시험입력": { "굴착직경": 4.5, "작업거리": 1.2, "회전속도": 8, "투과깊이": 0.5, "현장조건": "보통", "E입력": 0 }, @@ -3308,6 +2862,7 @@ "이름": "펌프식 준설선 1시간당 준설능력 — 건설 공통 8-2-29", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-29", + "부품": { "갈래": "작업량 틀", "기종": ["펌프 준설선"] }, "개정": [2010, 2011], "소유": "공용", "비고": "Q = q·bo·E/746 · E = E1×E2×E3×E4 · bo = 디젤 공칭 × 0.8 · 터빈 × 0.9", @@ -3387,6 +2942,7 @@ "이름": "그래브 준설선 1시간당 준설량 — 건설 공통 8-2-30", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-30", + "부품": { "갈래": "작업량 틀", "기종": ["그래브 준설선"] }, "개정": [2010, 2011], "소유": "공용", "비고": "Q = 3,600·q·k·f·E/cm · E = E1×E2 · 수심 10m 기준 1m 증감마다 2초 증감 · 표의 괄호 값 = 쇄암 · 발파후 준설", @@ -3463,6 +3019,7 @@ "이름": "쇄암선(중추식) 시간당 작업능력 — 건설 공통 8-2-31", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-31", + "부품": { "갈래": "작업량 틀", "기종": [] }, "개정": [2011], "소유": "공용", "비고": "Q = 60·d·S·E/(t + n/p)", @@ -3496,6 +3053,7 @@ "이름": "이동식 임목파쇄기 시간당 파쇄능력 — 건설 공통 8-2-32", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-32", + "부품": { "갈래": "작업량 틀", "기종": ["이동식 임목파쇄기"] }, "개정": [2007, 2011], "소유": "공용", "비고": "93.25㎾ Q = 6.0 · 354.35∼402.84㎾ Q = q·K·S·E", @@ -3521,6 +3079,7 @@ "이름": "하천골재채취선 시간당 준설량 — 건설 공통 8-2-33", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-33", + "부품": { "갈래": "작업량 틀", "기종": ["하천골재채취선"] }, "개정": [2005], "소유": "공용", "비고": "Q = q·b·E/746", diff --git a/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json b/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json index 5ca29e6e6..e53c015b2 100644 --- a/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json +++ b/resources/master_data/로직_건설품셈_13장_플랜트설비공사.json @@ -8793,6 +8793,7 @@ "이름": "아세틸렌량의 환산(ℓ → ㎏)", "결과단위": "㎏", "출처": "건설품셈 기계설비 13-2-7", + "부품": { "갈래": "계수식" }, "소유": "공용", "비고": "아세틸렌(㎏) = 아세틸렌(ℓ) × 26g/22.4ℓ ÷ 1,000 · 26g = 1mol당 분자량 · 22.4ℓ = 표준상태 1mol당량", "시험입력": { "아세틸렌부피": 1000 }, diff --git a/resources/master_data/로직_산림품셈_03장_작업장_관리.json b/resources/master_data/로직_산림품셈_03장_작업장_관리.json index 5ac1df373..f67fc2a01 100644 --- a/resources/master_data/로직_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/로직_산림품셈_03장_작업장_관리.json @@ -83,6 +83,7 @@ "이름": "임산물 운반로 신설 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 3-4-2 1", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "해석: 원문 단가산출 예시의 값(q 0.2 · K 0.7 · f 0.71 · E 0.45 · Cm 18)을 표준 값으로 씀", diff --git a/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json b/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json index 0f2566c88..24cc4b820 100644 --- a/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json +++ b/resources/master_data/로직_산림품셈_05장_식재_및_녹화.json @@ -1199,6 +1199,7 @@ "이름": "복사이식 굴착기 시간당 작업량(Q1)", "결과단위": "㎥/hr", "출처": "산림품셈 5-29 주④", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "해석: q = 주③ 굴착기 용량 1.0㎥ · 주⑤ Q=Q1×T 는 단위가 안 맞아(㎥/시간 × m) 넣지 않음", diff --git a/resources/master_data/로직_산림품셈_09장_토공.json b/resources/master_data/로직_산림품셈_09장_토공.json index 464589b58..3316584d8 100644 --- a/resources/master_data/로직_산림품셈_09장_토공.json +++ b/resources/master_data/로직_산림품셈_09장_토공.json @@ -46,6 +46,7 @@ "이름": "토사깍기(기계 · 굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-3-2", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "E 원문 「0.55∼0.45」 — 설계자 입력 · f 는 주④ 0.77", @@ -158,6 +159,7 @@ "이름": "암절취 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-4-2", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "f 원문 「1/1.40」 그대로 계산", @@ -299,6 +301,7 @@ "이름": "발파암 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-5-3", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "f 원문 「1/1.625」 그대로 계산", @@ -388,6 +391,7 @@ "이름": "콘크리트 깨기 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-7-1 주⑥", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "입력": [], @@ -825,6 +829,7 @@ "이름": "측구터파기 토사 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-12-1", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "입력": [], @@ -2028,6 +2033,7 @@ "이름": "되메우기 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-14-1", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "입력": [], @@ -2080,6 +2086,7 @@ "이름": "다짐(플레이트콤펙터) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-14-2", + "부품": { "갈래": "작업량 틀", "기종": ["플레이트 콤팩터"] }, "개정": [2026], "소유": "공용", "비고": "돈 로직 GF000249", @@ -2121,6 +2128,7 @@ "이름": "표토제거 답(畓)구간(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎡/hr", "출처": "산림품셈 9-15-1", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "T(표토두께) 설계자 입력 · Q=Q1/T · f 원문 「1/1.30」 그대로 계산", @@ -2165,6 +2173,7 @@ "이름": "표토제거 답(畓)외구간(불도저 19ton) 시간당 작업량", "결과단위": "㎡/hr", "출처": "산림품셈 9-15-2", + "부품": { "갈래": "작업량 틀", "기종": ["불도저"] }, "개정": [2026], "소유": "공용", "비고": "돈 로직 GF000250 · f 원문 「1/1.3」 그대로 계산", @@ -2224,6 +2233,7 @@ "이름": "노체포설 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-16-1 주②", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"] }, "개정": [2026], "소유": "공용", "입력": [], @@ -2270,6 +2280,7 @@ "이름": "노체다짐 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-16-2 주②", + "부품": { "갈래": "작업량 틀", "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] }, "개정": [2026], "소유": "공용", "입력": [], @@ -2341,6 +2352,7 @@ "이름": "비다짐(정지 · 불도저 19ton) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-17-2", + "부품": { "갈래": "작업량 틀", "기종": ["불도저"] }, "개정": [2026], "소유": "공용", "비고": "돈 로직 GF000251 · E 원문 「(0.55-0.1)」 · f 원문 「1/1.3」 그대로 계산", @@ -2395,6 +2407,7 @@ "이름": "층따기(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-18", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "f 원문 「1/1.3」 그대로 계산", @@ -2641,6 +2654,7 @@ "이름": "섞기(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-22", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "f 원문 「1/1.25」 그대로 계산 · 주③ Q=Q1*T 는 단위가 맞지 않아 Q1 까지만", diff --git a/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json b/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json index 9080d16db..9bad8e51b 100644 --- a/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json +++ b/resources/master_data/로직_산림품셈_10장_자재․장비_운반.json @@ -12,6 +12,7 @@ "이름": "골재운반 덤프트럭 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 10-3-1 주", + "부품": { "갈래": "작업량 틀", "기종": ["덤프트럭"] }, "개정": [2026], "소유": "공용", "비고": "원문은 식만(Q=60×qt×f×E/㎝) — qt · f · E · ㎝ 값 없음 · 설계자 입력", @@ -518,6 +519,7 @@ "이름": "4륜구동 1톤 덤프트럭 시간당 기계경비", "결과단위": "원/시간", "출처": "산림품셈 10-9", + "부품": { "갈래": "기계경비식" }, "개정": [2026], "소유": "공용", "비고": "손료·운전경비는 2.5톤(0602-0025) · 주연료 2.5톤의 80% · 중기가격 조달청 등재가격은 요소 없음 · 작업량(건설품셈 덤프트럭 식)·10-9-1 운반량 쓰는 식 요소 없음 · 주② 제잡비 비율 원문에 없음", @@ -673,6 +675,7 @@ "이름": "불도저 운반 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 10-11 주②", + "부품": { "갈래": "작업량 틀", "기종": ["불도저"] }, "개정": [2026], "소유": "공용", "비고": "해석: 파쇄암·발파암의 E 는 「암석」 줄", diff --git a/resources/master_data/로직_산림품셈_11장_가설.json b/resources/master_data/로직_산림품셈_11장_가설.json index 1ab42eadb..8d1f89822 100644 --- a/resources/master_data/로직_산림품셈_11장_가설.json +++ b/resources/master_data/로직_산림품셈_11장_가설.json @@ -93,6 +93,7 @@ "이름": "쇄석·혼합석 부설 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 11-4 주①", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "해석: 원문 식 「3600×90×k×f×E/㎝」의 90 을 q(버킷용량)로 봄", diff --git a/resources/master_data/로직_산림품셈_13장_구조물.json b/resources/master_data/로직_산림품셈_13장_구조물.json index 42ed818c5..b4957c668 100644 --- a/resources/master_data/로직_산림품셈_13장_구조물.json +++ b/resources/master_data/로직_산림품셈_13장_구조물.json @@ -1323,6 +1323,7 @@ "이름": "토양개량제 부설 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 13-13-2", + "부품": { "갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q" }, "개정": [2026], "소유": "공용", "비고": "식은 9장 같은 꼴 표(9-12-1 등)와 같이 Q=3600×q×k×f×E/㎝", diff --git a/resources/master_data/수식문서/수식_건설품셈_01장_적용기준.json b/resources/master_data/수식문서/수식_건설품셈_01장_적용기준.json index 8e86736a9..b5ebadc4c 100644 --- a/resources/master_data/수식문서/수식_건설품셈_01장_적용기준.json +++ b/resources/master_data/수식문서/수식_건설품셈_01장_적용기준.json @@ -6,7 +6,7 @@ "꼴": "수식문서", "판": 1, "키": "GC001140", - "이름": "인력운반(지게) — 건설 공통 1-2-7", + "이름": "지게운반", "결과단위": "원/자재 단위", "출처": "건설품셈 공통 1-2-7 2 · 3", "변수": [ @@ -55,7 +55,7 @@ "꼴": "수식문서", "판": 1, "키": "GC001141", - "이름": "인력운반(기계설비) — 건설 공통 1-2-7", + "이름": "인력운반(기계설비)", "결과단위": "원/자재 단위", "출처": "건설품셈 공통 1-2-7 5", "변수": [ @@ -102,7 +102,7 @@ "꼴": "수식문서", "판": 1, "키": "GC001142", - "이름": "벽돌운반 — 건설 공통 1-2-7", + "이름": "벽돌운반", "결과단위": "원/1,000매", "출처": "건설품셈 공통 1-2-7 4", "변수": [ @@ -128,6 +128,9 @@ "이름": "건설폐기물 발생량 — 건설 공통 1-5-4", "결과단위": "ton", "출처": "건설품셈 공통 1-5-4 2", + "부품": { + "갈래": "계수식" + }, "결과": "Q", "변수": [ {"열쇠": "공사", "기호": "S", "아래": "공사", "위": "", "이름": "공사", "단위": "", "설명": "신축 · 해체", "출처": "건설품셈 공통 1-5-4 2", "연결": {"갈래": "선택", "목록": ["신축", "해체"], "값": "신축"}}, @@ -157,6 +160,9 @@ "이름": "공사용수 — 건설 공통 1-5-6", "결과단위": "㎥", "출처": "건설품셈 공통 1-5-6 3", + "부품": { + "갈래": "계수식" + }, "결과": "Q", "변수": [ {"열쇠": "구분", "기호": "S", "아래": "구분", "위": "", "이름": "구분", "단위": "", "설명": "용수 구분", "출처": "건설품셈 공통 1-5-6 3", "연결": {"갈래": "선택", "목록": ["거푸집씻기", "콘크리트혼합 및 양생", "경량콘크리트혼합 및 양생", "보통벽돌쌓기", "돌쌓기모르타르", "돌씻기", "미장", "타일붙임모르타르", "타일씻기"], "값": "콘크리트혼합 및 양생"}}, diff --git a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json index e6969ee61..5462af5b2 100644 --- a/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json +++ b/resources/master_data/수식문서/수식_건설품셈_08장_건설기계.json @@ -9,6 +9,9 @@ "이름": "굴착기(무한궤도) 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"], "값": "0201-0012"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -55,6 +58,9 @@ "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060"], "값": "1730-0015"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -97,6 +103,9 @@ "이름": "기계 시간당 기계경비(8-1-7 손료 보정)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060", "0102-0015", "0102-0028", "0102-0033", "0121-0013", "0301-0057", "0301-0076", "0301-0095", "0301-0115", "0301-0134", "0301-0153", "0301-0172", "0301-0287", "0602-0060", "0602-0080", "0602-0105"], "값": "1730-0015"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -145,6 +154,9 @@ "이름": "굴착기(무한궤도)+부착용 집게 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7 · 8-4-8 주⑤", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["0201-0060", "0201-0080", "0201-0020", "0201-0040", "0201-0070", "0201-0100"], "값": "0201-0060"}}, {"열쇠": "집게", "기호": "S", "아래": "집게", "위": "", "이름": "집게", "단위": "", "연결": {"갈래": "선택", "목록": ["7206-0070", "7206-0020", "7206-0100"], "값": "7206-0070"}}, @@ -199,6 +211,9 @@ "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7 · 8-4-8 주⑤", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"], "값": "0201-0012"}}, {"열쇠": "브레이커", "기호": "S", "아래": "브레이커", "위": "", "이름": "브레이커", "단위": "", "연결": {"갈래": "선택", "목록": ["0230-0002", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010"], "값": "0230-0002"}}, @@ -253,6 +268,9 @@ "이름": "기계 시간당 기계경비(조종원 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1630-0080", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400", "7505-0100"], "값": "4611-0350"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -345,6 +363,9 @@ "이름": "기계 시간당 기계경비(조종원직종 입력)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["2106-0002", "2106-0003", "2106-0005", "6802-0040", "6802-0060", "6802-0100", "6802-0120", "6802-0135", "6802-0160", "6801-0010", "6803-0100", "6803-0120", "4430-0400", "3302-0038", "3801-0795", "7330-0010", "7954-1119", "7110-0025", "7110-0013", "7120-0746", "7101-0450"], "값": "2106-0002"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -387,6 +408,9 @@ "이름": "기계 시간당 기계경비(연료 없는 기종)", "결과단위": "원/시간", "출처": "건설품셈 공통 8-1-5 · 8-1-6", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["5401-0017"], "값": "5401-0017"}}, {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, @@ -423,6 +447,9 @@ "이름": "시공능력 산정 기본식 Q = n·q·f·E — 건설 공통 8-1-4", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-1-4", + "부품": { + "갈래": "계수식" + }, "결과": "Q", "변수": [ {"열쇠": "싸이클시간", "기호": "cm", "아래": "", "위": "", "이름": "싸이클시간", "단위": "", "설명": "1회 작업싸이클 시간(cm)", "출처": "건설품셈 공통 8-1-4 주⑤", "연결": {"갈래": "입력", "값": 21}}, @@ -451,6 +478,10 @@ "이름": "불도저 시간당 작업량 — 건설 공통 8-2-1", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-1", + "부품": { + "갈래": "작업량 틀", + "기종": ["불도저"] + }, "결과": "Q", "변수": [ {"열쇠": "종별", "기호": "S", "아래": "종별", "위": "", "이름": "종별", "단위": "", "설명": "불도저 종별", "출처": "건설품셈 공통 8-2-1 1 가", "연결": {"갈래": "선택", "목록": ["무한궤도", "타이어"], "값": "무한궤도"}}, @@ -500,42 +531,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001145", - "이름": "불도저 작업량당 기계경비 — 건설 공통 8-2-1", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-1 · 건설품셈 공통 8-1-4", - "변수": [ - {"열쇠": "종별", "기호": "S", "아래": "종별", "위": "", "이름": "종별", "단위": "", "설명": "불도저 종별", "출처": "건설품셈 공통 8-2-1 1 가", "연결": {"갈래": "선택", "목록": ["무한궤도", "타이어"], "값": "무한궤도"}}, - {"열쇠": "급수", "기호": "S", "아래": "급수", "위": "", "이름": "급수", "단위": "", "설명": "불도저 급수(ton)", "출처": "건설품셈 공통 8-2-1 1 가", "연결": {"갈래": "선택", "목록": ["4(초습지)", "7", "10", "12", "13(습지)", "19", "32", "15", "28", "33"], "값": "19"}}, - {"열쇠": "운반거리", "기호": "L", "아래": "", "위": "", "이름": "운반거리", "단위": "m", "설명": "운반거리(L)", "출처": "건설품셈 공통 8-2-1 2", "연결": {"갈래": "입력", "값": 30}}, - {"열쇠": "거리구분", "기호": "S", "아래": "거리구분", "위": "", "이름": "거리구분", "단위": "", "설명": "운반거리계수(e) 표의 거리 칸", "출처": "건설품셈 공통 8-2-1 1 나", "연결": {"갈래": "선택", "목록": ["10이하", "20", "30", "40", "50", "60", "70", "80"], "값": "30"}}, - {"열쇠": "현장조건", "기호": "S", "아래": "현장조건", "위": "", "이름": "현장조건", "단위": "", "설명": "흙 상태", "출처": "건설품셈 공통 8-2-1 1 다", "연결": {"갈래": "선택", "목록": ["자연상태", "흐트러진 상태"], "값": "자연상태"}}, - {"열쇠": "등급", "기호": "S", "아래": "등급", "위": "", "이름": "등급", "단위": "", "설명": "작업 조건 등급", "출처": "건설품셈 공통 8-2-1 1 다", "연결": {"갈래": "선택", "목록": ["양호", "보통", "불량"], "값": "보통"}}, - {"열쇠": "토질명", "기호": "S", "아래": "토질명", "위": "", "이름": "토질명", "단위": "", "설명": "토질", "출처": "건설품셈 공통 8-2-1 1 다", "연결": {"갈래": "선택", "목록": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "값": "모래, 사질토"}}, - {"열쇠": "E보정", "기호": "E", "아래": "보정", "위": "", "이름": "E보정", "단위": "", "설명": "작업효율에서 뺄 경우", "출처": "건설품셈 공통 8-2-1 1 다 주", "연결": {"갈래": "선택", "목록": ["해당 없음", "정지작업을 겸하는 경우", "터파기"], "값": "해당 없음"}}, - {"열쇠": "전진단", "기호": "S", "아래": "전진단", "위": "", "이름": "전진단", "단위": "", "설명": "전진 기어 단", "출처": "건설품셈 공통 8-2-1 2 주", "연결": {"갈래": "선택", "목록": [1, 2, 3, 4], "값": 1}}, - {"열쇠": "후진단", "기호": "S", "아래": "후진단", "위": "", "이름": "후진단", "단위": "", "설명": "후진 기어 단", "출처": "건설품셈 공통 8-2-1 2 주", "연결": {"갈래": "선택", "목록": [1, 2, 3], "값": 1}}, - {"열쇠": "f", "기호": "f", "아래": "", "위": "", "이름": "체적환산계수", "단위": "", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4", "연결": {"갈래": "입력", "값": 1}}, - {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "출처": "건설품셈 공통 8-2-1", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "설명": "불도저 규격", "연결": {"갈래": "선택", "목록": ["0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "0102-0015", "0102-0028", "0102-0033", "0121-0004", "0121-0013"], "값": "0101-0007"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}}, - {"열쇠": "작업량", "기호": "Q", "아래": "", "위": "", "이름": "시간당 작업량", "단위": "㎥/hr", "연결": {"갈래": "단가산출", "키": "GC001144", "입력": {"종별": "S_종별", "급수": "S_급수", "운반거리": "L", "거리구분": "S_거리구분", "현장조건": "S_현장조건", "등급": "S_등급", "토질명": "S_토질명", "E보정": "E_보정", "전진단": "S_전진단", "후진단": "S_후진단", "f": "f", "E입력": "E_입력"}}}, - {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(8-1-7 손료 보정)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000269", "입력": {"기계": "S_기계", "지역": "S_지역", "보정작업": "S_보정작업"}}} - ], - "식": [], - "금액": [ - {"이름": "불도저", "단위": "시간", "비목": "묶음", "수량": "1 ÷ Q", "단가": "P_기계시간당", "출처": "건설품셈 공통 8-1-4"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -543,6 +538,10 @@ "이름": "리퍼(유압식) 시간당 파쇄량 — 건설 공통 8-2-2", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["유압식 리퍼"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "트랙터 규격(ton)", "출처": "건설품셈 공통 8-2-2 1", "연결": {"갈래": "선택", "목록": [20, 30], "값": 20}}, @@ -579,6 +578,11 @@ "이름": "굴착기 시간당 작업량 — 건설 공통 8-2-3", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-3", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "버킷용량" + }, "결과": "Q", "변수": [ {"열쇠": "버킷용량", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "㎥", "설명": "버킷용량(q)", "출처": "건설품셈 공통 8-2-3", "연결": {"갈래": "입력", "값": 1}}, @@ -616,42 +620,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001148", - "이름": "굴착기(무한궤도) 작업량당 기계경비 — 건설 공통 8-2-3", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-3 · 건설품셈 공통 8-1-4", - "변수": [ - {"열쇠": "버킷용량", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "㎥", "설명": "버킷용량(q)", "출처": "건설품셈 공통 8-2-3", "연결": {"갈래": "입력", "값": 1}}, - {"열쇠": "규격구분", "기호": "S", "아래": "규격구분", "위": "", "이름": "규격구분", "단위": "㎥", "설명": "싸이클시간 표의 규격 칸", "출처": "건설품셈 공통 8-2-3 3", "연결": {"갈래": "선택", "목록": ["0.12∼0.4", "0.6∼0.8", "1.0∼1.2", "2.0"], "값": "1.0∼1.2"}}, - {"열쇠": "선회각도", "기호": "θ", "아래": "선회", "위": "", "이름": "선회각도", "단위": "도", "설명": "선회 각도", "출처": "건설품셈 공통 8-2-3 3", "연결": {"갈래": "선택", "목록": [45, 90, 135, 180], "값": 135}}, - {"열쇠": "버킷조건", "기호": "S", "아래": "버킷조건", "위": "", "이름": "버킷조건", "단위": "", "설명": "버킷계수(K) 현장조건", "출처": "건설품셈 공통 8-2-3 1", "연결": {"갈래": "선택", "목록": ["용이하게 굴착할 수 있는 연한 토질로서 버킷에 산적으로 가득찰 때가 많은 조건이 좋은 모래, 보통토인 경우", "위의 토질보다 약간 단단한 토질로서 버킷에 거의 가득 채울 수 있는 모래, 보통토 및 조건이 좋은 점토인 경우", "버킷에 가득 채우기가 어렵거나 가벼운 발파를 필요로 하는 것으로서 단단한 점토질, 점토, 역토질인 경우", "버킷에 넣기 어렵고 불규칙한 공극이 생기는 것으로서 발파 또는 리퍼작업 등에 의하여 얻어진 암과 파쇄암, 호박돌, 역 등인 경우"], "값": "위의 토질보다 약간 단단한 토질로서 버킷에 거의 가득 채울 수 있는 모래, 보통토 및 조건이 좋은 점토인 경우"}}, - {"열쇠": "현장조건", "기호": "S", "아래": "현장조건", "위": "", "이름": "현장조건", "단위": "", "설명": "흙 상태", "출처": "건설품셈 공통 8-2-3 2", "연결": {"갈래": "선택", "목록": ["자연상태", "흐트러진 상태"], "값": "흐트러진 상태"}}, - {"열쇠": "등급", "기호": "S", "아래": "등급", "위": "", "이름": "등급", "단위": "", "설명": "작업 조건 등급", "출처": "건설품셈 공통 8-2-3 2", "연결": {"갈래": "선택", "목록": ["양호", "보통", "불량"], "값": "보통"}}, - {"열쇠": "토질명", "기호": "S", "아래": "토질명", "위": "", "이름": "토질명", "단위": "", "설명": "토질", "출처": "건설품셈 공통 8-2-3 2", "연결": {"갈래": "선택", "목록": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "값": "모래, 사질토"}}, - {"열쇠": "E보정", "기호": "E", "아래": "보정", "위": "", "이름": "E보정", "단위": "", "설명": "작업효율에서 뺄 경우", "출처": "건설품셈 공통 8-2-3 2 주", "연결": {"갈래": "선택", "목록": ["해당 없음", "터파기"], "값": "해당 없음"}}, - {"열쇠": "협소현장", "기호": "S", "아래": "협소현장", "위": "", "이름": "협소현장", "단위": "", "설명": "주택가 상하수도관로부설 등 협소현장(주⑦)", "출처": "건설품셈 공통 8-2-3 2 주⑦", "연결": {"갈래": "선택", "목록": ["해당 없음", "상하수도관로부설 등 협소현장"], "값": "해당 없음"}}, - {"열쇠": "f", "기호": "f", "아래": "", "위": "", "이름": "체적환산계수", "단위": "", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4", "연결": {"갈래": "입력", "값": 0.7}}, - {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "출처": "건설품셈 공통 8-2-3", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, - {"열쇠": "싸이클입력", "기호": "X", "아래": "싸이클", "위": "", "이름": "싸이클입력", "단위": "sec", "설명": "1회 싸이클시간을 직접 넣을 때 — 0 이면 표", "출처": "건설품셈 공통 8-2-3 3", "연결": {"갈래": "입력", "값": 0, "범위": [0, 30]}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "설명": "굴착기(무한궤도) 규격", "연결": {"갈래": "선택", "목록": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"], "값": "0201-0012"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}}, - {"열쇠": "작업량", "기호": "Q", "아래": "", "위": "", "이름": "시간당 작업량", "단위": "㎥/hr", "연결": {"갈래": "단가산출", "키": "GC001147", "입력": {"버킷용량": "q", "규격구분": "S_규격구분", "선회각도": "θ_선회", "버킷조건": "S_버킷조건", "현장조건": "S_현장조건", "등급": "S_등급", "토질명": "S_토질명", "E보정": "E_보정", "협소현장": "S_협소현장", "f": "f", "E입력": "E_입력", "싸이클입력": "X_싸이클"}}}, - {"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도) 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000267", "입력": {"기계": "S_기계", "지역": "S_지역", "보정작업": "S_보정작업"}}} - ], - "식": [], - "금액": [ - {"이름": "굴착기(무한궤도)", "단위": "시간", "비목": "묶음", "수량": "1 ÷ Q", "단가": "P_굴착기무한궤", "출처": "건설품셈 공통 8-1-4"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -659,6 +627,10 @@ "이름": "트랜처 시간당 작업량 — 건설 공통 8-2-4", "결과단위": "m/hr", "출처": "건설품셈 공통 8-2-4", + "부품": { + "갈래": "작업량 틀", + "기종": ["트랜처"] + }, "결과": "Q", "변수": [ {"열쇠": "작업거리", "기호": "L", "아래": "", "위": "", "이름": "작업거리", "단위": "m", "설명": "1열 실작업거리(L · 편도)", "출처": "건설품셈 공통 8-2-4 2", "연결": {"갈래": "입력", "값": 100}}, @@ -701,6 +673,11 @@ "이름": "로더 운전시간당 작업량 — 건설 공통 8-2-5", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-5", + "부품": { + "갈래": "작업량 틀", + "기종": ["로더"], + "규격": "버킷용량" + }, "결과": "Q", "변수": [ {"열쇠": "기종", "기호": "S", "아래": "기종", "위": "", "이름": "기종", "단위": "", "설명": "로더 기종", "출처": "건설품셈 공통 8-2-5 1", "연결": {"갈래": "선택", "목록": ["무한궤도식", "타이어식"], "값": "타이어식"}}, @@ -746,43 +723,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001151", - "이름": "로더 작업량당 기계경비 — 건설 공통 8-2-5", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-5 · 건설품셈 공통 8-1-4", - "변수": [ - {"열쇠": "기종", "기호": "S", "아래": "기종", "위": "", "이름": "기종", "단위": "", "설명": "로더 기종", "출처": "건설품셈 공통 8-2-5 1", "연결": {"갈래": "선택", "목록": ["무한궤도식", "타이어식"], "값": "타이어식"}}, - {"열쇠": "버킷용량", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "㎥", "설명": "버킷용량(q)", "출처": "건설품셈 공통 8-2-5", "연결": {"갈래": "입력", "값": 1.34}}, - {"열쇠": "작업방법", "기호": "S", "아래": "작업방법", "위": "", "이름": "작업방법", "단위": "", "설명": "버킷에 담는 방법", "출처": "건설품셈 공통 8-2-5 1", "연결": {"갈래": "선택", "목록": ["산적상태에서 담을 때", "지면부터 굴착 집토하여 담을 때"], "값": "산적상태에서 담을 때"}}, - {"열쇠": "적재조건", "기호": "S", "아래": "적재조건", "위": "", "이름": "적재조건", "단위": "", "설명": "t1 현장조건", "출처": "건설품셈 공통 8-2-5 1", "연결": {"갈래": "선택", "목록": ["용이한 경우", "보통인 경우", "약간곤란한 경우", "곤란한 경우"], "값": "보통인 경우"}}, - {"열쇠": "K표", "기호": "K", "아래": "표", "위": "", "이름": "K표", "단위": "", "설명": "버킷계수 표 갈래", "출처": "건설품셈 공통 8-2-5 2", "연결": {"갈래": "선택", "목록": ["산적상태 적재", "자연상태·기타 적재"], "값": "산적상태 적재"}}, - {"열쇠": "K조건", "기호": "K", "아래": "조건", "위": "", "이름": "K조건", "단위": "", "설명": "버킷계수 현장조건(고른 표의 줄)", "출처": "건설품셈 공통 8-2-5 2", "연결": {"갈래": "선택", "목록": ["굴착기계로 깎거나 쌓아모은 산적상태에서 적재하는 것으로 굴착력을 필요로 하지 않고 쉽게 버킷에 산적할 수 있는 것, 즉 조건이 좋은 모래, 보통토 등", "흐트러진 산적상태에서 적재하는 것으로 위 상태보다 약간 삽날이 들어가기 어려운 토질로서 버킷에 가득 채울 수 있는 것, 즉 점토, 역질토", "모래, 사력보통토, 점토, 역질토 등 직접 자연상태에서 굴착적재 할 수 있는 여건으로 버킷에 평적에 약간 미달되게 채울 수 있는 것", "버킷에 가득 채울 수 없는 것으로 다른 기계로 쌓아 모아놓은 부순돌 및 점질토나 역질토로서 굳어진 덩어리상태로 되어 있는 것", "버킷에 넣기 어렵고 허술하며 불규칙한 공극이 생긴 것, 예를 들면 발파 또는 리퍼로 깎은 암괴, 호박돌, 역 등"], "값": "굴착기계로 깎거나 쌓아모은 산적상태에서 적재하는 것으로 굴착력을 필요로 하지 않고 쉽게 버킷에 산적할 수 있는 것, 즉 조건이 좋은 모래, 보통토 등"}}, - {"열쇠": "주행거리", "기호": "ℓ", "아래": "", "위": "", "이름": "주행거리", "단위": "m", "설명": "편도주행거리(ℓ) — 0 이면 표준 8m", "출처": "건설품셈 공통 8-2-5", "연결": {"갈래": "입력", "값": 0}}, - {"열쇠": "현장조건", "기호": "S", "아래": "현장조건", "위": "", "이름": "현장조건", "단위": "", "설명": "흙 상태", "출처": "건설품셈 공통 8-2-5 3", "연결": {"갈래": "선택", "목록": ["자연상태", "흐트러진 상태"], "값": "흐트러진 상태"}}, - {"열쇠": "등급", "기호": "S", "아래": "등급", "위": "", "이름": "등급", "단위": "", "설명": "작업 조건 등급", "출처": "건설품셈 공통 8-2-5 3", "연결": {"갈래": "선택", "목록": ["양호", "보통", "불량"], "값": "양호"}}, - {"열쇠": "토질명", "기호": "S", "아래": "토질명", "위": "", "이름": "토질명", "단위": "", "설명": "토질", "출처": "건설품셈 공통 8-2-5 3", "연결": {"갈래": "선택", "목록": ["모래, 사질토", "자갈섞인 흙, 점성토", "파쇄암"], "값": "모래, 사질토"}}, - {"열쇠": "E보정", "기호": "E", "아래": "보정", "위": "", "이름": "E보정", "단위": "", "설명": "작업효율에서 뺄 경우", "출처": "건설품셈 공통 8-2-5 3 주", "연결": {"갈래": "선택", "목록": ["해당 없음", "터파기"], "값": "해당 없음"}}, - {"열쇠": "f", "기호": "f", "아래": "", "위": "", "이름": "체적환산계수", "단위": "", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4", "연결": {"갈래": "입력", "값": 1}}, - {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "출처": "건설품셈 공통 8-2-5", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "설명": "로더 규격", "연결": {"갈래": "선택", "목록": ["0301-0057", "0301-0076", "0301-0095", "0301-0115", "0301-0134", "0301-0153", "0301-0172", "0301-0287", "0302-0057", "0302-0095", "0302-0134"], "값": "0301-0057"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}}, - {"열쇠": "작업량", "기호": "Q", "아래": "", "위": "", "이름": "운전시간당 작업량", "단위": "㎥/hr", "연결": {"갈래": "단가산출", "키": "GC001150", "입력": {"기종": "S_기종", "버킷용량": "q", "작업방법": "S_작업방법", "적재조건": "S_적재조건", "K표": "K_표", "K조건": "K_조건", "주행거리": "ℓ", "현장조건": "S_현장조건", "등급": "S_등급", "토질명": "S_토질명", "E보정": "E_보정", "f": "f", "E입력": "E_입력"}}}, - {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(8-1-7 손료 보정)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000269", "입력": {"기계": "S_기계", "지역": "S_지역", "보정작업": "S_보정작업"}}} - ], - "식": [], - "금액": [ - {"이름": "로더", "단위": "시간", "비목": "묶음", "수량": "1 ÷ Q", "단가": "P_기계시간당", "출처": "건설품셈 공통 8-1-4"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -790,6 +730,10 @@ "이름": "모터 스크레이퍼 시간당 작업량 — 건설 공통 8-2-6", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-6", + "부품": { + "갈래": "작업량 틀", + "기종": ["스크레이퍼"] + }, "결과": "Q", "변수": [ {"열쇠": "적재함용적", "기호": "V", "아래": "적재함", "위": "", "이름": "적재함용적", "단위": "㎥", "설명": "적재함 용적", "출처": "건설품셈 공통 8-2-6", "연결": {"갈래": "입력", "값": 10}}, @@ -834,6 +778,10 @@ "이름": "모터 그레이더 1시간당 작업면적 — 건설 공통 8-2-7", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-7", + "부품": { + "갈래": "작업량 틀", + "기종": ["모터그레이더"] + }, "결과": "A", "변수": [ {"열쇠": "작업거리", "기호": "D", "아래": "", "위": "", "이름": "작업거리", "단위": "m", "설명": "1회의 작업거리(D · 편도)", "출처": "건설품셈 공통 8-2-7", "연결": {"갈래": "입력", "값": 100}}, @@ -874,6 +822,10 @@ "이름": "모터 그레이더 1시간당 작업토량 — 건설 공통 8-2-7", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-7", + "부품": { + "갈래": "작업량 틀", + "기종": ["모터그레이더"] + }, "결과": "A", "변수": [ {"열쇠": "작업거리", "기호": "D", "아래": "", "위": "", "이름": "작업거리", "단위": "m", "설명": "1회의 작업거리(D · 편도)", "출처": "건설품셈 공통 8-2-7", "연결": {"갈래": "입력", "값": 100}}, @@ -917,6 +869,11 @@ "이름": "덤프트럭 1시간당 작업량 — 건설 공통 8-2-8", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-8", + "부품": { + "갈래": "작업량 틀", + "기종": ["덤프트럭"], + "규격": "적재용량" + }, "결과": "Q", "변수": [ {"열쇠": "적재용량", "기호": "T", "아래": "", "위": "", "이름": "적재용량", "단위": "ton", "설명": "덤프트럭의 적재용량(T)", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 15}}, @@ -974,48 +931,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001156", - "이름": "덤프트럭 작업량당 기계경비 — 건설 공통 8-2-8", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-8 · 건설품셈 공통 8-1-4", - "변수": [ - {"열쇠": "적재용량", "기호": "T", "아래": "", "위": "", "이름": "적재용량", "단위": "ton", "설명": "덤프트럭의 적재용량(T)", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 15}}, - {"열쇠": "단위중량", "기호": "W", "아래": "단위", "위": "", "이름": "단위중량", "단위": "ton/㎥", "설명": "자연상태 토석의 단위중량(γt · 습윤밀도)", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 1.8}}, - {"열쇠": "체적변화율", "기호": "L", "아래": "", "위": "", "이름": "체적변화율", "단위": "", "설명": "체적환산계수에서의 체적변화율(L = 흐트러진/자연)", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 1.25}}, - {"열쇠": "f", "기호": "f", "아래": "", "위": "", "이름": "체적환산계수", "단위": "", "설명": "체적환산계수", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 0.7}}, - {"열쇠": "운반거리", "기호": "L", "아래": "운반", "위": "", "이름": "운반거리", "단위": "㎞", "설명": "운반거리(편도)", "출처": "건설품셈 공통 8-2-8 2", "연결": {"갈래": "입력", "값": 0.08}}, - {"열쇠": "도로상태", "기호": "S", "아래": "도로상태", "위": "", "이름": "도로상태", "단위": "", "설명": "운반도로 상태", "출처": "건설품셈 공통 8-2-8 3", "연결": {"갈래": "선택", "목록": ["토취장 또는 토사장 등 열악한 조건의 도로", "교차가 힘든 산간지도로 및 제방 등의 도로", "교차가 가능한 산간지도로 및 제방도로, 미포장도로", "2차로 이상의 공사용도로", "2차로 교통량 및 교통대기가 많은 시가지 포장도로(7,000대/일 이상)", "4차로 이상의 교통량 및 교통대기가 많은 시가지 포장도로(40,000대/일 이상)", "2차로 시가지 포장도로(7,000∼2,000대/일)", "4차로 이상의 시가지 포장도로(40,000대/일 미만)", "2차로 교외 포장도로(2,000대/일 이상)", "4차로 이상의 교외 포장도로(40,000대/일 이상)", "2차로 교외 포장도로(2,000대/일 미만)", "4차로 이상의 교외 포장도로(40,000대/일 미만)", "2차로 고속도로 또는 교통량(편도) 1일 40,000대 이상의 4차로 고속도로", "4차로 고속도로(편도 교통량 1일 40,000대 미만)"], "값": "교차가 힘든 산간지도로 및 제방 등의 도로"}}, - {"열쇠": "적하토질", "기호": "S", "아래": "적하토질", "위": "", "이름": "적하토질", "단위": "", "설명": "적하시간 표의 토질", "출처": "건설품셈 공통 8-2-8 4", "연결": {"갈래": "선택", "목록": ["모래, 역, 호박돌", "점질토, 점토"], "값": "모래, 역, 호박돌"}}, - {"열쇠": "적하조건", "기호": "S", "아래": "적하조건", "위": "", "이름": "적하조건", "단위": "", "설명": "적하 작업조건", "출처": "건설품셈 공통 8-2-8 4", "연결": {"갈래": "선택", "목록": ["양호", "보통", "불량"], "값": "불량"}}, - {"열쇠": "적재장소", "기호": "S", "아래": "적재장소", "위": "", "이름": "적재장소", "단위": "", "설명": "적재장소 진입 조건(t4)", "출처": "건설품셈 공통 8-2-8 5", "연결": {"갈래": "선택", "목록": ["적재장소가 넓어서 트럭이 자유로이 목적장소에 진입할 수 있을 때", "적재장소가 넓지는 않으나 목적장소에 불편없이 진입할 수 있을 때", "적재장소가 좁아서 목적장소에 진입하는데 불편을 느낄 때"], "값": "적재장소가 넓지는 않으나 목적장소에 불편없이 진입할 수 있을 때"}}, - {"열쇠": "덮개", "기호": "S", "아래": "덮개", "위": "", "이름": "덮개", "단위": "", "설명": "적재함 덮개 설치 · 해체(t5)", "출처": "건설품셈 공통 8-2-8 6", "연결": {"갈래": "선택", "목록": ["해당 없음", "인력에 의한 경우", "자동덮개시설의 경우"], "값": "해당 없음"}}, - {"열쇠": "세륜", "기호": "S", "아래": "세륜", "위": "", "이름": "세륜", "단위": "", "설명": "세륜기 통과(t6)", "출처": "건설품셈 공통 8-2-8 7", "연결": {"갈래": "선택", "목록": ["안 함", "함"], "값": "안 함"}}, - {"열쇠": "적재방법", "기호": "S", "아래": "적재방법", "위": "", "이름": "적재방법", "단위": "", "설명": "적재시간(t1) 산출 갈래", "출처": "건설품셈 공통 8-2-8 1 · 8 · 9", "연결": {"갈래": "선택", "목록": ["적재기계", "인력", "직접 입력"], "값": "적재기계"}}, - {"열쇠": "적재싸이클", "기호": "cms", "아래": "", "위": "", "이름": "적재싸이클", "단위": "sec", "설명": "적재기계의 1회 싸이클시간(cms)", "출처": "건설품셈 공통 8-2-8 8", "연결": {"갈래": "입력", "값": 21}}, - {"열쇠": "적재버킷", "기호": "q", "아래": "", "위": "", "이름": "적재버킷", "단위": "㎥", "설명": "적재기계의 버킷용량(q)", "출처": "건설품셈 공통 8-2-8 8", "연결": {"갈래": "입력", "값": 1}}, - {"열쇠": "버킷계수", "기호": "k", "아래": "", "위": "", "이름": "버킷계수", "단위": "", "설명": "적재기계의 버킷계수(k)", "출처": "건설품셈 공통 8-2-8 8", "연결": {"갈래": "입력", "값": 0.9}}, - {"열쇠": "적재효율", "기호": "Es", "아래": "", "위": "", "이름": "적재효율", "단위": "", "설명": "적재기계의 작업효율(Es)", "출처": "건설품셈 공통 8-2-8 8", "연결": {"갈래": "입력", "값": 0.75}}, - {"열쇠": "인력종류", "기호": "S", "아래": "인력종류", "위": "", "이름": "인력종류", "단위": "", "설명": "인력 적재 종류", "출처": "건설품셈 공통 8-2-8 9", "연결": {"갈래": "선택", "목록": ["토사류", "석재류"], "값": "토사류"}}, - {"열쇠": "적재시간입력", "기호": "T", "아래": "적재", "위": "", "이름": "적재시간입력", "단위": "분", "설명": "적재시간(t1)을 직접 넣을 때(적재방법 「직접 입력」)", "출처": "건설품셈 공통 8-2-8 1", "연결": {"갈래": "입력", "값": 0}}, - {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 0.9", "출처": "건설품셈 공통 8-2-8", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "설명": "덤프트럭 규격", "연결": {"갈래": "선택", "목록": ["0602-0025", "0602-0045", "0602-0060", "0602-0105", "0602-0150"], "값": "0602-0025"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "작업량", "기호": "Q", "아래": "", "위": "", "이름": "1시간당 작업량", "단위": "㎥/hr", "연결": {"갈래": "단가산출", "키": "GC001155", "입력": {"적재용량": "T", "단위중량": "W_단위", "체적변화율": "L", "f": "f", "운반거리": "L_운반", "도로상태": "S_도로상태", "적하토질": "S_적하토질", "적하조건": "S_적하조건", "적재장소": "S_적재장소", "덮개": "S_덮개", "세륜": "S_세륜", "적재방법": "S_적재방법", "적재싸이클": "cms", "적재버킷": "q", "버킷계수": "k", "적재효율": "Es", "인력종류": "S_인력종류", "적재시간입력": "T_적재", "E입력": "E_입력"}}}, - {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "S_기계", "지역": "S_지역"}}} - ], - "식": [], - "금액": [ - {"이름": "덤프트럭", "단위": "시간", "비목": "묶음", "수량": "1 ÷ Q", "단가": "P_기계시간당", "출처": "건설품셈 공통 8-1-4"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -1023,6 +938,10 @@ "이름": "롤러 시간당 다짐토량 — 건설 공통 8-2-9", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-9", + "부품": { + "갈래": "작업량 틀", + "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] + }, "결과": "Q", "변수": [ {"열쇠": "다짐기계", "기호": "S", "아래": "다짐기계", "위": "", "이름": "다짐기계", "단위": "", "설명": "다짐기계", "출처": "건설품셈 공통 8-2-9 1", "연결": {"갈래": "선택", "목록": ["머캐덤롤러", "탠덤롤러", "타이어롤러", "불도저", "자주식, 양족식 롤러", "진동롤러(자주식)"], "값": "타이어롤러"}}, @@ -1065,6 +984,10 @@ "이름": "롤러 시간당 다짐면적 — 건설 공통 8-2-9", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-9", + "부품": { + "갈래": "작업량 틀", + "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] + }, "결과": "Q", "변수": [ {"열쇠": "다짐기계", "기호": "S", "아래": "다짐기계", "위": "", "이름": "다짐기계", "단위": "", "설명": "다짐기계", "출처": "건설품셈 공통 8-2-9 1", "연결": {"갈래": "선택", "목록": ["머캐덤롤러", "탠덤롤러", "타이어롤러", "불도저", "자주식, 양족식 롤러", "진동롤러(자주식)"], "값": "타이어롤러"}}, @@ -1098,39 +1021,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001159", - "이름": "롤러 작업량당 기계경비 — 건설 공통 8-2-9", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-9 · 건설품셈 공통 8-1-4", - "변수": [ - {"열쇠": "다짐기계", "기호": "S", "아래": "다짐기계", "위": "", "이름": "다짐기계", "단위": "", "설명": "다짐기계", "출처": "건설품셈 공통 8-2-9 1", "연결": {"갈래": "선택", "목록": ["머캐덤롤러", "탠덤롤러", "타이어롤러", "불도저", "자주식, 양족식 롤러", "진동롤러(자주식)"], "값": "타이어롤러"}}, - {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "다짐기계 규격(ton)", "출처": "건설품셈 공통 8-2-9 1", "연결": {"갈래": "선택", "목록": ["6∼8", "8∼10", "10∼12", "12∼15", "5∼8", "10∼14", "8∼15", "15∼25", "12", "19", "2.5", "4.4", "6.0", "10.0"], "값": "8∼15"}}, - {"열쇠": "다짐층", "기호": "S", "아래": "다짐층", "위": "", "이름": "다짐층", "단위": "", "설명": "표준다짐속도 칸", "출처": "건설품셈 공통 8-2-9 1", "연결": {"갈래": "선택", "목록": ["노체, 축제, 노상", "보조기층, 기층", "표층"], "값": "노체, 축제, 노상"}}, - {"열쇠": "다짐횟수", "기호": "N", "아래": "", "위": "", "이름": "다짐횟수", "단위": "회", "설명": "소요다짐횟수(N)", "출처": "건설품셈 공통 8-2-9 2", "연결": {"갈래": "입력", "값": 4}}, - {"열쇠": "E공종", "기호": "E", "아래": "공종", "위": "", "이름": "E공종", "단위": "", "설명": "작업효율 표의 공종", "출처": "건설품셈 공통 8-2-9 3", "연결": {"갈래": "선택", "목록": ["표층", "기층, 보조기층", "노체, 축제, 노상"], "값": "노체, 축제, 노상"}}, - {"열쇠": "E기계", "기호": "E", "아래": "기계", "위": "", "이름": "E기계", "단위": "", "설명": "작업효율 표의 다짐기계", "출처": "건설품셈 공통 8-2-9 3", "연결": {"갈래": "선택", "목록": ["머캐덤롤러", "타이어롤러", "탠덤롤러", "진동롤러", "불도저", "양족식롤러(자주식)"], "값": "불도저"}}, - {"열쇠": "현장조건", "기호": "S", "아래": "현장조건", "위": "", "이름": "현장조건", "단위": "", "설명": "작업 조건", "출처": "건설품셈 공통 8-2-9 3", "연결": {"갈래": "선택", "목록": ["양호", "보통", "불량"], "값": "보통"}}, - {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 표 값", "출처": "건설품셈 공통 8-2-9", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, - {"열쇠": "두께", "기호": "D", "아래": "", "위": "", "이름": "두께", "단위": "m", "설명": "펴는 흙의 두께(D · 다져진 상태)", "출처": "건설품셈 공통 8-2-9 2 주②", "연결": {"갈래": "입력", "값": 0.3}}, - {"열쇠": "f", "기호": "f", "아래": "", "위": "", "이름": "체적환산계수", "단위": "", "설명": "체적환산계수 — 토질 · 상태 변화율(건설 공통 1-2-3)", "출처": "건설품셈 공통 8-1-4", "연결": {"갈래": "입력", "값": 1}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "설명": "롤러 규격", "연결": {"갈래": "선택", "목록": ["1106-0010", "1106-0012", "1206-0008", "1305-0007", "1306-0025", "1306-0100", "1306-0120", "1406-0008", "1406-0015", "1506-0032"], "값": "1106-0010"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "작업량", "기호": "Q", "아래": "", "위": "", "이름": "시간당 다짐토량", "단위": "㎥/hr", "연결": {"갈래": "단가산출", "키": "GC001157", "입력": {"다짐기계": "S_다짐기계", "규격": "S_규격", "다짐층": "S_다짐층", "다짐횟수": "N", "E공종": "E_공종", "E기계": "E_기계", "현장조건": "S_현장조건", "E입력": "E_입력", "두께": "D", "f": "f"}}}, - {"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "S_기계", "지역": "S_지역"}}} - ], - "식": [], - "금액": [ - {"이름": "롤러", "단위": "시간", "비목": "묶음", "수량": "1 ÷ Q", "단가": "P_기계시간당", "출처": "건설품셈 공통 8-1-4"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -1138,6 +1028,10 @@ "이름": "아스팔트 플랜트 시간당 생산능력 — 건설 공통 8-2-10", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-10", + "부품": { + "갈래": "작업량 틀", + "기종": ["아스팔트 믹싱플랜트"] + }, "결과": "Q", "변수": [ {"열쇠": "플랜트규격", "기호": "S", "아래": "플랜트규격", "위": "", "이름": "플랜트규격", "단위": "", "설명": "플랜트 규격(ton)", "출처": "건설품셈 공통 8-2-10 1", "연결": {"갈래": "선택", "목록": [40, 60, 80, 100, 120], "값": 60}}, @@ -1162,6 +1056,10 @@ "이름": "스테이빌라이저(노상안정기) 시간당 작업량 — 건설 공통 8-2-11", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-11", + "부품": { + "갈래": "작업량 틀", + "기종": ["스테이빌라이저"] + }, "결과": "A", "변수": [ {"열쇠": "로터폭", "기호": "B", "아래": "로터", "위": "", "이름": "로터폭", "단위": "m", "설명": "Rotor 폭", "출처": "건설품셈 공통 8-2-11 1", "연결": {"갈래": "입력", "값": 2.4}}, @@ -1197,6 +1095,10 @@ "이름": "죠 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { + "갈래": "작업량 틀", + "기종": ["죠 크러셔"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "죠 크러셔 규격", "출처": "건설품셈 공통 8-2-12 1 다", "연결": {"갈래": "선택", "목록": ["025040", "025060", "045091", "063101", "106121"], "값": "025060"}}, @@ -1223,6 +1125,10 @@ "이름": "롤 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { + "갈래": "작업량 틀", + "기종": ["롤 크러셔"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "롤 크러셔 규격", "출처": "건설품셈 공통 8-2-12 1 라", "연결": {"갈래": "선택", "목록": ["139076", "101063", "104076", "076076", "076045", "076063", "060040", "040040"], "값": "076063"}}, @@ -1249,6 +1155,10 @@ "이름": "이동식 크러셔 시간당 생산능력 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { + "갈래": "작업량 틀", + "기종": ["크러셔"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "이동식 크러셔 규격(ton)", "출처": "건설품셈 공통 8-2-12 2", "연결": {"갈래": "선택", "목록": [50, 100, 150, 200], "값": 100}}, @@ -1275,6 +1185,10 @@ "이름": "스크린 전체 통과량 — 건설 공통 8-2-12", "결과단위": "ton/hr", "출처": "건설품셈 공통 8-2-12", + "부품": { + "갈래": "작업량 틀", + "기종": ["스크린"] + }, "결과": "Q", "변수": [ {"열쇠": "체규격", "기호": "S", "아래": "체규격", "위": "", "이름": "체규격", "단위": "㎜", "설명": "체의 규격", "출처": "건설품셈 공통 8-2-12 1 마", "연결": {"갈래": "선택", "목록": [2.5, 5, 6, 10, 13, 16, 19, 22, 25, 30, 40, 45, 50, 65, 80, 90, 100], "값": 25}}, @@ -1303,31 +1217,6 @@ "끝전": "비목버림" } }, - { - "꼴": "수식문서", - "판": 1, - "키": "GC001166", - "이름": "대형브레이커 작업량당 기계경비 — 건설 공통 8-2-13", - "결과단위": "원/㎥", - "출처": "건설품셈 공통 8-2-13 2", - "변수": [ - {"열쇠": "작업능력", "기호": "q", "아래": "작업", "위": "", "이름": "작업능력", "단위": "㎥/hr", "설명": "대형브레이커 작업능력(Q · 표 범위 안 값)", "출처": "건설품셈 공통 8-2-13 2", "연결": {"갈래": "입력", "값": 3.3}}, - {"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200"], "값": "0201-0070"}}, - {"열쇠": "브레이커", "기호": "S", "아래": "브레이커", "위": "", "이름": "브레이커", "단위": "", "연결": {"갈래": "선택", "목록": ["0230-0002", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010"], "값": "0230-0007"}}, - {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, - {"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}}, - {"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000271", "입력": {"기계": "S_기계", "브레이커": "S_브레이커", "지역": "S_지역", "보정작업": "S_보정작업"}}} - ], - "식": [], - "금액": [ - {"이름": "굴착기+대형브레이커", "단위": "시간", "비목": "묶음", "수량": "1 ÷ q_작업", "단가": "P_굴착기무한궤", "출처": "건설품셈 공통 8-2-13 2"} - ], - "끝수": { - "줄": {"방법": "버림", "자리": 1}, - "비목": {"방법": "버림", "자리": 0}, - "끝전": "비목버림" - } - }, { "꼴": "수식문서", "판": 1, @@ -1335,6 +1224,10 @@ "이름": "압쇄기(콘크리트 소할용) 시간당 작업량 — 건설 공통 8-2-14", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-14", + "부품": { + "갈래": "작업량 틀", + "기종": ["압쇄기"] + }, "결과": "Q", "변수": [ {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 값", "출처": "건설품셈 공통 8-2-14", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, @@ -1361,6 +1254,10 @@ "이름": "법면다짐기 시간당 작업량 — 건설 공통 8-2-15", "결과단위": "㎡/hr", "출처": "건설품셈 공통 8-2-15", + "부품": { + "갈래": "작업량 틀", + "기종": ["유압식 진동콤팩터"] + }, "결과": "Q", "변수": [ {"열쇠": "구분", "기호": "S", "아래": "구분", "위": "", "이름": "구분", "단위": "", "설명": "장비", "출처": "건설품셈 공통 8-2-15 2", "연결": {"갈래": "선택", "목록": ["유압식 진동콤팩터", "법면다짐판"], "값": "유압식 진동콤팩터"}}, @@ -1384,6 +1281,10 @@ "이름": "골재세척설비 시간당 작업량 — 건설 공통 8-2-16", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-16", + "부품": { + "갈래": "작업량 틀", + "기종": ["골재세척설비"] + }, "결과": "Q", "변수": [ {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "설명": "작업효율을 직접 넣을 때 — 0 이면 원문 값", "출처": "건설품셈 공통 8-2-16", "연결": {"갈래": "입력", "값": 0, "범위": [0, 1]}}, @@ -1410,6 +1311,11 @@ "이름": "콘크리트 믹서 시간당 생산량 — 건설 공통 8-2-17", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-17", + "부품": { + "갈래": "작업량 틀", + "기종": ["콘크리트 믹서"], + "규격": "믹서용량" + }, "결과": "Q", "변수": [ {"열쇠": "믹서용량", "기호": "q", "아래": "", "위": "", "이름": "믹서용량", "단위": "㎥", "설명": "콘크리트 믹서용량(q)", "출처": "건설품셈 공통 8-2-17", "연결": {"갈래": "입력", "값": 0.5}}, @@ -1437,6 +1343,10 @@ "이름": "콘크리트 배치플랜트(강제 혼합식) 시간당 작업량 — 건설 공통 8-2-18", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-18", + "부품": { + "갈래": "작업량 틀", + "기종": ["콘크리트 배치플랜트"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "배치플랜트 규격", "출처": "건설품셈 공통 8-2-18 1", "연결": {"갈래": "선택", "목록": ["60㎥/h (96㎾)", "90㎥/h (144㎾)", "120㎥/h (160㎾)", "150㎥/h (177㎾))", "180㎥/h (213㎾))", "210㎥/h (233㎾))"], "값": "60㎥/h (96㎾)"}}, @@ -1472,6 +1382,11 @@ "이름": "콘크리트 믹서트럭 시간당 운반량 — 건설 공통 8-2-19", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-19", + "부품": { + "갈래": "작업량 틀", + "기종": ["콘크리트 믹서트럭"], + "규격": "적재용량" + }, "결과": "Q", "변수": [ {"열쇠": "적재용량", "기호": "W", "아래": "", "위": "", "이름": "적재용량", "단위": "㎥", "설명": "믹서트럭 적재용량(W)", "출처": "건설품셈 공통 8-2-19 1", "연결": {"갈래": "입력", "값": 6}}, @@ -1527,6 +1442,10 @@ "이름": "콘크리트 덤프트럭 시간당 운반량 — 건설 공통 8-2-19", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-19", + "부품": { + "갈래": "작업량 틀", + "기종": ["덤프트럭"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "덤프트럭 규격(톤)", "출처": "건설품셈 공통 8-2-19 2 가", "연결": {"갈래": "선택", "목록": [8, 10.5, 15], "값": 10.5}}, @@ -1574,6 +1493,10 @@ "이름": "기관차 시간당 작업량 — 건설 공통 8-2-20", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-20", + "부품": { + "갈래": "작업량 틀", + "기종": ["디젤 기관차"] + }, "결과": "Q", "변수": [ {"열쇠": "기관차", "기호": "S", "아래": "기관차", "위": "", "이름": "기관차", "단위": "", "설명": "기관차 규격", "출처": "건설품셈 공통 8-2-20", "연결": {"갈래": "선택", "목록": ["5t", "7t"], "값": "5t"}}, @@ -1616,6 +1539,10 @@ "이름": "경운기 시간당 작업량 — 건설 공통 8-2-21", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-21", + "부품": { + "갈래": "작업량 틀", + "기종": ["경운기"] + }, "결과": "Q", "변수": [ {"열쇠": "적재량", "기호": "q", "아래": "", "위": "", "이름": "적재량", "단위": "㎥", "설명": "흐트러진 상태의 1회 적재량(q)", "출처": "건설품셈 공통 8-2-21", "연결": {"갈래": "입력", "값": 0.5}}, @@ -1651,6 +1578,10 @@ "이름": "디젤 파일 해머 파일 1본당 시공시간 — 강관파일 — 건설 공통 8-2-22", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-22", + "부품": { + "갈래": "작업량 틀", + "기종": ["디젤 파일 해머"] + }, "결과": "Tc", "변수": [ {"열쇠": "토질", "기호": "S", "아래": "토질", "위": "", "이름": "토질", "단위": "", "설명": "토질계수(α) 표의 토질", "출처": "건설품셈 공통 8-2-22 1 가 (1)", "연결": {"갈래": "선택", "목록": ["점토・부식토", "실트・로움・모래", "자갈"], "값": "실트・로움・모래"}}, @@ -1712,6 +1643,10 @@ "이름": "디젤 파일 해머 파일 1본당 시공시간 — 콘크리트 파일(PC · RC) — 건설 공통 8-2-22", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-22", + "부품": { + "갈래": "작업량 틀", + "기종": ["디젤 파일 해머"] + }, "결과": "Tc", "변수": [ {"열쇠": "토질", "기호": "S", "아래": "토질", "위": "", "이름": "토질", "단위": "", "설명": "토질계수(α) 표의 토질", "출처": "건설품셈 공통 8-2-22 1 가 (1)", "연결": {"갈래": "선택", "목록": ["점토・부식토", "실트・로움・모래", "자갈"], "값": "실트・로움・모래"}}, @@ -1762,6 +1697,10 @@ "이름": "유압 파일 해머 파일 1본당 시공시간 — 강관파일 — 건설 공통 8-2-23", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-23", + "부품": { + "갈래": "작업량 틀", + "기종": ["유압 파일 해머"] + }, "결과": "Q", "변수": [ {"열쇠": "평균N", "기호": "X", "아래": "평균N", "위": "", "이름": "평균N", "단위": "", "설명": "타입층의 평균 N치", "출처": "건설품셈 공통 8-2-23 1 가 (1)", "연결": {"갈래": "입력", "값": 15}}, @@ -1795,6 +1734,10 @@ "이름": "유압 파일 해머 파일 1본당 시공시간 — 콘크리트 파일(PC · RC · PHC) — 건설 공통 8-2-23", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-23", + "부품": { + "갈래": "작업량 틀", + "기종": ["유압 파일 해머"] + }, "결과": "Q", "변수": [ {"열쇠": "평균N", "기호": "X", "아래": "평균N", "위": "", "이름": "평균N", "단위": "", "설명": "타입층의 평균 N치", "출처": "건설품셈 공통 8-2-23 1 가 (1)", "연결": {"갈래": "입력", "값": 25}}, @@ -1824,6 +1767,10 @@ "이름": "진동파일 해머 파일 1본당 시공시간 — H파일 — 건설 공통 8-2-24", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-24", + "부품": { + "갈래": "작업량 틀", + "기종": ["진동파일 해머"] + }, "결과": "Q", "변수": [ {"열쇠": "작업", "기호": "S", "아래": "작업", "위": "", "이름": "작업", "단위": "", "설명": "항타 · 항발", "출처": "건설품셈 공통 8-2-24 1", "연결": {"갈래": "선택", "목록": ["항타", "항발"], "값": "항타"}}, @@ -1876,6 +1823,10 @@ "이름": "진동파일 해머 파일 1본당 시공시간 — 강널말뚝 — 건설 공통 8-2-24", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-24", + "부품": { + "갈래": "작업량 틀", + "기종": ["진동파일 해머"] + }, "결과": "Q", "변수": [ {"열쇠": "종류", "기호": "S", "아래": "종류", "위": "", "이름": "종류", "단위": "", "설명": "강널말뚝 종류", "출처": "건설품셈 공통 8-2-24 2 나 (1)", "연결": {"갈래": "선택", "목록": ["II-Type (400×100×10.5)", "III-Type (400×150×13)", "IV-Type (400×170×15.5)"], "값": "III-Type (400×150×13)"}}, @@ -1915,6 +1866,10 @@ "이름": "진동파일해머(워터제트 병용 압입공) 파일 1본당 시공시간 — 건설 공통 8-2-25", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-25", + "부품": { + "갈래": "작업량 틀", + "기종": ["진동파일 해머", "워터젯트"] + }, "결과": "Q", "변수": [ {"열쇠": "근입길이", "기호": "L", "아래": "", "위": "", "이름": "근입길이", "단위": "m", "설명": "근입길이(L)", "출처": "건설품셈 공통 8-2-25 2 가", "연결": {"갈래": "입력", "값": 12}}, @@ -1955,6 +1910,10 @@ "이름": "유압식 압입 인발기 강널말뚝 1본당 시공시간 — 건설 공통 8-2-26", "결과단위": "분/본", "출처": "건설품셈 공통 8-2-26", + "부품": { + "갈래": "작업량 틀", + "기종": ["유압식 압입 인발기"] + }, "결과": "Q", "변수": [ {"열쇠": "작업", "기호": "S", "아래": "작업", "위": "", "이름": "작업", "단위": "", "설명": "압입 · 인발", "출처": "건설품셈 공통 8-2-26 2", "연결": {"갈래": "선택", "목록": ["압입", "인발"], "값": "압입"}}, @@ -1997,7 +1956,7 @@ "꼴": "수식문서", "판": 1, "키": "GC001184", - "이름": "수중펌프 설치 및 해체(1개소당) — 건설 공통 8-2-27", + "이름": "수중펌프 설치 및 해체", "결과단위": "원/개소", "출처": "건설품셈 공통 8-2-27 5", "변수": [ @@ -2024,6 +1983,10 @@ "이름": "터널전단면 굴착기(TBM) 1시간당 작업량 — 건설 공통 8-2-28", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-28", + "부품": { + "갈래": "작업량 틀", + "기종": ["터널전단면 굴착기"] + }, "결과": "Q", "변수": [ {"열쇠": "굴착직경", "기호": "D", "아래": "", "위": "", "이름": "굴착직경", "단위": "m", "설명": "굴착직경(D)", "출처": "건설품셈 공통 8-2-28 1", "연결": {"갈래": "입력", "값": 4.5}}, @@ -2061,6 +2024,10 @@ "이름": "펌프식 준설선 1시간당 준설능력 — 건설 공통 8-2-29", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-29", + "부품": { + "갈래": "작업량 틀", + "기종": ["펌프 준설선"] + }, "결과": "Q", "변수": [ {"열쇠": "토질", "기호": "S", "아래": "토질", "위": "", "이름": "토질", "단위": "", "설명": "토질 분류", "출처": "건설품셈 공통 8-2-29 2", "연결": {"갈래": "선택", "목록": ["점성토", "사질토"], "값": "사질토"}}, @@ -2101,6 +2068,10 @@ "이름": "그래브 준설선 1시간당 준설량 — 건설 공통 8-2-30", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-30", + "부품": { + "갈래": "작업량 틀", + "기종": ["그래브 준설선"] + }, "결과": "Q", "변수": [ {"열쇠": "버킷용량", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "㎥", "설명": "버킷용량(q)", "출처": "건설품셈 공통 8-2-30 3", "연결": {"갈래": "선택", "목록": [0.65, 1.0, 1.5, 3.0, 5.0, 6.0, 7.5, 12.5, 16.0, 25.0], "값": 3.0}}, @@ -2149,6 +2120,10 @@ "이름": "쇄암선(중추식) 시간당 작업능력 — 건설 공통 8-2-31", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-31", + "부품": { + "갈래": "작업량 틀", + "기종": [] + }, "결과": "Q", "변수": [ {"열쇠": "분류", "기호": "S", "아래": "분류", "위": "", "이름": "분류", "단위": "", "설명": "토질분류", "출처": "건설품셈 공통 8-2-31 1", "연결": {"갈래": "선택", "목록": ["자갈섞인 토사", "암반"], "값": "암반"}}, @@ -2179,6 +2154,10 @@ "이름": "이동식 임목파쇄기 시간당 파쇄능력 — 건설 공통 8-2-32", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-32", + "부품": { + "갈래": "작업량 틀", + "기종": ["이동식 임목파쇄기"] + }, "결과": "Q", "변수": [ {"열쇠": "규격", "기호": "S", "아래": "규격", "위": "", "이름": "규격", "단위": "", "설명": "임목파쇄기 규격", "출처": "건설품셈 공통 8-2-32 1 · 2", "연결": {"갈래": "선택", "목록": ["93.25㎾", "354.35㎾", "402.84㎾"], "값": "354.35㎾"}}, @@ -2208,6 +2187,10 @@ "이름": "하천골재채취선 시간당 준설량 — 건설 공통 8-2-33", "결과단위": "㎥/hr", "출처": "건설품셈 공통 8-2-33", + "부품": { + "갈래": "작업량 틀", + "기종": ["하천골재채취선"] + }, "결과": "Q", "변수": [ {"열쇠": "분류", "기호": "S", "아래": "분류", "위": "", "이름": "분류", "단위": "", "설명": "구분", "출처": "건설품셈 공통 8-2-33 2", "연결": {"갈래": "선택", "목록": ["모래질토사", "자갈섞인 모래질토사"], "값": "모래질토사"}}, diff --git a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json index 99414a1f3..4867e2c34 100644 --- a/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json +++ b/resources/master_data/수식문서/수식_건설품셈_13장_플랜트설비공사.json @@ -6505,6 +6505,9 @@ "이름": "아세틸렌량의 환산(ℓ → ㎏)", "결과단위": "㎏", "출처": "건설품셈 기계설비 13-2-7", + "부품": { + "갈래": "계수식" + }, "결과": "Q", "변수": [ {"열쇠": "아세틸렌부피", "기호": "V", "아래": "아세틸렌", "위": "", "이름": "아세틸렌부피", "단위": "ℓ", "설명": "아세틸렌 부피", "출처": "건설품셈 기계설비 13-2-7", "연결": {"갈래": "입력", "값": 1000}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json index a8f8bf098..87f96079f 100644 --- a/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/수식문서/수식_산림품셈_03장_작업장_관리.json @@ -69,6 +69,11 @@ "이름": "임산물 운반로 신설 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 3-4-2 1", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "QF000028", "조건": {"구분": "'q(버킷용량)'"}, "칸": "값"}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json b/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json index d337de903..bed3bc127 100644 --- a/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json +++ b/resources/master_data/수식문서/수식_산림품셈_05장_식재_및_녹화.json @@ -1096,6 +1096,11 @@ "이름": "복사이식 굴착기 시간당 작업량(Q1)", "결과단위": "㎥/hr", "출처": "산림품셈 5-29 주④", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "출처": "산림품셈 5-29 주③ 1.0㎥", "연결": {"갈래": "표", "표": "QF000146", "조건": {"구분": "'적용 굴착기(무한궤도형)'"}, "칸": "값"}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_09장_토공.json b/resources/master_data/수식문서/수식_산림품셈_09장_토공.json index 7afc0da44..3341d3cb1 100644 --- a/resources/master_data/수식문서/수식_산림품셈_09장_토공.json +++ b/resources/master_data/수식문서/수식_산림품셈_09장_토공.json @@ -53,6 +53,11 @@ "이름": "토사깍기(기계 · 굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-3-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "E입력", "기호": "E", "아래": "입력", "위": "", "이름": "E입력", "단위": "", "출처": "산림품셈 9-3-2", "연결": {"갈래": "입력", "값": 0.45, "범위": [0.45, 0.55]}}, @@ -143,6 +148,11 @@ "이름": "암절취 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-4-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000123", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, @@ -280,6 +290,11 @@ "이름": "발파암 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-5-3", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000124", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, @@ -355,6 +370,11 @@ "이름": "콘크리트 깨기 집토(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-7-1 주⑥", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000125", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, @@ -675,6 +695,11 @@ "이름": "측구터파기 토사 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-12-1", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000126", "조건": {"구분": "'굴착기 버킷'"}, "칸": "적용"}}, @@ -1546,6 +1571,11 @@ "이름": "되메우기 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-14-1", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000129", "조건": {"구분": "'굴착기 버킷'"}, "칸": "적용"}}, @@ -1600,6 +1630,10 @@ "이름": "다짐(플레이트콤펙터) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-14-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["플레이트 콤팩터"] + }, "결과": "Q", "변수": [ {"열쇠": "A", "기호": "A", "아래": "", "위": "", "이름": "A", "단위": "", "연결": {"갈래": "표", "표": "QF000289", "조건": {"기호": "'A'"}, "칸": "적용"}}, @@ -1649,6 +1683,11 @@ "이름": "표토제거 답(畓)구간(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎡/hr", "출처": "산림품셈 9-15-1", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "T", "기호": "T", "아래": "", "위": "", "이름": "표토두께", "단위": "m", "출처": "산림품셈 9-15-1 주②", "연결": {"갈래": "입력", "값": 1}}, @@ -1700,6 +1739,10 @@ "이름": "표토제거 답(畓)외구간(불도저 19ton) 시간당 작업량", "결과단위": "㎡/hr", "출처": "산림품셈 9-15-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["불도저"] + }, "결과": "Q", "변수": [ {"열쇠": "T", "기호": "T", "아래": "", "위": "", "이름": "표토두께", "단위": "", "연결": {"갈래": "표", "표": "QF000291", "조건": {"구분": "'T(표토두께)'"}, "칸": "적용"}}, @@ -1755,6 +1798,10 @@ "이름": "노체포설 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-16-1 주②", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"] + }, "결과": "Q", "변수": [ {"열쇠": "q0", "기호": "q", "아래": "0", "위": "", "이름": "q0", "단위": "", "연결": {"갈래": "표", "표": "CF000098", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, @@ -1804,6 +1851,10 @@ "이름": "노체다짐 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-16-2 주②", + "부품": { + "갈래": "작업량 틀", + "기종": ["머캐덤 롤러", "탠덤롤러", "진동롤러", "타이어 롤러", "양족식 롤러"] + }, "결과": "Q", "변수": [ {"열쇠": "V", "기호": "V", "아래": "", "위": "", "이름": "다짐속도", "단위": "㎞/hr", "연결": {"갈래": "표", "표": "CF000099", "조건": {"구분": "'V(다짐속도,km/hr)'"}, "칸": "적용"}}, @@ -1874,6 +1925,10 @@ "이름": "비다짐(정지 · 불도저 19ton) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-17-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["불도저"] + }, "결과": "Q", "변수": [ {"열쇠": "L", "기호": "L", "아래": "", "위": "", "이름": "L", "단위": "", "연결": {"갈래": "표", "표": "QF000292", "조건": {"구분": "'L'"}, "칸": "적용"}}, @@ -1928,6 +1983,11 @@ "이름": "층따기(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-18", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000131", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, @@ -2189,6 +2249,11 @@ "이름": "섞기(굴착기 0.7㎥) 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 9-22", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000132", "조건": {"구분": "'무한궤도 굴착기'"}, "칸": "적용"}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json index 418def676..793a302ac 100644 --- a/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json +++ b/resources/master_data/수식문서/수식_산림품셈_10장_자재․장비_운반.json @@ -9,6 +9,10 @@ "이름": "골재운반 덤프트럭 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 10-3-1 주", + "부품": { + "갈래": "작업량 틀", + "기종": ["덤프트럭"] + }, "결과": "Q", "변수": [ {"열쇠": "qt", "기호": "qt", "아래": "", "위": "", "이름": "덤프트럭 1회 적재량", "단위": "㎥", "연결": {"갈래": "입력", "값": 0}}, @@ -469,6 +473,9 @@ "이름": "4륜구동 1톤 덤프트럭 시간당 기계경비", "결과단위": "원/시간", "출처": "산림품셈 10-9", + "부품": { + "갈래": "기계경비식" + }, "변수": [ {"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}}, {"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "산림품셈 10-9 · 건설품셈 공통 8-1-5 12", "연결": {"갈래": "식"}}, @@ -593,6 +600,10 @@ "이름": "불도저 운반 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 10-11 주②", + "부품": { + "갈래": "작업량 틀", + "기종": ["불도저"] + }, "결과": "Q", "변수": [ {"열쇠": "재료", "기호": "S", "아래": "재료", "위": "", "이름": "재료", "단위": "", "연결": {"갈래": "선택", "목록": ["토사", "파쇄암", "발파암"], "값": "토사"}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_11장_가설.json b/resources/master_data/수식문서/수식_산림품셈_11장_가설.json index 4c98cb0cc..ea80a99d2 100644 --- a/resources/master_data/수식문서/수식_산림품셈_11장_가설.json +++ b/resources/master_data/수식문서/수식_산림품셈_11장_가설.json @@ -91,6 +91,11 @@ "이름": "쇄석·혼합석 부설 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 11-4 주①", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "QF000349", "조건": {"구분": "'q(버킷용량)'"}, "칸": "적용"}}, diff --git a/resources/master_data/수식문서/수식_산림품셈_13장_구조물.json b/resources/master_data/수식문서/수식_산림품셈_13장_구조물.json index 74a591052..666a68e5b 100644 --- a/resources/master_data/수식문서/수식_산림품셈_13장_구조물.json +++ b/resources/master_data/수식문서/수식_산림품셈_13장_구조물.json @@ -1082,6 +1082,11 @@ "이름": "토양개량제 부설 굴착기 시간당 작업량", "결과단위": "㎥/hr", "출처": "산림품셈 13-13-2", + "부품": { + "갈래": "작업량 틀", + "기종": ["굴착기"], + "규격": "q" + }, "결과": "Q", "변수": [ {"열쇠": "q", "기호": "q", "아래": "", "위": "", "이름": "버킷용량", "단위": "", "연결": {"갈래": "표", "표": "CF000158", "조건": {"구분": "'굴착기 버킷'"}, "칸": "적용"}}, diff --git a/resources/tester/fixtures/m01_791_1_logic_fingerprint.json b/resources/tester/fixtures/m01_791_1_logic_fingerprint.json index e0461b712..cb1ff516e 100644 --- a/resources/tester/fixtures/m01_791_1_logic_fingerprint.json +++ b/resources/tester/fixtures/m01_791_1_logic_fingerprint.json @@ -1140,28 +1140,22 @@ "GC001142": "a71c65aaf3eaa7fb", "GC001143": "67408ddb506d5fff", "GC001144": "7b207014a8648623", - "GC001145": "c84f872700bca009", "GC001146": "d0cb3999201a30ac", "GC001147": "a0b23be6a9736a77", - "GC001148": "9adaa6b9f1d8c1d0", "GC001149": "aeed5c7c9e52efef", "GC001150": "1375791d7dd5ce45", - "GC001151": "1890f61af6e6e49f", "GC001152": "6ae99c99ea7e4c9f", "GC001153": "813d46800b48b946", "GC001154": "cbbd35887f079546", "GC001155": "0b6c9c3dc014fc51", - "GC001156": "1e7e8101574b5f87", "GC001157": "19c727d732d3c6c2", "GC001158": "108ec420791670b2", - "GC001159": "f867477915fd53c9", "GC001160": "3ca4cdd646f5e7b1", "GC001161": "0cb1fa1eeda920fb", "GC001162": "c3d278e87f2fded4", "GC001163": "c1216e4fdd0db9e4", "GC001164": "ac5b787938ec3ca2", "GC001165": "798aee41ed132310", - "GC001166": "b6940f94ad685bae", "GC001167": "ab6999dcf7f7dfe6", "GC001168": "29e3a9b048eaf277", "GC001169": "6cda5f96e9b1895b", @@ -1520,5 +1514,5 @@ "GF000336": "a9b9e0bb0f7da1f0", "GX000004": "a3f4c454a5764d6e" }, - "마스터": "e65874951c3ab169" + "마스터": "e6010f1c04492168" } diff --git a/resources/tester/test_713_3_list.py b/resources/tester/test_713_3_list.py index e4cb339ba..9d005bbcc 100644 --- a/resources/tester/test_713_3_list.py +++ b/resources/tester/test_713_3_list.py @@ -11,22 +11,22 @@ COLS = {"묶음", "묶음수", "작업량", "부모", "부르는수", "불리는 def test_목록_줄_수_그대로_덧칸_다_있음() -> None: got = router.get_logics()["logics"] assert ( - len(got) == 1519 - ) # 묶음은 화면 접기만 — 목록 줄은 정본 로직 수 그대로(713-4 1,461 + 782 건설 기초 54 + 794-2b 잔토처리 1 + 794-2a2 셋) + len(got) == 1440 + ) # 묶음은 화면 접기만 — 정본 로직 1,513(713-4 1,461 + 782 건설 기초 54 + 794-2b 1 + 794-2a2 셋 − 813-2 지움 6) − 부품 73(813-2) assert all(COLS <= set(x) for x in got) def test_묶음_작업량_부르기() -> None: more = store_list.extras() - # 한 절에 로직 둘 넘음(조사 12-B ㉯ 323 → 713-4 합침 · 지움 뒤 307 → 782 건설 기초 335 → 794-2b 3-2-1 둘 337) - assert sum(1 for v in more.values() if v["묶음"]) == 337 + # 한 절에 로직 둘 넘음(조사 12-B ㉯ 323 → 713-4 합침 · 지움 뒤 307 → 782 건설 기초 335 → 794-2b 3-2-1 둘 337 → 813-2 작업량당 기계경비 6 지움 328) + assert sum(1 for v in more.values() if v["묶음"]) == 328 work = {k for k, v in more.items() if v["작업량"]} assert len(work) == 65 # 27 − 터파기 작업량 6(713-4 · 단가 로직 안에 품음) + 782 시공능력 44 assert more["GF000098"]["부모"] == ["GF000099"] # 측구터파기 작업량 → 단가 로직 밑 assert more["GC000268"]["불리는수"] >= 300 # 기계경비 로직 assert ( - sum(1 for v in more.values() if v["부르는수"]) == 513 - ) # 506 + 782 건설 기초 6 + 794-2a2 채집 GX 1 + sum(1 for v in more.values() if v["부르는수"]) == 507 + ) # 506 + 782 건설 기초 6 + 794-2a2 채집 GX 1 − 813-2 작업량당 기계경비 6 assert more["GC000294"]["관련"] == ["GC000292"] # 비고 「1-5-6(주③)」 # 인재경 줄 수 = 712-1 묶음 파생 · 못 가른 「하위」 = 복합 하위 로직 부르기 31(조사 12-B ③4) assert sum(v["인재경"]["하위"] for v in more.values()) == 31 diff --git a/resources/tester/test_782_3_capacity_82.py b/resources/tester/test_782_3_capacity_82.py index 6af92cb10..49f4f013d 100644 --- a/resources/tester/test_782_3_capacity_82.py +++ b/resources/tester/test_782_3_capacity_82.py @@ -189,18 +189,7 @@ def test_롤러_임목파쇄_브레이커(whole): assert mean(rows["구조물의 평균두께 30㎝ 미만"]["무근구조물"]) == D("4.6") assert mean(rows["구조물의 평균두께 30㎝ 이상"]["철근구조물"]) == D("2.05") assert mean(rows["구조물의 평균두께 30㎝ 미만"]["철근구조물"]) == D("2.45") - cost = mf.run( - whole, - "GC001166", - { - "작업능력": D("3.6"), - "기계": "0201-0070", - "브레이커": "0230-0007", - "지역": "전국평균", - "보정작업": "해당 없음", - }, - ) - assert cost["줄"][0]["수량"] == 1 / D("3.6") + # 대형브레이커 작업량당 기계경비(GC001166) = 813-2 지움 — 단가산출에 기계 하나 넣어 만듦 def test_기본식_원가로직(whole): @@ -210,10 +199,7 @@ def test_기본식_원가로직(whole): {"싸이클시간": 21, "시간단위": "초", "작업량": D("0.9"), "f": D("0.7"), "E": D("0.75")}, ) assert got["중간"]["n"] == D("171.43") and got["결과"] == D("81.001") - rows = cm.load([LOGIC])[LOGIC]["줄"] - cost = next(r for r in rows if r["키"] == "GC001148") - got = mf.run(whole, "GC001148", cm.시험입력(cost)) - assert got["줄"][0]["수량"] == 1 / D("81.000") + # 굴착기 작업량당 기계경비(GC001148) = 813-2 지움 — 기계 넣기 + 작업량 틀(test_813_2_parts) def test_아세틸렌(whole): @@ -228,7 +214,8 @@ def test_check_master(): new = [t["키"] for t in files[COEF]["표"] if "CC000349" <= t["키"] <= "CC000494"] assert len(new) == 146 logic = [r["키"] for r in files[LOGIC]["줄"] if r["키"] >= "GC001143"] - assert logic == [f"GC00{n}" for n in range(1143, 1191)] + gone = (1145, 1148, 1151, 1156, 1159, 1166) # 813-2 작업량당 기계경비 지움 + assert logic == [f"GC00{n}" for n in range(1143, 1191) if n not in gone] assert files[LOGIC13]["줄"][-1]["키"] == "GC001191" for name in (LOGIC, LOGIC13): for row in files[name]["줄"]: diff --git a/resources/tester/test_791_1_engine_semantics.py b/resources/tester/test_791_1_engine_semantics.py index 52ff16880..5fb632b80 100644 --- a/resources/tester/test_791_1_engine_semantics.py +++ b/resources/tester/test_791_1_engine_semantics.py @@ -71,7 +71,7 @@ def test_M01_로직_전부_전후_같음(): if saved["마스터"] != _master_hash(): pytest.skip("마스터 데이터 바뀜 — 지문 다시 뜸: python resources/tester/test_791_1_…py") now = fingerprints() - assert len(now) == len(saved["결과"]) >= 1515 + assert len(now) == len(saved["결과"]) >= 1513 # 813-2 작업량당 기계경비 6 지움 diff = sorted(k for k in now if now[k] != saved["결과"].get(k)) assert not diff, f"셈 결과 바뀐 로직 {len(diff)} 개: {diff[:10]}" diff --git a/resources/tester/test_796_1_formula_model.py b/resources/tester/test_796_1_formula_model.py index 7bd244406..2b68941da 100644 --- a/resources/tester/test_796_1_formula_model.py +++ b/resources/tester/test_796_1_formula_model.py @@ -1,6 +1,6 @@ """796-1 수식 모델 지킴 — 엔진 로직 ↔ 수식 나무 양방향 · 변수 표 · 추가 · 삭제 · 이름 전파 · 검사. -① 예시 셋(요구서_655 · `_분석_655-B` 2 장) — GC001148 굴착기(블록 GC001147 Q = 81 · GC000267) · +① 예시 셋(요구서_655 · `_분석_655-B` 2 장) — GF000089 토사깍기(블록 GF000088 Q = 39.293 · GC000267 · 813-2 GC001148 지움 뒤) · GC001140 지게(N · M · 42,917) · GC000211 거푸집(시공량 45 · 형틀목공 5/45 · 공구손료 1 %). ② 왕복 — M01 로직 식 전부: 엔진 식 → 나무 → 엔진 식 = 같은 식 나무 · 예시 셋 = 같은 금액. ③ 고침 — 식 고침 = 금액 바뀜 · 변수 더함 · 지움(쓰는 곳 막음) · 이름 · 기호 · 첨자 전파(식 그대로) · @@ -34,7 +34,7 @@ me = importlib.import_module("master_equation_edit") revise = importlib.import_module("M01_MasterData.M01_MasterData_Store_Revise") REAL = store.FOLDER -EXCAVATOR, AFRAME, FORM = "GC001148", "GC001140", "GC000211" +EXCAVATOR, AFRAME, FORM = "GF000089", "GC001140", "GC000211" GIVEN = {AFRAME: {"운반회수": 0}, FORM: {"유형": "보통", "높이감률": 0}, EXCAVATOR: {}} @@ -72,28 +72,28 @@ def edit(key: str, edits: list) -> tuple[dict, list[str]]: # ── ① 예시 셋 ───────────────────────────────────────────────────────── -def test_굴착기_블록_Q_81_노재경(): +def test_굴착기_블록_Q_노재경(): eq = export(EXCAVATOR) block = line(eq, "작업량")["이름식"] - assert block["꼴"] == "블록" and block["열쇠"] == "GC001147" and block["값"] == D("81.000") + assert block["꼴"] == "블록" and block["열쇠"] == "GF000088" and block["값"] == D("39.293") sub = block["나무"] q = line(sub, "결과", "결과") - assert q["결과"]["기호"] == "Q" and q["값"] == D("81.000") - frac = q["이름식"] - assert frac["꼴"] == "분수" and frac["아래"]["열쇠"] == "cm" + assert q["결과"]["기호"] == "Q" and q["값"] == D("39.293") + frac = q["이름식"]["인자"][0] # 반올림(분수, 3) + assert frac["꼴"] == "분수" and frac["아래"]["열쇠"] == "Cm" assert [v["기호"] for v in vars_in(frac["위"])] == ["q", "K", "f", "E"] nums = [x.get("글") for x in vars_in(q["값식"])] or [] shown = json.dumps(q["값식"], default=str, ensure_ascii=False) - assert all(f'"글": "{n}"' in shown for n in ("3,600", "1", "0.9", "0.7", "0.75", "21")) + assert all(f'"글": "{n}"' in shown for n in ("3,600", "0.7", "0.9", "0.77", "0.45", "20")) assert nums == [] # 값 꼴엔 변수 없음 - item = line(eq, "굴착기(무한궤도)", "호표") + item = line(eq, "굴착기(무한궤도) 0.7㎥", "호표") price = item["이름식"]["좌"] assert price["꼴"] == "블록" and price["열쇠"] == "GC000267" assert set(price["묶음"]) == {"노무비", "재료비", "경비", "계"} assert item["이름식"]["우"]["꼴"] == "분수" # 1 / 작업량 cut = mcut.engine(whole(), EXCAVATOR, raw(EXCAVATOR)) assert eq["합계"] == {k: cut[k] for k in mf.BUNDLE} - assert eq["합계"]["계"] == D("641.0") + assert eq["합계"]["계"] == D("2190.0") def test_지게_N_M_42917(): diff --git a/resources/tester/test_796_2c_m02_equation_full.py b/resources/tester/test_796_2c_m02_equation_full.py index 0375c2a8d..3ba10cffa 100644 --- a/resources/tester/test_796_2c_m02_equation_full.py +++ b/resources/tester/test_796_2c_m02_equation_full.py @@ -23,7 +23,12 @@ from resources.tester.test_795_1_sk_copy import _calc, _doc, _imported_row, _sav from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 SUMS = ("노무비", "재료비", "경비", "계") -BASIC = {"종류": "로직", "로직": "GC001148", "수량": 2, "입력값": {}} # 굴착기 시간당 641 +BASIC = { + "종류": "로직", + "로직": "GF000089", + "수량": 2, + "입력값": {}, +} # 토사깍기(기계) — 굴착기 0.7㎥ 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤) MAN = {"종류": "인력", "요소": "LB000002", "이름": "sub2임시 인부", "단위": "인", "수량": 1} FORM = "GC000211" # 합판거푸집 — 형틀목공 · 보통인부 · 중간 시공량 MINE = {"층": "personal", "주인": "7/42"} @@ -91,13 +96,13 @@ def test_A_added_removed_covered_total_equals_sk(made): assert cover["계"] == D("137895") # 품 0.5 × 단가 275,790 # 뺌 — 지운 줄 = 묶음 아님(식 줄 표지로만 · 아래 `out["뺌"]`) assert "labor:LB000002#0" not in rows and "보통인부" not in rows - # 더함 — 기초(굴착기 641 × 2) = 묶음 · 인력 = 직접 묶음 속 더한 줄 · 기초는 수식 나무 + # 더함 — 기초(토사깍기 2,190 × 2) = 묶음 · 인력 = 직접 묶음 속 더한 줄 · 기초는 수식 나무 basic = rows["add:#0"] man = next(x for r in eq["소계"]["줄"] for x in r.get("낱줄", []) if x["상태"] == "더함") assert basic["상태"] == man["상태"] == "더함" and basic["갈래"] == "블록" assert [r["갈래"] for r in eq["소계"]["줄"]] == ["블록", "직접"] assert basic["계"] > 0 and man["계"] > 0 - assert basic["나무"]["열쇠"] == "GC001148" and basic["나무"]["줄"] + assert basic["나무"]["열쇠"] == "GF000089" and basic["나무"]["줄"] # 나무 합계 × 수량 = 행(같은 입력) assert abs(basic["나무"]["합계"]["계"] * 2 - basic["계"]) <= 2 # 고친 칸 표지 — 수식 탭 줄(식 덮개 · 품 · 뺌) diff --git a/resources/tester/test_796_2e_unlink_basis.py b/resources/tester/test_796_2e_unlink_basis.py index bb6ce708d..f5271c07e 100644 --- a/resources/tester/test_796_2e_unlink_basis.py +++ b/resources/tester/test_796_2e_unlink_basis.py @@ -22,7 +22,7 @@ from resources.tester.test_795_1_sk_copy import _calc, _changed, _doc, _fake, _l from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 FORM = "GC000211" # 합판거푸집 -BASIC = "GC001148" # 굴착기 시간당 — 호표 0 = 시간당 기계 × (1 / 작업량) +BASIC = "GF000089" # 토사깍기(기계) · 굴착기 시간당 — 호표 0 = 시간당 기계 × (1 / 작업량) ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub3임시 굴착기"} ONLY = "LB000048" # 굴착기 기초에만 닿는 노임(거푸집엔 안 닿음) DOUBLE = [{"할일": "식", "자리": "호표:0", "식": "3 / 작업량", "까닭": "sub3임시 시험"}] @@ -60,7 +60,7 @@ def test_unlink_keeps_amount_and_freezes_rows(made): assert price == was and _sum(me, doc) == was cut = doc["더한줄"][0]["끊음"] keys = {r["키"] for r in cut["로직"]} - assert BASIC in keys and "GC000267" in keys and "GC001147" in keys # 하위 로직까지 + assert BASIC in keys and "GC000267" in keys and "GF000088" in keys # 하위 로직까지 assert not any(k[:2] in skc.RES for k in keys) # 단가는 SK 사본 한 곳 assert _live().logic(BASIC) == row_before # M01 안 바뀜 with store.at(me): diff --git a/resources/tester/test_799_1_subtotals.py b/resources/tester/test_799_1_subtotals.py index 620694717..c6ab1e17b 100644 --- a/resources/tester/test_799_1_subtotals.py +++ b/resources/tester/test_799_1_subtotals.py @@ -48,15 +48,15 @@ def _closes(eq: dict) -> None: def test_굴착기_블록_한_줄_끝수(): - eq = _export("GC001148") + eq = _export("GF000089") rows = eq["소계"]["줄"] - assert [r["갈래"] for r in rows] == ["블록"] and rows[0]["이름"] == "굴착기(무한궤도)" + assert [r["갈래"] for r in rows] == ["블록"] and rows[0]["이름"] == "굴착기(무한궤도) 0.7㎥" assert (rows[0]["노무비"], rows[0]["재료비"], rows[0]["경비"]) == ( - D("437.1"), - D("88.2"), - D("116.0"), + D("901.2"), + D("665.0"), + D("624.8"), ) - assert eq["합계"]["계"] == D("641.0") and eq["소계"]["끝수"]["계"] == D("-0.3") + assert eq["합계"]["계"] == D("2190.0") and eq["소계"]["끝수"]["계"] == D("-1.0") _closes(eq) diff --git a/resources/tester/test_800_1_var_symbols.py b/resources/tester/test_800_1_var_symbols.py index c193e53f9..04646b351 100644 --- a/resources/tester/test_800_1_var_symbols.py +++ b/resources/tester/test_800_1_var_symbols.py @@ -117,7 +117,7 @@ def _words(tree, out: list) -> None: _words(v, out) -SAMPLE = ("GC000211", "GC001148", "GC001147", "GC001140", "GC001144", "GC000087", "GC000303") +SAMPLE = ("GC000211", "GF000089", "GC001147", "GC001140", "GC001144", "GC000087", "GC000303") def test_수식_내보내기_표본_한글_기호_0(): diff --git a/resources/tester/test_800_2_equation_layout.py b/resources/tester/test_800_2_equation_layout.py index c96badf8d..d92b9bf4b 100644 --- a/resources/tester/test_800_2_equation_layout.py +++ b/resources/tester/test_800_2_equation_layout.py @@ -105,7 +105,7 @@ def test_candidates(): def test_groups_add_up_to_total(): """800-4 — 묶음(블록 · 자원 · 덧줄) 소계 합 + 끝수 = 합계(비목마다) · 한 묶음 소계 = 노 + 재 + 경.""" - for key in ("GC001148", "GC000211"): + for key in ("GF000089", "GC000211"): eqn = eq.read(key, {})["수식"] sub, total = eqn["소계"], eqn["합계"] for k in ("노무비", "재료비", "경비"): diff --git a/resources/tester/test_801_3_groups.py b/resources/tester/test_801_3_groups.py index 16ece1cee..c21bde280 100644 --- a/resources/tester/test_801_3_groups.py +++ b/resources/tester/test_801_3_groups.py @@ -69,10 +69,10 @@ def test_거푸집_한_묶음_굴착기_한_묶음(): eq = _export("GC000211") assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"] assert eq["소계"]["줄"][0]["이름"] == "인력 · 재료" - eq = _export("GC001148") + eq = _export("GF000089") rows = eq["소계"]["줄"] - assert [(r["갈래"], r["이름"]) for r in rows] == [("블록", "굴착기(무한궤도)")] - assert eq["합계"]["계"] == D("641.0") and eq["소계"]["끝수"]["계"] == D("-0.3") + assert [(r["갈래"], r["이름"]) for r in rows] == [("블록", "굴착기(무한궤도) 0.7㎥")] + assert eq["합계"]["계"] == D("2190.0") and eq["소계"]["끝수"]["계"] == D("-1.0") def test_M01_전수_묶음_합_끝수_합계_안쪽_말_0(): diff --git a/resources/tester/test_802_2_convert.py b/resources/tester/test_802_2_convert.py index 3b999d9d7..e4dd16c26 100644 --- a/resources/tester/test_802_2_convert.py +++ b/resources/tester/test_802_2_convert.py @@ -38,7 +38,7 @@ def test_로직_전수_기본_옵션_같음(result): files, _ = m01.loaded() count = sum(len(d.get("줄") or []) for d in files.values() if d.get("그룹") == "로직") info = result["요약"] - assert info["로직"] == count >= 1519 + assert info["로직"] == count >= 1513 # 813-2 지움 6 assert info["기본 같음"] == count and info["기본 다름"] == 0 assert info["변환 알림"] == 0 @@ -82,7 +82,7 @@ def test_다름_목록_까닭(result): def test_저장_같은_것만_다시_셈(result): saved = ml.load_all() assert set(saved) == result["저장"] - assert "GC000100" in saved and len(saved) == result["요약"]["저장"] >= 1519 + assert "GC000100" in saved and len(saved) == result["요약"]["저장"] >= 1513 base = next(x for x in result["대조"] if x["로직"] == "GC000100" and not x["옵션"]) assert ( base["같음"] diff --git a/resources/tester/test_802_4a_m02_doc.py b/resources/tester/test_802_4a_m02_doc.py index 1c86c5261..db29f9e22 100644 --- a/resources/tester/test_802_4a_m02_doc.py +++ b/resources/tester/test_802_4a_m02_doc.py @@ -29,7 +29,7 @@ from resources.tester.test_795_2_sc_copy import _calc as _sc_calc from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 FORM = "GC000211" # 합판거푸집(옵션 많음) -BASIC = "GC001148" # 굴착기 시간당 +BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤) ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub2임시 굴착기"} diff --git a/resources/tester/test_802_4b2_m02_screen.py b/resources/tester/test_802_4b2_m02_screen.py index 32be01347..7de07eff0 100644 --- a/resources/tester/test_802_4b2_m02_screen.py +++ b/resources/tester/test_802_4b2_m02_screen.py @@ -33,7 +33,7 @@ from resources.tester.test_m02_structure import write_sample_table ROOT = Path(__file__).resolve().parents[2] HERE = os.path.dirname(os.path.abspath(__file__)) FORM = "GC000211" # 합판거푸집 — 폼타이 · 측압 · 규격 옵션 -BASIC = "GC001148" # 굴착기 시간당 +BASIC = "GF000089" # 토사깍기(기계) — 굴착기 시간당 × 1 / 작업량(813-2 GC001148 지움 뒤) ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub3임시 굴착기"} @@ -208,7 +208,7 @@ def test_꺼진줄_계산변수_거침(): assert got["새"] == [2] -def test_꺼진줄_전수_1518(): +def test_꺼진줄_전수_1513(): docs = dc.folder() links = dc.ml.Links(dc.skc.whole_(), docs) keys, cases, rows = [], [], [] @@ -218,7 +218,7 @@ def test_꺼진줄_전수_1518(): cases.append({"문서": doc, "값": ans.get("값") or {}}) rows.append(ans.get("줄") or []) got = _off(cases) - assert len(keys) >= 1518 + assert len(keys) >= 1513 # 813-2 지움 6 new = sum(len(g["새"]) for g in got) assert all(set(g["옛"]) <= set(g["새"]) for g in got) and new > sum(len(g["옛"]) for g in got) for key, g, r in zip(keys, got, rows): diff --git a/resources/tester/test_804_2_line_stack.py b/resources/tester/test_804_2_line_stack.py index 2ba1c41b6..405006245 100644 --- a/resources/tester/test_804_2_line_stack.py +++ b/resources/tester/test_804_2_line_stack.py @@ -69,9 +69,9 @@ def _num(v): return None if v is None else float(v) -def test_왕복_전수_1518(world): +def test_왕복_전수_1513(world): docs = world["docs"] - assert len(docs) >= 1518 + assert len(docs) >= 1513 # 813-2 지움 6 got = _ts("왕복", [{"문서": d} for d in docs.values()]) bad = {f: [k for k, g in zip(docs, got) if not g[f]] for f in ("같음", "금액", "기호", "차림")} assert bad == {"같음": [], "금액": [], "기호": [], "차림": []} diff --git a/resources/tester/test_804_7_line_stack_fix.py b/resources/tester/test_804_7_line_stack_fix.py index f43e0e478..787a92f8e 100644 --- a/resources/tester/test_804_7_line_stack_fix.py +++ b/resources/tester/test_804_7_line_stack_fix.py @@ -154,7 +154,7 @@ def test_머리_단추_다섯_저장_한_번() -> None: def test_요소키_기호_이름_0_전수() -> None: docs = doc_screen.docs() - assert len(docs) >= 1518 + assert len(docs) >= 1513 # 813-2 지움 6 bad = [ (k, v.get("기호"), v.get("아래")) for k, d in docs.items() diff --git a/resources/tester/test_812_3_native_layout.py b/resources/tester/test_812_3_native_layout.py index 41868858c..b27cea1e3 100644 --- a/resources/tester/test_812_3_native_layout.py +++ b/resources/tester/test_812_3_native_layout.py @@ -215,7 +215,7 @@ def test_sweep_money_same_and_ts_round_trip() -> None: # ── (c) M01 ────────────────────────────────────────────────────────────── def test_m01_docs_layout_unchanged() -> None: docs = dc.folder() - assert len(docs) >= 1518 + assert len(docs) >= 1513 # 813-2 지움 6 cases = [{"키": k, "문서": d, "파이썬": _items(d)} for k, d in docs.items()] got = _ts(cases) bad = {f: [g["키"] for g in got if not g[f]] for f in ("같음", "거울")} diff --git a/resources/tester/test_813_2_parts.py b/resources/tester/test_813_2_parts.py new file mode 100644 index 000000000..9bc8d93c1 --- /dev/null +++ b/resources/tester/test_813_2_parts.py @@ -0,0 +1,111 @@ +"""813-2 지킴 — 보조 문서 부품 표지 · 작업량당 기계경비 6 지움 · 목록에서 뺌 · 기계 → 부품. + +① 표지 = 보조 문서 83 − 지움 6 − 단가산출로 남김 4 = 73(기계경비식 9 · 작업량 틀 60 · 계수식 4) · 옛 로직 줄 = 수식 문서 +② 지움 6 = 수식 문서 · 옛 로직 · 키대장 「키」 에 없음 · 키대장 「폐기」 에 있음 · 부르는 곳 0 · 이름 꼬리 4 = 원문 제목 +③ 목록(M01 `/logics` · M02 `/logic-lib/logics`)에서 뺌 · 키로 열기 · 부르기는 그대로(끊긴 단가산출 연결 0) +④ 기계 → 부품 — 굴착기 = 기계경비식 GC000267(기계 9) + 작업량 틀 · 0.7㎥ 원문번호 → 틀 문서(규격 열쇠) +⑤ 변환기(옛 로직 → 수식 문서)가 표지 칸을 옮김 +금액 전후 = `test_791_1_engine_semantics.py` 지문(남은 1,513 같음) · `test_802_2_convert.py` 변환 대조. +""" + +from __future__ import annotations + +import importlib +import json +from collections import Counter + +from M01_MasterData import M01_MasterData_Doc as doc_store +from M01_MasterData import M01_MasterData_Parts as parts +from M01_MasterData import M01_MasterData_Router as m01_router +from M01_MasterData import M01_MasterData_Store as store +from M02_MasterTemplete import M02_Logic_Lib_Read as lib_read +from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos + +cv = importlib.import_module("master_doc_convert") + +GONE = ["GC001145", "GC001148", "GC001151", "GC001156", "GC001159", "GC001166"] +NAMES = { + "GC001140": "지게운반", + "GC001141": "인력운반(기계설비)", + "GC001142": "벽돌운반", + "GC001184": "수중펌프 설치 및 해체", +} + + +def _rows() -> dict[str, dict]: + files = store.loaded()[0] + return { + str(r["키"]): r + for d in files.values() + if d.get("그룹") == "로직" + for r in d.get("줄") or [] + } + + +def test_표지_73_갈래별_수_로직_문서_같음(): + tags = parts.parts() + assert Counter(t["갈래"] for t in tags.values()) == { + "기계경비식": 9, + "작업량 틀": 60, + "계수식": 4, + } + rows = _rows() + assert all(rows[k].get("부품") == t for k, t in tags.items()) + assert {k for k, r in rows.items() if r.get("부품")} == set(tags) + tpl = {k: t for k, t in tags.items() if t["갈래"] == "작업량 틀"} + assert all(isinstance(t["기종"], list) for t in tpl.values()) + assert tpl["GF000088"] == {"갈래": "작업량 틀", "기종": ["굴착기"], "규격": "q"} + + +def test_지움_6_이름_4(): + docs = doc_store.docs() + rows = _rows() + led = json.loads((store.FOLDER / "_키대장.json").read_text(encoding="utf-8")) + for k in GONE: + assert k not in docs and k not in rows and k not in led["키"] and k in led["폐기"] + assert len(docs) == len(rows) == 1513 + for k, name in NAMES.items(): + assert docs[k]["이름"] == rows[k]["이름"] == name and "부품" not in docs[k] + + +def test_목록에서_뺌_키로_열림_부르기_그대로(): + tags = parts.parts() + m01 = {x["키"] for x in m01_router.get_logics()["logics"]} + m02 = {x["키"] for x in combos.get_logics()["logics"]} + assert not (m01 | m02) & set(tags) and len(m01) == len(m02) == 1513 - 73 + for k in ("GC000267", "GF000088", "GC001191"): + assert doc_store.read(k)["문서"]["부품"] == tags[k] + assert m01_router.get_logic(k)["logic"]["키"] == k + docs = doc_store.docs() + links = [ + str((v.get("연결") or {}).get("키")) + for d in docs.values() + for v in d.get("변수") or [] + if (v.get("연결") or {}).get("갈래") == "단가산출" + and "{" not in str((v.get("연결") or {}).get("키")) + ] + assert links and all(k in docs for k in links) # 끊긴 부름 0 · 지운 6 부르는 곳 0 + assert sum(1 for k in links if tags.get(k, {}).get("갈래") == "기계경비식") >= 600 + + +def test_기계_부품(): + got = m01_router.get_parts("기계", "굴착기") + rule = {x["키"]: x["기계수"] for x in got["기계경비식"]} + assert rule["GC000267"] == 9 + tpl = {x["키"] for x in got["작업량 틀"]} + assert {"GC001147", "GF000088"} <= tpl and len(tpl) == 15 + coef = m01_router.get_parts("계수식")["계수식"] + assert {x["키"] for x in coef} == {"GC001143", "GC001191", "GC001192", "GC001193"} + one = lib_read.get_machine_parts("0201-0070") + assert one["기계"] == {"이름": "굴착기(무한궤도)", "규격": "0.7㎥", "상세구분": "굴착기"} + by = {t["키"]: t for t in one["틀"]} + assert by["GC001147"]["규격"] == "버킷용량" and by["GF000088"]["문서"]["결과"] == "Q" + + +def test_변환기가_표지를_옮김(): + whole = store.loaded()[1] + rows = _rows() + doc, _ = cv.convert(whole, rows["GF000088"]) + assert doc["부품"] == rows["GF000088"]["부품"] + assert list(doc).index("부품") == list(doc).index("출처") + 1 + assert "부품" not in cv.convert(whole, rows["GF000089"])[0] From 0b91e3780613aa82758dccb5a60202585dc50f7d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 20:50:00 +0900 Subject: [PATCH 29/29] =?UTF-8?q?feat(813-2):=20M01=20=E3=80=8C=EA=B8=B0?= =?UTF-8?q?=EA=B3=84=E3=80=8D=20=C2=B7=20=E3=80=8C=EA=B3=84=EC=88=98?= =?UTF-8?q?=E3=80=8D=20=EC=95=84=EB=9E=98=20=EB=B6=80=ED=92=88=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=C2=B7=20=EC=A4=84=20=EC=8C=93=EA=B8=B0=20[+]=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=E3=80=8C=EC=8B=9D=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M01 기계 › 상세구분(기종) = 「시간당 기계경비 계산식」(그 기종 기계가 쓰는 방식 · 기계 수) · 「작업량 공식 틀」 · 계수 = 「계수식」 넷 - 이름 누름 = 줄 쌓기 읽기 창(고침 없음) · [단가산출 기초에서 열기] = 지금 수식 탭에서 고침(목록 밖 부품 키도 키로 읽어 엶) - 줄 쌓기 [+] 메뉴 「식 — 공식 틀」 → 「식」 - 할 일 4 ② 공식 묶음 [찾아 넣기] 걷기 · ③ 기계 넣기 + 작업량 틀 = 요구 739 · 740 로 뺌(지금 동작 그대로) Co-Authored-By: Claude Opus 5.5 --- A00_Common/line_stack_text.ts | 2 +- M01_MasterData/M01_MasterData_UI_LogicLab.ts | 8 +- M01_MasterData/M01_MasterData_UI_Page.ts | 4 + M01_MasterData/M01_MasterData_UI_Parts.ts | 105 +++++++++++++++++++ M01_MasterData/M01_MasterData_UI_Style.css | 18 ++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_Parts.ts diff --git a/A00_Common/line_stack_text.ts b/A00_Common/line_stack_text.ts index ff8df78b7..7d712d363 100644 --- a/A00_Common/line_stack_text.ts +++ b/A00_Common/line_stack_text.ts @@ -14,7 +14,7 @@ const TEXT = { Add_Item: ["+ 그룹", "+ Group"], Add_Line: ["줄 더함", "Add line"], Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"], - Add_식: ["식 — 공식 틀", "Formula"], + Add_식: ["식", "Formula"], Del_Item: ["그룹 지움", "Delete group"], Del: ["지움", "Delete"], Item: ["그룹", "Group"], diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 319f5f606..7f1f1d0d8 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -571,7 +571,13 @@ export async function mountM01LogicLab( showLoadingOverlay(); const openKeyOn = async (key: string): Promise => { if (!(await leave())) return; - const found = allLogics.find((x) => x.키 === key); + // 목록에 없는 부품 문서(813-2) = 키로 읽어 엶 + const found = + allLogics.find((x) => x.키 === key) ?? + (await fetchLogic(key).then( + (o) => o.logic as unknown as { 구분: string; 키: string }, + () => null, + )); if (found) await open(logicId(found.구분, found.키), found.구분, found.키); else showToast(tx("Load_Failed"), "error"); }; diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 1e093674f..decbba1ce 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -48,6 +48,7 @@ import { } from "./M01_MasterData_UI_Side"; import { loadTable } from "./M01_MasterData_UI_Logic_Note"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; +import { renderParts } from "./M01_MasterData_UI_Parts"; import { renderTables } from "./M01_MasterData_UI_Tables"; import type { LogicHandle } from "./M01_MasterData_UI_LogicLab"; import "./M01_MasterData_UI_Style.css"; @@ -84,6 +85,7 @@ function buildPage(): HTMLElement { attrs: { hidden: "" }, }); const body = el("div", { className: "m01-master__body" }); + const partsHost = el("div"); // 기계 · 계수 표 아래 부품 문서(813-2) // 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각 const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] }); const head = el("div", { @@ -99,6 +101,7 @@ function buildPage(): HTMLElement { notice, el("p", { className: "m01-master__hint", text: L("M01_WideHint") }), body, + partsHost, ], }); @@ -173,6 +176,7 @@ function buildPage(): HTMLElement { : pick.group === "요율" ? renderRateRows(body, pick, query) : renderRows(body, pick, query); + renderParts(partsHost, pick, (key) => openLogicTab(key)); showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []); }; diff --git a/M01_MasterData/M01_MasterData_UI_Parts.ts b/M01_MasterData/M01_MasterData_UI_Parts.ts new file mode 100644 index 000000000..e6b864cc3 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Parts.ts @@ -0,0 +1,105 @@ +/* ============================================================================= + * M01_MasterData_UI_Parts.ts + * M01 「기계」 · 「계수」 표 아래 부품 문서(PLAN 813-2 · 요구 722-3 · 724~732) — 단가산출 기초 목록에서 뺀 보조 문서 자리 + * 기계 · 상세구분(기종) 고름 = 「시간당 기계경비 계산식」(그 기종 기계가 쓰는 방식 · 기계 수) · 「작업량 공식 틀」(그 기종 틀) + * 계수 = 「계수식」(기계 무관 · 돈 아닌 식 넷) + * 이름 누름 = 줄 쌓기 읽기 창(`createStackScreen` · 고침 없음) · [단가산출 기초에서 열기] = 지금 수식 탭 그대로 고침 + * 서버 = `GET /api/m01/parts?group&detail`(`M01_MasterData_Parts.of_kind`) + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { createButton } from "@ui/ui_template_button"; +import { el } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { createText } from "@ui/ui_template_text"; +import { createStackScreen } from "../A00_Common/line_stack_screen"; +import type { Pick } from "./M01_MasterData_UI_Side"; + +interface Part { + 키: string; + 이름: string; + 결과단위: string; + 기계수?: number; +} +type Parts = Partial>; + +const T = { + 기계경비식: ["시간당 기계경비 계산식", "Hourly machine cost formula"], + "작업량 틀": ["작업량 공식 틀", "Output formula templates"], + 계수식: ["계수식", "Coefficient formulas"], + Open: ["단가산출 기초에서 열기", "Open in unit price basis"], + Machines: ["기계", "machines"], + None: ["없음", "None"], + Close: ["닫기", "Close"], +} as const; +const tx = (k: keyof typeof T): string => T[k][currentLanguageIndex === 1 ? 1 : 0]; +const KINDS = ["기계경비식", "작업량 틀", "계수식"] as const; + +/** 표 아래 부품 칸 — 기계(상세구분 있음) · 계수만 · 그 밖 = 비움 */ +export function renderParts(host: HTMLElement, pick: Pick, openLogic: (key: string) => void): void { + host.replaceChildren(); + const group = pick.group === "계수" ? "계수식" : pick.group === "기계" ? "기계" : ""; + if (!group || (group === "기계" && !pick.detail)) return; + let stack: ReturnType | null = null; + /** 줄 쌓기 읽기 창 — 고침 없음 · [단가산출 기초에서 열기] = 수식 탭에서 고침 */ + const show = (p: Part): void => { + stack ??= createStackScreen({ base: "/m01/logic/doc", admin: false, cache: "m01_parts_view" }); + const view = stack; + openModal({ + title: `${p.키} · ${p.이름}`, + closeLabel: tx("Close"), + width: "1100px", + wide: true, + mount: (body, close, actions) => { + actions.append( + createButton({ + label: tx("Open"), + variant: "ghost", + size: 28, + onClick: () => { + close(); + openLogic(p.키); + }, + }), + ); + body.append(view.root); + view.draw(p.키, true); + }, + }); + }; + const q = new URLSearchParams({ group, detail: pick.detail ?? "" }); + const mine = (host.dataset.partsAsk = q.toString()); // 늦게 온 옛 답은 버림 + void fetch(`${API_BASE_URL}/m01/parts?${q}`, { credentials: "include" }) + .then((r): Promise | Parts => (r.ok ? (r.json() as Promise) : {})) + .catch((): Parts => ({})) + .then((got) => { + if (host.dataset.partsAsk !== mine) return; + const blocks = KINDS.filter((k) => k in got).map((k) => { + const list = got[k] ?? []; + const items = list.map((p) => { + const b = createButton({ + label: `${p.이름}${p.기계수 ? ` · ${tx("Machines")} ${p.기계수}` : ""}`, + variant: "ghost", + tone: "link", + size: 28, + onClick: () => show(p), + }); + b.dataset.part = p.키; + return b; + }); + return el("section", { + className: "m01-parts__block", + attrs: { "data-parts": k }, + children: [ + el("h3", { className: "m01-parts__title", text: tx(k) }), + items.length + ? el("div", { className: "m01-parts__list", children: items }) + : createText(tx("None")), + ], + }); + }); + if (blocks.length) + host.replaceChildren(el("div", { className: "m01-parts", children: blocks })); + }); +} diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index 277e16c7f..71f77c7ae 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -170,3 +170,21 @@ table.m01-master__grid { gap: var(--spacing-8); padding: var(--spacing-4) var(--spacing-8); } + +/* 부품 문서(813-2) — 기계 · 계수 표 아래 */ +.m01-parts { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + margin-top: var(--spacing-16); +} +.m01-parts__title { + margin: 0 0 var(--spacing-4); + font-size: var(--text-body); + color: var(--color-text); +} +.m01-parts__list { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4); +}