도면 출력 추가: 수리 종단면도, 관망도(색인도·부분도·수압표), 범례·색상 선택, DXF(AutoCAD 2018)

- 데모 v2 이후 개발 PC의 변경을 옮김 (이 저장소의 이름 변경·README 정리는 유지)
- 서버 기본 DEM, DEM 등고선, 속성 테이블 범위 선택, WNTR 교차 검증 시험 포함
- 새 라이브러리: ezdxf(Python), maplibre-contour(npm)
- 변경 내역: docs/SPipe_패치내역_도면출력_2026-11-06.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SPipe EngineeringandClaude Opus 5.5 committed 2026-11-06 15:03:21 +09:00
1 parent 14f07181db
commit e3f386addf
33 files changed
+3454 -482

No files matched your search

+48 -2
View File
@@ -16,6 +16,7 @@ from .columnar import to_columnar
from .gisio import read_vector
from . import dem as dem_store
from . import gisops
from .drawing_dxf import drawing_to_dxf
from urllib.parse import quote
from .jobs import get_job
from .epanet import EN, EpanetAPI, EpanetException, FLOW_UNITS, PRESS_UNITS, QUAL_TYPES
@@ -186,6 +187,12 @@ async def dem_upload(file: UploadFile = File(...)):
pass
@app.get("/api/dem-base")
async def dem_base():
"""Server-provided DEMs (registered with `python -m backend.dem register ...`)."""
return await _dem_run(dem_store.list_base)
@app.get("/api/dem/{dem_id}")
async def dem_get(dem_id: str):
return await _dem_run(dem_store.dem_info, dem_id)
@@ -219,9 +226,9 @@ async def dem_hydro(dem_id: str, request: Request):
"""Body: {"op": "streams", "minAreaKm2": 0.1} or {"op": "watershed", "points": [[x, y]], "crs": "...", "snap": m}."""
body = await request.json()
if body.get("op") == "streams":
return await _dem_run(dem_store.streams, dem_id, float(body.get("minAreaKm2") or 0.1), timeout=600)
return await _dem_run(dem_store.streams, dem_id, float(body.get("minAreaKm2") or 0.1), body.get("bbox"), body.get("crs"), timeout=600)
if body.get("op") == "watershed":
return await _dem_run(dem_store.watershed, dem_id, body.get("points") or [], body.get("crs"), float(body.get("snap") or 50), timeout=600)
return await _dem_run(dem_store.watershed, dem_id, body.get("points") or [], body.get("crs"), float(body.get("snap") or 50), body.get("bbox"), timeout=600)
raise HTTPException(status_code=400, detail="op must be streams or watershed")
@@ -257,6 +264,45 @@ async def gis_reproject(request: Request):
return await _dem_run(gisops.reproject_fc, body.get("featureCollection"), body.get("from"), body.get("to"))
_UESC = re.compile(r"\\U\+([0-9A-Fa-f]{4})")
_CRLF = "\r\n"
def dxf_to_cp949(text: str) -> bytes:
"""R12 DXF from the browser (non-ASCII written as backslash-U+XXXX) -> Korean
code page (ANSI_949), which Korean CAD reads; characters outside CP949 stay escaped."""
def unesc(m: re.Match) -> str:
ch = chr(int(m.group(1), 16))
try:
ch.encode("cp949")
return ch
except UnicodeEncodeError:
return m.group(0)
out = _UESC.sub(unesc, text)
ver = _CRLF.join(["$ACADVER", "1", "AC1009", ""])
out = out.replace(ver, ver + _CRLF.join(["9", "$DWGCODEPAGE", "3", "ANSI_949", ""]), 1)
return out.encode("cp949", errors="replace")
@app.post("/api/drawing/dxf")
async def drawing_dxf(request: Request):
"""Body: {"drawing": {...items}, "name": ...} -> DXF R2018 (ezdxf, UTF-8 text);
legacy {"dxf": "<R12 DXF text>"} -> CP949 R12."""
body = await request.json()
name = str(body.get("name") or "drawing") + ".dxf"
if isinstance(body.get("drawing"), dict):
data = await _dem_run(drawing_to_dxf, body["drawing"], timeout=120)
return Response(content=data, media_type="application/dxf",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(name)}"})
text = str(body.get("dxf") or "")
if not text.startswith("0" + _CRLF + "SECTION") or len(text) > 50_000_000:
raise HTTPException(status_code=400, detail="DXF가 올바르지 않습니다.")
name = str(body.get("name") or "drawing") + ".dxf"
return Response(content=dxf_to_cp949(text), media_type="application/dxf",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(name)}"})
@app.get("/api/health")
def health():
return {"status": "ok"}