diff --git a/.gitignore b/.gitignore
index d70bb9c..80fc43f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ node_modules
dist
.DS_Store
*.local
+
+docs/extracted/
diff --git a/docs/도면분석.md b/docs/도면분석.md
index 7060c27..be400e2 100644
--- a/docs/도면분석.md
+++ b/docs/도면분석.md
@@ -1,71 +1,57 @@
-# 광터지하차도 BLOCK7 원본 도면 1차 분석
+# BLOCK7 원본 재분석 및 모델 적용 근거
-## 분석 목적과 파일
+원본: `public/drawings/광터지하차도_일반도(BLOCK7).dxf`. 분석 재현: `python3 scripts/analyze_dxf.py`. 이전 문서의 가정과 사용자 지시는 이번 모델의 확정 근거로 사용하지 않는다.
-이번 프로젝트는 기존 3D 생성 코드를 가져오지 않고 원본 DXF에서 다시 시작한다. 기존 프로젝트의 분석은 참고 문서(`참고_기존도면분석.md`)에 보관하며, 아래 값은 새 프로젝트에서 원본 DXF를 다시 읽어 확인한 결과다.
+## 원본과 도면 연결
-| 항목 | 확인 결과 | 상태 |
-|---|---|---|
-| 원본 | `public/drawings/광터지하차도_일반도(BLOCK7).dxf` | 보유 |
-| 형식/문자 코드 | DXF AC1027, ANSI_949 | 파일 헤더에서 확인 |
-| `$INSUNITS` | 0 (미지정) | 단위 미확정. 도면 치수·측점·표고와 대조해 환산 필요 |
-| 도면 구성 | 일반도 13번·14번에 평면·상부평면·종단면·단면·상세 포함 | 제목 문자에서 확인 |
+- AC1027 DXF이며 본문 문자는 UTF-8로 정상 판독된다. 헤더의 ANSI_949와 달리 CP949로 읽으면 한글이 깨진다. `$INSUNITS=0`은 단위 미지정이다.
+- 일반도 13: 평면·상부평면·종단 S=1:100. 일반도 14: A-A/B-B S=1:100, 측구 S=1:30, 시공이음 등 S=1:20. 축척은 표현 축척이며 모델 좌표에 100을 다시 곱하지 않는다.
+- 14번 단면 폭 치수열 800+8.750+1.000+8.750+800과 구조선 X 차 20100을 대조하면 CAD 1000단위=1m가 된다. 상·하단 EL 차와 Y 차도 같은 환산에 일치한다.
+- A-A/B-B는 제목·상단 EL·하단 EL·동일 X 치수열로 대응시켰다. 단면 측점은 299.000/319.000, 종단면 구조 경계 측점은 299.010/318.990이다. 각각 10mm 어긋나며 신축이음 등 경계 기준의 차이인지 확정되지 않았다.
+- 평면의 BLOCK7 표기 주변 CS-CONC-MAJR 선 21835/21836은 각각 20.100m이다. 단면 폭도 20.100m여서 이전 문서의 종방향 중심선 판단은 근거가 부족하다. 선형중심 문자는 (1069233.4121,-81231.4642), (1034233.4121,-81231.4642)에 있으나 대응 엔티티와 절단선은 미확정이다. 이 후보 방향을 모델 중심선으로 적용하지 않는다.
+- 상부평면과 평면을 포함한 원본 검토 SVG를 제공한다. SVG는 LINE·TEXT·공동구 LINE 발췌이며 전체 CAD 렌더러가 아니다. 다른 블록·해칭·폴리라인·기호는 원본 DXF에서 확인해야 한다.
-## 시트와 축척
+## 적용값 표
-| 도면 | 표기 축척 | 사용할 근거 |
-|---|---:|---|
-| 일반도 13번 평면도 | 1:100 | 평면 배치와 BLOCK7 선형 후보 확인 |
-| 일반도 13번 상부평면도 | 1:100 | 상부 외곽과 평면 배치의 교차 확인 |
-| 일반도 13번 종단면도 | 1:100 | 측점, 바닥고, 벽체 상단 EL의 길이방향 기준 |
-| 일반도 14번 단면 A-A | 1:100 | 시점 단면의 구조 윤곽·치수·EL |
-| 일반도 14번 단면 B-B | 1:100 | 종점 단면의 구조 윤곽·치수·EL |
-| 일반도 14번 측구 상세 | 1:30 | 측구 단면과 표기 치수 |
-| 일반도 14번 기타 상세 | 1:20 표기 확인 | BLOCK7 적용 대상인지 상세 위치로 대조 전까지 모델에 추가하지 않음 |
+| 대상 | 값 | 상태 | 도면 번호·도면 내 위치와 근거 |
+|---|---|---|---|
+| 구조 경계 측점 | 299.010 / 318.990m | 도면 명시값 | 13 종단, 문자 (1050295.169,-97151.037)/(1073692.169,-97151.037) |
+| 경계 사이 거리 | 19.980m | 계산으로 도출한 값 | 위 두 측점 차; 단면 연결에 사용할 경우 배치 확인 필요 |
+| 단면 자체 측점 | 299.000 / 319.000m | 도면 명시값 | 14 A-A/B-B 중앙 STA 문자; 종단 경계와 10mm 차이 |
+| 단면 원점 X | 1128486.218066491 | 계산으로 도출한 값 | 14 폭 치수 20.100의 양끝 중앙 |
+| A-A 상단/하단 | EL 106.675 / 101.636m | 도면 명시값 | 14 A-A 좌측 EL 문자 |
+| B-B 상단/하단 | EL 107.307 / 101.069m | 도면 명시값 | 14 B-B 좌측 EL 문자 |
+| 상단 CAD Y | A -74271.37855593952 / B -91211.37855593952 | 계산으로 도출한 값 | 14 두 벽체 상단 수평선과 EL 표기의 대응 |
+| 벽체 전체 높이 | A 5.039m / B 6.238m | 계산으로 도출한 값 | 각 상·하단 차; 바닥판을 포함한 높이 |
+| 종단 상면 표기 | A 102.436 / B 101.869m | 도면 명시값 | 13 종단 좌우 EL 문자 |
+| 상면-하단 차 | 0.800m | 계산으로 도출한 값 | 위 종단 EL과 14 하단 EL 차 |
+| 폭 치수열 | 0.800+8.750+1.000+8.750+0.800=20.100m | 도면 명시값 | 14 단면 아래 치수열 |
+| 가장 바깥 보조선 폭 | 20.300m | 계산으로 도출한 값 | 14 좌표 측정 X=중심±10150; 표기 폭 20.100과 기준이 다름 |
+| 횡단경사 | 양쪽 바깥으로 -2% | 도면 명시값・계산으로 도출한 값 | 14 S=-2.000% 및 포장선 X ±0.5→±7.75에서 Y 145mm 하강 / 7250mm |
+| 단면 중심 기준 EL | 미확정 | 확인 필요 | 벽체 상단 EL을 도로 중심 EL로 대체하지 않음 |
+| 공동구·상부 상세 | 폭 경계 1.000m, 전체 높이 경계 2.100m | 계산으로 도출한 값 | 14 블록 A$C2017021411375356, 로컬 X -500~500/Y -900~1200. 좌표 측정이며 명시 치수 아님 |
+| 공동구 좌우 위치 | 중심에서 ±8.750m | 계산으로 도출한 값 | 14 네 INSERT의 X 좌표, 우측은 X반전 |
+| 공동구 기준 Y | A -77447.89255584464 / B -95669.3155558447 | 계산으로 도출한 값 | 14 INSERT 원점. 선·반원 전체를 원형대로 변환 |
+| 측구 단면 내부 폭 | 0.400m | 계산으로 도출한 값 | 14 단면 CS-CONC-MAJR X ±7.8~±8.2 좌표 측정 |
+| 측구 상세 표기 | 50 / 200 등 | 도면 명시값 | 14 측구 상세, S=1:30。단면 내 깊이와 같다고 단정하지 않음 |
+| 평면 중심선·단면 방향 | 미확정 | 확인 필요 | 13 평면·상부평면의 절단 위치와 엔티티 대응 미완료 |
+| 중간 단면·연장 | 미확정 | 확인 필요 | 두 단면 사이 선형 보간은 별도 가정, 실제 중간 형상 검증 안 됨 |
-## 기준 측점·표고·치수
+## 구조선 대조
-| 대상/값 | 값 | 상태 | 근거와 위치 |
-|---|---:|---|---|
-| A-A 측점 | STA. 0K+299.010 | 도면 명시값 | 일반도 13번 종단면도 시작 측점 및 14번 A-A 대응 |
-| B-B 측점 | STA. 0K+318.990 | 도면 명시값 | 일반도 13번 종단면도 끝 측점 및 14번 B-B 대응 |
-| A-A~B-B 길이 | 19.980m | 계산으로 도출한 값 | 두 측점 차 `318.990 - 299.010` |
-| A-A 벽체 상단/하단 | EL. 106.675 / 101.636 | 도면 명시값 | 일반도 14번 단면 A-A |
-| B-B 벽체 상단/하단 | EL. 107.307 / 101.069 | 도면 명시값 | 일반도 14번 단면 B-B |
-| A-A 벽체 높이 | 5.039m | 계산으로 도출한 값 | 상·하단 EL 차 |
-| B-B 벽체 높이 | 6.238m | 계산으로 도출한 값 | 상·하단 EL 차 |
-| 단면 주요 폭 치수열 | 0.800 + 8.750 + 1.000 + 8.750 + 0.800 = 20.100m | 도면 표기값·계산 | 일반도 14번 A-A/B-B 폭 치수. 20.100m가 가장 바깥 CAD 선의 전체 폭과 같은 기준인지는 추가 대조 필요 |
-| 종단 별도 구간 표기 | L=19.882m 등 | 확인 필요 | 측점 차 19.980m와 다른 기준점 또는 구간일 수 있어 A-A~B-B 모델 길이로 사용하지 않음 |
-| 횡단 경사 | S=-2.000% | 도면 명시값 | 평면·단면 주변 주기. 낮아지는 방향은 도면 방향과 좌표를 교차 확인해야 함 |
-| 측구 표기 치수 | 50, 200 (도면 단위 확인 필요) | 도면 명시값/단위 확인 필요 | 일반도 14번 측구 상세. 상세 축척으로 잰 값과 별도 관리 |
+각 단면의 X 1118000~1139000, 상단 Y부터 아래 7500·위 100 범위에 양 끝이 들어오는 LINE 중 두 레이어를 추출했다. 이 범위는 주요 단면과 하부 100mm 보조 윤곽을 포함한다.
-## 같은 구조물을 도면 간 연결하는 방법
+- A-A/B-B 각각 MAJR 108개 + MINR 6개 = 114개. 기존 90+6 기록에는 하부·벽체 등의 선이 빠져 있었다.
+- 각 대응 선의 레이어와 X 끝점이 일치한다. 원본 HANDLE을 모델에 저장해 다시 찾을 수 있다.
+- 포장 반복선·겹친 선도 삭제하지 않는다. 반복선을 모두 실제 콘크리트 표면으로 판단하지 않으며 기본 보기에서는 끌 수 있는 별도 그룹이다.
+- 좌우 공동구·상부 상세는 블록당 LINE 18개+ARC 2개. 우측 X반전, 각 단면 INSERT Y를 적용한다. 반원은 36분할 표시하므로 현 높이 오차는 약 0.03mm 이하이다. 임의 100mm 벽두께를 만들지 않는다.
+- 원본 선 기반 객체 154개(단면 LINE 114쌍, 블록 엔티티 20쌍×좌우). 부재 체적이 아니라 대응 윤곽 객체의 수이다.
-- 평면도에서 BLOCK7 표기와 종방향 구조 윤곽 및 단면 절단 표식을 식별한다.
-- 평면도 A-A/B-B 위치를 종단면도의 STA. 0K+299.010 / 0K+318.990과 대조한다.
-- 해당 측점에 일반도 14번 단면 A-A/B-B를 대응시키고, 상·하단 EL 및 폭 치수를 확인한다.
-- 측구 상세는 단면 하부의 측구 후보와 위치·폭·깊이·방향을 비교한 뒤 적용 범위를 확정한다.
-- `CS-CONC-MAJR`, `CS-CONC-MINR`는 모델 윤곽 후보 레이어다. 해당 레이어의 선 전부를 구조 경계로 단정하지 않고, 치수선·해칭·중복선 여부를 시트 화면 및 주변 치수와 구분한다.
+## 확인 항목과 모델 제한
-## 평면 선형 1차 판독
-
-평면도에서 `(BLOCK7)` 표기와 폭 치수열 부근의 `CS-CONC-MAJR` 종방향 선 후보를 찾았다. 후보 끝 좌표는 A-A측 `(1055963.4484, -71293.2356)`, B-B측 `(1056081.6830, -91392.8878)`이고, CAD 좌표 길이는 약 20.100m다. 종단 측점 차 19.980m보다 0.120m 길다.
-
-이 선은 DXF에서 직선 엔티티로 저장되어 있고, 이 구간을 따라가는 중간 원호나 추가 곡선점을 아직 확인하지 못했다. 따라서 이 선을 실제 평면 선형 중심선으로 확정하지 않는다. 별도의 `선형중심` 문자도 있으나, BLOCK7 구간 후보 및 A-A/B-B 절단 위치와 같은 선을 가리키는지 도면 화면에서 대조가 필요하다. 기존 프로젝트의 모델 방향을 그대로 복사하지 않고 이 대응을 새로 확인한다.
-
-## 정보 부족·오류 후보
-
-- DXF 헤더 단위는 미지정이므로 CAD 좌표를 실제 단위로 그대로 간주할 수 없다.
-- 기준선 후보의 길이 20.100m와 측점 차 19.980m가 다르다. 기준점, 선의 종류, 치수 보조선 끝을 확인해야 한다.
-- 단면 좌표 범위에는 주 구조선·반복선·상세선이 함께 있어 좌표창만으로 선 개수를 확정할 수 없다. A-A/B-B별 엔티티를 도면 창 기준으로 정밀하게 선별하고 중복·해칭을 판별해야 한다.
-- 벽체 상단 EL이 종단면 시작·끝과 단면 A-A/B-B에서 일치하는지 표기 위치별로 최종 대조한다.
-- 0.800m 치수의 부재 기준, 공동구 외곽의 상세 적용, 측구의 BLOCK7 구간 연장 범위는 추가 확인이 필요하다.
-- 평면 중심선의 곡률 자료가 원본에 없거나 판독되지 않으면 임의 곡선을 만들지 않고 사용자 확인 대상으로 둔다.
-
-## 다음 모델링 단계
-
-1. 평면도에서 BLOCK7 중심선 후보와 A-A/B-B 절단 위치를 화면 및 DXF 엔티티로 확정한다.
-2. 단면 A-A/B-B에서 두 콘크리트 레이어를 부재·해칭별로 나누고 실제 윤곽과 치수열을 대조한다.
-3. 치수와 근거가 연결된 주요 단면 형상부터 별도 객체로 생성한다.
-4. 중간 구간 정보가 없는 표면은 추정 여부를 표시하고, 닫힌 체적처럼 오해되지 않게 한다.
-5. 주요 치수·표고·부재 배치를 원도면과 대조하고 결과를 기록한다.
+1. 평면 선형중심에 해당하는 엔티티·A/B 절단 위치·중심 EL이 미확정이다. 확인되지 않은 위치·곡률을 도면 확정값으로 만들지 않는다.
+2. 단면 자체 측점 20.000m 간격과 종단 구조 경계 19.980m 차이의 원인을 확인해야 한다.
+3. 종단 벽체 상단 EL 106.675/107.307은 단면 상단 표기와 각각 일치한다. 도로 중심 EL과의 일치는 검증하지 못했다.
+4. 포장 반복선, 하부·외측 보조선의 재료와 체적 경계 해석은 미검증이다. 닫힌 체적·수량 산출용 모델로 해석하지 않는다.
+5. 측구 상세 S=1:30은 별도 검토 자료로 제공한다. 단면 측구와의 깊이·시종점 적용은 미확정이며 상세 치수로 단면을 임의 교체하지 않는다.
+6. 철근, 지반, 되메우기, 이음 상세의 구간 적용, 재료·정착 상세는 모델링하지 않았다. 공동구 블록의 가는 상부 윤곽은 존재하는 그대로 보존하며 근거 없는 금속 난간을 추가하지 않는다.
diff --git a/public/analysis/단면A.svg b/public/analysis/단면A.svg
new file mode 100644
index 0000000..c3cdf41
--- /dev/null
+++ b/public/analysis/단면A.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/analysis/단면B.svg b/public/analysis/단면B.svg
new file mode 100644
index 0000000..1413028
--- /dev/null
+++ b/public/analysis/단면B.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/analysis/종단검토.svg b/public/analysis/종단검토.svg
new file mode 100644
index 0000000..0b25c43
--- /dev/null
+++ b/public/analysis/종단검토.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/analysis/측구상세.svg b/public/analysis/측구상세.svg
new file mode 100644
index 0000000..2d63d4a
--- /dev/null
+++ b/public/analysis/측구상세.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/analysis/평면검토.svg b/public/analysis/평면검토.svg
new file mode 100644
index 0000000..32b45b9
--- /dev/null
+++ b/public/analysis/평면검토.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/analyze_dxf.py b/scripts/analyze_dxf.py
new file mode 100644
index 0000000..0b50134
--- /dev/null
+++ b/scripts/analyze_dxf.py
@@ -0,0 +1,95 @@
+import json,pathlib,collections,math
+ROOT=pathlib.Path(__file__).resolve().parents[1]
+raw=(ROOT/'public/drawings/광터지하차도_일반도(BLOCK7).dxf').read_text(encoding='utf8',errors='replace').splitlines()
+pairs=[(int(raw[i].strip()),raw[i+1].strip()) for i in range(0,len(raw)-1,2)]
+entities=[]; current=None; section=''; block=None; blocks={}
+for code,value in pairs:
+ if code==0:
+ if current:
+ if section=='ENTITIES': entities.append(current)
+ elif section=='BLOCKS' and block and current['type'] not in ('BLOCK','ENDBLK'): blocks.setdefault(block,[]).append(current)
+ current={'type':value,'data':[]}
+ if value=='ENDBLK': block=None
+ elif current:
+ current['data'].append((code,value))
+ if current['type']=='SECTION' and code==2: section=value
+ if current['type']=='BLOCK' and code==2: block=value
+
+def get(e,c,d=None): return next((v for k,v in e['data'] if k==c),d)
+def point(e,c=10): return [float(get(e,c,0)),float(get(e,c+10,0))]
+def layer(e): return get(e,8,'')
+out=ROOT/'docs/extracted';out.mkdir(exist_ok=True)
+texts=[{'text':get(e,1,''),'p':point(e),'layer':layer(e)} for e in entities if e['type'] in ('TEXT','MTEXT')]
+lines=[{'a':point(e),'b':point(e,11),'layer':layer(e),'handle':get(e,5)} for e in entities if e['type']=='LINE']
+for name,data in [('texts',texts),('lines',lines),('blocks',blocks),('inserts',[{'name':get(e,2),'p':point(e),'sx':get(e,41,'1'),'sy':get(e,42,'1'),'rotation':get(e,50,'0'),'layer':layer(e)} for e in entities if e['type']=='INSERT'])]:
+ (out/(name+'.json')).write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf8')
+print('Entities',collections.Counter(e['type'] for e in entities))
+for t in texts:
+ if any(s in t['text'] for s in ('BLOCK7','106.675','107.307','101.636','101.069','102.436','101.869','299.010','318.990','선형','측구','20.100')): print(t)
+for name,top in [('A',-74271),('B',-91211)]:
+ selected=[l for l in lines if l['layer'] in ('CS-CONC-MAJR','CS-CONC-MINR') and all(1118000
{l["handle"]} / {l["layer"]}')
+ for t in texts:
+ x,y=t['p']
+ if x0{html.escape(t["text"])}')
+ # 공동구 삽입 블록의 실제 선 포함
+ for e in entities:
+ if e['type']=='INSERT' and get(e,2)==ductName:
+ x,y=point(e);sx=float(get(e,41,1));sy=float(get(e,42,1))
+ if x0')
+ svg=f''
+ (previewDir/(title+'.svg')).write_text(svg,encoding='utf8')
+print('모델 추출:',len(parts),'객체. 단면 원본 선 114쌍, 공동구 20쌍 × 좌우.')