Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f51865c57b | ||
|
|
c9e1cc6eaa | ||
|
|
7182b6cbb4 | ||
|
|
bff1502ecf | ||
|
|
1fda705b75 | ||
|
|
c7acfccd51 | ||
|
|
7c5f129712 | ||
|
|
3bd2340609 | ||
|
|
d22c5008f0 | ||
|
|
54c855ab1a | ||
|
|
2668f17bf2 | ||
|
|
9d357de4d1 | ||
|
|
364b329c54 | ||
|
|
87b445965b | ||
|
|
a0b0929e13 | ||
|
|
5fec2743cb | ||
|
|
3d0d484524 | ||
|
|
2b26b7c095 | ||
|
|
fb7b4b021b | ||
|
|
370bd3d7cd | ||
|
|
42316104df | ||
|
|
da7b13a568 | ||
|
|
2e0bfb6fc8 | ||
|
|
3922b330d5 | ||
|
|
0c23173a1a | ||
|
|
a0ec74b09b | ||
|
|
7efe9097ce | ||
|
|
91c4a4ad63 | ||
|
|
bc91a8166f | ||
|
|
a68e4a0366 | ||
|
|
5856cfb423 | ||
|
|
a2d0d6057f | ||
|
|
b75f3d4855 | ||
|
|
a282618d90 | ||
|
|
e9d79957c8 | ||
|
|
9ae6cf0945 | ||
|
|
fa7d0032c3 | ||
|
|
bb2af4bdf5 | ||
|
|
7b9de649e5 | ||
|
|
89ebf852a9 | ||
|
|
81fe1f1549 | ||
|
|
e3c9ad0953 | ||
|
|
bac087ce63 | ||
|
|
d15b237100 | ||
|
|
e6869d17be | ||
|
|
06220fa312 | ||
|
|
3800a75b78 | ||
|
|
5e64a4f2ed | ||
|
|
51a36669a8 | ||
|
|
9f152fef90 | ||
|
|
914e3dbda8 | ||
|
|
ba71911edc | ||
|
|
25a4a84138 | ||
|
|
74de932eb9 | ||
|
|
34959f5b4e | ||
|
|
a4c7bcabcf | ||
|
|
a17c9ebbb4 | ||
|
|
079748e5e7 | ||
|
|
66e7d4d90d | ||
|
|
1da8dd74e2 | ||
|
|
8361487b6b | ||
|
|
d7714f7ef2 | ||
|
|
f4311d7f95 | ||
|
|
603066f370 | ||
|
|
a5cc458252 | ||
|
|
b06bf368b6 | ||
|
|
e00e43ca61 | ||
|
|
59c2607418 | ||
|
|
9835fb42b3 | ||
|
|
4b8b80ae97 | ||
|
|
fc0370883f | ||
|
|
41b3165543 | ||
|
|
886347471e | ||
|
|
4ef3650454 | ||
|
|
80e5302192 | ||
|
|
a025a61209 | ||
|
|
4eb92d381d | ||
|
|
372d759eb7 | ||
|
|
55b679932e | ||
|
|
796a267205 | ||
|
|
6b969f4a7c | ||
|
|
4b02dde153 | ||
|
|
4e3950eb67 | ||
|
|
7e3ba306f3 | ||
|
|
5bdd360235 | ||
|
|
e1f85e5d86 | ||
|
|
842d372312 | ||
|
|
a14497a771 | ||
|
|
08b1138c96 | ||
|
|
a756fd11ce | ||
|
|
d3440e9158 | ||
|
|
686d87bad9 | ||
|
|
804bd5c8f8 | ||
|
|
d2d679a8e6 | ||
|
|
aaa9f57908 | ||
|
|
61ef001034 | ||
|
|
4b51ef186f | ||
|
|
786c4a73dd | ||
|
|
e5134cdb68 | ||
|
|
ea36df58cc | ||
|
|
3aa7cc06b6 | ||
|
|
7fab9b6685 | ||
|
|
b425a6010b | ||
|
|
43edcdc3c5 | ||
|
|
986418024a | ||
|
|
67bebe5ec2 | ||
|
|
fbc315fdba | ||
|
|
8f75c6dc9f | ||
|
|
5c2b23211e | ||
|
|
789f62adf0 | ||
|
|
18524460e8 | ||
|
|
929f8eb4a2 | ||
|
|
5af88a511e | ||
|
|
1d31d2f3a9 | ||
|
|
ad73ddec2b | ||
|
|
b1b2ad1ddb | ||
|
|
6cbda0e126 | ||
|
|
36b26209df |
No files matched your search
@@ -12,3 +12,6 @@ __pycache__/
|
||||
*.zip
|
||||
pierzainer_ground/data/cache/
|
||||
pierzainer_ground/data/output/**/cache/
|
||||
backup/
|
||||
verify/
|
||||
도면추출/output/
|
||||
@@ -1,6 +1,14 @@
|
||||
# PIERZAINER · PIER & ABUT
|
||||
|
||||
교각 Excel을 읽어 형상을 생성하고 원도면 기반 지반면·위성사진·2D 지도를 함께 표시하는 프로젝트입니다.
|
||||
교각·교대 Excel을 읽어 형상과 철근배근을 생성하고 원도면 기반 지반면·위성사진·2D 지도를 함께 표시하는 프로젝트입니다.
|
||||
|
||||
## 현재 작업 상태 (2026-11-05 확인)
|
||||
|
||||
- 현재 로컬 작업 브랜치는 `master`이며 제품 코드 기준 커밋은 `91c4a4a`입니다. 아래 11월 2일 복구 기록은 당시 이력입니다.
|
||||
- 교대 ② 형상계획 카드 6개와 ③ 철근배근 6개 부재(받침블럭·벽체·흉벽/브라켓·확대기초·날개벽·접속슬래브)가 구현되어 있습니다.
|
||||
- A1 표본으로 ③ 입력 재계산·삽도/3D 표시·철근 선택·파일 다운로드·새 페이지 저장 복원을 검증했습니다. 설계 미결 사항과 다른 입력의 동작은 별도 확인 대상입니다.
|
||||
- 최신 진행 기록은 `../../context-migration/컨텍스트_2026-11-03_교각_교대_진행상황.md`, 남은 항목은 `../../context-migration/OPEN_미해결목록.md`에서 관리합니다.
|
||||
- 로컬 `origin/master` 추적 정보 대비 89커밋 앞선 상태였습니다. 실제 원격은 조회하지 않았습니다. 브랜치 전환이나 이력 변경 전에 현재 상태를 확인하세요.
|
||||
|
||||
## 실행
|
||||
|
||||
@@ -11,19 +19,45 @@ npm run dev -- --host 127.0.0.1 --port 5173
|
||||
|
||||
브라우저에서 http://127.0.0.1:5173/ 을 엽니다. 랜딩페이지의 Excel 가져오기와 실제 교량 샘플 선택을 사용합니다.
|
||||
|
||||
랜딩페이지의 **도면 불러오기**를 누르면 프로그램 내부의 도면추출 팝업이 열립니다. DWG 폴더를 선택하고 자동 추출을 시작하면 진행 로그와 결과가 팝업에 표시됩니다. 결과의 **교각 불러오기**로 바로 Excel 인식·형상 생성 단계에 연결하거나 **Excel 저장**으로 파일을 받을 수 있습니다. 원래 추출 엔진은 화면 없이 실행하며 미확정 값은 검토 대상으로 유지합니다.
|
||||
|
||||
결과는 프로젝트의 `도면추출/output/작업ID/`에 저장합니다. 선택한 원본 도면 폴더는 수정하지 않습니다. 팝업을 닫아도 추출은 계속되며 다시 열어 결과를 확인할 수 있습니다.
|
||||
|
||||
배포 빌드를 로컬에서 실행하려면 `npm run build` 후 `npm run preview -- --host 127.0.0.1`을 사용합니다. 도면추출은 이 PC의 Windows 로컬 서버와 포함된 `도면추출/app` 엔진을 사용하므로, `dist`만 올린 정적 웹사이트에서는 추출을 실행할 수 없습니다.
|
||||
|
||||
## 유지하는 폴더
|
||||
|
||||
- `src/`, `index.html`: 실제 교각 프로그램
|
||||
- `랜딩페이지/`: 랜딩 화면과 지도 포함 통합 샘플 12개
|
||||
- `pierzainer_ground/`: 지반 생성 프로그램, 원본 자료, 최종 지반 JSON 및 재현에 필요한 데이터
|
||||
- `도면추출_연동패키지_20261102/`: 도면 추출 프로그램과 교각 샘플 Excel
|
||||
- `public/`: 프로그램 리소스 및 기본 Excel 샘플
|
||||
- `tests/`, `scripts/`, `verify/`: 자동 검사와 유지보수 스크립트
|
||||
- `도면추출/`: 도면 추출 프로그램과 교각 샘플 Excel (기본 P1 샘플 포함)
|
||||
- `tests/`, `scripts/`, `verify/`: 현재 교각·Excel·지도 기능의 자동 검사와 유지보수 도구
|
||||
|
||||
`node_modules/`, `dist/`, `.verify/`, 지반 `cache/`는 실행·검증 중 재생성되는 파일이며 Git에 추가하지 않습니다.
|
||||
|
||||
## 2026-11-02 혼합 파일 복구
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
npm test -- --maxWorkers=2
|
||||
npm run build
|
||||
```
|
||||
|
||||
A1 화면 검증 스크립트는 `scripts/verify-abut-workflow.cjs`입니다. 개발 서버와 별도 프로필 Chrome의 로컬 CDP 포트(기본 9444)가 필요합니다.
|
||||
|
||||
현장타설 실배근의 현행 UI 검증은 `node scripts/verify-cast-rebar.cjs`를 사용합니다. Windows Chrome 사용 시 Windows Node로 실행합니다. 샘플 Excel에서 9개 교각·기초 조합을 생성하고 실제 주철근/횡철근/커플러 행·삽도·입력 재생성·불일치 경고·재료표·파일 저장/새 페이지 복원·헤더 3개 폭을 검사합니다. 결과는 `.verify/cast-rebar-20261106/`에 저장합니다. `verify-unified-input.cjs`는 실배근 연결 전 통합 입력 단계의 검증이고, `verify-cast-input.cjs`와 `verify-cast-detail.cjs`는 통합 전 분리 카드 선택자를 사용한 과거 검증 스크립트입니다.
|
||||
|
||||
```text
|
||||
node scripts/verify-abut-workflow.cjs <input.xlsx> <output-directory> <download-directory>
|
||||
```
|
||||
|
||||
Windows Chrome을 쓰면 Windows Node로 실행하고 입력·출력 경로도 Windows 경로 또는 WSL UNC 경로로 전달합니다. 결과 JSON·화면 캡처·SVG·프로젝트 표본은 `.verify/abut-workflow-20261105/`에 보관했습니다. `report.json`의 실행 결과와 범위를 확인하세요. 단순 재열기 검사는 같은 인자 뒤에 `--resume`을 붙일 수 있습니다.
|
||||
|
||||
## 2026-11-02 혼합 파일 복구 (당시 기록)
|
||||
|
||||
혼합 전 Pierzainer 커밋 `d6735a6`에서 로컬 브랜치 `recovery/pierzainer-20261102`를 만들었습니다. π형 Excel 인식, 전·후열 받침 수 불일치 허용, 랜딩페이지 샘플 연결을 포함합니다. 기존 ITAM 브랜치와 원격 기록은 변경하지 않았습니다.
|
||||
|
||||
복구본과 분리한 파일은 상위 `_recovery/20261102_mixed_projects/`에 보관했습니다. 기존 `master`는 ITAM 코드이므로 그 브랜치로 전환하면 화면과 파일이 다시 바뀝니다. 현재 복구 브랜치에서 작업하세요. 이 작업에서는 원격 push를 수행하지 않았습니다.
|
||||
당시 복구 기록에는 분리 파일을 상위 `_recovery/20261102_mixed_projects/`에 보관하고, 당시 `master`가 ITAM 코드였다고 적혀 있습니다. 2026-11-05 현재 로컬 `master`는 Pierzainer 코드(`91c4a4a`)이므로 과거 안내에 따라 복구 브랜치로 전환하지 마세요. 당시 복구 보관 경로의 현존 여부는 이번 작업에서 재확인하지 않았습니다. 이번 검증·기록 정리에서도 원격 push와 브랜치 전환은 수행하지 않았습니다.
|
||||
|
||||
## 정리 기준
|
||||
|
||||
실행에 필요한 코드·원본 도면·샘플 Excel·최종 지반 JSON은 유지합니다. 과거 개발용 도구와 옛 좌표 기준의 검증 파일은 정리했으며 백업 목록은 상위 `_recovery/20261103_cleanup/`에 있습니다.
|
||||
+82
-26
@@ -11,22 +11,26 @@
|
||||
<header class="top">
|
||||
<div class="brand">
|
||||
<strong>Pierzainer</strong>
|
||||
<span>교각 설계</span>
|
||||
<span id="project-line" class="proj">프로젝트 미지정</span>
|
||||
</div>
|
||||
<nav class="steps" aria-label="단계">
|
||||
<button id="nav-shape" class="step current" type="button" aria-current="step">① 형상 생성 <em id="step1-status">미생성</em></button>
|
||||
<button id="nav-plan" class="step" type="button">② 형상계획 <em id="step2-status">예정</em></button>
|
||||
<button id="nav-rebar" class="step" type="button">③ 철근배근 <em id="step3-status">예정</em></button>
|
||||
<button id="nav-quantity" class="step" type="button">④ 재료표 <em id="step4-status">예정</em></button>
|
||||
<button id="nav-shape" class="step current" data-label="① 형상 생성" aria-label="① 형상 생성" type="button" aria-current="step">① 형상 <em id="step1-status"></em></button>
|
||||
<button id="nav-plan" class="step" data-label="② 형상계획" aria-label="② 형상계획" type="button">② 계획 <em id="step2-status"></em></button>
|
||||
<button id="nav-rebar" class="step" data-label="③ 철근배근" aria-label="③ 철근배근" type="button">③ 배근 <em id="step3-status"></em></button>
|
||||
<button id="nav-drawing" class="step" data-label="④ 도면작성" aria-label="④ 도면작성" type="button">④ 도면 <em id="step-drawing-status"></em></button>
|
||||
</nav>
|
||||
<div class="actions">
|
||||
<span id="save-status" class="savest">저장 전</span>
|
||||
<button id="home-from-app" class="btn-s" type="button" hidden>홈으로</button>
|
||||
<button id="home-from-app" class="btn-s" type="button" hidden>홈</button>
|
||||
<button id="save-project" class="btn-s" type="button" disabled title="프로젝트 파일 저장 (Ctrl+S)">저장</button>
|
||||
<button id="open-import" class="btn-s" type="button">엑셀 불러오기</button>
|
||||
<button id="open-project" class="btn-s" type="button">프로젝트 열기</button>
|
||||
<button id="regen" class="btn-p" type="button" disabled>선택 교각 형상 생성</button>
|
||||
<details id="file-menu" class="file-menu">
|
||||
<summary class="btn-s">불러오기</summary>
|
||||
<div class="file-menu-items">
|
||||
<button id="open-import" class="btn small" type="button">엑셀 불러오기</button>
|
||||
<button id="open-project" class="btn small" type="button">프로젝트 열기</button>
|
||||
</div>
|
||||
</details>
|
||||
<button id="regen" class="btn-p" type="button" disabled>생성</button>
|
||||
<input id="project-file" type="file" accept=".json" hidden />
|
||||
</div>
|
||||
</header>
|
||||
@@ -43,7 +47,7 @@
|
||||
|
||||
<main class="grid" id="view-shape">
|
||||
<aside class="panel piers" aria-labelledby="pier-title">
|
||||
<p id="pier-title" class="lbl">교각 목록</p>
|
||||
<p id="pier-title" class="lbl">교각·교대 목록</p>
|
||||
<div class="pier-tools">
|
||||
<label>형식
|
||||
<select id="type-filter"></select>
|
||||
@@ -54,7 +58,7 @@
|
||||
<p id="pier-empty" class="muted">불러온 교각이 없습니다.</p>
|
||||
<p class="pnote">시트 1장 = 교각 1기입니다. 오류가 난 교각만 건너뛰고 나머지는 생성합니다.</p>
|
||||
|
||||
<section class="pier-input" aria-labelledby="input-title">
|
||||
<section id="pier-info" class="pier-input" aria-labelledby="input-title">
|
||||
<p id="input-title" class="lbl">입력값</p>
|
||||
<div class="pier-table-wrap">
|
||||
<table id="inputs-all" class="grid-table compact">
|
||||
@@ -71,6 +75,27 @@
|
||||
<button id="edit-regen" class="btn primary" type="button" disabled>다시 생성 · 변경 0</button>
|
||||
</section>
|
||||
</section>
|
||||
<section id="abut-panel" class="pier-input" hidden aria-labelledby="abut-panel-title">
|
||||
<p id="abut-panel-title" class="lbl"><span id="abut-panel-name"></span> 입력값 <span class="badge">입력</span>
|
||||
<button id="abut-panel-edit-toggle" class="btn small" type="button" aria-expanded="false">편집</button></p>
|
||||
<dl id="abut-panel-input" class="kv wide"></dl>
|
||||
<div id="abut-panel-editor" hidden>
|
||||
<div id="abut-panel-basic" class="edit-row"></div>
|
||||
<details class="abut-group">
|
||||
<summary>받침 값</summary>
|
||||
<div id="abut-panel-bearings" class="edit-row"></div>
|
||||
</details>
|
||||
<details class="abut-group">
|
||||
<summary>형상 (벽체·흉벽, 저판, 말뚝)</summary>
|
||||
<div id="abut-panel-shape" class="edit-row"></div>
|
||||
</details>
|
||||
<p id="abut-panel-msg" class="muted" aria-live="polite"></p>
|
||||
</div>
|
||||
<details class="abut-group">
|
||||
<summary>상세 (계산값)</summary>
|
||||
<dl id="abut-panel-summary" class="kv wide"></dl>
|
||||
</details>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section class="panel viewer" id="viewer-block" aria-labelledby="viewer-title">
|
||||
@@ -159,15 +184,30 @@
|
||||
|
||||
<main class="grid plan" id="view-plan" hidden>
|
||||
<aside class="panel piers" aria-labelledby="plan-pier-title">
|
||||
<p id="plan-pier-title" class="lbl">교각 목록</p>
|
||||
<p id="plan-pier-title" class="lbl">교각·교대 목록</p>
|
||||
<div class="phead" aria-hidden="true"><span>교각</span><span>변경</span></div>
|
||||
<ul id="plan-pier-items" class="pier-items"></ul>
|
||||
<p class="lbl abut-members-title">부재 · 교대</p>
|
||||
<ul id="abut-members" class="members abut-members" aria-label="교대 부재"></ul>
|
||||
<p class="lbl members-title">부재 · 5개</p>
|
||||
<ul id="members" class="members"></ul>
|
||||
</aside>
|
||||
|
||||
<div class="center-col">
|
||||
<div class="viewer-slot" id="slot-plan"></div>
|
||||
<!-- ② 교대 부재 카드 (시안, 2026-11-03): the card of the row picked in 부재 · 교대, under the 3D view. -->
|
||||
<section id="abut-panel-plan" class="panel viewer abut-card" hidden aria-labelledby="abut-panel-plan-title">
|
||||
<div class="viewer-head">
|
||||
<h2 id="abut-panel-plan-title"><span id="abut-panel-plan-member">교량받침 · 받침블럭 · 솔플레이트</span> <span class="muted small" id="abut-panel-plan-name"></span></h2>
|
||||
</div>
|
||||
<div id="abut-panel-plan-bearings" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-wing" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-slab" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-ground" class="edit-row"></div>
|
||||
<div id="abut-panel-plan-shape" class="edit-row abut-shape-view"></div>
|
||||
<div id="abut-panel-plan-footing" class="edit-row abut-shape-view"></div>
|
||||
<p id="abut-panel-plan-msg" class="muted" aria-live="polite"></p>
|
||||
</section>
|
||||
<section class="panel viewer" aria-labelledby="member-title">
|
||||
<div class="viewer-head">
|
||||
<h2 id="member-title">코핑</h2>
|
||||
@@ -227,6 +267,8 @@
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<main id="view-drawing" hidden aria-label="교각 일반도 작성"></main>
|
||||
|
||||
<main class="grid quantity" id="view-quantity" hidden>
|
||||
<aside class="panel piers" aria-labelledby="quantity-pier-title">
|
||||
<p id="quantity-pier-title" class="lbl">교각 목록</p>
|
||||
@@ -236,6 +278,7 @@
|
||||
</aside>
|
||||
<section class="panel viewer" aria-labelledby="quantity-title">
|
||||
<div class="viewer-head">
|
||||
<button id="quantity-back" class="btn-o" type="button">← ③ 철근배근</button>
|
||||
<h2 id="quantity-title">철근재료표</h2>
|
||||
<button id="quantity-export" class="btn-o" type="button">엑셀로 내보내기</button>
|
||||
<button id="quantity-print" class="btn-o" type="button">인쇄</button>
|
||||
@@ -252,7 +295,16 @@
|
||||
<ul id="rebar-tabs" class="members"></ul>
|
||||
<p class="pnote">형상은 ② 형상계획 기준입니다. 형상이 바뀌면 "형상 바뀜"으로 표시합니다.</p>
|
||||
</aside>
|
||||
<div class="center-col">
|
||||
<div class="center-col review-workspace" id="review-workspace">
|
||||
<div class="workspace-tools" aria-label="검토 공간 조절">
|
||||
<label class="model-height-control" for="review-model-height">모델 높이
|
||||
<input id="review-model-height" type="range" min="180" max="560" step="20" value="240" />
|
||||
<output id="review-model-height-value" for="review-model-height">240 px</output>
|
||||
</label>
|
||||
<button id="review-model-toggle" class="btn small" type="button" aria-expanded="true" aria-controls="slot-rebar">모델 접기</button>
|
||||
<button id="review-inputs" class="btn small" type="button">입력 보기</button>
|
||||
<button id="review-table" class="btn small" type="button">철근표 보기</button>
|
||||
</div>
|
||||
<div class="viewer-slot" id="slot-rebar"></div>
|
||||
<section class="panel viewer" aria-labelledby="rebar-title">
|
||||
<div class="viewer-head"><h2 id="rebar-title">철근표</h2></div>
|
||||
@@ -263,24 +315,26 @@
|
||||
<aside class="panel results" aria-labelledby="rebar-issues-title">
|
||||
<h2 id="rebar-issues-title">알림</h2>
|
||||
<ul id="rebar-issues" class="issues"></ul>
|
||||
<h2>검토 필요 (결정 대기)</h2>
|
||||
<ul class="issues">
|
||||
<li>Q-B5 B1·B3·B5·B7 다리 길이 (제안값 사용)</li>
|
||||
<li>Q-B6 B2·B6 이음 위치와 길이</li>
|
||||
<li>Q-C9 P4–B11 간섭: 무시로 결정, 문서 반영 필요</li>
|
||||
</ul>
|
||||
<h2>부재 검토</h2>
|
||||
<p id="review-summary" class="muted small" aria-live="polite"></p>
|
||||
<ul id="review-tasks" class="issues review-tasks"></ul>
|
||||
<section class="common" aria-labelledby="common-title">
|
||||
<p id="common-title" class="lbl">공통 설정</p>
|
||||
<h3 id="common-title">공통 설정</h3>
|
||||
<fieldset class="material-fields">
|
||||
<legend>재료</legend>
|
||||
<label class="mat">fck (MPa) <input id="mat-fck" type="number" step="any" /></label>
|
||||
<label class="mat">D 규격 강도 <select id="mat-grade-d"><option value="300" selected>SD300</option><option value="400">SD400</option><option value="500">SD500</option></select></label>
|
||||
<label class="mat">H 규격 강도 <select id="mat-grade-h"><option value="300">SD300</option><option value="400" selected>SD400</option><option value="500">SD500</option></select></label>
|
||||
<label class="mat">S 규격 강도 <select id="mat-grade-s"><option value="300">SD300</option><option value="400">SD400</option><option value="500" selected>SD500</option></select></label>
|
||||
<label class="mat">재료표 할증 (%) · 기본 0 가정 <input id="mat-waste" type="number" step="0.5" min="0" /></label>
|
||||
<p class="lbl">굽힘 내면반경 (× 철근 지름)</p>
|
||||
</fieldset>
|
||||
<fieldset class="material-fields">
|
||||
<legend>굽힘 내면반경 (× 철근 지름)</legend>
|
||||
<label class="mat">D16 이하 <input id="bend-small16" type="number" step="0.5" min="0.5" /></label>
|
||||
<label class="mat">D19–D25 <input id="bend-main25" type="number" step="0.5" min="0.5" /></label>
|
||||
<label class="mat">D29–D35 <input id="bend-main35" type="number" step="0.5" min="0.5" /></label>
|
||||
<label class="mat">D38 이상 <input id="bend-main51" type="number" step="0.5" min="0.5" /></label>
|
||||
</fieldset>
|
||||
<p id="mat-note" class="muted small"></p>
|
||||
</section>
|
||||
</aside>
|
||||
@@ -353,20 +407,22 @@
|
||||
<div class="dlg-col">
|
||||
<section>
|
||||
<h3>미리보기</h3>
|
||||
<p id="preview-title" class="muted"></p>
|
||||
<p id="preview-title" class="muted" aria-live="polite"></p>
|
||||
<h4>교각 정보</h4>
|
||||
<div class="pv-scroll"><table id="pv-pier" class="grid-table"></table></div>
|
||||
<h4>교대 정보</h4>
|
||||
<div class="pv-scroll"><table id="pv-abuts" class="grid-table"></table></div>
|
||||
<h4>받침</h4>
|
||||
<div class="pv-scroll">
|
||||
<table id="pv-bearings" class="grid-table">
|
||||
<thead><tr><th>교각</th><th>번호</th><th>x (m)</th><th>y (m)</th><th>z (m)</th><th>방위각</th><th>위치</th></tr></thead>
|
||||
<thead><tr><th>구조물</th><th>번호</th><th>x (m)</th><th>y (m)</th><th>z (m)</th><th>방위각</th><th>위치</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h4>IP</h4>
|
||||
<div class="pv-scroll">
|
||||
<table id="pv-ips" class="grid-table">
|
||||
<thead><tr><th>교각</th><th>점</th><th>x (m)</th><th>y (m)</th><th>꺾임각</th></tr></thead>
|
||||
<thead><tr><th>구조물</th><th>점</th><th>x (m)</th><th>y (m)</th><th>꺾임각</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -375,9 +431,9 @@
|
||||
</div>
|
||||
|
||||
<footer class="dlg-foot">
|
||||
<span id="dlg-selected" class="muted"></span>
|
||||
<span id="dlg-selected" class="muted" aria-live="polite"></span>
|
||||
<button id="dlg-cancel" class="btn" type="button">취소</button>
|
||||
<button id="dlg-apply" class="btn primary" type="button" disabled>선택한 교각 0기 형상 생성</button>
|
||||
<button id="dlg-apply" class="btn primary" type="button" disabled>불러올 구조물을 선택하세요</button>
|
||||
</footer>
|
||||
</dialog>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "교각 3D 형상 생성 웹앱 (1단계: 형상 생성)",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
|
||||
@@ -16,7 +16,7 @@ import traceback
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
OUTPUT = ROOT / 'data/output/samples_20261102'
|
||||
PACKAGE = ROOT.parent / '도면추출_연동패키지_20261102'
|
||||
PACKAGE = ROOT.parent / '도면추출'
|
||||
|
||||
|
||||
def save(path, value):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5183",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "CSSAA14",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 1,
|
||||
"bridge": "산외교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "CA002,CA0021,CAA001,CAA002",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 2,
|
||||
"bridge": "운곡교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "*Model_Space",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "PDF_SOUNDING",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 3,
|
||||
"bridge": "대호지교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\03_original_pdf_soundings.dxf",
|
||||
"candidates": [],
|
||||
"output": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\output\\samples_20261102\\03_대호지교",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "CA002,CA0021,CAA001,CAA002",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 4,
|
||||
"bridge": "목동교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\04_@서울-세종 9공구 1000현황.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample4)_π형교각_목동교\\01 공통사항(목동교 종평면도).dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "F0017111,F0017114,F0027217,F0027132",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 5,
|
||||
"bridge": "승천천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\05_project_topography_original.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample5)_π형교각_승천천교\\01 승천천교(DR) 종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "xref-현황(191219)",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 6,
|
||||
"bridge": "계문철교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample6)_π형교각_계문철교\\01 계문철교 종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "xref-현황(191219)",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 7,
|
||||
"bridge": "기양천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample7)_π형교각_기양천교\\01 기양천교종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "XR_현황",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 8,
|
||||
"bridge": "옥성교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "*Model_Space",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "XR_현황-수정$0$Point",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 9,
|
||||
"bridge": "새재1교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "*Model_Space",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "CA002,CA0021,CAA001,CAA002",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 10,
|
||||
"bridge": "도하4교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5186",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "*Model_Space",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "CA002,CA0021,CAA001,CAA002",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 11,
|
||||
"bridge": "곡천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"terrain_crs": "EPSG:5181",
|
||||
"cad_axes": "YX",
|
||||
"terrain_block": "",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"piers": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"boreholes": "",
|
||||
"layers": "CAA001,CAA002",
|
||||
"margin": 100.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sample": 12,
|
||||
"bridge": "감북교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"sample": 1,
|
||||
"bridge": "산외교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg"
|
||||
@@ -12,7 +12,7 @@
|
||||
{
|
||||
"sample": 2,
|
||||
"bridge": "운곡교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg"
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"sample": 3,
|
||||
"bridge": "대호지교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\03_original_pdf_soundings.dxf",
|
||||
"candidates": [],
|
||||
"output": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\output\\samples_20261102\\03_대호지교",
|
||||
@@ -930,7 +930,7 @@
|
||||
{
|
||||
"sample": 4,
|
||||
"bridge": "목동교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\04_@서울-세종 9공구 1000현황.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample4)_π형교각_목동교\\01 공통사항(목동교 종평면도).dwg"
|
||||
@@ -956,7 +956,7 @@
|
||||
{
|
||||
"sample": 5,
|
||||
"bridge": "승천천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\05_project_topography_original.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample5)_π형교각_승천천교\\01 승천천교(DR) 종평면도.dwg"
|
||||
@@ -984,7 +984,7 @@
|
||||
{
|
||||
"sample": 6,
|
||||
"bridge": "계문철교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample6)_π형교각_계문철교\\01 계문철교 종평면도.dwg"
|
||||
@@ -1011,7 +1011,7 @@
|
||||
{
|
||||
"sample": 7,
|
||||
"bridge": "기양천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample7)_π형교각_기양천교\\01 기양천교종평면도.dwg"
|
||||
@@ -1038,7 +1038,7 @@
|
||||
{
|
||||
"sample": 8,
|
||||
"bridge": "옥성교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg"
|
||||
@@ -1048,7 +1048,7 @@
|
||||
{
|
||||
"sample": 9,
|
||||
"bridge": "새재1교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg"
|
||||
@@ -1058,7 +1058,7 @@
|
||||
{
|
||||
"sample": 10,
|
||||
"bridge": "도하4교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg"
|
||||
@@ -1068,7 +1068,7 @@
|
||||
{
|
||||
"sample": 11,
|
||||
"bridge": "곡천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg"
|
||||
@@ -1078,7 +1078,7 @@
|
||||
{
|
||||
"sample": 12,
|
||||
"bridge": "감북교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{
|
||||
"sample": 1,
|
||||
"bridge": "산외교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플01_산외교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample1)_π형교각_산외교\\02.종평면도(울산)_2017.0619-수정.dwg"
|
||||
@@ -41,7 +41,7 @@
|
||||
{
|
||||
"sample": 2,
|
||||
"bridge": "운곡교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플02_운곡교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample2)_T형교각_운곡교\\C0050202-001-운곡교 종평면도(완도방향).dwg"
|
||||
@@ -75,7 +75,7 @@
|
||||
{
|
||||
"sample": 3,
|
||||
"bridge": "대호지교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플03_대호지교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\03_original_pdf_soundings.dxf",
|
||||
"candidates": [],
|
||||
"output": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\output\\samples_20261102\\03_대호지교",
|
||||
@@ -1007,7 +1007,7 @@
|
||||
{
|
||||
"sample": 4,
|
||||
"bridge": "목동교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플04_목동교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\04_@서울-세종 9공구 1000현황.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample4)_π형교각_목동교\\01 공통사항(목동교 종평면도).dwg"
|
||||
@@ -1057,7 +1057,7 @@
|
||||
{
|
||||
"sample": 5,
|
||||
"bridge": "승천천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플05_승천천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\05_project_topography_original.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample5)_π형교각_승천천교\\01 승천천교(DR) 종평면도.dwg"
|
||||
@@ -1109,7 +1109,7 @@
|
||||
{
|
||||
"sample": 6,
|
||||
"bridge": "계문철교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플06_계문철교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample6)_π형교각_계문철교\\01 계문철교 종평면도.dwg"
|
||||
@@ -1160,7 +1160,7 @@
|
||||
{
|
||||
"sample": 7,
|
||||
"bridge": "기양천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플07_기양천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\pierzainer_ground\\data\\input\\samples_20261102\\recovered\\06_서수평장1-현황+선형+노리-20200410.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample7)_π형교각_기양천교\\01 기양천교종평면도.dwg"
|
||||
@@ -1211,7 +1211,7 @@
|
||||
{
|
||||
"sample": 8,
|
||||
"bridge": "옥성교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플08_옥성교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample8)_T형교각_옥성교\\01 옥성교 종평면도.dwg"
|
||||
@@ -1245,7 +1245,7 @@
|
||||
{
|
||||
"sample": 9,
|
||||
"bridge": "새재1교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플09_새재1교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample9)_T형교각_새재1교\\01 새재1교 종평면도.dwg"
|
||||
@@ -1279,7 +1279,7 @@
|
||||
{
|
||||
"sample": 10,
|
||||
"bridge": "도하4교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플10_도하4교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample10)_T형교각_도하4교\\01.종평면도.dwg"
|
||||
@@ -1313,7 +1313,7 @@
|
||||
{
|
||||
"sample": 11,
|
||||
"bridge": "곡천교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플11_곡천교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample11)_π형교각_곡천교\\01 종평면도_곡천교.dwg"
|
||||
@@ -1347,7 +1347,7 @@
|
||||
{
|
||||
"sample": 12,
|
||||
"bridge": "감북교",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출_연동패키지_20261102\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"workbook": "C:\\Users\\User\\Desktop\\Pier_v1-main\\Pier_v1\\도면추출\\samples\\샘플12_감북교_교각입력데이터.xlsx",
|
||||
"drawing": "C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg",
|
||||
"candidates": [
|
||||
"C:\\Users\\User\\Desktop\\도면추출\\엔지니어(sample12)_π형교각_감북교\\01 감북교 종평면도.dwg"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 프로그램 내부 지반 생성 팝업
|
||||
|
||||
현재 지반 생성 버튼은 별도 Windows 창 대신 프로그램 내부 dialog를 연다.
|
||||
- Excel 시작: 현재 Excel이 연결되며 종평면도 DWG/DXF를 선택한 뒤 지반 생성을 누른다.
|
||||
- 도면 시작: 추출 폴더의 종평면도 원본과 Excel이 연결되며 지반 생성을 누른다.
|
||||
- 입력을 바꾸려면 팝업의 파일 선택을 사용한다. Excel 연결 정보가 없는 프로젝트는 Excel도 선택한다.
|
||||
- 기존 Python build_from_files 엔진을 창 없이 실행한다. BAT를 브라우저 안에 직접 삽입하는 방식이 아니며, 독립 실행용 BAT는 유지한다.
|
||||
- 진행 로그와 오류를 팝업에 표시한다. 닫아도 작업은 계속되며 다시 열 수 있다.
|
||||
- 완료 결과는 기존 JSON 로더에 전달한다. 지반·지층 패널을 벗어나면 복귀 후 반영한다.
|
||||
- 결과는 data/output/app_<작업 ID>에 저장한다. 원본 자료는 변경하지 않는다.
|
||||
- Windows 로컬 Vite 서버가 필요하다. 정적 호스팅에서는 실행할 수 없다.
|
||||
|
||||
검증: TypeScript 검사 및 기존 후보 선택 테스트 3건 통과. 브라우저에서 실제 prepare API, 팝업 표시, 입력 누락 오류, 닫기 및 즉시 재열기 확인. 이번 팝업 수정에서 새 도면 전체 생성은 실행하지 않았다.
|
||||
|
||||
주요 파일: ground-link.ts, ground-link-server.ts, ground-popup.css, pierzainer_ground/web_job.py. 기존 main/landing 입력 연결을 재사용한다.
|
||||
@@ -0,0 +1,59 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdir,writeFile,readFile,readdir } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Plugin } from 'vite';
|
||||
export const GROUND_LINK_API='/__pierzainer/ground-link';
|
||||
export function startGroundBat(bat:string,context:string,cwd:string){
|
||||
return spawn(process.env.ComSpec??'cmd.exe',['/d','/s','/c',`""${bat}""`],{cwd,windowsHide:true,windowsVerbatimArguments:true,env:{...process.env,PIERZAINER_GROUND_CONTEXT:context},stdio:'ignore'});
|
||||
}
|
||||
|
||||
export async function findGroundDrawing(folder:string):Promise<string|undefined>{
|
||||
const matches:string[]=[];
|
||||
const walk=async(dir:string,depth:number)=>{for(const entry of await readdir(dir,{withFileTypes:true})){const file=path.join(dir,entry.name);if(entry.isDirectory()&&depth<2)await walk(file,depth+1);else if(entry.isFile()&&/\.(dwg|dxf)$/i.test(entry.name)&&/종평면|종평|종단|평종면/.test(entry.name))matches.push(file);}};
|
||||
if(folder)await walk(folder,0);const originals=matches.filter(file=>/\.dwg$/i.test(file));return originals.length===1?originals[0]:matches.length===1?matches[0]:undefined;
|
||||
}
|
||||
export function groundLinkPlugin(root:string):Plugin{
|
||||
const base=path.join(root,'pierzainer_ground'),cache=path.join(base,'data/cache/app-link'),stages=new Map<string,{excel:string;folder:string;name:string}>(),jobs=new Map<string,{output:string;logs:string;state:string}>();
|
||||
const handler=async(req:any,res:any,next:()=>void)=>{
|
||||
const pathname=new URL(req.url??'/','http://localhost').pathname;if(!pathname.startsWith(GROUND_LINK_API+'/'))return next();
|
||||
const reply=(code:number,value:unknown)=>{res.writeHead(code,{'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store'});res.end(JSON.stringify(value));};
|
||||
const host=req.headers.host??'',address=req.socket.remoteAddress??'';
|
||||
if(req.method!=='POST'||! /^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)||!['127.0.0.1','::1','::ffff:127.0.0.1'].includes(address)||req.headers['x-pierzainer-action']!=='ground-link'||req.headers.origin&&req.headers.origin!==`http://${host}`&&req.headers.origin!==`https://${host}`){reply(403,{error:'이 PC의 Pierzainer에서 실행하세요.'});return;}
|
||||
try{
|
||||
let raw='';for await(const part of req){raw+=part;if(raw.length>280*1024*1024)throw new Error('Excel 파일이 너무 큽니다(최대 20MB).');}const body=JSON.parse(raw||'{}'),action=pathname.slice(GROUND_LINK_API.length+1);
|
||||
if(action==='stage'){
|
||||
if(typeof body.name!=='string'||!body.name.toLowerCase().endsWith('.xlsx')||typeof body.base64!=='string'||!/^[A-Za-z0-9+/]*={0,2}$/.test(body.base64))throw new Error('교각 Excel(.xlsx) 파일을 선택하세요.');
|
||||
const bytes=Buffer.from(body.base64,'base64');if(bytes.length>20*1024*1024||bytes[0]!==80||bytes[1]!==75)throw new Error('유효한 xlsx 파일이 아닙니다.');
|
||||
const id=randomUUID(),dir=path.join(cache,id);await mkdir(dir,{recursive:true});const excel=path.join(dir,'piers.xlsx');await writeFile(excel,bytes);stages.set(id,{excel,name:body.name,folder:typeof body.folder==='string'?body.folder:''});reply(200,{id});return;
|
||||
}
|
||||
if(action==='prepare'){
|
||||
const stage=stages.get(body.stageId);const drawing=stage?.folder?await findGroundDrawing(stage.folder):undefined;reply(200,{drawing:drawing??'',excelSelected:!!stage,excelName:stage?.name??''});return;
|
||||
}
|
||||
if(action==='upload'){
|
||||
if(typeof body.name!=='string'||!/\.(dwg|dxf|xlsx)$/i.test(body.name)||typeof body.base64!=='string'||! /^[A-Za-z0-9+/]*={0,2}$/.test(body.base64))throw new Error('DWG/DXF 또는 Excel을 선택하세요.');
|
||||
const bytes=Buffer.from(body.base64,'base64');if(bytes.length>200*1024*1024)throw new Error('최대 200MB입니다.');const dir=path.join(cache,randomUUID());await mkdir(dir,{recursive:true});const file=path.join(dir,path.basename(body.name));await writeFile(file,bytes);reply(200,{file});return;
|
||||
}
|
||||
if(action==='launch'){
|
||||
const stage=stages.get(body.stageId),drawing=String(body.drawing??''),piers=String(body.excel??stage?.excel??'');
|
||||
const original=stage?.folder?await findGroundDrawing(stage.folder):undefined;
|
||||
const cached=(file:string)=>{const r=path.relative(cache,path.resolve(file));return !!r&&!r.startsWith('..')&&!path.isAbsolute(r);};
|
||||
if(!existsSync(drawing)||!/\.(dwg|dxf)$/i.test(drawing)||drawing!==original&&!cached(drawing))throw new Error('종평면도를 선택하세요.');
|
||||
if(!existsSync(piers)||!/\.xlsx$/i.test(piers)||!cached(piers))throw new Error('교각 Excel을 선택하세요.');
|
||||
const id=randomUUID(),output=path.join(base,'data/output',`app_${id}`),job={output,logs:'',state:'running'};jobs.set(id,job);
|
||||
const python=existsSync(path.join(base,'.venv/Scripts/python.exe'))?path.join(base,'.venv/Scripts/python.exe'):'python';
|
||||
const child=spawn(python,['-B','-u','-X','utf8','-m','pierzainer_ground.web_job',drawing,piers,output],{cwd:base,windowsHide:true,stdio:['ignore','pipe','pipe']});
|
||||
const append=(data:Buffer)=>{job.logs=(job.logs+data.toString('utf8')).slice(-200000);};child.stdout.on('data',append);child.stderr.on('data',append);
|
||||
child.once('error',error=>{job.state='error';job.logs+='\n'+error.message;});child.once('close',code=>{job.state=code===0?'done':'error';});reply(200,{id});return;
|
||||
}
|
||||
if(action==='result'){
|
||||
const job=jobs.get(body.id);if(!job)throw new Error('지반 생성 작업을 찾을 수 없습니다.');
|
||||
if(job.state!=='done'){reply(200,{ready:false,state:job.state,logs:job.logs});return;}
|
||||
for(const filename of ['ground_model_3d_with_maps.json','ground_model.json']){const file=path.join(job.output,filename);if(existsSync(file)){const text=await readFile(file,'utf8');JSON.parse(text);reply(200,{ready:true,filename,text,state:job.state,logs:job.logs});return;}}
|
||||
reply(200,{ready:false});return;
|
||||
}reply(404,{error:'지원하지 않는 지반 연동 요청입니다.'});
|
||||
}catch(error){reply(400,{error:(error as Error).message});}
|
||||
};
|
||||
return{name:'pierzainer-ground-link',configureServer(server){server.middlewares.use((req,res,next)=>void handler(req,res,next));},configurePreviewServer(server){server.middlewares.use((req,res,next)=>void handler(req,res,next));}};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe,it,expect } from 'vitest';
|
||||
import { mkdir,writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { findGroundDrawing } from './ground-link-server';
|
||||
const base=path.join(import.meta.dirname,'data/cache/link-tests');
|
||||
describe('지반 프로그램 원본 종평면도 연결',()=>{
|
||||
it('Excel 시작은 도면을 임의로 지정하지 않는다',async()=>expect(await findGroundDrawing('')).toBeUndefined());
|
||||
it('폴더의 종평면도 원본 DWG를 선택하고 변환 DXF와 중복하지 않는다',async()=>{const dir=path.join(base,randomUUID());await mkdir(dir,{recursive:true});await writeFile(path.join(dir,'산외교 종평면도.dwg'),'');await writeFile(path.join(dir,'종평면도.dxf'),'');await writeFile(path.join(dir,'받침배치도.dwg'),'');expect(await findGroundDrawing(dir)).toBe(path.join(dir,'산외교 종평면도.dwg'));});
|
||||
it('서로 다른 종평면도 원본이 여럿이면 사용자 선택으로 남긴다',async()=>{const dir=path.join(base,randomUUID());await mkdir(dir,{recursive:true});await writeFile(path.join(dir,'A교 종평면도.dwg'),'');await writeFile(path.join(dir,'B교 종평면도.dwg'),'');expect(await findGroundDrawing(dir)).toBeUndefined();});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import './ground-popup.css';
|
||||
const API='/__pierzainer/ground-link';
|
||||
let pending:{file:File;folder?:string}|undefined,staged:Promise<string|undefined>=Promise.resolve(undefined),version=0;
|
||||
const request=async(action:string,body:object)=>{const r=await fetch(`${API}/${action}`,{method:'POST',headers:{'Content-Type':'application/json','X-Pierzainer-Action':'ground-link'},body:JSON.stringify(body)});const value=await r.json();if(!r.ok)throw new Error(value.error??'지반 프로그램 연동 실패');return value;};
|
||||
const base64=(file:File)=>new Promise<string>((resolve,reject)=>{const reader=new FileReader();reader.onerror=()=>reject(new Error('Excel 파일을 읽지 못했습니다.'));reader.onload=()=>resolve(String(reader.result).split(',')[1]);reader.readAsDataURL(file);});
|
||||
document.addEventListener('change',event=>{const input=event.target as HTMLInputElement;if(input.id==='file'&&input.files?.[0])pending={file:input.files[0]};if(input.id==='project-file'){pending=undefined;version++;staged=Promise.resolve(undefined);}},true);
|
||||
window.addEventListener('pierzainer:ground-source',(event:Event)=>{const d=(event as CustomEvent).detail;pending={file:new File([d.bytes],d.name,{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),folder:d.folder};});
|
||||
document.addEventListener('click',event=>{if(!(event.target as Element).closest('#dlg-apply')||!pending)return;const source=pending;pending=undefined;version++;staged=(async()=>{const data=await request('stage',{name:source.file.name,base64:await base64(source.file),folder:source.folder});return data.id as string;})();staged.catch(()=>{});},true);
|
||||
|
||||
let opening=false,running=false,launchSerial=0;
|
||||
let popup:HTMLDialogElement;
|
||||
function getPopup(){
|
||||
if(popup)return popup;
|
||||
popup=document.createElement('dialog');popup.className='ground-popup';popup.setAttribute('aria-labelledby','ground-popup-title');
|
||||
popup.innerHTML=`<header><div><small>GROUND MODEL</small><h2 id="ground-popup-title">지반 모델 생성</h2></div><button type="button" data-close aria-label="닫기">×</button></header>
|
||||
<p>종평면도와 교각 Excel로 지반면·위성사진·2D 지도 JSON을 생성합니다.</p>
|
||||
<label for="ground-drawing-file">종평면도 DXF / DWG</label><div class="ground-file-row"><span data-drawing-name></span><button type="button" data-pick="drawing">종평면도 선택</button><input id="ground-drawing-file" data-drawing type="file" accept=".dwg,.dxf" hidden></div>
|
||||
<label for="ground-excel-file">교각 Excel</label><div class="ground-file-row"><span data-excel-name></span><button type="button" data-pick="excel">Excel 선택</button><input id="ground-excel-file" data-excel type="file" accept=".xlsx" hidden></div>
|
||||
<p class="ground-popup-note">기존 생성 엔진을 사용합니다. 지도 다운로드에는 인터넷 연결이 필요합니다.</p>
|
||||
<button type="button" data-run>지반 생성</button><p data-status role="status" aria-live="polite"></p><pre data-logs tabindex="0" aria-label="생성 진행 로그"></pre><footer><button type="button" data-close>닫기</button></footer>`;
|
||||
popup.querySelectorAll<HTMLButtonElement>('[data-pick]').forEach(button=>button.addEventListener('click',()=>popup.querySelector<HTMLInputElement>(`[data-${button.dataset.pick}]`)!.click()));
|
||||
popup.querySelectorAll('[data-close]').forEach(button=>button.addEventListener('click',()=>popup.close()));document.body.append(popup);return popup;
|
||||
}
|
||||
window.addEventListener('pierzainer:open-ground',async()=>{
|
||||
if(opening&&popup?.open)return;if(running&&popup){popup.showModal();opening=true;return;}opening=true;
|
||||
const dialog=getPopup(),ticket=++launchSerial,snapshot=version;
|
||||
const status=dialog.querySelector<HTMLElement>('[data-status]')!,logs=dialog.querySelector<HTMLElement>('[data-logs]')!,run=dialog.querySelector<HTMLButtonElement>('[data-run]')!;
|
||||
const drawingInput=dialog.querySelector<HTMLInputElement>('[data-drawing]')!,excelInput=dialog.querySelector<HTMLInputElement>('[data-excel]')!;
|
||||
drawingInput.value='';excelInput.value='';drawingInput.disabled=excelInput.disabled=false;dialog.querySelector('[data-drawing-name]')!.textContent='파일 확인 중…';dialog.querySelector('[data-excel-name]')!.textContent='파일 확인 중…';run.disabled=true;status.textContent='입력 파일 확인 중…';logs.textContent='';dialog.showModal();
|
||||
let stageId:string|undefined,drawing='';
|
||||
try{stageId=await staged;const prepared=await request('prepare',{stageId});drawing=prepared.drawing;
|
||||
const display=(kind:string,name:string,connected:boolean)=>{dialog.querySelector(`[data-${kind}-name]`)!.textContent=name;dialog.querySelector(`[data-pick="${kind}"]`)!.textContent=connected?'파일 변경':kind==='drawing'?'종평면도 선택':'Excel 선택';};
|
||||
const drawingName=drawing.split(/[\\/]/).pop()||'종평면도를 선택하세요.';const excelName=prepared.excelName||'교각 Excel을 선택하세요.';
|
||||
display('drawing',drawingName,!!drawing);display('excel',excelName,prepared.excelSelected);
|
||||
drawingInput.onchange=()=>display('drawing',drawingInput.files?.[0]?.name||drawingName,!!drawingInput.files?.[0]||!!drawing);
|
||||
excelInput.onchange=()=>display('excel',excelInput.files?.[0]?.name||excelName,!!excelInput.files?.[0]||prepared.excelSelected);status.textContent='입력을 확인하고 지반 생성을 누르세요.';run.disabled=false;
|
||||
}catch(error){status.textContent=(error as Error).message;opening=false;return;}
|
||||
run.onclick=async()=>{
|
||||
if(snapshot!==version){status.textContent='프로젝트가 변경되었습니다. 닫고 다시 열어 주세요.';return;}
|
||||
run.disabled=true;drawingInput.disabled=excelInput.disabled=true;
|
||||
try{
|
||||
const upload=async(file:File)=>(await request('upload',{name:file.name,base64:await base64(file)})).file as string;
|
||||
status.textContent='입력 파일 전달 중…';const chosen=drawingInput.files?.[0],excel=excelInput.files?.[0];
|
||||
if(chosen)drawing=await upload(chosen);if(!drawing)throw new Error('종평면도를 선택하세요.');
|
||||
const excelPath=excel?await upload(excel):undefined;
|
||||
if(!excelPath&&!stageId)throw new Error('교각 Excel을 선택하세요.');
|
||||
const job=await request('launch',{stageId,drawing,excel:excelPath});running=true;status.textContent='지반·지도 생성 중… 닫아도 생성은 계속됩니다.';
|
||||
const deadline=Date.now()+30*60*1000;
|
||||
const poll=async()=>{
|
||||
if(snapshot!==version||ticket!==launchSerial){running=false;return;}
|
||||
try{
|
||||
const result=await request('result',{id:job.id});logs.textContent=result.logs??'';logs.scrollTop=logs.scrollHeight;
|
||||
if(result.state==='error')throw new Error('지반 생성 실패. 진행 로그를 확인하세요.');
|
||||
if(result.ready){
|
||||
const input=document.querySelector<HTMLInputElement>('#ground-import');
|
||||
status.textContent=input?'지반 생성 완료. 프로젝트에 불러왔습니다.':'지반 생성 완료. 지반·지층 화면으로 돌아오면 불러옵니다.';
|
||||
if(input){const transfer=new DataTransfer();transfer.items.add(new File([result.text],result.filename,{type:'application/json'}));input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;return;}
|
||||
}
|
||||
if(Date.now()>deadline)throw new Error('응답 대기 시간이 지났습니다. 결과 폴더를 확인하세요.');setTimeout(poll,2000);
|
||||
}catch(error){status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
|
||||
};setTimeout(poll,1000);
|
||||
}catch(error){status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
|
||||
};
|
||||
});
|
||||
// Closing hides the panel; the current job and its polling continue.
|
||||
document.addEventListener('close',event=>{if(event.target===popup)opening=false;},true);
|
||||
@@ -0,0 +1,5 @@
|
||||
.ground-popup{box-sizing:border-box;width:min(860px,94vw);max-height:90dvh;overflow:auto;border:1px solid #d2dfed;border-radius:16px;padding:26px;background:#fff;color:#14263d;box-shadow:0 24px 90px #10263f40;font-family:inherit}
|
||||
.ground-popup::backdrop{background:#10263f70;backdrop-filter:blur(3px)}
|
||||
.ground-popup header{display:flex;align-items:center;justify-content:space-between}.ground-popup h2{margin:6px 0 16px;font-size:23px}.ground-popup small{color:#087fcb;letter-spacing:.14em}
|
||||
.ground-popup button{font:inherit;cursor:pointer;padding:9px 18px;border-radius:7px;border:1px solid #cbd9e8;background:#f2f7fc;color:#14263d}.ground-popup button:disabled{opacity:.5;cursor:wait}.ground-popup [data-run]{background:#087fcb;color:white;border-color:#087fcb;margin-top:18px}.ground-popup label{display:block;font-weight:600;margin-top:20px}.ground-popup input{display:block;margin-top:9px;max-width:100%;font:inherit}.ground-popup [data-drawing-name],.ground-popup [data-excel-name]{overflow-wrap:anywhere;font-size:12px;color:#64748b;margin-top:7px}.ground-popup-note{font-size:12px;color:#64748b}.ground-popup pre{height:210px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f8fc;border:1px solid #dce6f0;border-radius:8px;padding:12px;font-size:12px}.ground-popup footer{text-align:right}.ground-popup [data-status]{font-size:13px}
|
||||
.ground-popup .ground-file-row{display:flex;align-items:center;gap:14px;margin-top:10px;padding:12px 14px;border:1px solid #dce6f0;border-radius:8px;background:#f8fafc}.ground-popup .ground-file-row span{flex:1;min-width:0;overflow-wrap:anywhere;font-size:14px;color:#14263d;margin:0}.ground-popup .ground-file-row button{flex-shrink:0}.ground-popup input[hidden]{display:none}
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Two-file, AI-free ground-model generator."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import json
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
@@ -10,16 +11,34 @@ from tkinter import filedialog, messagebox, scrolledtext, ttk
|
||||
from .drawing_job import ROOT, build_from_files
|
||||
|
||||
|
||||
def read_launch_context():
|
||||
filename=os.environ.get('PIERZAINER_GROUND_CONTEXT')
|
||||
if not filename:return {}
|
||||
file=Path(filename).resolve()
|
||||
if not file.is_relative_to((ROOT/'data/cache').resolve()):raise ValueError('지반 연동 설정 경로가 올바르지 않습니다.')
|
||||
data=json.loads(file.read_text(encoding='utf-8'))
|
||||
for key,extensions in [('drawing',('.dwg','.dxf')),('piers',('.xlsx',))]:
|
||||
value=data.get(key,'')
|
||||
if value and (not Path(value).is_file() or Path(value).suffix.lower() not in extensions):raise ValueError(f'{key} 입력 파일이 없습니다.')
|
||||
if data.get('output') and not Path(data['output']).resolve().is_relative_to((ROOT/'data/output').resolve()):raise ValueError('결과 폴더는 지반 프로젝트 내부여야 합니다.')
|
||||
if data.get('ready_file') and not Path(data['ready_file']).resolve().is_relative_to((ROOT/'data/cache').resolve()):raise ValueError('실행 확인 경로가 올바르지 않습니다.')
|
||||
return data
|
||||
|
||||
|
||||
class GroundModelApp(tk.Tk):
|
||||
def __init__(self):
|
||||
def __init__(self,context=None):
|
||||
super().__init__()
|
||||
self.title('Pierzainer 지반모델링 · 도면 + Excel 자동 생성 v2')
|
||||
self.geometry('1000x640');self.minsize(850,540)
|
||||
self.drawing=tk.StringVar();self.piers=tk.StringVar()
|
||||
context=context or {};self._linked_output=context.get('output')
|
||||
self.drawing=tk.StringVar(value=context.get('drawing',''));self.piers=tk.StringVar(value=context.get('piers',''))
|
||||
self.output=tk.StringVar(value=str(ROOT/'data/output'))
|
||||
self.status=tk.StringVar(value='종평면도와 교각 Excel을 선택하세요')
|
||||
self.messages=queue.Queue();self.busy=False
|
||||
self._build();self.after(100,self._poll)
|
||||
if self._linked_output or self.drawing.get():self._output_folder()
|
||||
if context.get('message'):self.status.set(context['message'])
|
||||
if context.get('ready_file'):Path(context['ready_file']).write_text('ready',encoding='utf-8')
|
||||
|
||||
def _build(self):
|
||||
main=ttk.Frame(self,padding=16);main.pack(fill='both',expand=True)
|
||||
@@ -28,12 +47,12 @@ class GroundModelApp(tk.Tk):
|
||||
for row,key,label in ((1,'drawing','① 종평면도 DXF/DWG'),(2,'piers','② 교각 Excel')):
|
||||
ttk.Label(main,text=label).grid(row=row,column=0,sticky='w',pady=6)
|
||||
ttk.Entry(main,textvariable=getattr(self,key)).grid(row=row,column=1,sticky='ew',padx=10)
|
||||
ttk.Button(main,text='찾기',command=lambda k=key:self._browse(k)).grid(row=row,column=2)
|
||||
ttk.Button(main,text='종평면도 선택하기' if key=='drawing' else 'Excel 선택하기',command=lambda k=key:self._browse(k)).grid(row=row,column=2)
|
||||
ttk.Label(main,text='결과 저장 위치').grid(row=3,column=0,sticky='w',pady=6)
|
||||
ttk.Entry(main,textvariable=self.output,state='readonly').grid(row=3,column=1,columnspan=2,sticky='ew',padx=(10,0))
|
||||
ttk.Label(main,text='두 파일만 선택하면 지반면 + 위성사진 + 2D 지도를 생성합니다. AI 연결 없이 OSM/Esri 인터넷 지도를 사용합니다.\n원본에 좌표계 표기가 없으면 프로젝트 기본 EPSG:5186을 적용하고, 좌표계 가정으로 표시합니다.',foreground='#4f6070',wraplength=920).grid(row=4,column=0,columnspan=3,sticky='w',pady=12)
|
||||
buttons=ttk.Frame(main);buttons.grid(row=5,column=0,columnspan=3,sticky='ew',pady=6)
|
||||
self.run_button=ttk.Button(buttons,text='지반 모델 자동 생성',command=self._start_run);self.run_button.pack(side='left')
|
||||
self.run_button=ttk.Button(buttons,text='지반 생성',command=self._start_run);self.run_button.pack(side='left')
|
||||
ttk.Button(buttons,text='결과 폴더 열기',command=self._open_out).pack(side='left',padx=10)
|
||||
ttk.Label(main,textvariable=self.status).grid(row=6,column=0,columnspan=3,sticky='w',pady=6)
|
||||
self.log=scrolledtext.ScrolledText(main,height=18,font=('Consolas',10))
|
||||
@@ -52,7 +71,7 @@ class GroundModelApp(tk.Tk):
|
||||
|
||||
def _output_folder(self):
|
||||
name=re.sub(r'[^\w.-]+','_',Path(self.drawing.get()).stem).strip('.') or 'new_drawing'
|
||||
self.output.set(str(ROOT/'data/output'/('drawing_'+name)))
|
||||
self.output.set(self._linked_output or str(ROOT/'data/output'/('drawing_'+name)))
|
||||
|
||||
def _open_out(self):
|
||||
path=Path(self.output.get())
|
||||
@@ -93,7 +112,7 @@ class GroundModelApp(tk.Tk):
|
||||
|
||||
|
||||
def main():
|
||||
app=GroundModelApp();app.mainloop();return 0
|
||||
app=GroundModelApp(read_launch_context());app.mainloop();return 0
|
||||
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Run the existing generator for the in-app dialog, without a native window."""
|
||||
import sys
|
||||
from .drawing_job import build_from_files
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
build_from_files(sys.argv[1], sys.argv[2], lambda message: print(message, flush=True), output=sys.argv[3])
|
||||
except Exception as error:
|
||||
print(str(error), file=sys.stderr, flush=True)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,29 @@
|
||||
import os,json,unittest,uuid
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from pierzainer_ground.app import read_launch_context,GroundModelApp,ROOT
|
||||
|
||||
class AppLinkTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cache=ROOT/'data/cache'/('gui_link_test_'+uuid.uuid4().hex);self.cache.mkdir(parents=True)
|
||||
self.excel=ROOT.parent/'도면추출/samples/샘플01_산외교_교각입력데이터.xlsx'
|
||||
self.drawing=ROOT/'data/input/sanwoe/02.종평면도(울산)_2017.0619-수정.dwg'
|
||||
def config(self,drawing=''):
|
||||
return dict(piers=str(self.excel),drawing=drawing,output=str(ROOT/'data/output'/self.cache.name),ready_file=str(self.cache/'ready'))
|
||||
def test_excel_has_empty_drawing_and_keeps_workbook(self):
|
||||
file=self.cache/'context.json';file.write_text(json.dumps(self.config()),encoding='utf-8')
|
||||
with patch.dict(os.environ,PIERZAINER_GROUND_CONTEXT=str(file)):
|
||||
context=read_launch_context();self.assertEqual(context['drawing'],'');self.assertEqual(context['piers'],str(self.excel))
|
||||
def test_native_gui_prefills_both_and_calls_existing_pipeline(self):
|
||||
context=self.config(str(self.drawing));app=GroundModelApp(context);app.withdraw()
|
||||
try:
|
||||
self.assertEqual(app.drawing.get(),str(self.drawing));self.assertEqual(app.piers.get(),str(self.excel));self.assertEqual(app.output.get(),context['output']);self.assertEqual(app.run_button['text'],'지반 생성')
|
||||
with patch('pierzainer_ground.app.build_from_files',return_value={'map_status':{'status':'mapped'}}) as run:
|
||||
app._start_run();kind,message=app.messages.get(timeout=3);self.assertEqual(kind,'done');self.assertEqual(run.call_args.args[:2],(str(self.drawing),str(self.excel)));self.assertEqual(run.call_args.kwargs['output'],context['output'])
|
||||
finally:
|
||||
for token in app.tk.call('after','info'):app.after_cancel(token)
|
||||
app.destroy()
|
||||
def test_bad_output_is_rejected(self):
|
||||
context=self.config();context['output']=str(ROOT.parent);file=self.cache/'context.json';file.write_text(json.dumps(context),encoding='utf-8')
|
||||
with patch.dict(os.environ,PIERZAINER_GROUND_CONTEXT=str(file)),self.assertRaises(ValueError):read_launch_context()
|
||||
if __name__=='__main__':unittest.main()
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 01 · 산외교","bridge":"산외교","client":"","author":"","crs":"EPSG:5187","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(inferred_location_checked)."},"source":{"fileName":"샘플01_산외교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":71.0632,"footingBottomEl":42.763,"groundEl":52.099,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":180993.7987,"y":324479.775,"angleDeg":0},{"name":"IP2","x":180993.0547,"y":324478.3023,"angleDeg":0},{"name":"IP3","x":180988.2522,"y":324468.7966,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":180991.8227,"y":324477.5242,"z":71.4178,"azimuthDeg":296.79913888888893,"row":"전열"},{"no":2,"x":180988.9134,"y":324471.7116,"z":71.2882,"azimuthDeg":296.7994166666667,"row":"전열"},{"no":3,"x":180993.2484,"y":324476.8038,"z":71.4401,"azimuthDeg":296.81624999999997,"row":"후열"},{"no":4,"x":180990.2925,"y":324471.0147,"z":71.3096,"azimuthDeg":296.81597222222223,"row":"후열"}],"bearingAzimuthFrame":"model-xy"}},{"id":"P2","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P2","sheet":"Pier","type":"T형","copingTopEl":71.9164,"footingBottomEl":38.916,"groundEl":41.319,"foundationType":"말뚝기초","pileLengthMm":7000,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":181033.8891,"y":324459.3055,"angleDeg":0},{"name":"IP2","x":181033.1327,"y":324457.8391,"angleDeg":0},{"name":"IP3","x":181028.2505,"y":324448.3741,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":181031.8943,"y":324457.0714,"z":72.2629,"azimuthDeg":297.2810833333333,"row":"전열"},{"no":2,"x":181028.9384,"y":324451.2823,"z":72.1334,"azimuthDeg":297.2813611111111,"row":"전열"},{"no":3,"x":181033.3136,"y":324456.3392,"z":72.2932,"azimuthDeg":297.29819444444445,"row":"후열"},{"no":4,"x":181030.3037,"y":324450.578,"z":72.1626,"azimuthDeg":297.29783333333336,Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 02 · 운곡교","bridge":"운곡교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플02_운곡교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":259864.578,"z":43.84,"azimuthDeg":14.036000000000001,"row":"전열"},{"no":5,"x":179142.867,"y":259864.1591,"z":43.912,"azimuthDeg":14.031000000000006,"row":"전열"},{"no":6,"x":179133.2876,"y":259867.0998,"z":43.6,"azimuthDeg":12.74124999999998,"row":"후열"},{"no":7,"x":179135.7577,"y":259866.6809,"z":43.672,"azimuthDeg":12.737694444444458,"row":"후열"},{"no":8,"x":179138.2278,"y":259866.262,"z":43.744,"azimuthDeg":12.734138888888879,"row":"후열"},{"no":9,"x":179140.6978,"y":259865.8431,"z":43.816,"azimuthDeg":12.730611111111102,"row":"후열"},{"no":10,"x":179143.1679,"y":259865.4242,"z":43.887,"azimuthDeg":12.72705555555558,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":259864.578,"z":43.84,"azimuthDeg":14.036000000000001,"row":"전열"},{"no":5,"x":179142.867,"y":259864.1591,"z":43.912,"azimuthDeg":14.031000000000006,"row":"전열"},{"no":6,"x":179133.2876,"y":259867.0998,"z":43.6,"azimuthDeg":12.74124999999998,"row":"후열"},{"no":7,"x":179135.7577,"y":259866.6809,"z":43.672,"azimuthDeg":12.737694444444458,"row":"후열"},{"no":8,"x":179138.2278,"y":259866.262,"z":43.744,"azimuthDeg":12.734138888888879,"row":"후열"},{"no":9,"x":179140.6978,"y":259865.8431,"z":43.816,"azimuthDeg":12.730611111111102,"row":"후열"},{"no":10,"x":179143.1679,"y":259865.4242,"z":43.887,"azimuthDeg":12.72705555555558,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":43.411,"footingBottomEl":26.208,"groundEl":28.931,"foundationType":"말뚝기초","pileLengthMm":24400,"copingWidthMm":3500,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":179132.091213,"y":259866.694501,"angleDeg":0},{"name":"IP2","x":179144.095968,"y":259864.658395,"angleDeg":0},{"name":"IP3","x":179144.086088,"y":259864.66007,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":179132.9863,"y":259865.8348,"z":43.625,"azimuthDeg":14.051138888888886,"row":"전열"},{"no":2,"x":179135.4565,"y":259865.4158,"z":43.697,"azimuthDeg":14.046083333333343,"row":"전열"},{"no":3,"x":179137.9267,"y":259864.9969,"z":43.768,"azimuthDeg":14.041055555555545,"row":"전열"},{"no":4,"x":179140.3968,"y":25986Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 03 · 대호지교","bridge":"대호지교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플03_대호지교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":7.738,"footingBottomEl":1.4,"groundEl":-0.342,"foundationType":"현장타설말뚝","pileLengthMm":29802,"copingWidthMm":3500,"copingLengthMm":24500,"columnCount":2,"ips":[{"name":"IP1","x":153837.294388,"y":480726.410803,"angleDeg":0},{"name":"IP2","x":153833.31775,"y":480714.93,"angleDeg":0},{"name":"IP3","x":153829.341111,"y":480703.449196,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":153836.266,"y":480725.3912,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":2,"x":153835.2514,"y":480722.462,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":3,"x":153834.2368,"y":480719.5327,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":4,"x":153833.2222,"y":480716.6034,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":5,"x":153832.2076,"y":480713.6742,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":6,"x":153831.193,"y":480710.7449,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":7,"x":153830.1783,"y":480707.8157,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":8,"x":153829.1637,"y":480704.8864,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":9,"x":153837.4944,"y":480724.9657,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":10,"x":153836.4798,"y":480722.0365,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":11,"x":153835.4652,"y":480719.1072,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":12,"x":153834.4506,"y":480716.178,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":13,"x":153833.436,"y":480713.2487,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":14,"x":153832.4213,"y":480710.3195,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":15,"x":153831.4067,"y":480707.3902,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":16,"x":153830.3921,"y":480704.4609,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":7.738,"footingBottomEl":1.4,"groundEl":-0.342,"foundationType":"현장타설말뚝","pileLengthMm":29802,"copingWidthMm":3500,"copingLengthMm":24500,"columnCount":2,"ips":[{"name":"IP1","x":153837.294388,"y":480726.410803,"angleDeg":0},{"name":"IP2","x":153833.31775,"y":480714.93,"angleDeg":0},{"name":"IP3","x":153829.341111,"y":480703.449196,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":153836.266,"y":480725.3912,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":2,"x":153835.2514,"y":480722.462,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":3,"x":153834.2368,"y":480719.5327,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":4,"x":153833.2222,"y":480716.6034,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":5,"x":153832.2076,"y":480713.6742,"z":8.0038,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":6,"x":153831.193,"y":480710.7449,"z":7.9418,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":7,"x":153830.1783,"y":480707.8157,"z":7.8798,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":8,"x":153829.1637,"y":480704.8864,"z":7.8178,"azimuthDeg":109.10472222222222,"row":"전열"},{"no":9,"x":153837.4944,"y":480724.9657,"z":7.8243,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":10,"x":153836.4798,"y":480722.0365,"z":7.8863,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":11,"x":153835.4652,"y":480719.1072,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":12,"x":153834.4506,"y":480716.178,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":13,"x":153833.436,"y":480713.2487,"z":8.0103,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":14,"x":153832.4213,"y":480710.3195,"z":7.9483,"azimuthDeg":109.10472222222222,"row":"후열"},{"no":15,"x":153831.4067,"y":480707.3902,"z":7.8863,"azimuthDeg":109.10472222222222,"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 04 · 목동교","bridge":"목동교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플04_목동교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3","x":218140.182759,"y":531075.355006,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":218129.3712,"y":531065.8269,"z":133.9411,"azimuthDeg":321.06191666666666,"row":"전열"},{"no":2,"x":218131.265,"y":531067.3497,"z":133.9896,"azimuthDeg":321.0619722222222,"row":"전열"},{"no":3,"x":218133.3535,"y":531069.0291,"z":134.043,"azimuthDeg":321.06202777777776,"row":"전열"},{"no":4,"x":218135.4421,"y":531070.7086,"z":134.0965,"azimuthDeg":321.06208333333336,"row":"전열"},{"no":5,"x":218137.5306,"y":531072.3881,"z":134.15,"azimuthDeg":321.06213888888885,"row":"전열"},{"no":6,"x":218139.4243,"y":531073.9109,"z":134.1985,"azimuthDeg":321.06219444444446,"row":"전열"},{"no":7,"x":218128.5503,"y":531066.8348,"z":133.953,"azimuthDeg":320.60875,"row":"후열"},{"no":8,"x":218130.4438,"y":531068.3578,"z":134.0015,"azimuthDeg":320.60897222222223,"row":"후열"},{"no":9,"x":218132.5324,"y":531070.0373,"z":134.055,"azimuthDeg":320.60922222222223,"row":"후열"},{"no":10,"x":218134.6209,"y":531071.7168,"z":134.1084,"azimuthDeg":320.6094444444444,"row":"후열"},{"no":11,"x":218136.7095,"y":531073.3962,"z":134.1619,"azimuthDeg":320.60966666666667,"row":"후열"},{"no":12,"x":218138.6034,"y":531074.9188,"z":134.2103,"azimuthDeg":320.6098888888889,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3","x":218140.182759,"y":531075.355006,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":218129.3712,"y":531065.8269,"z":133.9411,"azimuthDeg":321.06191666666666,"row":"전열"},{"no":2,"x":218131.265,"y":531067.3497,"z":133.9896,"azimuthDeg":321.0619722222222,"row":"전열"},{"no":3,"x":218133.3535,"y":531069.0291,"z":134.043,"azimuthDeg":321.06202777777776,"row":"전열"},{"no":4,"x":218135.4421,"y":531070.7086,"z":134.0965,"azimuthDeg":321.06208333333336,"row":"전열"},{"no":5,"x":218137.5306,"y":531072.3881,"z":134.15,"azimuthDeg":321.06213888888885,"row":"전열"},{"no":6,"x":218139.4243,"y":531073.9109,"z":134.1985,"azimuthDeg":321.06219444444446,"row":"전열"},{"no":7,"x":218128.5503,"y":531066.8348,"z":133.953,"azimuthDeg":320.60875,"row":"후열"},{"no":8,"x":218130.4438,"y":531068.3578,"z":134.0015,"azimuthDeg":320.60897222222223,"row":"후열"},{"no":9,"x":218132.5324,"y":531070.0373,"z":134.055,"azimuthDeg":320.60922222222223,"row":"후열"},{"no":10,"x":218134.6209,"y":531071.7168,"z":134.1084,"azimuthDeg":320.6094444444444,"row":"후열"},{"no":11,"x":218136.7095,"y":531073.3962,"z":134.1619,"azimuthDeg":320.60966666666667,"row":"후열"},{"no":12,"x":218138.6034,"y":531074.9188,"z":134.2103,"azimuthDeg":320.6098888888889,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":133.8541,"footingBottomEl":110.854,"groundEl":118.79,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":16500,"columnCount":2,"ips":[{"name":"IP1","x":218127.628104,"y":531065.25932,"angleDeg":0},{"name":"IP2","x":218138.896901,"y":531074.320997,"angleDeg":0},{"name":"IP3"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 05 · 승천천교","bridge":"승천천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플05_승천천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[],"history":[{"version":"sample-20261102","note":"교각 Excel과 원도면 지반·위성사진·2D 지도 통합. 좌표·스키마·형상 검증.","at":"2026-11-02T14:34:50.364983"}],"bearingBasis":"excel","groundModel":{"schema":2,"generated":"2026-11-02","piers":[{"id":"P1","centerEl":55,"excelEl":null}],"terrainGrid":{"origin":[458809.512,224222.818],"step":5,"nx":41,"ny":41,"z":[null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.322,55.896,55.661,55.11,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.689,56.161,55.677,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.78,55.857,55.371,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55.009,55.119,55.196,55.273,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,55,null,null,null,nullLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 06 · 계문철교","bridge":"계문철교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플06_계문철교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":18.5041,"footingBottomEl":4.804,"groundEl":7.06,"foundationType":"말뚝기초","pileLengthMm":21000,"copingWidthMm":3500,"copingLengthMm":21000,"columnCount":2,"ips":[{"name":"IP1","x":194908.7711,"y":376920.628019,"angleDeg":0},{"name":"IP2","x":194909.621705,"y":376910.212695,"angleDeg":0},{"name":"IP3","x":194910.472309,"y":376899.797371,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":194908.2982,"y":376919.5055,"z":19.6384,"azimuthDeg":83.45638888888891,"row":"전열"},{"no":2,"x":194908.5459,"y":376916.4756,"z":19.4854,"azimuthDeg":83.447,"row":"전열"},{"no":3,"x":194908.7936,"y":376913.4458,"z":19.3333,"azimuthDeg":83.43755555555555,"row":"전열"},{"no":4,"x":194909.0413,"y":376910.4159,"z":19.1822,"azimuthDeg":83.42797222222225,"row":"전열"},{"no":5,"x":194909.2891,"y":376907.386,"z":19.0292,"azimuthDeg":83.41830555555555,"row":"전열"},{"no":6,"x":194909.5368,"y":376904.3561,"z":18.8771,"azimuthDeg":83.40855555555555,"row":"전열"},{"no":7,"x":194909.7845,"y":376901.3262,"z":18.7251,"azimuthDeg":83.39869444444446,"row":"전열"},{"no":8,"x":194909.4601,"y":376919.6004,"z":19.6539,"azimuthDeg":87.6826388888889,"row":"후열"},{"no":9,"x":194909.7075,"y":376916.5705,"z":19.5019,"azimuthDeg":87.69441666666665,"row":"후열"},{"no":10,"x":194909.9548,"y":376913.5406,"z":19.3499,"azimuthDeg":87.7063333333333,"row":"후열"},{"no":11,"x":194910.2022,"y":376910.5106,"z":19.1979,"azimuthDeg":87.71836111111111,"row":"후열"},{"no":12,"x":194910.4496,"y":376907.4807,"z":19.0459,"azimuthDeg":87.7305,"row":"후열"},{"no":13,"x":194910.6969,"y":376904.4508,"z":18.8939,"azimuthDeg":87.74277777777775,"row":"후열"},{"no":14,"x":194910.9443,"y":376901.4209,"z":18.7419,"azimuthDeg":87.7551666666667,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":18.5041,"footingBottomEl":4.804,"groundEl":7.06,"foundationType":"말뚝기초","pileLengthMm":21000,"copingWidthMm":3500,"copingLengthMm":21000,"columnCount":2,"ips":[{"name":"IP1","x":194908.7711,"y":376920.628019,"angleDeg":0},{"name":"IP2","x":194909.621705,"y":376910.212695,"angleDeg":0},{"name":"IP3","x":194910.472309,"y":376899.797371,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":194908.2982,"y":376919.5055,"z":19.6384,"azimuthDeg":83.45638888888891,"row":"전열"},{"no":2,"x":194908.5459,"y":376916.4756,"z":19.4854,"azimuthDeg":83.447,"row":"전열"},{"no":3,"x":194908.7936,"y":376913.4458,"z":19.3333,"azimuthDeg":83.43755555555555,"row":"전열"},{"no":4,"x":194909.0413,"y":376910.4159,"z":19.1822,"azimuthDeg":83.42797222222225,"row":"전열"},{"no":5,"x":194909.2891,"y":376907.386,"z":19.0292,"azimuthDeg":83.41830555555555,"row":"전열"},{"no":6,"x":194909.5368,"y":376904.3561,"z":18.8771,"azimuthDeg":83.40855555555555,"row":"전열"},{"no":7,"x":194909.7845,"y":376901.3262,"z":18.7251,"azimuthDeg":83.39869444444446,"row":"전열"},{"no":8,"x":194909.4601,"y":376919.6004,"z":19.6539,"azimuthDeg":87.6826388888889,"row":"후열"},{"no":9,"x":194909.7075,"y":376916.5705,"z":19.5019,"azimuthDeg":87.69441666666665,"row":"후열"},{"no":10,"x":194909.9548,"y":376913.5406,"z":19.3499,"azimuthDeg":87.7063333333333,"row":"후열"},{"no":11,"x":194910.2022,"y":376910.5106,"z":19.1979,"azimuthDeg":87.71836111111111,"row":"후열"},{"no":12,"x":194910.4496,"y":376907.4807,"z":19.0459,"azimuthDeg":87.7305,"row":"후열"},{"no":13,"x":194910.6969,"y":376904.4508,"z":18.8939,"azimuthDeg":87.74277777777775,"row":"후열"},{"no":14,"x":194910.9443,"y":376901.4209,"z":18.7419,"azimuthDeg":87.7551666666667,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"PieLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 07 · 기양천교","bridge":"기양천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플07_기양천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":15.7665,"footingBottomEl":0.966,"groundEl":5.97,"foundationType":"말뚝기초","pileLengthMm":13600,"copingWidthMm":3500,"copingLengthMm":22500,"columnCount":2,"ips":[{"name":"IP1","x":195901.933872,"y":376676.684353,"angleDeg":0},{"name":"IP2","x":195904.750042,"y":376666.017388,"angleDeg":0},{"name":"IP3","x":195907.564243,"y":376655.357883,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":195901.7932,"y":376675.2306,"z":15.9498,"azimuthDeg":94.7138333333333,"row":"전열"},{"no":2,"x":195902.6065,"y":376672.1472,"z":16.0318,"azimuthDeg":94.67783333333335,"row":"전열"},{"no":3,"x":195903.4195,"y":376669.0643,"z":16.1134,"azimuthDeg":94.642,"row":"전열"},{"no":4,"x":195904.2324,"y":376665.9822,"z":16.1956,"azimuthDeg":94.60630555555554,"row":"전열"},{"no":5,"x":195905.045,"y":376662.9006,"z":16.2764,"azimuthDeg":94.57072222222223,"row":"전열"},{"no":6,"x":195905.8574,"y":376659.8197,"z":16.3568,"azimuthDeg":94.53527777777776,"row":"전열"},{"no":7,"x":195906.6696,"y":376656.7394,"z":16.4367,"azimuthDeg":94.4999722222222,"row":"전열"},{"no":8,"x":195902.9584,"y":376675.1501,"z":15.9811,"azimuthDeg":93.19136111111112,"row":"후열"},{"no":9,"x":195903.7717,"y":376672.0673,"z":16.0627,"azimuthDeg":93.15808333333337,"row":"후열"},{"no":10,"x":195904.5848,"y":376668.9852,"z":16.1438,"azimuthDeg":93.12494444444445,"row":"후열"},{"no":11,"x":195905.3977,"y":376665.9037,"z":16.2256,"azimuthDeg":93.09191666666669,"row":"후열"},{"no":12,"x":195906.2105,"y":376662.8229,"z":16.3059,"azimuthDeg":93.05902777777777,"row":"후열"},{"no":13,"x":195907.0231,"y":376659.7426,"z":16.3859,"azimuthDeg":93.02625,"row":"후열"},{"no":14,"x":195907.8355,"y":376656.663,"z":16.4654,"azimuthDeg":92.99358333333333,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":15.7665,"footingBottomEl":0.966,"groundEl":5.97,"foundationType":"말뚝기초","pileLengthMm":13600,"copingWidthMm":3500,"copingLengthMm":22500,"columnCount":2,"ips":[{"name":"IP1","x":195901.933872,"y":376676.684353,"angleDeg":0},{"name":"IP2","x":195904.750042,"y":376666.017388,"angleDeg":0},{"name":"IP3","x":195907.564243,"y":376655.357883,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":195901.7932,"y":376675.2306,"z":15.9498,"azimuthDeg":94.7138333333333,"row":"전열"},{"no":2,"x":195902.6065,"y":376672.1472,"z":16.0318,"azimuthDeg":94.67783333333335,"row":"전열"},{"no":3,"x":195903.4195,"y":376669.0643,"z":16.1134,"azimuthDeg":94.642,"row":"전열"},{"no":4,"x":195904.2324,"y":376665.9822,"z":16.1956,"azimuthDeg":94.60630555555554,"row":"전열"},{"no":5,"x":195905.045,"y":376662.9006,"z":16.2764,"azimuthDeg":94.57072222222223,"row":"전열"},{"no":6,"x":195905.8574,"y":376659.8197,"z":16.3568,"azimuthDeg":94.53527777777776,"row":"전열"},{"no":7,"x":195906.6696,"y":376656.7394,"z":16.4367,"azimuthDeg":94.4999722222222,"row":"전열"},{"no":8,"x":195902.9584,"y":376675.1501,"z":15.9811,"azimuthDeg":93.19136111111112,"row":"후열"},{"no":9,"x":195903.7717,"y":376672.0673,"z":16.0627,"azimuthDeg":93.15808333333337,"row":"후열"},{"no":10,"x":195904.5848,"y":376668.9852,"z":16.1438,"azimuthDeg":93.12494444444445,"row":"후열"},{"no":11,"x":195905.3977,"y":376665.9037,"z":16.2256,"azimuthDeg":93.09191666666669,"row":"후열"},{"no":12,"x":195906.2105,"y":376662.8229,"z":16.3059,"azimuthDeg":93.05902777777777,"row":"후열"},{"no":13,"x":195907.0231,"y":376659.7426,"z":16.3859,"azimuthDeg":93.02625,"row":"후열"},{"no":14,"x":195907.8355,"y":376656.663,"z":16.4654,"azimuthDeg":92.99358333333333,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheetLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 08 · 옥성교","bridge":"옥성교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플08_옥성교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":4,"x":207597.1436,"y":429975.9134,"z":21.4115,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":5,"x":207594.814,"y":429975.0741,"z":21.3509,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":6,"x":207604.868,"y":429977.4835,"z":21.4193,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":7,"x":207602.5384,"y":429976.6442,"z":21.4529,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":8,"x":207600.2088,"y":429975.8048,"z":21.4865,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":9,"x":207597.8792,"y":429974.9655,"z":21.4326,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":10,"x":207595.5496,"y":429974.1262,"z":21.372,"azimuthDeg":142.18688888888886,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":4,"x":207597.1436,"y":429975.9134,"z":21.4115,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":5,"x":207594.814,"y":429975.0741,"z":21.3509,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":6,"x":207604.868,"y":429977.4835,"z":21.4193,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":7,"x":207602.5384,"y":429976.6442,"z":21.4529,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":8,"x":207600.2088,"y":429975.8048,"z":21.4865,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":9,"x":207597.8792,"y":429974.9655,"z":21.4326,"azimuthDeg":142.18688888888886,"row":"후열"},{"no":10,"x":207595.5496,"y":429974.1262,"z":21.372,"azimuthDeg":142.18688888888886,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":21.1279,"footingBottomEl":12.175,"groundEl":18.09,"foundationType":"말뚝기초","pileLengthMm":22000,"copingWidthMm":3500,"copingLengthMm":13500,"columnCount":1,"ips":[{"name":"IP1","x":207605.780013,"y":429978.461366,"angleDeg":0},{"name":"IP2","x":207599.646855,"y":429976.251707,"angleDeg":0},{"name":"IP3","x":207593.513698,"y":429974.042048,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":207604.1324,"y":429978.4314,"z":21.3981,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":2,"x":207601.8028,"y":429977.5921,"z":21.4317,"azimuthDeg":142.18688888888886,"row":"전열"},{"no":3,"x":207599.4732,"y":429976.7528,"z":21.4653,"azimuthDeg":142.18688888888886,"row":"전열Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 09 · 새재1교","bridge":"새재1교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플09_새재1교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":208540.3859,"y":428105.3832,"z":33.0931,"azimuthDeg":139.0895277777778,"row":"전열"},{"no":5,"x":208538.4314,"y":428103.8136,"z":33.1941,"azimuthDeg":139.0984166666667,"row":"전열"},{"no":6,"x":208546.9329,"y":428109.3626,"z":32.7757,"azimuthDeg":134.73080555555555,"row":"후열"},{"no":7,"x":208544.978,"y":428107.7926,"z":32.8767,"azimuthDeg":134.75805555555553,"row":"후열"},{"no":8,"x":208543.0231,"y":428106.2227,"z":32.9777,"azimuthDeg":134.78505555555557,"row":"후열"},{"no":9,"x":208541.0682,"y":428104.6528,"z":33.0787,"azimuthDeg":134.8118333333333,"row":"후열"},{"no":10,"x":208539.1134,"y":428103.083,"z":33.1797,"azimuthDeg":134.83838888888891,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":208540.3859,"y":428105.3832,"z":33.0931,"azimuthDeg":139.0895277777778,"row":"전열"},{"no":5,"x":208538.4314,"y":428103.8136,"z":33.1941,"azimuthDeg":139.0984166666667,"row":"전열"},{"no":6,"x":208546.9329,"y":428109.3626,"z":32.7757,"azimuthDeg":134.73080555555555,"row":"후열"},{"no":7,"x":208544.978,"y":428107.7926,"z":32.8767,"azimuthDeg":134.75805555555553,"row":"후열"},{"no":8,"x":208543.0231,"y":428106.2227,"z":32.9777,"azimuthDeg":134.78505555555557,"row":"후열"},{"no":9,"x":208541.0682,"y":428104.6528,"z":33.0787,"azimuthDeg":134.8118333333333,"row":"후열"},{"no":10,"x":208539.1134,"y":428103.083,"z":33.1797,"azimuthDeg":134.83838888888891,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":32.5316,"footingBottomEl":10.032,"groundEl":16.95,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":208547.804218,"y":428110.699674,"angleDeg":0},{"name":"IP2","x":208542.956234,"y":428106.806474,"angleDeg":0},{"name":"IP3","x":208538.108532,"y":428102.913503,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":208546.25,"y":428110.0923,"z":32.7901,"azimuthDeg":139.06241666666665,"row":"전열"},{"no":2,"x":208544.2952,"y":428108.5226,"z":32.8911,"azimuthDeg":139.07152777777776,"row":"전열"},{"no":3,"x":208542.3406,"y":428106.9529,"z":32.9921,"azimuthDeg":139.08055555555552,"row":"전열"},{"no":4,"x":2085Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 10 · 도하4교","bridge":"도하4교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플10_도하4교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"T형","copingTopEl":98.552,"footingBottomEl":78.852,"groundEl":85.365,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":200987.223238,"y":583704.910196,"angleDeg":0},{"name":"IP2","x":200987.580804,"y":583703.299406,"angleDeg":0},{"name":"IP3","x":200989.888734,"y":583692.902485,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":200987.095,"y":583702.013,"z":98.859,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":2,"x":200988.549,"y":583695.473,"z":98.725,"azimuthDeg":77.47163888888889,"row":"전열"},{"no":3,"x":200988.565,"y":583702.34,"z":98.847,"azimuthDeg":77.49702777777776,"row":"후열"},{"no":4,"x":200990.015,"y":583695.798,"z":98.713,"azimuthDeg":77.49700000000001,"row":"후열"}],"bearingAzimuthFrame":"model-xy"}},{"id":"P2","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P2","sheet":"Pier","type":"T형","copingTopEl":98.108,"footingBottomEl":78.408,"groundEl":85.754,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":4000,"copingLengthMm":12500,"columnCount":1,"ips":[{"name":"IP1","x":201041.036417,"y":583716.399861,"angleDeg":0},{"name":"IP2","x":201041.367881,"y":583714.783498,"angleDeg":0},{"name":"IP3","x":201043.507331,"y":583704.350605,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":201040.861,"y":583713.505,"z":98.415,"azimuthDeg":78.39847222222221,"row":"전열"},{"no":2,"x":201042.209,"y":583706.942,"z":98.281,"azimuthDeg":78.39847222222221,"row":"전열"},{"no":3,"x":201042.336,"y":583713.808,"z":98.402,"azimuthDeg":78.42386111111114,"row":"후열"},{"no":4,"x":201043.68,"y":583707.244,"z":98.268,"azimuthDeg":78.42383333333333,"row"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 11 · 곡천교","bridge":"곡천교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(project_default_not_source_verified)."},"source":{"fileName":"샘플11_곡천교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":226999.795,"y":492646.0899,"z":87.3778,"azimuthDeg":347.36733333333336,"row":"전열"},{"no":2,"x":227002.2906,"y":492647.414,"z":87.2989,"azimuthDeg":347.3780277777778,"row":"전열"},{"no":3,"x":227004.7862,"y":492648.7381,"z":87.2201,"azimuthDeg":347.38875,"row":"전열"},{"no":4,"x":227007.282,"y":492650.0623,"z":87.1413,"azimuthDeg":347.39947222222224,"row":"전열"},{"no":5,"x":227009.7779,"y":492651.3866,"z":87.0623,"azimuthDeg":347.4102222222222,"row":"전열"},{"no":6,"x":227012.2739,"y":492652.7109,"z":86.9834,"azimuthDeg":347.421,"row":"전열"},{"no":7,"x":226999.5196,"y":492647.3558,"z":87.3832,"azimuthDeg":348.11983333333336,"row":"후열"},{"no":8,"x":227002.0154,"y":492648.6799,"z":87.3043,"azimuthDeg":348.1310555555556,"row":"후열"},{"no":9,"x":227004.5113,"y":492650.0041,"z":87.2254,"azimuthDeg":348.1422777777778,"row":"후열"},{"no":10,"x":227007.0073,"y":492651.3283,"z":87.1466,"azimuthDeg":348.15352777777775,"row":"후열"},{"no":11,"x":227009.5035,"y":492652.6527,"z":87.0676,"azimuthDeg":348.1647777777778,"row":"후열"},{"no":12,"x":227011.9998,"y":492653.9771,"z":86.9886,"azimuthDeg":348.17605555555554,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":226999.795,"y":492646.0899,"z":87.3778,"azimuthDeg":347.36733333333336,"row":"전열"},{"no":2,"x":227002.2906,"y":492647.414,"z":87.2989,"azimuthDeg":347.3780277777778,"row":"전열"},{"no":3,"x":227004.7862,"y":492648.7381,"z":87.2201,"azimuthDeg":347.38875,"row":"전열"},{"no":4,"x":227007.282,"y":492650.0623,"z":87.1413,"azimuthDeg":347.39947222222224,"row":"전열"},{"no":5,"x":227009.7779,"y":492651.3866,"z":87.0623,"azimuthDeg":347.4102222222222,"row":"전열"},{"no":6,"x":227012.2739,"y":492652.7109,"z":86.9834,"azimuthDeg":347.421,"row":"전열"},{"no":7,"x":226999.5196,"y":492647.3558,"z":87.3832,"azimuthDeg":348.11983333333336,"row":"후열"},{"no":8,"x":227002.0154,"y":492648.6799,"z":87.3043,"azimuthDeg":348.1310555555556,"row":"후열"},{"no":9,"x":227004.5113,"y":492650.0041,"z":87.2254,"azimuthDeg":348.1422777777778,"row":"후열"},{"no":10,"x":227007.0073,"y":492651.3283,"z":87.1466,"azimuthDeg":348.15352777777775,"row":"후열"},{"no":11,"x":227009.5035,"y":492652.6527,"z":87.0676,"azimuthDeg":348.1647777777778,"row":"후열"},{"no":12,"x":227011.9998,"y":492653.9771,"z":86.9886,"azimuthDeg":348.17605555555554,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":86.8393,"footingBottomEl":61.44,"groundEl":64.692,"foundationType":"직접기초","pileLengthMm":0,"copingWidthMm":3500,"copingLengthMm":17000,"columnCount":2,"ips":[{"name":"IP1","x":226998.64978,"y":492646.179517,"angleDeg":0},{"name":"IP2","x":226998.640605,"y":492646.174649,"angleDeg":0},{"name":"IP3","x":227013.515037,"y":492654.066886,"angleDeg":0},{"Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"project":{"name":"샘플 12 · 감북교","bridge":"감북교","client":"","author":"","crs":"EPSG:5186","memo":"도면추출 연동 샘플 + 원도면 지반. 설계 지반고 유지. 지도 CRS는 원자료 확인이 필요한 가정(inferred_false_northing_convention_not_source_verified)."},"source":{"fileName":"샘플12_감북교_교각입력데이터.xlsx","sheets":["Pier","IP","Bearing"]},"piers":[{"id":"P1","input":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":56.5998,"footingBottomEl":43.604,"groundEl":44.64,"foundationType":"현장타설말뚝","pileLengthMm":17566,"copingWidthMm":3000,"copingLengthMm":20000,"columnCount":2,"ips":[{"name":"IP1","x":214529.86606,"y":547194.678176,"angleDeg":0},{"name":"IP2","x":214548.146791,"y":547202.413639,"angleDeg":0},{"name":"IP3","x":214548.137582,"y":547202.409742,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":214531.1568,"y":547194.6814,"z":56.8196,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":2,"x":214533.8276,"y":547195.8116,"z":56.8776,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":3,"x":214536.4983,"y":547196.9417,"z":56.9356,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":4,"x":214539.169,"y":547198.0718,"z":56.9936,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":5,"x":214541.8398,"y":547199.2019,"z":57.0516,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":6,"x":214544.5105,"y":547200.332,"z":57.1096,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":7,"x":214547.1812,"y":547201.4622,"z":57.1676,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":8,"x":214530.7671,"y":547195.6024,"z":56.7998,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":9,"x":214533.4379,"y":547196.7325,"z":56.8578,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":10,"x":214536.1086,"y":547197.8626,"z":56.9158,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":11,"x":214538.7793,"y":547198.9927,"z":56.9738,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":12,"x":214541.4501,"y":547200.1229,"z":57.0318,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":13,"x":214544.1208,"y":547201.253,"z":57.0898,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":14,"x":214546.7916,"y":547202.3831,"z":57.1478,"azimuthDeg":337.06444444444446,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"defaults":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"pileThicknessMm":12,"copingH1Mm":1000,"copingH2Mm":1500,"levelWidthMm":2100,"name":"P1","sheet":"Pier","type":"2주형","copingTopEl":56.5998,"footingBottomEl":43.604,"groundEl":44.64,"foundationType":"현장타설말뚝","pileLengthMm":17566,"copingWidthMm":3000,"copingLengthMm":20000,"columnCount":2,"ips":[{"name":"IP1","x":214529.86606,"y":547194.678176,"angleDeg":0},{"name":"IP2","x":214548.146791,"y":547202.413639,"angleDeg":0},{"name":"IP3","x":214548.137582,"y":547202.409742,"angleDeg":0},{"name":"IP4","x":0,"y":0,"angleDeg":0}],"bearings":[{"no":1,"x":214531.1568,"y":547194.6814,"z":56.8196,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":2,"x":214533.8276,"y":547195.8116,"z":56.8776,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":3,"x":214536.4983,"y":547196.9417,"z":56.9356,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":4,"x":214539.169,"y":547198.0718,"z":56.9936,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":5,"x":214541.8398,"y":547199.2019,"z":57.0516,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":6,"x":214544.5105,"y":547200.332,"z":57.1096,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":7,"x":214547.1812,"y":547201.4622,"z":57.1676,"azimuthDeg":337.06444444444446,"row":"전열"},{"no":8,"x":214530.7671,"y":547195.6024,"z":56.7998,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":9,"x":214533.4379,"y":547196.7325,"z":56.8578,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":10,"x":214536.1086,"y":547197.8626,"z":56.9158,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":11,"x":214538.7793,"y":547198.9927,"z":56.9738,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":12,"x":214541.4501,"y":547200.1229,"z":57.0318,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":13,"x":214544.1208,"y":547201.253,"z":57.0898,"azimuthDeg":337.06444444444446,"row":"후열"},{"no":14,"x":214546.7916,"y":547202.3831,"z":57.1478,"azimuthDeg":337.06444444444446,"row":"후열"}],"bearingAzimuthFrame":"model-xy"},"draft":{"columnDiameterMm":2000,"footingLengthMm":10400,"footingWidthMm":8150,"footingHeightMm":2000,"blindingMarginMm":100,"blindingThicknessMm":100,"pileKind":"강관","pileOuterMm":609.6,"Line truncated
|
||||
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Headless Chrome screenshot of the sample page (WSL2 → Windows Chrome).
|
||||
# Usage: scripts/capture-sample.sh <output.png> [url]
|
||||
# Requires the dev server: npm run dev -- --host 127.0.0.1 --port 5173
|
||||
set -euo pipefail
|
||||
OUT_WSL="${1:?output png path (WSL path)}"
|
||||
URL="${2:-http://127.0.0.1:5173/?sample=1}"
|
||||
CHROME="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
|
||||
mkdir -p "$(dirname "$OUT_WSL")"
|
||||
timeout 200 "$CHROME" --headless=new --no-sandbox --use-gl=angle --use-angle=swiftshader \
|
||||
--enable-unsafe-swiftshader --enable-logging=stderr --v=0 --window-size=1600,1000 \
|
||||
--timeout=45000 --screenshot="$(wslpath -w "$OUT_WSL")" "$URL"
|
||||
@@ -0,0 +1,26 @@
|
||||
// Read-only diagnosis: concrete scene vs rebar paths for every pier/foundation combination.
|
||||
import { createServer } from 'vite';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
const server = await createServer({ configFile: false, server: { middlewareMode: true, hmr: false, watch: null }, appType: 'custom' });
|
||||
try {
|
||||
const { loadP1 } = await server.ssrLoadModule('/tests/helpers.ts');
|
||||
const { buildPier } = await server.ssrLoadModule('/src/core/geometry.ts');
|
||||
const { buildPierScene } = await server.ssrLoadModule('/src/view/scene.ts');
|
||||
const { buildRebar3D } = await server.ssrLoadModule('/src/view/rebar3d.ts');
|
||||
const { rebarSummary } = await server.ssrLoadModule('/src/core/rebar/summary.ts');
|
||||
const result = [];
|
||||
for (const [type, columnCount] of [['T형', 1], ['2주형', 2], ['3주형', 3]]) {
|
||||
for (const foundationType of ['직접기초', '말뚝기초', '현장타설말뚝']) {
|
||||
const model = buildPier({ ...loadP1(), type, columnCount, foundationType });
|
||||
const record = { type, foundationType, columns: model.columns.length, footings: model.footings.length, piles: model.piles.length };
|
||||
for (const [name, run] of Object.entries({ concrete: () => buildPierScene(model), rebar3d: () => buildRebar3D(model), summary: () => rebarSummary(model) })) {
|
||||
try { run(); record[name] = { ok: true }; }
|
||||
catch (error) { record[name] = { ok: false, message: error.message, stack: error.stack.split('\n').slice(0, 5) }; }
|
||||
}
|
||||
result.push(record);
|
||||
}
|
||||
}
|
||||
await mkdir('.verify/cast-in-place-20261105', { recursive: true });
|
||||
await writeFile('.verify/cast-in-place-20261105/diagnosis.json', JSON.stringify(result, null, 2));
|
||||
console.table(result.map(({ type, foundationType, concrete, rebar3d, summary }) => ({ type, foundationType, concrete: concrete.ok, rebar3d: rebar3d.ok, summary: summary.ok })));
|
||||
} finally { await server.close(); }
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createServer } from 'vite';
|
||||
import { readFileSync } from 'node:fs';
|
||||
const server = await createServer({ server: { middlewareMode: true, hmr: false } });
|
||||
try {
|
||||
const { buildPier } = await server.ssrLoadModule('/src/core/geometry.ts');
|
||||
const { generateDrawing, DEFAULT_SETTINGS } = await server.ssrLoadModule('/도면작성/drawing.ts');
|
||||
const {presentationEntities} = await server.ssrLoadModule('/도면작성/presentation.ts');
|
||||
const {entityBox,viewCollisions,occupiedBounds} = await server.ssrLoadModule('/도면작성/occupancy.ts');
|
||||
const {bearingDetail} = await server.ssrLoadModule('/도면작성/bearing-detail.ts');
|
||||
const sample = JSON.parse(readFileSync('랜딩페이지/assets/samples/'+(process.argv[2]||'sample01.pzr.json'), 'utf8'));
|
||||
const input = sample.piers[0].input;
|
||||
const doc = generateDrawing(buildPier(input), input, sample.project, DEFAULT_SETTINGS);
|
||||
console.log(JSON.stringify(doc.quality.issues.map(q => ({q, a:doc.views.find(v=>v.id===q.view)?.entities[q.first], b:doc.views.find(v=>v.id===q.view)?.entities[q.second]})), null, 2));
|
||||
console.log(JSON.stringify(doc.views.map(v=>({id:v.id,scale:v.scale,slot:v.slot}))));
|
||||
for(const q of doc.quality.issues.filter(q=>q.kind==='text-text'))console.log(JSON.stringify(presentationEntities(doc.views.find(v=>v.id===q.view)).filter(p=>p.entity.kind==='text'&&[q.first,q.second].includes(p.index)).map(p=>({entity:p.entity,box:entityBox(p.entity)}))));
|
||||
for(const scale of [50,75,100,125,150,200,250,300]){
|
||||
const {entities}=bearingDetail(buildPier(input),scale);
|
||||
for(const e of entities){if(e.kind==='dimension')e.height=3*scale;if(e.kind==='text')e.height=Math.max(e.height,2.5*scale);}
|
||||
const v={id:'detail',entities,scale},b=occupiedBounds(v);
|
||||
console.log(JSON.stringify({scale,width:(b.maxX-b.minX)/scale,issues:viewCollisions(v).map(q=>({a:entities[q.first],b:entities[q.second]}))}));
|
||||
}
|
||||
} finally { await server.close(); }
|
||||
@@ -0,0 +1,52 @@
|
||||
// Read the user's linked artifact in an isolated browser profile, without changing its content.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const out = path.resolve(__dirname, '../.verify/design-system-artifact-20261105');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
async function inspect(target, name, screenshot) {
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const message = JSON.parse(data);
|
||||
const wait = pending.get(message.id);
|
||||
if (!wait) return;
|
||||
clearTimeout(wait.timer); pending.delete(message.id);
|
||||
if (message.error) wait.reject(new Error(JSON.stringify(message.error))); else wait.resolve(message.result);
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 15000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
try {
|
||||
await send('Runtime.enable');
|
||||
if (screenshot) {
|
||||
await send('Page.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
}
|
||||
const text = await send('Runtime.evaluate', { expression: 'document.body.innerText', returnByValue: true });
|
||||
const html = await send('Runtime.evaluate', { expression: 'document.documentElement.outerHTML', returnByValue: true });
|
||||
const frames = await send('Runtime.evaluate', { expression: '[...document.querySelectorAll("iframe")].map(f=>({src:f.src,title:f.title,srcdocLength:f.srcdoc.length}))', returnByValue: true });
|
||||
fs.writeFileSync(path.join(out, `${name}.txt`), text.result?.value || '');
|
||||
fs.writeFileSync(path.join(out, `${name}.html`), html.result?.value || '');
|
||||
fs.writeFileSync(path.join(out, `${name}-frames.json`), JSON.stringify(frames.result?.value, null, 2));
|
||||
console.log(JSON.stringify({ name, title: target.title, url: target.url, text: (text.result?.value || '').slice(0, 10000), frames: frames.result?.value }));
|
||||
if (screenshot) {
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, 'overview.png'), Buffer.from(shot.data, 'base64'));
|
||||
}
|
||||
} finally { ws.close(); }
|
||||
}
|
||||
(async () => {
|
||||
let targets = await (await fetch('http://127.0.0.1:9444/json')).json();
|
||||
const page = targets.find((target) => target.type === 'page' && target.url.includes('9LPwY9MMugHfvF5auhpVW2'));
|
||||
if (!page) throw new Error('Linked artifact page not found');
|
||||
await inspect(page, 'page', true);
|
||||
targets = await (await fetch('http://127.0.0.1:9444/json')).json();
|
||||
let index = 0;
|
||||
for (const target of targets.filter((target) => target.type === 'iframe')) await inspect(target, `frame-${++index}`, false);
|
||||
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,21 @@
|
||||
// Read focused-control and native-menu behaviour in the last verification tab.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
(async () => {
|
||||
const pages = await (await fetch('http://127.0.0.1:9444/json/list')).json();
|
||||
const page = pages.find(p=>p.url==='http://127.0.0.1:5173/');
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise(r=>ws.onopen=r);
|
||||
let seq=0; const pending=new Map();
|
||||
ws.onmessage=({data})=>{const m=JSON.parse(data);if(pending.has(m.id)){pending.get(m.id)(m.result);pending.delete(m.id);}};
|
||||
const send=(method,params={})=>new Promise(r=>{const id=++seq;pending.set(id,r);ws.send(JSON.stringify({id,method,params}));});
|
||||
const ev=async expression=>(await send('Runtime.evaluate',{expression,returnByValue:true})).result.value;
|
||||
const inspect=`({open:document.getElementById('file-menu').open,active:document.activeElement.outerHTML,summaryTab:document.querySelector('#file-menu summary').tabIndex})`;
|
||||
const result={before:await ev(inspect)};
|
||||
await ev(`document.querySelector('#file-menu summary').focus()`);
|
||||
await send('Input.dispatchKeyEvent',{type:'keyDown',key:'Enter',code:'Enter',text:'\r',windowsVirtualKeyCode:13});
|
||||
await send('Input.dispatchKeyEvent',{type:'keyUp',key:'Enter',code:'Enter',windowsVirtualKeyCode:13});
|
||||
result.after=await ev(inspect);
|
||||
fs.writeFileSync(path.resolve(__dirname,'../.verify/unified-input-20261106/keyboard-diagnosis.json'),JSON.stringify(result,null,2));
|
||||
console.log(result);ws.close();
|
||||
})().catch(e=>{console.error(e);process.exitCode=1;});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Read-only design sample capture, fresh isolated CDP target. Run via Windows Node.
|
||||
const fs=require('node:fs'), path=require('node:path');
|
||||
const root=path.resolve(__dirname,'..'),out=path.join(root,'.verify/impeccable-20261105/a-after');
|
||||
fs.mkdirSync(out,{recursive:true});
|
||||
// Preserve the first animation-timed sample before the one allowed corrective batch.
|
||||
if(fs.existsSync(path.join(out,'raw.json'))){const archive=path.join(out,'initial');fs.mkdirSync(archive,{recursive:true});for(const f of fs.readdirSync(out))if(/\.(png|json)$/.test(f))fs.copyFileSync(path.join(out,f),path.join(archive,f));}
|
||||
const report={started:new Date().toISOString(),views:[],errors:[]};
|
||||
const delay=ms=>new Promise(r=>setTimeout(r,ms));let ws;
|
||||
(async()=>{
|
||||
const target=await(await fetch('http://127.0.0.1:9444/json/new?'+encodeURIComponent('http://127.0.0.1:5173/'),{method:'PUT'})).json();report.target=target.id;
|
||||
ws=new WebSocket(target.webSocketDebuggerUrl);await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j});
|
||||
let id=0;const pending=new Map();ws.onmessage=({data})=>{const m=JSON.parse(data);if(m.id){const p=pending.get(m.id);if(p){pending.delete(m.id);clearTimeout(p.t);m.error?p.j(Error(JSON.stringify(m.error))):p.r(m.result)}}else if(m.method==='Runtime.exceptionThrown')report.errors.push(m.params)};
|
||||
const send=(method,params={})=>new Promise((r,j)=>{const n=++id,t=setTimeout(()=>j(Error('Timeout '+method)),20000);pending.set(n,{r,j,t});ws.send(JSON.stringify({id:n,method,params}))});
|
||||
const ev=async expression=>{const r=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value};
|
||||
const until=async(expression)=>{for(let n=0;n<100;n++){if(await ev(expression))return;await delay(200)}throw Error('Wait failed '+expression)};
|
||||
const click=selector=>ev(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing ${selector}');e.click()})()`);
|
||||
const cap=async name=>{await delay(600);fs.writeFileSync(path.join(out,name+'.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64'));const facts=await ev(`({text:document.body.innerText,viewport:{width:innerWidth,height:innerHeight,scrollWidth:document.documentElement.scrollWidth,scrollHeight:document.documentElement.scrollHeight},controls:[...document.querySelectorAll('button,input,select,summary')].filter(e=>e.getBoundingClientRect().width&&e.getBoundingClientRect().height).map(e=>({tag:e.tagName,id:e.id,text:e.innerText||e.getAttribute('aria-label')||e.getAttribute('placeholder'),r:(()=>{const r=e.getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height}})(),font:getComputedStyle(e).fontSize}))})`);report.views.push({name,...facts});fs.writeFileSync(path.join(out,'raw.json'),JSON.stringify(report,null,2))};
|
||||
await send('Page.enable');await send('Runtime.enable');await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
||||
await until(`!!document.querySelector('[data-card="excel"] button')`);await delay(2500);
|
||||
await click('[data-card="excel"] button');
|
||||
await ev(`document.getElementById('project-name').value='디자인 검토 A';document.getElementById('project-name').dispatchEvent(new Event('input',{bubbles:true}))`);
|
||||
const doc=await send('DOM.getDocument'),node=await send('DOM.querySelector',{nodeId:doc.root.nodeId,selector:'#file'});
|
||||
await send('DOM.setFileInputFiles',{nodeId:node.nodeId,files:[path.resolve(root,'../../RAG/운곡교P1입력.xlsx')]});
|
||||
await until(`!document.getElementById('dlg-apply').disabled`);await cap('01-import-1600');
|
||||
report.importBefore=await ev(`({scope:document.querySelector('#dlg-selected').textContent,preview:document.querySelector('#pv-abut')?.innerText,button:document.querySelector('#dlg-apply').textContent})`);
|
||||
await click('#import-abut-A1');report.importDeselected=await ev(`({scope:document.querySelector('#dlg-selected').textContent,button:document.querySelector('#dlg-apply').textContent,preview:document.querySelector('#pv-abut')?.innerText})`);await click('#import-abut-A1');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1280,height:800,deviceScaleFactor:1,mobile:false});await cap('02-import-1280');
|
||||
await click('#dlg-apply');await until(`!document.getElementById('import-dialog').open`);
|
||||
await click('#nav-plan');await until(`[...document.querySelectorAll('#plan-pier-items button')].some(b=>b.textContent.includes('A1')&&b.textContent.includes('교대'))`);
|
||||
await ev(`[...document.querySelectorAll('#plan-pier-items button')].find(b=>b.textContent.includes('A1')&&b.textContent.includes('교대')).click()`);await delay(4500);
|
||||
await click('#nav-rebar');await until(`document.querySelectorAll('#rebar-tabs button').length===6`);
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
||||
if(await ev(`document.querySelector('#review-model-toggle').getAttribute('aria-expanded')==='false'`))await click('#review-model-toggle');
|
||||
await ev(`document.querySelector('#review-model-height').value='240';document.querySelector('#review-model-height').dispatchEvent(new Event('input',{bubbles:true}))`);await cap('03-rebar-1600');
|
||||
report.footTasks=await ev(`document.querySelector('#review-summary').textContent+' '+document.querySelector('#review-tasks').innerText`);
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1280,height:800,deviceScaleFactor:1,mobile:false});await cap('04-rebar-1280');
|
||||
await ev(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.includes('날개벽')).click()`);
|
||||
report.wingTasks=await ev(`document.querySelector('#review-summary').textContent+' '+document.querySelector('#review-tasks').innerText`);
|
||||
await click('#review-tasks button');report.taskJump=await ev(`({focused:document.activeElement?.innerText,selected:document.querySelector('#rebar-table').innerText.includes('강조 중')})`);
|
||||
await ev(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.includes('확대기초')).click()`);
|
||||
await ev(`document.querySelector('#review-model-height').value='180';document.querySelector('#review-model-height').dispatchEvent(new Event('input',{bubbles:true}))`);
|
||||
await click('#review-inputs');report.inputJump=await ev(`({focused:document.activeElement?.id,scroll:document.querySelector('#review-workspace > .viewer').scrollTop})`);
|
||||
await click('#review-model-toggle');await click('#review-table');
|
||||
await ev(`document.querySelector('#view-rebar > .results').scrollTop=250`);await cap('05-collapsed-table-material-1280');
|
||||
report.end=await ev(`({modelHidden:getComputedStyle(document.querySelector('#slot-rebar')).display,expanded:document.querySelector('#review-model-toggle').getAttribute('aria-expanded'),focused:document.activeElement.tagName,frames:[...document.querySelectorAll('#view-rebar > .panel,#review-workspace,#review-workspace > .viewer')].map(e=>({id:e.id,class:e.className,overflow:getComputedStyle(e).overflow,clientHeight:e.clientHeight,scrollHeight:e.scrollHeight,scrollTop:e.scrollTop}))})`);
|
||||
report.finished=new Date().toISOString();fs.writeFileSync(path.join(out,'raw.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));
|
||||
await send('Page.close');ws.close();
|
||||
})().catch(e=>{report.failure=String(e.stack||e);fs.writeFileSync(path.join(out,'raw.json'),JSON.stringify(report,null,2));console.error(e);if(ws)ws.close();process.exitCode=1});
|
||||
@@ -0,0 +1,52 @@
|
||||
// Independent design evidence: only the newly created Chrome tab is modified.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const [input, out] = process.argv.slice(2);
|
||||
const report = { views: [], console: [], exceptions: [], started: new Date().toISOString() };
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
let ws;
|
||||
async function main() {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?http://127.0.0.1:5173/', { method: 'PUT' })).json();
|
||||
report.targetId = target.id;
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r,j) => {ws.onopen=r; ws.onerror=j;});
|
||||
let next=0; const pending=new Map();
|
||||
ws.onmessage=({data})=> {const m=JSON.parse(data); if(m.id){const p=pending.get(m.id);if(p){clearTimeout(p.t);pending.delete(m.id);m.error?p.j(m.error):p.r(m.result);}} else if(m.method==='Runtime.consoleAPICalled')report.console.push({type:m.params.type,args:m.params.args.map(x=>x.value??x.description)});else if(m.method==='Runtime.exceptionThrown')report.exceptions.push(m.params.exceptionDetails);};
|
||||
const send=(method,params={})=>new Promise((r,j)=>{const id=++next;const t=setTimeout(()=>j(new Error(method+' timeout')),20000);pending.set(id,{r,j,t});ws.send(JSON.stringify({id,method,params}));});
|
||||
const ev=async expression=>{const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.text);return r.result.value;};
|
||||
const until=async e=>{for(let n=0;n<100;n++){if(await ev(e))return;await sleep(150);}throw new Error('Missing state '+e);};
|
||||
const click=async e=>ev(`(()=>{const n=${e};if(!n||n.disabled)throw new Error('Missing/disabled click');n.click();return true;})()`);
|
||||
await send('Runtime.enable'); await send('Page.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
||||
await until(`typeof window.loadBytesToApp==='function'`);
|
||||
report.injectionPreflight=await ev(`(()=>{document.title='[Human] Design evidence B';let s=document.createElement('script');s.textContent='window.__designBMutation=true';document.head.append(s);return window.__designBMutation===true;})()`);
|
||||
const measure=async name=>{
|
||||
if(process.env.OVERLAY_URL) report.overlayInjection=await ev(`new Promise(resolve=>{document.title='[Human] Design evidence B';scrollTo(0,0);const s=document.createElement('script');s.src=${JSON.stringify(process.env.OVERLAY_URL)};s.onload=()=>setTimeout(()=>resolve({loaded:true}),2600);s.onerror=()=>resolve({loaded:false});document.head.append(s);})`);
|
||||
await sleep(350);
|
||||
const facts=await ev(`(()=>{const visible=e=>{const r=e.getBoundingClientRect();return r.width>0&&r.height>0&&getComputedStyle(e).visibility!=='hidden'};const rect=e=>{const r=e.getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right,bottom:r.bottom}};const controls=[...document.querySelectorAll('button,input,select,textarea')].filter(visible);const fields=controls.filter(e=>/INPUT|SELECT|TEXTAREA/.test(e.tagName));const named=e=>!!(e.labels?.length||e.getAttribute('aria-label')||e.getAttribute('aria-labelledby')||e.title);return {viewport:{w:innerWidth,h:innerHeight},doc:{w:document.documentElement.scrollWidth,h:document.documentElement.scrollHeight},text:document.body.innerText,controls:controls.map(e=>({tag:e.tagName,id:e.id,text:(e.innerText||e.getAttribute('aria-label')||e.value||'').slice(0,90),...rect(e),font:getComputedStyle(e).fontSize,disabled:e.disabled})),unnamedFields:fields.filter(e=>!named(e)).map(e=>({id:e.id,type:e.type,...rect(e)})),overflow:[...document.querySelectorAll('body *')].filter(visible).filter(e=>e.scrollWidth>e.clientWidth+2&&e.clientWidth>0).map(e=>({tag:e.tagName,id:e.id,cls:e.className,client:e.clientWidth,scroll:e.scrollWidth,overflowX:getComputedStyle(e).overflowX,...rect(e)})).slice(0,40),active:{id:document.activeElement.id,tag:document.activeElement.tagName},dialogs:[...document.querySelectorAll('dialog[open]')].map(rect)};})()`);
|
||||
facts.name=name;report.views.push(facts);
|
||||
const shot=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,name+'.png'),Buffer.from(shot.data,'base64'));
|
||||
};
|
||||
await measure('01-landing-desktop');
|
||||
await click(`document.querySelector('[data-card="excel"] button')`);
|
||||
await ev(`document.getElementById('project-name').value='디자인 독립 검증 B'`);
|
||||
const doc=await send('DOM.getDocument');const file=await send('DOM.querySelector',{nodeId:doc.root.nodeId,selector:'#file'});
|
||||
await send('DOM.setFileInputFiles',{nodeId:file.nodeId,files:[input]});
|
||||
await until(`!document.getElementById('dlg-apply').disabled`);
|
||||
await measure('02-import-desktop');
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await click(`document.getElementById('nav-plan')`);
|
||||
await click(`[...document.querySelectorAll('#plan-pier-items button')].find(b=>b.textContent.includes('교대')&&b.textContent.includes('A1'))`);
|
||||
await measure('03-A1-plan-desktop');
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.includes('교대')&&b.textContent.includes('A1'))`);
|
||||
await until(`document.querySelectorAll('#rebar-tabs button').length===6`);
|
||||
await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('벽체'))`);
|
||||
await measure('04-A1-rebar-desktop');
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1280,height:800,deviceScaleFactor:1,mobile:false});
|
||||
await measure('05-A1-rebar-laptop');
|
||||
await send('Input.dispatchKeyEvent',{type:'keyDown',key:'Tab',code:'Tab',windowsVirtualKeyCode:9});await send('Input.dispatchKeyEvent',{type:'keyUp',key:'Tab',code:'Tab',windowsVirtualKeyCode:9});
|
||||
report.keyboardFocus=await ev(`(()=>{const e=document.activeElement;const s=getComputedStyle(e);return {tag:e.tagName,id:e.id,text:e.innerText?.slice(0,80),outline:s.outline,focusVisible:e.matches(':focus-visible')}})()`);
|
||||
report.result='COMPLETE';
|
||||
}
|
||||
fs.mkdirSync(out,{recursive:true});main().catch(e=>{report.result='PARTIAL';report.failure=e.stack;process.exitCode=1;}).finally(()=>{report.finished=new Date().toISOString();fs.writeFileSync(path.join(out,'browser.json'),JSON.stringify(report,null,2));console.log(JSON.stringify({result:report.result,views:report.views.length,failure:report.failure}));ws?.close();});
|
||||
@@ -0,0 +1,166 @@
|
||||
// Build non-destructive fixtures and verify full-name identity and error disclosure via CDP.
|
||||
// Run with Windows Node while the local app and a dedicated Chrome CDP port 9444 are running.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const XLSX = require('xlsx');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const out = path.join(root, '.verify', 'abut-names-20261105');
|
||||
const report = { checks: [], errors: [], started: new Date().toISOString() };
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const base = XLSX.readFile(path.resolve(root, '../../RAG/운곡교P1입력.xlsx'));
|
||||
const blocks = { info: [], ips: [], bearings: [] };
|
||||
for (const sheet of base.SheetNames) {
|
||||
const rows = XLSX.utils.sheet_to_json(base.Sheets[sheet], { header: 1, defval: null });
|
||||
for (let h = 0; h < rows.length; h++) {
|
||||
if (rows[h][0] !== '교대명') continue;
|
||||
const kind = ({ 형식: 'info', IP점: 'ips', 받침번호: 'bearings' })[rows[h][1]];
|
||||
if (!kind) continue;
|
||||
let name;
|
||||
for (let i = h + 1; i < rows.length && rows[i][1] != null && rows[i][1] !== ''; i++) {
|
||||
name = rows[i][0] || name;
|
||||
if (name === 'A1') blocks[kind].push(['A2-완도', ...rows[i].slice(1)]);
|
||||
}
|
||||
}
|
||||
for (const row of rows) if (row[0] === 'A1') row[0] = 'A1-완도';
|
||||
base.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
|
||||
}
|
||||
assert.equal(blocks.info.length, 1);
|
||||
blocks.info[0][1] = 'A2';
|
||||
XLSX.utils.book_append_sheet(base, XLSX.utils.aoa_to_sheet([
|
||||
['교대명', '형식'], ...blocks.info, [],
|
||||
['교대명', 'IP점'], ...blocks.ips, [],
|
||||
['교대명', '받침번호'], ...blocks.bearings,
|
||||
]), '종점교대');
|
||||
const validFile = path.join(out, 'full-names.xlsx');
|
||||
XLSX.writeFile(base, validFile);
|
||||
const bad = XLSX.readFile(validFile);
|
||||
for (const sheet of bad.SheetNames) {
|
||||
const rows = XLSX.utils.sheet_to_json(bad.Sheets[sheet], { header: 1, defval: null });
|
||||
for (const row of rows) if (row[0] === 'A1-완도' && row[1] === 'A1') row[1] = 'A3';
|
||||
bad.Sheets[sheet] = XLSX.utils.aoa_to_sheet(rows);
|
||||
}
|
||||
const badFile = path.join(out, 'invalid-type.xlsx');
|
||||
XLSX.writeFile(bad, badFile);
|
||||
let ws;
|
||||
let send;
|
||||
async function main() {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let seq = 0;
|
||||
let loaded;
|
||||
let downloadGuid;
|
||||
const done = new Set();
|
||||
const pending = new Map();
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout ${method}`)); }, 20000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
ws.onmessage = ({ data }) => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id) {
|
||||
const wait = pending.get(msg.id); if (!wait) return;
|
||||
clearTimeout(wait.timer); pending.delete(msg.id);
|
||||
if (msg.error) wait.reject(new Error(JSON.stringify(msg.error))); else wait.resolve(msg.result);
|
||||
} else if (msg.method === 'Page.loadEventFired') loaded?.();
|
||||
else if (msg.method === 'Runtime.exceptionThrown') report.errors.push(msg.params.exceptionDetails.exception?.description || msg.params.exceptionDetails.text);
|
||||
else if (msg.method === 'Runtime.consoleAPICalled' && msg.params.type === 'error') report.errors.push(msg.params.args.map((a) => a.value ?? a.description).join(' '));
|
||||
else if (msg.method === 'Browser.downloadWillBegin') downloadGuid = msg.params.guid;
|
||||
else if (msg.method === 'Browser.downloadProgress' && msg.params.state === 'completed') done.add(msg.params.guid);
|
||||
};
|
||||
const ev = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async (expression) => {
|
||||
for (let i = 0; i < 100; i++) { if (await ev(expression)) return; await delay(150); }
|
||||
throw new Error(`Timeout: ${expression}`);
|
||||
};
|
||||
const click = (expression) => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled target'); e.click(); })()`);
|
||||
const check = (name, condition) => { assert.ok(condition, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const shot = async (name) => {
|
||||
await delay(250);
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, `${name}.png`), Buffer.from(r.data, 'base64'));
|
||||
};
|
||||
const file = async (selector, name) => {
|
||||
const doc = await send('DOM.getDocument');
|
||||
const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
|
||||
await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [name] });
|
||||
};
|
||||
const navigate = async () => {
|
||||
let timer;
|
||||
const load = new Promise((resolve, reject) => { loaded = resolve; timer = setTimeout(() => reject(new Error('Load timeout')), 20000); });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
try { await load; } finally { clearTimeout(timer); loaded = undefined; }
|
||||
await until(`typeof window.loadBytesToApp === 'function' && !!document.querySelector('[data-card="excel"] button')`);
|
||||
};
|
||||
const select = (name) => click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes(${JSON.stringify(name)}) && b.textContent.includes('교대'))`);
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
const downloads = 'C:\\Users\\User\\AppData\\Local\\Temp\\pierzainer-abut-name-20261105\\downloads';
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
||||
await navigate();
|
||||
await click(`document.querySelector('[data-card="excel"] button')`);
|
||||
await ev(`document.getElementById('project-name').value = '복합 교대명 검증'; document.getElementById('project-name').dispatchEvent(new Event('input'));`);
|
||||
await file('#file', validFile);
|
||||
await until(`document.getElementById('dlg-apply').textContent.includes('교대 2기')`);
|
||||
check('both full names recognized without cascade errors', await ev(`document.getElementById('pv-abuts').textContent.includes('A1-완도') && document.getElementById('pv-abuts').textContent.includes('A2-완도') && !document.querySelector('#sheets .bad')`));
|
||||
check('full-name checkboxes stay distinct', await ev(`document.getElementById('import-abut-A1-완도').checked && document.getElementById('import-abut-A2-완도').checked`));
|
||||
await shot('01-full-names-import');
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await select('A1-완도');
|
||||
check('full name selects the start abutment', await ev(`document.getElementById('rebar-note').textContent.startsWith('A1-완도') && document.querySelectorAll('#rebar-tabs button').length === 6`));
|
||||
await select('A2-완도');
|
||||
check('full name selects the end abutment independently', await ev(`document.getElementById('rebar-note').textContent.startsWith('A2-완도')`));
|
||||
const order = await ev(`[...document.querySelectorAll('#rebar-pier-items button')].map(b => b.textContent)`);
|
||||
check('type-based order is preserved', order[0].includes('A1-완도') && order[1].includes('P1') && order[2].includes('A2-완도'));
|
||||
await shot('02-independent-abutments');
|
||||
await click(`document.getElementById('save-project')`);
|
||||
await until(`document.getElementById('save-dialog').open`);
|
||||
await click(`document.getElementById('sv-save')`);
|
||||
for (let i = 0; i < 100 && !(downloadGuid && done.has(downloadGuid)); i++) await delay(150);
|
||||
assert.ok(downloadGuid && done.has(downloadGuid), 'actual download completes');
|
||||
const text = fs.readFileSync(path.join(downloads, downloadGuid), 'utf8');
|
||||
const project = JSON.parse(text);
|
||||
const expectedNames = ['A1-완도', 'A2-완도'];
|
||||
assert.deepEqual(project.abutments.map((a) => a.name), expectedNames);
|
||||
assert.deepEqual(project.abutDefaults.map((a) => a.name), expectedNames);
|
||||
report.checks.push('actual download preserves both names and defaults');
|
||||
const saved = path.join(out, 'full-names.pzr.json'); fs.writeFileSync(saved, text);
|
||||
await navigate();
|
||||
await ev('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`);
|
||||
await file('#project-file', saved);
|
||||
await until(`document.querySelector('#toast-host').textContent.includes('열었습니다')`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await select('A1-완도'); await select('A2-완도');
|
||||
check('fresh reopen regenerates full-name abutments', await ev(`document.getElementById('save-status').textContent === '저장됨' && document.getElementById('rebar-note').textContent.startsWith('A2-완도')`));
|
||||
await shot('03-reopened');
|
||||
await click(`document.getElementById('open-import')`);
|
||||
await file('#file', badFile);
|
||||
await until(`document.getElementById('dlg-apply').textContent.includes('교대 1기') && !!document.querySelector('#sheets details')`);
|
||||
check('one bad definition does not disable another name', await ev(`document.getElementById('import-abut-A2-완도').checked && !document.getElementById('import-abut-A2-완도').disabled`));
|
||||
check('dependent rows are collapsed under the root', await ev(`document.querySelectorAll('#sheets details').length === 1 && !document.querySelector('#sheets details').open && document.querySelector('#sheets details summary').textContent.includes('8개 행') && document.querySelectorAll('#sheets details p').length === 8`));
|
||||
await shot('04-root-error-collapsed');
|
||||
await click(`document.querySelector('#sheets details summary')`);
|
||||
check('dependent rows remain available for diagnosis', await ev(`document.querySelector('#sheets details').open && document.querySelector('#sheets details').textContent.includes('IP') && document.querySelector('#sheets details').textContent.includes('받침')`));
|
||||
await shot('05-related-rows');
|
||||
check('no browser runtime errors', report.errors.length === 0);
|
||||
report.result = 'PASS';
|
||||
}
|
||||
main().catch((error) => { report.result = 'FAIL'; report.failure = error.stack; process.exitCode = 1; }).finally(async () => {
|
||||
if (report.result === 'FAIL' && send) {
|
||||
try { const shot = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, 'failure.png'), Buffer.from(shot.data, 'base64')); } catch { /* Keep the original error. */ }
|
||||
}
|
||||
report.finished = new Date().toISOString(); fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify({ result: report.result, checks: report.checks.length, errors: report.errors, failure: report.failure }));
|
||||
if (send) { try { await send('Page.close'); } catch { /* The tab may already be closed. */ } }
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
// 교대만 가져오기 (2026-11-06): CDP 9444 Chrome + Vite 5173. Imports the sample workbook with P1 unchecked and A1 only,
|
||||
// then walks ① ② ③, and checks for runtime exceptions; screenshots go to .verify/abut-only-20261106.
|
||||
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/abut-only-20261106');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [], console: [] };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0; const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') report.console.push(m.params.args.map(a => a.value ?? a.description).join(' '));
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); });
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expr => { for (let i = 0; i < 100; i++) { if (await ev(expr)) return; await delay(150); } throw new Error(`Wait failed: ${expr}`); };
|
||||
const click = expr => ev(`(() => { const e = ${expr}; if (!e || e.disabled) throw new Error('Missing/disabled: ' + ${JSON.stringify(expr)}); e.click(); })()`);
|
||||
const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const shot = async name => { await delay(400); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); };
|
||||
try {
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.loadBytesToApp === 'function'`);
|
||||
await ev(`(async () => { const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer(); window.loadBytesToApp(new Uint8Array(b), '운곡교P1입력.xlsx'); })()`);
|
||||
await until(`!!document.getElementById('import-abut-A1')`);
|
||||
// Uncheck every pier, keep A1.
|
||||
await ev(`(() => { for (const c of document.querySelectorAll('input[data-import-kind="pier"]')) if (c.checked) { c.checked = false; c.dispatchEvent(new Event('change', { bubbles: true })); } })()`);
|
||||
await ev(`(() => { const c = document.getElementById('import-abut-A1'); if (!c.checked) { c.checked = true; c.dispatchEvent(new Event('change', { bubbles: true })); } })()`);
|
||||
await ev(`(() => { const e = document.getElementById('project-name'); e.value = '교대만 검증'; e.dispatchEvent(new Event('input', { bubbles: true })); })()`);
|
||||
const label = await ev(`document.getElementById('dlg-apply').textContent`);
|
||||
check(`apply button allows A1 only: "${label}"`, !(await ev(`document.getElementById('dlg-apply').disabled`)) && label.includes('교각 0기 + 교대 1기'));
|
||||
check('dialog note says abutments only', (await ev(`document.getElementById('dlg-selected').textContent`)).includes('교대만 가져옵니다'));
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await until(`!document.querySelector('.app').hidden`);
|
||||
await shot('1-shape');
|
||||
check('① list shows A1', (await ev(`document.body.innerText`)).includes('A1 · A1 교대'));
|
||||
// WebGL canvases cannot be read back without preserveDrawingBuffer: the 3D view is judged on the screenshots.
|
||||
for (const [nav, name] of [['nav-plan', '2-plan'], ['nav-rebar', '3-rebar'], ['nav-shape', '4-shape-again']]) {
|
||||
await click(`document.getElementById('${nav}')`);
|
||||
await shot(name);
|
||||
// Pick the abutment in the visible list of this stage, when there is one.
|
||||
await ev(`(() => { const b = [...document.querySelectorAll('main:not([hidden]) button')].find(x => x.textContent.startsWith('A1 ·')); if (b) b.click(); })()`);
|
||||
await shot(name + '-A1');
|
||||
}
|
||||
check('no runtime exceptions (교대만)', report.errors.length === 0);
|
||||
// Regression: P1 + A1 together still import, and P1 front-row solplates pair with A1.
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.loadBytesToApp === 'function'`);
|
||||
await ev(`(async () => { const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer(); window.loadBytesToApp(new Uint8Array(b), '운곡교P1입력.xlsx'); })()`);
|
||||
await until(`!!document.getElementById('import-abut-A1')`);
|
||||
await ev(`(() => { const e = document.getElementById('project-name'); e.value = '교각+교대 검증'; e.dispatchEvent(new Event('input', { bubbles: true })); })()`);
|
||||
check('P1 + A1 apply label', (await ev(`document.getElementById('dlg-apply').textContent`)).includes('교각 1기 + 교대 1기'));
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await until(`!document.querySelector('.app').hidden`);
|
||||
await click(`document.getElementById('nav-plan')`);
|
||||
await shot('5-pier-plan');
|
||||
check('no runtime exceptions (교각 + 교대)', report.errors.length === 0);
|
||||
} catch (err) {
|
||||
report.failure = String(err && err.stack || err);
|
||||
console.error(report.failure);
|
||||
} finally {
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify({ checks: report.checks.length, errors: report.errors, console: report.console.slice(0, 5), failure: report.failure }, null, 2));
|
||||
ws.close();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,90 @@
|
||||
// ③ 교대 벽체 카드 화면 확인 (2026-11-04): 헤드리스 Chrome(CDP)으로 입력 파일을 불러와 A1 → ③ → 벽체를 열고 스크린샷을 남긴다.
|
||||
// 사용 (WSL Node ≥ 22, 개발 서버 실행 중):
|
||||
// chrome-headless-shell --headless --remote-debugging-port=9444 & (~/.cache/ms-playwright/chromium_headless_shell-*/)
|
||||
// node scripts/verify-abut-wall.cjs <input.xlsx> <out dir>
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const APP_URL = process.env.APP_URL || 'http://127.0.0.1:5173/';
|
||||
const CDP = process.env.CDP || 'http://127.0.0.1:9444';
|
||||
const [INPUT_FILE, OUT_DIR] = process.argv.slice(2);
|
||||
if (!INPUT_FILE || !OUT_DIR) throw new Error('INPUT_FILE and OUT_DIR are required');
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
const targets = await (await fetch(`${CDP}/json`)).json();
|
||||
const page = targets.find((t) => t.type === 'page');
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((res, rej) => ((ws.onopen = res), (ws.onerror = rej)));
|
||||
let seq = 0;
|
||||
const waiting = new Map();
|
||||
const consoleEvents = [];
|
||||
ws.onmessage = (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && waiting.has(m.id)) {
|
||||
waiting.get(m.id)(m);
|
||||
waiting.delete(m.id);
|
||||
} else if (m.method === 'Runtime.exceptionThrown') consoleEvents.push(`exception: ${m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text}`);
|
||||
else if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') consoleEvents.push(`console.error: ${m.params.args.map((a) => a.value ?? a.description ?? '').join(' ')}`);
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((res) => { const id = ++seq; waiting.set(id, res); ws.send(JSON.stringify({ id, method, params })); });
|
||||
const evalJs = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 400));
|
||||
return r.result.result.value;
|
||||
};
|
||||
const shot = async (name, full = false) => {
|
||||
const params = { format: 'png' };
|
||||
if (full) {
|
||||
const m = await send('Page.getLayoutMetrics');
|
||||
params.clip = { x: 0, y: 0, width: 1600, height: Math.min(m.result.cssContentSize.height, 6000), scale: 1 };
|
||||
params.captureBeyondViewport = true;
|
||||
}
|
||||
const r = await send('Page.captureScreenshot', params);
|
||||
fs.writeFileSync(path.join(OUT_DIR, name), Buffer.from(r.result.data, 'base64'));
|
||||
console.log(`SHOT ${name}`);
|
||||
};
|
||||
const click = (js) => evalJs(`(() => { const e = ${js}; if (!e) throw new Error('없음: ${js.replace(/'/g, '')}'); e.click(); return true; })()`);
|
||||
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: APP_URL });
|
||||
await sleep(4000);
|
||||
await click(`document.getElementById('open-import')`);
|
||||
await sleep(500);
|
||||
const doc = await send('DOM.getDocument', { depth: -1 });
|
||||
const q = await send('DOM.querySelector', { nodeId: doc.result.root.nodeId, selector: '#file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId: q.result.nodeId, files: [INPUT_FILE] });
|
||||
await sleep(3000);
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await sleep(2500);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await sleep(1500);
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find((b) => b.textContent.includes('교대'))`);
|
||||
await sleep(2000);
|
||||
await click(`[...document.querySelectorAll('#rebar-tabs button')].find((b) => b.textContent.startsWith('벽체'))`);
|
||||
await sleep(2000);
|
||||
console.log('STATUS', await evalJs(`document.getElementById('step3-status').textContent`));
|
||||
console.log('TABS', await evalJs(`[...document.querySelectorAll('#rebar-tabs button')].map((b) => b.textContent).join(' | ')`));
|
||||
console.log('NOTE', await evalJs(`document.getElementById('rebar-note').textContent`));
|
||||
console.log('ROWS', await evalJs(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter((r) => r.cells.length > 2).map((r) => [r.cells[2].textContent, r.cells[7].textContent, r.cells[8].textContent].join(' ')).join(' | ')`));
|
||||
console.log('ISSUES', await evalJs(`document.getElementById('rebar-issues').innerText.replace(/\\n+/g, ' / ')`));
|
||||
console.log('BARS3D', await evalJs(`document.querySelectorAll('canvas').length`));
|
||||
await shot('wall-1-section.png', true);
|
||||
await click(`[...document.querySelectorAll('.abut-optt')].find((b) => b.textContent.startsWith('교대축 단면'))`);
|
||||
await sleep(1200);
|
||||
await shot('wall-2-elevation.png', true);
|
||||
// W9 행 선택 → 선택만 보기.
|
||||
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find((r) => r.cells[2] && r.cells[2].textContent === 'W9')`);
|
||||
await sleep(800);
|
||||
await click(`[...document.querySelectorAll('#rebar-table .rebar-select button')].find((b) => b.textContent === '선택만 보기')`);
|
||||
await sleep(1500);
|
||||
console.log('SELECTED', await evalJs(`document.querySelector('#rebar-table .rebar-select span').textContent`));
|
||||
await shot('wall-3-w9-only.png');
|
||||
console.log(`CONSOLE_EVENTS ${consoleEvents.length}`);
|
||||
for (const e of consoleEvents) console.log(` ${e}`);
|
||||
ws.close();
|
||||
}
|
||||
main().catch((e) => { console.error('FAILED', e); process.exit(1); });
|
||||
@@ -0,0 +1,241 @@
|
||||
// Verify A1's six rebar cards, rendered views, edits and project round trip via CDP.
|
||||
// Run with Windows Node when CDP is served by Windows Chrome:
|
||||
// node scripts/verify-abut-workflow.cjs <input.xlsx> <output-directory> <download-directory>
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const [input, out, downloads, mode] = process.argv.slice(2);
|
||||
if (!input || !out || !downloads) throw new Error('input, output and download directories are required');
|
||||
const base = process.env.CDP || 'http://127.0.0.1:9444';
|
||||
const url = process.env.APP_URL || 'http://127.0.0.1:5173/';
|
||||
const report = mode === '--resume'
|
||||
? JSON.parse(fs.readFileSync(path.join(out, 'report.json'), 'utf8'))
|
||||
: { input, url, started: new Date().toISOString(), cards: [], errors: [], checks: [] };
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
let ws;
|
||||
|
||||
async function main() {
|
||||
console.log('Connecting to Chrome', base);
|
||||
const targets = await (await fetch(`${base}/json`, { signal: AbortSignal.timeout(10000) })).json();
|
||||
const target = targets.find((t) => t.type === 'page');
|
||||
assert.ok(target, 'Chrome page target');
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let next = 0;
|
||||
const pending = new Map();
|
||||
const completed = new Set();
|
||||
let downloadGuid;
|
||||
let loaded;
|
||||
ws.onmessage = ({ data }) => {
|
||||
const message = JSON.parse(data);
|
||||
if (message.id) {
|
||||
const waiter = pending.get(message.id);
|
||||
if (!waiter) return;
|
||||
clearTimeout(waiter.timer);
|
||||
pending.delete(message.id);
|
||||
if (message.error) waiter.reject(new Error(JSON.stringify(message.error)));
|
||||
else waiter.resolve(message.result);
|
||||
} else if (message.method === 'Page.loadEventFired') loaded?.();
|
||||
else if (message.method === 'Runtime.exceptionThrown') {
|
||||
report.errors.push(message.params.exceptionDetails.exception?.description || message.params.exceptionDetails.text);
|
||||
} else if (message.method === 'Runtime.consoleAPICalled' && message.params.type === 'error') {
|
||||
report.errors.push(message.params.args.map((v) => v.value ?? v.description ?? '').join(' '));
|
||||
} else if (message.method === 'Browser.downloadWillBegin') downloadGuid = message.params.guid;
|
||||
else if (message.method === 'Browser.downloadProgress' && message.params.state === 'completed') completed.add(message.params.guid);
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++next;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, 20000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evaluate = async (expression) => {
|
||||
const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
|
||||
return result.result.value;
|
||||
};
|
||||
const navigate = async () => {
|
||||
let timer;
|
||||
const event = new Promise((resolve, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('Page load timeout')), 20000);
|
||||
loaded = resolve;
|
||||
});
|
||||
await send('Page.navigate', { url });
|
||||
try { await event; } finally { clearTimeout(timer); loaded = undefined; }
|
||||
await until(() => evaluate(`typeof window.loadBytesToApp === 'function'`), 'app event handlers loaded');
|
||||
};
|
||||
const until = async (test, label) => {
|
||||
const end = Date.now() + 20000;
|
||||
while (Date.now() < end) {
|
||||
if (await test()) return;
|
||||
await delay(150);
|
||||
}
|
||||
report.timeoutPage = await evaluate('document.body.innerText');
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, 'failure.png'), Buffer.from(shot.data, 'base64'));
|
||||
throw new Error(`Timeout: ${label}`);
|
||||
};
|
||||
const click = (expression) => evaluate(`(() => { const node = ${expression}; if (!node) throw new Error('Missing click target'); if (node.disabled) throw new Error('Disabled click target'); node.click(); return true; })()`);
|
||||
const selectCard = async (label) => {
|
||||
await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(label)}))`);
|
||||
await until(() => evaluate(`document.querySelector('#rebar-table svg') !== null`), label);
|
||||
await delay(250);
|
||||
};
|
||||
const screenshot = async (name) => {
|
||||
const shot = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, `${name}.png`), Buffer.from(shot.data, 'base64'));
|
||||
};
|
||||
const upload = async (selector, file) => {
|
||||
const doc = await send('DOM.getDocument');
|
||||
const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector });
|
||||
assert.ok(node.nodeId, selector);
|
||||
await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [file] });
|
||||
};
|
||||
const editFirst = async (value) => evaluate(`(() => {
|
||||
const node = document.querySelector('#rebar-table .abut-rebar-inputs input[type=number]');
|
||||
if (!node) throw new Error('Missing rebar input');
|
||||
const old = { id: node.id, value: node.value };
|
||||
node.value = ${JSON.stringify(value)};
|
||||
node.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return old;
|
||||
})()`);
|
||||
const rows = () => evaluate(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter(r => r.cells.length > 2).map(r => r.innerText)`);
|
||||
const restoreView = async (card) => {
|
||||
for (const text of card.context) {
|
||||
await click(`[...document.querySelectorAll('#rebar-table .abut-optt')].find(b => b.textContent === ${JSON.stringify(text)})`);
|
||||
}
|
||||
};
|
||||
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
||||
await send('Page.addScriptToEvaluateOnNewDocument', { source: `
|
||||
window.__drawCalls = 0;
|
||||
for (const type of [window.WebGLRenderingContext, window.WebGL2RenderingContext]) {
|
||||
if (!type) continue;
|
||||
for (const method of ['drawElements', 'drawArrays', 'drawElementsInstanced', 'drawArraysInstanced']) {
|
||||
const original = type.prototype[method];
|
||||
if (original) type.prototype[method] = function(...args) { window.__drawCalls++; return original.apply(this, args); };
|
||||
}
|
||||
}
|
||||
` });
|
||||
if (mode !== '--resume') {
|
||||
await navigate();
|
||||
await until(() => evaluate(`document.querySelector('[data-card="excel"] button') !== null && typeof window.loadBytesToApp === 'function'`), 'app loaded');
|
||||
await click(`document.querySelector('[data-card="excel"] button')`);
|
||||
await evaluate(`const name = document.getElementById('project-name'); name.value = 'A1 화면 검증'; name.dispatchEvent(new Event('input', { bubbles: true }));`);
|
||||
await upload('#file', input);
|
||||
await until(() => evaluate(`!document.getElementById('dlg-apply').disabled`), 'Excel import');
|
||||
report.import = await evaluate(`({ sheets: document.getElementById('sheets').innerText, selection: document.getElementById('dlg-selected').textContent })`);
|
||||
await screenshot('01-import');
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await until(() => evaluate(`[...document.querySelectorAll('#rebar-pier-items button')].some(b => b.textContent.includes('교대'))`), 'abutment list');
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('교대') && b.textContent.includes('A1'))`);
|
||||
await until(() => evaluate(`document.querySelectorAll('#rebar-tabs button').length === 6`), 'six member cards');
|
||||
|
||||
for (const [index, label] of ['받침블럭', '벽체', '흉벽', '확대기초', '날개벽', '접속슬래브'].entries()) {
|
||||
await selectCard(label);
|
||||
const before = await rows();
|
||||
assert.ok(before.length, `${label}: non-empty rebar table`);
|
||||
const card = { label, rows: before, views: [] };
|
||||
const views = await evaluate(`[...document.querySelectorAll('#rebar-table .abut-optt')].map(b => b.textContent)`);
|
||||
assert.ok(views.length >= 2, `${label}: view tabs`);
|
||||
for (const [viewIndex, view] of views.entries()) {
|
||||
const drawBefore = await evaluate('window.__drawCalls');
|
||||
await click(`[...document.querySelectorAll('#rebar-table .abut-optt')].find(b => b.textContent === ${JSON.stringify(view)})`);
|
||||
await delay(350);
|
||||
const facts = await evaluate(`({ svg: !!document.querySelector('#rebar-table svg'), badSvg: /(?:NaN|Infinity)/.test(document.querySelector('#rebar-table svg')?.outerHTML || ''), drawCalls: window.__drawCalls, canvases: [...document.querySelectorAll('canvas')].map(c => ({ width: c.width, height: c.height })) })`);
|
||||
assert.ok(facts.svg && !facts.badSvg, `${label}/${view}: finite SVG`);
|
||||
assert.ok(facts.drawCalls > drawBefore, `${label}/${view}: WebGL draw calls`);
|
||||
await screenshot(`${String(index + 2).padStart(2, '0')}-${index}-${viewIndex}`);
|
||||
fs.writeFileSync(path.join(out, `${index}-${viewIndex}.svg`), await evaluate(`document.querySelector('#rebar-table svg').outerHTML`));
|
||||
card.views.push({ view, ...facts });
|
||||
}
|
||||
const originalValue = await evaluate(`document.querySelector('#rebar-table .abut-rebar-inputs input[type=number]').value`);
|
||||
const changedValue = originalValue === '150' ? '200' : '150';
|
||||
const old = await editFirst(changedValue);
|
||||
const changed = await rows();
|
||||
assert.notDeepEqual(changed, before, `${label}: input recomputes rows`);
|
||||
assert.equal(await evaluate(`document.getElementById(${JSON.stringify(old.id)}).value`), changedValue);
|
||||
card.edit = { ...old, changedValue, changedRows: changed };
|
||||
{
|
||||
const marks = await evaluate(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter(r => r.cells.length > 2).map(r => r.cells[2].textContent)`);
|
||||
assert.ok(marks[0], `${label}: selectable mark`);
|
||||
await click(`document.querySelector('#rebar-table table.rebar-table tbody tr')`);
|
||||
await delay(150);
|
||||
if (!await evaluate(`document.querySelector('#rebar-table .rebar-select button:last-child').getAttribute('aria-pressed') === 'true'`)) {
|
||||
await click(`[...document.querySelectorAll('#rebar-table .rebar-select button')].find(b => b.textContent === '선택만 보기')`);
|
||||
}
|
||||
await delay(250);
|
||||
card.selection = await evaluate(`document.querySelector('#rebar-table .rebar-select').innerText`);
|
||||
assert.ok(card.selection.includes('선택 1 /'), `${label}: one selected bar`);
|
||||
await screenshot(`${String(index + 2).padStart(2, '0')}-${index}-selected`);
|
||||
}
|
||||
card.context = await evaluate(`[...document.querySelectorAll('#rebar-table .abut-optt[aria-pressed="true"]')].map(b => b.textContent)`);
|
||||
report.cards.push(card);
|
||||
console.log('Verified card', label);
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
}
|
||||
|
||||
// Snapshot all tables after every edit, including dependent members and the wing side.
|
||||
for (const card of report.cards) {
|
||||
await selectCard(card.label);
|
||||
await restoreView(card);
|
||||
card.edit.changedRows = await rows();
|
||||
}
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
await click(`document.getElementById('save-project')`);
|
||||
await until(() => evaluate(`document.getElementById('save-dialog').open`), 'save dialog');
|
||||
await evaluate(`document.getElementById('sv-name').value = 'A1 화면 검증'; document.getElementById('sv-bridge').value = 'A1-verification'`);
|
||||
await click(`document.getElementById('sv-save')`);
|
||||
await until(async () => downloadGuid && completed.has(downloadGuid), 'download completes');
|
||||
const downloaded = fs.readFileSync(path.join(downloads, downloadGuid), 'utf8');
|
||||
const project = JSON.parse(downloaded);
|
||||
report.savedAbutments = project.abutments;
|
||||
assert.ok(project.abutments?.length, 'project contains abutments');
|
||||
const savedFile = path.join(out, 'A1-roundtrip.pzr.json');
|
||||
fs.writeFileSync(savedFile, downloaded);
|
||||
report.checks.push('actual project download');
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
}
|
||||
await navigate();
|
||||
// Enter the app shell through its existing sample entry, then open the saved file.
|
||||
// This uses no original Excel file for reconstruction.
|
||||
await evaluate('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`);
|
||||
await upload('#project-file', path.join(out, 'A1-roundtrip.pzr.json'));
|
||||
await until(() => evaluate(`document.querySelector('#toast-host')?.textContent.includes('열었습니다') && document.getElementById('save-status').textContent === '저장됨'`), 'open saved project');
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('교대') && b.textContent.includes('A1'))`);
|
||||
for (const card of report.cards) {
|
||||
await selectCard(card.label);
|
||||
await restoreView(card);
|
||||
const restored = await evaluate(`document.getElementById(${JSON.stringify(card.edit.id)})?.value`);
|
||||
assert.equal(restored, card.edit.changedValue, `${card.label}: restored input`);
|
||||
assert.deepEqual(await rows(), card.edit.changedRows, `${card.label}: restored computed rows`);
|
||||
card.roundtrip = 'PASS';
|
||||
}
|
||||
await screenshot('09-restored');
|
||||
report.checks.push('6 member cards', 'all SVG view tabs', 'WebGL drawing', '6 input edits', '6 bar selections', 'actual project download', 'fresh page project reopen', '6 restored inputs and computed tables');
|
||||
assert.equal(report.errors.length, 0, 'browser runtime errors');
|
||||
delete report.failure;
|
||||
delete report.timeoutPage;
|
||||
report.checks = [...new Set(report.checks)];
|
||||
report.result = 'PASS';
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
report.result = 'FAIL';
|
||||
report.failure = error.stack;
|
||||
process.exitCode = 1;
|
||||
}).finally(() => {
|
||||
report.finished = new Date().toISOString();
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify({ result: report.result, cards: report.cards.length, errors: report.errors, failure: report.failure }));
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
// Actual app verification in an isolated Windows Chrome, CDP port 9444.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/cast-detail-20261105');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [] };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let ws, send;
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0, download;
|
||||
const completed = new Set(), pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (m.method === 'Browser.downloadWillBegin') download = m.params.guid;
|
||||
if (m.method === 'Browser.downloadProgress' && m.params.state === 'completed') completed.add(m.params.guid);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 25000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => { for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); } throw new Error(`Wait failed: ${expression}`); };
|
||||
const click = expression => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled target: ' + ${JSON.stringify(expression)}); e.click(); })()`);
|
||||
const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const file = async name => {
|
||||
const doc = await send('DOM.getDocument');
|
||||
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId, files: [name] });
|
||||
};
|
||||
const shot = async name => { await delay(250); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); };
|
||||
const member = text => click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(text)}))`);
|
||||
const select = (id, name) => click(`[...document.querySelectorAll('#${id} button')].find(b => b.textContent.startsWith(${JSON.stringify(name + ' ·')}))`);
|
||||
const set = (id, value) => ev(`document.getElementById(${JSON.stringify(id)}).value = ${JSON.stringify(String(value))}`);
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
const downloads = 'C:\\Users\\User\\AppData\\Local\\Temp\\pierzainer-cast-detail-20261105\\downloads';
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject === 'function'`);
|
||||
const fixture = await ev(`(async () => {
|
||||
const XLSX = await import('/node_modules/xlsx/xlsx.mjs');
|
||||
const {parseWorkbook} = await import('/src/core/parseWorkbook.ts');
|
||||
const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base = parseWorkbook(XLSX.read(b)).piers.find(p=>p.name==='P1');
|
||||
const piers=[];
|
||||
for (const [type,columnCount] of [['T형',1],['2주형',2],['3주형',3]]) for (const foundationType of ['현장타설말뚝','직접기초','말뚝기초']) {
|
||||
const input={...base,name:'P'+(piers.length+1),type,columnCount,foundationType};
|
||||
piers.push({id:input.name,input,defaults:input,draft:input});
|
||||
}
|
||||
return {schemaVersion:1,project:{name:'현장타설 입력 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
const fixturePath = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(fixturePath, JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`); await file(fixturePath);
|
||||
await until(`document.querySelectorAll('#plan-pier-items button').length === 9`);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const cast = [1,4,7].includes(i);
|
||||
await click(`document.getElementById('nav-plan')`); await select('plan-pier-items', 'P'+i);
|
||||
await click(`[...document.querySelectorAll('#members button')].find(b => b.textContent.startsWith(${JSON.stringify(cast ? '현장타설말뚝' : '확대기초')}))`);
|
||||
check(`P${i} plan: appropriate foundation controls`, await ev(cast
|
||||
? `!!document.querySelector('#members button[disabled]') && !!document.querySelector('#member-fields #edit-pileLengthMm') && !document.querySelector('#member-fields #edit-footingHeightMm')`
|
||||
: `!!document.querySelector('#member-fields #edit-footingHeightMm:not([disabled])')`));
|
||||
await click(`document.getElementById('nav-rebar')`); await member('코핑');
|
||||
check(`P${i} entering rebar builds coping drawings immediately`, await ev(`!!document.querySelector('#rebar-table figure svg')`));
|
||||
await member('기둥');
|
||||
check(`P${i} rebar: dedicated or legacy column`, await ev(cast
|
||||
? `!!document.querySelector('[data-cast-part="column"]') && [...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('확대기초')).disabled`
|
||||
: `!!document.getElementById('column-nDiv') && !document.querySelector('[data-cast-part]')`));
|
||||
}
|
||||
await click(`document.getElementById('nav-plan')`); await select('plan-pier-items','P1');
|
||||
await click(`[...document.querySelectorAll('#members button')].find(b=>b.textContent.startsWith('현장타설말뚝'))`);
|
||||
const ground = fixture.piers[0].input.groundEl, tip = ground-fixture.piers[0].input.pileLengthMm/1000;
|
||||
check('clarified rock EL offsets and fixed beta default appear in the shape card', await ev(`Number(document.getElementById('cip-shape-betaLengthMm').value)===9000 && Number(document.getElementById('cip-shape-weatheredRockEl').value)===${tip+5} && Number(document.getElementById('cip-shape-bedrockEl').value)===${tip+1}`));
|
||||
check('independent clay EL is initially blank and casing fields inactive', await ev(`document.getElementById('cip-shape-clayBottomEl').value==='' && document.getElementById('cip-shape-casingTopEl').disabled`));
|
||||
await click(`document.getElementById('cip-casing-on')`);
|
||||
await set('cip-shape-clayBottomEl',ground-5);
|
||||
await ev(`document.getElementById('cip-shape-clayBottomEl').dispatchEvent(new Event('change'))`);
|
||||
check('casing defaults follow ground plus 3 m and bedrock; independent clay persists', await ev(`!document.getElementById('cip-shape-casingTopEl').disabled && Number(document.getElementById('cip-shape-casingTopEl').value)===${ground+3} && Number(document.getElementById('cip-shape-casingBottomEl').value)===${tip+1} && Number(document.getElementById('cip-shape-clayBottomEl').value)===${ground-5}`));
|
||||
await click(`document.getElementById('member-regen')`); await shot('00-shape-decisions');
|
||||
await click(`document.getElementById('nav-rebar')`); await select('rebar-pier-items','P1'); await member('기둥');
|
||||
check('round-column two-layer defaults and coupler mode are populated', await ev(`document.getElementById('cip-column-mainCount').value==='46' && document.getElementById('cip-column-innerMainCount').value==='46' && document.getElementById('cip-column-coverReference').value==='main-center' && document.getElementById('cip-column-tieSpacingMm').value==='300' && document.getElementById('cip-connection-mode').value==='coupler'`));
|
||||
await member('현장타설말뚝');
|
||||
check('pile bottom cover defaults to 250 mm', await ev(`document.getElementById('cip-pile-endCoverMm').value==='250'`));
|
||||
await member('기둥');
|
||||
await set('cip-column-mainDiameterMm',32); await set('cip-column-mainCount',46); await set('cip-column-coverMm',100);
|
||||
await set('cip-connection-mode','continuous'); await click(`document.getElementById('cip-column-apply')`);
|
||||
await until(`document.getElementById('cip-column-mainCount')?.value === '46'`);
|
||||
await shot('01-column-input');
|
||||
await member('현장타설말뚝');
|
||||
await set('cip-pile-cageLengthMm',999999); await click(`document.getElementById('cip-pile-apply')`);
|
||||
check('invalid cage length rejected with inline reason', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('철근망 길이가 말뚝 길이보다')`));
|
||||
await set('cip-pile-cageLengthMm',12000); await set('cip-pile-mainCount',46); await set('cip-pile-tieKind','spiral');
|
||||
await click(`document.getElementById('cip-pile-apply')`);
|
||||
await until(`document.getElementById('cip-pile-cageLengthMm')?.value === '12000'`);
|
||||
check('pile panel shares the column connection', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`));
|
||||
await shot('02-pile-input');
|
||||
// ④ is no longer a header step (header ①②③, 2026-11-06): open the 재료표 from ③.
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('quantity clearly marks incomplete design', await ev(`document.getElementById('quantity-body').textContent.includes('전체 수량 미완료') && document.getElementById('quantity-body').textContent.includes('접합부')`));
|
||||
await shot('03-quantity-incomplete');
|
||||
await click(`document.getElementById('save-project')`);
|
||||
if (await ev(`document.getElementById('save-dialog').open`)) await click(`document.getElementById('sv-save')`);
|
||||
for (let i=0;i<120 && !(download && completed.has(download));i++) await delay(150);
|
||||
assert.ok(download && completed.has(download), 'project download completes');
|
||||
const savedText = fs.readFileSync(path.join(downloads,download),'utf8');
|
||||
const saved = JSON.parse(savedText), dedicated = saved.piers.find(p=>p.input.name==='P1').input.castInPlaceRebar;
|
||||
const shape = saved.piers.find(p=>p.input.name==='P1').input.castInPlaceShape;
|
||||
check('project preserves explicit shape settings without freezing automatic rock ELs',shape.casingOn===true && shape.clayBottomEl===ground-5 && shape.bedrockEl===undefined);
|
||||
check('download preserves independent column, pile and connection', dedicated.column.mainCount===46 && dedicated.pile.cageLengthMm===12000 && dedicated.connection.mode==='continuous');
|
||||
const savedPath = path.join(out,'saved.pzr.json'); fs.writeFileSync(savedPath,savedText);
|
||||
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});
|
||||
await until(`typeof window.openSampleProject === 'function'`); await ev('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`); await file(savedPath);
|
||||
await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`); await select('rebar-pier-items','P1'); await member('현장타설말뚝');
|
||||
check('fresh reopen restores pile values and shared connection', await ev(`document.getElementById('cip-pile-cageLengthMm').value==='12000' && document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`));
|
||||
await member('기둥');
|
||||
check('fresh reopen restores independent column values', await ev(`document.getElementById('cip-column-mainCount').value==='46'`));
|
||||
await click(`document.getElementById('nav-plan')`); await select('plan-pier-items','P1');
|
||||
await click(`[...document.querySelectorAll('#members button')].find(b=>b.textContent.startsWith('현장타설말뚝'))`);
|
||||
check('fresh reopen restores casing toggle and independent clay EL',await ev(`document.getElementById('cip-casing-on').checked && Number(document.getElementById('cip-shape-clayBottomEl').value)===${ground-5}`));
|
||||
check('no runtime exceptions in actual app',report.errors.length===0);
|
||||
report.result='PASS';
|
||||
})().catch(e=>{ report.result='FAIL'; report.failure=e.stack; process.exitCode=1; }).finally(async()=>{
|
||||
if(report.result==='FAIL' && send) try { const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,'failure.png'),Buffer.from(r.data,'base64')); } catch {}
|
||||
fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({result:report.result,checks:report.checks.length,errors:report.errors,failure:report.failure}));
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
// Actual app verification in an isolated Windows Chrome, CDP port 9444.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/cast-input-20261105');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [] };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let ws, send;
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0, download;
|
||||
const completed = new Set(), pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (m.method === 'Browser.downloadWillBegin') download = m.params.guid;
|
||||
if (m.method === 'Browser.downloadProgress' && m.params.state === 'completed') completed.add(m.params.guid);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 25000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => { for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); } throw new Error(`Wait failed: ${expression}`); };
|
||||
const click = expression => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled target: ' + ${JSON.stringify(expression)}); e.click(); })()`);
|
||||
const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const file = async name => {
|
||||
const doc = await send('DOM.getDocument');
|
||||
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId, files: [name] });
|
||||
};
|
||||
const shot = async name => { await delay(250); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); };
|
||||
const member = text => click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(text)}))`);
|
||||
const select = (id, name) => click(`[...document.querySelectorAll('#${id} button')].find(b => b.textContent.startsWith(${JSON.stringify(name + ' ·')}))`);
|
||||
const set = (id, value) => ev(`document.getElementById(${JSON.stringify(id)}).value = ${JSON.stringify(String(value))}`);
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
const downloads = 'C:\\Users\\User\\AppData\\Local\\Temp\\pierzainer-cast-input-20261105\\downloads';
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject === 'function'`);
|
||||
const fixture = await ev(`(async () => {
|
||||
const XLSX = await import('/node_modules/xlsx/xlsx.mjs');
|
||||
const {parseWorkbook} = await import('/src/core/parseWorkbook.ts');
|
||||
const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base = parseWorkbook(XLSX.read(b)).piers.find(p=>p.name==='P1');
|
||||
const piers=[];
|
||||
for (const [type,columnCount] of [['T형',1],['2주형',2],['3주형',3]]) for (const foundationType of ['현장타설말뚝','직접기초','말뚝기초']) {
|
||||
const input={...base,name:'P'+(piers.length+1),type,columnCount,foundationType};
|
||||
piers.push({id:input.name,input,defaults:input,draft:input});
|
||||
}
|
||||
return {schemaVersion:1,project:{name:'현장타설 입력 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
const fixturePath = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(fixturePath, JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`); await file(fixturePath);
|
||||
await until(`document.querySelectorAll('#plan-pier-items button').length === 9`);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const cast = [1,4,7].includes(i);
|
||||
await click(`document.getElementById('nav-plan')`); await select('plan-pier-items', 'P'+i);
|
||||
await click(`[...document.querySelectorAll('#members button')].find(b => b.textContent.startsWith(${JSON.stringify(cast ? '현장타설말뚝' : '확대기초')}))`);
|
||||
check(`P${i} plan: appropriate foundation controls`, await ev(cast
|
||||
? `!!document.querySelector('#members button[disabled]') && !!document.querySelector('#member-fields #edit-pileLengthMm') && !document.querySelector('#member-fields #edit-footingHeightMm')`
|
||||
: `!!document.querySelector('#member-fields #edit-footingHeightMm:not([disabled])')`));
|
||||
await click(`document.getElementById('nav-rebar')`); await member('코핑');
|
||||
check(`P${i} entering rebar builds coping drawings immediately`, await ev(`!!document.querySelector('#rebar-table figure svg')`));
|
||||
await member('기둥');
|
||||
check(`P${i} rebar: dedicated or legacy column`, await ev(cast
|
||||
? `!!document.querySelector('[data-cast-part="column"]') && [...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('확대기초')).disabled`
|
||||
: `!!document.getElementById('column-nDiv') && !document.querySelector('[data-cast-part]')`));
|
||||
}
|
||||
await select('rebar-pier-items','P1'); await member('기둥');
|
||||
await set('cip-column-mainDiameterMm',32); await set('cip-column-mainCount',46); await set('cip-column-coverMm',100);
|
||||
await set('cip-connection-mode','continuous'); await click(`document.getElementById('cip-column-apply')`);
|
||||
await until(`document.getElementById('cip-column-mainCount')?.value === '46'`);
|
||||
await shot('01-column-input');
|
||||
await member('현장타설말뚝');
|
||||
await set('cip-pile-cageLengthMm',999999); await click(`document.getElementById('cip-pile-apply')`);
|
||||
check('invalid cage length rejected with inline reason', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('철근망 길이가 말뚝 길이보다')`));
|
||||
await set('cip-pile-cageLengthMm',12000); await set('cip-pile-mainCount',46); await set('cip-pile-tieKind','spiral');
|
||||
await click(`document.getElementById('cip-pile-apply')`);
|
||||
await until(`document.getElementById('cip-pile-cageLengthMm')?.value === '12000'`);
|
||||
check('pile panel shares the column connection', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`));
|
||||
await shot('02-pile-input');
|
||||
// ④ is no longer a header step (header ①②③, 2026-11-06): open the 재료표 from ③.
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('quantity clearly marks incomplete design', await ev(`document.getElementById('quantity-body').textContent.includes('전체 수량 미완료') && document.getElementById('quantity-body').textContent.includes('접합부')`));
|
||||
await shot('03-quantity-incomplete');
|
||||
await click(`document.getElementById('save-project')`);
|
||||
if (await ev(`document.getElementById('save-dialog').open`)) await click(`document.getElementById('sv-save')`);
|
||||
for (let i=0;i<120 && !(download && completed.has(download));i++) await delay(150);
|
||||
assert.ok(download && completed.has(download), 'project download completes');
|
||||
const savedText = fs.readFileSync(path.join(downloads,download),'utf8');
|
||||
const saved = JSON.parse(savedText), dedicated = saved.piers.find(p=>p.input.name==='P1').input.castInPlaceRebar;
|
||||
check('download preserves independent column, pile and connection', dedicated.column.mainCount===46 && dedicated.pile.cageLengthMm===12000 && dedicated.connection.mode==='continuous');
|
||||
const savedPath = path.join(out,'saved.pzr.json'); fs.writeFileSync(savedPath,savedText);
|
||||
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});
|
||||
await until(`typeof window.openSampleProject === 'function'`); await ev('window.openSampleProject()');
|
||||
await click(`document.getElementById('open-project')`); await file(savedPath);
|
||||
await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`); await select('rebar-pier-items','P1'); await member('현장타설말뚝');
|
||||
check('fresh reopen restores pile values and shared connection', await ev(`document.getElementById('cip-pile-cageLengthMm').value==='12000' && document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`));
|
||||
await member('기둥');
|
||||
check('fresh reopen restores independent column values', await ev(`document.getElementById('cip-column-mainCount').value==='46'`));
|
||||
check('no runtime exceptions in actual app',report.errors.length===0);
|
||||
report.result='PASS';
|
||||
})().catch(e=>{ report.result='FAIL'; report.failure=e.stack; process.exitCode=1; }).finally(async()=>{
|
||||
if(report.result==='FAIL' && send) try { const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,'failure.png'),Buffer.from(r.data,'base64')); } catch {}
|
||||
fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({result:report.result,checks:report.checks.length,errors:report.errors,failure:report.failure}));
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
// Actual app: isolated Windows Chrome on CDP 9444, Vite on 5173. Run with Windows Node.
|
||||
const fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict');
|
||||
const out=path.resolve(__dirname,process.argv[2]||'../.verify/cast-rebar-20261106'); fs.mkdirSync(out,{recursive:true});
|
||||
const report={checks:[],errors:[],started:new Date().toISOString()};
|
||||
const delay=ms=>new Promise(r=>setTimeout(r,ms)); let ws,send;
|
||||
(async()=>{
|
||||
const target=await(await fetch('http://127.0.0.1:9444/json/new?about:blank',{method:'PUT'})).json();
|
||||
ws=new WebSocket(target.webSocketDebuggerUrl); await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;});
|
||||
let seq=0,download; const pending=new Map(), completed=new Set();
|
||||
ws.onmessage=({data})=>{const m=JSON.parse(data),p=pending.get(m.id);if(p){clearTimeout(p.timer);pending.delete(m.id);m.error?p.reject(new Error(JSON.stringify(m.error))):p.resolve(m.result);}
|
||||
if(m.method==='Runtime.exceptionThrown')report.errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);
|
||||
if(m.method==='Browser.downloadWillBegin')download=m.params.guid;
|
||||
if(m.method==='Browser.downloadProgress'&&m.params.state==='completed')completed.add(m.params.guid);
|
||||
if(m.method==='Page.javascriptDialogOpening'&&m.params.type==='beforeunload')send('Page.handleJavaScriptDialog',{accept:true}).catch(e=>report.errors.push(e.message));
|
||||
};
|
||||
send=(method,params={})=>new Promise((resolve,reject)=>{const id=++seq,timer=setTimeout(()=>{pending.delete(id);reject(new Error('Timeout '+method));},30000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}));});
|
||||
const ev=async expression=>{const r=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.exception?.description||r.exceptionDetails.text);return r.result.value;};
|
||||
const until=async expression=>{for(let i=0;i<120;i++){if(await ev(expression))return;await delay(150);}throw new Error('Wait: '+expression);};
|
||||
const click=expression=>ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing/disabled target');e.click();})()`);
|
||||
const set=(id,value)=>ev(`(()=>{const e=document.getElementById(${JSON.stringify(id)});e.value=${JSON.stringify(String(value))};e.dispatchEvent(new Event(e.tagName==='SELECT'?'change':'input',{bubbles:true}));})()`);
|
||||
const check=(name,value)=>{assert.ok(value,name);report.checks.push(name);console.log('PASS',name);};
|
||||
const shot=async name=>{await delay(250);const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,name+'.png'),Buffer.from(r.data,'base64'));};
|
||||
const open=async file=>{await click(`document.querySelector('#file-menu summary')`);await click(`document.getElementById('open-project')`);const doc=await send('DOM.getDocument');const{nodeId}=await send('DOM.querySelector',{nodeId:doc.root.nodeId,selector:'#project-file'});await send('DOM.setFileInputFiles',{nodeId,files:[file]});};
|
||||
const pier=name=>click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith(${JSON.stringify(name+' ·')}))`);
|
||||
const member=text=>click(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith(${JSON.stringify(text)}))`);
|
||||
const table=`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter(r=>r.cells.length===14).map(r=>({name:r.cells[2].textContent,size:r.cells[3].textContent,grade:r.cells[4].textContent,count:Number(r.cells[11].textContent)}))`;
|
||||
const downloads=path.join(out,'downloads');fs.mkdirSync(downloads,{recursive:true});
|
||||
await send('Page.enable');await send('Runtime.enable');await send('DOM.enable');
|
||||
await send('Browser.setDownloadBehavior',{behavior:'allowAndName',downloadPath:downloads,eventsEnabled:true});
|
||||
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
||||
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});await until(`typeof window.openSampleProject==='function'`);
|
||||
const fixture=await ev(`(async()=>{
|
||||
const XLSX=await import('/node_modules/xlsx/xlsx.mjs'),{parseWorkbook}=await import('/src/core/parseWorkbook.ts');
|
||||
const bytes=await(await fetch('/samples/'+encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base=parseWorkbook(XLSX.read(bytes)).piers.find(p=>p.name==='P1'),piers=[];
|
||||
for(const[type,columnCount]of[['T형',1],['2주형',2],['3주형',3]])for(const foundationType of ['현장타설말뚝','직접기초','말뚝기초']){
|
||||
const input={...base,name:'P'+(piers.length+1),type,columnCount,foundationType,groundEl:30,pileLengthMm:24000,
|
||||
...(foundationType==='현장타설말뚝'?{castInPlaceShape:{clayBottomEl:16},castInPlaceRebar:{column:{endZoneLengthMm:2000}}}:{})};
|
||||
piers.push({id:input.name,input,defaults:input,draft:input});
|
||||
}return{schemaVersion:1,project:{name:'현장타설 실배근 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'9개 합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
const file=path.join(out,'fixture.pzr.json');fs.writeFileSync(file,JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()');await open(file);await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
for(let i=1;i<=9;i++){
|
||||
await pier('P'+i);await member('기둥');const cast=[1,4,7].includes(i);
|
||||
const rows=await ev(table);
|
||||
check(`P${i}: appropriate real main and tie rows`,cast
|
||||
? ['기둥-P1','기둥-P2','말뚝-P1','말뚝-P2','기둥-P3','말뚝-P3','기둥-P5','말뚝-P5','접합-P1 커플러'].every(n=>rows.some(r=>r.name===n&&r.count>0))
|
||||
: ['P1','P2','P3','P5'].every(n=>rows.some(r=>r.name===n&&r.count>0)));
|
||||
check(`P${i}: list reports actual support status`,await ev(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith('P${i} ·')).parentElement.textContent.includes('${cast||i>=4?'일부 계산':'계산됨'}')`));
|
||||
}
|
||||
for(const i of [4,5,6,7,8,9]){
|
||||
await pier('P'+i);await member('코핑');const rows=await ev(table);
|
||||
check(`P${i}: multi drawing diameters reach actual table`,[['C1','H25'],['C7','H29'],['C8','H25'],['C10','H22'],['C12','H16'],['C14','H16']].every(([name,size])=>rows.some(r=>r.name===name&&r.size===size&&r.count>0)));
|
||||
check(`P${i}: multi defaults and remaining detail notice are visible`,await ev(`document.getElementById('coping-spacingS').value==='300'&&document.getElementById('coping-sideSpacing').value==='200'&&document.getElementById('coping-c14UpperSpacing').value==='600'&&document.getElementById('rebar-issues').textContent.includes('cycle별 이음')`));
|
||||
}
|
||||
await shot('00-multi-coping');
|
||||
await pier('P1');await member('기둥');
|
||||
check('dedicated drawing uses generated front and section paths',await ev(`!!document.querySelector('[data-cast-part] figure svg polyline') && document.querySelectorAll('[data-cast-part] figure svg circle').length>50`));
|
||||
check('missing bottom P6 remains explicit rather than hiding all reinforcement',await ev(`document.getElementById('rebar-issues').textContent.includes('말뚝 저부 P6 검토안 미적용')`));
|
||||
await set('review-model-height',420);await shot('01-real-rebar');
|
||||
const baseline=await ev(table);
|
||||
await set('cip-common-mainCount',50);await set('cip-common-mainDiameterMm',35.8);await set('cip-common-mainGradeMpa',400);await click(`document.getElementById('cip-apply')`);
|
||||
const edited=await ev(table);
|
||||
check('shared edits regenerate physical piece counts, diameters and grades',edited.filter(r=>r.name.endsWith('-P1')).every(r=>r.count%50===0&&r.count>=50&&r.size==='Ø35.8'&&r.grade==='SD400'));
|
||||
check('regeneration changes actual quantities rather than just form values',JSON.stringify(edited)!==JSON.stringify(baseline));
|
||||
await set('cip-pile-tieKind','spiral');await click(`document.getElementById('cip-apply')`);
|
||||
check('spiral produces its own real table rows', (await ev(table)).some(r=>r.name==='말뚝-P3 나선'&&r.count>0));
|
||||
await set('cip-pile-mainCount',48);await click(`document.getElementById('cip-apply')`);
|
||||
check('mismatched joints remove interface couplers and report the mismatch',(await ev(table)).every(r=>!r.name.startsWith('접합-'))&&await ev(`document.getElementById('rebar-issues').textContent.includes('불일치')`));
|
||||
await set('cip-pile-mainCount',50);await click(`document.getElementById('cip-apply')`);
|
||||
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find(r=>r.cells[2].textContent==='말뚝-P1')`);
|
||||
check('pile table selection highlights the corresponding generated mark',await ev(`[...document.querySelectorAll('#rebar-table tr.hl')].some(r=>r.cells[2].textContent==='말뚝-P1')`));
|
||||
await click(`[...document.querySelectorAll('#rebar-table button')].find(b=>b.textContent==='선택만 보기')`);
|
||||
await shot('02-pile-main-selected');
|
||||
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find(r=>r.cells[2].textContent==='말뚝-P1')`);
|
||||
await click(`[...document.querySelectorAll('#rebar-table button')].find(b=>b.textContent==='선택만 보기')`);
|
||||
await ev(`(()=>{const d=[...document.querySelectorAll('[data-cast-part] details')].find(d=>d.querySelector('summary').textContent.includes('삽도'));d.open=true;d.scrollIntoView({block:'start'});})()`);await shot('03-drawing');
|
||||
for(const width of [1280,1600,1920]){
|
||||
await send('Emulation.setDeviceMetricsOverride',{width,height:1000,deviceScaleFactor:1,mobile:false});await delay(150);
|
||||
check(`header ${width} keeps one row and no horizontal overflow`,await ev(`document.querySelector('.top').getBoundingClientRect().height<=65&&document.documentElement.scrollWidth<=innerWidth`));
|
||||
}
|
||||
// ④ is no longer a header step (header ①②③, 2026-11-06): open the 재료표 from ③.
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('quantity contains actual dedicated mains and reports remaining incomplete details',await ev(`document.getElementById('quantity-body').textContent.includes('말뚝-P1')&&document.getElementById('quantity-body').textContent.includes('기둥-P1')&&document.getElementById('quantity-body').textContent.includes('전체 수량 미완료')`));
|
||||
await shot('04-quantity');
|
||||
await click(`document.getElementById('nav-rebar')`);await member('기둥');const beforeSave=await ev(table);
|
||||
await click(`document.getElementById('save-project')`);if(await ev(`document.getElementById('save-dialog').open`))await click(`document.getElementById('sv-save')`);
|
||||
for(let i=0;i<120&&!(download&&completed.has(download));i++)await delay(150);assert.ok(download&&completed.has(download),'download completes');
|
||||
const text=fs.readFileSync(path.join(downloads,download),'utf8'),savedPath=path.join(out,'saved.pzr.json');fs.writeFileSync(savedPath,text);
|
||||
const saved=JSON.parse(text).piers.find(p=>p.input.name==='P1').input.castInPlaceRebar;
|
||||
check('download preserves generating inputs',saved.column.mainCount===50&&saved.pile.mainCount===50&&saved.pile.tieKind==='spiral'&&saved.column.mainGradeMpa===400);
|
||||
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});await until(`typeof window.openSampleProject==='function'`);await ev('window.openSampleProject()');await open(savedPath);await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`);await pier('P1');await member('기둥');
|
||||
check('fresh-page restore regenerates identical physical bar rows',JSON.stringify(await ev(table))===JSON.stringify(beforeSave));
|
||||
check('no runtime exceptions',report.errors.length===0);report.result='PASS';
|
||||
})().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(async()=>{
|
||||
if(report.result==='FAIL'&&send)try{const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,'failure.png'),Buffer.from(r.data,'base64'));}catch{}
|
||||
fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify({result:report.result,checks:report.checks.length,failure:report.failure}));ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
// CDP regression checks for the four scoped design improvements. Run with Windows Node.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const out = path.join(root, '.verify', 'design-fixes-20261105');
|
||||
const report = { checks: [], errors: [], captures: [], started: new Date().toISOString() };
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
let ws;
|
||||
let send;
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
async function main() {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?http://127.0.0.1:5173/', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout ${method}`)); }, 20000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
ws.onmessage = ({ data }) => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id) {
|
||||
const wait = pending.get(msg.id);
|
||||
if (!wait) return;
|
||||
clearTimeout(wait.timer); pending.delete(msg.id);
|
||||
if (msg.error) wait.reject(new Error(JSON.stringify(msg.error))); else wait.resolve(msg.result);
|
||||
} else if (msg.method === 'Runtime.exceptionThrown') report.errors.push(msg.params.exceptionDetails.exception?.description || msg.params.exceptionDetails.text);
|
||||
else if (msg.method === 'Runtime.consoleAPICalled' && msg.params.type === 'error') report.errors.push(msg.params.args.map((a) => a.value ?? a.description).join(' '));
|
||||
};
|
||||
const ev = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async (expression) => {
|
||||
for (let i = 0; i < 100; i++) { if (await ev(expression)) return; await delay(150); }
|
||||
throw new Error(`Timeout: ${expression}`);
|
||||
};
|
||||
const click = (expression) => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled control'); e.click(); })()`);
|
||||
const shot = async (name) => {
|
||||
await delay(200);
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, `${name}.png`), Buffer.from(r.data, 'base64'));
|
||||
report.captures.push(name);
|
||||
};
|
||||
const check = (name, condition) => { assert.ok(condition, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const upload = async () => {
|
||||
const doc = await send('DOM.getDocument');
|
||||
const node = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [] });
|
||||
await send('DOM.setFileInputFiles', { nodeId: node.nodeId, files: [path.resolve(root, '../../RAG/운곡교P1입력.xlsx')] });
|
||||
await until(`!document.getElementById('dlg-apply').disabled && document.getElementById('import-abut-A1')?.checked === true`);
|
||||
};
|
||||
const member = async (label) => { await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(label)}))`); await delay(100); };
|
||||
const lower = () => ev(`document.querySelector('#review-workspace > section').getBoundingClientRect().height`);
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await until(`!!document.querySelector('[data-card="excel"] button') && typeof window.loadBytesToApp === 'function'`);
|
||||
await click(`document.querySelector('[data-card="excel"] button')`);
|
||||
await ev(`document.getElementById('project-name').value = '디자인 개선 검증'; document.getElementById('project-name').dispatchEvent(new Event('input'));`);
|
||||
await upload();
|
||||
check('combined import scope', await ev(`document.getElementById('dlg-apply').textContent === '교각 1기 + 교대 1기 가져오기'`));
|
||||
check('A1 source and dimensions preview', await ev(`document.getElementById('pv-abuts').textContent.includes('A1') && document.getElementById('pv-abuts').textContent.includes('12.197') && document.querySelector('#sheets > li:first-child').textContent.includes('교대 인식 결과')`));
|
||||
await shot('01-import-both');
|
||||
await click(`document.getElementById('import-abut-A1')`);
|
||||
check('uncheck updates preview and count', await ev(`document.getElementById('dlg-apply').textContent.includes('교대 0기') && document.getElementById('pv-abuts').textContent.includes('아니오')`));
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
check('unchecked abutment is not applied', await ev(`![...document.querySelectorAll('#pier-items button')].some(b => b.textContent.includes('교대'))`));
|
||||
await click(`document.getElementById('open-import')`);
|
||||
await upload();
|
||||
await click(`document.getElementById('dlg-apply')`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('A1') && b.textContent.includes('교대'))`);
|
||||
await member('확대기초');
|
||||
check('A1 footing excludes pier review questions', await ev(`document.getElementById('review-summary').textContent.includes('A1') && document.getElementById('review-summary').textContent.includes('미결 0건') && !document.getElementById('review-tasks').textContent.includes('Q-C9')`));
|
||||
await delay(4500); await shot('02-workspace-desktop');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
|
||||
await delay(300);
|
||||
if (await ev(`document.getElementById('review-model-toggle').getAttribute('aria-expanded') === 'false'`)) await click(`document.getElementById('review-model-toggle')`);
|
||||
await ev(`const size = document.getElementById('review-model-height'); size.value = size.max; size.dispatchEvent(new Event('input'));`);
|
||||
await delay(200);
|
||||
report.layoutBefore = await lower();
|
||||
await ev(`const range = document.getElementById('review-model-height'); range.value = '180'; range.dispatchEvent(new Event('input'));`);
|
||||
await delay(200);
|
||||
report.layoutResized = await lower();
|
||||
check('height control expands evidence area', report.layoutResized >= report.layoutBefore);
|
||||
check('compact viewer tools stay inside model', await ev(`document.querySelector('.viewer-stack-right').getBoundingClientRect().bottom <= document.getElementById('viewer-block').getBoundingClientRect().bottom + 1`));
|
||||
await click(`document.getElementById('review-model-toggle')`);
|
||||
await delay(200);
|
||||
report.layoutCollapsed = await lower();
|
||||
check('collapse adds model area to evidence', report.layoutCollapsed > report.layoutResized + 150);
|
||||
await click(`document.getElementById('review-table')`);
|
||||
check('table shortcut focuses and reveals table', await ev(`document.activeElement.matches('#rebar-table table.rebar-table') && document.activeElement.getBoundingClientRect().top >= document.querySelector('#review-workspace > section').getBoundingClientRect().top - 2`));
|
||||
await shot('03-table-laptop');
|
||||
await click(`document.getElementById('review-inputs')`);
|
||||
check('input shortcut preserves keyboard tab order', await ev(`document.activeElement.matches('#rebar-table input') && document.activeElement.tabIndex === 0`));
|
||||
await shot('04-inputs-laptop');
|
||||
check('layout preference persisted', await ev(`JSON.parse(localStorage.getItem('pierzainer.review-layout')).collapsed === true`));
|
||||
await click(`document.getElementById('review-model-toggle')`);
|
||||
await member('받침블럭');
|
||||
check('block review is scoped', await ev(`document.getElementById('review-tasks').textContent.includes('Q-S4') && !document.getElementById('review-tasks').textContent.includes('Q-C9')`));
|
||||
await click(`document.querySelector('#review-tasks button')`);
|
||||
check('review action selects correct bar and row', await ev(`document.querySelector('#rebar-table .rebar-select').textContent.includes('선택 1 /') && document.activeElement.matches('tr') && document.activeElement.textContent.includes('S1')`));
|
||||
await member('날개벽');
|
||||
check('wing review shows only relevant unresolved work', await ev(`document.getElementById('review-summary').textContent.includes('미결 2건') && document.getElementById('review-tasks').textContent.includes('E19') && document.getElementById('review-tasks').textContent.includes('V-A2')`));
|
||||
await shot('05-review-and-materials');
|
||||
report.fields = await ev(`[...document.querySelectorAll('.material-fields .mat')].map(label => { const r = label.getBoundingClientRect(); const c = label.querySelector('input,select').getBoundingClientRect(); return { label: label.textContent, left: r.left, top: r.top, width: r.width, controlLeft: c.left, controlRight: c.right, bottom: r.bottom }; })`);
|
||||
check('material fields align without overlap', report.fields.every((f, i, list) => f.controlRight <= f.left + f.width + 1 && (i === 0 || f.top >= list[i - 1].bottom)));
|
||||
check('document has no horizontal overflow at laptop width', await ev('document.documentElement.scrollWidth === innerWidth'));
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b => b.textContent.includes('P1') && !b.textContent.includes('교대'))`);
|
||||
await member('기둥');
|
||||
check('existing decision is not presented as unresolved', await ev(`document.getElementById('review-tasks').textContent.includes('Q-C9') && document.getElementById('review-tasks').textContent.includes('결정됨') && document.getElementById('review-summary').textContent.includes('미결 0건')`));
|
||||
check('no runtime errors', report.errors.length === 0);
|
||||
report.result = 'PASS';
|
||||
}
|
||||
main().catch((error) => { report.result = 'FAIL'; report.failure = error.stack; process.exitCode = 1; }).finally(async () => {
|
||||
if (report.result === 'FAIL' && send) {
|
||||
try { const shot = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, 'failure.png'), Buffer.from(shot.data, 'base64')); } catch { /* preserve original failure */ }
|
||||
}
|
||||
report.finished = new Date().toISOString();
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify({ result: report.result, checks: report.checks.length, errors: report.errors, failure: report.failure }));
|
||||
if (send) { try { await send('Page.close'); } catch { /* tab may already be closed */ } }
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173.
|
||||
const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/merge-20261106');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [] };
|
||||
let ws, send;
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; });
|
||||
let seq = 0; const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => reject(new Error('Timeout ' + method)), 30000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => {
|
||||
for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); }
|
||||
throw new Error('Wait ' + expression);
|
||||
};
|
||||
const check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const click = expression => ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing target');e.click();})()`);
|
||||
const shot = async name => {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64'));
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject==='function'`);
|
||||
const fixture = await ev(`(async()=>{
|
||||
const XLSX=await import('/node_modules/xlsx/xlsx.mjs'),{parseWorkbook}=await import('/src/core/parseWorkbook.ts');
|
||||
const bytes=await(await fetch('/'+encodeURIComponent('도면추출')+'/samples/'+encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base=parseWorkbook(XLSX.read(bytes)).piers.find(p=>p.name==='P1');
|
||||
if(!base)throw new Error('Missing sample pier');
|
||||
const piers=['직접기초','현장타설말뚝'].map((foundationType,i)=>{const input={...base,name:'P'+(i+1),foundationType,type:'T형',columnCount:1,groundEl:30,pileLengthMm:24000,castInPlaceShape:{clayBottomEl:16}};return{id:input.name,input,defaults:input,draft:input};});
|
||||
return{schemaVersion:1,project:{name:'병합 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
const file = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(file, JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()');
|
||||
await click(`document.querySelector('#file-menu summary')`); await click(`document.getElementById('open-project')`);
|
||||
const doc = await send('DOM.getDocument');
|
||||
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId, files: [file] });
|
||||
await until(`document.querySelectorAll('#rebar-pier-items button').length===2`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
check('local rebar screen renders', await ev(`!document.getElementById('view-rebar').hidden&&!!document.querySelector('#rebar-table table')`));
|
||||
await click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith('P2 ·'))`);
|
||||
await click(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('기둥'))`);
|
||||
check('unified cast input and physical rebar preserved', await ev(`!!document.getElementById('cip-apply')&&document.getElementById('rebar-table').textContent.includes('말뚝-P1')`));
|
||||
await shot('01-cast');
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('quantity workspace from origin renders', await ev(`!document.getElementById('view-quantity').hidden&&!!document.querySelector('.takeoff-workspace .takeoff-toolbar')`));
|
||||
await click(`document.querySelector('[data-takeoff-mode="materials"]')`);
|
||||
check('local material table preserved', await ev(`document.getElementById('quantity-body').textContent.includes('말뚝-P1')`));
|
||||
await shot('02-materials');
|
||||
await click(`document.getElementById('nav-drawing')`);
|
||||
await until(`!!document.querySelector('#view-drawing svg')`);
|
||||
check('origin drawing screen renders finite geometry', await ev(`!document.getElementById('view-drawing').hidden&&!/NaN|Infinity/.test(document.querySelector('#view-drawing svg').outerHTML)`));
|
||||
await shot('03-drawing');
|
||||
await click(`document.getElementById('nav-plan')`);
|
||||
check('plan remains reachable after drawing', await ev(`!document.getElementById('view-plan').hidden`));
|
||||
for (const width of [1280, 1600, 1920]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await delay(150);
|
||||
check(`one-row header preserved at ${width}`, await ev(`document.querySelector('.top').getBoundingClientRect().height<=65&&document.documentElement.scrollWidth<=innerWidth`));
|
||||
}
|
||||
check('no runtime exceptions', report.errors.length === 0);
|
||||
report.result = 'PASS';
|
||||
})().catch(e => { report.result = 'FAIL'; report.failure = e.stack; process.exitCode = 1; }).finally(() => {
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 2));
|
||||
console.log(JSON.stringify(report)); ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
// Run with Windows Node against the task's isolated Chrome CDP on port 9444.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const out = path.resolve(__dirname, '../.verify/multicol-coping-20261105');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
(async () => {
|
||||
const targets = await (await fetch('http://127.0.0.1:9444/json')).json();
|
||||
const page = targets.find(t => t.type === 'page' && t.url.startsWith('http://127.0.0.1:5173'));
|
||||
if (!page) throw new Error('Task browser page missing');
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let seq = 0;
|
||||
const pending = new Map();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const msg = JSON.parse(data);
|
||||
const p = pending.get(msg.id);
|
||||
if (p) { pending.delete(msg.id); clearTimeout(p.timer); msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result); }
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq;
|
||||
const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 20000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
try {
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1200, height: 750, deviceScaleFactor: 1, mobile: false });
|
||||
const results = [];
|
||||
for (const type of ['2주형', '3주형']) {
|
||||
const res = await send('Runtime.evaluate', { awaitPromise: true, returnByValue: true, expression: `
|
||||
(async () => {
|
||||
const THREE = await import('/node_modules/.vite/deps/three.js');
|
||||
const XLSX = await import('/node_modules/xlsx/xlsx.mjs');
|
||||
const { parseWorkbook } = await import('/src/core/parseWorkbook.ts');
|
||||
const { buildPier } = await import('/src/core/geometry.ts');
|
||||
const { buildPierScene } = await import('/src/view/scene.ts');
|
||||
const buf = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base = parseWorkbook(XLSX.read(buf)).piers.find(p => p.name === 'P1');
|
||||
if (!base) throw new Error('P1 sample missing');
|
||||
// 3주형 needs a 20 m coping for the 오목형 spans (P1 length leaves no flat part, RB-M01).
|
||||
const m = buildPier({ ...base, ...(${type === '3주형'} ? { copingLengthMm: 20000 } : {}), type: ${JSON.stringify(type)}, columnCount: ${type === '2주형' ? 2 : 3} });
|
||||
const built = buildPierScene(m);
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0xe9eef4);
|
||||
const group = new THREE.Group();
|
||||
group.add(built.parts.coping, built.parts.columns); scene.add(group);
|
||||
const center = new THREE.Vector3();
|
||||
const bounds = new THREE.Box3().setFromObject(built.parts.coping);
|
||||
bounds.getCenter(center);
|
||||
// Align the camera along the cross-axis to make both interior haunches visible.
|
||||
const u = new THREE.Vector3(m.u.e, 0, -m.u.n);
|
||||
const cross = new THREE.Vector3(m.nrm.e, 0, -m.nrm.n);
|
||||
const camera = new THREE.PerspectiveCamera(36, 1200/750, 0.1, 1000);
|
||||
const target = center.clone().add(new THREE.Vector3(0, -0.9, 0));
|
||||
camera.position.copy(target).addScaledVector(cross, m.copingL * 1.6).addScaledVector(u, -m.copingL * 0.12).add(new THREE.Vector3(0,-2.5,0));
|
||||
camera.lookAt(target);
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x687784, 2.4));
|
||||
const light = new THREE.DirectionalLight(0xffffff, 2); light.position.copy(camera.position).add(new THREE.Vector3(0,10,0)); scene.add(light);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
||||
renderer.setSize(1200,750); renderer.localClippingEnabled = true;
|
||||
document.body.replaceChildren(renderer.domElement); document.body.style.margin = '0';
|
||||
renderer.render(scene,camera);
|
||||
const profile = m.copingParts[0].profile;
|
||||
const raised = profile.filter(([,z]) => Math.abs(z-m.levelBottom-0.5)<1e-8).length;
|
||||
if (raised !== (m.columns.length-1)*2) throw new Error('Raised span count mismatch');
|
||||
if (Math.abs(Math.min(...profile.map(p=>p[1]))-m.levelBottom)>1e-8) throw new Error('Underside below column Level');
|
||||
if (!m.columns.every(c=>c.top===m.levelBottom)) throw new Error('Column connection changed');
|
||||
return { type: ${JSON.stringify(type)}, raisedVertices: raised, levelBottom:m.levelBottom, interiorBottom:m.levelBottom+0.5, columns:m.columns.length, profile, screenshot:renderer.domElement.toDataURL('image/png') };
|
||||
})()
|
||||
` });
|
||||
if (res.exceptionDetails) throw new Error(res.exceptionDetails.exception?.description || res.exceptionDetails.text);
|
||||
const { screenshot, ...result } = res.result.value;
|
||||
fs.writeFileSync(path.join(out, `${type}.png`), Buffer.from(screenshot.split(',')[1], 'base64'));
|
||||
results.push(result);
|
||||
}
|
||||
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(results, null, 2));
|
||||
console.log(JSON.stringify(results.map(({ profile, ...r }) => r)));
|
||||
} finally { ws.close(); }
|
||||
})().catch(e => { console.error(e); process.exitCode = 1; });
|
||||
@@ -1,43 +0,0 @@
|
||||
// Clicks the template download button in the import dialog through Chrome DevTools
|
||||
// and reports the saved file. Run with Windows Node (same machine as Chrome):
|
||||
// node verify-template-download.cjs "C:\\Users\\...\\Temp\\verify-downloads"
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const DL_DIR = process.argv[2];
|
||||
const CDP = 'http://127.0.0.1:9444';
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(DL_DIR, { recursive: true });
|
||||
const page = (await (await fetch(`${CDP}/json`)).json()).find((t) => t.type === 'page');
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
||||
let seq = 0;
|
||||
const waiting = new Map();
|
||||
ws.onmessage = (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && waiting.has(m.id)) { waiting.get(m.id)(m); waiting.delete(m.id); }
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((res) => {
|
||||
const id = ++seq; waiting.set(id, res); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evalJs = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 300));
|
||||
return r.result.result.value;
|
||||
};
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: DL_DIR, eventsEnabled: false });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await sleep(4000);
|
||||
await evalJs(`document.getElementById('open-import').click()`);
|
||||
await sleep(500);
|
||||
await evalJs(`document.getElementById('download-template').click()`);
|
||||
await sleep(4000);
|
||||
const files = fs.readdirSync(DL_DIR);
|
||||
console.log('DOWNLOADED', JSON.stringify(files));
|
||||
for (const f of files) console.log('SIZE', f, fs.statSync(path.join(DL_DIR, f)).size);
|
||||
ws.close();
|
||||
}
|
||||
main().catch((e) => { console.error('FAILED', e); process.exit(1); });
|
||||
@@ -1,113 +0,0 @@
|
||||
// UI verification driven through the Chrome DevTools Protocol.
|
||||
// Run with Windows Node (same machine as Chrome), e.g.:
|
||||
// node verify-ui.cjs "C:\\Users\\...\\input.xlsx" "\\\\wsl.localhost\\Ubuntu\\...\\shots"
|
||||
// Chrome must be started with --remote-debugging-port=9444 (see DOC/05_검증).
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const APP_URL = process.env.APP_URL || 'http://127.0.0.1:5173/';
|
||||
const CDP = process.env.CDP || 'http://127.0.0.1:9444';
|
||||
const INPUT_FILE = process.argv[2];
|
||||
const OUT_DIR = process.argv[3];
|
||||
if (!INPUT_FILE || !OUT_DIR) throw new Error('INPUT_FILE and OUT_DIR are required');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
const targets = await (await fetch(`${CDP}/json`)).json();
|
||||
const page = targets.find((t) => t.type === 'page');
|
||||
if (!page) throw new Error('no page target');
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
await new Promise((res, rej) => {
|
||||
ws.onopen = res;
|
||||
ws.onerror = rej;
|
||||
});
|
||||
|
||||
let seq = 0;
|
||||
const waiting = new Map();
|
||||
// Console and exception events, collected from the first navigation onward (V10 ③).
|
||||
const consoleEvents = [];
|
||||
ws.onmessage = (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && waiting.has(m.id)) {
|
||||
waiting.get(m.id)(m);
|
||||
waiting.delete(m.id);
|
||||
} else if (m.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(m.params.type)) {
|
||||
const text = m.params.args.map((a) => a.value ?? a.description ?? '').join(' ');
|
||||
consoleEvents.push(`console.${m.params.type}: ${text}`);
|
||||
} else if (m.method === 'Runtime.exceptionThrown') {
|
||||
const d = m.params.exceptionDetails;
|
||||
consoleEvents.push(`exception: ${d.exception?.description || d.text}`);
|
||||
} else if (m.method === 'Log.entryAdded' && ['error', 'warning'].includes(m.params.entry.level)) {
|
||||
consoleEvents.push(`log.${m.params.entry.level}: ${m.params.entry.text} ${m.params.entry.url || ''}`.trim());
|
||||
}
|
||||
};
|
||||
const send = (method, params = {}) =>
|
||||
new Promise((res) => {
|
||||
const id = ++seq;
|
||||
waiting.set(id, res);
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evalJs = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 400));
|
||||
return r.result.result.value;
|
||||
};
|
||||
const shot = async (name) => {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' });
|
||||
fs.writeFileSync(path.join(OUT_DIR, name), Buffer.from(r.result.data, 'base64'));
|
||||
console.log(`SHOT ${name}`);
|
||||
};
|
||||
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
await send('Log.enable');
|
||||
await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
await send('Page.navigate', { url: APP_URL });
|
||||
await sleep(4000);
|
||||
|
||||
// V11 — dialog with the user's three-sheet workbook.
|
||||
await evalJs(`document.getElementById('open-import').click()`);
|
||||
await sleep(500);
|
||||
await evalJs(`(() => { const i = document.getElementById('project-name'); i.value = '운곡교 검증';
|
||||
i.dispatchEvent(new Event('input')); return true; })()`);
|
||||
const doc = await send('DOM.getDocument', { depth: -1 });
|
||||
const q = await send('DOM.querySelector', { nodeId: doc.result.root.nodeId, selector: '#file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId: q.result.nodeId, files: [INPUT_FILE] });
|
||||
await sleep(3000);
|
||||
|
||||
console.log('SHEETS_TEXT_BEGIN');
|
||||
console.log(await evalJs(`document.getElementById('sheets').innerText`));
|
||||
console.log('SHEETS_TEXT_END');
|
||||
console.log('SELECTED', await evalJs(`document.getElementById('dlg-selected').textContent`));
|
||||
console.log('APPLY', await evalJs(`(() => { const b = document.getElementById('dlg-apply');
|
||||
return b.textContent + ' | disabled=' + b.disabled; })()`));
|
||||
await shot('v11_dialog_user_file.png');
|
||||
|
||||
await evalJs(`document.getElementById('dlg-apply').click()`);
|
||||
await sleep(2000);
|
||||
console.log('STATUS', await evalJs(`document.getElementById('status').textContent`));
|
||||
console.log('PIERS', await evalJs(`[...document.querySelectorAll('#pier-items li')].map(li => li.textContent.trim()).join(' | ')`));
|
||||
console.log('RESULT_AXIS', await evalJs(`document.getElementById('res-axis').innerText.replace(/\\n+/g, ' / ')`));
|
||||
console.log('RESULT_FOUND', await evalJs(`document.getElementById('res-found').innerText.replace(/\\n+/g, ' / ')`));
|
||||
await shot('v11_main_user_file.png');
|
||||
|
||||
// V10 ② — the four view presets.
|
||||
for (const label of ['정면', '측면', '평면', '아이소']) {
|
||||
await evalJs(`[...document.querySelectorAll('#views button')].find((b) => b.textContent === '${label}').click()`);
|
||||
await sleep(1500);
|
||||
await shot(`v10_view_${label}.png`);
|
||||
console.log('PRESSED', label, await evalJs(`[...document.querySelectorAll('#views button')].filter((b) => b.getAttribute('aria-pressed') === 'true').map((b) => b.textContent).join(',')`));
|
||||
}
|
||||
|
||||
// V10 ③ — every console error/warning and uncaught exception seen during the run above.
|
||||
console.log(`CONSOLE_EVENTS ${consoleEvents.length}`);
|
||||
for (const e of consoleEvents) console.log(` ${e}`);
|
||||
ws.close();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('FAILED', e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
// Run with Windows Node against an isolated Chrome CDP session (port 9444).
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const out = path.resolve(__dirname, '../.verify/unified-input-20261106');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const report = { checks: [], errors: [], layouts: [] };
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let ws, send;
|
||||
(async () => {
|
||||
const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json();
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
let seq = 0, download;
|
||||
const pending = new Map(), completed = new Set();
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data), p = pending.get(m.id);
|
||||
if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); }
|
||||
if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (m.method === 'Browser.downloadWillBegin') download = m.params.guid;
|
||||
if (m.method === 'Browser.downloadProgress' && m.params.state === 'completed') completed.add(m.params.guid);
|
||||
};
|
||||
send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++seq, timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 25000);
|
||||
pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const ev = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text);
|
||||
return r.result.value;
|
||||
};
|
||||
const until = async expression => { for (let i = 0; i < 100; i++) { if (await ev(expression)) return; await delay(150); } throw new Error(`Wait failed: ${expression}`); };
|
||||
const click = expression => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled target'); e.click(); })()`);
|
||||
const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); };
|
||||
const set = (id, value) => ev(`(() => {const e=document.getElementById(${JSON.stringify(id)}); e.value=${JSON.stringify(String(value))}; e.dispatchEvent(new Event(e.tagName==='SELECT'?'change':'input',{bubbles:true}));})()`);
|
||||
const shot = async name => { const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); };
|
||||
const member = text => click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(text)}))`);
|
||||
const selectPier = name => click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith(${JSON.stringify(name+' ·')}))`);
|
||||
const open = async name => {
|
||||
await click(`document.querySelector('#file-menu summary')`);
|
||||
await click(`document.getElementById('open-project')`);
|
||||
const doc = await send('DOM.getDocument');
|
||||
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' });
|
||||
await send('DOM.setFileInputFiles', { nodeId, files: [name] });
|
||||
};
|
||||
await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable');
|
||||
await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
const downloads = path.join(out, 'downloads'); fs.mkdirSync(downloads, { recursive: true });
|
||||
await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true });
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' });
|
||||
await until(`typeof window.openSampleProject==='function'`);
|
||||
const fixture = await ev(`(async () => {
|
||||
const XLSX=await import('/node_modules/xlsx/xlsx.mjs');
|
||||
const {parseWorkbook}=await import('/src/core/parseWorkbook.ts');
|
||||
const bytes=await (await fetch('/samples/'+encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
||||
const base=parseWorkbook(XLSX.read(bytes)).piers.find(p=>p.name==='P1');
|
||||
const piers=[];
|
||||
for(const [type,columnCount] of [['T형',1],['2주형',2],['3주형',3]]) for(const foundationType of ['현장타설말뚝','직접기초','말뚝기초']) {
|
||||
const input={...base,name:'P'+(piers.length+1),type,columnCount,foundationType};
|
||||
piers.push({id:input.name,input,defaults:input,draft:input});
|
||||
}
|
||||
return {schemaVersion:1,project:{name:'입력 통합 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]};
|
||||
})()`);
|
||||
fixture.project.name = '아주 긴 프로젝트명으로 말줄임과 한 줄 헤더를 검증하는 프로젝트';
|
||||
fixture.project.bridge = fixture.project.name;
|
||||
fixture.piers[0].input.castInPlaceRebar = {
|
||||
column: { mainCount: 46, mainDiameterMm: 32, coverMm: 100, coverReference: 'main-center' },
|
||||
pile: { mainCount: 48, mainDiameterMm: 32, coverMm: 100, coverReference: 'clear', tieKind: 'spiral', tieSpacingMm: 200, cageLengthMm: 12000 },
|
||||
connection: { mode: 'coupler', couplerLengthMm: 165 },
|
||||
};
|
||||
fixture.piers[0].draft = fixture.piers[0].input;
|
||||
const fixturePath = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(fixturePath, JSON.stringify(fixture));
|
||||
await ev('window.openSampleProject()'); await open(fixturePath);
|
||||
await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const cast = [1, 4, 7].includes(i);
|
||||
await selectPier('P'+i); await member('기둥');
|
||||
check(`P${i}: correct foundation-specific member list and editor`, await ev(cast
|
||||
? `document.querySelectorAll('#rebar-tabs button').length===3 && !!document.querySelector('[data-cast-part="combined"]') && ![...document.querySelectorAll('#rebar-tabs button')].some(b=>b.textContent.includes('확대기초'))`
|
||||
: `!!document.getElementById('column-nDiv') && [...document.querySelectorAll('#rebar-tabs button')].some(b=>b.textContent.startsWith('확대기초')) && !document.querySelector('[data-cast-part]')`));
|
||||
}
|
||||
await selectPier('P4'); await member('기둥');
|
||||
check('equal defaults show one shared value with exceptions collapsed', await ev(`!document.getElementById('cip-exceptions').open && document.getElementById('cip-common-mainCount').value==='46'`));
|
||||
await set('cip-common-coverMm', 120); await set('cip-common-coverReference', 'clear'); await click(`document.getElementById('cip-apply')`);
|
||||
check('explicit common cover and reference apply to both segments', await ev(`document.getElementById('cip-column-coverMm').value==='120' && document.getElementById('cip-pile-coverMm').value==='120' && document.getElementById('cip-column-coverReference').value==='clear' && document.getElementById('cip-pile-coverReference').value==='clear'`));
|
||||
await selectPier('P1'); await member('기둥');
|
||||
check('unequal counts and equal cover numbers with different references remain distinct', await ev(`document.getElementById('cip-exceptions').open && document.getElementById('cip-common-mainCount').value==='' && document.getElementById('cip-common-coverMm').value==='' && document.getElementById('cip-column-mainCount').value==='46' && document.getElementById('cip-pile-mainCount').value==='48'`));
|
||||
check('one connection editor and one save button', await ev(`document.querySelectorAll('#cip-connection-mode').length===1 && document.querySelectorAll('#cip-apply').length===1`));
|
||||
await set('cip-common-innerMainDiameterMm', 35.8); await click(`document.getElementById('cip-apply')`);
|
||||
check('shared edit updates both segments and preserves unrelated exceptions', await ev(`document.getElementById('cip-column-innerMainDiameterMm').value==='35.8' && document.getElementById('cip-pile-innerMainDiameterMm').value==='35.8' && document.getElementById('cip-column-mainCount').value==='46' && document.getElementById('cip-pile-mainCount').value==='48' && document.getElementById('cip-pile-coverReference').value==='clear' && document.getElementById('cip-column-coverReference').value==='main-center'`));
|
||||
await set('cip-pile-cageLengthMm', 999999); await click(`document.getElementById('cip-apply')`);
|
||||
check('invalid cage length is rejected inline', await ev(`document.querySelector('[data-cast-part]').textContent.includes('철근망 길이가 말뚝 길이보다')`));
|
||||
await set('cip-pile-cageLengthMm', 12000); await set('cip-common-mainCount', 46.5); await click(`document.getElementById('cip-apply')`);
|
||||
check('fractional main counts are rejected', await ev(`document.querySelector('[role="status"]').textContent!==undefined && document.querySelector('[data-cast-part]').textContent.includes('양수 정수')`));
|
||||
await set('cip-common-mainCount', 50); await set('cip-pile-mainCount', 52); await set('cip-connection-mode', 'continuous');
|
||||
await click(`document.getElementById('cip-apply')`);
|
||||
check('explicit pile exception overrides a shared edit without changing column', await ev(`document.getElementById('cip-column-mainCount').value==='50' && document.getElementById('cip-pile-mainCount').value==='52' && document.getElementById('cip-pile-tieSpacingMm').value==='200' && document.getElementById('cip-connection-couplerLengthMm').value==='165'`));
|
||||
check('pending review remains visible', await ev(`document.getElementById('review-tasks').textContent.includes('CP-F1') && document.getElementById('nav-rebar').getAttribute('aria-label').includes('현장타설 배근 미확정')`));
|
||||
await shot('01-unified-input');
|
||||
for (const width of [1280,1600,1920]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { width, height: 1000, deviceScaleFactor: 1, mobile: false }); await delay(200);
|
||||
const layout = await ev(`(() => {const h=document.querySelector('.top'),r=h.getBoundingClientRect();return {width:innerWidth,height:r.height,overflow:document.documentElement.scrollWidth>innerWidth,rows:[...h.children].map(e=>Math.round(e.getBoundingClientRect().top)),project:document.getElementById('project-line').textContent};})()`);
|
||||
report.layouts.push(layout);
|
||||
check(`header ${width}: single row and no document overflow`, layout.height<=65 && !layout.overflow);
|
||||
await shot('header-'+width);
|
||||
}
|
||||
check('duplicate project/bridge name is omitted while full title remains available', await ev(`document.getElementById('project-line').title===${JSON.stringify(fixture.project.name+' · 교각 9기')}`));
|
||||
await click(`document.querySelector('#file-menu summary')`);
|
||||
check('file menu opens', await ev(`document.getElementById('file-menu').open`));
|
||||
await ev(`document.getElementById('open-project').focus()`);
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
||||
check('Escape closes the menu and restores summary focus', await ev(`!document.getElementById('file-menu').open && document.activeElement===document.querySelector('#file-menu summary')`));
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', text: '\r', windowsVirtualKeyCode: 13 });
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 });
|
||||
check('keyboard Enter opens the native file menu', await ev(`document.getElementById('file-menu').open`));
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
||||
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
||||
check('Tab reaches the Excel import action', await ev(`document.activeElement.id==='open-import'`));
|
||||
await click(`document.getElementById('open-import')`);
|
||||
check('Excel action opens the existing import dialog and closes the menu', await ev(`!document.getElementById('file-menu').open && !!document.querySelector('dialog[open]')`));
|
||||
await ev(`document.querySelector('dialog[open]').close()`);
|
||||
await click(`document.getElementById('save-project')`);
|
||||
if (await ev(`document.getElementById('save-dialog').open`)) await click(`document.getElementById('sv-save')`);
|
||||
for (let i=0;i<120 && !(download && completed.has(download));i++) await delay(150);
|
||||
assert.ok(download && completed.has(download), 'download completes');
|
||||
const savedText = fs.readFileSync(path.join(downloads,download), 'utf8');
|
||||
const saved = JSON.parse(savedText).piers.find(p=>p.input.name==='P1').input.castInPlaceRebar;
|
||||
check('download retains independent segment settings and connection schema', saved.column.mainCount===50 && saved.pile.mainCount===52 && saved.pile.coverReference==='clear' && saved.column.coverReference==='main-center' && saved.connection.mode==='continuous' && !saved.common);
|
||||
const savedPath = path.join(out,'saved.pzr.json'); fs.writeFileSync(savedPath,savedText);
|
||||
await send('Page.navigate', { url: 'http://127.0.0.1:5173/' }); await until(`typeof window.openSampleProject==='function'`);
|
||||
await ev('window.openSampleProject()'); await open(savedPath); await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
||||
await click(`document.getElementById('nav-rebar')`); await selectPier('P1'); await member('기둥');
|
||||
check('fresh-page restore retains exceptions and shared changes', await ev(`document.getElementById('cip-exceptions').open && document.getElementById('cip-column-mainCount').value==='50' && document.getElementById('cip-pile-mainCount').value==='52' && document.getElementById('cip-pile-innerMainDiameterMm').value==='35.8' && document.getElementById('cip-connection-mode').value==='continuous'`));
|
||||
// Follow dedicated warnings to verify both segment targets resolve to one card.
|
||||
for (const text of ['RB-CIP01', 'RB-CIP02', 'RB-F00']) {
|
||||
await click(`[...document.querySelectorAll('#rebar-issues li')].find(e=>e.textContent.includes(${JSON.stringify(text)}))?.querySelector('button')`);
|
||||
check(`${text}: warning opens unified editor`, await ev(`!!document.getElementById('cip-apply') && document.querySelector('#rebar-tabs button[aria-pressed="true"]').textContent.startsWith('기둥·말뚝')`));
|
||||
}
|
||||
// ④ is no longer a header step (header ①②③, 2026-11-06): open the 재료표 from ③.
|
||||
await click(`document.getElementById('nav-rebar')`);
|
||||
await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`);
|
||||
check('quantity remains explicitly incomplete', await ev(`document.getElementById('quantity-body').textContent.includes('전체 수량 미완료')`));
|
||||
check('no runtime exceptions', report.errors.length===0);
|
||||
report.result='PASS';
|
||||
})().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(async()=>{
|
||||
if(report.result==='FAIL' && send) try {const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,'failure.png'),Buffer.from(r.data,'base64'));}catch{}
|
||||
fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({result:report.result,checks:report.checks.length,failure:report.failure}));
|
||||
ws?.close();
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수.
|
||||
*
|
||||
* - 솔플레이트 (결정 2026-11-06, spec_abut §5·QA-15, src/core/sole-plate.ts): S = 같은 거더의 짝 받침(A1 ↔ P1 전열,
|
||||
* A2 ↔ Pn)으로 계산, 짝이 없으면 교대 종단경사 i(엑셀 Abut). L = A + 100, W = B + 100 (A × B 기본 700 × 640, 교각 P1
|
||||
* 전열 값 가정). 받침이 1열이라 T는 교대 전체에 하나: max(22, ⌈22 + L·|S|/2⌉). 거더 하면 고정 → 블럭 상면 = z − (T − T_ref).
|
||||
* - h3 기본값 = 받침 시트의 받침높이 − T_ref (A1 275 − 30 = 245): 엑셀 받침높이가 정한 거더 하면을 그대로 둔다
|
||||
* (시안은 교각 P1의 h3 181을 가정했으나 교대 시트에 받침높이가 있어 그 값을 지킨다).
|
||||
* - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T). z = 패드 상면 = EL.X.
|
||||
*/
|
||||
import type { BearingPair } from '../bearing-pair';
|
||||
import { type BearingSole, SOLE_DEFAULTS, structureSoles } from '../sole-plate';
|
||||
import { ABUT_PAD_MM, type AbutModel } from './geometry';
|
||||
import type { AbutMembers } from './members';
|
||||
|
||||
/** 교대 솔플레이트 입력 (2026-11-06). Every value optional; absent = the default. */
|
||||
export interface AbutBearingRule {
|
||||
/** 받침 본체 높이 h3 (mm). Absent = 받침높이 − T_ref. */
|
||||
h3Mm?: number;
|
||||
tRefMm: number;
|
||||
tMinMm: number;
|
||||
/** 솔플 여유 L − A = W − B (mm). */
|
||||
marginMm: number;
|
||||
/** 받침 평면 A (거더 방향) × B (mm), every bearing. */
|
||||
aMm: number;
|
||||
bMm: number;
|
||||
/** T typed (mm). Absent = 자동. */
|
||||
tMm?: number;
|
||||
/** S typed for every bearing (%). Absent = 짝 받침 계산. */
|
||||
slopePct?: number;
|
||||
}
|
||||
|
||||
export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { tRefMm: SOLE_DEFAULTS.tRefMm, tMinMm: SOLE_DEFAULTS.tMinMm, marginMm: SOLE_DEFAULTS.marginMm, aMm: SOLE_DEFAULTS.aMm, bMm: SOLE_DEFAULTS.bMm };
|
||||
/** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */
|
||||
export const ABUT_BLOCK_MM = 1200;
|
||||
export const ABUT_PAD_PLAN_MM = 1000;
|
||||
/** I01: 블럭 콘크리트 높이가 피복 50 + 철근 지름 13보다 작으면 블럭 철근을 교좌면 아래로 묻는다 (spec_abut §6.6). */
|
||||
const I01_MIN_MM = 50 + 13;
|
||||
|
||||
const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
|
||||
|
||||
/** Keys the 2026-11-06 rule reads; values stored by the C안 (lengthMm, widthMm, kind) are ignored. */
|
||||
const RULE_KEYS = ['h3Mm', 'tRefMm', 'tMinMm', 'marginMm', 'aMm', 'bMm', 'tMm', 'slopePct'] as const;
|
||||
|
||||
export function resolveBearingRule(changes?: Partial<AbutBearingRule> & Record<string, unknown>): AbutBearingRule {
|
||||
const own = Object.fromEntries(Object.entries(changes ?? {}).filter(([k]) => (RULE_KEYS as readonly string[]).includes(k)));
|
||||
return { ...ABUT_BEARING_RULE_DEFAULT, ...own };
|
||||
}
|
||||
|
||||
/** Bridge context of the abutment card: the girder pairs and the 교대 종단경사 i (decimal). */
|
||||
export interface AbutSoleContext {
|
||||
pairs?: BearingPair[];
|
||||
slopeI?: number;
|
||||
/** 엑셀 방위각 per bearing number (deg model 진북), when an applied 계산 방위각 is in the model. */
|
||||
inputAzimuthDeg?: Record<string, number>;
|
||||
}
|
||||
|
||||
/** Solplate of every bearing of the abutment (one row). */
|
||||
export function abutSoles(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): BearingSole[] {
|
||||
const plates = Object.fromEntries(model.bearings.map((b) => [String(b.no), { aMm: rule.aMm, bMm: rule.bMm }]));
|
||||
return structureSoles(
|
||||
model.name,
|
||||
model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0, inputAzimuthDeg: ctx.inputAzimuthDeg?.[String(b.no)] })),
|
||||
{
|
||||
h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates,
|
||||
tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm },
|
||||
slopePct: rule.slopePct === undefined ? undefined : Object.fromEntries(model.bearings.map((b) => [String(b.no), rule.slopePct!])),
|
||||
},
|
||||
ctx.pairs ?? [],
|
||||
ctx.slopeI === undefined ? undefined : ctx.slopeI * 100,
|
||||
);
|
||||
}
|
||||
|
||||
export interface AbutElevationRow {
|
||||
no: number;
|
||||
/** IP3(① 쪽)에서 잰 위치 (m). */
|
||||
sFromIp3M: number;
|
||||
seg: string;
|
||||
seatEl: number;
|
||||
h1: number;
|
||||
h2: number;
|
||||
/** 패드 상면 = EL.X (m). */
|
||||
z: number;
|
||||
h3: number;
|
||||
h4: number;
|
||||
E: number;
|
||||
/** 솔플레이트 상면 EL (m) = 교좌면 + E. */
|
||||
solplateTopEl: number;
|
||||
/** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */
|
||||
stepGapM: number | null;
|
||||
/** 받침 방위각 (deg): CAD (엑셀 값, 동 = 0° 반시계) / 진북 (북에서 시계). 입력이 없으면 null. */
|
||||
azimuthCadDeg: number | null;
|
||||
azimuthNorthDeg: number | null;
|
||||
/** 블럭 평면 회전 (deg), 교대축 직각 기준. */
|
||||
rotDeg: number;
|
||||
}
|
||||
|
||||
export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutElevationRow[] {
|
||||
const half = ABUT_BLOCK_MM / 2000;
|
||||
const soles = new Map(abutSoles(model, rule, heightMm, ctx).map((x) => [x.no, x]));
|
||||
return model.bearings.map((b) => {
|
||||
const plan = soles.get(b.no)!;
|
||||
// 거더 하면 고정: the block concrete loses T − T_ref (2026-11-06, Q-SH7).
|
||||
const h1 = b.blockHeightMm - ABUT_PAD_MM - plan.dropMm;
|
||||
const E = h1 + ABUT_PAD_MM + plan.h3 + plan.T;
|
||||
const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half);
|
||||
return {
|
||||
no: b.no,
|
||||
sFromIp3M: model.length - b.s,
|
||||
seg: CIRCLED[b.no - 1] ?? String(b.no),
|
||||
seatEl: b.seatEl,
|
||||
h1,
|
||||
h2: ABUT_PAD_MM,
|
||||
z: b.z - plan.dropMm / 1000,
|
||||
h3: plan.h3,
|
||||
h4: plan.T,
|
||||
E,
|
||||
solplateTopEl: b.seatEl + E / 1000,
|
||||
stepGapM: gaps.length ? Math.min(...gaps) : null,
|
||||
azimuthCadDeg: b.azimuthDeg === undefined ? null : (((90 - b.azimuthDeg) % 360) + 360) % 360,
|
||||
azimuthNorthDeg: b.azimuthDeg === undefined ? null : ((b.azimuthDeg % 360) + 360) % 360,
|
||||
rotDeg: b.rotDeg,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export interface AbutSolplateRow {
|
||||
no: number;
|
||||
/** 기울기 (%). */
|
||||
S: number;
|
||||
/** S 근거 (계산 / 입력 / 가정 / 수정됨 / 없음) and the 짝 받침, e.g. "P1-①". */
|
||||
source: string;
|
||||
pair?: string;
|
||||
/** 방위각 검사 (″): input azimuth against the pair direction; absent without a pair. */
|
||||
azimuthSec?: number;
|
||||
pairKind?: 'girder' | 'chord';
|
||||
/** 방위각 입력 (엑셀) and 계산 (짝 받침), deg model 진북; 계산 absent without a pair. */
|
||||
azimuthInputDeg: number;
|
||||
azimuthCalcDeg?: number;
|
||||
L: number;
|
||||
W: number;
|
||||
T: number;
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** 검산 문구 "T1 − T2 ≈ L·|S|/100" (W08, 1 mm 이내). */
|
||||
check: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutSolplateRow[] {
|
||||
return abutSoles(model, rule, heightMm, ctx).map((p) => {
|
||||
const rhs = (p.L * Math.abs(p.slopePct)) / 100;
|
||||
return {
|
||||
no: p.no, S: p.slopePct, source: p.slopeSource, azimuthSec: p.azimuthCheckSec, azimuthInputDeg: p.azimuthInputDeg, azimuthCalcDeg: p.azimuthCalcDeg,
|
||||
pair: p.pair ? `${p.pair.structure}-${CIRCLED[p.pair.no - 1] ?? p.pair.no}${p.pairKind === 'chord' ? ' (현)' : ''}` : undefined, pairKind: p.pairKind,
|
||||
L: p.L, W: p.W, T: p.T, T1: p.T1, T2: p.T2,
|
||||
check: `${p.T1 - p.T2} ≈ ${rhs.toFixed(1)} ${Math.abs(p.T1 - p.T2 - rhs) <= 1 + 1e-9 ? '일치' : '불일치 (W08)'}`,
|
||||
warnings: p.warnings,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Input problems of the 교대 솔플레이트 입력. Empty when it can build a solplate. */
|
||||
export function abutBearingRuleIssues(rule: AbutBearingRule, heightMm: number): string[] {
|
||||
const out: string[] = [];
|
||||
for (const k of ['tRefMm', 'tMinMm', 'aMm', 'bMm'] as const) if (!(Number.isFinite(rule[k]) && rule[k] > 0)) out.push(`${k}는 0보다 커야 합니다.`);
|
||||
if (!(Number.isFinite(rule.marginMm) && rule.marginMm >= 0)) out.push('솔플 여유는 0 이상이어야 합니다.');
|
||||
if (rule.tMm !== undefined && !(Number.isFinite(rule.tMm) && rule.tMm > 0)) out.push('T는 0보다 커야 합니다.');
|
||||
if (rule.slopePct !== undefined && !Number.isFinite(rule.slopePct)) out.push('S는 숫자여야 합니다.');
|
||||
const h3 = rule.h3Mm ?? heightMm - rule.tRefMm;
|
||||
if (!(Number.isFinite(h3) && h3 > 0)) out.push(`받침 높이 h3 (${h3})는 0보다 커야 합니다. 받침높이 ${heightMm}와 T_ref를 확인하세요.`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */
|
||||
export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): string[] {
|
||||
const rows = abutElevationRows(model, rule, heightMm, ctx);
|
||||
const minH1 = Math.min(...rows.map((r) => r.h1));
|
||||
const half = ABUT_BLOCK_MM / 2000;
|
||||
// Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front.
|
||||
const tCentres = model.bearings.map((b) => model.frontSide * b.lateral);
|
||||
const backGap = Math.min(...tCentres.map((t) => t - half));
|
||||
const frontGap = Math.min(...tCentres.map((t) => members.shape.wallFrontM - (t + half)));
|
||||
const stepGaps = rows.map((r) => r.stepGapM).filter((g): g is number => g !== null);
|
||||
const minStep = stepGaps.length ? Math.min(...stepGaps) : null;
|
||||
return [
|
||||
`블럭 콘크리트 ${minH1} > 0 (W02 ${minH1 > 0 ? '없음' : '있음'})`,
|
||||
`블럭이 교좌면 안 (흉벽 ${Math.round(backGap * 1000)} · 전면 ${Math.round(frontGap * 1000)}, W04 ${backGap >= 0 && frontGap >= 0 ? '없음' : '있음'})`,
|
||||
minStep === null ? '블럭 끝 ~ 계단 경계: 경계 없음 (W03 없음)' : `블럭 끝 ~ 계단 경계 최소 ${minStep.toFixed(3)} m (W03 ${minStep >= 0 ? '없음' : '있음'})`,
|
||||
`${minH1} ≥ ${I01_MIN_MM} → I01 ${minH1 >= I01_MIN_MM ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 교대 입력 편집 (2026-11-03, 교대 편집 카드): changes one value of an abutment record with checks. Pure: it returns a new
|
||||
* record and never changes the old one. Errors are in Korean and name the field.
|
||||
*/
|
||||
import type { AbutRecord } from './parse';
|
||||
import type { WingForm } from './geometry';
|
||||
import { WING_DIMS_DEFAULT, wingDimsIssues, type WingDims } from './wings';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, shapeIssues, type AbutShape } from './shape';
|
||||
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
||||
import type { AbutGround } from './ground';
|
||||
import { footingRebarIssues, resolveFootingRebar, type AbutFootingRebarSettings } from './rebar/footing';
|
||||
import { resolveWallRebar, wallRebarIssues, type AbutWallRebarSettings } from './rebar/wall';
|
||||
import { parapetRebarIssues, resolveParapetRebar, type AbutParapetRebarSettings } from './rebar/parapet';
|
||||
import { resolveWingRebar, wingRebarIssues, type AbutWingRebarSettings } from './rebar/wing';
|
||||
import { resolveSlabRebar, slabRebarIssues, type AbutSlabRebarSettings } from './rebar/slab';
|
||||
import { blockRebarIssues, resolveBlockRebar, type AbutBlockSettings } from './rebar/block';
|
||||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||||
|
||||
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
|
||||
|
||||
export const ABUT_EDIT_FIELDS: { key: AbutEditKey; label: string; unit: string }[] = [
|
||||
{ key: 'seatEl', label: '교좌면 EL (구간 ①)', unit: 'm' },
|
||||
{ key: 'foundationBottomEl', label: '기초 하단 EL', unit: 'm' },
|
||||
{ key: 'groundEl', label: '지반고 EL', unit: 'm' },
|
||||
{ key: 'pileLengthMm', label: '말뚝길이', unit: 'mm' },
|
||||
{ key: 'skewDeg', label: 'skew', unit: '°' },
|
||||
{ key: 'slopeI', label: '종단경사 i', unit: '' },
|
||||
];
|
||||
|
||||
/** The 저판 thickness (m) below the 기초 하단 is not part of this check; the 교좌면 must sit above the 기초 하단. */
|
||||
export function applyAbutEdit(rec: AbutRecord, key: AbutEditKey, value: number): AbutRecord {
|
||||
const label = ABUT_EDIT_FIELDS.find((f) => f.key === key)?.label ?? key;
|
||||
if (!Number.isFinite(value)) throw new Error(`${label}은(는) 숫자여야 합니다.`);
|
||||
if (key === 'pileLengthMm' && !(value > 0)) throw new Error(`${label}은(는) 0보다 커야 합니다.`);
|
||||
if (key === 'skewDeg' && !(value > 0 && value < 180)) throw new Error(`${label}은(는) 0 초과 180 미만이어야 합니다.`);
|
||||
if (key === 'slopeI' && Math.abs(value) > 0.2) throw new Error(`${label}은(는) ±0.2 이내여야 합니다.`);
|
||||
const next: AbutRecord = { ...rec, [key]: value };
|
||||
if (next.seatEl <= next.foundationBottomEl) {
|
||||
throw new Error('교좌면 EL은 기초 하단 EL보다 높아야 합니다.');
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 날개벽 형식은 측별로 정한다 (2026-11-04: "좌우 같게" 없음). */
|
||||
export function setWingForm(rec: AbutRecord, side: 'left' | 'right', form: WingForm): AbutRecord {
|
||||
const current = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
return { ...rec, wings: { ...current, [side]: form } };
|
||||
}
|
||||
|
||||
/**
|
||||
* 반대쪽에도 적용 (2026-11-04): copies this side's wing dims to the other side once (no lasting link).
|
||||
* 두께 · 끝 높이 · 헌치는 늘 복사하고, 길이는 두 측의 형식이 같을 때만 복사한다 (단축형 길이 기본 = 뒷굽 끝).
|
||||
* The other side must have a wing.
|
||||
*/
|
||||
export function copyWingDims(rec: AbutRecord, side: 'left' | 'right'): AbutRecord {
|
||||
const other = side === 'left' ? 'right' : 'left';
|
||||
const forms = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
if (forms[other] === 'none') throw new Error('반대쪽 날개벽이 없습니다.');
|
||||
const from = rec.wingDims?.[side] ?? {};
|
||||
const to: Partial<WingDims> = { ...(rec.wingDims?.[other] ?? {}) };
|
||||
for (const key of ['thicknessMm', 'endHeightMm', 'haunchMm'] as const) {
|
||||
if (from[key] === undefined) delete to[key];
|
||||
else to[key] = from[key];
|
||||
}
|
||||
if (forms[side] === forms[other]) {
|
||||
if (from.lengthMm === undefined) delete to.lengthMm;
|
||||
else to.lengthMm = from.lengthMm;
|
||||
}
|
||||
return { ...rec, wingDims: { ...(rec.wingDims ?? {}), [other]: to } };
|
||||
}
|
||||
|
||||
/** 날개벽 치수 입력 칸 (② 날개벽 카드, 시안 순서). 단축형에는 끝 높이가 없다. */
|
||||
export const WING_DIM_FIELDS: { key: keyof WingDims; label: string; general: boolean; short: boolean }[] = [
|
||||
{ key: 'lengthMm', label: '길이 (mm)', general: true, short: true },
|
||||
{ key: 'thicknessMm', label: '두께 (mm)', general: true, short: true },
|
||||
{ key: 'endHeightMm', label: '끝 높이 (mm)', general: true, short: false },
|
||||
{ key: 'haunchMm', label: '헌치 (mm, 300 × 300의 한 변)', general: true, short: true },
|
||||
];
|
||||
|
||||
/** Changes one wing dim of one side. Length, thickness and end height must be > 0, the haunch ≥ 0. */
|
||||
export function applyWingDim(rec: AbutRecord, side: 'left' | 'right', key: keyof WingDims, value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const issues = wingDimsIssues({ ...WING_DIMS_DEFAULT, [key]: value });
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, wingDims: { ...(rec.wingDims ?? {}), [side]: { ...(rec.wingDims?.[side] ?? {}), [key]: value } } };
|
||||
}
|
||||
|
||||
/** 받침 값 편집 (2026-11-03): z (패드 상면, m) or 교좌면 override (m, undefined = 자동 — 블럭 높이 규칙을 따른다). */
|
||||
export function applyBearingEdit(rec: AbutRecord, no: number, key: 'z' | 'seatEl', value: number | undefined): AbutRecord {
|
||||
const target = rec.bearings.find((b) => b.no === no);
|
||||
if (!target) throw new Error(`받침 ${no}번이 없습니다.`);
|
||||
if (key === 'z' && (value === undefined || !Number.isFinite(value))) throw new Error('z는 숫자여야 합니다.');
|
||||
if (key === 'seatEl' && value !== undefined && !Number.isFinite(value)) throw new Error('교좌면 EL은 숫자이거나 비워야 합니다.');
|
||||
const next = rec.bearings.map((b) => {
|
||||
if (b.no !== no) return b;
|
||||
if (key === 'z') return { ...b, z: value as number };
|
||||
return value === undefined ? { ...b, seatEl: undefined } : { ...b, seatEl: value };
|
||||
});
|
||||
const out: AbutRecord = { ...rec, bearings: next };
|
||||
// The 교좌면 of every bearing must still lie above the 기초 하단 (the 저판 thickness is checked in the geometry step).
|
||||
const lowest = Math.min(...out.bearings.map((b) => b.seatEl ?? out.seatEl + (b.z - Math.min(...out.bearings.map((x) => x.z)))));
|
||||
if (lowest <= out.foundationBottomEl) throw new Error(`받침 ${no}번 교좌면 EL이 기초 하단 EL보다 낮거나 같습니다.`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 기본값 복원: the record as imported from the workbook (the wing forms go back to 양측 일반형). */
|
||||
export function restoreAbutDefault(defaults: AbutRecord): AbutRecord {
|
||||
return JSON.parse(JSON.stringify(defaults)) as AbutRecord;
|
||||
}
|
||||
|
||||
/** 형상 입력 change (벽체·흉벽, 확대기초·말뚝, mm): refused with a Korean message when the shape would not build. */
|
||||
/** Several shape values at once (e.g. the 열 위치 "650 · 2000 · 1300 · 650"), checked together. */
|
||||
export function applyShapeEdits(rec: AbutRecord, values: Partial<AbutShape>): AbutRecord {
|
||||
for (const v of Object.values(values)) if (!Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const shape = { ...(rec.shape ?? {}), ...values };
|
||||
const issues = [...shapeIssues(shape), ...slabFitIssues(rec.slab, shape)];
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, shape };
|
||||
}
|
||||
|
||||
export function applyShapeEdit(rec: AbutRecord, key: keyof AbutShape, value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const shape = { ...(rec.shape ?? {}), [key]: value };
|
||||
const issues = [...shapeIssues(shape), ...slabFitIssues(rec.slab, shape)];
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, shape };
|
||||
}
|
||||
|
||||
/** 브라켓 높이 (mm) of a shape input, default when absent. */
|
||||
function bracketHeightMm(shape: Partial<AbutShape> | undefined): number {
|
||||
return { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(shape) }.bracketHeightMm;
|
||||
}
|
||||
|
||||
/** A lower 브라켓 must still hold the 접속슬래브 under the 흉벽 상단. */
|
||||
function slabFitIssues(slab: Partial<AbutSlab> | undefined, shape: Partial<AbutShape>): string[] {
|
||||
const s = resolveSlab(slab);
|
||||
const h = bracketHeightMm(shape);
|
||||
return s.thicknessMm > h ? [`브라켓 높이(${h})는 접속슬래브 두께(${s.thicknessMm}) 이상이어야 합니다.`] : [];
|
||||
}
|
||||
|
||||
/** 접속슬래브 형식 (2026-11-04); the form also sets the slope (매립형 하향 4 %, 노출형 종단경사). */
|
||||
export function setSlabForm(rec: AbutRecord, form: SlabForm): AbutRecord {
|
||||
const { slope: _old, ...kept } = (rec.slab ?? {}) as Partial<AbutSlab> & { slope?: unknown };
|
||||
const slab: Partial<AbutSlab> = { ...kept, form };
|
||||
const issues = slabIssues(resolveSlab(slab), bracketHeightMm(rec.shape));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, slab };
|
||||
}
|
||||
|
||||
/** 접속슬래브 입력칸 (② 접속슬래브 카드, 시안 순서). 헌치 길이는 노출형만. */
|
||||
export const SLAB_EDIT_FIELDS: { key: 'lengthMm' | 'thicknessMm' | 'parapetGapMm' | 'wingGapMm' | 'haunchLengthMm'; label: string; exposedOnly?: boolean; note?: string }[] = [
|
||||
{ key: 'lengthMm', label: '길이 (mm)' },
|
||||
{ key: 'thicknessMm', label: '두께 (mm)' },
|
||||
{ key: 'parapetGapMm', label: '흉벽과 간격 (mm)' },
|
||||
{ key: 'wingGapMm', label: '날개벽과 간격 (mm)' },
|
||||
{ key: 'haunchLengthMm', label: '헌치 길이 (mm)', exposedOnly: true, note: '가정 · 삽도 비율' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 지반·지층 입력 (2026-11-04): merges the given values into the record's 지반; undefined removes a value. Numbers must be finite.
|
||||
* The values are checked by the card (G01–G14), not refused here, as on the pier card.
|
||||
*/
|
||||
export function setAbutGround(rec: AbutRecord, patch: Partial<AbutGround>): AbutRecord {
|
||||
for (const [key, v] of Object.entries(patch)) {
|
||||
if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
if (key === 'strataRows' && (v as { bottomEl: number }[]).some((r) => !Number.isFinite(r.bottomEl))) throw new Error('지층 하단 EL은 숫자여야 합니다.');
|
||||
}
|
||||
const ground: AbutGround = { ...(rec.ground ?? {}), ...patch };
|
||||
for (const key of Object.keys(ground) as (keyof AbutGround)[]) if (ground[key] === undefined) delete ground[key];
|
||||
return { ...rec, ground };
|
||||
}
|
||||
|
||||
/**
|
||||
* ③ 저판 배근 입력 (2026-11-04): merges the values; undefined removes one (a 철근별 간격 goes back to the 기본간격).
|
||||
* Refused with a Korean message when the settings would not lay out.
|
||||
*/
|
||||
export function applyFootingRebar(rec: AbutRecord, patch: Partial<AbutFootingRebarSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const footing: Partial<AbutFootingRebarSettings> = { ...(rec.rebar?.footing ?? {}), ...patch };
|
||||
for (const key of Object.keys(footing) as (keyof AbutFootingRebarSettings)[]) if (footing[key] === undefined) delete footing[key];
|
||||
const issues = footingRebarIssues(resolveFootingRebar(footing));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), footing } };
|
||||
}
|
||||
|
||||
/** Changes ③ 벽체 배근 values (only the changed ones are kept; undefined clears one), refused with a Korean message. */
|
||||
export function applyWallRebar(rec: AbutRecord, patch: Partial<AbutWallRebarSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const wall: Partial<AbutWallRebarSettings> = { ...(rec.rebar?.wall ?? {}), ...patch };
|
||||
for (const key of Object.keys(wall) as (keyof AbutWallRebarSettings)[]) if (wall[key] === undefined) delete wall[key];
|
||||
const issues = wallRebarIssues(resolveWallRebar(wall));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), wall } };
|
||||
}
|
||||
|
||||
/** Changes ③ 흉벽 · 브라켓 배근 values (only the changed ones are kept; undefined clears one), refused with a Korean message. */
|
||||
export function applyParapetRebar(rec: AbutRecord, patch: Partial<AbutParapetRebarSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const parapet: Partial<AbutParapetRebarSettings> = { ...(rec.rebar?.parapet ?? {}), ...patch };
|
||||
for (const key of Object.keys(parapet) as (keyof AbutParapetRebarSettings)[]) if (parapet[key] === undefined) delete parapet[key];
|
||||
const issues = parapetRebarIssues(resolveParapetRebar(parapet));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), parapet } };
|
||||
}
|
||||
|
||||
/** Changes ③ 날개벽 배근 values (좌우 공통, 사용자 결정 2026-11-04; only the changed ones are kept), refused with a Korean message. */
|
||||
export function applyWingRebar(rec: AbutRecord, patch: Partial<AbutWingRebarSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const wing: Partial<AbutWingRebarSettings> = { ...(rec.rebar?.wing ?? {}), ...patch };
|
||||
for (const key of Object.keys(wing) as (keyof AbutWingRebarSettings)[]) if (wing[key] === undefined) delete wing[key];
|
||||
const issues = wingRebarIssues(resolveWingRebar(wing));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), wing } };
|
||||
}
|
||||
|
||||
/** Changes ③ 접속슬래브 배근 values (only the changed ones are kept; undefined clears one), refused with a Korean message. */
|
||||
export function applySlabRebar(rec: AbutRecord, patch: Partial<AbutSlabRebarSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const slab: Partial<AbutSlabRebarSettings> = { ...(rec.rebar?.slab ?? {}), ...patch };
|
||||
for (const key of Object.keys(slab) as (keyof AbutSlabRebarSettings)[]) if (slab[key] === undefined) delete slab[key];
|
||||
const issues = slabRebarIssues(resolveSlabRebar(slab));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), slab } };
|
||||
}
|
||||
|
||||
/** Changes ③ 받침블럭 배근 values (only the changed ones are kept; undefined clears one), refused with a Korean message. */
|
||||
export function applyBlockRebar(rec: AbutRecord, patch: Partial<AbutBlockSettings>): AbutRecord {
|
||||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||||
const block: Partial<AbutBlockSettings> = { ...(rec.rebar?.block ?? {}), ...patch };
|
||||
for (const key of Object.keys(block) as (keyof AbutBlockSettings)[]) if (block[key] === undefined) delete block[key];
|
||||
const issues = blockRebarIssues(resolveBlockRebar(block));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, rebar: { ...(rec.rebar ?? {}), block } };
|
||||
}
|
||||
|
||||
/** Changes one 접속슬래브 dim, refused with a Korean message when the slab would not build. */
|
||||
export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[number]['key'], value: number): AbutRecord {
|
||||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||||
const slab = { ...(rec.slab ?? {}), [key]: value };
|
||||
const issues = slabIssues(resolveSlab(slab), bracketHeightMm(rec.shape));
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, slab };
|
||||
}
|
||||
|
||||
|
||||
/** 형상 입력 fields on the 교대 카드 (시안 ② 벽체·흉벽, 확대기초·말뚝), in the order of the mockup. */
|
||||
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'wall' | 'footing'; note?: string }[] = [
|
||||
// 벽체 · 흉벽 (시안 입력 격자 순서). 벽체 두께는 자동 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭.
|
||||
{ key: 'wallFrontMm', label: '교좌면 폭 (mm)', group: 'wall' },
|
||||
{ key: 'parapetHeightMm', label: '벽체 높이 · 받침 ① 기준 연직 (mm)', group: 'wall', note: '받침 ① 교좌면 ~ 흉벽 상단' },
|
||||
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)', group: 'wall' },
|
||||
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)', group: 'wall', note: '= 접속슬래브 카드 흉벽 높이' },
|
||||
{ key: 'bracketWidthMm', label: '브라켓 폭 (mm)', group: 'wall' },
|
||||
{ key: 'shrinkJointCount', label: '수축줄눈 개수', group: 'wall' },
|
||||
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||||
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||||
{ key: 'seatStepIgnoreMm', label: '인접 무시 기준 (mm)', group: 'wall' },
|
||||
// 확대기초 · 말뚝 (분리 카드, 시안 순서). 뒷굽 · 길이 · 선단 EL은 자동, 열 위치와 열별 본수는 카드에서 따로 입력한다.
|
||||
{ key: 'footingWidthMm', label: '폭 (mm)', group: 'footing' },
|
||||
{ key: 'footingThicknessMm', label: '두께 (mm)', group: 'footing' },
|
||||
{ key: 'footingToeMm', label: '앞굽 (mm)', group: 'footing' },
|
||||
{ key: 'blindingMarginMm', label: '버림 여유 한쪽 (mm)', group: 'footing' },
|
||||
{ key: 'blindingThicknessMm', label: '버림 두께 (mm)', group: 'footing' },
|
||||
{ key: 'pileDiameterMm', label: '말뚝 지름 (mm)', group: 'footing', note: 'QA-5' },
|
||||
{ key: 'pileEdgeMm', label: '연단 · 길이 방향 (mm)', group: 'footing' },
|
||||
{ key: 'pileSpacingMm', label: '본수 규칙 간격 (mm)', group: 'footing' },
|
||||
];
|
||||
|
||||
/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */
|
||||
/** 교대 솔플레이트 입력 칸 (2026-11-06). Optional ones (h3, T, S) go back to the automatic value when cleared. */
|
||||
export const BEARING_RULE_FIELDS: { key: keyof AbutBearingRule; label: string; optional?: boolean }[] = [
|
||||
{ key: 'h3Mm', label: '받침 높이 h3 (mm, 비우면 받침높이 − T_ref)', optional: true },
|
||||
{ key: 'tRefMm', label: 'T_ref · 엑셀 z 산정 때 두께 (mm)' },
|
||||
{ key: 'tMinMm', label: '솔플 최소두께 T_min (mm)' },
|
||||
{ key: 'marginMm', label: '솔플 여유 L − A = W − B (mm)' },
|
||||
{ key: 'aMm', label: '받침 A · 거더 방향 (mm)' },
|
||||
{ key: 'bMm', label: '받침 B (mm)' },
|
||||
{ key: 'tMm', label: '솔플레이트 T (mm, 비우면 자동)', optional: true },
|
||||
{ key: 'slopePct', label: 'S 직접 수정 (%, 비우면 짝 받침 계산)', optional: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Changes one 솔플레이트 입력 value (2026-11-06); `undefined` clears an optional one back to automatic. The result must
|
||||
* still build a solplate (positive sizes, h3 > 0 with the 교대 받침높이).
|
||||
*/
|
||||
export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | undefined): AbutRecord {
|
||||
const bearingRule: Record<string, unknown> = { ...(rec.bearingRule ?? {}) };
|
||||
if (value === undefined) delete bearingRule[key];
|
||||
else {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) throw new Error('솔플레이트 입력 값은 숫자여야 합니다.');
|
||||
bearingRule[key] = n;
|
||||
}
|
||||
const issues = abutBearingRuleIssues(resolveBearingRule(bearingRule), rec.bearingHeightMm);
|
||||
if (issues.length > 0) throw new Error(issues[0]);
|
||||
return { ...rec, bearingRule };
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 교대 형상 · AA1 (2026-11-03, spec_abut §1·§4.1–4.2): 교대축, 전면 쪽, 교좌면 위의 받침, 교좌면 EL과 블럭 높이를 계산한다.
|
||||
* 순수 함수. 좌표는 (E, N) m 단위, EL은 m 단위, 길이는 별도 표기가 없으면 mm 단위다.
|
||||
*
|
||||
* - 축: IP1 → IP3 선(교대중심선), 길이 L. u = IP1 → IP3 단위벡터, nLeft = u를 반시계 90° 돌린 벡터.
|
||||
* - 받침 s: IP1에서 u 방향으로 잰 거리(m). 축에서 벗어난 거리(lateral): nLeft 방향 거리(m).
|
||||
* - z = 패드 상면(EL.X), 교각과 같다(2026-11-03 결정 1). 교좌면에서 패드 상면까지 블럭 높이는 125 mm
|
||||
* (콘크리트 75 + 패드 50). 교좌면 EL은 콘크리트 하면이다.
|
||||
* - 교좌면: 교대 전체에 교좌면 EL 하나를 입력받는다(첫 받침 ①). 블럭 높이 h = min(z) − 교좌면으로 모든 받침이 같고,
|
||||
* 받침 i의 교좌면 = z_i − h. 받침이 자기 교좌면 EL(seatEl)을 직접 주면 그 값을 쓰고, 블럭 높이는 z_i − seatEl이 된다
|
||||
* (2026-11-03 결정 3).
|
||||
* - 전면: 받침이 있는 쪽. A1은 전면이 1 → n 방향의 왼쪽, 흉벽은 오른쪽에 있다. A2는 반대다(spec_abut §1).
|
||||
* 맞지 않으면 AW03 경고 대상이다.
|
||||
*/
|
||||
|
||||
export type AbutType = 'A1' | 'A2';
|
||||
|
||||
export interface AbutPoint {
|
||||
e: number;
|
||||
n: number;
|
||||
}
|
||||
|
||||
export interface AbutBearing {
|
||||
no: number;
|
||||
/** 위치 (E, N), m. */
|
||||
pos: AbutPoint;
|
||||
/** 패드 상면 EL (z), m. */
|
||||
z: number;
|
||||
/** 이 받침의 교좌면 EL (m). 없으면 z에서 교대 블럭 높이를 뺀 값을 쓴다. */
|
||||
seatEl?: number;
|
||||
/** 받침 방위각 (deg, model-xy: 북에서 시계방향, 교각과 같은 변환 swapXYAzimuth). 없으면 블럭은 교대축에 맞춘다. */
|
||||
azimuthDeg?: number;
|
||||
}
|
||||
|
||||
/** 날개벽 형식, 측별(spec_abut §4.7): 일반형 / 단축형 / 없음. */
|
||||
export type WingForm = 'general' | 'short' | 'none';
|
||||
|
||||
export interface AbutInput {
|
||||
name: string;
|
||||
type: AbutType;
|
||||
/** 첫 받침(구간 ①)의 교좌면 EL (m). 블럭 높이 h = min(z) − seatEl. */
|
||||
seatEl: number;
|
||||
ip1: AbutPoint;
|
||||
ip2: AbutPoint;
|
||||
ip3: AbutPoint;
|
||||
bearings: AbutBearing[];
|
||||
/** skew (deg): 받침축을 시계방향으로 돌린 선이 날개벽 선이다(spec_abut §1). 없으면 날개벽 검사를 하지 않는다. */
|
||||
skewDeg?: number;
|
||||
/** 종단경사 i (소수, 교량진행방향 기준, (−) = 낮아짐). 날개벽 경사는 A1이 −i, A2가 +i다. */
|
||||
slopeI?: number;
|
||||
/** AW01용 거더 방향 CAD 각 (deg). 없으면 AW01을 검사하지 않는다. */
|
||||
girderAzimuthDeg?: number;
|
||||
/** 측별 날개벽 형식: 'left' = 받침 ① 쪽 끝, 'right' = 받침 n 쪽 끝(spec §4.7). 기본은 양측 일반형. */
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
/** 측별 날개벽 두께 (mm). 없으면 450. 접속슬래브 폭에 쓴다. */
|
||||
wingThicknessMm?: { left?: number; right?: number };
|
||||
/** 접속슬래브와 날개벽 안쪽면 사이 간격 (mm, ② 접속슬래브 카드). 없으면 20. */
|
||||
slabWingGapMm?: number;
|
||||
/** 기초 하단 EL (m), 말뚝길이 (mm), 기초 형식 (AA3, 저판·말뚝). 없으면 저판·말뚝을 만들지 않는다. */
|
||||
foundationBottomEl?: number;
|
||||
pileLengthMm?: number;
|
||||
foundationType?: string;
|
||||
}
|
||||
|
||||
export interface AbutWingModel {
|
||||
/** 날개벽 선, CAD 각 (deg), 배면 쪽(예제: A1 ≈ 255.32, A2 ≈ 75.32). */
|
||||
lineDeg: number;
|
||||
/** 날개벽 선과 거더 방향의 사잇각, 0–90°로 접음(AW01 허용 1°). */
|
||||
girderDiffDeg: number;
|
||||
aw01Ok: boolean;
|
||||
/** 날개벽 경사 (%), (+) = 벽체에서 멀어질수록 높아짐. */
|
||||
slopePct: number;
|
||||
/** 날개벽 사이 접속슬래브 폭 (mm) = L − Σ(두께 / |sin Δ| + 날개벽과 간격), 있는 날개벽마다. */
|
||||
slabWidthMm: number;
|
||||
wings: { left: WingForm; right: WingForm };
|
||||
}
|
||||
|
||||
export interface AbutBearingModel {
|
||||
no: number;
|
||||
/** IP1에서 u 방향 거리 (m). */
|
||||
s: number;
|
||||
/** nLeft 방향으로 잰 축에서의 부호 있는 거리 (m). */
|
||||
lateral: number;
|
||||
z: number;
|
||||
seatEl: number;
|
||||
/** 교좌면에서 패드 상면까지 블럭 높이 (mm), 패드 포함. */
|
||||
blockHeightMm: number;
|
||||
/** 받침 방위각 (deg, model-xy). 입력이 없으면 undefined. */
|
||||
azimuthDeg?: number;
|
||||
/** 블럭 평면 회전 (deg, −90 … 90): 받침 방위각과 교대축 직각(전면) 방향 사이의 각. 장면에서는 rotateY(이 값). 입력이 없으면 0. */
|
||||
rotDeg: number;
|
||||
}
|
||||
|
||||
export interface AbutModel {
|
||||
name: string;
|
||||
type: AbutType;
|
||||
/** IP1 (축의 시작점, s = 0). */
|
||||
ip1: AbutPoint;
|
||||
/** 축 길이 IP1 → IP3 (m). */
|
||||
length: number;
|
||||
u: AbutPoint;
|
||||
nLeft: AbutPoint;
|
||||
/** 받침이 축의 nLeft 쪽이면 +1, 반대쪽이면 −1. */
|
||||
frontSide: 1 | -1;
|
||||
/** 받침이 1 → n 방향의 왼쪽이면 true (A1 배치). */
|
||||
frontLeftOfBearingDir: boolean;
|
||||
/** 입력 순서의 받침. */
|
||||
bearings: AbutBearingModel[];
|
||||
/** 축 위 계단 경계(IP1에서 m), 이웃 받침 사이의 중간. */
|
||||
stepBounds: number[];
|
||||
/** AW03: 배치가 형식과 맞으면 true. */
|
||||
layoutMatchesType: boolean;
|
||||
/** 날개벽 선, 경사, AW01, 접속슬래브 폭. 입력에 skew가 없으면 없다. */
|
||||
wing?: AbutWingModel;
|
||||
}
|
||||
|
||||
const WING_THICKNESS_MM = 450;
|
||||
const SLAB_CLEARANCE_MM = 20;
|
||||
const AW01_TOLERANCE_DEG = 1;
|
||||
|
||||
/** 공통 규칙의 패드 두께 (mm). 블럭 높이에 포함된다(패드 포함). */
|
||||
export const ABUT_PAD_MM = 50;
|
||||
|
||||
const sub = (a: AbutPoint, b: AbutPoint): AbutPoint => ({ e: a.e - b.e, n: a.n - b.n });
|
||||
|
||||
/**
|
||||
* 블럭 평면 회전 (deg): the bearing azimuth direction d = (sin az, cos az) seen in the axis frame (s along u, t along the front),
|
||||
* folded to −90 … 90 because the block is symmetric. 0 when the azimuth is missing.
|
||||
*/
|
||||
export function bearingRotDeg(azimuthDeg: number | undefined, u: AbutPoint, front: AbutPoint): number {
|
||||
if (azimuthDeg === undefined || !Number.isFinite(azimuthDeg)) return 0;
|
||||
const r = (azimuthDeg * Math.PI) / 180;
|
||||
const d = { e: Math.sin(r), n: Math.cos(r) };
|
||||
let phi = (Math.atan2(d.e * u.e + d.n * u.n, d.e * front.e + d.n * front.n) * 180) / Math.PI;
|
||||
if (phi > 90) phi -= 180;
|
||||
if (phi < -90) phi += 180;
|
||||
return phi;
|
||||
}
|
||||
const dot = (a: AbutPoint, b: AbutPoint) => a.e * b.e + a.n * b.n;
|
||||
const cross = (a: AbutPoint, b: AbutPoint) => a.e * b.n - a.n * b.e;
|
||||
|
||||
export function buildAbut(input: AbutInput): AbutModel {
|
||||
if (input.bearings.length < 2) throw new Error(`교대 ${input.name}: 받침이 2개 이상 필요합니다.`);
|
||||
const axisVec = sub(input.ip3, input.ip1);
|
||||
const length = Math.hypot(axisVec.e, axisVec.n);
|
||||
if (!(length > 0)) throw new Error(`교대 ${input.name}: IP1과 IP3이 같은 점입니다.`);
|
||||
const u = { e: axisVec.e / length, n: axisVec.n / length };
|
||||
const nLeft = { e: -u.n, n: u.e };
|
||||
|
||||
const lateral = input.bearings.map((b) => dot(sub(b.pos, input.ip1), nLeft));
|
||||
const meanLateral = lateral.reduce((a, x) => a + x, 0) / lateral.length;
|
||||
const frontSide: 1 | -1 = meanLateral >= 0 ? 1 : -1;
|
||||
|
||||
// 1 → n 방향: 첫 받침(①)에서 마지막 받침까지. 전면 = 받침 쪽. 형식과 맞는지 확인한다.
|
||||
const first = input.bearings[0];
|
||||
const last = input.bearings[input.bearings.length - 1];
|
||||
const dir = sub(last.pos, first.pos);
|
||||
const frontVec = { e: nLeft.e * frontSide, n: nLeft.n * frontSide };
|
||||
const frontLeftOfBearingDir = cross(dir, frontVec) > 0;
|
||||
const layoutMatchesType = input.type === 'A1' ? frontLeftOfBearingDir : !frontLeftOfBearingDir;
|
||||
|
||||
const minZ = Math.min(...input.bearings.map((b) => b.z));
|
||||
const bearings: AbutBearingModel[] = input.bearings.map((b, i) => {
|
||||
const s = dot(sub(b.pos, input.ip1), u);
|
||||
const seatEl = b.seatEl ?? input.seatEl + (b.z - minZ);
|
||||
const blockHeightMm = Math.round((b.z - seatEl) * 1000);
|
||||
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm, azimuthDeg: b.azimuthDeg, rotDeg: bearingRotDeg(b.azimuthDeg, u, frontVec) };
|
||||
});
|
||||
|
||||
const sorted = [...bearings].sort((a, b) => a.s - b.s);
|
||||
const stepBounds = sorted.slice(1).map((b, i) => (sorted[i].s + b.s) / 2);
|
||||
|
||||
const wing = input.skewDeg === undefined ? undefined : buildWing(input, u, nLeft, frontSide, dir, length);
|
||||
return { name: input.name, type: input.type, ip1: input.ip1, length, u, nLeft, frontSide, frontLeftOfBearingDir, bearings, stepBounds, layoutMatchesType, wing };
|
||||
}
|
||||
|
||||
const deg = (rad: number) => (rad * 180) / Math.PI;
|
||||
const norm360 = (x: number) => ((x % 360) + 360) % 360;
|
||||
/** 두 선 방향의 차이(deg), 0–90°로 접는다. 선에는 방향이 없다. */
|
||||
const lineDiff = (a: number, b: number) => {
|
||||
const d = norm360(a - b) % 180;
|
||||
return Math.min(d, 180 - d);
|
||||
};
|
||||
|
||||
function buildWing(input: AbutInput, u: AbutPoint, nLeft: AbutPoint, frontSide: 1 | -1, dir: AbutPoint, length: number): AbutWingModel {
|
||||
const skew = input.skewDeg!;
|
||||
const bearingCad = norm360(deg(Math.atan2(dir.n, dir.e)));
|
||||
// skew만큼 시계방향으로 돌린 선이 날개벽 선이다. 배면 쪽(받침 반대쪽)이 방향을 정한다.
|
||||
const line = norm360(bearingCad - skew);
|
||||
const back = { e: -nLeft.e * frontSide, n: -nLeft.n * frontSide };
|
||||
const radLine = (line * Math.PI) / 180;
|
||||
const pick = Math.cos(radLine) * back.e + Math.sin(radLine) * back.n > 0 ? line : norm360(line + 180);
|
||||
const axisCad = norm360(deg(Math.atan2(u.n, u.e)));
|
||||
const sinDelta = Math.abs(Math.sin(((pick - axisCad) * Math.PI) / 180));
|
||||
const wings = input.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||||
// 날개벽 두께는 측별 입력 (2026-11-04, 없으면 450): 축 방향 폭 = 두께 / |sin Δ|.
|
||||
const thick = { left: input.wingThicknessMm?.left ?? WING_THICKNESS_MM, right: input.wingThicknessMm?.right ?? WING_THICKNESS_MM };
|
||||
const gap = input.slabWingGapMm ?? SLAB_CLEARANCE_MM;
|
||||
const present = (f: WingForm, side: 'left' | 'right') => (f === 'none' ? 0 : thick[side] / sinDelta + gap);
|
||||
const slabWidthMm = Math.round(length * 1000 - present(wings.left, 'left') - present(wings.right, 'right'));
|
||||
const girderDiffDeg = input.girderAzimuthDeg === undefined ? NaN : lineDiff(pick, input.girderAzimuthDeg);
|
||||
const slopeI = input.slopeI ?? 0;
|
||||
return {
|
||||
lineDeg: pick,
|
||||
girderDiffDeg,
|
||||
aw01Ok: Number.isNaN(girderDiffDeg) ? true : girderDiffDeg <= AW01_TOLERANCE_DEG,
|
||||
slopePct: (input.type === 'A1' ? -slopeI : slopeI) * 100,
|
||||
slabWidthMm,
|
||||
wings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 교대 지반·지층 (2026-11-04, 시안 ② 지반·지층 카드 = 교각과 같은 카드). 순수 함수.
|
||||
*
|
||||
* - 입력은 교각과 같은 이름이다: 위치(육상 · 하상 · 해상), 지하수위, 설계홍수위 · 세굴, 고조위 · 저조위, 지층 행.
|
||||
* 지반고는 엑셀 입력(①에서 수정)이다. 표시만 하고 형상 계산에는 쓰지 않는다.
|
||||
* - 검사는 교각과 같다(G01–G14, 말뚝 근입 G04). 지반고가 저판 하면보다 낮으면 흙덮개가 없다는 정보 AI01을 더한다.
|
||||
*/
|
||||
import { SUPPORT_STRATUM, validateGround, validatePileEmbedment, validateStrataNValues, type GroundCheckInput } from '../ground';
|
||||
import type { Issue, PierInput } from '../types';
|
||||
import type { AbutMembers } from './members';
|
||||
import type { AbutRecord } from './parse';
|
||||
|
||||
export type AbutGround = Pick<PierInput, 'groundPosition' | 'groundWaterOn' | 'groundWaterEl' | 'floodLevelEl' | 'scourDepthM' | 'highWaterEl' | 'lowWaterEl' | 'strataRows'>;
|
||||
|
||||
/** 말뚝 두께 (mm): spec_abut 말뚝 Φ600 × 12T. Used only by the 해상 부식 여유 check. */
|
||||
const ABUT_PILE_THICKNESS_MM = 12;
|
||||
|
||||
/** The check input of an abutment: its 지반 values, 지반고 and 저판 하면 (기초 하단 when the 저판 is not built). */
|
||||
export function abutGroundCheckInput(rec: AbutRecord, members?: AbutMembers): GroundCheckInput {
|
||||
return {
|
||||
name: rec.name,
|
||||
groundEl: rec.groundEl,
|
||||
footingBottomEl: members?.footingBottomEl ?? rec.foundationBottomEl,
|
||||
pileThicknessMm: ABUT_PILE_THICKNESS_MM,
|
||||
...(rec.ground ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 연암 상면 EL: the bottom of the row above the 연암 row (지반고 for the first row); undefined without a 연암 row. */
|
||||
export function supportTopEl(groundEl: number, rows: { name: string; bottomEl: number }[] | undefined): number | undefined {
|
||||
const list = rows ?? [];
|
||||
const k = list.findIndex((r) => r.name === SUPPORT_STRATUM);
|
||||
if (k < 0) return undefined;
|
||||
return k === 0 ? groundEl : list[k - 1].bottomEl;
|
||||
}
|
||||
|
||||
/** Issues of the 지반·지층 card: the pier checks plus AI01 (지반고 below the 저판 하면). */
|
||||
export function abutGroundIssues(rec: AbutRecord, members?: AbutMembers): Issue[] {
|
||||
const input = abutGroundCheckInput(rec, members);
|
||||
const issues: Issue[] = [];
|
||||
validateGround(input, issues);
|
||||
validateStrataNValues(input, issues);
|
||||
if (members && members.pileBottomEl !== undefined && members.piles.length > 0) {
|
||||
validatePileEmbedment(input, { piles: [{ bottom: members.pileBottomEl, diameter: members.shape.pileDiameterM }] }, issues);
|
||||
}
|
||||
if (rec.groundEl < input.footingBottomEl) {
|
||||
issues.push({
|
||||
code: 'AI01',
|
||||
level: 'I',
|
||||
pier: rec.name,
|
||||
message: `지반고 ${rec.groundEl.toFixed(3)}이 저판 하면 ${input.footingBottomEl.toFixed(3)}보다 ${(input.footingBottomEl - rec.groundEl).toFixed(3)} 낮아 흙덮개가 없습니다.`,
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
/** True when any 지반 value differs from the default (육상, 지하수위 없음, 지층 행 없음). */
|
||||
export function abutGroundChanged(ground: AbutGround | undefined): boolean {
|
||||
if (!ground) return false;
|
||||
return Object.entries(ground).some(([key, v]) => {
|
||||
if (v === undefined) return false;
|
||||
if (key === 'groundPosition') return v !== '육상';
|
||||
if (key === 'groundWaterOn') return v === true;
|
||||
if (key === 'strataRows') return (v as unknown[]).length > 0;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Issue } from '../types';
|
||||
|
||||
export interface AbutImportIssueGroup {
|
||||
name?: string;
|
||||
primary: Issue[];
|
||||
related: Issue[];
|
||||
}
|
||||
|
||||
/** Keep root errors visible while retaining every dependent row in a disclosure. */
|
||||
export function groupAbutImportIssues(issues: readonly Issue[]): AbutImportIssueGroup[] {
|
||||
const groups: { name?: string; issues: Issue[] }[] = [];
|
||||
const byName = new Map<string, { name: string; issues: Issue[] }>();
|
||||
for (const issue of issues) {
|
||||
if (issue.pier === undefined) {
|
||||
groups.push({ issues: [issue] });
|
||||
continue;
|
||||
}
|
||||
let group = byName.get(issue.pier);
|
||||
if (!group) {
|
||||
group = { name: issue.pier, issues: [] };
|
||||
byName.set(issue.pier, group);
|
||||
groups.push(group);
|
||||
}
|
||||
group.issues.push(issue);
|
||||
}
|
||||
return groups.map(({ name, issues: items }) => {
|
||||
const primary = items.filter((issue) => issue.code !== 'AP04');
|
||||
const related = items.filter((issue) => issue.code === 'AP04');
|
||||
if (!primary.length && related.length) primary.push({
|
||||
...related[0],
|
||||
message: `교대 ${name ?? ''}: 교대 정보 행이 없어 IP·받침 ${related.length}개 행을 연결하지 못했습니다. 전체 교대명을 확인하세요.`,
|
||||
});
|
||||
return { name, primary, related };
|
||||
});
|
||||
}
|
||||
Loaded 100 of 2810 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user