diff --git a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts index 6248f2fd6..162b01c2c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts +++ b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts @@ -233,6 +233,14 @@ export function updatePlanKind( }); } +/** 종류 차례 — 끌어 놓은 차례 그대로(140-2 · 놓는 즉시 · 번호 바뀐 장은 서버가 확정 풂). */ +export function reorderPlanKinds(projectId: string, ids: string[]): Promise { + return requestJson(`/projects/${projectId}/plan-kinds-order`, { + method: "PUT", + body: JSON.stringify({ ids }), + }); +} + export function deletePlanKind(projectId: string, kindId: string): Promise { return requestJson(`/projects/${projectId}/plan-kinds/${encodeURIComponent(kindId)}`, { method: "DELETE", diff --git a/B07_DesignDetail/B07_DesignDetail_Router_PlanKinds.py b/B07_DesignDetail/B07_DesignDetail_Router_PlanKinds.py index a12340f21..6e0b7078c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_PlanKinds.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_PlanKinds.py @@ -23,6 +23,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import ( delete_kind, list_kinds, read_marks, + reorder_kinds, update_kind, write_marks, ) @@ -45,6 +46,10 @@ class PlanKindRequest(BaseModel): scale: int | None = None +class PlanKindsOrderRequest(BaseModel): + ids: list[str] = Field(default_factory=list) + + class PlanMark(BaseModel): chainage_m: float = Field(ge=0) refuge: bool = False @@ -102,6 +107,17 @@ async def post_plan_kind(project_id: UUID, request: PlanKindRequest) -> Any: return _error(400, exc) +@router.put("/{project_id}/plan-kinds-order") +async def put_plan_kinds_order(project_id: UUID, request: PlanKindsOrderRequest) -> Any: + """종류 차례 — 모달에서 줄을 끌어 놓은 차례 그대로(140-2 · 놓는 즉시).""" + try: + root = await _project_root(project_id) + await asyncio.to_thread(reorder_kinds, root, request.ids) + return await asyncio.to_thread(_state, root) + except ValueError as exc: + return _error(400, exc) + + @router.put("/{project_id}/plan-kinds/{kind_id}") async def put_plan_kind(project_id: UUID, kind_id: str, request: PlanKindRequest) -> Any: """종류 업데이트 — 이름 · 조각을 고친다(기본 넷도).""" diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support_Plan.py b/B07_DesignDetail/B07_DesignDetail_Router_Support_Plan.py index 97410d0a3..978a18774 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support_Plan.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support_Plan.py @@ -261,16 +261,39 @@ def update_kind( "scale": chosen_scale, } atomic_write_json(path, _document(kind, order)) + _release_kinds(project_root, {kind_id}) + return kind + + +def _release_kinds(project_root: Path, kind_ids: set[str]) -> None: + """그 종류들의 확정 평면도 장을 푼다 — 다시 그려야 하는 장(조각 · 축척 · 번호가 바뀜).""" manifest = _read_manifest(project_root) released = False for drawing_id, entry in manifest["drawings"].items(): match = PLAN_KIND_ID.fullmatch(drawing_id) - if match and match.group(1) == kind_id and entry.get("confirmed"): + if match and match.group(1) in kind_ids and entry.get("confirmed"): entry["confirmed"] = False released = True if released: _write_manifest(project_root, manifest) - return kind + + +def reorder_kinds(project_root: Path, ids: list[str]) -> None: + """종류 차례를 ids 대로 다시 쓴다(140-2). 앞선 종류 묶음이 바뀐 종류 = 장 번호가 바뀜 → 확정 풂.""" + before = [kind["id"] for kind in _materialize(project_root)] + if sorted(ids) != sorted(before): + raise ValueError("종류 목록이 바뀌었습니다. 모달을 다시 열어 주세요.") + folder = _kinds_dir(project_root) + for order, kind_id in enumerate(ids): + path = folder / f"{kind_id}.json" + atomic_write_json(path, {**_read_json(path), "order": order}) + moved = { + kind_id + for index, kind_id in enumerate(ids) + if set(ids[:index]) != set(before[: before.index(kind_id)]) + } + if moved: + _release_kinds(project_root, moved) def delete_kind(project_root: Path, kind_id: str) -> None: diff --git a/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts b/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts index 55634f479..1b3145360 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts @@ -13,6 +13,7 @@ import { createSelectField, showToast } from "@ui/ui_template_elements"; import { stationFields } from "../B05_Profile/B05_Profile_UI_Structures_Fields"; import { addPlanKind, + deletePlanKind, savePlanMarks, updatePlanKind, type DesignDrawingItem, @@ -60,8 +61,20 @@ const LAYOUTS: { value: PlanKindLayout; text: string }[] = [ { value: "watershed", text: "유역도" }, ]; -/** 종류 — 공통 항목. 종류를 고르면 든 조각이 체크로 보이고 [업데이트] · 「새 종류」면 양식 고르고 [추가]. */ -export function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement { +/** 종류 폼 — 밖(장 단추)에서 고른 종류로 폼을 맞추는 `pick` 과 함께. */ +export interface KindForm { + root: HTMLElement; + pick: (kindId: string) => void; +} + +/** 종류 — 맨 위 [종류 ▼] 아래 상세. 종류를 고르면 든 조각이 체크로 보이고 [업데이트] · [지우기] · + * 「새 종류」면 양식 고르고 [추가]. onPick = 드롭다운으로 고른 종류(미리보기 따라감 · 140-2). */ +export function kindForm( + state: PlanKindsState, + controls: PlanKindsControls, + selected = "", + onPick?: (kindId: string) => void, +): KindForm { const form = el("div", "b07-plan-tools__block"); form.append(el("strong", "", "종류")); const pick = createSelectField({ @@ -71,7 +84,10 @@ export function kindForm(state: PlanKindsState, controls: PlanKindsControls): HT ], compact: true, ariaLabel: "종류", - onChange: () => show(), + onChange: () => { + show(); + if (pick.select.value) onPick?.(pick.select.value); + }, }); const layout = createSelectField({ options: LAYOUTS, @@ -91,6 +107,8 @@ export function kindForm(state: PlanKindsState, controls: PlanKindsControls): HT pieceList.append(...boxes.map((box) => box.root)); const action = el("button", "b07-plan-tools__button"); action.type = "button"; + const remove = el("button", "b07-plan-tools__button", "지우기"); + remove.type = "button"; // 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다. const showPieces = (): void => { const watershed = layout.select.value === "watershed"; @@ -118,9 +136,18 @@ export function kindForm(state: PlanKindsState, controls: PlanKindsControls): HT layout.select.value = kind?.layout ?? "plan"; layout.select.disabled = Boolean(kind); action.textContent = kind ? "업데이트" : "추가"; + remove.hidden = !kind; showPieces(); }; + if (state.kinds.some((kind) => kind.id === selected)) pick.select.value = selected; show(); + remove.addEventListener("click", () => { + const kind = state.kinds.find((item) => item.id === pick.select.value); + if (!kind) return; + if (!window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`)) + return; + void run(() => deletePlanKind(controls.projectId, kind.id), controls); + }); action.addEventListener("click", () => { const pieces = boxes.filter((box) => box.input.checked).map((box) => box.input.value); const kindId = pick.select.value; @@ -135,8 +162,16 @@ export function kindForm(state: PlanKindsState, controls: PlanKindsControls): HT controls, ); }); - form.append(pick.root, layout.root, scale.root, name, pieceList, action); - return form; + const actions = el("div", "b07-plan-tools__actions"); + actions.append(action, remove); + form.append(pick.root, layout.root, scale.root, name, pieceList, actions); + return { + root: form, + pick: (kindId) => { + pick.select.value = kindId; + show(); + }, + }; } export function marksForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement { diff --git a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts index 7cff7b98c..af52ce824 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts @@ -2,7 +2,8 @@ * B07_DesignDetail_UI_PlanManage.ts * [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음. * - * 왼쪽 = 종류 목록(× 지우기) · 종류 추가 · 업데이트 폼 · 대피소 · 사토장 자리 · 평면도 장 목록 + * 왼쪽 = [종류 ▼] 상세 폼(추가 · 업데이트 · 지우기) · 평면도 장(종류마다 한 줄 [≡] 이름 [1][2] · + * 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 저장 140-2) · 대피소 · 사토장 자리 * 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 캔버스만 `bare`) · 확정 배지 · [CAD 에서 열기] * 고치기는 지금 서버 길 그대로(종류 파일) — 고칠 때마다 모달 안을 다시 받고, * 닫을 때 `onClosed(changed)` 로 좌측 목록 · 도면을 다시 읽음. @@ -12,10 +13,10 @@ import { showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createCadHost } from "@ui/cad_host/cad_host"; import { - deletePlanKind, fetchDesignDrawing, fetchDesignDrawingList, fetchPlanKinds, + reorderPlanKinds, type CadDrawing, type DesignDrawingItem, type PlanKindsState, @@ -25,6 +26,7 @@ import { kindForm, marksForm, run, + type KindForm, type PlanKindsControls, } from "./B07_DesignDetail_UI_PlanKinds"; @@ -58,6 +60,10 @@ export function openPlanManager(controls: PlanManageControls): void { let closed = false; /** 미리보기 중인 장 — 늦게 온 응답이 다른 장을 덮지 않게 이것과 맞춰 봄. */ let picked: string | undefined; + /** 드롭다운 · 줄 강조의 종류 — 장 단추를 누르면 그 장의 종류로 따라감. */ + let pickedKind = ""; + let form: KindForm | undefined; + let sheets: DesignDrawingItem[] = []; const preview = createCadHost({ title: "평면도 미리보기" }); const side = el("div", "b07-plan-manage__side"); const head = el("div", "b07-plan-manage__head"); @@ -73,46 +79,88 @@ export function openPlanManager(controls: PlanManageControls): void { }, }; - const kindList = (state: PlanKindsState, sheets: DesignDrawingItem[]): HTMLElement => { - const list = el("div", "b07-plan-tools__block b07-plan-manage__kinds"); - list.append(el("strong", "", "종류 목록")); + const kindSheets = (kindId: string) => + groupPlanSheets(sheets.filter((item) => item.plan_kind === kindId))[0]?.sheets ?? []; + + /** 장 단추 → 미리보기 + 드롭다운이 그 종류로. */ + const pickSheet = (drawing: DesignDrawingItem, label: string): void => { + pickedKind = drawing.plan_kind ?? ""; + form?.pick(pickedKind); + void show(drawing, label); + }; + + /** 「평면도 장」 한 블록 — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서(놓는 즉시). */ + const sheetBlock = (state: PlanKindsState): HTMLElement => { + const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets"); + list.append(el("strong", "", "평면도 장")); + const ids = state.kinds.map((kind) => kind.id); + let dragId: string | null = null; for (const kind of state.kinds) { - const row = el("div", "b07-plan-kind__head"); + const row = el("div", "b07-plan-kind__head b07-plan-manage__row"); row.dataset.planKind = kind.id; - const count = sheets.filter((item) => item.plan_kind === kind.id).length; - row.append(el("span", "b07-plan-kind__name", `${kind.name} · ${count}장`)); - const remove = button("b07-plan-tools__remove", "×", () => { - if ( - !window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`) - ) - return; - void run(() => deletePlanKind(projectId, kind.id), edits); + row.draggable = true; + const grip = el("span", "b07-plan-manage__grip", "≡"); + grip.title = "끌어 놓아 종류 순서 바꾸기"; + const group = kindSheets(kind.id); + // 한 장이면 이름이 곧 장 단추 · 여러 장이면 이름 + 작은 장 단추 + const name = + group.length === 1 + ? button("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () => + pickSheet(group[0].drawing, group[0].label), + ) + : el("span", "b07-plan-kind__name", kind.name); + if (group.length === 1) { + name.dataset.drawingId = group[0].drawing.id; + name.dataset.confirmed = String(group[0].drawing.confirmed); + } + row.append(grip, name); + if (group.length > 1) + group.forEach(({ drawing, label }, index) => { + const item = button("b07-plan-manage__sheet", String(index + 1), () => + pickSheet(drawing, label), + ); + item.title = label; + item.dataset.drawingId = drawing.id; + item.dataset.confirmed = String(drawing.confirmed); + row.append(item); + }); + // 끌기 꼴 = 공용 탭 줄(`cad_host_tabs.ts`) — 세로라 위 · 아래 반으로 앞 · 뒤 + row.addEventListener("dragstart", (event) => { + dragId = kind.id; + event.dataTransfer?.setData("text/plain", kind.id); + if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; + }); + row.addEventListener("dragend", () => (dragId = null)); + row.addEventListener("dragover", (event) => { + if (!dragId) return; + event.preventDefault(); + const box = row.getBoundingClientRect(); + row.dataset.drop = event.clientY > box.top + box.height / 2 ? "after" : "before"; + }); + row.addEventListener("dragleave", () => delete row.dataset.drop); + row.addEventListener("drop", (event) => { + event.preventDefault(); + const after = row.dataset.drop === "after"; + delete row.dataset.drop; + if (!dragId || dragId === kind.id) return; + const order = ids.filter((id) => id !== dragId); + order.splice(order.indexOf(kind.id) + (after ? 1 : 0), 0, dragId); + dragId = null; + if (order.join() !== ids.join()) void run(() => reorderPlanKinds(projectId, order), edits); }); - remove.title = `「${kind.name}」 종류 지우기`; - row.append(remove); list.append(row); } return list; }; - const sheetList = (sheets: DesignDrawingItem[]): HTMLElement => { - const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets"); - list.append(el("strong", "", "평면도 장")); - for (const group of groupPlanSheets(sheets)) { - for (const { drawing, label } of group.sheets) { - const item = button("b07-drawing-button", label, () => void show(drawing, label)); - item.dataset.drawingId = drawing.id; - item.dataset.confirmed = String(drawing.confirmed); - list.append(item); - } - } - return list; - }; - - const markActive = (): void => + const markActive = (): void => { side - .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") + .querySelectorAll(".b07-plan-manage__sheet") .forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked))); + side + .querySelectorAll(".b07-plan-manage__row") + .forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind))); + }; /** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */ const show = async (drawing: DesignDrawingItem, label: string): Promise => { @@ -156,15 +204,18 @@ export function openPlanManager(controls: PlanManageControls): void { fetchDesignDrawingList(projectId), ]); if (closed) return; - const sheets = list.drawings.filter((item) => item.kind === "plan"); - side.replaceChildren( - kindList(state, sheets), - kindForm(state, edits), - marksForm(state, edits), - sheetList(sheets), - ); + sheets = list.drawings.filter((item) => item.kind === "plan"); const all = groupPlanSheets(sheets).flatMap((group) => group.sheets); const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0]; + if (keep) pickedKind = keep.drawing.plan_kind ?? ""; + // 드롭다운으로 고른 종류 → 그 종류 첫 장 미리보기 + 줄 강조 + form = kindForm(state, edits, pickedKind, (kindId) => { + pickedKind = kindId; + const first = kindSheets(kindId)[0]; + if (first) void show(first.drawing, first.label); + else markActive(); + }); + side.replaceChildren(form.root, sheetBlock(state), marksForm(state, edits)); if (keep) void show(keep.drawing, keep.label); else { picked = undefined; diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Style.css b/B07_DesignDetail/B07_DesignDetail_UI_Style.css index b0c45377c..f691eabfd 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Style.css +++ b/B07_DesignDetail/B07_DesignDetail_UI_Style.css @@ -310,6 +310,56 @@ color: var(--color-success); } +/* 「평면도 장」 한 블록(140-2) — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서 */ +.b07-plan-manage__row { + padding: 2px var(--spacing-4); + border: 1px solid transparent; + border-radius: var(--radius-buttons); +} + +.b07-plan-manage__row[data-active="true"] { + border-color: var(--color-border); + background: var(--color-mist-violet); +} + +.b07-plan-manage__row[data-drop="before"] { + box-shadow: inset 0 2px 0 var(--color-primary); +} + +.b07-plan-manage__row[data-drop="after"] { + box-shadow: inset 0 -2px 0 var(--color-primary); +} + +.b07-plan-manage__grip { + color: var(--color-text-muted); + cursor: grab; +} + +.b07-plan-manage__sheet { + min-width: 26px; + padding: 2px var(--spacing-4); + border: 1px solid var(--color-border); + border-left: 3px solid transparent; + border-radius: var(--radius-buttons); + background: var(--color-surface); + color: var(--color-text); + font-size: var(--text-caption); + cursor: pointer; +} + +.b07-plan-manage__sheet.b07-plan-kind__name { + text-align: left; +} + +.b07-plan-manage__sheet[data-confirmed="true"] { + border-left-color: var(--color-success); +} + +.b07-plan-manage__sheet[data-active="true"] { + color: var(--color-primary); + border-color: var(--color-primary); +} + .b07-plan-tools__block { display: flex; flex-direction: column; diff --git a/resources/tester/helper_130_b07_plan_manage.cjs b/resources/tester/helper_130_b07_plan_manage.cjs index dddea378f..612ce5081 100644 --- a/resources/tester/helper_130_b07_plan_manage.cjs +++ b/resources/tester/helper_130_b07_plan_manage.cjs @@ -1,4 +1,5 @@ -/* 130-1 [평면도 관리] 모달 헬퍼 — 좌측 「평면도」 묶음 · 모달(종류 추가 · 업데이트 · 지우기 · 장 미리보기)을 +/* 130-1 · 140-2 [평면도 관리] 모달 헬퍼 — 좌측 「평면도」 묶음 · 모달(종류 추가 · 업데이트 · 지우기 · 장 미리보기 · + * 드롭다운 따라가기 · 줄 끌어 순서)을 * Node 로 돌려 표준출력 JSON 한 벌로. TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 · * 서버 길(`_Api_Fetch`)은 메모리 가짜 · 공용 모달 · CAD · 드롭다운 · 체크박스는 기록용 가짜. */ @@ -52,6 +53,12 @@ class FakeNode { click() { (this.listeners.click || []).forEach((handler) => handler({})); } + fire(type, event = {}) { + (this.listeners[type] || []).forEach((handler) => handler({ preventDefault() {}, ...event })); + } + getBoundingClientRect() { + return { top: 0, height: 20, left: 0, width: 100 }; + } /** `.a` · `.a .b`(자손) 만. */ querySelectorAll(selector) { const parts = selector @@ -204,6 +211,12 @@ const API = { return copy(); }, savePlanMarks: async () => copy(), + reorderPlanKinds: async (_, ids) => { + calls.push(["reorder", ids]); + state.kinds = ids.map((id) => state.kinds.find((k) => k.id === id)); + confirmed.delete("plan_k_route"); // 번호 바뀐 장 확정 풂(서버 몫 흉내) + return copy(); + }, invalidateDesignDrawing: async (_, id) => { calls.push(["invalidate", id]); confirmed.delete(id); @@ -258,14 +271,10 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan await settle(); const modal = modals[0]; const cad = cads[0]; - const kindRows = () => - modal.body - .querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head") - .map((row) => row.dataset.planKind); + const rows = () => modal.body.querySelectorAll(".b07-plan-manage__sheets .b07-plan-manage__row"); + const kindRows = () => rows().map((row) => row.dataset.planKind); const sheetIds = () => - modal.body - .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") - .map((b) => b.dataset.drawingId); + modal.body.querySelectorAll(".b07-plan-manage__sheet").map((b) => b.dataset.drawingId); const kindPick = () => selects.filter((s) => s.label === "종류").at(-1); const action = () => all(modal.body).find( @@ -302,16 +311,17 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan await settle(); out.update = { call: calls.find((c) => c[0] === "update"), - rowText: modal.body - .querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head") - .find((row) => row.dataset.planKind === "k_new").textContent, + rowText: rows().find((row) => row.dataset.planKind === "k_new").textContent, }; - // 지우기 — 그 줄의 × - const row = modal.body - .querySelectorAll(".b07-plan-manage__kinds .b07-plan-kind__head") - .find((r) => r.dataset.planKind === "k_new"); - byText(row, "BUTTON", "×")[0].click(); + // 지우기 — 드롭다운에서 그 종류를 고르고 폼 안 [지우기] + kindPick().change("k_new"); + out.removeHiddenForNew = (() => { + kindPick().change(""); + return byText(modal.body, "BUTTON", "지우기")[0].hidden; + })(); + kindPick().change("k_new"); + byText(modal.body, "BUTTON", "지우기")[0].click(); await settle(); await settle(); out.remove = { @@ -323,7 +333,7 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan // 미리보기 장 바꾸기 — 「지형 2장」 → 「노선배치도」(확정 배지만 · [확정 풀기] 없음 140-1) const pickSheet = async (id) => { modal.body - .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") + .querySelectorAll(".b07-plan-manage__sheet") .find((b) => b.dataset.drawingId === id) .click(); await settle(); @@ -334,12 +344,50 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan preview: cad.loads.at(-1).drawing.entities[0].id, head: head().textContent, active: modal.body - .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") + .querySelectorAll(".b07-plan-manage__sheet") .filter((b) => b.dataset.active === "true") .map((b) => b.dataset.drawingId), + sheetTexts: rows()[0] + .querySelectorAll(".b07-plan-manage__sheet") + .map((b) => b.textContent), + dropdown: kindPick().select.value, + activeRow: rows() + .filter((r) => r.dataset.active === "true") + .map((r) => r.dataset.planKind), }; await pickSheet("plan_k_route"); out.routeHead = head().textContent; + out.routeDropdown = kindPick().select.value; + + // 드롭다운으로 종류 고름 → 그 종류 첫 장 미리보기 + 줄 강조 + kindPick().change("k_terrain"); + await settle(); + out.dropdownPick = { + preview: cad.loads.at(-1).drawing.entities[0].id, + activeRow: rows() + .filter((r) => r.dataset.active === "true") + .map((r) => r.dataset.planKind), + }; + + // 줄 끌어 놓기 — 제자리면 부르지 않음 · 「노선배치도」를 「지형」 위 반에 놓으면 앞으로(놓는 즉시) + const dragTo = async (fromId, toId, clientY) => { + const from = rows().find((r) => r.dataset.planKind === fromId); + const to = rows().find((r) => r.dataset.planKind === toId); + from.fire("dragstart", {}); + to.fire("dragover", { clientY }); + to.fire("drop", {}); + from.fire("dragend"); + await settle(); + await settle(); + }; + await dragTo("k_route", "k_terrain", 15); // 아래 반 = 뒤 = 제자리 + out.dragSame = calls.filter((c) => c[0] === "reorder").length; + await dragTo("k_route", "k_terrain", 5); + out.drag = { + call: calls.find((c) => c[0] === "reorder"), + kinds: kindRows(), + sheets: sheetIds(), + }; // 닫기 — 고친 게 있으니 changed = true · 미리보기 듣기 멈춤 modal.opts.beforeClose(); @@ -351,7 +399,7 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan await settle(); const modal2 = modals[1]; modal2.body - .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") + .querySelectorAll(".b07-plan-manage__sheet") .find((b) => b.dataset.drawingId === "plan_k_route") .click(); await settle(); diff --git a/resources/tester/test_130_b07_plan_manage.py b/resources/tester/test_130_b07_plan_manage.py index 23542bb27..ca7023a5d 100644 --- a/resources/tester/test_130_b07_plan_manage.py +++ b/resources/tester/test_130_b07_plan_manage.py @@ -3,8 +3,10 @@ 못박는 것 - 좌측 「평면도」 = 장 단추 + [평면도 관리] 하나 · 종류 폼 · 대피소 칸 · × 없음. - - 단추 → 공용 모달(큰 창) · 안에 종류 목록 · 종류 폼 · 대피소 · 사토장 자리 · 장 목록 · 첫 장 미리보기(보기 전용). - - 종류 추가 · 업데이트(축척 포함) · 지우기 = 지금 서버 길 · 고칠 때마다 모달 안을 다시 받음. + - 단추 → 공용 모달(큰 창) · 안에 [종류 ▼] 폼 · 평면도 장 블록(종류마다 한 줄 · 작은 장 단추) · 대피소 · 사토장 자리 · + 첫 장 미리보기(보기 전용). + - 종류 추가 · 업데이트(축척 포함) · 지우기(폼 안) = 지금 서버 길 · 고칠 때마다 모달 안을 다시 받음. + - 장 단추 → 드롭다운 따라감 · 드롭다운 → 그 종류 첫 장 · 줄 끌어 놓기 = 종류 순서(놓는 즉시 · 140-2). - 미리보기 장 바꾸기 · 캔버스만(bare) · 확정 배지만(확정 풀기 없음 140-1) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id. Node 헬퍼 `helper_130_b07_plan_manage.cjs` 가 화면 그릇 흉내로 돌림. 화면은 사용자 확인. @@ -67,10 +69,11 @@ def test_종류_추가(out): def test_종류_업데이트(out): assert out["updateLabel"] == "업데이트" assert out["update"]["call"] == ["update", "k_new", "sub6임시2", [], 1000] - assert out["update"]["rowText"].startswith("sub6임시2 · 1장") + assert out["update"]["rowText"] == "≡sub6임시2" # 한 장이면 이름이 곧 장 단추 def test_종류_지우기(out): + assert out["removeHiddenForNew"] is True # 「새 종류」엔 [지우기] 없음 assert out["remove"]["call"] == ["delete", "k_new"] assert out["remove"]["confirms"] == 1 assert out["remove"]["kinds"] == ["k_terrain", "k_route"] @@ -81,6 +84,21 @@ def test_미리보기_장_바꾸기(out): assert s2["preview"] == "plan_k_terrain_2" assert s2["active"] == ["plan_k_terrain_2"] assert "확정 전" in s2["head"] and "확정 풀기" not in s2["head"] + assert s2["sheetTexts"] == ["1", "2"] # 여러 장 = 작은 장 단추 + assert s2["dropdown"] == "k_terrain" and s2["activeRow"] == ["k_terrain"] + assert out["routeDropdown"] == "k_route" + + +def test_드롭다운이면_그_종류_첫_장(out): + assert out["dropdownPick"] == {"preview": "plan_k_terrain", "activeRow": ["k_terrain"]} + + +def test_줄_끌어_놓기는_종류_순서(out): + assert out["dragSame"] == 0 # 제자리면 서버 안 부름 + drag = out["drag"] + assert drag["call"] == ["reorder", ["k_route", "k_terrain"]] + assert drag["kinds"] == ["k_route", "k_terrain"] + assert drag["sheets"] == ["plan_k_route", "plan_k_terrain", "plan_k_terrain_2"] def test_확정이면_배지만(out): diff --git a/resources/tester/test_b07_plan_kinds.py b/resources/tester/test_b07_plan_kinds.py index ca84bd61c..7f2827860 100644 --- a/resources/tester/test_b07_plan_kinds.py +++ b/resources/tester/test_b07_plan_kinds.py @@ -10,6 +10,7 @@ ⑦ 용지도 종류(93장) — 기본 종류 · 옛 종류 폴더에 한 번 더함(지우면 안 살아남) · 용지 조각 층 ⑧ 배수유역도 종류(109장) — 양식 watershed · 한 장 목록 · 지우기 · 다시 만들기 · IP 표 두 줄 차례 · 용지도 상자 안 선 0 +⑨ 종류 차례(140-2) — 끌어 놓은 차례 그대로 · 다시 읽어 같음 · 앞선 종류가 바뀐 종류만 확정 풂 · 목록 어긋나면 거절 """ import json @@ -43,6 +44,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import ( list_kinds, plan_kind_items, read_marks, + reorder_kinds, slope_records, update_kind, write_marks, @@ -442,3 +444,27 @@ def test_ip_table_two_lines_left_to_right_top_to_bottom() -> None: assert origins[3][0] == origins[0][0] # 다음 줄은 다시 왼쪽 assert tops[3] == 5.0 + 8 * 5.0 # 마지막 줄이 도곽 왼쪽 아래 기준(옛 한 줄 자리와 같음) assert tops[0] - tops[3] == 8 * 5.0 + 3.0 # 줄 간격 그대로 + + +def test_reorder_kinds_order_and_release_renumbered(tmp_path: Path) -> None: + root = tmp_path / "project" + manifest = _read_manifest(root) + manifest["drawings"] = { + "plan_terrain": {"confirmed": True}, + "plan_route": {"confirmed": True}, + "plan_layout": {"confirmed": True}, + } + _write_manifest(root, manifest) + before = [k["id"] for k in list_kinds(root)] + assert before[:3] == ["terrain", "route", "layout"] + after = ["route", "terrain", *before[2:]] # 앞 둘만 바꿈 + reorder_kinds(root, after) + assert [k["id"] for k in list_kinds(root)] == after # 다시 읽어 같음 + drawings = _read_manifest(root)["drawings"] + assert drawings["plan_terrain"]["confirmed"] is False # 번호 바뀜 + assert drawings["plan_route"]["confirmed"] is False + assert drawings["plan_layout"]["confirmed"] is True # 앞선 종류 묶음 그대로 = 번호 그대로 + with pytest.raises(ValueError): + reorder_kinds(root, after[:-1]) # 빠진 종류 + with pytest.raises(ValueError): + reorder_kinds(root, [*after[:-1], "terrain"]) # 겹친 종류