feat(위성): 위성 조각 프로젝트당 150 MB 한도 · 오래 안 본 것부터 지움 · 한 장짜리 위성 걷음 · B 저장 복사에서 조각 뺌 (PLAN 590-2 · 요구 452 · 453)

- 조각 저장본 한 벌(_derived/plan_tile) — 처음 열 때 한 번 셈 · 받을 때 더함 · 넘치면 묵은 것부터 80 % 까지 · 하루 넘게 묵은 것만 본 때 찍음
- 한 장짜리 위성 그림 안 만듦(메타 = 지도 틀만) · vworld-map 위성 404 · 미리 받기 줄 걷음
- 옛 캔버스(D03 · B04 · B05 배수유역) = 위성 바탕 없이 + 알림
- 저장 떠 두기에서 조각 폴더 뺌
- 시험 590_2 새로 · 283 한 본문 손 고침 실패는 고치기 전부터(이 고침과 무관)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 09:41:00 +09:00
co-authored by Claude Opus 5.5
parent 1c0c44f9e2
commit a17ad1dc41
14 changed files with 232 additions and 71 deletions
+10 -5
View File
@@ -74,7 +74,11 @@ def download_vworld_satellite_map(
) -> Path | None:
"""PRJ 좌표 정보와 지형 범위(bounds)를 바탕으로,
해당 임도 산림 지역을 감싸는 VWorld 지도 타일을 다운로드하여 하나의 PNG 텍스처로 병합합니다.
위성(`Satellite`)은 그림 없이 메타(지도 틀)만 씀 → 메타 경로를 돌려줌(590-2 · 위성 바탕 =
GL 조각 `plan/tile`).
"""
meta_only = layer_name.lower() == "satellite"
output_dir.mkdir(parents=True, exist_ok=True)
file_name = f"vworld_{layer_name.lower()}.png"
map_png_path = output_dir / file_name
@@ -118,11 +122,11 @@ def download_vworld_satellite_map(
break
zoom -= 1
# 4. 개별 타일 다운로드 및 이미지 병합
map_img = Image.new("RGBA", (tile_w * 256, tile_h * 256))
# 4. 개별 타일 다운로드 및 이미지 병합(위성 = 건너뜀)
map_img = Image.new("RGBA", (1, 1) if meta_only else (tile_w * 256, tile_h * 256))
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
for ty in range(y_start, y_end + 1):
for ty in range(y_start, y_end + 1) if not meta_only else ():
for tx in range(x_start, x_end + 1):
try:
normalized_layer = layer_name.lower()
@@ -176,7 +180,8 @@ def download_vworld_satellite_map(
img_x_max, img_y_min = rev_transformer.transform(lon_right, lat_bottom)
# 6. 이미지 저장 및 메타데이터 작성
map_img.save(map_png_path, "PNG")
if not meta_only:
map_img.save(map_png_path, "PNG")
meta = {
"x_min": img_x_min,
@@ -194,7 +199,7 @@ def download_vworld_satellite_map(
}
meta_json_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
return map_png_path
return meta_json_path if meta_only else map_png_path
# 경위도 격자 — 그림 상자와 둘레 한 폭씩(도엽이 상자 밖 0.8 폭까지 · 3 × 3 폭) · 한 변 25 마디
+8 -4
View File
@@ -78,7 +78,14 @@ async def get_vworld_meta(
async def get_vworld_map(
project_id: UUID, request: Request, layer_name: str = "satellite"
) -> Response | JSONResponse:
"""배경 지도 레이어 PNG 이미지를 반환합니다(ETag — 바뀌지 않았으면 304)."""
"""배경 지도 레이어 PNG 이미지를 반환합니다(ETag — 바뀌지 않았으면 304).
위성은 한 장 그림을 안 씀(590-2 · GL 조각 `plan/tile`) → 404."""
if layer_name.lower() == "satellite":
return JSONResponse(
status_code=404,
content={"status": "error", "message": "위성 한 장 그림은 쓰지 않습니다."},
)
pool = get_db_pool()
try:
async with pool.acquire() as connection:
@@ -93,9 +100,6 @@ async def get_vworld_map(
map_name = f"vworld_{target_layer}.png"
map_path = target_dir / map_name
if not map_path.exists() and target_layer == "satellite":
map_path = target_dir / "vworld_map.png"
if not map_path.exists():
return JSONResponse(
status_code=404,
+7 -3
View File
@@ -5,7 +5,7 @@
* 따라감 — 카메라 = 화면 가운데 1차 맞춤(`logic/…_MapGl_Frame` · 닮음꼴) + 남는 늘림 = 지도 칸 CSS 행렬 · 위 그림(노선 · 관 · 측점 · 고르기)은
* 위 캔버스 그대로 · 누름 · 끌기 판정 그대로.
* 처음 = 켬 · 끔 = 옛 캔버스(취향 `aislo:pref:map.canvas` = "1" · 세 화면 하나) · WebGL 못 씀 · 잃음 =
* 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
* 저절로 옛 캔버스 + 알림(취향은 안 바꿈) · 옛 캔버스 = 위성 바탕 없음(590-2 · 위성 = 조각만).
* ========================================================================== */
import type { ExpressionSpecification, FilterSpecification } from "maplibre-gl";
@@ -25,7 +25,10 @@ import { glCameraOf } from "./logic/B04_PreProcess_UI_MapGl_Frame";
/** 옛 캔버스로 그림("1") — 처음 = GL. */
const CANVAS_PREF_KEY = "aislo:pref:map.canvas";
export const GL_TOGGLE_TITLE = "그래픽 카드 그림(바탕 겹을 GPU 로 · 끔 = 옛 캔버스 그림)";
export const GL_TOGGLE_TITLE =
"그래픽 카드 그림(바탕 겹을 GPU 로 · 끔 = 옛 캔버스 그림 · 위성 바탕 없음)";
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
const NO_SATELLITE_NOTE = "옛 그림은 위성 바탕 없이 그림";
export interface TmGl<K extends string> {
gl: GlMap<K>;
@@ -68,7 +71,7 @@ export function createTmGl<K extends string>(opts: {
themeNames: opts.themeNames,
onFail: (why) => {
if (!wanted) return;
showToast(`그래픽 카드 그림을 못 써 옛 그림으로 — ${why}`, "info");
showToast(`그래픽 카드 그림을 못 써 옛 그림으로 — ${why} · ${NO_SATELLITE_NOTE}`, "info");
use(false);
},
});
@@ -87,6 +90,7 @@ export function createTmGl<K extends string>(opts: {
toggle() {
writeByKey(CANVAS_PREF_KEY, active ? "1" : "0");
use(!active);
if (!active) showToast(NO_SATELLITE_NOTE, "info");
},
follow(meta, normalizer, toScreen, width, height) {
if (!active || !meta || !normalizer || width <= 0 || height <= 0) return;
@@ -502,6 +502,8 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
surfaceGl.setLayer(layer, data);
});
BACKGROUND_LAYERS.forEach((layer) => {
// 위성 = GL 조각만(옛 캔버스 = 위성 바탕 없음 · 590-2)
if (layer === "satellite") return;
// 주소에 시각을 붙이면 브라우저가 매번 다시 받는다. 서버가 ETag를 주므로 그대로 쓴다.
backgroundImages.get(layer)!.src = getVWorldMapUrl(projectId, layer);
});