Merge remote-tracking branch 'origin/sub_laptop_5' into sub_laptop_1
This commit is contained in:
@@ -289,6 +289,21 @@ def edit(key: str, row: Any, version: str) -> dict[str, Any]:
|
||||
return {"키": key, "판": _save(doc, version), "combo": fixed}
|
||||
|
||||
|
||||
def drop_practice(version: str) -> dict[str, Any]:
|
||||
"""실무문서(마스터 조합 표 = 옛 일위대가 조합 · PLAN 39-2) 통째 지우기 — 줄을 모두 비움 ·
|
||||
구조물이 쓰는 조합이 있으면 409(아무것도 안 지움) · 판 다르면 409 · 마스터 층에서만."""
|
||||
if heads()[-1] != MASTER:
|
||||
raise store.StoreError(400, "실무문서는 마스터 층에만 있음")
|
||||
doc, have = _load()
|
||||
if have != (version or ""):
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
keys = [str(r.get("키")) for r in doc.get("줄") or []]
|
||||
_refuse_used(keys)
|
||||
if keys:
|
||||
_m01(combo_store._write, [{"op": "delete", "key": k} for k in keys], have)
|
||||
return {**listing(), "지운조합": keys}
|
||||
|
||||
|
||||
def delete(key: str, version: str) -> dict[str, Any]:
|
||||
"""지우기 — 이 층 구조물 조합이 쓰면 409 + 항목 이름들(지우지 않음)."""
|
||||
if heads()[-1] == MASTER:
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Practice.ts
|
||||
* 좌측 「실무문서」 컨테이너(구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 · PLAN 39-2) —
|
||||
* 줄 하나를 누르면 메인 칸에 옛 마스터 일위대가 조합(`일위대가조합.json` 통째) 목록 · 상세(읽기만) ·
|
||||
* [통째 지우기](물음 두 번 → `DELETE /api/m02/practice` · 서버도 시스템 관리자 · 쓰는 곳 0 확인).
|
||||
* 읽기 = 마스터 조합 길(`Labor_Api`) · 복사 원본 고르기는 목록 화면(ItemList)의 실무문서 탭 몫.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
fetchCombo,
|
||||
fetchComboList,
|
||||
type ComboList,
|
||||
type ComboOne,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
|
||||
export interface PracticeSectionOptions {
|
||||
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
/** 메인 칸에 화면을 띄움 */
|
||||
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
|
||||
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export interface PracticeSection {
|
||||
title: string;
|
||||
body: HTMLElement;
|
||||
/** 다른 화면을 열었음 — 고름 표시만 지움 */
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
|
||||
/** 서버 까닭 글 — `{errors}` · 판 다름 · 글 */
|
||||
function reasonOf(detail: unknown, status: number): string {
|
||||
if (typeof detail === "string") return detail;
|
||||
const d = detail as { errors?: string[]; stale?: unknown } | null;
|
||||
if (d?.errors?.length) return d.errors.join(" · ");
|
||||
return d?.stale ? L("M02_PracticeStale") : `HTTP ${status}`;
|
||||
}
|
||||
|
||||
/** 통째 지우기 — 줄을 모두 비움(판 = 읽은 표 판) */
|
||||
async function dropPractice(판: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE_URL}/m02/practice?판=${encodeURIComponent(판)}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
if (res.ok) return;
|
||||
const data = (await res.json().catch(() => ({}))) as { detail?: unknown };
|
||||
throw new Error(reasonOf(data.detail, res.status));
|
||||
}
|
||||
|
||||
const table = (heads: string[], rows: HTMLElement[]): HTMLElement =>
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-practice__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
||||
}),
|
||||
el("tbody", { children: rows }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const cells = (values: string[]): HTMLElement[] =>
|
||||
values.map((v) => el("td", { text: v, attrs: { title: v } }));
|
||||
|
||||
export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSection {
|
||||
const row = el("button", {
|
||||
className: "m02-side__item ui-sidebar-row",
|
||||
text: L("M02_PracticeRow"),
|
||||
attrs: { type: "button", "data-practice-row": "" },
|
||||
});
|
||||
const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [row] });
|
||||
const view = el("div", { className: "m02-practice" });
|
||||
let opened = false;
|
||||
let nav = 0; // 늦게 온 응답 버림
|
||||
const mark = (on: boolean): void => {
|
||||
opened = on;
|
||||
row.classList.toggle("is-active", on);
|
||||
};
|
||||
row.addEventListener("click", () => void open());
|
||||
|
||||
async function open(): Promise<void> {
|
||||
if (opened || !(await opt.confirmLeave())) return;
|
||||
opt.show(L("M02_PracticeTitle"), view);
|
||||
opt.onOpen();
|
||||
mark(true);
|
||||
await drawList();
|
||||
}
|
||||
|
||||
async function drawList(): Promise<void> {
|
||||
const my = ++nav;
|
||||
const stop = showLoading(view);
|
||||
let list: ComboList;
|
||||
try {
|
||||
list = await fetchComboList();
|
||||
} catch (error) {
|
||||
stop();
|
||||
if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawList()));
|
||||
return;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
if (my !== nav) return;
|
||||
const drop = createButton({ label: L("M02_PracticeDrop"), variant: "danger", size: "sm" });
|
||||
drop.disabled = !list.줄.length;
|
||||
drop.addEventListener("click", () => void dropAll(list, drop));
|
||||
const rows = list.줄.map((c) => {
|
||||
const tr = el("tr", {
|
||||
className: "m02-practice__row",
|
||||
attrs: { "data-practice-key": c.키 },
|
||||
children: cells([c.키, c.이름, c.구분 ?? "", c.단위 ?? "", String(c.count)]),
|
||||
});
|
||||
tr.addEventListener("click", () => void drawOne(c.키));
|
||||
return tr;
|
||||
});
|
||||
view.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-practice__bar",
|
||||
children: [
|
||||
el("span", {
|
||||
className: "m02-practice__note",
|
||||
text: L("M02_PracticeCount").replace("{n}", String(list.줄.length)),
|
||||
}),
|
||||
drop,
|
||||
],
|
||||
}),
|
||||
rows.length
|
||||
? table(
|
||||
[
|
||||
L("M02_PracticeKey"),
|
||||
L("M02_Name"),
|
||||
L("M02_PracticeGroup"),
|
||||
L("M02_PracticeUnit"),
|
||||
L("M02_PracticeLogics"),
|
||||
],
|
||||
rows,
|
||||
)
|
||||
: el("p", { className: "m02-practice__note", text: L("M02_PracticeEmpty") }),
|
||||
);
|
||||
}
|
||||
|
||||
/** 상세 — 읽기만(이름 · 키 · 구분 · 단위 · 비고 + 담은 로직 표) */
|
||||
async function drawOne(key: string): Promise<void> {
|
||||
const my = ++nav;
|
||||
const stop = showLoading(view);
|
||||
let one: ComboOne;
|
||||
try {
|
||||
one = await fetchCombo(key);
|
||||
} catch (error) {
|
||||
stop();
|
||||
if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawOne(key)));
|
||||
return;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
if (my !== nav) return;
|
||||
const back = createButton({ label: L("M02_PracticeBack"), variant: "ghost", size: "sm" });
|
||||
back.addEventListener("click", () => void drawList());
|
||||
const c = one.combo;
|
||||
const info = [
|
||||
[L("M02_PracticeKey"), c.키],
|
||||
[L("M02_PracticeGroup"), c.구분 ?? ""],
|
||||
[L("M02_PracticeUnit"), c.단위 ?? ""],
|
||||
].map(([k, v]) => el("span", { className: "m02-practice__note", text: `${k} ${v}` }));
|
||||
const lines = one.줄.map((x) =>
|
||||
el("tr", {
|
||||
children: cells([
|
||||
String(x.차례),
|
||||
x.로직,
|
||||
x.ok ? (x.이름 ?? "") : (x.까닭 ?? ""),
|
||||
x.결과단위 ?? "",
|
||||
x.메모 ?? "",
|
||||
]),
|
||||
}),
|
||||
);
|
||||
view.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-practice__bar",
|
||||
children: [back, el("strong", { text: c.이름 }), ...info],
|
||||
}),
|
||||
...(c.비고 ? [el("p", { className: "m02-practice__memo", text: c.비고 })] : []),
|
||||
lines.length
|
||||
? table(
|
||||
[
|
||||
L("M02_PracticeOrder"),
|
||||
L("M02_PracticeKey"),
|
||||
L("M02_Name"),
|
||||
L("M02_PracticeUnit"),
|
||||
L("M02_PracticeMemo"),
|
||||
],
|
||||
lines,
|
||||
)
|
||||
: el("p", { className: "m02-practice__note", text: L("M02_PracticeNoLogic") }),
|
||||
);
|
||||
}
|
||||
|
||||
/** 통째 지우기 — 물음 두 번 · 서버가 쓰는 곳 · 판을 다시 봄 */
|
||||
async function dropAll(list: ComboList, button: HTMLButtonElement): Promise<void> {
|
||||
const n = String(list.줄.length);
|
||||
if (!(await showConfirmDialog(L("M02_PracticeDropAsk").replace("{n}", n), L("M02_Delete"))))
|
||||
return;
|
||||
if (!(await showConfirmDialog(L("M02_PracticeDropAsk2"), L("M02_PracticeDrop")))) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await dropPractice(list.판);
|
||||
showToast(L("M02_PracticeDropped").replace("{n}", n), "success");
|
||||
} catch (error) {
|
||||
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
|
||||
}
|
||||
await drawList();
|
||||
}
|
||||
|
||||
return { title: L("M02_PracticeTitle"), body, clear: () => mark(false) };
|
||||
}
|
||||
@@ -168,6 +168,15 @@ def delete_combo(
|
||||
return _call(_root(session, layer, owner, write=True), combos.delete, key, 판)
|
||||
|
||||
|
||||
@router.delete("/practice")
|
||||
def delete_practice(판: str = "", session: Session = Depends(verify_session)) -> dict:
|
||||
"""실무문서 통째 지우기(PLAN 39-2) — 시스템 관리자만 · 마스터 조합 표 줄을 모두 비움 ·
|
||||
구조물이 쓰는 조합이 있으면 409."""
|
||||
if not access.is_system_admin(session):
|
||||
raise HTTPException(status_code=403, detail="실무문서는 시스템 관리자만")
|
||||
return _call(_root(session, "system", None, write=True), combos.drop_practice, 판)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/combos/tree")
|
||||
def post_tree(
|
||||
layer: Layer,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { buildMatsetSection } from "./M02_MasterTemplete_Matset";
|
||||
import { buildOptionsSection } from "./M02_MasterTemplete_Options";
|
||||
import { buildPracticeSection } from "./M02_MasterTemplete_Practice";
|
||||
import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
const KINDS: { kind: Kind; label: () => string }[] = [
|
||||
@@ -147,7 +148,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
confirmLeave: opt.confirmLeave,
|
||||
show: opt.showView,
|
||||
close: () => opt.onOpen(null),
|
||||
onOpen: () => (options.clear(), (allPicked = null), paint()),
|
||||
onOpen: () => (options.clear(), practice?.clear(), (allPicked = null), paint()),
|
||||
});
|
||||
const matsetSection = section(matset.title, matset.body, ...matset.actions);
|
||||
// 펼칠 때마다 새로 받음 — 재료산출 옆 판이 갈라 낸 조합도 바로 보이게
|
||||
@@ -159,7 +160,7 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
isAdmin: canEdit(layer, opt.isAdmin),
|
||||
confirmLeave: opt.confirmLeave,
|
||||
show: opt.showView,
|
||||
onOpen: () => (matset.clear(), (allPicked = null), paint()),
|
||||
onOpen: () => (matset.clear(), practice?.clear(), (allPicked = null), paint()),
|
||||
});
|
||||
/** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */
|
||||
const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => {
|
||||
@@ -191,9 +192,22 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
도면: section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")),
|
||||
}
|
||||
: null;
|
||||
// 실무문서(옛 마스터 일위대가 조합 · PLAN 39-2) — 구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만
|
||||
const practice =
|
||||
layer === "system" && opt.isAdmin
|
||||
? buildPracticeSection({
|
||||
confirmLeave: opt.confirmLeave,
|
||||
show: opt.showView,
|
||||
onOpen: () => (matset.clear(), options.clear(), (allPicked = null), paint()),
|
||||
})
|
||||
: null;
|
||||
const listHost = el("div", {
|
||||
className: "m02-side__lists ui-sidebar-panel",
|
||||
children: [summary, ...(extras ? [extras.옵션, extras.도면] : [])],
|
||||
children: [
|
||||
summary,
|
||||
...(extras ? [extras.옵션, extras.도면] : []),
|
||||
...(practice ? [section(practice.title, practice.body)] : []),
|
||||
],
|
||||
});
|
||||
attachCollapsible(listHost);
|
||||
|
||||
@@ -285,12 +299,14 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
paintButtons();
|
||||
if (opt.getOpen()) matset.clear();
|
||||
if (opt.getOpen()) options.clear();
|
||||
if (opt.getOpen()) practice?.clear();
|
||||
}
|
||||
|
||||
async function selectAll(kind: AllKind): Promise<void> {
|
||||
if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;
|
||||
matset.clear();
|
||||
options.clear();
|
||||
practice?.clear();
|
||||
allPicked = kind;
|
||||
opt.onAll(kind); // 메인 칸 보기 화면 — 연 뒤 `paint` 가 이 줄을 고른 채 그림
|
||||
paint();
|
||||
|
||||
@@ -22,6 +22,46 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 실무문서 화면(PLAN 39-2) — 위 막대(개수 · 통째 지우기 / 목록으로 · 이름 · 정보) + 표 */
|
||||
.m02-practice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m02-practice__bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m02-practice__bar > button:last-child:not(:first-child) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.m02-practice__note {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m02-practice__memo {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.m02-practice__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m02-practice__row:hover {
|
||||
background: var(--color-mist-violet);
|
||||
}
|
||||
|
||||
.m02-side__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""실무문서(PLAN 39-2) — 옛 마스터 일위대가 조합 표 통째 · 시스템 관리자만 통째 지우기.
|
||||
|
||||
못박는 것
|
||||
- `DELETE /api/m02/practice?판=` = 시스템 관리자만(그 밖 403) · 판 다르면 409 ·
|
||||
구조물이 쓰는 조합이 있으면 409(아무것도 안 지움) · 지우면 줄 0 · 마스터 틀 검사 그대로.
|
||||
- 화면(`Practice.ts`) = 마스터 층 · 시스템 관리자만 · 물음 두 번 · 상세 읽기만.
|
||||
정본(마스터)은 읽기만 · 쓰기는 임시 사본.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
||||
from resources.tester.test_m02_combo_layers import _as, _list, master_copy, world # noqa: F401
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
|
||||
|
||||
def _drop(client, 판: str, want: int) -> dict:
|
||||
res = client.delete("/api/m02/practice", params={"판": 판})
|
||||
assert res.status_code == want, res.text
|
||||
return res.json()
|
||||
|
||||
|
||||
def test_통째_지우기_시스템_관리자만_판_쓰는_곳(world, master_copy, monkeypatch): # noqa: F811
|
||||
admin = _as(world, "SYSTEM_ADMIN")
|
||||
before = _list(admin, "system")
|
||||
assert before["줄"] # 사본에 옛 조합이 있음
|
||||
for role in ("USER", "ADMIN"):
|
||||
_drop(_as(world, role), before["판"], 403)
|
||||
admin = _as(world, "SYSTEM_ADMIN")
|
||||
_drop(admin, "옛판", 409)
|
||||
used = before["줄"][0]["키"]
|
||||
monkeypatch.setattr(combo_store, "combo_users", lambda k: ["항목 하나"] if k == used else [])
|
||||
got = _drop(admin, before["판"], 409)
|
||||
assert "항목 하나" in json.dumps(got, ensure_ascii=False)
|
||||
assert len(_list(admin, "system")["줄"]) == len(before["줄"]) # 아무것도 안 지움
|
||||
monkeypatch.setattr(combo_store, "combo_users", lambda k: [])
|
||||
got = _drop(admin, before["판"], 200)
|
||||
assert got["줄"] == [] and len(got["지운조합"]) == len(before["줄"])
|
||||
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
|
||||
assert doc["줄"] == [] and m01.cm.check_form(combos.FILE, doc) == []
|
||||
again = _list(admin, "system")
|
||||
assert _drop(admin, again["판"], 200)["지운조합"] == [] # 빈 표 = 그대로
|
||||
|
||||
|
||||
def test_화면_시스템_관리자_마스터만_물음_두_번_읽기만():
|
||||
side = (M02 / "M02_MasterTemplete_UI_Side.ts").read_text("utf-8")
|
||||
assert 'layer === "system" && opt.isAdmin' in side
|
||||
order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0]
|
||||
assert order.index("extras.도면") < order.index("practice") # 도면 양식 아래
|
||||
view = (M02 / "M02_MasterTemplete_Practice.ts").read_text("utf-8")
|
||||
assert view.count("showConfirmDialog(") == 2
|
||||
assert "dropPractice(" in view and "createInputField" not in view # 고칠 칸 없음
|
||||
@@ -120,11 +120,14 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에
|
||||
assert side.index("옵션: section(options.title") < side.index(
|
||||
'도면: section(L("M02_KindDrawing")'
|
||||
)
|
||||
# 차례 = 구조물 조합(일위대가는 그 하위 · 39-2) → 옵션 → 도면 양식
|
||||
assert "children: [summary, ...(extras ? [extras.옵션, extras.도면" in side
|
||||
# 자재 조합을 열면 이 줄 · 「전체 항목」 고름 지움 / 이 줄을 열면 자재 조합 · 「전체 항목」 고름 지움
|
||||
assert "onOpen: () => (options.clear(), (allPicked = null), paint())" in side
|
||||
assert "onOpen: () => (matset.clear(), (allPicked = null), paint())" in side
|
||||
# 차례 = 구조물 조합(일위대가는 그 하위 · 39-2) → 옵션 → 도면 양식 → 실무문서
|
||||
order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0]
|
||||
assert (
|
||||
order.index("summary") < order.index("extras.옵션, extras.도면") < order.index("practice")
|
||||
)
|
||||
# 자재 조합을 열면 이 줄 · 실무문서 · 「전체 항목」 고름 지움 / 이 줄을 열면 자재 조합 · … 지움
|
||||
assert "onOpen: () => (options.clear(), practice?.clear(), (allPicked = null), paint())" in side
|
||||
assert "onOpen: () => (matset.clear(), practice?.clear(), (allPicked = null), paint())" in side
|
||||
paint = side.split("function paint(): void {")[1].split("\n }\n")[0]
|
||||
assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움
|
||||
api = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts").read_text("utf-8")
|
||||
|
||||
@@ -176,4 +176,30 @@ export const ui_locales_m2 = {
|
||||
M02_TreeDone: ["나무를 고침", "Tree updated"],
|
||||
M02_KindLabor: ["일위대가", "Unit costs"],
|
||||
M02_ItemsAll: ["전체 항목", "All items"],
|
||||
M02_PracticeTitle: ["실무문서", "Practice documents"],
|
||||
M02_PracticeRow: ["실무문서 목록", "Practice document list"],
|
||||
M02_PracticeCount: [
|
||||
"옛 마스터 일위대가 조합 {n}개 — 보기 · 복사 원본만(고칠 수 없음)",
|
||||
"{n} old master unit-cost combos — view and copy source only",
|
||||
],
|
||||
M02_PracticeEmpty: ["실무문서 없음", "No practice documents"],
|
||||
M02_PracticeKey: ["키", "Key"],
|
||||
M02_PracticeGroup: ["구분", "Group"],
|
||||
M02_PracticeUnit: ["단위", "Unit"],
|
||||
M02_PracticeLogics: ["로직 수", "Logics"],
|
||||
M02_PracticeOrder: ["차례", "No."],
|
||||
M02_PracticeMemo: ["메모", "Memo"],
|
||||
M02_PracticeNoLogic: ["담은 로직 없음", "No logics"],
|
||||
M02_PracticeBack: ["목록으로", "Back to list"],
|
||||
M02_PracticeDrop: ["통째 지우기", "Delete all"],
|
||||
M02_PracticeDropAsk: ["실무문서 {n}개를 모두 지울까요?", "Delete all {n} practice documents?"],
|
||||
M02_PracticeDropAsk2: [
|
||||
"되돌릴 수 없음 — 정말 통째로 지울까요?",
|
||||
"This cannot be undone — really delete all?",
|
||||
],
|
||||
M02_PracticeDropped: ["실무문서 {n}개를 지움", "Deleted {n} practice documents"],
|
||||
M02_PracticeStale: [
|
||||
"그 사이 실무문서가 바뀜 — 목록을 다시 받음",
|
||||
"Practice documents changed — reloaded",
|
||||
],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user