Merge origin/master with current Pierzainer development

Preserve local abutment, cast-in-place rebar and compact header features alongside remote drawing, ground-link and quantity workspaces. Adapt drawing and takeoff consumers to generated solplate plans, retain current P1 regression coverage, and resolve bearing-detail annotation collisions.

Validation: 122 test files / 975 tests passed; production build passed; 10 browser integration checks passed.
This commit is contained in:
ypshim committed 2026-11-06 17:10:33 +09:00
commit f51865c57b
2645 files changed
+64918 -4910

No files matched your search

+2
View File
@@ -13,3 +13,5 @@ __pycache__/
pierzainer_ground/data/cache/
pierzainer_ground/data/output/**/cache/
backup/
verify/
도면추출/output/
+12 -3
View File
@@ -19,14 +19,19 @@ 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에 추가하지 않습니다.
@@ -52,3 +57,7 @@ Windows Chrome을 쓰면 Windows Node로 실행하고 입력·출력 경로도 W
혼합 전 Pierzainer 커밋 `d6735a6`에서 로컬 브랜치 `recovery/pierzainer-20261102`를 만들었습니다. π형 Excel 인식, 전·후열 받침 수 불일치 허용, 랜딩페이지 샘플 연결을 포함합니다. 기존 ITAM 브랜치와 원격 기록은 변경하지 않았습니다.
당시 복구 기록에는 분리 파일을 상위 `_recovery/20261102_mixed_projects/`에 보관하고, 당시 `master`가 ITAM 코드였다고 적혀 있습니다. 2026-11-05 현재 로컬 `master`는 Pierzainer 코드(`91c4a4a`)이므로 과거 안내에 따라 복구 브랜치로 전환하지 마세요. 당시 복구 보관 경로의 현존 여부는 이번 작업에서 재확인하지 않았습니다. 이번 검증·기록 정리에서도 원격 push와 브랜치 전환은 수행하지 않았습니다.
## 정리 기준
실행에 필요한 코드·원본 도면·샘플 Excel·최종 지반 JSON은 유지합니다. 과거 개발용 도구와 옛 좌표 기준의 검증 파일은 정리했으며 백업 목록은 상위 `_recovery/20261103_cleanup/`에 있습니다.
+3
View File
@@ -17,6 +17,7 @@
<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>
@@ -266,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>
+1
View File
@@ -4,6 +4,7 @@
"description": "교각 3D 형상 생성 웹앱 (1단계: 형상 생성)",
"scripts": {
"dev": "vite",
"preview": "vite preview",
"build": "tsc --noEmit && vite build",
"typecheck": "tsc --noEmit",
"test": "vitest run"
+1 -1
View File
@@ -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"
+15
View File
@@ -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 입력 연결을 재사용한다.
+59
View File
@@ -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));}};
}
+11
View File
@@ -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();});
});
+66
View File
@@ -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);
+5
View File
@@ -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}
+25 -6
View File
@@ -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)
+29
View File
@@ -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()
@@ -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
-12
View File
@@ -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"
+22
View File
@@ -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(); }
+83
View File
@@ -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();
});
-43
View File
@@ -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); });
-113
View File
@@ -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);
});
+4 -3
View File
@@ -50,11 +50,12 @@ export interface ColumnInput {
* the other values stay as in COLUMN_P1.
*/
export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1, columnIndex = 0): ColumnInput {
if (!model.footings.length) throw new Error('현장타설말뚝 기둥은 전용 배근 입력을 사용합니다. 확대기초 정착 규칙은 적용하지 않습니다.');
// No footing (현장타설말뚝, merged 2026-11-06 with origin's guard): Hc still runs to the column bottom (= 지반고) and Hf is 0.
// The cast-in-place rebar itself comes from cast-layout.ts; callers only use the footing rules when a footing exists.
const col = model.columns[columnIndex];
const globalS = col.s + model.stretches[col.k].base;
const seg = model.segments.find((g) => globalS >= g.s0 - 1e-9 && globalS <= g.s1 + 1e-9) ?? model.segments[0];
const footing = model.footings.find(f => f.k === col.k && Math.abs(f.s - col.s) <= f.length / 2)!;
const footing = model.footings.find(f => f.k === col.k && Math.abs(f.s - col.s) <= f.length / 2);
const Hc = Math.round((col.top - col.bottom) * 1000 * 10) / 10;
const hcop = Math.round((seg.top - col.top) * 1000 * 10) / 10;
// C3 (상면 3단) = the lowest coping top −127 (C1 cover) −2·s_lay (상면 주철근 단 간격 input); P1 was 43.170 at 57.2.
@@ -64,7 +65,7 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1,
// e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23).
const eTop = Math.round((seg.top - (col.top + seg.top) / 2) * 10000) / 10;
return { ...base, ...model.columnRebar, footing: model.footingRebar, Hc, hcop, eP1, eTop, D: model.columnDiameterMm,
Hf: (footing.top - footing.bottom) * 1000, bothEndZones: model.columns.length > 1 };
Hf: footing ? (footing.top - footing.bottom) * 1000 : 0, bothEndZones: model.columns.length > 1 };
}
export const COLUMN_P1: ColumnInput = {
+36 -26
View File
@@ -1,4 +1,6 @@
import './style.css';
import { createDrawingWorkspace } from '../도면작성/workspace';
import { createTakeoffWorkspace } from '../수량산출/workspace';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { swapXYAzimuth } from './core/coordinate-frame';
@@ -145,8 +147,8 @@ interface Pier extends PierValues {
}
/** Part toggles in the order of 시안 ① "부재 표시". Each toggle controls one or more scene parts. */
/** The stages: ① 형상 생성, ② 형상계획, ③ 철근배근. 'quantity' is the 철근재료표 screen opened from ③ (시안: header ①②③, 2026-11-06). */
type Stage = 'shape' | 'plan' | 'rebar' | 'quantity';
/** ① 형상 생성, ② 형상계획, ③ 철근배근, ④ 도면작성 (origin). 'quantity' is the 재료표 · 수량산출 screen opened from ③ (시안: no header step, 2026-11-06). */
type Stage = 'shape' | 'plan' | 'rebar' | 'drawing' | 'quantity';
/** 부재 표시 toggles (2026-11-03). `pier` parts follow the pier scenes; `abut` names follow the abutment groups (abut-scene.ts). */
const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
@@ -279,6 +281,21 @@ let backupTimer: number | undefined;
const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T;
const drawingWorkspace = createDrawingWorkspace($('view-drawing'), () => ({
project: state.project, selected: state.active,
piers: state.piers.map(p => ({ input: p.input, model: p.model, dirty: pendingKeys(p).length > 0 })),
}), selectPier);
const quantityRebarTon = new Map<string,number|null>();
const quantityWorkspace = createTakeoffWorkspace($('view-quantity'), () => ({
project: state.project, selected: state.active,
piers: state.piers.map(p => ({input:p.input,model:p.model,dirty:pendingKeys(p).length>0,rebarTon:quantityRebarTon.get(p.input.name)})),
}), selectPier, (name,data) => {
const pier=state.piers.find(p=>p.input.name===name);if(!pier)return;
pier.input={...pier.input,quantityTakeoff:data};pier.draft={...pier.draft,quantityTakeoff:data};
renderSaveStatus();scheduleBackup();
});
function el<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, cls?: string): HTMLElementTagNameMap[K] {
const node = document.createElement(tag);
if (text !== undefined) node.textContent = text;
@@ -2046,6 +2063,7 @@ function renderHeader(): void {
$('project-line').textContent = parts.join(' · ');
$('project-line').title = parts.join(' · ');
setHeaderStatus('step1-status', '', pier?.model ? '생성됨' : '미생성');
setHeaderStatus('step-drawing-status', '', pier?.model ? '가능' : '형상 필요');
renderSaveStatus();
const total = pendingTotal();
const targets = generationTargets();
@@ -2214,6 +2232,7 @@ function renderAll(): void {
renderReviewTasks();
renderRebarColor();
if (state.stage === 'quantity') renderQuantityScreen();
if (state.stage === 'drawing') drawingWorkspace.update();
scheduleBackup();
}
@@ -2686,20 +2705,22 @@ function setStage(stage: Stage): void {
// Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout.
// Entering or leaving ② or ③ changes which objects are drawn (visibility.ts), so the scene is rebuilt then.
const selectionOnly = (s: Stage) => s === 'plan' || s === 'rebar';
if ((allPiers !== null) !== multiSceneOn() || selectionOnly(stage) !== selectionOnly(previous) || (stage === 'rebar') !== (previous === 'rebar')) setupScene();
// ④ 도면작성 hides the 3D view, so the scene is not rebuilt for it (origin).
if (stage !== 'drawing' && ((allPiers !== null) !== multiSceneOn() || selectionOnly(stage) !== selectionOnly(previous) || (stage === 'rebar') !== (previous === 'rebar'))) setupScene();
if (state.scene) setRebarView(state.scene, stage === 'rebar');
for (const g of abutGroups) setRebarViewOn(g, stage === 'rebar');
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
$<HTMLElement>('view-rebar').hidden = stage !== 'rebar';
$<HTMLElement>('view-quantity').hidden = stage !== 'quantity';
$<HTMLElement>('view-drawing').hidden = stage !== 'drawing';
if (stage === 'quantity') renderQuantityScreen();
// Move the viewer after its new parent is visible, so that resize() measures the real size.
placeViewer(stage);
if (rebar3d) rebar3d.group.visible = stage === 'rebar';
// The highlight is the 'current' class (style.css); aria-current is set alongside it for assistive tech.
// The 재료표 screen belongs to ③, so ③ stays highlighted while it is open.
const nav: [string, Stage[]][] = [['nav-shape', ['shape']], ['nav-plan', ['plan']], ['nav-rebar', ['rebar', 'quantity']]];
const nav: [string, Stage[]][] = [['nav-shape', ['shape']], ['nav-plan', ['plan']], ['nav-rebar', ['rebar', 'quantity']], ['nav-drawing', ['drawing']]];
for (const [id, keys] of nav) {
const btn = $<HTMLButtonElement>(id);
btn.classList.toggle('current', keys.includes(stage));
@@ -4605,6 +4626,9 @@ function renderQuantityScreen(): void {
const entries = targets.flatMap((pier) =>
pier.model ? [{ pier, q: pierQuantity(pier) }] : [],
);
quantityRebarTon.clear();
for(const {pier,q} of entries)quantityRebarTon.set(pier.input.name,q.missing.length?null:q.kg/1000);
quantityWorkspace.refresh();
// 전체 with more than one pier: the bridge total first (교량 전체 집계); the per-pier tables follow.
if (state.quantityAll && entries.length > 1) body.appendChild(bridgeTotalBlock(aggregateQuantities(entries.map((e) => e.q)), entries.length));
for (const { pier, q } of entries) {
@@ -5105,7 +5129,12 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void {
importInput.value = '';
}
});
const importLabel = el('label', '지반 모델 파일 불러오기 (json)');
const groundLaunch = el('button', '지반 모델 파일 불러오기 · 지반 생성 프로그램');
groundLaunch.type = 'button';groundLaunch.className = 'btn';
groundLaunch.addEventListener('click', () => window.dispatchEvent(new Event('pierzainer:open-ground')));
wrap.appendChild(groundLaunch);
const linkStatus = el('p', '현재 Excel을 사용하여 종평면도에서 지반을 생성합니다.', 'muted small');linkStatus.id='ground-link-status';wrap.appendChild(linkStatus);
const importLabel = el('label', '기존 지반 JSON 직접 불러오기');
importLabel.appendChild(importInput);
wrap.appendChild(importLabel);
@@ -6016,16 +6045,8 @@ function applyImport(): void {
setStatus(`${state.piers.length}기 형상을 생성했습니다.${abutNote} 정보·경고는 오른쪽 알림에서 확인하세요.`);
}
function openImport(): void {
const landing = document.querySelector<HTMLDivElement>('.landing-overlay');
if (landing && !landing.hidden) return;
const dlg = $<HTMLDialogElement>('import-dialog');
if (!dlg.open) dlg.showModal();
$<HTMLInputElement>('project-name').focus();
}
async function fetchSample(): Promise<Uint8Array> {
const res = await fetch(new URL('samples/운곡교P1입력.xlsx', document.baseURI));
const res = await fetch(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
if (!res.ok) throw new Error(`샘플 응답 ${res.status}`);
return new Uint8Array(await res.arrayBuffer());
}
@@ -6034,7 +6055,6 @@ declare global {
interface Window {
loadBytesToApp?: (bytes: Uint8Array, fileName: string) => void;
openSampleProject?: () => Promise<void>;
openDrawingFlow?: () => void;
}
}
@@ -6056,16 +6076,6 @@ window.openSampleProject = async () => {
setError(error instanceof Error ? error.message : '샘플 교량 모델을 불러오지 못했습니다.');
}
};
window.openDrawingFlow = () => {
const app = document.querySelector<HTMLElement>('.app');
const landing = document.querySelector<HTMLDivElement>('.landing-overlay');
if (app) {
app.hidden = false;
app.setAttribute('aria-hidden', 'false');
}
if (landing) landing.hidden = true;
setStatus('도면 입력은 현재 개발 중입니다. 기존 도면 분석 모듈과 연결되기 전까지는 파일 선택 흐름만 유지합니다.');
};
/** Piers that need a (re)build: never generated, or with changes not yet in the 3D model. */
function generationTargets(): Pier[] {
@@ -6087,7 +6097,6 @@ function regenerateChanged(): void {
}
// --- events -----------------------------------------------------------------------------
$<HTMLButtonElement>('open-import').addEventListener('click', openImport);
$<HTMLButtonElement>('dlg-close').addEventListener('click', () => {
$<HTMLDialogElement>('import-dialog').close();
document.querySelector<HTMLElement>('.landing-overlay')?.classList.remove('modal-open');
@@ -6102,6 +6111,7 @@ $<HTMLButtonElement>('nav-shape').addEventListener('click', () => setStage('shap
$<HTMLButtonElement>('nav-plan').addEventListener('click', () => setStage('plan'));
$<HTMLButtonElement>('nav-rebar').addEventListener('click', () => setStage('rebar'));
$<HTMLButtonElement>('quantity-back').addEventListener('click', () => setStage('rebar'));
$<HTMLButtonElement>('nav-drawing').addEventListener('click', () => setStage('drawing'));
$<HTMLInputElement>('mat-fck').addEventListener('change', (ev) => setMaterial('fck', (ev.target as HTMLInputElement).value));
$<HTMLInputElement>('mat-waste').addEventListener('change', (ev) => setWaste((ev.target as HTMLInputElement).value));
for (const k of ['small16', 'main25', 'main35', 'main51'] as const) {
+113
View File
@@ -0,0 +1,113 @@
import { EventEmitter } from 'node:events';
import { PassThrough, Readable } from 'node:stream';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import type { ChildProcess } from 'node:child_process';
import type { IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DRAWING_API_PATH, drawingLauncherPlugin } from '../랜딩페이지/drawing-launcher';
class Worker extends EventEmitter {
stdout = new PassThrough(); stderr = new PassThrough(); kill = vi.fn();
send(value: object) { this.stdout.write(JSON.stringify(value) + '\n'); }
}
const base = path.resolve('.verify');
const roots: string[] = [];
function fixture(options: { missing?: boolean; fail?: boolean; platform?: NodeJS.Platform } = {}) {
mkdirSync(base, { recursive: true });
const root = mkdtempSync(path.join(base, 'drawing-api-')); roots.push(root);
const folder = path.join(root, 'DWG & 도면'); mkdirSync(folder);
const workers: Worker[] = [];
const start = vi.fn((_file: string, args: string[]) => {
const worker = new Worker(); workers.push(worker);
queueMicrotask(() => {
if (options.fail) worker.emit('error', new Error('worker unavailable'));
else if (args[2] === 'choose') { worker.send({ type: 'folder', folder }); worker.emit('close', 0); }
});
return worker as unknown as ChildProcess;
});
const plugin = drawingLauncherPlugin(root, {
platform: options.platform ?? 'win32', start,
exists: file => options.missing ? false : file.endsWith('python.exe') || file.endsWith('drawing-worker.py') || existsSync(file),
});
let handle!: (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
(plugin.configureServer as Function)({ middlewares: { use(fn: typeof handle) { handle = fn; } } });
const request = (action: string, body: object = {}, overrides: { method?: string; origin?: string; action?: string; address?: string; host?: string; raw?: string } = {}) => new Promise<{ status: number; body: any; bytes?: Buffer }>(resolve => {
const req = Readable.from([overrides.raw ?? JSON.stringify(body)]) as unknown as IncomingMessage;
Object.assign(req, { url: `${DRAWING_API_PATH}/${action}`, method: overrides.method ?? 'POST', headers: {
host: overrides.host ?? '127.0.0.1:5173', origin: overrides.origin ?? 'http://127.0.0.1:5173', 'x-pierzainer-action': overrides.action ?? 'drawing-extractor',
}, socket: { remoteAddress: overrides.address ?? '127.0.0.1' } });
let status = 0, json = true, sent = false;
const res = { get headersSent() { return sent; }, writeHead(code: number, headers: Record<string, string>) { status = code; json = headers['Content-Type'].includes('application/json'); sent = true; },
end(text: string | Buffer) { resolve(json ? { status, body: JSON.parse(String(text)) } : { status, body: null, bytes: Buffer.from(text) }); } } as unknown as ServerResponse;
handle(req, res, () => resolve({ status: 404, body: {} }));
});
const finish = (outside = false) => {
const args = start.mock.calls[start.mock.calls.length - 1][1];
const output = args[args.indexOf('--output')+1];
const file = path.join(outside ? root : output, '결과.xlsx'); writeFileSync(file, 'test-workbook');
const worker = workers[workers.length-1]; worker.send({ type: 'log', message: '도면 판독 완료' });
worker.send({ type: 'result', result: { status: 'needs_review', output_dir: output, review_count: 2, failed_file_count: 0,
workbooks: [{ name: '테스트 교량', xlsx_path: file, review_count: 2 }] } });
worker.emit('close', 0);
return file;
};
return { root, folder, request, start, workers, finish };
}
afterEach(() => {
for (const root of roots.splice(0)) {
if (!root.startsWith(base + path.sep)) throw new Error('Unsafe test fixture path');
rmSync(root, { recursive: true, force: true });
}
});
describe('in-app drawing extraction API', () => {
it('only accepts local same-origin POST requests with the application header', async () => {
const f = fixture();
expect((await f.request('start', {}, { method: 'GET' })).status).toBe(405);
for (const headers of [{ origin: 'https://other.example' }, { action: '' }, { address: '192.168.0.2' }, { host: 'other.example' }])
expect((await f.request('start', {}, headers)).status).toBe(403);
expect(f.start).not.toHaveBeenCalled();
});
it('returns folder selection without starting the standalone GUI', async () => {
const f = fixture(); expect((await f.request('choose-folder')).body.folder).toBe(f.folder);
expect(f.start.mock.calls[0][1][2]).toBe('choose');
expect(f.start.mock.calls[0][1].join(' ')).not.toContain('desktop_app.py');
expect((await f.request('status')).body.job).toBeNull();
});
it('tracks progress, preserves review warnings, and serves only its generated workbook', async () => {
const f = fixture(); const response = await f.request('start', { folder: f.folder });
expect(response.status).toBe(202); const jobId = response.body.job.id;
expect(f.start.mock.calls[0][1][3]).toBe(f.folder);
expect((await f.request('workbook', { jobId, index: 0 })).status).toBe(400);
const file = f.finish();
const state = (await f.request('status', { jobId })).body.job;
expect(state.status).toBe('needs_review'); expect(state.logs).toContain('도면 판독 완료');
expect(state.result.reviewCount).toBe(2);
const download = await f.request('workbook', { jobId, index: 0, path: 'unrelated.txt' });
expect(download.status).toBe(200); expect(download.bytes).toEqual(readFileSync(file));
expect((await f.request('workbook', { jobId, index: -1 })).status).toBe(400);
expect((await f.request('workbook', { jobId, index: 20 })).status).toBe(404);
});
it('does not run two extraction jobs concurrently', async () => {
const f = fixture(); const states = await Promise.all([f.request('start', { folder: f.folder }), f.request('start', { folder: f.folder })]);
expect(states.map(s => s.status).sort()).toEqual([202, 409]); expect(f.start).toHaveBeenCalledTimes(1); f.finish();
});
it('refuses files outside the controlled output even if a worker lists them', async () => {
const f = fixture(); const { body } = await f.request('start', { folder: f.folder }); f.finish(true);
expect((await f.request('workbook', { jobId: body.job.id, index: 0 })).status).toBe(403);
});
it('reports worker failures so the popup can retry', async () => {
const f = fixture({ fail: true }); const { body } = await f.request('start', { folder: f.folder });
const job = (await f.request('status', { jobId: body.job.id })).body.job;
expect(job.status).toBe('error'); expect(job.error).toContain('worker unavailable');
});
it('validates folders, JSON, and request size before starting a worker', async () => {
const f = fixture();
expect((await f.request('start', { folder: 'missing-folder' })).status).toBe(400);
expect((await f.request('start', {}, { raw: 'bad-json' })).status).toBe(400);
expect((await f.request('start', {}, { raw: 'a'.repeat(8193) })).status).toBe(413);
expect(f.start).not.toHaveBeenCalled();
expect((await fixture({ missing: true }).request('status')).status).toBe(503);
expect((await fixture({ platform: 'linux' }).request('status')).status).toBe(501);
});
});
+2 -2
View File
@@ -8,9 +8,9 @@ import { type BearingPair, girderPairs } from '../src/core/bearing-pair';
import { buildPier, type PierModel } from '../src/core/geometry';
/** Spec workbook (RAG/Pier ui_xxx.xlsx, sheet intent.PR, all tables on one sheet). Used by single-sheet tests. */
const SPEC_PATH = fileURLToPath(new URL('../public/samples/P1.xlsx', import.meta.url));
const SPEC_PATH = fileURLToPath(new URL('../도면추출/samples/P1.xlsx', import.meta.url));
/** User input (RAG/운곡교P1입력.xlsx, three sheets 교각 · IP · 받침). This is the P1 input used by the app and tests. */
const INPUT_PATH = fileURLToPath(new URL('../public/samples/운곡교P1입력.xlsx', import.meta.url));
const INPUT_PATH = fileURLToPath(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
export function loadWorkbook(path: string = SPEC_PATH): XLSX.WorkBook {
return XLSX.read(readFileSync(path), { type: 'buffer' });
+2 -2
View File
@@ -4,9 +4,9 @@ import { fileURLToPath } from 'node:url';
import { parseWorkbook } from '../src/core/parseWorkbook';
import { loadWorkbook } from './helpers';
// The standing input is RAG/운곡교P1입력.xlsx. The app serves a copy in public/samples; the two must stay identical.
// The standing input is RAG/운곡교P1입력.xlsx. The app uses the copy in 도면추출/samples.
const RAG_INPUT = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const APP_COPY = fileURLToPath(new URL('../public/samples/운곡교P1입력.xlsx', import.meta.url));
const APP_COPY = fileURLToPath(new URL('../도면추출/samples/운곡교P1입력.xlsx', import.meta.url));
describe('표준 입력 파일 (운곡교P1입력.xlsx)', () => {
it('the app sample is byte-identical to RAG/운곡교P1입력.xlsx', () => {
+2 -1
View File
@@ -6,9 +6,10 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true,
"isolatedModules": true,
"types": ["node"]
},
"include": ["src", "tests"]
"include": ["src", "tests", "랜딩페이지", "vite.config.ts"]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

+10
View File
@@ -0,0 +1,10 @@
import { quantityPlugin } from './수량산출/server.ts';
import { groundLinkPlugin } from './pierzainer_ground/ground-link-server.ts';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import { drawingLauncherPlugin } from './랜딩페이지/drawing-launcher.ts';
import { generalArrangementDwgPlugin } from './도면작성/dwg-server.ts';
export default defineConfig({
plugins: [quantityPlugin(fileURLToPath(new URL('.', import.meta.url))),groundLinkPlugin(fileURLToPath(new URL('.', import.meta.url))),drawingLauncherPlugin(fileURLToPath(new URL('.', import.meta.url))),generalArrangementDwgPlugin(fileURLToPath(new URL('.', import.meta.url)))],
});
+4
View File
@@ -0,0 +1,4 @@
.cache/
__pycache__/
*.pyc
output/
+7
View File
@@ -0,0 +1,7 @@
# 받침 상세 문자치수와 치수표
상세 A의 A/B(받침 본체 평면), C(전·후열 중심 간격), D(일체 교좌면 길이), S(%), T/T1/T2(솔플레이트), h1/h2/h3/E(높이)를 문자 기호로 표시한다.
표는 기존 bearingSchedule의 현재 모델 계산값을 사용한다. A/B/C/T1/T2/E 치수 객체는 실제 mm 수치를 value로 유지하고 label만 문자로 표시한다. SVG와 DXF DIMENSION 문자 재정의에 같은 label을 적용한다. D는 일체 교좌면이 있는 경우만 표기한다. 미확인 제원은 값을 임의로 만들지 않는다. 첨부 참고 이미지의 3100, 1200, 50 등을 현재 모델의 실제값으로 복사하지 않는다.
기존 받침 번호와 EL.X, 높이·표고표를 유지한다. 수정 범위는 도면작성 폴더 내부다.
+9
View File
@@ -0,0 +1,9 @@
# DWG 호환성 점검 (2026-11-06)
현재 번들 LibreDWG dxf2dwg는 R2000/R2004까지만 지원하고 최신 버전은 지원하지 않는다. 기존 구현은 R2004 객체 누락 때문에 R2000을 사용한다. 이번에 버전 표기만 변경하거나 검증되지 않은 최신 버전을 생성하지 않았다.
최근 출력 원본 source.dxf를 재변환했다. R2000 미지원 MATERIAL 및 ACAD_MATERIAL 사전을 변환용 중간 DXF에서 제외하고, 재질 참조를 초기화했다. 원본 DXF와 프로젝트 데이터는 변경하지 않는다. ezdxf 저장에는 기본 재질이 필요하므로 저장 후 변환 입력에만 적용한다. 색상은 기존 ACI 변환을 유지한다.
결과: output/compatibility-20261106/general.dwg. 모델 8911객체, 배치 9463객체, 치수 44개 및 객체 지문/레이어/문자스타일/출력설정 일치. 이전 잘못된 MATERIAL placeholder 복구 3건은 0건으로 줄었다. 최대 치수 오차 1.1e-11mm.
단, 동일 LibreDWG 재읽기 검증이며 실제 사용자 CAD 열기 검증이 아니다. decoder에는 중복 핸들 #144 및 ENDBLK 경고가 남는다. 따라서 이 수정이 열리지 않는 원인을 확정 해결했다고 판정하지 않는다. CAD 제품·버전·오류 메시지에 따라 독립 CAD 읽기 또는 신뢰할 수 있는 최신 DWG 변환기가 필요하다.
+67
View File
@@ -0,0 +1,67 @@
> 최신 원본 양식 보강과 변경된 치수 스타일(문자 3mm, 점 기호)은 [REFERENCE_PARITY.md](REFERENCE_PARITY.md)를 우선 참조하세요. 아래는 이전 품질 개선 단계의 기록입니다.
# 교각 일반도 품질 개선 보고서 — 2026-11-03
## 기준 도면과 기존 결과의 차이
기준은 이 폴더의 `[교각-T 형] 01 일반도_20260619.dwg`이다. 첨부 원본과 SHA-256이 같은 파일이며 원본을 수정하지 않았다. 원본의 CXBLKA1 도곽/로고, 표제란 38개 속성, NOTE 21줄을 유지했다. 정면·측면 상단, 두부보·기초 평면 하단, 우측 상세·제원표·키플랜·NOTE 구성도 유지했다.
기존 결과는 고정 슬롯과 형상 경계 중심으로 축척을 정해, 문자와 치수선까지 고려한 점유 영역이 맞지 않았다. 전체·세부 치수가 같은 공간을 차지하고 출력용 문자가 작았으며, 긴 기둥 생략 표시와 실치수의 배치 관계도 미흡했다. 개선 순서는 점유 영역 측정, 치수 레인, 공통 축척·정렬, 출력 스타일, CAD 재읽기 검증이다.
원본 HS_050의 dimtxt=3, 화살표=1을 참고했다. 원본 레이어는 BYDEFAULT 선가중치를 사용하며 CTB가 없으므로 출력 선두께를 원본과 동일하다고 단정하지 않는다. 새 문서는 외형/숨은선/중심선/치수선/해치에 명시적인 선가중치를 지정한다.
## 구현 내용
- 실제 3D 모델 `PierModel`의 투영 형상을 유지한다. 기둥·말뚝 생략은 표시 변환이며 실제 길이·EL·모델 공간 치수는 유지한다.
- 문자 회전·한글 폭·화살표·보조선·기둥 생략을 포함한 점유 영역을 계산한다. 정면/측면은 공통 축척과 EL 정렬을 사용하고, 형상별 열 너비·행 높이와 평면도 축척을 산정한다.
- 짧은 세부 치수를 먼저 배치하고 전체 치수를 외측 레인으로 옮긴다. 짧은 치수의 화살표는 외부로 배치한다. 치수 보조선에는 간격과 연장 길이를 적용한다.
- 생성 치수 문자 2.8mm, EL 2.6mm, 표·일반 문자 최소 2.5mm를 용지 기준으로 관리한다. 원본 도곽 속성/NOTE 문자는 별도 편집값을 유지하므로 이 최소값의 대상이 아니다.
- 받침 12개 초과 시 표 행 간격을 4.2mm에서 3.5mm로 줄이되 2.5mm 문자와 셀 안쪽 여백을 유지한다. 표제란·NOTE 사이의 제한된 영역을 넘기지 않도록 한다.
- 번호가 많은 키플랜은 교각 번호·시종점·북향 문자를 함께 검사하고 필요시 간격과 리더를 조정한다. 상세 EL 문자는 실제 경계 상자로 간섭을 피한다.
- 원본 표제란 시작 경계(841×594 기준 y=537)를 보호한다. 치수표 하단 설명까지 포함해 y=527 이내에 배치한다. 원본 셀보다 긴 편집 속성과 NOTE 초과도 검사한다.
- `품질 검사`에서 문자/외형·문자/문자·도면 간·보호 영역 간섭을 확인하고 해당 위치를 강조하거나 결과 JSON을 저장한다. 의도된 중심 번호/단면 기호는 별도 취급한다. 수동 과대 축척은 임의 축소하지 않고 범위 초과를 보고한다.
문자 경계는 폰트 폭을 보수적으로 추정한 검사다. 실제 CAD 글꼴 대체까지 보장하는 완전한 간섭 해석은 아니며 선의 의도된 연결·표 격자·치수 끝점은 간섭으로 세지 않는다.
## DWG 출력 및 정확성
기존 브라우저 벡터/DXF 경로를 유지하고 로컬 서버의 `DWG 내보내기`를 추가했다. 기존 ground 가상환경과 동봉 LibreDWG를 재사용한다. 새 라이브러리·AI·외부 네트워크는 사용하지 않는다. 정적 파일 호스팅만으로는 Python 변환 경로가 실행되지 않는다.
DWG 2000(AC1015)으로 실제 변환한 후 다시 읽는다. R2004 변환 실험에서는 객체 유실이 확인되어 해당 경로를 사용하지 않는다. 변환기에서 수직 치수 각도를 잃는 문제는 중간 DXF의 AcDbRotatedDimension 그룹 50을 명시해 해결했다. 유효하지 않은 0.10mm 해치 선두께는 CAD 허용값 0.09mm로 수정했다.
다운로드 전에 다음 항목을 비교하며 불일치시 실패로 처리한다.
- Model/Paper별 객체 좌표·문자·해치 패턴과 경계 지문
- 원본 부재를 추적하는 PZ_GA XDATA
- 네이티브 치수 측정값(허용 오차 0.000001mm)
- 레이어 선가중치, 치수 스타일, 문자 폰트, 용지와 출력 설정
Model Space의 정면/측면/평면은 1:1 mm의 생략 없는 형상이다. Layout1은 화면과 공유하는 편집 가능한 용지 도면 객체이며 연동 뷰포트는 아니다. 생략된 Paper 치수 표시는 선/문자로 구성하고 Model에는 실제 네이티브 DIMENSION을 저장한다. 아이소메트릭은 제목에 표시한 생략 여부를 따른다.
R2000에서는 RGB가 ACI로 근사된다. 재읽기 도구가 재질 사전의 잘못된 ACDBPLACEHOLDER 3개를 정리하는 내용도 검증 JSON에 기록한다. 형상·문자·치수 일치와 이 메타데이터 정리를 구분한다. **실제 AutoCAD 직접 열기·출력 검증은 미실시**이며 설치된 AutoCAD 실행 환경이 없다.
## 검증 자료와 사용 방법
`output/quality-review/comparison.html`에서 보관된 개선 전 SVG, 개선 후 SVG, 기준 DWG 렌더링을 비교한다. 다른 교각인 기준 DWG와 픽셀 일치율을 만들지 않았다. 자동 결과의 `before`는 현재 입력의 새 배치 엔진 적용 직전 검사값이며, 보관된 과거 SVG의 간섭 개수와 동일한 의미가 아니다.
도면작성 화면에서 교각을 선택하고 자동 축척으로 생성한 뒤 `품질 검사`와 `DWG 내보내기`를 사용한다. 내보낸 파일 옆의 `.validation.json`은 해당 DWG의 SHA-256과 검증 결과를 담는다. 검증 데이터는 `.cache/`에, 검토 산출물은 `output/quality-review/`에 보존한다.
Chrome 1366×768와 1920×1080에서 페이지 오류와 가로 넘침 없이 도면 선택·팬/줌·레이어·용지 변경, 도곽 편집/JSON 저장·불러오기, DWG 다운로드를 확인했다. 기존 철근/재료표/형상 탭 전환과 500mm 코핑 길이 변경 후 모델 재생성에 따른 도면 갱신도 확인했다.
전체 테스트 538개 중 534개 통과, 기존 실패 4건은 `axis.test.ts`, `bearing-coordinate-frame.test.ts`, `integrated-block.test.ts`, `validate-and-foundation.test.ts`이다. 이들 코어 기능은 변경하지 않았으며 커밋하지 않았다. 타입 검사와 프로덕션 빌드는 통과했다(기존 번들 크기 경고 유지).
도면작성 테스트 14개는 모두 통과했다. 전체 프로젝트 키플랜을 포함한 실제 샘플 교각 63개에서 배치 엔진 적용 직전 검사 339건이 최종 0건으로 줄었고, 투영 형상 변경은 0건이다. 반복 입력의 배치 일치, 강제 과대 축척의 범위 초과 검출, 의도적으로 겹친 문자 검출, 치수표 하단의 실제 표제란 경계 내 배치도 확인했다. 이 0건은 정의된 보수적 간섭 검사의 결과이며 모든 CAD 글꼴·인쇄 조건에서의 무간섭 보증은 아니다.
최종 운곡교 P1 및 산외교 π형 DWG는 `output/quality-review/final/`에 있다. 각각 49개/54개의 네이티브 치수를 포함하며 재읽기 최대 치수 오차는 약 7.3×10⁻¹²mm, 감사 오류는 0건이다. 객체·출처·스타일·용지 설정 비교도 통과했다. 각 파일의 `.validation.json`에 개별 결과를 보존한다.
## 변경 파일과 범위
신규 핵심 모듈: `cad-style.ts`, `dimension-graphics.ts`, `occupancy.ts`, `quality-layout.ts`, `frame-quality.ts`, `quality-panel.ts`, `dwg_export.py`, `dwg-server.ts`.
연결/출력/표현 변경: `drawing.ts`, `types.ts`, `presentation.ts`, `project-views.ts`, `bearing-detail.ts`, `sheet.ts`, `svg.ts`, `dxf.ts`, `workspace.ts`, `workspace.css`, `drawing.test.ts`, `.gitignore`, `README.md`.
이번 품질 개선의 폴더 외 수정은 `vite.config.ts`의 DWG 경로 플러그인 등록이다. 기존 `index.html`/`src/main.ts` 탭 연결 변경은 이전 도면작성 구현이며 이번에는 수정하지 않았다. 기존 철근·재료표·형상 코어를 바꾸지 않았다.
## 남은 한계
현재 63개 실제 샘플은 받침 본체/솔플레이트 상세 제원이 없어 A/B/S 등은 미확인으로 남는다. 모델에 없는 변단면 기초·모따기·도로 선형·교대와 시공 조건은 복제하지 않는다. 완전한 부재 가림 판정, 다주 교각의 모든 개별 높이 연쇄 치수, 실제 CAD 폰트·CTB 출력 검토는 추가 확인이 필요하다. 이 결과를 설계 검토 없이 완성 납품 도면으로 판정하지 않는다.
+133
View File
@@ -0,0 +1,133 @@
# 교각 일반도 자동작성
2026-11-04 기준 양식 보강: [REFERENCE_PARITY.md](REFERENCE_PARITY.md). 도곽 BLOCK/38개 ATTRIB, 점 치수, 원본 절단 기호, 말뚝 중심 및 제원 지시선, 도곽 개별 CAD 색상 보존을 추가했다.
현재 프로젝트에서 생성된 `PierModel`을 읽어 정면·측면·두부보 평면·기초 평면·기둥 A-A 단면을 벡터 도면으로 작성한다. 화면 캡처나 별도 교각 모델은 사용하지 않는다. 현재 단계는 **기존 형상 기반의 검토용 일반도**이며 최종 납품 도면의 모든 상세 표현을 완성한 단계는 아니다.
## 품질 개선 및 실제 DWG 출력
최신 변경과 검증 범위는 [QUALITY_REPORT.md](QUALITY_REPORT.md)를 참고한다. 실제 문자·치수 점유 영역 기반 자동 배치, 치수 레인, 간섭 검사 패널, DWG 2000 변환 및 재읽기 검증을 추가했다. 개선 전후/원본 비교와 검증된 DWG는 `output/quality-review/`에 있다. 생성 표/일반 문자는 최소 2.5mm, 치수 2.8mm, EL 2.6mm이며 원본 도곽/NOTE 문자는 별도 설정을 유지한다. 이번 외부 변경은 DWG 서버 경로를 등록하는 `vite.config.ts`에 한정한다.
## 첨부 DWG 도곽·노트·키플랜·등각도 반영 (2026-11-03)
첨부된 Downloads DWG와 폴더의 참고 DWG는 SHA-256이 동일함을 확인했다. 첨부가 CAD에서 열려 있어 읽기 공유 모드로 해시를 확인했으며 원본은 변경하지 않았다. 원본의 **CXBLKA1 도곽 블록**을 분해해 로고·경계·표제란의 실제 벡터 261개, 편집 가능한 ATTRIB 38개, NOTE 원문 21줄을 추출했다. 곡선은 0.05mm(A1 기준) 오차로 폴리라인화했고 로고의 해치 내부 구멍과 회색 표제란도 보존했다. 실제 데이터는 `reference-template.ts`, 재추출 도구는 `extract_template.py`이다.
원본의 841×594 기준 배치 체계를 A1/A2/A3에 맞춰 적용했다. 횡단면/측면은 좌측 상단, 두부보/기초 평면은 하단, ISOMETRIC VIEW와 상세 A는 우측 중간, KEY PLAN/NOTE/LEGEND/REFERENCE는 우측 끝, 받침 변화 치수표는 우측 하단, 원본 표제란은 최하단이다. 받침 높이·표고표는 도면 종류의 별도 보기와 DXF 모델 공간에서 유지한다.
**도곽·노트 편집 / 불러오기** 버튼에서 표제란 38개 값, 노트, 참조 도면, 문자 크기, 원본 로고/도곽 표시, 키플랜 시종점 표기를 바꿀 수 있다. `{{project}}`, `{{bridge}}`, `{{pier}}`, `{{scale}}`, `{{paper}}`를 현재 도면 값으로 치환한다. 설정은 프로젝트 이름/교량별 브라우저 저장소에 보관하며, 전체 도곽의 CAD 객체/속성 위치가 포함된 JSON으로 저장·불러올 수도 있다. 사용자 도곽은 내보낸 JSON의 프리미티브/좌표를 수정해 교체할 수 있다. **임의 DWG를 브라우저에서 직접 해석하는 기능은 아니다.** 일반 프로젝트 `.pzr` 저장 파일에 도곽 설정을 추가하는 코어 변경은 하지 않았다.
NOTE는 원본의 강도/시공 문구를 복사해 편집 가능한 참고 문서로 제공한다. 원본 fck=35MPa를 현 프로젝트의 재료값으로 자동 지정하거나 검증된 설계조건으로 간주하지 않는다. 편집창과 도면에 참고 원문임을 표시하며 현 프로젝트 조건에 맞게 수정한다. 노트 영역 초과 시 경고한다.
- **키플랜:** 현재 프로젝트에 실제 생성된 교각들의 E/N 좌표와 코핑 외곽을 사용한다. 교각 연결 방향으로 배치하고 북향 화살표도 함께 회전한다. 선택 교각을 구별하고 이름/시종점 표기를 넣는다. 연결선은 교각 중심 연결선이며, 미제공 도로 선형·교대를 추정하지 않는다.
- **아이소메트릭:** 기존 3D 부재 프리즘과 원기둥을 벡터 등각 투영한다. 네 방향 전환과 부재별 면 음영을 지원한다. 생략 옵션을 켜면 일정 단면의 긴 구간을 잘라 표시하고 ‘생략’으로 명시한다. 원래 3D 모델은 변하지 않는다.
- **해치:** 실제 외곽선으로 콘크리트 면을 채우고 버림/말뚝 등에 ANSI31 표현을 적용한다. 생략 구간의 해치는 실제로 잘라 빈 구간을 채우지 않는다. DXF에 네이티브 HATCH를 쓰고 로고 등 다중 경계는 내부 구멍을 유지한다. 레이어 및 해치 옵션으로 표시를 전환한다.
- **추가 치수:** 코핑 폭/높이와 실제 끝단 높이, 기둥 지름/다주별 높이, 기초 개별 폭/길이, 말뚝 배치 간격, 받침 끝단 여유/열간 간격을 추가했다. 기존 전고·EL·받침 간격도 유지한다. 각 값은 실제 모델의 좌표/투영점으로 계산한다. 모델에 없는 모따기/변단면을 원본처럼 보이게 만들지는 않는다.
추가 모듈은 `template.ts`, `sheet-editor.ts`, `project-views.ts`, `hatches.ts`, `component-dimensions.ts`이며 이번 변경은 전부 `도면작성` 내부이다. 원본 도곽의 편집 값과 표시 객체는 화면/SVG/출력/DXF Layout1에서 공유한다. DXF의 정면/측면/평면 모델 공간은 생략하지 않은 실치수이며, 아이소메트릭은 제목에 명시한 표시 방식의 투영 그림이다.
검증: 신규 도면 테스트 14개 통과, 샘플 교각 63개 생성/원본 불변성 확인. 타입 검사·프로덕션 빌드 통과. 1366×768/1920×1080에서 도곽/노트 수정, 프로젝트별 브라우저 저장, JSON 내보내기/불러오기, 해치 전환, 아이소메트릭 방향, 교각 변경·형상 갱신·기존 탭 전환을 확인했다. 전체 테스트는 538개 중 534개 통과하며 기존 실패 4건은 유지된다. DXF 감사 오류/수정 0건이며 네이티브 치수값과 실제 도면 객체를 재비교했다. 실제 AutoCAD 열기는 미검증이다.
## 원본 일반도 체계 보완 (2026-11-03)
기둥과 말뚝의 긴 일정 단면 구간을 생략선으로 잘라 표시한다. 기본값은 생략 표현이며 왼쪽 체크박스에서 전체 길이로 전환할 수 있다. 다주 교각은 모든 기둥에 공통되는 일정 단면 구간만 생략하므로 두부보/기초 접합부를 자르지 않는다. 실제 `PierModel`·원본 도면 객체·치수값·EL은 변경하지 않는다. 정면·측면은 자동 설정에서 동일 축척을 사용하며 기둥 생략부에는 첫 기둥의 실제 지름 A-A 단면을 함께 표시한다.
원본 일반도의 실제 표 제목과 항목을 확인했다: **받침 및 교좌면 상세 변화 치수표 / NO., A, B, C, D, S(%), 솔플레이트 크기(교축/교직), 두께 T/T1/T2**. 이 체계를 신규 일반도의 우측에 적용하고, EL.X와 h1/h2/h3/T/E는 별도 높이·표고표 보기로 제공한다. 모든 받침을 전·후열 및 실제 번호로 구분한다. 평면도에는 원형 번호와 리더를 붙인다. 상세 A는 선택한 받침과 기존 프로그램의 순서별 짝을 그리며, 좌측 받침 선택에서 다른 번호로 전환할 수 있다. 열별 개수가 달라도 짝이 없는 받침을 임의로 연결하지 않는다.
- A/B/S는 `BlockModel.plan`, 솔플 크기·두께는 기존 `solplateGeometry`에서 가져온다. T1/T2는 같은 모델의 경사 규칙을 사용한다.
- C는 전열 교좌면 축에 투영한 전·후열 중심 간격이며 전열 행에 표기한다. 꺾임과 회전은 기존 stretch와 블럭 회전을 사용한다.
- D는 **실제 일체 교좌면이 있는 경우에만** 모델의 일체 길이를 표기한다. 분리 교좌면 및 미대응 받침에 일체 길이를 만들어 넣지 않는다.
- h1=블럭 높이, h2=몰탈 높이, h3=받침 높이, E=h1+h2+h3+T, EL.X=몰탈 상면이다.
- 현재 63개 샘플에는 본체·솔플레이트 계획 제원 A/B/S가 저장되어 있지 않다. 해당 값과 파생값은 **미확인**, 비해당 C/D는 **—**로 명확히 구분한다. 원본 일반도의 700/640/1298/2500 같은 특정 교각의 값을 다른 교각에 복사하지 않는다. 모델에 실제 제원이 있을 때는 자동으로 상세와 표에 반영한다.
- 화면과 출력, DXF `Layout1`은 같은 벡터 도면 객체를 사용한다. DXF 모델 공간은 생략하지 않은 실치수 mm를 유지하고 `Layout1`은 생략 표현·상세·치수표를 용지 mm로 담는다. Layout1 치수는 실값이 적힌 표시 객체이며 모델 공간에는 네이티브 DIMENSION을 유지한다.
이번 보완 코드는 전부 `도면작성` 내부에 한정했다. 기존 탭 연결 코드에는 추가 변경을 하지 않았다.
## 실행
기존 프로그램을 실행하고 프로젝트/샘플을 연 다음 **철근배근 → 도면작성 → 재료표** 사이의 도면작성 탭을 선택한다. 교각을 고르면 생성된 형상을 즉시 표시한다. 기존 형상 입력을 수정했지만 재생성하지 않았다면 도면은 생성본을 유지하고 미반영 안내를 표시한다. 기존 화면에서 형상을 재생성한 뒤 돌아오면 새 모델을 자동 반영한다.
왼쪽에서 교각·표시 도면·용지·축척·치수/중심선/EL·레이어를 설정한다. 중앙 화면은 휠 확대·축소, 드래그 이동, 전체 맞춤, 객체 클릭 시 원본 부재 확인을 지원한다. 개별 도면 선택 시 다른 도면은 숨긴다. 상단에서 품질 검사, DXF/DWG 내보내기 및 출력 미리보기, 하단에서 SVG 저장을 실행한다. 출력은 브라우저의 인쇄/PDF 기능을 사용한다.
도면 종류에서 **상세 A**, **받침 및 교좌면 상세 변화 치수표**를 고르면 해당 부분만 크게 확인할 수 있다. 용지/축척 설정은 일반 투영도에 적용하며 상세는 자체 맞춤 축척, 치수표는 일정한 용지 글자 크기를 유지한다.
## 실제 참고 도면 분석
분석 대상은 이 폴더의 `[교각-T 형] 01 일반도_20260619.dwg`와 `02 배근도`이다. 기존 LibreDWG 변환기를 읽기 전용으로 사용해 `.cache`에 DXF를 만들고, ezdxf로 객체·레이어·치수 스타일·배치·문자를 분석했다. 원본 일반도를 렌더링해 실제 배치도 확인했다. 상세 결과와 원본 해시는 `reference-analysis.json`, 재실행 도구는 `analyze_reference.py`이다. 원본 DWG는 변경하지 않았다.
- 일반도 원본 단위는 mm(`INSUNITS=4`). 모델 공간에 치수가 다른 T형 교각 도면과 설명이 여러 개 배치되어 있다. 특정 한 교각의 치수를 템플릿으로 복제하지 않았다.
- 상단에는 횡단 정면과 측면 단면, 하단에는 코핑·기초 평면, 우측에는 상세/주기, 최하단에는 표제란을 배치하는 체계를 사용한다.
- 주요 레이어는 `CS-DIML`, `CS-DIMT`, `CS-CONC-REFR-HIDD`, `CS-XXXXCS-CENT`, `CX-BORD-*`이며 외형·숨은선·중심선·치수·문자를 구분한다. 신규 문서는 대응되는 의미별 레이어와 도곽/해치 레이어를 구분한다.
- 일반도에는 DIMENSION 150개, TEXT 575개가 있다. 주요 원본 문자 높이 300/350/360, 제목 500/600은 원본 모델 공간 값이다. 신규 문자는 용지 기준 최소 2.5mm (치수 2.8mm, EL 2.6mm)를 선택한 축척에 따라 모델 공간으로 환산한다. 특정 원본의 확대 배치 치수를 그대로 적용하지 않는다.
- 치수 스타일에 `HS_050`(dimscale=50, dimtxt=3, dimlfac=1), `1_1(typ1)` 등이 있다. 모델 형상은 실치수, 도면별 표시 축척은 별도라는 원칙을 적용한다.
- 원본에는 기둥 절단 기호, 변단면 기초 및 모따기 등 현재 생성 모델에 없는 상세가 있다. 이를 임의로 만들지 않는다.
- 초기 배치 분석에서는 Model의 A3와 Layout1의 Letter 메타데이터가 서로 달랐다. 후속 도곽 블록 분석에서 `CXBLKA1`의 841×594 비율과 `TD_HSCAL=S=1:200@A3` 속성을 직접 확인했다. 기본 A3 가로를 유지하되 새 모델의 배치 축척은 형상에 따라 자동 산정하고 사용자가 조정할 수 있다.
## 데이터 흐름과 모듈
`기존 state.project / state.active / state.piers[].input·model` → `DrawingSnapshot` → `projection.ts` → `drawing.ts` → `svg.ts / dxf.ts` → `workspace.ts`
| 파일 | 역할 |
|---|---|
| `types.ts` | mm 단위 도면 객체, 의미 레이어, 원본 부재 연결, 용지/축척 형식 |
| `projection.ts` | 기존 로컬 프레임/mitre 클리핑 재사용, 부재 프리즘·원기둥의 실제 좌표 투영과 외곽선 계산 |
| `breaks.ts` | 기둥·말뚝 공통 일정 단면 구간의 생략, 표시 좌표 변환과 생략선 |
| `bearing-detail.ts` | 실제 받침별 변화 치수표·높이 표, 선택한 교좌면의 단면·평면 상세 |
| `presentation.ts`, `sheet.ts` | 실치수 객체를 생략 표현으로 배치하고 화면/출력/DXF Layout1에서 공유 |
| `drawing.ts` | 도면 배치, 중심선, 실치수 치수선, 표고, 받침 번호/간격, 기둥 단면 |
| `svg.ts` | 동일 도면 객체의 벡터 미리보기·출력, 용지 좌표 환산 |
| `dxf.ts` | 실치수 DXF LINE/LWPOLYLINE/CIRCLE/TEXT/DIMENSION, 레이어·선종·부재 XDATA |
| `workspace.ts`, `workspace.css` | 독립 작업 화면, 설정, 선택, 줌/팬/맞춤, 내보내기/출력 |
| `drawing.test.ts` | 실제 샘플과 꺾임 형상의 기하·단위·축척·원본 불변성 검증 |
| `verify_dxf.py` | DXF 재읽기, 감사, 실치수 치수값·부재 연결 검증 |
외부 수정은 `index.html`의 탭/화면 컨테이너 추가와 `src/main.ts`의 상태 전달·탭 라우팅에 한정한다. 코어 형상/철근/재료표 계산은 수정하지 않았다.
기존 `stretchMatrix`, `mitrePlane`, `blockFramePoint`, `solplateGeometry`를 그대로 사용한다. 기존 3D 장면의 `(E-pivot.E, EL, -(N-pivot.N))` 좌표를 첫 로컬 축으로 정투영한다. 정면은 축선 방향 거리/EL, 측면은 법선 방향 거리/EL, 평면은 축선/법선 거리이다. 화면 북방향으로 임의 재회전하거나 지도 좌표를 보정하지 않는다. 기존 m 단위를 도면 mm로 환산한다. 각 객체의 교각명·부재명·인덱스는 화면 선택과 DXF `PZ_GA` XDATA로 추적한다.
T형·2주형(π형)·3주형, 꺾임/단차와 비대칭 받침은 생성된 모델의 실제 부재 배열에서 읽는다. 정투영 치수는 꺾인 축선의 펼친 길이와 다르므로 경고한다. 전체 외곽 치수에는 표시된 버림/말뚝 등도 포함된다. 주요 수직 치수는 첫 기둥 순높이, 첫 기초 두께, 코핑 전체 높이 및 전체 외곽 높이이며 다주 교각의 개별 높이는 형상과 EL을 함께 검토해야 한다.
## CAD 및 출력 기준
DXF 모델 공간은 **1:1 mm**이다. 도면끼리 겹치지 않도록 뷰별 평행이동만 수행하며 구조물 좌표를 용지 축척으로 줄이지 않는다. DIMENSION은 네이티브 치수 객체와 익명 표시 블록을 함께 저장한다. 치수값은 투영한 실제 두 점으로 산정한다. EL 문자는 원래 m 표고를 유지한다. A3/A2/A1 용지 배치는 화면/인쇄와 DXF `Layout1`에 공유한다. Layout1은 네이티브 선·폴리라인·원·문자로 작성한 용지 도면이며 모델 공간을 참조하는 연동 뷰포트는 아니다.
레이어: STRUCTURE, HIDDEN, CENTER, DIMENSION, TEXT, ELEVATION, BEARING, SECTION, HATCH, FRAME. HIDDEN은 기본 숨김이며 켜면 부재별 투영 내부 형상선을 표시한다. 완전한 부재 간 은선 제거 엔진은 아직 없다.
## 검증 (2026-11-03, 상세 수치·한계는 QUALITY_REPORT.md 참조)
- 타입 검사 통과. 프로덕션 번들 생성 통과(기존 대형 번들 경고는 유지).
- 신규 14개 테스트 통과. 샘플의 63개 교각(T형 31, 2주형 32)을 모두 투영했다. 원본 모델 불변성, mm 환산, 치수 산정, 용지/축척 변경 시 구조 좌표 불변성, 생략 전후 실제 치수 불변성, 받침 제원이 있는 경우와 없는 경우의 표/상세를 확인했다. 도곽/노트 편집과 JSON 검증, 키플랜의 실제 교각 목록, 등각 방향, 해치 절단도 검증했다. 63개 자동 배치에서 용지 범위 초과 경고는 없었다.
- 명시적인 검증 전용 60° 꺾임 T형/2주형/3주형으로 mitre 클리핑, 평면 기둥 수 및 유한 좌표를 확인했다. 검증 형상을 프로젝트 데이터로 저장하거나 사용하지 않는다.
- Chrome 1366×768 / 1920×1080 화면 검증. 도면 생성, 교각 변경, 개별 도면 선택, 확대/축소/팬/맞춤, 레이어, A2/A3 변경, DXF 다운로드, 출력 미리보기, 기존 철근/재료표/형상 탭 전환을 확인했다. 페이지 오류 0건, 가로 넘침 없음.
- 기존 코핑 길이 입력을 500mm 변경하는 브라우저 검증에서, 재생성 전에는 도면이 유지되고 미반영 안내를 표시했다. 재생성 후 도면이 자동 갱신됨을 확인했다. 검증 프로젝트는 저장하지 않았다.
- DXF 재읽기와 감사에서 오류/수정 0건. 모든 모델 공간 치수의 측정값을 원본 도면 객체와 10⁻⁶mm 허용치로 비교했다. Layout1 객체 및 부재 XDATA, 원본 도곽과 네이티브 해치를 확인하고 실제 DXF Layout1도 별도 렌더링해 검토했다. 세로 치수의 회전각 저장 위치 오류를 재읽기 검사로 찾아 수정했다. 최신 수치는 `.cache/dxf-validation-generated-general.json`에 기록한다.
- 전체 테스트 538개 중 534개 통과, 기존 실패 4개 유지: `axis.test.ts`, `bearing-coordinate-frame.test.ts`, `integrated-block.test.ts`, `validate-and-foundation.test.ts`. 다른 기능의 좌표/검증 규칙은 이번 범위에서 변경하지 않았다. 전체 통과가 아니므로 자동 커밋하지 않았다.
- 실제 AutoCAD 실행 환경이 없어 **AutoCAD에서 직접 열기 검증은 미실시**. ezdxf 감사 통과와 AutoCAD 실검증을 구분한다.
재실행(프로젝트 루트):
```powershell
npm run typecheck
npx vitest run 도면작성/drawing.test.ts
& ./pierzainer_ground/.venv/Scripts/python.exe 도면작성/verify_dxf.py
npx vite build --outDir 도면작성/.cache/build
```
분석·검증 캐시, 다운로드한 DXF와 미리보기는 `.cache/`에 모으고 Git에서 제외한다. 실행 소스에서 캐시를 읽지는 않는다.
## 후속 개발 및 한계
- 원본 도면의 정확한 선가중치/표제란/모든 문자 충돌 회피까지 재현하는 최종 납품용 정리는 추가 작업이다. 기둥·말뚝 생략선과 원형 받침 번호, 받침 상세/치수표는 구현되어 있다.
- 모델에 없는 변단면 기초·모따기·철근·받침 실형상을 추정하지 않는다. 받침 계획 제원이 없는 경우 블럭/몰탈과 중심 위치만 표시하고 누락 안내를 보여 준다.
- A-A는 첫 기둥의 일정한 원형 단면이다. 생략부에 함께 표시하고 개별 단면 보기로도 확인한다. 임의 절단 위치, 복잡한 부재 단면, 모든 원본 상세의 자동 인식은 미구현이다.
- 부재 간 완전한 가림 판정, CAD 실제 글꼴을 포함한 완전한 간섭 판정, 다주 교각의 모든 개별 높이 연쇄 치수, 모델 공간과 연동되는 DXF 뷰포트, AutoCAD 실검증은 후속 범위이다.
- 대호지교/감북교의 기존 현장타설말뚝 후속 화면 오류와 승천천교의 원본 방위각 누락 등 기존 샘플 제약은 그대로 유지한다. 도면작성 때문에 외부 기능을 수정하지 않았다.
### 아이소메트릭 높이·EL 정보
표고 EL 설정이 켜져 있으면 아이소메트릭 구조물의 실제 투영점에서 지반고, 말뚝/기초 하단, 교좌면(블럭 상면), 몰탈 상면으로 지시선을 연결한다. 기둥 높이 H는 실제 상·하단 사이 치수선(mm)으로 표시하고 EL은 m 단위다. 방향 변경 시 동일한 투영식을 사용하며 구조물 아래 정보 목록은 제거했다. 현재 각 부재 종류의 첫 부재를 지시하며 없는 부재는 지시선을 생성하지 않는다. 지반고는 입력 지반고가 첫 기둥 수직선과 만나는 높이를 가리키며 지표면 형상을 새로 생성하지 않는다. 치수/EL 표시 설정을 따르고 모델은 변경하지 않는다.
### 기초 좌표·말뚝 주석
기초 평면에는 현재 직사각형 기초의 실제 네 모서리 X(N)·Y(E)를 m 단위 소수 3자리로 지시한다. 로컬 모델점을 회전·위치변환한 좌표를 사용하며, 참고 도면의 모따기 형상·좌표를 복사하지 않는다. 횡단면도에는 기초 상단의 C-C 절단 위치 지시선이 있고 기초 평면 제목과 연결한다. 기존 A-A 기둥 단면 지시선도 유지한다. 말뚝 생략 경계의 간격을 좁혀 이중 꺾임으로 표시하며 모델 공간과 길이값은 그대로 둔다. 횡단면/측면에는 입력 말뚝 종류(없는 경우 미확인), 본수, 실제 외경, 확인 가능한 두께 및 모델 길이 범위를 지시선 주석으로 표시한다. '내부굴착복합말뚝' 등 미입력 공법명은 추정하지 않는다.
+46
View File
@@ -0,0 +1,46 @@
# 기준 일반도 재현 현황 — 2026-11-04
기준: `[교각-T 형] 01 일반도_20260619.dwg`. 원본은 보존하고, 기존 읽기 모듈로 변환된 객체를 분석했다. 상세한 객체별 색상 분포/레이어/폰트는 `reference-style-analysis.json`에 기록했다.
## 이번에 실제 반영한 변경
- CS-CONC-MAJR: ACI 4(시안), CS-DIML/CS-XXXXCS-CENT/CS-SYMB: ACI 1(빨강), CS-DIMT 및 표 문자: ACI 3(초록). 기존 임의 네이비 색상을 제거했다. 흰 종이 미리보기의 ACI 7은 검정으로 표시한다.
- DXF 레이어의 선가중치는 원본의 BYDEFAULT(-3)를 따른다. 원본 CTB가 제공되지 않았으므로 출력 굵기가 같다고 단정하지 않는다. 화면의 구별용 선굵기와 CAD의 원본 선가중치는 구분한다.
- A-ISONS01, A-HIDDN01, A-CENTR01, CENTER의 원본 패턴을 등록했다. 구조 외형/치수/기호 및 숨은선에 대응한다.
- 원본의 주된 Standard 폰트 gulim.ttc를 생성 문자에 적용했다. TB_TEXT, TB_WBTXT, ST1, 굴림체, 돋음 스타일과 원본 폰트 파일명도 등록했다. 실제 설치되지 않은 글꼴은 CAD 대체가 발생할 수 있다. 글꼴 파일을 임의 복사하지 않았다.
- 표 문자/격자는 CS-TABL-TEX2 / CS-TABL-LIN2로 분리했다. 치수 문자와 치수선도 CS-DIMT / CS-DIML로 분리한다.
- 콘크리트 면 채움에는 원본에서 다수 확인된 ACI 254의 회색(#cccccc)을 적용했다. 아이소메트릭의 면별 음영은 형상 가독성을 위해 유지했다.
- 도곽 CXBLKA1에서 추출한 실제 벡터·로고·38개 속성·NOTE 21줄과 기존 편집 기능을 유지했다. 기존 현재 모델 데이터, 치수, EL, 받침 번호 및 제원표를 사용한다.
## 아직 동일하지 않은 항목
추가 구현: 원본 HS_050의 점(_Dot) 끝기호, 치수 문자 3mm, 보조선 간격 5mm, 치수 레인 9mm를 적용했다. 실제 CZSECMU-ARW2의 선·삼각 채움·문자 좌표를 모듈화해 단면 절단선에 적용했다. 기초 평면 말뚝 중심 마크와 실제 지름·본수 지시선을 추가했다. 원본 도곽 개별 객체의 CAD 레이어·색상·선가중치를 추출해 보존한다. 형상 차이에 따른 치수 레인의 실제 위치는 자동 배치되므로 원본 좌표의 복제는 아니다. 일반/표 2.5mm 및 EL 2.6mm는 가독성 기준을 유지한다.
원본은 여러 교각과 블록/개별 객체 색상 재정의가 섞여 있어 레이어 기본색만으로 모든 객체가 같아지지는 않는다. 도곽은 PZ_CXBLKA1 BLOCK으로 재구성하고 표제란 38개 태그를 ATTDEF/ATTRIB으로 저장한다. 빈 값도 속성으로 보존하므로 CAD 속성 편집이 가능하다. 원본 로고/곡선의 허용오차 폴리라인화는 유지한다. 모델에 없는 모따기/변단면 기초/받침 제원은 미확인으로 유지한다.
## 지질주상도 확장
`geology-extension.ts`에 출처가 있는 주상도 입력과 독립 용지 영역을 받는 인터페이스를 추가했다. 데이터가 없으면 패널을 만들지 않는다. 실제 연결 시 별도 영역 또는 추가 장에 배치하고 기존 점유 영역 검사에 포함해야 한다. 현 단계에서 허구의 주상도나 빈 공간을 채우는 그림을 생성하지 않는다.
## DWG 제한
이전 내보낸 DWG가 사용자 CAD에서 열리지 않았다는 보고가 있다. LibreDWG 재읽기 통과는 실제 AutoCAD 호환성 보증이 아니다. 이번 산출물도 내부 왕복 검증과 CAD 직접 열기 검증을 구분해야 한다. 실제 사용 CAD 이름/버전/오류 문구 또는 정상 DWG writer가 확인되기 전까지 **AutoCAD에서 열림·편집 가능 검증 완료라고 판정하지 않는다.** 원본 DWG를 변환 템플릿으로 덮어쓰지 않는다.
## 검증
도면작성 테스트 14개 통과, 타입 검사 통과. 마지막 스타일 변경 후 원시 CAD 생성 테스트를 다시 통과했다. 실제 DWG 직접 열기와 CTB를 적용한 색상·굵기 인쇄 비교는 미완료다. 기존 3D/철근/재료표 코어는 수정하지 않았다.
## 2차 구현 변경 파일
`reference-symbols.ts`: 원본 단면 절단 기호. `dimension-graphics.ts`, `cad-style.ts`: 원본 점 치수 및 여백. `drawing.ts`: 실제 말뚝 중심·지름/본수 리더. `dxf.ts`: 도곽 BLOCK/ATTDEF/ATTRIB, _Dot 블록 및 네이티브 치수 참조. `extract_template.py`, `reference-template.ts`, `types.ts`, `svg.ts`: 원본 도곽 개별 객체의 CAD 색상/레이어 보존. `template.ts`: 빈 표제란 필드 보존. `quality-layout.ts`: 간섭 검사 캐시. `dwg_export.py`, `verify_dxf.py`: BLOCK/ATTRIB 내용과 치수 점 기호 재읽기 검사.
표제란 38개 속성은 CAD 원시 객체 재읽기로 확인한다. DWG 변환 후에도 블록 정의·속성값·삽입 좌표가 유지되는지 검사한다. 원본 값이 없는 시공 상세·받침 제원 및 지질주상도는 생성하지 않는다. 사용 CAD에서의 직접 열기 검증은 별도 미완료 항목이다.
최종 검사에서 PZ_CXBLKA1의 ATTDEF/ATTRIB 38개, 빈 속성 유지, Paper Space 속성 소유 관계, 네이티브 DIMENSION의 _Dot 참조를 추가 확인했다. 원본 블록 좌표와 별도로 실제 치수와 표고는 현재 모델을 유지한다. 치수 문자 확대/절단 기호 때문에 달라진 점유 영역은 자동 축척·배치에 반영한다. 전체 일괄 투영 검사는 BSP 면 가림 처리와 병행 테스트 시간을 고려해 120초 한도로 실행한다.
실제 출력: `output/reference-parity-attributes/운곡교_P1_일반도.dwg`. 원본 모델 49개 네이티브 치수와 블록/속성의 왕복 일치, DXF 감사 오류/수정 0건을 확인했다. DWG 재읽기에서는 기존 재질 사전 placeholder 3개 정리 경고가 유지되므로 무결한 AutoCAD 출력으로 단정하지 않는다.
최종 회귀 검사: 도면작성 14개 통과, 샘플 교각 63개 간섭 0건/원본 투영 형상 변경 0건. 전체 538개 중 534개 통과, 기존 코어 실패 4건(axis, bearing-coordinate-frame, integrated-block, validate-and-foundation)은 유지한다. 타입 검사·빌드 통과. 커밋하지 않았다. 실제 AutoCAD 열기/CTB 출력 검사는 미실시다.
Loaded 100 of 2645 files, more files were not shown because too many files have changed in this diff. Show more