Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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자 선택 동기화의
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 나눔)
|
||||
"수량 원천: 수기 입력(직접비)",
|
||||
"수량 원천: 수기 입력 공종",
|
||||
],
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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()")
|
||||
)
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user