diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 5462c2491..6f95b8585 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -106,8 +106,6 @@ export const STATE_REGISTRY = { "input-prefs": { bucket: "pref", scope: "global" }, /** B05 3D 영역 투영(316-2) `perspective · ortho` · 없으면 원근. */ "b05-3d-projection": { bucket: "pref", scope: "global" }, - /** 배수유역 계산 `manual · auto`(347-2) · 없으면 수동. */ - "drainage-mode": { bucket: "pref", scope: "global" }, "section-panel-height": { bucket: "pref", scope: "global", diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 2e1ff00ae..f19ec6448 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -410,9 +410,22 @@ async function alreadyConfirmed(projectId: string): Promise { /** B06 [>] · 단계 판 B07~B09(496-1) — 확정 뒤 그 주소 · 실패 · 자료 서기 전 = 머묾. */ export function confirmAndGo(shared: DesignPageShared, route: RoutePath): void { if (!shared.ready) return void showToast(L("Design_Next_NotReady"), "info"); - void alreadyConfirmed(shared.projectId).then((skip) => - skip ? goToWorkflowStage(shared.projectId, route) : saveSharedDesign(shared, true, route), - ); + const go = () => + alreadyConfirmed(shared.projectId).then((skip) => + skip ? goToWorkflowStage(shared.projectId, route) : saveSharedDesign(shared, true, route), + ); + void drainageFirst(shared).then((ready) => (ready ? go() : undefined)); +} + +/** 배수유역도가 낡은 채 B07 로 — 확인 창 → [업데이트] 끝난 뒤 감 · 취소 · 실패 = 머묾(요구 326-6). */ +async function drainageFirst(shared: DesignPageShared): Promise { + const side = shared.profile?.side; + if (!side?.stale()) return true; + if (!(await showConfirmDialog(L("Design_Drainage_Stale"), L("Design_Drainage_Stale_Go")))) + return false; + if (await side.update()) return true; + showToast(L("Design_Drainage_Stale_Failed"), "error"); + return false; } /** diff --git a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts index 5f27a3034..9b74f5d61 100644 --- a/B04_PreProcess/B04_PreProcess_Api_Fetch.ts +++ b/B04_PreProcess/B04_PreProcess_Api_Fetch.ts @@ -599,6 +599,8 @@ export interface DetailBasinResponse { stale?: string | null; /** 관 자리 후보(347-3) — 제안만 · [적용] 때 관 초안에. */ pipe_candidates?: Array<{ chainage_m: number; lonlat: [number, number] }>; + /** 이 답을 셈한 노선(사업지 CRS m) — 배수유역도 노선선 · 관 자리가 유역과 짝(요구 326-6). */ + route_xy?: Array<[number, number]>; } let lastNote = { text: "", at: 0 }; diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index a79e155a1..8dec4add1 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -340,6 +340,8 @@ def _raw_payload( # ── 아래는 B05 배수유역도가 그리는 데 필요한 값. B04 지도는 자체 오버레이가 있어 # 쓰지 않지만, 두 화면이 같은 응답을 받아야 결과가 갈리지 않는다(2026-08-01 일원화). "route_lonlat": detail.route_lonlat, + # 이 답을 셈한 노선(사업지 CRS m) — B05 배수유역도 노선선 · 관 자리 · 유역이 늘 짝(요구 326-6) + "route_xy": [[round(v.x, 3), round(v.y, 3)] for v in context.vertices], # 2차 전체 유역 외곽선 = 분수령. 해석 결과 그대로(손으로 고치는 기능 없음). "main_polygon_lonlat": detail.basin_lonlat, "grid_cell_m": detail.grid_cell_m, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts b/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts index 2bf510e3d..0aa64a118 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts @@ -18,9 +18,8 @@ export interface DrainageChrome { root: HTMLElement; panelHandle: ReturnType; layerButtons: HTMLElement; - analyzeButton: HTMLButtonElement; - deleteButton: HTMLButtonElement; - autoButton: HTMLButtonElement; + /** [업데이트](요구 326-6) — 지금 노선으로 처음부터 셈 · 배선은 `B05_Profile_UI_Drainage_Live`. */ + updateButton: HTMLButtonElement; /** [자동 제안](510) — 후보 목록 · 배선은 패널(`B05_Profile_UI_Drainage_Propose`). */ proposeButton: HTMLButtonElement; viewport: HTMLElement; @@ -38,12 +37,6 @@ export interface DrainageChrome { export interface DrainageChromeLabels { title: string; - analyze: string; - analyzeTip: string; - deleteSelected: string; - deleteSelectedTip: string; - auto: string; - autoTip: string; imageAlt: string; statusNeedRoute: string; } @@ -77,32 +70,16 @@ export function createDrainageChrome( layerButtons.addEventListener("pointerdown", (event) => event.stopPropagation()); header.append(title); - // 세부유역 산정 — B04가 미리 분석해 둔 결과를 읽어 관을 보충하고 세부유역만 나눈다. - // 격자 해석은 하지 않으므로 즉시 끝난다. - const analyzeButton = document.createElement("button"); - analyzeButton.type = "button"; - analyzeButton.className = "b05-drainage__analyze"; - analyzeButton.textContent = labels.analyze; - analyzeButton.title = labels.analyzeTip; - // 선택된 배관 삭제 — 편집 모드에서 마커를 선택해야 활성화된다. - const deleteButton = document.createElement("button"); - deleteButton.type = "button"; - deleteButton.className = "b05-drainage__analyze b05-drainage__tool"; - deleteButton.textContent = labels.deleteSelected; - deleteButton.title = labels.deleteSelectedTip; - deleteButton.disabled = true; - // 배관 배치 초기화 — 편집한 배관 배치를 버리고 세류선 교차 자리만(초안 · 369). - const autoButton = document.createElement("button"); - autoButton.type = "button"; - autoButton.className = "b05-drainage__analyze b05-drainage__tool"; - autoButton.textContent = labels.auto; - autoButton.title = labels.autoTip; + const updateButton = document.createElement("button"); + updateButton.type = "button"; + updateButton.className = "b05-drainage__analyze b05-drainage__update"; + updateButton.textContent = "업데이트"; const proposeButton = document.createElement("button"); proposeButton.type = "button"; proposeButton.className = "b05-drainage__analyze b05-drainage__tool b05-drainage__propose"; proposeButton.textContent = ui_locales.B05_Drainage_Btn_Propose[currentLanguageIndex]; proposeButton.title = ui_locales.B05_Drainage_Btn_Propose_Tip[currentLanguageIndex]; - header.append(analyzeButton, deleteButton, autoButton, proposeButton); + header.append(updateButton, proposeButton); const viewport = document.createElement("div"); viewport.className = "b05-drainage__viewport"; @@ -160,9 +137,7 @@ export function createDrainageChrome( root, panelHandle, layerButtons, - analyzeButton, - deleteButton, - autoButton, + updateButton, proposeButton, viewport, backgroundImage, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Interact.ts b/B05_Profile/B05_Profile_UI_Drainage_Interact.ts index 4f4bc3896..8818be515 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Interact.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Interact.ts @@ -25,8 +25,7 @@ import type { createMapContextMenu } from "@ui/ui_template_context_menu"; /** 이만큼(px) 이하로 움직였다 뗐으면 클릭으로 본다 — 손떨림으로 선택이 안 되는 일을 막는다. */ const CLICK_SLOP_PX = 4; /** 지도 집기 · 유역 고르기 · 팬에서 빼는 지도 위 요소. */ -const HTML_CONTROLS = - "button, input, select, textarea, a, label, [role='button'], .b05-drainage__veil"; +const HTML_CONTROLS = "button, input, select, textarea, a, label, [role='button']"; export interface DrainageInteractParams { viewport: HTMLElement; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Live.ts b/B05_Profile/B05_Profile_UI_Drainage_Live.ts index e3a4116aa..5504e3f80 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Live.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Live.ts @@ -1,30 +1,25 @@ /* ============================================================================= * B05_Profile_UI_Drainage_Live.ts - * 배수유역 자동/수동 다시 셈(347-2) — 묶음 drainage 단계 · ⚙ 설정 창 · 흐림 + [업데이트] · 구석 - * 「계산 중」 · 「지금 노선으로 셈」 요청(차례 번호 · 끊기). 규칙은 `logic/B05_Profile_Drainage_Live.ts`. + * 배수유역 [업데이트](요구 326-6 · 347-2) — 묶음 drainage 단계 · 낡음 표시 · 구석 「계산 중」 · + * 「지금 노선으로 셈」 요청(차례 번호 · 끊기). 규칙은 `logic/B05_Profile_Drainage_Live.ts`. * * - 단계(`stage`)는 계산 묶음(`design_pipeline` · 붙이기 = sub7)이 노선 초안 · 관이 바뀔 때 부름 — * 기다리지 않고 null 을 돌려줌(배수유역은 뒤 단계 · 출력 없음). - * - 노선 초안이 있으면 「낡음」 — 자동 = 흐림 없이 3 s 조용한 뒤 셈(구석 「계산 중」만) · 수동 = 칸만 - * 흐림 + 가운데 [업데이트]. 낡은 동안 제안 [적용]은 끔(두 모드 · 새 답이 오면 다시 켬). - * - 다시 조작하면 처음으로 — 떠 있는 요청을 끊고(AbortController) 다시 흐림 또는 다시 3 s. - * 묶음이 같은 노선 · 관으로 다시 돌면(입력 지문 같음) 그대로 둠 — 떠 있는 답을 받음. + * - 노선 초안이 있으면 「낡음」 — 저절로 셈하지 않음 · 옛 그림 그대로 · [업데이트] 단추에 표시만 + * (508-2 [3D 업데이트] 꼴). 낡은 동안 제안 [적용] · [자동 제안]은 끔(새 답이 오면 다시 켬). + * - [업데이트] = 늘 지금 노선(초안 · 저장 노선)으로 배수유역을 처음부터 셈 — 노선이 그대로여도. + * 뒤에서 조용히(덮개 · 잠금 없음) · 답이 오면 그림을 갈아 끼움. + * - 다시 조작하면 떠 있는 요청을 끊고(AbortController) 다시 낡음. 묶음이 같은 노선 · 관으로 다시 + * 돌면(입력 지문 같음) 그대로 둠 — 떠 있는 답을 받음. * - 초안이 없어지면(되돌리기 · 버림) 저장 배수유역을 다시 받아(열 때와 같은 길 · 계산 아님) 그 답의 - * 낡음대로 — 떠 있던 초안용 답은 버림 · 자동도 3 s 셈 대신 저장분. - * - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 흐림 + [업데이트] · 자동이면 3 s 뒤. + * 낡음대로 — 떠 있던 초안용 답은 버림. + * - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 낡음 표시. * - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림. - * - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로. - * - [배관 배치 초기화] = 세류선 교차 관만 · 초안(세션) · [저장] 때 정본 · 답이 오면 사진첩 한 걸음(늦은 - * 답도 앞 걸음에 안 묻힘) · 노선 초안 중이거나 저장 분석이 낡았으면(옛 노선) 「지금 노선으로 셈」 길로 - * (저장 분석으로 셈 = 옛 노선 자리 · 409). - * - [자동 제안](510 · 후보 목록 = `B05_Profile_UI_Drainage_Propose`) — 낡은 동안 끔 · 낡으면 목록 닫음. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createProgressCircle } from "@ui/ui_template_progress"; -import { openModal } from "@ui/ui_template_modal"; -import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state"; import type { StageFn } from "../A00_Common/design/logic/design_pipeline_types"; import { computeDetailBasins, @@ -36,23 +31,20 @@ import { draftMakesStale, inputKey, liveRequestBody, - parseDrainageMode, - QUIET_MS, - type DrainageMode, } from "./logic/B05_Profile_Drainage_Live"; import type { DetailRunner } from "./B05_Profile_UI_Drainage_Run"; import { createDrainageSuggest, type DrainageSuggest } from "./B05_Profile_UI_Drainage_Suggest"; -const MODE_KEY = "drainage-mode"; - export interface DrainageLiveDeps { - /** 패널 뿌리 — 머리(⚙) · 지도(흐림 · 구석 「계산 중」)를 이 안에서 찾음. */ + /** 패널 뿌리 — 머리([자동 제안]) · 지도(구석 「계산 중」)를 이 안에서 찾음. */ root: HTMLElement; + /** [업데이트] 단추(머리 줄 · `_Drainage_Chrome`). */ + updateButton: HTMLButtonElement; /** 제안 목록을 이 앞에 둠. */ basinList: HTMLElement; pipeRequest: () => DetailPipeInput[]; runner: DetailRunner; - /** [적용] · [자동 제안] 답이 화면에 오른 뒤 — 세션 초안에 담고 사진첩 한 걸음(패널이 콜백으로 이음). */ + /** [적용] 답이 화면에 오른 뒤 — 세션 초안에 담고 사진첩 한 걸음(패널이 콜백으로 이음). */ rememberPipes: () => void; /** 저장 배수유역 다시 받기(열 때와 같은 길 · 조용히) — 답을 화면에 올렸으면 true. */ loadSaved: () => Promise; @@ -63,14 +55,12 @@ export interface DrainageLiveDeps { export interface DrainageLive { /** 계산 묶음 drainage 단계 — 노선 초안 · 관이 바뀐 차례에 부름(출력 없음). */ stage: StageFn<"drainage">; - /** 낡음을 지금 노선으로 다시 셈(자동 · [업데이트]와 같은 길). */ - refresh(): void; - /** 서버 답이 「낡음」 — 흐림 + [업데이트](자동이면 3 s 뒤 셈). 이미 낡음이면 그대로. */ + /** [업데이트] — 지금 노선으로 처음부터 셈 · 답을 화면에 올렸으면 true. */ + refresh(): Promise; + /** 서버 답이 「낡음」 — 낡음 표시. 이미 낡음이면 그대로. */ markStale(): void; - /** [확정]이 서버에서 다시 셈 — 낡음 지움(떠 있는 요청 · 기다림도 끊음). */ + /** [확정]이 서버에서 다시 셈 — 낡음 지움(떠 있는 요청도 끊음). */ markFresh(): void; - /** [배관 배치 초기화] — 세류선 교차 관만으로 초안(369 · 저장분은 [저장] 때). */ - resetToStreams(): void; readonly stale: boolean; suggest: DrainageSuggest; } @@ -78,7 +68,7 @@ export interface DrainageLive { /** 「지금 노선으로 셈」 — 서버 `post_drainage_live` · 작업본에 안 씀. */ async function fetchLive( projectId: string, - body: ReturnType | (Proposal & { generation: number }), + body: ReturnType, signal: AbortSignal, ): Promise { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/drainage/live`, { @@ -93,102 +83,75 @@ async function fetchLive( return payload; } -export function readDrainageMode(): DrainageMode { - return parseDrainageMode(readStateRaw(MODE_KEY)); -} - const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex]; -/** 서버 「낡음」 글 머리 — 짝 `B04_PreProcess_Router_Basins.STALE_MESSAGE`. */ -const STALE_PREFIX = "배수유역 낡음"; - -/** [배관 배치 초기화] 요청 — 관 목록 없음(null) = 세류선 교차만. */ -type Proposal = { points: DetailPipeInput[] | null }; +const UPDATE_LABEL = "업데이트"; +const UPDATE_TIP = + "지금 노선으로 배수유역을 처음부터 다시 셉니다 — 뒤에서 셈 · 끝나면 그림을 바꿈."; +const UPDATE_STALE_TIP = + "노선 · 관 자리가 바뀌어 배수유역도가 낡았습니다 — 누르면 지금 노선으로 다시 셈."; export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { const viewport = deps.root.querySelector(".b05-drainage__viewport") as HTMLElement; const header = deps.root.querySelector(".b05-drainage__header") as HTMLElement; const propose = header.querySelector(".b05-drainage__propose"); - /** 관 초안 요청 — 답이 화면에 오르면 세션 초안 + 사진첩 한 걸음(400 ms 넘은 답도 제 걸음). */ - const draft = (request: (projectId: string) => Promise): void => { + const update = deps.updateButton; + const suggest = createDrainageSuggest(deps.pipeRequest, (points) => { const projectId = deps.runner.projectId(); if (!projectId) return; void deps.runner - .run(() => request(projectId)) + .run(() => computeDetailBasins(projectId, points)) .then((ok) => { - if (!ok) return; - deps.rememberPipes(); + if (ok) deps.rememberPipes(); }); - }; - const suggest = createDrainageSuggest(deps.pipeRequest, (points) => - draft((projectId) => computeDetailBasins(projectId, points)), - ); + }); deps.basinList.before(suggest.root); - let mode = readDrainageMode(); let route: unknown = null; let stale = false; /** 마지막 단계에 노선 초안이 있었나 — 없어지면 저장분으로 돌아옴. */ let drafted = false; /** 저장분 다시 받기 답이 「낡음」이었나(`markStale` 이 세움). */ let savedStale = false; - /** 저장 분석이 옛 노선(서버 「낡음」 답 · [확정] 전까지) — [배관 배치 초기화]는 지금 노선으로 셈. */ - let analysisStale = false; let generation = 0; /** 마지막 단계 입력 지문 — 같으면 묶음이 다시 돈 것뿐. */ let key = ""; - let timer = 0; let inflight: AbortController | null = null; - const veil = document.createElement("div"); - veil.className = "b05-drainage__veil"; - veil.hidden = true; - const update = document.createElement("button"); - update.type = "button"; - update.className = "b05-drainage__analyze b05-drainage__update"; - update.textContent = "업데이트"; - update.title = "노선 · 관 자리가 바뀌어 배수유역이 낡았습니다 — 지금 노선으로 다시 셉니다."; - veil.append(update); const busy = createProgressCircle({ size: 22, label: "계산 중" }); busy.root.classList.add("b05-drainage__busy"); busy.root.hidden = true; - viewport.append(veil, busy.root); - - const gear = document.createElement("button"); - gear.type = "button"; - gear.className = "b05-drainage__analyze b05-drainage__tool b05-drainage__gear"; - gear.textContent = "⚙"; - gear.title = "배수유역 계산 설정 — 수동(기본) / 자동"; - header.append(gear); + viewport.append(busy.root); const paint = (): void => { - deps.root.classList.toggle("is-stale", stale && mode === "manual"); + deps.root.classList.toggle("is-stale", stale); + update.classList.toggle("is-pending", stale); + update.disabled = inflight !== null; + update.textContent = inflight ? "계산 중…" : UPDATE_LABEL; + update.title = stale ? UPDATE_STALE_TIP : UPDATE_TIP; suggest.setStale(stale); if (propose) { propose.disabled = stale; propose.title = L(stale ? "B05_Drainage_Btn_Propose_Stale" : "B05_Drainage_Btn_Propose_Tip"); } - veil.hidden = !stale || mode === "auto" || inflight !== null; busy.root.hidden = inflight === null; }; function cancel(): void { - window.clearTimeout(timer); - timer = 0; inflight?.abort(); inflight = null; } - /** 지금 노선으로 셈 — `proposal` 이면 그 관 목록 · 방식으로(답 = 관 초안 · 사진첩 한 걸음). */ - function refresh(proposal?: Proposal): void { + /** 지금 노선으로 처음부터 셈 — 노선 초안이 있으면 그 노선 · 없으면 저장 노선. */ + function refresh(): Promise { const projectId = deps.runner.projectId(); - if (!projectId) return; + if (!projectId) return Promise.resolve(false); cancel(); const sent = { generation: ++generation, key }; const controller = new AbortController(); inflight = controller; paint(); - const body = { ...liveRequestBody(sent.generation, route, deps.pipeRequest()), ...proposal }; - void deps.runner + const body = liveRequestBody(sent.generation, route, deps.pipeRequest()); + return deps.runner .run(async () => { const response = await fetchLive(projectId, body, controller.signal); // 옛 차례 · 서버가 그만둔 답 — 감싸기가 조용히 버리게 끊김으로 돌려줌. @@ -197,42 +160,23 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { return response; }, true) .then((applied) => { - if (inflight !== controller) return; + if (inflight !== controller) return applied; inflight = null; if (applied) stale = false; - if (applied && proposal) { - deps.rememberPipes(); - } paint(); + return applied; }); } - /** [배관 배치 초기화] — 저장 분석이 지금 노선과 같으면 빠른 길(detail-basins) · - * 그 길이 「낡음」(409 · 화면이 아직 몰랐던 낡음)이면 곧바로 지금 노선으로 셈 길로 다시. */ - function proposeDraft(proposal: Proposal): void { - if (analysisStale || draftMakesStale(route)) return refresh(proposal); - draft(async (projectId) => { - try { - return await computeDetailBasins(projectId, proposal.points); - } catch (error) { - if (!(error instanceof Error) || !error.message.startsWith(STALE_PREFIX)) throw error; - analysisStale = true; - queueMicrotask(() => refresh(proposal)); // 이 요청은 조용히 버림(뒤 차례가 받음) - throw new DOMException("stale", "AbortError"); - } - }); - } - - /** 평면 · 관 자리 조작 — 처음으로(요청 끊기 · 다시 흐림 또는 다시 3 s). */ + /** 평면 · 관 자리 조작 — 떠 있는 요청을 끊고 낡음(저절로 셈하지 않음). */ function invalidate(): void { cancel(); stale = true; deps.onStale?.(); - if (mode === "auto") timer = window.setTimeout(refresh, QUIET_MS); paint(); } - /** 초안이 없어짐 — 저장분을 다시 받아 그 답의 낡음대로(흐림은 답이 올 때까지 그대로). */ + /** 초안이 없어짐 — 저장분을 다시 받아 그 답의 낡음대로(표시는 답이 올 때까지 그대로). */ function backToSaved(): void { cancel(); const sent = ++generation; @@ -241,51 +185,12 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { void deps.loadSaved().then((applied) => { if (sent !== generation) return; stale = !applied || savedStale; - if (stale && mode === "auto") timer = window.setTimeout(refresh, QUIET_MS); paint(); }); } - function openSettings(): void { - openModal({ - title: "배수유역 계산", - closeLabel: "닫기", - mount(body, close) { - const choices: Array<[DrainageMode, string, string]> = [ - ["manual", "수동(기본)", "노선 · 관 자리가 바뀌면 칸만 흐려지고 [업데이트]를 누를 때 셈"], - ["auto", "자동", "조작이 3초 멈추면 뒤에서 조용히 셈(화면을 막지 않음)"], - ]; - choices.forEach(([value, label, hint]) => { - const row = document.createElement("label"); - row.className = "b05-drainage__mode"; - const input = document.createElement("input"); - input.type = "radio"; - input.name = "b05-drainage-mode"; - input.checked = mode === value; - input.addEventListener("change", () => { - setMode(value); - close(); - }); - const text = document.createElement("span"); - text.textContent = `${label} — ${hint}`; - row.append(input, text); - body.append(row); - }); - }, - }); - } - - function setMode(next: DrainageMode): void { - mode = next; - writeStateRaw(MODE_KEY, next); - window.clearTimeout(timer); - timer = 0; - if (stale && next === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS); - paint(); - } - - update.addEventListener("click", () => refresh()); - gear.addEventListener("click", openSettings); + update.addEventListener("click", () => void refresh()); + paint(); return { stage(ctx) { @@ -299,23 +204,19 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive { else if (wasDraft) backToSaved(); return null; }, - refresh: () => refresh(), + refresh, suggest, markStale() { savedStale = true; - analysisStale = true; if (stale) return; stale = true; - if (mode === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS); paint(); }, markFresh() { cancel(); stale = false; - analysisStale = false; paint(); }, - resetToStreams: () => proposeDraft({ points: null }), get stale() { return stale; }, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 76b1b7ef4..f91f86466 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -83,12 +83,6 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra const chrome = createDrainageChrome( { title: L("B05_Drainage_Title"), - analyze: L("B05_Drainage_Btn_Analyze"), - analyzeTip: L("B05_Drainage_Btn_Analyze_Tip"), - deleteSelected: L("B05_Drainage_Btn_DeleteSelected"), - deleteSelectedTip: L("B05_Drainage_Btn_DeleteSelected_Tip"), - auto: L("B05_Drainage_Btn_Auto"), - autoTip: L("B05_Drainage_Btn_Auto_Tip"), imageAlt: L("B05_Drainage_ImageAlt"), statusNeedRoute: L("B05_Drainage_Status_NeedRoute"), }, @@ -102,9 +96,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra root, panelHandle, layerButtons, - analyzeButton, - deleteButton, - autoButton, + updateButton, proposeButton, viewport, backgroundImage, @@ -245,7 +237,6 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra markedChainage, stationIntervalM: MAP_STATION_INTERVAL_M, intervalSpans, - candidates: propose.candidates(), }); updateImageTransform(); } @@ -260,10 +251,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra const pipeColor = (chainage: number): string => pipeMarkerColor(basins, chainage); - /** 마커 선택 ↔ 유역 강조 동기화 + 삭제 버튼 갱신(편집 폼은 사이드 소관). */ + /** 마커 선택 ↔ 유역 강조 동기화(편집 폼은 사이드 소관). */ function syncPipeSelection(notify = true): void { const index = pipeEditor.selected(); - deleteButton.disabled = index === null; const picked = index === null ? null : pipeEditor.pipes()[index]; selectBasin(basinIndexOfPipe(basins, picked ?? null), notify); // 유역 유무와 무관하게 선택 자체를 알린다 — 그래프·3D·리스트 동기화(재진입은 @@ -304,12 +294,17 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra callbacks.onBasinSelected?.(picked ? picked.chainage_m : null); } - /** 응답을 화면 상태로 옮긴다. B04 지도와 **같은 엔드포인트·같은 응답**을 쓴다 - * — 두 화면이 다른 결과를 보이면 안 되기 때문이다(2026-08-01 일원화). */ + /** 응답을 화면 상태로 옮긴다. B04 지도와 같은 엔드포인트 · 같은 응답(2026-08-01 일원화). */ function apply(response: DetailBasinResponse): void { + // 낡은 답(노선 [저장] 뒤 등 · 세부 유역 없음) = 옛 그림 그대로 · 관만 받음 · [업데이트] 표시(326-6) + const keep = !!response.stale && basins.length > 0; + if (response.stale) live.markStale(); + if (keep) return void takePipes(response); basins = response.basins; live.suggest.update(response); - if (response.stale) live.markStale(); // 노선 [저장] 뒤 등 — 흐림 + [업데이트](자동이면 3 s 뒤) + // 노선선 = 이 답을 셈한 노선 — 유역 · 관 자리와 늘 짝(초안 노선 [업데이트] 뒤 빈 곳 · 326-6) + const answered = response.route_xy?.map(([x, y]) => ({ x, y })) ?? []; + if (answered.length) takeRoute(answered, false); mainBoundary = response.main_polygon_lonlat ?? []; upstreamLines = (response.upstream_lonlat ?? []) as Array>; flowArrows = (response.flow_arrows ?? []) as FlowArrow[]; @@ -323,7 +318,11 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra maxStrength = built.maximum; legend.update(maxStrength, show.strength); maxHotspotArea = hotspots.reduce((max, spot) => (spot.area > max ? spot.area : max), 0); - // 실제 사용된 배관 목록으로 마커를 동기화한다(유역 없는 관 포함). + takePipes(response); + } + + /** 답의 관 목록(유역 없는 관 포함) · 시설 정보를 마커 · 폼에 올림(유역 그림은 그대로). */ + function takePipes(response: DetailBasinResponse): void { pipeEditor.setPipes( response.pipe_points.map((pipe) => ({ chainage_m: pipe.chainage_m, @@ -351,7 +350,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra /** 세부유역 요청 한 번 — 버튼 잠금 · 진행 문구 · 오류 · 최신 응답만(347-2 에 나눔). */ const runner = createDetailRunner({ projectId: () => projectId, - analyzeButton, + updateButton, status, showProgress, apply, @@ -395,9 +394,10 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra * 시설 종류·구간·옵션은 계산기에서 사라지므로 요청에 되붙인다. */ const analyze = batched(() => run(() => computeDetailBasins(projectId as string, pipeRequest()))); - // 자동/수동 다시 셈(347-2) · 제안 목록(347-3 · [적용] = 관 초안으로 세부 유역 다시 나눔). + // [업데이트](326-6 · 347-2) · 제안 목록(347-3 · [적용] = 관 초안으로 세부 유역 다시 나눔). const live = createDrainageLive({ root, + updateButton, basinList, pipeRequest, runner, @@ -416,13 +416,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra item.stream_label ? { facility: "pipe", stream_label: item.stream_label } : undefined, ), ), - redraw: scheduleDraw, }); basinList.before(propose.root); proposeButton.addEventListener("click", () => void propose.open()); - analyzeButton.addEventListener("click", () => void analyze()); - deleteButton.addEventListener("click", () => void pipeEditor.deleteSelected()); - autoButton.addEventListener("click", live.resetToStreams); // 세류선만 · 초안(369) function addPipe(chainageM: number, attributes?: FacilityAttributes): void { chainageM = snapStructureStation(chainageM); // 관 기준 측점 = 정수 m(234-1) @@ -432,8 +428,20 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra pipeEditor.addAtChainage(chainageM); } + let routeFromAnswer = false; // 노선선이 답에서 왔나 — 그 뒤 페이지 노선은 그림 안 바꿈(326-6) + + function takeRoute(points: ReadonlyArray, fit: boolean): void { + routeFromAnswer ||= !fit; + routePoints = points; + routeLayer = meta && points.length > 1 ? prepareMetricPolyline(points, meta) : null; + // 강도 색칠은 백엔드가 준 1m 구간 값과 인덱스를 맞춰야 하므로 같은 규칙으로 다시 찍는다. + strengthSamples = resampleRoute(points); + pipeEditor.setContext(meta, points); + if (routeLayer && fit) fitToRoute(); + scheduleDraw(); + } + /** 노선 전체가 보이도록 배율·중심을 맞춘다(초기 보기 = 도로 기준 줌인). - * * B05는 노선 주변 배수유역을 보는 화면이라 도로에 맞춰 확대한 상태로 연다 * (2026-08-01 사용자 지시). 배경 전체를 보려면 휠로 축소하면 된다. * 노선이 없으면 배경 전체를 그대로 보여준다. */ @@ -672,14 +680,10 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra scheduleDraw(); }, setRoute(points) { - routePoints = points; - routeLayer = meta && points.length > 1 ? prepareMetricPolyline(points, meta) : null; - // 강도 색칠은 백엔드가 준 1m 구간 값과 인덱스를 맞춰야 하므로 같은 규칙으로 다시 찍는다. - strengthSamples = resampleRoute(points); - pipeEditor.setContext(meta, points); - if (routeLayer) fitToRoute(); - scheduleDraw(); + if (!routeFromAnswer) takeRoute(points, true); // 페이지 노선 = 첫 답 전만 · 그 뒤 답의 노선(326-6) }, + stale: () => live.stale, + update: () => live.refresh(), dispose() { loadSequence += 1; if (frameHandle) { diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts index e30eba54a..4129e849d 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts @@ -52,6 +52,10 @@ export interface DrainagePanel { removePipe: (chainageM: number) => void; /** 계산 묶음 drainage 단계(347-2) — 붙이기 = 묶음 쪽. */ pipelineStage: StageFn<"drainage">; + /** 배수유역도가 낡았나(노선 · 관 자리가 바뀜 · 서버 「낡음」) — B06 [>] 확인 창(326-6). */ + stale: () => boolean; + /** [업데이트] — 지금 노선으로 처음부터 셈 · 답을 화면에 올렸으면 true. */ + update: () => Promise; /** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */ selectBasinByChainage: (chainageM: number | null) => void; /** 밖(리스트·그래프·3D)에서 관을 고른다 — 마커 선택·유역 강조를 맞춘다. diff --git a/B05_Profile/B05_Profile_UI_Drainage_Propose.ts b/B05_Profile/B05_Profile_UI_Drainage_Propose.ts index 469ba638e..a97bac2b7 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Propose.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Propose.ts @@ -1,7 +1,7 @@ /* ============================================================================= * B05_Profile_UI_Drainage_Propose.ts * 배수유역 칸 [자동 제안](510) — 서버가 후보를 셈(`post_pipe_proposals` · 세류선 교차 + 간격 보충 · - * 있는 관 · 지운 관 상태) · 화면은 목록 + 배수유역도 점선 원 · 고른 것만 관 넣기 길(`addPipe`)로. + * 있는 관 · 지운 관 상태) · 화면은 목록만(점선 원 걷음 · 326-6) · 고른 것만 관 넣기 길(`addPipe`)로. * * - 있는 관(라벨 짝 · 교차 5 m 안 관) = 목록에서 뺌(머리 글에 수만) · 지운 관 = 「지운 관」 표시 · * 고르면 다시 넣음(서버가 지움 표지 걷음). @@ -42,8 +42,6 @@ export interface DrainagePropose { /** 서버에 후보를 물어 목록을 엶. */ open(): Promise; close(): void; - /** 목록에 오른 후보 누가거리 — 배수유역도 점선 원. */ - candidates(): number[]; } export function createDrainagePropose(deps: { @@ -51,7 +49,6 @@ export function createDrainagePropose(deps: { pipeRequest: () => DetailPipeInput[]; /** 고른 후보를 관 넣기 길로(패널 `addPipe` 와 같은 길). */ addPipes: (picks: PipeProposal[]) => void; - redraw: () => void; }): DrainagePropose { const root = document.createElement("div"); root.className = "b05-drainage__suggest b05-drainage__proposals"; @@ -82,7 +79,6 @@ export function createDrainagePropose(deps: { shown = []; picked.clear(); list.replaceChildren(); - deps.redraw(); }; const render = (response: ProposalResponse): void => { @@ -110,7 +106,6 @@ export function createDrainagePropose(deps: { ); add.disabled = true; root.hidden = false; - deps.redraw(); }; add.addEventListener("click", () => { @@ -150,6 +145,5 @@ export function createDrainagePropose(deps: { } }, close, - candidates: () => (root.hidden ? [] : shown.map((item) => item.chainage_m)), }; } diff --git a/B05_Profile/B05_Profile_UI_Drainage_Render.ts b/B05_Profile/B05_Profile_UI_Drainage_Render.ts index 706b7af6e..54395e6cb 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Render.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Render.ts @@ -70,8 +70,6 @@ export interface DrainageScene { stationIntervalM: number; /** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */ intervalSpans: ReadonlyArray; - /** 관 자리 후보 누가거리(347-3 · 제안만) — 계획선 위 점선 원. */ - candidates: ReadonlyArray; } export function drawDrainageScene( @@ -170,20 +168,6 @@ export function drawDrainageScene( scene.maxHotspotArea, ); } - // 관 자리 후보 — 점선 원(배관 마커 아래 · 제안만이라 채우지 않음). - scene.candidates.forEach((chainage) => { - const sample = scene.strengthSamples[Math.round(chainage)]; - if (!sample) return; - const [x, y] = projector.toScreen(sample.x, sample.y); - context.save(); - context.setLineDash([3, 3]); - context.lineWidth = 2; - context.strokeStyle = "#1e88e5"; - context.beginPath(); - context.arc(x, y, 9, 0, Math.PI * 2); - context.stroke(); - context.restore(); - }); // 배관(관 매설) 마커 — 계획선 위. scene.pipeEditor.draw(context, view, scene.pipeColor); // 측점 선택 마킹 — 계획선 위 다이아몬드 표식(2026-08-05 사용자 지시. 3자 선택 동기화의 diff --git a/B05_Profile/B05_Profile_UI_Drainage_Run.ts b/B05_Profile/B05_Profile_UI_Drainage_Run.ts index be63c32ef..b10a4de06 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Run.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Run.ts @@ -17,7 +17,7 @@ type StatusKey = export interface DetailRunnerDeps { projectId: () => string | null; - analyzeButton: HTMLButtonElement; + updateButton: HTMLButtonElement; status: HTMLElement; showProgress: (ratio: number | null, label: string | null) => void; apply: (response: DetailBasinResponse) => void; @@ -33,14 +33,14 @@ export interface DetailRunner { } export function createDetailRunner(deps: DetailRunnerDeps): DetailRunner { - const { analyzeButton, status, showProgress, label } = deps; + const { updateButton, status, showProgress, label } = deps; let requestSeq = 0; // 세부유역 요청 — 최신 응답만 채택 return { projectId: deps.projectId, async run(request, quiet = false) { if (!deps.projectId()) return false; if (!quiet) { - analyzeButton.disabled = true; + updateButton.disabled = true; status.hidden = false; status.textContent = label("B05_Drainage_Status_Analyzing"); showProgress(null, label("B05_Drainage_Status_Analyzing")); @@ -63,7 +63,7 @@ export function createDetailRunner(deps: DetailRunnerDeps): DetailRunner { return false; } finally { if (seq === requestSeq) { - analyzeButton.disabled = false; + updateButton.disabled = false; showProgress(null, null); } } diff --git a/B05_Profile/B05_Profile_UI_Style_Drainage.css b/B05_Profile/B05_Profile_UI_Style_Drainage.css index a3f553295..1783176c1 100644 --- a/B05_Profile/B05_Profile_UI_Style_Drainage.css +++ b/B05_Profile/B05_Profile_UI_Style_Drainage.css @@ -173,7 +173,7 @@ cursor: default; } -/* 배관 편집 도구 버튼 — "유역 산정" 우측에 나란히(auto 마진 해제). */ +/* 머리 줄 둘째 단추([자동 제안]) — [업데이트] 우측에 나란히(auto 마진 해제). */ .b05-drainage__tool { margin-left: var(--spacing-4, 4px); } @@ -267,32 +267,21 @@ display: none; } -/* 배수유역 낡음(347-2) — 칸만 흐림 · 가운데 [업데이트] · 구석 「계산 중」(조작은 막지 않음). */ -.b05-drainage.is-stale .b05-drainage__canvas, -.b05-drainage.is-stale .b05-drainage__image, -.b05-drainage.is-stale .b05-drainage__summary, -.b05-drainage.is-stale .b05-drainage__basins, -.b05-drainage.is-stale .b05-drainage__suggest { - filter: blur(2px); - opacity: 0.6; +/* 배수유역 낡음(326-6) — 옛 그림 그대로 · [업데이트] 단추에 점([3D 업데이트] 꼴) · 구석 「계산 중」. */ +.b05-drainage__update.is-pending { + position: relative; + border-color: var(--color-warning, #d98a00); } -.b05-drainage__veil { +.b05-drainage__update.is-pending::after { + content: ""; position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; -} - -.b05-drainage__veil[hidden] { - display: none; -} - -.b05-drainage__veil .b05-drainage__update { - margin-left: 0; /* 머리 줄 단추의 오른쪽 붙임(auto) 풂 — 흐린 칸 가운데 */ - pointer-events: auto; + top: 3px; + right: 3px; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-warning, #d98a00); } .b05-drainage__busy { @@ -305,14 +294,6 @@ display: none; } -.b05-drainage__mode { - display: flex; - gap: var(--spacing-8); - align-items: center; - padding: var(--spacing-8) 0; - color: var(--color-text-body); -} - /* 제안 목록(347-3) — 유역 목록 위 · [적용] 한 번. */ .b05-drainage__suggest { flex: 0 0 auto; diff --git a/B05_Profile/logic/B05_Profile_Drainage_Live.ts b/B05_Profile/logic/B05_Profile_Drainage_Live.ts index caf190ffd..fc258c740 100644 --- a/B05_Profile/logic/B05_Profile_Drainage_Live.ts +++ b/B05_Profile/logic/B05_Profile_Drainage_Live.ts @@ -1,23 +1,14 @@ /* ============================================================================= * B05_Profile/logic/B05_Profile_Drainage_Live.ts - * 배수유역 자동/수동 다시 셈(347-2) — 순수 규칙만. 화면 · fetch 는 `B05_Profile_UI_Drainage_Live.ts`. + * 배수유역 [업데이트](요구 326-6 · 347-2) — 순수 규칙만. 화면 · fetch 는 `B05_Profile_UI_Drainage_Live.ts`. * * - 대상 = 평면(노선 초안) · 관 자리만 — 종단 · 횡단 조작은 배수유역 결과를 안 바꿈(묶음 deps 와 같음). * - 노선 초안이 없으면(저장 노선 그대로) 낡음 아님 — 관 조작은 세부 유역 나눔(0.2 s)이 바로 맞춤. - * - 자동 = 조용한 뒤 `QUIET_MS` · 수동 = 흐림 + [업데이트] · 기본 수동. + * - 저절로 셈하지 않음 — [업데이트]를 누를 때만 지금 노선으로 처음부터 셈. * - 묶음이 같은 입력으로 다시 돌면(입력 지문 같음) 낡음 · 떠 있는 요청을 그대로 둠. * - 답 채택 = 보낸 차례 · 입력 지문이 지금과 같고 서버가 그만두지 않았을 때만(늦은 답 버림). * ========================================================================== */ -export type DrainageMode = "manual" | "auto"; - -/** 자동 모드 — 마지막 조작 뒤 이만큼 조용하면 셈(ms · 341 결정 거리 6 · 3 초). */ -export const QUIET_MS = 3000; - -/** 계정 화면 설정 글자 → 모드(없거나 모르는 값 = 수동). */ -export const parseDrainageMode = (raw: string | null | undefined): DrainageMode => - raw === "auto" ? "auto" : "manual"; - /** 묶음 drainage 단계 입력 → 낡았는가 — 노선 초안이 있을 때만. */ export const draftMakesStale = (route: unknown): boolean => route !== null && route !== undefined; diff --git a/resources/tester/helper_347_2_drainage_live.cjs b/resources/tester/helper_347_2_drainage_live.cjs index 22dc10d7c..3ac337c17 100644 --- a/resources/tester/helper_347_2_drainage_live.cjs +++ b/resources/tester/helper_347_2_drainage_live.cjs @@ -34,12 +34,7 @@ const applied = sug.applySuggestions(pipes, suggestions); process.stdout.write( JSON.stringify({ - quietMs: live.QUIET_MS, - modes: [ - live.parseDrainageMode(null), - live.parseDrainageMode("auto"), - live.parseDrainageMode("x"), - ], + noAutoMode: live.QUIET_MS === undefined && live.parseDrainageMode === undefined, // 326-6 stale: [live.draftMakesStale(null), live.draftMakesStale({ rev: 1 })], accept: [ live.acceptLive({ status: "success", generation: 3 }, at(3), at(3)), diff --git a/resources/tester/helper_349_drainage_live_flow.cjs b/resources/tester/helper_349_drainage_live_flow.cjs index 9e0306e01..eb44b6f09 100644 --- a/resources/tester/helper_349_drainage_live_flow.cjs +++ b/resources/tester/helper_349_drainage_live_flow.cjs @@ -1,4 +1,4 @@ -/* 349 배수유역 자동/수동 흐름 헬퍼 — 화면 파일 `B05_Profile_UI_Drainage_Live.ts` · `_Run.ts` · +/* 349 · 326-6 배수유역 [업데이트] 흐름 헬퍼 — 화면 파일 `B05_Profile_UI_Drainage_Live.ts` · `_Run.ts` · * `_Suggest.ts` 를 그 자리에서 트랜스파일해 가짜 DOM · 시계 · fetch 로 돌리고 단계별 상태를 JSON 으로 냄. */ const fs = require("fs"); @@ -86,21 +86,10 @@ const flush = async () => { /* ---- 모듈 읽기(그 자리 트랜스파일 · 바깥 모듈은 가짜) ---- */ const state = new Map(); const detailCalls = []; -const detailAutos = []; // [자동 제안](369) 방식 — 없으면 null let records = 0; // 사진첩 한 걸음(369) -let detailStale = false; // detail-basins 가 409 낡음으로 답함(369 — 화면이 몰랐던 낡음) -let modeRow = 1; // ⚙ · [자동 제안] 창에서 고를 줄 — ⚙ 0 수동 · 1 자동 / 제안 0 전체 · 1 지정 포함 const stubs = { "@config/config_frontend": { API_BASE_URL: "/api" }, "@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) }, - // ⚙ 창 — 둘째 줄(자동) 라디오를 고름 - "@ui/ui_template_modal": { - openModal: (options) => { - const body = el("modal"); - options.mount(body, () => undefined); - body.children[modeRow].children[0].fire("change"); - }, - }, "@ui/ui_template_locale": { currentLanguageIndex: 0, ui_locales: new Proxy({}, { get: (_target, key) => [key, key] }), @@ -110,10 +99,8 @@ const stubs = { writeStateRaw: (key, value) => state.set(key, value), }, "../B04_PreProcess/B04_PreProcess_Api_Fetch": { - computeDetailBasins: async (projectId, points, auto) => { + computeDetailBasins: async (projectId, points) => { detailCalls.push(points); - detailAutos.push(auto ?? null); - if (detailStale) throw new Error("배수유역 낡음 — 노선이 분석 때와 다릅니다."); return { basins: [], pipe_points: points ?? [], stale: null }; }, }, @@ -145,6 +132,7 @@ async function main() { const header = el("header"); const root = el("root"); const propose = el("button"); + const update = el("button"); header.querySelector = (selector) => (selector === ".b05-drainage__propose" ? propose : null); root.querySelector = (selector) => selector === ".b05-drainage__viewport" @@ -163,7 +151,7 @@ async function main() { const status = el("status"); const runner = createDetailRunner({ projectId: () => "p1", - analyzeButton: el("button"), + updateButton: update, status, showProgress: () => undefined, apply: (response) => { @@ -177,6 +165,7 @@ async function main() { }); live = createDrainageLive({ root, + updateButton: update, basinList: el("list"), pipeRequest: () => pipes, runner, @@ -188,13 +177,12 @@ async function main() { return runner.run(() => new Promise((resolve) => (savedAnswer = resolve)), true); }, }); - const [veil, busy] = viewport.children; - const update = veil.children[0]; - const gear = header.children[0]; + const [busy] = viewport.children; const look = () => ({ stale: live.stale, - blur: root.classList.contains("is-stale"), - veil: !veil.hidden, + pending: update.classList.contains("is-pending"), + off: update.disabled, + label: update.textContent, busy: !busy.hidden, fetches: calls.length, applyOff: live.suggest.root.children[0].children[1].disabled, @@ -221,11 +209,14 @@ async function main() { ...extra, }); - // 수동 — 꼭짓점 옮김 = 흐림 + [업데이트] · 판 번호만 바뀐 같은 노선 = 그대로 + out.start = look(); + // 꼭짓점 옮김 = 낡음 표시만 · 저절로 셈 없음 · 판 번호만 바뀐 같은 노선 = 그대로 stage(draft(1, 50)); out.moved = look(); stage(draft(2, 50)); out.sameAgain = look(); + advance(60000); + out.noAuto = look(); // [업데이트] → 요청 하나 · 묶음이 같은 입력으로 다시 돌아도 답을 받음 update.fire("click"); out.sent = { ...look(), body: calls[0].body }; @@ -244,7 +235,7 @@ async function main() { await flush(); out.applied = { points: detailCalls[0], remembered }; - // 다시 옮김 → 요청 → 그 사이 또 옮김 = 앞 요청 끊김 · 다시 흐림 + // 다시 옮김 → 요청 → 그 사이 또 옮김 = 앞 요청 끊김 · 다시 낡음 stage(draft(4, 80)); update.fire("click"); stage(draft(5, 90)); @@ -253,64 +244,36 @@ async function main() { live.suggest.root.children[0].children[1].fire("click"); await flush(); out.staleApply = detailCalls.length; - update.fire("click"); + // [업데이트] 약속 — 답이 화면에 오르면 참(B06 [>] 확인 창 뒤 이동 · 326-6) + const done = live.refresh(); answer(calls[2]); + out.promised = await done; await flush(); out.fresh = look(); + // 낡지 않아도 [업데이트] = 다시 셈(노선 · 관 그대로 · 326-6) + update.fire("click"); + out.again = { ...look(), route: calls[3].body.route }; + answer(calls[3]); + await flush(); + out.againDone = look(); - // 노선 [저장] 뒤 — 서버 답이 「낡음」 = 흐림 + [업데이트](수동) + // 노선 [저장] 뒤 — 서버 답이 「낡음」 = 낡음 표시 · 저절로 셈 없음 stage(null); await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); out.savedStale = { ...look(), status: status.textContent }; - - // ⚙ 자동 — 바꾸는 순간 3 s 뒤 셈 · 흐림 막 · [업데이트] 없음 - gear.fire("click"); - out.autoSwitch = look(); - advance(2999); - out.auto2999 = look(); - advance(1); - out.auto3000 = { ...look(), route: calls[3].body.route }; - answer(calls[3]); - await flush(); - out.autoDone = look(); - // 자동 · 꼭짓점 옮김 = 조용히 3 s 뒤 · 그 사이 또 옮기면 처음부터 - stage(draft(6, 120)); - advance(2000); - stage(draft(7, 130)); - advance(2000); - out.autoRestart = look(); - advance(1000); + advance(60000); + out.savedStaleNoAuto = look(); + update.fire("click"); answer(calls[4]); await flush(); - out.autoMoved = { ...look(), mode: state.get("drainage-mode") }; - // 자동 · 저장 뒤 낡음 = 3 s 뒤 조용히 - await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); - out.autoSavedStale = look(); - advance(3000); - answer(calls[5]); - await flush(); - out.autoSavedDone = look(); + out.savedDone = look(); - // 349-0i 되돌리기 — 자동: 끌기 → 낡음 → Ctrl+Z(초안 없어짐) = 3 s 셈 대신 저장분 - // 저장분 답에도 관경 제안 하나 — [적용] 켜짐은 낡음 · 관경/시설 제안 유무로 갈림(369) + // 되돌리기 — 끌기 → [업데이트] 요청 중 → Ctrl+Z = 초안용 답 끊고 저장분 · 표시는 답 올 때까지 그대로 const savedOk = { basins: [{ chainage_m: 100, recommended_facility: "pipe", recommended_diameter_mm: 1200 }], pipe_points: pipes, stale: null, }; - stage(draft(20, 300)); - advance(1000); - stage(null); - out.autoUndoWaiting = { ...look(), savedLoads }; - advance(5000); - out.autoUndoNoFetch = look(); - savedAnswer(savedOk); - await flush(); - out.autoUndo = { ...look(), savedLoads }; - - // 수동: 끌기 → [업데이트] 요청 중 → Ctrl+Z = 초안용 답 끊고 저장분 · 흐림은 답 올 때까지 - modeRow = 0; - gear.fire("click"); stage(draft(21, 310)); update.fire("click"); const draftCall = calls[calls.length - 1]; @@ -319,64 +282,17 @@ async function main() { savedAnswer(savedOk); await flush(); out.undo = { ...look(), savedLoads }; - // 저장분 자체가 낡음 = 흐림 · [업데이트] 그대로 + // 저장분 자체가 낡음 = 낡음 표시 그대로 stage(draft(22, 320)); stage(null); savedAnswer({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" }); await flush(); - out.undoSavedStale = { ...look(), savedLoads, mode: state.get("drainage-mode") }; - // [자동 제안] — 낡은 동안 끔 · 누름은 패널 후보 목록(510 · 여기 안 셈) · [배관 배치 초기화] = 관 목록 없이(세류선만) + out.undoSavedStale = { ...look(), savedLoads }; + // [자동 제안] — 낡은 동안 끔 · 새 답이면 켬 out.proposeStale = { off: propose.disabled, title: propose.title }; - const beforeStale = detailCalls.length; - propose.fire("click"); - await flush(); - out.proposeStale.calls = detailCalls.length - beforeStale; live.markFresh(); out.proposeFresh = { off: propose.disabled, title: propose.title }; - const n0 = detailCalls.length; - const remembered0 = remembered; - const records0 = records; - propose.fire("click"); - await flush(); - live.resetToStreams(); - await flush(); - out.propose = { - autos: detailAutos.slice(n0), - points: detailCalls.slice(n0).map((points) => (points === null ? null : points.length)), - remembered: remembered - remembered0, - records: records - records0, - }; - // 화면이 몰랐던 낡음 — 빠른 길이 409 낡음이면 곧바로 지금 노선으로 셈 길로 다시 - detailStale = true; - const fallbackFrom = calls.length; - live.resetToStreams(); - await flush(); - const fallbackCall = calls[calls.length - 1]; - out.proposeFallback = { - sent: calls.length - fallbackFrom, - points: fallbackCall.body.points, - status: status.textContent, - }; - answer(fallbackCall); - await flush(); - detailStale = false; - // 저장 분석이 옛 노선(서버 낡음 답) → [업데이트] 뒤 [자동 제안] = 지금 노선으로 셈 길에 방식 실음 - await runner.run(async () => ({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" })); - update.fire("click"); - answer(calls[calls.length - 1]); - await flush(); - const viaDetail = detailCalls.length; - live.resetToStreams(); - const resetCall = calls[calls.length - 1]; - answer(resetCall); - await flush(); - out.proposeLive = { - resetPoints: resetCall.body.points, - resetAuto: resetCall.body.auto ?? null, - detailCalls: detailCalls.length - viaDetail, - remembered: remembered - remembered0, - records: records - records0, - }; + out.records = records; // 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄 const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts")); const a = { id: "a1", chainage_m: 100 }; diff --git a/resources/tester/helper_510_propose.cjs b/resources/tester/helper_510_propose.cjs index 6f0676ebf..539268144 100644 --- a/resources/tester/helper_510_propose.cjs +++ b/resources/tester/helper_510_propose.cjs @@ -69,7 +69,6 @@ const { createFacilityStore } = load("B05_Profile", "B05_Profile_UI_Drainage_Fac pipeRequest: () => [{ chainage_m: 150, source: "user" }], addPipes: (picks) => picks.forEach((item) => added.push([item.chainage_m, item.stream_label?.stream ?? null])), - redraw: () => undefined, }); await propose.open(); const [head, list] = propose.root.children; @@ -80,7 +79,7 @@ const { createFacilityStore } = load("B05_Profile", "B05_Profile_UI_Drainage_Fac rows: list.children.map((row) => row.children[0].children[1].text.trim()), title: title.textContent, addOff: add.disabled, - candidates: propose.candidates(), + noCircles: propose.candidates === undefined, // 점선 원 걷음(326-6) }; boxes[1].checked = true; boxes[1].fire("change"); @@ -88,7 +87,7 @@ const { createFacilityStore } = load("B05_Profile", "B05_Profile_UI_Drainage_Fac boxes[2].fire("change"); add.fire("click"); out.added = added; - out.afterClose = { hidden: propose.root.hidden, candidates: propose.candidates() }; + out.afterClose = { hidden: propose.root.hidden }; // 관 속성 보관소 — 응답 라벨이 다음 요청에 실림(라벨만 있는 맨 배관도 보관) const store = createFacilityStore(); diff --git a/resources/tester/test_171_1_manual_input_label.py b/resources/tester/test_171_1_manual_input_label.py index 9cd4645bc..5d57a1643 100644 --- a/resources/tester/test_171_1_manual_input_label.py +++ b/resources/tester/test_171_1_manual_input_label.py @@ -16,7 +16,7 @@ SPOTS = { "B09_Estimation/B09_Estimation_Sangeun.py": ['"수기 줄" if'], "B09_Estimation/B09_Estimation_UI_Tabs_Base.ts": ["견적 단가는 수기 입력"], "B09_Estimation/B09_Estimation_UI_Tabs_Total.ts": ['"수기 입력"'], - "ui_template/ui_template_locale_b2.ts": [ + "ui_template/ui_template_locale_b5.ts": [ # B08~B11 칸 = b5(512-2 나눔) "수량 원천: 수기 입력(직접비)", "수량 원천: 수기 입력 공종", ], diff --git a/resources/tester/test_298_add_reanalyze.py b/resources/tester/test_298_add_reanalyze.py index a29f2c90f..de24be33b 100644 --- a/resources/tester/test_298_add_reanalyze.py +++ b/resources/tester/test_298_add_reanalyze.py @@ -107,7 +107,9 @@ def test_다시_나눔은_세션_초안으로_계산만_저장_안_함(): load = _body(PANEL, "const loadSaved = ", "const analyze = ") assert "readPendingPipes(projectId as string)" in load assert "computeDetailBasins(projectId as string, pending)" in load - analyze = _body(PANEL, "const analyze = ", "analyzeButton.addEventListener") + analyze = _body( + PANEL, "const analyze = ", "const live = createDrainageLive(" + ) # 326-6 [유역 재분할] 걷음 assert "computeDetailBasins(" in analyze run = RUN[RUN.index("async run(") :] assert "saveDetailPipePoints" not in run + load + analyze diff --git a/resources/tester/test_347_2_drainage_live_ui.py b/resources/tester/test_347_2_drainage_live_ui.py index ef94e14fd..d2678aec8 100644 --- a/resources/tester/test_347_2_drainage_live_ui.py +++ b/resources/tester/test_347_2_drainage_live_ui.py @@ -1,6 +1,6 @@ """347-2 · 347-3 배수유역 자동/수동 · 제안 — 화면 쪽 순수 규칙(logic/ 두 파일). -- 기본 수동 · 자동 = 3 초 조용한 뒤 · 노선 초안이 있을 때만 낡음. +- 자동 모드 없음(요구 326-6 · [업데이트]만) · 노선 초안이 있을 때만 낡음. - 늦은 답 버림: 옛 차례 · 서버가 그만둔 답 · 답 차례가 다른 것은 안 올림. - 제안: 관경(배관 · 추천 ≠ 지금) · 시설(BOX암거 · 세월교) · 관 자리(기존 관 근처 뺌) · [적용] = 한 번에 얹음 · 입력 목록은 안 고침(자동 바꿈 없음). @@ -30,8 +30,7 @@ def r(): def test_mode_and_stale(r): - assert r["quietMs"] == 3000 - assert r["modes"] == ["manual", "auto", "manual"] + assert r["noAutoMode"] assert r["stale"] == [False, True] diff --git a/resources/tester/test_349_drainage_live_flow.py b/resources/tester/test_349_drainage_live_flow.py index 753d70263..b74d18a13 100644 --- a/resources/tester/test_349_drainage_live_flow.py +++ b/resources/tester/test_349_drainage_live_flow.py @@ -1,13 +1,12 @@ -"""349 배수유역 자동/수동 흐름 — 화면 파일(`B05_Profile_UI_Drainage_Live` · `_Run` · `_Suggest`)을 +"""349 · 326-6 배수유역 [업데이트] 흐름 — 화면 파일(`B05_Profile_UI_Drainage_Live` · `_Run` · `_Suggest`)을 가짜 DOM · 시계 · fetch 로 돌림(`helper_349_drainage_live_flow.cjs`). -- 수동: 꼭짓점 옮김 = 흐림 + [업데이트] · [업데이트] 뒤 묶음이 같은 노선 · 관으로 다시 돌아도 답을 받음 - (입력 지문) · 답이 오면 흐림 · 「계산 중」 사라짐 · 제안 「제안 N건」 · [적용]. -- 요청 중 또 옮기면 앞 요청 끊고 다시 흐림. -- 노선 [저장] 뒤 서버 답이 「낡음」 = 수동은 흐림 + [업데이트] · 자동은 3 s 뒤 조용히. -- ⚙ 자동: 흐림 없음 · 3 s 조용한 뒤 셈 · 그 사이 또 옮기면 처음부터 · [업데이트] 막 없음(349-0e). -- 낡은 동안 제안 [적용] 끔(두 모드) · 새 답이 오면 다시 켬(349-0e). -- 되돌리기로 초안이 없어지면 저장분 다시 받기 · 그 답의 낡음대로 · 초안용 답 끊음 · 자동도 3 s 셈 대신(349-0i). +- 꼭짓점 옮김 = 옛 그림 그대로 · [업데이트] 단추에 낡음 점만 · 저절로 셈 없음(시계 60 s 흘려도). +- [업데이트] = 뒤에서 조용히 셈(단추만 「계산 중…」 · 구석 표시) · 묶음이 같은 입력으로 다시 돌아도 답을 받음. +- 낡지 않아도 [업데이트] = 다시 셈(326-6) · 약속은 답이 오르면 참(B06 [>] 확인 창 뒤 이동). +- 요청 중 또 옮기면 앞 요청 끊고 다시 낡음 · 낡은 동안 제안 [적용] · [자동 제안] 끔. +- 노선 [저장] 뒤 서버 답이 「낡음」 = 낡음 표시 · 저절로 셈 없음. +- 되돌리기로 초안이 없어지면 저장분 다시 받기 · 그 답의 낡음대로 · 초안용 답 끊음(349-0i). """ import json @@ -33,30 +32,40 @@ def r(): STALE_TITLE = "노선이 바뀌어 제안이 낡음 — 새 계산 뒤 적용" +IDLE = "업데이트" +BUSY = "계산 중…" -def state(stale, veil, busy, fetches, auto=False): +def state(stale, busy, fetches): return { "stale": stale, - "blur": stale and not auto, # 자동 = 흐림 없음 - "veil": veil, + "pending": stale, # 낡음 = 단추 점(흐림 · 덮개 없음) + "off": busy, # 셈 동안 단추만 잠금 + "label": BUSY if busy else IDLE, "busy": busy, "fetches": fetches, "applyOff": stale, } -def look(row): - return {k: v for k, v in row.items() if k != "applyTitle"} +def look(row, *skip): + return {k: v for k, v in row.items() if k not in ("applyTitle", *skip)} -def test_manual_update_answer_lands_after_same_rerun(r): - assert look(r["moved"]) == state(True, True, False, 0) - assert look(r["sameAgain"]) == state(True, True, False, 0) # 판 번호만 바뀜 — 다시 안 흐림 - assert r["sent"]["fetches"] == 1 and r["sent"]["busy"] and not r["sent"]["veil"] +def test_moved_keeps_old_picture_no_auto(r): + assert look(r["start"]) == {**state(False, False, 0), "applyOff": True} # 제안 없음 + assert look(r["moved"]) == state(True, False, 0) + assert look(r["sameAgain"]) == state(True, False, 0) # 판 번호만 바뀜 + assert look(r["noAuto"]) == state(True, False, 0) # 60 s 지나도 셈 없음 + + +def test_update_answer_lands_after_same_rerun(r): + assert look(r["sent"], "body") == state(True, True, 1) + assert r["sent"]["body"]["route"]["rev"] == 2 # 지금 초안 노선으로 rerun = r["rerunWhileBusy"] assert not rerun["aborted"] and rerun["busy"] # 같은 입력 다시 돎 — 요청 그대로 - assert look(r["answered"]) == {**state(False, False, False, 1), "applied": 1} + assert look(r["answered"], "applied") == state(False, False, 1) + assert r["answered"]["applied"] == 1 def test_suggestions_and_apply(r): @@ -65,92 +74,49 @@ def test_suggestions_and_apply(r): assert len(r["suggest"]["rows"]) == 1 points = {p["chainage_m"]: p for p in r["applied"]["points"]} assert points[100]["options"]["pipe_diameter_mm"] == 1200 - assert 200 not in points # 관 자리 후보는 [적용]이 안 더함 — [자동 제안] 몫(369) + assert 200 not in points assert r["applied"]["remembered"] == 1 - - -def test_자동_제안_두_방식_초기화_세류선만_한_걸음(r): - assert r["proposeStale"] == { - "off": True, - "title": "B05_Drainage_Btn_Propose_Stale", - "calls": 0, # 낡은 동안 끔 — 눌러도 요청 없음 - } - assert r["proposeFresh"] == {"off": False, "title": "B05_Drainage_Btn_Propose_Tip"} - # [자동 제안] 누름 = 패널 후보 목록(510 · test_510) — 여기선 초기화만 초안 - assert r["propose"] == { - "autos": [None], - "points": [None], # 초기화 = 목록 없이(세류선만) - "remembered": 1, - "records": 1, # 답마다 사진첩 한 걸음 - } - # 저장 분석이 옛 노선 — 지금 노선으로 셈 길(detail-basins 안 부름) · 같은 초안 · 한 걸음 - assert r["proposeLive"] == { - "resetPoints": None, - "resetAuto": None, - "detailCalls": 0, - "remembered": 3, # 앞 「409 → 다시」 한 번 포함 - "records": 3, - } - fallback = r["proposeFallback"] - assert fallback["sent"] == 1 and fallback["points"] is None - assert "낡음" not in fallback["status"] # 409 글을 오류로 안 띄움(조용히 다시) + assert r["records"] == 1 # [적용] 한 번 = 사진첩 한 걸음 def test_move_while_busy_cancels(r): moved = r["movedWhileBusy"] - assert moved["aborted"] and moved["veil"] and not moved["busy"] - assert look(r["fresh"]) == state(False, False, False, 3) + assert moved["aborted"] and look(moved, "aborted") == state(True, False, 2) + assert r["promised"] is True + assert look(r["fresh"]) == state(False, False, 3) + + +def test_update_always_recomputes(r): + assert look(r["again"], "route") == {**state(False, True, 4), "applyOff": False} + assert look(r["againDone"]) == state(False, False, 4) def test_stale_blocks_apply(r): - assert r["movedWhileBusy"]["applyOff"] assert r["movedWhileBusy"]["applyTitle"] == STALE_TITLE assert r["staleApply"] == 1 # 눌러도 옛 제안이 안 들어감(앞 [적용] 한 번뿐) - assert not r["fresh"]["applyOff"] and r["fresh"]["applyTitle"] != STALE_TITLE + assert r["fresh"]["applyTitle"] != STALE_TITLE + assert r["proposeStale"] == {"off": True, "title": "B05_Drainage_Btn_Propose_Stale"} + assert r["proposeFresh"] == {"off": False, "title": "B05_Drainage_Btn_Propose_Tip"} -def test_saved_stale_shows_update(r): - assert {k: v for k, v in look(r["savedStale"]).items() if k != "status"} == state( - True, True, False, 3 - ) +def test_saved_stale_marks_only(r): + assert look(r["savedStale"], "status") == state(True, False, 4) assert r["savedStale"]["status"] == "배수유역 낡음" - - -def test_auto_mode(r): - assert look(r["autoSwitch"]) == state( - True, False, False, 3, True - ) # 자동 — 흐림 없음 · [업데이트] 없음 - assert r["auto2999"]["fetches"] == 3 - assert r["auto3000"]["fetches"] == 4 and r["auto3000"]["route"] is None - assert r["auto3000"]["busy"] and not r["auto3000"]["blur"] # 계산 중 = 구석 표시만 - assert look(r["autoDone"]) == state(False, False, False, 4) - assert look(r["autoRestart"]) == state( - True, False, False, 4, True - ) # 2 s + 2 s — 처음부터 다시 3 s - assert look(r["autoMoved"]) == {**state(False, False, False, 5), "mode": "auto"} - assert look(r["autoSavedStale"]) == state(True, False, False, 5, True) - assert look(r["autoSavedDone"]) == state(False, False, False, 6) + assert look(r["savedStaleNoAuto"]) == state(True, False, 4) + assert look(r["savedDone"]) == state(False, False, 5) def test_undo_back_to_saved(r): - # 자동 — 끌기 뒤 1 s 에 되돌리기: 3 s 셈 없음 · 저장분 하나 · 답 오면 낡음 풂 - assert look(r["autoUndoWaiting"]) == {**state(True, False, False, 6, True), "savedLoads": 2} - assert r["autoUndoNoFetch"]["fetches"] == 6 - assert look(r["autoUndo"]) == {**state(False, False, False, 6), "savedLoads": 2} - # 수동 — [업데이트] 요청 중 되돌리기: 초안용 요청 끊음 · 저장분 답 올 때까지 흐림 그대로 waiting = r["undoWaiting"] - assert waiting["aborted"] and waiting["stale"] and waiting["blur"] and not waiting["busy"] - assert look(r["undo"]) == {**state(False, False, False, 7), "savedLoads": 3} - # 저장분 자체가 낡음 = 흐림 · [업데이트] · [적용] 꺼짐 그대로 - assert look(r["undoSavedStale"]) == { - **state(True, True, False, 7), - "savedLoads": 4, - "mode": "manual", - } + assert waiting["aborted"] and look(waiting, "aborted", "savedLoads") == state(True, False, 6) + assert look(r["undo"], "savedLoads") == state(False, False, 6) + assert r["undo"]["savedLoads"] == 2 + assert look(r["undoSavedStale"], "savedLoads") == state(True, False, 6) + assert r["undoSavedStale"]["savedLoads"] == 3 def test_input_key(r): - assert look(r["keys"]) == { + assert r["keys"] == { "rev": True, "order": True, "node": False, diff --git a/resources/tester/test_359_3_b05_drainage_btn_noun.py b/resources/tester/test_359_3_b05_drainage_btn_noun.py index 2f2e29e60..1a3ed4de8 100644 --- a/resources/tester/test_359_3_b05_drainage_btn_noun.py +++ b/resources/tester/test_359_3_b05_drainage_btn_noun.py @@ -7,7 +7,9 @@ LOCALE = Path(__file__).resolve().parents[2] / "ui_template" / "ui_template_loca def test_drainage_buttons_are_nouns(): text = LOCALE.read_text(encoding="utf-8") - assert 'B05_Drainage_Btn_Analyze: ["유역 재분할",' in text - assert 'B05_Drainage_Btn_DeleteSelected: ["관 삭제",' in text + # [유역 재분할] · [관 삭제] · [배관 배치 초기화] 걷음 → [업데이트] 하나(요구 326-6) + assert "B05_Drainage_Btn_Analyze" not in text + assert "B05_Drainage_Btn_DeleteSelected" not in text + assert "B05_Drainage_Btn_Auto:" not in text assert "유역 다시 나누기" not in text assert "선택한 관 삭제" not in text diff --git a/resources/tester/test_472_3_calc_registry.py b/resources/tester/test_472_3_calc_registry.py index c7addd471..4812d13dd 100644 --- a/resources/tester/test_472_3_calc_registry.py +++ b/resources/tester/test_472_3_calc_registry.py @@ -80,6 +80,7 @@ SESSION_PULLS = ( "ui_template/ui_template_locale_b2.ts", "ui_template/ui_template_locale_b3.ts", "ui_template/ui_template_locale_b4.ts", + "ui_template/ui_template_locale_b5.ts", "ui_template/ui_template_locale_common.ts", "ui_template/ui_template_locale_m1.ts", "ui_template/ui_template_locale_m2.ts", diff --git a/resources/tester/test_506_3_b07_cad_cleanup.py b/resources/tester/test_506_3_b07_cad_cleanup.py index 747e63775..f7fba88a9 100644 --- a/resources/tester/test_506_3_b07_cad_cleanup.py +++ b/resources/tester/test_506_3_b07_cad_cleanup.py @@ -46,7 +46,7 @@ def test_6b_host_save_클래스_없음(): def test_4_2_안_쓰는_글_없음(): - locale = _read(ROOT / "ui_template" / "ui_template_locale_b2.ts") + locale = _read(ROOT / "ui_template" / "ui_template_locale_b5.ts") # B08 칸 = b5(512-2 나눔) for key in ( "B08_Quantity_Side_Pending", "B08_Quantity_Btn_Confirm", diff --git a/resources/tester/test_510_auto_pipe_propose.py b/resources/tester/test_510_auto_pipe_propose.py index d2d34de73..9b1011e17 100644 --- a/resources/tester/test_510_auto_pipe_propose.py +++ b/resources/tester/test_510_auto_pipe_propose.py @@ -152,7 +152,7 @@ def test_화면_고른_것만_넣음_라벨_왕복(): "측점 420 m — 간격 보충(흐름이 모이는 자리)", ] # held 는 목록 밖 assert got["title"] == "자동 제안 3건 · 있는 관 1 뺌" - assert got["addOff"] is True and got["candidates"] == [50, 320, 420] + assert got["addOff"] is True and got["noCircles"] # 배수유역도 점선 원 없음(326-6) assert got["added"] == [[320, "S4"], [420, None]] # 고른 둘만 - assert got["afterClose"] == {"hidden": True, "candidates": []} + assert got["afterClose"] == {"hidden": True} assert got["carried"] == {"stream": "S4", "order": 1} # 응답 → 보관소 → 요청 diff --git a/resources/tester/test_512_2_drainage_update.py b/resources/tester/test_512_2_drainage_update.py new file mode 100644 index 000000000..e09aecb64 --- /dev/null +++ b/resources/tester/test_512_2_drainage_update.py @@ -0,0 +1,68 @@ +"""512-2(요구 326-6) 배수유역도 [업데이트] — 점선 원 · 단추 셋 걷음 · 노선선 = 답의 노선 · B06 [>] 확인 창. + +- 서버 답 `route_xy` = 그 답을 셈한 노선(사업지 m) — 화면 노선선 · 관 자리 · 유역이 늘 짝 + (초안 노선 [업데이트] 뒤 저장 노선을 그려 생기던 「노선 곁 빈 곳」). +- 낡은 답(세부 유역 없음)이 와도 그림이 있으면 옛 그림 그대로 · 관만 받음. +- 흐름(낡음 · 늘 다시 셈 · 저절로 셈 없음)은 test_349 · 화면 실측은 PLAN 512-2. +""" + +from pathlib import Path +from uuid import uuid4 + +from B04_PreProcess import B04_PreProcess_Router_Basins as basins +from common_util.common_util_drainage_context import DrainageContext +from common_util.common_util_drainage_detail import DrainageDetail, pipes_from_chainages +from common_util.common_util_route_geometry import RouteVertex + +ROOT = Path(__file__).resolve().parents[2] +B05 = ROOT / "B05_Profile" + + +def _read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_답에_셈한_노선이_실림(): + vertices = [RouteVertex(10.0, 20.0, 100.0, 0.0), RouteVertex(310.1234, 20.0, 90.0, 300.1234)] + context = DrainageContext(stored_path="x", project_root=ROOT, vertices=vertices) + detail = DrainageDetail(route_lonlat=[], pipes=pipes_from_chainages(vertices, [100.0])) + points = basins.carry_facility_attributes(basins._rebuilt(detail), []) + payload = basins._payload(uuid4(), context, detail, points, False) + assert payload["route_xy"] == [[10.0, 20.0], [310.123, 20.0]] + + +def test_단추는_업데이트_자동제안_둘(): + chrome = _read(B05 / "B05_Profile_UI_Drainage_Chrome.ts") + assert "header.append(updateButton, proposeButton);" in chrome + for gone in ("analyzeButton", "deleteButton", "autoButton"): + assert gone not in chrome + locale = _read(ROOT / "ui_template" / "ui_template_locale_b2.ts") + assert "B05_Drainage_Btn_Auto:" not in locale and "B05_Drainage_Btn_Analyze" not in locale + + +def test_점선_원_없음(): + render = _read(B05 / "B05_Profile_UI_Drainage_Render.ts") + assert "candidates" not in render and "setLineDash" not in render + assert "candidates" not in _read(B05 / "B05_Profile_UI_Drainage_Propose.ts") + + +def test_노선선은_답의_노선_낡은_답은_옛_그림(): + panel = _read(B05 / "B05_Profile_UI_Drainage_Panel.ts") + apply = panel[panel.index("function apply(") : panel.index("function takePipes(")] + assert apply.index("if (keep) return void takePipes(response);") < apply.index("basins = ") + assert "const keep = !!response.stale && basins.length > 0;" in apply + assert "takeRoute(answered, false)" in apply + assert "if (!routeFromAnswer) takeRoute(points, true);" in panel # 페이지 노선 = 첫 답 전만 + + +def test_B06_다음은_낡으면_확인_뒤_업데이트(): + shared = _read(ROOT / "A00_Common" / "design" / "design_shared.ts") + go = shared[shared.index("export function confirmAndGo(") :] + go = go[: go.index("\n}\n", go.index("async function drainageFirst("))] + assert "void drainageFirst(shared).then((ready) => (ready ? go() : undefined));" in go + first = go[go.index("async function drainageFirst(") :] + assert ( + first.index("side?.stale()") + < first.index("showConfirmDialog(") + < first.index("side.update()") + ) diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index a285a8a60..51b1be1e4 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -12,7 +12,8 @@ * - ui_template_locale_common.ts : 공통 (액션/상태/폼/네비게이션/워크플로우/앱 셸) * - ui_template_locale_a.ts : A 그룹 (로그인 전 A01~A09) * - ui_template_locale_b1.ts : B 그룹 전반부 (B01~B04) - * - ui_template_locale_b2.ts : B 그룹 후반부 (B05~B11) + * - ui_template_locale_b2.ts : B 그룹 후반부 (B05~B07) + * - ui_template_locale_b5.ts : B 그룹 끝 (B08~B11) * * 이 파일의 export 시그니처는 분할 이전과 동일하므로 소비처 import 수정은 불필요하다. * ========================================================================== */ @@ -24,6 +25,7 @@ import { ui_locales_a } from "./ui_template_locale_a"; import { ui_locales_b1 } from "./ui_template_locale_b1"; import { ui_locales_b2 } from "./ui_template_locale_b2"; import { ui_locales_b3 } from "./ui_template_locale_b3"; +import { ui_locales_b5 } from "./ui_template_locale_b5"; import { ui_locales_m1 } from "./ui_template_locale_m1"; import { ui_locales_m2 } from "./ui_template_locale_m2"; @@ -60,6 +62,7 @@ export const ui_locales = { ...ui_locales_b1, ...ui_locales_b2, ...ui_locales_b3, + ...ui_locales_b5, ...ui_locales_m1, ...ui_locales_m2, } as const; diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 499da23d1..4cada153b 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -1,6 +1,6 @@ /* ============================================================================= * ui_template_locale_b2.ts - * 다국어 사전 — B 그룹 후반부 (B05_Profile ~ B11_Status) + * 다국어 사전 — B 그룹 후반부 (B05_Profile ~ B07_DesignDetail) · B08~B11 은 `ui_template_locale_b5.ts` * * 워크플로우 진행 단계 라벨은 ui_template_locale_common.ts 의 WF_Step_* 키를 * 재사용한다 (중복 등록 방지). @@ -30,23 +30,6 @@ export const ui_locales_b2 = { "유역 안쪽 상류 세류망을 굵게 강조합니다.", "Emphasize the upstream stream network inside the basin.", ], - // 버튼 이름만으로 무엇이 바뀌는지 알기 어려워 동작을 이름·툴팁에 함께 적는다 - // (2026-08-19 사용자 지시 4). - B05_Drainage_Btn_Analyze: ["유역 재분할", "Recompute sub-basins"], - B05_Drainage_Btn_Analyze_Tip: [ - "B04에서 분석해 둔 배수유역을 불러와, 지금 배치된 배관을 기준으로 세부유역(관이 담당하는 구역)을 다시 나눕니다. 관을 더하지 않습니다 — 간격 보충은 [자동 제안]. B04 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.", - "Reloads the B04 drainage analysis and re-splits sub-basins around the current culverts without adding any — use [Auto suggest] to fill spacing gaps. Run the analysis in B04 first if none exists.", - ], - B05_Drainage_Btn_DeleteSelected: ["관 삭제", "Delete selected culvert"], - B05_Drainage_Btn_DeleteSelected_Tip: [ - "지도에서 고른 배관 한 개를 지웁니다. 관을 먼저 눌러 고른 뒤에 쓸 수 있습니다.", - "Removes the culvert selected on the map. Select a culvert marker first.", - ], - B05_Drainage_Btn_Auto: ["배관 배치 초기화", "Reset culvert layout"], - B05_Drainage_Btn_Auto_Tip: [ - "손으로 넣거나 옮기거나 지운 배관을 모두 버리고, 세류선(계곡)이 노선과 만나는 자리에만 배관을 둡니다. 유역 구획도 함께 다시 나뉩니다. 초안이라 [저장] 때 남습니다.", - "Discards every manual culvert edit and keeps culverts only where streams cross the route; sub-basins are re-split as well. This is a draft — it is kept on [Save].", - ], B05_Drainage_Menu_Add: ["배수관 추가", "Add culvert"], B05_Drainage_Menu_Delete: ["배수관 삭제", "Remove culvert"], B05_Drainage_Layer_Hotspots: ["집중유역", "Inflow hotspots"], @@ -563,444 +546,6 @@ export const ui_locales_b2 = { "이 장은 측점 여러 개를 담습니다. 지반·계획 정보는 측점 도면에서 봅니다.", "This sheet holds several stations. Ground and plan details live on each station drawing.", ], - - /* --- B08_Quantity 수량 산출 --- */ - B08_Quantity_Title: ["수량산출", "Quantity"], - B08_Quantity_Tab_Earthwork: ["토공", "Earthwork"], - B08_Quantity_Grid_Loading: ["토적표를 만드는 중입니다…", "Building the earthwork table…"], - B08_Quantity_Grid_Empty: [ - "측점 단면적이 아직 없습니다. 횡단 설계를 먼저 마치세요.", - "No cross-section areas yet. Finish the cross-section design first.", - ], - B08_Quantity_Grid_Failed: [ - "토적표를 불러오지 못했습니다.", - "Failed to load the earthwork table.", - ], - B08_Quantity_Tab_Summary: ["토공집계", "Earthwork Summary"], - B08_Quantity_Tab_Haul: ["운반거리", "Haul Distance"], - B08_Quantity_Tab_StructureSummary: ["구조물 집계표", "Structure Summary"], - B08_Quantity_Tab_UnitQuantity: ["구조물 원단위", "Structure Unit Quantity"], - B08_Quantity_Tab_Material: ["자재총괄", "Material Summary"], - B08_Quantity_Tab_Preparation: ["준비공·사방공", "Preparation & Erosion Control"], - B08_Quantity_Side_Method_Label: ["시공법", "Method"], - B08_Quantity_Method_Unset: ["안 정함", "Not set"], - B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"], - B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"], - B08_Quantity_Placing_NotApplied: [ - "콘크리트 물량이 「콘크리트 타설」 공종으로 견적에 넘어갑니다.", - "The placing method changes this work item's unit price — concrete volume is handed over as a placing work item.", - ], - B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"], - B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"], - B08_Quantity_Side_TopsoilTarget_Label: ["표토제거 대상", "Topsoil removal target"], - B08_Quantity_TopsoilTarget_RoadAndCut: [ - "노면 + 절토 사면 (기본 · 법 문언)", - "Road surface + cut slope (default · law)", - ], - B08_Quantity_TopsoilTarget_RoadOnly: ["노면만", "Road surface only"], - B08_Quantity_Side_TopsoilTarget_Hint: [ - "기본 근거: 산림자원법 시행규칙 별표2 Ⅰ.2.차.(6) 「노면·절토대상지에 있는 … 표토는 전량 제거한 후 … 운반하고 쌓아두어야 한다」 — 성토 사면은 「제거·정리」라 대상 밖. 노면만: 실무 영월 한 건(1m당 3.34㎡)이 노면만 셈 — 한 건이라 기본으로 삼지 않고 고를 수 있게 둠(면적 약 3배 차이)", - "Default basis: the forest-road rule names road surface and cut areas; fill slopes are only cleared. Road-only follows one field case (Yeongwol, 3.34 ㎡ per m) — selectable, not the default (about 3× area difference)", - ], - B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"], - // ── 확정 3차(2026-09-09) 로 정한 값들 — **화면에 칸으로 세우고 근거를 보인다.** - // 사용자 지시: 「값을 코드에 박고 끝내지 말 것 · 대신 페이지에 남길 것」. - B08_Quantity_Side_Structure: ["구조물·사토", "Structures & Spoil"], - B08_Quantity_Side_BenchCut_Label: ["층따기 길이(m)", "Bench cut length (m)"], - B08_Quantity_Side_BenchCut_Hint: [ - "면적 × 이 값 = ㎥ — 비우면 층따기 줄이 막힙니다(확정 2차 ①)", - "Area × this = ㎥ — blank keeps the bench-cut row blocked", - ], - B08_Quantity_Side_BenchCut_Suggest: [ - "층따기 표준치수 — 최소 높이 토사 500 mm 이상 · 암반 400 mm 이상 · 폭 1 m 이상(기계 작업을 할 때는 3 m 이상). 근거: KDS 44 30 00 도로토공 2.3.4 (4) ② 「기초지반의 종류에 따른 층따기의 표준치수」", - "Bench cut standard dimensions — minimum height: soil ≥ 500 mm · rock ≥ 400 mm · width ≥ 1 m (≥ 3 m for machine work). Basis: KDS 44 30 00 Road Earthwork 2.3.4 (4) ②", - ], - B08_Quantity_Side_Rubble_Label: ["기초잡석 두께(m)", "Rubble base thickness (m)"], - B08_Quantity_Side_Rubble_Hint: [ - "폭은 버림 폭과 같음(KCS 34 50 05) · 두께 0.2m 는 사용자 확정 3차 ② — 품셈 12-25 는 두께를 정하지 않음", - "Width equals the blinding width (KCS 34 50 05); 0.2 m thickness is a user decision — the standard gives none", - ], - B08_Quantity_Side_SpoilDistance_Label: ["사토장까지 거리(m)", "Distance to spoil site (m)"], - B08_Quantity_Side_SpoilDistance_Hint: [ - "사토장을 노선에 놓으면 그 측점까지 거리가 계산되어 이 값은 안 씁니다 — 안 놓았을 때의 대체값입니다", - "Used only until a spoil site is placed on the route — then the distance is computed from that station", - ], - B08_Quantity_Side_Water_Label: ["구조물터파기 용수", "Structure trench water"], - B08_Quantity_Water_Dry: ["육상(용수 없음)", "Dry"], - B08_Quantity_Water_Wet: ["용수", "Wet"], - B08_Quantity_Water_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"], - B08_Quantity_Side_Water_Hint: [ - "⚠ 「육상」은 통상값이고 사용자 확정이 아닙니다(확정 3차 ④) — 품셈 9-13 의 18구분이 이 값으로 갈립니다", - "⚠ “Dry” is a customary default, not a user decision — it selects one of the 18 sub-items", - ], - B08_Quantity_Side_SeedSpray_Label: ["초류종자살포 비탈면 토질", "Seed spray slope soil"], - B08_Quantity_SeedSpray_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], - B08_Quantity_Side_SeedSpray_Hint: [ - "품셈 5-24 씨앗뿜어붙이기가 5-24-1 기계/일반 · 5-24-2 기계/마사토 둘로 갈림 — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다", - "Pumsem 5-24 splits into general / decomposed-granite soil — no suggestion; unset leaves the bill row with an input reason", - ], - B08_Quantity_Side_RootRemoval_Label: ["제근 굴착기 크기", "Root removal excavator size"], - B08_Quantity_RootRemoval_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], - B08_Quantity_RootRemoval_Suggest: ["제안(비우면 안 정함):", "Suggested (blank = not set):"], - B08_Quantity_RootRemoval_FillSuggested: ["제안값 넣기", "Fill suggested"], - B08_Quantity_Side_StandVolume_Label: ["임목축적 등급", "Stand volume class"], - B08_Quantity_StandVolume_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"], - B08_Quantity_StandVolume_Low: ["소림(30~60㎥/㏊)", "Low (30–60 ㎥/ha)"], - B08_Quantity_StandVolume_Mid: ["중림(60~90㎥/㏊)", "Mid (60–90 ㎥/ha)"], - B08_Quantity_StandVolume_High: ["밀림(90㎥/㏊ 이상)", "High (90+ ㎥/ha)"], - B08_Quantity_Side_StandVolume_Hint: [ - "제근(품셈 9-21)이 이 등급으로 갈립니다 — ⚠ 본수가 아니라 축적이고, 산림조사부·영림계획에서 옮겨 적는 값입니다", - "Root removal (9-21) splits by this class — stand volume, not tree count; copied from the forest survey", - ], - B08_Quantity_Side_Chipping: ["임목파쇄", "Wood Chipping"], - B08_Quantity_Chipping_Label: ["임목파쇄를 셀 것인가", "Count wood chipping"], - B08_Quantity_Chipping_Off: ["안 셈(기본)", "No (default)"], - B08_Quantity_Chipping_On: ["셈", "Yes"], - B08_Quantity_Chipping_Volume: ["파쇄 부피(㎥)", "Chipping volume (㎥)"], - B08_Quantity_Side_Chipping_Hint: [ - "기본은 안 셉니다(확정 5차 5번) — 현장에 따라 필요하면 켜세요. ⚠ 켜도 부피를 넣어야 값이 섭니다(실무는 부피로 셈)", - "Off by default — turn on per site. Volume must be entered for the row to carry a quantity", - ], - B08_Quantity_Side_TreeWaste: ["임목폐기물", "Tree Waste"], - B08_Quantity_TreeWaste_Stems: ["표본지 1,000㎡당 본수", "Stems per 1,000 ㎡ (sample plot)"], - B08_Quantity_TreeWaste_Dbh: ["평균 흉고직경(㎝)", "Mean DBH (cm)"], - B08_Quantity_TreeWaste_Height: ["평균 수고(m)", "Mean tree height (m)"], - B08_Quantity_TreeWaste_UnitWeight: ["단위체적중량(㎏/㎥)", "Unit weight (kg/㎥)"], - B08_Quantity_Side_TreeWaste_Hint: [ - "현장 조사값 넷을 넣으면 준비공 「임목폐기물 처리」가 톤으로 섭니다. 산식: 지상부 WA = 0.5 × 3.14 × (흉고직경/2)² × 수고 × (1+0.3) × 단위체적중량 × 본수 · 본수 = 1,000㎡당 본수 × 벌목·지장목제거 면적 — 환경친화적인 도로건설 지침 부록1 · 한국건설기술연구원(2012) 3.2. 단위체적중량 참고: 소나무·침엽수 1,210~1,250 · 단풍·산벚 1,250~1,300 · 느티 1,300~1,340 · 상수리·졸참 1,340 이상(조경설계요람). 기본값 없음 — 수종·임분마다 다름", - "Enter the four field-survey values to raise the tree-waste row in tonnes. WA = 0.5 × 3.14 × (DBH/2)² × height × 1.3 × unit weight × stems (road guideline appendix 1, KICT 2012). No defaults — values differ by species and stand", - ], - B08_Quantity_TreeWaste_RootMethod: ["뿌리부 산정법", "Root estimate"], - B08_Quantity_TreeWaste_RootRatio: [ - "분배비 WA × 15/85 (기본)", - "Share ratio WA × 15/85 (default)", - ], - B08_Quantity_TreeWaste_RootBall: ["뿌리분 체적 × 1,300㎏/㎥", "Root-ball volume × 1,300 kg/㎥"], - B08_Quantity_TreeWaste_RootBallDiameter: [ - "뿌리분 직경 D(m, 흉고직경 아님) — 체적법만", - "Root-ball diameter D (m, not DBH) — volume method", - ], - B08_Quantity_TreeWaste_Root_Hint: [ - "환경친화적인 도로건설 지침 부록1 이 두 방법을 둡니다. 분배비(기본): 뿌리 15 : 지상 85(일반 수목류, 한국건설기술연구원 2012). 뿌리분 체적: V = πD²(D/2 + RH2/3) = 0.3927D³ · 뿌리분 중량 = V × 1,300㎏/㎥ × 본수. ⚠ 지침은 D 를 「뿌리분의 직경(DBH, m)」으로 적어 흉고직경인지 뿌리분 직경인지 갈리지 않음 — 흉고직경으로 두지 않고 따로 받습니다. 참고: 실무 실정보고(한 현장)는 뿌리 14 : 86 · 0.65톤/㎥ 를 씀", - "Road guideline appendix 1 gives two methods: share ratio 15:85 (default), or root-ball volume 0.3927·D³ × 1,300 kg/㎥ × stems. The guideline labels D ambiguously, so it is entered separately", - ], - B08_Quantity_TreeWaste_Price: ["처리단가(원/톤) — 수동", "Disposal price (KRW/t) — manual"], - B08_Quantity_TreeWaste_Price_Hint: [ - "⚠ 공표 단가 없음(한국건설자원협회 중간처리단가 미확보) — 넣으면 금액이 서되 「미확정」으로 셉니다(빨간 테두리). 비목은 경비 — 원가계산서 폐기물처리비 줄로 가고 일반관리비·이윤 밑수에 듭니다(예정가격작성기준 제19조③18호)", - "No published price — a manual price raises the amount as unconfirmed (red outline). Cost category is expense, inside the overhead and profit bases", - ], - B08_Quantity_TreeWaste_Separate: ["분리발주", "Separate disposal contract"], - B08_Quantity_TreeWaste_Separate_Off: ["분리발주 아님(기본)", "No (default)"], - B08_Quantity_TreeWaste_Separate_On: ["분리발주", "Yes"], - B08_Quantity_TreeWaste_Separate_Hint: [ - "⚠ 임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 · 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물) — 기본 꺼짐으로 원가계산서 경비에 듭니다. 이 칸은 다른 건설폐기물(위탁처리 100톤 이상 분리발주 대상)이 생길 때 쓰는 칸 — 켜면 총원가 밖에 서고 총공사비에만 더합니다. 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)", - "Tree waste is not subject to separate ordering (Ministry of Environment reply; classified as business general waste) — off by default, inside the expense category. Use this switch for other construction waste (100 t or more). When on, it sits outside total cost. 5 t or more requires a discharge report", - ], - B08_Quantity_Side_FaceDressing: ["면고르기", "Face dressing"], - B08_Quantity_FaceDressing_Cut: ["절토면 토질", "Cut face soil"], - B08_Quantity_FaceDressing_Fill: ["성토면 시공·토질", "Fill face method·soil"], - B08_Quantity_FaceDressing_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], - B08_Quantity_FaceDressing_FillArea: ["성토면 면적(㎡)", "Fill face area (㎡)"], - B08_Quantity_FaceDressing_CutArea: ["절토면 면적(㎡)", "Cut face area (㎡)"], - B08_Quantity_FaceDressing_Area_Placeholder: ["파종 면적 그대로", "Same as seeding area"], - B08_Quantity_FaceDressing_Suggest: ["제안(비우면 안 정함):", "Suggested (blank = not set):"], - B08_Quantity_FaceDressing_FillSuggested: ["제안값 넣기", "Fill suggested"], - B08_Quantity_Side_FaceDressing_Hint: [ - "품셈 9-19-1 원문 표 두 벌(절토면 토질 6 · 성토면 시공·토질) — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다. 면적은 비우면 초류종자살포 면적(× 반영률) 그대로 · 넣으면 그 값으로 덮어씀", - "Standard estimate 9-19-1 tables (cut face soil · fill face method/soil) — no suggested value; unset leaves the bill row with an input reason. Area left blank uses the seeding area (× ratio); a value overrides it", - ], - B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"], - B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"], - B08_Quantity_Side_SubgradeCompaction_Hint: [ - "기본은 안 셉니다 — 근거: 산림품셈 9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」. 켜면 토공집계에 「노체다짐」 줄이 성토량(㎥)으로 섭니다. [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 이미 셉니다", - "Off by default — forest standard 9-16-2 note ⑤ applies it only to large fills needing layer compaction. When on, a compaction row equal to the fill volume is added; the excavator of the note ③ pairing is already in the fill (spreading) row", - ], - B08_Quantity_Side_Frame: ["규준틀 개소당 재료", "Batter Board Materials (per unit)"], - B08_Quantity_Side_Frame_Hint: [ - "⚠ 채워진 값은 실무 관측값(제안값)이고 법정 기준이 아닙니다 — 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둡니다. 손율은 원문값(비탈 50% · 수평 80%)", - "Pre-filled values are field observations, not a legal standard — the standard only says “per design quantity”", - ], - B08_Quantity_Frame_Square: ["각재 50×50 (㎥/개소)", "Square timber (㎥/ea)"], - B08_Quantity_Frame_Board: ["판재 T12 (㎥/개소)", "Board T12 (㎥/ea)"], - B08_Quantity_Frame_Nail: ["못 (㎏/개소)", "Nails (㎏/ea)"], - B08_Quantity_Side_Ancillary: ["부대시설 개소", "Ancillary Facilities"], - B08_Quantity_Side_Ancillary_Hint: [ - "개소를 넣어야 줄이 섭니다 — 원문이 배치 간격을 정하지 않아 연장÷500 같은 산식을 쓰지 않습니다(확정 13)", - "Counts are design input — the source sets no spacing rule, so no formula is used", - ], - B08_Quantity_Ancillary_national_point_sign: ["국가지점번호판(개소)", "National point signs"], - B08_Quantity_Ancillary_guide_sign: ["임도 안내판(개소)", "Guide signs"], - B08_Quantity_Ancillary_gate: ["차단기(개소)", "Gates"], - B08_Quantity_Ancillary_site_container: ["가설창고(개소)", "Site containers"], - B08_Quantity_Ancillary_flood_supplies: ["수방대책 자재(식)", "Flood supplies (set)"], - B08_Quantity_Side_TopsoilHaul_Label: ["표토 운반거리(m)", "Topsoil haul distance (m)"], - B08_Quantity_Side_TopsoilHaul_Hint: [ - "⚠ 별표2 는 표토를 「전량 제거한 후 최고 홍수위보다 높은 장소로 운반하고 쌓아두어야」로 정합니다 — 거리를 넣으면 운반 줄이 섭니다. 표토제거는 노면 + 절토 사면 ㎡ 로 서고, 두께는 운반 부피(㎡ × 두께)에만 씁니다. 제거 품(9-15-2)에 20m 압토가 들어 있어 운반은 20m 를 넘는 몫만 셉니다", - "The law requires hauling and stockpiling removed topsoil — enter the distance to raise that row. Removal counts road surface + cut slope in ㎡; thickness only sizes the haul volume. The removal rate already includes a 20 m push, so only the excess is hauled", - ], - B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"], - B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"], - B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"], - B08_Quantity_Placing_Current: ["적용 중", "In use"], - B08_Quantity_Placing_Default_Tag: ["기본값 — 확인 필요", "default — needs review"], - B08_Quantity_Placing_Hint: ["참고 단가(B09 산출):", "Reference unit price (from B09):"], - B08_Quantity_Placing_Ready: ["레디믹스트", "Ready-mixed"], - B08_Quantity_Placing_Machine: ["기계비빔", "Machine-mixed"], - B08_Quantity_Placing_Hand: ["인력비빔", "Hand-mixed"], - B08_Quantity_Placing_Default_Notice: [ - "⚠ 기본값 「레디믹스트」로 계산 중 — 아직 안 정한 값입니다. 타설 방식에 따라 단가가 갈립니다.", - "⚠ Using the default (ready-mixed) — not yet decided. Unit prices differ by method.", - ], - B08_Quantity_Material_Failed: [ - "자재총괄을 불러오지 못했습니다.", - "Failed to load the material summary.", - ], - B08_Quantity_Haul_Missing: [ - "운반계획이 아직 없습니다. 노선설계에서 [확정]을 누르면 만들어집니다.", - "No haul plan yet. Press [Confirm] on the profile design to build it.", - ], - B08_Quantity_Haul_Excluded: ["내역 제외", "Not billed"], - B08_Quantity_Side_Ratios: ["반영률(%)", "Application ratios (%)"], - /* 반영률 항목 이름 — 서버 키를 사람이 읽는 말로. 거창 실무 시트 문구를 따른다. */ - B08_Quantity_Ratio_FillCompaction: ["성토면다짐", "Fill slope compaction"], - B08_Quantity_Ratio_SeedFill: ["초류종자살포(성토면)", "Seed spray (fill)"], - B08_Quantity_Ratio_SeedCut: ["초류종자살포(절토면)", "Seed spray (cut)"], - B08_Quantity_Ratio_TreeRemoval: ["지장목제거", "Obstacle removal"], - B08_Quantity_Side_RockSet: ["암 갈래 세트", "Rock class set"], - B08_Quantity_Side_RockRatios: [ - "암 갈래 구성비(%) — 암 총량 기준", - "Rock class split (%) of rock volume", - ], - B08_Quantity_Btn_Save: ["저장", "Save"], - B08_Quantity_Save_Success: ["산출 조건을 저장했습니다.", "Calculation settings saved."], - B08_Quantity_Save_Failed: ["산출 조건을 저장하지 못했습니다.", "Failed to save the settings."], - B08_Quantity_Unsaved: ["저장하지 않은 변경이 있습니다.", "You have unsaved changes."], - B08_Quantity_Side_Method: ["산출법", "Method"], - B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"], - B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"], - B08_Quantity_Factor_Reach: [ - "이 계수는 유토곡선·운반표·기초단가에도 같이 닿습니다.", - "These factors also feed the mass-haul curve, haul table and basis units.", - ], - B08_Quantity_Factor_Default: ["기본값", "Default"], - B08_Quantity_Side_HaulLimits: ["운반장비 거리 경계", "Haul equipment distance limits"], - B08_Quantity_Side_DozerLimit_Label: ["도쟈 한계거리(m)", "Dozer haul limit (m)"], - B08_Quantity_Side_FreeHaul: ["종무대(바꾸지 않음)", "Free haul (fixed)"], - B08_Quantity_Side_HaulLimits_Reach: [ - "유토곡선 운반 배분(무대·도쟈·덤프)에 닿습니다 — 바꾼 뒤 종횡단에서 다시 저장해야 운반표가 따라옵니다.", - "Feeds the mass-haul split (free/dozer/dump) — re-save the profile/cross sections for the haul table to follow.", - ], - B08_Quantity_Factor_Range: ["품셈 범위", "Standard range"], - B08_Quantity_Factor_OutOfRange: [ - "품셈 범위 밖입니다 — 사유를 적어 주세요(토질시험 값일 수 있습니다).", - "Outside the standard range — please note why (it may be a soil-test value).", - ], - B08_Quantity_Factor_Reason: ["사유", "Reason"], - B08_Quantity_Factor_Pumsem: ["품셈 체적변화율(C)", "Standard volume-change (C)"], - - /* --- B09_Estimation 원가계산 --- */ - B09_Estimation_Title: ["원가계산", "Cost Estimate"], - B09_Estimation_Tab_CostSheet: ["공사원가계산서", "Cost Statement"], - B09_Estimation_Tab_Boq: ["설계내역서", "Bill of Quantities"], - B09_Estimation_Boq_Total: ["내역서 합계", "Bill total"], - B09_Estimation_PB_Empty: [ - "설계내역서를 먼저 불러오면 단가산출서가 섭니다.", - "Load the bill first and the price-basis sheets appear.", - ], - B09_Estimation_PB_Pick: ["왼쪽에서 산출서를 고르세요.", "Pick a sheet on the left."], - B09_Estimation_PB_Ref: ["참조", "Refers to"], - B09_Estimation_Mat_Contractor: ["사급 자재 (도급 재료비)", "Contractor-supplied (in the bill)"], - B09_Estimation_Mat_Owner: [ - "관급 자재 — 총원가 밖 별도 표기", - "Owner-supplied — listed outside the total cost", - ], - B09_Estimation_Mat_Unknown: [ - "관급·사급이 안 갈린 것 — 어느 합계에도 안 넣습니다", - "Supply type undecided — excluded from both totals", - ], - B09_Estimation_Mat_None: [ - "이 프로젝트에는 자재 줄이 없습니다 — 구조물이 없거나 자재가 안 나온 상태입니다.", - "This project has no material rows yet.", - ], - B09_Estimation_Mat_Empty: [ - "설계내역서를 먼저 불러오면 자재대가 섭니다.", - "Load the bill first and the material sheet appears.", - ], - B09_Estimation_Boq_Precision: [ - "수량 표시는 소수 2자리, 계산은 전정밀 — 표시값끼리 곱하면 끝자리가 다릅니다.", - "Quantities are shown to 2 decimals but computed at full precision — multiplying the shown values gives a slightly different last digit.", - ], - B09_Estimation_Boq_Excluded: [ - "검산용 줄 — 수량만 보이고 금액을 매기지 않습니다", - "Check rows — quantity only, never priced", - ], - B09_Estimation_Boq_NotOurs: [ - "여기서 세지 않는 줄 (다른 표에 있거나 이 노선에 없음)", - "Counted elsewhere or not present on this route", - ], - B09_Estimation_Boq_NeedsInput: [ - "입력하면 풀리는 것 (설계 화면에서 값을 고르면 금액이 섭니다)", - "Waiting on input — pick the value on the design screen and the amount appears", - ], - B09_Estimation_Boq_NeedsWork: [ - "우리가 만들어야 하는 것 (원단위·전개식 없음)", - "Needs build — unit data or formula is missing", - ], - B09_Estimation_Boq_Missing: [ - "금액을 못 세운 줄 — 0 으로 안 채웁니다", - "Rows without an amount — shown as-is, not zero-filled", - ], - B09_Estimation_Boq_Materials: ["자재 (별도 벌)", "Materials (separate set)"], - B09_Estimation_Boq_NoMaterialPrice: [ - "사급 자재 단가가 아직 없어 자재비가 빠져 있습니다 — 지금 합계는 모자란 값입니다.", - "Contractor-supplied material prices are missing, so material cost is absent — this total is short.", - ], - B09_Estimation_Boq_Load: ["B08 수량 불러오기", "Load B08 quantities"], - B09_Estimation_Boq_Failed: [ - "B08 인계 자료를 받지 못했습니다.", - "Could not load the B08 handoff.", - ], - B09_Estimation_Tab_UnitPrice: ["일위대가", "Unit Price"], - B09_Estimation_Tab_PriceBasis: ["단가산출근거", "Price Basis"], - B09_Estimation_Tab_Machine: ["중기", "Equipment"], - B09_Estimation_Tab_Duration: ["공사기간", "Duration"], - B09_Estimation_Tab_Supply: ["관급·사급", "Supplied Materials"], - B09_Estimation_Tab_BaseData: ["기초자료", "Base Data"], - B09_Estimation_Tab_DesignDoc: ["설계서 구성", "Design Document Set"], - B09_Estimation_Tab_BasisSheet: ["산출기초", "Calculation Basis"], - B09_Estimation_Group_Condition: ["공사 조건", "Project Conditions"], - B09_Estimation_Group_RateVersion: ["요율 판", "Rate Edition"], - B09_Estimation_Group_Supplied: ["관급자재", "Owner-Supplied"], - B09_Estimation_Group_Profit: ["이윤 조정", "Profit Adjustment"], - B09_Estimation_Field_DirectMaterial: ["직접재료비", "Direct Material"], - B09_Estimation_Field_DirectLabor: ["직접노무비", "Direct Labor"], - B09_Estimation_Field_DirectExpense: ["직접경비", "Direct Expense"], - B09_Estimation_Field_Duration: ["공사기간(일)", "Duration (days)"], - B09_Estimation_Field_WorkType: ["공종", "Work Type"], - B09_Estimation_Field_OwnerMaterial: ["순자재대", "Net Material"], - B09_Estimation_Field_ProcurementFee: ["조달수수료", "Procurement Fee"], - B09_Estimation_Field_ProfitAdjust: ["조정액", "Adjustment"], - B09_Estimation_Field_TargetContract: ["목표 도급공사비", "Target Contract"], - B09_Estimation_Btn_Recalc: ["재계산", "Recalculate"], - B09_Estimation_Btn_Confirm: ["확정", "Confirm"], - B09_Estimation_Col_Item: ["비목", "Item"], - B09_Estimation_Col_Amount: ["금액", "Amount"], - B09_Estimation_Col_Rate: ["요율", "Rate"], - B09_Estimation_Col_Basis: ["산출근거", "Basis"], - B09_Estimation_Col_Note: ["비고", "Note"], - B09_Estimation_Adopted: ["채택", "Adopted"], - B09_Estimation_NotAdopted: ["미채택", "Not adopted"], - B09_Estimation_Suggest_Adjust: [ - "목표 도급공사비를 맞추려면 이윤을 이만큼 깎아야 합니다 — 적용하려면 조정액에 직접 넣으세요.", - "To hit the target contract amount, profit must be reduced by this much — enter it in Adjustment to apply.", - ], - B09_Estimation_Calc_Failed: ["원가계산에 실패했습니다.", "Cost calculation failed."], - B09_Estimation_Confirm_Success: [ - "원가계산 단계를 확정했습니다.", - "Cost estimate stage confirmed.", - ], - B09_Estimation_Confirm_Failed: [ - "원가계산 단계 확정에 실패했습니다.", - "Failed to confirm the cost estimate stage.", - ], - B09_Estimation_Tab_Pending: ["준비 중", "Coming soon"], - B09_Estimation_UP_List: ["일위대가 목록표", "Unit Price Index"], - B09_Estimation_UP_Detail: ["일위대가표", "Unit Price Sheet"], - B09_Estimation_UP_Pick: ["목록에서 항목을 고르세요.", "Pick an item from the index."], - B09_Estimation_UP_Drill: ["펼쳐 보기", "Open"], - B09_Estimation_Col_Name: ["명칭", "Name"], - B09_Estimation_Col_Spec: ["규격", "Spec"], - B09_Estimation_Col_Unit: ["단위", "Unit"], - B09_Estimation_Col_Qty: ["수량", "Qty"], - B09_Estimation_Col_Source: ["원천", "Source"], - B09_Estimation_Col_Material: ["재료비", "Material"], - B09_Estimation_Col_Labor: ["노무비", "Labor"], - B09_Estimation_Col_Expense: ["경비", "Expense"], - B09_Estimation_Col_Total: ["합계", "Total"], - B09_Estimation_UP_SumOk: ["합계 = 재료+노무+경비 일치", "Total = M+L+E ✓"], - B09_Estimation_UP_SumBad: ["⚠ 합계가 재료+노무+경비와 다릅니다", "⚠ Total ≠ M+L+E"], - B09_Estimation_UP_RoundGap: [ - "행별로 0.1원 미만을 버려 합계 끝자리가 다릅니다 (정상). 자르기 전 합계:", - "Rows are floored to 0.1 KRW, so the total's last digit differs (expected). Unrounded total:", - ], - B09_Estimation_Group_Quantity: ["수량", "Quantities"], - B09_Estimation_Field_Quantities: [ - "공종별 수량 (한 줄에 「공종코드=수량」)", - 'Quantities (one "code=qty" per line)', - ], - B09_Estimation_Src_Manual: ["수량 원천: 수기 입력(직접비)", "Source: manual direct costs"], - B09_Estimation_Src_Quantities: [ - "수량 원천: 수기 입력 공종 수량 × 일위대가", - "Source: manual quantities × unit prices", - ], - B09_Estimation_Missing_UP: [ - "수량은 있는데 단가가 없는 공종 — 총액에서 빠졌습니다:", - "Quantities without a unit price — excluded from the total:", - ], - B09_Estimation_UP_Load_Failed: ["일위대가를 못 불러왔습니다.", "Failed to load unit prices."], - - /* --- B10_Payment 결재 --- */ - B10_Payment_Title: ["결재", "Payment"], - B10_Payment_Subtitle: [ - "세금계산서 발행과 계좌 입금 절차를 확인하세요.", - "Review the tax invoice and bank transfer process.", - ], - B10_Payment_Invoice_Title: ["세금계산서 발행 요청", "Tax Invoice Request"], - B10_Payment_Invoice_Description: [ - "사업자 정보와 발행 금액은 견적 확정 후 연결됩니다.", - "Business details and the invoice amount will be linked after the estimate is finalized.", - ], - B10_Payment_Invoice_Request: ["발행 요청", "Request Invoice"], - B10_Payment_Invoice_Status: ["요청 전", "Not Requested"], - B10_Payment_Deposit_Title: ["계좌 입금 안내", "Bank Transfer Guide"], - B10_Payment_Deposit_Account: ["입금 계좌", "Deposit Account"], - B10_Payment_Deposit_Amount: ["입금 금액", "Deposit Amount"], - B10_Payment_Deposit_Pending: ["견적 확정 후 표시", "Shown after estimate confirmation"], - B10_Payment_Deposit_Note: [ - "입금 확인 후 설계문서와 DWG 다운로드가 허용됩니다.", - "Design documents and DWG downloads are enabled after the deposit is confirmed.", - ], - - /* --- B11_Status 상태 출력 --- */ - B11_Status_Title: ["처리 상태", "Status"], - B11_Status_Subtitle: [ - "결재·문서 생성 상태를 확인하고 결과물을 내려받으세요.", - "Check payment and document status, and download results.", - ], - // 자료 준비 화면 (대시보드 → 작업 화면 진입 시 3D·등고선 선적재) - B11_Loading_Title: ["자료 준비 중", "Preparing data"], - B11_Loading_Subtitle: ["잠시만 기다려 주세요.", "This will take a moment."], - B11_Loading_Message: [ - "작업 화면에서 바로 쓸 수 있도록 3D 지표면과 등고선을 준비합니다.", - "Loading the 3D surface and contours so the workspace opens instantly.", - ], - B11_Loading_Start: ["자료를 준비하는 중…", "Preparing data…"], - B11_Loading_NoProject: [ - "프로젝트가 선택되지 않았습니다. 대시보드에서 프로젝트를 먼저 고르세요.", - "No project selected. Choose a project on the dashboard first.", - ], - B11_Loading_Failed: [ - "자료를 준비하지 못했습니다. 분석 결과나 저장 경로에 문제가 있을 수 있습니다. 담당자에게 연락해 주세요.", - "Could not prepare the data. The analysis result or storage path may be broken. Please contact support.", - ], - B11_Loading_NoSurface: [ - "아직 확정된 지표면이 없습니다. 필수 파일을 올리면 지표면 분석이 자동으로 진행되고, 끝나면 이 화면이 자료를 준비합니다.", - "No confirmed surface yet. Upload the required files — the surface analysis runs automatically, and this screen prepares the data once it finishes.", - ], - B11_Loading_Btn_FileInput: ["파일 입력", "File input"], - B11_Loading_Btn_Continue: ["계속", "Continue"], - B11_Loading_Btn_Dashboard: ["대시보드", "Dashboard"], - B11_Status_Flow_Title: ["결재 진행 상태", "Payment Progress"], - B11_Status_Step_Request: ["발행 요청", "Invoice Requested"], - B11_Status_Step_Issue: ["세금계산서 발행", "Invoice Issued"], - B11_Status_Step_Deposit: ["입금 확인", "Deposit Confirmed"], - B11_Status_Step_Complete: ["완료", "Complete"], - B11_Status_NotStarted: ["시작 전", "Not Started"], - B11_Status_Download_Title: ["결과물 다운로드", "Result Downloads"], - B11_Status_Download_Description: [ - "입금 확인이 완료되면 설계문서와 DWG 다운로드가 활성화됩니다.", - "Design document and DWG downloads are enabled after deposit confirmation.", - ], B06_Std_Note_Rock: ["암", "Rock"], B06_Std_Note_Paved: ["포장", "Paved"], B05_Drainage_Btn_Propose: ["자동 제안", "Auto suggest"], @@ -1028,4 +573,14 @@ export const ui_locales_b2 = { ], Design_Route_Moved_Go: ["B07 로", "Go to B07"], Design_Route_Moved_Stay: ["머묾", "Stay"], + /* 배수유역도 낡은 채 B06 [>](요구 326-6) — 확인 → [업데이트] 뒤 이동 */ + Design_Drainage_Stale: [ + "배수유역도가 지금 노선 · 관 자리보다 낡았음 — 배수유역을 다시 셈(업데이트)한 뒤 B07 로 넘어감.", + "The drainage map is older than the current route and culverts — it will be recomputed (Update) before going to B07.", + ], + Design_Drainage_Stale_Go: ["업데이트 뒤 이동", "Update and go"], + Design_Drainage_Stale_Failed: [ + "배수유역 업데이트 실패 — 머묾.", + "Drainage update failed — staying.", + ], } as const satisfies Record; diff --git a/ui_template/ui_template_locale_b5.ts b/ui_template/ui_template_locale_b5.ts new file mode 100644 index 000000000..bd4c5f6ed --- /dev/null +++ b/ui_template/ui_template_locale_b5.ts @@ -0,0 +1,449 @@ +/* ============================================================================= + * ui_template_locale_b5.ts + * 다국어 사전 — B 그룹 끝 (B08_Quantity ~ B11_Status) + * + * b2 가 700줄을 넘어 B08~B11 칸을 그대로 뗌. 신규 문구는 해당 섹션 최하단에 추가. + * 이 파일은 사전 데이터만 담는다. 합성·언어 전환·t() 헬퍼는 ui_template_locale.ts 참조. + * ========================================================================== */ + +import type { LocaleEntry } from "./ui_template_locale_common"; + +export const ui_locales_b5 = { + /* --- B08_Quantity 수량 산출 --- */ + B08_Quantity_Title: ["수량산출", "Quantity"], + B08_Quantity_Tab_Earthwork: ["토공", "Earthwork"], + B08_Quantity_Grid_Loading: ["토적표를 만드는 중입니다…", "Building the earthwork table…"], + B08_Quantity_Grid_Empty: [ + "측점 단면적이 아직 없습니다. 횡단 설계를 먼저 마치세요.", + "No cross-section areas yet. Finish the cross-section design first.", + ], + B08_Quantity_Grid_Failed: [ + "토적표를 불러오지 못했습니다.", + "Failed to load the earthwork table.", + ], + B08_Quantity_Tab_Summary: ["토공집계", "Earthwork Summary"], + B08_Quantity_Tab_Haul: ["운반거리", "Haul Distance"], + B08_Quantity_Tab_StructureSummary: ["구조물 집계표", "Structure Summary"], + B08_Quantity_Tab_UnitQuantity: ["구조물 원단위", "Structure Unit Quantity"], + B08_Quantity_Tab_Material: ["자재총괄", "Material Summary"], + B08_Quantity_Tab_Preparation: ["준비공·사방공", "Preparation & Erosion Control"], + B08_Quantity_Side_Method_Label: ["시공법", "Method"], + B08_Quantity_Method_Unset: ["안 정함", "Not set"], + B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"], + B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"], + B08_Quantity_Placing_NotApplied: [ + "콘크리트 물량이 「콘크리트 타설」 공종으로 견적에 넘어갑니다.", + "The placing method changes this work item's unit price — concrete volume is handed over as a placing work item.", + ], + B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"], + B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"], + B08_Quantity_Side_TopsoilTarget_Label: ["표토제거 대상", "Topsoil removal target"], + B08_Quantity_TopsoilTarget_RoadAndCut: [ + "노면 + 절토 사면 (기본 · 법 문언)", + "Road surface + cut slope (default · law)", + ], + B08_Quantity_TopsoilTarget_RoadOnly: ["노면만", "Road surface only"], + B08_Quantity_Side_TopsoilTarget_Hint: [ + "기본 근거: 산림자원법 시행규칙 별표2 Ⅰ.2.차.(6) 「노면·절토대상지에 있는 … 표토는 전량 제거한 후 … 운반하고 쌓아두어야 한다」 — 성토 사면은 「제거·정리」라 대상 밖. 노면만: 실무 영월 한 건(1m당 3.34㎡)이 노면만 셈 — 한 건이라 기본으로 삼지 않고 고를 수 있게 둠(면적 약 3배 차이)", + "Default basis: the forest-road rule names road surface and cut areas; fill slopes are only cleared. Road-only follows one field case (Yeongwol, 3.34 ㎡ per m) — selectable, not the default (about 3× area difference)", + ], + B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"], + // ── 확정 3차(2026-09-09) 로 정한 값들 — **화면에 칸으로 세우고 근거를 보인다.** + // 사용자 지시: 「값을 코드에 박고 끝내지 말 것 · 대신 페이지에 남길 것」. + B08_Quantity_Side_Structure: ["구조물·사토", "Structures & Spoil"], + B08_Quantity_Side_BenchCut_Label: ["층따기 길이(m)", "Bench cut length (m)"], + B08_Quantity_Side_BenchCut_Hint: [ + "면적 × 이 값 = ㎥ — 비우면 층따기 줄이 막힙니다(확정 2차 ①)", + "Area × this = ㎥ — blank keeps the bench-cut row blocked", + ], + B08_Quantity_Side_BenchCut_Suggest: [ + "층따기 표준치수 — 최소 높이 토사 500 mm 이상 · 암반 400 mm 이상 · 폭 1 m 이상(기계 작업을 할 때는 3 m 이상). 근거: KDS 44 30 00 도로토공 2.3.4 (4) ② 「기초지반의 종류에 따른 층따기의 표준치수」", + "Bench cut standard dimensions — minimum height: soil ≥ 500 mm · rock ≥ 400 mm · width ≥ 1 m (≥ 3 m for machine work). Basis: KDS 44 30 00 Road Earthwork 2.3.4 (4) ②", + ], + B08_Quantity_Side_Rubble_Label: ["기초잡석 두께(m)", "Rubble base thickness (m)"], + B08_Quantity_Side_Rubble_Hint: [ + "폭은 버림 폭과 같음(KCS 34 50 05) · 두께 0.2m 는 사용자 확정 3차 ② — 품셈 12-25 는 두께를 정하지 않음", + "Width equals the blinding width (KCS 34 50 05); 0.2 m thickness is a user decision — the standard gives none", + ], + B08_Quantity_Side_SpoilDistance_Label: ["사토장까지 거리(m)", "Distance to spoil site (m)"], + B08_Quantity_Side_SpoilDistance_Hint: [ + "사토장을 노선에 놓으면 그 측점까지 거리가 계산되어 이 값은 안 씁니다 — 안 놓았을 때의 대체값입니다", + "Used only until a spoil site is placed on the route — then the distance is computed from that station", + ], + B08_Quantity_Side_Water_Label: ["구조물터파기 용수", "Structure trench water"], + B08_Quantity_Water_Dry: ["육상(용수 없음)", "Dry"], + B08_Quantity_Water_Wet: ["용수", "Wet"], + B08_Quantity_Water_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"], + B08_Quantity_Side_Water_Hint: [ + "⚠ 「육상」은 통상값이고 사용자 확정이 아닙니다(확정 3차 ④) — 품셈 9-13 의 18구분이 이 값으로 갈립니다", + "⚠ “Dry” is a customary default, not a user decision — it selects one of the 18 sub-items", + ], + B08_Quantity_Side_SeedSpray_Label: ["초류종자살포 비탈면 토질", "Seed spray slope soil"], + B08_Quantity_SeedSpray_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], + B08_Quantity_Side_SeedSpray_Hint: [ + "품셈 5-24 씨앗뿜어붙이기가 5-24-1 기계/일반 · 5-24-2 기계/마사토 둘로 갈림 — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다", + "Pumsem 5-24 splits into general / decomposed-granite soil — no suggestion; unset leaves the bill row with an input reason", + ], + B08_Quantity_Side_RootRemoval_Label: ["제근 굴착기 크기", "Root removal excavator size"], + B08_Quantity_RootRemoval_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], + B08_Quantity_RootRemoval_Suggest: ["제안(비우면 안 정함):", "Suggested (blank = not set):"], + B08_Quantity_RootRemoval_FillSuggested: ["제안값 넣기", "Fill suggested"], + B08_Quantity_Side_StandVolume_Label: ["임목축적 등급", "Stand volume class"], + B08_Quantity_StandVolume_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"], + B08_Quantity_StandVolume_Low: ["소림(30~60㎥/㏊)", "Low (30–60 ㎥/ha)"], + B08_Quantity_StandVolume_Mid: ["중림(60~90㎥/㏊)", "Mid (60–90 ㎥/ha)"], + B08_Quantity_StandVolume_High: ["밀림(90㎥/㏊ 이상)", "High (90+ ㎥/ha)"], + B08_Quantity_Side_StandVolume_Hint: [ + "제근(품셈 9-21)이 이 등급으로 갈립니다 — ⚠ 본수가 아니라 축적이고, 산림조사부·영림계획에서 옮겨 적는 값입니다", + "Root removal (9-21) splits by this class — stand volume, not tree count; copied from the forest survey", + ], + B08_Quantity_Side_Chipping: ["임목파쇄", "Wood Chipping"], + B08_Quantity_Chipping_Label: ["임목파쇄를 셀 것인가", "Count wood chipping"], + B08_Quantity_Chipping_Off: ["안 셈(기본)", "No (default)"], + B08_Quantity_Chipping_On: ["셈", "Yes"], + B08_Quantity_Chipping_Volume: ["파쇄 부피(㎥)", "Chipping volume (㎥)"], + B08_Quantity_Side_Chipping_Hint: [ + "기본은 안 셉니다(확정 5차 5번) — 현장에 따라 필요하면 켜세요. ⚠ 켜도 부피를 넣어야 값이 섭니다(실무는 부피로 셈)", + "Off by default — turn on per site. Volume must be entered for the row to carry a quantity", + ], + B08_Quantity_Side_TreeWaste: ["임목폐기물", "Tree Waste"], + B08_Quantity_TreeWaste_Stems: ["표본지 1,000㎡당 본수", "Stems per 1,000 ㎡ (sample plot)"], + B08_Quantity_TreeWaste_Dbh: ["평균 흉고직경(㎝)", "Mean DBH (cm)"], + B08_Quantity_TreeWaste_Height: ["평균 수고(m)", "Mean tree height (m)"], + B08_Quantity_TreeWaste_UnitWeight: ["단위체적중량(㎏/㎥)", "Unit weight (kg/㎥)"], + B08_Quantity_Side_TreeWaste_Hint: [ + "현장 조사값 넷을 넣으면 준비공 「임목폐기물 처리」가 톤으로 섭니다. 산식: 지상부 WA = 0.5 × 3.14 × (흉고직경/2)² × 수고 × (1+0.3) × 단위체적중량 × 본수 · 본수 = 1,000㎡당 본수 × 벌목·지장목제거 면적 — 환경친화적인 도로건설 지침 부록1 · 한국건설기술연구원(2012) 3.2. 단위체적중량 참고: 소나무·침엽수 1,210~1,250 · 단풍·산벚 1,250~1,300 · 느티 1,300~1,340 · 상수리·졸참 1,340 이상(조경설계요람). 기본값 없음 — 수종·임분마다 다름", + "Enter the four field-survey values to raise the tree-waste row in tonnes. WA = 0.5 × 3.14 × (DBH/2)² × height × 1.3 × unit weight × stems (road guideline appendix 1, KICT 2012). No defaults — values differ by species and stand", + ], + B08_Quantity_TreeWaste_RootMethod: ["뿌리부 산정법", "Root estimate"], + B08_Quantity_TreeWaste_RootRatio: [ + "분배비 WA × 15/85 (기본)", + "Share ratio WA × 15/85 (default)", + ], + B08_Quantity_TreeWaste_RootBall: ["뿌리분 체적 × 1,300㎏/㎥", "Root-ball volume × 1,300 kg/㎥"], + B08_Quantity_TreeWaste_RootBallDiameter: [ + "뿌리분 직경 D(m, 흉고직경 아님) — 체적법만", + "Root-ball diameter D (m, not DBH) — volume method", + ], + B08_Quantity_TreeWaste_Root_Hint: [ + "환경친화적인 도로건설 지침 부록1 이 두 방법을 둡니다. 분배비(기본): 뿌리 15 : 지상 85(일반 수목류, 한국건설기술연구원 2012). 뿌리분 체적: V = πD²(D/2 + RH2/3) = 0.3927D³ · 뿌리분 중량 = V × 1,300㎏/㎥ × 본수. ⚠ 지침은 D 를 「뿌리분의 직경(DBH, m)」으로 적어 흉고직경인지 뿌리분 직경인지 갈리지 않음 — 흉고직경으로 두지 않고 따로 받습니다. 참고: 실무 실정보고(한 현장)는 뿌리 14 : 86 · 0.65톤/㎥ 를 씀", + "Road guideline appendix 1 gives two methods: share ratio 15:85 (default), or root-ball volume 0.3927·D³ × 1,300 kg/㎥ × stems. The guideline labels D ambiguously, so it is entered separately", + ], + B08_Quantity_TreeWaste_Price: ["처리단가(원/톤) — 수동", "Disposal price (KRW/t) — manual"], + B08_Quantity_TreeWaste_Price_Hint: [ + "⚠ 공표 단가 없음(한국건설자원협회 중간처리단가 미확보) — 넣으면 금액이 서되 「미확정」으로 셉니다(빨간 테두리). 비목은 경비 — 원가계산서 폐기물처리비 줄로 가고 일반관리비·이윤 밑수에 듭니다(예정가격작성기준 제19조③18호)", + "No published price — a manual price raises the amount as unconfirmed (red outline). Cost category is expense, inside the overhead and profit bases", + ], + B08_Quantity_TreeWaste_Separate: ["분리발주", "Separate disposal contract"], + B08_Quantity_TreeWaste_Separate_Off: ["분리발주 아님(기본)", "No (default)"], + B08_Quantity_TreeWaste_Separate_On: ["분리발주", "Yes"], + B08_Quantity_TreeWaste_Separate_Hint: [ + "⚠ 임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 · 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물) — 기본 꺼짐으로 원가계산서 경비에 듭니다. 이 칸은 다른 건설폐기물(위탁처리 100톤 이상 분리발주 대상)이 생길 때 쓰는 칸 — 켜면 총원가 밖에 서고 총공사비에만 더합니다. 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)", + "Tree waste is not subject to separate ordering (Ministry of Environment reply; classified as business general waste) — off by default, inside the expense category. Use this switch for other construction waste (100 t or more). When on, it sits outside total cost. 5 t or more requires a discharge report", + ], + B08_Quantity_Side_FaceDressing: ["면고르기", "Face dressing"], + B08_Quantity_FaceDressing_Cut: ["절토면 토질", "Cut face soil"], + B08_Quantity_FaceDressing_Fill: ["성토면 시공·토질", "Fill face method·soil"], + B08_Quantity_FaceDressing_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], + B08_Quantity_FaceDressing_FillArea: ["성토면 면적(㎡)", "Fill face area (㎡)"], + B08_Quantity_FaceDressing_CutArea: ["절토면 면적(㎡)", "Cut face area (㎡)"], + B08_Quantity_FaceDressing_Area_Placeholder: ["파종 면적 그대로", "Same as seeding area"], + B08_Quantity_FaceDressing_Suggest: ["제안(비우면 안 정함):", "Suggested (blank = not set):"], + B08_Quantity_FaceDressing_FillSuggested: ["제안값 넣기", "Fill suggested"], + B08_Quantity_Side_FaceDressing_Hint: [ + "품셈 9-19-1 원문 표 두 벌(절토면 토질 6 · 성토면 시공·토질) — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다. 면적은 비우면 초류종자살포 면적(× 반영률) 그대로 · 넣으면 그 값으로 덮어씀", + "Standard estimate 9-19-1 tables (cut face soil · fill face method/soil) — no suggested value; unset leaves the bill row with an input reason. Area left blank uses the seeding area (× ratio); a value overrides it", + ], + B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"], + B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"], + B08_Quantity_Side_SubgradeCompaction_Hint: [ + "기본은 안 셉니다 — 근거: 산림품셈 9-16-2 [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」. 켜면 토공집계에 「노체다짐」 줄이 성토량(㎥)으로 섭니다. [주]③ 굴착기+진동롤러 조합 중 굴착기는 성토(포설) 줄이 이미 셉니다", + "Off by default — forest standard 9-16-2 note ⑤ applies it only to large fills needing layer compaction. When on, a compaction row equal to the fill volume is added; the excavator of the note ③ pairing is already in the fill (spreading) row", + ], + B08_Quantity_Side_Frame: ["규준틀 개소당 재료", "Batter Board Materials (per unit)"], + B08_Quantity_Side_Frame_Hint: [ + "⚠ 채워진 값은 실무 관측값(제안값)이고 법정 기준이 아닙니다 — 품셈 11-2·11-3 [주]④ 는 「재료량은 설계수량에 따른다」로만 둡니다. 손율은 원문값(비탈 50% · 수평 80%)", + "Pre-filled values are field observations, not a legal standard — the standard only says “per design quantity”", + ], + B08_Quantity_Frame_Square: ["각재 50×50 (㎥/개소)", "Square timber (㎥/ea)"], + B08_Quantity_Frame_Board: ["판재 T12 (㎥/개소)", "Board T12 (㎥/ea)"], + B08_Quantity_Frame_Nail: ["못 (㎏/개소)", "Nails (㎏/ea)"], + B08_Quantity_Side_Ancillary: ["부대시설 개소", "Ancillary Facilities"], + B08_Quantity_Side_Ancillary_Hint: [ + "개소를 넣어야 줄이 섭니다 — 원문이 배치 간격을 정하지 않아 연장÷500 같은 산식을 쓰지 않습니다(확정 13)", + "Counts are design input — the source sets no spacing rule, so no formula is used", + ], + B08_Quantity_Ancillary_national_point_sign: ["국가지점번호판(개소)", "National point signs"], + B08_Quantity_Ancillary_guide_sign: ["임도 안내판(개소)", "Guide signs"], + B08_Quantity_Ancillary_gate: ["차단기(개소)", "Gates"], + B08_Quantity_Ancillary_site_container: ["가설창고(개소)", "Site containers"], + B08_Quantity_Ancillary_flood_supplies: ["수방대책 자재(식)", "Flood supplies (set)"], + B08_Quantity_Side_TopsoilHaul_Label: ["표토 운반거리(m)", "Topsoil haul distance (m)"], + B08_Quantity_Side_TopsoilHaul_Hint: [ + "⚠ 별표2 는 표토를 「전량 제거한 후 최고 홍수위보다 높은 장소로 운반하고 쌓아두어야」로 정합니다 — 거리를 넣으면 운반 줄이 섭니다. 표토제거는 노면 + 절토 사면 ㎡ 로 서고, 두께는 운반 부피(㎡ × 두께)에만 씁니다. 제거 품(9-15-2)에 20m 압토가 들어 있어 운반은 20m 를 넘는 몫만 셉니다", + "The law requires hauling and stockpiling removed topsoil — enter the distance to raise that row. Removal counts road surface + cut slope in ㎡; thickness only sizes the haul volume. The removal rate already includes a 20 m push, so only the excess is hauled", + ], + B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"], + B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"], + B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"], + B08_Quantity_Placing_Current: ["적용 중", "In use"], + B08_Quantity_Placing_Default_Tag: ["기본값 — 확인 필요", "default — needs review"], + B08_Quantity_Placing_Hint: ["참고 단가(B09 산출):", "Reference unit price (from B09):"], + B08_Quantity_Placing_Ready: ["레디믹스트", "Ready-mixed"], + B08_Quantity_Placing_Machine: ["기계비빔", "Machine-mixed"], + B08_Quantity_Placing_Hand: ["인력비빔", "Hand-mixed"], + B08_Quantity_Placing_Default_Notice: [ + "⚠ 기본값 「레디믹스트」로 계산 중 — 아직 안 정한 값입니다. 타설 방식에 따라 단가가 갈립니다.", + "⚠ Using the default (ready-mixed) — not yet decided. Unit prices differ by method.", + ], + B08_Quantity_Material_Failed: [ + "자재총괄을 불러오지 못했습니다.", + "Failed to load the material summary.", + ], + B08_Quantity_Haul_Missing: [ + "운반계획이 아직 없습니다. 노선설계에서 [확정]을 누르면 만들어집니다.", + "No haul plan yet. Press [Confirm] on the profile design to build it.", + ], + B08_Quantity_Haul_Excluded: ["내역 제외", "Not billed"], + B08_Quantity_Side_Ratios: ["반영률(%)", "Application ratios (%)"], + /* 반영률 항목 이름 — 서버 키를 사람이 읽는 말로. 거창 실무 시트 문구를 따른다. */ + B08_Quantity_Ratio_FillCompaction: ["성토면다짐", "Fill slope compaction"], + B08_Quantity_Ratio_SeedFill: ["초류종자살포(성토면)", "Seed spray (fill)"], + B08_Quantity_Ratio_SeedCut: ["초류종자살포(절토면)", "Seed spray (cut)"], + B08_Quantity_Ratio_TreeRemoval: ["지장목제거", "Obstacle removal"], + B08_Quantity_Side_RockSet: ["암 갈래 세트", "Rock class set"], + B08_Quantity_Side_RockRatios: [ + "암 갈래 구성비(%) — 암 총량 기준", + "Rock class split (%) of rock volume", + ], + B08_Quantity_Btn_Save: ["저장", "Save"], + B08_Quantity_Save_Success: ["산출 조건을 저장했습니다.", "Calculation settings saved."], + B08_Quantity_Save_Failed: ["산출 조건을 저장하지 못했습니다.", "Failed to save the settings."], + B08_Quantity_Unsaved: ["저장하지 않은 변경이 있습니다.", "You have unsaved changes."], + B08_Quantity_Side_Method: ["산출법", "Method"], + B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"], + B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"], + B08_Quantity_Factor_Reach: [ + "이 계수는 유토곡선·운반표·기초단가에도 같이 닿습니다.", + "These factors also feed the mass-haul curve, haul table and basis units.", + ], + B08_Quantity_Factor_Default: ["기본값", "Default"], + B08_Quantity_Side_HaulLimits: ["운반장비 거리 경계", "Haul equipment distance limits"], + B08_Quantity_Side_DozerLimit_Label: ["도쟈 한계거리(m)", "Dozer haul limit (m)"], + B08_Quantity_Side_FreeHaul: ["종무대(바꾸지 않음)", "Free haul (fixed)"], + B08_Quantity_Side_HaulLimits_Reach: [ + "유토곡선 운반 배분(무대·도쟈·덤프)에 닿습니다 — 바꾼 뒤 종횡단에서 다시 저장해야 운반표가 따라옵니다.", + "Feeds the mass-haul split (free/dozer/dump) — re-save the profile/cross sections for the haul table to follow.", + ], + B08_Quantity_Factor_Range: ["품셈 범위", "Standard range"], + B08_Quantity_Factor_OutOfRange: [ + "품셈 범위 밖입니다 — 사유를 적어 주세요(토질시험 값일 수 있습니다).", + "Outside the standard range — please note why (it may be a soil-test value).", + ], + B08_Quantity_Factor_Reason: ["사유", "Reason"], + B08_Quantity_Factor_Pumsem: ["품셈 체적변화율(C)", "Standard volume-change (C)"], + + /* --- B09_Estimation 원가계산 --- */ + B09_Estimation_Title: ["원가계산", "Cost Estimate"], + B09_Estimation_Tab_CostSheet: ["공사원가계산서", "Cost Statement"], + B09_Estimation_Tab_Boq: ["설계내역서", "Bill of Quantities"], + B09_Estimation_Boq_Total: ["내역서 합계", "Bill total"], + B09_Estimation_PB_Empty: [ + "설계내역서를 먼저 불러오면 단가산출서가 섭니다.", + "Load the bill first and the price-basis sheets appear.", + ], + B09_Estimation_PB_Pick: ["왼쪽에서 산출서를 고르세요.", "Pick a sheet on the left."], + B09_Estimation_PB_Ref: ["참조", "Refers to"], + B09_Estimation_Mat_Contractor: ["사급 자재 (도급 재료비)", "Contractor-supplied (in the bill)"], + B09_Estimation_Mat_Owner: [ + "관급 자재 — 총원가 밖 별도 표기", + "Owner-supplied — listed outside the total cost", + ], + B09_Estimation_Mat_Unknown: [ + "관급·사급이 안 갈린 것 — 어느 합계에도 안 넣습니다", + "Supply type undecided — excluded from both totals", + ], + B09_Estimation_Mat_None: [ + "이 프로젝트에는 자재 줄이 없습니다 — 구조물이 없거나 자재가 안 나온 상태입니다.", + "This project has no material rows yet.", + ], + B09_Estimation_Mat_Empty: [ + "설계내역서를 먼저 불러오면 자재대가 섭니다.", + "Load the bill first and the material sheet appears.", + ], + B09_Estimation_Boq_Precision: [ + "수량 표시는 소수 2자리, 계산은 전정밀 — 표시값끼리 곱하면 끝자리가 다릅니다.", + "Quantities are shown to 2 decimals but computed at full precision — multiplying the shown values gives a slightly different last digit.", + ], + B09_Estimation_Boq_Excluded: [ + "검산용 줄 — 수량만 보이고 금액을 매기지 않습니다", + "Check rows — quantity only, never priced", + ], + B09_Estimation_Boq_NotOurs: [ + "여기서 세지 않는 줄 (다른 표에 있거나 이 노선에 없음)", + "Counted elsewhere or not present on this route", + ], + B09_Estimation_Boq_NeedsInput: [ + "입력하면 풀리는 것 (설계 화면에서 값을 고르면 금액이 섭니다)", + "Waiting on input — pick the value on the design screen and the amount appears", + ], + B09_Estimation_Boq_NeedsWork: [ + "우리가 만들어야 하는 것 (원단위·전개식 없음)", + "Needs build — unit data or formula is missing", + ], + B09_Estimation_Boq_Missing: [ + "금액을 못 세운 줄 — 0 으로 안 채웁니다", + "Rows without an amount — shown as-is, not zero-filled", + ], + B09_Estimation_Boq_Materials: ["자재 (별도 벌)", "Materials (separate set)"], + B09_Estimation_Boq_NoMaterialPrice: [ + "사급 자재 단가가 아직 없어 자재비가 빠져 있습니다 — 지금 합계는 모자란 값입니다.", + "Contractor-supplied material prices are missing, so material cost is absent — this total is short.", + ], + B09_Estimation_Boq_Load: ["B08 수량 불러오기", "Load B08 quantities"], + B09_Estimation_Boq_Failed: [ + "B08 인계 자료를 받지 못했습니다.", + "Could not load the B08 handoff.", + ], + B09_Estimation_Tab_UnitPrice: ["일위대가", "Unit Price"], + B09_Estimation_Tab_PriceBasis: ["단가산출근거", "Price Basis"], + B09_Estimation_Tab_Machine: ["중기", "Equipment"], + B09_Estimation_Tab_Duration: ["공사기간", "Duration"], + B09_Estimation_Tab_Supply: ["관급·사급", "Supplied Materials"], + B09_Estimation_Tab_BaseData: ["기초자료", "Base Data"], + B09_Estimation_Tab_DesignDoc: ["설계서 구성", "Design Document Set"], + B09_Estimation_Tab_BasisSheet: ["산출기초", "Calculation Basis"], + B09_Estimation_Group_Condition: ["공사 조건", "Project Conditions"], + B09_Estimation_Group_RateVersion: ["요율 판", "Rate Edition"], + B09_Estimation_Group_Supplied: ["관급자재", "Owner-Supplied"], + B09_Estimation_Group_Profit: ["이윤 조정", "Profit Adjustment"], + B09_Estimation_Field_DirectMaterial: ["직접재료비", "Direct Material"], + B09_Estimation_Field_DirectLabor: ["직접노무비", "Direct Labor"], + B09_Estimation_Field_DirectExpense: ["직접경비", "Direct Expense"], + B09_Estimation_Field_Duration: ["공사기간(일)", "Duration (days)"], + B09_Estimation_Field_WorkType: ["공종", "Work Type"], + B09_Estimation_Field_OwnerMaterial: ["순자재대", "Net Material"], + B09_Estimation_Field_ProcurementFee: ["조달수수료", "Procurement Fee"], + B09_Estimation_Field_ProfitAdjust: ["조정액", "Adjustment"], + B09_Estimation_Field_TargetContract: ["목표 도급공사비", "Target Contract"], + B09_Estimation_Btn_Recalc: ["재계산", "Recalculate"], + B09_Estimation_Btn_Confirm: ["확정", "Confirm"], + B09_Estimation_Col_Item: ["비목", "Item"], + B09_Estimation_Col_Amount: ["금액", "Amount"], + B09_Estimation_Col_Rate: ["요율", "Rate"], + B09_Estimation_Col_Basis: ["산출근거", "Basis"], + B09_Estimation_Col_Note: ["비고", "Note"], + B09_Estimation_Adopted: ["채택", "Adopted"], + B09_Estimation_NotAdopted: ["미채택", "Not adopted"], + B09_Estimation_Suggest_Adjust: [ + "목표 도급공사비를 맞추려면 이윤을 이만큼 깎아야 합니다 — 적용하려면 조정액에 직접 넣으세요.", + "To hit the target contract amount, profit must be reduced by this much — enter it in Adjustment to apply.", + ], + B09_Estimation_Calc_Failed: ["원가계산에 실패했습니다.", "Cost calculation failed."], + B09_Estimation_Confirm_Success: [ + "원가계산 단계를 확정했습니다.", + "Cost estimate stage confirmed.", + ], + B09_Estimation_Confirm_Failed: [ + "원가계산 단계 확정에 실패했습니다.", + "Failed to confirm the cost estimate stage.", + ], + B09_Estimation_Tab_Pending: ["준비 중", "Coming soon"], + B09_Estimation_UP_List: ["일위대가 목록표", "Unit Price Index"], + B09_Estimation_UP_Detail: ["일위대가표", "Unit Price Sheet"], + B09_Estimation_UP_Pick: ["목록에서 항목을 고르세요.", "Pick an item from the index."], + B09_Estimation_UP_Drill: ["펼쳐 보기", "Open"], + B09_Estimation_Col_Name: ["명칭", "Name"], + B09_Estimation_Col_Spec: ["규격", "Spec"], + B09_Estimation_Col_Unit: ["단위", "Unit"], + B09_Estimation_Col_Qty: ["수량", "Qty"], + B09_Estimation_Col_Source: ["원천", "Source"], + B09_Estimation_Col_Material: ["재료비", "Material"], + B09_Estimation_Col_Labor: ["노무비", "Labor"], + B09_Estimation_Col_Expense: ["경비", "Expense"], + B09_Estimation_Col_Total: ["합계", "Total"], + B09_Estimation_UP_SumOk: ["합계 = 재료+노무+경비 일치", "Total = M+L+E ✓"], + B09_Estimation_UP_SumBad: ["⚠ 합계가 재료+노무+경비와 다릅니다", "⚠ Total ≠ M+L+E"], + B09_Estimation_UP_RoundGap: [ + "행별로 0.1원 미만을 버려 합계 끝자리가 다릅니다 (정상). 자르기 전 합계:", + "Rows are floored to 0.1 KRW, so the total's last digit differs (expected). Unrounded total:", + ], + B09_Estimation_Group_Quantity: ["수량", "Quantities"], + B09_Estimation_Field_Quantities: [ + "공종별 수량 (한 줄에 「공종코드=수량」)", + 'Quantities (one "code=qty" per line)', + ], + B09_Estimation_Src_Manual: ["수량 원천: 수기 입력(직접비)", "Source: manual direct costs"], + B09_Estimation_Src_Quantities: [ + "수량 원천: 수기 입력 공종 수량 × 일위대가", + "Source: manual quantities × unit prices", + ], + B09_Estimation_Missing_UP: [ + "수량은 있는데 단가가 없는 공종 — 총액에서 빠졌습니다:", + "Quantities without a unit price — excluded from the total:", + ], + B09_Estimation_UP_Load_Failed: ["일위대가를 못 불러왔습니다.", "Failed to load unit prices."], + + /* --- B10_Payment 결재 --- */ + B10_Payment_Title: ["결재", "Payment"], + B10_Payment_Subtitle: [ + "세금계산서 발행과 계좌 입금 절차를 확인하세요.", + "Review the tax invoice and bank transfer process.", + ], + B10_Payment_Invoice_Title: ["세금계산서 발행 요청", "Tax Invoice Request"], + B10_Payment_Invoice_Description: [ + "사업자 정보와 발행 금액은 견적 확정 후 연결됩니다.", + "Business details and the invoice amount will be linked after the estimate is finalized.", + ], + B10_Payment_Invoice_Request: ["발행 요청", "Request Invoice"], + B10_Payment_Invoice_Status: ["요청 전", "Not Requested"], + B10_Payment_Deposit_Title: ["계좌 입금 안내", "Bank Transfer Guide"], + B10_Payment_Deposit_Account: ["입금 계좌", "Deposit Account"], + B10_Payment_Deposit_Amount: ["입금 금액", "Deposit Amount"], + B10_Payment_Deposit_Pending: ["견적 확정 후 표시", "Shown after estimate confirmation"], + B10_Payment_Deposit_Note: [ + "입금 확인 후 설계문서와 DWG 다운로드가 허용됩니다.", + "Design documents and DWG downloads are enabled after the deposit is confirmed.", + ], + + /* --- B11_Status 상태 출력 --- */ + B11_Status_Title: ["처리 상태", "Status"], + B11_Status_Subtitle: [ + "결재·문서 생성 상태를 확인하고 결과물을 내려받으세요.", + "Check payment and document status, and download results.", + ], + // 자료 준비 화면 (대시보드 → 작업 화면 진입 시 3D·등고선 선적재) + B11_Loading_Title: ["자료 준비 중", "Preparing data"], + B11_Loading_Subtitle: ["잠시만 기다려 주세요.", "This will take a moment."], + B11_Loading_Message: [ + "작업 화면에서 바로 쓸 수 있도록 3D 지표면과 등고선을 준비합니다.", + "Loading the 3D surface and contours so the workspace opens instantly.", + ], + B11_Loading_Start: ["자료를 준비하는 중…", "Preparing data…"], + B11_Loading_NoProject: [ + "프로젝트가 선택되지 않았습니다. 대시보드에서 프로젝트를 먼저 고르세요.", + "No project selected. Choose a project on the dashboard first.", + ], + B11_Loading_Failed: [ + "자료를 준비하지 못했습니다. 분석 결과나 저장 경로에 문제가 있을 수 있습니다. 담당자에게 연락해 주세요.", + "Could not prepare the data. The analysis result or storage path may be broken. Please contact support.", + ], + B11_Loading_NoSurface: [ + "아직 확정된 지표면이 없습니다. 필수 파일을 올리면 지표면 분석이 자동으로 진행되고, 끝나면 이 화면이 자료를 준비합니다.", + "No confirmed surface yet. Upload the required files — the surface analysis runs automatically, and this screen prepares the data once it finishes.", + ], + B11_Loading_Btn_FileInput: ["파일 입력", "File input"], + B11_Loading_Btn_Continue: ["계속", "Continue"], + B11_Loading_Btn_Dashboard: ["대시보드", "Dashboard"], + B11_Status_Flow_Title: ["결재 진행 상태", "Payment Progress"], + B11_Status_Step_Request: ["발행 요청", "Invoice Requested"], + B11_Status_Step_Issue: ["세금계산서 발행", "Invoice Issued"], + B11_Status_Step_Deposit: ["입금 확인", "Deposit Confirmed"], + B11_Status_Step_Complete: ["완료", "Complete"], + B11_Status_NotStarted: ["시작 전", "Not Started"], + B11_Status_Download_Title: ["결과물 다운로드", "Result Downloads"], + B11_Status_Download_Description: [ + "입금 확인이 완료되면 설계문서와 DWG 다운로드가 활성화됩니다.", + "Design document and DWG downloads are enabled after deposit confirmation.", + ], +} as const satisfies Record;