refactor(B04/B05/공통): 지도 색상 토큰화, 배수유역 패널 i18n, 소개 페이지 단계명 통일
- ui_template_theme.css에 2D 지도 벡터 팔레트(--map-*) 33종을 유일한 정의처로 등록하고, 캔버스에서 CSS 변수를 읽는 공통 유틸 ui_template_palette.ts(themeColor)를 신설. 값은 한 번만 읽어 캐시하고 data-theme 변경 시 비운다(다크 전환 대응). - B04 지도·유역 오버레이·흐름 화살표와 B05 배수유역도·유역선 편집·배관 마커의 하드코딩 색상을 전부 토큰 조회로 교체. 계획선 색·굵기, 후광색은 공용 함수로 일원화. - B05 배수유역 패널의 사용자 문구를 전부 ui_locales로 이관(제목, 레이어 토글 5종, 도구 버튼 5종, 툴팁, 상태 문구 8종, 유역 제원 표기). B04 유역 분석 오버레이의 버튼·갈래 토글·진행/실패 안내도 함께 전환. 관리자 진단용 결과 판독문은 원문 유지. - A02 프로그램 소개의 6단계 제목과 A01 히어로 문구의 단계 나열을 진행단계 이름 (전처리/종단설계/횡단설계/상세설계/수량산출/설계도서)과 일치시킴. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@
|
||||
* 화면 변환은 호출부가 `project`로 넘긴다 — 이 파일은 좌표계를 모른다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { haloColor } from "./B04_wf1_Surface_UI_MapRender";
|
||||
|
||||
/** 화살표 1개 — [가로, 세로, 방위(도), 도로 도달, 셀 수]. 앞 두 값의 좌표계는 호출부가 정한다. */
|
||||
export type FlowArrow = [number, number, number, boolean, number];
|
||||
|
||||
@@ -23,9 +26,9 @@ const MIN_LENGTH_PX = 9;
|
||||
/** 화면을 가득 채우지 않도록 두는 상한(px). */
|
||||
const MAX_LENGTH_PX = 40;
|
||||
|
||||
const TO_ROAD_COLOR = "rgba(153, 27, 27, 0.95)";
|
||||
const AWAY_COLOR = "rgba(30, 64, 175, 0.95)";
|
||||
const HALO_COLOR = "rgba(255, 255, 255, 0.9)";
|
||||
/* 색 값의 정의처는 `ui_template_theme.css`(`--map-*`)다 — 여기서 값을 새로 정하지 않는다. */
|
||||
const toRoadColor = (): string => themeColor("--map-flow-to-road-line", "rgba(153, 27, 27, 0.95)");
|
||||
const awayColor = (): string => themeColor("--map-flow-away-arrow", "rgba(30, 64, 175, 0.95)");
|
||||
|
||||
/** 화살표 좌표를 캔버스 픽셀로 옮기는 함수. */
|
||||
export type ArrowProjector = (a: number, b: number) => readonly [number, number];
|
||||
@@ -69,8 +72,8 @@ export function drawFlowArrows(
|
||||
const tipY = y + unitY * reach;
|
||||
// 어두운 배경·채움색 위에서도 읽히도록 흰 테두리를 한 겹 깔고 그 위에 색을 얹는다.
|
||||
for (const [color, lineWidth] of [
|
||||
[HALO_COLOR, width + 1.4] as const,
|
||||
[reaches ? TO_ROAD_COLOR : AWAY_COLOR, width] as const,
|
||||
[haloColor(), width + 1.4] as const,
|
||||
[reaches ? toRoadColor() : awayColor(), width] as const,
|
||||
]) {
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { VWorldMeta } from "./B04_wf1_Surface_Api_Fetch";
|
||||
|
||||
// 2D 지도 벡터 레이어 렌더 엔진.
|
||||
@@ -22,13 +23,17 @@ export type GeoJsonCollection = {
|
||||
export type MarkerKind = "dot" | "x";
|
||||
|
||||
/** 상류 세류망 강조 색 — 유역 판정의 기준선이라 가장 굵고 진하게 둔다. */
|
||||
const UPSTREAM_LINE_COLOR = "rgba(29, 78, 216, 0.95)";
|
||||
const upstreamLineColor = (): string => themeColor("--map-upstream", "rgba(29, 78, 216, 0.95)");
|
||||
|
||||
/** 배경 위에서 선·글자가 묻히지 않게 뒤에 까는 흰 테두리. */
|
||||
export const haloColor = (): string => themeColor("--map-halo", "rgba(255, 255, 255, 0.9)");
|
||||
|
||||
/**
|
||||
* 계획선(노선) 표기 색 — 주황. B04 2D 지도와 B05 배수유역도가 **같은 값**을 쓴다.
|
||||
* 계획선(노선) 표기 색 — B04 2D 지도와 B05 배수유역도가 **같은 값**을 쓴다.
|
||||
* 두 화면에서 같은 선을 다른 색으로 그리면 같은 것인지 알아볼 수 없다.
|
||||
* 색 값 자체는 `ui_template_theme.css`의 `--map-route`가 유일한 정의처다.
|
||||
*/
|
||||
export const ROUTE_LINE_COLOR = "#f97316";
|
||||
export const routeLineColor = (): string => themeColor("--map-route", "#f97316");
|
||||
/** 계획선 굵기(px) — 다른 레이어보다 굵게 둬야 배경 위에서 바로 눈에 띈다. */
|
||||
export const ROUTE_LINE_WIDTH = 2.4;
|
||||
|
||||
@@ -513,7 +518,7 @@ export function drawPreparedLabels(
|
||||
if (x < -margin || x > view.width + margin) continue;
|
||||
if (y < -margin || y > view.height + margin) continue;
|
||||
context.lineWidth = 3;
|
||||
context.strokeStyle = "rgba(255, 255, 255, 0.9)";
|
||||
context.strokeStyle = haloColor();
|
||||
context.strokeText(feature.labelText, x, y);
|
||||
context.fillStyle = color;
|
||||
context.fillText(feature.labelText, x, y);
|
||||
@@ -567,13 +572,13 @@ export function drawFilledRing(
|
||||
context.arc(centerX, centerY, 11, 0, Math.PI * 2);
|
||||
context.fillStyle = color;
|
||||
context.fill();
|
||||
context.strokeStyle = "rgba(255, 255, 255, 0.9)";
|
||||
context.strokeStyle = haloColor();
|
||||
context.lineWidth = 1.5;
|
||||
context.stroke();
|
||||
context.font = "600 12px sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillStyle = "#1f2937";
|
||||
context.fillStyle = themeColor("--map-label-text", "#1f2937");
|
||||
context.fillText(entry.label, centerX, centerY);
|
||||
}
|
||||
|
||||
@@ -591,7 +596,7 @@ export function drawUpstreamLines(
|
||||
context.lineWidth = 4;
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
context.strokeStyle = UPSTREAM_LINE_COLOR;
|
||||
context.strokeStyle = upstreamLineColor();
|
||||
lines.forEach((line) => {
|
||||
if (line.length < 2) return;
|
||||
context.beginPath();
|
||||
@@ -628,7 +633,7 @@ export function drawRidgeRing(
|
||||
});
|
||||
context.closePath();
|
||||
context.save();
|
||||
context.strokeStyle = "#92400e";
|
||||
context.strokeStyle = themeColor("--map-basin-outline", "#92400e");
|
||||
context.lineWidth = 1.8;
|
||||
context.setLineDash([7, 4]);
|
||||
context.stroke();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { fetchCachedSheetLayer } from "../A00_Common/b_asset_cache";
|
||||
import {
|
||||
fetchGisGeoJson,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
drawPreparedLayer,
|
||||
prepareLayer,
|
||||
prepareMetricPolyline,
|
||||
ROUTE_LINE_COLOR,
|
||||
routeLineColor,
|
||||
ROUTE_LINE_WIDTH,
|
||||
type GeoJsonCollection,
|
||||
type MapRect,
|
||||
@@ -75,18 +76,22 @@ const GIS_DEFAULT_ON: Record<GisLayer, boolean> = {
|
||||
/** 등고 라벨(계곡선 수치) 기본 표시 여부. */
|
||||
const CONTOUR_LABEL_DEFAULT_ON = false;
|
||||
|
||||
const GIS_LAYER_COLORS: Record<GisLayer, string> = {
|
||||
지적도: "#f97316",
|
||||
행정구역_시군구: "#7c3aed",
|
||||
행정구역_읍면동: "#22c55e",
|
||||
등고선: "#fdba74",
|
||||
도엽_등고선: "#a5b4fc",
|
||||
도엽_하천중심선: "#2563eb",
|
||||
도엽_표고점: "#f9a8d4",
|
||||
도엽_성절토: "#f43f5e",
|
||||
도엽_옹벽석축: "#0f766e",
|
||||
/** 레이어별 색은 `ui_template_theme.css`의 `--map-*`가 정의처다. 여기는 이름만 잇는다.
|
||||
* (fallback 값은 CSS가 아직 안 붙은 첫 프레임 대비용 안전값) */
|
||||
const GIS_LAYER_COLOR_TOKENS: Record<GisLayer, [name: string, fallback: string]> = {
|
||||
지적도: ["--map-cadastral", "#f97316"],
|
||||
행정구역_시군구: ["--map-sigungu", "#7c3aed"],
|
||||
행정구역_읍면동: ["--map-eupmyeondong", "#22c55e"],
|
||||
등고선: ["--map-contour", "#fdba74"],
|
||||
도엽_등고선: ["--map-sheet-contour", "#a5b4fc"],
|
||||
도엽_하천중심선: ["--map-sheet-stream", "#2563eb"],
|
||||
도엽_표고점: ["--map-sheet-elev-point", "#f9a8d4"],
|
||||
도엽_성절토: ["--map-sheet-cutfill", "#f43f5e"],
|
||||
도엽_옹벽석축: ["--map-sheet-wall", "#0f766e"],
|
||||
};
|
||||
|
||||
const gisLayerColor = (layer: GisLayer): string => themeColor(...GIS_LAYER_COLOR_TOKENS[layer]);
|
||||
|
||||
// 등고 라벨 표기 대상 레이어와 표고 속성 키 (gpkg=CTRLN_HG, 도엽=등고수치)
|
||||
const CONTOUR_LABEL_KEYS: Partial<Record<GisLayer, string[]>> = {
|
||||
등고선: ["CTRLN_HG"],
|
||||
@@ -261,7 +266,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
routeButton.type = "button";
|
||||
routeButton.className = "b04-map__layer-button b04-map__layer-button--gis is-active";
|
||||
routeButton.textContent = L("B04_Surface_Map_PlannedRoute");
|
||||
routeButton.style.setProperty("--b04-layer-color", ROUTE_LINE_COLOR);
|
||||
routeButton.style.setProperty("--b04-layer-color", routeLineColor());
|
||||
routeButton.setAttribute("aria-pressed", "true");
|
||||
routeButton.addEventListener("click", () => {
|
||||
showRoute = !showRoute;
|
||||
@@ -273,7 +278,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
|
||||
GIS_LAYERS.forEach((layer) => {
|
||||
gisButtons.append(
|
||||
makeLayerButton(gisLabels[layer], activeGisLayers, layer, GIS_LAYER_COLORS[layer]),
|
||||
makeLayerButton(gisLabels[layer], activeGisLayers, layer, gisLayerColor(layer)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -392,7 +397,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
const prepared = preparedLayers.get(layer);
|
||||
if (!prepared) return;
|
||||
context.lineWidth = isContourLayer(layer) ? 0.7 : 1.5;
|
||||
context.strokeStyle = GIS_LAYER_COLORS[layer];
|
||||
context.strokeStyle = gisLayerColor(layer);
|
||||
drawPreparedLayer(context, prepared, view, layer === "도엽_표고점" ? "x" : "dot");
|
||||
});
|
||||
if (showContourLabels) {
|
||||
@@ -402,7 +407,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
(Object.keys(CONTOUR_LABEL_KEYS) as GisLayer[]).forEach((layer) => {
|
||||
if (!activeGisLayers.has(layer)) return;
|
||||
const prepared = preparedLayers.get(layer);
|
||||
if (prepared) drawPreparedLabels(context, prepared, view, GIS_LAYER_COLORS[layer]);
|
||||
if (prepared) drawPreparedLabels(context, prepared, view, gisLayerColor(layer));
|
||||
});
|
||||
}
|
||||
// 배수유역 오버레이는 GIS 레이어 위에 얹는다 — 격자·화살표가 등고선을 덮어야 읽힌다.
|
||||
@@ -410,7 +415,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
// 계획선은 맨 위에 둔다 — 다른 레이어에 덮이면 노선이 어디로 지나는지 읽을 수 없다.
|
||||
if (showRoute && routeLayer) {
|
||||
context.lineWidth = ROUTE_LINE_WIDTH;
|
||||
context.strokeStyle = ROUTE_LINE_COLOR;
|
||||
context.strokeStyle = routeLineColor();
|
||||
drawPreparedLayer(context, routeLayer, view, "dot");
|
||||
}
|
||||
updateImageTransform();
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { fetchWatershedAnalysis, type WatershedAnalysis } from "./B04_wf1_Surface_Api_Fetch";
|
||||
import { drawFlowArrows } from "./B04_wf1_Surface_UI_FlowArrows";
|
||||
import { drawUpstreamLines, type Normalizer, type ViewState } from "./B04_wf1_Surface_UI_MapRender";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* 배수유역 분석 오버레이 (B04 — 관리자 확인용)
|
||||
*
|
||||
@@ -15,32 +21,57 @@ import { drawUpstreamLines, type Normalizer, type ViewState } from "./B04_wf1_Su
|
||||
* · 2차 전체 배수유역 외곽선(갈색 파선) · 기본 관 위치
|
||||
* ========================================================================== */
|
||||
|
||||
/* 색 값의 정의처는 `ui_template_theme.css`(`--map-*`)다 — 여기서 값을 새로 정하지 않는다.
|
||||
(인자로 준 값은 CSS가 아직 안 붙은 첫 프레임 대비용 안전값) */
|
||||
// 해석 격자 셀 선 — 등고선·세류 위에 얹으므로 흰색으로 둔다.
|
||||
const GRID_LINE_COLOR = "rgba(255, 255, 255, 0.55)";
|
||||
const gridLineColor = (): string => themeColor("--map-grid-line", "rgba(255, 255, 255, 0.55)");
|
||||
// 흐름 판정 색 — 도로로 물이 오는 셀은 적색, 오지 않는 셀은 파랑 채움 + 백색 화살표.
|
||||
const FLOW_TO_ROAD_FILL = "rgba(220, 38, 38, 0.28)";
|
||||
const FLOW_TO_ROAD_LINE = "rgba(153, 27, 27, 0.95)";
|
||||
const FLOW_AWAY_FILL = "rgba(37, 99, 235, 0.22)";
|
||||
const FLOW_AWAY_LINE = "rgba(255, 255, 255, 0.95)";
|
||||
const flowToRoadFill = (): string =>
|
||||
themeColor("--map-flow-to-road-fill", "rgba(220, 38, 38, 0.28)");
|
||||
const flowToRoadLine = (): string =>
|
||||
themeColor("--map-flow-to-road-line", "rgba(153, 27, 27, 0.95)");
|
||||
const flowAwayFill = (): string => themeColor("--map-flow-away-fill", "rgba(37, 99, 235, 0.22)");
|
||||
const flowAwayLine = (): string => themeColor("--map-flow-away-line", "rgba(255, 255, 255, 0.95)");
|
||||
/** 등고선 TIN 밖이라 표고가 없어 판정하지 못한 셀 — 미도달(파랑)과 구분한다. */
|
||||
const FLOW_UNKNOWN_FILL = "rgba(120, 113, 108, 0.18)";
|
||||
const flowUnknownFill = (): string =>
|
||||
themeColor("--map-flow-unknown-fill", "rgba(120, 113, 108, 0.18)");
|
||||
/** 화살표가 이보다 작으면 뭉개져 읽히지 않으므로 채움색만 남긴다(px). */
|
||||
const ARROW_MIN_PX = 7;
|
||||
/** 화면상 화살표 간격 목표(px). 1m 격자를 도엽 배율로 보면 셀이 1~2px라 셀마다 그릴 수 없다.
|
||||
* 이 간격이 되도록 셀을 건너뛰며 표본만 그린다 — 흐름장을 읽는 표준 방식이다. */
|
||||
const ARROW_SPACING_PX = 22;
|
||||
/** 2차 전체 배수유역 외곽선 = 분수령. */
|
||||
const BASIN_RING_COLOR = "rgba(146, 64, 14, 0.95)";
|
||||
const basinRingColor = (): string => themeColor("--map-basin-ring", "rgba(146, 64, 14, 0.95)");
|
||||
/** 기본 관 마커. */
|
||||
const PIPE_COLOR = "rgba(249, 115, 22, 0.95)";
|
||||
const pipeMarkerColor = (): string => themeColor("--map-pipe-marker", "rgba(249, 115, 22, 0.95)");
|
||||
|
||||
/** 개별로 켜고 끌 수 있는 오버레이 갈래. */
|
||||
/** 개별로 켜고 끌 수 있는 오버레이 갈래. 문구는 locale, 색은 theme.css가 정의처다. */
|
||||
const PARTS = [
|
||||
{ key: "primary", label: "1차 유역", color: "#059669" },
|
||||
{ key: "basin", label: "2차 유역", color: "#92400e" },
|
||||
{ key: "flow", label: "유역 방향", color: "#2563eb" },
|
||||
{ key: "arrows", label: "평균 흐름", color: "#7c3aed" },
|
||||
] as const;
|
||||
{
|
||||
key: "primary",
|
||||
labelKey: "B04_Surface_Watershed_Part_Primary",
|
||||
token: ["--map-primary-region", "#059669"],
|
||||
},
|
||||
{
|
||||
key: "basin",
|
||||
labelKey: "B04_Surface_Watershed_Part_Basin",
|
||||
token: ["--map-basin-outline", "#92400e"],
|
||||
},
|
||||
{
|
||||
key: "flow",
|
||||
labelKey: "B04_Surface_Watershed_Part_Flow",
|
||||
token: ["--map-flow-region", "#2563eb"],
|
||||
},
|
||||
{
|
||||
key: "arrows",
|
||||
labelKey: "B04_Surface_Watershed_Part_Arrows",
|
||||
token: ["--map-flow-arrow", "#7c3aed"],
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
key: string;
|
||||
labelKey: keyof typeof ui_locales;
|
||||
token: readonly [string, string];
|
||||
}>;
|
||||
type PartKey = (typeof PARTS)[number]["key"];
|
||||
|
||||
export interface WatershedOverlay {
|
||||
@@ -82,13 +113,10 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "b04-map__layer-button b04-map__layer-button--gis";
|
||||
button.textContent = "유역 분석";
|
||||
button.style.setProperty("--b04-layer-color", "#dc2626");
|
||||
button.textContent = L("B04_Surface_Watershed_Btn");
|
||||
button.style.setProperty("--b04-layer-color", themeColor("--color-danger", "#dc2626"));
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
button.title =
|
||||
"계획 노선(B03 업로드)과 도엽 등고선·세류선으로 배수유역을 처음부터 다시 분석합니다. " +
|
||||
"30초 안팎이 걸리며 결과는 영구저장소에 남습니다. " +
|
||||
"저장된 결과는 지도를 열 때 자동으로 표시되므로, 조건을 바꿨을 때만 누르면 됩니다.";
|
||||
button.title = L("B04_Surface_Watershed_Btn_Tip");
|
||||
|
||||
// 갈래별 표시 여부. 전체 토글(button)이 꺼져 있으면 이 값과 무관하게 아무것도 안 그린다.
|
||||
// 1차 유역·유역 방향은 격자가 지도를 덮어 판독을 방해하므로 기본 꺼짐(2026-08-01 사용자 지시).
|
||||
@@ -104,8 +132,8 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
const initialActive = shownParts[part.key];
|
||||
element.className =
|
||||
"b04-map__layer-button b04-map__layer-button--gis" + (initialActive ? " is-active" : "");
|
||||
element.textContent = part.label;
|
||||
element.style.setProperty("--b04-layer-color", part.color);
|
||||
element.textContent = L(part.labelKey);
|
||||
element.style.setProperty("--b04-layer-color", themeColor(part.token[0], part.token[1]));
|
||||
element.setAttribute("aria-pressed", String(initialActive));
|
||||
element.addEventListener("click", () => {
|
||||
shownParts[part.key] = !shownParts[part.key];
|
||||
@@ -201,7 +229,7 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
if (!bytes) {
|
||||
// 흐름 판정 전 — 격자만 흰 선으로 보여 준다.
|
||||
if (cellPx >= 2) {
|
||||
context.strokeStyle = GRID_LINE_COLOR;
|
||||
context.strokeStyle = gridLineColor();
|
||||
context.lineWidth = 0.5;
|
||||
context.beginPath();
|
||||
for (let col = colStart; col <= colEnd; col += 1) {
|
||||
@@ -254,19 +282,19 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
// 표고가 없어 판정 못한 셀 — 미도달(파랑 채움)과 구분해야 오독이 없다.
|
||||
const unanalyzed = azimuth === codes.invalid;
|
||||
context.fillStyle = unanalyzed
|
||||
? FLOW_UNKNOWN_FILL
|
||||
? flowUnknownFill()
|
||||
: reaches
|
||||
? FLOW_TO_ROAD_FILL
|
||||
: FLOW_AWAY_FILL;
|
||||
? flowToRoadFill()
|
||||
: flowAwayFill();
|
||||
context.fillRect(x, y, cellW, cellH);
|
||||
if (cellPx >= 2) {
|
||||
context.strokeStyle = GRID_LINE_COLOR;
|
||||
context.strokeStyle = gridLineColor();
|
||||
context.lineWidth = 0.5;
|
||||
context.strokeRect(x, y, cellW, cellH);
|
||||
}
|
||||
// arrowPx = 0 이면 표본에서 빠진 셀이라 채움만 하고 끝낸다.
|
||||
if (arrowPx < ARROW_MIN_PX || unanalyzed) return;
|
||||
const stroke = reaches ? FLOW_TO_ROAD_LINE : FLOW_AWAY_LINE;
|
||||
const stroke = reaches ? flowToRoadLine() : flowAwayLine();
|
||||
const midX = x + cellW / 2;
|
||||
const midY = y + cellH / 2;
|
||||
if (azimuth === codes.sink) {
|
||||
@@ -377,7 +405,7 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
if (region.basin_polygon_lonlat.length > 2) {
|
||||
context.setLineDash([8, 5]);
|
||||
context.lineWidth = 2.5;
|
||||
context.strokeStyle = BASIN_RING_COLOR;
|
||||
context.strokeStyle = basinRingColor();
|
||||
strokeLonLat(context, region.basin_polygon_lonlat, map, view);
|
||||
}
|
||||
context.setLineDash([]);
|
||||
@@ -390,12 +418,13 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
const y = (1 - (pipe.lat - map.latMin) / map.latRange) * ay + by;
|
||||
context.beginPath();
|
||||
context.arc(x, y, 7, 0, Math.PI * 2);
|
||||
context.fillStyle = PIPE_COLOR;
|
||||
context.fillStyle = pipeMarkerColor();
|
||||
context.fill();
|
||||
context.lineWidth = 1.5;
|
||||
context.strokeStyle = "#111827";
|
||||
const markerText = themeColor("--map-marker-text", "#111827");
|
||||
context.strokeStyle = markerText;
|
||||
context.stroke();
|
||||
context.fillStyle = "#111827";
|
||||
context.fillStyle = markerText;
|
||||
context.font = "bold 10px sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
@@ -449,11 +478,13 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
if (!projectId || busy) return;
|
||||
busy = true;
|
||||
button.disabled = true;
|
||||
button.textContent = refresh ? "분석 중…" : "불러오는 중…";
|
||||
button.textContent = L(
|
||||
refresh ? "B04_Surface_Watershed_Btn_Analyzing" : "B04_Surface_Watershed_Btn_Loading",
|
||||
);
|
||||
say(
|
||||
refresh
|
||||
? "배수유역을 처음부터 다시 분석하는 중입니다. 30초 안팎 걸립니다…"
|
||||
: "저장된 배수유역 분석을 불러오는 중…",
|
||||
L(
|
||||
refresh ? "B04_Surface_Watershed_Status_Analyzing" : "B04_Surface_Watershed_Status_Loading",
|
||||
),
|
||||
);
|
||||
const started = performance.now();
|
||||
try {
|
||||
@@ -462,24 +493,27 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
button.classList.add("is-active");
|
||||
button.setAttribute("aria-pressed", "true");
|
||||
const seconds = ((performance.now() - started) / 1000).toFixed(1);
|
||||
const origin = analysis.from_cache ? "저장분" : `재산정 ${seconds}초`;
|
||||
const origin = analysis.from_cache
|
||||
? L("B04_Surface_Watershed_Origin_Cached")
|
||||
: L("B04_Surface_Watershed_Origin_Recomputed").replace("{seconds}", seconds);
|
||||
say(`[${origin}] ${regionSummary(analysis)}`);
|
||||
} catch (error) {
|
||||
analysis = null;
|
||||
shown = false;
|
||||
button.classList.remove("is-active");
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
const message = error instanceof Error ? error.message : "배수유역을 불러오지 못했습니다.";
|
||||
const message =
|
||||
error instanceof Error ? error.message : L("B04_Surface_Watershed_LoadFailed");
|
||||
// 저장분이 아직 없는 것은 오류가 아니다 — 무엇을 눌러야 하는지 알려 준다.
|
||||
say(
|
||||
refresh
|
||||
? `유역 분석 실패: ${message}`
|
||||
: "저장된 배수유역 분석이 없습니다. [유역 분석]을 누르세요.",
|
||||
? L("B04_Surface_Watershed_Failed").replace("{message}", message)
|
||||
: L("B04_Surface_Watershed_NoSaved"),
|
||||
);
|
||||
} finally {
|
||||
busy = false;
|
||||
button.disabled = false;
|
||||
button.textContent = "유역 분석";
|
||||
button.textContent = L("B04_Surface_Watershed_Btn");
|
||||
onChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { Normalizer, ViewState } from "../B04_wf1_Surface/B04_wf1_Surface_UI_MapRender";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import {
|
||||
haloColor,
|
||||
type Normalizer,
|
||||
type ViewState,
|
||||
} from "../B04_wf1_Surface/B04_wf1_Surface_UI_MapRender";
|
||||
|
||||
/* =============================================================================
|
||||
* 2차 전체 배수유역 외곽선 편집 (B05 배수유역도)
|
||||
@@ -14,8 +19,10 @@ import type { Normalizer, ViewState } from "../B04_wf1_Surface/B04_wf1_Surface_U
|
||||
/** 편집 핸들 반경(px)과 잡을 수 있는 여유. */
|
||||
const HANDLE_RADIUS_PX = 4;
|
||||
const HANDLE_HIT_PX = 9;
|
||||
const HANDLE_COLOR = "rgba(146, 64, 14, 0.95)";
|
||||
const HANDLE_MOVED_COLOR = "rgba(220, 38, 38, 0.95)";
|
||||
/* 색 값의 정의처는 `ui_template_theme.css`(`--map-*`)다 — 여기서 값을 새로 정하지 않는다. */
|
||||
const handleColor = (): string => themeColor("--map-boundary-handle", "rgba(146, 64, 14, 0.95)");
|
||||
const handleMovedColor = (): string =>
|
||||
themeColor("--map-boundary-handle-moved", "rgba(220, 38, 38, 0.95)");
|
||||
/** 같은 자리로 볼 오차(도). 대략 0.1m 수준. */
|
||||
const SAME_POINT_EPSILON = 1e-6;
|
||||
|
||||
@@ -123,14 +130,14 @@ export function createBoundaryEditor(onChange: () => void): BoundaryEditor {
|
||||
const affine = affineOf(view);
|
||||
context.save();
|
||||
context.lineWidth = 1.2;
|
||||
context.strokeStyle = "rgba(255, 255, 255, 0.9)";
|
||||
context.strokeStyle = haloColor();
|
||||
points.forEach((point, index) => {
|
||||
const [x, y] = toScreen(point, normalizer, affine);
|
||||
if (x < -20 || y < -20 || x > view.width + 20 || y > view.height + 20) return;
|
||||
const base = bases[index];
|
||||
context.beginPath();
|
||||
context.arc(x, y, HANDLE_RADIUS_PX, 0, Math.PI * 2);
|
||||
context.fillStyle = base && !samePoint(point, base) ? HANDLE_MOVED_COLOR : HANDLE_COLOR;
|
||||
context.fillStyle = base && !samePoint(point, base) ? handleMovedColor() : handleColor();
|
||||
context.fill();
|
||||
context.stroke();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { DRAINAGE_SHEET_LAYERS, fetchCachedSheetLayer } from "../A00_Common/b_asset_cache";
|
||||
import {
|
||||
fetchVWorldMeta,
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
drawUpstreamLines,
|
||||
prepareLayer,
|
||||
prepareMetricPolyline,
|
||||
ROUTE_LINE_COLOR,
|
||||
routeLineColor,
|
||||
ROUTE_LINE_WIDTH,
|
||||
type GeoJsonCollection,
|
||||
type MapRect,
|
||||
@@ -47,24 +49,25 @@ import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
const DRAINAGE_LAYERS = DRAINAGE_SHEET_LAYERS;
|
||||
type DrainageLayer = (typeof DRAINAGE_LAYERS)[number];
|
||||
|
||||
const LAYER_COLORS: Record<DrainageLayer, string> = {
|
||||
도엽_등고선: "#a5b4fc",
|
||||
도엽_하천중심선: "#2563eb",
|
||||
/** 색 값의 정의처는 `ui_template_theme.css`(`--map-*`)다. 여기는 이름만 잇는다. */
|
||||
const LAYER_COLOR_TOKENS: Record<DrainageLayer, [name: string, fallback: string]> = {
|
||||
도엽_등고선: ["--map-sheet-contour", "#a5b4fc"],
|
||||
도엽_하천중심선: ["--map-sheet-stream", "#2563eb"],
|
||||
};
|
||||
|
||||
const LAYER_LABELS: Record<DrainageLayer, string> = {
|
||||
도엽_등고선: "등고선",
|
||||
도엽_하천중심선: "세류",
|
||||
const layerColor = (layer: DrainageLayer): string => themeColor(...LAYER_COLOR_TOKENS[layer]);
|
||||
|
||||
const LAYER_LABEL_KEYS: Record<DrainageLayer, keyof typeof ui_locales> = {
|
||||
도엽_등고선: "B05_Drainage_Layer_Contour",
|
||||
도엽_하천중심선: "B05_Drainage_Layer_Stream",
|
||||
};
|
||||
|
||||
/** 도엽 레이어가 아닌 표시 토글의 띠 색 — 지도에 그려지는 선 색과 맞춘다. */
|
||||
const ARROW_TOGGLE_COLOR = "#7c3aed";
|
||||
const UPSTREAM_TOGGLE_COLOR = "#1d4ed8";
|
||||
const arrowToggleColor = (): string => themeColor("--map-flow-arrow", "#7c3aed");
|
||||
const upstreamToggleColor = (): string => themeColor("--map-upstream-toggle", "#1d4ed8");
|
||||
/** 위성사진은 선이 아니라 배경이라 맞출 선 색이 없다 — 중립 회색을 띠 색으로 쓴다. */
|
||||
const SATELLITE_TOGGLE_COLOR = "#64748b";
|
||||
const satelliteToggleColor = (): string => themeColor("--map-satellite-toggle", "#64748b");
|
||||
|
||||
/** 계획선 색은 B04 2D 지도와 같은 값을 쓴다(정의처: MapRender). */
|
||||
const ROUTE_COLOR = ROUTE_LINE_COLOR;
|
||||
const COLLAPSED_KEY = "b05-route-drainage-collapsed";
|
||||
/** 드래그로 조절한 패널 폭(px) 보관 키 — 브라우저 세션 동안만 유지한다. */
|
||||
const WIDTH_KEY = "b05-route-drainage-width";
|
||||
@@ -73,8 +76,8 @@ const MIN_PANEL_WIDTH = 320;
|
||||
/** 상한은 하단 패널 폭의 70%까지(사용자 지시) — 종단면도가 최소한 30%는 남아야 한다. */
|
||||
const MAX_PANEL_WIDTH_RATIO = 0.7;
|
||||
|
||||
/** 유역 오버레이 파스텔 색상. 번호 순으로 돌려쓴다(사용자 지시: 파스텔톤). */
|
||||
const BASIN_COLORS = [
|
||||
/** 유역 오버레이 파스텔 색상 8종(정의처: `--map-basin-1`~`-8`). 번호 순으로 돌려쓴다. */
|
||||
const BASIN_COLOR_FALLBACKS = [
|
||||
"rgba(167, 216, 199, 0.45)",
|
||||
"rgba(247, 208, 168, 0.45)",
|
||||
"rgba(186, 199, 240, 0.45)",
|
||||
@@ -85,6 +88,16 @@ const BASIN_COLORS = [
|
||||
"rgba(240, 219, 168, 0.45)",
|
||||
] as const;
|
||||
|
||||
/** 유역 번호(1부터)에 대응하는 채움색. 8종을 넘어가면 처음부터 다시 쓴다. */
|
||||
function basinColor(index: number): string {
|
||||
const slot = (index - 1) % BASIN_COLOR_FALLBACKS.length;
|
||||
return themeColor(`--map-basin-${slot + 1}`, BASIN_COLOR_FALLBACKS[slot]);
|
||||
}
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
export interface DrainagePanel {
|
||||
root: HTMLElement;
|
||||
/** 프로젝트가 정해지면 배경지도·도엽 레이어를 불러온다. */
|
||||
@@ -108,7 +121,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const header = document.createElement("div");
|
||||
header.className = "b05-drainage__header";
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = "배수유역도";
|
||||
title.textContent = L("B05_Drainage_Title");
|
||||
const layerButtons = document.createElement("div");
|
||||
layerButtons.className = "b05-drainage__layers";
|
||||
header.append(title, layerButtons);
|
||||
@@ -118,35 +131,31 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const analyzeButton = document.createElement("button");
|
||||
analyzeButton.type = "button";
|
||||
analyzeButton.className = "b05-drainage__analyze";
|
||||
analyzeButton.textContent = "세부유역 산정";
|
||||
analyzeButton.title =
|
||||
"B04에서 분석해 둔 배수유역을 불러와 관을 보충하고 세부유역을 나눕니다. " +
|
||||
"분석 결과가 없으면 B04에서 먼저 실행해야 합니다.";
|
||||
analyzeButton.textContent = L("B05_Drainage_Btn_Analyze");
|
||||
analyzeButton.title = L("B05_Drainage_Btn_Analyze_Tip");
|
||||
// 배관 편집 토글 — 켜면 계획선 클릭으로 배관 추가, 마커 드래그로 이동.
|
||||
const editButton = document.createElement("button");
|
||||
editButton.type = "button";
|
||||
editButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
editButton.textContent = "배관 편집";
|
||||
editButton.textContent = L("B05_Drainage_Btn_PipeEdit");
|
||||
editButton.setAttribute("aria-pressed", "false");
|
||||
// 선택된 배관 삭제 — 편집 모드에서 마커를 선택해야 활성화된다.
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.type = "button";
|
||||
deleteButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
deleteButton.textContent = "선택 삭제";
|
||||
deleteButton.textContent = L("B05_Drainage_Btn_DeleteSelected");
|
||||
deleteButton.disabled = true;
|
||||
// 자동 제안으로 되돌리기 — 편집한 배관 배치를 버리고 백엔드 자동 제안으로 재산정.
|
||||
const autoButton = document.createElement("button");
|
||||
autoButton.type = "button";
|
||||
autoButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
autoButton.textContent = "자동 제안";
|
||||
autoButton.textContent = L("B05_Drainage_Btn_Auto");
|
||||
// 유역선 편집 토글 — 켜면 외곽선 위 핸들을 잡아 유역 경계를 손으로 고친다.
|
||||
const boundaryButton = document.createElement("button");
|
||||
boundaryButton.type = "button";
|
||||
boundaryButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
boundaryButton.textContent = "유역선 편집";
|
||||
boundaryButton.title =
|
||||
"2차 전체 배수유역 외곽선 위 포인트를 끌어 경계를 고칩니다. " +
|
||||
"옮긴 값은 종단 경로 확정 시 저장 여부를 묻습니다.";
|
||||
boundaryButton.textContent = L("B05_Drainage_Btn_BoundaryEdit");
|
||||
boundaryButton.title = L("B05_Drainage_Btn_BoundaryEdit_Tip");
|
||||
boundaryButton.setAttribute("aria-pressed", "false");
|
||||
header.append(analyzeButton, editButton, deleteButton, autoButton, boundaryButton);
|
||||
|
||||
@@ -154,13 +163,13 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
viewport.className = "b05-drainage__viewport";
|
||||
const backgroundImage = document.createElement("img");
|
||||
backgroundImage.className = "b05-drainage__image";
|
||||
backgroundImage.alt = "배경 위성지도";
|
||||
backgroundImage.alt = L("B05_Drainage_ImageAlt");
|
||||
backgroundImage.draggable = false;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "b05-drainage__canvas";
|
||||
const status = document.createElement("span");
|
||||
status.className = "b05-drainage__status";
|
||||
status.textContent = "노선을 확정하면 배수유역도가 표시됩니다.";
|
||||
status.textContent = L("B05_Drainage_Status_NeedRoute");
|
||||
// 지도 정중앙 로딩 서클 — 배경도·도엽 레이어·유역 산정이 끝날 때까지 화면이 비어 보인다.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
@@ -255,39 +264,39 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
// 배경 위성사진 — 등고선 앞에 둔다(2026-08-01 사용자 지시). 사진이 어두워 유역 채움색이
|
||||
// 묻힐 때 끄고 본다. 캔버스가 아니라 배경 이미지라 표시 여부만 직접 바꾼다.
|
||||
addLayerToggle(
|
||||
"위성사진",
|
||||
SATELLITE_TOGGLE_COLOR,
|
||||
L("B05_Drainage_Layer_Satellite"),
|
||||
satelliteToggleColor(),
|
||||
true,
|
||||
(next) => {
|
||||
backgroundImage.hidden = !next;
|
||||
},
|
||||
"배경 위성사진을 보이거나 숨깁니다.",
|
||||
L("B05_Drainage_Layer_Satellite_Tip"),
|
||||
);
|
||||
DRAINAGE_LAYERS.forEach((layer) => {
|
||||
addLayerToggle(LAYER_LABELS[layer], LAYER_COLORS[layer], true, (next) => {
|
||||
addLayerToggle(L(LAYER_LABEL_KEYS[layer]), layerColor(layer), true, (next) => {
|
||||
if (next) activeLayers.add(layer);
|
||||
else activeLayers.delete(layer);
|
||||
});
|
||||
});
|
||||
// 흐름 화살표 — 도면이 지저분해질 때 끄기 위한 토글(등고선·세류와 같은 줄·같은 양식).
|
||||
addLayerToggle(
|
||||
"흐름 화살표",
|
||||
ARROW_TOGGLE_COLOR,
|
||||
L("B05_Drainage_Layer_Arrows"),
|
||||
arrowToggleColor(),
|
||||
showArrows,
|
||||
(next) => {
|
||||
showArrows = next;
|
||||
},
|
||||
"B04에서 산출한 평균 흐름 방향을 보이거나 숨깁니다.",
|
||||
L("B05_Drainage_Layer_Arrows_Tip"),
|
||||
);
|
||||
// 상류 세류선 강조 — 유역 판정의 기준선이라 항상 같은 굵기·색으로 얹는다.
|
||||
addLayerToggle(
|
||||
"상류 세류",
|
||||
UPSTREAM_TOGGLE_COLOR,
|
||||
L("B05_Drainage_Layer_Upstream"),
|
||||
upstreamToggleColor(),
|
||||
showUpstream,
|
||||
(next) => {
|
||||
showUpstream = next;
|
||||
},
|
||||
"유역 안쪽 상류 세류망을 굵게 강조합니다.",
|
||||
L("B05_Drainage_Layer_Upstream_Tip"),
|
||||
);
|
||||
|
||||
function updateImageTransform(): void {
|
||||
@@ -317,7 +326,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
// 세부유역 채움을 가장 아래에 깔아 등고선·세류 판독을 가리지 않게 한다.
|
||||
if (normalizer) {
|
||||
basins.forEach((basin) => {
|
||||
const color = BASIN_COLORS[(basin.index - 1) % BASIN_COLORS.length];
|
||||
const color = basinColor(basin.index);
|
||||
drawFilledRing(
|
||||
context,
|
||||
{ ring: basin.polygon_lonlat, label: String(basin.index) },
|
||||
@@ -339,7 +348,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const prepared = preparedLayers.get(layer);
|
||||
if (!prepared) return;
|
||||
context.lineWidth = layer === "도엽_등고선" ? 0.7 : 1.5;
|
||||
context.strokeStyle = LAYER_COLORS[layer];
|
||||
context.strokeStyle = layerColor(layer);
|
||||
drawPreparedLayer(context, prepared, view, "dot");
|
||||
});
|
||||
// 상류 세류선 강조 — 유역 채움 위, 흐름 화살표 아래(2026-08-01 사용자 지시).
|
||||
@@ -349,7 +358,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
}
|
||||
if (routeLayer) {
|
||||
context.lineWidth = ROUTE_LINE_WIDTH;
|
||||
context.strokeStyle = ROUTE_COLOR;
|
||||
context.strokeStyle = routeLineColor();
|
||||
drawPreparedLayer(context, routeLayer, view, "dot");
|
||||
}
|
||||
// 평균 흐름 화살표 — 유역 채움 위, 배관 마커 아래. 좌표는 사업지 CRS(m)라
|
||||
@@ -391,8 +400,8 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
/** 배관 마커 색 — 같은 누가거리 유역의 파스텔색(불투명). 유역이 없으면 회색. */
|
||||
function pipeColor(chainage: number): string {
|
||||
const basin = basins.find((item) => Math.abs(item.chainage_m - chainage) < 0.51);
|
||||
if (!basin) return "#e5e7eb";
|
||||
return BASIN_COLORS[(basin.index - 1) % BASIN_COLORS.length].replace(/0\.45\)$/, "1)");
|
||||
if (!basin) return themeColor("--map-pipe-orphan", "#e5e7eb");
|
||||
return basinColor(basin.index).replace(/0\.45\)$/, "1)");
|
||||
}
|
||||
|
||||
/** 마커 선택 ↔ 유역 목록 선택 동기화 + 삭제 버튼 활성화. */
|
||||
@@ -426,16 +435,23 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "b05-drainage__basin-index";
|
||||
badge.textContent = String(basin.index);
|
||||
badge.style.background = BASIN_COLORS[(basin.index - 1) % BASIN_COLORS.length];
|
||||
badge.style.background = basinColor(basin.index);
|
||||
const metrics = document.createElement("span");
|
||||
metrics.className = "b05-drainage__basin-metrics";
|
||||
// 관경은 수식 미확정이라 백엔드가 null을 주며, 확정 전까지 "미정"으로 표기한다.
|
||||
const pipe =
|
||||
basin.pipe_diameter_mm === null ? "미정" : `Ø${Math.round(basin.pipe_diameter_mm)}mm`;
|
||||
metrics.textContent =
|
||||
`면적 ${formatArea(basin.area_m2)} · 표고 ${basin.relief_m.toFixed(1)}m · ` +
|
||||
`유하 ${Math.round(basin.flow_length_m)}m · 관경 ${pipe}`;
|
||||
row.title = `측점 누가거리 ${basin.chainage_m.toFixed(1)}m`;
|
||||
basin.pipe_diameter_mm === null
|
||||
? L("B05_Drainage_Basin_Undecided")
|
||||
: `Ø${Math.round(basin.pipe_diameter_mm)}mm`;
|
||||
metrics.textContent = L("B05_Drainage_Basin_Metrics")
|
||||
.replace("{area}", formatArea(basin.area_m2))
|
||||
.replace("{relief}", basin.relief_m.toFixed(1))
|
||||
.replace("{flow}", String(Math.round(basin.flow_length_m)))
|
||||
.replace("{pipe}", pipe);
|
||||
row.title = L("B05_Drainage_Basin_Chainage").replace(
|
||||
"{chainage}",
|
||||
basin.chainage_m.toFixed(1),
|
||||
);
|
||||
row.append(badge, metrics);
|
||||
row.addEventListener("click", () => {
|
||||
selectedBasin = selectedBasin === basin.index ? null : basin.index;
|
||||
@@ -456,8 +472,8 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
if (!projectId) return;
|
||||
analyzeButton.disabled = true;
|
||||
status.hidden = false;
|
||||
status.textContent = "세부유역을 산정하는 중…";
|
||||
showProgress(null, "세부유역을 산정하는 중…");
|
||||
status.textContent = L("B05_Drainage_Status_Analyzing");
|
||||
showProgress(null, L("B05_Drainage_Status_Analyzing"));
|
||||
try {
|
||||
const chainages = !auto && pipeEditor.pipes().length > 0 ? pipeEditor.chainages() : undefined;
|
||||
const response = await fetchDrainageBasins(projectId, chainages);
|
||||
@@ -480,11 +496,12 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
renderBasinList();
|
||||
syncPipeSelection();
|
||||
status.hidden = basins.length > 0;
|
||||
if (basins.length === 0) status.textContent = "산정된 배수유역이 없습니다.";
|
||||
if (basins.length === 0) status.textContent = L("B05_Drainage_Status_NoBasin");
|
||||
scheduleDraw();
|
||||
} catch (error) {
|
||||
status.hidden = false;
|
||||
status.textContent = error instanceof Error ? error.message : "세부유역 산정에 실패했습니다.";
|
||||
status.textContent =
|
||||
error instanceof Error ? error.message : L("B05_Drainage_Status_AnalyzeFailed");
|
||||
} finally {
|
||||
analyzeButton.disabled = false;
|
||||
showProgress(null, null);
|
||||
@@ -550,11 +567,11 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
preparedLayers.clear();
|
||||
backgroundImage.removeAttribute("src");
|
||||
status.hidden = false;
|
||||
status.textContent = "배경도를 불러오는 중…";
|
||||
showProgress(0, "배경도를 불러오는 중…");
|
||||
status.textContent = L("B05_Drainage_Status_LoadingBase");
|
||||
showProgress(0, L("B05_Drainage_Status_LoadingBase"));
|
||||
try {
|
||||
const nextMeta = await fetchVWorldMeta(activeProjectId, "satellite");
|
||||
showProgress(1 / 3, "도엽 레이어를 불러오는 중…");
|
||||
showProgress(1 / 3, L("B05_Drainage_Status_LoadingSheets"));
|
||||
const loaded = await Promise.all(
|
||||
DRAINAGE_LAYERS.map(async (layer) => {
|
||||
try {
|
||||
@@ -580,16 +597,17 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
if (routePoints.length > 1) routeLayer = prepareMetricPolyline(routePoints, nextMeta);
|
||||
pipeEditor.setContext(nextMeta, routePoints);
|
||||
status.hidden = featureCount > 0;
|
||||
if (featureCount === 0) status.textContent = "도엽 레이어가 없습니다. B04에서 임포트하세요.";
|
||||
if (featureCount === 0) status.textContent = L("B05_Drainage_Status_NoSheets");
|
||||
fitToRoute();
|
||||
scheduleDraw();
|
||||
showProgress(2 / 3, "세부유역을 산정하는 중…");
|
||||
showProgress(2 / 3, L("B05_Drainage_Status_Analyzing"));
|
||||
// B04 분석 결과를 읽어 오는 것뿐이라 즉시 끝난다 — 페이지에 들어오면 바로 보여 준다.
|
||||
void analyze(true);
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return;
|
||||
status.hidden = false;
|
||||
status.textContent = error instanceof Error ? error.message : "배경도를 불러오지 못했습니다.";
|
||||
status.textContent =
|
||||
error instanceof Error ? error.message : L("B05_Drainage_Status_LoadFailed");
|
||||
showProgress(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { VWorldMeta } from "../B04_wf1_Surface/B04_wf1_Surface_Api_Fetch";
|
||||
import type { ViewState } from "../B04_wf1_Surface/B04_wf1_Surface_UI_MapRender";
|
||||
|
||||
@@ -224,9 +225,12 @@ export function createPipeEditor(onChange: () => void): PipeEditor {
|
||||
context.fillStyle = colorOf(pipe.chainage_m, position);
|
||||
context.fill();
|
||||
context.lineWidth = isSelected ? 2.5 : 1.5;
|
||||
context.strokeStyle = isSelected ? "#111827" : "#374151";
|
||||
const markerText = themeColor("--map-marker-text", "#111827");
|
||||
context.strokeStyle = isSelected
|
||||
? markerText
|
||||
: themeColor("--map-marker-outline", "#374151");
|
||||
context.stroke();
|
||||
context.fillStyle = "#111827";
|
||||
context.fillStyle = markerText;
|
||||
context.font = "bold 10px sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
@@ -234,7 +238,7 @@ export function createPipeEditor(onChange: () => void): PipeEditor {
|
||||
// 누가거리 라벨 — 마커 우상단.
|
||||
context.font = "10px sans-serif";
|
||||
context.textAlign = "left";
|
||||
context.fillStyle = "#1f2937";
|
||||
context.fillStyle = themeColor("--map-label-text", "#1f2937");
|
||||
context.fillText(
|
||||
`${pipe.chainage_m.toFixed(0)}m`,
|
||||
screen.x + radius + 3,
|
||||
|
||||
@@ -133,9 +133,10 @@ export const ui_locales = {
|
||||
A07_Register_Title: ["회원가입", "Sign up"],
|
||||
|
||||
/* --- A01_Home 상세 --- */
|
||||
/* 단계를 늘어놓는 문구는 진행단계 이름(WF_Step_*)과 같은 말을 쓴다(2026-08-01 사용자 지시). */
|
||||
A01_Home_Hero_Subtitle: [
|
||||
"LAS 지형 데이터부터 최적 경로, 종횡단, 견적까지 한 번에.",
|
||||
"From LAS terrain data to optimal routes, cross-sections, and estimates — all in one.",
|
||||
"LAS 지형 데이터부터 종단설계·횡단설계, 수량산출, 설계도서까지 한 번에.",
|
||||
"From LAS terrain data to profile and cross design, quantities, and design documents — all in one.",
|
||||
],
|
||||
A01_Home_Hero_CtaPrimary: ["지금 시작하기", "Get Started"],
|
||||
A01_Home_Hero_CtaSecondary: ["프로그램 살펴보기", "Explore Features"],
|
||||
@@ -167,32 +168,34 @@ export const ui_locales = {
|
||||
A02_ProgDetail_Hero_Cta: ["무료로 시작하기", "Start for Free"],
|
||||
|
||||
A02_ProgDetail_Workflow_SectionTitle: ["6단계 설계 워크플로우", "6-Stage Design Workflow"],
|
||||
A02_ProgDetail_Step1_Title: ["1. 지표면 분석", "1. Surface Analysis"],
|
||||
/* 단계 이름은 진행단계 오버레이(WF_Step_*)와 같은 말을 쓴다 — 소개 페이지와 실제 화면의
|
||||
단계 이름이 다르면 사용자가 같은 단계인지 알 수 없다(2026-08-01 사용자 지시). */
|
||||
A02_ProgDetail_Step1_Title: ["1. 전처리", "1. Preprocess"],
|
||||
A02_ProgDetail_Step1_Desc: [
|
||||
"LAS 포인트클라우드에서 지면점을 필터링하고 15종 지표면 모델을 생성합니다.",
|
||||
"Filter ground points from the LAS point cloud and generate 15 surface models.",
|
||||
],
|
||||
A02_ProgDetail_Step2_Title: ["2. 경로 설계", "2. Route Design"],
|
||||
A02_ProgDetail_Step2_Title: ["2. 종단설계", "2. Profile Design"],
|
||||
A02_ProgDetail_Step2_Desc: [
|
||||
"경사·곡선반경·회피구역을 반영해 최적 임도 노선을 자동 탐색합니다.",
|
||||
"Auto-search optimal routes reflecting grade, curve radius, and avoidance zones.",
|
||||
],
|
||||
A02_ProgDetail_Step3_Title: ["3. 종·횡단 생성", "3. Profile & Cross-section"],
|
||||
A02_ProgDetail_Step3_Title: ["3. 횡단설계", "3. Cross Design"],
|
||||
A02_ProgDetail_Step3_Desc: [
|
||||
"확정 노선을 따라 종단면과 횡단면을 자동 추출합니다.",
|
||||
"Automatically extract longitudinal and cross sections along the confirmed route.",
|
||||
],
|
||||
A02_ProgDetail_Step4_Title: ["4. 상세 설계", "4. Detailed Design"],
|
||||
A02_ProgDetail_Step4_Title: ["4. 상세설계", "4. Detail Design"],
|
||||
A02_ProgDetail_Step4_Desc: [
|
||||
"구조물, 배수, 절·성토 등 세부 설계 요소를 편집합니다.",
|
||||
"Edit detailed design elements such as structures, drainage, and cut/fill.",
|
||||
],
|
||||
A02_ProgDetail_Step5_Title: ["5. 수량 산출", "5. Quantity Takeoff"],
|
||||
A02_ProgDetail_Step5_Title: ["5. 수량산출", "5. Quantity"],
|
||||
A02_ProgDetail_Step5_Desc: [
|
||||
"토공량과 구조물 수량을 자동 계산합니다.",
|
||||
"Automatically calculate earthwork volumes and structure quantities.",
|
||||
],
|
||||
A02_ProgDetail_Step6_Title: ["6. 견적 / 문서", "6. Estimation / Docs"],
|
||||
A02_ProgDetail_Step6_Title: ["6. 설계도서", "6. Design Docs"],
|
||||
A02_ProgDetail_Step6_Desc: [
|
||||
"견적서와 설계도서를 Excel / DWG / PDF로 출력합니다.",
|
||||
"Export estimates and design documents as Excel / DWG / PDF.",
|
||||
@@ -652,6 +655,40 @@ export const ui_locales = {
|
||||
B04_Surface_Map_GisLayer: ["국가 GIS 레이어", "National GIS Layer"],
|
||||
B04_Surface_Map_None: ["없음", "None"],
|
||||
B04_Surface_Map_PlannedRoute: ["계획선", "Planned route"],
|
||||
/* 배수유역 분석 오버레이(관리자 확인용) — 조작 문구만 다국어로 둔다.
|
||||
결과 상세 판독문(regionSummary)은 진단용 원문이라 그대로 둔다. */
|
||||
B04_Surface_Watershed_Btn: ["유역 분석", "Basin analysis"],
|
||||
B04_Surface_Watershed_Btn_Tip: [
|
||||
"계획 노선(B03 업로드)과 도엽 등고선·세류선으로 배수유역을 처음부터 다시 분석합니다. 30초 안팎이 걸리며 결과는 영구저장소에 남습니다. 저장된 결과는 지도를 열 때 자동으로 표시되므로, 조건을 바꿨을 때만 누르면 됩니다.",
|
||||
"Re-runs the drainage analysis from scratch using the planned route (uploaded in B03) and the sheet contours and streams. It takes about 30 seconds and the result is stored permanently. A stored result is shown automatically when the map opens, so press this only after changing the inputs.",
|
||||
],
|
||||
B04_Surface_Watershed_Btn_Analyzing: ["분석 중…", "Analyzing…"],
|
||||
B04_Surface_Watershed_Btn_Loading: ["불러오는 중…", "Loading…"],
|
||||
B04_Surface_Watershed_Part_Primary: ["1차 유역", "Primary region"],
|
||||
B04_Surface_Watershed_Part_Basin: ["2차 유역", "Full basin"],
|
||||
B04_Surface_Watershed_Part_Flow: ["유역 방향", "Flow cells"],
|
||||
B04_Surface_Watershed_Part_Arrows: ["평균 흐름", "Mean flow"],
|
||||
B04_Surface_Watershed_Status_Analyzing: [
|
||||
"배수유역을 처음부터 다시 분석하는 중입니다. 30초 안팎 걸립니다…",
|
||||
"Re-running the drainage analysis from scratch. This takes about 30 seconds…",
|
||||
],
|
||||
B04_Surface_Watershed_Status_Loading: [
|
||||
"저장된 배수유역 분석을 불러오는 중…",
|
||||
"Loading the stored drainage analysis…",
|
||||
],
|
||||
B04_Surface_Watershed_LoadFailed: [
|
||||
"배수유역을 불러오지 못했습니다.",
|
||||
"Failed to load the drainage analysis.",
|
||||
],
|
||||
/* {message}=원인 */
|
||||
B04_Surface_Watershed_Failed: ["유역 분석 실패: {message}", "Basin analysis failed: {message}"],
|
||||
B04_Surface_Watershed_NoSaved: [
|
||||
"저장된 배수유역 분석이 없습니다. [유역 분석]을 누르세요.",
|
||||
"No stored drainage analysis. Press [Basin analysis].",
|
||||
],
|
||||
B04_Surface_Watershed_Origin_Cached: ["저장분", "Cached"],
|
||||
/* {seconds}=재산정에 걸린 시간(초) */
|
||||
B04_Surface_Watershed_Origin_Recomputed: ["재산정 {seconds}초", "Recomputed in {seconds}s"],
|
||||
B04_Surface_Map_PlannedRouteEmpty: [
|
||||
"B03에서 계획노선 파일을 올리면 표시됩니다.",
|
||||
"Shown after the planned route file is uploaded in B03.",
|
||||
@@ -691,6 +728,65 @@ export const ui_locales = {
|
||||
B04_Surface_Analyze_Failed: ["지표면 분석에 실패했습니다.", "Surface analysis failed."],
|
||||
B04_Surface_Load_Failed: ["모델 목록을 불러오지 못했습니다.", "Failed to load models."],
|
||||
|
||||
/* --- B05_wf2_Route 배수유역도 패널 (하단 패널 안쪽 우측) --- */
|
||||
B05_Drainage_Title: ["배수유역도", "Drainage Basins"],
|
||||
B05_Drainage_Layer_Satellite: ["위성사진", "Satellite"],
|
||||
B05_Drainage_Layer_Satellite_Tip: [
|
||||
"배경 위성사진을 보이거나 숨깁니다.",
|
||||
"Show or hide the satellite basemap.",
|
||||
],
|
||||
B05_Drainage_Layer_Contour: ["등고선", "Contours"],
|
||||
B05_Drainage_Layer_Stream: ["세류", "Streams"],
|
||||
B05_Drainage_Layer_Arrows: ["흐름 화살표", "Flow arrows"],
|
||||
B05_Drainage_Layer_Arrows_Tip: [
|
||||
"B04에서 산출한 평균 흐름 방향을 보이거나 숨깁니다.",
|
||||
"Show or hide the mean flow direction computed in B04.",
|
||||
],
|
||||
B05_Drainage_Layer_Upstream: ["상류 세류", "Upstream streams"],
|
||||
B05_Drainage_Layer_Upstream_Tip: [
|
||||
"유역 안쪽 상류 세류망을 굵게 강조합니다.",
|
||||
"Emphasize the upstream stream network inside the basin.",
|
||||
],
|
||||
B05_Drainage_Btn_Analyze: ["세부유역 산정", "Compute sub-basins"],
|
||||
B05_Drainage_Btn_Analyze_Tip: [
|
||||
"B04에서 분석해 둔 배수유역을 불러와 관을 보충하고 세부유역을 나눕니다. 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.",
|
||||
"Loads the drainage analysis from B04, fills in culverts, and splits sub-basins. Run the analysis in B04 first if none exists.",
|
||||
],
|
||||
B05_Drainage_Btn_PipeEdit: ["배관 편집", "Edit culverts"],
|
||||
B05_Drainage_Btn_DeleteSelected: ["선택 삭제", "Delete selected"],
|
||||
B05_Drainage_Btn_Auto: ["자동 제안", "Auto suggestion"],
|
||||
B05_Drainage_Btn_BoundaryEdit: ["유역선 편집", "Edit basin outline"],
|
||||
B05_Drainage_Btn_BoundaryEdit_Tip: [
|
||||
"2차 전체 배수유역 외곽선 위 포인트를 끌어 경계를 고칩니다. 옮긴 값은 종단 경로 확정 시 저장 여부를 묻습니다.",
|
||||
"Drag the points on the overall basin outline to correct it. You will be asked whether to save the moved points when the route is confirmed.",
|
||||
],
|
||||
B05_Drainage_ImageAlt: ["배경 위성지도", "Satellite basemap"],
|
||||
B05_Drainage_Status_NeedRoute: [
|
||||
"노선을 확정하면 배수유역도가 표시됩니다.",
|
||||
"The drainage map appears once the route is confirmed.",
|
||||
],
|
||||
B05_Drainage_Status_Analyzing: ["세부유역을 산정하는 중…", "Computing sub-basins…"],
|
||||
B05_Drainage_Status_NoBasin: ["산정된 배수유역이 없습니다.", "No drainage basin was computed."],
|
||||
B05_Drainage_Status_AnalyzeFailed: [
|
||||
"세부유역 산정에 실패했습니다.",
|
||||
"Failed to compute sub-basins.",
|
||||
],
|
||||
B05_Drainage_Status_LoadingBase: ["배경도를 불러오는 중…", "Loading the basemap…"],
|
||||
B05_Drainage_Status_LoadingSheets: ["도엽 레이어를 불러오는 중…", "Loading map sheet layers…"],
|
||||
B05_Drainage_Status_NoSheets: [
|
||||
"도엽 레이어가 없습니다. B04에서 임포트하세요.",
|
||||
"No map sheet layer found. Import them in B04.",
|
||||
],
|
||||
B05_Drainage_Status_LoadFailed: ["배경도를 불러오지 못했습니다.", "Failed to load the basemap."],
|
||||
B05_Drainage_Basin_Undecided: ["미정", "TBD"],
|
||||
/* {area}=면적, {relief}=표고차, {flow}=유하장, {pipe}=관경 */
|
||||
B05_Drainage_Basin_Metrics: [
|
||||
"면적 {area} · 표고 {relief}m · 유하 {flow}m · 관경 {pipe}",
|
||||
"Area {area} · Relief {relief}m · Flow {flow}m · Pipe {pipe}",
|
||||
],
|
||||
/* {chainage}=측점 누가거리(m) */
|
||||
B05_Drainage_Basin_Chainage: ["측점 누가거리 {chainage}m", "Station chainage {chainage}m"],
|
||||
|
||||
/* --- B05_wf2_Route 경로 설계 --- */
|
||||
B05_Route_Title: ["종단설계", "Profile Design"],
|
||||
B05_Route_Group_Points: ["경로 제어점", "Route Control Points"],
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/* =============================================================================
|
||||
* ui_template_palette.ts
|
||||
* 캔버스(2D 지도)용 색 조회 — 색 값의 정의처는 `ui_template_theme.css` 하나뿐이다.
|
||||
*
|
||||
* CSS로 칠할 수 없는 `<canvas>` 그림도 색은 테마 변수에서 읽어야 화면마다 값이
|
||||
* 갈라지지 않는다. `getComputedStyle`은 호출할 때마다 스타일 재계산을 유발하므로
|
||||
* 한 번 읽은 값은 캐시하고, 테마가 바뀔 때(`data-theme` 변경)만 비운다.
|
||||
* ========================================================================== */
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
let watching = false;
|
||||
|
||||
/** 테마가 바뀌면 캐시를 버린다 — 다음 렌더에서 새 값으로 다시 읽는다. */
|
||||
function watchThemeChange(): void {
|
||||
if (watching || typeof MutationObserver === "undefined") return;
|
||||
watching = true;
|
||||
new MutationObserver(() => cache.clear()).observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테마 CSS 변수 하나를 읽는다.
|
||||
*
|
||||
* @param name 변수 이름(`--map-route` 처럼 두 하이픈까지 포함)
|
||||
* @param fallback 스타일이 아직 붙기 전(초기 렌더)이나 변수가 없을 때 쓸 값
|
||||
*/
|
||||
export function themeColor(name: string, fallback: string): string {
|
||||
const cached = cache.get(name);
|
||||
if (cached !== undefined) return cached;
|
||||
watchThemeChange();
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const resolved = value || fallback;
|
||||
// 값이 비어 있으면(스타일 미적용) 캐시하지 않는다 — 다음 렌더에서 다시 읽게 둔다.
|
||||
if (value) cache.set(name, resolved);
|
||||
return resolved;
|
||||
}
|
||||
@@ -191,6 +191,59 @@
|
||||
--z-overlay: 900;
|
||||
--z-modal: 1000;
|
||||
--z-toast: 1100;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 9. 2D 지도 벡터 팔레트 (B04 배경지도 · B05 배수유역도 공용)
|
||||
* 캔버스에 그리는 색이라 CSS로 직접 칠할 수 없다 — TS가 이 변수를 읽어 쓴다
|
||||
* (`ui_template_palette.ts`). 여기가 유일한 정의처이며 화면별 복제 금지.
|
||||
* 배경이 위성사진이라 라이트/다크 공통으로 같은 색을 쓴다.
|
||||
* ------------------------------------------------------------------------ */
|
||||
--map-route: #f97316; /* 계획선 (B04 지도·B05 배수유역도 공통) */
|
||||
--map-cadastral: #f97316;
|
||||
--map-sigungu: #7c3aed;
|
||||
--map-eupmyeondong: #22c55e;
|
||||
--map-contour: #fdba74;
|
||||
--map-sheet-contour: #a5b4fc;
|
||||
--map-sheet-stream: #2563eb;
|
||||
--map-sheet-elev-point: #f9a8d4;
|
||||
--map-sheet-cutfill: #f43f5e;
|
||||
--map-sheet-wall: #0f766e;
|
||||
--map-flow-arrow: #7c3aed;
|
||||
--map-upstream: rgba(29, 78, 216, 0.95); /* 상류 세류망 강조선 */
|
||||
--map-upstream-toggle: #1d4ed8; /* 토글 버튼 색띠 */
|
||||
--map-satellite-toggle: #64748b; /* 배경사진은 선 색이 없어 중립 회색 */
|
||||
--map-pipe-orphan: #e5e7eb; /* 유역이 없는 배관 마커 */
|
||||
--map-pipe-marker: rgba(249, 115, 22, 0.95); /* B04 기본 관 마커 */
|
||||
|
||||
/* 배수유역 오버레이(B04 지도) */
|
||||
--map-halo: rgba(255, 255, 255, 0.9); /* 선·글자 뒤에 까는 흰 테두리 */
|
||||
--map-label-text: #1f2937; /* 지도 위 라벨 글자 */
|
||||
--map-marker-text: #111827; /* 관 마커 번호 글자·선택된 마커 테두리 */
|
||||
--map-marker-outline: #374151; /* 선택되지 않은 마커 테두리 */
|
||||
--map-basin-outline: #92400e; /* 2차 유역 외곽선(분수령) */
|
||||
--map-basin-ring: rgba(146, 64, 14, 0.95);
|
||||
--map-boundary-handle: rgba(146, 64, 14, 0.95); /* 유역선 편집 핸들 */
|
||||
--map-boundary-handle-moved: rgba(220, 38, 38, 0.95); /* 사용자가 옮긴 핸들 */
|
||||
--map-primary-region: #059669; /* 1차 유역 */
|
||||
--map-flow-region: #2563eb; /* 유역 방향 토글 */
|
||||
--map-grid-line: rgba(255, 255, 255, 0.55); /* 해석 격자 셀 선 */
|
||||
--map-flow-to-road-fill: rgba(220, 38, 38, 0.28);
|
||||
--map-flow-to-road-line: rgba(153, 27, 27, 0.95);
|
||||
--map-flow-away-fill: rgba(37, 99, 235, 0.22);
|
||||
--map-flow-away-line: rgba(255, 255, 255, 0.95);
|
||||
--map-flow-away-arrow: rgba(30, 64, 175, 0.95);
|
||||
--map-flow-unknown-fill: rgba(120, 113, 108, 0.18); /* 표고가 없어 판정 못한 셀 */
|
||||
|
||||
/* 유역 채움 파스텔 8종 — 유역 번호 순으로 돌려쓴다(사용자 지시: 파스텔톤).
|
||||
알파 0.45는 비선택 유역을 0.18로 낮출 때 문자열로 치환하므로 형식을 바꾸지 말 것. */
|
||||
--map-basin-1: rgba(167, 216, 199, 0.45);
|
||||
--map-basin-2: rgba(247, 208, 168, 0.45);
|
||||
--map-basin-3: rgba(186, 199, 240, 0.45);
|
||||
--map-basin-4: rgba(241, 183, 199, 0.45);
|
||||
--map-basin-5: rgba(214, 226, 168, 0.45);
|
||||
--map-basin-6: rgba(202, 186, 227, 0.45);
|
||||
--map-basin-7: rgba(168, 214, 232, 0.45);
|
||||
--map-basin-8: rgba(240, 219, 168, 0.45);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user