Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f36953e2 |
@@ -160,3 +160,25 @@
|
|||||||
|
|
||||||
- M2 고침(안): 가름의 잣대 = 금액(노 · 재 · 경 · 계)보다 **구성 줄(손료계수 · 주연료량 · 조종원 — 품셈 값)** — 원파일은 구성 표 · 엑셀은 「기계경비」 시트(산출근거 줄)에 있음 · 가격 · 유가는 값이라 잣대에서 뺌.
|
- M2 고침(안): 가름의 잣대 = 금액(노 · 재 · 경 · 계)보다 **구성 줄(손료계수 · 주연료량 · 조종원 — 품셈 값)** — 원파일은 구성 표 · 엑셀은 「기계경비」 시트(산출근거 줄)에 있음 · 가격 · 유가는 값이라 잣대에서 뺌.
|
||||||
- M1 = PLAN 816-1 로 진행(요구 770-1).
|
- M1 = PLAN 816-1 로 진행(요구 770-1).
|
||||||
|
|
||||||
|
## O. 구글 시트 「수동연결(자재)」 · 「수동연결(기계)」 검토(요구 772 · 시트 = AISLO_이슈관리대장 · 읽기 = 드라이브 커넥터로 xlsx 받아 `tmp/q772_sheet.xlsx`)
|
||||||
|
|
||||||
|
기계 30 줄(사용자 메모 대조):
|
||||||
|
|
||||||
|
| 갈래 | 줄 | 내용 |
|
||||||
|
|---|---|---|
|
||||||
|
| 이미 이어짐(목록이 옛 것 — 815-2 별칭 다섯 합치기 전에 뜬 목록) | 4 | 바이브레이트 → EQ000248 · 버킷준설기 → EQ000425 · 아스팔트페이버(피니셔) 3.0m → EQ000193 · 1.7m → EQ000192 |
|
||||||
|
| 816 에서 처리 중 | 8 | 덤프트럭 1.0Ton · 굴착기 1.0 · 0.6㎥ · 굴삭기(1.0m3) 둘 · 크레인(트럭) 10 · 15Ton · 엔진톱 18" |
|
||||||
|
| 메모 글 검색으로 바로 나옴(별칭만 넣으면 됨) | 3 | 덤프자동덮개 15톤 → EQ000078 덤프트럭 자동덮개시설 15톤용 · 24톤 → EQ000080 · 트럭트랙터및트레일러 20톤 → EQ000183 트럭 트랙터 및 평판트레일러 20ton |
|
||||||
|
| 확인 필요 | 2 | 커터(콘크리트 및 아스팔트용) 320-440mm ↔ EQ000234 320~400(규격 범위 다름) · 크로울러드릴 탑승유압식 ↔ EQ000337 110㎾ / EQ000338 150㎾(가져온 쪽 규격 없음) |
|
||||||
|
| 우리 표 · 현행 품셈에 없음 | 13 | 발전기 5.5 · 7 · 10kW · 브레이카 0.18㎥ · 모노펌프 · 에어 호스 · 파워트로웰 · 콘크리트펌프용 파이프 여섯(EST 는 기계 손료로 둠) |
|
||||||
|
|
||||||
|
- 크레인(트럭) 10 · 15Ton: 사용자 메모 = 트럭탑재형 크레인 · EST 구성 줄 = 크레인(타이어)와 같은 값(2,298 · 136,400 천원 · 3.8ℓ — 트럭탑재형 10ton 은 2,598 · 91,593 천원 · 10.3ℓ) → 사용자 판단 대기.
|
||||||
|
|
||||||
|
자재 100 줄(브레인이 한 줄씩 읽음 · 사용자 메모 칸은 비어 있음):
|
||||||
|
|
||||||
|
- 우리 줄 하나로 또렷(표기만 다름 — 별칭만 넣으면 됨) 65 줄: 2 · 18 · 26 → MT000467 / 5 · 25 → MT000470 / 19 → MT000472 / 15 · 17 → MT000209 / 16 · 50 · 52 · 20 → MT000018 / 21 · 65 → MT000499 / 3 · 7 · 51 · 53 → MT000393 / 10 → MT009202 / 27 · 63 → MT007111 / 28 → MT005677 / 31 → MT005687 / 35 · 68 → MT006544 / 37 → MT002926 / 62 · 85 → MT002861 / 87 → MT002859 / 40 · 95 → MT003202 / 96 → MT003241 / 42 · 54 → MT005749 / 44 · 94 → MT000481 / 47 → MT000487 / 55 · 74 → MT003163 / 73 → MT003165 / 56 → MT003166 / 66 · 84 → MT000453 / 67 → MT007130 / 71 → MT023493 / 72 → MT023491 / 77 → MT023654 / 78 → MT023655 / 75 → MT002536 / 82 → MT006490 / 86 → MT006540 / 89 → MT015883 / 91 → MT015887 / 90 → MT007025 / 92 → MT007032 / 98 → MT003311 / 100 → MT003372 / 57 → MT003398 / 4 → MT006763 / 49 → MT006665 / 48 → MT006667 / 33 · 79 · 81 → MT006664 / 83 → MT006788 (번호 = 시트 행 차례 · 머리 뺀 1 ~ 100)
|
||||||
|
- 사람이 골라야 함(우리 줄 여럿 · 규격이 조금 다름) 31 줄: 1 접착제 · 6 결속선(보통철선 대 어닐링철선) · 13 · 14 · 23 · 24 · 58 · 59 종자 · 32 미송(판재) · 45 철물 · 70 어닐링철선 #6 · 36 PVC용접봉 · 69 용접봉(연강용) · 29 BUTTON BIT · 41 표지판지주 3.5M · 64 강관비계 6m · 93 잔디 · 22 에폭시수지 · 46 각재(말뚝) · 76 보조강재 · 97 공사안내표지판 · 99 윙카호스 · 11 · 12 낙석방지책 · 38 혼합골재 · 61 코어비트 · 30 샹크 · 34 유기질퇴비 · 60 우드칩 · 80 부산물비료 · 88 휠라
|
||||||
|
- 잇지 않음 · 없음 4 줄: 8 파형강관 · 39 이형철근(「별도계상」 = 자리만) · 9 금강사 · 43 떼
|
||||||
|
- 못 이은 까닭 = 정확 일치만 잇는 규칙(이름 낱말 차례 「유로폼(패널)」 ↔ 「유로폼패널」 · 「내수합판1급」 ↔ 「합판(내수)」 · 규격 표기 12mm ↔ 12t · 차례 뒤집힘 T5×200 ↔ 200×5T · 덧글 KSM · 국산 · 지역 꼬리) · 가까운 줄 제안이 틀린 것도 있음(신나 → 신나무 · 중하중그레이팅 → 다른 치수).
|
||||||
|
- 구글 시트 직접 쓰기 토큰 만료(invalid_grant) — 사용자가 데스크탑에 있어 랩탑 로그인 창을 못 누름 · 읽기는 드라이브 커넥터로 됨.
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
"""EST 테스트 앱 사용자 양식 보관함(원본 양식목록 등록·삭제·열기)."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
|
|
||||||
def node(script: str, *args: str):
|
|
||||||
out = subprocess.check_output(["node", "-e", script, *args], cwd=ROOT)
|
|
||||||
return json.loads(out.decode("utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_store_saves_replaces_lists_and_deletes(tmp_path):
|
|
||||||
result = node(
|
|
||||||
"""
|
|
||||||
const f = require('./tools/est-test/forms-store.cjs'), dir = process.argv[1];
|
|
||||||
const errors = [];
|
|
||||||
const t = (fn) => { try { fn(); errors.push('ok'); } catch (e) { errors.push(e.message.slice(0, 6)); } };
|
|
||||||
f.save(dir, { kind: 'wonga', name: '회사 기본', data: [{ Name: 'a' }] });
|
|
||||||
f.save(dir, { kind: 'wonga', name: '회사 기본', data: [{ Name: 'b' }] });
|
|
||||||
f.save(dir, { kind: 'document', name: '표지', form: 1, data: 'x' });
|
|
||||||
const listed = f.read(dir).map((x) => [x.kind, x.name, x.data]);
|
|
||||||
t(() => f.save(dir, { kind: 'bad', name: 'x', data: 1 }));
|
|
||||||
t(() => f.save(dir, { kind: 'wonga', name: ' ', data: 1 }));
|
|
||||||
t(() => f.save(dir, { kind: 'wonga', name: 'x' }));
|
|
||||||
t(() => f.remove(dir, 'wonga', '없는 양식'));
|
|
||||||
f.remove(dir, 'wonga', '회사 기본');
|
|
||||||
process.stdout.write(JSON.stringify({ listed, errors, left: f.read(dir).map((x) => x.name) }));
|
|
||||||
""",
|
|
||||||
str(tmp_path),
|
|
||||||
)
|
|
||||||
assert result["listed"] == [["wonga", "회사 기본", [{"Name": "b"}]], ["document", "표지", "x"]]
|
|
||||||
assert [e[:5] for e in result["errors"]] == ["양식 종류", "양식 이름", "양식 내용", "지울 양식"]
|
|
||||||
assert result["left"] == ["표지"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_template_drops_amounts_and_rejects_bad_rows():
|
|
||||||
result = node(
|
|
||||||
"""
|
|
||||||
const fs = require('fs'), vm = require('vm');
|
|
||||||
const ctx = vm.createContext({ structuredClone });
|
|
||||||
vm.runInContext(fs.readFileSync('tools/est-test/native-forms-library.js', 'utf8'), ctx);
|
|
||||||
const out = vm.runInContext(`(() => {
|
|
||||||
const rows = EstFormsLibrary.strip([{ Gubun: 'N', Name: '직접 노무비', Cost: 100, VSusic: 'N' }]);
|
|
||||||
const errors = [];
|
|
||||||
for (const bad of [[], null, [1]]) { try { EstFormsLibrary.validRows(bad); } catch (e) { errors.push(e.message.slice(0, 9)); } }
|
|
||||||
return { rows, errors, other: EstFormsLibrary.handle('other', {}, null) };
|
|
||||||
})()`, ctx);
|
|
||||||
process.stdout.write(JSON.stringify(out));
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
assert result["rows"] == [{"Gubun": "N", "Name": "직접 노무비", "VSusic": "N"}]
|
|
||||||
assert len(result["errors"]) == 3
|
|
||||||
assert result["other"] is False
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
"""EST 테스트 앱 예정공정표 눈금 단위(원본 1·5·10·15·20일, 1주, 1개월)."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
|
|
||||||
def run_js(code: str):
|
|
||||||
runner = f"""
|
|
||||||
const fs = require('fs'), vm = require('vm');
|
|
||||||
const ctx = vm.createContext({{ structuredClone, console }});
|
|
||||||
vm.runInContext(fs.readFileSync('tools/est-test/native-schedule.js', 'utf8'), ctx);
|
|
||||||
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
|
|
||||||
"""
|
|
||||||
return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_day_week_month_ticks():
|
|
||||||
result = run_js(
|
|
||||||
"""(() => {
|
|
||||||
const t = (s, e, k, n) => EstSchedule.ticks(s, e, k, n);
|
|
||||||
const week = t('2026-01-01', '2026-01-28', '1w');
|
|
||||||
const month = t('2026-01-15', '2026-04-10', '1m');
|
|
||||||
const tenth = t('2026-01-01', '2026-01-30', '10d');
|
|
||||||
const many = t('2026-01-01', '2026-12-31', '1d', 60);
|
|
||||||
let bad = ''; try { t('2026-01-01', '2026-01-02', 'x'); } catch (e) { bad = e.message; }
|
|
||||||
return { week: week.ticks.map((x) => [x.label, Math.round(x.left)]),
|
|
||||||
month: month.ticks.map((x) => x.label), tenth: tenth.ticks.map((x) => x.label),
|
|
||||||
cut: many.cut, count: many.ticks.length, empty: t('', '', '1d').ticks.length, bad };
|
|
||||||
})()"""
|
|
||||||
)
|
|
||||||
assert result["week"] == [["01-01", 0], ["01-08", 25], ["01-15", 50], ["01-22", 75]]
|
|
||||||
assert result["month"] == ["2026-01", "2026-02", "2026-03", "2026-04"]
|
|
||||||
assert result["tenth"] == ["01-01", "01-11", "01-21"]
|
|
||||||
assert result["cut"] is True and result["count"] == 60
|
|
||||||
assert result["empty"] == 0
|
|
||||||
assert result["bad"] == "눈금 단위를 고르세요."
|
|
||||||
@@ -452,11 +452,6 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
|||||||
- [x] 낙찰률 적용 범위(원본 TFRMBIMOGBIUL 내역서낙찰·공종별) — 전체낙찰율/비목별, 전체·선택한 줄·선택한 공종 하위, 낙찰 적용된 줄 제외. 기존 계산(`EstBusiness.bid`)에 행 묶음 지정만 더함. 편집 메뉴에 낙찰률 적용·당초단가 복원.
|
- [x] 낙찰률 적용 범위(원본 TFRMBIMOGBIUL 내역서낙찰·공종별) — 전체낙찰율/비목별, 전체·선택한 줄·선택한 공종 하위, 낙찰 적용된 줄 제외. 기존 계산(`EstBusiness.bid`)에 행 묶음 지정만 더함. 편집 메뉴에 낙찰률 적용·당초단가 복원.
|
||||||
- 검증완료: `test_est_bid.py` 2개(공종 하위만 변경·밖 불변, 적용된 줄 제외·빈 선택·제목 없음·음수 거부), 낙찰 관련 업무·설계변경 시험 포함 41개 통과. ORCA 브라우저에서 「구조물공」 선택 → 전체 80% 공종별 → 하위 4줄만 변경·순공사비 45,293,066 → 42,598,790, 되돌리기 원상.
|
- 검증완료: `test_est_bid.py` 2개(공종 하위만 변경·밖 불변, 적용된 줄 제외·빈 선택·제목 없음·음수 거부), 낙찰 관련 업무·설계변경 시험 포함 41개 통과. ORCA 브라우저에서 「구조물공」 선택 → 전체 80% 공종별 → 하위 4줄만 변경·순공사비 45,293,066 → 42,598,790, 되돌리기 원상.
|
||||||
- 남음: 근거자료낙찰 탭(공종별 낙찰율 목록), 「연금·건강보험료·안전관리비 고정」.
|
- 남음: 근거자료낙찰 탭(공종별 낙찰율 목록), 「연금·건강보험료·안전관리비 고정」.
|
||||||
- [x] 예정공정표 눈금 단위(원본 TFRMSCHEDULE mnu1Day·5Day·10Day·15Day·20Day·1Week·1Month) — 기간 칸 머리에 눈금 날짜, 막대 뒤 눈금선, 60개 초과 시 앞 60개.
|
|
||||||
- 검증완료: `test_est_schedule_scale.py`(1주·1개월·10일 눈금 위치·날짜, 60개 자르기, 빈 기간, 잘못된 단위 거부) 통과. ORCA 브라우저에서 1개월 → 1주 전환 시 머리 날짜 03-01·03-08… 11개, 글자 간격 47px 이상 확인 후 샘플 복구.
|
|
||||||
- [x] 원가계산서 사용자 양식(원본 TFRMWONGABASE popupSaveAs·popupFormats·popupLoadFrom, TFRMFORMATFILELIST 등록·삭제·열기·파일) — 서버 보관함 `data/forms.json`(`forms-store.cjs`, 종류 wonga·document), 등록·목록·열기·삭제·파일 내려받기/불러오기. 금액 제외, 열 때 도급액 고정 해제(저장 금액을 쓰는 옵션이라 금액 없는 양식에서 0이 됨을 확인).
|
|
||||||
- 검증완료: `test_est_forms_library.py` 2개(등록·같은 이름 교체·목록·삭제·잘못된 종류/이름/내용 거부, 금액 제외·잘못된 행 거부·다른 동작 무시) 통과. ORCA 브라우저에서 등록 → 목록 31행 → 원가 표를 3행으로 줄인 뒤 열기 → 31행 복구·도급액 79,603,324(고정 해제) → 삭제·보관함 비움, 샘플 복구.
|
|
||||||
- 남음: 기본양식으로 저장(새 공사 기본값), 갑지·표지 문서 양식.
|
|
||||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||||
- [ ] 남은 핵심(실무 빈도순):
|
- [ ] 남은 핵심(실무 빈도순):
|
||||||
1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기.
|
1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기.
|
||||||
|
|||||||
@@ -22,10 +22,6 @@
|
|||||||
|
|
||||||
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
||||||
|
|
||||||
`원가계산서 → 원가계산서 양식 목록·등록`은 현재 원가계산서의 항목·수식·요율을 이름 붙여 이 앱 `data/forms.json`에 등록하고, 목록에서 열기·삭제하며, 파일로 내려받거나 불러옵니다. 금액은 남기지 않고 열 때 다시 계산하므로 도급액 고정은 풀립니다.
|
|
||||||
|
|
||||||
예정공정표의 `눈금`은 원본처럼 1일·5일·10일·15일·20일·1주·1개월 단위로 기간 칸 위에 날짜를 적고 막대 뒤에 세로 눈금선을 그립니다. 눈금이 60개를 넘으면 앞 60개만 그립니다.
|
|
||||||
|
|
||||||
`편집 → 낙찰률 적용`은 원본 낙찰율 창처럼 전체낙찰율(한 비율) 또는 비목별낙찰율을 고르고, 전체 줄·선택한 줄·선택한 공종(하위 줄 모두)에 적용합니다. 이미 낙찰이 적용된 줄은 제외할 수 있고, `당초단가(100%) 복원`으로 되돌립니다.
|
`편집 → 낙찰률 적용`은 원본 낙찰율 창처럼 전체낙찰율(한 비율) 또는 비목별낙찰율을 고르고, 전체 줄·선택한 줄·선택한 공종(하위 줄 모두)에 적용합니다. 이미 낙찰이 적용된 줄은 제외할 수 있고, `당초단가(100%) 복원`으로 되돌립니다.
|
||||||
|
|
||||||
`파일 → 프로젝트 추가`는 다른 공사 파일(ESTX·웹 JSON)의 순공사비 위 내역 행 전체를 하위 자료와 함께 이 공사의 순공사비 위에 합칩니다. 같은 코드 자료는 이 공사의 것을 그대로 씁니다.
|
`파일 → 프로젝트 추가`는 다른 공사 파일(ESTX·웹 JSON)의 순공사비 위 내역 행 전체를 하위 자료와 함께 이 공사의 순공사비 위에 합칩니다. 같은 코드 자료는 이 공사의 것을 그대로 씁니다.
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
// 원본 양식목록(TFRMFORMATFILELIST: 등록·삭제·열기)의 사용자 양식 보관함 — 이 앱 data/forms.json 한 파일.
|
|
||||||
// 종류(kind)별 이름 목록. 같은 종류·이름이면 바꿔 쓴다.
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
|
|
||||||
const kinds = new Set(["wonga", "document"]);
|
|
||||||
const LIMIT = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
function file(directory) {
|
|
||||||
return path.join(directory, "forms.json");
|
|
||||||
}
|
|
||||||
function read(directory) {
|
|
||||||
const target = file(directory);
|
|
||||||
if (!fs.existsSync(target)) return [];
|
|
||||||
const list = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
||||||
return Array.isArray(list) ? list : [];
|
|
||||||
}
|
|
||||||
function write(directory, list) {
|
|
||||||
fs.mkdirSync(directory, { recursive: true });
|
|
||||||
const target = file(directory),
|
|
||||||
temporary = `${target}.tmp`;
|
|
||||||
fs.writeFileSync(temporary, JSON.stringify(list), "utf8");
|
|
||||||
fs.renameSync(temporary, target);
|
|
||||||
}
|
|
||||||
function check(kind, name) {
|
|
||||||
if (!kinds.has(kind)) throw Error("양식 종류가 올바르지 않습니다.");
|
|
||||||
if (typeof name !== "string" || !name.trim() || name.length > 100)
|
|
||||||
throw Error("양식 이름은 1~100자입니다.");
|
|
||||||
}
|
|
||||||
function save(directory, entry) {
|
|
||||||
check(entry?.kind, entry?.name);
|
|
||||||
if (entry.data == null) throw Error("양식 내용이 없습니다.");
|
|
||||||
if (JSON.stringify(entry.data).length > LIMIT) throw Error("양식은 2MB 이하만 등록합니다.");
|
|
||||||
const list = read(directory).filter((f) => !(f.kind === entry.kind && f.name === entry.name));
|
|
||||||
list.push({
|
|
||||||
kind: entry.kind,
|
|
||||||
name: entry.name.trim(),
|
|
||||||
form: entry.form ?? null,
|
|
||||||
data: entry.data,
|
|
||||||
saved: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
write(directory, list);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
function remove(directory, kind, name) {
|
|
||||||
check(kind, name);
|
|
||||||
const list = read(directory),
|
|
||||||
next = list.filter((f) => !(f.kind === kind && f.name === name));
|
|
||||||
if (next.length === list.length) throw Error("지울 양식이 없습니다.");
|
|
||||||
write(directory, next);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { read, save, remove };
|
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
<script defer src="native-excel-map.js"></script>
|
<script defer src="native-excel-map.js"></script>
|
||||||
<script defer src="native-transfer.js"></script>
|
<script defer src="native-transfer.js"></script>
|
||||||
<script defer src="native-bid.js"></script>
|
<script defer src="native-bid.js"></script>
|
||||||
<script defer src="native-forms-library.js"></script>
|
|
||||||
<script defer src="native-actions.js"></script>
|
<script defer src="native-actions.js"></script>
|
||||||
<script defer src="native-business-actions.js"></script>
|
<script defer src="native-business-actions.js"></script>
|
||||||
<script defer src="native-app.js"></script>
|
<script defer src="native-app.js"></script>
|
||||||
@@ -151,7 +150,6 @@
|
|||||||
><button data-action="template">원가 서식 선택</button
|
><button data-action="template">원가 서식 선택</button
|
||||||
><button data-action="settings">자릿수·도급액 고정</button
|
><button data-action="settings">자릿수·도급액 고정</button
|
||||||
><button data-action="reset-wonga">원가계산서 초기화</button>
|
><button data-action="reset-wonga">원가계산서 초기화</button>
|
||||||
<button data-action="wonga-forms">원가계산서 양식 목록·등록</button>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -605,8 +605,7 @@ const EstBusinessActions = (() => {
|
|||||||
(typeof EstQuantitySheets !== "undefined" && EstQuantitySheets.handle(name, state, node)) ||
|
(typeof EstQuantitySheets !== "undefined" && EstQuantitySheets.handle(name, state, node)) ||
|
||||||
(typeof EstExcelMap !== "undefined" && EstExcelMap.handle(name, state)) ||
|
(typeof EstExcelMap !== "undefined" && EstExcelMap.handle(name, state)) ||
|
||||||
(typeof EstTransfer !== "undefined" && EstTransfer.handle(name, state)) ||
|
(typeof EstTransfer !== "undefined" && EstTransfer.handle(name, state)) ||
|
||||||
(typeof EstBid !== "undefined" && EstBid.handle(name, state)) ||
|
(typeof EstBid !== "undefined" && EstBid.handle(name, state))
|
||||||
(typeof EstFormsLibrary !== "undefined" && EstFormsLibrary.handle(name, state, node))
|
|
||||||
) {
|
) {
|
||||||
EstApp.render();
|
EstApp.render();
|
||||||
} else if (name === "load-contract" || name === "load-bid") {
|
} else if (name === "load-contract" || name === "load-bid") {
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
// 원본 원가계산서 양식(TFRMWONGABASE popupSaveAs·popupFormats·popupLoadFrom)과 양식목록(TFRMFORMATFILELIST)
|
|
||||||
// — 현재 원가 표를 이름 붙여 서버 보관함(data/forms.json)에 등록하고, 목록에서 열기·삭제, 파일로 내려받기·불러오기.
|
|
||||||
const EstFormsLibrary = (() => {
|
|
||||||
// 원가 표 양식으로 남길 필드 — 금액(Cost)은 계산 결과라 남기지 않는다.
|
|
||||||
function strip(rows) {
|
|
||||||
return rows.map((row) => {
|
|
||||||
const { Cost, ...rest } = row;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function validRows(rows) {
|
|
||||||
if (!Array.isArray(rows) || !rows.length) throw Error("원가계산서 양식에 행이 없습니다.");
|
|
||||||
if (rows.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
|
|
||||||
throw Error("원가계산서 양식 행이 올바르지 않습니다.");
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function list() {
|
|
||||||
return (await EstApp.api("/api/forms")).filter((f) => f.kind === "wonga");
|
|
||||||
}
|
|
||||||
async function show(state) {
|
|
||||||
const { esc } = EstApp,
|
|
||||||
forms = await list();
|
|
||||||
EstApp.showDialog(
|
|
||||||
"원가계산서 양식 목록",
|
|
||||||
`<p>현재 원가계산서의 항목·수식·요율을 양식으로 등록하고, 다른 공사에서 열어 씁니다. 금액은 열 때 다시 계산합니다.</p><button data-action="wonga-form-register">현재 원가계산서 등록</button> <button data-action="wonga-form-export">파일로 내려받기</button> <button data-action="wonga-form-import">파일에서 불러오기</button><table class="sheet" style="margin-top:8px"><thead><tr><th>양식 이름</th><th>행</th><th>등록</th><th>작업</th></tr></thead><tbody>${forms.map((f) => `<tr><td>${esc(f.name)}</td><td>${f.data.length}</td><td>${esc(f.saved.slice(0, 10))}</td><td><button data-action="wonga-form-open" data-name="${esc(f.name)}">열기</button><button data-action="wonga-form-delete" data-name="${esc(f.name)}">삭제</button></td></tr>`).join("") || '<tr><td colspan="4">등록된 양식이 없습니다.</td></tr>'}</tbody></table>`,
|
|
||||||
);
|
|
||||||
state.formsLibrary = forms;
|
|
||||||
}
|
|
||||||
function apply(state, rows, name) {
|
|
||||||
validRows(rows);
|
|
||||||
if (!confirm(`현재 원가계산서를 「${name}」 양식으로 바꾸시겠습니까?`)) return;
|
|
||||||
EstApp.byId("dialog").close();
|
|
||||||
// 도급액 고정은 저장된 금액을 그대로 쓰므로 금액 없는 양식에서는 풀어야 다시 계산된다(기초 뼈대와 같음).
|
|
||||||
const fixed = state.project.options.TotalFix;
|
|
||||||
EstApp.change((project) => {
|
|
||||||
project.options.TotalFix = false;
|
|
||||||
project.tables.Wonga = structuredClone(rows);
|
|
||||||
});
|
|
||||||
state.notice = `원가계산서를 「${name}」 양식으로 바꿨습니다.${fixed ? " 도급액 고정은 풀었으니 필요하면 다시 맞추세요." : ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(name, state, node) {
|
|
||||||
if (name === "wonga-forms") await show(state);
|
|
||||||
else if (name === "wonga-form-register") {
|
|
||||||
const title = prompt("양식 이름", state.project.name || "원가계산서");
|
|
||||||
if (!title) return;
|
|
||||||
const data = strip(validRows(state.project.tables.Wonga));
|
|
||||||
await EstApp.api("/api/forms", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ kind: "wonga", name: title, data }),
|
|
||||||
});
|
|
||||||
await show(state);
|
|
||||||
} else if (name === "wonga-form-open") {
|
|
||||||
const form = state.formsLibrary?.find((f) => f.name === node.dataset.name);
|
|
||||||
if (!form) throw Error("양식을 찾을 수 없습니다. 목록을 다시 여세요.");
|
|
||||||
apply(state, form.data, form.name);
|
|
||||||
} else if (name === "wonga-form-delete") {
|
|
||||||
if (!confirm(`「${node.dataset.name}」 양식을 지우시겠습니까?`)) return;
|
|
||||||
await EstApp.api("/api/forms/delete", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ kind: "wonga", name: node.dataset.name }),
|
|
||||||
});
|
|
||||||
await show(state);
|
|
||||||
} else if (name === "wonga-form-export") {
|
|
||||||
const data = strip(validRows(state.project.tables.Wonga));
|
|
||||||
EstApp.download(
|
|
||||||
`${state.project.name || "원가계산서"}.wonga.json`,
|
|
||||||
JSON.stringify({ kind: "est-web-wonga-1", rows: data }, null, 2),
|
|
||||||
);
|
|
||||||
} else if (name === "wonga-form-import") {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "file";
|
|
||||||
input.accept = ".json";
|
|
||||||
input.addEventListener("change", async () => {
|
|
||||||
try {
|
|
||||||
const file = input.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
const parsed = JSON.parse(await file.text());
|
|
||||||
if (parsed?.kind !== "est-web-wonga-1") throw Error("원가계산서 양식 파일이 아닙니다.");
|
|
||||||
apply(state, parsed.rows, file.name);
|
|
||||||
} catch (error) {
|
|
||||||
state.notice = error.message;
|
|
||||||
}
|
|
||||||
EstApp.render();
|
|
||||||
});
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const names = new Set([
|
|
||||||
"wonga-forms",
|
|
||||||
"wonga-form-register",
|
|
||||||
"wonga-form-open",
|
|
||||||
"wonga-form-delete",
|
|
||||||
"wonga-form-export",
|
|
||||||
"wonga-form-import",
|
|
||||||
]);
|
|
||||||
// 동작 사슬은 동기 — 이름만 먼저 가리고 서버 요청은 뒤에서 돌린다.
|
|
||||||
function handle(name, state, node) {
|
|
||||||
if (!names.has(name)) return false;
|
|
||||||
run(name, state, node).catch((error) => {
|
|
||||||
state.notice = error.message;
|
|
||||||
EstApp.render();
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { strip, validRows, handle };
|
|
||||||
})();
|
|
||||||
@@ -152,55 +152,12 @@ const EstSchedule = (() => {
|
|||||||
function clearSchedule(project) {
|
function clearSchedule(project) {
|
||||||
project.schedule = [];
|
project.schedule = [];
|
||||||
}
|
}
|
||||||
// 원본 예정공정표 눈금(mnu1Day·mnu5Day·mnu10Day·mnu15Day·mnu20Day·mnu1Week·mnu1Month).
|
|
||||||
const scales = [
|
|
||||||
["1d", "1일", 1],
|
|
||||||
["5d", "5일", 5],
|
|
||||||
["10d", "10일", 10],
|
|
||||||
["15d", "15일", 15],
|
|
||||||
["20d", "20일", 20],
|
|
||||||
["1w", "1주", 7],
|
|
||||||
["1m", "1개월", 0],
|
|
||||||
];
|
|
||||||
function ticks(start, end, scale, limit = 60) {
|
|
||||||
if (!start || !end) return { ticks: [], cut: false };
|
|
||||||
const first = day(start),
|
|
||||||
span = day(end) - first + 1,
|
|
||||||
step = scales.find(([key]) => key === scale)?.[2];
|
|
||||||
if (step == null) throw Error("눈금 단위를 고르세요.");
|
|
||||||
const out = [];
|
|
||||||
if (step) for (let d = first; d < first + span; d += step) out.push(d);
|
|
||||||
else {
|
|
||||||
const date = new Date(first * 86400000);
|
|
||||||
for (let y = date.getUTCFullYear(), m = date.getUTCMonth(); ; m++) {
|
|
||||||
const t = Date.UTC(y, m, 1) / 86400000;
|
|
||||||
if (t >= first + span) break;
|
|
||||||
out.push(Math.max(t, first));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cut = out.length > limit;
|
|
||||||
return {
|
|
||||||
cut,
|
|
||||||
ticks: out.slice(0, limit).map((d) => ({
|
|
||||||
left: ((d - first) / span) * 100,
|
|
||||||
label: new Date(d * 86400000).toISOString().slice(step ? 5 : 0, step ? 10 : 7),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function render() {
|
function render() {
|
||||||
const { esc, number, state } = EstApp,
|
const { esc, number, state } = EstApp,
|
||||||
output = calculate(state.project);
|
output = calculate(state.project);
|
||||||
state.scheduleScale ||= "1m";
|
|
||||||
const grid = ticks(output.start, output.end, state.scheduleScale),
|
|
||||||
lines = grid.ticks
|
|
||||||
.map((t) => `linear-gradient(#999,#999) ${t.left}% 0/1px 100% no-repeat`)
|
|
||||||
.join(","),
|
|
||||||
width = Math.max(180, grid.ticks.length * 44),
|
|
||||||
header = `<div style="position:relative;height:14px;font-size:10px;min-width:${width}px">${grid.ticks.map((t) => `<span style="position:absolute;left:${t.left}%">${esc(t.label)}</span>`).join("")}</div>`,
|
|
||||||
scaleSelect = `<label>눈금 <select data-schedule-scale>${scales.map(([key, label]) => `<option value="${key}" ${key === state.scheduleScale ? "selected" : ""}>${label}</option>`).join("")}</select></label>${grid.cut ? "<span>눈금이 많아 앞 60개만 표시</span>" : ""}`;
|
|
||||||
const rowsCount = output.rows.length;
|
const rowsCount = output.rows.length;
|
||||||
EstApp.byId("work-content").innerHTML =
|
EstApp.byId("work-content").innerHTML =
|
||||||
`<div class="business-tools"><button data-action="schedule-add">공정 추가</button><button data-action="schedule-import">내역 일괄 추가</button><button data-action="schedule-weights">내역 금액으로 가중치 갱신</button><button data-action="schedule-progress">최근 확정 기성으로 진도 갱신</button><button data-action="schedule-clear" ${rowsCount === 0 ? "disabled" : ""}>공정 전체 비우기</button>${scaleSelect}<span>시작 ${esc(output.start)} · 종료 ${esc(output.end)} · 가중치 합계 ${number(output.weight)} · 입력 진도 가중평균 ${number(output.progress)}%</span></div><p>기간과 진도는 직접 입력합니다. 가중치는 공종 금액 또는 사용자가 정한 값을 사용합니다.</p><table class="sheet"><thead><tr>${["코드", "공정명", "시작일", "종료일", "가중치", "진도(%)", "기간", "작업"].map((h) => `<th>${h === "기간" ? `기간${header}` : h}</th>`).join("")}</tr></thead><tbody>${output.rows.map((r, i) => `<tr>${["Code", "Name", "start", "end", "weight", "progress"].map((k) => `<td><input data-schedule="${k}" data-index="${i}" type="${["start", "end"].includes(k) ? "date" : ["weight", "progress"].includes(k) ? "number" : "text"}" value="${esc(r[k])}" step="any"></td>`).join("")}<td style="min-width:${width}px"><div style="height:12px;background:${lines ? `${lines},` : ""}#ddd"><div style="margin-left:${r.left}%;width:${r.width}%;height:12px;background:#316ea5"></div></div></td><td><button data-action="schedule-move" data-index="${i}" data-direction="-1" ${i === 0 ? "disabled" : ""}>↑</button><button data-action="schedule-move" data-index="${i}" data-direction="1" ${i === rowsCount - 1 ? "disabled" : ""}>↓</button><button data-action="schedule-delete" data-index="${i}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
`<div class="business-tools"><button data-action="schedule-add">공정 추가</button><button data-action="schedule-import">내역 일괄 추가</button><button data-action="schedule-weights">내역 금액으로 가중치 갱신</button><button data-action="schedule-progress">최근 확정 기성으로 진도 갱신</button><button data-action="schedule-clear" ${rowsCount === 0 ? "disabled" : ""}>공정 전체 비우기</button><span>시작 ${esc(output.start)} · 종료 ${esc(output.end)} · 가중치 합계 ${number(output.weight)} · 입력 진도 가중평균 ${number(output.progress)}%</span></div><p>기간과 진도는 직접 입력합니다. 가중치는 공종 금액 또는 사용자가 정한 값을 사용합니다.</p><table class="sheet"><thead><tr>${["코드", "공정명", "시작일", "종료일", "가중치", "진도(%)", "기간", "작업"].map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${output.rows.map((r, i) => `<tr>${["Code", "Name", "start", "end", "weight", "progress"].map((k) => `<td><input data-schedule="${k}" data-index="${i}" type="${["start", "end"].includes(k) ? "date" : ["weight", "progress"].includes(k) ? "number" : "text"}" value="${esc(r[k])}" step="any"></td>`).join("")}<td style="min-width:180px"><div style="height:12px;background:#ddd"><div style="margin-left:${r.left}%;width:${r.width}%;height:12px;background:#316ea5"></div></div></td><td><button data-action="schedule-move" data-index="${i}" data-direction="-1" ${i === 0 ? "disabled" : ""}>↑</button><button data-action="schedule-move" data-index="${i}" data-direction="1" ${i === rowsCount - 1 ? "disabled" : ""}>↓</button><button data-action="schedule-delete" data-index="${i}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||||
}
|
}
|
||||||
function install() {
|
function install() {
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
@@ -256,11 +213,6 @@ const EstSchedule = (() => {
|
|||||||
});
|
});
|
||||||
document.addEventListener("change", (event) => {
|
document.addEventListener("change", (event) => {
|
||||||
const node = event.target;
|
const node = event.target;
|
||||||
if (node.dataset.scheduleScale !== undefined) {
|
|
||||||
EstApp.state.scheduleScale = node.value;
|
|
||||||
EstApp.render();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!node.dataset.schedule) return;
|
if (!node.dataset.schedule) return;
|
||||||
const p = structuredClone(EstApp.state.project),
|
const p = structuredClone(EstApp.state.project),
|
||||||
row = p.schedule[Number(node.dataset.index)],
|
row = p.schedule[Number(node.dataset.index)],
|
||||||
@@ -279,7 +231,6 @@ const EstSchedule = (() => {
|
|||||||
return {
|
return {
|
||||||
validate,
|
validate,
|
||||||
calculate,
|
calculate,
|
||||||
ticks,
|
|
||||||
fromEstimate,
|
fromEstimate,
|
||||||
refreshWeights,
|
refreshWeights,
|
||||||
refreshProgress,
|
refreshProgress,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const exchange = require("./exchange.cjs");
|
|||||||
const { exportEstx } = require("./export-estx.cjs");
|
const { exportEstx } = require("./export-estx.cjs");
|
||||||
const jdl = require("./jdl.cjs");
|
const jdl = require("./jdl.cjs");
|
||||||
const { inspectAccom } = require("./inspect-accom.cjs");
|
const { inspectAccom } = require("./inspect-accom.cjs");
|
||||||
const forms = require("./forms-store.cjs");
|
|
||||||
const base = __dirname,
|
const base = __dirname,
|
||||||
port = Number(process.env.EST_TEST_PORT || 5175),
|
port = Number(process.env.EST_TEST_PORT || 5175),
|
||||||
host = process.env.EST_TEST_HOST || "100.65.141.15";
|
host = process.env.EST_TEST_HOST || "100.65.141.15";
|
||||||
@@ -65,7 +64,6 @@ const allowed = new Set([
|
|||||||
"native-excel-map.js",
|
"native-excel-map.js",
|
||||||
"native-transfer.js",
|
"native-transfer.js",
|
||||||
"native-bid.js",
|
"native-bid.js",
|
||||||
"native-forms-library.js",
|
|
||||||
"native-issues.js",
|
"native-issues.js",
|
||||||
"native-attachments.js",
|
"native-attachments.js",
|
||||||
"native-standard.js",
|
"native-standard.js",
|
||||||
@@ -154,16 +152,6 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (url.pathname === "/api/forms" && ["GET", "POST"].includes(req.method)) {
|
|
||||||
const input = req.method === "POST" ? JSON.parse((await body(req)).toString("utf8")) : null;
|
|
||||||
reply(res, 200, input ? forms.save(dataDirectory, input) : forms.read(dataDirectory));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (req.method === "POST" && url.pathname === "/api/forms/delete") {
|
|
||||||
const input = JSON.parse((await body(req)).toString("utf8"));
|
|
||||||
reply(res, 200, forms.remove(dataDirectory, input.kind, input.name));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (req.method === "GET" && url.pathname === "/api/projects") {
|
if (req.method === "GET" && url.pathname === "/api/projects") {
|
||||||
reply(res, 200, list());
|
reply(res, 200, list());
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user