3 Commits
Author SHA1 Message Date
eomsangdon 7beca06a06 Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_2' into sub_laptop_3 2026-10-29 20:13:28 +09:00
eomsangdonandClaude Opus 5.5 41392b7e1c chore(계획): 요구 447 분석 — B04 · B05 지도 겹 어긋남(수렴각 회전) · PLAN 589-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:13:20 +09:00
eomsangdonandClaude Opus 5.5 99db748a7b feat(D03): 노선분석 위성 조각 서버 기억 — 받은 조각 _derived/plan_tile 에 두고 다시 안 받음 (PLAN 586-2 ④ · 요구 448)
- GET …/plan/tile/{z}/{x}/{y} = 프로젝트 _derived/plan_tile/Satellite/{z}/{x}_{y}.jpeg 먼저 · 없으면 브이월드에서 받아 원자 쓰기
- 프로젝트 실경로 기억(조각마다 DB 안 부름)
- 시험: test_586_tm_frame 두 번 받기 = 브이월드 한 번 · 반쯤 쓴 조각 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 20:03:35 +09:00
3 changed files with 217 additions and 5 deletions
+36 -5
View File
@@ -3,7 +3,7 @@
- `GET /api/projects/{id}/plan/terrain/extent` — 받기 전 범위(상자 · 도엽 · 칸 수 · 받은 범위)
- `POST …/plan/terrain` — 사업 전체 지형 한 벌 받기(뒤에서 · 누를 때만) · 끝나면 `surface_models` 행
- `GET …/plan/progress` — 일의 진행률
- `GET …/plan/tile/{z}/{x}/{y}` — 위성 조각 대리(586-1 GL 바탕 시험)
- `GET …/plan/tile/{z}/{x}/{y}` — 위성 조각 대리 · 서버 기억(586 GL 바탕)
- `GET …/plan/layers/sheet?key=` · `…/cadastral` · `…/raster?kind=slope|hillshade|landslide` ·
`…/zones` · `…/soils` · `…/bands` · `…/lines?kind=` · `…/zero-order` ·
`…/forest-roads`(GET · POST 올리기) · `…/landslide-zones` — 지도 겹
@@ -21,7 +21,7 @@ import json
import logging
from pathlib import Path
from typing import Any
from uuid import UUID
from uuid import UUID, uuid4
from fastapi import APIRouter, File, HTTPException, Request, Response, UploadFile
from pydantic import BaseModel
@@ -350,22 +350,53 @@ async def plan_layer_landslide_zones(project_id: UUID) -> dict[str, Any]:
return {"status": "success", **value}
# 위성 조각 기억 — 프로젝트 → 실경로(조각마다 DB 를 안 부름)
_TILE_ROOTS: dict[UUID, Path] = {}
TILE_KIND = "plan_tile"
async def _tile_root(project_id: UUID) -> Path:
root = _TILE_ROOTS.get(project_id)
if root is None:
async with get_db_pool().acquire() as connection:
stored = await get_project_storage_relative_path(connection, project_id)
if not stored:
raise HTTPException(status_code=404, detail="프로젝트 저장 경로를 찾을 수 없습니다.")
root = _TILE_ROOTS[project_id] = Path(resolve_stored_project_path(stored))
return root
@router.get("/{project_id}/plan/tile/{z}/{x}/{y}", response_model=None)
async def plan_satellite_tile(project_id: UUID, z: int, x: int, y: int) -> Response:
"""위성 조각(PLAN 586-1 GL 바탕 시험 · 브이월드 Satellite z6~19) — 키가 화면에 안 새게 대리."""
"""위성 조각(PLAN 586 GL 바탕 · 브이월드 Satellite z6~19) — 키가 화면에 안 새게 대리 ·
받은 조각 = `_derived/plan_tile/Satellite/{z}/{x}_{y}.jpeg`(지문 = 겹 · 조각 번호 ·
지워도 다시 받음)."""
import urllib.request
from B04_PreProcess.B04_PreProcess_Engine_VWorld import VWORLD_API_KEY
from common_util.common_util_derived_store import derived_dir
_ = project_id
if not (6 <= z <= 19 and 0 <= x < 2**z and 0 <= y < 2**z):
raise HTTPException(status_code=400, detail="조각 번호가 범위를 벗어났습니다.")
path = (
derived_dir(await _tile_root(project_id), TILE_KIND)
/ "Satellite"
/ str(z)
/ f"{x}_{y}.jpeg"
)
url = f"http://api.vworld.kr/req/wmts/1.0.0/{VWORLD_API_KEY}/Satellite/{z}/{y}/{x}.jpeg"
def fetch() -> bytes:
if path.is_file():
return path.read_bytes()
request = urllib.request.Request(url, headers={"User-Agent": "Aislo/1.0"})
with urllib.request.urlopen(request, timeout=8) as response:
return response.read()
tile = response.read()
path.parent.mkdir(parents=True, exist_ok=True)
part = path.with_suffix(f".{uuid4().hex}.part")
part.write_bytes(tile)
part.replace(path) # 반쯤 쓴 조각을 남기지 않음
return tile
try:
tile = await asyncio.to_thread(fetch)
@@ -0,0 +1,149 @@
# 447 분석 — B04 · B05 지도 겹 어긋남(「같은 직사각형」 가정)
조사만 · 코드 · git · storage · DB 손대지 않음. 셈 = `tmp/brain_447/calc_447.py`(겹마다 화면 식 그대로 · pyproj 정답) · `calc_447_fix.py`(고친 뒤 나머지) · `calc_447_open.py`(처음 화면 · 크게 당김) · 결과 글 `calc_447_out.txt`.
맞춤 확인: 같은 식으로 검증_sub2_D 상자 귀퉁이 = **63.62 m** → 586-0 실측 63.6 m 와 같음(셈 틀 맞음).
## 0. 한눈에
| 화면 | 겹(좌표계) | 「같은 직사각형」 가정 | 어긋남(B 검증 다섯 = 용화) | D 지역 크기였다면 |
| --- | --- | --- | --- | --- |
| B04 하단 지도 | 위성 · 혼합 · 백지도 그림(웹 메르카토르 타일) · 지적 · 행정 · 도엽(경위도) · 계획선 · 측점 · 흐름 강도 · 관(TM) · 유역 · 분수령(서버가 TM→경위도) | 있음 — 경위도 ↔ TM · 그림 ↔ 경위도(contain 비율) | 노선↔도엽 귀퉁이 3.9 m · 위성↔도엽 7.5 m · 노선 자리 위성↔노선 5.2 m | 64 m · 71 m |
| B05 평면 노선 편집 | 도엽 하천중심선 등 · 도엽 등고선(LAS 없을 때)(경위도) · 노선 · LAS 등고선 · 구조물(TM) · 위성 없음 | 있음 — 경위도 ↔ TM | 귀퉁이 3.9 m · 노선 자리 1.6 m | 64 m |
| B05 배수유역도 | 위성 그림(기본 꺼짐) · 도엽 · 유역 채움 · 분수령 · 상류 세류(경위도) · 노선 · 관 · 강도 · 화살표 · 구간 띠 · 측점(TM) | 있음 — B04 와 같음 | B04 와 같음 | B04 와 같음 |
- 「어긋남 0」 자리 = 그림 상자 왼위 ↔ 오른아래 대각선(경위도 ↔ TM) · 그림 상자 세로 가운데 줄(위성 ↔ 경위도). 둘 다 가운데를 지남 → 「가운데는 맞고 가장자리만 어긋남」.
- 저장 좌표 영향 없음(지금도) — 누름 → TM · 노선 · 관 · 구조물 그리기가 모두 TM 한 길(어긋나 보이는 것은 경위도 · 위성 쪽).
## 1. 화면별 겹 · 좌표계 · 화면 식
공용 식(B04_PreProcess_UI_MapRender.ts):
- `computeMapRect` 117~131 — 뷰포트 안에 **TM 상자 비율**(`width_meters/height_meters`)로 맞춘 네모.
- `normalizedToScreen` 324~332 · `affineOf` 313 — 0~1 정규화 → 화면(배율 · 옮김).
- 경위도 길 G — `createNormalizer` 107~115 · `lonLatToScreen` 335~346 · `_Prepare.ts` `projectRing` 59~67 · `labelAnchorOf` 157~172: `n = ((lon−lon_min)/lonR, 1−(lat−lat_min)/latR)`.
- TM 길 T — `metricToScreen` 352~361 · `createMetricProjector` 141~159 · `_Prepare.ts` `prepareMetricPolyline(s)` 252 · 298: `n = ((x−x_min)/W, 1−(y−y_min)/H)` — 주석 그대로 「meta의 x/y 범위와 lon/lat 범위가 같은 사각형」.
- 상자 메타 S·G·T 공통 — 서버 `B04_PreProcess_Engine_VWorld.py` 167~191: 타일 상자의 **왼위(lon_left, lat_top) · 오른아래(lon_right, lat_bottom) 두 귀퉁이만** pyproj 로 TM → `x_min · y_max · x_max · y_min`. 나머지 두 귀퉁이는 맞추지 않음.
- 위성 그림 S — 같은 파일 117~160: VWorld WMTS(EPSG:3857 타일) 이어 붙임 → 그림 가로 = 경도 선형 · 세로 = **메르카토르 y 선형**(위도 선형 아님).
### B04 하단 지도(`B04_PreProcess_UI_MapViewer.ts`)
- 위성 · 혼합 · 백지도: `<img>` 105~111 · 주소 511 · `updateImageTransform` 311~318(CSS translate · scale) · CSS `B04_PreProcess_UI_Style_Map.css` 80~83 `object-fit: contain` → 그림은 **그림 px 비율**(2560×3328)로 놓임 · 캔버스 겹은 **TM 상자 비율** 네모(mapRect) — 두 네모 크기가 다름.
- 지적 · 행정 · 등고선 · 도엽_*(CRS84 경위도 GeoJSON): 454 · 507 `prepareLayer(data, normalizer)` → G.
- 계획선(TM): 493~494 `prepareMetricPolyline` → T · 측점 눈금 421~428 `createMetricProjector` → T.
- 흐름 강도 `B04_PreProcess_UI_FlowStrength.ts` 157 · 329 → T · 고른 고리 207 → G.
- 세부 유역 `B04_PreProcess_UI_Basins.ts` 관 242 · 266 → T · 유역 채움 287~290 · 501~507 → G(`Router_Basins.py` 399 서버가 TM 경계를 pyproj 로 경위도로 바꿔 줌).
- 배수유역 관리 겹 `B04_PreProcess_UI_Watershed.ts` 165~181 · 200~217 · 385~406 · 419~436 — G 식을 손으로 네 번 베낌 · TM 1 m 격자를 `bbox_lonlat` 의 경위도 최소 · 최대 직사각형에 폄(격자도 회전분만큼 어긋남).
- 「같은 직사각형」: G ↔ T(회전 · 사다리꼴) · S ↔ G(contain 비율 · 메르카토르 세로).
### B05 평면 노선 편집(`B05_Profile_UI_RouteEdit*.ts`)
- 메타 = 위성 메타(`_Drainage_Parts.ts` 464) · 위성 그림 없음.
- 도엽 겹(하천중심선 등) `_RouteEdit_Contour.ts` 195~199 · 도엽 등고선(LAS 없을 때) 111 → G.
- LAS 등고선 101 · 104 `prepareMetricPolylines` → T · 노선 · 구조물 `_RouteEdit.ts` 306~310 `metricToScreen` · `screenToMetric` → T · 추천 노선 `_Recommend_Map.ts` 143 → T.
- `sheetCoversBand` `_RouteEdit_Contour.ts` 117~ — TM → 경위도를 같은 직사각형으로(받을 범위 판정만 · 띠 여유 안).
- 「같은 직사각형」: G(도엽) ↔ T(노선 · LAS 등고선). 주석(09-12 ⑥) 「도엽 등고선과 LAS 등고선은 서로 어긋난다」 중 일부가 이 흠일 수 있음(아래 5).
### B05 배수유역도(`B05_Profile_UI_Drainage*`)
- 위성: `_Drainage_Chrome.ts` 91~93 · `_Drainage_Panel.ts` 188~194 · 485 · CSS `B05_Profile_UI_Style_Drainage.css` 122~127 `object-fit: contain` → S(B04 와 같은 흠).
- 도엽 `_Drainage_Panel.ts` 477~482 → G · 유역 채움 · 분수령 · 상류 세류 `_Drainage_Render.ts` 85~131 · 고르기 `_Drainage_Interact.ts` 143~152 → G.
- 노선 426 · 486 `prepareMetricPolyline` → T · 관 `_Drainage_Pipes.ts` 96~130(제 식 · 같은 직사각형) → T · 강도 · 구간 띠 · 화살표 `_Drainage_Render.ts` 139~ → T · 확대 한도 `_Drainage_ZoomRange.ts` 53~57 G 식 베낌.
- 「같은 직사각형」: B04 와 같음.
## 2. 어긋남 크기(셈)
검증 프로젝트는 땅이 둘뿐 — B 다섯(랩탑메인 · sub1~4) = 용화 한 상자 · D 다섯 = 같은 D 상자. 큰 상자 둘(가이드_예정노선 · 4250c7bc)도 함께.
| 땅 | 좌표계 | 중앙 자오선과 경도 차 | 수렴각 γ | 상자 | 그림 px |
| --- | --- | --- | --- | --- | --- |
| B 용화(B 다섯) | Korean 1985 Modified East Belt(Bessel · TOWGS84 · EPSG 번호 없음 = 5176 꼴) | 129.0866 − 129.0029 = 0.084° | 0.050° | 2454 × 3170 m | 2560×3328 |
| D(D 다섯) | EPSG:5186 | 127.9495 − 127 = 0.950° | 0.562° | 5462 × 3386 m | 2816×1792 |
| 가이드_예정노선 | EPSG:5186 | 0.958° | 0.567° | 7956 × 5819 m | 4096×3072 |
상자 통째(같은 땅 한 점이 두 겹에서 벌어진 거리 · m):
| 땅 | 노선↔도엽 T-G 귀퉁이(오른위 · 왼아래) | 위성↔도엽 S-G 좌우 가장자리 | 위성↔노선 S-T 최대 |
| --- | --- | --- | --- |
| B 용화 | 3.94 · 3.22 (왼위 · 오른아래 0 · 가운데 0.24) | 7.52 | 7.52 |
| D | 63.62 · 62.46 (가운데 0.68) | 70.54 | 70.54 |
| 가이드_예정노선 | 99.13 · 95.89 | 98.46 | 98.46 |
화면 px(뷰포트 가정 B04 1400×560 · B05 1200×700 — 화면을 띄우지 않아 가정값):
- B 용화 처음 화면(설계 노선 + 200 m · 배율 2.33 · B04 0.41 px/m): 노선 가운데 T-G 1.6 m / 0.6 px · S-G 6.4 m / 2.6 px · S-T 5.2 m / 2.1 px · 화면 안(그림 있는 곳) 최대 T-G 2.7 m / 1.1 px · S-G 7.4 m / 3.1 px.
- B 용화 크게 당김(화면 폭 20 m · 70 px/m): 노선 자리 T-G 109 px · S-G 447 px · S-T 360 px · 상자 가장자리 T-G 276 px · S-G 527 px.
- D 크기(배율 1 · 0.165 px/m): T-G 10.5 px · S-G 11.7 px · 크게 당기면 4,000 px 넘음.
### 까닭 나눔
노선↔도엽(T-G):
| 까닭 | B 용화 | D | 큰 상자 |
| --- | --- | --- | --- |
| 도북 · 진북 회전(수렴각 γ = 중앙 자오선과 경도 차 × sin 위도) — 귀퉁이 ≈ γ × 대각선 | 3.5 m | 63.0 m | 97.5 m |
| 축척 차 — 위도 따라 경도 1° 길이(cos 위도)가 달라 상자가 사다리꼴(상자 안 수렴각 변화) | ≤ 0.46 m | ≤ 1.07 m | ≤ 2.68 m |
| 축척 차 — TM 축척계수 k 변화(상자 안) | 0.001 m(k 1.0000005~1.0000009) | 0.06 m(k 1.0000840~1.0000954) | 0.1 m |
| 축척 차 — k 고른 몫 · 원점 차(가산 원점 · Bessel 데이텀 이동) | 0 m — 두 귀퉁이를 pyproj 로 맞춰 흡수 | 0 m | 0 m |
위성↔도엽(S-G):
| 까닭 | B 용화 | D |
| --- | --- | --- |
| contain 비율 차 — TM 상자 비율 대 그림 px 비율(+0.62 % · +2.65 %) | 7.52 m | 70.54 m |
| └ 두 귀퉁이 맞춤 기울기 γ(H/W + W/H)(회전 탓) | +0.18 % ≈ 2.2 m | +2.19 % ≈ 59.8 m |
| └ 웹 메르카토르 px 가로세로가 땅과 다름(1 − M/N · 회전 없어도 남음) | +0.43 % ≈ 5.3 m | +0.44 % ≈ 12.0 m |
| 메르카토르 세로 비선형(그림 = 메르카토르 y 선형 · 도엽 = 위도 선형) | 0.15 m | 0.17 m |
위성↔노선(S-T) = 둘이 겹친 것(방향에 따라 더해지거나 빠짐).
## 3. 고칠 길
### 권장 — TM 을 주인으로(B 안 · D03 과 방향만 반대)
D03 은 바탕이 MapLibre(웹 메르카토르 GL)라 TM 겹을 경위도 쪽으로 옮김(586-0). B04 · B05 는 바탕이 캔버스 + `<img>` 한 장이고 설계값 · 누름 · 재기가 모두 TM → **경위도 겹과 그림만 TM 으로 옮기는 쪽이 작음**. TM 길(`metricToScreen` · `screenToMetric` · `createMetricProjector` · `prepareMetricPolyline(s)` · `_Drainage_Pipes` 제 식)은 손대지 않음 → 저장 좌표 영향 없음이 구조로 보장.
데이텀 문제: B 검증 다섯이 모두 Bessel(TOWGS84 WKT) → D03 TS 변환(`D03_PlanAnalysis_Tm.ts` · GRS80 5185~5188 · 5179 만)은 `tmParams` = null → **D03 틀을 그대로 공용으로 올리면 B 검증 프로젝트는 하나도 안 고쳐짐**. 그래서 변환은 서버 pyproj 에 맡김.
1. 서버 `B04_PreProcess_Router_GIS.py` `get_vworld_meta` 33~60 — 응답에 `tm_grid`(그림 상자 경위도 9×9 마디의 TM 좌표 · `resolve_project_crs` · pyproj)를 붙임. 응답 때 셈 · storage 안 씀 · D03 은 모르는 칸이라 그대로.
- 쌍선형 나머지(상자 안 최대): 9×9 = 용화 0.001 m · D 0.006 m · 큰 상자 0.014 m(2×2 네 귀퉁이만이면 0.09 · 0.43 · 0.90 m — 크게 당기면 보임).
2. 공용 `B04_PreProcess_UI_MapRender.ts` · `logic/B04_PreProcess_UI_MapRender_Prepare.ts`
- `Normalizer` 에 고를 칸 `toNorm?(lon, lat)` — 새 `createTmNormalizer(meta)`(또는 옵션)일 때만 채움: 격자 쌍선형 → TM → `((x−x_min)/W, 1−(y−y_min)/H)`. `createNormalizer` 는 그대로(D03 · 옛 시험 보호).
- `projectRing` 59~67 · `labelAnchorOf` 157~172 · `lonLatToScreen` 335~346 이 `toNorm` 있으면 씀 → 도엽 · 지적 · 행정 · 유역 채움 · 분수령 · 상류 세류 · 고른 고리(MapOverlays · Basins · FlowStrength · Drainage_Render · Drainage_Interact)가 한 번에 TM 자리로.
- 손으로 베낀 G 식 — `B04_PreProcess_UI_Watershed.ts` 네 곳 → `lonLatToScreen` · 격자 네모는 `bbox_lonlat` 네 귀퉁이를 `toNorm`(TM 격자는 TM 에서 곧은 네모) · `_Drainage_ZoomRange.ts` 53~57 → `toNorm`.
- 부르는 곳 셋만 `createTmNormalizer` 로: B04 `MapViewer.ts` 492 · B05 `_Drainage_Panel.ts` 477 · `_RouteEdit_Contour.ts` 195. (`VWorldMeta` 타입에 `tm_grid?` 한 줄.)
3. 위성 그림 — `B04_PreProcess_UI_MapViewer.ts` `updateImageTransform` 311~318 · `_Drainage_Panel.ts` 188~194 · CSS 두 곳(`object-fit: contain` · `transform-origin: center` 뺌 → 원점 0 0 · 그림 제 크기 · `matrix(…)`).
- 행렬 = 586-0 `localTmFrame` 생각 그대로: 그림 px → (경도 선형 · 메르카토르 y) → 경위도 → `toNorm` → 화면, 이 길을 **화면 가운데에서 1차로** 맞춤(옮김 · 확대 때마다 다시 · 이미 그때마다 부름).
- 나머지: 화면 폭 2 km 0.07 m(0.05 px) · 200 m 0.0007 m · 20 m 0 — 상자 통째(배율 1)도 0.24 m(용화) · 0.6 m(D) · 1.4 m(큰 상자) = 0.1 px 안팎.
- 세 귀퉁이 고정 행렬(더 짧음)이면 상자 안 0.24 · 0.6 · 1.4 m 가 그대로 → 크게 당기면 수십 px · 권하지 않음.
4. 그대로 둠 — `sheetCoversBand`(받을 범위 판정 · 띠 여유 안 · 영향 ≤ 귀퉁이 T-G) · 축척 막대(TM 기준이라 맞음).
다른 화면 영향:
- D03 — `createNormalizer` · `prepareLayer` · `lonLatToScreen` 을 쓰지만 `toNorm` 없는 길 → 바뀜 없음(586-0 · 586-1 그대로).
- B05 · B06 한 페이지 · A00 설계 화면이 띄우는 편집 · 배수유역도 → 같은 부품이라 함께 고쳐짐.
- 시각만 바뀜: 도엽 · 위성 · 유역 채움이 노선 쪽으로 옮겨 앉음(용화 노선 자리 위성 5~6 m · 도엽 1.6 m).
값(저장 좌표) 영향 없음 확인:
- 쓰는 길 = 누름 → `screenToMetric`(`_RouteEdit_View.ts` 41~52) · 관 = 노선 누가거리(`_Drainage_Pipes.ts` 92~113) · 구조물 · 측점 = TM → 모두 안 고침.
- 경위도 겹에서 TM 으로 되돌려 저장하는 길 없음(G 역변환은 D03 `_UI_Map.ts` 에만) · 등고선 고르기 · 자물쇠 = 높이값만(`_RouteEdit_ContourLock.ts`).
- 지난 저장값(사용자가 어긋난 위성 · 도엽을 보고 놓은 관 등)은 그대로 — 고쳐도 옮기지 않음.
시험 자리:
- 새 `resources/tester/test_447_tm_grid.py` + `helper_447_tm_grid.cjs`(진짜 TS 돌림 · 586 꼴): 서버 `tm_grid` 왼위 · 오른아래 마디 = 메타 `x_min · y_max · x_max · y_min` 1 mm 안 · 안 마디 = pyproj(용화 Bessel WKT · 5186) · TS `toNorm` 대 pyproj ≤ 0.02 m · `tm_grid` 없으면 옛 식과 비트 같음 · 그림 행렬 — 그림 귀퉁이 px 의 화면 자리 = `toNorm` 자리 0.5 px 안(당긴 화면) · 옛 꼴 어긋남 지킴(용화 귀퉁이 ≥ 3 m · D ≥ 60 m).
- 함께 돌릴 옛 시험(고친 대상): `test_582_contour_lod` · `test_509_1_sheet_clip_route` · `test_509_6_sheet_clip_drainage` · `test_281_1_drainage_zoom_range` · `test_b05_routeedit_view` · `test_586_tm_frame`(D03 안 바뀜 확인).
### 다른 길 — D03 과 같은 방향(A 안 · 비권장)
- TM 겹을 경위도 바탕으로(`localTmFrame` 공용화 · `D03_PlanAnalysis_Tm.ts` → `common_util/` 옮김).
- 고칠 곳이 많음 — `metricToScreen` · `createMetricProjector` · `screenToMetric`(D03 옛 꼴도 씀) · `_Drainage_Pipes` 제 식 · `prepareMetricPolyline(s)` · 편집 `toScreen` · `toMetric` · 추천 노선 · 보던 화면 기억 · 맞춤 — 누름 → 저장 좌표 길까지 건드림.
- Bessel 이라 B 검증 다섯은 「모르는 좌표계 = 옛 꼴」로 남음(TS 에 Bessel + 7 변수 이동을 더해야 함).
- 위성 contain 흠은 따로 고쳐야 함(그림을 mapRect 에 늘려 채움 · 메르카토르 세로 0.15~0.5 m 남음).
- 나머지(처음 화면 귀퉁이): 용화 0.22 m(B04) · 0.12 m(B05).
## 4. 쉬운 말 세 줄
- 지도는 투명지 석 장(위성 사진 · 도엽 · 노선)을 「똑같은 네모」라 여기고 **왼쪽 위 · 오른쪽 아래 두 귀퉁이에만 핀을 꽂아** 겹쳐 둔 꼴.
- 그런데 노선 장은 살짝 돌아 있고(용화 0.05° · D 지역 0.56° — 지구가 둥글어 설계 좌표의 「위쪽」과 경도선의 「위쪽」이 다름), 위성 장은 가로가 살짝 좁음(0.6 % · D 2.6 % — 웹 지도 사진의 가로세로 비율이 땅과 조금 다름).
- 그래서 핀 두 개를 잇는 대각선(가운데 포함) 위는 꼭 맞고 다른 귀퉁이 · 좌우 가장자리로 갈수록 벌어짐 — 용화 귀퉁이 노선↔도엽 4 m · 위성↔도엽 7.5 m(노선 자리 위성↔노선 5 m) · D 지역 크기였다면 64 m · 71 m.
## 5. 확인 못 함
- 실제 화면 px — 화면을 띄우지 않음 · 뷰포트(B04 1400×560 · B05 1200×700)는 가정 · m 값은 뷰포트와 무관.
- B04 처음 화면이 쓰는 `confirmed.route_bounds` 가 B05 설계 노선 범위와 같은지 — 설계 노선(`planned_route_nodes.csv`) 범위로 셈.
- 곁 발견: 용화 설계 노선 동쪽 끝(x 209,038)이 위성 상자(x_max 208,900) 밖 138 m — 위성을 받은 범위 · 노선이 바뀐 때 다시 받는지 확인 못 함.
- 도엽 등고선 ↔ LAS 등고선 어긋남(`_RouteEdit_Contour.ts` 머리 주석) 중 이 흠 몫 — 화면 몫은 용화에서 노선 자리 1.6 m · 귀퉁이 3.9 m 까지 · 자료 자체 차는 확인 못 함.
- VWorld 타일이 정확히 EPSG:3857 격자인지 — 서버 코드 식(OSM 타일식) 그대로 가정.
- D03 Bessel 프로젝트(지금 D 검증은 모두 5186) — 586-0 은 옛 꼴로 남음 · 위 `tm_grid` 를 D03 이 써도 되는지는 따로.
+32
View File
@@ -134,3 +134,35 @@ def test_위성_조각_대리는_범위_밖을_막음():
with pytest.raises(HTTPException) as error:
asyncio.run(plan_satellite_tile(uuid.uuid4(), z, x, y))
assert error.value.status_code == 400
def test_위성_조각은_서버에_기억해_다시_안_받음(tmp_path, monkeypatch):
"""586-2 — 받은 조각 = 프로젝트 `_derived/plan_tile` · 두 번째는 브이월드에 안 물음."""
import asyncio
import urllib.request
import uuid
from D03_PlanAnalysis import D03_PlanAnalysis_Router as router
project = uuid.uuid4()
monkeypatch.setitem(router._TILE_ROOTS, project, tmp_path)
calls = []
class Reply:
def __enter__(self):
return self
def __exit__(self, *_):
return False
def read(self):
return b"jpeg"
monkeypatch.setattr(urllib.request, "urlopen", lambda *a, **k: calls.append(1) or Reply())
for _ in range(2):
reply = asyncio.run(router.plan_satellite_tile(project, 14, 13990, 6380))
assert reply.body == b"jpeg"
assert len(calls) == 1
saved = tmp_path / "_derived" / "plan_tile" / "Satellite" / "14" / "13990_6380.jpeg"
assert saved.read_bytes() == b"jpeg"
assert not list(saved.parent.glob("*.part"))