Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-10-22 20:13:49 +09:00
29 changed files with 766 additions and 963 deletions
-2
View File
@@ -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",
+16 -3
View File
@@ -410,9 +410,22 @@ async function alreadyConfirmed(projectId: string): Promise<boolean> {
/** 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<boolean> {
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;
}
/**
@@ -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 };
@@ -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,
+8 -33
View File
@@ -18,9 +18,8 @@ export interface DrainageChrome {
root: HTMLElement;
panelHandle: ReturnType<typeof createWorkflowPanelHandle>;
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,
@@ -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;
+47 -146
View File
@@ -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<boolean>;
@@ -63,14 +55,12 @@ export interface DrainageLiveDeps {
export interface DrainageLive {
/** 계산 묶음 drainage 단계 — 노선 초안 · 관이 바뀐 차례에 부름(출력 없음). */
stage: StageFn<"drainage">;
/** 낡음을 지금 노선으로 다시 셈(자동 · [업데이트]와 같은 길). */
refresh(): void;
/** 서버 답이 「낡음」 — 흐림 + [업데이트](자동이면 3 s 뒤 셈). 이미 낡음이면 그대로. */
/** [업데이트] — 지금 노선으로 처음부터 셈 · 답을 화면에 올렸으면 true. */
refresh(): Promise<boolean>;
/** 서버 답이 「낡음」 — 낡음 표시. 이미 낡음이면 그대로. */
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<typeof liveRequestBody> | (Proposal & { generation: number }),
body: ReturnType<typeof liveRequestBody>,
signal: AbortSignal,
): Promise<DetailBasinResponse & { status?: string; generation?: number }> {
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<HTMLElement>(".b05-drainage__viewport") as HTMLElement;
const header = deps.root.querySelector<HTMLElement>(".b05-drainage__header") as HTMLElement;
const propose = header.querySelector<HTMLButtonElement>(".b05-drainage__propose");
/** 관 초안 요청 — 답이 화면에 오르면 세션 초안 + 사진첩 한 걸음(400 ms 넘은 답도 제 걸음). */
const draft = (request: (projectId: string) => Promise<DetailBasinResponse>): 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<boolean> {
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;
},
+34 -30
View File
@@ -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<Array<[number, number]>>;
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<RoutePoint>, 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) {
@@ -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<boolean>;
/** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */
selectBasinByChainage: (chainageM: number | null) => void;
/** 밖(리스트·그래프·3D)에서 관을 고른다 — 마커 선택·유역 강조를 맞춘다.
@@ -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<void>;
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)),
};
}
@@ -70,8 +70,6 @@ export interface DrainageScene {
stationIntervalM: number;
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */
intervalSpans: ReadonlyArray<RouteSpanBand>;
/** 관 자리 후보 누가거리(347-3 · 제안만) — 계획선 위 점선 원. */
candidates: ReadonlyArray<number>;
}
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자 선택 동기화의
+4 -4
View File
@@ -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);
}
}
+13 -32
View File
@@ -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;
+2 -11
View File
@@ -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;
@@ -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)),
@@ -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 };
+2 -3
View File
@@ -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();
@@ -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 나눔)
"수량 원천: 수기 입력(직접비)",
"수량 원천: 수기 입력 공종",
],
+3 -1
View File
@@ -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
@@ -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]
+50 -84
View File
@@ -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,
@@ -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
@@ -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",
@@ -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",
@@ -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} # 응답 → 보관소 → 요청
@@ -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()")
)
+4 -1
View File
@@ -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;
+11 -456
View File
@@ -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<string, LocaleEntry>;
+449
View File
@@ -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<string, LocaleEntry>;