fix(M02): 좌측 고름 표시가 두 줄 동시에 남던 것 — 열린 것 한 줄만

- 원인: 좌측 목록 줄은 목록만 · 구조물 열 줄은 열 줄만 다시 그려 이전 컨테이너 고름이 남음 · 카드 [도면 수정] 등은 좌측에 안 알림
- 고침: 편집 칸이 무엇을 열든(좌측 · 카드 · 다시 읽기) `onOpened` 로 좌측 `paint` 한 곳을 부름 — 목록 · 열 줄 · 단추를 함께 그림 · 따로 든 고름 상태(`picked` · 안 쓰던 `select`) 지움
- 확인: ORCA 5180 에서 카드 [도면 · 면적산출 · 재료산출 수정] → 다른 컨테이너 · 같은 컨테이너 다른 줄 · 집계표로 돌아옴 17 걸음 모두 고름 줄 1 개(고치기 전 2 개 재현) · 만든 것 없음 · 탭 닫음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zGnt85s95baQ72rhKBCJ4
This commit is contained in:
2026-09-28 22:28:15 +09:00
co-authored by Claude Opus 5.5
parent 513cebe023
commit 990e8e6733
4 changed files with 36 additions and 23 deletions
@@ -93,7 +93,12 @@ export function canEdit(layer: Layer, isAdmin: boolean): boolean {
return layer === "project" || layer === "personal" || (layer === "system" && isAdmin);
}
export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
/** `onOpened` — 무엇을 열든(좌측 · 카드 [도면 수정] · 다시 읽기) 연 바로 뒤 부름(좌측 고름 표시) */
export function createMain(
isAdmin: boolean,
onSaved?: () => void,
onOpened?: () => void,
): MainHandle {
const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") });
const reload = createButton({ label: L("M02_Reload"), variant: "ghost" });
const save = createButton({ label: L("M02_Save"), variant: "filled" });
@@ -357,6 +362,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle {
drop();
showNotice([]);
sel = next;
onOpened?.();
bar();
if (!next) {
title.textContent = L("M02_PickTemplate");
@@ -17,7 +17,11 @@ import "./M02_MasterTemplete_UI_Style.css";
export async function renderM02MasterTemplate(root: HTMLElement): Promise<void> {
const user = await fetchSessionUser().catch(() => null);
const isAdmin = user?.role === "SYSTEM_ADMIN";
const main = createMain(isAdmin, () => void side.refresh());
const main = createMain(
isAdmin,
() => void side.refresh(),
() => side.paint(),
);
const side = buildSide({
isAdmin,
onOpen: (sel) => void main.open(sel),
@@ -80,8 +80,8 @@ export interface SideOptions {
export interface SideHandle {
root: HTMLElement;
refresh: () => Promise<void>;
/** 좌측 고름 표시만 옮김 — 화면은 열지 않음 */
select: (kind: Kind, name: string) => void;
/** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */
paint: () => void;
}
/** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */
@@ -102,8 +102,6 @@ export function buildSide(opt: SideOptions): SideHandle {
let numbers: Record<string, StructureNumbers> = {};
let seq = 0;
let picked: { kind: Kind; name: string } | null = null;
const section = (title: string, body: HTMLElement, ...more: HTMLElement[]): HTMLElement =>
el("section", {
className: "m02-side__group ui-collapsible ui-sidebar-section ui-sidebar-card is-collapsed",
@@ -140,9 +138,8 @@ export function buildSide(opt: SideOptions): SideHandle {
const root = el("div", { className: "m02-side ui-sidebar-panel", children: [listHost] });
/** 고름 표시 — `picked`(방금 누름) 이 있으면 그것 · 없으면 지금 열린 것과 비교 */
/** 고름 표시 — 편집 칸에 지금 열린 것 하나뿐 */
const isPicked = (kind: Kind, name: string): boolean => {
if (picked) return picked.kind === kind && picked.name === name;
const cur = opt.getOpen();
return !!cur && cur.layer === layer && cur.kind === kind && cur.name === name;
};
@@ -199,21 +196,23 @@ export function buildSide(opt: SideOptions): SideHandle {
for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind;
}
/** 고름 표시는 여기서만 — 목록 · 열 줄을 함께 그려야 두 줄이 동시에 안 남음 */
function paint(): void {
paintList();
paintColumns();
paintButtons();
}
// 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음)
async function select(info: TemplateInfo): Promise<void> {
if (!isOpen(info) && !(await opt.confirmLeave())) return;
picked = null;
opt.onOpen({ layer, projectId: null, kind: info.종류, name: info.이름 });
paintList();
paintButtons();
}
async function selectColumn(col: StructureColumn, kind: ColumnKind): Promise<void> {
if (!isPicked(kind, col.id) && !(await opt.confirmLeave())) return;
picked = null;
// 제목은 편집 화면이 문서 번호를 앞에 붙임(「SD010101 돌기슭막이 …」)
opt.onOpen({ layer, projectId: null, kind, name: col.id, label: columnHead(col) });
paintColumns();
paintButtons();
}
async function refreshStructure(): Promise<void> {
@@ -328,13 +327,5 @@ export function buildSide(opt: SideOptions): SideHandle {
}
void refresh();
return {
root,
refresh,
select: (kind, name) => {
picked = { kind, name };
paintList();
paintColumns();
},
};
return { root, refresh, paint };
}
@@ -148,6 +148,18 @@ def test_좌측_새로_본떠_지우기는_표_도면_컨테이너_안에_하나
assert "양식: name" in side.split("function copyTemplate")[1] # 복사본 머리 이름 = 새 이름
def test_좌측_고름_표시는_열린_것_한_줄뿐():
"""카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 열 줄을 함께 그림."""
side = _text(SIDE)
assert "picked" not in side # 따로 든 고름 상태 없음 — 편집 칸의 지금 열린 것만 봄
assert re.search(r"function paint\(\): void \{\s*paintList\(\);\s*paintColumns\(\);", side)
for fn in ("async function select(", "async function selectColumn("):
body = side.split(fn)[1].split("\n }\n")[0]
assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음
assert re.search(r"sel = next;\s*onOpened\?\.\(\);", _text(MAIN))
assert "() => side.paint()" in _text(M02 / "M02_MasterTemplete_UI_Page.ts")
def test_리본_접힘_모드에서_탭으로_편_것은_밖을_누르면_다시_접힘():
ribbon = _text(ROOT / "B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx")
assert "if (collapsed) setPeek(true);" in ribbon # 탭 = 잠깐 펼침(접힘 모드 유지)