diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index ce63338b4..acb2e894e 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -81,7 +81,7 @@ import { frameOf } from "@util/common_util_view_window"; import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots"; import { noticeDroppedEdits } from "./design_save"; import { createProfileOverlays } from "./design_profile_overlays"; -import { bindProfileViewport } from "./design_profile_viewport"; +import { bindProfileViewport, createSelectionReveal } from "./design_profile_viewport"; import "../../B06_Section/B06_Section_UI_Style.css"; // SVG 차트 색상(.b06-chart__*)의 정의처는 _Style_Cross.css다. 이걸 빼면 B05로 바로 진입했을 때 // 배경 rect가 브라우저 기본 fill(검정)로 그려진다 — B06을 먼저 방문해야 정상으로 보이던 원인. @@ -461,6 +461,14 @@ export function createDesignProfile(ctx: DesignProfileContext) { }); } + /** 다른 영역에서 고른 자리가 화면 밖이면 가운데로(칠한 뒤 · `design_profile_viewport`). */ + const reveal = createSelectionReveal(body); + const revealPick = (): void => + reveal( + (selectedStationId ?? selectedStructureId) + ? `${selectedStationId}|${selectedStructureId}` + : null, + ); // 가로 스크롤 · 휠 · 크기 바뀜은 `design_profile_viewport` 몫(700줄 한계). const unbindViewport = bindProfileViewport({ body, @@ -551,6 +559,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { // 3D·리스트·배수유역도에서 온 선택도 같은 자리 알약을 함께 켠다(2026-08-17). selectedStructureId = structureIdAtStation(stationId); draw(); + revealPick(); }, /** 횡단배수 최소고를 편집에서 강제할지(사이드 체크박스). 경고 표시는 무관하게 유지된다. */ setEnforceMinCover(enforce: boolean) { @@ -606,6 +615,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { const keep = structureId !== null && structureIdAtStation(selectedStationId) === structureId; selectedStationId = keep ? selectedStationId : own; draw(); + revealPick(); }, /** 이어 공사 시작 기준(측점번호·누가거리 오프셋)을 반영해 측점 라벨·누가거리 표시를 옮긴다. */ setStationDisplay(next: { station: number; cumulative: number }) { diff --git a/A00_Common/design/design_profile_viewport.ts b/A00_Common/design/design_profile_viewport.ts index 3be92e541..54346620c 100644 --- a/A00_Common/design/design_profile_viewport.ts +++ b/A00_Common/design/design_profile_viewport.ts @@ -68,3 +68,33 @@ export function bindProfileViewport(deps: ProfileViewportDeps): () => void { resizeObserver.observe(body); return () => resizeObserver.disconnect(); } + +/** 고른 것이 보이는 칸 가장자리에서 이만큼(폭 비율) 안쪽이면 그대로 둔다. */ +const REVEAL_EDGE = 0.1; + +/** + * 고른 측점선(없으면 고른 구조물 알약)이 종단도 보이는 칸 밖이면 가운데로 민다 — 다른 영역(횡단 카드 · + * 3D · 배수유역도 · 좌측 목록)에서 고른 자리도 종단에서 바로 보이게. 칠한 결과(강조된 선 · 알약)를 + * 보고 옮긴다 · 같은 고름을 다시 칠할 때는 안 밂(사용자가 옮겨 본 자리를 지킴) · 그래프 안에서 + * 누른 것은 이미 보이는 자리라 안 움직임. `key` = 고름 열쇠(풀면 null). + */ +export function createSelectionReveal(body: HTMLElement): (key: string | null) => void { + let shown: string | null = null; + return (key) => { + if (key === null || key === shown) { + shown = key; + return; + } + const mark = + body.querySelector(".b06-chart__station--selected") ?? + body.querySelector(".b05-structure__mark.is-selected"); + if (!mark || body.clientWidth <= 0) return; // 아직 안 그려짐 — 다음 칠하기에서 + shown = key; + const box = mark.getBoundingClientRect(); + const view = body.getBoundingClientRect(); + const at = box.left + box.width / 2 - view.left; + if (at >= view.width * REVEAL_EDGE && at <= view.width * (1 - REVEAL_EDGE)) return; + // 바로 옮김 — 고른 뒤 잇따르는 다시 그리기가 부드러운 이동을 끊음(다시 그리기는 지금 자리를 지킴) + body.scrollLeft += at - view.width / 2; + }; +} diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index ae132eb75..c52d54512 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -196,3 +196,4 @@ 165. (원문 · 10-11) 1. B07의 횡단도에 측구를 표현할때 노란색으로 측구가 하나더있어보임. 확인후 삭제 2. B07의 배관이 구분이 어려움, 색상변경,(박스암거도) 3. B07 좌측 패널의 사이즈는 요소들의 버튼과 입력창의 양식은 B05/B06 좌측 패널의 구조물 배치의 공용양식 사용. 이때 B06의 다른 컨테이너도 포함. B05 이상의 페이지들의 좌측패널의 컨테이너 공용양식 사용확인, 이때 패널의 하단에 버튼셋들은 예외. 4. B05/B06 좌측패널의 구조물 리스트나 B06의 횡단도, 배수유역도의 번호 및 영역, 테이블열, 3D 측점 하이라이트 기능은 다연동되어 있는데 지금 배수유역과 테이블 열 하이라이트 기능을 잃은것같아. 이건 정말 조심히 개선해야하는데 어제 저녁쯤에 상당히 오랫동안 분석해서 나온결과야. 주요 문제점은 노면공의 범위 값에 의한 하이라이트 충돌문제였어. 제대로 확인한 후에 4번만 너가 직접 작업하고 브라우져 검증해줘. 166. (원문 · 10-11 · 브레인 보고 답) 1. CAD 미리넣는 기능에서 사용자가 어떻게 사용할지를 몰라서 일단은 좌측패널 옵션과 구조물조합이름을 다 넣어주면 내가 추가로 선정하겠음. 2. 상세 창 처음화면은 전체가 다안보여도 상관없어. 3. 표열 강조가 무슨의미인지 자기 측점이 없는게 있을수 있나 무슨 말인지 모름. 4. 깃 싱크해도됨. 5. B05/B06 좌측 패널의 구조물 리스트 상단에 정수 맞춤 버튼이 있는데 역할을 전방위 적으로 코드를 확인해줘. 내가 원하는 방향은 전처리과정에서 확보된 배관지점들을 결정할때 반올림하여 횡단배수들의 위치를 결정되어한다. 이후 사용자들이 조절을 하거나 다른 구조물 전체 중 어떤 걸 넣더라고 지정된 위치는 그냥 정수값을 받고 지정해야한다는 거야. 혹시 문제가 있는지 확인해줘. 그리고 만약 구조물 측점과 횡단도 측점이 같은 측점으로 뭉쳐진다면 상위인 구조물 측점이 존재하고 일반측점은 비활성화되어야해. 그리고 만약 구조물 측점을 옮기게 되면 비활성화된 일반측점은 활성화하고 구조물 측점을 이동하는거야. 이건 하이라이트 기능들과 많이 연동되어있어. 간혹 두가지 중첩된 상태로 보이는 상황이거든 정말 자세히 검토해줘. <<< 에게 왠지 3번 항목과 연관되지 않을까 생각됨. 167. (원문 · 10-11 · 조사 AK 답) 너가 뭔가 오해한것 같음. 1. 현재 측점을 가지는 구조물들을 유지해 필요하면 내가 나중에 추가할 수도 있음. 2. 프로그램에서 측점의 수치를 중 '측점 + 거리' or 누가거리에서 소수점을 사용하지 않겠다는 의미야. m 단위로 결정한다는 말이지. 3. 단추는 필요없어. 그냥 소수가 아닌 정수로만 제어하겠다는 의미야. 그러니 옛데이터도 그렇게 되어야겠지. 겹침 개선해. 지금 개발하면서 반복적으로 발생되고 있는 문제야. 이게 아마 하나씩 구조물 측점을 하나씩 추가하면서 발생된 문제같은데 근본적으로 문제를 해결해줘. 현재 전체적인 로직과 개발 방향은 잡힌상태야. 기능 확장과 동일 요소에 적용같은 방향만 있을것같은데 근본적인 문제해결을 위해 리팩토링이 필요하다면 말해. +168. (원문 · 10-11 · 272 뒤) 이상하네 B05와 B06은 하나의 페이지로 동작하게 만들어져 있어. 그렇다면 신규 추가나 이동에 대해 바로 반영이 되면 좋겠는데? 그리고 종단도에 다른 영역들의 선택에 따라 종단 그래프에서도 활성화가 잘되는데 이떄 해당지점으로 이동해주는 기능이 누락되었어. 이전 코드적용 가능한지 확인해봐. 조건을 측전 활성화 결과를 보고 해당지점으로 이동하게 하는게 좋을것같아. / 해당지점을 화면의 중간에 오면 더 좋고 diff --git a/resources/tester/helper_273_1_profile_reveal.cjs b/resources/tester/helper_273_1_profile_reveal.cjs new file mode 100644 index 000000000..5e084fc25 --- /dev/null +++ b/resources/tester/helper_273_1_profile_reveal.cjs @@ -0,0 +1,60 @@ +/* 273-1 종단 고름 → 화면 밖이면 가운데로 — 진짜 `createSelectionReveal`(design_profile_viewport.ts)을 + * 가짜 스크롤러로 돌림. 표준출력 JSON 한 벌. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const js = ts.transpileModule( + fs.readFileSync(path.join(ROOT, "A00_Common/design/design_profile_viewport.ts"), "utf8"), + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }, +).outputText; +const box = { exports: {} }; +const stubs = { + "../../B05_Profile/B05_Profile_UI_Profile_Wheel": { attachWheelHorizontalScroll: () => {} }, + "@util/common_util_chart_ywindow": { needsFullRedraw: () => false }, + "@util/common_util_view_window": { viewOf: () => globalThis }, +}; +new Function("exports", "module", "require", js)(box.exports, box, (name) => { + if (!stubs[name]) throw new Error(`stub 없음: ${name}`); + return stubs[name]; +}); + +/** 폭 700 스크롤러 — 고른 선은 내용 좌표 `lineAt`(없으면 null). */ +function scroller(lineAt) { + const body = { + scrollLeft: 0, + clientWidth: 700, + line: lineAt, + getBoundingClientRect: () => ({ left: 0, width: 700 }), + querySelector(selector) { + if (this.line === null || !selector.includes("--selected")) return null; + const x = this.line - this.scrollLeft; + return { getBoundingClientRect: () => ({ left: x - 1, width: 2 }) }; + }, + }; + return body; +} + +const out = {}; +const far = scroller(3000); +const reveal = box.exports.createSelectionReveal(far); +reveal("a"); +out.far = far.scrollLeft; // 3000 - 350 +far.scrollLeft = 100; // 사용자가 옮겨 봄 +reveal("a"); // 같은 고름 다시 칠함 +out.same = far.scrollLeft; + +const near = scroller(400); +box.exports.createSelectionReveal(near)("b"); +out.near = near.scrollLeft; // 보이는 자리 — 안 밂 + +const late = scroller(null); +const revealLate = box.exports.createSelectionReveal(late); +revealLate("c"); // 아직 안 그려짐 +late.line = 2000; +revealLate("c"); // 그린 뒤 같은 고름 — 이제 밂 +out.late = late.scrollLeft; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_273_1_profile_reveal.py b/resources/tester/test_273_1_profile_reveal.py new file mode 100644 index 000000000..13f948ef4 --- /dev/null +++ b/resources/tester/test_273_1_profile_reveal.py @@ -0,0 +1,56 @@ +"""273-1 — 다른 영역(횡단 카드 · 3D · 배수유역도 · 좌측 목록)에서 고르면 종단 그래프가 그 측점을 +가운데로 옮김(요구 168). 칠한 결과(강조된 측점선 · 알약)를 보고 화면 밖일 때만 · 같은 고름을 다시 +칠하면 안 밂 · 그래프 안에서 누른 것(보이는 자리)은 안 움직임. +진짜 TS 는 `helper_273_1_profile_reveal.cjs` 가 Node 로 돌림. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + +pytestmark = pytest.mark.skipif( + not (ROOT / "config" / "node_modules" / "typescript").is_dir(), + reason="프론트엔드 의존성 미설치", +) + + +@pytest.fixture(scope="module") +def ts() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HERE / "helper_273_1_profile_reveal.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + cwd=str(ROOT), + check=False, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_화면_밖이면_가운데로(ts) -> None: + assert ts["far"] == 3000 - 350 + + +def test_같은_고름을_다시_칠하면_안_밂(ts) -> None: + assert ts["same"] == 100 + + +def test_보이는_자리면_안_움직임(ts) -> None: + assert ts["near"] == 0 + + +def test_그리기_전이면_다음_칠하기에서_옮김(ts) -> None: + assert ts["late"] == 2000 - 350 + + +def test_종단도가_고름마다_부름() -> None: + src = (ROOT / "A00_Common/design/design_profile.ts").read_text(encoding="utf-8") + assert src.count("revealPick();") == 2