From 39afd6f0476d96ab011fd11b2e256ab2a97cc187 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:55:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore(plan):=2010-5=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20=EC=9B=90=EB=AC=B8=20=EB=8F=99=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- docs/raw/사용자요구/2026-10-05_수정요구.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index c027a5d46..fb1497120 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -304,3 +304,16 @@ B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이 (10-6 사용자) 다음으로 종단 패널의 종단도에서 알약선택시 나오는 선택정보 창에서 내용은 옵션 제목은 필요없고 구분기호 하나 넣고 옵션값만 구조물 이름 행과 2행은 옵션값만으로 표현해도 됨 + +(10-6 사용자) +횡단도에서 형상의 제약조건이 어떤게 있는지 확인 좀 해줘. 배관과 흙막이, 옹벽관련 내용들만 일단 찾아줘. + +(10-6 사용자) +다음으로 좌측 패널에서 서브 컨테이너의 항목을 선택/변경/생성 할 경우에 횡단도에서 선택한것처럼 이동되어야할것같아. + +(10-6 사용자) +추가로 배관의 도수로/돌붙임의 경우에는 복수 값이 아니야. 생성이 되었다면 추가 버튼이 없어져야함. + +(10-6 사용자) +1. 종단도의 구조물 알약의 이름이 구조물군의 종류 값과 매칭이 안됨. 같은 값을 보게 개선. +2. 종단도에서 배관이 선택한 상태에서 다른 지점에 옹벽을 추가하기 위해 마우스 오른쪽 버튼을 클릭후 사면안정을 눌렀더니 하위 리스트는 안보이고 리셋된 상태가 됨. 확인 From c75925ae77e60c0c4b5831f6bb46475417e2a7f4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 16:06:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(structure):=20=EC=A2=85=EB=8B=A8=20?= =?UTF-8?q?=EC=95=8C=EC=95=BD=20=EA=B8=80=20=3D=20=EC=A2=85=EB=A5=98=20?= =?UTF-8?q?=EC=B9=B8=20=EC=9D=B4=EB=A6=84=20=C2=B7=20=EC=9A=B0=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EB=A9=94=EB=89=B4=20=ED=81=B4=EB=A6=AD=EC=9D=B4=20?= =?UTF-8?q?=EA=B7=B8=EB=9E=98=ED=94=84=20=EC=84=A0=ED=83=9D=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=EB=A1=9C=20=EC=83=88=EC=A7=80=20=EC=95=8A=EC=9D=8C(54?= =?UTF-8?q?-49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알약 글 = 좌측 「종류」 칸 · 목록 · 정보 창 1행과 같은 이름(약호 · 기타 머리 자르기 뺌) · 같은 이름 여럿이면 번호 - 알약 겹침 판정 = 이름 글 폭 어림(고정 52px 뺌) - 공용 우클릭 메뉴 안 click 은 메뉴에서 멈춤 — 배관 고른 채 군 머리를 누르면 그래프 빈 곳 클릭으로 새어 폼 리셋 · 메뉴 사라짐 - 시험: test_54_9_etc_pill 새 규칙 · 헬퍼 json 불러오기 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- .../B05_Profile_UI_Structures_Marks.ts | 69 +++++++------------ resources/tester/helper_54_9_etc_pill.cjs | 1 + resources/tester/test_54_9_etc_pill.py | 38 ++++------ ui_template/ui_template_context_menu.ts | 3 + 4 files changed, 43 insertions(+), 68 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Structures_Marks.ts b/B05_Profile/B05_Profile_UI_Structures_Marks.ts index 0fc00d26a..a2a03a295 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Marks.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Marks.ts @@ -37,10 +37,9 @@ const SOLO_PILL_HEIGHT_PX = 22; const ROW_GAP_PX = 2; /** 레인 위아래 여백(px) — 그래프를 밀어내되 여유는 최소로. */ const LANE_PADDING_PX = 2; -/** 두 알약이 겹친다고 볼 가로 거리(px). 3자 라벨 폭 기준. */ -const OVERLAP_PX = 52; -/** 기타 알약 머리 글자 수 — 약호(3자 이하)와 같은 폭(54-9). */ -const ETC_PILL_CHARS = 3; +/** 알약 폭 어림(px) — 글자당 · 좌우 여백+테(54-49 이름 글이라 폭이 제각각). */ +const PILL_CHAR_PX = 11; +const PILL_PAD_PX = 20; /** 라벨을 위로 비키는 높이(px) — 그래프 바닥 ▼ 단추(바닥 2 + 높이 17) + 틈 2(54-14). */ const EDIT_BUTTON_CLEARANCE_PX = 21; @@ -83,19 +82,24 @@ function positionText(structure: StructureInstance, intervalM: number): string { function rowAssignments( structures: ReadonlyArray, x: (chainageM: number) => number, + labelOf: (structure: StructureInstance) => string, ): Map { const rows = new Map(); const sorted = [...structures].sort( (left, right) => structureAnchorM(left) - structureAnchorM(right), ); const centers = sorted.map((structure) => x(structureAnchorM(structure))); + const halves = sorted.map( + (structure) => ([...labelOf(structure)].length * PILL_CHAR_PX + PILL_PAD_PX) / 2, + ); let row = 0; sorted.forEach((structure, index) => { const id = structure.structure_id ?? ""; const near = - (index > 0 && centers[index] - centers[index - 1] < OVERLAP_PX) || - (index < sorted.length - 1 && centers[index + 1] - centers[index] < OVERLAP_PX); + (index > 0 && centers[index] - centers[index - 1] < halves[index] + halves[index - 1]) || + (index < sorted.length - 1 && + centers[index + 1] - centers[index] < halves[index] + halves[index + 1]); if (!near) { // 앞뒤 어디와도 안 겹친다 — 혼자이므로 레인 세로 가운데. rows.set(id, null); @@ -110,27 +114,8 @@ function rowAssignments( return rows; } -/** 이름마다 남과 갈리는 가장 짧은 머리 — 빈칸 뺌 · 적어도 3글자 · 이름보다 길지 않음. */ -function distinctHeads(names: ReadonlyArray): Map { - const letters = names.map((name) => [...name.replace(/\s+/g, "")]); - return new Map( - names.map((name, index) => { - const own = letters[index]; - let length = ETC_PILL_CHARS; - letters.forEach((other, otherIndex) => { - if (otherIndex === index) return; - let common = 0; - while (common < own.length && own[common] === other[common]) common += 1; - length = Math.max(length, common + 1); - }); - return [name, own.slice(0, length).join("")]; - }), - ); -} - -/** 표시 이름 · 알약 글(구조물 id 별). 표시 이름 = 좌측 목록과 같은 중복 번호(시점에 가까운 순 - * "이름 1/2…", 2026-08-19) — 툴팁 · 벌룬 제목이 씀. 알약 = 약호 + 그 번호. 기타는 약호가 모두 - * 「기타」라 손 이름 머리(distinctHeads) + 번호 · 빈 이름 = 「기타 N」(54-9). */ +/** 표시 이름 · 알약 글(구조물 id 별) — 둘 다 좌측 「종류」 칸 · 목록과 같은 이름(54-49). + * 같은 이름이 여럿이면 시점에 가까운 순 「이름 1/2…」 · 기타 = 손 이름(빈 이름 = 「기타」). */ export function structurePillTexts( structures: ReadonlyArray, typeMap: ReadonlyMap, @@ -138,22 +123,14 @@ export function structurePillTexts( const sorted = [...structures].sort( (left, right) => structureAnchorM(left) - structureAnchorM(right), ); - const rawNames = sorted.map( - (entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id, + const numbered = numberDuplicateNames( + sorted.map((entry) => etcName(entry) ?? typeMap.get(entry.type_id)?.name ?? entry.type_id), ); - const numbered = numberDuplicateNames(rawNames); - const heads = distinctHeads([ - ...new Set(rawNames.filter((_name, index) => sorted[index].type_id === "etc")), - ]); return new Map( - sorted.map((entry, index) => { - const name = numbered[index]; - const suffix = name.slice(rawNames[index].length); // " 1" 또는 "" - const head = etcName(entry) - ? heads.get(rawNames[index]) - : typeMap.get(entry.type_id)?.style?.abbr; - return [entry.structure_id ?? "", { name, pill: `${head ?? "?"}${suffix}` }]; - }), + sorted.map((entry, index) => [ + entry.structure_id ?? "", + { name: numbered[index], pill: numbered[index] }, + ]), ); } @@ -167,11 +144,14 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement lane.style.height = `${STRUCTURE_LANE_HEIGHT_PX}px`; const typeMap = new Map(options.types.map((type) => [type.type_id, type])); - const rows = rowAssignments(options.structures, options.x); - // 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 알약 글·툴팁·벌룬 제목이 // 모두 이 번호를 따른다. const texts = structurePillTexts(options.structures, typeMap); + const rows = rowAssignments( + options.structures, + options.x, + (structure) => texts.get(structure.structure_id ?? "")?.pill ?? "", + ); options.structures.forEach((structure) => { const id = structure.structure_id; @@ -214,8 +194,7 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement mark.style.top = `${top}px`; mark.style.borderColor = type?.style?.color ?? "#888"; mark.style.color = type?.style?.color ?? "#888"; - // 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호 · - // 기타는 손 이름 머리(54-9). + // 라벨 = 「종류」 칸과 같은 이름 + 중복 번호(54-49). mark.textContent = pill; mark.title = `${displayName} ${positionText(structure, options.stationIntervalM)}`; mark.dataset.structureId = id; diff --git a/resources/tester/helper_54_9_etc_pill.cjs b/resources/tester/helper_54_9_etc_pill.cjs index 4cb1a00d6..807b5d93f 100644 --- a/resources/tester/helper_54_9_etc_pill.cjs +++ b/resources/tester/helper_54_9_etc_pill.cjs @@ -34,6 +34,7 @@ function loadTs(file) { cache.set(full, box.exports); const localRequire = (request) => { if (STUBS[request]) return STUBS[request]; + if (request.endsWith(".json")) return require(path.join(path.dirname(full), request)); if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); return require(request); diff --git a/resources/tester/test_54_9_etc_pill.py b/resources/tester/test_54_9_etc_pill.py index 0adec35b0..042849e80 100644 --- a/resources/tester/test_54_9_etc_pill.py +++ b/resources/tester/test_54_9_etc_pill.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -"""54-9 종단 알약 글 — 기타가 여럿이어도 알약끼리 구별됨. +"""54-9 → 54-49 종단 알약 글 = 좌측 「종류」 칸 · 목록과 같은 이름. -• 손 이름 앞 3글자 · 같은 이름이면 목록과 같은 번호 · 빈 이름 = 「기타 N」 -• 머리가 같은 다른 이름은 갈릴 때까지 늘림 -• 다른 종류 알약(약호 + 중복 번호) · 툴팁 · 목록 이름(손 이름 그대로)은 그대로 +• 종류 이름(정의 이름) · 기타는 손 이름 · 같은 이름 여럿이면 목록과 같은 번호 · 빈 이름 = 「기타 N」 +• 약호 · 머리 자르기(옛 54-9 규칙) 안 씀 — 알약 = 툴팁 이름 화면 그릇 없이 Node 헬퍼가 TS 를 트랜스파일해 돌린다(`helper_54_9_etc_pill.cjs`). B05 · B06 은 같은 알약 레인(`buildStructureLane`)을 씀 — 실제 화면은 ORCA 로 따로 본다. """ @@ -31,38 +30,31 @@ def out(): return json.loads(proc.stdout) -def test_53_2_기타_다섯_알약이_서로_다르다(out): +def test_53_2_기타_다섯_알약이_손_이름과_번호(out): pills = {key: value["pill"] for key, value in out["scenario"].items()} assert pills == { - "e1": "배수로 1", + "e1": "배수로정비 1", "e2": "울타리", "e3": "기타 1", - "e4": "배수로 2", + "e4": "배수로정비 2", "e5": "기타 2", } - assert len(set(pills.values())) == 5 -def test_툴팁_이름은_손_이름_그대로(out): - names = [value["name"] for value in out["scenario"].values()] - assert names == ["배수로정비 1", "울타리", "기타 1", "배수로정비 2", "기타 2"] +def test_알약은_툴팁_이름과_같다(out): + for group in out.values(): + for value in group.values(): + assert value["pill"] == value["name"] -def test_머리가_같은_다른_이름은_갈릴_때까지_늘린다(out): - pills = {key: value["pill"] for key, value in out["prefix"].items()} - assert pills == { - "p1": "콘크리트블", - "p2": "콘크리트옹", - "p3": "배수로", - "p4": "배수로공", - "p5": "기타시", - "p6": "기타", - } +def test_머리가_같은_이름도_자르지_않는다(out): + pills = [value["pill"] for value in out["prefix"].values()] + assert pills == ["콘크리트 블록", "콘크리트 옹벽", "배수로", "배수로공", "기타 시설", "기타"] -def test_다른_종류_알약은_약호와_번호_그대로(out): +def test_다른_종류_알약은_종류_이름과_번호(out): pills = {key: value["pill"] for key, value in out["others"].items()} - assert pills == {"w1": "옹 1", "w2": "옹 2", "x1": "가로등", "x2": "기타"} + assert pills == {"w1": "옹벽 1", "w2": "옹벽 2", "x1": "가로등", "x2": "기타"} def test_알약_레인이_글을_한_곳에서_받는다(): diff --git a/ui_template/ui_template_context_menu.ts b/ui_template/ui_template_context_menu.ts index 4861ceab8..48874fbb6 100644 --- a/ui_template/ui_template_context_menu.ts +++ b/ui_template/ui_template_context_menu.ts @@ -82,6 +82,9 @@ export function createMapContextMenu(blockClass: string): MapContextMenu { const element = document.createElement("div"); element.className = `${blockClass}__context-menu ui-ctx-menu`; element.hidden = true; + // 메뉴 안 click 은 메뉴에서 끝냄 — 그릇(종단 그래프 등)의 「빈 곳 클릭 = 선택 해제」로 새면 + // 폼이 리셋되고 다시 그려져 메뉴가 사라짐(54-49). + element.addEventListener("click", (event) => event.stopPropagation()); const registryEntry = { close,