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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]) # 겹친 종류
|
||||
|
||||
Reference in New Issue
Block a user