feat(B07): 평면도 관리 모달 종류 드롭다운 상세 · 장 한 블록 · 끌어 놓기 종류 순서(140-2)

- 맨 위 [종류 ▼] 상세 폼 · × 대신 폼 안 [지우기]
- 평면도 장 = 종류마다 한 줄 [≡] 이름 [1][2] · 장 단추 누르면 미리보기 + 드롭다운 따라감
- 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 PUT plan-kinds-order(reorder_kinds) · 번호 바뀐 장 확정 풂

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
This commit is contained in:
2026-10-09 14:51:11 +09:00
co-authored by Claude Opus 5.5
parent eebe72a7f4
commit 6df477a522
9 changed files with 343 additions and 68 deletions
@@ -233,6 +233,14 @@ export function updatePlanKind(
});
}
/** 종류 차례 — 끌어 놓은 차례 그대로(140-2 · 놓는 즉시 · 번호 바뀐 장은 서버가 확정 풂). */
export function reorderPlanKinds(projectId: string, ids: string[]): Promise<PlanKindsState> {
return requestJson(`/projects/${projectId}/plan-kinds-order`, {
method: "PUT",
body: JSON.stringify({ ids }),
});
}
export function deletePlanKind(projectId: string, kindId: string): Promise<PlanKindsState> {
return requestJson(`/projects/${projectId}/plan-kinds/${encodeURIComponent(kindId)}`, {
method: "DELETE",
@@ -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:
"""종류 업데이트 — 이름 · 조각을 고친다(기본 넷도)."""
@@ -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:
@@ -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 {
@@ -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<CadDrawing>({ 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<HTMLElement>(".b07-plan-manage__sheets .b07-drawing-button")
.querySelectorAll<HTMLElement>(".b07-plan-manage__sheet")
.forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked)));
side
.querySelectorAll<HTMLElement>(".b07-plan-manage__row")
.forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind)));
};
/** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */
const show = async (drawing: DesignDrawingItem, label: string): Promise<void> => {
@@ -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;
@@ -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;
+67 -19
View File
@@ -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();
+21 -3
View File
@@ -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):
+26
View File
@@ -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"]) # 겹친 종류