Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_1
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 · 휠 방향 고름 · 표 */
|
||||
.app-settings__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.app-settings__radios {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.app-settings__radios > label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-settings__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.app-settings__table th,
|
||||
.app-settings__table td {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.app-settings__table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.app-settings__table input[aria-invalid="true"] {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.app-settings__error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.app-settings__error:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/* =============================================================================
|
||||
* app_settings.ts
|
||||
* ☰ [설정] 창(PLAN 332-2 · 요구 224-8) — 탭 셋: ① 마우스 ② CAD 단축키 ③ 다른 곳 단축키(읽기 전용).
|
||||
*
|
||||
* - 고침은 창 안 사본에만 · [저장] 때만 계정 pref(`input-prefs`)로 한 번 씀 — 읽기는 `input_prefs.ts`
|
||||
* - 저장 안 한 고침이 있으면 닫을 때 물음(askLeave) · 잘못 있는 별칭은 저장 안 함
|
||||
* - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { askLeave } from "@ui/ui_template_leave_dialog";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { writeState } from "./b_page_state";
|
||||
import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs";
|
||||
import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad";
|
||||
import "./app_settings.css";
|
||||
|
||||
type LocaleKey = Parameters<typeof L>[0];
|
||||
type TabId = "mouse" | "cad" | "keys";
|
||||
|
||||
const TABS: [TabId, LocaleKey][] = [
|
||||
["mouse", "App_Settings_Tab_Mouse"],
|
||||
["cad", "App_Settings_Tab_Cad"],
|
||||
["keys", "App_Settings_Tab_Keys"],
|
||||
];
|
||||
|
||||
const WHEEL: [WheelZoom, LocaleKey][] = [
|
||||
["pull", "App_Settings_Wheel_Pull"],
|
||||
["push", "App_Settings_Wheel_Push"],
|
||||
];
|
||||
|
||||
const MOUSE_RULES: LocaleKey[] = [
|
||||
"App_Settings_Rule_Pan",
|
||||
"App_Settings_Rule_Pick",
|
||||
"App_Settings_Rule_Right",
|
||||
"App_Settings_Rule_Pinch",
|
||||
];
|
||||
|
||||
/** ③ 안내표 줄 — [자리, 키] */
|
||||
const KEY_ROWS: [LocaleKey, LocaleKey][] = [
|
||||
["App_Settings_Keys_All", "App_Settings_Keys_All_Keys"],
|
||||
["App_Settings_Keys_Profile", "App_Settings_Keys_Profile_Keys"],
|
||||
["App_Settings_Keys_RouteEdit", "App_Settings_Keys_RouteEdit_Keys"],
|
||||
["App_Settings_Keys_Section", "App_Settings_Keys_Section_Keys"],
|
||||
["App_Settings_Keys_Cad", "App_Settings_Keys_Cad_Keys"],
|
||||
["App_Settings_Keys_Table", "App_Settings_Keys_Table_Keys"],
|
||||
["App_Settings_Keys_M02", "App_Settings_Keys_M02_Keys"],
|
||||
];
|
||||
|
||||
/** 비교 · 저장 꼴 — 별칭 명령 차례를 맞춤 */
|
||||
const normalize = (p: InputPrefs): InputPrefs => ({
|
||||
wheelZoom: p.wheelZoom,
|
||||
cadAliases: Object.fromEntries(Object.entries(p.cadAliases).sort(([a], [b]) => (a < b ? -1 : 1))),
|
||||
});
|
||||
const snapshot = (p: InputPrefs): string => JSON.stringify(normalize(p));
|
||||
|
||||
function mouseTab(draft: InputPrefs): HTMLElement {
|
||||
const group = el("div", {
|
||||
className: "app-settings__radios",
|
||||
attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") },
|
||||
});
|
||||
for (const [mode, key] of WHEEL) {
|
||||
const input = el("input", {
|
||||
attrs: { type: "radio", name: "app-settings-wheel", value: mode },
|
||||
});
|
||||
input.checked = draft.wheelZoom === mode;
|
||||
input.addEventListener("change", () => (draft.wheelZoom = mode));
|
||||
group.append(el("label", { children: [input, L(key)] }));
|
||||
}
|
||||
return el("div", {
|
||||
children: [
|
||||
el("h4", { text: L("App_Settings_Wheel") }),
|
||||
group,
|
||||
el("h4", { text: L("App_Settings_Rules") }),
|
||||
el("ul", { children: MOUSE_RULES.map((key) => el("li", { text: L(key) })) }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function keysTab(): HTMLElement {
|
||||
const rows = KEY_ROWS.map(([place, keys]) =>
|
||||
el("tr", { children: [el("th", { text: L(place) }), el("td", { text: L(keys) })] }),
|
||||
);
|
||||
return el("table", {
|
||||
className: "app-settings__table",
|
||||
children: [el("tbody", { children: rows })],
|
||||
});
|
||||
}
|
||||
|
||||
/** ☰ [설정] — 창을 열고 지금 설정을 사본으로 고침 */
|
||||
export function openAppSettings(): void {
|
||||
const draft = normalize(inputPrefs());
|
||||
let saved = snapshot(draft);
|
||||
const cad = buildCadAliasTab(draft.cadAliases);
|
||||
const panels: Record<TabId, HTMLElement> = {
|
||||
mouse: mouseTab(draft),
|
||||
cad: cad.root,
|
||||
keys: keysTab(),
|
||||
};
|
||||
const tabs = el("div", {
|
||||
className: "app-settings__tabs ui-subtabs",
|
||||
attrs: { role: "tablist" },
|
||||
});
|
||||
const show = (id: TabId): void => {
|
||||
tabs.replaceChildren(
|
||||
...TABS.map(([tab, key]) => {
|
||||
const button = createButton({
|
||||
label: L(key),
|
||||
variant: tab === id ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => show(tab),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(tab === id));
|
||||
button.dataset.settingsTab = tab;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id;
|
||||
};
|
||||
|
||||
/** [저장] — 잘못 있는 별칭이면 CAD 탭을 보이고 안 씀 */
|
||||
const save = (): boolean => {
|
||||
const errors = checkCadAliases(draft.cadAliases);
|
||||
cad.mark(errors);
|
||||
if (Object.keys(errors).length) {
|
||||
show("cad");
|
||||
showToast(L("App_Settings_Cad_Invalid"), "error");
|
||||
return false;
|
||||
}
|
||||
if (snapshot(draft) !== saved) writeState("input-prefs", normalize(draft));
|
||||
saved = snapshot(draft);
|
||||
showToast(L("App_Settings_Saved"), "success");
|
||||
return true;
|
||||
};
|
||||
|
||||
const saveButton = createButton({
|
||||
label: L("Common_Btn_Save"),
|
||||
variant: "filled",
|
||||
size: "sm",
|
||||
onClick: () => void save(),
|
||||
});
|
||||
openModal({
|
||||
title: L("App_Settings"),
|
||||
closeLabel: L("Common_Btn_Close"),
|
||||
dialogClass: "app-settings",
|
||||
actions: [saveButton],
|
||||
beforeClose: async () => {
|
||||
if (snapshot(draft) === saved) return true;
|
||||
const choice = await askLeave(L("App_Settings_Leave"));
|
||||
return choice === "discard" || (choice === "save" && save());
|
||||
},
|
||||
mount: (body) => {
|
||||
show("mouse");
|
||||
body.append(tabs, ...Object.values(panels));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/* =============================================================================
|
||||
* app_settings_cad.ts
|
||||
* ☰ [설정] ② CAD 단축키 탭(PLAN 332-2) — 명령 표(명령 · 한글 이름 · 기본 별칭 · 내 별칭) · 검사.
|
||||
*
|
||||
* - 명령 목록 = `openwebcad/src/commands/command-catalog.json`(registry 거울 · commands.*.ts 는 xstate 가 딸려 와서 JSON 한 벌)
|
||||
* - acad.pgp 식 덮어쓰기 — CAD 는 명령 이름 > 내 별칭 > 기본 별칭 순서로 찾음(배선은 332-3)
|
||||
* - 검사 = 영문 · 숫자만 · 내 별칭끼리 겹침 금지 · 명령 이름과 같은 별칭 금지
|
||||
* ========================================================================== */
|
||||
|
||||
import catalog from "../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
|
||||
export interface CadCommandRow {
|
||||
id: string;
|
||||
label: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export const CAD_COMMANDS = catalog as CadCommandRow[];
|
||||
|
||||
export type AliasError = "chars" | "dup" | "name";
|
||||
|
||||
const ERROR_TEXT: Record<AliasError, Parameters<typeof L>[0]> = {
|
||||
chars: "App_Settings_Cad_ErrChars",
|
||||
dup: "App_Settings_Cad_ErrDup",
|
||||
name: "App_Settings_Cad_ErrName",
|
||||
};
|
||||
|
||||
/** 칸 글 → 별칭 목록(쉼표 · 빈칸으로 가름 · 대문자) */
|
||||
export const parseAliases = (text: string): string[] =>
|
||||
text
|
||||
.toUpperCase()
|
||||
.split(/[\s,]+/)
|
||||
.filter(Boolean);
|
||||
|
||||
/** 내 별칭 검사 — `{명령: 첫 잘못}` · 비면 통과 */
|
||||
export function checkCadAliases(
|
||||
aliases: Record<string, string[]>,
|
||||
commands: CadCommandRow[] = CAD_COMMANDS,
|
||||
): Record<string, AliasError> {
|
||||
const ids = new Set(commands.map((c) => c.id));
|
||||
const owner = new Map<string, string>();
|
||||
const errors: Record<string, AliasError> = {};
|
||||
for (const [id, list] of Object.entries(aliases)) {
|
||||
for (const alias of list) {
|
||||
const first = owner.get(alias);
|
||||
const error: AliasError | null = !/^[A-Z0-9]+$/.test(alias)
|
||||
? "chars"
|
||||
: ids.has(alias)
|
||||
? "name"
|
||||
: first !== undefined
|
||||
? "dup"
|
||||
: null;
|
||||
if (!error) owner.set(alias, id);
|
||||
else {
|
||||
errors[id] ??= error;
|
||||
if (first !== undefined) errors[first] ??= "dup";
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** 탭 몸 — `aliases` 를 칸 입력대로 고쳐 씀 · `mark` 로 잘못 칸 표시 */
|
||||
export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
root: HTMLElement;
|
||||
mark: (errors: Record<string, AliasError>) => void;
|
||||
} {
|
||||
const message = el("p", { className: "app-settings__error", attrs: { role: "alert" } });
|
||||
const inputs = new Map<string, HTMLInputElement>();
|
||||
const rows = CAD_COMMANDS.map((command) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { type: "text", "aria-label": `${command.id} ${L("App_Settings_Cad_Mine")}` },
|
||||
});
|
||||
input.value = (aliases[command.id] ?? []).join(", ");
|
||||
input.dataset.cadCommand = command.id;
|
||||
input.addEventListener("input", () => {
|
||||
const list = parseAliases(input.value);
|
||||
if (list.length) aliases[command.id] = list;
|
||||
else delete aliases[command.id];
|
||||
});
|
||||
inputs.set(command.id, input);
|
||||
const cells = [command.id, command.label, command.aliases.join(", ")].map((text) =>
|
||||
el("td", { text }),
|
||||
);
|
||||
return el("tr", { children: [...cells, el("td", { children: [input] })] });
|
||||
});
|
||||
const head = el("tr", {
|
||||
children: (
|
||||
[
|
||||
"App_Settings_Cad_Command",
|
||||
"App_Settings_Cad_Label",
|
||||
"App_Settings_Cad_Default",
|
||||
"App_Settings_Cad_Mine",
|
||||
] as const
|
||||
).map((key) => el("th", { text: L(key) })),
|
||||
});
|
||||
const table = el("table", {
|
||||
className: "app-settings__table",
|
||||
children: [el("thead", { children: [head] }), el("tbody", { children: rows })],
|
||||
});
|
||||
const mark = (errors: Record<string, AliasError>): void => {
|
||||
for (const [id, input] of inputs) {
|
||||
const error = errors[id];
|
||||
if (error) input.setAttribute("aria-invalid", "true");
|
||||
else input.removeAttribute("aria-invalid");
|
||||
input.title = error ? L(ERROR_TEXT[error]) : "";
|
||||
}
|
||||
const kinds = [...new Set(Object.values(errors))];
|
||||
message.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
|
||||
};
|
||||
const root = el("div", {
|
||||
children: [el("p", { text: L("App_Settings_Cad_Help") }), message, table],
|
||||
});
|
||||
return { root, mark };
|
||||
}
|
||||
+14
-2
@@ -171,10 +171,14 @@ function buildDrawer(header: HTMLElement): HTMLElement[] {
|
||||
burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open")));
|
||||
scrim.addEventListener("click", () => setOpen(false));
|
||||
header.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false)); // ☰ 에 포커스가 있어도 닫힘
|
||||
// 로고 · 진행 단계 · 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
|
||||
// 로고 · 진행 단계 · 메뉴 · 로그인 · [설정] 을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠)
|
||||
drawer.addEventListener("click", (e) => {
|
||||
const hit = (e.target as Element).closest("button");
|
||||
if (hit?.closest(".app-drawer__brand, .app-drawer__steps, .app-drawer__nav, .app-drawer__auth"))
|
||||
if (
|
||||
hit?.closest(
|
||||
".app-drawer__brand, .app-drawer__steps, .app-drawer__nav, .app-drawer__auth, .app-drawer__settings",
|
||||
)
|
||||
)
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
@@ -199,6 +203,14 @@ function buildDrawer(header: HTMLElement): HTMLElement[] {
|
||||
onClick: onApp_Theme_Toggle_Click,
|
||||
}),
|
||||
);
|
||||
// ☰ [설정](332) — 마우스 · 단축키 창 · 처음 누를 때 불러옴
|
||||
const settingsButton = createButton({
|
||||
label: L("App_Settings"),
|
||||
variant: "pill",
|
||||
onClick: () => void import("./app_settings").then((m) => m.openAppSettings()),
|
||||
});
|
||||
settingsButton.classList.add("app-drawer__settings");
|
||||
prefs.append(settingsButton);
|
||||
const auth = document.createElement("div");
|
||||
auth.className = "app-drawer__row app-drawer__auth";
|
||||
void fillAuthActions(auth);
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
[
|
||||
{"id": "LINE", "label": "선", "aliases": ["L"]},
|
||||
{"id": "PLINE", "label": "폴리선", "aliases": ["PL"]},
|
||||
{"id": "CIRCLE", "label": "원", "aliases": ["C"]},
|
||||
{"id": "ARC", "label": "호", "aliases": ["A"]},
|
||||
{"id": "RECTANG", "label": "직사각형", "aliases": ["REC"]},
|
||||
{"id": "POLYGON", "label": "다각형", "aliases": ["POL"]},
|
||||
{"id": "ELLIPSE", "label": "타원", "aliases": ["EL"]},
|
||||
{"id": "SPLINE", "label": "스플라인", "aliases": ["SPL"]},
|
||||
{"id": "MLINE", "label": "다중선", "aliases": ["ML"]},
|
||||
{"id": "MLSTYLE", "label": "다중선 스타일", "aliases": []},
|
||||
{"id": "XLINE", "label": "구성선", "aliases": ["XL"]},
|
||||
{"id": "RAY", "label": "광선", "aliases": []},
|
||||
{"id": "POINT", "label": "점", "aliases": ["PO"]},
|
||||
{"id": "DONUT", "label": "도넛", "aliases": ["DO"]},
|
||||
{"id": "DIVIDE", "label": "등분", "aliases": ["DIV"]},
|
||||
{"id": "MEASURE", "label": "길이분할", "aliases": ["ME"]},
|
||||
{"id": "HATCH", "label": "해치", "aliases": ["H", "BH"]},
|
||||
{"id": "GRADIENT", "label": "그라데이션", "aliases": ["GD"]},
|
||||
{"id": "BOUNDARY", "label": "경계", "aliases": ["BO"]},
|
||||
{"id": "REGION", "label": "영역", "aliases": ["REG"]},
|
||||
{"id": "WIPEOUT", "label": "와이프아웃", "aliases": []},
|
||||
{"id": "MOVE", "label": "이동", "aliases": ["M"]},
|
||||
{"id": "COPY", "label": "복사", "aliases": ["CO", "CP"]},
|
||||
{"id": "ROTATE", "label": "회전", "aliases": ["RO"]},
|
||||
{"id": "SCALE", "label": "축척", "aliases": ["SC"]},
|
||||
{"id": "ALIGN", "label": "정렬", "aliases": ["AL"]},
|
||||
{"id": "MIRROR", "label": "대칭", "aliases": ["MI"]},
|
||||
{"id": "OFFSET", "label": "간격띄우기", "aliases": ["O"]},
|
||||
{"id": "ARRAY", "label": "배열", "aliases": ["AR"]},
|
||||
{"id": "TRIM", "label": "자르기", "aliases": ["TR"]},
|
||||
{"id": "EXTEND", "label": "연장", "aliases": ["EX"]},
|
||||
{"id": "STRETCH", "label": "신축", "aliases": ["S"]},
|
||||
{"id": "FILLET", "label": "모깎기", "aliases": ["F"]},
|
||||
{"id": "CHAMFER", "label": "모따기", "aliases": ["CHA"]},
|
||||
{"id": "BLEND", "label": "곡선 혼합", "aliases": []},
|
||||
{"id": "BREAK", "label": "끊기", "aliases": ["BR"]},
|
||||
{"id": "BREAKATPOINT", "label": "점에서 끊기", "aliases": []},
|
||||
{"id": "JOIN", "label": "결합", "aliases": ["J"]},
|
||||
{"id": "EXPLODE", "label": "분해", "aliases": ["X"]},
|
||||
{"id": "ERASE", "label": "지우기", "aliases": ["E"]},
|
||||
{"id": "LENGTHEN", "label": "길이조정", "aliases": ["LEN"]},
|
||||
{"id": "PEDIT", "label": "폴리선 편집", "aliases": ["PE"]},
|
||||
{"id": "HATCHEDIT", "label": "해치 편집", "aliases": ["HE"]},
|
||||
{"id": "DRAWORDER", "label": "맨 앞으로", "aliases": ["DR"]},
|
||||
{"id": "DRAWORDERBACK", "label": "맨 뒤로", "aliases": []},
|
||||
{"id": "MATCHPROP", "label": "특성 일치", "aliases": ["MA"]},
|
||||
{"id": "OVERKILL", "label": "중복 객체 삭제", "aliases": []},
|
||||
{"id": "REVERSE", "label": "방향 반전", "aliases": []},
|
||||
{"id": "SETBYLAYER", "label": "ByLayer로", "aliases": []},
|
||||
{"id": "ALIGNLEFT", "label": "왼쪽", "aliases": []},
|
||||
{"id": "ALIGNCENTERH", "label": "가로 중앙", "aliases": []},
|
||||
{"id": "ALIGNRIGHT", "label": "오른쪽", "aliases": []},
|
||||
{"id": "ALIGNTOP", "label": "위", "aliases": []},
|
||||
{"id": "ALIGNCENTERV", "label": "세로 중앙", "aliases": []},
|
||||
{"id": "ALIGNBOTTOM", "label": "아래", "aliases": []},
|
||||
{"id": "MTEXT", "label": "여러 줄 문자", "aliases": ["T", "MT"]},
|
||||
{"id": "TEXT", "label": "단일 행 문자", "aliases": ["DT"]},
|
||||
{"id": "STYLE", "label": "문자 스타일", "aliases": ["ST"]},
|
||||
{"id": "TEXTEDIT", "label": "문자 편집", "aliases": ["TEDIT"]},
|
||||
{"id": "FIND", "label": "찾기·대치", "aliases": []},
|
||||
{"id": "DIM", "label": "치수", "aliases": []},
|
||||
{"id": "DIMLINEAR", "label": "선형 치수", "aliases": ["DLI"]},
|
||||
{"id": "DIMALIGNED", "label": "정렬 치수", "aliases": ["DAL"]},
|
||||
{"id": "DIMANGULAR", "label": "각도 치수", "aliases": ["DAN"]},
|
||||
{"id": "DIMARC", "label": "호 길이 치수", "aliases": ["DAR"]},
|
||||
{"id": "DIMRADIUS", "label": "반지름 치수", "aliases": ["DRA"]},
|
||||
{"id": "DIMDIAMETER", "label": "지름 치수", "aliases": ["DDI"]},
|
||||
{"id": "DIMORDINATE", "label": "세로좌표 치수", "aliases": ["DOR"]},
|
||||
{"id": "DIMBASELINE", "label": "기준선 치수", "aliases": ["DBA"]},
|
||||
{"id": "DIMCONTINUE", "label": "연속 치수", "aliases": ["DCO"]},
|
||||
{"id": "QDIM", "label": "빠른 치수", "aliases": []},
|
||||
{"id": "CENTERMARK", "label": "중심 표식", "aliases": []},
|
||||
{"id": "CENTERLINE", "label": "중심선", "aliases": []},
|
||||
{"id": "DIMSTYLE", "label": "치수 스타일", "aliases": ["D"]},
|
||||
{"id": "DIMUPDATE", "label": "치수 업데이트", "aliases": []},
|
||||
{"id": "DIMSPACE", "label": "치수 간격", "aliases": []},
|
||||
{"id": "MLEADER", "label": "다중 지시선", "aliases": ["MLD"]},
|
||||
{"id": "MLEADERSTYLE", "label": "지시선 스타일", "aliases": ["MLS"]},
|
||||
{"id": "MLEADERALIGN", "label": "지시선 정렬", "aliases": []},
|
||||
{"id": "TABLE", "label": "테이블", "aliases": ["TB"]},
|
||||
{"id": "TABLESTYLE", "label": "테이블 스타일", "aliases": ["TS"]},
|
||||
{"id": "TABLEEDIT", "label": "칸 편집", "aliases": []},
|
||||
{"id": "TABLEROW", "label": "행 넣기·지우기", "aliases": []},
|
||||
{"id": "TABLECOL", "label": "열 넣기·지우기", "aliases": []},
|
||||
{"id": "TABLEMERGE", "label": "칸 병합", "aliases": []},
|
||||
{"id": "TABLEUNMERGE", "label": "병합 해제", "aliases": []},
|
||||
{"id": "PLACETABLE", "label": "표 놓기", "aliases": []},
|
||||
{"id": "REVCLOUD", "label": "구름형 리비전", "aliases": []},
|
||||
{"id": "ANNOSCALE", "label": "주석 축척", "aliases": []},
|
||||
{"id": "LAYER", "label": "도면층 특성", "aliases": ["LA"]},
|
||||
{"id": "LAYCURSET", "label": "현재 도면층 설정", "aliases": []},
|
||||
{"id": "LAYOFF", "label": "도면층 끄기", "aliases": []},
|
||||
{"id": "LAYON", "label": "모두 켜기", "aliases": []},
|
||||
{"id": "LAYFRZ", "label": "동결", "aliases": []},
|
||||
{"id": "LAYTHW", "label": "동결 해제", "aliases": []},
|
||||
{"id": "LAYLCK", "label": "잠금", "aliases": []},
|
||||
{"id": "LAYULK", "label": "잠금 해제", "aliases": []},
|
||||
{"id": "LAYISO", "label": "분리", "aliases": []},
|
||||
{"id": "LAYUNISO", "label": "분리 해제", "aliases": []},
|
||||
{"id": "LAYERP", "label": "이전 상태", "aliases": []},
|
||||
{"id": "LAYERSTATE", "label": "도면층 상태 저장", "aliases": ["LAS"]},
|
||||
{"id": "LAYERSTATERESTORE", "label": "도면층 상태 복원", "aliases": []},
|
||||
{"id": "LAYMCH", "label": "도면층 일치", "aliases": []},
|
||||
{"id": "LAYCUR", "label": "현재 도면층으로", "aliases": []},
|
||||
{"id": "LAYMRG", "label": "도면층 병합", "aliases": []},
|
||||
{"id": "LAYDEL", "label": "도면층 삭제", "aliases": []},
|
||||
{"id": "LAYWALK", "label": "도면층 탐색", "aliases": []},
|
||||
{"id": "COLOR", "label": "색상", "aliases": ["COL"]},
|
||||
{"id": "LINETYPE", "label": "선종류", "aliases": ["LT"]},
|
||||
{"id": "LWEIGHT", "label": "선가중치", "aliases": ["LW"]},
|
||||
{"id": "PROPERTIES", "label": "특성", "aliases": ["CH", "MO", "PR"]},
|
||||
{"id": "QUICKPROPERTIES", "label": "빠른 특성", "aliases": ["QP"]},
|
||||
{"id": "TRANSPARENCY", "label": "투명도", "aliases": []},
|
||||
{"id": "GROUP", "label": "그룹", "aliases": ["G"]},
|
||||
{"id": "UNGROUP", "label": "그룹 해제", "aliases": []},
|
||||
{"id": "DIST", "label": "거리", "aliases": ["DI"]},
|
||||
{"id": "RADIUS", "label": "반지름", "aliases": []},
|
||||
{"id": "ANGLE", "label": "각도", "aliases": []},
|
||||
{"id": "AREA", "label": "면적", "aliases": ["AA"]},
|
||||
{"id": "MEASUREGEOM", "label": "빠른 측정", "aliases": ["MEA"]},
|
||||
{"id": "ID", "label": "점 좌표", "aliases": []},
|
||||
{"id": "LIST", "label": "리스트", "aliases": ["LI"]},
|
||||
{"id": "QSELECT", "label": "빠른 선택", "aliases": []},
|
||||
{"id": "QUICKCALC", "label": "계산기", "aliases": ["QC"]},
|
||||
{"id": "SELECTSIMILAR", "label": "유사 선택", "aliases": []},
|
||||
{"id": "ISOLATEOBJECTS", "label": "객체 분리", "aliases": []},
|
||||
{"id": "HIDEOBJECTS", "label": "객체 숨기기", "aliases": []},
|
||||
{"id": "UNISOLATEOBJECTS", "label": "객체 분리 종료", "aliases": ["UNHIDE", "UNISOLATE"]},
|
||||
{"id": "COPYCLIP", "label": "복사", "aliases": []},
|
||||
{"id": "CUTCLIP", "label": "잘라내기", "aliases": []},
|
||||
{"id": "PASTECLIP", "label": "붙여넣기", "aliases": []},
|
||||
{"id": "COPYBASE", "label": "기준점 복사", "aliases": []},
|
||||
{"id": "PASTEORIG", "label": "원래 좌표로 붙여넣기", "aliases": []},
|
||||
{"id": "PASTEBLOCK", "label": "그룹으로 붙여넣기", "aliases": []},
|
||||
{"id": "IMAGEATTACH", "label": "이미지 부착", "aliases": ["IAT"]},
|
||||
{"id": "BLOCK", "label": "블록 작성", "aliases": ["B"]},
|
||||
{"id": "INSERT", "label": "블록 삽입", "aliases": ["I"]},
|
||||
{"id": "SELECT", "label": "선택", "aliases": ["SE"]},
|
||||
{"id": "ZOOM", "label": "범위 줌", "aliases": ["Z"]},
|
||||
{"id": "ZOOMIN", "label": "확대", "aliases": []},
|
||||
{"id": "ZOOMOUT", "label": "축소", "aliases": []},
|
||||
{"id": "REGEN", "label": "재생성", "aliases": ["RE"]},
|
||||
{"id": "PAN", "label": "초점이동", "aliases": ["P"]},
|
||||
{"id": "QSAVE", "label": "저장", "aliases": ["SAVE"]},
|
||||
{"id": "RECOVER", "label": "백업 복구", "aliases": []},
|
||||
{"id": "EXPORT", "label": "JSON 내보내기", "aliases": ["EXP"]},
|
||||
{"id": "EXPORTDXF", "label": "DXF 내보내기", "aliases": []},
|
||||
{"id": "EXPORTDWG", "label": "DWG 내보내기", "aliases": []},
|
||||
{"id": "EXPORTSVG", "label": "SVG 내보내기", "aliases": []},
|
||||
{"id": "EXPORTPNG", "label": "PNG 내보내기", "aliases": []},
|
||||
{"id": "TOAREA", "label": "면적산출로 보내기", "aliases": []},
|
||||
{"id": "UNDO", "label": "실행 취소", "aliases": ["U"]},
|
||||
{"id": "REDO", "label": "다시 실행", "aliases": []}
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* CAD 명령 목록 거울(PLAN 332-2) — `commands/command-catalog.json` 이 registry(`commands.*.ts`)와 같은지.
|
||||
* ☰ [설정] CAD 단축키 탭은 xstate 가 딸려 오지 않게 JSON 만 읽음 — 명령을 더하거나 별칭을 바꾸면 JSON 도 같이.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad command-catalog
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import catalog from "../../../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
|
||||
import { getAllCommands } from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
|
||||
|
||||
describe("command-catalog.json", () => {
|
||||
it("registry 와 같은 명령 · 한글 이름 · 기본 별칭 · 차례", () => {
|
||||
const live = getAllCommands().map((c) => ({
|
||||
id: c.id,
|
||||
label: c.label,
|
||||
aliases: c.aliases ?? [],
|
||||
}));
|
||||
expect(catalog).toEqual(live);
|
||||
});
|
||||
|
||||
it("기본 별칭 = 영문 · 숫자 · 안 겹침 · 명령 이름과 다름", () => {
|
||||
const ids = new Set(catalog.map((c) => c.id));
|
||||
const aliases = catalog.flatMap((c) => c.aliases);
|
||||
expect(aliases.every((a) => /^[A-Z0-9]+$/.test(a))).toBe(true);
|
||||
expect(new Set(aliases).size).toBe(aliases.length);
|
||||
expect(aliases.filter((a) => ids.has(a))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/* PLAN 332-2 단위검증 헬퍼 — ☰ [설정] CAD 별칭 검사(`A00_Common/app_settings_cad.ts`)를 Node 로 돌림.
|
||||
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_app_settings.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const file = path.join(ROOT, "A00_Common", "app_settings_cad.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const req = (p) =>
|
||||
p.endsWith(".json")
|
||||
? require(path.join(ROOT, "A00_Common", p))
|
||||
: p === "@ui/ui_template_locale"
|
||||
? { t: (k) => k }
|
||||
: {};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
const m = box.exports;
|
||||
|
||||
const check = (aliases) => m.checkCadAliases(aliases);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
commands: m.CAD_COMMANDS.length,
|
||||
parse: [m.parseAliases(" ll, Cc x1 "), m.parseAliases(""), m.parseAliases(",, ")],
|
||||
ok: check({ LINE: ["LL"], CIRCLE: ["CC", "C2"], COPY: ["C"] }),
|
||||
chars: check({ LINE: ["L!"], CIRCLE: ["원"] }),
|
||||
dup: check({ LINE: ["LL"], CIRCLE: ["LL"], ARC: ["AA", "AA"] }),
|
||||
name: check({ CIRCLE: ["LINE"] }),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,187 @@
|
||||
"""PLAN 332-2 — ☰ [설정] 창(`A00_Common/app_settings.ts` · `app_settings_cad.ts` · `app_shell.ts`).
|
||||
|
||||
① CAD 별칭 검사(Node) — 영문 · 숫자만 · 겹침 금지 · 명령 이름 금지 · 칸 글 나눔
|
||||
② 실제 브라우저(sub2 화면 서버 5177 · 로그인 없음 = 이 브라우저만 · 새 창이라 남는 것 없음)
|
||||
☰ → [설정] = 서랍 닫히고 창 · 탭 셋 전환 · 고쳐도 [저장] 전엔 안 씀 · 닫을 때 물음(취소 = 머묾 ·
|
||||
버림 = 안 씀 · 저장 = 한 번) · [저장] 한 번 = 한 번 씀 · 저장 뒤 읽기 층 방향 뒤집힘 · 잘못 별칭은 안 씀
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
BASE = "http://localhost:5177" # sub2 화면 서버(OWNERS 자기 줄)
|
||||
KEY = "aislo:pref:input-prefs"
|
||||
|
||||
# 앱 스크립트보다 먼저 — 설정 키에 쓰는 횟수를 셈
|
||||
COUNT_WRITES = f"""
|
||||
window.__writes = [];
|
||||
const set = Storage.prototype.setItem;
|
||||
Storage.prototype.setItem = function (k, v) {{
|
||||
if (k === {json.dumps(KEY)}) window.__writes.push(v);
|
||||
return set.call(this, k, v);
|
||||
}};
|
||||
"""
|
||||
# 앱이 불러온 그 읽기 층 사본(vite 가 ?t= 를 붙였을 수 있음)
|
||||
WHEEL_IN = (
|
||||
"async (d) => (await import(performance.getEntriesByType('resource')"
|
||||
".map((e) => e.name).find((n) => n.includes('/input_prefs.ts')))).wheelIn(d)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def node() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_app_settings.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_별칭_검사(node):
|
||||
assert node["commands"] == 153
|
||||
assert node["parse"] == [["LL", "CC", "X1"], [], []]
|
||||
assert node["ok"] == {} # 남의 기본 별칭(C = CIRCLE)을 내 것으로 덮는 건 됨(acad.pgp 식)
|
||||
assert node["chars"] == {"LINE": "chars", "CIRCLE": "chars"}
|
||||
assert node["dup"] == {"LINE": "dup", "CIRCLE": "dup", "ARC": "dup"}
|
||||
assert node["name"] == {"CIRCLE": "name"}
|
||||
|
||||
|
||||
def _up() -> bool:
|
||||
try:
|
||||
urllib.request.urlopen(BASE, timeout=3)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _open_settings(page):
|
||||
page.click(".app-burger")
|
||||
page.click(".app-drawer__settings")
|
||||
page.wait_for_selector(".app-settings")
|
||||
assert not page.evaluate(
|
||||
"document.querySelector('.app-header, header')?.classList.contains('is-drawer-open')"
|
||||
)
|
||||
|
||||
|
||||
def _writes(page) -> list:
|
||||
return page.evaluate("window.__writes")
|
||||
|
||||
|
||||
def _visible_panel_has(page, selector: str) -> bool:
|
||||
return page.evaluate(
|
||||
"(s) => [...document.querySelectorAll('.app-settings .ui-modal__body > div:not([role]), "
|
||||
".app-settings .ui-modal__body > table')].some((p) => !p.hidden && p.querySelector(s))",
|
||||
selector,
|
||||
)
|
||||
|
||||
|
||||
def _save(page):
|
||||
page.click(".app-settings .ui-modal__actions button")
|
||||
|
||||
|
||||
def _close(page):
|
||||
page.click(".app-settings .ui-modal__top > button")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐")
|
||||
def test_설정_창_탭_저장_물음():
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel="msedge", headless=True)
|
||||
ctx = browser.new_context(
|
||||
viewport={"width": 390, "height": 844}, has_touch=True, is_mobile=True
|
||||
)
|
||||
ctx.add_init_script(COUNT_WRITES)
|
||||
page = ctx.new_page()
|
||||
page.goto(f"{BASE}/#/a01-home")
|
||||
page.wait_for_selector(".app-burger")
|
||||
assert page.evaluate(WHEEL_IN, 100) is True # 기본 = 당김
|
||||
|
||||
# ☰ → [설정] · 탭 셋 · 첫 탭 = 마우스
|
||||
_open_settings(page)
|
||||
tabs = page.evaluate(
|
||||
"[...document.querySelectorAll('.app-settings [role=tab]')]"
|
||||
".map((b) => [b.dataset.settingsTab, b.getAttribute('aria-selected')])"
|
||||
)
|
||||
assert tabs == [["mouse", "true"], ["cad", "false"], ["keys", "false"]]
|
||||
assert _visible_panel_has(page, "input[type=radio]")
|
||||
page.click("[data-settings-tab=cad]")
|
||||
assert _visible_panel_has(page, "input[data-cad-command]")
|
||||
assert not _visible_panel_has(page, "input[type=radio]")
|
||||
assert page.locator(".app-settings input[data-cad-command]").count() == 153
|
||||
page.click("[data-settings-tab=keys]")
|
||||
assert (
|
||||
page.evaluate(
|
||||
"document.querySelector('[data-settings-tab=keys]').getAttribute('aria-selected')"
|
||||
)
|
||||
== "true"
|
||||
)
|
||||
page.click("[data-settings-tab=mouse]")
|
||||
|
||||
# 밀기 고름 — [저장] 전엔 안 씀 · 닫으면 물음 · 취소 = 머묾
|
||||
page.check("input[name=app-settings-wheel][value=push]")
|
||||
assert _writes(page) == []
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_selector(".ui-confirm")
|
||||
page.click(".ui-confirm button >> nth=0") # 취소
|
||||
assert page.locator(".app-settings").count() == 1 and _writes(page) == []
|
||||
|
||||
# [저장] = 한 번 씀 · 읽기 층 방향 뒤집힘 · 닫을 때 안 물음
|
||||
_save(page)
|
||||
assert len(_writes(page)) == 1
|
||||
assert json.loads(_writes(page)[0]) == {"wheelZoom": "push", "cadAliases": {}}
|
||||
assert page.evaluate(WHEEL_IN, 100) is False and page.evaluate(WHEEL_IN, -100) is True
|
||||
_save(page) # 고친 것 없음 — 또 안 씀
|
||||
assert len(_writes(page)) == 1
|
||||
_close(page)
|
||||
assert page.locator(".app-settings").count() == 0
|
||||
assert page.locator(".ui-confirm").count() == 0
|
||||
|
||||
# CAD 별칭 — 잘못이면 안 씀 · 칸 표시
|
||||
_open_settings(page)
|
||||
assert page.is_checked("input[name=app-settings-wheel][value=push]") # 저장값으로 엶
|
||||
page.click("[data-settings-tab=cad]")
|
||||
line = "input[data-cad-command=LINE]"
|
||||
circle = "input[data-cad-command=CIRCLE]"
|
||||
for line_v, circle_v in (("L!", ""), ("LL", "ll"), ("", "LINE")):
|
||||
page.fill(line, line_v)
|
||||
page.fill(circle, circle_v)
|
||||
_save(page)
|
||||
assert page.inner_text(".app-settings__error").strip(), (line_v, circle_v)
|
||||
assert page.locator(".app-settings input[aria-invalid=true]").count() >= 1
|
||||
assert len(_writes(page)) == 1
|
||||
page.fill(line, "ll")
|
||||
page.fill(circle, "")
|
||||
_save(page)
|
||||
assert page.inner_text(".app-settings__error").strip() == ""
|
||||
assert len(_writes(page)) == 2
|
||||
assert json.loads(_writes(page)[1]) == {"wheelZoom": "push", "cadAliases": {"LINE": ["LL"]}}
|
||||
|
||||
# 닫을 때 물음 — 버림 = 안 씀 · 저장 = 한 번 더
|
||||
page.click("[data-settings-tab=mouse]")
|
||||
page.check("input[name=app-settings-wheel][value=pull]")
|
||||
_close(page)
|
||||
page.click(".ui-confirm button >> nth=1") # 버리고 넘어감
|
||||
assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 2
|
||||
assert page.evaluate(WHEEL_IN, 100) is False
|
||||
_open_settings(page)
|
||||
page.check("input[name=app-settings-wheel][value=pull]")
|
||||
_close(page)
|
||||
page.click(".ui-confirm button >> nth=2") # 저장 후 넘어감
|
||||
assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 3
|
||||
assert json.loads(_writes(page)[2])["wheelZoom"] == "pull"
|
||||
assert page.evaluate(WHEEL_IN, 100) is True
|
||||
browser.close()
|
||||
@@ -12,14 +12,16 @@ sub1 서버(5174)에서 모든 페이지를 네 크기로 열어 문제 수를
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = "http://localhost:5174"
|
||||
PROJECT_NAME = "검증_sub1"
|
||||
# 다른 창 서버로 돌릴 때 — AISLO_UI_BASE · AISLO_UI_PROJECT(OWNERS 자기 줄)
|
||||
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174")
|
||||
PROJECT_NAME = os.environ.get("AISLO_UI_PROJECT", "검증_sub1")
|
||||
PAGES = [
|
||||
"a01-home",
|
||||
"a02-prog-detail",
|
||||
|
||||
@@ -8,11 +8,12 @@ sub1 화면 서버(5174) 첫 화면을 터치 기기로 열고 시험용 상자
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
BASE = "http://localhost:5174"
|
||||
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174") # 다른 창 서버 = 환경값
|
||||
|
||||
|
||||
def _up() -> bool:
|
||||
|
||||
@@ -109,6 +109,74 @@ export const ui_locales_common = {
|
||||
"그림 위를 길게 누르면 메뉴 · 두 손가락으로 확대",
|
||||
"Long-press the drawing for the menu · pinch to zoom",
|
||||
],
|
||||
/* ☰ [설정] 창(332) — 마우스 · CAD 단축키 · 다른 곳 단축키 */
|
||||
App_Settings: ["설정", "Settings"],
|
||||
App_Settings_Tab_Mouse: ["마우스", "Mouse"],
|
||||
App_Settings_Tab_Cad: ["CAD 단축키", "CAD shortcuts"],
|
||||
App_Settings_Tab_Keys: ["다른 곳 단축키", "Other shortcuts"],
|
||||
App_Settings_Wheel: ["휠 확대 방향(모든 화면)", "Wheel zoom-in direction (all views)"],
|
||||
App_Settings_Wheel_Pull: ["당기면 확대(휠을 몸 쪽으로)", "Pull to zoom in (wheel toward you)"],
|
||||
App_Settings_Wheel_Push: ["밀면 확대(휠을 앞으로)", "Push to zoom in (wheel away)"],
|
||||
App_Settings_Rules: ["마우스 단추", "Mouse buttons"],
|
||||
App_Settings_Rule_Pan: ["화면 옮기기 = 가운데 단추 끌기", "Pan = drag with the middle button"],
|
||||
App_Settings_Rule_Pick: ["고르기 = 왼쪽 단추", "Select = left button"],
|
||||
App_Settings_Rule_Right: [
|
||||
"오른쪽 단추 = 메뉴 · CAD 에서는 Enter",
|
||||
"Right button = menu · Enter in CAD",
|
||||
],
|
||||
App_Settings_Rule_Pinch: [
|
||||
"트랙패드 · 손가락 벌림 = 늘 확대(휠 방향과 상관없음)",
|
||||
"Trackpad · finger spread = always zoom in (whatever the wheel direction)",
|
||||
],
|
||||
App_Settings_Cad_Help: [
|
||||
"CAD 는 명령 이름 > 내 별칭 > 기본 별칭 차례로 찾음 · 영문 · 숫자만 · 여러 개는 쉼표로",
|
||||
"CAD looks up command name > my alias > default alias · letters and digits only · comma for several",
|
||||
],
|
||||
App_Settings_Cad_Command: ["명령", "Command"],
|
||||
App_Settings_Cad_Label: ["이름", "Name"],
|
||||
App_Settings_Cad_Default: ["기본 별칭", "Default alias"],
|
||||
App_Settings_Cad_Mine: ["내 별칭", "My alias"],
|
||||
App_Settings_Cad_ErrChars: ["영문 · 숫자만 씀", "Letters and digits only"],
|
||||
App_Settings_Cad_ErrDup: ["같은 별칭이 두 번 있음", "Same alias used twice"],
|
||||
App_Settings_Cad_ErrName: ["명령 이름과 같은 별칭은 못 씀", "Alias cannot be a command name"],
|
||||
App_Settings_Cad_Invalid: ["별칭에 잘못이 있어 저장 안 함", "Not saved — fix the aliases"],
|
||||
App_Settings_Saved: ["설정 저장함", "Settings saved"],
|
||||
App_Settings_Leave: ["저장 안 한 설정이 있음", "You have unsaved settings"],
|
||||
App_Settings_Keys_All: ["모든 화면", "All views"],
|
||||
App_Settings_Keys_All_Keys: [
|
||||
"Esc = ☰ · 창 닫기 · 고름 풀기 · Z 누른 채 또는 오른쪽 0.4초 누름 = 돋보기(휠 = 배율)",
|
||||
"Esc = ☰ · close window · clear selection · hold Z or right button 0.4 s = loupe (wheel = zoom)",
|
||||
],
|
||||
App_Settings_Keys_Profile: ["B05 종단", "B05 profile"],
|
||||
App_Settings_Keys_Profile_Keys: [
|
||||
"Ctrl+Z 되돌림 · Ctrl+Shift+Z 다시 · 방향키 = 옮김 · 자유지정 Enter = 끝 · Esc · Backspace",
|
||||
"Ctrl+Z undo · Ctrl+Shift+Z redo · arrows = move · free mode Enter = finish · Esc · Backspace",
|
||||
],
|
||||
App_Settings_Keys_RouteEdit: ["B05 노선 편집", "B05 route edit"],
|
||||
App_Settings_Keys_RouteEdit_Keys: [
|
||||
"Ctrl+Z 되돌림 · Ctrl+Y · Ctrl+Shift+Z 다시",
|
||||
"Ctrl+Z undo · Ctrl+Y · Ctrl+Shift+Z redo",
|
||||
],
|
||||
App_Settings_Keys_Section: ["B06 횡단", "B06 cross section"],
|
||||
App_Settings_Keys_Section_Keys: [
|
||||
"방향키 · ▲▼ = 측점 옮김 · Enter · Space = 고름 · Esc",
|
||||
"Arrows · ▲▼ = move station · Enter · Space = pick · Esc",
|
||||
],
|
||||
App_Settings_Keys_Cad: ["CAD", "CAD"],
|
||||
App_Settings_Keys_Cad_Keys: [
|
||||
"F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · 방향키 = 화면 옮김 · 글자 = 명령",
|
||||
"F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · arrows = pan · letters = command",
|
||||
],
|
||||
App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"],
|
||||
App_Settings_Keys_Table_Keys: [
|
||||
"Enter · F2 = 입력 · 방향키 = 칸 옮김 · Esc = 그만",
|
||||
"Enter · F2 = edit · arrows = move cell · Esc = cancel",
|
||||
],
|
||||
App_Settings_Keys_M02: ["M02 양식", "M02 templates"],
|
||||
App_Settings_Keys_M02_Keys: [
|
||||
"Ctrl+S 저장 · Ctrl+C · Ctrl+V · Delete · Esc · Space+끌기 = 화면 옮김 · Ctrl+휠 = 배율 · 시트 = 엑셀 키",
|
||||
"Ctrl+S save · Ctrl+C · Ctrl+V · Delete · Esc · Space+drag = pan · Ctrl+wheel = zoom · sheet = Excel keys",
|
||||
],
|
||||
App_Footer_Copyright: [
|
||||
"© 2026 임도설계 및 견적 자동화. All rights reserved.",
|
||||
"© 2026 Forest Road Design & Estimation. All rights reserved.",
|
||||
|
||||
Reference in New Issue
Block a user