260718_7
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*
|
||||
* 백엔드 계약 (B06_wf3_ProfileCross_Router.py):
|
||||
* POST /api/projects/{project_id}/sections/generate → 종횡단 생성 + DB 기록
|
||||
* GET /api/projects/{project_id}/sections/context → 확정 경로 + 기본 옵션
|
||||
* GET /api/projects/{project_id}/sections/{route_id} → 종단 요약 조회
|
||||
* POST /api/projects/{project_id}/sections/{route_id}/confirm → 종횡단 확정
|
||||
*
|
||||
@@ -38,12 +39,31 @@ export interface SectionGenerateResponse {
|
||||
longitudinal_file_path: string;
|
||||
}
|
||||
|
||||
export interface SectionOptionDefaults {
|
||||
station_interval_m: number;
|
||||
cross_half_width_m: number;
|
||||
cross_sample_interval_m: number;
|
||||
long_sample_interval_m: number;
|
||||
}
|
||||
|
||||
export interface SectionContextResponse {
|
||||
project_id: string;
|
||||
route_id: number | null;
|
||||
filter_key: string | null;
|
||||
method: string | null;
|
||||
smooth: boolean | null;
|
||||
crs_epsg: number | null;
|
||||
defaults: SectionOptionDefaults;
|
||||
}
|
||||
|
||||
/** 종단 요약 조회 결과 (SectionSummaryResponse) */
|
||||
export interface SectionSummaryResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
route_id: number;
|
||||
longitudinal: Record<string, unknown> | null;
|
||||
length_m: number | null;
|
||||
cross_section_count: number;
|
||||
}
|
||||
|
||||
/** 종횡단 확정 결과 (SectionConfirmResponse) */
|
||||
@@ -89,6 +109,13 @@ export async function generateSections(
|
||||
});
|
||||
}
|
||||
|
||||
/** 최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 조회한다. */
|
||||
export async function fetchSectionContext(projectId: string): Promise<SectionContextResponse> {
|
||||
return requestJson<SectionContextResponse>(`/projects/${projectId}/sections/context`, {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
/** 경로의 종단면 요약을 조회한다. */
|
||||
export async function getSections(
|
||||
projectId: string,
|
||||
|
||||
@@ -24,6 +24,31 @@ def _validate_stage_path(relative_path: str) -> str:
|
||||
return normalized.as_posix()
|
||||
|
||||
|
||||
async def get_confirmed_route_context(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> dict[str, Any] | None:
|
||||
"""프로젝트의 최신 확정 경로와 연결된 지표면 좌표계를 조회한다."""
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT r.id AS route_id, sm.crs_epsg
|
||||
FROM routes r
|
||||
LEFT JOIN surface_models sm ON sm.id = r.surface_model_id
|
||||
WHERE r.project_id = %s AND r.status = 'CONFIRMED'
|
||||
ORDER BY r.computed_at DESC, r.id DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(str(project_id),),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"route_id": int(row["route_id"]),
|
||||
"crs_epsg": int(row["crs_epsg"]) if row["crs_epsg"] is not None else None,
|
||||
}
|
||||
|
||||
|
||||
async def delete_sections_for_route(connection: aiomysql.Connection, route_id: int) -> None:
|
||||
"""경로 재생성 전에 기존 종횡단 레코드를 삭제한다 (멱등 재실행)."""
|
||||
async with connection.cursor() as cursor:
|
||||
@@ -113,7 +138,7 @@ async def get_longitudinal_section(
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT id, longitudinal_file_path, status, computed_at
|
||||
SELECT id, data, longitudinal_file_path, status, computed_at
|
||||
FROM longitudinal_sections
|
||||
WHERE project_id = %s AND route_id = %s
|
||||
ORDER BY id DESC
|
||||
@@ -124,14 +149,26 @@ async def get_longitudinal_section(
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
data = row[1]
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
return {
|
||||
"id": int(row[0]),
|
||||
"longitudinal_file_path": row[1],
|
||||
"status": row[2],
|
||||
"computed_at": row[3].isoformat() if row[3] else None,
|
||||
"data": data if isinstance(data, dict) else None,
|
||||
"longitudinal_file_path": row[2],
|
||||
"status": row[3],
|
||||
"computed_at": row[4].isoformat() if row[4] else None,
|
||||
}
|
||||
|
||||
|
||||
async def count_cross_sections(connection: aiomysql.Connection, route_id: int) -> int:
|
||||
"""경로에 저장된 횡단면 개수를 반환한다."""
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute("SELECT COUNT(*) FROM cross_sections WHERE route_id = %s", (route_id,))
|
||||
row = await cursor.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
|
||||
async def confirm_sections_for_route(connection: aiomysql.Connection, route_id: int) -> None:
|
||||
"""경로의 종횡단면 상태를 CONFIRMED로 변경한다."""
|
||||
async with connection.cursor() as cursor:
|
||||
|
||||
@@ -14,18 +14,23 @@ from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine import run_section_generat
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine_Section import SectionGenerationOptions
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
|
||||
confirm_sections_for_route,
|
||||
count_cross_sections,
|
||||
create_longitudinal_section,
|
||||
delete_sections_for_route,
|
||||
get_confirmed_route_context,
|
||||
get_longitudinal_section,
|
||||
insert_cross_sections,
|
||||
)
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import (
|
||||
SectionConfirmResponse,
|
||||
SectionContextResponse,
|
||||
SectionGenerateRequest,
|
||||
SectionGenerateResponse,
|
||||
SectionOptionDefaults,
|
||||
SectionSummaryResponse,
|
||||
)
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_workflow_state import complete_stage, fail_stage, start_stage
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
@@ -148,6 +153,38 @@ async def generate_sections(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/sections/context", response_model=SectionContextResponse)
|
||||
async def get_section_context(project_id: UUID) -> SectionContextResponse | JSONResponse:
|
||||
"""최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 반환한다."""
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
route_context = await get_confirmed_route_context(connection, project_id)
|
||||
surface_params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
|
||||
defaults = SectionGenerationOptions()
|
||||
return SectionContextResponse(
|
||||
project_id=str(project_id),
|
||||
route_id=route_context["route_id"] if route_context else None,
|
||||
filter_key=surface_params["source_filter"] if route_context else None,
|
||||
method=surface_params["method"] if route_context else None,
|
||||
smooth=bool(surface_params["smooth"]) if route_context else None,
|
||||
crs_epsg=route_context["crs_epsg"] if route_context else None,
|
||||
defaults=SectionOptionDefaults(
|
||||
station_interval_m=defaults.station_interval_m,
|
||||
cross_half_width_m=defaults.cross_half_width_m,
|
||||
cross_sample_interval_m=defaults.cross_sample_interval_m,
|
||||
long_sample_interval_m=defaults.long_sample_interval_m,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B06 종횡단 컨텍스트 조회 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "종횡단 컨텍스트 조회 중 오류가 발생했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/sections/{route_id}", response_model=SectionSummaryResponse)
|
||||
async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryResponse | JSONResponse:
|
||||
"""경로의 종단면 요약을 조회한다."""
|
||||
@@ -155,8 +192,15 @@ async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryRespons
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
longitudinal = await get_longitudinal_section(connection, project_id, route_id)
|
||||
cross_section_count = await count_cross_sections(connection, route_id)
|
||||
data = longitudinal.get("data") if longitudinal else None
|
||||
length_m = data.get("length_m") if isinstance(data, dict) else None
|
||||
return SectionSummaryResponse(
|
||||
project_id=str(project_id), route_id=route_id, longitudinal=longitudinal
|
||||
project_id=str(project_id),
|
||||
route_id=route_id,
|
||||
longitudinal=longitudinal,
|
||||
length_m=length_m,
|
||||
cross_section_count=cross_section_count,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B06 종횡단 조회 실패: project_id=%s", project_id)
|
||||
|
||||
@@ -44,6 +44,27 @@ class SectionConfirmResponse(BaseModel):
|
||||
confirmed: bool = True
|
||||
|
||||
|
||||
class SectionOptionDefaults(BaseModel):
|
||||
"""config에서 읽은 종횡단 생성 기본 옵션."""
|
||||
|
||||
station_interval_m: float
|
||||
cross_half_width_m: float
|
||||
cross_sample_interval_m: float
|
||||
long_sample_interval_m: float
|
||||
|
||||
|
||||
class SectionContextResponse(BaseModel):
|
||||
"""B06 진입 시 필요한 확정 경로 컨텍스트와 기본 옵션."""
|
||||
|
||||
project_id: str
|
||||
route_id: int | None = None
|
||||
filter_key: str | None = None
|
||||
method: str | None = None
|
||||
smooth: bool | None = None
|
||||
crs_epsg: int | None = None
|
||||
defaults: SectionOptionDefaults
|
||||
|
||||
|
||||
class SectionSummaryResponse(BaseModel):
|
||||
"""종횡단 요약 조회 결과."""
|
||||
|
||||
@@ -51,3 +72,5 @@ class SectionSummaryResponse(BaseModel):
|
||||
project_id: str
|
||||
route_id: int
|
||||
longitudinal: dict[str, Any] | None = None
|
||||
length_m: float | None = None
|
||||
cross_section_count: int = 0
|
||||
|
||||
@@ -30,8 +30,13 @@ import {
|
||||
} from "../A00_Common/b_workflow_nav";
|
||||
import {
|
||||
confirmSections,
|
||||
fetchSectionContext,
|
||||
generateSections,
|
||||
getSections,
|
||||
type SectionContextResponse,
|
||||
type SectionGenerateRequest,
|
||||
type SectionGenerateResponse,
|
||||
type SectionSummaryResponse,
|
||||
} from "./B06_wf3_ProfileCross_Api_Fetch";
|
||||
import "./B06_wf3_ProfileCross_UI_Style.css";
|
||||
|
||||
@@ -51,6 +56,18 @@ function buildGroup(legend: string): HTMLElement {
|
||||
return group;
|
||||
}
|
||||
|
||||
/** 읽기 전용 경로 컨텍스트 표시 행. */
|
||||
function buildInfoLine(label: string): { root: HTMLElement; value: HTMLElement } {
|
||||
const root = document.createElement("div");
|
||||
root.className = "b06-profile__info-line";
|
||||
const key = document.createElement("span");
|
||||
key.textContent = label;
|
||||
const value = document.createElement("strong");
|
||||
value.textContent = "-";
|
||||
root.append(key, value);
|
||||
return { root, value };
|
||||
}
|
||||
|
||||
/** 숫자 입력값을 파싱. 빈 값이면 null. */
|
||||
function parseNumber(value: string): number | null {
|
||||
const trimmed = value.trim();
|
||||
@@ -61,26 +78,22 @@ function parseNumber(value: string): number | null {
|
||||
|
||||
export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
let currentRouteId: number | null = null;
|
||||
let sectionContext: SectionContextResponse | null = null;
|
||||
|
||||
/* ---- 좌측: 대상 경로 ---- */
|
||||
const routeGroup = buildGroup(L("B06_Profile_Group_Route"));
|
||||
const routeIdField = createInputField({
|
||||
label: L("B06_Profile_Field_RouteId"),
|
||||
type: "number",
|
||||
min: 1,
|
||||
});
|
||||
const filterField = createInputField({ label: L("B06_Profile_Field_Filter"), type: "text" });
|
||||
const methodField = createInputField({
|
||||
label: L("B06_Profile_Field_Method"),
|
||||
type: "text",
|
||||
value: "dtm",
|
||||
});
|
||||
const crsField = createInputField({
|
||||
label: L("B06_Profile_Field_Crs"),
|
||||
type: "text",
|
||||
placeholder: "EPSG:5178",
|
||||
});
|
||||
routeGroup.append(routeIdField.root, filterField.root, methodField.root, crsField.root);
|
||||
const routeIdInfo = buildInfoLine(L("B06_Profile_Field_RouteId"));
|
||||
const filterInfo = buildInfoLine(L("B06_Profile_Field_Filter"));
|
||||
const methodInfo = buildInfoLine(L("B06_Profile_Field_Method"));
|
||||
const smoothInfo = buildInfoLine(L("B06_Profile_Field_Smooth"));
|
||||
const crsInfo = buildInfoLine(L("B06_Profile_Field_Crs"));
|
||||
routeGroup.append(
|
||||
routeIdInfo.root,
|
||||
filterInfo.root,
|
||||
methodInfo.root,
|
||||
smoothInfo.root,
|
||||
crsInfo.root,
|
||||
);
|
||||
|
||||
/* ---- 좌측: 측점/횡단 옵션 ---- */
|
||||
const optionGroup = buildGroup(L("B06_Profile_Group_Options"));
|
||||
@@ -101,20 +114,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
type: "number",
|
||||
});
|
||||
|
||||
const smoothLabel = document.createElement("label");
|
||||
smoothLabel.className = "b06-profile__check";
|
||||
const smoothBox = document.createElement("input");
|
||||
smoothBox.type = "checkbox";
|
||||
const smoothText = document.createElement("span");
|
||||
smoothText.textContent = L("B06_Profile_Field_Smooth");
|
||||
smoothLabel.append(smoothBox, smoothText);
|
||||
|
||||
optionGroup.append(
|
||||
stationField.root,
|
||||
halfWidthField.root,
|
||||
crossSampleField.root,
|
||||
longSampleField.root,
|
||||
smoothLabel,
|
||||
);
|
||||
|
||||
const generateButton = createButton({
|
||||
@@ -150,6 +154,14 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
resultBody.append(empty);
|
||||
}
|
||||
|
||||
function renderResultMessage(message: string): void {
|
||||
resultBody.replaceChildren();
|
||||
const text = document.createElement("p");
|
||||
text.className = "b06-profile__empty";
|
||||
text.textContent = message;
|
||||
resultBody.append(text);
|
||||
}
|
||||
|
||||
function metricRow(label: string, value: string): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "b06-profile__metric";
|
||||
@@ -172,6 +184,19 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSummary(result: SectionSummaryResponse): void {
|
||||
const path = result.longitudinal?.longitudinal_file_path;
|
||||
resultBody.replaceChildren();
|
||||
resultBody.append(
|
||||
metricRow(
|
||||
L("B06_Profile_Result_Length"),
|
||||
result.length_m === null ? "-" : result.length_m.toFixed(2),
|
||||
),
|
||||
metricRow(L("B06_Profile_Result_CrossCount"), String(result.cross_section_count)),
|
||||
metricRow(L("B06_Profile_Result_Path"), typeof path === "string" ? path : "-"),
|
||||
);
|
||||
}
|
||||
|
||||
const resultCard = document.createElement("div");
|
||||
resultCard.className = "b06-profile__result";
|
||||
resultCard.append(resultTitle, resultBody);
|
||||
@@ -183,43 +208,45 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
async function onB06_Profile_Generate_Click(): Promise<void> {
|
||||
function buildGenerateRequest(): SectionGenerateRequest | null {
|
||||
if (
|
||||
sectionContext?.route_id === null ||
|
||||
!sectionContext?.route_id ||
|
||||
!sectionContext.filter_key ||
|
||||
!sectionContext.method
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
route_id: sectionContext.route_id,
|
||||
filter_key: sectionContext.filter_key,
|
||||
method: sectionContext.method,
|
||||
smooth: sectionContext.smooth ?? false,
|
||||
crs: sectionContext.crs_epsg === null ? null : `EPSG:${sectionContext.crs_epsg}`,
|
||||
station_interval_m: parseNumber(stationField.input.value),
|
||||
cross_half_width_m: parseNumber(halfWidthField.input.value),
|
||||
cross_sample_interval_m: parseNumber(crossSampleField.input.value),
|
||||
long_sample_interval_m: parseNumber(longSampleField.input.value),
|
||||
};
|
||||
}
|
||||
|
||||
async function onB06_Profile_Generate_Click(): Promise<boolean> {
|
||||
const projectId = getProjectId();
|
||||
if (!projectId) return;
|
||||
|
||||
const routeId = parseNumber(routeIdField.input.value);
|
||||
if (routeId === null || !Number.isInteger(routeId) || routeId <= 0) {
|
||||
routeIdField.setError(L("B06_Profile_Error_RouteId"));
|
||||
return;
|
||||
}
|
||||
routeIdField.setError();
|
||||
|
||||
const filterKey = filterField.input.value.trim();
|
||||
if (!filterKey) {
|
||||
filterField.setError(L("B06_Profile_Error_Filter"));
|
||||
return;
|
||||
}
|
||||
filterField.setError();
|
||||
if (!projectId) return false;
|
||||
const request = buildGenerateRequest();
|
||||
if (!request) return false;
|
||||
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const result = await generateSections(projectId, {
|
||||
route_id: routeId,
|
||||
filter_key: filterKey,
|
||||
method: methodField.input.value.trim() || "dtm",
|
||||
smooth: smoothBox.checked,
|
||||
crs: crsField.input.value.trim() || null,
|
||||
station_interval_m: parseNumber(stationField.input.value),
|
||||
cross_half_width_m: parseNumber(halfWidthField.input.value),
|
||||
cross_sample_interval_m: parseNumber(crossSampleField.input.value),
|
||||
long_sample_interval_m: parseNumber(longSampleField.input.value),
|
||||
});
|
||||
const result = await generateSections(projectId, request);
|
||||
currentRouteId = result.route_id;
|
||||
renderResult(result);
|
||||
showToast(L("B06_Profile_Generate_Success"), "success");
|
||||
return true;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : L("B06_Profile_Generate_Failed");
|
||||
showToast(`${L("B06_Profile_Generate_Failed")} ${detail}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
@@ -228,11 +255,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
async function onB06_Profile_Confirm_Click(): Promise<void> {
|
||||
const projectId = getProjectId();
|
||||
if (!projectId) return;
|
||||
const routeId = currentRouteId ?? parseNumber(routeIdField.input.value);
|
||||
if (routeId === null || !Number.isInteger(routeId) || routeId <= 0) {
|
||||
routeIdField.setError(L("B06_Profile_Error_RouteId"));
|
||||
return;
|
||||
}
|
||||
const routeId = currentRouteId;
|
||||
if (routeId === null) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await confirmSections(projectId, routeId);
|
||||
@@ -250,11 +274,12 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
let workflowState: WorkflowState | undefined;
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
if (projectId) {
|
||||
try {
|
||||
workflowState = await fetchWorkflowState(projectId);
|
||||
} catch {
|
||||
/* 조회 실패 시 stages 미전달 → 전체 이동 허용 */
|
||||
}
|
||||
const [contextResult, workflowResult] = await Promise.allSettled([
|
||||
fetchSectionContext(projectId),
|
||||
fetchWorkflowState(projectId),
|
||||
]);
|
||||
if (contextResult.status === "fulfilled") sectionContext = contextResult.value;
|
||||
if (workflowResult.status === "fulfilled") workflowState = workflowResult.value;
|
||||
}
|
||||
|
||||
const layout = createWorkflowLayout({
|
||||
@@ -273,4 +298,45 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
},
|
||||
});
|
||||
root.replaceChildren(layout.root);
|
||||
|
||||
if (!projectId || !sectionContext) {
|
||||
generateButton.disabled = true;
|
||||
confirmButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const context = sectionContext;
|
||||
routeIdInfo.value.textContent = context.route_id === null ? "-" : String(context.route_id);
|
||||
filterInfo.value.textContent = context.filter_key ?? "-";
|
||||
methodInfo.value.textContent = context.method ?? "-";
|
||||
smoothInfo.value.textContent = context.smooth === null ? "-" : context.smooth ? "ON" : "OFF";
|
||||
crsInfo.value.textContent = context.crs_epsg === null ? "-" : `EPSG:${context.crs_epsg}`;
|
||||
stationField.input.value = String(context.defaults.station_interval_m);
|
||||
halfWidthField.input.value = String(context.defaults.cross_half_width_m);
|
||||
crossSampleField.input.value = String(context.defaults.cross_sample_interval_m);
|
||||
longSampleField.input.value = String(context.defaults.long_sample_interval_m);
|
||||
|
||||
if (context.route_id === null) {
|
||||
generateButton.disabled = true;
|
||||
confirmButton.disabled = true;
|
||||
renderResultMessage(L("B06_Profile_No_Confirmed_Route"));
|
||||
return;
|
||||
}
|
||||
|
||||
currentRouteId = context.route_id;
|
||||
confirmButton.disabled = true;
|
||||
try {
|
||||
const existing = await getSections(projectId, context.route_id);
|
||||
if (existing.longitudinal) {
|
||||
renderSummary(existing);
|
||||
confirmButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
renderResultMessage(L("B06_Profile_Auto_Generating"));
|
||||
const generated = await onB06_Profile_Generate_Click();
|
||||
confirmButton.disabled = !generated;
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : L("B06_Profile_Generate_Failed");
|
||||
showToast(`${L("B06_Profile_Generate_Failed")} ${detail}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,23 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* --- 체크박스 --- */
|
||||
.b06-profile__check {
|
||||
/* --- 읽기 전용 경로 컨텍스트 --- */
|
||||
.b06-profile__info-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-body);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-profile__check input {
|
||||
accent-color: var(--color-primary);
|
||||
.b06-profile__info-line span {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b06-profile__info-line strong {
|
||||
font-family: var(--font-mono);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* --- 액션 버튼 행 --- */
|
||||
|
||||
@@ -732,17 +732,25 @@ export const ui_locales = {
|
||||
B06_Profile_Field_RouteId: ["경로 ID (routes.id)", "Route ID"],
|
||||
B06_Profile_Field_Filter: ["지면 필터", "Ground filter"],
|
||||
B06_Profile_Field_Method: ["지표면 표현", "Surface method"],
|
||||
B06_Profile_Field_Crs: ["좌표계 (선택)", "CRS (optional)"],
|
||||
B06_Profile_Field_Crs: ["좌표계", "CRS"],
|
||||
B06_Profile_Group_Options: ["측점·횡단 옵션", "Station & Cross Options"],
|
||||
B06_Profile_Field_StationInterval: ["측점 간격(m)", "Station interval (m)"],
|
||||
B06_Profile_Field_CrossHalfWidth: ["횡단 반폭(m)", "Cross half-width (m)"],
|
||||
B06_Profile_Field_CrossSample: ["횡단 샘플 간격(m)", "Cross sample interval (m)"],
|
||||
B06_Profile_Field_LongSample: ["종단 샘플 간격(m)", "Long sample interval (m)"],
|
||||
B06_Profile_Field_Smooth: ["지표면 스무딩", "Smooth surface"],
|
||||
B06_Profile_Btn_Generate: ["종·횡단 생성", "Generate Sections"],
|
||||
B06_Profile_Btn_Generate: ["종·횡단 재생성", "Regenerate Sections"],
|
||||
B06_Profile_Btn_Confirm: ["종·횡단 확정", "Confirm Sections"],
|
||||
B06_Profile_Result_Title: ["종·횡단 생성 결과", "Section Result"],
|
||||
B06_Profile_Result_Empty: ["아직 생성된 종·횡단이 없습니다.", "No sections generated yet."],
|
||||
B06_Profile_No_Confirmed_Route: [
|
||||
"확정된 경로가 없습니다. 먼저 경로를 확정하세요.",
|
||||
"No confirmed route. Confirm a route first.",
|
||||
],
|
||||
B06_Profile_Auto_Generating: [
|
||||
"기본 옵션으로 종·횡단을 자동 생성하고 있습니다.",
|
||||
"Generating sections automatically with the default options.",
|
||||
],
|
||||
B06_Profile_Result_Length: ["종단 연장(m)", "Longitudinal length (m)"],
|
||||
B06_Profile_Result_CrossCount: ["횡단 개수", "Cross-section count"],
|
||||
B06_Profile_Result_Path: ["종단 파일", "Longitudinal file"],
|
||||
|
||||
Reference in New Issue
Block a user