From eb7d39d8bedb060495906f6b1fac666846f9477b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 13:53:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore(docs):=20=EC=88=98=EC=A0=95=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20=EA=B3=B5=EC=9C=A0=20=ED=8C=8C=EC=9D=BC=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 1461f04dd..9782f31dc 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -223,3 +223,6 @@ B05에서 3D 영역의 상단 버튼들 중 원근으로, 직교로 버튼의 다음 수정 - 횡단도의 컨테이너 폭이 너무 커서 높이가 높아짐. 최대/최소 컨테이너가 폭을 조절하여 1.5행 정도가 보이게 조절하는 방법이 있는지 확인. 스케일을 망가뜨리면 안됨 - 종단 하단 패널 전체를 브라우져 창에서 별도로 동작하게 할수 있는지 확인 + +(10-6 사용자) +횡단 카드 폭과 종단 하단 패널(테이블, 유토곡선, 배수유역도 포함) 둘다 너의 제안대로 반영해줘. 반영해주면 내가 직접 검증하겠음 From 6912246fb086fc3ebd439a23dcbf8fdec3b9ebe5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 14:00:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(B06):=2054-34=20=ED=9A=A1=EB=8B=A8=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=95=9C=20=EC=A4=84=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=C2=B7=20=EC=B9=B4=EB=93=9C=20=ED=8F=AD=20?= =?UTF-8?q?400~800?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보기 칸 높이에 첫 줄 + 둘째 줄 절반이 드는 가장 적은 카드 수를 고름 (카드 폭 400~800px 상수는 Section_Common 한 곳). 그림은 px 그대로. 창 크기 · 패널 높이 변화 때 다시 맞춤 (높이만 바뀌면 카드 수가 바뀔 때만 다시 그림). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- B06_Section/B06_Section_UI_Section_Columns.ts | 47 ++++++++++++ B06_Section/B06_Section_UI_Section_Common.ts | 7 +- B06_Section/B06_Section_UI_Section_View.ts | 71 +++++++++++++------ 3 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 B06_Section/B06_Section_UI_Section_Columns.ts diff --git a/B06_Section/B06_Section_UI_Section_Columns.ts b/B06_Section/B06_Section_UI_Section_Columns.ts new file mode 100644 index 000000000..36653df8f --- /dev/null +++ b/B06_Section/B06_Section_UI_Section_Columns.ts @@ -0,0 +1,47 @@ +/* ============================================================================= + * B06_Section_UI_Section_Columns.ts + * 횡단 카드 한 줄 카드 수 고르기 — 보기 칸 높이에 첫 줄 + 둘째 줄 절반이 들어가는 + * 가장 적은 카드 수. 카드 폭은 최소~최대 폭 안(그림은 축척 그대로 · 폭만 바뀜). + * ========================================================================== */ + +import { + CROSS_CARD_CHROME, + CROSS_GRID_GAP, + CROSS_GRID_MAX_WIDTH, + CROSS_GRID_MIN_WIDTH, +} from "./B06_Section_UI_Section_Common"; + +export interface CrossColumns { + columns: number; + cardWidth: number; +} + +const widthFor = (columns: number, viewWidth: number): number => + Math.min(CROSS_GRID_MAX_WIDTH, (viewWidth - (columns - 1) * CROSS_GRID_GAP) / columns); + +/** + * `heightsFor(cardWidth, count)` — 그 폭일 때 앞 `count` 장의 카드 자연 높이. + * `viewHeight` 가 0 이하(숨김 · 아직 안 잰 칸)면 가장 큰 카드 쪽을 고른다. + */ +export function pickCrossColumns( + viewWidth: number, + viewHeight: number, + heightsFor: (cardWidth: number, count: number) => number[], +): CrossColumns { + const unit = CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP; + const most = Math.max(1, Math.floor((viewWidth + CROSS_GRID_GAP) / unit)); + const fewest = Math.min( + most, + Math.max(1, Math.ceil((viewWidth + CROSS_GRID_GAP) / (CROSS_GRID_MAX_WIDTH + CROSS_GRID_GAP))), + ); + for (let columns = fewest; columns < most; columns += 1) { + const cardWidth = widthFor(columns, viewWidth); + if (viewHeight <= 0) return { columns, cardWidth }; + const heights = heightsFor(cardWidth, columns * 2); + const first = Math.max(0, ...heights.slice(0, columns)) + CROSS_CARD_CHROME; + const second = Math.max(0, ...heights.slice(columns)) + CROSS_CARD_CHROME; + const needed = heights.length > columns ? first + CROSS_GRID_GAP + second / 2 : first; + if (needed <= viewHeight) return { columns, cardWidth }; + } + return { columns: most, cardWidth: widthFor(most, viewWidth) }; +} diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index f745e7b63..79f63d0c1 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -35,8 +35,11 @@ export const LONG_WIDTH = 1200; export const LONG_HEIGHT = 220; export const CROSS_WIDTH = 560; export const CROSS_HEIGHT = 250; -// 한 행 맞춤 기준 최소 카드 폭 — 조금 더 넓은 화면 필요(480→560, 약 +17%). -export const CROSS_GRID_MIN_WIDTH = 560; +// 카드 폭 범위 — 한 줄 카드 수는 보기 칸 높이로 정한다(`_Section_Columns`). +export const CROSS_GRID_MIN_WIDTH = 400; +export const CROSS_GRID_MAX_WIDTH = 800; +// 카드에서 그림 높이 밖 몫 — 머리 · 설계 줄 · 꼬리(실측 135). +export const CROSS_CARD_CHROME = 135; export const CROSS_GRID_GAP = 16; // bottom은 측점 라벨 한 줄 몫이다. X축 제목을 그래프 상단으로 올린 뒤 남던 여백을 걷어내, // 종단면도 아래 라벨이 그 밑 유토곡선과의 틈 **가운데**에 오도록 좁혔다(2026-08-02 사용자 지시). diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index b857bea3e..1b5a20121 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -42,9 +42,8 @@ import { CROSS_HEIGHT } from "./B06_Section_UI_Section_Common"; import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu"; import { culvertLinkFor as culvertLink } from "./B06_Section_UI_Cross_Culvert_Wire"; import type { CulvertLink } from "./B06_Section_UI_Cross_Culvert_Wire"; +import { pickCrossColumns } from "./B06_Section_UI_Section_Columns"; import { - CROSS_GRID_GAP, - CROSS_GRID_MIN_WIDTH, CROSS_WIDTH, type CrossDesignChange, type DesignChangeHandler, @@ -93,6 +92,8 @@ export function createSectionView( let currentStationInterval: number | undefined; let structureEdit: SectionStructureEdit | null = null; let renderWidth = 0; + let renderHeight = 0; + let drawnColumns = 0; let resizeTimer = 0; // 카드 단위 재빌드에 재사용하는 렌더 컨텍스트 (draw에서 갱신) let cachedStationInterval = 1; @@ -136,6 +137,33 @@ export function createSectionView( return Number.isFinite(width) ? Math.max(0, width) : 0; }; + const contentHeight = (): number => { + const style = getComputedStyle(cards); + const pad = (value: string): number => Number.parseFloat(value) || 0; + const height = cards.clientHeight - pad(style.paddingTop) - pad(style.paddingBottom); + return Number.isFinite(height) ? Math.max(0, height) : 0; + }; + + /** 카드 폭에서 앞 `count` 장의 기하 — 한 줄 카드 수 고르기 · 그리기가 함께 쓴다. */ + const plotBasesFor = (detail: SectionDetailResponse, cardWidth: number, count: number) => + detail.cross_sections + .slice(0, count) + .map((section) => + crossPlotBase( + section, + currentExaggeration, + cardWidth, + effectiveCardHalfWidth(section, stationWidth?.widthFor(section) ?? currentCrossHalfWidth), + designElevationAt(detail.longitudinal.design_profiles, section.chainage_m), + ), + ); + const naturalHeightsOf = (bases: ReadonlyArray): number[] => + bases.map((base) => Math.max(base?.naturalHeight ?? CROSS_HEIGHT, CROSS_HEIGHT)); + const pickColumns = (detail: SectionDetailResponse) => + pickCrossColumns(renderWidth, renderHeight, (cardWidth, count) => + naturalHeightsOf(plotBasesFor(detail, cardWidth, count)), + ); + /** * 선택 카드를 위 칸 안에서 보이게 한다 — `start`로 카드 **상단**(측점 라벨)을 칸 위에 * 붙인다(2026-08-04 사용자 지적). 목록 맨 끝 카드는 스크롤이 바닥에 걸려 그 자리까지 못 @@ -316,14 +344,14 @@ export function createSectionView( grid.addEventListener("click", (event) => { if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection(); }); - const columnCount = Math.max( - 1, - Math.floor((renderWidth + CROSS_GRID_GAP) / (CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP)), - ); + // 열 수 = 보기 칸 높이에 1.5줄이 드는 가장 적은 수 · 카드 폭 400~800(`_Section_Columns`). + const picked = pickColumns(detail); + const columnCount = picked.columns; + drawnColumns = columnCount; // CSS auto-fill의 자체 열 수와 JS columnCount가 어긋나면 같은 행 높이 그룹핑이 실제 - // 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수를 JS가 명시해 일치시킨다. - grid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`; - cachedCardWidth = (renderWidth - (columnCount - 1) * CROSS_GRID_GAP) / columnCount; + // 렌더 행과 달라져 카드 내부가 뒤죽박죽된다(N-2-6). 열 수 · 폭을 JS가 명시해 일치시킨다. + grid.style.gridTemplateColumns = `repeat(${columnCount}, ${picked.cardWidth}px)`; + cachedCardWidth = picked.cardWidth; cachedRowHeight.clear(); if (detail.cross_sections.length) { // 1차: 각 카드의 자연 높이(250px 바닥 적용) 측정 → 같은 행(columnCount 단위) 최댓값을 행 높이로. @@ -332,18 +360,8 @@ export function createSectionView( // 그것이 진입에서 `draw` 자기 시간의 대부분이었다(2026-09-06 CPU 프로파일). // 반폭도 카드가 실제로 쓰는 값(`effectiveCardHalfWidth`, 자동 줌아웃 포함)으로 맞춰 // 재 둔다 — 카드·행 높이가 어긋나지 않는다. - const plotBases = sections.map((section) => - crossPlotBase( - section, - currentExaggeration, - cachedCardWidth, - effectiveCardHalfWidth(section, stationWidth?.widthFor(section) ?? currentCrossHalfWidth), - designElevationAt(detail.longitudinal.design_profiles, section.chainage_m), - ), - ); - const naturalHeights = plotBases.map((base) => - Math.max(base?.naturalHeight ?? CROSS_HEIGHT, CROSS_HEIGHT), - ); + const plotBases = plotBasesFor(detail, cachedCardWidth, sections.length); + const naturalHeights = naturalHeightsOf(plotBases); // 2차: 행별 최댓값으로 강제 높이를 정해 같은 행 카드를 동일 높이로 렌더한다. for (let start = 0; start < sections.length; start += columnCount) { const rowHeight = Math.max(...naturalHeights.slice(start, start + columnCount)); @@ -401,11 +419,17 @@ export function createSectionView( const resizeObserver = new ResizeObserver(() => { const nextWidth = contentWidth(); - if (!Number.isFinite(nextWidth) || nextWidth <= 0 || Math.abs(nextWidth - renderWidth) < 1) - return; + const nextHeight = contentHeight(); + if (!Number.isFinite(nextWidth) || nextWidth <= 0) return; + const widthMoved = Math.abs(nextWidth - renderWidth) >= 1; + if (!widthMoved && Math.abs(nextHeight - renderHeight) < 1) return; window.clearTimeout(resizeTimer); resizeTimer = window.setTimeout(() => { renderWidth = nextWidth; + renderHeight = nextHeight; + // 높이만 바뀌면(아래 패널 끌기 · 접기) 한 줄 카드 수가 바뀔 때만 다시 그린다. + if (!widthMoved && currentDetail && pickColumns(currentDetail).columns === drawnColumns) + return; draw(); }, 150); }); @@ -442,6 +466,7 @@ export function createSectionView( // 진입 시 측점을 자동으로 고르지 않는다(2026-08-02 사용자 지시) — 0측점이 선택된 채로 // 시작하면 사용자가 고르지도 않은 카드가 강조돼 있고 유토곡선 말풍선도 떠 있다. renderWidth = contentWidth(); + renderHeight = contentHeight(); draw(); if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(cards)); },